From 6b05616c5578dd00d1a1bc34d2a67067b2b25a79 Mon Sep 17 00:00:00 2001 From: Arne Kiesewetter Date: Wed, 23 Apr 2025 23:53:17 +0200 Subject: [PATCH 1/2] Mostly implement parsing generic types from the search bar --- ComponentSelectorAdditions/ComponentResult.cs | 5 +++ ComponentSelectorAdditions/DefaultConfig.cs | 7 +++ ComponentSelectorAdditions/DefaultHandler.cs | 7 ++- ComponentSelectorAdditions/SearchBar.cs | 41 ++++++++++++----- ComponentSelectorAdditions/SelectorPath.cs | 44 +++++++++++++++++++ 5 files changed, 92 insertions(+), 12 deletions(-) diff --git a/ComponentSelectorAdditions/ComponentResult.cs b/ComponentSelectorAdditions/ComponentResult.cs index f038a66..57ced2a 100644 --- a/ComponentSelectorAdditions/ComponentResult.cs +++ b/ComponentSelectorAdditions/ComponentResult.cs @@ -42,6 +42,11 @@ public sealed class ComponentResult [MemberNotNullWhen(true, nameof(Group), nameof(GroupName))] public bool HasGroup => Group is not null; + /// + /// Gets whether this Type is a concrete generic. + /// + public bool IsConcreteGeneric => Type.IsGenericType && !IsGeneric; + /// /// Gets whether this Type is a generic type definition. /// diff --git a/ComponentSelectorAdditions/DefaultConfig.cs b/ComponentSelectorAdditions/DefaultConfig.cs index 0d1dd20..c3197a4 100644 --- a/ComponentSelectorAdditions/DefaultConfig.cs +++ b/ComponentSelectorAdditions/DefaultConfig.cs @@ -18,6 +18,8 @@ public sealed class DefaultConfig : ConfigSection new ConfigKeyRange(32, 64) }; + private static readonly DefiningConfigKey _useSeparateConcreteGenericColor = new("UseSeparateConcreteGenericColor", "Use a blend between the generic component buttons' green and the non-generic component buttons' cyan for concrete generics.", () => true); + /// /// Gets this config's instance. /// @@ -41,6 +43,11 @@ public sealed class DefaultConfig : ConfigSection /// The height in canvas units. public float IndirectButtonHeight => _indirectButtonHeight; + /// + /// Gets whether to use a blend between the generic component buttons' green and the non-generic component buttons' cyan for concrete generics. + /// + public bool UseSeperateConcreteGenericColor => _useSeparateConcreteGenericColor; + /// public override Version Version { get; } = new Version(1, 0, 0); diff --git a/ComponentSelectorAdditions/DefaultHandler.cs b/ComponentSelectorAdditions/DefaultHandler.cs index 7d6f68d..f00fe60 100644 --- a/ComponentSelectorAdditions/DefaultHandler.cs +++ b/ComponentSelectorAdditions/DefaultHandler.cs @@ -23,6 +23,8 @@ public sealed class DefaultHandler : ConfiguredResoniteMonkey, ICancelableEventHandler, ICancelableEventHandler, IEventHandler, IEventHandler { + private static colorX _presetConcreteColor = MathX.Average(RadiantUI_Constants.Sub.GREEN, RadiantUI_Constants.Sub.CYAN); + /// public int Priority => HarmonyLib.Priority.Normal; @@ -197,8 +199,11 @@ void ICancelableEventHandler.Handle(BuildComponentBut var selector = eventData.Selector; var component = eventData.Component; + var tint = component.IsConcreteGeneric && ConfigSection.UseSeperateConcreteGenericColor + ? _presetConcreteColor + : component.IsGeneric ? RadiantUI_Constants.Sub.GREEN : RadiantUI_Constants.Sub.CYAN; + var category = GetPrettyPath(component.Category, eventData.RootCategory); - var tint = component.IsGeneric ? RadiantUI_Constants.Sub.GREEN : RadiantUI_Constants.Sub.CYAN; ButtonEventHandler callback = component.IsGeneric ? selector.OpenGenericTypesPressed : selector.OnAddComponentPressed; var argument = $"{(component.IsGeneric ? $"{path.Path}/{component.Type.AssemblyQualifiedName}" : selector.World.Types.EncodeType(component.Type))}{(component.IsGeneric && path.HasGroup ? $"?{path.Group}" : "")}"; diff --git a/ComponentSelectorAdditions/SearchBar.cs b/ComponentSelectorAdditions/SearchBar.cs index fb48c80..645929a 100644 --- a/ComponentSelectorAdditions/SearchBar.cs +++ b/ComponentSelectorAdditions/SearchBar.cs @@ -3,7 +3,6 @@ using FrooxEngine; using MonkeyLoader.Patching; using MonkeyLoader.Resonite; -using MonkeyLoader.Resonite.UI; using System; using System.Collections.Generic; using System.Linq; @@ -12,6 +11,7 @@ using MonkeyLoader.Events; using System.Threading; using System.Globalization; +using MonkeyLoader; namespace ComponentSelectorAdditions { @@ -19,8 +19,9 @@ internal sealed class SearchBar : ConfiguredResoniteMonkey, ICancelableEventHandler { private const string ProtoFluxPath = "/ProtoFlux/Runtimes/Execution/Nodes"; - private static readonly char[] _searchSplits = new[] { ' ', ',', '+', '|' }; + public override bool CanBeDisabled => true; + public int Priority => HarmonyLib.Priority.VeryHigh; public bool SkipCanceled => true; @@ -60,12 +61,10 @@ public void Handle(BuildSelectorHeaderEvent eventData) public void Handle(EnumerateCategoriesEvent eventData) { - if (!eventData.Path.HasSearch || ((eventData.Path.IsSelectorRoot || ConfigSection.AlwaysSearchRoot) && eventData.Path.Search.Length < 3)) + if (!eventData.Path.HasSearch || ((eventData.Path.IsSelectorRoot || ConfigSection.AlwaysSearchRoot) && eventData.Path.Search.Length < 3 && eventData.Path.SearchFragments.Length > 0)) return; - var search = eventData.Path.Search.Split(_searchSplits, StringSplitOptions.RemoveEmptyEntries); - - foreach (var category in SearchCategories(PickSearchCategory(eventData), search)) + foreach (var category in SearchCategories(PickSearchCategory(eventData), eventData.Path.SearchFragments)) eventData.AddItem(category); eventData.Canceled = true; @@ -76,26 +75,46 @@ public void Handle(EnumerateComponentsEvent eventData) if (!eventData.Path.HasSearch || (eventData.Path.IsSelectorRoot && eventData.Path.Search.Length < 3)) return; - var search = eventData.Path.Search.Split(_searchSplits, StringSplitOptions.RemoveEmptyEntries); var searchCategory = PickSearchCategory(eventData); var results = searchCategory.Elements - .Select(type => (Category: searchCategory, Type: type, Matches: SearchContains(type.Name, search))) + .Select(type => (Category: searchCategory, Type: type, Matches: SearchContains(type.Name, eventData.Path.SearchFragments))) .Concat( SearchCategories(searchCategory) .SelectMany(category => category.Elements - .Select(type => (Category: category, Type: type, Matches: SearchContains(type.Name, search))))) - .Where(match => match.Matches > 0) - .OrderByDescending(match => match.Matches) + .Select(type => (Category: category, Type: type, Matches: SearchContains(type.Name, eventData.Path.SearchFragments))))) + .Where(match => match.Matches > 0) // Extra weight for generic results when there's a generic in the search: + .OrderByDescending(match => match.Type.IsGenericTypeDefinition && eventData.Path.HasSearchGeneric ? match.Matches + 100 : match.Matches) .ThenBy(match => match.Type.Name) .Select(match => (Component: new ComponentResult(match.Category, match.Type), Order: -match.Matches)); var remaining = ConfigSection.MaxResultCount; var knownGroups = new HashSet(); + var parsedGeneric = eventData.Path.HasSearchGeneric ? eventData.Selector.World.Types.ParseNiceType(eventData.Path.SearchGeneric, true) : null; foreach (var result in results.TakeWhile(result => (!result.Component.HasGroup || knownGroups.Add(result.Component.Group) ? --remaining : remaining) >= 0)) + { eventData.AddItem(result.Component, result.Order); + if (result.Component.IsGeneric && parsedGeneric is not null) + { + try + { + var concreteType = result.Component.Type.MakeGenericType(parsedGeneric); + + if (!concreteType.IsValidGenericType(true)) + continue; + + --remaining; + eventData.AddItem(new(result.Component.Category, concreteType), result.Order - 1); + } + catch (Exception ex) + { + Logger.Warn(ex.LogFormat($"Failed to make generic type for component [{result.Component.NiceName}] with [{parsedGeneric.GetNiceName()}] (from \"{eventData.Path.GenericType}\")!")); + } + } + } + eventData.Canceled = true; } diff --git a/ComponentSelectorAdditions/SelectorPath.cs b/ComponentSelectorAdditions/SelectorPath.cs index d789197..1107d11 100644 --- a/ComponentSelectorAdditions/SelectorPath.cs +++ b/ComponentSelectorAdditions/SelectorPath.cs @@ -18,8 +18,13 @@ public sealed class SelectorPath /// public const string SearchSegment = "Search"; + private static readonly char _genericParamEnd = '>'; + private static readonly char _genericParamStart = '<'; + private static readonly char[] _pathSeparators = { '/', '\\' }; + private static readonly char[] _searchSplits = new[] { ' ', ',', '+', '|' }; + /// /// Gets whether this path targets a generic type. /// @@ -42,6 +47,12 @@ public sealed class SelectorPath [MemberNotNullWhen(true, nameof(Search))] public bool HasSearch => !string.IsNullOrWhiteSpace(Search); + /// + /// Gets whether this path has a generic argument for the search. + /// + [MemberNotNullWhen(true, nameof(SearchGeneric))] + public bool HasSearchGeneric => !string.IsNullOrWhiteSpace(SearchGeneric); + /// /// Gets whether this path targets the root category. /// @@ -72,9 +83,42 @@ public sealed class SelectorPath /// public string? Search { get; } + /// + /// Gets this path's search fragments (before the generic argument). + /// + public string[] SearchFragments { get; } = Array.Empty(); + + /// + /// Gets this path's generic argument for the search. + /// + public string? SearchGeneric { get; } + internal SelectorPath(string? rawPath, string? search, bool genericType, string? group, bool isSelectorRoot) { Search = search; + + if (!string.IsNullOrWhiteSpace(search)) + { + var genericParamStartIndex = search!.IndexOf(_genericParamStart); + + if (genericParamStartIndex > 0) + { + var generic = search[(genericParamStartIndex + 1)..]; + var starts = generic.Count(static c => c == _genericParamStart); + var ends = generic.Count(static c => c == _genericParamEnd); + + if (starts > ends) // Automatically add any missing > + generic += new string(_genericParamEnd, starts - ends); + else if (ends > starts) // Probably not gonna happen often, but if someone adds a closing > to much... + generic = generic.Remove(generic.Length - (ends - starts)); + + SearchGeneric = generic; + search = search[..genericParamStartIndex]; + } + + SearchFragments = search.Split(_searchSplits, StringSplitOptions.RemoveEmptyEntries); + } + GenericType = genericType; Group = group; IsSelectorRoot = isSelectorRoot; From 1428d976f7acb2e5ad63e719a567799fc8079617 Mon Sep 17 00:00:00 2001 From: Arne Kiesewetter Date: Thu, 24 Apr 2025 21:43:53 +0200 Subject: [PATCH 2/2] Finalize generic search argument handling and add tooltips --- ComponentSelectorAdditions/DefaultConfig.cs | 16 +++++++--- ComponentSelectorAdditions/DefaultHandler.cs | 8 ++--- .../Events/EnumerateComponentsEvent.cs | 32 +++++++++++++++---- ComponentSelectorAdditions/Locale/de.json | 4 +++ ComponentSelectorAdditions/Locale/en.json | 4 +++ ComponentSelectorAdditions/SearchBar.cs | 12 ++++--- ComponentSelectorAdditions/SelectorPath.cs | 10 +++--- 7 files changed, 64 insertions(+), 22 deletions(-) diff --git a/ComponentSelectorAdditions/DefaultConfig.cs b/ComponentSelectorAdditions/DefaultConfig.cs index c3197a4..b6e7e4d 100644 --- a/ComponentSelectorAdditions/DefaultConfig.cs +++ b/ComponentSelectorAdditions/DefaultConfig.cs @@ -1,5 +1,7 @@ -using MonkeyLoader.Configuration; +using Elements.Core; +using MonkeyLoader.Configuration; using System; +using System.Diagnostics.CodeAnalysis; namespace ComponentSelectorAdditions { @@ -18,7 +20,7 @@ public sealed class DefaultConfig : ConfigSection new ConfigKeyRange(32, 64) }; - private static readonly DefiningConfigKey _useSeparateConcreteGenericColor = new("UseSeparateConcreteGenericColor", "Use a blend between the generic component buttons' green and the non-generic component buttons' cyan for concrete generics.", () => true); + private static readonly DefiningConfigKey _separateConcreteGenericColor = new("separateConcreteGenericColor", "The color to use for concrete generic buttons, if defined. Defaults to a blend between the generic component buttons' green and the non-generic component buttons' cyan.", () => colorX.FromHexCode("#255447")); /// /// Gets this config's instance. @@ -44,9 +46,15 @@ public sealed class DefaultConfig : ConfigSection public float IndirectButtonHeight => _indirectButtonHeight; /// - /// Gets whether to use a blend between the generic component buttons' green and the non-generic component buttons' cyan for concrete generics. + /// Gets the color to use for concrete generic buttons, if defined. /// - public bool UseSeperateConcreteGenericColor => _useSeparateConcreteGenericColor; + public colorX? SeparateConcreteGenericColor => _separateConcreteGenericColor; + + /// + /// Gets whether the SeparateConcreteGenericColor is defined. + /// + [MemberNotNullWhen(true, nameof(SeparateConcreteGenericColor))] + public bool UseSeparateConcreteGenericColor => _separateConcreteGenericColor.GetValue().HasValue; /// public override Version Version { get; } = new Version(1, 0, 0); diff --git a/ComponentSelectorAdditions/DefaultHandler.cs b/ComponentSelectorAdditions/DefaultHandler.cs index f00fe60..80a653e 100644 --- a/ComponentSelectorAdditions/DefaultHandler.cs +++ b/ComponentSelectorAdditions/DefaultHandler.cs @@ -12,6 +12,7 @@ using System.Text; using System.Threading.Tasks; using System.IO; +using MonkeyLoader.Resonite.UI; namespace ComponentSelectorAdditions { @@ -23,8 +24,6 @@ public sealed class DefaultHandler : ConfiguredResoniteMonkey, ICancelableEventHandler, ICancelableEventHandler, IEventHandler, IEventHandler { - private static colorX _presetConcreteColor = MathX.Average(RadiantUI_Constants.Sub.GREEN, RadiantUI_Constants.Sub.CYAN); - /// public int Priority => HarmonyLib.Priority.Normal; @@ -64,6 +63,7 @@ public static TextField MakeGenericArgumentInput(UIBuilder ui, Type component, T { var textField = ui.TextField(parseRTF: false); textField.Text.NullContent.AssignLocaleString(Mod.GetLocaleString("EnterType")); + textField.Slot.GetComponent