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