testss
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace UnityEditor.Searcher
|
||||
{
|
||||
[PublicAPI]
|
||||
public class Searcher
|
||||
{
|
||||
public ISearcherAdapter Adapter { get; }
|
||||
public Comparison<SearcherItem> SortComparison { get; set; }
|
||||
|
||||
readonly List<SearcherDatabaseBase> m_Databases;
|
||||
|
||||
public Searcher(SearcherDatabaseBase database, string title)
|
||||
: this(new List<SearcherDatabaseBase> { database }, title, null)
|
||||
{ }
|
||||
|
||||
public Searcher(IEnumerable<SearcherDatabaseBase> databases, string title)
|
||||
: this(databases, title, null)
|
||||
{ }
|
||||
|
||||
public Searcher(SearcherDatabaseBase database, ISearcherAdapter adapter = null)
|
||||
: this(new List<SearcherDatabaseBase> { database }, adapter)
|
||||
{ }
|
||||
|
||||
public Searcher(IEnumerable<SearcherDatabaseBase> databases, ISearcherAdapter adapter = null)
|
||||
: this(databases, string.Empty, adapter)
|
||||
{ }
|
||||
|
||||
Searcher(IEnumerable<SearcherDatabaseBase> databases, string title, ISearcherAdapter adapter)
|
||||
{
|
||||
m_Databases = new List<SearcherDatabaseBase>();
|
||||
var databaseId = 0;
|
||||
foreach (var database in databases)
|
||||
{
|
||||
// This is needed for sorting items between databases.
|
||||
database.OverwriteId(databaseId);
|
||||
databaseId++;
|
||||
|
||||
m_Databases.Add(database);
|
||||
}
|
||||
|
||||
Adapter = adapter ?? new SearcherAdapter(title);
|
||||
}
|
||||
|
||||
public void BuildIndices()
|
||||
{
|
||||
foreach (var database in m_Databases)
|
||||
{
|
||||
database.BuildIndex();
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<SearcherItem> Search(string query)
|
||||
{
|
||||
query = query.ToLower();
|
||||
|
||||
var results = new List<SearcherItem>();
|
||||
float maxScore = 0;
|
||||
foreach (var database in m_Databases)
|
||||
{
|
||||
var localResults = database.Search(query, out var localMaxScore);
|
||||
if (localMaxScore > maxScore)
|
||||
{
|
||||
// skip the highest scored item in the local results and
|
||||
// insert it back as the first item. The first item should always be
|
||||
// the highest scored item. The order of the other items does not matter
|
||||
// because they will be reordered to recreate the tree properly.
|
||||
if (results.Count > 0)
|
||||
{
|
||||
// backup previous best result
|
||||
results.Add(results[0]);
|
||||
// replace it with the new best result
|
||||
results[0] = localResults[0];
|
||||
// add remaining results at the end
|
||||
results.AddRange(localResults.Skip(1));
|
||||
}
|
||||
else // best result will be the first item
|
||||
results.AddRange(localResults);
|
||||
|
||||
maxScore = localMaxScore;
|
||||
}
|
||||
else // no new best result just append everything
|
||||
{
|
||||
results.AddRange(localResults);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public class AnalyticsEvent
|
||||
{
|
||||
[PublicAPI]
|
||||
public enum EventType{ Pending, Picked, Cancelled }
|
||||
public readonly EventType eventType;
|
||||
public readonly string currentSearchFieldText;
|
||||
public AnalyticsEvent(EventType eventType, string currentSearchFieldText)
|
||||
{
|
||||
this.eventType = eventType;
|
||||
this.currentSearchFieldText = currentSearchFieldText;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEditor.Searcher
|
||||
{
|
||||
public enum ItemExpanderState
|
||||
{
|
||||
Hidden,
|
||||
Collapsed,
|
||||
Expanded
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public interface ISearcherAdapter
|
||||
{
|
||||
VisualElement MakeItem();
|
||||
VisualElement Bind(VisualElement target, SearcherItem item, ItemExpanderState expanderState, string text);
|
||||
|
||||
string Title { get; }
|
||||
bool HasDetailsPanel { get; }
|
||||
bool AddAllChildResults { get; }
|
||||
float InitialSplitterDetailRatio { get; }
|
||||
void OnSelectionChanged(IEnumerable<SearcherItem> items);
|
||||
void InitDetailsPanel(VisualElement detailsPanel);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public class SearcherAdapter : ISearcherAdapter
|
||||
{
|
||||
const string k_EntryName = "smartSearchItem";
|
||||
const int k_IndentDepthFactor = 15;
|
||||
|
||||
readonly VisualTreeAsset m_DefaultItemTemplate;
|
||||
public virtual string Title { get; }
|
||||
public virtual bool HasDetailsPanel => true;
|
||||
public virtual bool AddAllChildResults => true;
|
||||
|
||||
Label m_DetailsLabel;
|
||||
public virtual float InitialSplitterDetailRatio => 1.0f;
|
||||
|
||||
public SearcherAdapter(string title)
|
||||
{
|
||||
Title = title;
|
||||
m_DefaultItemTemplate = Resources.Load<VisualTreeAsset>("SearcherItem");
|
||||
}
|
||||
|
||||
public virtual VisualElement MakeItem()
|
||||
{
|
||||
// Create a visual element hierarchy for this search result.
|
||||
var item = m_DefaultItemTemplate.CloneTree();
|
||||
return item;
|
||||
}
|
||||
|
||||
public virtual VisualElement Bind(VisualElement element, SearcherItem item, ItemExpanderState expanderState, string query)
|
||||
{
|
||||
var indent = element.Q<VisualElement>("itemIndent");
|
||||
indent.style.width = item.Depth * k_IndentDepthFactor;
|
||||
|
||||
var expander = element.Q<VisualElement>("itemChildExpander");
|
||||
|
||||
var icon = expander.Query("expanderIcon").First();
|
||||
icon.ClearClassList();
|
||||
|
||||
switch (expanderState)
|
||||
{
|
||||
case ItemExpanderState.Expanded:
|
||||
icon.AddToClassList("Expanded");
|
||||
break;
|
||||
|
||||
case ItemExpanderState.Collapsed:
|
||||
icon.AddToClassList("Collapsed");
|
||||
break;
|
||||
}
|
||||
|
||||
var nameLabelsContainer = element.Q<VisualElement>("labelsContainer");
|
||||
nameLabelsContainer.Clear();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
nameLabelsContainer.Add(new Label(item.Name));
|
||||
else
|
||||
SearcherHighlighter.HighlightTextBasedOnQuery(nameLabelsContainer, item.Name, query);
|
||||
|
||||
element.userData = item;
|
||||
element.name = k_EntryName;
|
||||
|
||||
return expander;
|
||||
}
|
||||
|
||||
public virtual void InitDetailsPanel(VisualElement detailsPanel)
|
||||
{
|
||||
m_DetailsLabel = new Label();
|
||||
detailsPanel.Add(m_DetailsLabel);
|
||||
}
|
||||
|
||||
public virtual void OnSelectionChanged(IEnumerable<SearcherItem> items)
|
||||
{
|
||||
if (m_DetailsLabel != null)
|
||||
{
|
||||
var itemsList = items.ToList();
|
||||
m_DetailsLabel.text = itemsList.Any() ? itemsList[0].Help : "No results";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,679 @@
|
||||
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;
|
||||
Searcher m_Searcher;
|
||||
string m_SuggestedTerm;
|
||||
string m_Text = string.Empty;
|
||||
Action<SearcherItem> m_SelectionCallback;
|
||||
Action<Searcher.AnalyticsEvent> m_AnalyticsDataCallback;
|
||||
ListView m_ListView;
|
||||
TextField m_SearchTextField;
|
||||
VisualElement m_SearchTextInput;
|
||||
VisualElement m_DetailsPanel;
|
||||
VisualElement m_SearcherPanel;
|
||||
|
||||
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_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 => m_SelectionCallback((SearcherItem)obj.FirstOrDefault());
|
||||
m_ListView.onSelectionChange += selectedItems => m_Searcher.Adapter.OnSelectionChanged(selectedItems.OfType<SearcherItem>().ToList());
|
||||
#else
|
||||
m_ListView.onItemChosen += obj => m_SelectionCallback((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);
|
||||
|
||||
RegisterCallback<AttachToPanelEvent>(OnEnterPanel);
|
||||
RegisterCallback<DetachFromPanelEvent>(OnLeavePanel);
|
||||
|
||||
// TODO: HACK - ListView's scroll view steals focus using the scheduler.
|
||||
EditorApplication.update += HackDueToListViewScrollViewStealingFocus;
|
||||
|
||||
style.flexGrow = 1;
|
||||
}
|
||||
|
||||
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 CancelSearch()
|
||||
{
|
||||
OnSearchTextFieldTextChanged(InputEvent.GetPooled(m_Text, string.Empty));
|
||||
m_SelectionCallback(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)
|
||||
{
|
||||
m_Searcher = searcher;
|
||||
m_SelectionCallback = selectionCallback;
|
||||
m_AnalyticsDataCallback = analyticsDataCallback;
|
||||
|
||||
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())
|
||||
{
|
||||
var scrollToItem = results.First();
|
||||
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 = m_Searcher.Adapter.MakeItem;
|
||||
m_ListView.Refresh();
|
||||
|
||||
SetSelectedElementInResultsList(visibleIndex);
|
||||
}
|
||||
|
||||
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 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);
|
||||
expander.RegisterCallback<MouseDownEvent>(ExpandOrCollapse);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
m_ListView.Refresh();
|
||||
}
|
||||
|
||||
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
|
||||
m_ListView.Refresh();
|
||||
|
||||
// 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:
|
||||
m_SelectionCallback(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)
|
||||
{
|
||||
m_SelectionCallback((SearcherItem)m_ListView.selectedItem);
|
||||
m_AnalyticsDataCallback?.Invoke(new Searcher.AnalyticsEvent(Searcher.AnalyticsEvent.EventType.Picked, m_SearchTextField.value));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_SelectionCallback(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;
|
||||
case KeyCode.UpArrow:
|
||||
case KeyCode.DownArrow:
|
||||
case KeyCode.PageUp:
|
||||
case KeyCode.PageDown:
|
||||
index = m_ListView.selectedIndex;
|
||||
if (index >= 0 && index < m_ListView.itemsSource.Count)
|
||||
m_ListView.OnKeyDown(keyDownEvent);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,393 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.Searcher
|
||||
{
|
||||
[PublicAPI]
|
||||
public class SearcherDatabase : SearcherDatabaseBase
|
||||
{
|
||||
Dictionary<string, IReadOnlyList<ValueTuple<string, float>>> m_Index = new Dictionary<string, IReadOnlyList<ValueTuple<string, float>>>();
|
||||
|
||||
class Result
|
||||
{
|
||||
public SearcherItem item;
|
||||
public float maxScore;
|
||||
}
|
||||
|
||||
const bool k_IsParallel = true;
|
||||
|
||||
public Func<string, SearcherItem, bool> MatchFilter { get; set; }
|
||||
|
||||
public static SearcherDatabase Create(
|
||||
List<SearcherItem> items,
|
||||
string databaseDirectory,
|
||||
bool serializeToFile = true
|
||||
)
|
||||
{
|
||||
if (serializeToFile && databaseDirectory != null && !Directory.Exists(databaseDirectory))
|
||||
Directory.CreateDirectory(databaseDirectory);
|
||||
|
||||
var database = new SearcherDatabase(databaseDirectory, items);
|
||||
|
||||
if (serializeToFile)
|
||||
database.SerializeToFile();
|
||||
|
||||
database.BuildIndex();
|
||||
return database;
|
||||
}
|
||||
|
||||
public static SearcherDatabase Load(string databaseDirectory)
|
||||
{
|
||||
if (!Directory.Exists(databaseDirectory))
|
||||
throw new InvalidOperationException("databaseDirectory not found.");
|
||||
|
||||
var database = new SearcherDatabase(databaseDirectory, null);
|
||||
database.LoadFromFile();
|
||||
database.BuildIndex();
|
||||
|
||||
return database;
|
||||
}
|
||||
|
||||
public SearcherDatabase(IReadOnlyCollection<SearcherItem> db)
|
||||
: this("", db)
|
||||
{
|
||||
}
|
||||
|
||||
SearcherDatabase(string databaseDirectory, IReadOnlyCollection<SearcherItem> db)
|
||||
: base(databaseDirectory)
|
||||
{
|
||||
m_ItemList = new List<SearcherItem>();
|
||||
var nextId = 0;
|
||||
|
||||
if (db != null)
|
||||
foreach (var item in db)
|
||||
AddItemToIndex(item, ref nextId, null);
|
||||
}
|
||||
|
||||
public override List<SearcherItem> Search(string query, out float localMaxScore)
|
||||
{
|
||||
// Match assumes the query is trimmed
|
||||
query = query.Trim(' ', '\t');
|
||||
localMaxScore = 0;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
if (MatchFilter == null)
|
||||
return m_ItemList;
|
||||
|
||||
// ReSharper disable once RedundantLogicalConditionalExpressionOperand
|
||||
if (k_IsParallel && m_ItemList.Count > 100)
|
||||
return FilterMultiThreaded(query);
|
||||
|
||||
return FilterSingleThreaded(query);
|
||||
}
|
||||
|
||||
var finalResults = new List<SearcherItem> { null };
|
||||
var max = new Result();
|
||||
var tokenizedQuery = new List<string>();
|
||||
foreach (var token in Tokenize(query))
|
||||
{
|
||||
tokenizedQuery.Add(token.Trim().ToLower());
|
||||
}
|
||||
|
||||
// ReSharper disable once RedundantLogicalConditionalExpressionOperand
|
||||
if (k_IsParallel && m_ItemList.Count > 100)
|
||||
SearchMultithreaded(query, tokenizedQuery, max, finalResults);
|
||||
else
|
||||
SearchSingleThreaded(query, tokenizedQuery, max, finalResults);
|
||||
|
||||
localMaxScore = max.maxScore;
|
||||
if (max.item != null)
|
||||
finalResults[0] = max.item;
|
||||
else
|
||||
finalResults.RemoveAt(0);
|
||||
|
||||
return finalResults;
|
||||
}
|
||||
|
||||
protected virtual bool Match(string query, IReadOnlyList<string> tokenizedQuery, SearcherItem item, out float score)
|
||||
{
|
||||
var filter = MatchFilter?.Invoke(query, item) ?? true;
|
||||
return Match(tokenizedQuery, item.Path, out score) && filter;
|
||||
}
|
||||
|
||||
List<SearcherItem> FilterSingleThreaded(string query)
|
||||
{
|
||||
var result = new List<SearcherItem>();
|
||||
|
||||
foreach (var searcherItem in m_ItemList)
|
||||
{
|
||||
if (!MatchFilter.Invoke(query, searcherItem))
|
||||
continue;
|
||||
|
||||
result.Add(searcherItem);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
List<SearcherItem> FilterMultiThreaded(string query)
|
||||
{
|
||||
var result = new List<SearcherItem>();
|
||||
var count = Environment.ProcessorCount;
|
||||
var tasks = new Task[count];
|
||||
var lists = new List<SearcherItem>[count];
|
||||
var itemsPerTask = (int)Math.Ceiling(m_ItemList.Count / (float)count);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var i1 = i;
|
||||
tasks[i] = Task.Run(() =>
|
||||
{
|
||||
lists[i1] = new List<SearcherItem>();
|
||||
|
||||
for (var j = 0; j < itemsPerTask; j++)
|
||||
{
|
||||
var index = j + itemsPerTask * i1;
|
||||
if (index >= m_ItemList.Count)
|
||||
break;
|
||||
|
||||
var item = m_ItemList[index];
|
||||
if (!MatchFilter.Invoke(query, item))
|
||||
continue;
|
||||
|
||||
lists[i1].Add(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Task.WaitAll(tasks);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
result.AddRange(lists[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
readonly float k_ScoreCutOff = 0.33f;
|
||||
|
||||
void SearchSingleThreaded(string query, IReadOnlyList<string> tokenizedQuery, Result max, ICollection<SearcherItem> finalResults)
|
||||
{
|
||||
List<Result> results = new List<Result>();
|
||||
|
||||
foreach (var item in m_ItemList)
|
||||
{
|
||||
float score = 0;
|
||||
if (query.Length == 0 || Match(query, tokenizedQuery, item, out score))
|
||||
{
|
||||
if (score > max.maxScore)
|
||||
{
|
||||
max.item = item;
|
||||
max.maxScore = score;
|
||||
}
|
||||
results.Add(new Result() { item = item, maxScore = score});
|
||||
}
|
||||
}
|
||||
|
||||
PostprocessResults(results, finalResults, max);
|
||||
}
|
||||
|
||||
void SearchMultithreaded(string query, IReadOnlyList<string> tokenizedQuery, Result max, List<SearcherItem> finalResults)
|
||||
{
|
||||
var count = Environment.ProcessorCount;
|
||||
var tasks = new Task[count];
|
||||
var localResults = new Result[count];
|
||||
var queue = new ConcurrentQueue<Result>();
|
||||
var itemsPerTask = (int)Math.Ceiling(m_ItemList.Count / (float)count);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var i1 = i;
|
||||
localResults[i1] = new Result();
|
||||
tasks[i] = Task.Run(() =>
|
||||
{
|
||||
var result = localResults[i1];
|
||||
for (var j = 0; j < itemsPerTask; j++)
|
||||
{
|
||||
var index = j + itemsPerTask * i1;
|
||||
if (index >= m_ItemList.Count)
|
||||
break;
|
||||
var item = m_ItemList[index];
|
||||
float score = 0;
|
||||
if (query.Length == 0 || Match(query, tokenizedQuery, item, out score))
|
||||
{
|
||||
if (score > result.maxScore)
|
||||
{
|
||||
result.maxScore = score;
|
||||
result.item = item;
|
||||
}
|
||||
|
||||
queue.Enqueue(new Result { item = item, maxScore = score });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Task.WaitAll(tasks);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (localResults[i].maxScore > max.maxScore)
|
||||
{
|
||||
max.maxScore = localResults[i].maxScore;
|
||||
max.item = localResults[i].item;
|
||||
}
|
||||
}
|
||||
|
||||
PostprocessResults(queue, finalResults, max);
|
||||
}
|
||||
|
||||
void PostprocessResults(IEnumerable<Result> results, ICollection<SearcherItem> items, Result max)
|
||||
{
|
||||
foreach (var result in results)
|
||||
{
|
||||
var normalizedScore = result.maxScore / max.maxScore;
|
||||
if (result.item != null && result.item != max.item && normalizedScore > k_ScoreCutOff)
|
||||
{
|
||||
items.Add(result.item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void BuildIndex()
|
||||
{
|
||||
m_Index.Clear();
|
||||
|
||||
foreach (var item in m_ItemList)
|
||||
{
|
||||
if (!m_Index.ContainsKey(item.Path))
|
||||
{
|
||||
List<ValueTuple<string, float>> terms = new List<ValueTuple<string, float>>();
|
||||
|
||||
// If the item uses synonyms to return results for similar words/phrases, add them to the search terms
|
||||
IList<string> tokens = null;
|
||||
if (item.Synonyms == null)
|
||||
tokens = Tokenize(item.Name);
|
||||
else
|
||||
tokens = Tokenize(string.Format("{0} {1}", item.Name, string.Join(" ", item.Synonyms)));
|
||||
|
||||
string tokenSuite = "";
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
var t = token.ToLower();
|
||||
if (t.Length > 1)
|
||||
{
|
||||
terms.Add(new ValueTuple<string, float>(t, 0.8f));
|
||||
}
|
||||
|
||||
if (tokenSuite.Length > 0)
|
||||
{
|
||||
tokenSuite += " " + t;
|
||||
terms.Add(new ValueTuple<string, float>(tokenSuite, 1f));
|
||||
}
|
||||
else
|
||||
{
|
||||
tokenSuite = t;
|
||||
}
|
||||
}
|
||||
|
||||
// Add a term containing all the uppercase letters (CamelCase World BBox => CCWBB)
|
||||
var initialList = Regex.Split(item.Name, @"\P{Lu}+");
|
||||
var initials = string.Concat(initialList).Trim();
|
||||
if (!string.IsNullOrEmpty(initials))
|
||||
terms.Add(new ValueTuple<string, float>(initials.ToLower(), 0.5f));
|
||||
|
||||
m_Index.Add(item.Path, terms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static IList<string> Tokenize(string s)
|
||||
{
|
||||
var knownTokens = new HashSet<string>();
|
||||
var tokens = new List<string>();
|
||||
|
||||
// Split on word boundaries
|
||||
foreach (var t in Regex.Split(s, @"\W"))
|
||||
{
|
||||
// Split camel case words
|
||||
var tt = Regex.Split(t, @"(\p{Lu}+\P{Lu}*)");
|
||||
foreach (var ttt in tt)
|
||||
{
|
||||
var tttt = ttt.Trim();
|
||||
if (!string.IsNullOrEmpty(tttt) && !knownTokens.Contains(tttt))
|
||||
{
|
||||
knownTokens.Add(tttt);
|
||||
tokens.Add(tttt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
bool Match(IReadOnlyList<string> tokenizedQuery, string itemPath, out float score)
|
||||
{
|
||||
itemPath = itemPath.Trim();
|
||||
if (itemPath == "")
|
||||
{
|
||||
if (tokenizedQuery.Count == 0)
|
||||
{
|
||||
score = 1;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
score = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
IReadOnlyList<ValueTuple<string, float>> indexTerms;
|
||||
if (!m_Index.TryGetValue(itemPath, out indexTerms))
|
||||
{
|
||||
score = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
float maxScore = 0.0f;
|
||||
foreach (var t in indexTerms)
|
||||
{
|
||||
float scoreForTerm = 0f;
|
||||
var querySuite = "";
|
||||
var querySuiteFactor = 1.25f;
|
||||
foreach (var q in tokenizedQuery)
|
||||
{
|
||||
if (t.Item1.StartsWith(q))
|
||||
{
|
||||
scoreForTerm += t.Item2 * q.Length / t.Item1.Length;
|
||||
}
|
||||
|
||||
if (querySuite.Length > 0)
|
||||
{
|
||||
querySuite += " " + q;
|
||||
if (t.Item1.StartsWith(querySuite))
|
||||
{
|
||||
scoreForTerm += t.Item2 * querySuiteFactor * querySuite.Length / t.Item1.Length;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
querySuite = q;
|
||||
}
|
||||
|
||||
querySuiteFactor *= querySuiteFactor;
|
||||
}
|
||||
|
||||
maxScore = Mathf.Max(maxScore, scoreForTerm);
|
||||
}
|
||||
|
||||
score = maxScore;
|
||||
return score > 0;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using JetBrains.Annotations;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.Searcher
|
||||
{
|
||||
[PublicAPI]
|
||||
public abstract class SearcherDatabaseBase
|
||||
{
|
||||
protected const string k_SerializedJsonFile = "/SerializedDatabase.json";
|
||||
public string DatabaseDirectory { get; set; }
|
||||
|
||||
public IList<SearcherItem> ItemList => m_ItemList;
|
||||
|
||||
// ReSharper disable once Unity.RedundantSerializeFieldAttribute
|
||||
[SerializeField]
|
||||
protected List<SearcherItem> m_ItemList;
|
||||
|
||||
protected SearcherDatabaseBase(string databaseDirectory)
|
||||
{
|
||||
DatabaseDirectory = databaseDirectory;
|
||||
}
|
||||
|
||||
public virtual void BuildIndex() { }
|
||||
|
||||
public abstract List<SearcherItem> Search(string query, out float localMaxScore);
|
||||
|
||||
internal void OverwriteId(int newId)
|
||||
{
|
||||
Id = newId;
|
||||
}
|
||||
|
||||
internal int Id { get; private set; }
|
||||
|
||||
protected void LoadFromFile()
|
||||
{
|
||||
var reader = new StreamReader(DatabaseDirectory + k_SerializedJsonFile);
|
||||
var serializedData = reader.ReadToEnd();
|
||||
reader.Close();
|
||||
|
||||
EditorJsonUtility.FromJsonOverwrite(serializedData, this);
|
||||
|
||||
foreach (var item in m_ItemList)
|
||||
{
|
||||
item.OverwriteDatabase(this);
|
||||
item.ReInitAfterLoadFromFile();
|
||||
}
|
||||
}
|
||||
|
||||
protected void SerializeToFile()
|
||||
{
|
||||
if (DatabaseDirectory == null)
|
||||
return;
|
||||
var serializedData = EditorJsonUtility.ToJson(this, true);
|
||||
var writer = new StreamWriter(DatabaseDirectory + k_SerializedJsonFile, false);
|
||||
writer.Write(serializedData);
|
||||
writer.Close();
|
||||
}
|
||||
|
||||
protected void AddItemToIndex(SearcherItem item, ref int lastId, Action<SearcherItem> action)
|
||||
{
|
||||
m_ItemList.Insert(lastId, item);
|
||||
|
||||
// We can only set the id here as we only know the final index of the item here.
|
||||
item.OverwriteId(lastId);
|
||||
item.GeneratePath();
|
||||
|
||||
action?.Invoke(item);
|
||||
|
||||
lastId++;
|
||||
|
||||
// This is used for sorting results between databases.
|
||||
item.OverwriteDatabase(this);
|
||||
|
||||
if (!item.HasChildren)
|
||||
return;
|
||||
|
||||
var childrenIds = new List<int>();
|
||||
foreach (SearcherItem child in item.Children)
|
||||
{
|
||||
AddItemToIndex(child, ref lastId, action);
|
||||
childrenIds.Add(child.Id);
|
||||
}
|
||||
|
||||
item.OverwriteChildrenIds(childrenIds);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEditor.Searcher
|
||||
{
|
||||
static class SearcherHighlighter
|
||||
{
|
||||
const char k_StartHighlightSeparator = '{';
|
||||
const char k_EndHighlightSeparator = '}';
|
||||
const string k_HighlightedStyleClassName = "Highlighted";
|
||||
|
||||
public static void HighlightTextBasedOnQuery(VisualElement container, string text, string query)
|
||||
{
|
||||
var formattedText = text;
|
||||
var queryParts = query.Split(new[] {" "}, StringSplitOptions.RemoveEmptyEntries);
|
||||
var regex = string.Empty;
|
||||
for (var index = 0; index < queryParts.Length; index++)
|
||||
{
|
||||
var queryPart = queryParts[index];
|
||||
regex += $"({queryPart})";
|
||||
if (index < queryParts.Length - 1)
|
||||
regex += "|";
|
||||
}
|
||||
|
||||
var matches = Regex.Matches(formattedText, regex, RegexOptions.IgnoreCase);
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
formattedText = formattedText.Replace(match.Value,
|
||||
$"{k_StartHighlightSeparator}{match.Value}{k_EndHighlightSeparator}");
|
||||
}
|
||||
|
||||
BuildHighlightLabels(container, formattedText);
|
||||
}
|
||||
|
||||
static void BuildHighlightLabels(VisualElement container, string formattedHighlightText)
|
||||
{
|
||||
if (string.IsNullOrEmpty(formattedHighlightText))
|
||||
return;
|
||||
|
||||
var substring = string.Empty;
|
||||
var highlighting = false;
|
||||
var skipCount = 0;
|
||||
foreach (var character in formattedHighlightText.ToCharArray())
|
||||
{
|
||||
switch (character)
|
||||
{
|
||||
// Skip embedded separators
|
||||
// Ex:
|
||||
// Query: middle e
|
||||
// Text: Middle Eastern
|
||||
// Formatted Text: {Middl{e}} {E}ast{e}rn
|
||||
// ^ ^
|
||||
case k_StartHighlightSeparator when highlighting:
|
||||
skipCount++;
|
||||
continue;
|
||||
case k_StartHighlightSeparator: {
|
||||
highlighting = true;
|
||||
if (!string.IsNullOrEmpty(substring))
|
||||
{
|
||||
container.Add(new Label(substring));
|
||||
substring = string.Empty;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
case k_EndHighlightSeparator when skipCount > 0:
|
||||
skipCount--;
|
||||
continue;
|
||||
case k_EndHighlightSeparator: {
|
||||
var label = new Label(substring);
|
||||
label.AddToClassList(k_HighlightedStyleClassName);
|
||||
container.Add(label);
|
||||
|
||||
highlighting = false;
|
||||
substring = string.Empty;
|
||||
|
||||
continue;
|
||||
}
|
||||
default:
|
||||
substring += character;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(substring))
|
||||
{
|
||||
var label = new Label(substring);
|
||||
if (highlighting)
|
||||
label.AddToClassList(k_HighlightedStyleClassName);
|
||||
container.Add(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.Searcher
|
||||
{
|
||||
[PublicAPI]
|
||||
[Serializable]
|
||||
public class SearcherItem
|
||||
{
|
||||
[SerializeField] int m_Id;
|
||||
[SerializeField] List<int> m_ChildrenIds;
|
||||
[SerializeField] string m_Name;
|
||||
[SerializeField] string m_Help;
|
||||
[SerializeField] string[] m_Synonyms;
|
||||
|
||||
public int Id => m_Id;
|
||||
|
||||
public virtual string Name => m_Name;
|
||||
|
||||
public string Path { get; private set; }
|
||||
|
||||
public string Help => m_Help;
|
||||
|
||||
public string[] Synonyms { get { return m_Synonyms; } set { m_Synonyms = value; } }
|
||||
|
||||
public int Depth => Parent?.Depth + 1 ?? 0;
|
||||
|
||||
public SearcherItem Parent { get; private set; }
|
||||
public SearcherDatabaseBase Database { get; private set; }
|
||||
public List<SearcherItem> Children { get; private set; }
|
||||
public bool HasChildren => Children.Count > 0;
|
||||
|
||||
public SearcherItem(string name, string help = "", List<SearcherItem> children = null)
|
||||
{
|
||||
m_Id = -1;
|
||||
Parent = null;
|
||||
Database = null;
|
||||
|
||||
m_Name = name;
|
||||
m_Help = help;
|
||||
|
||||
Children = new List<SearcherItem>();
|
||||
if (children == null)
|
||||
return;
|
||||
|
||||
Children = children;
|
||||
foreach (var child in children)
|
||||
child.OverwriteParent(this);
|
||||
}
|
||||
|
||||
public void AddChild(SearcherItem child)
|
||||
{
|
||||
if (child == null)
|
||||
throw new ArgumentNullException(nameof(child));
|
||||
|
||||
if (Database != null)
|
||||
throw new InvalidOperationException(
|
||||
"Cannot add more children to an item that was already used in a database.");
|
||||
|
||||
if (Children == null)
|
||||
Children = new List<SearcherItem>();
|
||||
|
||||
Children.Add(child);
|
||||
child.OverwriteParent(this);
|
||||
}
|
||||
|
||||
internal void OverwriteId(int newId)
|
||||
{
|
||||
m_Id = newId;
|
||||
}
|
||||
|
||||
void OverwriteParent(SearcherItem newParent)
|
||||
{
|
||||
Parent = newParent;
|
||||
}
|
||||
|
||||
internal void OverwriteDatabase(SearcherDatabaseBase newDatabase)
|
||||
{
|
||||
Database = newDatabase;
|
||||
}
|
||||
|
||||
internal void OverwriteChildrenIds(List<int> childrenIds)
|
||||
{
|
||||
m_ChildrenIds = childrenIds;
|
||||
}
|
||||
|
||||
internal void GeneratePath()
|
||||
{
|
||||
if (Parent != null)
|
||||
Path = Parent.Path + " ";
|
||||
else
|
||||
Path = string.Empty;
|
||||
Path += Name;
|
||||
}
|
||||
|
||||
internal void ReInitAfterLoadFromFile()
|
||||
{
|
||||
if (Children == null)
|
||||
Children = new List<SearcherItem>();
|
||||
|
||||
foreach (var id in m_ChildrenIds)
|
||||
{
|
||||
var child = Database.ItemList[id];
|
||||
Children.Add(child);
|
||||
child.OverwriteParent(this);
|
||||
}
|
||||
|
||||
GeneratePath();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{nameof(Id)}: {Id}, {nameof(Name)}: {Name}, {nameof(Depth)}: {Depth}";
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,428 @@
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user