// ---------------------------------------------------------------------------- // Based on https://github.com/roboryantron/UnityEditorJunkie made by Ryan Hipple // ---------------------------------------------------------------------------- using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; namespace Unity.Tutorials.Core.Editor { /// /// A popup window that displays a list of options and may use a search /// string to filter the displayed content. /// internal class SearchablePopup : PopupWindowContent { /// /// Height of each element in the popup list. /// const float k_RowHeight = 16.0f; /// /// How far to indent list entries. /// const float k_RowIndent = 8.0f; /// /// Name to use for the text field for search. /// const string k_SearchControlName = "EnumSearchText"; /// /// Show a new SearchablePopup. /// /// Rectangle of the button that triggered the popup. /// List of strings to choose from. /// Index of the currently selected string. /// Callback to trigger when a choice is made. public static void Show(Rect activatorRect, string[] options, int current, Action onSelectionMade) { SearchablePopup win = new SearchablePopup(options, current, onSelectionMade); PopupWindow.Show(activatorRect, win); } /// /// Show a new SearchablePopup. /// /// Rectangle of the button that triggered the popup. /// List of GUIContent to choose from. /// Index of the currently selected string. /// Callback to trigger when a choice is made. public static void Show(Rect activatorRect, GUIContent[] options, int current, Action onSelectionMade) { Show(activatorRect, options.Select(o => o.text).ToArray(), current, onSelectionMade); } /// /// Force the focused window to redraw. This can be used to make the /// popup more responsive to mouse movement. /// static void Repaint() { EditorWindow.focusedWindow.Repaint(); } /// /// Draw a generic box. /// /// Where to draw. /// Color to tint the box. static void DrawBox(Rect rect, Color tint) { Color c = GUI.color; GUI.color = tint; GUI.Box(rect, "", s_Selection); GUI.color = c; } /// /// Stores a list of strings and can return a subset of that list that /// matches a given filter string. /// class FilteredList { /// /// An entry in the filtererd list, mapping the text to the /// original index. /// public struct Entry { public int Index; public string Text; } /// /// All posibile items in the list. /// readonly string[] m_AllItems; /// /// Create a new filtered list. /// /// All The items to filter. public FilteredList(string[] items) { m_AllItems = items; Entries = new List(); UpdateFilter(""); } /// /// The current string filtering the list. /// public string Filter { get; set; } /// All valid entries for the current filter. public List Entries { get; set; } /// /// Total possible entries in the list. /// public int MaxLength { get { return m_AllItems.Length; } } /// /// Sets a new filter string and updates the Entries that match the /// new filter if it has changed. /// /// String to use to filter the list. /// /// True if the filter is updated, false if newFilter is the same /// as the current Filter and no update is necessary. /// public bool UpdateFilter(string filter) { if (Filter == filter) { return false; } Filter = filter; Entries.Clear(); for (int i = 0; i < m_AllItems.Length; i++) { if (string.IsNullOrEmpty(Filter) || m_AllItems[i].ToLower().Contains(Filter.ToLower())) { Entry entry = new Entry { Index = i, Text = m_AllItems[i] }; if (string.Equals(m_AllItems[i], Filter, StringComparison.CurrentCultureIgnoreCase)) { Entries.Insert(0, entry); } else { Entries.Add(entry); } } } return true; } } /// /// Callback to trigger when an item is selected. /// readonly Action m_OnSelectionMade; /// /// Index of the item that was selected when the list was opened. /// readonly int m_CurrentIndex; /// /// Container for all available options that does the actual string /// filtering of the content. /// readonly FilteredList m_List; /// /// Scroll offset for the vertical scroll area. /// Vector2 m_Scroll; /// /// Index of the item under the mouse or selected with the keyboard. /// int m_HoverIndex; /// /// An item index to scroll to on the next draw. /// int m_ScrollToIndex; /// /// An offset to apply after scrolling to scrollToIndex. This can be /// used to control if the selection appears at the top, bottom, or /// center of the popup. /// float m_ScrollOffset; // GUIStyles implicitly cast from a string. This triggers a lookup into // the current skin which will be the editor skin and lets us get some // built-in styles. static GUIStyle s_SearchBox = "ToolbarSeachTextField"; static GUIStyle s_CancelButton = "ToolbarSeachCancelButton"; static GUIStyle s_DisabledCancelButton = "ToolbarSeachCancelButtonEmpty"; static GUIStyle s_Selection = "SelectionRect"; SearchablePopup(string[] names, int currentIndex, Action onSelectionMade) { m_List = new FilteredList(names); m_CurrentIndex = currentIndex; m_OnSelectionMade = onSelectionMade; m_HoverIndex = currentIndex; m_ScrollToIndex = currentIndex; m_ScrollOffset = GetWindowSize().y - k_RowHeight * 2; } /// /// Called when the Popup opens /// public override void OnOpen() { base.OnOpen(); // Force a repaint every frame to be responsive to mouse hover. EditorApplication.update += Repaint; } /// /// Called when the Popup closes /// public override void OnClose() { base.OnClose(); EditorApplication.update -= Repaint; } /// /// Gets the size of the window /// /// The size of the window public override Vector2 GetWindowSize() { return new Vector2(base.GetWindowSize().x * 2, Mathf.Min(600, m_List.MaxLength * k_RowHeight + EditorStyles.toolbar.fixedHeight)); } /// /// Draws the popup using IMGUI /// /// The rect of the window to be drawn public override void OnGUI(Rect rect) { Rect searchRect = new Rect(0, 0, rect.width, EditorStyles.toolbar.fixedHeight); Rect scrollRect = Rect.MinMaxRect(0, searchRect.yMax, rect.xMax, rect.yMax); HandleKeyboard(); DrawSearch(searchRect); DrawSelectionArea(scrollRect); } void DrawSearch(Rect rect) { if (Event.current.type == EventType.Repaint) { EditorStyles.toolbar.Draw(rect, false, false, false, false); } Rect searchRect = new Rect(rect); searchRect.xMin += 6; searchRect.xMax -= 6; searchRect.y += 2; searchRect.width -= s_CancelButton.fixedWidth; GUI.FocusControl(k_SearchControlName); GUI.SetNextControlName(k_SearchControlName); string newText = GUI.TextField(searchRect, m_List.Filter, s_SearchBox); if (m_List.UpdateFilter(newText)) { m_HoverIndex = 0; m_Scroll = Vector2.zero; } searchRect.x = searchRect.xMax; searchRect.width = s_CancelButton.fixedWidth; if (string.IsNullOrEmpty(m_List.Filter)) { GUI.Box(searchRect, GUIContent.none, s_DisabledCancelButton); } else if (GUI.Button(searchRect, "x", s_CancelButton)) { m_List.UpdateFilter(""); m_Scroll = Vector2.zero; } } void DrawSelectionArea(Rect scrollRect) { Rect contentRect = new Rect(0, 0, scrollRect.width - GUI.skin.verticalScrollbar.fixedWidth, m_List.Entries.Count * k_RowHeight); m_Scroll = GUI.BeginScrollView(scrollRect, m_Scroll, contentRect); Rect rowRect = new Rect(0, 0, scrollRect.width, k_RowHeight); for (int i = 0; i < m_List.Entries.Count; i++) { if (m_ScrollToIndex == i && (Event.current.type == EventType.Repaint || Event.current.type == EventType.Layout)) { Rect r = new Rect(rowRect); r.y += m_ScrollOffset; GUI.ScrollTo(r); m_ScrollToIndex = -1; m_Scroll.x = 0; } if (rowRect.Contains(Event.current.mousePosition)) { if (Event.current.type == EventType.MouseMove || Event.current.type == EventType.ScrollWheel) { m_HoverIndex = i; } if (Event.current.type == EventType.MouseDown) { m_OnSelectionMade(m_List.Entries[i].Index); EditorWindow.focusedWindow.Close(); } } DrawRow(rowRect, i); rowRect.y = rowRect.yMax; } GUI.EndScrollView(); } void DrawRow(Rect rowRect, int i) { if (m_List.Entries[i].Index == m_CurrentIndex) { DrawBox(rowRect, Color.cyan); } else if (i == m_HoverIndex) { DrawBox(rowRect, Color.white); } Rect labelRect = new Rect(rowRect); labelRect.xMin += k_RowIndent; GUI.Label(labelRect, m_List.Entries[i].Text); } /// /// Process keyboard input to navigate the choices or make a selection. /// void HandleKeyboard() { if (Event.current.type != EventType.KeyDown) return; if (Event.current.keyCode == KeyCode.DownArrow) { m_HoverIndex = Mathf.Min(m_List.Entries.Count - 1, m_HoverIndex + 1); Event.current.Use(); m_ScrollToIndex = m_HoverIndex; m_ScrollOffset = k_RowHeight; } if (Event.current.keyCode == KeyCode.UpArrow) { m_HoverIndex = Mathf.Max(0, m_HoverIndex - 1); Event.current.Use(); m_ScrollToIndex = m_HoverIndex; m_ScrollOffset = -k_RowHeight; } if (Event.current.keyCode == KeyCode.Return) { if (m_HoverIndex >= 0 && m_HoverIndex < m_List.Entries.Count) { m_OnSelectionMade(m_List.Entries[m_HoverIndex].Index); EditorWindow.focusedWindow.Close(); } } if (Event.current.keyCode == KeyCode.Escape) { EditorWindow.focusedWindow.Close(); } } } }