807 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			807 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System;
 | |
| using System.Collections.Generic;
 | |
| using System.Linq;
 | |
| using UnityEngine;
 | |
| using UnityEngine.UIElements;
 | |
| using UnityEditor.UIElements;
 | |
| 
 | |
| namespace UnityEditor.Searcher
 | |
| {
 | |
|     class SearcherControl : VisualElement
 | |
|     {
 | |
|         // Window constants.
 | |
|         const string k_WindowTitleLabel = "windowTitleLabel";
 | |
|         const string k_WindowDetailsPanel = "windowDetailsVisualContainer";
 | |
|         const string k_WindowResultsScrollViewName = "windowResultsScrollView";
 | |
|         const string k_WindowSearchTextFieldName = "searchBox";
 | |
|         const string k_WindowAutoCompleteLabelName = "autoCompleteLabel";
 | |
|         const string k_WindowSearchIconName = "searchIcon";
 | |
|         const string k_WindowResizerName = "windowResizer";
 | |
|         const string kWindowSearcherPanel = "searcherVisualContainer";
 | |
|         const int k_TabCharacter = 9;
 | |
| 
 | |
|         Label m_AutoCompleteLabel;
 | |
|         IEnumerable<SearcherItem> m_Results;
 | |
|         List<SearcherItem> m_VisibleResults;
 | |
|         HashSet<SearcherItem> m_ExpandedResults;
 | |
|         HashSet<SearcherItem> m_MultiSelectSelection;
 | |
|         Dictionary<SearcherItem, Toggle> m_SearchItemToVisualToggle;
 | |
|         Searcher m_Searcher;
 | |
|         string m_SuggestedTerm;
 | |
|         string m_Text = string.Empty;
 | |
|         Action<SearcherItem> m_SelectionCallback;
 | |
|         Action<Searcher.AnalyticsEvent> m_AnalyticsDataCallback;
 | |
|         Func<IEnumerable<SearcherItem>, string, SearcherItem> m_SearchResultsFilterCallback;
 | |
|         ListView m_ListView;
 | |
|         TextField m_SearchTextField;
 | |
|         VisualElement m_SearchTextInput;
 | |
|         VisualElement m_DetailsPanel;
 | |
|         VisualElement m_SearcherPanel;
 | |
|         VisualElement m_ContentContainer;
 | |
|         Button m_ConfirmButton;
 | |
| 
 | |
|         internal Label TitleLabel { get; }
 | |
|         internal VisualElement Resizer { get; }
 | |
| 
 | |
|         public SearcherControl()
 | |
|         {
 | |
|             // Load window template.
 | |
|             var windowUxmlTemplate = Resources.Load<VisualTreeAsset>("SearcherWindow");
 | |
| 
 | |
|             // Clone Window Template.
 | |
|             var windowRootVisualElement = windowUxmlTemplate.CloneTree();
 | |
|             windowRootVisualElement.AddToClassList("content");
 | |
| 
 | |
|             windowRootVisualElement.StretchToParentSize();
 | |
| 
 | |
|             // Add Window VisualElement to window's RootVisualContainer
 | |
|             Add(windowRootVisualElement);
 | |
| 
 | |
|             m_VisibleResults = new List<SearcherItem>();
 | |
|             m_ExpandedResults = new HashSet<SearcherItem>();
 | |
|             m_MultiSelectSelection = new HashSet<SearcherItem>();
 | |
|             m_SearchItemToVisualToggle = new Dictionary<SearcherItem, Toggle>();
 | |
| 
 | |
|             m_ListView = this.Q<ListView>(k_WindowResultsScrollViewName);
 | |
| 
 | |
|             if (m_ListView != null)
 | |
|             {
 | |
|                 m_ListView.bindItem = Bind;
 | |
|                 m_ListView.RegisterCallback<KeyDownEvent>(SetSelectedElementInResultsList);
 | |
| 
 | |
| #if UNITY_2020_1_OR_NEWER
 | |
|                 m_ListView.onItemsChosen += obj => OnListViewSelect((SearcherItem)obj.FirstOrDefault());
 | |
|                 m_ListView.onSelectionChange += selectedItems => m_Searcher.Adapter.OnSelectionChanged(selectedItems.OfType<SearcherItem>().ToList());
 | |
| #else
 | |
|                 m_ListView.onItemChosen += obj => OnListViewSelect((SearcherItem)obj);
 | |
|                 m_ListView.onSelectionChanged += selectedItems => m_Searcher.Adapter.OnSelectionChanged(selectedItems.OfType<SearcherItem>());
 | |
| #endif
 | |
|                 m_ListView.focusable = true;
 | |
|                 m_ListView.tabIndex = 1;
 | |
|             }
 | |
| 
 | |
|             m_DetailsPanel = this.Q(k_WindowDetailsPanel);
 | |
| 
 | |
|             TitleLabel = this.Q<Label>(k_WindowTitleLabel);
 | |
| 
 | |
|             m_SearcherPanel = this.Q(kWindowSearcherPanel);
 | |
| 
 | |
|             m_SearchTextField = this.Q<TextField>(k_WindowSearchTextFieldName);
 | |
|             if (m_SearchTextField != null)
 | |
|             {
 | |
|                 m_SearchTextField.focusable = true;
 | |
|                 m_SearchTextField.RegisterCallback<InputEvent>(OnSearchTextFieldTextChanged, TrickleDown.TrickleDown);
 | |
| 
 | |
|                 m_SearchTextInput = m_SearchTextField.Q(TextInputBaseField<string>.textInputUssName);
 | |
|                 m_SearchTextInput.RegisterCallback<KeyDownEvent>(OnSearchTextFieldKeyDown,  TrickleDown.TrickleDown);
 | |
|             }
 | |
| 
 | |
|             m_AutoCompleteLabel = this.Q<Label>(k_WindowAutoCompleteLabelName);
 | |
| 
 | |
|             Resizer = this.Q(k_WindowResizerName);
 | |
| 
 | |
|             m_ContentContainer = this.Q("unity-content-container");
 | |
| 
 | |
|             m_ConfirmButton = this.Q<Button>("confirmButton");
 | |
| #if UNITY_2019_3_OR_NEWER
 | |
|             m_ConfirmButton.clicked += OnConfirmMultiselect;
 | |
| #else
 | |
|             m_ConfirmButton.clickable.clicked += OnConfirmMultiselect;
 | |
| #endif
 | |
| 
 | |
|             RegisterCallback<AttachToPanelEvent>(OnEnterPanel);
 | |
|             RegisterCallback<DetachFromPanelEvent>(OnLeavePanel);
 | |
| 
 | |
|             // TODO: HACK - ListView's scroll view steals focus using the scheduler.
 | |
|             EditorApplication.update += HackDueToListViewScrollViewStealingFocus;
 | |
| 
 | |
|             style.flexGrow = 1;
 | |
|         }
 | |
| 
 | |
| 		void OnConfirmMultiselect()
 | |
|         {
 | |
|             if (m_MultiSelectSelection.Count == 0)
 | |
|             {
 | |
|                 m_SelectionCallback(null);
 | |
|                 return;
 | |
|             } 
 | |
|             foreach (SearcherItem item in m_MultiSelectSelection)
 | |
|             {
 | |
|                 m_SelectionCallback(item);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         void HackDueToListViewScrollViewStealingFocus()
 | |
|         {
 | |
|             m_SearchTextInput?.Focus();
 | |
|             // ReSharper disable once DelegateSubtraction
 | |
|             EditorApplication.update -= HackDueToListViewScrollViewStealingFocus;
 | |
|         }
 | |
| 
 | |
|         void OnEnterPanel(AttachToPanelEvent e)
 | |
|         {
 | |
|             RegisterCallback<KeyDownEvent>(OnKeyDown);
 | |
|         }
 | |
| 
 | |
|         void OnLeavePanel(DetachFromPanelEvent e)
 | |
|         {
 | |
|             UnregisterCallback<KeyDownEvent>(OnKeyDown);
 | |
|         }
 | |
| 
 | |
|         void OnKeyDown(KeyDownEvent e)
 | |
|         {
 | |
|             if (e.keyCode == KeyCode.Escape)
 | |
|             {
 | |
|                 CancelSearch();
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         void OnListViewSelect(SearcherItem item)
 | |
|         {
 | |
|             if (!m_Searcher.Adapter.MultiSelectEnabled)
 | |
|             {
 | |
|                 m_SelectionCallback(item);
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 ToggleItemForMultiSelect(item, !m_MultiSelectSelection.Contains(item));
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         void CancelSearch()
 | |
|         {
 | |
|             OnSearchTextFieldTextChanged(InputEvent.GetPooled(m_Text, string.Empty));
 | |
|             OnListViewSelect(null);
 | |
|             m_AnalyticsDataCallback?.Invoke(new Searcher.AnalyticsEvent(Searcher.AnalyticsEvent.EventType.Cancelled, m_SearchTextField.value));
 | |
|         }
 | |
| 
 | |
|         public void Setup(Searcher searcher, Action<SearcherItem> selectionCallback, Action<Searcher.AnalyticsEvent> analyticsDataCallback, Func<IEnumerable<SearcherItem>, string, SearcherItem> searchResultsFilterCallback)
 | |
|         {
 | |
|             m_Searcher = searcher;
 | |
|             m_SelectionCallback = selectionCallback;
 | |
|             m_AnalyticsDataCallback = analyticsDataCallback;
 | |
|             m_SearchResultsFilterCallback = searchResultsFilterCallback;
 | |
|             
 | |
| 
 | |
|             if (m_Searcher.Adapter.MultiSelectEnabled) {
 | |
|                 AddToClassList("searcher__multiselect");
 | |
|             }
 | |
| 
 | |
|             if (m_Searcher.Adapter.HasDetailsPanel)
 | |
|             {
 | |
|                 m_Searcher.Adapter.InitDetailsPanel(m_DetailsPanel);
 | |
|                 m_DetailsPanel.RemoveFromClassList("hidden");
 | |
|                 m_DetailsPanel.style.flexGrow = m_Searcher.Adapter.InitialSplitterDetailRatio;
 | |
|                 m_SearcherPanel.style.flexGrow = 1;
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 m_DetailsPanel.AddToClassList("hidden");
 | |
| 
 | |
|                 var splitter = m_DetailsPanel.parent;
 | |
| 
 | |
|                 splitter.parent.Insert(0,m_SearcherPanel);
 | |
|                 splitter.parent.Insert(1, m_DetailsPanel);
 | |
| 
 | |
|                 splitter.RemoveFromHierarchy();
 | |
|             }
 | |
| 
 | |
|             
 | |
| 
 | |
|             TitleLabel.text = m_Searcher.Adapter.Title;
 | |
|             if (string.IsNullOrEmpty(TitleLabel.text))
 | |
|             {
 | |
|                 TitleLabel.parent.style.visibility = Visibility.Hidden;
 | |
|                 TitleLabel.parent.style.position = Position.Absolute;
 | |
|             }
 | |
| 
 | |
|             m_Searcher.BuildIndices();
 | |
|             Refresh();
 | |
|         }
 | |
| 
 | |
|         void Refresh()
 | |
|         {
 | |
|             var query = m_Text;
 | |
|             m_Results = m_Searcher.Search(query);
 | |
|             GenerateVisibleResults();
 | |
| 
 | |
|             // The first item in the results is always the highest scored item.
 | |
|             // We want to scroll to and select this item.
 | |
|             var visibleIndex = -1;
 | |
|             m_SuggestedTerm = string.Empty;
 | |
| 
 | |
|             var results = m_Results.ToList();
 | |
|             if (results.Any())
 | |
|             {
 | |
|                 SearcherItem scrollToItem = m_SearchResultsFilterCallback?.Invoke(results, query);
 | |
|                 if(scrollToItem == null)
 | |
|                     scrollToItem = results.First();
 | |
|                 visibleIndex = m_VisibleResults.IndexOf(scrollToItem);
 | |
| 
 | |
|                 // If we're trying to scroll to a result that is not visible in a single category,
 | |
|                 // we need to add that result and its hierarchy back to the visible results
 | |
|                 // This prevents searcher suggesting a single collapsed category that the user then needs to manually expand regardless
 | |
|                 if (visibleIndex == -1 && m_VisibleResults.Count() == 1)
 | |
|                 {
 | |
|                     SearcherItem currentItemRoot = scrollToItem;
 | |
|                     var idSet = new HashSet<SearcherItem>();
 | |
|                     while (currentItemRoot.Parent != null)
 | |
|                     {
 | |
|                         currentItemRoot = currentItemRoot.Parent;
 | |
|                     }
 | |
|                     idSet.Add(currentItemRoot);
 | |
|                     AddResultChildren(currentItemRoot, idSet);
 | |
|                     visibleIndex = m_VisibleResults.IndexOf(scrollToItem);
 | |
|                 }
 | |
| 
 | |
|                 var cursorIndex = m_SearchTextField.cursorIndex;
 | |
| 
 | |
|                 if (query.Length > 0)
 | |
|                 {
 | |
|                     var strings = scrollToItem.Name.Split(' ');
 | |
|                     var wordStartIndex = cursorIndex == 0 ? 0 : query.LastIndexOf(' ', cursorIndex - 1) + 1;
 | |
|                     var word = query.Substring(wordStartIndex, cursorIndex - wordStartIndex);
 | |
| 
 | |
|                     if (word.Length > 0)
 | |
|                         foreach (var t in strings)
 | |
|                         {
 | |
|                             if (t.StartsWith(word, StringComparison.OrdinalIgnoreCase))
 | |
|                             {
 | |
|                                 m_SuggestedTerm = t;
 | |
|                                 break;
 | |
|                             }
 | |
|                         }
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             m_ListView.itemsSource = m_VisibleResults;
 | |
|             m_ListView.makeItem = MakeItem;
 | |
|             RefreshListView();
 | |
|             
 | |
|             SetSelectedElementInResultsList(visibleIndex);
 | |
|         }
 | |
| 
 | |
|         VisualElement MakeItem()
 | |
|         {
 | |
|             VisualElement item = m_Searcher.Adapter.MakeItem();
 | |
|             if (m_Searcher.Adapter.MultiSelectEnabled)
 | |
|             {
 | |
|                 var selectionToggle = item.Q<Toggle>("itemToggle");
 | |
|                 if (selectionToggle != null)
 | |
|                 {
 | |
|                     selectionToggle.RegisterValueChangedCallback(changeEvent =>
 | |
|                     {
 | |
|                         SearcherItem searcherItem = item.userData as SearcherItem;
 | |
|                         ToggleItemForMultiSelect(searcherItem, changeEvent.newValue);
 | |
|                     });
 | |
|                 }
 | |
|             }
 | |
|             return item;
 | |
|         }
 | |
| 
 | |
|         void GenerateVisibleResults()
 | |
|         {
 | |
|             if (string.IsNullOrEmpty(m_Text))
 | |
|             {
 | |
|                 m_ExpandedResults.Clear();
 | |
|                 RemoveChildrenFromResults();
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             RegenerateVisibleResults();
 | |
|             ExpandAllParents();
 | |
|         }
 | |
| 
 | |
|         void ExpandAllParents()
 | |
|         {
 | |
|             m_ExpandedResults.Clear();
 | |
|             foreach (var item in m_VisibleResults)
 | |
|                 if (item.HasChildren)
 | |
|                     m_ExpandedResults.Add(item);
 | |
|         }
 | |
| 
 | |
|         void RemoveChildrenFromResults()
 | |
|         {
 | |
|             m_VisibleResults.Clear();
 | |
|             var parents = new HashSet<SearcherItem>();
 | |
| 
 | |
|             foreach (var item in m_Results.Where(i => !parents.Contains(i)))
 | |
|             {
 | |
|                 var currentParent = item;
 | |
| 
 | |
|                 while (true)
 | |
|                 {
 | |
|                     if (currentParent.Parent == null)
 | |
|                     {
 | |
|                         if (parents.Contains(currentParent))
 | |
|                             break;
 | |
| 
 | |
|                         parents.Add(currentParent);
 | |
|                         m_VisibleResults.Add(currentParent);
 | |
|                         break;
 | |
|                     }
 | |
| 
 | |
|                     currentParent = currentParent.Parent;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             if (m_Searcher.SortComparison != null)
 | |
|                 m_VisibleResults.Sort(m_Searcher.SortComparison);
 | |
|         }
 | |
| 
 | |
|         void RegenerateVisibleResults()
 | |
|         {
 | |
|             var idSet = new HashSet<SearcherItem>();
 | |
|             m_VisibleResults.Clear();
 | |
| 
 | |
|             foreach (var item in m_Results.Where(item => !idSet.Contains(item)))
 | |
|             {
 | |
|                 idSet.Add(item);
 | |
|                 m_VisibleResults.Add(item);
 | |
| 
 | |
|                 var currentParent = item.Parent;
 | |
|                 while (currentParent != null)
 | |
|                 {
 | |
|                     if (!idSet.Contains(currentParent))
 | |
|                     {
 | |
|                         idSet.Add(currentParent);
 | |
|                         m_VisibleResults.Add(currentParent);
 | |
|                     }
 | |
| 
 | |
|                     currentParent = currentParent.Parent;
 | |
|                 }
 | |
| 
 | |
|                 AddResultChildren(item, idSet);
 | |
|             }
 | |
| 
 | |
|             var comparison = m_Searcher.SortComparison ?? ((i1, i2) =>
 | |
|             {
 | |
|                 var result = i1.Database.Id - i2.Database.Id;
 | |
|                 return result != 0 ? result : i1.Id - i2.Id;
 | |
|             });
 | |
|             m_VisibleResults.Sort(comparison);
 | |
|         }
 | |
| 
 | |
|         void AddResultChildren(SearcherItem item, ISet<SearcherItem> idSet)
 | |
|         {
 | |
|             if (!item.HasChildren)
 | |
|                 return;
 | |
|             if (m_Searcher.Adapter.AddAllChildResults)
 | |
|             {
 | |
|                 //add all children results for current search term 
 | |
|                 // eg "Book" will show both "Cook Book" and "Cooking" as children
 | |
|                 foreach (var child in item.Children)
 | |
|                 {
 | |
|                     if (!idSet.Contains(child))
 | |
|                     {
 | |
|                         idSet.Add(child);
 | |
|                         m_VisibleResults.Add(child);
 | |
|                     }
 | |
| 
 | |
|                     AddResultChildren(child, idSet);
 | |
|                 }
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 foreach (var child in item.Children)
 | |
|                 {
 | |
|                     //only add child results if the child matches the search term 
 | |
|                     // eg "Book" will show "Cook Book" but not "Cooking" as a child
 | |
|                     if (!m_Results.Contains(child))
 | |
|                         continue;
 | |
| 
 | |
|                     if (!idSet.Contains(child))
 | |
|                     {
 | |
|                         idSet.Add(child);
 | |
|                         m_VisibleResults.Add(child);
 | |
|                     }
 | |
| 
 | |
|                     AddResultChildren(child, idSet);
 | |
|                 }
 | |
|             } 
 | |
|         }
 | |
| 
 | |
|         bool HasChildResult(SearcherItem item)
 | |
|         {
 | |
|             if (m_Results.Contains(item))
 | |
|                 return true;
 | |
| 
 | |
|             foreach (var child in item.Children)
 | |
|             {
 | |
|                 if (HasChildResult(child))
 | |
|                     return true;
 | |
|             }
 | |
| 
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         ItemExpanderState GetExpanderState(int index)
 | |
|         {
 | |
|             var item = m_VisibleResults[index];
 | |
| 
 | |
|             foreach (var child in item.Children)
 | |
|             {
 | |
|                 if (!m_VisibleResults.Contains(child) && !HasChildResult(child))
 | |
|                     continue;
 | |
| 
 | |
|                 return m_ExpandedResults.Contains(item) ? ItemExpanderState.Expanded : ItemExpanderState.Collapsed;
 | |
|             }
 | |
| 
 | |
|             return item.Children.Count != 0 ? ItemExpanderState.Collapsed : ItemExpanderState.Hidden;
 | |
|         }
 | |
| 
 | |
|         void Bind(VisualElement target, int index)
 | |
|         {
 | |
|             var item = m_VisibleResults[index];
 | |
|             var expanderState = GetExpanderState(index);
 | |
|             var expander = m_Searcher.Adapter.Bind(target, item, expanderState, m_Text);
 | |
|             var selectionToggle = target.Q<Toggle>("itemToggle");
 | |
|             if (selectionToggle != null)
 | |
|             {
 | |
|                 selectionToggle.SetValueWithoutNotify(m_MultiSelectSelection.Contains(item));
 | |
|                 m_SearchItemToVisualToggle[item] = selectionToggle;
 | |
|             }
 | |
|             expander.RegisterCallback<MouseDownEvent>(ExpandOrCollapse);
 | |
|         }
 | |
| 
 | |
|         void ToggleItemForMultiSelect(SearcherItem item, bool selected)
 | |
|         {
 | |
|             if (selected)
 | |
|             {
 | |
|                 m_MultiSelectSelection.Add(item);
 | |
|             } else
 | |
|             {
 | |
|                 m_MultiSelectSelection.Remove(item);
 | |
|             }
 | |
| 
 | |
|             Toggle toggle;
 | |
|             if (m_SearchItemToVisualToggle.TryGetValue(item, out toggle))
 | |
|             {
 | |
|                 toggle.SetValueWithoutNotify(selected);
 | |
|             }
 | |
| 
 | |
|             foreach (var child in item.Children)
 | |
|             {
 | |
|                 ToggleItemForMultiSelect(child, selected);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         static void GetItemsToHide(SearcherItem parent, ref HashSet<SearcherItem> itemsToHide)
 | |
|         {
 | |
|             if (!parent.HasChildren)
 | |
|             {
 | |
|                 itemsToHide.Add(parent);
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             foreach (var child in parent.Children)
 | |
|             {
 | |
|                 itemsToHide.Add(child);
 | |
|                 GetItemsToHide(child, ref itemsToHide);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         void HideUnexpandedItems()
 | |
|         {
 | |
|             // Hide unexpanded children.
 | |
|             var itemsToHide = new HashSet<SearcherItem>();
 | |
|             foreach (var item in m_VisibleResults)
 | |
|             {
 | |
|                 if (m_ExpandedResults.Contains(item))
 | |
|                     continue;
 | |
| 
 | |
|                 if (!item.HasChildren)
 | |
|                     continue;
 | |
| 
 | |
|                 if (itemsToHide.Contains(item))
 | |
|                     continue;
 | |
| 
 | |
|                 // We need to hide its children.
 | |
|                 GetItemsToHide(item, ref itemsToHide);
 | |
|             }
 | |
| 
 | |
|             foreach (var item in itemsToHide)
 | |
|                 m_VisibleResults.Remove(item);
 | |
|         }
 | |
| 
 | |
|         void RefreshListView()
 | |
|         {
 | |
|             m_SearchItemToVisualToggle.Clear();
 | |
| #if UNITY_2021_2_OR_NEWER
 | |
|             m_ListView.Rebuild();
 | |
| #else
 | |
|             m_ListView.Refresh();
 | |
| #endif
 | |
|         }
 | |
| 
 | |
|         // ReSharper disable once UnusedMember.Local
 | |
|         void RefreshListViewOn()
 | |
|         {
 | |
|             // TODO: Call ListView.Refresh() when it is fixed.
 | |
|             // Need this workaround until then.
 | |
|             // See: https://fogbugz.unity3d.com/f/cases/1027728/
 | |
|             // And: https://gitlab.internal.unity3d.com/upm-packages/editor/com.unity.searcher/issues/9
 | |
| 
 | |
|             var scrollView = m_ListView.Q<ScrollView>();
 | |
| 
 | |
|             var scroller = scrollView?.Q<Scroller>("VerticalScroller");
 | |
|             if (scroller == null)
 | |
|                 return;
 | |
| 
 | |
|             var oldValue = scroller.value;
 | |
|             scroller.value = oldValue + 1.0f;
 | |
|             scroller.value = oldValue - 1.0f;
 | |
|             scroller.value = oldValue;
 | |
|         }
 | |
| 
 | |
|         void Expand(SearcherItem item)
 | |
|         {
 | |
|             m_ExpandedResults.Add(item);
 | |
| 
 | |
|             RegenerateVisibleResults();
 | |
|             HideUnexpandedItems();
 | |
| 
 | |
|             RefreshListView();
 | |
|         }
 | |
| 
 | |
|         void Collapse(SearcherItem item)
 | |
|         {
 | |
|             // if it's already collapsed or not collapsed
 | |
|             if (!m_ExpandedResults.Remove(item))
 | |
|             {
 | |
|                 // this case applies for a left arrow key press
 | |
|                 if (item.Parent != null)
 | |
|                     SetSelectedElementInResultsList(m_VisibleResults.IndexOf(item.Parent));
 | |
| 
 | |
|                 // even if it's a root item and has no parents, do nothing more
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             RegenerateVisibleResults();
 | |
|             HideUnexpandedItems();
 | |
| 
 | |
|             // TODO: understand what happened
 | |
|             RefreshListView();
 | |
| 
 | |
|             // RefreshListViewOn();
 | |
|         }
 | |
| 
 | |
|         void ExpandOrCollapse(MouseDownEvent evt)
 | |
|         {
 | |
|             if (!(evt.target is VisualElement expanderLabel))
 | |
|                 return;
 | |
| 
 | |
|             VisualElement itemElement = expanderLabel.GetFirstAncestorOfType<TemplateContainer>();
 | |
| 
 | |
|             if (!(itemElement?.userData is SearcherItem item)
 | |
|                 || !item.HasChildren
 | |
|                 || !expanderLabel.ClassListContains("Expanded") && !expanderLabel.ClassListContains("Collapsed"))
 | |
|                 return;
 | |
| 
 | |
|             if (!m_ExpandedResults.Contains(item))
 | |
|                 Expand(item);
 | |
|             else
 | |
|                 Collapse(item);
 | |
| 
 | |
|             evt.StopImmediatePropagation();
 | |
|         }
 | |
| 
 | |
|         void OnSearchTextFieldTextChanged(InputEvent inputEvent)
 | |
|         {
 | |
|             var text = inputEvent.newData;
 | |
| 
 | |
|             if (string.Equals(text, m_Text))
 | |
|                 return;
 | |
| 
 | |
|             // This is necessary due to OnTextChanged(...) being called after user inputs that have no impact on the text.
 | |
|             // Ex: Moving the caret.
 | |
|             m_Text = text;
 | |
| 
 | |
|             // If backspace is pressed and no text remain, clear the suggestion label.
 | |
|             if (string.IsNullOrEmpty(text))
 | |
|             {
 | |
|                 this.Q(k_WindowSearchIconName).RemoveFromClassList("Active");
 | |
| 
 | |
|                 // Display the unfiltered results list.
 | |
|                 Refresh();
 | |
| 
 | |
|                 m_AutoCompleteLabel.text = String.Empty;
 | |
|                 m_SuggestedTerm = String.Empty;
 | |
| 
 | |
|                 SetSelectedElementInResultsList(0);
 | |
| 
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             if (!this.Q(k_WindowSearchIconName).ClassListContains("Active"))
 | |
|                 this.Q(k_WindowSearchIconName).AddToClassList("Active");
 | |
| 
 | |
|             Refresh();
 | |
| 
 | |
|             // Calculate the start and end indexes of the word being modified (if any).
 | |
|             var cursorIndex = m_SearchTextField.cursorIndex;
 | |
| 
 | |
|             // search toward the beginning of the string starting at the character before the cursor
 | |
|             // +1 because we want the char after a space, or 0 if the search fails
 | |
|             var wordStartIndex = cursorIndex == 0 ? 0 : (text.LastIndexOf(' ', cursorIndex - 1) + 1);
 | |
| 
 | |
|             // search toward the end of the string from the cursor index
 | |
|             var wordEndIndex = text.IndexOf(' ', cursorIndex);
 | |
|             if (wordEndIndex == -1) // no space found, assume end of string
 | |
|                 wordEndIndex = text.Length;
 | |
| 
 | |
|             // Clear the suggestion term if the caret is not within a word (both start and end indexes are equal, ex: (space)caret(space))
 | |
|             // or the user didn't append characters to a word at the end of the query.
 | |
|             if (wordStartIndex == wordEndIndex || wordEndIndex < text.Length)
 | |
|             {
 | |
|                 m_AutoCompleteLabel.text = string.Empty;
 | |
|                 m_SuggestedTerm = string.Empty;
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             var word = text.Substring(wordStartIndex, wordEndIndex - wordStartIndex);
 | |
| 
 | |
|             if (!string.IsNullOrEmpty(m_SuggestedTerm))
 | |
|             {
 | |
|                 var wordSuggestion =
 | |
|                     word + m_SuggestedTerm.Substring(word.Length, m_SuggestedTerm.Length - word.Length);
 | |
|                 text = text.Remove(wordStartIndex, word.Length);
 | |
|                 text = text.Insert(wordStartIndex, wordSuggestion);
 | |
|                 m_AutoCompleteLabel.text = text;
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 m_AutoCompleteLabel.text = String.Empty;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         void OnSearchTextFieldKeyDown(KeyDownEvent keyDownEvent)
 | |
|         {
 | |
|             // First, check if we cancelled the search.
 | |
|             if (keyDownEvent.keyCode == KeyCode.Escape)
 | |
|             {
 | |
|                 CancelSearch();
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             // For some reason the KeyDown event is raised twice when entering a character.
 | |
|             // As such, we ignore one of the duplicate event.
 | |
|             // This workaround was recommended by the Editor team. The cause of the issue relates to how IMGUI works
 | |
|             // and a fix was not in the works at the moment of this writing.
 | |
|             if (keyDownEvent.character == k_TabCharacter)
 | |
|             {
 | |
|                 // Prevent switching focus to another visual element.
 | |
|                 keyDownEvent.PreventDefault(); 
 | |
| 
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             // If Tab is pressed, complete the query with the suggested term.
 | |
|             if (keyDownEvent.keyCode == KeyCode.Tab)
 | |
|             {
 | |
|                 // Used to prevent the TAB input from executing it's default behavior. We're hijacking it for auto-completion.
 | |
|                 keyDownEvent.PreventDefault();
 | |
| 
 | |
|                 if (!string.IsNullOrEmpty(m_SuggestedTerm))
 | |
|                 {
 | |
|                     SelectAndReplaceCurrentWord();
 | |
|                     m_AutoCompleteLabel.text = string.Empty;
 | |
| 
 | |
|                     // TODO: Revisit, we shouldn't need to do this here.
 | |
|                     m_Text = m_SearchTextField.text;
 | |
| 
 | |
|                     Refresh();
 | |
| 
 | |
|                     m_SuggestedTerm = string.Empty;
 | |
|                 }
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 SetSelectedElementInResultsList(keyDownEvent);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         void SelectAndReplaceCurrentWord()
 | |
|         {
 | |
|             var s = m_SearchTextField.value;
 | |
|             var lastWordIndex = s.LastIndexOf(' ');
 | |
|             lastWordIndex++;
 | |
| 
 | |
|             var newText = s.Substring(0, lastWordIndex) + m_SuggestedTerm;
 | |
| 
 | |
|             // Wait for SelectRange api to reach trunk
 | |
| //#if UNITY_2018_3_OR_NEWER
 | |
| //            m_SearchTextField.value = newText;
 | |
| //            m_SearchTextField.SelectRange(m_SearchTextField.value.Length, m_SearchTextField.value.Length);
 | |
| //#else
 | |
|             // HACK - relies on the textfield moving the caret when being assigned a value and skipping
 | |
|             // all low surrogate characters
 | |
|             var magicMoveCursorToEndString = new string('\uDC00', newText.Length);
 | |
|             m_SearchTextField.value = magicMoveCursorToEndString;
 | |
|             m_SearchTextField.value = newText;
 | |
| 
 | |
| //#endif
 | |
|         }
 | |
| 
 | |
|         void SetSelectedElementInResultsList(KeyDownEvent keyDownEvent)
 | |
|         {
 | |
|             int index;
 | |
|             switch (keyDownEvent.keyCode)
 | |
|             {
 | |
|                 case KeyCode.Escape:
 | |
|                     OnListViewSelect(null);
 | |
|                     m_AnalyticsDataCallback?.Invoke(new Searcher.AnalyticsEvent(Searcher.AnalyticsEvent.EventType.Cancelled, m_SearchTextField.value));
 | |
|                     break;
 | |
|                 case KeyCode.Return:
 | |
|                 case KeyCode.KeypadEnter:
 | |
|                     if (m_ListView.selectedIndex != -1)
 | |
|                     {
 | |
|                         OnListViewSelect((SearcherItem)m_ListView.selectedItem);
 | |
|                         m_AnalyticsDataCallback?.Invoke(new Searcher.AnalyticsEvent(Searcher.AnalyticsEvent.EventType.Picked, m_SearchTextField.value));
 | |
|                     }
 | |
|                     else
 | |
|                     {
 | |
|                         OnListViewSelect(null);
 | |
|                         m_AnalyticsDataCallback?.Invoke(new Searcher.AnalyticsEvent(Searcher.AnalyticsEvent.EventType.Cancelled, m_SearchTextField.value));
 | |
|                     }
 | |
|                     break;
 | |
|                 case KeyCode.LeftArrow:
 | |
|                     index = m_ListView.selectedIndex;
 | |
|                     if (index >= 0 && index < m_ListView.itemsSource.Count)
 | |
|                         Collapse(m_ListView.selectedItem as SearcherItem);
 | |
|                     break;
 | |
|                 case KeyCode.RightArrow:
 | |
|                     index = m_ListView.selectedIndex;
 | |
|                     if (index >= 0 && index < m_ListView.itemsSource.Count)
 | |
|                         Expand(m_ListView.selectedItem as SearcherItem);
 | |
|                     break;
 | |
|                 
 | |
|                 // Fixes bug: https://fogbugz.unity3d.com/f/cases/1358016/
 | |
|                 case KeyCode.UpArrow:
 | |
|                 case KeyCode.PageUp:
 | |
|                     if (m_ListView.selectedIndex > 0)
 | |
|                         SetSelectedElementInResultsList(m_ListView.selectedIndex - 1);
 | |
|                     break;
 | |
| 
 | |
|                 case KeyCode.DownArrow:
 | |
|                 case KeyCode.PageDown:
 | |
|                     if (m_ListView.selectedIndex < 0)
 | |
|                         SetSelectedElementInResultsList(0); 
 | |
|                     else
 | |
|                         SetSelectedElementInResultsList(m_ListView.selectedIndex + 1);
 | |
|                     break;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         void SetSelectedElementInResultsList(int selectedIndex)
 | |
|         {
 | |
|             var newIndex = selectedIndex >= 0 && selectedIndex < m_VisibleResults.Count ? selectedIndex : -1;
 | |
|             if (newIndex < 0)
 | |
|                 return;
 | |
| 
 | |
|             m_ListView.selectedIndex = newIndex;
 | |
|             m_ListView.ScrollToItem(m_ListView.selectedIndex);
 | |
|         }
 | |
|     }
 | |
| }
 |