No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

AdvancedDropdownWindow.cs 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. #if UNITY_EDITOR
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using UnityEditor;
  6. using UnityEditor.Callbacks;
  7. namespace UnityEngine.InputSystem.Editor
  8. {
  9. internal class AdvancedDropdownWindow : EditorWindow
  10. {
  11. private static readonly float kBorderThickness = 1f;
  12. private static readonly float kRightMargin = 13f;
  13. private AdvancedDropdownGUI m_Gui;
  14. private AdvancedDropdownDataSource m_DataSource;
  15. private AdvancedDropdownState m_State;
  16. private AdvancedDropdownItem m_CurrentlyRenderedTree;
  17. protected AdvancedDropdownItem renderedTreeItem => m_CurrentlyRenderedTree;
  18. private AdvancedDropdownItem m_AnimationTree;
  19. private float m_NewAnimTarget;
  20. private long m_LastTime;
  21. private bool m_ScrollToSelected = true;
  22. private float m_InitialSelectionPosition;
  23. ////FIXME: looks like a bug?
  24. #pragma warning disable CS0649
  25. private Rect m_ButtonRectScreenPos;
  26. private Stack<AdvancedDropdownItem> m_ViewsStack = new Stack<AdvancedDropdownItem>();
  27. private bool m_DirtyList = true;
  28. private string m_Search = "";
  29. private bool hasSearch => !string.IsNullOrEmpty(m_Search);
  30. protected internal string searchString
  31. {
  32. get => m_Search;
  33. set
  34. {
  35. var isNewSearch = string.IsNullOrEmpty(m_Search) && !string.IsNullOrEmpty(value);
  36. m_Search = value;
  37. m_DataSource.RebuildSearch(m_Search);
  38. m_CurrentlyRenderedTree = m_DataSource.mainTree;
  39. if (hasSearch)
  40. {
  41. m_CurrentlyRenderedTree = m_DataSource.searchTree;
  42. if (isNewSearch || state.GetSelectedIndex(m_CurrentlyRenderedTree) < 0)
  43. state.SetSelectedIndex(m_CurrentlyRenderedTree, 0);
  44. m_ViewsStack.Clear();
  45. }
  46. }
  47. }
  48. internal bool m_ShowHeader = true;
  49. internal bool showHeader
  50. {
  51. get => m_ShowHeader;
  52. set => m_ShowHeader = value;
  53. }
  54. internal bool m_Searchable = true;
  55. internal bool searchable
  56. {
  57. get => m_Searchable;
  58. set => m_Searchable = value;
  59. }
  60. internal bool m_closeOnSelection = true;
  61. internal bool closeOnSelection
  62. {
  63. get => m_closeOnSelection;
  64. set => m_closeOnSelection = value;
  65. }
  66. protected virtual bool isSearchFieldDisabled { get; set; }
  67. protected bool m_SetInitialSelectionPosition = true;
  68. public AdvancedDropdownWindow()
  69. {
  70. m_InitialSelectionPosition = 0f;
  71. }
  72. protected virtual bool setInitialSelectionPosition => m_SetInitialSelectionPosition;
  73. protected internal AdvancedDropdownState state
  74. {
  75. get => m_State;
  76. set => m_State = value;
  77. }
  78. protected internal AdvancedDropdownGUI gui
  79. {
  80. get => m_Gui;
  81. set => m_Gui = value;
  82. }
  83. protected internal AdvancedDropdownDataSource dataSource
  84. {
  85. get => m_DataSource;
  86. set => m_DataSource = value;
  87. }
  88. public event Action<AdvancedDropdownWindow> windowClosed;
  89. public event Action windowDestroyed;
  90. public event Action<AdvancedDropdownItem> selectionChanged;
  91. protected virtual void OnEnable()
  92. {
  93. m_DirtyList = true;
  94. }
  95. protected virtual void OnDestroy()
  96. {
  97. // This window sets 'editingTextField = true' continuously, through EditorGUI.FocusTextInControl(),
  98. // for the searchfield in its AdvancedDropdownGUI so here we ensure to clean up. This fixes the issue that
  99. // EditorGUI.IsEditingTextField() was returning true after e.g the Add Component Menu closes
  100. EditorGUIUtility.editingTextField = false;
  101. GUIUtility.keyboardControl = 0;
  102. windowDestroyed?.Invoke();
  103. }
  104. public static T CreateAndInit<T>(Rect rect, AdvancedDropdownState state) where T : AdvancedDropdownWindow
  105. {
  106. var instance = CreateInstance<T>();
  107. instance.m_State = state;
  108. instance.Init(rect);
  109. return instance;
  110. }
  111. public void Init(Rect buttonRect)
  112. {
  113. var screenPoint = GUIUtility.GUIToScreenPoint(new Vector2(buttonRect.x, buttonRect.y));
  114. m_ButtonRectScreenPos.x = screenPoint.x;
  115. m_ButtonRectScreenPos.y = screenPoint.y;
  116. if (m_State == null)
  117. m_State = new AdvancedDropdownState();
  118. if (m_DataSource == null)
  119. m_DataSource = new MultiLevelDataSource();
  120. if (m_Gui == null)
  121. m_Gui = new AdvancedDropdownGUI();
  122. m_Gui.state = m_State;
  123. m_Gui.Init();
  124. // Has to be done before calling Show / ShowWithMode
  125. screenPoint = GUIUtility.GUIToScreenPoint(new Vector2(buttonRect.x, buttonRect.y));
  126. buttonRect.x = screenPoint.x;
  127. buttonRect.y = screenPoint.y;
  128. OnDirtyList();
  129. m_CurrentlyRenderedTree = hasSearch ? m_DataSource.searchTree : m_DataSource.mainTree;
  130. ShowAsDropDown(buttonRect, CalculateWindowSize(m_ButtonRectScreenPos, out var requiredDropdownSize));
  131. // If the dropdown is as height as the screen height, give it some margin
  132. if (position.height < requiredDropdownSize.y)
  133. {
  134. var pos = position;
  135. pos.y += 5;
  136. pos.height -= 10;
  137. position = pos;
  138. }
  139. if (setInitialSelectionPosition)
  140. {
  141. m_InitialSelectionPosition = m_Gui.GetSelectionHeight(m_DataSource, buttonRect);
  142. }
  143. wantsMouseMove = true;
  144. SetSelectionFromState();
  145. }
  146. void SetSelectionFromState()
  147. {
  148. var selectedIndex = m_State.GetSelectedIndex(m_CurrentlyRenderedTree);
  149. while (selectedIndex >= 0)
  150. {
  151. var child = m_State.GetSelectedChild(m_CurrentlyRenderedTree);
  152. if (child == null)
  153. break;
  154. selectedIndex = m_State.GetSelectedIndex(child);
  155. if (selectedIndex < 0)
  156. break;
  157. m_ViewsStack.Push(m_CurrentlyRenderedTree);
  158. m_CurrentlyRenderedTree = child;
  159. }
  160. }
  161. protected virtual Vector2 CalculateWindowSize(Rect buttonRect, out Vector2 requiredDropdownSize)
  162. {
  163. requiredDropdownSize = m_Gui.CalculateContentSize(m_DataSource);
  164. // Add 1 pixel for each border
  165. requiredDropdownSize.x += kBorderThickness * 2;
  166. requiredDropdownSize.y += kBorderThickness * 2;
  167. requiredDropdownSize.x += kRightMargin;
  168. requiredDropdownSize.y += m_Gui.searchHeight;
  169. if (showHeader)
  170. {
  171. requiredDropdownSize.y += m_Gui.headerHeight;
  172. }
  173. requiredDropdownSize.y = Mathf.Clamp(requiredDropdownSize.y, minSize.y, maxSize.y);
  174. var adjustedButtonRect = buttonRect;
  175. adjustedButtonRect.y = 0;
  176. adjustedButtonRect.height = requiredDropdownSize.y;
  177. // Stretch to the width of the button
  178. if (requiredDropdownSize.x < buttonRect.width)
  179. {
  180. requiredDropdownSize.x = buttonRect.width;
  181. }
  182. // Apply minimum size
  183. if (requiredDropdownSize.x < minSize.x)
  184. {
  185. requiredDropdownSize.x = minSize.x;
  186. }
  187. if (requiredDropdownSize.y < minSize.y)
  188. {
  189. requiredDropdownSize.y = minSize.y;
  190. }
  191. return requiredDropdownSize;
  192. }
  193. internal void OnGUI()
  194. {
  195. m_Gui.BeginDraw(this);
  196. GUI.Label(new Rect(0, 0, position.width, position.height), GUIContent.none, Styles.background);
  197. if (m_DirtyList)
  198. {
  199. OnDirtyList();
  200. }
  201. HandleKeyboard();
  202. if (searchable)
  203. OnGUISearch();
  204. if (m_NewAnimTarget != 0 && Event.current.type == EventType.Layout)
  205. {
  206. var now = DateTime.Now.Ticks;
  207. var deltaTime = (now - m_LastTime) / (float)TimeSpan.TicksPerSecond;
  208. m_LastTime = now;
  209. m_NewAnimTarget = Mathf.MoveTowards(m_NewAnimTarget, 0, deltaTime * 4);
  210. if (m_NewAnimTarget == 0)
  211. {
  212. m_AnimationTree = null;
  213. }
  214. Repaint();
  215. }
  216. var anim = m_NewAnimTarget;
  217. // Smooth the animation
  218. anim = Mathf.Floor(anim) + Mathf.SmoothStep(0, 1, Mathf.Repeat(anim, 1));
  219. if (anim == 0)
  220. {
  221. DrawDropdown(0, m_CurrentlyRenderedTree);
  222. }
  223. else if (anim < 0)
  224. {
  225. // Go to parent
  226. // m_NewAnimTarget goes -1 -> 0
  227. DrawDropdown(anim, m_CurrentlyRenderedTree);
  228. DrawDropdown(anim + 1, m_AnimationTree);
  229. }
  230. else // > 0
  231. {
  232. // Go to child
  233. // m_NewAnimTarget 1 -> 0
  234. DrawDropdown(anim - 1, m_AnimationTree);
  235. DrawDropdown(anim, m_CurrentlyRenderedTree);
  236. }
  237. m_Gui.EndDraw(this);
  238. }
  239. public void ReloadData()
  240. {
  241. OnDirtyList();
  242. }
  243. private void OnDirtyList()
  244. {
  245. m_DirtyList = false;
  246. m_DataSource.ReloadData();
  247. if (hasSearch)
  248. {
  249. m_DataSource.RebuildSearch(searchString);
  250. if (state.GetSelectedIndex(m_CurrentlyRenderedTree) < 0)
  251. {
  252. state.SetSelectedIndex(m_CurrentlyRenderedTree, 0);
  253. }
  254. }
  255. }
  256. private void OnGUISearch()
  257. {
  258. m_Gui.DrawSearchField(isSearchFieldDisabled, m_Search, (newSearch) =>
  259. {
  260. searchString = newSearch;
  261. });
  262. }
  263. private void HandleKeyboard()
  264. {
  265. var evt = Event.current;
  266. if (evt.type == EventType.KeyDown)
  267. {
  268. // Special handling when in new script panel
  269. if (SpecialKeyboardHandling(evt))
  270. {
  271. return;
  272. }
  273. // Always do these
  274. if (evt.keyCode == KeyCode.DownArrow)
  275. {
  276. m_State.MoveDownSelection(m_CurrentlyRenderedTree);
  277. m_ScrollToSelected = true;
  278. evt.Use();
  279. }
  280. if (evt.keyCode == KeyCode.UpArrow)
  281. {
  282. m_State.MoveUpSelection(m_CurrentlyRenderedTree);
  283. m_ScrollToSelected = true;
  284. evt.Use();
  285. }
  286. if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
  287. {
  288. var selected = m_State.GetSelectedChild(m_CurrentlyRenderedTree);
  289. if (selected != null)
  290. {
  291. if (selected.children.Any())
  292. {
  293. GoToChild();
  294. }
  295. else
  296. {
  297. if (selectionChanged != null)
  298. {
  299. selectionChanged(m_State.GetSelectedChild(m_CurrentlyRenderedTree));
  300. }
  301. if (closeOnSelection)
  302. {
  303. CloseWindow();
  304. }
  305. }
  306. }
  307. evt.Use();
  308. }
  309. // Do these if we're not in search mode
  310. if (!hasSearch)
  311. {
  312. if (evt.keyCode == KeyCode.LeftArrow || evt.keyCode == KeyCode.Backspace)
  313. {
  314. GoToParent();
  315. evt.Use();
  316. }
  317. if (evt.keyCode == KeyCode.RightArrow)
  318. {
  319. var idx = m_State.GetSelectedIndex(m_CurrentlyRenderedTree);
  320. if (idx > -1 && m_CurrentlyRenderedTree.children.ElementAt(idx).children.Any())
  321. {
  322. GoToChild();
  323. }
  324. evt.Use();
  325. }
  326. if (evt.keyCode == KeyCode.Escape)
  327. {
  328. Close();
  329. evt.Use();
  330. }
  331. }
  332. }
  333. }
  334. private void CloseWindow()
  335. {
  336. windowClosed?.Invoke(this);
  337. Close();
  338. }
  339. internal AdvancedDropdownItem GetSelectedItem()
  340. {
  341. return m_State.GetSelectedChild(m_CurrentlyRenderedTree);
  342. }
  343. protected virtual bool SpecialKeyboardHandling(Event evt)
  344. {
  345. return false;
  346. }
  347. private void DrawDropdown(float anim, AdvancedDropdownItem group)
  348. {
  349. // Start of animated area (the part that moves left and right)
  350. var areaPosition = new Rect(0, 0, position.width, position.height);
  351. // Adjust to the frame
  352. areaPosition.x += kBorderThickness;
  353. areaPosition.y += kBorderThickness;
  354. areaPosition.height -= kBorderThickness * 2;
  355. areaPosition.width -= kBorderThickness * 2;
  356. GUILayout.BeginArea(m_Gui.GetAnimRect(areaPosition, anim));
  357. // Header
  358. if (showHeader)
  359. m_Gui.DrawHeader(group, GoToParent, m_ViewsStack.Count > 0);
  360. DrawList(group);
  361. GUILayout.EndArea();
  362. }
  363. private void DrawList(AdvancedDropdownItem item)
  364. {
  365. // Start of scroll view list
  366. m_State.SetScrollState(item, GUILayout.BeginScrollView(m_State.GetScrollState(item), GUIStyle.none, GUI.skin.verticalScrollbar));
  367. EditorGUIUtility.SetIconSize(m_Gui.iconSize);
  368. Rect selectedRect = new Rect();
  369. for (var i = 0; i < item.children.Count(); i++)
  370. {
  371. var child = item.children.ElementAt(i);
  372. var selected = m_State.GetSelectedIndex(item) == i;
  373. if (child.IsSeparator())
  374. {
  375. GUIHelpers.DrawLineSeparator(child.name);
  376. }
  377. else
  378. {
  379. m_Gui.DrawItem(child, child.name, child.icon, child.enabled, child.children.Any(), selected, hasSearch);
  380. }
  381. var r = GUILayoutUtility.GetLastRect();
  382. if (selected)
  383. selectedRect = r;
  384. // Skip input handling for the tree used for animation
  385. if (item != m_CurrentlyRenderedTree)
  386. continue;
  387. // Select the element the mouse cursor is over.
  388. // Only do it on mouse move - keyboard controls are allowed to overwrite this until the next time the mouse moves.
  389. if ((Event.current.type == EventType.MouseMove || Event.current.type == EventType.MouseDrag) && child.enabled)
  390. {
  391. if (!selected && r.Contains(Event.current.mousePosition))
  392. {
  393. m_State.SetSelectedIndex(item, i);
  394. Event.current.Use();
  395. }
  396. }
  397. if (Event.current.type == EventType.MouseUp && r.Contains(Event.current.mousePosition) && child.enabled)
  398. {
  399. m_State.SetSelectedIndex(item, i);
  400. var selectedChild = m_State.GetSelectedChild(item);
  401. if (selectedChild.children.Any())
  402. {
  403. GoToChild();
  404. }
  405. else if (!selectedChild.IsSeparator())
  406. {
  407. selectionChanged?.Invoke(selectedChild);
  408. if (closeOnSelection)
  409. {
  410. CloseWindow();
  411. GUIUtility.ExitGUI();
  412. }
  413. }
  414. Event.current.Use();
  415. }
  416. }
  417. EditorGUIUtility.SetIconSize(Vector2.zero);
  418. GUILayout.EndScrollView();
  419. // Scroll to selected on windows creation
  420. if (m_ScrollToSelected && m_InitialSelectionPosition != 0)
  421. {
  422. var diffOfPopupAboveTheButton = m_ButtonRectScreenPos.y - position.y;
  423. diffOfPopupAboveTheButton -= m_Gui.searchHeight + m_Gui.headerHeight;
  424. m_State.SetScrollState(item, new Vector2(0, m_InitialSelectionPosition - diffOfPopupAboveTheButton));
  425. m_ScrollToSelected = false;
  426. m_InitialSelectionPosition = 0;
  427. }
  428. // Scroll to show selected
  429. else if (m_ScrollToSelected && Event.current.type == EventType.Repaint)
  430. {
  431. m_ScrollToSelected = false;
  432. Rect scrollRect = GUILayoutUtility.GetLastRect();
  433. if (selectedRect.yMax - scrollRect.height > m_State.GetScrollState(item).y)
  434. {
  435. m_State.SetScrollState(item, new Vector2(0, selectedRect.yMax - scrollRect.height));
  436. Repaint();
  437. }
  438. if (selectedRect.y < m_State.GetScrollState(item).y)
  439. {
  440. m_State.SetScrollState(item, new Vector2(0, selectedRect.y));
  441. Repaint();
  442. }
  443. }
  444. }
  445. protected void GoToParent()
  446. {
  447. if (m_ViewsStack.Count == 0)
  448. return;
  449. m_LastTime = DateTime.Now.Ticks;
  450. if (m_NewAnimTarget > 0)
  451. m_NewAnimTarget = -1 + m_NewAnimTarget;
  452. else
  453. m_NewAnimTarget = -1;
  454. m_AnimationTree = m_CurrentlyRenderedTree;
  455. var parentItem = m_ViewsStack.Pop();
  456. m_State.ClearSelectionOnItem(m_CurrentlyRenderedTree);
  457. if (parentItem != null)
  458. {
  459. var suggestedIndex = parentItem.GetIndexOfChild(m_CurrentlyRenderedTree);
  460. m_State.SetSelectionOnItem(parentItem, suggestedIndex);
  461. }
  462. m_CurrentlyRenderedTree = parentItem;
  463. }
  464. private void GoToChild()
  465. {
  466. m_ViewsStack.Push(m_CurrentlyRenderedTree);
  467. m_LastTime = DateTime.Now.Ticks;
  468. if (m_NewAnimTarget < 0)
  469. m_NewAnimTarget = 1 + m_NewAnimTarget;
  470. else
  471. m_NewAnimTarget = 1;
  472. m_AnimationTree = m_CurrentlyRenderedTree;
  473. m_CurrentlyRenderedTree = m_State.GetSelectedChild(m_CurrentlyRenderedTree);
  474. }
  475. [DidReloadScripts]
  476. private static void OnScriptReload()
  477. {
  478. CloseAllOpenWindows<AdvancedDropdownWindow>();
  479. }
  480. protected static void CloseAllOpenWindows<T>()
  481. {
  482. var windows = Resources.FindObjectsOfTypeAll(typeof(T));
  483. foreach (var window in windows)
  484. {
  485. try
  486. {
  487. ((EditorWindow)window).Close();
  488. }
  489. catch
  490. {
  491. DestroyImmediate(window);
  492. }
  493. }
  494. }
  495. private static class Styles
  496. {
  497. public static readonly GUIStyle background = "grey_border";
  498. public static readonly GUIStyle previewHeader = new GUIStyle(EditorStyles.label).WithPadding(new RectOffset(5, 5, 1, 2));
  499. public static readonly GUIStyle previewText = new GUIStyle(EditorStyles.wordWrappedLabel).WithPadding(new RectOffset(3, 5, 4, 4));
  500. }
  501. }
  502. }
  503. #endif // UNITY_EDITOR