using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using UnityEngine; using UnityEngine.UIElements; namespace UnityEditor.Searcher { [PublicAPI] public class SearcherWindow : EditorWindow { [PublicAPI] public struct Alignment { [PublicAPI] public enum Horizontal { Left = 0, Center, Right } [PublicAPI] public enum Vertical { Top = 0, Center, Bottom } public readonly Vertical vertical; public readonly Horizontal horizontal; public Alignment(Vertical v, Horizontal h) { vertical = v; horizontal = h; } } const string k_DatabaseDirectory = "/../Library/Searcher"; static readonly float k_SearcherDefaultWidth = 300; static readonly float k_DetailsDefaultWidth = 200; static readonly float k_DefaultHeight = 300; static readonly Vector2 k_MinSize = new Vector2(300, 150); static Vector2 s_Size = Vector2.zero; static IEnumerable s_Items; static Searcher s_Searcher; static Func s_ItemSelectedDelegate; Action m_AnalyticsDataDelegate; SearcherControl m_SearcherControl; Vector2 m_OriginalMousePos; Rect m_OriginalWindowPos; Rect m_NewWindowPos; bool m_IsMouseDownOnResizer; bool m_IsMouseDownOnTitle; Focusable m_FocusedBefore; static Vector2 Size { get { if (s_Size == Vector2.zero) { s_Size = s_Searcher != null && s_Searcher.Adapter.HasDetailsPanel ? new Vector2(k_SearcherDefaultWidth + k_DetailsDefaultWidth, k_DefaultHeight) : new Vector2(k_SearcherDefaultWidth, k_DefaultHeight); } return s_Size; } set => s_Size = value; } public static void Show( EditorWindow host, IList items, string title, Func itemSelectedDelegate, Vector2 displayPosition, Alignment align = default) { Show(host, items, title, Application.dataPath + k_DatabaseDirectory, itemSelectedDelegate, displayPosition, align); } public static void Show( EditorWindow host, IList items, ISearcherAdapter adapter, Func itemSelectedDelegate, Vector2 displayPosition, Action analyticsDataDelegate, Alignment align = default) { Show(host, items, adapter, Application.dataPath + k_DatabaseDirectory, itemSelectedDelegate, displayPosition, analyticsDataDelegate, align); } public static void Show( EditorWindow host, IList items, string title, string directoryPath, Func itemSelectedDelegate, Vector2 displayPosition, Alignment align = default) { s_Items = items; var databaseDir = directoryPath; var database = SearcherDatabase.Create(s_Items.ToList(), databaseDir); s_Searcher = new Searcher(database, title); Show(host, s_Searcher, itemSelectedDelegate, displayPosition, null, align); } public static void Show( EditorWindow host, IEnumerable items, ISearcherAdapter adapter, string directoryPath, Func itemSelectedDelegate, Vector2 displayPosition, Action analyticsDataDelegate, Alignment align = default) { s_Items = items; var databaseDir = directoryPath; var database = SearcherDatabase.Create(s_Items.ToList(), databaseDir); s_Searcher = new Searcher(database, adapter); Show(host, s_Searcher, itemSelectedDelegate, displayPosition, analyticsDataDelegate, align); } public static void Show( EditorWindow host, Searcher searcher, Func itemSelectedDelegate, Vector2 displayPosition, Action analyticsDataDelegate, Alignment align = default) { var position = GetPosition(host, displayPosition, align); var rect = new Rect(GetPositionWithAlignment(position + host.position.position, Size, align), Size); Show(host, searcher, itemSelectedDelegate, analyticsDataDelegate, rect); } public static void Show( EditorWindow host, Searcher searcher, Func itemSelectedDelegate, Action analyticsDataDelegate, Rect rect) { s_Searcher = searcher; s_ItemSelectedDelegate = itemSelectedDelegate; var window = CreateInstance(); window.m_AnalyticsDataDelegate = analyticsDataDelegate; window.position = rect; window.ShowPopup(); window.Focus(); } public static Vector2 GetPositionWithAlignment(Vector2 pos, Vector2 size, Alignment align) { var x = pos.x; var y = pos.y; switch (align.horizontal) { case Alignment.Horizontal.Center: x -= size.x / 2; break; case Alignment.Horizontal.Right: x -= size.x; break; } switch (align.vertical) { case Alignment.Vertical.Center: y -= size.y / 2; break; case Alignment.Vertical.Bottom: y -= size.y; break; } return new Vector2(x, y); } static Vector2 GetPosition(EditorWindow host, Vector2 displayPosition, Alignment align) { var x = displayPosition.x; var y = displayPosition.y; // Searcher overlaps with the right boundary. if (x + Size.x >= host.position.size.x) { switch (align.horizontal) { case Alignment.Horizontal.Center: x -= Size.x / 2; break; case Alignment.Horizontal.Right: x -= Size.x; break; } } // The displayPosition should be in window world space but the // EditorWindow.position is actually the rootVisualElement // rectangle, not including the tabs area. So we need to do a // small correction here. y -= host.rootVisualElement.resolvedStyle.top; // Searcher overlaps with the bottom boundary. if (y + Size.y >= host.position.size.y) { switch (align.vertical) { case Alignment.Vertical.Center: y -= Size.y / 2; break; case Alignment.Vertical.Bottom: y -= Size.y; break; } } return new Vector2(x, y); } void OnEnable() { m_SearcherControl = new SearcherControl(); m_SearcherControl.Setup(s_Searcher, SelectionCallback, OnAnalyticsDataCallback, s_Searcher.Adapter.OnSearchResultsFilter); m_SearcherControl.TitleLabel.RegisterCallback(OnTitleMouseDown); m_SearcherControl.TitleLabel.RegisterCallback(OnTitleMouseUp); m_SearcherControl.Resizer.RegisterCallback(OnResizerMouseDown); m_SearcherControl.Resizer.RegisterCallback(OnResizerMouseUp); var root = rootVisualElement; root.style.flexGrow = 1; root.Add(m_SearcherControl); } void OnDisable() { m_SearcherControl.TitleLabel.UnregisterCallback(OnTitleMouseDown); m_SearcherControl.TitleLabel.UnregisterCallback(OnTitleMouseUp); m_SearcherControl.Resizer.UnregisterCallback(OnResizerMouseDown); m_SearcherControl.Resizer.UnregisterCallback(OnResizerMouseUp); } void OnTitleMouseDown(MouseDownEvent evt) { if (evt.button != (int)MouseButton.LeftMouse) return; m_IsMouseDownOnTitle = true; m_NewWindowPos = position; m_OriginalWindowPos = position; m_OriginalMousePos = evt.mousePosition; m_FocusedBefore = rootVisualElement.panel.focusController.focusedElement; m_SearcherControl.TitleLabel.RegisterCallback(OnTitleMouseMove); m_SearcherControl.TitleLabel.RegisterCallback(OnSearcherKeyDown); m_SearcherControl.TitleLabel.CaptureMouse(); } void OnTitleMouseUp(MouseUpEvent evt) { if (evt.button != (int)MouseButton.LeftMouse) return; if (!m_SearcherControl.TitleLabel.HasMouseCapture()) return; FinishMove(); } void FinishMove() { m_SearcherControl.TitleLabel.UnregisterCallback(OnTitleMouseMove); m_SearcherControl.TitleLabel.UnregisterCallback(OnSearcherKeyDown); m_SearcherControl.TitleLabel.ReleaseMouse(); m_FocusedBefore?.Focus(); m_IsMouseDownOnTitle = false; } void OnTitleMouseMove(MouseMoveEvent evt) { var delta = evt.mousePosition - m_OriginalMousePos; // TODO Temporary fix for Visual Scripting 1st drop. Find why position.position is 0,0 on MacOs in MouseMoveEvent // Bug occurs with Unity 2019.2.0a13 #if UNITY_EDITOR_OSX m_NewWindowPos = new Rect(m_NewWindowPos.position + delta, position.size); #else m_NewWindowPos = new Rect(position.position + delta, position.size); #endif Repaint(); } void OnResizerMouseDown(MouseDownEvent evt) { if (evt.button != (int)MouseButton.LeftMouse) return; m_IsMouseDownOnResizer = true; m_NewWindowPos = position; m_OriginalWindowPos = position; m_OriginalMousePos = evt.mousePosition; m_FocusedBefore = rootVisualElement.panel.focusController.focusedElement; m_SearcherControl.Resizer.RegisterCallback(OnResizerMouseMove); m_SearcherControl.Resizer.RegisterCallback(OnSearcherKeyDown); m_SearcherControl.Resizer.CaptureMouse(); } void OnResizerMouseUp(MouseUpEvent evt) { if (evt.button != (int)MouseButton.LeftMouse) return; if (!m_SearcherControl.Resizer.HasMouseCapture()) return; FinishResize(); } void FinishResize() { m_SearcherControl.Resizer.UnregisterCallback(OnResizerMouseMove); m_SearcherControl.Resizer.UnregisterCallback(OnSearcherKeyDown); m_SearcherControl.Resizer.ReleaseMouse(); m_FocusedBefore?.Focus(); m_IsMouseDownOnResizer = false; } void OnResizerMouseMove(MouseMoveEvent evt) { var delta = evt.mousePosition - m_OriginalMousePos; Size = m_OriginalWindowPos.size + delta; Size = new Vector2(Math.Max(k_MinSize.x, Size.x), Math.Max(k_MinSize.y, Size.y)); // TODO Temporary fix for Visual Scripting 1st drop. Find why position.position is 0,0 on MacOs in MouseMoveEvent // Bug occurs with Unity 2019.2.0a13 #if UNITY_EDITOR_OSX m_NewWindowPos = new Rect(m_NewWindowPos.position, Size); #else m_NewWindowPos = new Rect(position.position, Size); #endif Repaint(); } void OnSearcherKeyDown(KeyDownEvent evt) { if (evt.keyCode == KeyCode.Escape) { if (m_IsMouseDownOnTitle) { FinishMove(); position = m_OriginalWindowPos; } else if (m_IsMouseDownOnResizer) { FinishResize(); position = m_OriginalWindowPos; } } } void OnGUI() { if ((m_IsMouseDownOnTitle || m_IsMouseDownOnResizer) && Event.current.type == EventType.Layout) position = m_NewWindowPos; } void SelectionCallback(SearcherItem item) { // Don't close the window if a category is selected (only categories/titles have children, node entries are leaf elements) // We want to prevent collapsing the window due to accidental double-clicks on a title entry, for instance if (item != null && item.HasChildren) return; if (s_ItemSelectedDelegate == null || s_ItemSelectedDelegate(item)) Close(); } void OnAnalyticsDataCallback(Searcher.AnalyticsEvent item) { m_AnalyticsDataDelegate?.Invoke(item); } void OnLostFocus() { if (m_IsMouseDownOnTitle) { FinishMove(); } else if (m_IsMouseDownOnResizer) { FinishResize(); } // TODO: HACK - ListView's scroll view steals focus using the scheduler. EditorApplication.update += HackDueToCloseOnLostFocusCrashing; } // See: https://fogbugz.unity3d.com/f/cases/1004504/ void HackDueToCloseOnLostFocusCrashing() { // Notify user that the searcher action was cancelled. s_ItemSelectedDelegate?.Invoke(null); Close(); // ReSharper disable once DelegateSubtraction EditorApplication.update -= HackDueToCloseOnLostFocusCrashing; } } }