Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ComponentSelectorAdditions/ComponentResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public sealed class ComponentResult
[MemberNotNullWhen(true, nameof(Group), nameof(GroupName))]
public bool HasGroup => Group is not null;

/// <summary>
/// Gets whether this <see cref="Type">Type</see> is a concrete generic.
/// </summary>
public bool IsConcreteGeneric => Type.IsGenericType && !IsGeneric;

/// <summary>
/// Gets whether this <see cref="Type">Type</see> is a generic type definition.
/// </summary>
Expand Down
17 changes: 16 additions & 1 deletion ComponentSelectorAdditions/DefaultConfig.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using MonkeyLoader.Configuration;
using Elements.Core;
using MonkeyLoader.Configuration;
using System;
using System.Diagnostics.CodeAnalysis;

namespace ComponentSelectorAdditions
{
Expand All @@ -18,6 +20,8 @@ public sealed class DefaultConfig : ConfigSection
new ConfigKeyRange<float>(32, 64)
};

private static readonly DefiningConfigKey<colorX?> _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"));

/// <summary>
/// Gets this config's instance.
/// </summary>
Expand All @@ -41,6 +45,17 @@ public sealed class DefaultConfig : ConfigSection
/// <value>The height in canvas units.</value>
public float IndirectButtonHeight => _indirectButtonHeight;

/// <summary>
/// Gets the color to use for concrete generic buttons, if defined.
/// </summary>
public colorX? SeparateConcreteGenericColor => _separateConcreteGenericColor;

/// <summary>
/// Gets whether the <see cref="SeparateConcreteGenericColor">SeparateConcreteGenericColor</see> is defined.
/// </summary>
[MemberNotNullWhen(true, nameof(SeparateConcreteGenericColor))]
public bool UseSeparateConcreteGenericColor => _separateConcreteGenericColor.GetValue().HasValue;

/// <inheritdoc/>
public override Version Version { get; } = new Version(1, 0, 0);

Expand Down
7 changes: 6 additions & 1 deletion ComponentSelectorAdditions/DefaultHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Text;
using System.Threading.Tasks;
using System.IO;
using MonkeyLoader.Resonite.UI;

namespace ComponentSelectorAdditions
{
Expand Down Expand Up @@ -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<Button>()?.WithTooltip(Mod.GetLocaleString("EnterType.Tooltip", "parameter", genericArgument.Name));

return textField;
}, out var label);
Expand Down Expand Up @@ -197,8 +199,11 @@ void ICancelableEventHandler<BuildComponentButtonEvent>.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<string> 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}" : "")}";

Expand Down
32 changes: 26 additions & 6 deletions ComponentSelectorAdditions/Events/EnumerateComponentsEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,27 @@ public sealed class EnumerateComponentsEvent : CancelableSortedItemsEvent<Compon
public Predicate<Type> ComponentFilter { get; }

public override IEnumerable<ComponentResult> 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<Type> RootCategory { get; }

public ComponentSelector Selector { get; }

/// <inheritdoc/>
Expand All @@ -32,5 +43,14 @@ internal EnumerateComponentsEvent(ComponentSelector selector, SelectorPath path,
RootCategory = rootCategory;
ComponentFilter = componentFilter;
}

private static int GenericnessRating(KeyValuePair<ComponentResult, int> entry)
=> entry.Key.Type.IsGenericType ? (entry.Key.Type.IsGenericTypeDefinition ? 1 : 0) : 2;

private static string Name(KeyValuePair<ComponentResult, int> entry)
=> entry.Key.GroupName ?? entry.Key.Type.Name;

private static int Value(KeyValuePair<ComponentResult, int> entry)
=> entry.Value;
}
}
4 changes: 4 additions & 0 deletions ComponentSelectorAdditions/Locale/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<i>Suchen ...</i>",
"ComponentSelectorAdditions.Search.Clear": "Leert das Suchfeld.",
"ComponentSelectorAdditions.Search.Tooltip": "<b>Gebe hier deine Suchbegriffe ein. Die Ergebnisse erscheinen, wenn du das tippen pausierst.</b><size=50%><br/><br/></size>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.<br/>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": "<i>Typ eingeben...</i>",
"ComponentSelectorAdditions.EnterType.Tooltip": "<b>Gebe hier die Typdefinition für den generischen Parameter {parameter} ein. Dieser darf uneindeutig sein.</b><size=50%><br/><br/></size>Die Typdefinition sollte so geschrieben werden wie in C#, genauso wie Komponentennamen es zeigen. Ein Integer Field wäre zum Beispiel: IField<int>",

"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.",
Expand Down
4 changes: 4 additions & 0 deletions ComponentSelectorAdditions/Locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
"authors": [ "Banane9" ],
"messages": {
"ComponentSelectorAdditions.Search": "<i>Search ...</i>",
"ComponentSelectorAdditions.Search.Clear": "Clears the search field.",
"ComponentSelectorAdditions.Search.Tooltip": "<b>Enter your search terms here. Results will appear when you pause typing.</b><size=50%><br/><br/></size>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.<br/>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": "<i>Enter Type ...</i>",
"ComponentSelectorAdditions.EnterType.Tooltip": "<b>Enter your type definition for the generic parameter {parameter} here. This is allowed to be ambiguous.</b><size=50%><br/><br/></size>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<int>",

"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.",
Expand Down
43 changes: 33 additions & 10 deletions ComponentSelectorAdditions/SearchBar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -12,15 +11,19 @@
using MonkeyLoader.Events;
using System.Threading;
using System.Globalization;
using MonkeyLoader;
using MonkeyLoader.Resonite.UI;
using FrooxEngine.UIX;

namespace ComponentSelectorAdditions
{
internal sealed class SearchBar : ConfiguredResoniteMonkey<SearchBar, SearchConfig>, IEventHandler<BuildSelectorHeaderEvent>,
ICancelableEventHandler<EnumerateCategoriesEvent>, ICancelableEventHandler<EnumerateComponentsEvent>
{
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;
Expand All @@ -41,6 +44,8 @@ public void Handle(BuildSelectorHeaderEvent eventData)

ui.Style.FlexibleWidth = 1;
var textField = ui.TextField(null!, parseRTF: false);
textField.Slot.GetComponent<Button>()?.WithTooltip(Mod.GetLocaleString("Search.Tooltip"));

var details = new SelectorSearchBar(searchLayout, textField.Editor.Target, () => ConfigSection.SearchRefreshDelay);
eventData.SearchBar = details;

Expand All @@ -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<ButtonValueSet<string>>();
clearAction.TargetValue.Target = details.Text.Content;

Expand All @@ -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;
Expand All @@ -76,26 +79,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)))))
.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)
.Select(match => (Component: new ComponentResult(match.Category, match.Type), Order: -match.Matches));

var remaining = ConfigSection.MaxResultCount;
var knownGroups = new HashSet<string>();
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;
}

Expand Down
46 changes: 46 additions & 0 deletions ComponentSelectorAdditions/SelectorPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ public sealed class SelectorPath
/// </summary>
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' };

/// <summary>
/// Gets whether this path targets a generic type.
/// </summary>
Expand All @@ -42,6 +47,12 @@ public sealed class SelectorPath
[MemberNotNullWhen(true, nameof(Search))]
public bool HasSearch => !string.IsNullOrWhiteSpace(Search);

/// <summary>
/// Gets whether this path has a <see cref="SearchGeneric">generic argument</see> for the search.
/// </summary>
[MemberNotNullWhen(true, nameof(SearchGeneric))]
public bool HasSearchGeneric => !string.IsNullOrWhiteSpace(SearchGeneric);

/// <summary>
/// Gets whether this path targets the root category.
/// </summary>
Expand Down Expand Up @@ -72,9 +83,44 @@ public sealed class SelectorPath
/// </summary>
public string? Search { get; }

/// <summary>
/// Gets this path's search fragments (before the <see cref="SearchGeneric">generic argument</see>).
/// </summary>
public string[] SearchFragments { get; } = Array.Empty<string>();

/// <summary>
/// Gets this path's generic argument for the search.
/// </summary>
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;
Expand Down
Loading