UnitsAttributeDrawer.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. #if UNITY_EDITOR && UNITY_IMGUI
  3. using Animancer.Editor;
  4. using System;
  5. using UnityEditor;
  6. using UnityEngine;
  7. using static Animancer.Editor.AnimancerGUI;
  8. namespace Animancer.Units.Editor
  9. {
  10. /// <summary>[Editor-Only] A <see cref="PropertyDrawer"/> for fields with a <see cref="UnitsAttribute"/>.</summary>
  11. /// https://kybernetik.com.au/animancer/api/Animancer.Units.Editor/UnitsAttributeDrawer
  12. [CustomPropertyDrawer(typeof(UnitsAttribute), true)]
  13. public class UnitsAttributeDrawer : PropertyDrawer
  14. {
  15. /************************************************************************************************************************/
  16. /// <summary>The attribute on the field being drawn.</summary>
  17. public UnitsAttribute Attribute { get; private set; }
  18. /// <summary>The converters used to generate display strings for each of the fields.</summary>
  19. public CompactUnitConversionCache[] DisplayConverters { get; private set; }
  20. /************************************************************************************************************************/
  21. /// <summary>Gathers the <see cref="Attribute"/> and sets up the <see cref="DisplayConverters"/>.</summary>
  22. public void Initialize()
  23. => Initialize(attribute);
  24. /// <summary>Gathers the <see cref="Attribute"/> and sets up the <see cref="DisplayConverters"/>.</summary>
  25. public void Initialize(Attribute attribute)
  26. {
  27. if (Attribute != null)
  28. return;
  29. Attribute = (UnitsAttribute)attribute;
  30. var suffixes = Attribute.Suffixes;
  31. DisplayConverters = new CompactUnitConversionCache[suffixes.Length];
  32. for (int i = 0; i < suffixes.Length; i++)
  33. DisplayConverters[i] = new(suffixes[i]);
  34. }
  35. /************************************************************************************************************************/
  36. /// <inheritdoc/>
  37. public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
  38. {
  39. var lineCount = GetLineCount(property, label);
  40. return LineHeight * lineCount + StandardSpacing * (lineCount - 1);
  41. }
  42. /// <summary>Determines how many lines tall the `property` should be.</summary>
  43. protected virtual int GetLineCount(SerializedProperty property, GUIContent label)
  44. => EditorGUIUtility.wideMode
  45. ? 1
  46. : 2;
  47. /************************************************************************************************************************/
  48. /// <summary>Begins a GUI property block to be ended by <see cref="EndProperty"/>.</summary>
  49. protected static void BeginProperty(
  50. Rect area,
  51. SerializedProperty property,
  52. ref GUIContent label,
  53. out float value)
  54. {
  55. label = EditorGUI.BeginProperty(area, label, property);
  56. EditorGUI.BeginChangeCheck();
  57. value = property.floatValue;
  58. }
  59. /// <summary>Ends a GUI property block started by <see cref="BeginProperty"/>.</summary>
  60. protected static void EndProperty(
  61. Rect area,
  62. SerializedProperty property,
  63. ref float value)
  64. {
  65. if (TryUseClickEvent(area, 2))
  66. DefaultValues.SetToDefault(ref value, property);
  67. if (EditorGUI.EndChangeCheck())
  68. property.floatValue = value;
  69. EditorGUI.EndProperty();
  70. }
  71. /************************************************************************************************************************/
  72. /// <summary>Draws this attribute's fields for the `property`.</summary>
  73. public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
  74. {
  75. Initialize();
  76. BeginProperty(area, property, ref label, out var value);
  77. DoFieldGUI(area, label, ref value);
  78. EndProperty(area, property, ref value);
  79. }
  80. /************************************************************************************************************************/
  81. private static readonly int TextFieldHash = "EditorTextField".GetHashCode();
  82. /// <summary>Draws this attribute's fields.</summary>
  83. public void DoFieldGUI(Rect area, GUIContent label, ref float value)
  84. {
  85. var isMultiLine = area.height >= LineHeight * 2;
  86. area.height = LineHeight;
  87. DoOptionalBeforeGUI(
  88. Attribute.IsOptional,
  89. area,
  90. out var toggleArea,
  91. out var guiWasEnabled,
  92. out var previousLabelWidth);
  93. var hasLabel = label != null && !string.IsNullOrEmpty(label.text);
  94. Rect allFieldArea;
  95. if (isMultiLine)
  96. {
  97. EditorGUI.LabelField(area, label);
  98. label = null;
  99. NextVerticalArea(ref area);
  100. EditorGUI.indentLevel++;
  101. allFieldArea = EditorGUI.IndentedRect(area);
  102. EditorGUI.indentLevel--;
  103. }
  104. else if (hasLabel)
  105. {
  106. var labelXMax = area.x + EditorGUIUtility.labelWidth;
  107. allFieldArea = new(labelXMax, area.y, area.xMax - labelXMax, area.height);
  108. }
  109. else
  110. {
  111. allFieldArea = area;
  112. }
  113. CountActiveFields(out var count, out var last);
  114. var currentEvent = Event.current;
  115. var beforeControlID = GUIUtility.GetControlID(TextFieldHash, FocusType.Passive, area);
  116. if (float.IsNaN(value) &&
  117. Attribute.DisabledText is not null &&
  118. currentEvent.type == EventType.Repaint &&
  119. !area.Contains(currentEvent.mousePosition) &&
  120. !HasKeyboardControl(beforeControlID, beforeControlID + count))
  121. {
  122. var dragArea = area;
  123. dragArea.width = EditorGUIUtility.labelWidth;
  124. EditorGUIUtility.AddCursorRect(dragArea, MouseCursor.SlideArrow);
  125. label ??= GUIContent.none;
  126. EditorGUI.TextField(area, label, Attribute.DisabledText);
  127. for (int i = 1; i < count; i++)
  128. GUIUtility.GetControlID(TextFieldHash, FocusType.Keyboard, area);
  129. }
  130. else
  131. {
  132. var width = (allFieldArea.width - StandardSpacing * (count - 1)) / count;
  133. var fieldArea = new Rect(allFieldArea.x, allFieldArea.y, width, allFieldArea.height);
  134. var displayValue = GetDisplayValue(value, Attribute.DefaultValue);
  135. // Draw the active fields.
  136. for (int i = 0; i < Attribute.Multipliers.Length; i++)
  137. {
  138. var multiplier = Attribute.Multipliers[i];
  139. if (float.IsNaN(multiplier))
  140. continue;
  141. if (hasLabel)
  142. {
  143. fieldArea.xMin = area.xMin;
  144. }
  145. else if (i < last)
  146. {
  147. fieldArea.width = width;
  148. fieldArea.xMax = AnimancerUtilities.Round(fieldArea.xMax);
  149. }
  150. else
  151. {
  152. fieldArea.xMax = area.xMax;
  153. }
  154. EditorGUI.BeginChangeCheck();
  155. var fieldValue = displayValue * multiplier;
  156. fieldValue = DoSpecialFloatField(fieldArea, label, fieldValue, DisplayConverters[i]);
  157. label = null;
  158. hasLabel = false;
  159. if (EditorGUI.EndChangeCheck())
  160. value = fieldValue / multiplier;
  161. fieldArea.x += fieldArea.width + StandardSpacing;
  162. }
  163. }
  164. DoOptionalAfterGUI(
  165. Attribute.IsOptional,
  166. toggleArea,
  167. ref value,
  168. Attribute.DefaultValue,
  169. guiWasEnabled,
  170. previousLabelWidth);
  171. Validate.ValueRule(ref value, Attribute.Rule);
  172. }
  173. /************************************************************************************************************************/
  174. /// <summary>Counts the number of active <see cref="UnitsAttribute.Multipliers"/>.</summary>
  175. private void CountActiveFields(out int count, out int last)
  176. {
  177. count = 0;
  178. last = 0;
  179. for (int i = 0; i < Attribute.Multipliers.Length; i++)
  180. {
  181. if (!float.IsNaN(Attribute.Multipliers[i]))
  182. {
  183. count++;
  184. last = i;
  185. }
  186. }
  187. }
  188. /************************************************************************************************************************/
  189. /// <summary>Is the <see cref="GUIUtility.keyboardControl"/> in the specified range (inclusive)?</summary>
  190. private static bool HasKeyboardControl(int minControlID, int maxControlID)
  191. {
  192. var keyboardControl = GUIUtility.keyboardControl;
  193. return keyboardControl >= minControlID && keyboardControl <= maxControlID;
  194. }
  195. /************************************************************************************************************************/
  196. /// <summary>
  197. /// Draws a <see cref="EditorGUI.FloatField(Rect, GUIContent, float)"/> with an alternate string
  198. /// when it's not selected (for example, "1" might display as "1s" to indicate "seconds").
  199. /// </summary>
  200. /// <remarks>
  201. /// This method treats most <see cref="EventType"/>s normally,
  202. /// but for <see cref="EventType.Repaint"/> it instead draws a text field with the converted string.
  203. /// </remarks>
  204. public static float DoSpecialFloatField(
  205. Rect area,
  206. GUIContent label,
  207. float value,
  208. CompactUnitConversionCache toString)
  209. {
  210. if (label != null && !string.IsNullOrEmpty(label.text))
  211. {
  212. if (Event.current.type != EventType.Repaint)
  213. return EditorGUI.FloatField(area, label, value);
  214. var dragArea = new Rect(area.x, area.y, EditorGUIUtility.labelWidth, area.height);
  215. EditorGUIUtility.AddCursorRect(dragArea, MouseCursor.SlideArrow);
  216. var text = toString.Convert(value, area.width - EditorGUIUtility.labelWidth);
  217. EditorGUI.TextField(area, label, text);
  218. }
  219. else
  220. {
  221. var indentLevel = EditorGUI.indentLevel;
  222. EditorGUI.indentLevel = 0;
  223. if (Event.current.type != EventType.Repaint)
  224. value = EditorGUI.FloatField(area, value);
  225. else
  226. EditorGUI.TextField(area, toString.Convert(value, area.width));
  227. EditorGUI.indentLevel = indentLevel;
  228. }
  229. return value;
  230. }
  231. /************************************************************************************************************************/
  232. /// <summary>Prepares the details for drawing a toggle to set the field to <see cref="float.NaN"/>.</summary>
  233. /// <remarks>Call this before drawing the field then call <see cref="DoOptionalAfterGUI"/> after it.</remarks>
  234. public void DoOptionalBeforeGUI(
  235. bool isOptional,
  236. Rect area,
  237. out Rect toggleArea,
  238. out bool guiWasEnabled,
  239. out float previousLabelWidth)
  240. {
  241. toggleArea = area;
  242. guiWasEnabled = GUI.enabled;
  243. previousLabelWidth = EditorGUIUtility.labelWidth;
  244. if (!isOptional)
  245. return;
  246. toggleArea.x += previousLabelWidth;
  247. toggleArea.width = ToggleWidth;
  248. EditorGUIUtility.labelWidth += toggleArea.width;
  249. EditorGUIUtility.AddCursorRect(toggleArea, MouseCursor.Arrow);
  250. // We need to draw the toggle after everything else to it goes on top of the label. But we want it to
  251. // get priority for input events, so we disable the other controls during those events in its area.
  252. var currentEvent = Event.current;
  253. if (guiWasEnabled && toggleArea.Contains(currentEvent.mousePosition))
  254. {
  255. switch (currentEvent.type)
  256. {
  257. case EventType.Repaint:
  258. case EventType.Layout:
  259. break;
  260. default:
  261. GUI.enabled = false;
  262. break;
  263. }
  264. }
  265. }
  266. /************************************************************************************************************************/
  267. /// <summary>Draws a toggle to set the `value` to <see cref="float.NaN"/> when disabled.</summary>
  268. public void DoOptionalAfterGUI(
  269. bool isOptional,
  270. Rect area,
  271. ref float value,
  272. float defaultValue,
  273. bool guiWasEnabled,
  274. float previousLabelWidth)
  275. {
  276. GUI.enabled = guiWasEnabled;
  277. EditorGUIUtility.labelWidth = previousLabelWidth;
  278. if (!isOptional)
  279. return;
  280. area.x += StandardSpacing;
  281. var wasEnabled = !float.IsNaN(value);
  282. // Use the EditorGUI method instead to properly handle EditorGUI.showMixedValue.
  283. //var isEnabled = GUI.Toggle(area, wasEnabled, GUIContent.none);
  284. var indentLevel = EditorGUI.indentLevel;
  285. EditorGUI.indentLevel = 0;
  286. var isEnabled = EditorGUI.Toggle(area, wasEnabled);
  287. EditorGUI.indentLevel = indentLevel;
  288. if (isEnabled != wasEnabled)
  289. {
  290. value = isEnabled ? defaultValue : float.NaN;
  291. Deselect();
  292. }
  293. }
  294. /************************************************************************************************************************/
  295. /// <summary>Returns the value that should be displayed for a given field.</summary>
  296. public static float GetDisplayValue(float value, float defaultValue)
  297. => float.IsNaN(value) ? defaultValue : value;
  298. /************************************************************************************************************************/
  299. }
  300. }
  301. #endif