ManualMixerTransitionDrawer.cs 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. #if UNITY_EDITOR && UNITY_IMGUI
  3. using Animancer.Units.Editor;
  4. using System;
  5. using System.Collections.Generic;
  6. using UnityEditor;
  7. using UnityEditorInternal;
  8. using UnityEngine;
  9. using static Animancer.Editor.AnimancerGUI;
  10. using Object = UnityEngine.Object;
  11. namespace Animancer.Editor
  12. {
  13. /// <inheritdoc/>
  14. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/ManualMixerTransitionDrawer
  15. [CustomPropertyDrawer(typeof(ManualMixerTransition), true)]
  16. public class ManualMixerTransitionDrawer : TransitionDrawer
  17. {
  18. /************************************************************************************************************************/
  19. /// <summary>Should two lines be used to draw each child?</summary>
  20. public static readonly BoolPref
  21. TwoLineMode = new(
  22. nameof(ManualMixerTransitionDrawer) + "." + nameof(TwoLineMode),
  23. "Two Line Mode",
  24. true);
  25. /************************************************************************************************************************/
  26. /// <summary>The property currently being drawn.</summary>
  27. /// <remarks>
  28. /// Normally each property has its own drawer,
  29. /// but arrays share a single drawer for all elements.
  30. /// </remarks>
  31. public static SerializedProperty CurrentProperty { get; private set; }
  32. /// <summary>The <see cref="ManualMixerTransition{TState}.Animations"/> field.</summary>
  33. public static SerializedProperty CurrentAnimations { get; private set; }
  34. /// <summary>The <see cref="ManualMixerTransition{TState}.Speeds"/> field.</summary>
  35. public static SerializedProperty CurrentSpeeds { get; private set; }
  36. /// <summary>The <see cref="ManualMixerTransition{TState}.SynchronizeChildren"/> field.</summary>
  37. public static SerializedProperty CurrentSynchronizeChildren { get; private set; }
  38. private readonly Dictionary<string, ReorderableList>
  39. PropertyPathToStates = new();
  40. private ReorderableList _MultiSelectDummyList;
  41. /************************************************************************************************************************/
  42. /// <summary>Gather the details of the `property`.</summary>
  43. /// <remarks>
  44. /// This method gets called by every <see cref="GetPropertyHeight"/> and <see cref="OnGUI"/> call since
  45. /// Unity uses the same <see cref="PropertyDrawer"/> instance for each element in a collection, so it
  46. /// needs to gather the details associated with the current property.
  47. /// </remarks>
  48. protected virtual ReorderableList GatherDetails(SerializedProperty property)
  49. {
  50. InitializeMode(property);
  51. GatherSubProperties(property);
  52. if (property.hasMultipleDifferentValues)
  53. {
  54. return _MultiSelectDummyList ??= new(new List<Object>(), typeof(Object))
  55. {
  56. elementHeight = LineHeight,
  57. displayAdd = false,
  58. displayRemove = false,
  59. footerHeight = 0,
  60. drawHeaderCallback = DoAnimationHeaderGUI,
  61. drawNoneElementCallback = area
  62. => EditorGUI.LabelField(area, "Multi-editing animations is not supported"),
  63. };
  64. }
  65. if (CurrentAnimations == null)
  66. return null;
  67. var path = property.propertyPath;
  68. if (!PropertyPathToStates.TryGetValue(path, out var states))
  69. {
  70. states = new(CurrentAnimations.serializedObject, CurrentAnimations)
  71. {
  72. drawHeaderCallback = DoChildListHeaderGUI,
  73. elementHeightCallback = GetElementHeight,
  74. drawElementCallback = DoElementGUI,
  75. onAddCallback = OnAddElement,
  76. onRemoveCallback = OnRemoveElement,
  77. onReorderCallbackWithDetails = OnReorderList,
  78. drawFooterCallback = DoChildListFooterGUI,
  79. };
  80. PropertyPathToStates.Add(path, states);
  81. }
  82. states.serializedProperty = CurrentAnimations;
  83. return states;
  84. }
  85. /************************************************************************************************************************/
  86. /// <summary>
  87. /// Called every time a `property` is drawn to find the relevant child properties and store them to be
  88. /// used in <see cref="GetPropertyHeight"/> and <see cref="OnGUI"/>.
  89. /// </summary>
  90. protected virtual void GatherSubProperties(SerializedProperty property)
  91. {
  92. CurrentProperty = property;
  93. CurrentAnimations = property.FindPropertyRelative(ManualMixerTransition.AnimationsField);
  94. CurrentSpeeds = property.FindPropertyRelative(ManualMixerTransition.SpeedsField);
  95. CurrentSynchronizeChildren = property.FindPropertyRelative(ManualMixerTransition.SynchronizeChildrenField);
  96. if (!property.hasMultipleDifferentValues &&
  97. CurrentAnimations != null &&
  98. CurrentSpeeds != null &&
  99. CurrentSpeeds.arraySize != 0)
  100. CurrentSpeeds.arraySize = CurrentAnimations.arraySize;
  101. }
  102. /************************************************************************************************************************/
  103. /// <summary>
  104. /// Adds a menu item that will call <see cref="GatherSubProperties"/> then run the specified
  105. /// `function`.
  106. /// </summary>
  107. protected void AddPropertyModifierFunction(GenericMenu menu, string label,
  108. MenuFunctionState state, Action<SerializedProperty> function)
  109. {
  110. Serialization.AddPropertyModifierFunction(menu, CurrentProperty, label, state, (property) =>
  111. {
  112. GatherSubProperties(property);
  113. function(property);
  114. });
  115. }
  116. /// <summary>
  117. /// Adds a menu item that will call <see cref="GatherSubProperties"/> then run the specified
  118. /// `function`.
  119. /// </summary>
  120. protected void AddPropertyModifierFunction(GenericMenu menu, string label,
  121. Action<SerializedProperty> function)
  122. {
  123. Serialization.AddPropertyModifierFunction(menu, CurrentProperty, label, (property) =>
  124. {
  125. GatherSubProperties(property);
  126. function(property);
  127. });
  128. }
  129. /************************************************************************************************************************/
  130. /// <inheritdoc/>
  131. public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
  132. {
  133. var height = EditorGUI.GetPropertyHeight(property, label);
  134. if (property.isExpanded)
  135. {
  136. var states = GatherDetails(property);
  137. if (states != null)
  138. height += StandardSpacing +
  139. states.GetHeight();
  140. if (CurrentAnimations != null)
  141. height -= StandardSpacing +
  142. EditorGUI.GetPropertyHeight(CurrentAnimations, label);
  143. if (CurrentSpeeds != null)
  144. height -= StandardSpacing +
  145. EditorGUI.GetPropertyHeight(CurrentSpeeds, label);
  146. if (CurrentSynchronizeChildren != null)
  147. height -= StandardSpacing +
  148. EditorGUI.GetPropertyHeight(CurrentSynchronizeChildren, label);
  149. }
  150. return height;
  151. }
  152. /************************************************************************************************************************/
  153. private SerializedProperty _RootProperty;
  154. private ReorderableList _CurrentChildList;
  155. /// <inheritdoc/>
  156. public override void OnGUI(Rect area, SerializedProperty property, GUIContent label)
  157. {
  158. _RootProperty = null;
  159. base.OnGUI(area, property, label);
  160. if (_RootProperty == null ||
  161. !_RootProperty.isExpanded)
  162. return;
  163. using (new DrawerContext(_RootProperty))
  164. {
  165. if (Context.Transition == null)
  166. return;
  167. _CurrentChildList = GatherDetails(_RootProperty);
  168. if (_CurrentChildList == null)
  169. return;
  170. var indentLevel = EditorGUI.indentLevel;
  171. area.yMin = area.yMax - _CurrentChildList.GetHeight();
  172. EditorGUI.indentLevel++;
  173. area = EditorGUI.IndentedRect(area);
  174. EditorGUI.indentLevel = 0;
  175. _CurrentChildList.DoList(area);
  176. EditorGUI.indentLevel = indentLevel;
  177. TryCollapseArrays();
  178. }
  179. }
  180. /************************************************************************************************************************/
  181. /// <inheritdoc/>
  182. protected override void DoChildPropertyGUI(
  183. ref Rect area,
  184. SerializedProperty rootProperty,
  185. SerializedProperty property,
  186. GUIContent label)
  187. {
  188. if (Context.Transition != null)
  189. {
  190. area.height = 0;
  191. // If we find the Animations property, hide it to draw it last.
  192. var path = property.propertyPath;
  193. if (path.EndsWith("." + ManualMixerTransition.AnimationsField))
  194. {
  195. _RootProperty = rootProperty;
  196. return;
  197. }
  198. else if (_RootProperty != null)
  199. {
  200. // If we already found the Animations property, also hide Speeds and Synchronize Children.
  201. if (path.EndsWith("." + ManualMixerTransition.SpeedsField) ||
  202. path.EndsWith("." + ManualMixerTransition.SynchronizeChildrenField))
  203. return;
  204. }
  205. }
  206. base.DoChildPropertyGUI(ref area, rootProperty, property, label);
  207. }
  208. /************************************************************************************************************************/
  209. private static float _SpeedLabelWidth;
  210. private static float _SyncLabelWidth;
  211. /// <summary>Splits the specified `area` into separate sections.</summary>
  212. protected static void SplitListRect(Rect area, bool isHeader,
  213. out Rect animation, out Rect speed, out Rect sync)
  214. {
  215. if (_SpeedLabelWidth == 0)
  216. _SpeedLabelWidth = AnimancerGUI.CalculateWidth(EditorStyles.popup, "Speed");
  217. if (_SyncLabelWidth == 0)
  218. _SyncLabelWidth = AnimancerGUI.CalculateWidth(EditorStyles.popup, "Sync");
  219. var spacing = StandardSpacing;
  220. var syncWidth = isHeader ?
  221. _SyncLabelWidth :
  222. ToggleWidth - spacing;
  223. var speedWidth = _SpeedLabelWidth + _SyncLabelWidth - syncWidth;
  224. if (!isHeader)
  225. {
  226. // Don't use Clamp because the max might be smaller than the min.
  227. var max = Math.Max(area.height, area.width * 0.25f - 30);
  228. speedWidth = Math.Min(speedWidth, max);
  229. }
  230. area.width += spacing;
  231. if (TwoLineMode && !isHeader)
  232. {
  233. animation = area;
  234. area.y += area.height;
  235. sync = StealFromRight(ref area, syncWidth, spacing);
  236. speed = area;
  237. }
  238. else
  239. {
  240. sync = StealFromRight(ref area, syncWidth, spacing);
  241. speed = StealFromRight(ref area, speedWidth, spacing);
  242. animation = area;
  243. }
  244. }
  245. /************************************************************************************************************************/
  246. #region Headers
  247. /************************************************************************************************************************/
  248. /// <summary>Draws the headdings of the child list.</summary>
  249. protected virtual void DoChildListHeaderGUI(Rect area)
  250. {
  251. SplitListRect(area, true, out var animationArea, out var speedArea, out var syncArea);
  252. DoAnimationHeaderGUI(animationArea);
  253. DoSpeedHeaderGUI(speedArea);
  254. DoSyncHeaderGUI(syncArea);
  255. }
  256. /************************************************************************************************************************/
  257. /// <summary>Draws an "Animation" header.</summary>
  258. protected void DoAnimationHeaderGUI(Rect area)
  259. {
  260. using (var label = PooledGUIContent.Acquire("Animation",
  261. $"The animations that will be used for each child state" +
  262. $"\n\nCtrl + Click to allow picking Transition Assets" +
  263. $" (or anything that implements {nameof(ITransition)})"))
  264. {
  265. DoHeaderDropdownGUI(area, CurrentAnimations, label, menu =>
  266. {
  267. menu.AddItem(new(TwoLineMode.MenuItem), TwoLineMode.Value, () =>
  268. {
  269. TwoLineMode.Value = !TwoLineMode.Value;
  270. ReSelectCurrentObjects();
  271. });
  272. });
  273. }
  274. }
  275. /************************************************************************************************************************/
  276. #region Speeds
  277. /************************************************************************************************************************/
  278. /// <summary>Draws a "Speed" header.</summary>
  279. protected void DoSpeedHeaderGUI(Rect area)
  280. {
  281. using (var label = PooledGUIContent.Acquire("Speed", Strings.Tooltips.Speed))
  282. {
  283. DoHeaderDropdownGUI(area, CurrentSpeeds, label, menu =>
  284. {
  285. AddPropertyModifierFunction(menu, "Reset All to 1",
  286. CurrentSpeeds.arraySize == 0 ? MenuFunctionState.Selected : MenuFunctionState.Normal,
  287. (_) => CurrentSpeeds.arraySize = 0);
  288. AddPropertyModifierFunction(menu, "Normalize Durations", MenuFunctionState.Normal, NormalizeDurations);
  289. });
  290. }
  291. }
  292. /************************************************************************************************************************/
  293. /// <summary>
  294. /// Recalculates the <see cref="CurrentSpeeds"/> depending on the <see cref="AnimationClip.length"/> of
  295. /// their animations so that they all take the same amount of time to play fully.
  296. /// </summary>
  297. private static void NormalizeDurations(SerializedProperty property)
  298. {
  299. var speedCount = CurrentSpeeds.arraySize;
  300. var lengths = new float[CurrentAnimations.arraySize];
  301. if (lengths.Length <= 1)
  302. return;
  303. int nonZeroLengths = 0;
  304. float totalLength = 0;
  305. float totalSpeed = 0;
  306. for (int i = 0; i < lengths.Length; i++)
  307. {
  308. var state = CurrentAnimations.GetArrayElementAtIndex(i).objectReferenceValue;
  309. if (AnimancerUtilities.TryGetLength(state, out var length) &&
  310. length > 0)
  311. {
  312. nonZeroLengths++;
  313. totalLength += length;
  314. lengths[i] = length;
  315. if (speedCount > 0)
  316. totalSpeed += CurrentSpeeds.GetArrayElementAtIndex(i).floatValue;
  317. }
  318. }
  319. if (nonZeroLengths == 0)
  320. return;
  321. var averageLength = totalLength / nonZeroLengths;
  322. var averageSpeed = speedCount > 0 ? totalSpeed / nonZeroLengths : 1;
  323. CurrentSpeeds.arraySize = lengths.Length;
  324. InitializeSpeeds(speedCount);
  325. for (int i = 0; i < lengths.Length; i++)
  326. {
  327. if (lengths[i] == 0)
  328. continue;
  329. CurrentSpeeds.GetArrayElementAtIndex(i).floatValue = averageSpeed * lengths[i] / averageLength;
  330. }
  331. TryCollapseArrays();
  332. }
  333. /************************************************************************************************************************/
  334. /// <summary>
  335. /// Initializes every element in the <see cref="CurrentSpeeds"/> array
  336. /// from the `start` to the end of the array to contain a value of 1.
  337. /// </summary>
  338. public static void InitializeSpeeds(int start)
  339. {
  340. var count = CurrentSpeeds.arraySize;
  341. while (start < count)
  342. CurrentSpeeds.GetArrayElementAtIndex(start++).floatValue = 1;
  343. }
  344. /************************************************************************************************************************/
  345. #endregion
  346. /************************************************************************************************************************/
  347. #region Sync
  348. /************************************************************************************************************************/
  349. /// <summary>Draws a "Sync" header.</summary>
  350. protected void DoSyncHeaderGUI(Rect area)
  351. {
  352. using (var label = PooledGUIContent.Acquire("Sync",
  353. "Determines which child states have their normalized times constantly synchronized"))
  354. {
  355. DoHeaderDropdownGUI(area, CurrentSpeeds, label, menu =>
  356. {
  357. var syncCount = CurrentSynchronizeChildren.arraySize;
  358. var allState = syncCount == 0 ? MenuFunctionState.Selected : MenuFunctionState.Normal;
  359. AddPropertyModifierFunction(menu, "All", allState,
  360. (_) => CurrentSynchronizeChildren.arraySize = 0);
  361. var syncNone = syncCount == CurrentAnimations.arraySize;
  362. if (syncNone)
  363. {
  364. for (int i = 0; i < syncCount; i++)
  365. {
  366. if (CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue)
  367. {
  368. syncNone = false;
  369. break;
  370. }
  371. }
  372. }
  373. var noneState = syncNone ? MenuFunctionState.Selected : MenuFunctionState.Normal;
  374. AddPropertyModifierFunction(menu, "None", noneState, (_) =>
  375. {
  376. var count = CurrentSynchronizeChildren.arraySize = CurrentAnimations.arraySize;
  377. for (int i = 0; i < count; i++)
  378. CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue = false;
  379. });
  380. AddPropertyModifierFunction(menu, "Invert", MenuFunctionState.Normal, (_) =>
  381. {
  382. var count = CurrentSynchronizeChildren.arraySize;
  383. for (int i = 0; i < count; i++)
  384. {
  385. var property = CurrentSynchronizeChildren.GetArrayElementAtIndex(i);
  386. property.boolValue = !property.boolValue;
  387. }
  388. var newCount = CurrentSynchronizeChildren.arraySize = CurrentAnimations.arraySize;
  389. for (int i = count; i < newCount; i++)
  390. CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue = false;
  391. });
  392. AddPropertyModifierFunction(menu, "Non-Stationary", MenuFunctionState.Normal, (_) =>
  393. {
  394. var count = CurrentAnimations.arraySize;
  395. for (int i = 0; i < count; i++)
  396. {
  397. var state = CurrentAnimations.GetArrayElementAtIndex(i).objectReferenceValue;
  398. if (state == null)
  399. continue;
  400. if (i >= syncCount)
  401. {
  402. CurrentSynchronizeChildren.arraySize = i + 1;
  403. for (int j = syncCount; j < i; j++)
  404. CurrentSynchronizeChildren.GetArrayElementAtIndex(j).boolValue = true;
  405. syncCount = i + 1;
  406. }
  407. CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue =
  408. AnimancerUtilities.TryGetAverageVelocity(state, out var velocity) &&
  409. velocity != default;
  410. }
  411. TryCollapseSync();
  412. });
  413. });
  414. }
  415. }
  416. /************************************************************************************************************************/
  417. private static void SyncNone()
  418. {
  419. var count = CurrentSynchronizeChildren.arraySize = CurrentAnimations.arraySize;
  420. for (int i = 0; i < count; i++)
  421. CurrentSynchronizeChildren.GetArrayElementAtIndex(i).boolValue = false;
  422. }
  423. /************************************************************************************************************************/
  424. #endregion
  425. /************************************************************************************************************************/
  426. /// <summary>Draws the GUI for a header dropdown button.</summary>
  427. public static void DoHeaderDropdownGUI(
  428. Rect area,
  429. SerializedProperty property,
  430. GUIContent content,
  431. Action<GenericMenu> populateMenu)
  432. {
  433. if (property != null)
  434. EditorGUI.BeginProperty(area, GUIContent.none, property);
  435. if (populateMenu != null)
  436. {
  437. if (EditorGUI.DropdownButton(area, content, FocusType.Passive))
  438. {
  439. var menu = new GenericMenu();
  440. populateMenu(menu);
  441. menu.ShowAsContext();
  442. }
  443. }
  444. else
  445. {
  446. GUI.Label(area, content);
  447. }
  448. if (property != null)
  449. EditorGUI.EndProperty();
  450. }
  451. /************************************************************************************************************************/
  452. /// <summary>Draws the footer of the child list.</summary>
  453. protected virtual void DoChildListFooterGUI(Rect area)
  454. {
  455. ReorderableList.defaultBehaviours.DrawFooter(area, _CurrentChildList);
  456. EditorGUI.BeginChangeCheck();
  457. area.xMax = EditorGUIUtility.labelWidth + IndentSize;
  458. area.y++;
  459. area.height = LineHeight;
  460. using (var label = PooledGUIContent.Acquire("Count"))
  461. {
  462. var indentLevel = EditorGUI.indentLevel;
  463. EditorGUI.indentLevel = 0;
  464. var labelWidth = EditorGUIUtility.labelWidth;
  465. EditorGUIUtility.labelWidth = CalculateLabelWidth(label.text);
  466. var count = EditorGUI.DelayedIntField(area, label, _CurrentChildList.count);
  467. if (EditorGUI.EndChangeCheck())
  468. ResizeList(count);
  469. EditorGUIUtility.labelWidth = labelWidth;
  470. EditorGUI.indentLevel = indentLevel;
  471. }
  472. }
  473. /************************************************************************************************************************/
  474. #endregion
  475. /************************************************************************************************************************/
  476. /// <summary>Calculates the height of the state at the specified `index`.</summary>
  477. protected virtual float GetElementHeight(int index)
  478. => TwoLineMode
  479. ? LineHeight * 2
  480. : LineHeight;
  481. /************************************************************************************************************************/
  482. /// <summary>Draws the GUI of the state at the specified `index`.</summary>
  483. private void DoElementGUI(Rect area, int index, bool isActive, bool isFocused)
  484. {
  485. if (index < 0 || index > CurrentAnimations.arraySize)
  486. return;
  487. area.height = LineHeight;
  488. var state = CurrentAnimations.GetArrayElementAtIndex(index);
  489. var speed = CurrentSpeeds.arraySize > 0
  490. ? CurrentSpeeds.GetArrayElementAtIndex(index)
  491. : null;
  492. DoElementGUI(area, index, state, speed);
  493. }
  494. /************************************************************************************************************************/
  495. /// <summary>Draws the GUI of the animation at the specified `index`.</summary>
  496. protected virtual void DoElementGUI(Rect area, int index,
  497. SerializedProperty animation, SerializedProperty speed)
  498. {
  499. SplitListRect(area, false, out var animationArea, out var speedArea, out var syncArea);
  500. DoAnimationField(animationArea, animation);
  501. DoSpeedFieldGUI(speedArea, speed, index);
  502. DoSyncToggleGUI(syncArea, index);
  503. }
  504. /************************************************************************************************************************/
  505. /// <summary>
  506. /// Draws an <see cref="EditorGUI.ObjectField(Rect, GUIContent, Object, Type, bool)"/> that accepts
  507. /// <see cref="AnimationClip"/>s and <see cref="ITransition"/>s
  508. /// </summary>
  509. public static void DoAnimationField(Rect area, SerializedProperty property)
  510. {
  511. EditorGUI.BeginProperty(area, GUIContent.none, property);
  512. var targetObject = property.serializedObject.targetObject;
  513. var oldReference = property.objectReferenceValue;
  514. var currentEvent = Event.current;
  515. var isDrag =
  516. currentEvent.type == EventType.DragUpdated ||
  517. currentEvent.type == EventType.DragPerform;
  518. var type =
  519. isDrag ||
  520. currentEvent.control ||
  521. currentEvent.commandName == "ObjectSelectorUpdated"
  522. ? typeof(Object)
  523. : typeof(AnimationClip);
  524. var allowSceneObjects = targetObject != null && !EditorUtility.IsPersistent(targetObject);
  525. EditorGUI.BeginChangeCheck();
  526. var newReference = EditorGUI.ObjectField(
  527. area,
  528. GUIContent.none,
  529. oldReference,
  530. type,
  531. allowSceneObjects);
  532. if (EditorGUI.EndChangeCheck())
  533. {
  534. if (newReference == null ||
  535. (IsClipOrTransition(newReference) && newReference != targetObject))
  536. property.objectReferenceValue = newReference;
  537. }
  538. if (isDrag && area.Contains(currentEvent.mousePosition))
  539. {
  540. var objects = DragAndDrop.objectReferences;
  541. if (objects.Length != 1 ||
  542. !IsClipOrTransition(objects[0]) ||
  543. objects[0] == targetObject)
  544. DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
  545. }
  546. EditorGUI.EndProperty();
  547. }
  548. /// <summary>Is the `clipOrTransition` an <see cref="AnimationClip"/> or <see cref="ITransition"/>?</summary>
  549. public static bool IsClipOrTransition(object clipOrTransition)
  550. => clipOrTransition is AnimationClip || clipOrTransition is ITransition;
  551. /************************************************************************************************************************/
  552. private static CompactUnitConversionCache _XSuffixCache;
  553. /// <summary>
  554. /// Draws a toggle to enable or disable <see cref="ManualMixerState.SynchronizedChildren"/> for the child
  555. /// at the specified `index`.
  556. /// </summary>
  557. protected void DoSpeedFieldGUI(Rect area, SerializedProperty speed, int index)
  558. {
  559. if (speed != null)
  560. {
  561. EditorGUI.PropertyField(area, speed, GUIContent.none);
  562. }
  563. else// If this element doesn't have its own speed property, just show 1.
  564. {
  565. EditorGUI.BeginProperty(area, GUIContent.none, CurrentSpeeds);
  566. _XSuffixCache ??= new("x");
  567. var value = UnitsAttributeDrawer.DoSpecialFloatField(
  568. area,
  569. null,
  570. 1,
  571. _XSuffixCache);
  572. // Middle Click toggles from 1 to -1.
  573. if (TryUseClickEvent(area, 2))
  574. value = -1;
  575. if (value != 1)
  576. {
  577. CurrentSpeeds.InsertArrayElementAtIndex(0);
  578. CurrentSpeeds.GetArrayElementAtIndex(0).floatValue = 1;
  579. CurrentSpeeds.arraySize = CurrentAnimations.arraySize;
  580. CurrentSpeeds.GetArrayElementAtIndex(index).floatValue = value;
  581. }
  582. EditorGUI.EndProperty();
  583. }
  584. }
  585. /************************************************************************************************************************/
  586. /// <summary>
  587. /// Draws a toggle to enable or disable <see cref="ManualMixerState.SynchronizedChildren"/>
  588. /// for the child at the specified `index`.
  589. /// </summary>
  590. protected void DoSyncToggleGUI(Rect area, int index)
  591. {
  592. var syncProperty = CurrentSynchronizeChildren;
  593. var syncFlagCount = syncProperty.arraySize;
  594. var enabled = true;
  595. if (index < syncFlagCount)
  596. {
  597. syncProperty = syncProperty.GetArrayElementAtIndex(index);
  598. enabled = syncProperty.boolValue;
  599. }
  600. EditorGUI.BeginChangeCheck();
  601. EditorGUI.BeginProperty(area, GUIContent.none, syncProperty);
  602. enabled = GUI.Toggle(area, enabled, GUIContent.none);
  603. EditorGUI.EndProperty();
  604. if (EditorGUI.EndChangeCheck())
  605. {
  606. if (index < syncFlagCount)
  607. {
  608. syncProperty.boolValue = enabled;
  609. }
  610. else
  611. {
  612. syncProperty.arraySize = index + 1;
  613. for (int i = syncFlagCount; i < index; i++)
  614. {
  615. syncProperty.GetArrayElementAtIndex(i).boolValue = true;
  616. }
  617. syncProperty.GetArrayElementAtIndex(index).boolValue = enabled;
  618. }
  619. }
  620. }
  621. /************************************************************************************************************************/
  622. /// <summary>
  623. /// Called when adding a new state to the list to ensure that
  624. /// any other relevant arrays have new elements added as well.
  625. /// </summary>
  626. private void OnAddElement(ReorderableList list)
  627. {
  628. var index = list.index;
  629. if (index < 0 || Event.current.button == 1)// Right Click to add at the end.
  630. {
  631. index = CurrentAnimations.arraySize - 1;
  632. if (index < 0)
  633. index = 0;
  634. }
  635. OnAddElement(index);
  636. }
  637. /// <summary>
  638. /// Called when adding a new state to the list to ensure that
  639. /// any other relevant arrays have new elements added as well.
  640. /// </summary>
  641. protected virtual void OnAddElement(int index)
  642. {
  643. CurrentAnimations.InsertArrayElementAtIndex(index);
  644. if (CurrentSpeeds.arraySize > 0)
  645. CurrentSpeeds.InsertArrayElementAtIndex(index);
  646. if (CurrentSynchronizeChildren.arraySize > index)
  647. CurrentSynchronizeChildren.InsertArrayElementAtIndex(index);
  648. }
  649. /************************************************************************************************************************/
  650. /// <summary>
  651. /// Called when removing a state from the list to ensure that
  652. /// any other relevant arrays have elements removed as well.
  653. /// </summary>
  654. protected virtual void OnRemoveElement(ReorderableList list)
  655. {
  656. var index = list.index;
  657. Serialization.RemoveArrayElement(CurrentAnimations, index);
  658. if (CurrentSpeeds.arraySize > index)
  659. Serialization.RemoveArrayElement(CurrentSpeeds, index);
  660. if (CurrentSynchronizeChildren.arraySize > index)
  661. Serialization.RemoveArrayElement(CurrentSynchronizeChildren, index);
  662. }
  663. /************************************************************************************************************************/
  664. /// <summary>Sets the number of items in the child list.</summary>
  665. protected virtual void ResizeList(int size)
  666. {
  667. CurrentAnimations.arraySize = size;
  668. if (CurrentSpeeds.arraySize > size)
  669. CurrentSpeeds.arraySize = size;
  670. if (CurrentSynchronizeChildren.arraySize > size)
  671. CurrentSynchronizeChildren.arraySize = size;
  672. }
  673. /************************************************************************************************************************/
  674. /// <summary>
  675. /// Called when reordering states in the list to ensure that
  676. /// any other relevant arrays have their corresponding elements reordered as well.
  677. /// </summary>
  678. protected virtual void OnReorderList(ReorderableList list, int oldIndex, int newIndex)
  679. {
  680. CurrentSpeeds.MoveArrayElement(oldIndex, newIndex);
  681. var syncCount = CurrentSynchronizeChildren.arraySize;
  682. if (Math.Max(oldIndex, newIndex) >= syncCount)
  683. {
  684. CurrentSynchronizeChildren.arraySize++;
  685. CurrentSynchronizeChildren.GetArrayElementAtIndex(syncCount).boolValue = true;
  686. CurrentSynchronizeChildren.arraySize = newIndex + 1;
  687. }
  688. CurrentSynchronizeChildren.MoveArrayElement(oldIndex, newIndex);
  689. }
  690. /************************************************************************************************************************/
  691. /// <summary>
  692. /// Calls <see cref="TryCollapseSpeeds"/> and <see cref="TryCollapseSync"/>.
  693. /// </summary>
  694. public static void TryCollapseArrays()
  695. {
  696. if (CurrentProperty == null ||
  697. CurrentProperty.hasMultipleDifferentValues)
  698. return;
  699. TryCollapseSpeeds();
  700. TryCollapseSync();
  701. }
  702. /************************************************************************************************************************/
  703. /// <summary>
  704. /// If every element in the <see cref="CurrentSpeeds"/> array is 1,
  705. /// this method sets the array size to 0.
  706. /// </summary>
  707. public static void TryCollapseSpeeds()
  708. {
  709. var property = CurrentSpeeds;
  710. if (property == null)
  711. return;
  712. var speedCount = property.arraySize;
  713. if (speedCount <= 0)
  714. return;
  715. for (int i = 0; i < speedCount; i++)
  716. {
  717. if (property.GetArrayElementAtIndex(i).floatValue != 1)
  718. return;
  719. }
  720. property.arraySize = 0;
  721. }
  722. /************************************************************************************************************************/
  723. /// <summary>
  724. /// Removes any true elements from the end of the <see cref="CurrentSynchronizeChildren"/> array.
  725. /// </summary>
  726. public static void TryCollapseSync()
  727. {
  728. var property = CurrentSynchronizeChildren;
  729. if (property == null)
  730. return;
  731. var count = property.arraySize;
  732. var changed = false;
  733. for (int i = count - 1; i >= 0; i--)
  734. {
  735. if (property.GetArrayElementAtIndex(i).boolValue)
  736. {
  737. count = i;
  738. changed = true;
  739. }
  740. else
  741. {
  742. break;
  743. }
  744. }
  745. if (changed)
  746. property.arraySize = count;
  747. }
  748. /************************************************************************************************************************/
  749. }
  750. }
  751. #endif