/// <summary> /// Initiate a search and return all search items matching the search context. Other items can be found later using the asynchronous searches. /// </summary> /// <param name="context">The current search context</param> /// <returns>A list of search items matching the search query.</returns> public static List <SearchItem> GetItems(SearchContext context, SearchFlags options = SearchFlags.Default) { // Stop all search sessions every time there is a new search. context.sessions.StopAllAsyncSearchSessions(); if (context.actionId == null && string.IsNullOrEmpty(context.searchText) && !context.wantsMore) { return(new List <SearchItem>(0)); } var allItems = new List <SearchItem>(3); #if QUICKSEARCH_DEBUG var debugProviderList = context.providers.ToList(); using (new DebugTimer($"Search get items {String.Join(", ", debugProviderList.Select(p=>p.name.id))} -> {context.searchQuery}")); #endif foreach (var provider in context.providers) { using (var fetchTimer = new DebugTimer(null)) { try { var iterator = provider.fetchItems(context, allItems, provider); if (iterator != null) { if (options.HasFlag(SearchFlags.Synchronous)) { var stackedEnumerator = new StackedEnumerator <SearchItem>(iterator); while (stackedEnumerator.MoveNext()) { if (stackedEnumerator.Current != null) { allItems.Add(stackedEnumerator.Current); } } } else { var session = context.sessions.GetProviderSession(provider.name.id); session.Reset(iterator, k_MaxFetchTimeMs); if (!session.FetchSome(allItems, k_MaxFetchTimeMs)) { session.Stop(); } } } provider.RecordFetchTime(fetchTimer.timeMs); } catch (Exception ex) { UnityEngine.Debug.LogException(new Exception($"Failed to get fetch {provider.name.displayName} provider items.", ex)); } } } if (!options.HasFlag(SearchFlags.Sorted)) { return(allItems); } allItems.Sort(SortItemComparer); return(allItems.GroupBy(i => i.id).Select(i => i.First()).ToList()); }
private void DrawItems(SearchContext context) { UpdateScrollAreaOffset(); context.totalItemCount = m_FilteredItems.Count; using (var scrollViewScope = new EditorGUILayout.ScrollViewScope(m_ScrollPosition)) { m_ScrollPosition = scrollViewScope.scrollPosition; var itemCount = m_FilteredItems.Count; var availableHeight = position.height - m_ScrollViewOffset.yMax; var itemSkipCount = Math.Max(0, (int)(m_ScrollPosition.y / Styles.itemRowHeight)); var itemDisplayCount = Math.Max(0, Math.Min(itemCount, (int)(availableHeight / Styles.itemRowHeight) + 2)); var topSpaceSkipped = itemSkipCount * Styles.itemRowHeight; int rowIndex = itemSkipCount; var limitCount = Math.Max(0, Math.Min(itemDisplayCount, itemCount - itemSkipCount)); if (limitCount > 0) { if (topSpaceSkipped > 0) { GUILayout.Space(topSpaceSkipped); } foreach (var item in m_FilteredItems.GetRange(itemSkipCount, limitCount)) { try { DrawItem(item, context, rowIndex++); } #if QUICKSEARCH_DEBUG catch (Exception ex) { Debug.LogError($"m_FilteredItems.Count={m_FilteredItems.Count}, itemSkipCount={itemSkipCount}, limitCount={limitCount}, m_SelectedIndex={m_SelectedIndex}"); Debug.LogException(ex); } #else catch { // ignored } #endif } var bottomSpaceSkipped = (itemCount - rowIndex) * Styles.itemRowHeight; if (bottomSpaceSkipped > 0) { GUILayout.Space(bottomSpaceSkipped); } HandleItemEvents(itemCount, context); // Fix selected index display if out of virtual scrolling area if (Event.current.type == EventType.Repaint && m_FocusSelectedItem && m_SelectedIndex >= 0) { ScrollToItem(itemSkipCount + 1, itemSkipCount + itemDisplayCount - 2, m_SelectedIndex); m_FocusSelectedItem = false; } } else { if (String.IsNullOrEmpty(m_Context.searchBoxText.Trim())) { GUILayout.Box("What are you looking for?\nJust start typing...", Styles.noResult, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true)); } else { GUILayout.Box("No result for query \"" + m_Context.searchBoxText + "\"\n" + "Try something else?", Styles.noResult, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true)); } } } }
private void OnSearchItemsReceived(SearchContext context, IEnumerable <SearchItem> items) { ProcessItems(context, items); }
/// <summary> /// Helper function to match a string against the SearchContext. This will try to match the search query against each tokens of content (similar to the AddComponent menu workflow) /// </summary> /// <param name="context">Search context containing the searchQuery that we try to match.</param> /// <param name="content">String content that will be tokenized and use to match the search query.</param> /// <param name="ignoreCase">Perform matching ignoring casing.</param> /// <returns>Has a match occurred.</returns> public static bool MatchSearchGroups(SearchContext context, string content, bool ignoreCase = false) { return(MatchSearchGroups(context.searchQuery, context.searchWords, content, out _, out _, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)); }
private void DrawToolbar(SearchContext context) { if (context == null) { return; } GUILayout.BeginHorizontal(Styles.toolbar); { var rightRect = EditorGUILayout.GetControlRect(GUILayout.MaxWidth(32f), GUILayout.ExpandHeight(true)); if (EditorGUI.DropdownButton(rightRect, Styles.filterButtonContent, FocusType.Passive, Styles.filterButton) || m_ShowFilterWindow) { if (FilterWindow.canShow) { rightRect.x += 12f; rightRect.y -= 3f; if (m_ShowFilterWindow) { rightRect.y += 30f; } m_ShowFilterWindow = false; if (FilterWindow.ShowAtPosition(this, rightRect)) { GUIUtility.ExitGUI(); } } } EditorGUI.BeginChangeCheck(); using (new BlinkCursorScope(m_CursorBlinking, new Color(0, 0, 0, 0.01f))) { var userSearchQuery = context.searchBoxText; if (!String.IsNullOrEmpty(m_CycledSearch) && (Event.current.type == EventType.Repaint || Event.current.type == EventType.Layout)) { userSearchQuery = m_CycledSearch; m_CycledSearch = null; m_SearchBoxFocus = true; GUI.changed = true; } GUI.SetNextControlName(k_QuickSearchBoxName); context.searchBoxText = EditorGUILayout.TextField(userSearchQuery, Styles.searchField, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true)); } if (String.IsNullOrEmpty(context.searchBoxText)) { EditorGUI.BeginDisabledGroup(true); EditorGUI.TextArea(GUILayoutUtility.GetLastRect(), "search anything...", Styles.placeholderTextStyle); EditorGUI.EndDisabledGroup(); } if (!String.IsNullOrEmpty(context.searchBoxText)) { if (GUILayout.Button(Icons.clear, Styles.searchFieldClear, GUILayout.Width(24), GUILayout.Height(24))) { context.searchBoxText = ""; GUI.changed = true; GUI.FocusControl(null); } } if (EditorGUI.EndChangeCheck() || m_FilteredItems == null) { m_SelectedIndex = -1; m_ScrollPosition.y = 0; SearchService.SearchTextChanged(context); m_FilteredItems = SearchService.GetItems(context); UpdateWindowTitle(); } #if QUICKSEARCH_DEBUG DrawDebugTools(); #endif } GUILayout.EndHorizontal(); }
/// <summary> /// Helper function to create a new search item for the current provider. /// </summary> /// <param name="context">Search context from the query that generates this item.</param> /// <param name="id">Unique id of the search item. This is used to remove duplicates to the user view.</param> /// <returns>The newly created search item attached to the current search provider.</returns> public SearchItem CreateItem(SearchContext context, string id) { return(CreateItem(context, id, 0, null, null, null, null)); }
public SearchApiSession(SearchProvider provider) { context = new SearchContext(new [] { provider }); }
private static void DrawDescription(SearchContext context, SearchItem item) { var description = SearchContent.FormatDescription(item, context, 2048); GUILayout.Label(description, Styles.previewDescription); }
public void Draw(SearchContext context, float width) { var selection = context.searchView.selection; var selectionCount = selection.Count; if (selectionCount == 0) { return; } var lastItem = selection.Last(); using (var scrollView = new EditorGUILayout.ScrollViewScope(m_ScrollPosition, GUILayout.Width(width), GUILayout.ExpandHeight(true))) { var showOptions = lastItem.provider.showDetailsOptions; if (showOptions.HasFlag(ShowDetailsOptions.Inspector) && Event.current.type == EventType.Layout) { SetupEditors(selection); } if (selectionCount > 1) { GUILayout.Label($"Selected {selectionCount} items", Styles.previewDescription); } else { if (showOptions.HasFlag(ShowDetailsOptions.Preview)) { if (showOptions.HasFlag(ShowDetailsOptions.Inspector)) { if (m_Editors != null && m_Editors.Length == 1 && m_Editors[0].HasPreviewGUI()) { var e = m_Editors[0]; var previewRect = EditorGUILayout.GetControlRect(GUILayout.MaxWidth(width), GUILayout.MaxHeight(width)); if (previewRect.width > 0 && previewRect.height > 0) { e.OnPreviewGUI(previewRect, Styles.largePreview); } } else { DrawPreview(context, lastItem, width); } } else { DrawPreview(context, lastItem, width); } } if (showOptions.HasFlag(ShowDetailsOptions.Description)) { DrawDescription(context, lastItem); } } if (showOptions.HasFlag(ShowDetailsOptions.Inspector)) { DrawInspector(selection, width); } if (showOptions.HasFlag(ShowDetailsOptions.Actions)) { DrawActions(context); } m_ScrollPosition = scrollView.scrollPosition; } }
public void ShowItemContextualMenu(SearchItem item, SearchContext context, Rect contextualActionPosition) { // Nothing to do. }
public SearchRequest(ExpressionType type, SearchContext searchContext) : this(type) { pendingQueries.Enqueue(searchContext); }
public void ExecuteAction(SearchAction action, SearchItem[] items, SearchContext context, bool endSearch = true) { selectCallback?.Invoke(items.FirstOrDefault(), false); }
public SearchContext CreateContext() { var context = new SearchContext(providers, searchQuery); return(context); }
/// <summary> /// Helper function to match a string against the SearchContext. This will try to match the search query against each tokens of content (similar to the AddComponent menu workflow) /// </summary> /// <param name="context">Search context containing the searchQuery that we try to match.</param> /// <param name="content">String content that will be tokenized and use to match the search query.</param> /// <param name="useLowerTokens">Perform matching ignoring casing.</param> /// <returns>Has a match occurred.</returns> public static bool MatchSearchGroups(SearchContext context, string content, bool useLowerTokens = false) { return(MatchSearchGroups(context.searchQuery, useLowerTokens ? context.tokenizedSearchQueryLower : context.tokenizedSearchQuery, content, out _, out _, useLowerTokens ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); }
public static SearchQuery Create(SearchContext context, string description = null, Texture2D icon = null) { return(Create(context.searchText, context.providers.Select(p => p.name.id), description, icon)); }
public SortedSearchList(SearchContext searchContext) : base(searchContext) { Clear(); }
public static GUIContent FormatDescription(SearchItem item, SearchContext context, float availableSpace, bool useColor = true) { var desc = item.GetDescription(context); if (String.IsNullOrEmpty(desc)) { return(Styles.emptyContent); } var content = Take(desc); if (item.options == SearchItemOptions.None || Event.current.type != EventType.Repaint) { return(content); } var truncatedDesc = desc; var truncated = false; if (useColor) { if (item.options.HasFlag(SearchItemOptions.Ellipsis)) { int maxCharLength = Utils.GetNumCharactersThatFitWithinWidth(Styles.itemDescription, truncatedDesc + "...", availableSpace); if (maxCharLength < 0) { maxCharLength = truncatedDesc.Length; } truncated = desc.Length > maxCharLength; if (truncated) { if (item.options.HasFlag(SearchItemOptions.RightToLeft)) { truncatedDesc = "..." + desc.Replace("<b>", "").Replace("</b>", ""); truncatedDesc = truncatedDesc.Substring(Math.Max(0, truncatedDesc.Length - maxCharLength)); } else { truncatedDesc = desc.Substring(0, Math.Min(maxCharLength, desc.Length)) + "..."; } } } if (context != null) { if (item.options.HasFlag(SearchItemOptions.Highlight)) { var parts = context.searchQuery.Split('*', ' ', '.').Where(p => p.Length > 2); foreach (var p in parts) { truncatedDesc = Regex.Replace(truncatedDesc, Regex.Escape(p), string.Format(Styles.highlightedTextColorFormat, "$0"), RegexOptions.IgnoreCase); } } else if (item.options.HasFlag(SearchItemOptions.FuzzyHighlight)) { long score = 1; var matches = new List <int>(); var sq = Utils.CleanString(context.searchQuery.ToLowerInvariant()); if (FuzzySearch.FuzzyMatch(sq, Utils.CleanString(truncatedDesc), ref score, matches)) { truncatedDesc = RichTextFormatter.FormatSuggestionTitle(truncatedDesc, matches); } } } } content.text = truncatedDesc; if (truncated) { content.tooltip = Utils.StripHTML(desc); } return(content); }
public AsyncSearchList(SearchContext searchContext) : base(searchContext) { m_UnorderedItems = new List <SearchItem>(); }
/// <summary> /// Create a Search item that will be bound to the SeaechProvider. /// </summary> /// <param name="context">Search context from the query that generates this item.</param> /// <param name="id">Unique id of the search item. This is used to remove duplicates to the user view.</param> /// <param name="label">The search item label is displayed on the first line of the search item UI widget.</param> /// <param name="description">The search item description is displayed on the second line of the search item UI widget.</param> /// <param name="thumbnail">The search item thumbnail is displayed left to the item label and description as a preview.</param> /// <param name="data">User data used to recover more information about a search item. Generally used in fetchLabel, fetchDescription, etc.</param> /// <returns>New SearchItem</returns> public SearchItem CreateItem(SearchContext context, string id, string label, string description, Texture2D thumbnail, object data) { return(CreateItem(context, id, 0, label, description, thumbnail, data)); }
public BaseSearchList(SearchContext searchContext) { context = searchContext; context.asyncItemReceived += OnAsyncItemsReceived; }
private void OnAsyncItemsReceived(SearchContext context, IEnumerable <SearchItem> items) { onAsyncItemsReceived?.Invoke(items.Select(item => item.id)); }
public static string[] GetKeywords(SearchContext context, string lastToken) { throw new NotSupportedException(); }
public static void AutoCompletion(Rect rect, SearchContext context, ISearchView view) { if (s_DiscardAutoComplete || controlID <= 0) { return; } if (s_AutoCompleting && Event.current.type == EventType.MouseDown && !s_AutoCompleteRect.Contains(Event.current.mousePosition)) { s_DiscardAutoComplete = true; s_AutoCompleting = false; return; } var te = GetTextEditor(); var cursorPosition = te.cursorIndex; if (cursorPosition == 0) { return; } var searchText = context.searchText; var lastTokenStartPos = searchText.LastIndexOf(' ', Math.Max(0, te.cursorIndex - 1)); var lastToken = lastTokenStartPos == -1 ? searchText : searchText.Substring(lastTokenStartPos + 1); var keywords = SearchService.GetKeywords(context, lastToken).Where(k => !k.Equals(lastToken, StringComparison.OrdinalIgnoreCase)).ToArray(); if (keywords.Length > 0) { const int maxAutoCompleteCount = 16; s_AutoCompleteMaxIndex = Math.Min(keywords.Length, maxAutoCompleteCount); if (!s_AutoCompleting) { s_AutoCompleteIndex = 0; } if (Event.current.type == EventType.Repaint) { var content = new GUIContent(context.searchText.Substring(0, context.searchText.Length - lastToken.Length)); var offset = Styles.searchField.CalcSize(content).x; s_AutoCompleteRect = rect; s_AutoCompleteRect.x += offset; s_AutoCompleteRect.y = rect.yMax; s_AutoCompleteRect.width = 250; s_AutoCompleteRect.x = Math.Min(rect.width - s_AutoCompleteRect.width - 25, s_AutoCompleteRect.x); } var lt = lastToken; var autoFill = DoAutoComplete(lastToken, keywords, maxAutoCompleteCount, 0.1f); if (autoFill == null) { // No more results s_AutoCompleting = false; s_AutoCompleteIndex = -1; } else if (autoFill != lastToken) { var regex = new Regex(Regex.Escape(lastToken), RegexOptions.IgnoreCase); autoFill = regex.Replace(autoFill, ""); view.SetSearchText(context.searchText.Insert(cursorPosition, autoFill)); } else { s_AutoCompleting = true; } } else { s_AutoCompleting = false; s_AutoCompleteIndex = -1; } }
/// <summary> /// Initiate a search and return all search items matching the search context. Other items can be found later using the asynchronous searches. /// </summary> /// <param name="context">The current search context</param> /// <param name="options">Options defining how the query will be performed</param> /// <returns>A list of search items matching the search query.</returns> public static List <SearchItem> GetItems(SearchContext context, SearchFlags options = SearchFlags.Default) { DebugInfo.gcFetch = GC.GetTotalMemory(false); // Stop all search sessions every time there is a new search. context.sessions.StopAllAsyncSearchSessions(); context.searchFinishTime = context.searchStartTime = EditorApplication.timeSinceStartup; context.sessionEnded -= OnSearchEnded; context.sessionEnded += OnSearchEnded; #if SHOW_SEARCH_PROGRESS if (Progress.Exists(context.progressId)) { Progress.Finish(context.progressId, Progress.Status.Succeeded); } context.progressId = Progress.Start($"Searching...", options: Progress.Options.Indefinite); #endif if (options.HasFlag(SearchFlags.WantsMore)) { context.wantsMore = true; } if (options.HasFlag(SearchFlags.Synchronous)) { context.options |= SearchFlags.Synchronous; } int fetchProviderCount = 0; var allItems = new List <SearchItem>(3); #if QUICKSEARCH_DEBUG var debugProviderList = context.providers.ToList(); using (new DebugTimer($"Search get items {String.Join(", ", debugProviderList.Select(p=>p.name.id))} -> {context.searchQuery}")); #endif foreach (var provider in context.providers) { try { var watch = new System.Diagnostics.Stopwatch(); watch.Start(); fetchProviderCount++; var iterator = provider.fetchItems(context, allItems, provider); if (iterator != null && options.HasFlag(SearchFlags.Synchronous)) { var stackedEnumerator = new StackedEnumerator <SearchItem>(iterator); while (stackedEnumerator.MoveNext()) { if (stackedEnumerator.Current != null) { allItems.Add(stackedEnumerator.Current); } } } else { var session = context.sessions.GetProviderSession(context, provider.name.id); session.Reset(context, iterator, k_MaxFetchTimeMs); session.Start(); var sessionEnded = !session.FetchSome(allItems, k_MaxFetchTimeMs); if (options.HasFlag(SearchFlags.FirstBatchAsync)) { session.SendItems(allItems); } if (sessionEnded) { session.Stop(); } } provider.RecordFetchTime(watch.Elapsed.TotalMilliseconds); } catch (Exception ex) { UnityEngine.Debug.LogException(new Exception($"Failed to get fetch {provider.name.displayName} provider items.", ex)); } } if (fetchProviderCount == 0) { OnSearchEnded(context); context.sessions.StopAllAsyncSearchSessions(); } DebugInfo.gcFetch = GC.GetTotalMemory(false) - DebugInfo.gcFetch; if (!options.HasFlag(SearchFlags.Sorted)) { return(allItems); } allItems.Sort(SortItemComparer); return(allItems.GroupBy(i => i.id).Select(i => i.First()).ToList()); }
private void HandleKeyboardNavigation(SearchContext context) { // TODO: support page down and page up var evt = Event.current; if (evt.type == EventType.KeyDown) { var prev = m_SelectedIndex; if (evt.keyCode == KeyCode.DownArrow) { if (m_SelectedIndex == -1 && evt.modifiers.HasFlag(EventModifiers.Alt)) { m_CycledSearch = SearchService.CyclePreviousSearch(-1); GUI.FocusControl(null); } else { m_SelectedIndex = Math.Min(m_SelectedIndex + 1, m_FilteredItems.Count - 1); Event.current.Use(); } } else if (evt.keyCode == KeyCode.UpArrow) { if (m_SelectedIndex >= 0) { m_SelectedIndex = Math.Max(-1, m_SelectedIndex - 1); if (m_SelectedIndex == -1) { m_SearchBoxFocus = true; } Event.current.Use(); } else if (evt.modifiers.HasFlag(EventModifiers.Alt)) { m_CycledSearch = SearchService.CyclePreviousSearch(+1); GUI.FocusControl(null); } } else if (evt.keyCode == KeyCode.PageDown) { m_SelectedIndex = Math.Min(m_SelectedIndex + GetDisplayItemCount() - 1, m_FilteredItems.Count - 1); Event.current.Use(); } else if (evt.keyCode == KeyCode.PageUp) { m_SelectedIndex = Math.Max(0, m_SelectedIndex - GetDisplayItemCount()); Event.current.Use(); } else if (evt.keyCode == KeyCode.RightArrow && evt.modifiers.HasFlag(EventModifiers.Alt)) { if (m_SelectedIndex != -1) { var item = m_FilteredItems.ElementAt(m_SelectedIndex); var menuPositionY = (m_SelectedIndex + 1) * Styles.itemRowHeight - m_ScrollPosition.y + Styles.itemRowHeight / 2.0f; ShowItemContextualMenu(item, context, new Rect(position.width - Styles.actionButtonSize, menuPositionY, 1, 1)); Event.current.Use(); } } else if (evt.keyCode == KeyCode.LeftArrow && evt.modifiers.HasFlag(EventModifiers.Alt)) { m_ShowFilterWindow = true; Event.current.Use(); } else if (evt.keyCode == KeyCode.KeypadEnter || evt.keyCode == KeyCode.Return) { var selectedIndex = m_SelectedIndex; if (selectedIndex == -1 && m_FilteredItems != null && m_FilteredItems.Count > 0) { selectedIndex = 0; } if (selectedIndex != -1 && m_FilteredItems != null) { int actionIndex = 0; if (evt.modifiers.HasFlag(EventModifiers.Alt)) { actionIndex = 1; if (evt.modifiers.HasFlag(EventModifiers.Control)) { actionIndex = 2; if (evt.modifiers.HasFlag(EventModifiers.Shift)) { actionIndex = 3; } } } var item = m_FilteredItems.ElementAt(selectedIndex); if (item.provider.actions.Any()) { Event.current.Use(); actionIndex = Math.Max(0, Math.Min(actionIndex, item.provider.actions.Count - 1)); ExecuteAction(item.provider.actions[actionIndex], item, context); GUIUtility.ExitGUI(); } } } else if (evt.keyCode == KeyCode.Escape) { CloseSearchWindow(); Event.current.Use(); } else { GUI.FocusControl(k_QuickSearchBoxName); } if (prev != m_SelectedIndex) { m_FocusSelectedItem = true; Repaint(); } } if (m_FilteredItems == null || m_FilteredItems.Count == 0) { m_SearchBoxFocus = true; } }
public virtual void BeginSession(UnityEditor.SearchService.ISearchContext context) { provider = SearchService.Providers.First(p => p.name.id == providerId); this.context = new SearchContext(new [] { provider }); }
private void ExecuteAction(SearchAction action, SearchItem item, SearchContext context) { SearchService.LastSearch = context.searchBoxText; action.handler(item, context); CloseSearchWindow(); }
public virtual void EndSession(UnityEditor.SearchService.ISearchContext context) { StopAsyncResults(); this.context = null; }
public SearchRequest(ExpressionType type, SearchContext searchContext, Action <SearchRequest> finishedHandler = null) : this(type, finishedHandler) { pendingQueries.Enqueue(searchContext); }
public AsyncSearchSession(SearchContext context) { m_Context = context; }