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