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);
|
||
|
|
||
|
m_SearchTextInput = m_SearchTextField.Q(TextInputBaseField<string>.textInputUssName);
|
||
|
m_SearchTextInput.RegisterCallback<KeyDownEvent>(OnSearchTextFieldKeyDown);
|
||
|
}
|
||
|
|
||
|
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);
|
||
|
}
|
||
|
}
|
||
|
}
|