暫無描述
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SearcherControl.cs 29KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using UnityEngine;
  5. using UnityEngine.UIElements;
  6. using UnityEditor.UIElements;
  7. namespace UnityEditor.Searcher
  8. {
  9. class SearcherControl : VisualElement
  10. {
  11. // Window constants.
  12. const string k_WindowTitleLabel = "windowTitleLabel";
  13. const string k_WindowDetailsPanel = "windowDetailsVisualContainer";
  14. const string k_WindowResultsScrollViewName = "windowResultsScrollView";
  15. const string k_WindowSearchTextFieldName = "searchBox";
  16. const string k_WindowAutoCompleteLabelName = "autoCompleteLabel";
  17. const string k_WindowSearchIconName = "searchIcon";
  18. const string k_WindowResizerName = "windowResizer";
  19. const string kWindowSearcherPanel = "searcherVisualContainer";
  20. const int k_TabCharacter = 9;
  21. Label m_AutoCompleteLabel;
  22. IEnumerable<SearcherItem> m_Results;
  23. List<SearcherItem> m_VisibleResults;
  24. HashSet<SearcherItem> m_ExpandedResults;
  25. HashSet<SearcherItem> m_MultiSelectSelection;
  26. Dictionary<SearcherItem, Toggle> m_SearchItemToVisualToggle;
  27. Searcher m_Searcher;
  28. string m_SuggestedTerm;
  29. string m_Text = string.Empty;
  30. Action<SearcherItem> m_SelectionCallback;
  31. Action<Searcher.AnalyticsEvent> m_AnalyticsDataCallback;
  32. Func<IEnumerable<SearcherItem>, string, SearcherItem> m_SearchResultsFilterCallback;
  33. ListView m_ListView;
  34. TextField m_SearchTextField;
  35. VisualElement m_SearchTextInput;
  36. VisualElement m_DetailsPanel;
  37. VisualElement m_SearcherPanel;
  38. VisualElement m_ContentContainer;
  39. Button m_ConfirmButton;
  40. internal Label TitleLabel { get; }
  41. internal VisualElement Resizer { get; }
  42. public SearcherControl()
  43. {
  44. // Load window template.
  45. var windowUxmlTemplate = Resources.Load<VisualTreeAsset>("SearcherWindow");
  46. // Clone Window Template.
  47. var windowRootVisualElement = windowUxmlTemplate.CloneTree();
  48. windowRootVisualElement.AddToClassList("content");
  49. windowRootVisualElement.StretchToParentSize();
  50. // Add Window VisualElement to window's RootVisualContainer
  51. Add(windowRootVisualElement);
  52. m_VisibleResults = new List<SearcherItem>();
  53. m_ExpandedResults = new HashSet<SearcherItem>();
  54. m_MultiSelectSelection = new HashSet<SearcherItem>();
  55. m_SearchItemToVisualToggle = new Dictionary<SearcherItem, Toggle>();
  56. m_ListView = this.Q<ListView>(k_WindowResultsScrollViewName);
  57. if (m_ListView != null)
  58. {
  59. m_ListView.bindItem = Bind;
  60. m_ListView.RegisterCallback<KeyDownEvent>(SetSelectedElementInResultsList);
  61. #if UNITY_2020_1_OR_NEWER
  62. m_ListView.onItemsChosen += obj => OnListViewSelect((SearcherItem)obj.FirstOrDefault());
  63. m_ListView.onSelectionChange += selectedItems => m_Searcher.Adapter.OnSelectionChanged(selectedItems.OfType<SearcherItem>().ToList());
  64. #else
  65. m_ListView.onItemChosen += obj => OnListViewSelect((SearcherItem)obj);
  66. m_ListView.onSelectionChanged += selectedItems => m_Searcher.Adapter.OnSelectionChanged(selectedItems.OfType<SearcherItem>());
  67. #endif
  68. m_ListView.focusable = true;
  69. m_ListView.tabIndex = 1;
  70. }
  71. m_DetailsPanel = this.Q(k_WindowDetailsPanel);
  72. TitleLabel = this.Q<Label>(k_WindowTitleLabel);
  73. m_SearcherPanel = this.Q(kWindowSearcherPanel);
  74. m_SearchTextField = this.Q<TextField>(k_WindowSearchTextFieldName);
  75. if (m_SearchTextField != null)
  76. {
  77. m_SearchTextField.focusable = true;
  78. m_SearchTextField.RegisterCallback<InputEvent>(OnSearchTextFieldTextChanged, TrickleDown.TrickleDown);
  79. m_SearchTextInput = m_SearchTextField.Q(TextInputBaseField<string>.textInputUssName);
  80. m_SearchTextInput.RegisterCallback<KeyDownEvent>(OnSearchTextFieldKeyDown, TrickleDown.TrickleDown);
  81. }
  82. m_AutoCompleteLabel = this.Q<Label>(k_WindowAutoCompleteLabelName);
  83. Resizer = this.Q(k_WindowResizerName);
  84. m_ContentContainer = this.Q("unity-content-container");
  85. m_ConfirmButton = this.Q<Button>("confirmButton");
  86. #if UNITY_2019_3_OR_NEWER
  87. m_ConfirmButton.clicked += OnConfirmMultiselect;
  88. #else
  89. m_ConfirmButton.clickable.clicked += OnConfirmMultiselect;
  90. #endif
  91. RegisterCallback<AttachToPanelEvent>(OnEnterPanel);
  92. RegisterCallback<DetachFromPanelEvent>(OnLeavePanel);
  93. // TODO: HACK - ListView's scroll view steals focus using the scheduler.
  94. EditorApplication.update += HackDueToListViewScrollViewStealingFocus;
  95. style.flexGrow = 1;
  96. }
  97. void OnConfirmMultiselect()
  98. {
  99. if (m_MultiSelectSelection.Count == 0)
  100. {
  101. m_SelectionCallback(null);
  102. return;
  103. }
  104. foreach (SearcherItem item in m_MultiSelectSelection)
  105. {
  106. m_SelectionCallback(item);
  107. }
  108. }
  109. void HackDueToListViewScrollViewStealingFocus()
  110. {
  111. m_SearchTextInput?.Focus();
  112. // ReSharper disable once DelegateSubtraction
  113. EditorApplication.update -= HackDueToListViewScrollViewStealingFocus;
  114. }
  115. void OnEnterPanel(AttachToPanelEvent e)
  116. {
  117. RegisterCallback<KeyDownEvent>(OnKeyDown);
  118. }
  119. void OnLeavePanel(DetachFromPanelEvent e)
  120. {
  121. UnregisterCallback<KeyDownEvent>(OnKeyDown);
  122. }
  123. void OnKeyDown(KeyDownEvent e)
  124. {
  125. if (e.keyCode == KeyCode.Escape)
  126. {
  127. CancelSearch();
  128. }
  129. }
  130. void OnListViewSelect(SearcherItem item)
  131. {
  132. if (!m_Searcher.Adapter.MultiSelectEnabled)
  133. {
  134. m_SelectionCallback(item);
  135. }
  136. else
  137. {
  138. ToggleItemForMultiSelect(item, !m_MultiSelectSelection.Contains(item));
  139. }
  140. }
  141. void CancelSearch()
  142. {
  143. OnSearchTextFieldTextChanged(InputEvent.GetPooled(m_Text, string.Empty));
  144. OnListViewSelect(null);
  145. m_AnalyticsDataCallback?.Invoke(new Searcher.AnalyticsEvent(Searcher.AnalyticsEvent.EventType.Cancelled, m_SearchTextField.value));
  146. }
  147. public void Setup(Searcher searcher, Action<SearcherItem> selectionCallback, Action<Searcher.AnalyticsEvent> analyticsDataCallback, Func<IEnumerable<SearcherItem>, string, SearcherItem> searchResultsFilterCallback)
  148. {
  149. m_Searcher = searcher;
  150. m_SelectionCallback = selectionCallback;
  151. m_AnalyticsDataCallback = analyticsDataCallback;
  152. m_SearchResultsFilterCallback = searchResultsFilterCallback;
  153. if (m_Searcher.Adapter.MultiSelectEnabled) {
  154. AddToClassList("searcher__multiselect");
  155. }
  156. if (m_Searcher.Adapter.HasDetailsPanel)
  157. {
  158. m_Searcher.Adapter.InitDetailsPanel(m_DetailsPanel);
  159. m_DetailsPanel.RemoveFromClassList("hidden");
  160. m_DetailsPanel.style.flexGrow = m_Searcher.Adapter.InitialSplitterDetailRatio;
  161. m_SearcherPanel.style.flexGrow = 1;
  162. }
  163. else
  164. {
  165. m_DetailsPanel.AddToClassList("hidden");
  166. var splitter = m_DetailsPanel.parent;
  167. splitter.parent.Insert(0,m_SearcherPanel);
  168. splitter.parent.Insert(1, m_DetailsPanel);
  169. splitter.RemoveFromHierarchy();
  170. }
  171. TitleLabel.text = m_Searcher.Adapter.Title;
  172. if (string.IsNullOrEmpty(TitleLabel.text))
  173. {
  174. TitleLabel.parent.style.visibility = Visibility.Hidden;
  175. TitleLabel.parent.style.position = Position.Absolute;
  176. }
  177. m_Searcher.BuildIndices();
  178. Refresh();
  179. }
  180. void Refresh()
  181. {
  182. var query = m_Text;
  183. m_Results = m_Searcher.Search(query);
  184. GenerateVisibleResults();
  185. // The first item in the results is always the highest scored item.
  186. // We want to scroll to and select this item.
  187. var visibleIndex = -1;
  188. m_SuggestedTerm = string.Empty;
  189. var results = m_Results.ToList();
  190. if (results.Any())
  191. {
  192. SearcherItem scrollToItem = m_SearchResultsFilterCallback?.Invoke(results, query);
  193. if(scrollToItem == null)
  194. scrollToItem = results.First();
  195. visibleIndex = m_VisibleResults.IndexOf(scrollToItem);
  196. // If we're trying to scroll to a result that is not visible in a single category,
  197. // we need to add that result and its hierarchy back to the visible results
  198. // This prevents searcher suggesting a single collapsed category that the user then needs to manually expand regardless
  199. if (visibleIndex == -1 && m_VisibleResults.Count() == 1)
  200. {
  201. SearcherItem currentItemRoot = scrollToItem;
  202. var idSet = new HashSet<SearcherItem>();
  203. while (currentItemRoot.Parent != null)
  204. {
  205. currentItemRoot = currentItemRoot.Parent;
  206. }
  207. idSet.Add(currentItemRoot);
  208. AddResultChildren(currentItemRoot, idSet);
  209. visibleIndex = m_VisibleResults.IndexOf(scrollToItem);
  210. }
  211. var cursorIndex = m_SearchTextField.cursorIndex;
  212. if (query.Length > 0)
  213. {
  214. var strings = scrollToItem.Name.Split(' ');
  215. var wordStartIndex = cursorIndex == 0 ? 0 : query.LastIndexOf(' ', cursorIndex - 1) + 1;
  216. var word = query.Substring(wordStartIndex, cursorIndex - wordStartIndex);
  217. if (word.Length > 0)
  218. foreach (var t in strings)
  219. {
  220. if (t.StartsWith(word, StringComparison.OrdinalIgnoreCase))
  221. {
  222. m_SuggestedTerm = t;
  223. break;
  224. }
  225. }
  226. }
  227. }
  228. m_ListView.itemsSource = m_VisibleResults;
  229. m_ListView.makeItem = MakeItem;
  230. RefreshListView();
  231. SetSelectedElementInResultsList(visibleIndex);
  232. }
  233. VisualElement MakeItem()
  234. {
  235. VisualElement item = m_Searcher.Adapter.MakeItem();
  236. if (m_Searcher.Adapter.MultiSelectEnabled)
  237. {
  238. var selectionToggle = item.Q<Toggle>("itemToggle");
  239. if (selectionToggle != null)
  240. {
  241. selectionToggle.RegisterValueChangedCallback(changeEvent =>
  242. {
  243. SearcherItem searcherItem = item.userData as SearcherItem;
  244. ToggleItemForMultiSelect(searcherItem, changeEvent.newValue);
  245. });
  246. }
  247. }
  248. return item;
  249. }
  250. void GenerateVisibleResults()
  251. {
  252. if (string.IsNullOrEmpty(m_Text))
  253. {
  254. m_ExpandedResults.Clear();
  255. RemoveChildrenFromResults();
  256. return;
  257. }
  258. RegenerateVisibleResults();
  259. ExpandAllParents();
  260. }
  261. void ExpandAllParents()
  262. {
  263. m_ExpandedResults.Clear();
  264. foreach (var item in m_VisibleResults)
  265. if (item.HasChildren)
  266. m_ExpandedResults.Add(item);
  267. }
  268. void RemoveChildrenFromResults()
  269. {
  270. m_VisibleResults.Clear();
  271. var parents = new HashSet<SearcherItem>();
  272. foreach (var item in m_Results.Where(i => !parents.Contains(i)))
  273. {
  274. var currentParent = item;
  275. while (true)
  276. {
  277. if (currentParent.Parent == null)
  278. {
  279. if (parents.Contains(currentParent))
  280. break;
  281. parents.Add(currentParent);
  282. m_VisibleResults.Add(currentParent);
  283. break;
  284. }
  285. currentParent = currentParent.Parent;
  286. }
  287. }
  288. if (m_Searcher.SortComparison != null)
  289. m_VisibleResults.Sort(m_Searcher.SortComparison);
  290. }
  291. void RegenerateVisibleResults()
  292. {
  293. var idSet = new HashSet<SearcherItem>();
  294. m_VisibleResults.Clear();
  295. foreach (var item in m_Results.Where(item => !idSet.Contains(item)))
  296. {
  297. idSet.Add(item);
  298. m_VisibleResults.Add(item);
  299. var currentParent = item.Parent;
  300. while (currentParent != null)
  301. {
  302. if (!idSet.Contains(currentParent))
  303. {
  304. idSet.Add(currentParent);
  305. m_VisibleResults.Add(currentParent);
  306. }
  307. currentParent = currentParent.Parent;
  308. }
  309. AddResultChildren(item, idSet);
  310. }
  311. var comparison = m_Searcher.SortComparison ?? ((i1, i2) =>
  312. {
  313. var result = i1.Database.Id - i2.Database.Id;
  314. return result != 0 ? result : i1.Id - i2.Id;
  315. });
  316. m_VisibleResults.Sort(comparison);
  317. }
  318. void AddResultChildren(SearcherItem item, ISet<SearcherItem> idSet)
  319. {
  320. if (!item.HasChildren)
  321. return;
  322. if (m_Searcher.Adapter.AddAllChildResults)
  323. {
  324. //add all children results for current search term
  325. // eg "Book" will show both "Cook Book" and "Cooking" as children
  326. foreach (var child in item.Children)
  327. {
  328. if (!idSet.Contains(child))
  329. {
  330. idSet.Add(child);
  331. m_VisibleResults.Add(child);
  332. }
  333. AddResultChildren(child, idSet);
  334. }
  335. }
  336. else
  337. {
  338. foreach (var child in item.Children)
  339. {
  340. //only add child results if the child matches the search term
  341. // eg "Book" will show "Cook Book" but not "Cooking" as a child
  342. if (!m_Results.Contains(child))
  343. continue;
  344. if (!idSet.Contains(child))
  345. {
  346. idSet.Add(child);
  347. m_VisibleResults.Add(child);
  348. }
  349. AddResultChildren(child, idSet);
  350. }
  351. }
  352. }
  353. bool HasChildResult(SearcherItem item)
  354. {
  355. if (m_Results.Contains(item))
  356. return true;
  357. foreach (var child in item.Children)
  358. {
  359. if (HasChildResult(child))
  360. return true;
  361. }
  362. return false;
  363. }
  364. ItemExpanderState GetExpanderState(int index)
  365. {
  366. var item = m_VisibleResults[index];
  367. foreach (var child in item.Children)
  368. {
  369. if (!m_VisibleResults.Contains(child) && !HasChildResult(child))
  370. continue;
  371. return m_ExpandedResults.Contains(item) ? ItemExpanderState.Expanded : ItemExpanderState.Collapsed;
  372. }
  373. return item.Children.Count != 0 ? ItemExpanderState.Collapsed : ItemExpanderState.Hidden;
  374. }
  375. void Bind(VisualElement target, int index)
  376. {
  377. var item = m_VisibleResults[index];
  378. var expanderState = GetExpanderState(index);
  379. var expander = m_Searcher.Adapter.Bind(target, item, expanderState, m_Text);
  380. var selectionToggle = target.Q<Toggle>("itemToggle");
  381. if (selectionToggle != null)
  382. {
  383. selectionToggle.SetValueWithoutNotify(m_MultiSelectSelection.Contains(item));
  384. m_SearchItemToVisualToggle[item] = selectionToggle;
  385. }
  386. expander.RegisterCallback<MouseDownEvent>(ExpandOrCollapse);
  387. }
  388. void ToggleItemForMultiSelect(SearcherItem item, bool selected)
  389. {
  390. if (selected)
  391. {
  392. m_MultiSelectSelection.Add(item);
  393. } else
  394. {
  395. m_MultiSelectSelection.Remove(item);
  396. }
  397. Toggle toggle;
  398. if (m_SearchItemToVisualToggle.TryGetValue(item, out toggle))
  399. {
  400. toggle.SetValueWithoutNotify(selected);
  401. }
  402. foreach (var child in item.Children)
  403. {
  404. ToggleItemForMultiSelect(child, selected);
  405. }
  406. }
  407. static void GetItemsToHide(SearcherItem parent, ref HashSet<SearcherItem> itemsToHide)
  408. {
  409. if (!parent.HasChildren)
  410. {
  411. itemsToHide.Add(parent);
  412. return;
  413. }
  414. foreach (var child in parent.Children)
  415. {
  416. itemsToHide.Add(child);
  417. GetItemsToHide(child, ref itemsToHide);
  418. }
  419. }
  420. void HideUnexpandedItems()
  421. {
  422. // Hide unexpanded children.
  423. var itemsToHide = new HashSet<SearcherItem>();
  424. foreach (var item in m_VisibleResults)
  425. {
  426. if (m_ExpandedResults.Contains(item))
  427. continue;
  428. if (!item.HasChildren)
  429. continue;
  430. if (itemsToHide.Contains(item))
  431. continue;
  432. // We need to hide its children.
  433. GetItemsToHide(item, ref itemsToHide);
  434. }
  435. foreach (var item in itemsToHide)
  436. m_VisibleResults.Remove(item);
  437. }
  438. void RefreshListView()
  439. {
  440. m_SearchItemToVisualToggle.Clear();
  441. #if UNITY_2021_2_OR_NEWER
  442. m_ListView.Rebuild();
  443. #else
  444. m_ListView.Refresh();
  445. #endif
  446. }
  447. // ReSharper disable once UnusedMember.Local
  448. void RefreshListViewOn()
  449. {
  450. // TODO: Call ListView.Refresh() when it is fixed.
  451. // Need this workaround until then.
  452. // See: https://fogbugz.unity3d.com/f/cases/1027728/
  453. // And: https://gitlab.internal.unity3d.com/upm-packages/editor/com.unity.searcher/issues/9
  454. var scrollView = m_ListView.Q<ScrollView>();
  455. var scroller = scrollView?.Q<Scroller>("VerticalScroller");
  456. if (scroller == null)
  457. return;
  458. var oldValue = scroller.value;
  459. scroller.value = oldValue + 1.0f;
  460. scroller.value = oldValue - 1.0f;
  461. scroller.value = oldValue;
  462. }
  463. void Expand(SearcherItem item)
  464. {
  465. m_ExpandedResults.Add(item);
  466. RegenerateVisibleResults();
  467. HideUnexpandedItems();
  468. RefreshListView();
  469. }
  470. void Collapse(SearcherItem item)
  471. {
  472. // if it's already collapsed or not collapsed
  473. if (!m_ExpandedResults.Remove(item))
  474. {
  475. // this case applies for a left arrow key press
  476. if (item.Parent != null)
  477. SetSelectedElementInResultsList(m_VisibleResults.IndexOf(item.Parent));
  478. // even if it's a root item and has no parents, do nothing more
  479. return;
  480. }
  481. RegenerateVisibleResults();
  482. HideUnexpandedItems();
  483. // TODO: understand what happened
  484. RefreshListView();
  485. // RefreshListViewOn();
  486. }
  487. void ExpandOrCollapse(MouseDownEvent evt)
  488. {
  489. if (!(evt.target is VisualElement expanderLabel))
  490. return;
  491. VisualElement itemElement = expanderLabel.GetFirstAncestorOfType<TemplateContainer>();
  492. if (!(itemElement?.userData is SearcherItem item)
  493. || !item.HasChildren
  494. || !expanderLabel.ClassListContains("Expanded") && !expanderLabel.ClassListContains("Collapsed"))
  495. return;
  496. if (!m_ExpandedResults.Contains(item))
  497. Expand(item);
  498. else
  499. Collapse(item);
  500. evt.StopImmediatePropagation();
  501. }
  502. void OnSearchTextFieldTextChanged(InputEvent inputEvent)
  503. {
  504. var text = inputEvent.newData;
  505. if (string.Equals(text, m_Text))
  506. return;
  507. // This is necessary due to OnTextChanged(...) being called after user inputs that have no impact on the text.
  508. // Ex: Moving the caret.
  509. m_Text = text;
  510. // If backspace is pressed and no text remain, clear the suggestion label.
  511. if (string.IsNullOrEmpty(text))
  512. {
  513. this.Q(k_WindowSearchIconName).RemoveFromClassList("Active");
  514. // Display the unfiltered results list.
  515. Refresh();
  516. m_AutoCompleteLabel.text = String.Empty;
  517. m_SuggestedTerm = String.Empty;
  518. SetSelectedElementInResultsList(0);
  519. return;
  520. }
  521. if (!this.Q(k_WindowSearchIconName).ClassListContains("Active"))
  522. this.Q(k_WindowSearchIconName).AddToClassList("Active");
  523. Refresh();
  524. // Calculate the start and end indexes of the word being modified (if any).
  525. var cursorIndex = m_SearchTextField.cursorIndex;
  526. // search toward the beginning of the string starting at the character before the cursor
  527. // +1 because we want the char after a space, or 0 if the search fails
  528. var wordStartIndex = cursorIndex == 0 ? 0 : (text.LastIndexOf(' ', cursorIndex - 1) + 1);
  529. // search toward the end of the string from the cursor index
  530. var wordEndIndex = text.IndexOf(' ', cursorIndex);
  531. if (wordEndIndex == -1) // no space found, assume end of string
  532. wordEndIndex = text.Length;
  533. // Clear the suggestion term if the caret is not within a word (both start and end indexes are equal, ex: (space)caret(space))
  534. // or the user didn't append characters to a word at the end of the query.
  535. if (wordStartIndex == wordEndIndex || wordEndIndex < text.Length)
  536. {
  537. m_AutoCompleteLabel.text = string.Empty;
  538. m_SuggestedTerm = string.Empty;
  539. return;
  540. }
  541. var word = text.Substring(wordStartIndex, wordEndIndex - wordStartIndex);
  542. if (!string.IsNullOrEmpty(m_SuggestedTerm))
  543. {
  544. var wordSuggestion =
  545. word + m_SuggestedTerm.Substring(word.Length, m_SuggestedTerm.Length - word.Length);
  546. text = text.Remove(wordStartIndex, word.Length);
  547. text = text.Insert(wordStartIndex, wordSuggestion);
  548. m_AutoCompleteLabel.text = text;
  549. }
  550. else
  551. {
  552. m_AutoCompleteLabel.text = String.Empty;
  553. }
  554. }
  555. void OnSearchTextFieldKeyDown(KeyDownEvent keyDownEvent)
  556. {
  557. // First, check if we cancelled the search.
  558. if (keyDownEvent.keyCode == KeyCode.Escape)
  559. {
  560. CancelSearch();
  561. return;
  562. }
  563. // For some reason the KeyDown event is raised twice when entering a character.
  564. // As such, we ignore one of the duplicate event.
  565. // This workaround was recommended by the Editor team. The cause of the issue relates to how IMGUI works
  566. // and a fix was not in the works at the moment of this writing.
  567. if (keyDownEvent.character == k_TabCharacter)
  568. {
  569. // Prevent switching focus to another visual element.
  570. keyDownEvent.PreventDefault();
  571. return;
  572. }
  573. // If Tab is pressed, complete the query with the suggested term.
  574. if (keyDownEvent.keyCode == KeyCode.Tab)
  575. {
  576. // Used to prevent the TAB input from executing it's default behavior. We're hijacking it for auto-completion.
  577. keyDownEvent.PreventDefault();
  578. if (!string.IsNullOrEmpty(m_SuggestedTerm))
  579. {
  580. SelectAndReplaceCurrentWord();
  581. m_AutoCompleteLabel.text = string.Empty;
  582. // TODO: Revisit, we shouldn't need to do this here.
  583. m_Text = m_SearchTextField.text;
  584. Refresh();
  585. m_SuggestedTerm = string.Empty;
  586. }
  587. }
  588. else
  589. {
  590. SetSelectedElementInResultsList(keyDownEvent);
  591. }
  592. }
  593. void SelectAndReplaceCurrentWord()
  594. {
  595. var s = m_SearchTextField.value;
  596. var lastWordIndex = s.LastIndexOf(' ');
  597. lastWordIndex++;
  598. var newText = s.Substring(0, lastWordIndex) + m_SuggestedTerm;
  599. // Wait for SelectRange api to reach trunk
  600. //#if UNITY_2018_3_OR_NEWER
  601. // m_SearchTextField.value = newText;
  602. // m_SearchTextField.SelectRange(m_SearchTextField.value.Length, m_SearchTextField.value.Length);
  603. //#else
  604. // HACK - relies on the textfield moving the caret when being assigned a value and skipping
  605. // all low surrogate characters
  606. var magicMoveCursorToEndString = new string('\uDC00', newText.Length);
  607. m_SearchTextField.value = magicMoveCursorToEndString;
  608. m_SearchTextField.value = newText;
  609. //#endif
  610. }
  611. void SetSelectedElementInResultsList(KeyDownEvent keyDownEvent)
  612. {
  613. int index;
  614. switch (keyDownEvent.keyCode)
  615. {
  616. case KeyCode.Escape:
  617. OnListViewSelect(null);
  618. m_AnalyticsDataCallback?.Invoke(new Searcher.AnalyticsEvent(Searcher.AnalyticsEvent.EventType.Cancelled, m_SearchTextField.value));
  619. break;
  620. case KeyCode.Return:
  621. case KeyCode.KeypadEnter:
  622. if (m_ListView.selectedIndex != -1)
  623. {
  624. OnListViewSelect((SearcherItem)m_ListView.selectedItem);
  625. m_AnalyticsDataCallback?.Invoke(new Searcher.AnalyticsEvent(Searcher.AnalyticsEvent.EventType.Picked, m_SearchTextField.value));
  626. }
  627. else
  628. {
  629. OnListViewSelect(null);
  630. m_AnalyticsDataCallback?.Invoke(new Searcher.AnalyticsEvent(Searcher.AnalyticsEvent.EventType.Cancelled, m_SearchTextField.value));
  631. }
  632. break;
  633. case KeyCode.LeftArrow:
  634. index = m_ListView.selectedIndex;
  635. if (index >= 0 && index < m_ListView.itemsSource.Count)
  636. Collapse(m_ListView.selectedItem as SearcherItem);
  637. break;
  638. case KeyCode.RightArrow:
  639. index = m_ListView.selectedIndex;
  640. if (index >= 0 && index < m_ListView.itemsSource.Count)
  641. Expand(m_ListView.selectedItem as SearcherItem);
  642. break;
  643. // Fixes bug: https://fogbugz.unity3d.com/f/cases/1358016/
  644. case KeyCode.UpArrow:
  645. case KeyCode.PageUp:
  646. if (m_ListView.selectedIndex > 0)
  647. SetSelectedElementInResultsList(m_ListView.selectedIndex - 1);
  648. break;
  649. case KeyCode.DownArrow:
  650. case KeyCode.PageDown:
  651. if (m_ListView.selectedIndex < 0)
  652. SetSelectedElementInResultsList(0);
  653. else
  654. SetSelectedElementInResultsList(m_ListView.selectedIndex + 1);
  655. break;
  656. }
  657. }
  658. void SetSelectedElementInResultsList(int selectedIndex)
  659. {
  660. var newIndex = selectedIndex >= 0 && selectedIndex < m_VisibleResults.Count ? selectedIndex : -1;
  661. if (newIndex < 0)
  662. return;
  663. m_ListView.selectedIndex = newIndex;
  664. m_ListView.ScrollToItem(m_ListView.selectedIndex);
  665. }
  666. }
  667. }