#if UNITY_EDITOR using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.Callbacks; namespace UnityEngine.InputSystem.Editor { internal class AdvancedDropdownWindow : EditorWindow { private static readonly float kBorderThickness = 1f; private static readonly float kRightMargin = 13f; private AdvancedDropdownGUI m_Gui; private AdvancedDropdownDataSource m_DataSource; private AdvancedDropdownState m_State; private AdvancedDropdownItem m_CurrentlyRenderedTree; protected AdvancedDropdownItem renderedTreeItem => m_CurrentlyRenderedTree; private AdvancedDropdownItem m_AnimationTree; private float m_NewAnimTarget; private long m_LastTime; private bool m_ScrollToSelected = true; private float m_InitialSelectionPosition; ////FIXME: looks like a bug? #pragma warning disable CS0649 private Rect m_ButtonRectScreenPos; private Stack m_ViewsStack = new Stack(); private bool m_DirtyList = true; private string m_Search = ""; private bool hasSearch => !string.IsNullOrEmpty(m_Search); protected internal string searchString { get => m_Search; set { var isNewSearch = string.IsNullOrEmpty(m_Search) && !string.IsNullOrEmpty(value); m_Search = value; m_DataSource.RebuildSearch(m_Search); m_CurrentlyRenderedTree = m_DataSource.mainTree; if (hasSearch) { m_CurrentlyRenderedTree = m_DataSource.searchTree; if (isNewSearch || state.GetSelectedIndex(m_CurrentlyRenderedTree) < 0) state.SetSelectedIndex(m_CurrentlyRenderedTree, 0); m_ViewsStack.Clear(); } } } internal bool m_ShowHeader = true; internal bool showHeader { get => m_ShowHeader; set => m_ShowHeader = value; } internal bool m_Searchable = true; internal bool searchable { get => m_Searchable; set => m_Searchable = value; } internal bool m_closeOnSelection = true; internal bool closeOnSelection { get => m_closeOnSelection; set => m_closeOnSelection = value; } protected virtual bool isSearchFieldDisabled { get; set; } protected bool m_SetInitialSelectionPosition = true; public AdvancedDropdownWindow() { m_InitialSelectionPosition = 0f; } protected virtual bool setInitialSelectionPosition => m_SetInitialSelectionPosition; protected internal AdvancedDropdownState state { get => m_State; set => m_State = value; } protected internal AdvancedDropdownGUI gui { get => m_Gui; set => m_Gui = value; } protected internal AdvancedDropdownDataSource dataSource { get => m_DataSource; set => m_DataSource = value; } public event Action windowClosed; public event Action windowDestroyed; public event Action selectionChanged; protected virtual void OnEnable() { m_DirtyList = true; } protected virtual void OnDestroy() { // This window sets 'editingTextField = true' continuously, through EditorGUI.FocusTextInControl(), // for the searchfield in its AdvancedDropdownGUI so here we ensure to clean up. This fixes the issue that // EditorGUI.IsEditingTextField() was returning true after e.g the Add Component Menu closes EditorGUIUtility.editingTextField = false; GUIUtility.keyboardControl = 0; windowDestroyed?.Invoke(); } public static T CreateAndInit(Rect rect, AdvancedDropdownState state) where T : AdvancedDropdownWindow { var instance = CreateInstance(); instance.m_State = state; instance.Init(rect); return instance; } public void Init(Rect buttonRect) { var screenPoint = GUIUtility.GUIToScreenPoint(new Vector2(buttonRect.x, buttonRect.y)); m_ButtonRectScreenPos.x = screenPoint.x; m_ButtonRectScreenPos.y = screenPoint.y; if (m_State == null) m_State = new AdvancedDropdownState(); if (m_DataSource == null) m_DataSource = new MultiLevelDataSource(); if (m_Gui == null) m_Gui = new AdvancedDropdownGUI(); m_Gui.state = m_State; m_Gui.Init(); // Has to be done before calling Show / ShowWithMode screenPoint = GUIUtility.GUIToScreenPoint(new Vector2(buttonRect.x, buttonRect.y)); buttonRect.x = screenPoint.x; buttonRect.y = screenPoint.y; OnDirtyList(); m_CurrentlyRenderedTree = hasSearch ? m_DataSource.searchTree : m_DataSource.mainTree; ShowAsDropDown(buttonRect, CalculateWindowSize(m_ButtonRectScreenPos, out var requiredDropdownSize)); // If the dropdown is as height as the screen height, give it some margin if (position.height < requiredDropdownSize.y) { var pos = position; pos.y += 5; pos.height -= 10; position = pos; } if (setInitialSelectionPosition) { m_InitialSelectionPosition = m_Gui.GetSelectionHeight(m_DataSource, buttonRect); } wantsMouseMove = true; SetSelectionFromState(); } void SetSelectionFromState() { var selectedIndex = m_State.GetSelectedIndex(m_CurrentlyRenderedTree); while (selectedIndex >= 0) { var child = m_State.GetSelectedChild(m_CurrentlyRenderedTree); if (child == null) break; selectedIndex = m_State.GetSelectedIndex(child); if (selectedIndex < 0) break; m_ViewsStack.Push(m_CurrentlyRenderedTree); m_CurrentlyRenderedTree = child; } } protected virtual Vector2 CalculateWindowSize(Rect buttonRect, out Vector2 requiredDropdownSize) { requiredDropdownSize = m_Gui.CalculateContentSize(m_DataSource); // Add 1 pixel for each border requiredDropdownSize.x += kBorderThickness * 2; requiredDropdownSize.y += kBorderThickness * 2; requiredDropdownSize.x += kRightMargin; requiredDropdownSize.y += m_Gui.searchHeight; if (showHeader) { requiredDropdownSize.y += m_Gui.headerHeight; } requiredDropdownSize.y = Mathf.Clamp(requiredDropdownSize.y, minSize.y, maxSize.y); var adjustedButtonRect = buttonRect; adjustedButtonRect.y = 0; adjustedButtonRect.height = requiredDropdownSize.y; // Stretch to the width of the button if (requiredDropdownSize.x < buttonRect.width) { requiredDropdownSize.x = buttonRect.width; } // Apply minimum size if (requiredDropdownSize.x < minSize.x) { requiredDropdownSize.x = minSize.x; } if (requiredDropdownSize.y < minSize.y) { requiredDropdownSize.y = minSize.y; } return requiredDropdownSize; } internal void OnGUI() { m_Gui.BeginDraw(this); GUI.Label(new Rect(0, 0, position.width, position.height), GUIContent.none, Styles.background); if (m_DirtyList) { OnDirtyList(); } HandleKeyboard(); if (searchable) OnGUISearch(); if (m_NewAnimTarget != 0 && Event.current.type == EventType.Layout) { var now = DateTime.Now.Ticks; var deltaTime = (now - m_LastTime) / (float)TimeSpan.TicksPerSecond; m_LastTime = now; m_NewAnimTarget = Mathf.MoveTowards(m_NewAnimTarget, 0, deltaTime * 4); if (m_NewAnimTarget == 0) { m_AnimationTree = null; } Repaint(); } var anim = m_NewAnimTarget; // Smooth the animation anim = Mathf.Floor(anim) + Mathf.SmoothStep(0, 1, Mathf.Repeat(anim, 1)); if (anim == 0) { DrawDropdown(0, m_CurrentlyRenderedTree); } else if (anim < 0) { // Go to parent // m_NewAnimTarget goes -1 -> 0 DrawDropdown(anim, m_CurrentlyRenderedTree); DrawDropdown(anim + 1, m_AnimationTree); } else // > 0 { // Go to child // m_NewAnimTarget 1 -> 0 DrawDropdown(anim - 1, m_AnimationTree); DrawDropdown(anim, m_CurrentlyRenderedTree); } m_Gui.EndDraw(this); } public void ReloadData() { OnDirtyList(); } private void OnDirtyList() { m_DirtyList = false; m_DataSource.ReloadData(); if (hasSearch) { m_DataSource.RebuildSearch(searchString); if (state.GetSelectedIndex(m_CurrentlyRenderedTree) < 0) { state.SetSelectedIndex(m_CurrentlyRenderedTree, 0); } } } private void OnGUISearch() { m_Gui.DrawSearchField(isSearchFieldDisabled, m_Search, (newSearch) => { searchString = newSearch; }); } private void HandleKeyboard() { var evt = Event.current; if (evt.type == EventType.KeyDown) { // Special handling when in new script panel if (SpecialKeyboardHandling(evt)) { return; } // Always do these if (evt.keyCode == KeyCode.DownArrow) { m_State.MoveDownSelection(m_CurrentlyRenderedTree); m_ScrollToSelected = true; evt.Use(); } if (evt.keyCode == KeyCode.UpArrow) { m_State.MoveUpSelection(m_CurrentlyRenderedTree); m_ScrollToSelected = true; evt.Use(); } if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter) { var selected = m_State.GetSelectedChild(m_CurrentlyRenderedTree); if (selected != null) { if (selected.children.Any()) { GoToChild(); } else { if (selectionChanged != null) { selectionChanged(m_State.GetSelectedChild(m_CurrentlyRenderedTree)); } if (closeOnSelection) { CloseWindow(); } } } evt.Use(); } // Do these if we're not in search mode if (!hasSearch) { if (evt.keyCode == KeyCode.LeftArrow || evt.keyCode == KeyCode.Backspace) { GoToParent(); evt.Use(); } if (evt.keyCode == KeyCode.RightArrow) { var idx = m_State.GetSelectedIndex(m_CurrentlyRenderedTree); if (idx > -1 && m_CurrentlyRenderedTree.children.ElementAt(idx).children.Any()) { GoToChild(); } evt.Use(); } if (evt.keyCode == KeyCode.Escape) { Close(); evt.Use(); } } } } private void CloseWindow() { windowClosed?.Invoke(this); Close(); } internal AdvancedDropdownItem GetSelectedItem() { return m_State.GetSelectedChild(m_CurrentlyRenderedTree); } protected virtual bool SpecialKeyboardHandling(Event evt) { return false; } private void DrawDropdown(float anim, AdvancedDropdownItem group) { // Start of animated area (the part that moves left and right) var areaPosition = new Rect(0, 0, position.width, position.height); // Adjust to the frame areaPosition.x += kBorderThickness; areaPosition.y += kBorderThickness; areaPosition.height -= kBorderThickness * 2; areaPosition.width -= kBorderThickness * 2; GUILayout.BeginArea(m_Gui.GetAnimRect(areaPosition, anim)); // Header if (showHeader) m_Gui.DrawHeader(group, GoToParent, m_ViewsStack.Count > 0); DrawList(group); GUILayout.EndArea(); } private void DrawList(AdvancedDropdownItem item) { // Start of scroll view list m_State.SetScrollState(item, GUILayout.BeginScrollView(m_State.GetScrollState(item), GUIStyle.none, GUI.skin.verticalScrollbar)); EditorGUIUtility.SetIconSize(m_Gui.iconSize); Rect selectedRect = new Rect(); for (var i = 0; i < item.children.Count(); i++) { var child = item.children.ElementAt(i); var selected = m_State.GetSelectedIndex(item) == i; if (child.IsSeparator()) { GUIHelpers.DrawLineSeparator(child.name); } else { m_Gui.DrawItem(child, child.name, child.icon, child.enabled, child.children.Any(), selected, hasSearch); } var r = GUILayoutUtility.GetLastRect(); if (selected) selectedRect = r; // Skip input handling for the tree used for animation if (item != m_CurrentlyRenderedTree) continue; // Select the element the mouse cursor is over. // Only do it on mouse move - keyboard controls are allowed to overwrite this until the next time the mouse moves. if ((Event.current.type == EventType.MouseMove || Event.current.type == EventType.MouseDrag) && child.enabled) { if (!selected && r.Contains(Event.current.mousePosition)) { m_State.SetSelectedIndex(item, i); Event.current.Use(); } } if (Event.current.type == EventType.MouseUp && r.Contains(Event.current.mousePosition) && child.enabled) { m_State.SetSelectedIndex(item, i); var selectedChild = m_State.GetSelectedChild(item); if (selectedChild.children.Any()) { GoToChild(); } else if (!selectedChild.IsSeparator()) { selectionChanged?.Invoke(selectedChild); if (closeOnSelection) { CloseWindow(); GUIUtility.ExitGUI(); } } Event.current.Use(); } } EditorGUIUtility.SetIconSize(Vector2.zero); GUILayout.EndScrollView(); // Scroll to selected on windows creation if (m_ScrollToSelected && m_InitialSelectionPosition != 0) { var diffOfPopupAboveTheButton = m_ButtonRectScreenPos.y - position.y; diffOfPopupAboveTheButton -= m_Gui.searchHeight + m_Gui.headerHeight; m_State.SetScrollState(item, new Vector2(0, m_InitialSelectionPosition - diffOfPopupAboveTheButton)); m_ScrollToSelected = false; m_InitialSelectionPosition = 0; } // Scroll to show selected else if (m_ScrollToSelected && Event.current.type == EventType.Repaint) { m_ScrollToSelected = false; Rect scrollRect = GUILayoutUtility.GetLastRect(); if (selectedRect.yMax - scrollRect.height > m_State.GetScrollState(item).y) { m_State.SetScrollState(item, new Vector2(0, selectedRect.yMax - scrollRect.height)); Repaint(); } if (selectedRect.y < m_State.GetScrollState(item).y) { m_State.SetScrollState(item, new Vector2(0, selectedRect.y)); Repaint(); } } } protected void GoToParent() { if (m_ViewsStack.Count == 0) return; m_LastTime = DateTime.Now.Ticks; if (m_NewAnimTarget > 0) m_NewAnimTarget = -1 + m_NewAnimTarget; else m_NewAnimTarget = -1; m_AnimationTree = m_CurrentlyRenderedTree; var parentItem = m_ViewsStack.Pop(); m_State.ClearSelectionOnItem(m_CurrentlyRenderedTree); if (parentItem != null) { var suggestedIndex = parentItem.GetIndexOfChild(m_CurrentlyRenderedTree); m_State.SetSelectionOnItem(parentItem, suggestedIndex); } m_CurrentlyRenderedTree = parentItem; } private void GoToChild() { m_ViewsStack.Push(m_CurrentlyRenderedTree); m_LastTime = DateTime.Now.Ticks; if (m_NewAnimTarget < 0) m_NewAnimTarget = 1 + m_NewAnimTarget; else m_NewAnimTarget = 1; m_AnimationTree = m_CurrentlyRenderedTree; m_CurrentlyRenderedTree = m_State.GetSelectedChild(m_CurrentlyRenderedTree); } [DidReloadScripts] private static void OnScriptReload() { CloseAllOpenWindows(); } protected static void CloseAllOpenWindows() { var windows = Resources.FindObjectsOfTypeAll(typeof(T)); foreach (var window in windows) { try { ((EditorWindow)window).Close(); } catch { DestroyImmediate(window); } } } private static class Styles { public static readonly GUIStyle background = "grey_border"; public static readonly GUIStyle previewHeader = new GUIStyle(EditorStyles.label).WithPadding(new RectOffset(5, 5, 1, 2)); public static readonly GUIStyle previewText = new GUIStyle(EditorStyles.wordWrappedLabel).WithPadding(new RectOffset(3, 5, 4, 4)); } } } #endif // UNITY_EDITOR