chore: initial commit
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// A simple wrapper around <see cref="EditorPrefs"/> to get and set a bool.
|
||||
/// <para></para>
|
||||
/// If you're interested in a more comprehensive pref wrapper that supports more types, you should check out
|
||||
/// <see href="https://kybernetik.com.au/inspector-gadgets/docs/other/auto-prefs">Inspector Gadgets - Auto Prefs</see>.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/BoolPref
|
||||
///
|
||||
public class BoolPref
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The prefix which is automatically added before the <see cref="Key"/>.</summary>
|
||||
public const string KeyPrefix = nameof(Animancer) + "/";
|
||||
|
||||
/// <summary>The identifier with which this pref will be saved.</summary>
|
||||
public readonly string Key;
|
||||
|
||||
/// <summary>The label to use when adding a function to toggle this pref to a menu.</summary>
|
||||
public readonly string MenuItem;
|
||||
|
||||
/// <summary>The starting value to use for this pref if none was previously saved.</summary>
|
||||
public readonly bool DefaultValue;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private bool _HasValue;
|
||||
private bool _Value;
|
||||
|
||||
/// <summary>The current value of this pref.</summary>
|
||||
public bool Value
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_HasValue)
|
||||
{
|
||||
_HasValue = true;
|
||||
_Value = EditorPrefs.GetBool(Key, DefaultValue);
|
||||
}
|
||||
|
||||
return _Value;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_Value == value &&
|
||||
_HasValue)
|
||||
return;
|
||||
|
||||
_Value = value;
|
||||
_HasValue = true;
|
||||
EditorPrefs.SetBool(Key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns the current value of the `pref`.</summary>
|
||||
public static implicit operator bool(BoolPref pref)
|
||||
=> pref.Value;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="BoolPref"/>.</summary>
|
||||
public BoolPref(string menuItem, bool defaultValue = default)
|
||||
: this(null, menuItem, defaultValue) { }
|
||||
|
||||
/// <summary>Creates a new <see cref="BoolPref"/>.</summary>
|
||||
public BoolPref(string keyPrefix, string menuItem, bool defaultValue = default)
|
||||
{
|
||||
MenuItem = menuItem + " ?";
|
||||
Key = KeyPrefix + keyPrefix + menuItem;
|
||||
DefaultValue = defaultValue;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds a menu function to toggle the <see cref="Value"/> of this pref.</summary>
|
||||
public void AddToggleFunction(GenericMenu menu)
|
||||
{
|
||||
menu.AddItem(new(MenuItem), Value, () =>
|
||||
{
|
||||
Value = !Value;
|
||||
});
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a string containing the <see cref="Key"/> and <see cref="Value"/>.</summary>
|
||||
public override string ToString()
|
||||
=> $"{nameof(BoolPref)} (" +
|
||||
$"{nameof(Key)} = '{Key}'" +
|
||||
$", {nameof(Value)} = {Value})";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 55a4a1b506a9c7c4491880fdce5b69f7
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,132 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A <see cref="SerializableAttribute"/> reference to a <see cref="Type"/>.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/SerializableTypeReference
|
||||
[Serializable]
|
||||
public struct SerializableTypeReference : ISerializationCallbackReceiver
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private string _QualifiedName;
|
||||
|
||||
private Type _Type;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The <see cref="Type.AssemblyQualifiedName"/>.</summary>
|
||||
public string QualifiedName
|
||||
{
|
||||
readonly get => _QualifiedName;
|
||||
set
|
||||
{
|
||||
if (_QualifiedName == value)
|
||||
return;
|
||||
|
||||
_QualifiedName = value;
|
||||
_Type = null;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The referenced type.</summary>
|
||||
public Type Type
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_Type == null && !string.IsNullOrEmpty(_QualifiedName))
|
||||
_Type = Type.GetType(_QualifiedName);
|
||||
|
||||
return _Type;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_Type == value)
|
||||
return;
|
||||
|
||||
_QualifiedName = value?.AssemblyQualifiedName;
|
||||
_Type = value;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="SerializableTypeReference"/>.</summary>
|
||||
public SerializableTypeReference(string qualifiedName)
|
||||
{
|
||||
_QualifiedName = qualifiedName;
|
||||
_Type = null;
|
||||
}
|
||||
|
||||
/// <summary>Creates a new <see cref="SerializableTypeReference"/>.</summary>
|
||||
public SerializableTypeReference(Type type)
|
||||
{
|
||||
_QualifiedName = type?.AssemblyQualifiedName;
|
||||
_Type = type;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public readonly void OnBeforeSerialize() { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnAfterDeserialize()
|
||||
=> _Type = null;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Drawer
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] A <see cref="PropertyDrawer"/> for <see cref="SerializableTypeReference"/>.</summary>
|
||||
[CustomPropertyDrawer(typeof(SerializableTypeReference))]
|
||||
public class Drawer : PropertyDrawer
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
=> AnimancerGUI.LineHeight;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
var name = property.FindPropertyRelative(nameof(_QualifiedName));
|
||||
|
||||
var spacing = AnimancerGUI.StandardSpacing;
|
||||
var pickerArea = AnimancerGUI.StealFromRight(ref area, area.height + spacing, spacing);
|
||||
|
||||
name.stringValue = EditorGUI.TextField(area, label, name.stringValue);
|
||||
|
||||
var indentLevel = EditorGUI.indentLevel;
|
||||
EditorGUI.indentLevel = 0;
|
||||
|
||||
var picked = EditorGUI.ObjectField(pickerArea, null, typeof(Object), true);
|
||||
if (picked != null)
|
||||
name.stringValue = picked.GetType().AssemblyQualifiedName;
|
||||
|
||||
EditorGUI.indentLevel = indentLevel;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3fd94310174b9d469b36e945bd7f4a1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,171 @@
|
||||
// Serialization // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
// Shared File Last Modified: 2026-01-17.
|
||||
namespace Animancer.Editor
|
||||
// namespace InspectorGadgets.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] Various serialization utilities.</summary>
|
||||
public partial class Serialization
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// Directly serializing an <see cref="UnityEngine.Object"/> reference doesn't always work (such as with scene
|
||||
/// objects when entering Play Mode), so this class also serializes their instance ID and uses that if the
|
||||
/// direct reference fails.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ObjectReference
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField] private Object _Object;
|
||||
#if UNITY_6000_3_OR_NEWER
|
||||
[SerializeField] private EntityId _EntityID;
|
||||
#else
|
||||
[SerializeField] private int _InstanceID;
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The referenced <see cref="SerializedObject"/>.</summary>
|
||||
public Object Object
|
||||
{
|
||||
get
|
||||
{
|
||||
Initialize();
|
||||
return _Object;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The <see cref="Object.GetInstanceID"/>.</summary>
|
||||
#if UNITY_6000_3_OR_NEWER
|
||||
public EntityId EntityID => _EntityID;
|
||||
#else
|
||||
public int InstanceID => _InstanceID;
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ObjectReference"/> which wraps the specified
|
||||
/// <see cref="UnityEngine.Object"/>.
|
||||
/// </summary>
|
||||
public ObjectReference(Object obj)
|
||||
{
|
||||
_Object = obj;
|
||||
if (obj != null)
|
||||
{
|
||||
#if UNITY_6000_3_OR_NEWER
|
||||
_EntityID = obj.GetEntityId();
|
||||
#else
|
||||
_InstanceID = obj.GetInstanceID();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
#if UNITY_6000_3_OR_NEWER
|
||||
if (_Object == null)
|
||||
_Object = EditorUtility.EntityIdToObject(_EntityID);
|
||||
else
|
||||
_EntityID = _Object.GetEntityId();
|
||||
#else
|
||||
if (_Object == null)
|
||||
_Object = EditorUtility.InstanceIDToObject(_InstanceID);
|
||||
else
|
||||
_InstanceID = _Object.GetInstanceID();
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ObjectReference"/> which wraps the specified
|
||||
/// <see cref="UnityEngine.Object"/>.
|
||||
/// </summary>
|
||||
public static implicit operator ObjectReference(Object obj)
|
||||
=> new(obj);
|
||||
|
||||
/// <summary>Returns the target <see cref="Object"/>.</summary>
|
||||
public static implicit operator Object(ObjectReference reference)
|
||||
=> reference.Object;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new array of <see cref="ObjectReference"/>s representing the `objects`.</summary>
|
||||
public static ObjectReference[] Convert(params Object[] objects)
|
||||
{
|
||||
var references = new ObjectReference[objects.Length];
|
||||
for (int i = 0; i < objects.Length; i++)
|
||||
references[i] = objects[i];
|
||||
return references;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new array of <see cref="UnityEngine.Object"/>s containing the target <see cref="Object"/> of each
|
||||
/// of the `references`.
|
||||
/// </summary>
|
||||
public static Object[] Convert(params ObjectReference[] references)
|
||||
{
|
||||
var objects = new Object[references.Length];
|
||||
for (int i = 0; i < references.Length; i++)
|
||||
objects[i] = references[i];
|
||||
return objects;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Indicates whether both arrays refer to the same set of objects.</summary>
|
||||
public static bool AreSameObjects(ObjectReference[] references, Object[] objects)
|
||||
{
|
||||
if (references == null)
|
||||
return objects == null;
|
||||
|
||||
if (objects == null)
|
||||
return false;
|
||||
|
||||
if (references.Length != objects.Length)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < references.Length; i++)
|
||||
{
|
||||
if (references[i] != objects[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a string describing this object.</summary>
|
||||
public override string ToString()
|
||||
#if UNITY_6000_3_OR_NEWER
|
||||
=> $"Serialization.ObjectReference [{_EntityID}] {_Object}";
|
||||
#else
|
||||
=> $"Serialization.ObjectReference [{_InstanceID}] {_Object}";
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns true if the `reference` and <see cref="ObjectReference.Object"/> are not null.</summary>
|
||||
public static bool IsValid(this ObjectReference reference)
|
||||
=> reference?.Object != null;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9ff1cc079e8ef704a81b78998b39de92
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,287 @@
|
||||
// Serialization // Copyright 2018-2025 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
// Shared File Last Modified: 2023-08-12.
|
||||
namespace Animancer.Editor
|
||||
// namespace InspectorGadgets.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] Various serialization utilities.</summary>
|
||||
public partial class Serialization
|
||||
{
|
||||
/// <summary>[Editor-Only] A serializable reference to a <see cref="SerializedProperty"/>.</summary>
|
||||
[Serializable]
|
||||
public class PropertyReference
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField] private ObjectReference[] _TargetObjects;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The <see cref="SerializedObject.targetObject"/>.</summary>
|
||||
public ObjectReference TargetObject
|
||||
{
|
||||
get
|
||||
{
|
||||
return _TargetObjects != null && _TargetObjects.Length > 0 ?
|
||||
_TargetObjects[0] : null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The <see cref="SerializedObject.targetObjects"/>.</summary>
|
||||
public ObjectReference[] TargetObjects => _TargetObjects;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField] private ObjectReference _Context;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The <see cref="SerializedObject.context"/>.</summary>
|
||||
public ObjectReference Context => _Context;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField] private string _PropertyPath;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The <see cref="SerializedProperty.propertyPath"/>.</summary>
|
||||
public string PropertyPath => _PropertyPath;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[NonSerialized] private bool _IsInitialized;
|
||||
|
||||
/// <summary>Indicates whether the <see cref="Property"/> has been accessed.</summary>
|
||||
public bool IsInitialized => _IsInitialized;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[NonSerialized] private SerializedProperty _Property;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The referenced <see cref="SerializedProperty"/>.</summary>
|
||||
public SerializedProperty Property
|
||||
{
|
||||
get
|
||||
{
|
||||
Initialize();
|
||||
return _Property;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="PropertyReference"/> which wraps the specified `property`.
|
||||
/// </summary>
|
||||
public PropertyReference(SerializedProperty property)
|
||||
{
|
||||
_TargetObjects = ObjectReference.Convert(property.serializedObject.targetObjects);
|
||||
|
||||
_Context = property.serializedObject.context;
|
||||
_PropertyPath = property.propertyPath;
|
||||
|
||||
// Don't set the _Property. If it gets accessed we want to create out own instance.
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="PropertyReference"/> which wraps the specified `property`.
|
||||
/// </summary>
|
||||
public static implicit operator PropertyReference(SerializedProperty property)
|
||||
=> new(property);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the target <see cref="Property"/>.
|
||||
/// </summary>
|
||||
public static implicit operator SerializedProperty(PropertyReference reference)
|
||||
=> reference.Property;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
if (_IsInitialized)
|
||||
{
|
||||
if (!TargetsExist)
|
||||
Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
_IsInitialized = true;
|
||||
|
||||
if (string.IsNullOrEmpty(_PropertyPath) ||
|
||||
!TargetsExist)
|
||||
return;
|
||||
|
||||
var targetObjects = ObjectReference.Convert(_TargetObjects);
|
||||
var serializedObject = new SerializedObject(targetObjects, _Context);
|
||||
_Property = serializedObject.FindProperty(_PropertyPath);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Do the specified `property` and `targetObjects` match the targets of this reference?</summary>
|
||||
public bool IsTarget(SerializedProperty property, Object[] targetObjects)
|
||||
{
|
||||
if (_Property == null ||
|
||||
_Property.propertyPath != property.propertyPath ||
|
||||
_TargetObjects == null ||
|
||||
_TargetObjects.Length != targetObjects.Length)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < _TargetObjects.Length; i++)
|
||||
{
|
||||
if (_TargetObjects[i] != targetObjects[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Is there is at least one target and none of them are <c>null</c>?</summary>
|
||||
private bool TargetsExist
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_TargetObjects == null ||
|
||||
_TargetObjects.Length == 0)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < _TargetObjects.Length; i++)
|
||||
{
|
||||
if (_TargetObjects[i].Object == null)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="SerializedObject.Update"/> if the <see cref="Property"/> has been initialized.
|
||||
/// </summary>
|
||||
public void Update()
|
||||
{
|
||||
if (_Property == null)
|
||||
return;
|
||||
|
||||
if (!TargetsExist)
|
||||
{
|
||||
Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
_Property.serializedObject.Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="SerializedObject.ApplyModifiedProperties"/> if the <see cref="Property"/> has been initialized.
|
||||
/// </summary>
|
||||
public void ApplyModifiedProperties()
|
||||
{
|
||||
if (_Property == null)
|
||||
return;
|
||||
|
||||
if (!TargetsExist)
|
||||
{
|
||||
Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
_Property.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="SerializedObject.Dispose"/> if the <see cref="Property"/> has been initialized.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_Property != null)
|
||||
{
|
||||
_Property.serializedObject.Dispose();
|
||||
_Property = null;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gets the height needed to draw the target property.</summary>
|
||||
public float GetPropertyHeight()
|
||||
{
|
||||
if (_Property == null)
|
||||
return 0;
|
||||
|
||||
return EditorGUI.GetPropertyHeight(_Property, _Property.isExpanded);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the target object within the specified `area`.</summary>
|
||||
public void DoTargetGUI(Rect area)
|
||||
{
|
||||
area.height = EditorGUIUtility.singleLineHeight;
|
||||
|
||||
Initialize();
|
||||
|
||||
if (_Property == null)
|
||||
{
|
||||
GUI.Label(area, "Missing " + this);
|
||||
return;
|
||||
}
|
||||
|
||||
var targets = _Property.serializedObject.targetObjects;
|
||||
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
{
|
||||
var showMixedValue = EditorGUI.showMixedValue;
|
||||
EditorGUI.showMixedValue = targets.Length > 1;
|
||||
|
||||
var target = targets.Length > 0 ? targets[0] : null;
|
||||
EditorGUI.ObjectField(area, target, typeof(Object), true);
|
||||
|
||||
EditorGUI.showMixedValue = showMixedValue;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws the target property within the specified `area`.</summary>
|
||||
public void DoPropertyGUI(Rect area)
|
||||
{
|
||||
Initialize();
|
||||
|
||||
if (_Property == null)
|
||||
return;
|
||||
|
||||
_Property.serializedObject.Update();
|
||||
|
||||
GUI.BeginGroup(area);
|
||||
area.x = area.y = 0;
|
||||
|
||||
EditorGUI.PropertyField(area, _Property, _Property.isExpanded);
|
||||
|
||||
GUI.EndGroup();
|
||||
|
||||
_Property.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns true if the `reference` and <see cref="PropertyReference.Property"/> are not null.</summary>
|
||||
public static bool IsValid(this PropertyReference reference) => reference?.Property != null;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7e12894d2c9d204ab20b16c6091b5e2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3276da8ca0ff29d4ead727e9fc1b1d12
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,96 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only] A wrapper around a <see cref="SerializedProperty"/> representing an array field.</summary>
|
||||
public class SerializedArrayProperty
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private SerializedProperty _Property;
|
||||
|
||||
/// <summary>The target property.</summary>
|
||||
public SerializedProperty Property
|
||||
{
|
||||
get => _Property;
|
||||
set
|
||||
{
|
||||
_Property = value;
|
||||
Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private string _Path;
|
||||
|
||||
/// <summary>The cached <see cref="SerializedProperty.propertyPath"/> of the <see cref="Property"/>.</summary>
|
||||
public string Path => _Path ?? (_Path = Property.propertyPath);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private int _Count;
|
||||
|
||||
/// <summary>The cached <see cref="SerializedProperty.arraySize"/> of the <see cref="Property"/>.</summary>
|
||||
public int Count
|
||||
{
|
||||
get => _Count;
|
||||
set => Property.arraySize = _Count = value;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private bool _HasMultipleDifferentValues;
|
||||
private bool _GotHasMultipleDifferentValues;
|
||||
|
||||
/// <summary>The cached <see cref="SerializedProperty.hasMultipleDifferentValues"/> of the <see cref="Property"/>.</summary>
|
||||
public bool HasMultipleDifferentValues
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_GotHasMultipleDifferentValues)
|
||||
{
|
||||
_GotHasMultipleDifferentValues = true;
|
||||
_HasMultipleDifferentValues = Property.hasMultipleDifferentValues;
|
||||
}
|
||||
|
||||
return _HasMultipleDifferentValues;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Updates the cached <see cref="Count"/> and <see cref="HasMultipleDifferentValues"/>.</summary>
|
||||
public void Refresh()
|
||||
{
|
||||
_Path = null;
|
||||
_Count = _Property != null ? _Property.arraySize : 0;
|
||||
_GotHasMultipleDifferentValues = false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="SerializedProperty.GetArrayElementAtIndex"/> on the <see cref="Property"/>.</summary>
|
||||
/// <remarks>
|
||||
/// Returns <c>null</c> if the element is not actually a child of the <see cref="Property"/>, which can happen
|
||||
/// if multiple objects are selected with different array sizes.
|
||||
/// </remarks>
|
||||
public SerializedProperty GetElement(int index)
|
||||
{
|
||||
var element = Property.GetArrayElementAtIndex(index);
|
||||
if (!HasMultipleDifferentValues || element.propertyPath.StartsWith(Path))
|
||||
return element;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 445a76dd5b9602c4199e5f3315c30e18
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,81 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// A <see cref="SerializedDataEditorWindow{TObject, TData}"/> for <see cref="Component"/>s.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/SerializedComponentDataEditorWindow_2
|
||||
public abstract class SerializedComponentDataEditorWindow<TObject, TData> :
|
||||
SerializedDataEditorWindow<TObject, TData>
|
||||
where TObject : Component
|
||||
where TData : class, ICopyable<TData>, IEquatable<TData>, new()
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField] private GameObject _SourceGameObject;
|
||||
[SerializeField] private int _SourceComponentInstanceID;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override TObject SourceObject
|
||||
{
|
||||
get
|
||||
{
|
||||
// For whatever reason, component references in an EditorWindow can't survive entering Play Mode but
|
||||
// a GameObject or Transform reference can so we use that to recover the component.
|
||||
|
||||
// Storing the Instance ID also works, but seems to also survive restarting the Unity Editor which is
|
||||
// bad because the scene references inside the data don't survive that which would leave us
|
||||
// with an open window full of empty references. Working around that isn't worth the effort.
|
||||
|
||||
// So if the GameObject still exists, we use the Component's Instance ID to find it.
|
||||
|
||||
var source = base.SourceObject;
|
||||
|
||||
if (source == null && _SourceGameObject != null)
|
||||
{
|
||||
#if UNITY_6000_3_OR_NEWER
|
||||
var component = EditorUtility.EntityIdToObject(_SourceComponentInstanceID);
|
||||
#else
|
||||
var component = EditorUtility.InstanceIDToObject(_SourceComponentInstanceID);
|
||||
#endif
|
||||
source = base.SourceObject = component as TObject;
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool SourceObjectMightBePrefab
|
||||
=> true;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void CaptureData()
|
||||
{
|
||||
base.CaptureData();
|
||||
|
||||
if (SourceObject != null)
|
||||
{
|
||||
_SourceGameObject = SourceObject.gameObject;
|
||||
_SourceComponentInstanceID = SourceObject.GetInstanceID();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 04e51a8d149b753488a049d42b0e7a96
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,445 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// A window for managing a copy of some serialized data and applying or reverting it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This system assumes the implementation of <see cref="IEquatable{T}"/>
|
||||
/// compares the values of all fields in <typeparamref name="TData"/>.
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/SerializedDataEditorWindow_2
|
||||
public abstract class SerializedDataEditorWindow<TObject, TData> : EditorWindow
|
||||
where TObject : Object
|
||||
where TData : class, ICopyable<TData>, IEquatable<TData>, new()
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private TObject _SourceObject;
|
||||
|
||||
/// <summary>The object which contains the data this class manages.</summary>
|
||||
/// <remarks><see cref="SetAndCaptureSource"/> should generally be used instead of setting this property directly.</remarks>
|
||||
public virtual TObject SourceObject
|
||||
{
|
||||
get => _SourceObject;
|
||||
protected set => _SourceObject = value;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="Data"/> field of the <see cref="SourceObject"/>.</summary>
|
||||
public abstract TData SourceData { get; set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private TData _Data;
|
||||
|
||||
/// <summary>A copy of the <see cref="SourceData"/> being managed by this window.</summary>
|
||||
public ref TData Data
|
||||
=> ref _Data;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Is the <see cref="Data"/> managed by this window different to the <see cref="SourceData"/>?</summary>
|
||||
public virtual bool HasDataChanged
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_Data == null)
|
||||
return false;
|
||||
|
||||
var sourceData = SourceData;
|
||||
return sourceData != null && !_Data.Equals(sourceData);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogException(exception);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Initializes this window.</summary>
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
||||
EditorApplication.wantsToQuit += OnTryCloseEditor;
|
||||
Undo.undoRedoPerformed += Repaint;
|
||||
}
|
||||
|
||||
/// <summary>Cleans up this window.</summary>
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
||||
EditorApplication.wantsToQuit -= OnTryCloseEditor;
|
||||
Undo.undoRedoPerformed -= Repaint;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Prompts the user to <see cref="Apply"/> or <see cref="Revert"/>
|
||||
/// if there are changes in the <see cref="Data"/> when this window is closed.
|
||||
/// </summary>
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
var sourceObject = SourceObject;
|
||||
if (sourceObject == null ||
|
||||
!HasDataChanged ||
|
||||
titleContent == null)
|
||||
return;
|
||||
|
||||
if (EditorUtility.DisplayDialog(
|
||||
titleContent.text,
|
||||
$"Apply unsaved changes to '{sourceObject.name}'?",
|
||||
"Apply",
|
||||
"Revert"))
|
||||
{
|
||||
Apply();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Called before closing the Unity Editor to confirm that un-saved data is applied.</summary>
|
||||
private bool OnTryCloseEditor()
|
||||
{
|
||||
var sourceObject = SourceObject;
|
||||
if (sourceObject == null ||
|
||||
!HasDataChanged ||
|
||||
titleContent == null)
|
||||
return true;
|
||||
|
||||
var option = EditorUtility.DisplayDialogComplex(
|
||||
titleContent.text,
|
||||
$"Apply unsaved changes to '{sourceObject.name}'?",
|
||||
"Apply",
|
||||
"Cancel",
|
||||
"Revert");
|
||||
|
||||
switch (option)
|
||||
{
|
||||
case 0:// Apply.
|
||||
Apply();
|
||||
return true;
|
||||
|
||||
case 2:// Revert.
|
||||
Revert();
|
||||
return true;
|
||||
|
||||
case 1:// Cancel.
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="SourceObject"/> and captures the <see cref="Data"/>
|
||||
/// as a copy of its <see cref="SourceData"/>.
|
||||
/// </summary>
|
||||
protected void SetAndCaptureSource(TObject sourceObject)
|
||||
{
|
||||
_SourceObject = sourceObject;
|
||||
CaptureData();
|
||||
Repaint();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Override this to return <c>true</c> if the <see cref="SourceObject"/> could be part of a prefab
|
||||
/// to ensure that modifications are serialized properly.
|
||||
/// </summary>
|
||||
public virtual bool SourceObjectMightBePrefab
|
||||
=> false;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Saves the edited <see cref="Data"/> into the <see cref="SourceObject"/>.</summary>
|
||||
public virtual void Apply()
|
||||
{
|
||||
var sourceObject = SourceObject;
|
||||
if (sourceObject == null)
|
||||
return;
|
||||
|
||||
using (new ModifySerializedField(sourceObject, name, SourceObjectMightBePrefab))
|
||||
{
|
||||
SourceData = _Data.CopyableClone();
|
||||
|
||||
if (EditorUtility.IsPersistent(SourceObject))
|
||||
{
|
||||
var objects = SetPool.Acquire<Object>();
|
||||
GatherObjectReferences(sourceObject, objects);
|
||||
|
||||
foreach (var obj in objects)
|
||||
if (!EditorUtility.IsPersistent(obj))
|
||||
AssetDatabase.AddObjectToAsset(obj, SourceObject);
|
||||
|
||||
SetPool.Release(objects);
|
||||
}
|
||||
}
|
||||
|
||||
Repaint();
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gathers all objects referenced by the `root`.</summary>
|
||||
public static void GatherObjectReferences(Object root, HashSet<Object> objects)
|
||||
{
|
||||
using var serializedObject = new SerializedObject(root);
|
||||
var property = serializedObject.GetIterator();
|
||||
while (property.Next(true))
|
||||
{
|
||||
if (property.propertyType == SerializedPropertyType.ObjectReference)
|
||||
{
|
||||
var value = property.objectReferenceValue;
|
||||
if (value != null)
|
||||
objects.Add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Restores the <see cref="Data"/> to the original values from the <see cref="SourceData"/>.</summary>
|
||||
public virtual void Revert()
|
||||
{
|
||||
RecordUndo();
|
||||
CaptureData();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Stores a copy of the <see cref="SourceData"/> in the <see cref="Data"/>.</summary>
|
||||
protected virtual void CaptureData()
|
||||
{
|
||||
_Data = SourceData?.CopyableClone() ?? new();
|
||||
AnimancerReflection.TryInvoke(_Data, "OnValidate");
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Records the current state of this window so it can be undone later.</summary>
|
||||
public TData RecordUndo()
|
||||
=> RecordUndo(titleContent.text);
|
||||
|
||||
/// <summary>Records the current state of this window so it can be undone later.</summary>
|
||||
public virtual TData RecordUndo(string name)
|
||||
{
|
||||
Undo.RecordObject(this, name);
|
||||
Repaint();
|
||||
return _Data;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Opens a new <typeparamref name="TWindow"/> for the `sourceObject`
|
||||
/// or gives focus to an existing window that was already displaying it.
|
||||
/// </summary>
|
||||
public static TWindow Open<TWindow>(
|
||||
TObject sourceObject,
|
||||
bool onlyOneWindow = false,
|
||||
params Type[] desiredDockNextTo)
|
||||
where TWindow : SerializedDataEditorWindow<TObject, TData>
|
||||
{
|
||||
if (!onlyOneWindow)
|
||||
{
|
||||
foreach (var window in Resources.FindObjectsOfTypeAll<TWindow>())
|
||||
{
|
||||
if (window.SourceObject == sourceObject)
|
||||
{
|
||||
window.Show();
|
||||
window.SetAndCaptureSource(sourceObject);
|
||||
window.Focus();
|
||||
return window;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var newWindow = onlyOneWindow
|
||||
? GetWindow<TWindow>(desiredDockNextTo ?? Type.EmptyTypes)
|
||||
: CreateInstance<TWindow>();
|
||||
newWindow.Show();
|
||||
newWindow.SetAndCaptureSource(sourceObject);
|
||||
return newWindow;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Auto Apply
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="EditorPrefs"/> key for <see cref="AutoApply"/>.</summary>
|
||||
protected virtual string AutoApplyPref
|
||||
=> $"{titleContent.text}.{nameof(AutoApply)}";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private bool _HasLoadedAutoApply;
|
||||
private bool _AutoApply;
|
||||
private bool _EnabledAutoApplyInPlayMode;
|
||||
|
||||
/// <summary>Should changes be automatically applied as soon as they're made?</summary>
|
||||
public bool AutoApply
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_HasLoadedAutoApply)
|
||||
{
|
||||
_HasLoadedAutoApply = true;
|
||||
_AutoApply = EditorPrefs.GetBool(AutoApplyPref);
|
||||
}
|
||||
|
||||
return _AutoApply;
|
||||
}
|
||||
set
|
||||
{
|
||||
_HasLoadedAutoApply = true;
|
||||
_AutoApply = value;
|
||||
_EnabledAutoApplyInPlayMode = _AutoApply && EditorApplication.isPlayingOrWillChangePlaymode;
|
||||
EditorPrefs.SetBool(AutoApplyPref, value);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Handles entering and exiting Play Mode.</summary>
|
||||
protected virtual void OnPlayModeStateChanged(PlayModeStateChange change)
|
||||
{
|
||||
switch (change)
|
||||
{
|
||||
case PlayModeStateChange.EnteredPlayMode:
|
||||
if (HasDataChanged && focusedWindow != null)
|
||||
focusedWindow.ShowNotification(new($"{titleContent.text} window has un-applied changes"));
|
||||
break;
|
||||
|
||||
case PlayModeStateChange.ExitingPlayMode:
|
||||
if (_EnabledAutoApplyInPlayMode)
|
||||
AutoApply = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region GUI
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly GUIContent
|
||||
RevertLabel = new(
|
||||
"Revert",
|
||||
"Undo all changes made in this window"),
|
||||
ApplyLabel = new(
|
||||
"Apply",
|
||||
"Apply all changes made in this window to the source object"),
|
||||
AutoApplyLabel = new(
|
||||
"Auto",
|
||||
"Immediately apply all changes made in this window to the source object?" +
|
||||
"\n\nIf enabled in Play Mode, this toggle will be disabled when returning to Edit Mode.");
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the pixel width required for
|
||||
/// <see cref="DoApplyRevertGUI(Rect, Rect, Rect, ButtonGroupStyles)"/>.
|
||||
/// </summary>
|
||||
public float CalculateApplyRevertWidth(ButtonGroupStyles styles = default)
|
||||
{
|
||||
styles.CopyMissingStyles(ButtonGroupStyles.Button);
|
||||
return
|
||||
styles.left.CalculateWidth(RevertLabel) + 1 +
|
||||
styles.middle.CalculateWidth(ApplyLabel) + 1 +
|
||||
styles.right.CalculateWidth(AutoApplyLabel) + 1;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws GUI controls for <see cref="Revert"/>, <see cref="Apply"/>, and <see cref="AutoApply"/>.</summary>
|
||||
public void DoApplyRevertGUI(ButtonGroupStyles styles = default)
|
||||
{
|
||||
styles.CopyMissingStyles(ButtonGroupStyles.Button);
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
|
||||
var leftArea = GUILayoutUtility.GetRect(RevertLabel, styles.left);
|
||||
var middleArea = GUILayoutUtility.GetRect(ApplyLabel, styles.middle);
|
||||
var rightArea = GUILayoutUtility.GetRect(AutoApplyLabel, styles.right);
|
||||
|
||||
DoApplyRevertGUI(leftArea, middleArea, rightArea, styles);
|
||||
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws GUI controls for <see cref="Revert"/>, <see cref="Apply"/>, and <see cref="AutoApply"/>.</summary>
|
||||
public void DoApplyRevertGUI(Rect area, ButtonGroupStyles styles = default)
|
||||
{
|
||||
styles.CopyMissingStyles(ButtonGroupStyles.Button);
|
||||
|
||||
var leftArea = AnimancerGUI.StealFromLeft(ref area, styles.left.CalculateWidth(RevertLabel) + 1);
|
||||
var middleArea = AnimancerGUI.StealFromLeft(ref area, styles.middle.CalculateWidth(ApplyLabel) + 1);
|
||||
|
||||
DoApplyRevertGUI(leftArea, middleArea, area, styles);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws GUI controls for <see cref="Revert"/>, <see cref="Apply"/>, and <see cref="AutoApply"/>.</summary>
|
||||
public void DoApplyRevertGUI(
|
||||
Rect leftArea,
|
||||
Rect middleArea,
|
||||
Rect rightArea,
|
||||
ButtonGroupStyles styles = default)
|
||||
{
|
||||
styles.CopyMissingStyles(ButtonGroupStyles.Button);
|
||||
|
||||
var enabled = GUI.enabled;
|
||||
GUI.enabled = SourceObject != null && HasDataChanged;
|
||||
|
||||
// Revert.
|
||||
if (GUI.Button(leftArea, RevertLabel, styles.left))
|
||||
Revert();
|
||||
|
||||
// Apply.
|
||||
if (GUI.Button(middleArea, ApplyLabel, styles.middle))
|
||||
Apply();
|
||||
|
||||
// Auto Apply.
|
||||
var autoApply = AutoApply;
|
||||
if (autoApply && GUI.enabled)
|
||||
Apply();
|
||||
|
||||
GUI.enabled = enabled;
|
||||
|
||||
if (autoApply != GUI.Toggle(rightArea, autoApply, AutoApplyLabel, styles.right))
|
||||
AutoApply = !autoApply;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bfcdc4da46a31354690b49451d3ee75d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,188 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// A system which gathers information about <see cref="SerializeReference"/> fields to detect when multiple fields
|
||||
/// are referencing the same object.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/SharedReferenceCache
|
||||
public class SharedReferenceCache :
|
||||
IEnumerable<KeyValuePair<object, List<SharedReferenceCache.Field>>>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Static Caching
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly Dictionary<SerializedObject, SharedReferenceCache>
|
||||
SerializedObjectToCache = new();
|
||||
|
||||
/// <summary>Returns a cached <see cref="SharedReferenceCache"/> for the `serializedObject`.</summary>
|
||||
public static SharedReferenceCache Get(SerializedObject serializedObject)
|
||||
{
|
||||
CheckFlush(serializedObject);
|
||||
|
||||
if (!SerializedObjectToCache.TryGetValue(serializedObject, out var cache))
|
||||
SerializedObjectToCache.Add(serializedObject, cache = new(serializedObject));
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly HashSet<SerializedObject>
|
||||
NotRecentlyUsed = new();
|
||||
|
||||
private const double
|
||||
FlushInterval = 5;
|
||||
|
||||
private static double
|
||||
_LastFlushTime;
|
||||
|
||||
/// <summary>Discards any caches not used during the last <see cref="FlushInterval"/> when it elapses.</summary>
|
||||
private static void CheckFlush(SerializedObject serializedObject)
|
||||
{
|
||||
var currentTime = EditorApplication.timeSinceStartup;
|
||||
|
||||
if (currentTime >= _LastFlushTime + FlushInterval)
|
||||
{
|
||||
_LastFlushTime = currentTime;
|
||||
|
||||
foreach (var unused in NotRecentlyUsed)
|
||||
SerializedObjectToCache.Remove(unused);
|
||||
|
||||
NotRecentlyUsed.Clear();
|
||||
NotRecentlyUsed.UnionWith(SerializedObjectToCache.Keys);
|
||||
}
|
||||
|
||||
NotRecentlyUsed.Remove(serializedObject);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The number of editor updates that have occurred since startup.</summary>
|
||||
public static ulong FrameCount { get; private set; }
|
||||
|
||||
static SharedReferenceCache()
|
||||
{
|
||||
EditorApplication.update += () => FrameCount++;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Information about a field.</summary>
|
||||
public struct Field
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="Serialization.GetFriendlyPath"/> of the field.</summary>
|
||||
public string path;
|
||||
|
||||
/// <summary>The area where the field was last drawn.</summary>
|
||||
public Rect area;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="Field"/>.</summary>
|
||||
public Field(string path)
|
||||
{
|
||||
this.path = path;
|
||||
area = default;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private readonly SerializedObject
|
||||
SerializedObject;
|
||||
|
||||
private readonly Dictionary<object, List<Field>>
|
||||
ObjectToReferences = new();
|
||||
|
||||
private ulong
|
||||
_LastGatherFrameCount;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="SharedReferenceCache"/>.</summary>
|
||||
public SharedReferenceCache(SerializedObject serializedObject)
|
||||
{
|
||||
SerializedObject = serializedObject;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Should <see cref="GatherReferences"/> be called?</summary>
|
||||
public bool ShouldGather
|
||||
=> _LastGatherFrameCount != FrameCount;
|
||||
|
||||
/// <summary>Updates the cached reference info.</summary>
|
||||
public void GatherReferences()
|
||||
{
|
||||
_LastGatherFrameCount = FrameCount;
|
||||
|
||||
ObjectToReferences.Clear();
|
||||
|
||||
var property = SerializedObject.GetIterator();
|
||||
while (property.Next(true))
|
||||
{
|
||||
if (property.propertyType != SerializedPropertyType.ManagedReference)
|
||||
continue;
|
||||
|
||||
var reference = property.managedReferenceValue;
|
||||
if (reference == null)
|
||||
continue;
|
||||
|
||||
if (!ObjectToReferences.TryGetValue(reference, out var paths))
|
||||
ObjectToReferences.Add(reference, paths = new());
|
||||
|
||||
paths.Add(new(property.GetFriendlyPath()));
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Tries to get the info about all fields containing the `reference`.</summary>
|
||||
public bool TryGetInfo(object reference, out List<Field> references)
|
||||
{
|
||||
if (ShouldGather)
|
||||
GatherReferences();
|
||||
|
||||
return ObjectToReferences.TryGetValue(reference, out references);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns an enumerator for all references and their info.</summary>
|
||||
public Dictionary<object, List<Field>>.Enumerator GetEnumerator()
|
||||
{
|
||||
if (ShouldGather)
|
||||
GatherReferences();
|
||||
|
||||
return ObjectToReferences.GetEnumerator();
|
||||
}
|
||||
|
||||
/// <summary>Returns an enumerator for all references and their info.</summary>
|
||||
IEnumerator<KeyValuePair<object, List<Field>>> IEnumerable<KeyValuePair<object, List<Field>>>.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
/// <summary>Returns an enumerator for all references and their info.</summary>
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d6e2d2a5c363854194c80c149d996a2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,165 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.Editor
|
||||
{
|
||||
/// <summary>[Editor-Only]
|
||||
/// Stores data which needs to survive assembly reloading (such as from script compilation), but can be discarded
|
||||
/// when the Unity Editor is closed.
|
||||
/// </summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/TemporarySettings
|
||||
internal class TemporarySettings : ScriptableObject
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Instance
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static TemporarySettings _Instance;
|
||||
|
||||
/// <summary>Finds an existing instance of this class or creates a new one.</summary>
|
||||
private static TemporarySettings Instance
|
||||
=> AnimancerEditorUtilities.FindOrCreate(
|
||||
ref _Instance,
|
||||
HideFlags.HideAndDontSave | HideFlags.DontUnloadUnusedAsset);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
OnEnableSelection();
|
||||
}
|
||||
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
OnDisableSelection();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Event Selection
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private readonly Dictionary<Object, Dictionary<string, int>>
|
||||
ObjectToPropertyPathToSelectedEvent = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
public static int GetSelectedEvent(SerializedProperty property)
|
||||
{
|
||||
var instance = Instance;
|
||||
if (!instance.ObjectToPropertyPathToSelectedEvent.TryGetValue(property.serializedObject.targetObject, out var pathToSelection))
|
||||
return -1;
|
||||
else if (pathToSelection.TryGetValue(property.propertyPath, out var selection))
|
||||
return selection;
|
||||
else
|
||||
return -1;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
public static void SetSelectedEvent(SerializedProperty property, int eventIndex)
|
||||
{
|
||||
var pathToSelection = GetOrCreatePathToSelection(property.serializedObject.targetObject);
|
||||
if (eventIndex >= 0)
|
||||
pathToSelection[property.propertyPath] = eventIndex;
|
||||
else
|
||||
pathToSelection.Remove(property.propertyPath);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static Dictionary<string, int> GetOrCreatePathToSelection(Object obj)
|
||||
{
|
||||
var instance = Instance;
|
||||
if (!instance.ObjectToPropertyPathToSelectedEvent.TryGetValue(obj, out var pathToSelection))
|
||||
instance.ObjectToPropertyPathToSelectedEvent.Add(obj, pathToSelection = new());
|
||||
|
||||
return pathToSelection;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField] private Serialization.ObjectReference[] _EventSelectionObjects;
|
||||
[SerializeField] private string[] _EventSelectionPropertyPaths;
|
||||
[SerializeField] private int[] _EventSelectionIndices;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void OnDisableSelection()
|
||||
{
|
||||
var objects = new List<Serialization.ObjectReference>();
|
||||
var paths = new List<string>();
|
||||
var indices = new List<int>();
|
||||
|
||||
foreach (var objectToSelection in ObjectToPropertyPathToSelectedEvent)
|
||||
{
|
||||
foreach (var pathToSelection in objectToSelection.Value)
|
||||
{
|
||||
objects.Add(objectToSelection.Key);
|
||||
paths.Add(pathToSelection.Key);
|
||||
indices.Add(pathToSelection.Value);
|
||||
}
|
||||
}
|
||||
|
||||
_EventSelectionObjects = objects.ToArray();
|
||||
_EventSelectionPropertyPaths = paths.ToArray();
|
||||
_EventSelectionIndices = indices.ToArray();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private void OnEnableSelection()
|
||||
{
|
||||
if (_EventSelectionObjects == null ||
|
||||
_EventSelectionPropertyPaths == null ||
|
||||
_EventSelectionIndices == null)
|
||||
return;
|
||||
|
||||
var count = _EventSelectionObjects.Length;
|
||||
if (count > _EventSelectionPropertyPaths.Length)
|
||||
count = _EventSelectionPropertyPaths.Length;
|
||||
if (count > _EventSelectionIndices.Length)
|
||||
count = _EventSelectionIndices.Length;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var obj = _EventSelectionObjects[i];
|
||||
if (obj.IsValid())
|
||||
{
|
||||
var pathToSelection = GetOrCreatePathToSelection(obj);
|
||||
pathToSelection.Add(_EventSelectionPropertyPaths[i], _EventSelectionIndices[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Preview Models
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private List<GameObject> _PreviewModels;
|
||||
public static List<GameObject> PreviewModels
|
||||
{
|
||||
get
|
||||
{
|
||||
var instance = Instance;
|
||||
AnimancerEditorUtilities.RemoveMissingAndDuplicates(ref instance._PreviewModels);
|
||||
return instance._PreviewModels;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a30007eb6c59a040b2e70bb4748e76d
|
||||
timeCreated: 1516751545
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user