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