diff --git a/CherryPick_Config.cs b/CherryPick_Config.cs index fadddb4..2242294 100644 --- a/CherryPick_Config.cs +++ b/CherryPick_Config.cs @@ -22,4 +22,11 @@ public partial class CherryPick : ResoniteMod [AutoRegisterConfigKey] public static ModConfigurationKey ClearFocus = new("Clear focus", "When checked, the search buttons will clear the focus of the search bar.", () => true); + + [AutoRegisterConfigKey] + public static ModConfigurationKey UserExcludedCategories = new("Excluded categories", "Excludes specific categories from being searched into by path. Separate entries by semicolon.", () => "/ProtoFlux, /Example/Test"); + + [AutoRegisterConfigKey] + public static ModConfigurationKey SearchRefreshDelay = new("Search refresh delay", "Time to wait after search input change before refreshing the results. 0 to always refresh.", () => .4f); + } \ No newline at end of file diff --git a/CherryPicker.cs b/CherryPicker.cs index 5de5ee2..9a0e955 100644 --- a/CherryPicker.cs +++ b/CherryPicker.cs @@ -4,6 +4,7 @@ using FrooxEngine.UIX; using static CherryPick.CherryPick; using System.Runtime.CompilerServices; +using System; namespace CherryPick; @@ -67,7 +68,9 @@ public void PerformMatch(string query, int resultCount = 10) string[] splitQuery = query.Split(' '); - + string userexcludedcategories = Config!.GetValue(CherryPick.UserExcludedCategories).ToLower(); + string[] UserExcludedCategories = userexcludedcategories.Replace(" ", null).Split(','); + // The for loops are a bit hot and can cause minor // hitches if care isn't taken. Avoiding branch logic if possible @@ -78,12 +81,15 @@ public void PerformMatch(string query, int resultCount = 10) for (int i = 0; i < workerCount; i++) { WorkerDetails worker = details[i]; - float ratio = MatchRatioInsensitive(worker.LowerName, splitQuery); + if (!UserExcludedCategories.Any(worker.Path.ToLower().Contains) || userexcludedcategories == "") + { + float ratio = MatchRatioInsensitive(worker.LowerName, splitQuery); - _results.Add(ratio, worker); - int detailCount = _results.Count; + _results.Add(ratio, worker); + int detailCount = _results.Count; - _results.RemoveAt(detailCount - 1); + _results.RemoveAt(detailCount - 1); + } } } else @@ -92,12 +98,16 @@ public void PerformMatch(string query, int resultCount = 10) for (int i = 0; i < workerCount; i++) { WorkerDetails worker = details[i]; - float ratio = worker.Path.StartsWith(searchScope) ? MatchRatioInsensitive(worker.LowerName, splitQuery) : 0f; - _results.Add(ratio, worker); - int detailCount = _results.Count; + if (!UserExcludedCategories.Any(worker.Path.ToLower().Replace("/protoflux", "").Contains) || userexcludedcategories == "") + { + float ratio = worker.Path.StartsWith(searchScope) ? MatchRatioInsensitive(worker.LowerName, splitQuery) : 0f; + + _results.Add(ratio, worker); + int detailCount = _results.Count; - _results.RemoveAt(detailCount - 1); + _results.RemoveAt(detailCount - 1); + } } } @@ -188,95 +198,115 @@ public void ForceEditFinished(TextEditor editor) EditFinished(editor); } - + + int searchDelayToken = 0; public void EditChanged(TextEditor editor) { - if (searchRoot == null || - componentUIRoot == null || - editor == null || - onGenericPressed == null || - onAddPressed == null || - searchBuilder == null || - !IsReady) // You can't search until the cache is built! This is fine in most cases, but if you end up searching before then, too bad! - return; + int SearchRefreshDelay = (int)(1000 * Config!.GetValue(CherryPick.SearchRefreshDelay)); - - string txt = editor.Text.Target.Text; - if (txt == null) - return; + editor.StartTask(async () => + { + searchDelayToken++; + int LocalSearchDelayToken = searchDelayToken; + if (SearchRefreshDelay > 0) + { + await default(ToBackground); + await Task.Delay(SearchRefreshDelay); + await default(NextUpdate); + } - int genericStart = txt.IndexOf('<'); - string? matchTxt = null; - string? genericType = null; + // Only refresh UI with search results if there was no further update immediately following it + if (searchDelayToken!= LocalSearchDelayToken) + return; - if (genericStart > 0) - { - matchTxt = txt.Substring(0, genericStart); - genericType = txt.Substring(genericStart); - } - else - { - matchTxt = txt; - } + if (searchRoot == null || + componentUIRoot == null || + editor == null || + onGenericPressed == null || + onAddPressed == null || + searchBuilder == null || + !IsReady) // You can't search until the cache is built! This is fine in most cases, but if you end up searching before then, too bad! + return; + + string txt = editor.Text.Target.Text; + if (txt == null) + return; - searchRoot.DestroyChildren(); - int resultCount = Config!.GetValue(ResultCount); - resultCount = MathX.Min(resultCount, MAX_RESULT_COUNT); + int genericStart = txt.IndexOf('<'); + string? matchTxt = null; + string? genericType = null; - PerformMatch(matchTxt, resultCount); - foreach (var result in _results.Values) - { - bool isGenType = result.Type.IsGenericTypeDefinition; - string arg = ""; - - try + if (genericStart > 0) { - arg = isGenType ? Path.Combine(result.Path, result.Type.AssemblyQualifiedName) : searchRoot.World.Types().EncodeType(result.Type); + matchTxt = txt.Substring(0, genericStart); + genericType = txt.Substring(genericStart); } - catch (ArgumentException) + else { - CherryPick.Warn($"Tried to encode a non-data model type: {result.Type}"); - continue; + matchTxt = txt; } - var pressed = isGenType ? onGenericPressed : onAddPressed; - CreateButton(result, pressed, arg, searchBuilder, editor, RadiantUI_Constants.Sub.CYAN); - } + searchRoot.DestroyChildren(); + int resultCount = Config!.GetValue(ResultCount); + resultCount = MathX.Min(resultCount, MAX_RESULT_COUNT); - try - { - WorkerDetails firstGeneric = _results.Values.First(w => w.Type.IsGenericTypeDefinition); - if (genericType != null) + + PerformMatch(matchTxt, resultCount); + foreach (var result in _results.Values) { - string typeName = firstGeneric.Type.FullName; - typeName = typeName.Substring(0, typeName.IndexOf("`")) + genericType; - Type? constructed = NiceTypeParser.TryParse(typeName); + bool isGenType = result.Type.IsGenericTypeDefinition; + string arg = ""; + + try + { + arg = isGenType ? Path.Combine(result.Path, result.Type.AssemblyQualifiedName) : searchRoot.World.Types().EncodeType(result.Type); + } + catch (ArgumentException) + { + CherryPick.Warn($"Tried to encode a non-data model type: {result.Type}"); + continue; + } + + var pressed = isGenType ? onGenericPressed : onAddPressed; + CreateButton(result, pressed, arg, searchBuilder, editor, RadiantUI_Constants.Sub.CYAN); + } - if (constructed != null) + try + { + WorkerDetails firstGeneric = _results.Values.First(w => w.Type.IsGenericTypeDefinition); + if (genericType != null) { - try - { - string arg = searchRoot.World.Types().EncodeType(constructed); - } - catch (ArgumentException) + string typeName = firstGeneric.Type.FullName; + typeName = typeName.Substring(0, typeName.IndexOf("`")) + genericType; + Type? constructed = NiceTypeParser.TryParse(typeName); + + + if (constructed != null) { - CherryPick.Warn($"Tried to encode a non-data model type: {constructed}"); - return; + try + { + string arg = searchRoot.World.Types().EncodeType(constructed); + } + catch (ArgumentException) + { + CherryPick.Warn($"Tried to encode a non-data model type: {constructed}"); + return; + } + + WorkerDetails detail = new(constructed.GetNiceName(), firstGeneric.Path, constructed); + Button typeButton = CreateButton(detail, onAddPressed, searchRoot.World.Types().EncodeType(constructed), searchBuilder, editor, RadiantUI_Constants.Sub.ORANGE); + typeButton.Slot.OrderOffset = -1024; } - - WorkerDetails detail = new(constructed.GetNiceName(), firstGeneric.Path, constructed); - Button typeButton = CreateButton(detail, onAddPressed, searchRoot.World.Types().EncodeType(constructed), searchBuilder, editor, RadiantUI_Constants.Sub.ORANGE); - typeButton.Slot.OrderOffset = -1024; } } - } - catch (InvalidOperationException) { } // Swallow this exception in particular because First() will throw if nothing satisfies the lambda condition + catch (InvalidOperationException) { } // Swallow this exception in particular because First() will throw if nothing satisfies the lambda condition + }); }