// 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
{
/// [Editor-Only] Draws the Inspector GUI for a .
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/SerializableEventSequenceDrawer
[CustomPropertyDrawer(typeof(SerializableSequence), true)]
public class SerializableEventSequenceDrawer : PropertyDrawer
{
/************************************************************************************************************************/
///
public static UnityAction Repaint = RepaintEverything;
private readonly Dictionary>
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];
}
/************************************************************************************************************************/
///
/// Calculates the number of vertical pixels the `property` will occupy when it is drawn.
///
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;
}
/************************************************************************************************************************/
/// Draws the GUI for the `property`.
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();
}
}
}
/************************************************************************************************************************/
/// Draws the GUI fields for the event at the specified `index`.
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;
}
/************************************************************************************************************************/
/// Draws the time field for the event at the specified `index`.
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.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;
/// Draws the time field for the event at the specified `index`.
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();
}
}
/// Draws the time field for the event at the specified `index`.
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);
}
/************************************************************************************************************************/
/// Updates the to accomodate a changed event time.
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);
}
}
/************************************************************************************************************************/
/// Draws the GUI fields for the event at the specified `index`.
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
_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;
/// Draws a button to add a new event or remove the selected one.
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;
}
/************************************************************************************************************************/
/// Adds an event to the sequence represented by the given `context`.
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();
}
}
/************************************************************************************************************************/
/// Removes the event at the specified `index`.
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--;
}
}
}
/************************************************************************************************************************/
/// Sorts the events in the `context` according to their times.
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();
}
/************************************************************************************************************************/
///
/// Swaps array[index] with array[index - 1]
/// while accounting for the possibility of the `index` being beyond the bounds of the `array`.
///
private static void DynamicSwap(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
/************************************************************************************************************************/
/// Details of an .
public class Context : IDisposable
{
/************************************************************************************************************************/
/// The main property representing the field.
public SerializedProperty Property { get; private set; }
private SerializableSequence _Sequence;
/// Underlying value of the .
public SerializableSequence Sequence
{
get
{
if (_Sequence == null && Property.serializedObject.targetObjects.Length == 1)
_Sequence = Property.GetValue();
return _Sequence;
}
}
/// The property representing the backing field.
public readonly SerializedArrayProperty Times = new();
/// The property representing the backing field.
public readonly SerializedArrayProperty Names = new();
/// The property representing the backing field.
public readonly SerializedArrayProperty Callbacks = new();
/************************************************************************************************************************/
private int _SelectedEvent;
/// The index of the currently selected event.
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);
}
}
/************************************************************************************************************************/
/// The stack of active contexts.
private static readonly List Stack = new();
/// The number of active items in the .
private static int _ActiveIndex = -1;
/// The currently active instance.
public static Context Current { get; private set; }
/************************************************************************************************************************/
/// Adds a new representing the `property` to the stack and returns it.
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;
}
/// Sets this as the and returns it.
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));
}
/************************************************************************************************************************/
/// [] Calls .
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;
}
/************************************************************************************************************************/
/// Shorthand for .
public TransitionDrawer.DrawerContext TransitionContext
=> TransitionDrawer.Context;
/************************************************************************************************************************/
/// Creates a copy of this .
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
/************************************************************************************************************************/
/// [Editor-Only] Settings for .
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/SerializableEventSequenceDrawerSettings
[Serializable, InternalSerializableType]
public class SerializableEventSequenceDrawerSettings : AnimancerSettingsGroup
{
/************************************************************************************************************************/
///
public override string DisplayName
=> "Animancer Events";
///
public override int Index
=> 4;
/************************************************************************************************************************/
[SerializeField]
[Tooltip("Should Animancer Event Callbacks be hidden in the Inspector?")]
private bool _HideEventCallbacks;
/// Should Animancer Event Callbacks be hidden in the Inspector?
public static bool HideEventCallbacks
=> AnimancerSettingsGroup.Instance._HideEventCallbacks;
/************************************************************************************************************************/
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
#endif