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..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,6 +20,8 @@ public sealed class DefaultConfig : ConfigSection
new ConfigKeyRange(32, 64)
};
+ 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.
///
@@ -41,6 +45,17 @@ public sealed class DefaultConfig : ConfigSection
/// The height in canvas units.
public float IndirectButtonHeight => _indirectButtonHeight;
+ ///
+ /// Gets the color to use for concrete generic buttons, if defined.
+ ///
+ 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 7d6f68d..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
{
@@ -62,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()?.WithTooltip(Mod.GetLocaleString("EnterType.Tooltip", "parameter", genericArgument.Name));
return textField;
}, out var label);
@@ -197,8 +199,11 @@ void ICancelableEventHandler.Handle(BuildComponentBut
var selector = eventData.Selector;
var component = eventData.Component;
+ var tint = component.IsConcreteGeneric && ConfigSection.UseSeparateConcreteGenericColor
+ ? ConfigSection.SeparateConcreteGenericColor.Value
+ : 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/Events/EnumerateComponentsEvent.cs b/ComponentSelectorAdditions/Events/EnumerateComponentsEvent.cs
index bd14af6..3e46533 100644
--- a/ComponentSelectorAdditions/Events/EnumerateComponentsEvent.cs
+++ b/ComponentSelectorAdditions/Events/EnumerateComponentsEvent.cs
@@ -12,16 +12,27 @@ public sealed class EnumerateComponentsEvent : CancelableSortedItemsEvent ComponentFilter { get; }
public override IEnumerable Items
- => sortableItems
- .Where(entry => ComponentFilter(entry.Key.Type))
- .Where(entry => Selector.World.Types.IsSupported(entry.Key.Type))
- .OrderBy(entry => entry.Value)
- .ThenBy(entry => entry.Key.GroupName ?? entry.Key.Type.Name)
- .Select(entry => entry.Key);
+ {
+ get
+ {
+ var items = sortableItems
+ .Where(entry => ComponentFilter(entry.Key.Type))
+ .Where(entry => Selector.World.Types.IsSupported(entry.Key.Type))
+ .OrderBy(Value)
+ .ThenBy(Name);
+
+ // Sort for (concrete) genericness when search has a generic argument
+ if (Path.HasSearchGeneric)
+ items = items.ThenBy(GenericnessRating);
+
+ return items.Select(entry => entry.Key);
+ }
+ }
public SelectorPath Path { get; }
public CategoryNode RootCategory { get; }
+
public ComponentSelector Selector { get; }
///
@@ -32,5 +43,14 @@ internal EnumerateComponentsEvent(ComponentSelector selector, SelectorPath path,
RootCategory = rootCategory;
ComponentFilter = componentFilter;
}
+
+ private static int GenericnessRating(KeyValuePair entry)
+ => entry.Key.Type.IsGenericType ? (entry.Key.Type.IsGenericTypeDefinition ? 1 : 0) : 2;
+
+ private static string Name(KeyValuePair entry)
+ => entry.Key.GroupName ?? entry.Key.Type.Name;
+
+ private static int Value(KeyValuePair entry)
+ => entry.Value;
}
}
\ No newline at end of file
diff --git a/ComponentSelectorAdditions/Locale/de.json b/ComponentSelectorAdditions/Locale/de.json
index 68944c2..cf842a4 100644
--- a/ComponentSelectorAdditions/Locale/de.json
+++ b/ComponentSelectorAdditions/Locale/de.json
@@ -6,7 +6,11 @@
"ComponentSelectorAdditions.Description": "Dieser MonkeyLoader Mod für Resonite strukturiert Komponenten-Selektoren und ProtoFlux Nodebrowser neu und fügt eine Suchfunktion sowie Kategorien für zuletzt genutzte und favorisierte Komponenten / Nodes hinzu.",
"ComponentSelectorAdditions.Search": "Suchen ... ",
+ "ComponentSelectorAdditions.Search.Clear": "Leert das Suchfeld.",
+ "ComponentSelectorAdditions.Search.Tooltip": "Gebe hier deine Suchbegriffe ein. Die Ergebnisse erscheinen, wenn du das tippen pausierst. Mehrere Suchbegriffe können mit Leerzeichen getrennt werden. Für jeden Begriff wird überprüft, ob dieser im Namen einer Kategorie oder Komponente / Node enthalten ist. Je mehr Begriffe enthalten sind, desto besser wird das Ergebnis gewertet. Du kannst einen generischen Typparameter anwenden, indem du dessen volle Definition nach einem < eingibst, genauso wie du es bei der normallen Erstellung einer generischen Komponente tun würdest. Bereits das Hinzufügen eines < verbessert die Wertung aller generischen Ergebnisse.",
+
"ComponentSelectorAdditions.EnterType": "Typ eingeben... ",
+ "ComponentSelectorAdditions.EnterType.Tooltip": "Gebe hier die Typdefinition für den generischen Parameter {parameter} ein. Dieser darf uneindeutig sein. Die Typdefinition sollte so geschrieben werden wie in C#, genauso wie Komponentennamen es zeigen. Ein Integer Field wäre zum Beispiel: IField",
"ComponentSelectorAdditions.CaseSensitivityFix.Description": "Macht das Auswählen von eigenen generischen Typparametern unabhängig von Groß- und Kleinschreibung. Kann deaktiviert werden, wenn der Bug behoben wurde.",
"ComponentSelectorAdditions.CurrentPathIndicator.Description": "Fügt eine Anzeige für den Pfad der aktuellen Kategorie (und des generischen Typs) zu Komponenten-Selektoren hinzu.",
diff --git a/ComponentSelectorAdditions/Locale/en.json b/ComponentSelectorAdditions/Locale/en.json
index a668bcc..5d0a9ce 100644
--- a/ComponentSelectorAdditions/Locale/en.json
+++ b/ComponentSelectorAdditions/Locale/en.json
@@ -3,7 +3,11 @@
"authors": [ "Banane9" ],
"messages": {
"ComponentSelectorAdditions.Search": "Search ... ",
+ "ComponentSelectorAdditions.Search.Clear": "Clears the search field.",
+ "ComponentSelectorAdditions.Search.Tooltip": "Enter your search terms here. Results will appear when you pause typing. Multiple search terms can be split using spaces. Each term gets checked for inclusion in the name of categories or a component / node. The more terms match, the higher the result is ranked. You can apply a custom generic type by entering its full definition after a <, just like you would when normally creating a generic component. Simply adding the < will already rank up all generic results.",
+
"ComponentSelectorAdditions.EnterType": "Enter Type ... ",
+ "ComponentSelectorAdditions.EnterType.Tooltip": "Enter your type definition for the generic parameter {parameter} here. This is allowed to be ambiguous. The type definition should be written like it would be in C#, just like component names show it. For example, an integer field would be: IField",
"ComponentSelectorAdditions.CaseSensitivityFix.Description": "Makes picking custom generic type parameters case-insensitive. Can be disabled when the bug gets fixed.",
"ComponentSelectorAdditions.CurrentPathIndicator.Description": "Adds an indicator for the current category (and generic Type) path to omponent selectors.",
diff --git a/ComponentSelectorAdditions/SearchBar.cs b/ComponentSelectorAdditions/SearchBar.cs
index fb48c80..a7a5f98 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,9 @@
using MonkeyLoader.Events;
using System.Threading;
using System.Globalization;
+using MonkeyLoader;
+using MonkeyLoader.Resonite.UI;
+using FrooxEngine.UIX;
namespace ComponentSelectorAdditions
{
@@ -19,8 +21,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;
@@ -41,6 +44,8 @@ public void Handle(BuildSelectorHeaderEvent eventData)
ui.Style.FlexibleWidth = 1;
var textField = ui.TextField(null!, parseRTF: false);
+ textField.Slot.GetComponent()?.WithTooltip(Mod.GetLocaleString("Search.Tooltip"));
+
var details = new SelectorSearchBar(searchLayout, textField.Editor.Target, () => ConfigSection.SearchRefreshDelay);
eventData.SearchBar = details;
@@ -50,7 +55,7 @@ public void Handle(BuildSelectorHeaderEvent eventData)
ui.Style.FlexibleWidth = -1;
ui.Style.ButtonTextAlignment = Alignment.MiddleCenter;
- var clearButton = ui.Button("∅");
+ var clearButton = ui.Button("∅").WithTooltip(Mod.GetLocaleString("Search.Clear"));
var clearAction = clearButton.Slot.AttachComponent>();
clearAction.TargetValue.Target = details.Text.Content;
@@ -60,12 +65,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,15 +79,14 @@ 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)))))
+ .Select(type => (Category: category, Type: type, Matches: SearchContains(type.Name, eventData.Path.SearchFragments)))))
.Where(match => match.Matches > 0)
.OrderByDescending(match => match.Matches)
.ThenBy(match => match.Type.Name)
@@ -92,10 +94,31 @@ public void Handle(EnumerateComponentsEvent eventData)
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);
+ }
+ 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..bbfec20 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[] { ' ', '.', ',', ';', '?', '!', '+', '|', '&', '`', '´', '"', '(', ')', '/', '\\', '\n', '\r', '\t' };
+
///
/// 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,44 @@ 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))
+ {
+ search = search!.Replace('[', _genericParamStart).Replace(']', _genericParamEnd);
+
+ var genericParamStartIndex = search.IndexOf(_genericParamStart);
+
+ if (genericParamStartIndex > 0)
+ {
+ var generic = search[(genericParamStartIndex + 1)..].Trim();
+ 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 too many closing > ...
+ generic = generic.Remove(generic.Length - (ends - starts));
+
+ SearchGeneric = generic;
+ search = search[..genericParamStartIndex];
+ }
+
+ SearchFragments = search.Split(_searchSplits, StringSplitOptions.RemoveEmptyEntries);
+ }
+
GenericType = genericType;
Group = group;
IsSelectorRoot = isSelectorRoot;