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; | ||
|  |         } | ||
|  |     } | ||
|  | } |