429 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			429 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| 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<SearcherItem> s_Items;
 | |
|         static Searcher s_Searcher;
 | |
|         static Func<SearcherItem, bool> s_ItemSelectedDelegate;
 | |
| 
 | |
|         Action<Searcher.AnalyticsEvent> 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<SearcherItem> items,
 | |
|             string title,
 | |
|             Func<SearcherItem, bool> itemSelectedDelegate,
 | |
|             Vector2 displayPosition,
 | |
|             Alignment align = default)
 | |
|         {
 | |
|             Show(host, items, title, Application.dataPath + k_DatabaseDirectory, itemSelectedDelegate, displayPosition, align);
 | |
|         }
 | |
| 
 | |
|         public static void Show(
 | |
|             EditorWindow host,
 | |
|             IList<SearcherItem> items,
 | |
|             ISearcherAdapter adapter,
 | |
|             Func<SearcherItem, bool> itemSelectedDelegate,
 | |
|             Vector2 displayPosition,
 | |
|             Action<Searcher.AnalyticsEvent> analyticsDataDelegate,
 | |
|             Alignment align = default)
 | |
|         {
 | |
|             Show(host, items, adapter, Application.dataPath + k_DatabaseDirectory, itemSelectedDelegate,
 | |
|                 displayPosition, analyticsDataDelegate, align);
 | |
|         }
 | |
| 
 | |
|         public static void Show(
 | |
|             EditorWindow host,
 | |
|             IList<SearcherItem> items,
 | |
|             string title,
 | |
|             string directoryPath,
 | |
|             Func<SearcherItem, bool> 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<SearcherItem> items,
 | |
|             ISearcherAdapter adapter,
 | |
|             string directoryPath,
 | |
|             Func<SearcherItem, bool> itemSelectedDelegate,
 | |
|             Vector2 displayPosition,
 | |
|             Action<Searcher.AnalyticsEvent> 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<SearcherItem, bool> itemSelectedDelegate,
 | |
|             Vector2 displayPosition,
 | |
|             Action<Searcher.AnalyticsEvent> 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<SearcherItem, bool> itemSelectedDelegate,
 | |
|             Action<Searcher.AnalyticsEvent> analyticsDataDelegate,
 | |
|             Rect rect)
 | |
|         {
 | |
|             s_Searcher = searcher;
 | |
|             s_ItemSelectedDelegate = itemSelectedDelegate;
 | |
| 
 | |
|             var window = CreateInstance<SearcherWindow>();
 | |
|             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<MouseDownEvent>(OnTitleMouseDown);
 | |
|             m_SearcherControl.TitleLabel.RegisterCallback<MouseUpEvent>(OnTitleMouseUp);
 | |
| 
 | |
|             m_SearcherControl.Resizer.RegisterCallback<MouseDownEvent>(OnResizerMouseDown);
 | |
|             m_SearcherControl.Resizer.RegisterCallback<MouseUpEvent>(OnResizerMouseUp);
 | |
| 
 | |
|             var root = rootVisualElement;
 | |
|             root.style.flexGrow = 1;
 | |
|             root.Add(m_SearcherControl);
 | |
|         }
 | |
| 
 | |
|         void OnDisable()
 | |
|         {
 | |
|             m_SearcherControl.TitleLabel.UnregisterCallback<MouseDownEvent>(OnTitleMouseDown);
 | |
|             m_SearcherControl.TitleLabel.UnregisterCallback<MouseUpEvent>(OnTitleMouseUp);
 | |
| 
 | |
|             m_SearcherControl.Resizer.UnregisterCallback<MouseDownEvent>(OnResizerMouseDown);
 | |
|             m_SearcherControl.Resizer.UnregisterCallback<MouseUpEvent>(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<MouseMoveEvent>(OnTitleMouseMove);
 | |
|             m_SearcherControl.TitleLabel.RegisterCallback<KeyDownEvent>(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<MouseMoveEvent>(OnTitleMouseMove);
 | |
|             m_SearcherControl.TitleLabel.UnregisterCallback<KeyDownEvent>(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<MouseMoveEvent>(OnResizerMouseMove);
 | |
|             m_SearcherControl.Resizer.RegisterCallback<KeyDownEvent>(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<MouseMoveEvent>(OnResizerMouseMove);
 | |
|             m_SearcherControl.Resizer.UnregisterCallback<KeyDownEvent>(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;
 | |
|         }
 | |
|     }
 | |
| }
 |