#if !LITE || LITE_DLL // 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: 2020-03-05. // namespace Animancer.Editor // namespace InspectorGadgets.Editor namespace UltEvents.Editor { /// <summary>[Editor-Only] Various serialization utilities.</summary> public static partial class Serialization { /************************************************************************************************************************/ #region Public Static API /************************************************************************************************************************/ /// <summary>The text used in a <see cref="SerializedProperty.propertyPath"/> to denote array elements.</summary> public const string ArrayDataPrefix = ".Array.data[", ArrayDataSuffix = "]"; /************************************************************************************************************************/ /// <summary>Returns a user friendly version of the <see cref="SerializedProperty.propertyPath"/>.</summary> public static string GetFriendlyPath(this SerializedProperty property) { return property.propertyPath.Replace(ArrayDataPrefix, "["); } /************************************************************************************************************************/ #region Get Value /************************************************************************************************************************/ /// <summary>Gets the value of the specified <see cref="SerializedProperty"/>.</summary> 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 (accessor != null) return accessor.GetValue(targetObject); else return null; } } /************************************************************************************************************************/ /// <summary>Gets the value of the <see cref="SerializedProperty"/>.</summary> public static object GetValue(this SerializedProperty property) { return GetValue(property, property.serializedObject.targetObject); } /// <summary>Gets the value of the <see cref="SerializedProperty"/>.</summary> public static T GetValue<T>(this SerializedProperty property) { return (T)GetValue(property); } /// <summary>Gets the value of the <see cref="SerializedProperty"/>.</summary> public static void GetValue<T>(this SerializedProperty property, out T value) { value = (T)GetValue(property); } /************************************************************************************************************************/ /// <summary>Gets the value of the <see cref="SerializedProperty"/> for each of its target objects.</summary> public static T[] GetValues<T>(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 /************************************************************************************************************************/ /// <summary>Sets the value of the specified <see cref="SerializedProperty"/>.</summary> 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; } } /************************************************************************************************************************/ /// <summary>Sets the value of the <see cref="SerializedProperty"/>.</summary> 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 /************************************************************************************************************************/ /// <summary>Indicates whether both properties refer to the same underlying field.</summary> 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; } /************************************************************************************************************************/ /// <summary> /// Executes the `action` once with a new <see cref="SerializedProperty"/> for each of the /// <see cref="SerializedObject.targetObjects"/>. Or if there is only one target, it uses the `property`. /// </summary> public static void ForEachTarget(this SerializedProperty property, Action<SerializedProperty> 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(); } } } } /************************************************************************************************************************/ /// <summary> /// Adds a menu item to execute the specified `action` for each of the `property`s target objects. /// </summary> public static void AddPropertyModifierFunction(GenericMenu menu, SerializedProperty property, string label, Action<SerializedProperty> action) { menu.AddItem(new GUIContent(label), false, () => { ForEachTarget(property, action); }); } /************************************************************************************************************************/ /// <summary> /// 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. /// </summary> public static void ModifyValues<T>(this SerializedProperty property, Action<T> method, string undoName = "Inspector") { RecordUndo(property, undoName); var values = GetValues<T>(property); for (int i = 0; i < values.Length; i++) method(values[i]); OnPropertyChanged(property); } /************************************************************************************************************************/ /// <summary> /// Records the state of the specified `property` so it can be undone. /// </summary> public static void RecordUndo(this SerializedProperty property, string undoName = "Inspector") { Undo.RecordObjects(property.serializedObject.targetObjects, undoName); } /************************************************************************************************************************/ /// <summary> /// Updates the specified `property` and marks its target objects as dirty so any changes to a prefab will be saved. /// </summary> 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(); } /************************************************************************************************************************/ /// <summary> /// Returns the <see cref="SerializedPropertyType"/> that represents fields of the specified `type`. /// </summary> 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<Type, Dictionary<string, PropertyAccessor>> TypeToPathToAccessor = new Dictionary<Type, Dictionary<string, PropertyAccessor>>(); /************************************************************************************************************************/ /// <summary> /// Returns an <see cref="PropertyAccessor"/> that can be used to access the details of the specified `property`. /// </summary> 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); } /************************************************************************************************************************/ /// <summary> /// Returns an <see cref="PropertyAccessor"/> for a <see cref="SerializedProperty"/> with the specified `propertyPath` /// on the specified `type` of object. /// </summary> private static PropertyAccessor GetAccessor(string propertyPath, ref Type type) { Dictionary<string, PropertyAccessor> pathToAccessor; if (!TypeToPathToAccessor.TryGetValue(type, out pathToAccessor)) { pathToAccessor = new Dictionary<string, PropertyAccessor>(); 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; } /************************************************************************************************************************/ /// <summary> /// Returns a field with the specified `name` in the `declaringType` or any of its base types. /// </summary> 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 /************************************************************************************************************************/ /// <summary>[Editor-Only] /// A wrapper for accessing the underlying values and fields of a <see cref="SerializedProperty"/>. /// </summary> public class PropertyAccessor { /************************************************************************************************************************/ /// <summary>The accessor for the field which this accessor is nested inside.</summary> public readonly PropertyAccessor Parent; /// <summary>The field wrapped by this accessor.</summary> public readonly FieldInfo Field; /// <summary>The type of the wrapped <see cref="Field"/>.</summary> public readonly Type FieldType; /************************************************************************************************************************/ /// <summary>[Internal] Creates a new <see cref="PropertyAccessor"/>.</summary> internal PropertyAccessor(PropertyAccessor parent, FieldInfo field) : this(parent, field, field.FieldType) { } /// <summary>Creates a new <see cref="PropertyAccessor"/>.</summary> protected PropertyAccessor(PropertyAccessor parent, FieldInfo field, Type fieldType) { Parent = parent; Field = field; FieldType = fieldType; } /************************************************************************************************************************/ /// <summary> /// Gets the value of the from the <see cref="Parent"/> (if there is one), then uses it to get and return /// the value of the <see cref="Field"/>. /// </summary> public virtual object GetValue(object obj) { if (Parent != null) obj = Parent.GetValue(obj); if (ReferenceEquals(obj, null)) return null; return Field.GetValue(obj); } /// <summary> /// Gets the value of the from the <see cref="Parent"/> (if there is one), then uses it to get and return /// the value of the <see cref="Field"/>. /// </summary> public object GetValue(SerializedObject serializedObject) { return serializedObject != null ? GetValue(serializedObject.targetObject) : null; } /// <summary> /// Gets the value of the from the <see cref="Parent"/> (if there is one), then uses it to get and return /// the value of the <see cref="Field"/>. /// </summary> public object GetValue(SerializedProperty serializedProperty) { return serializedProperty != null ? GetValue(serializedProperty.serializedObject) : null; } /************************************************************************************************************************/ /// <summary> /// Gets the value of the from the <see cref="Parent"/> (if there is one), then uses it to set the value /// of the <see cref="Field"/>. /// </summary> public virtual void SetValue(object obj, object value) { if (Parent != null) obj = Parent.GetValue(obj); if (ReferenceEquals(obj, null)) return; Field.SetValue(obj, value); } /// <summary> /// Gets the value of the from the <see cref="Parent"/> (if there is one), then uses it to set the value /// of the <see cref="Field"/>. /// </summary> public void SetValue(SerializedObject serializedObject, object value) { if (serializedObject != null) SetValue(serializedObject.targetObject, value); } /// <summary> /// Gets the value of the from the <see cref="Parent"/> (if there is one), then uses it to set the value /// of the <see cref="Field"/>. /// </summary> public void SetValue(SerializedProperty serializedProperty, object value) { if (serializedProperty != null) SetValue(serializedProperty.serializedObject, value); } /************************************************************************************************************************/ /// <summary>Returns a description of this accessor's path.</summary> public override string ToString() { if (Parent != null) return Parent.ToString() + "." + Field.Name; else return Field.Name; } /************************************************************************************************************************/ /// <summary>Returns a this accessor's <see cref="SerializedProperty.propertyPath"/>.</summary> public virtual string GetPath() { if (Parent != null) return Parent.GetPath() + "." + Field.Name; else return Field.Name; } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region ArrayPropertyAccessor /************************************************************************************************************************/ /// <summary>[Editor-Only] /// An <see cref="PropertyAccessor"/> for a particular element in an array. /// </summary> public class ArrayPropertyAccessor : PropertyAccessor { /************************************************************************************************************************/ /// <summary>The index of the array element this accessor targets.</summary> public readonly int ElementIndex; /************************************************************************************************************************/ /// <summary>[Internal] Creates a new <see cref="ArrayPropertyAccessor"/>.</summary> internal ArrayPropertyAccessor(PropertyAccessor parent, FieldInfo field, int elementIndex) : base(parent, field, GetElementType(field.FieldType)) { ElementIndex = elementIndex; } /************************************************************************************************************************/ /// <summary>Returns the type of elements in the array.</summary> 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; } } /************************************************************************************************************************/ /// <summary> /// Gets the value of the from the <see cref="PropertyAccessor.Parent"/> (if there is one), then uses it to /// get and return the value of the <see cref="PropertyAccessor.Field"/>. /// </summary> 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; } /************************************************************************************************************************/ /// <summary> /// Gets the value of the from the <see cref="PropertyAccessor.Parent"/> (if there is one), then uses it to /// set the value of the <see cref="PropertyAccessor.Field"/>. /// </summary> 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."); } /************************************************************************************************************************/ /// <summary>Returns a description of this accessor's path.</summary> public override string ToString() { return string.Concat(base.ToString(), "[", ElementIndex.ToString(), "]"); } /************************************************************************************************************************/ /// <summary>Returns a this accessor's <see cref="SerializedProperty.propertyPath"/>.</summary> public override string GetPath() { return string.Concat(base.GetPath(), ArrayDataPrefix, ElementIndex.ToString(), ArrayDataSuffix); } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } #endif #endif