392 lines
15 KiB
C#
392 lines
15 KiB
C#
|
#if UNITY_EDITOR
|
||
|
using System;
|
||
|
using System.Collections.Generic;
|
||
|
using System.Reflection;
|
||
|
using System.Text;
|
||
|
using UnityEditor;
|
||
|
using UnityEngine.InputSystem.Utilities;
|
||
|
|
||
|
namespace UnityEngine.InputSystem.Editor
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// Helpers for working with <see cref="SerializedProperty"/> in the editor.
|
||
|
/// </summary>
|
||
|
internal static class SerializedPropertyHelpers
|
||
|
{
|
||
|
// Show a PropertyField with a greyed-out default text if the field is empty and not being edited.
|
||
|
// This is meant to communicate the fact that filling these properties is optional and that Unity will
|
||
|
// use reasonable defaults if left empty.
|
||
|
public static void PropertyFieldWithDefaultText(this SerializedProperty prop, GUIContent label, string defaultText)
|
||
|
{
|
||
|
GUI.SetNextControlName(label.text);
|
||
|
var rt = GUILayoutUtility.GetRect(label, GUI.skin.textField);
|
||
|
|
||
|
EditorGUI.PropertyField(rt, prop, label);
|
||
|
if (string.IsNullOrEmpty(prop.stringValue) && GUI.GetNameOfFocusedControl() != label.text && Event.current.type == EventType.Repaint)
|
||
|
{
|
||
|
using (new EditorGUI.DisabledScope(true))
|
||
|
{
|
||
|
rt.xMin += EditorGUIUtility.labelWidth;
|
||
|
GUI.skin.textField.Draw(rt, new GUIContent(defaultText), false, false, false, false);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static SerializedProperty GetParentProperty(this SerializedProperty property)
|
||
|
{
|
||
|
var path = property.propertyPath;
|
||
|
var lastDot = path.LastIndexOf('.');
|
||
|
if (lastDot == -1)
|
||
|
return null;
|
||
|
var parentPath = path.Substring(0, lastDot);
|
||
|
return property.serializedObject.FindProperty(parentPath);
|
||
|
}
|
||
|
|
||
|
public static SerializedProperty GetArrayPropertyFromElement(this SerializedProperty property)
|
||
|
{
|
||
|
// Arrays have a structure of 'arrayName.Array.data[index]'.
|
||
|
// Given property should be element and thus 'data[index]'.
|
||
|
var arrayProperty = property.GetParentProperty();
|
||
|
Debug.Assert(arrayProperty.name == "Array", "Expecting 'Array' property");
|
||
|
return arrayProperty.GetParentProperty();
|
||
|
}
|
||
|
|
||
|
public static int GetIndexOfArrayElement(this SerializedProperty property)
|
||
|
{
|
||
|
var propertyPath = property.propertyPath;
|
||
|
if (propertyPath[propertyPath.Length - 1] != ']')
|
||
|
return -1;
|
||
|
var lastIndexOfLeftBracket = propertyPath.LastIndexOf('[');
|
||
|
if (int.TryParse(
|
||
|
propertyPath.Substring(lastIndexOfLeftBracket + 1, propertyPath.Length - lastIndexOfLeftBracket - 2),
|
||
|
out var index))
|
||
|
return index;
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
public static Type GetArrayElementType(this SerializedProperty property)
|
||
|
{
|
||
|
Debug.Assert(property.isArray, $"Property {property.propertyPath} is not an array");
|
||
|
|
||
|
var fieldType = property.GetFieldType();
|
||
|
if (fieldType == null)
|
||
|
throw new ArgumentException($"Cannot determine managed field type of {property.propertyPath}",
|
||
|
nameof(property));
|
||
|
|
||
|
return fieldType.GetElementType();
|
||
|
}
|
||
|
|
||
|
public static void ResetValuesToDefault(this SerializedProperty property)
|
||
|
{
|
||
|
var isString = property.propertyType == SerializedPropertyType.String;
|
||
|
|
||
|
if (property.isArray && !isString)
|
||
|
{
|
||
|
property.ClearArray();
|
||
|
}
|
||
|
else if (property.hasChildren && !isString)
|
||
|
{
|
||
|
foreach (var child in property.GetChildren())
|
||
|
ResetValuesToDefault(child);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
switch (property.propertyType)
|
||
|
{
|
||
|
case SerializedPropertyType.Float:
|
||
|
property.floatValue = default(float);
|
||
|
break;
|
||
|
|
||
|
case SerializedPropertyType.Boolean:
|
||
|
property.boolValue = default(bool);
|
||
|
break;
|
||
|
|
||
|
case SerializedPropertyType.Enum:
|
||
|
case SerializedPropertyType.Integer:
|
||
|
property.intValue = default(int);
|
||
|
break;
|
||
|
|
||
|
case SerializedPropertyType.String:
|
||
|
property.stringValue = string.Empty;
|
||
|
break;
|
||
|
|
||
|
case SerializedPropertyType.ObjectReference:
|
||
|
property.objectReferenceValue = null;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static string ToJson(this SerializedObject serializedObject)
|
||
|
{
|
||
|
return JsonUtility.ToJson(serializedObject, prettyPrint: true);
|
||
|
}
|
||
|
|
||
|
// The following is functionality that allows turning Unity data into text and text
|
||
|
// back into Unity data. Given that this is essential functionality for any kind of
|
||
|
// copypaste support, I'm not sure why the Unity editor API isn't providing this out
|
||
|
// of the box. Internally, we do have support for this on a whole-object kind of level
|
||
|
// but not for parts of serialized objects.
|
||
|
|
||
|
/// <summary>
|
||
|
///
|
||
|
/// </summary>
|
||
|
/// <param name="property"></param>
|
||
|
/// <returns></returns>
|
||
|
/// <remarks>
|
||
|
/// Converting entire objects to JSON is easy using Unity's serialization system but we cannot
|
||
|
/// easily convert just a part of the serialized graph to JSON (or any text format for that matter)
|
||
|
/// and then recreate the same data from text through SerializedProperties. This method helps by manually
|
||
|
/// turning an arbitrary part of a graph into JSON which can then be used with <see cref="RestoreFromJson"/>
|
||
|
/// to write the data back into an existing property.
|
||
|
///
|
||
|
/// The primary use for this is copy-paste where serialized data needs to be stored in
|
||
|
/// <see cref="EditorGUIUtility.systemCopyBuffer"/>.
|
||
|
/// </remarks>
|
||
|
public static string CopyToJson(this SerializedProperty property, bool ignoreObjectReferences = false)
|
||
|
{
|
||
|
var buffer = new StringBuilder();
|
||
|
CopyToJson(property, buffer, ignoreObjectReferences);
|
||
|
return buffer.ToString();
|
||
|
}
|
||
|
|
||
|
public static void CopyToJson(this SerializedProperty property, StringBuilder buffer, bool ignoreObjectReferences = false)
|
||
|
{
|
||
|
CopyToJson(property, buffer, noPropertyName: true, ignoreObjectReferences: ignoreObjectReferences);
|
||
|
}
|
||
|
|
||
|
private static void CopyToJson(this SerializedProperty property, StringBuilder buffer, bool noPropertyName, bool ignoreObjectReferences)
|
||
|
{
|
||
|
var propertyType = property.propertyType;
|
||
|
if (ignoreObjectReferences && propertyType == SerializedPropertyType.ObjectReference)
|
||
|
return;
|
||
|
|
||
|
// Property name.
|
||
|
if (!noPropertyName)
|
||
|
{
|
||
|
buffer.Append('"');
|
||
|
buffer.Append(property.name);
|
||
|
buffer.Append('"');
|
||
|
buffer.Append(':');
|
||
|
}
|
||
|
|
||
|
// Strings are classified as arrays and have children.
|
||
|
var isString = propertyType == SerializedPropertyType.String;
|
||
|
|
||
|
// Property value.
|
||
|
if (property.isArray && !isString)
|
||
|
{
|
||
|
buffer.Append('[');
|
||
|
var arraySize = property.arraySize;
|
||
|
var isFirst = true;
|
||
|
for (var i = 0; i < arraySize; ++i)
|
||
|
{
|
||
|
var element = property.GetArrayElementAtIndex(i);
|
||
|
if (ignoreObjectReferences && element.propertyType == SerializedPropertyType.ObjectReference)
|
||
|
continue;
|
||
|
if (!isFirst)
|
||
|
buffer.Append(',');
|
||
|
CopyToJson(element, buffer, true, ignoreObjectReferences);
|
||
|
isFirst = false;
|
||
|
}
|
||
|
buffer.Append(']');
|
||
|
}
|
||
|
else if (property.hasChildren && !isString)
|
||
|
{
|
||
|
// Any structured data we represent as a JSON object.
|
||
|
|
||
|
buffer.Append('{');
|
||
|
var isFirst = true;
|
||
|
foreach (var child in property.GetChildren())
|
||
|
{
|
||
|
if (ignoreObjectReferences && child.propertyType == SerializedPropertyType.ObjectReference)
|
||
|
continue;
|
||
|
if (!isFirst)
|
||
|
buffer.Append(',');
|
||
|
CopyToJson(child, buffer, false, ignoreObjectReferences);
|
||
|
isFirst = false;
|
||
|
}
|
||
|
buffer.Append('}');
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
switch (propertyType)
|
||
|
{
|
||
|
case SerializedPropertyType.Enum:
|
||
|
case SerializedPropertyType.Integer:
|
||
|
buffer.Append(property.intValue);
|
||
|
break;
|
||
|
|
||
|
case SerializedPropertyType.Float:
|
||
|
buffer.Append(property.floatValue);
|
||
|
break;
|
||
|
|
||
|
case SerializedPropertyType.String:
|
||
|
buffer.Append('"');
|
||
|
buffer.Append(property.stringValue.Escape());
|
||
|
buffer.Append('"');
|
||
|
break;
|
||
|
|
||
|
case SerializedPropertyType.Boolean:
|
||
|
if (property.boolValue)
|
||
|
buffer.Append("true");
|
||
|
else
|
||
|
buffer.Append("false");
|
||
|
break;
|
||
|
|
||
|
////TODO: other property types
|
||
|
default:
|
||
|
throw new NotImplementedException($"Support for {property.propertyType} property type");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static void RestoreFromJson(this SerializedProperty property, string json)
|
||
|
{
|
||
|
var parser = new JsonParser(json);
|
||
|
RestoreFromJson(property, ref parser);
|
||
|
}
|
||
|
|
||
|
public static void RestoreFromJson(this SerializedProperty property, ref JsonParser parser)
|
||
|
{
|
||
|
var isString = property.propertyType == SerializedPropertyType.String;
|
||
|
|
||
|
if (property.isArray && !isString)
|
||
|
{
|
||
|
property.ClearArray();
|
||
|
parser.ParseToken('[');
|
||
|
while (!parser.ParseToken(']') && !parser.isAtEnd)
|
||
|
{
|
||
|
var index = property.arraySize;
|
||
|
property.InsertArrayElementAtIndex(index);
|
||
|
var elementProperty = property.GetArrayElementAtIndex(index);
|
||
|
RestoreFromJson(elementProperty, ref parser);
|
||
|
parser.ParseToken(',');
|
||
|
}
|
||
|
}
|
||
|
else if (property.hasChildren && !isString)
|
||
|
{
|
||
|
parser.ParseToken('{');
|
||
|
while (!parser.ParseToken('}') && !parser.isAtEnd)
|
||
|
{
|
||
|
parser.ParseStringValue(out var propertyName);
|
||
|
parser.ParseToken(':');
|
||
|
|
||
|
var childProperty = property.FindPropertyRelative(propertyName.ToString());
|
||
|
if (childProperty == null)
|
||
|
throw new ArgumentException($"Cannot find property '{propertyName}' in {property}", nameof(property));
|
||
|
|
||
|
RestoreFromJson(childProperty, ref parser);
|
||
|
parser.ParseToken(',');
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
switch (property.propertyType)
|
||
|
{
|
||
|
case SerializedPropertyType.Float:
|
||
|
{
|
||
|
parser.ParseNumber(out var num);
|
||
|
property.floatValue = (float)num.ToDouble();
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
case SerializedPropertyType.String:
|
||
|
{
|
||
|
parser.ParseStringValue(out var str);
|
||
|
property.stringValue = str.ToString();
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
case SerializedPropertyType.Boolean:
|
||
|
{
|
||
|
parser.ParseBooleanValue(out var b);
|
||
|
property.boolValue = b.ToBoolean();
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
case SerializedPropertyType.Enum:
|
||
|
case SerializedPropertyType.Integer:
|
||
|
{
|
||
|
parser.ParseNumber(out var num);
|
||
|
property.intValue = (int)num.ToInteger();
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
default:
|
||
|
throw new NotImplementedException(
|
||
|
$"Restoring property value of type {property.propertyType} (property: {property})");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static IEnumerable<SerializedProperty> GetChildren(this SerializedProperty property)
|
||
|
{
|
||
|
if (!property.hasChildren)
|
||
|
yield break;
|
||
|
|
||
|
using (var iter = property.Copy())
|
||
|
{
|
||
|
var end = iter.GetEndProperty(true);
|
||
|
|
||
|
// Go to first child.
|
||
|
if (!iter.Next(true))
|
||
|
yield break; // Shouldn't happen; we've already established we have children.
|
||
|
|
||
|
// Iterate over children.
|
||
|
while (!SerializedProperty.EqualContents(iter, end))
|
||
|
{
|
||
|
yield return iter;
|
||
|
if (!iter.Next(false))
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public static FieldInfo GetField(this SerializedProperty property)
|
||
|
{
|
||
|
var objectType = property.serializedObject.targetObject.GetType();
|
||
|
var currentSerializableType = objectType;
|
||
|
var pathComponents = property.propertyPath.Split('.');
|
||
|
|
||
|
FieldInfo result = null;
|
||
|
foreach (var component in pathComponents)
|
||
|
{
|
||
|
// Handle arrays. They are followed by "Array" and "data[N]" elements.
|
||
|
if (result != null && currentSerializableType.IsArray)
|
||
|
{
|
||
|
if (component == "Array")
|
||
|
continue;
|
||
|
|
||
|
if (component.StartsWith("data["))
|
||
|
{
|
||
|
currentSerializableType = currentSerializableType.GetElementType();
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
result = currentSerializableType.GetField(component,
|
||
|
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy);
|
||
|
if (result == null)
|
||
|
return null;
|
||
|
currentSerializableType = result.FieldType;
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
public static Type GetFieldType(this SerializedProperty property)
|
||
|
{
|
||
|
return GetField(property)?.FieldType;
|
||
|
}
|
||
|
|
||
|
public static void SetStringValue(this SerializedProperty property, string propertyName, string value)
|
||
|
{
|
||
|
var propertyRelative = property?.FindPropertyRelative(propertyName);
|
||
|
if (propertyRelative != null)
|
||
|
propertyRelative.stringValue = value;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
#endif // UNITY_EDITOR
|