// Animancer // Copyright 2020 Kybernetik // #if UNITY_EDITOR using System; using UnityEditor; using UnityEngine; namespace Animancer.Editor { /// [Editor-Only] Draws the Inspector GUI for a . [CustomPropertyDrawer(typeof(AnimancerState.Transition<>), true)] public class TransitionDrawer : PropertyDrawer { /************************************************************************************************************************/ /// The visual state of a drawer. private enum Mode { Uninitialised, Normal, AlwaysExpanded, } /// The current state of this drawer. private Mode _Mode; /************************************************************************************************************************/ /// /// If set, the field with this name will be drawn with the foldout arrow instead of in its default place. /// protected readonly string MainPropertyName; /// /// "." + (to avoid creating garbage repeatedly). /// protected readonly string MainPropertyPathSuffix; /************************************************************************************************************************/ /// Constructs a new . public TransitionDrawer() { } /// /// Constructs a new and sets the /// . /// public TransitionDrawer(string mainPropertyName) { MainPropertyName = mainPropertyName; MainPropertyPathSuffix = "." + mainPropertyName; } /************************************************************************************************************************/ /// /// Returns the property specified by the . /// private SerializedProperty GetMainProperty(SerializedProperty rootProperty) { if (MainPropertyName == null) return null; else return rootProperty.FindPropertyRelative(MainPropertyName); } /************************************************************************************************************************/ /// /// Calculates the number of vertical pixels the `property` will occupy when it is drawn. /// public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { InitialiseMode(property); var height = EditorGUI.GetPropertyHeight(property, label, true); if (property.isExpanded) { var mainProperty = GetMainProperty(property); if (mainProperty != null) height -= EditorGUI.GetPropertyHeight(mainProperty) + AnimancerGUI.StandardSpacing; var endTime = property.FindPropertyRelative(NormalizedStartTimeFieldName); if (endTime != null) height += AnimancerGUI.LineHeight + AnimancerGUI.StandardSpacing; } return height; } /************************************************************************************************************************/ /// /// Draws the root `property` GUI and calls for each of its children. /// public override void OnGUI(Rect area, SerializedProperty property, GUIContent label) { InitialiseMode(property); using (TransitionContext.Get(this, property)) { var isPreviewing = TransitionPreviewWindow.IsPreviewingCurrentProperty(); if (isPreviewing) EditorGUI.DrawRect(area, new Color(0.35f, 0.5f, 1, 0.2f)); float headerHeight; DoHeaderGUI(area, property, label, isPreviewing, out headerHeight); DoChildPropertiesGUI(area, headerHeight, property); } } /************************************************************************************************************************/ /// /// If the is , this method determines how it should start /// based on the number of properties in the `serializedObject`. If the only serialized field is an /// then it should start expanded. /// protected void InitialiseMode(SerializedProperty property) { if (_Mode == Mode.Uninitialised) { _Mode = Mode.AlwaysExpanded; var iterator = property.serializedObject.GetIterator(); iterator.Next(true); var count = 0; do { switch (iterator.propertyPath) { case "m_ObjectHideFlags": case "m_Script": break; default: count++; if (count > 1) { _Mode = Mode.Normal; return; } break; } } while (iterator.NextVisible(false)); } if (_Mode == Mode.AlwaysExpanded) property.isExpanded = true; } /************************************************************************************************************************/ private void DoHeaderGUI(Rect area, SerializedProperty property, GUIContent label, bool isPreviewing, out float height) { area.height = AnimancerGUI.LineHeight; DoPreviewButtonGUI(ref area, property, isPreviewing); label.text = AnimancerGUI.GetNarrowText(label.text); var mainProperty = GetMainProperty(property); if (mainProperty != null) { DoPropertyGUI(ref area, property, mainProperty, label); if (_Mode != Mode.AlwaysExpanded) { var hierarchyMode = EditorGUIUtility.hierarchyMode; EditorGUIUtility.hierarchyMode = true; property.isExpanded = EditorGUI.Foldout(area, property.isExpanded, GUIContent.none, true); EditorGUIUtility.hierarchyMode = hierarchyMode; } } else { area.height = EditorGUI.GetPropertyHeight(property, label, false); if (_Mode != Mode.AlwaysExpanded) { EditorGUI.PropertyField(area, property, label, false); } else { label = EditorGUI.BeginProperty(area, label, property); EditorGUI.LabelField(area, label); EditorGUI.EndProperty(); } } height = area.height; } /************************************************************************************************************************/ private void DoPreviewButtonGUI(ref Rect area, SerializedProperty property, bool wasPreviewing) { if (property.serializedObject.targetObjects.Length != 1 || !TransitionPreviewWindow.CanBePreviewed(property)) return; var buttonArea = AnimancerGUI.StealFromRight(ref area, area.height + AnimancerGUI.StandardSpacing * 2, AnimancerGUI.StandardSpacing); var content = AnimancerGUI.TempContent("", "Preview this transition"); content.image = TransitionPreviewWindow.Icon; var isPrevewing = GUI.Toggle(buttonArea, wasPreviewing, content, Styles.PreviewButtonStyle); if (wasPreviewing != isPrevewing) TransitionPreviewWindow.Open(property, isPrevewing); content.image = null; } private static class Styles { public static readonly GUIStyle PreviewButtonStyle = new GUIStyle(AnimancerGUI.MiniButton) { #if UNITY_2019_3_OR_NEWER padding = new RectOffset(0, 0, 0, 1), #else padding = new RectOffset(), #endif fixedWidth = 0, fixedHeight = 0, }; } /************************************************************************************************************************/ private void DoChildPropertiesGUI(Rect area, float headerHeight, SerializedProperty property) { if (!property.isExpanded && _Mode != Mode.AlwaysExpanded) return; area.y += headerHeight + AnimancerGUI.StandardSpacing; EditorGUI.indentLevel++; var rootProperty = property; property = property.Copy(); SerializedProperty eventsProperty = null; var depth = property.depth; property.NextVisible(true); while (property.depth > depth) { // Grab the Events property and draw it last. if (eventsProperty == null && property.propertyPath.EndsWith("._Events")) { eventsProperty = property.Copy(); } else if (MainPropertyPathSuffix == null || !property.propertyPath.EndsWith(MainPropertyPathSuffix)) { var label = AnimancerGUI.TempContent(property); DoPropertyGUI(ref area, rootProperty, property, label); AnimancerGUI.NextVerticalArea(ref area); } if (!property.NextVisible(false)) break; } if (eventsProperty != null) { var label = AnimancerGUI.TempContent(eventsProperty); DoPropertyGUI(ref area, rootProperty, eventsProperty, label); } EditorGUI.indentLevel--; } /************************************************************************************************************************/ /// /// Draws the `property` GUI in relation to the `rootProperty` which was passed into . /// protected virtual void DoPropertyGUI(ref Rect area, SerializedProperty rootProperty, SerializedProperty property, GUIContent label) { #if UNITY_2018_3_OR_NEWER // If we keep using the GUIContent that was passed into OnGUI then GetPropertyHeight will change it to // match the 'property' which we don't want. label = AnimancerGUI.TempContent(label.text, label.tooltip, false); #endif area.height = EditorGUI.GetPropertyHeight(property, label, true); if (property.propertyPath.EndsWith("._FadeDuration")) { var length = Context.MaximumDuration; AnimancerGUI.DoOptionalTimeField(ref area, label, property, false, length, AnimancerPlayable.DefaultFadeDuration, false); if (property.floatValue < 0) property.floatValue = 0; return; } if (property.propertyPath.EndsWith("._Speed")) { AnimancerGUI.DoOptionalTimeField(ref area, label, property, true, float.NaN, 1); return; } if (TryDoStartTimeField(ref area, rootProperty, property, label)) return; EditorGUI.PropertyField(area, property, label, true); } /************************************************************************************************************************/ /// The name of the backing field of . public const string NormalizedStartTimeFieldName = "_NormalizedStartTime"; /// /// If the `property` is a "Start Time" field, this method draws it as well as the "End Time" below it and /// returns true. /// public static bool TryDoStartTimeField(ref Rect area, SerializedProperty rootProperty, SerializedProperty property, GUIContent label) { if (!property.propertyPath.EndsWith("." + NormalizedStartTimeFieldName)) return false; // Start Time. label.text = AnimancerGUI.GetNarrowText("Start Time"); var length = Context.MaximumDuration; var defaultStartTime = AnimancerEvent.Sequence.GetDefaultNormalizedStartTime(Context.Transition.Speed); AnimancerGUI.DoOptionalTimeField(ref area, label, property, true, length, defaultStartTime); AnimancerGUI.NextVerticalArea(ref area); // End Time. var events = rootProperty.FindPropertyRelative("_Events"); using (var context = EventSequenceDrawer.Context.Get(events)) { var areaCopy = area; var index = Mathf.Max(0, context.TimeCount - 1); string callbackLabel; EventSequenceDrawer.DoEventTimeGUI(ref areaCopy, context, index, true, out callbackLabel); } return true; } /************************************************************************************************************************/ [InitializeOnLoadMethod] private static void OnPropertyContextMenu() { EditorApplication.contextualPropertyMenu += (menu, property) => { var accessor = property.GetAccessor(); while (accessor != null) { if (typeof(ITransitionDetailed).IsAssignableFrom(accessor.FieldType)) { property = property.serializedObject.FindProperty(accessor.GetPath()); var transition = (ITransitionDetailed)accessor.GetValue(property); transition.AddItemsToContextMenu(menu, property, accessor); return; } accessor = accessor.Parent; } }; } /************************************************************************************************************************/ #region Context /************************************************************************************************************************/ /// The current . public static TransitionContext Context { get; private set; } /************************************************************************************************************************/ /// Details of an . public sealed class TransitionContext : IDisposable { /************************************************************************************************************************/ /// The main property representing the field. public SerializedProperty Property { get; private set; } /// The actual transition object rerieved from the . public ITransitionDetailed Transition { get; private set; } /// The cached value of . public float MaximumDuration { get; private set; } /************************************************************************************************************************/ private static readonly TransitionContext Instance = new TransitionContext(); private TransitionContext() { } /// /// Returns a disposable representing the specified parameters. /// /// Note that the same instance is returned every time and it can be accessed via . /// public static IDisposable Get(TransitionDrawer drawer, SerializedProperty transitionProperty) { Debug.Assert(Context == null, "Cannot get a new context before the previous one is disposed."); Instance.Property = transitionProperty; Instance.Transition = transitionProperty.GetValue(); Instance.MaximumDuration = Instance.Transition.MaximumDuration; EditorGUI.BeginChangeCheck(); return Context = Instance; } /************************************************************************************************************************/ /// Clears the . public void Dispose() { if (EditorGUI.EndChangeCheck()) Instance.Property.serializedObject.ApplyModifiedProperties(); Context = null; Instance.Property = null; Instance.Transition = null; } /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } #endif