// Serialized Property Accessor // Copyright 2020 Kybernetik // #if UNITY_EDITOR using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEngine; using Object = UnityEngine.Object; // Shared File Last Modified: 2019-12-04. namespace Animancer.Editor // namespace InspectorGadgets.Editor // namespace UltEvents.Editor { /// [Editor-Only] Various serialization utilities. public static partial class Serialization { /************************************************************************************************************************/ #region Public Static API /************************************************************************************************************************/ /// The text used in a to denote array elements. public const string ArrayDataPrefix = ".Array.data[", ArrayDataSuffix = "]"; /************************************************************************************************************************/ /// Returns a user friendly version of the . public static string GetFriendlyPath(this SerializedProperty property) { return property.propertyPath.Replace(ArrayDataPrefix, "["); } /************************************************************************************************************************/ #region Get Value /************************************************************************************************************************/ /// Gets the value of the specified . public static object GetValue(this SerializedProperty property, object targetObject) { switch (property.propertyType) { case SerializedPropertyType.Boolean: return property.boolValue; case SerializedPropertyType.Float: return property.floatValue; case SerializedPropertyType.Integer: return property.intValue; case SerializedPropertyType.String: return property.stringValue; case SerializedPropertyType.Vector2: return property.vector2Value; case SerializedPropertyType.Vector3: return property.vector3Value; case SerializedPropertyType.Vector4: return property.vector4Value; case SerializedPropertyType.Quaternion: return property.quaternionValue; case SerializedPropertyType.Color: return property.colorValue; case SerializedPropertyType.AnimationCurve: return property.animationCurveValue; case SerializedPropertyType.Rect: return property.rectValue; case SerializedPropertyType.Bounds: return property.boundsValue; #if UNITY_2017_3_OR_NEWER case SerializedPropertyType.Vector2Int: return property.vector2IntValue; case SerializedPropertyType.Vector3Int: return property.vector3IntValue; case SerializedPropertyType.RectInt: return property.rectIntValue; case SerializedPropertyType.BoundsInt: return property.boundsIntValue; #endif case SerializedPropertyType.ObjectReference: return property.objectReferenceValue; case SerializedPropertyType.ExposedReference: return property.exposedReferenceValue; case SerializedPropertyType.ArraySize: return property.intValue; case SerializedPropertyType.FixedBufferSize: return property.fixedBufferSize; case SerializedPropertyType.Generic: case SerializedPropertyType.Enum: case SerializedPropertyType.LayerMask: case SerializedPropertyType.Gradient: case SerializedPropertyType.Character: default: var accessor = GetAccessor(property); //if (Event.current.type != EventType.Layout && Event.current.type != EventType.Repaint) Debug.Log(accessor); if (accessor != null) return accessor.GetValue(targetObject); else return null; } } /************************************************************************************************************************/ /// Gets the value of the . public static object GetValue(this SerializedProperty property) { return GetValue(property, property.serializedObject.targetObject); } /// Gets the value of the . public static T GetValue(this SerializedProperty property) { return (T)GetValue(property); } /// Gets the value of the . public static void GetValue(this SerializedProperty property, out T value) { value = (T)GetValue(property); } /************************************************************************************************************************/ /// Gets the value of the for each of its target objects. public static T[] GetValues(this SerializedProperty property) { try { var targetObjects = property.serializedObject.targetObjects; var values = new T[targetObjects.Length]; for (int i = 0; i < values.Length; i++) { values[i] = (T)GetValue(property, targetObjects[i]); } return values; } catch { return null; } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Set Value /************************************************************************************************************************/ /// Sets the value of the specified . public static void SetValue(this SerializedProperty property, object targetObject, object value) { switch (property.propertyType) { case SerializedPropertyType.Boolean: property.boolValue = (bool)value; break; case SerializedPropertyType.Float: property.floatValue = (float)value; break; case SerializedPropertyType.Integer: property.intValue = (int)value; break; case SerializedPropertyType.String: property.stringValue = (string)value; break; case SerializedPropertyType.Vector2: property.vector2Value = (Vector2)value; break; case SerializedPropertyType.Vector3: property.vector3Value = (Vector3)value; break; case SerializedPropertyType.Vector4: property.vector4Value = (Vector4)value; break; case SerializedPropertyType.Quaternion: property.quaternionValue = (Quaternion)value; break; case SerializedPropertyType.Color: property.colorValue = (Color)value; break; case SerializedPropertyType.AnimationCurve: property.animationCurveValue = (AnimationCurve)value; break; case SerializedPropertyType.Rect: property.rectValue = (Rect)value; break; case SerializedPropertyType.Bounds: property.boundsValue = (Bounds)value; break; #if UNITY_2017_3_OR_NEWER case SerializedPropertyType.Vector2Int: property.vector2IntValue = (Vector2Int)value; break; case SerializedPropertyType.Vector3Int: property.vector3IntValue = (Vector3Int)value; break; case SerializedPropertyType.RectInt: property.rectIntValue = (RectInt)value; break; case SerializedPropertyType.BoundsInt: property.boundsIntValue = (BoundsInt)value; break; #endif case SerializedPropertyType.ObjectReference: property.objectReferenceValue = (Object)value; break; case SerializedPropertyType.ExposedReference: property.exposedReferenceValue = (Object)value; break; case SerializedPropertyType.ArraySize: property.intValue = (int)value; break; case SerializedPropertyType.FixedBufferSize: throw new InvalidOperationException("SetValue failed: SerializedProperty.fixedBufferSize is read-only."); case SerializedPropertyType.Generic: case SerializedPropertyType.Enum: case SerializedPropertyType.LayerMask: case SerializedPropertyType.Gradient: case SerializedPropertyType.Character: default: var accessor = GetAccessor(property); if (accessor != null) accessor.SetValue(targetObject, value); break; } } /************************************************************************************************************************/ /// Sets the value of the . public static void SetValue(this SerializedProperty property, object value) { switch (property.propertyType) { case SerializedPropertyType.Boolean: property.boolValue = (bool)value; break; case SerializedPropertyType.Float: property.floatValue = (float)value; break; case SerializedPropertyType.Integer: property.intValue = (int)value; break; case SerializedPropertyType.String: property.stringValue = (string)value; break; case SerializedPropertyType.Vector2: property.vector2Value = (Vector2)value; break; case SerializedPropertyType.Vector3: property.vector3Value = (Vector3)value; break; case SerializedPropertyType.Vector4: property.vector4Value = (Vector4)value; break; case SerializedPropertyType.Quaternion: property.quaternionValue = (Quaternion)value; break; case SerializedPropertyType.Color: property.colorValue = (Color)value; break; case SerializedPropertyType.AnimationCurve: property.animationCurveValue = (AnimationCurve)value; break; case SerializedPropertyType.Rect: property.rectValue = (Rect)value; break; case SerializedPropertyType.Bounds: property.boundsValue = (Bounds)value; break; #if UNITY_2017_3_OR_NEWER case SerializedPropertyType.Vector2Int: property.vector2IntValue = (Vector2Int)value; break; case SerializedPropertyType.Vector3Int: property.vector3IntValue = (Vector3Int)value; break; case SerializedPropertyType.RectInt: property.rectIntValue = (RectInt)value; break; case SerializedPropertyType.BoundsInt: property.boundsIntValue = (BoundsInt)value; break; #endif case SerializedPropertyType.ObjectReference: property.objectReferenceValue = (Object)value; break; case SerializedPropertyType.ExposedReference: property.exposedReferenceValue = (Object)value; break; case SerializedPropertyType.ArraySize: property.intValue = (int)value; break; case SerializedPropertyType.FixedBufferSize: throw new InvalidOperationException("SetValue failed: SerializedProperty.fixedBufferSize is read-only."); case SerializedPropertyType.Generic: case SerializedPropertyType.Enum: case SerializedPropertyType.LayerMask: case SerializedPropertyType.Gradient: case SerializedPropertyType.Character: default: var accessor = GetAccessor(property); if (accessor != null) { var targets = property.serializedObject.targetObjects; for (int i = 0; i < targets.Length; i++) { accessor.SetValue(targets[i], value); } } break; } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ /// Indicates whether both properties refer to the same underlying field. public static bool AreSameProperty(SerializedProperty a, SerializedProperty b) { if (a == b) return true; if (a == null) return b == null; if (b == null) return false; if (a.propertyPath != b.propertyPath) return false; var aTargets = a.serializedObject.targetObjects; var bTargets = b.serializedObject.targetObjects; if (aTargets.Length != bTargets.Length) return false; for (int i = 0; i < aTargets.Length; i++) { if (aTargets[i] != bTargets[i]) return false; } return true; } /************************************************************************************************************************/ /// /// Executes the `action` once with a new for each of the /// . Or if there is only one target, it uses the `property`. /// public static void ForEachTarget(this SerializedProperty property, Action function, string undoName = "Inspector") { var targets = property.serializedObject.targetObjects; if (undoName != null) Undo.RecordObjects(targets, undoName); if (targets.Length == 1) { function(property); property.serializedObject.ApplyModifiedProperties(); } else { var path = property.propertyPath; for (int i = 0; i < targets.Length; i++) { using (var serializedObject = new SerializedObject(targets[i])) { property = serializedObject.FindProperty(path); function(property); property.serializedObject.ApplyModifiedProperties(); } } } } /************************************************************************************************************************/ /// /// Adds a menu item to execute the specified `action` for each of the `property`s target objects. /// public static void AddPropertyModifierFunction(GenericMenu menu, SerializedProperty property, string label, Action action) { menu.AddItem(new GUIContent(label), false, () => { ForEachTarget(property, action); }); } /************************************************************************************************************************/ /// /// Calls the specified `method` for each of the underlying values of the `property` (in case it represents /// multiple selected objects) and records an undo step for any modifications made. /// public static void ModifyValues(this SerializedProperty property, Action method, string undoName = "Inspector") { RecordUndo(property, undoName); var values = GetValues(property); for (int i = 0; i < values.Length; i++) method(values[i]); OnPropertyChanged(property); } /************************************************************************************************************************/ /// /// Records the state of the specified `property` so it can be undone. /// public static void RecordUndo(this SerializedProperty property, string undoName = "Inspector") { Undo.RecordObjects(property.serializedObject.targetObjects, undoName); } /************************************************************************************************************************/ /// /// Updates the specified `property` and marks its target objects as dirty so any changes to a prefab will be saved. /// public static void OnPropertyChanged(this SerializedProperty property) { var targets = property.serializedObject.targetObjects; // If this change is made to a prefab, this makes sure that any instances in the scene will be updated. for (int i = 0; i < targets.Length; i++) { EditorUtility.SetDirty(targets[i]); } property.serializedObject.Update(); } /************************************************************************************************************************/ /// /// Returns the that represents fields of the specified `type`. /// public static SerializedPropertyType GetPropertyType(Type type) { // Primitives. if (type == typeof(bool)) return SerializedPropertyType.Boolean; if (type == typeof(int)) return SerializedPropertyType.Integer; if (type == typeof(float)) return SerializedPropertyType.Float; if (type == typeof(string)) return SerializedPropertyType.String; if (type == typeof(LayerMask)) return SerializedPropertyType.LayerMask; // Vectors. if (type == typeof(Vector2)) return SerializedPropertyType.Vector2; if (type == typeof(Vector3)) return SerializedPropertyType.Vector3; if (type == typeof(Vector4)) return SerializedPropertyType.Vector4; if (type == typeof(Quaternion)) return SerializedPropertyType.Quaternion; // Other. if (type == typeof(Color) || type == typeof(Color32)) return SerializedPropertyType.Color; if (type == typeof(Gradient)) return SerializedPropertyType.Gradient; if (type == typeof(Rect)) return SerializedPropertyType.Rect; if (type == typeof(Bounds)) return SerializedPropertyType.Bounds; if (type == typeof(AnimationCurve)) return SerializedPropertyType.AnimationCurve; // Int Variants. #if UNITY_2017_3_OR_NEWER if (type == typeof(Vector2Int)) return SerializedPropertyType.Vector2Int; if (type == typeof(Vector3Int)) return SerializedPropertyType.Vector3Int; if (type == typeof(RectInt)) return SerializedPropertyType.RectInt; if (type == typeof(BoundsInt)) return SerializedPropertyType.BoundsInt; #endif // Special. if (typeof(Object).IsAssignableFrom(type)) return SerializedPropertyType.ObjectReference; if (type.IsEnum) return SerializedPropertyType.Enum; return SerializedPropertyType.Generic; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Accessor Pool /************************************************************************************************************************/ private static readonly Dictionary> TypeToPathToAccessor = new Dictionary>(); /************************************************************************************************************************/ /// /// Returns an that can be used to access the details of the specified `property`. /// public static PropertyAccessor GetAccessor(this SerializedProperty property) { var propertyPath = property.propertyPath; object targetObject = property.serializedObject.targetObject; var type = targetObject.GetType(); return GetAccessor(propertyPath, ref type); } /************************************************************************************************************************/ /// /// Returns an for a with the specified `propertyPath` /// on the specified `type` of object. /// private static PropertyAccessor GetAccessor(string propertyPath, ref Type type) { Dictionary pathToAccessor; if (!TypeToPathToAccessor.TryGetValue(type, out pathToAccessor)) { pathToAccessor = new Dictionary(); TypeToPathToAccessor.Add(type, pathToAccessor); } PropertyAccessor accessor; if (!pathToAccessor.TryGetValue(propertyPath, out accessor)) { var nameStartIndex = propertyPath.LastIndexOf('.'); string elementName; PropertyAccessor parent; // Array. if (nameStartIndex > 6 && nameStartIndex < propertyPath.Length - 7 && string.Compare(propertyPath, nameStartIndex - 6, ArrayDataPrefix, 0, 12) == 0) { var index = int.Parse(propertyPath.Substring(nameStartIndex + 6, propertyPath.Length - nameStartIndex - 7)); var nameEndIndex = nameStartIndex - 6; nameStartIndex = propertyPath.LastIndexOf('.', nameEndIndex - 1); elementName = propertyPath.Substring(nameStartIndex + 1, nameEndIndex - nameStartIndex - 1); FieldInfo field; if (nameStartIndex >= 0) { parent = GetAccessor(propertyPath.Substring(0, nameStartIndex), ref type); field = GetField(parent != null ? parent.FieldType : type, elementName); } else { parent = null; field = GetField(type, elementName); } if (field != null) accessor = new ArrayPropertyAccessor(parent, field, index); else accessor = null; } else// Single. { if (nameStartIndex >= 0) { elementName = propertyPath.Substring(nameStartIndex + 1); parent = GetAccessor(propertyPath.Substring(0, nameStartIndex), ref type); } else { elementName = propertyPath; parent = null; } var field = GetField(parent != null ? parent.FieldType : type, elementName); if (field != null) accessor = new PropertyAccessor(parent, field); else accessor = null; } pathToAccessor.Add(propertyPath, accessor); } if (accessor != null) type = accessor.Field.FieldType; return accessor; } /************************************************************************************************************************/ /// /// Returns a field with the specified `name` in the `declaringType` or any of its base types. /// private static FieldInfo GetField(Type declaringType, string name) { while (true) { var field = declaringType.GetField(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); if (field != null) return field; declaringType = declaringType.BaseType; if (declaringType == null) return null; } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region PropertyAccessor /************************************************************************************************************************/ /// [Editor-Only] /// A wrapper for accessing the underlying values and fields of a . /// public class PropertyAccessor { /************************************************************************************************************************/ /// The accessor for the field which this accessor is nested inside. public readonly PropertyAccessor Parent; /// The field wrapped by this accessor. public readonly FieldInfo Field; /// The type of the wrapped . public readonly Type FieldType; /************************************************************************************************************************/ /// [Internal] Creates a new . internal PropertyAccessor(PropertyAccessor parent, FieldInfo field) : this(parent, field, field.FieldType) { } /// Creates a new . protected PropertyAccessor(PropertyAccessor parent, FieldInfo field, Type fieldType) { Parent = parent; Field = field; FieldType = fieldType; } /************************************************************************************************************************/ /// /// Gets the value of the from the (if there is one), then uses it to get and return /// the value of the . /// public virtual object GetValue(object obj) { if (ReferenceEquals(obj, null)) return null; if (Parent != null) obj = Parent.GetValue(obj); return Field.GetValue(obj); } /// /// Gets the value of the from the (if there is one), then uses it to get and return /// the value of the . /// public object GetValue(SerializedObject serializedObject) { return serializedObject != null ? GetValue(serializedObject.targetObject) : null; } /// /// Gets the value of the from the (if there is one), then uses it to get and return /// the value of the . /// public object GetValue(SerializedProperty serializedProperty) { return serializedProperty != null ? GetValue(serializedProperty.serializedObject) : null; } /************************************************************************************************************************/ /// /// Gets the value of the from the (if there is one), then uses it to set the value /// of the . /// public virtual void SetValue(object obj, object value) { if (ReferenceEquals(obj, null)) return; if (Parent != null) obj = Parent.GetValue(obj); Field.SetValue(obj, value); } /// /// Gets the value of the from the (if there is one), then uses it to set the value /// of the . /// public void SetValue(SerializedObject serializedObject, object value) { if (serializedObject != null) SetValue(serializedObject.targetObject, value); } /// /// Gets the value of the from the (if there is one), then uses it to set the value /// of the . /// public void SetValue(SerializedProperty serializedProperty, object value) { if (serializedProperty != null) SetValue(serializedProperty.serializedObject, value); } /************************************************************************************************************************/ /// Returns a description of this accessor's path. public override string ToString() { if (Parent != null) return Parent.ToString() + "." + Field.Name; else return Field.Name; } /************************************************************************************************************************/ /// Returns a this accessor's . public virtual string GetPath() { if (Parent != null) return Parent.GetPath() + "." + Field.Name; else return Field.Name; } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region ArrayPropertyAccessor /************************************************************************************************************************/ /// [Editor-Only] /// An for a particular element in an array. /// public class ArrayPropertyAccessor : PropertyAccessor { /************************************************************************************************************************/ /// The index of the array element this accessor targets. public readonly int ElementIndex; /************************************************************************************************************************/ /// [Internal] Creates a new . internal ArrayPropertyAccessor(PropertyAccessor parent, FieldInfo field, int elementIndex) : base(parent, field, GetElementType(field.FieldType)) { ElementIndex = elementIndex; } /************************************************************************************************************************/ /// Returns the type of elements in the array. private static Type GetElementType(Type fieldType) { if (fieldType.IsArray) { return fieldType.GetElementType(); } else if (fieldType.IsGenericType) { return fieldType.GetGenericArguments()[0]; } else { Debug.LogWarning("SerializedPropertyArrayAccessor: unable to determine element type for " + fieldType); return fieldType; } } /************************************************************************************************************************/ /// /// Gets the value of the from the (if there is one), then uses it to /// get and return the value of the . /// public override object GetValue(object obj) { var collection = base.GetValue(obj); if (collection == null) return null; var list = collection as IList; if (list != null) { if (ElementIndex < list.Count) return list[ElementIndex]; else return null; } var enumerator = ((IEnumerable)collection).GetEnumerator(); for (int i = 0; i < ElementIndex; i++) { if (!enumerator.MoveNext()) return null; } return enumerator.Current; } /************************************************************************************************************************/ /// /// Gets the value of the from the (if there is one), then uses it to /// set the value of the . /// public override void SetValue(object obj, object value) { var collection = base.GetValue(obj); if (collection == null) return; var list = collection as IList; if (list != null) { if (ElementIndex < list.Count) list[ElementIndex] = value; return; } throw new InvalidOperationException("SetValue failed: " + Field + " doesn't implement IList."); } /************************************************************************************************************************/ /// Returns a description of this accessor's path. public override string ToString() { return string.Concat(base.ToString(), "[", ElementIndex.ToString(), "]"); } /************************************************************************************************************************/ /// Returns a this accessor's . public override string GetPath() { return string.Concat(base.GetPath(), ArrayDataPrefix, ElementIndex.ToString(), ArrayDataSuffix); } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } #endif