#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 { /// /// Helpers for working with in the editor. /// 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) { if (property == null) return -1; 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. /// /// /// /// /// /// /// 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 /// 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 /// . /// 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 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