You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
747 lines
30 KiB
C#
747 lines
30 KiB
C#
2 months ago
|
// Animancer // Copyright 2020 Kybernetik //
|
||
|
|
||
|
#if UNITY_EDITOR
|
||
|
|
||
|
using System;
|
||
|
using System.Collections;
|
||
|
using UnityEditor;
|
||
|
using UnityEngine;
|
||
|
|
||
|
namespace Animancer.Editor
|
||
|
{
|
||
|
/// <summary>[Editor-Only] Various GUI utilities used throughout Animancer.</summary>
|
||
|
public static class AnimancerGUI
|
||
|
{
|
||
|
/************************************************************************************************************************/
|
||
|
#region Standard Values
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>The highlight color used for fields showing a warning.</summary>
|
||
|
public static readonly Color
|
||
|
WarningFieldColor = new Color(1, 0.9f, 0.6f);
|
||
|
|
||
|
/// <summary>The highlight color used for fields showing an error.</summary>
|
||
|
public static readonly Color
|
||
|
ErrorFieldColor = new Color(1, 0.6f, 0.6f);
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary><see cref="GUILayout.ExpandWidth"/> set to false.</summary>
|
||
|
public static readonly GUILayoutOption[]
|
||
|
DontExpandWidth = { GUILayout.ExpandWidth(false) };
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>Wrapper around <see cref="EditorGUIUtility.singleLineHeight"/>.</summary>
|
||
|
public static float LineHeight
|
||
|
{
|
||
|
get { return EditorGUIUtility.singleLineHeight; }
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>Wrapper around <see cref="EditorGUIUtility.standardVerticalSpacing"/>.</summary>
|
||
|
public static float StandardSpacing
|
||
|
{
|
||
|
get { return EditorGUIUtility.standardVerticalSpacing; }
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
private static float _IndentSize = -1;
|
||
|
|
||
|
/// <summary>
|
||
|
/// The number of pixels of indentation for each <see cref="EditorGUI.indentLevel"/> increment.
|
||
|
/// </summary>
|
||
|
public static float IndentSize
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
if (_IndentSize < 0)
|
||
|
{
|
||
|
var indentLevel = EditorGUI.indentLevel;
|
||
|
EditorGUI.indentLevel = 1;
|
||
|
_IndentSize = EditorGUI.IndentedRect(new Rect()).x;
|
||
|
EditorGUI.indentLevel = indentLevel;
|
||
|
}
|
||
|
|
||
|
return _IndentSize;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
private static float _ToggleWidth = -1;
|
||
|
|
||
|
/// <summary>The width of a standard <see cref="GUISkin.toggle"/> with no label.</summary>
|
||
|
public static float ToggleWidth
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
if (_ToggleWidth == -1)
|
||
|
_ToggleWidth = GUI.skin.toggle.CalculateWidth(GUIContent.none);
|
||
|
return _ToggleWidth;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>The color of the standard label text.</summary>
|
||
|
public static Color TextColor
|
||
|
{
|
||
|
get { return GUI.skin.label.normal.textColor; }
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// A more compact <see cref="EditorStyles.miniButton"/> with a fixed size as a tiny box.
|
||
|
/// </summary>
|
||
|
public static readonly GUIStyle MiniButton = new GUIStyle(EditorStyles.miniButton)
|
||
|
{
|
||
|
margin = new RectOffset(0, 0, 2, 0),
|
||
|
padding = new RectOffset(2, 3, 2, 2),
|
||
|
alignment = TextAnchor.MiddleCenter,
|
||
|
fixedHeight = LineHeight,
|
||
|
fixedWidth = LineHeight - 1
|
||
|
};
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
#endregion
|
||
|
/************************************************************************************************************************/
|
||
|
#region Layout
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>Indicates where <see cref="LayoutSingleLineRect"/> should add the <see cref="StandardSpacing"/>.</summary>
|
||
|
public enum SpacingMode
|
||
|
{
|
||
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member.
|
||
|
None,
|
||
|
Before,
|
||
|
After,
|
||
|
BeforeAndAfter
|
||
|
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member.
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Uses <see cref="GUILayoutUtility.GetRect(float, float)"/> to get a <see cref="Rect"/> occupying a single
|
||
|
/// standard line with the <see cref="StandardSpacing"/> added according to the specified `spacing`.
|
||
|
/// </summary>
|
||
|
public static Rect LayoutSingleLineRect(SpacingMode spacing = SpacingMode.None)
|
||
|
{
|
||
|
Rect rect;
|
||
|
switch (spacing)
|
||
|
{
|
||
|
case SpacingMode.None:
|
||
|
return GUILayoutUtility.GetRect(0, LineHeight);
|
||
|
|
||
|
case SpacingMode.Before:
|
||
|
rect = GUILayoutUtility.GetRect(0, LineHeight + StandardSpacing);
|
||
|
rect.yMin += StandardSpacing;
|
||
|
return rect;
|
||
|
|
||
|
case SpacingMode.After:
|
||
|
rect = GUILayoutUtility.GetRect(0, LineHeight + StandardSpacing);
|
||
|
rect.yMax -= StandardSpacing;
|
||
|
return rect;
|
||
|
|
||
|
case SpacingMode.BeforeAndAfter:
|
||
|
rect = GUILayoutUtility.GetRect(0, LineHeight + StandardSpacing * 2);
|
||
|
rect.yMin += StandardSpacing;
|
||
|
rect.yMax -= StandardSpacing;
|
||
|
return rect;
|
||
|
|
||
|
default:
|
||
|
throw new ArgumentException("Unknown StandardSpacing: " + spacing, "spacing");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// If the <see cref="Rect.height"/> is positive, this method moves the <see cref="Rect.y"/> by that amount and
|
||
|
/// adds the <see cref="EditorGUIUtility.standardVerticalSpacing"/>.
|
||
|
/// </summary>
|
||
|
public static void NextVerticalArea(ref Rect area)
|
||
|
{
|
||
|
if (area.height > 0)
|
||
|
area.y += area.height + StandardSpacing;
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Subtracts the `width` from the left side of the `area` and returns a new <see cref="Rect"/> occupying the
|
||
|
/// removed section.
|
||
|
/// </summary>
|
||
|
public static Rect StealFromLeft(ref Rect area, float width, float padding = 0)
|
||
|
{
|
||
|
var newRect = new Rect(area.x, area.y, width, area.height);
|
||
|
area.xMin += width + padding;
|
||
|
return newRect;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Subtracts the `width` from the right side of the `area` and returns a new <see cref="Rect"/> occupying the
|
||
|
/// removed section.
|
||
|
/// </summary>
|
||
|
public static Rect StealFromRight(ref Rect area, float width, float padding = 0)
|
||
|
{
|
||
|
area.width -= width + padding;
|
||
|
return new Rect(area.xMax + padding, area.y, width, area.height);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Divides the given `area` such that the fields associated with both labels will have equal space
|
||
|
/// remaining after the labels themselves.
|
||
|
/// </summary>
|
||
|
public static void SplitHorizontally(Rect area, string label0, string label1,
|
||
|
out float width0, out float width1, out Rect rect0, out Rect rect1)
|
||
|
{
|
||
|
width0 = CalculateLabelWidth(label0);
|
||
|
width1 = CalculateLabelWidth(label1);
|
||
|
|
||
|
const float Padding = 1;
|
||
|
|
||
|
rect0 = rect1 = area;
|
||
|
|
||
|
var remainingWidth = area.width - width0 - width1 - Padding;
|
||
|
rect0.width = width0 + remainingWidth * 0.5f;
|
||
|
rect1.xMin = rect0.xMax + Padding;
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Calls <see cref="GUIStyle.CalcMinMaxWidth"/> and returns the max width.
|
||
|
/// </summary>
|
||
|
public static float CalculateWidth(this GUIStyle style, GUIContent content)
|
||
|
{
|
||
|
float _, width;
|
||
|
style.CalcMinMaxWidth(content, out _, out width);
|
||
|
return width;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Calls <see cref="GUIStyle.CalcMinMaxWidth"/> and returns the max width.
|
||
|
/// <para></para>
|
||
|
/// This method uses the <see cref="TempContent(string, string, bool)"/>.
|
||
|
/// </summary>
|
||
|
public static float CalculateWidth(this GUIStyle style, string content)
|
||
|
{
|
||
|
return style.CalculateWidth(TempContent(content));
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Creates a <see cref="ConversionCache{TKey, TValue}"/> for calculating the GUI width occupied by text using the
|
||
|
/// specified `style`.
|
||
|
/// </summary>
|
||
|
public static ConversionCache<string, float> CreateWidthCache(GUIStyle style)
|
||
|
{
|
||
|
return new ConversionCache<string, float>((text) => style.CalculateWidth(text));
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
private static ConversionCache<string, float> _LabelWidthCache;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Calls <see cref="GUIStyle.CalcMinMaxWidth"/> using <see cref="GUISkin.label"/> and returns the max
|
||
|
/// width. The result is cached for efficient reuse.
|
||
|
/// <para></para>
|
||
|
/// This method uses the <see cref="TempContent(string, string, bool)"/>.
|
||
|
/// </summary>
|
||
|
public static float CalculateLabelWidth(string text)
|
||
|
{
|
||
|
if (_LabelWidthCache == null)
|
||
|
_LabelWidthCache = CreateWidthCache(GUI.skin.label);
|
||
|
|
||
|
return _LabelWidthCache.Convert(text);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Begins a vertical layout group using the given style and decreases the
|
||
|
/// <see cref="EditorGUIUtility.labelWidth"/> to compensate for the indentation.
|
||
|
/// </summary>
|
||
|
public static void BeginVerticalBox(GUIStyle style)
|
||
|
{
|
||
|
if (style == null)
|
||
|
{
|
||
|
GUILayout.BeginVertical();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
GUILayout.BeginVertical(style);
|
||
|
EditorGUIUtility.labelWidth -= style.padding.left;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Ends a layout group started by <see cref="BeginVerticalBox"/> and restores the
|
||
|
/// <see cref="EditorGUIUtility.labelWidth"/>.
|
||
|
/// </summary>
|
||
|
public static void EndVerticalBox(GUIStyle style)
|
||
|
{
|
||
|
if (style != null)
|
||
|
EditorGUIUtility.labelWidth += style.padding.left;
|
||
|
|
||
|
GUILayout.EndVertical();
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
#endregion
|
||
|
/************************************************************************************************************************/
|
||
|
#region Labels
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>Used by <see cref="TempContent(string, string, bool)"/>.</summary>
|
||
|
private static GUIContent _TempContent;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Returns a <see cref="GUIContent"/> with the specified parameters. The same instance is returned by
|
||
|
/// every subsequent call.
|
||
|
/// </summary>
|
||
|
public static GUIContent TempContent(string text = null, string tooltip = null, bool narrowText = true)
|
||
|
{
|
||
|
if (_TempContent == null)
|
||
|
_TempContent = new GUIContent();
|
||
|
|
||
|
if (narrowText)
|
||
|
text = GetNarrowText(text);
|
||
|
|
||
|
_TempContent.text = text;
|
||
|
_TempContent.tooltip = tooltip;
|
||
|
return _TempContent;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Returns a <see cref="GUIContent"/> with the <see cref="SerializedProperty.displayName"/> and
|
||
|
/// <see cref="SerializedProperty.tooltip"/>. The same instance is returned by every subsequent call.
|
||
|
/// </summary>
|
||
|
public static GUIContent TempContent(SerializedProperty property, bool narrowText = true)
|
||
|
{
|
||
|
return TempContent(property.displayName, property.tooltip, narrowText);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
private static ConversionCache<float, string> _F1Cache;
|
||
|
private static GUIStyle _WeightLabelStyle;
|
||
|
private static float _WeightValueWidth;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Draws a label showing the `weight` aligned to the right side of the `area` and reduces its
|
||
|
/// <see cref="Rect.width"/> to remove that label from its area.
|
||
|
/// </summary>
|
||
|
public static void DoWeightLabel(ref Rect area, float weight)
|
||
|
{
|
||
|
if (_F1Cache == null)
|
||
|
{
|
||
|
_F1Cache = new ConversionCache<float, string>((value) => value.ToString("F1"));
|
||
|
_WeightLabelStyle = new GUIStyle(GUI.skin.label);
|
||
|
_WeightValueWidth = _WeightLabelStyle.CalculateWidth("0.0");
|
||
|
}
|
||
|
|
||
|
var weightArea = StealFromRight(ref area, _WeightValueWidth);
|
||
|
|
||
|
var label = _F1Cache.Convert(weight);
|
||
|
|
||
|
_WeightLabelStyle.normal.textColor = Color.Lerp(Color.grey, TextColor, weight);
|
||
|
_WeightLabelStyle.fontStyle = Mathf.Approximately(weight * 10, (int)(weight * 10)) ?
|
||
|
FontStyle.Normal : FontStyle.Italic;
|
||
|
|
||
|
GUI.Label(weightArea, label, _WeightLabelStyle);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>The <see cref="EditorGUIUtility.labelWidth"/> from before <see cref="BeginTightLabel"/>.</summary>
|
||
|
private static float _TightLabelWidth;
|
||
|
|
||
|
/// <summary>Stores the <see cref="EditorGUIUtility.labelWidth"/> and changes it to the exact width of the `label`.</summary>
|
||
|
public static string BeginTightLabel(string label)
|
||
|
{
|
||
|
_TightLabelWidth = EditorGUIUtility.labelWidth;
|
||
|
EditorGUIUtility.labelWidth = CalculateLabelWidth(label) + EditorGUI.indentLevel * IndentSize;
|
||
|
return GetNarrowText(label);
|
||
|
}
|
||
|
|
||
|
/// <summary>Reverts <see cref="EditorGUIUtility.labelWidth"/> to its previous value.</summary>
|
||
|
public static void EndTightLabel()
|
||
|
{
|
||
|
EditorGUIUtility.labelWidth = _TightLabelWidth;
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
private static ConversionCache<string, string> _NarrowTextCache;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Returns the `text` without any spaces if <see cref="EditorGUIUtility.wideMode"/> is false.
|
||
|
/// Otherwise simply returns the `text` without any changes.
|
||
|
/// </summary>
|
||
|
public static string GetNarrowText(string text)
|
||
|
{
|
||
|
if (EditorGUIUtility.wideMode ||
|
||
|
string.IsNullOrEmpty(text))
|
||
|
return text;
|
||
|
|
||
|
if (_NarrowTextCache == null)
|
||
|
_NarrowTextCache = new ConversionCache<string, string>((str) => str.Replace(" ", ""));
|
||
|
|
||
|
return _NarrowTextCache.Convert(text);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
#endregion
|
||
|
/************************************************************************************************************************/
|
||
|
#region Events
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Returns true and uses the current event if it is <see cref="EventType.MouseUp"/> inside the specified
|
||
|
/// `area`.
|
||
|
/// </summary>
|
||
|
public static bool TryUseClickEvent(Rect area, int button = -1)
|
||
|
{
|
||
|
var currentEvent = Event.current;
|
||
|
if (currentEvent.type == EventType.MouseUp &&
|
||
|
(button < 0 || currentEvent.button == button) &&
|
||
|
area.Contains(currentEvent.mousePosition))
|
||
|
{
|
||
|
GUI.changed = true;
|
||
|
currentEvent.Use();
|
||
|
return true;
|
||
|
}
|
||
|
else return false;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Returns true and uses the current event if it is <see cref="EventType.MouseUp"/> inside the last GUI Layout
|
||
|
/// <see cref="Rect"/> that was drawn.
|
||
|
/// </summary>
|
||
|
public static bool TryUseClickEventInLastRect(int button = -1)
|
||
|
{
|
||
|
return TryUseClickEvent(GUILayoutUtility.GetLastRect(), button);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Invokes `onDrop` if the <see cref="Event.current"/> is a drag and drop event inside the `dropArea`.
|
||
|
/// </summary>
|
||
|
public static void HandleDragAndDrop<T>(Rect dropArea, Func<T, bool> validate, Action<T> onDrop) where T : class
|
||
|
{
|
||
|
if (!dropArea.Contains(Event.current.mousePosition))
|
||
|
return;
|
||
|
|
||
|
bool isDrop;
|
||
|
switch (Event.current.type)
|
||
|
{
|
||
|
case EventType.DragUpdated:
|
||
|
isDrop = false;
|
||
|
break;
|
||
|
|
||
|
case EventType.DragPerform:
|
||
|
isDrop = true;
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var dragging = DragAndDrop.objectReferences;
|
||
|
TryDrop(dragging, validate, onDrop, isDrop);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Updates the <see cref="DragAndDrop.visualMode"/> of calls `onDrop` for each of the `objects`.
|
||
|
/// </summary>
|
||
|
private static void TryDrop<T>(IEnumerable objects, Func<T, bool> validate, Action<T> onDrop, bool isDrop) where T : class
|
||
|
{
|
||
|
if (objects == null)
|
||
|
return;
|
||
|
|
||
|
var droppedAny = false;
|
||
|
|
||
|
foreach (var obj in objects)
|
||
|
{
|
||
|
var t = obj as T;
|
||
|
|
||
|
if (t != null && (validate == null || validate(t)))
|
||
|
{
|
||
|
if (!isDrop)
|
||
|
{
|
||
|
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
|
||
|
break;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
onDrop(t);
|
||
|
droppedAny = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (droppedAny)
|
||
|
GUIUtility.ExitGUI();
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Uses <see cref="HandleDragAndDrop"/> to deal with drag and drop operations involving
|
||
|
/// <see cref="AnimationClip"/>s of <see cref="IAnimationClipSource"/>s.
|
||
|
/// </summary>
|
||
|
public static void HandleDragAndDropAnimations(Rect dropArea, Action<AnimationClip> onDrop)
|
||
|
{
|
||
|
HandleDragAndDrop(dropArea, (clip) => !clip.legacy, onDrop);
|
||
|
|
||
|
HandleDragAndDrop<IAnimationClipSource>(dropArea, null, (source) =>
|
||
|
{
|
||
|
var clips = ObjectPool.AcquireList<AnimationClip>();
|
||
|
source.GetAnimationClips(clips);
|
||
|
TryDrop(clips, (clip) => !clip.legacy, onDrop, true);
|
||
|
ObjectPool.Release(clips);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>Deselects any selected IMGUI control.</summary>
|
||
|
public static void Deselect()
|
||
|
{
|
||
|
GUIUtility.keyboardControl = 0;
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
#endregion
|
||
|
/************************************************************************************************************************/
|
||
|
#region Fields
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Draw a <see cref="EditorGUI.FloatField(Rect, GUIContent, float)"/> with an alternate cached string when it
|
||
|
/// is not selected (for example, "1" might become "1s" to indicate "seconds").
|
||
|
/// </summary>
|
||
|
public static float DoSpecialFloatField(Rect area, GUIContent label, float value, ConversionCache<float, string> toString)
|
||
|
{
|
||
|
// Treat most events normally, but when repainting show a text field with the cached string.
|
||
|
|
||
|
if (label != null)
|
||
|
{
|
||
|
if (Event.current.type != EventType.Repaint)
|
||
|
return EditorGUI.FloatField(area, label, value);
|
||
|
|
||
|
var dragArea = new Rect(area.x, area.y, EditorGUIUtility.labelWidth, area.height);
|
||
|
EditorGUIUtility.AddCursorRect(dragArea, MouseCursor.SlideArrow);
|
||
|
|
||
|
EditorGUI.TextField(area, label, toString.Convert(value));
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
var indentLevel = EditorGUI.indentLevel;
|
||
|
EditorGUI.indentLevel = 0;
|
||
|
|
||
|
if (Event.current.type != EventType.Repaint)
|
||
|
value = EditorGUI.FloatField(area, value);
|
||
|
else
|
||
|
EditorGUI.TextField(area, toString.Convert(value));
|
||
|
|
||
|
EditorGUI.indentLevel = indentLevel;
|
||
|
}
|
||
|
|
||
|
return value;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Draw a <see cref="EditorGUI.FloatField(Rect, GUIContent, float)"/> with an alternate cached string when it
|
||
|
/// is not selected (for example, "1" might become "1s" to indicate "seconds").
|
||
|
/// </summary>
|
||
|
public static void DoFloatFieldWithSuffix(Rect area, GUIContent label, SerializedProperty property,
|
||
|
ConversionCache<float, string> toString)
|
||
|
{
|
||
|
label = EditorGUI.BeginProperty(area, label, property);
|
||
|
EditorGUI.BeginChangeCheck();
|
||
|
var value = DoSpecialFloatField(area, label, property.floatValue, toString);
|
||
|
if (EditorGUI.EndChangeCheck())
|
||
|
property.floatValue = value;
|
||
|
EditorGUI.EndProperty();
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Draw a <see cref="GUI.Toggle(Rect, bool, GUIContent)"/> which sets the value to <see cref="float.NaN"/>
|
||
|
/// when disabled followed by two float fields to display the <see cref="SerializedProperty.floatValue"/> as
|
||
|
/// both normalized time and seconds.
|
||
|
/// </summary>
|
||
|
public static void DoOptionalTimeField(ref Rect area, GUIContent label, SerializedProperty property,
|
||
|
bool timeIsNormalized, float length, float defaultValue = 0, bool isOptional = true)
|
||
|
{
|
||
|
label = EditorGUI.BeginProperty(area, label, property);
|
||
|
EditorGUI.BeginChangeCheck();
|
||
|
|
||
|
var value = DoOptionalTimeField(ref area, label, property.floatValue, timeIsNormalized,
|
||
|
length, defaultValue, isOptional);
|
||
|
|
||
|
if (EditorGUI.EndChangeCheck())
|
||
|
property.floatValue = value;
|
||
|
EditorGUI.EndProperty();
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
private static ConversionCache<float, string> _XSuffixCache, _SSuffixCache;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Draw a <see cref="GUI.Toggle(Rect, bool, GUIContent)"/> which sets the value to <see cref="float.NaN"/>
|
||
|
/// when disabled followed by two float fields to display the `time` both normalized and in seconds.
|
||
|
/// </summary>
|
||
|
public static float DoOptionalTimeField(ref Rect area, GUIContent label, float time, bool timeIsNormalized,
|
||
|
float length, float defaultValue = 0, bool isOptional = true)
|
||
|
{
|
||
|
if (_XSuffixCache == null)
|
||
|
{
|
||
|
_XSuffixCache = new ConversionCache<float, string>((x) => x + "x");
|
||
|
_SSuffixCache = new ConversionCache<float, string>((s) => s + "s");
|
||
|
}
|
||
|
|
||
|
area.height = LineHeight;
|
||
|
|
||
|
bool showNormalized, showSeconds;
|
||
|
if (length > 0)
|
||
|
{
|
||
|
showNormalized = showSeconds = true;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
showNormalized = timeIsNormalized;
|
||
|
showSeconds = !timeIsNormalized;
|
||
|
}
|
||
|
|
||
|
var labelWidth = EditorGUIUtility.labelWidth;
|
||
|
var enabled = GUI.enabled;
|
||
|
|
||
|
var toggleArea = area;
|
||
|
if (isOptional)
|
||
|
{
|
||
|
toggleArea.x += EditorGUIUtility.labelWidth;
|
||
|
|
||
|
toggleArea.width = ToggleWidth;
|
||
|
EditorGUIUtility.labelWidth += toggleArea.width;
|
||
|
|
||
|
EditorGUIUtility.AddCursorRect(toggleArea, MouseCursor.Arrow);
|
||
|
|
||
|
// We need to draw the toggle after everything else to it goes on top of the label. But we want it to
|
||
|
// get priority for input events, so we disable the other controls during those events in its area.
|
||
|
var currentEvent = Event.current;
|
||
|
if (enabled && toggleArea.Contains(currentEvent.mousePosition))
|
||
|
{
|
||
|
switch (currentEvent.type)
|
||
|
{
|
||
|
case EventType.Repaint:
|
||
|
case EventType.Layout:
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
GUI.enabled = false;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else if (float.IsNaN(time))
|
||
|
{
|
||
|
time = defaultValue;
|
||
|
}
|
||
|
|
||
|
var displayTime = float.IsNaN(time) ? defaultValue : time;
|
||
|
|
||
|
var normalizedArea = area;
|
||
|
var secondsArea = area;
|
||
|
|
||
|
if (showNormalized)
|
||
|
{
|
||
|
if (showSeconds)
|
||
|
{
|
||
|
var split = (EditorGUIUtility.labelWidth + normalizedArea.xMax - StandardSpacing) * 0.5f;
|
||
|
normalizedArea.xMax = split;
|
||
|
secondsArea.xMin = split + StandardSpacing;
|
||
|
}
|
||
|
|
||
|
var normalizedTime = timeIsNormalized ? displayTime : displayTime / length;
|
||
|
|
||
|
EditorGUI.BeginChangeCheck();
|
||
|
normalizedTime = DoSpecialFloatField(normalizedArea, label, normalizedTime, _XSuffixCache);
|
||
|
if (EditorGUI.EndChangeCheck())
|
||
|
time = timeIsNormalized ? normalizedTime : normalizedTime * length;
|
||
|
}
|
||
|
|
||
|
EditorGUIUtility.labelWidth = labelWidth;
|
||
|
|
||
|
if (showSeconds)
|
||
|
{
|
||
|
var rawTime = timeIsNormalized ? displayTime * length : displayTime;
|
||
|
|
||
|
if (showNormalized)
|
||
|
label = null;
|
||
|
|
||
|
EditorGUI.BeginChangeCheck();
|
||
|
rawTime = DoSpecialFloatField(secondsArea, label, rawTime, _SSuffixCache);
|
||
|
if (EditorGUI.EndChangeCheck())
|
||
|
{
|
||
|
if (timeIsNormalized)
|
||
|
{
|
||
|
if (length != 0)
|
||
|
time = rawTime / length;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
time = rawTime;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
GUI.enabled = enabled;
|
||
|
|
||
|
if (isOptional)
|
||
|
DoOptionalTimeToggle(toggleArea, ref time, defaultValue);
|
||
|
|
||
|
return time;
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
private static void DoOptionalTimeToggle(Rect area, ref float time, float defaultValue)
|
||
|
{
|
||
|
#if UNITY_2019_3_OR_NEWER
|
||
|
area.x += 2;
|
||
|
#endif
|
||
|
|
||
|
var wasEnabled = !float.IsNaN(time);
|
||
|
|
||
|
var isEnabled = GUI.Toggle(area, wasEnabled, GUIContent.none);
|
||
|
|
||
|
if (isEnabled != wasEnabled)
|
||
|
{
|
||
|
time = isEnabled ? defaultValue : float.NaN;
|
||
|
Deselect();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
#endregion
|
||
|
/************************************************************************************************************************/
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#endif
|
||
|
|