// 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