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