You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
CrowdControl/Assets/Plugins/Animancer/Internal/Editor Utilities/Serialization/Serialization.cs

830 lines
37 KiB
C#

// 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
{
/// <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 (Event.current.type != EventType.Layout && Event.current.type != EventType.Repaint) Debug.Log(accessor);
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 (ReferenceEquals(obj, null))
return null;
if (Parent != null)
obj = Parent.GetValue(obj);
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 (ReferenceEquals(obj, null))
return;
if (Parent != null)
obj = Parent.GetValue(obj);
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