1302 lines
51 KiB
C#
1302 lines
51 KiB
C#
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
|
|
|
#if UNITY_EDITOR && UNITY_IMGUI
|
|
|
|
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
|
|
|
|
using Animancer.Editor.Previews;
|
|
using Animancer.Units;
|
|
using Animancer.Units.Editor;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEditor;
|
|
using UnityEditor.AnimatedValues;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
using static Animancer.Editor.AnimancerGUI;
|
|
using Object = UnityEngine.Object;
|
|
using SerializableSequence = Animancer.AnimancerEvent.Sequence.Serializable;
|
|
|
|
namespace Animancer.Editor
|
|
{
|
|
/// <summary>[Editor-Only] Draws the Inspector GUI for a <see cref="SerializableSequence"/>.</summary>
|
|
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/SerializableEventSequenceDrawer
|
|
[CustomPropertyDrawer(typeof(SerializableSequence), true)]
|
|
public class SerializableEventSequenceDrawer : PropertyDrawer
|
|
{
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary><see cref="RepaintEverything"/></summary>
|
|
public static UnityAction Repaint = RepaintEverything;
|
|
|
|
private readonly Dictionary<string, List<AnimBool>>
|
|
EventVisibility = new();
|
|
|
|
private AnimBool GetVisibility(Context context, int index)
|
|
{
|
|
var path = context.Property.propertyPath;
|
|
if (!EventVisibility.TryGetValue(path, out var list))
|
|
EventVisibility.Add(path, list = new());
|
|
|
|
while (list.Count <= index)
|
|
{
|
|
var visible = context.Property.isExpanded || context.SelectedEvent == index;
|
|
list.Add(new(visible, Repaint));
|
|
}
|
|
|
|
return list[index];
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Calculates the number of vertical pixels the `property` will occupy when it is drawn.
|
|
/// </summary>
|
|
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
|
{
|
|
if (property.hasMultipleDifferentValues)
|
|
return LineHeight;
|
|
|
|
using var context = Context.Get(property);
|
|
|
|
var height = LineHeight;
|
|
|
|
var count = Math.Max(1, context.Times.Count);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
height += CalculateEventHeight(context, i) * GetVisibility(context, i).faded;
|
|
}
|
|
|
|
var events = context.Sequence?.InitializedEvents;
|
|
if (events != null)
|
|
height += EventSequenceDrawer.Get(events).CalculateHeight(events) + StandardSpacing;
|
|
|
|
return height;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private float CalculateEventHeight(Context context, int index)
|
|
{
|
|
// Name.
|
|
var height = index < context.Times.Count - 1
|
|
? LineHeight + StandardSpacing
|
|
: 0;// End Events don't have a Name.
|
|
|
|
// Time.
|
|
height += AnimationTimeAttributeDrawer.GetPropertyHeight(null, null) + StandardSpacing;
|
|
|
|
// Callback.
|
|
if (!SerializableEventSequenceDrawerSettings.HideEventCallbacks || context.Callbacks.Count > 0)
|
|
{
|
|
height += index < context.Callbacks.Count
|
|
? EditorGUI.GetPropertyHeight(context.Callbacks.GetElement(index), null, false)
|
|
: DummyInvokableDrawer.Height;
|
|
height += StandardSpacing;
|
|
}
|
|
|
|
return height;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Draws the GUI for the `property`.</summary>
|
|
public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
|
|
{
|
|
var warnings = OptionalWarning.ProOnly.DisableTemporarily();
|
|
|
|
using var context = Context.Get(property);
|
|
|
|
DoHeaderGUI(ref area, label, context);
|
|
|
|
if (property.hasMultipleDifferentValues)
|
|
return;
|
|
|
|
EditorGUI.indentLevel++;
|
|
DoAllEventsGUI(ref area, context);
|
|
EditorGUI.indentLevel--;
|
|
|
|
var sequence = context.Sequence?.InitializedEvents;
|
|
if (sequence != null)
|
|
{
|
|
using (var content = PooledGUIContent.Acquire("Runtime Events",
|
|
$"The runtime {nameof(AnimancerEvent)}.{nameof(AnimancerEvent.Sequence)}" +
|
|
$" created from the serialized data above"))
|
|
{
|
|
EventSequenceDrawer.Get(sequence).DoGUI(ref area, sequence, content);
|
|
}
|
|
}
|
|
|
|
warnings.Enable();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private void DoHeaderGUI(ref Rect area, GUIContent label, Context context)
|
|
{
|
|
if (!EditorGUIUtility.hierarchyMode)
|
|
EditorGUI.indentLevel--;
|
|
|
|
area.height = LineHeight;
|
|
var headerArea = area;
|
|
NextVerticalArea(ref area);
|
|
|
|
label = EditorGUI.BeginProperty(headerArea, label, context.Property);
|
|
|
|
if (!context.Property.hasMultipleDifferentValues)
|
|
{
|
|
var addEventArea = StealFromRight(ref headerArea, headerArea.height, StandardSpacing);
|
|
DoAddRemoveEventButtonGUI(addEventArea, context);
|
|
}
|
|
|
|
if (context.TransitionContext.Transition != null)
|
|
{
|
|
EditorGUI.EndProperty();
|
|
|
|
TimelineGUI.DoGUI(headerArea, context, out var addEventNormalizedTime);
|
|
|
|
if (!float.IsNaN(addEventNormalizedTime))
|
|
{
|
|
AddEvent(context, addEventNormalizedTime);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
string summary;
|
|
if (context.Times.Count == 0)
|
|
{
|
|
summary = "[0] End Time 1";
|
|
}
|
|
else
|
|
{
|
|
var index = context.Times.Count - 1;
|
|
var endTime = context.Times.GetElement(index).floatValue;
|
|
summary = $"[{index}] End Time {endTime:G3}";
|
|
}
|
|
|
|
using (var content = PooledGUIContent.Acquire(summary))
|
|
EditorGUI.LabelField(headerArea, label, content);
|
|
|
|
EditorGUI.EndProperty();
|
|
}
|
|
|
|
EditorGUI.BeginChangeCheck();
|
|
context.Property.isExpanded =
|
|
EditorGUI.Foldout(headerArea, context.Property.isExpanded, GUIContent.none, true);
|
|
if (EditorGUI.EndChangeCheck())
|
|
context.SelectedEvent = -1;
|
|
|
|
if (!EditorGUIUtility.hierarchyMode)
|
|
EditorGUI.indentLevel++;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static readonly int EventTimeHash = "EventTime".GetHashCode();
|
|
|
|
private static int _HotControlAdjustRoot;
|
|
private static int _SelectedEventToHotControl;
|
|
|
|
private void DoAllEventsGUI(ref Rect area, Context context)
|
|
{
|
|
var currentEvent = Event.current;
|
|
var originalEventType = currentEvent.type;
|
|
if (originalEventType == EventType.Used)
|
|
return;
|
|
|
|
var rootControlID = GUIUtility.GetControlID(EventTimeHash - 1, FocusType.Passive);
|
|
|
|
var eventCount = Mathf.Max(1, context.Times.Count);
|
|
for (int i = 0; i < eventCount; i++)
|
|
{
|
|
var controlID = GUIUtility.GetControlID(EventTimeHash + i, FocusType.Passive);
|
|
|
|
if (rootControlID == _HotControlAdjustRoot &&
|
|
_SelectedEventToHotControl > 0 &&
|
|
i == context.SelectedEvent)
|
|
{
|
|
GUIUtility.hotControl = GUIUtility.keyboardControl = controlID + _SelectedEventToHotControl;
|
|
_SelectedEventToHotControl = 0;
|
|
_HotControlAdjustRoot = -1;
|
|
}
|
|
|
|
DoEventGUI(ref area, context, i, false);
|
|
|
|
if (currentEvent.type == EventType.Used && originalEventType == EventType.MouseUp)
|
|
{
|
|
context.SelectedEvent = i;
|
|
|
|
if (SortEvents(context))
|
|
{
|
|
_SelectedEventToHotControl = GUIUtility.keyboardControl - controlID;
|
|
_HotControlAdjustRoot = rootControlID;
|
|
Deselect();
|
|
}
|
|
|
|
GUIUtility.ExitGUI();
|
|
}
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Draws the GUI fields for the event at the specified `index`.</summary>
|
|
public void DoEventGUI(ref Rect area, Context context, int index, bool autoSort)
|
|
{
|
|
GetEventLabels(
|
|
index,
|
|
context,
|
|
out var nameLabel,
|
|
out var timeLabel,
|
|
out var callbackLabel,
|
|
out var defaultTime,
|
|
out var isEndEvent);
|
|
|
|
var y = area.y;
|
|
|
|
var visibility = GetVisibility(context, index);
|
|
visibility.target = context.Property.isExpanded || context.SelectedEvent == index;
|
|
|
|
var x = area.xMin;
|
|
area.xMin = 0;
|
|
|
|
area.height = CalculateEventHeight(context, index) * visibility.faded;
|
|
|
|
var offset = GuiOffset;
|
|
GuiOffset += area.position;
|
|
|
|
TypeSelectionButton.BeginDelayingLinkLines();
|
|
try
|
|
{
|
|
GUI.BeginGroup(area, GUIStyle.none);
|
|
|
|
if (visibility.faded > 0)
|
|
{
|
|
area.xMin = x;
|
|
area.y = 0;
|
|
|
|
DoNameGUI(ref area, context, index, nameLabel);
|
|
DoTimeGUI(ref area, context, index, autoSort, timeLabel, defaultTime, isEndEvent);
|
|
DoCallbackGUI(ref area, context, index, callbackLabel);
|
|
|
|
area.y = area.y * visibility.faded + y;
|
|
area.height *= visibility.faded;
|
|
}
|
|
|
|
GUI.EndGroup();
|
|
}
|
|
finally
|
|
{
|
|
GuiOffset = offset;
|
|
|
|
TypeSelectionButton.EndDelayingLinkLines();
|
|
}
|
|
|
|
area.xMin = x;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Draws the time field for the event at the specified `index`.</summary>
|
|
public static void DoNameGUI(
|
|
ref Rect area,
|
|
Context context,
|
|
int index,
|
|
string nameLabel)
|
|
{
|
|
if (nameLabel == null)
|
|
return;
|
|
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
area.height = LineHeight;
|
|
var fieldArea = area;
|
|
NextVerticalArea(ref area);
|
|
|
|
using (var label = PooledGUIContent.Acquire(nameLabel,
|
|
"An optional name which can be used to identify the event in code." +
|
|
" Leaving all names blank is recommended if you aren't using them."))
|
|
{
|
|
fieldArea = EditorGUI.PrefixLabel(fieldArea, label);
|
|
}
|
|
|
|
var indentLevel = EditorGUI.indentLevel;
|
|
EditorGUI.indentLevel = 0;
|
|
|
|
var nameProperty = index < context.Names.Count
|
|
? context.Names.GetElement(index)
|
|
: null;
|
|
|
|
var name = nameProperty?.objectReferenceValue;
|
|
|
|
DoNameWarningGUI(ref fieldArea, context, name);
|
|
|
|
var exitGUI = false;
|
|
|
|
if (nameProperty != null)
|
|
{
|
|
EditorGUI.PropertyField(fieldArea, nameProperty, GUIContent.none);
|
|
}
|
|
else
|
|
{
|
|
EditorGUI.BeginProperty(fieldArea, GUIContent.none, context.Names.Property);
|
|
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
name = StringAssetDrawer.DrawGUI(fieldArea, GUIContent.none, null, out exitGUI);
|
|
|
|
if (EditorGUI.EndChangeCheck() && name != null)
|
|
{
|
|
// Expand up to the new name.
|
|
// If we need to expand more than one slot, make sure all the new ones are null.
|
|
context.Names.Count++;
|
|
if (context.Names.Count < index + 1)
|
|
{
|
|
var nextProperty = context.Names.GetElement(context.Names.Count - 1);
|
|
nextProperty.objectReferenceValue = null;
|
|
context.Names.Count = index + 1;
|
|
}
|
|
|
|
// Get and assign the new property.
|
|
nameProperty = context.Names.GetElement(index);
|
|
nameProperty.objectReferenceValue = name;
|
|
}
|
|
|
|
if (!exitGUI)
|
|
EditorGUI.EndProperty();
|
|
|
|
}
|
|
|
|
EditorGUI.indentLevel = indentLevel;
|
|
|
|
if (EditorGUI.EndChangeCheck())
|
|
{
|
|
var events = context.Sequence?.InitializedEvents;
|
|
events?.SetName(index, name as StringAsset);
|
|
}
|
|
|
|
if (exitGUI)
|
|
{
|
|
context.Names.Property.serializedObject.ApplyModifiedProperties();
|
|
GUIUtility.ExitGUI();
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static void DoNameWarningGUI(ref Rect area, Context context, Object name)
|
|
{
|
|
var property = context.TransitionContext.Property;
|
|
var attribute = AttributeCache<EventNamesAttribute>.FindAttribute(property);
|
|
if (attribute == null || !attribute.HasNames)
|
|
return;
|
|
|
|
var icon = name == null || Array.IndexOf(attribute.Names, (StringReference)name.name) >= 0
|
|
? AnimancerIcons.Info
|
|
: AnimancerIcons.Warning;
|
|
|
|
var warningArea = StealFromLeft(ref area, area.height, StandardSpacing);
|
|
|
|
var tooltip = attribute.NamesToString("Expected Names:");
|
|
using (var content = PooledGUIContent.Acquire("", tooltip))
|
|
{
|
|
content.image = icon;
|
|
GUI.Label(warningArea, content);
|
|
content.image = null;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static readonly AnimationTimeAttributeDrawer
|
|
AnimationTimeAttributeDrawer = new();
|
|
|
|
static SerializableEventSequenceDrawer()
|
|
=> AnimationTimeAttributeDrawer.Initialize(
|
|
new AnimationTimeAttribute(AnimationTimeAttribute.Units.Normalized));
|
|
|
|
private static float _PreviousTime = float.NaN;
|
|
|
|
/// <summary>Draws the time field for the event at the specified `index`.</summary>
|
|
public static void DoTimeGUI(
|
|
ref Rect area,
|
|
Context context,
|
|
int index,
|
|
bool autoSort,
|
|
string timeLabel,
|
|
float defaultTime,
|
|
bool isEndEvent)
|
|
{
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
area.height = AnimationTimeAttributeDrawer.GetPropertyHeight(null, null);
|
|
var timeArea = area;
|
|
NextVerticalArea(ref area);
|
|
|
|
float normalizedTime;
|
|
|
|
using (var label = PooledGUIContent.Acquire(timeLabel,
|
|
isEndEvent ? Strings.Tooltips.EndTime : Strings.Tooltips.CallbackTime))
|
|
{
|
|
var length = context.TransitionContext.Transition != null
|
|
? context.TransitionContext.MaximumDuration
|
|
: float.NaN;
|
|
|
|
if (index < context.Times.Count)
|
|
{
|
|
var timeProperty = context.Times.GetElement(index);
|
|
if (timeProperty == null)// Multi-selection screwed up the property retrieval.
|
|
{
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
var propertyLabel = EditorGUI.BeginProperty(timeArea, label, context.Times.Property);
|
|
if (isEndEvent)
|
|
AnimationTimeAttributeDrawer.NextDefaultValue = defaultTime;
|
|
normalizedTime = float.NaN;
|
|
AnimationTimeAttributeDrawer.OnGUI(timeArea, propertyLabel, ref normalizedTime);
|
|
|
|
EditorGUI.EndProperty();
|
|
|
|
if (EditorGUI.EndChangeCheck())
|
|
{
|
|
context.Times.Count = context.Times.Count;
|
|
timeProperty = context.Times.GetElement(index);
|
|
timeProperty.floatValue = normalizedTime;
|
|
SyncEventTimeChange(context, index, normalizedTime);
|
|
}
|
|
}
|
|
else// Event time property was correctly retrieved.
|
|
{
|
|
var wasEditingTextField = EditorGUIUtility.editingTextField;
|
|
if (!wasEditingTextField)
|
|
_PreviousTime = float.NaN;
|
|
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
var propertyLabel = EditorGUI.BeginProperty(timeArea, label, timeProperty);
|
|
|
|
if (isEndEvent)
|
|
AnimationTimeAttributeDrawer.NextDefaultValue = defaultTime;
|
|
normalizedTime = timeProperty.floatValue;
|
|
AnimationTimeAttributeDrawer.OnGUI(timeArea, propertyLabel, ref normalizedTime);
|
|
|
|
EditorGUI.EndProperty();
|
|
|
|
if (TryUseClickEvent(timeArea, 2))
|
|
normalizedTime = float.NaN;
|
|
|
|
var isEditingTextField = EditorGUIUtility.editingTextField;
|
|
if (EditorGUI.EndChangeCheck() || (wasEditingTextField && !isEditingTextField))
|
|
{
|
|
if (float.IsNaN(normalizedTime))
|
|
{
|
|
RemoveEvent(context, index);
|
|
Deselect();
|
|
}
|
|
else if (isEndEvent)
|
|
{
|
|
timeProperty.floatValue = normalizedTime;
|
|
SyncEventTimeChange(context, index, normalizedTime);
|
|
}
|
|
else if (!autoSort && isEditingTextField)
|
|
{
|
|
_PreviousTime = normalizedTime;
|
|
}
|
|
else
|
|
{
|
|
if (!float.IsNaN(_PreviousTime))
|
|
{
|
|
if (Event.current.keyCode != KeyCode.Escape)
|
|
{
|
|
normalizedTime = _PreviousTime;
|
|
Deselect();
|
|
}
|
|
|
|
_PreviousTime = float.NaN;
|
|
}
|
|
|
|
WrapEventTime(context, ref normalizedTime);
|
|
|
|
timeProperty.floatValue = normalizedTime;
|
|
SyncEventTimeChange(context, index, normalizedTime);
|
|
|
|
if (autoSort)
|
|
SortEvents(context);
|
|
}
|
|
|
|
GUI.changed = true;
|
|
}
|
|
}
|
|
}
|
|
else// Dummy End Event (when there are no event times).
|
|
{
|
|
AnimancerUtilities.Assert(index == 0, "Dummy end event index != 0");
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
EditorGUI.BeginProperty(timeArea, GUIContent.none, context.Times.Property);
|
|
|
|
AnimationTimeAttributeDrawer.NextDefaultValue = defaultTime;
|
|
normalizedTime = float.NaN;
|
|
AnimationTimeAttributeDrawer.OnGUI(timeArea, label, ref normalizedTime);
|
|
|
|
EditorGUI.EndProperty();
|
|
|
|
if (EditorGUI.EndChangeCheck() && !float.IsNaN(normalizedTime))
|
|
{
|
|
context.Times.Count = 1;
|
|
var timeProperty = context.Times.GetElement(0);
|
|
timeProperty.floatValue = normalizedTime;
|
|
SyncEventTimeChange(context, 0, normalizedTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (EditorGUI.EndChangeCheck())
|
|
{
|
|
var eventType = Event.current.type;
|
|
if (eventType == EventType.Layout)
|
|
return;
|
|
|
|
if (eventType == EventType.Used)
|
|
{
|
|
normalizedTime = UnitsAttributeDrawer.GetDisplayValue(normalizedTime, defaultTime);
|
|
TransitionPreviewWindow.PreviewNormalizedTime = normalizedTime;
|
|
}
|
|
|
|
GUIUtility.ExitGUI();
|
|
}
|
|
}
|
|
|
|
/// <summary>Draws the time field for the event at the specified `index`.</summary>
|
|
public static void DoTimeGUI(ref Rect area, Context context, int index, bool autoSort)
|
|
{
|
|
GetEventLabels(
|
|
index,
|
|
context,
|
|
out var _,
|
|
out var timeLabel,
|
|
out var _,
|
|
out var defaultTime,
|
|
out var isEndEvent);
|
|
|
|
DoTimeGUI(
|
|
ref area,
|
|
context,
|
|
index,
|
|
autoSort,
|
|
timeLabel,
|
|
defaultTime,
|
|
isEndEvent);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Updates the <see cref="SerializableSequence.Events"/> to accomodate a changed event time.</summary>
|
|
public static void SyncEventTimeChange(Context context, int index, float normalizedTime)
|
|
{
|
|
var events = context.Sequence?.InitializedEvents;
|
|
if (events == null)
|
|
return;
|
|
|
|
if (index == events.Count)// End Event.
|
|
{
|
|
events.NormalizedEndTime = normalizedTime;
|
|
}
|
|
else// Regular Event.
|
|
{
|
|
events.SetNormalizedTime(index, normalizedTime);
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Draws the GUI fields for the event at the specified `index`.</summary>
|
|
public static void DoCallbackGUI(
|
|
ref Rect area,
|
|
Context context,
|
|
int index,
|
|
string callbackLabel)
|
|
{
|
|
if (SerializableEventSequenceDrawerSettings.HideEventCallbacks && context.Callbacks.Count == 0)
|
|
return;
|
|
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
using (var label = PooledGUIContent.Acquire(callbackLabel))
|
|
{
|
|
if (index < context.Callbacks.Count)
|
|
{
|
|
var callback = context.Callbacks.GetElement(index);
|
|
area.height = EditorGUI.GetPropertyHeight(callback, false);
|
|
|
|
EditorGUI.PropertyField(area, callback, label, false);
|
|
}
|
|
else if (DummyInvokableDrawer.DoGUI(ref area, label, context.Callbacks.Property, out var callback))
|
|
{
|
|
try
|
|
{
|
|
SerializableSequence.DisableCompactArrays = true;
|
|
|
|
if (index >= context.Times.Count)
|
|
{
|
|
context.Times.Property.InsertArrayElementAtIndex(index);
|
|
context.Times.Count++;
|
|
context.Times.GetElement(index).floatValue = float.NaN;
|
|
context.Times.Property.serializedObject.ApplyModifiedProperties();
|
|
}
|
|
|
|
context.Callbacks.Property.ForEachTarget(callbacksProperty =>
|
|
{
|
|
var accessor = callbacksProperty.GetAccessor();
|
|
var oldCallbacks = (Array)accessor.GetValue(callbacksProperty.serializedObject.targetObject);
|
|
|
|
Array newCallbacks;
|
|
if (oldCallbacks == null)
|
|
{
|
|
var elementType = accessor.GetFieldElementType(callbacksProperty);
|
|
newCallbacks = Array.CreateInstance(elementType, 1);
|
|
}
|
|
else
|
|
{
|
|
var elementType = oldCallbacks.GetType().GetElementType();
|
|
newCallbacks = Array.CreateInstance(elementType, index + 1);
|
|
Array.Copy(oldCallbacks, newCallbacks, oldCallbacks.Length);
|
|
}
|
|
|
|
newCallbacks.SetValue(callback, index);
|
|
accessor.SetValue(callbacksProperty, newCallbacks);
|
|
});
|
|
|
|
context.Callbacks.Property.OnPropertyChanged();
|
|
context.Callbacks.Property.GetArrayElementAtIndex(index).isExpanded = true;
|
|
context.Callbacks.Refresh();
|
|
}
|
|
finally
|
|
{
|
|
SerializableSequence.DisableCompactArrays = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (EditorGUI.EndChangeCheck())
|
|
{
|
|
if (index < context.Callbacks.Count)
|
|
{
|
|
var events = context.Sequence?.InitializedEvents;
|
|
if (events != null)
|
|
{
|
|
var animancerEvent = index < events.Count
|
|
? events[index]
|
|
: events.EndEvent;
|
|
|
|
if (AnimancerEvent.IsNullOrDummy(animancerEvent.callback))
|
|
{
|
|
context.Callbacks.Property.serializedObject.ApplyModifiedProperties();
|
|
var property = context.Callbacks.GetElement(index);
|
|
var callback = property.GetValue();
|
|
var invoke = SerializableSequence.GetInvoke(callback as IInvokable);
|
|
if (index < events.Count)
|
|
events.SetCallback(index, invoke);
|
|
else
|
|
events.OnEnd = invoke;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
NextVerticalArea(ref area);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static ConversionCache<int, string>
|
|
_NameLabelCache,
|
|
_TimeLabelCache,
|
|
_CallbackLabelCache;
|
|
|
|
private static void GetEventLabels(
|
|
int index,
|
|
Context context,
|
|
out string nameLabel,
|
|
out string timeLabel,
|
|
out string callbackLabel,
|
|
out float defaultTime,
|
|
out bool isEndEvent)
|
|
{
|
|
if (index >= context.Times.Count - 1)
|
|
{
|
|
nameLabel = null;
|
|
timeLabel = "End Time";
|
|
callbackLabel = "End Callback";
|
|
|
|
defaultTime = AnimancerEvent.Sequence.GetDefaultNormalizedEndTime(
|
|
context.TransitionContext.Transition?.Speed ?? 1);
|
|
isEndEvent = true;
|
|
}
|
|
else
|
|
{
|
|
if (_NameLabelCache == null)
|
|
{
|
|
_NameLabelCache = new((i) => $"Event {i} Name");
|
|
_TimeLabelCache = new((i) => $"Event {i} Time");
|
|
_CallbackLabelCache = new((i) => $"Event {i} Callback");
|
|
}
|
|
|
|
nameLabel = _NameLabelCache.Convert(index);
|
|
timeLabel = _TimeLabelCache.Convert(index);
|
|
callbackLabel = _CallbackLabelCache.Convert(index);
|
|
|
|
defaultTime = 0;
|
|
isEndEvent = false;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static void WrapEventTime(Context context, ref float normalizedTime)
|
|
{
|
|
var transition = context.TransitionContext.Transition;
|
|
if (transition != null && transition.IsLooping)
|
|
{
|
|
if (normalizedTime == 0)
|
|
return;
|
|
else if (normalizedTime % 1 == 0)
|
|
normalizedTime = AnimancerEvent.AlmostOne;
|
|
else
|
|
normalizedTime = AnimancerUtilities.Wrap01(normalizedTime);
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#region Event Modification
|
|
/************************************************************************************************************************/
|
|
|
|
private static GUIStyle _AddEventStyle;
|
|
private static GUIContent _AddEventContent;
|
|
|
|
/// <summary>Draws a button to add a new event or remove the selected one.</summary>
|
|
public void DoAddRemoveEventButtonGUI(Rect area, Context context)
|
|
{
|
|
if (ShowAddButton(context))
|
|
{
|
|
AnimancerIcons.IconContent(ref _AddEventContent, "Animation.AddEvent", Strings.ProOnlyTag + "Add event");
|
|
|
|
_AddEventStyle ??= new(EditorStyles.miniButton)
|
|
{
|
|
fixedHeight = 0,
|
|
padding = new(-1, 1, 0, 0),
|
|
};
|
|
|
|
if (GUI.Button(area, _AddEventContent, _AddEventStyle))
|
|
{
|
|
// If the target is currently being previewed, add the event at the currently selected time.
|
|
var state = TransitionPreviewWindow.GetCurrentState();
|
|
var normalizedTime = state != null ? state.NormalizedTime : float.NaN;
|
|
AddEvent(context, normalizedTime);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (GUI.Button(area, AnimancerIcons.ClearIcon("Remove selected event"), NoPaddingButtonStyle))
|
|
{
|
|
RemoveEvent(context, context.SelectedEvent);
|
|
}
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static bool ShowAddButton(Context context)
|
|
{
|
|
// Nothing selected = Add.
|
|
if (context.SelectedEvent < 0)
|
|
return true;
|
|
|
|
// No times means no events exist = Add.
|
|
if (context.Times.Count == 0)
|
|
return true;
|
|
|
|
// Regular event selected = Remove.
|
|
if (context.SelectedEvent < context.Times.Count - 1)
|
|
return false;
|
|
|
|
// End has non-default time = Remove.
|
|
if (!float.IsNaN(context.Times.GetElement(context.SelectedEvent).floatValue))
|
|
return false;
|
|
|
|
// End has non-empty callback = Remove.
|
|
// If the end callback was empty, the array would have been compacted.
|
|
if (context.Callbacks.Count == context.Times.Count)
|
|
return false;
|
|
|
|
// End has empty callback = Add.
|
|
return true;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Adds an event to the sequence represented by the given `context`.</summary>
|
|
public static void AddEvent(Context context, float normalizedTime)
|
|
{
|
|
// If the time is NaN, add it halfway between the last event and the end.
|
|
|
|
if (context.Times.Count == 0)
|
|
{
|
|
// Having any events means we need the end time too.
|
|
context.Times.Count = 2;
|
|
context.Times.GetElement(1).floatValue = float.NaN;
|
|
if (float.IsNaN(normalizedTime))
|
|
normalizedTime = 0.5f;
|
|
}
|
|
else
|
|
{
|
|
context.Times.Property.InsertArrayElementAtIndex(context.Times.Count - 1);
|
|
context.Times.Count++;
|
|
|
|
if (float.IsNaN(normalizedTime))
|
|
{
|
|
var transition = context.TransitionContext.Transition;
|
|
|
|
var previousTime = context.Times.Count >= 3
|
|
? context.Times.GetElement(context.Times.Count - 3).floatValue
|
|
: AnimancerEvent.Sequence.GetDefaultNormalizedStartTime(transition.Speed);
|
|
|
|
var endTime = context.Times.GetElement(context.Times.Count - 1).floatValue;
|
|
if (float.IsNaN(endTime))
|
|
endTime = AnimancerEvent.Sequence.GetDefaultNormalizedEndTime(transition.Speed);
|
|
|
|
normalizedTime = previousTime < endTime
|
|
? (previousTime + endTime) * 0.5f
|
|
: previousTime;
|
|
}
|
|
}
|
|
|
|
WrapEventTime(context, ref normalizedTime);
|
|
|
|
var newEvent = context.Times.Count - 2;
|
|
context.Times.GetElement(newEvent).floatValue = normalizedTime;
|
|
context.SelectedEvent = newEvent;
|
|
|
|
if (context.Callbacks.Count > newEvent)
|
|
{
|
|
context.Callbacks.Property.InsertArrayElementAtIndex(newEvent);
|
|
context.Callbacks.Property.serializedObject.ApplyModifiedProperties();
|
|
|
|
// Make sure the callback starts empty rather than copying an existing value.
|
|
var callback = context.Callbacks.GetElement(newEvent);
|
|
callback.SetValue(null);
|
|
context.Callbacks.Property.OnPropertyChanged();
|
|
}
|
|
|
|
// Update the runtime sequence accordingly.
|
|
var events = context.Sequence?.InitializedEvents;
|
|
events?.Add(normalizedTime, AnimancerEvent.DummyCallback);
|
|
|
|
OptionalWarning.UselessEvent.Disable();
|
|
|
|
if (Event.current != null)
|
|
{
|
|
GUI.changed = true;
|
|
GUIUtility.ExitGUI();
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Removes the event at the specified `index`.</summary>
|
|
public static void RemoveEvent(Context context, int index)
|
|
{
|
|
// If it's an End Event, set it to NaN.
|
|
if (index >= context.Times.Count - 1)
|
|
{
|
|
context.Times.GetElement(index).floatValue = float.NaN;
|
|
|
|
if (context.Callbacks.Count > index)
|
|
context.Callbacks.Count--;
|
|
|
|
Deselect();
|
|
|
|
// Update the runtime sequence accordingly.
|
|
var events = context.Sequence?.InitializedEvents;
|
|
if (events != null)
|
|
{
|
|
events.EndEvent = new(float.NaN, null);
|
|
}
|
|
}
|
|
else// Otherwise remove it.
|
|
{
|
|
context.Times.Property.DeleteArrayElementAtIndex(index);
|
|
context.Times.Count--;
|
|
|
|
// Update the runtime sequence accordingly.
|
|
var events = context.Sequence?.InitializedEvents;
|
|
events?.Remove(index);
|
|
|
|
if (index < context.Names.Count)
|
|
{
|
|
context.Names.Property.DeleteArrayElementAtIndex(index);
|
|
context.Names.Count--;
|
|
}
|
|
|
|
if (index < context.Callbacks.Count)
|
|
{
|
|
context.Callbacks.Property.DeleteArrayElementAtIndex(index);
|
|
context.Callbacks.Count--;
|
|
}
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Sorts the events in the `context` according to their times.</summary>
|
|
private static bool SortEvents(Context context)
|
|
{
|
|
if (context.Times.Count <= 2)
|
|
return false;
|
|
|
|
// The serializable sequence sorts itself in ISerializationCallbackReceiver.OnBeforeSerialize.
|
|
var selectedEvent = context.SelectedEvent;
|
|
var sorted = context.Property.serializedObject.ApplyModifiedProperties();
|
|
if (!sorted)
|
|
return false;
|
|
|
|
context.Property.serializedObject.Update();
|
|
context.Times.Refresh();
|
|
context.Names.Refresh();
|
|
context.Callbacks.Refresh();
|
|
return context.SelectedEvent != selectedEvent;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region OnBeforeSerialize
|
|
/************************************************************************************************************************/
|
|
|
|
[InitializeOnLoadMethod]
|
|
private static void InitializeOnBeforeSerialize()
|
|
=> SerializableSequence.OnBeforeSerialize += OnBeforeSerialize;
|
|
|
|
private static void OnBeforeSerialize(SerializableSequence sequence)
|
|
{
|
|
var warnings = OptionalWarning.ProOnly.DisableTemporarily();
|
|
|
|
var normalizedTimes = sequence.NormalizedTimes;
|
|
|
|
warnings.Enable();
|
|
|
|
if (normalizedTimes == null ||
|
|
normalizedTimes.Length <= 2)
|
|
{
|
|
sequence.CompactArrays();
|
|
return;
|
|
}
|
|
|
|
var eventContext = Context.Current;
|
|
var selectedEvent = eventContext?.Property != null
|
|
? eventContext.SelectedEvent
|
|
: -1;
|
|
|
|
var timeCount = normalizedTimes.Length - 1;
|
|
|
|
var previousTime = normalizedTimes[0];
|
|
|
|
// Bubble Sort based on the normalized times.
|
|
for (int i = 1; i < timeCount; i++)
|
|
{
|
|
var time = normalizedTimes[i];
|
|
if (time >= previousTime)
|
|
{
|
|
previousTime = time;
|
|
continue;
|
|
}
|
|
|
|
normalizedTimes.Swap(i, i - 1);
|
|
DynamicSwap(ref sequence.Callbacks, i);
|
|
DynamicSwap(ref sequence.Names, i);
|
|
|
|
if (selectedEvent == i)
|
|
selectedEvent = i - 1;
|
|
else if (selectedEvent == i - 1)
|
|
selectedEvent = i;
|
|
|
|
if (i == 1)
|
|
{
|
|
i = 0;
|
|
previousTime = float.NegativeInfinity;
|
|
}
|
|
else
|
|
{
|
|
i -= 2;
|
|
previousTime = normalizedTimes[i];
|
|
}
|
|
}
|
|
|
|
// If the current animation is looping, clamp all times within the 0-1 range.
|
|
var transitionContext = TransitionDrawer.Context;
|
|
if (transitionContext.Transition != null &&
|
|
transitionContext.Transition.IsLooping)
|
|
{
|
|
for (int i = normalizedTimes.Length - 1; i >= 0; i--)
|
|
{
|
|
var time = normalizedTimes[i];
|
|
if (time < 0)
|
|
normalizedTimes[i] = 0;
|
|
else if (time > AnimancerEvent.AlmostOne)
|
|
normalizedTimes[i] = AnimancerEvent.AlmostOne;
|
|
}
|
|
}
|
|
|
|
// If the selected event was moved adjust the selection.
|
|
if (eventContext?.Property != null && eventContext.SelectedEvent != selectedEvent)
|
|
{
|
|
eventContext.SelectedEvent = selectedEvent;
|
|
TransitionPreviewWindow.PreviewNormalizedTime = normalizedTimes[selectedEvent];
|
|
}
|
|
|
|
sequence.CompactArrays();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Swaps <c>array[index]</c> with <c>array[index - 1]</c>
|
|
/// while accounting for the possibility of the `index` being beyond the bounds of the `array`.
|
|
/// </summary>
|
|
private static void DynamicSwap<T>(ref T[] array, int index)
|
|
{
|
|
var count = array != null ? array.Length : 0;
|
|
|
|
if (index == count)
|
|
Array.Resize(ref array, ++count);
|
|
|
|
if (index < count)
|
|
array.Swap(index, index - 1);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region Context
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Details of an <see cref="AnimancerEvent.Sequence.Serializable"/>.</summary>
|
|
public class Context : IDisposable
|
|
{
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>The main property representing the <see cref="Sequence"/> field.</summary>
|
|
public SerializedProperty Property { get; private set; }
|
|
|
|
private SerializableSequence _Sequence;
|
|
|
|
/// <summary>Underlying value of the <see cref="Property"/>.</summary>
|
|
public SerializableSequence Sequence
|
|
{
|
|
get
|
|
{
|
|
if (_Sequence == null && Property.serializedObject.targetObjects.Length == 1)
|
|
_Sequence = Property.GetValue<SerializableSequence>();
|
|
return _Sequence;
|
|
}
|
|
}
|
|
|
|
/// <summary>The property representing the <see cref="SerializableSequence.NormalizedTimes"/> backing field.</summary>
|
|
public readonly SerializedArrayProperty Times = new();
|
|
|
|
/// <summary>The property representing the <see cref="SerializableSequence.Names"/> backing field.</summary>
|
|
public readonly SerializedArrayProperty Names = new();
|
|
|
|
/// <summary>The property representing the <see cref="SerializableSequence.Callbacks"/> backing field.</summary>
|
|
public readonly SerializedArrayProperty Callbacks = new();
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private int _SelectedEvent;
|
|
|
|
/// <summary>The index of the currently selected event.</summary>
|
|
public int SelectedEvent
|
|
{
|
|
get => _SelectedEvent;
|
|
set
|
|
{
|
|
if (Times != null && value >= 0 && (value < Times.Count || Times.Count == 0))
|
|
{
|
|
float normalizedTime;
|
|
if (Times.Count > 0)
|
|
{
|
|
normalizedTime = Times.GetElement(value).floatValue;
|
|
}
|
|
else
|
|
{
|
|
var transition = TransitionContext.Transition;
|
|
var speed = transition != null ? transition.Speed : 1;
|
|
normalizedTime = AnimancerEvent.Sequence.GetDefaultNormalizedEndTime(speed);
|
|
}
|
|
|
|
TransitionPreviewWindow.PreviewNormalizedTime = normalizedTime;
|
|
}
|
|
|
|
if (_SelectedEvent == value &&
|
|
Callbacks != null)
|
|
return;
|
|
|
|
_SelectedEvent = value;
|
|
TemporarySettings.SetSelectedEvent(Callbacks.Property, value);
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>The stack of active contexts.</summary>
|
|
private static readonly List<Context> Stack = new();
|
|
|
|
/// <summary>The number of active items in the <see cref="Stack"/>.</summary>
|
|
private static int _ActiveIndex = -1;
|
|
|
|
/// <summary>The currently active instance.</summary>
|
|
public static Context Current { get; private set; }
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Adds a new <see cref="Context"/> representing the `property` to the stack and returns it.</summary>
|
|
public static Context Get(SerializedProperty property)
|
|
{
|
|
_ActiveIndex++;
|
|
|
|
if (_ActiveIndex >= Stack.Count)
|
|
{
|
|
Current = new();
|
|
Stack.Add(Current);
|
|
}
|
|
else
|
|
{
|
|
Current = Stack[_ActiveIndex];
|
|
}
|
|
|
|
Current.Initialize(property);
|
|
EditorGUI.BeginChangeCheck();
|
|
return Current;
|
|
}
|
|
|
|
/// <summary>Sets this <see cref="Context"/> as the <see cref="Current"/> and returns it.</summary>
|
|
public Context SetAsCurrent()
|
|
{
|
|
Current = this;
|
|
EditorGUI.BeginChangeCheck();
|
|
return this;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private void Initialize(SerializedProperty property)
|
|
{
|
|
if (Property == property)
|
|
return;
|
|
|
|
Property = property;
|
|
_Sequence = null;
|
|
|
|
Times.Property = property.FindPropertyRelative(SerializableSequence.NormalizedTimesField);
|
|
Names.Property = property.FindPropertyRelative(SerializableSequence.NamesField);
|
|
Callbacks.Property = property.FindPropertyRelative(SerializableSequence.CallbacksField);
|
|
|
|
if (Names.Count > Times.Count)
|
|
Names.Count = Times.Count;
|
|
if (Callbacks.Count > Times.Count)
|
|
Callbacks.Count = Times.Count;
|
|
|
|
_SelectedEvent = TemporarySettings.GetSelectedEvent(Callbacks.Property);
|
|
_SelectedEvent = Mathf.Min(_SelectedEvent, Mathf.Max(Times.Count - 1, 0));
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[<see cref="IDisposable"/>] Calls <see cref="SerializedObject.ApplyModifiedProperties"/>.</summary>
|
|
public void Dispose()
|
|
{
|
|
if (this == Stack[_ActiveIndex])
|
|
_ActiveIndex--;
|
|
|
|
Stack.TryGet(_ActiveIndex, out var current);
|
|
Current = current;
|
|
|
|
if (EditorGUI.EndChangeCheck())
|
|
Property.serializedObject.ApplyModifiedProperties();
|
|
|
|
Property = null;
|
|
_Sequence = null;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Shorthand for <see cref="TransitionDrawer.Context"/>.</summary>
|
|
public TransitionDrawer.DrawerContext TransitionContext
|
|
=> TransitionDrawer.Context;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Creates a copy of this <see cref="Context"/>.</summary>
|
|
public Context Copy()
|
|
{
|
|
var copy = new Context
|
|
{
|
|
Property = Property,
|
|
_SelectedEvent = _SelectedEvent,
|
|
};
|
|
|
|
copy.Times.Property = Times.Property;
|
|
copy.Names.Property = Names.Property;
|
|
copy.Callbacks.Property = Callbacks.Property;
|
|
|
|
return copy;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#region Settings
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Editor-Only] Settings for <see cref="SerializableEventSequenceDrawer"/>.</summary>
|
|
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/SerializableEventSequenceDrawerSettings
|
|
[Serializable, InternalSerializableType]
|
|
public class SerializableEventSequenceDrawerSettings : AnimancerSettingsGroup
|
|
{
|
|
/************************************************************************************************************************/
|
|
|
|
/// <inheritdoc/>
|
|
public override string DisplayName
|
|
=> "Animancer Events";
|
|
|
|
/// <inheritdoc/>
|
|
public override int Index
|
|
=> 4;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
[SerializeField]
|
|
[Tooltip("Should Animancer Event Callbacks be hidden in the Inspector?")]
|
|
private bool _HideEventCallbacks;
|
|
|
|
/// <summary>Should Animancer Event Callbacks be hidden in the Inspector?</summary>
|
|
public static bool HideEventCallbacks
|
|
=> AnimancerSettingsGroup<SerializableEventSequenceDrawerSettings>.Instance._HideEventCallbacks;
|
|
|
|
/************************************************************************************************************************/
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
}
|
|
|
|
#endif
|
|
|