Add more filter options.

This commit is contained in:
Ottermandias 2024-06-07 16:34:09 +02:00
parent 2e9f184454
commit 50a7e7efb7
4 changed files with 171 additions and 76 deletions

@ -1 +1 @@
Subproject commit 5de708b27ed45c9cdead71742c7061ad9ce64323 Subproject commit ac176daf068f42d0b04a77dbc149f68a425fd460

View file

@ -1,6 +1,5 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;

View file

@ -23,17 +23,20 @@ namespace Penumbra.UI.ModsTab;
public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSystemSelector.ModState> public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSystemSelector.ModState>
{ {
private readonly CommunicatorService _communicator; private readonly CommunicatorService _communicator;
private readonly MessageService _messager; private readonly MessageService _messager;
private readonly Configuration _config; private readonly Configuration _config;
private readonly FileDialogService _fileDialog; private readonly FileDialogService _fileDialog;
private readonly ModManager _modManager; private readonly ModManager _modManager;
private readonly CollectionManager _collectionManager; private readonly CollectionManager _collectionManager;
private readonly TutorialService _tutorial; private readonly TutorialService _tutorial;
private readonly ModImportManager _modImportManager; private readonly ModImportManager _modImportManager;
private readonly IDragDropManager _dragDrop; private readonly IDragDropManager _dragDrop;
public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; private readonly ModSearchStringSplitter Filter = new();
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty;
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
public ModFileSystemSelector(IKeyState keyState, CommunicatorService communicator, ModFileSystem fileSystem, ModManager modManager, public ModFileSystemSelector(IKeyState keyState, CommunicatorService communicator, ModFileSystem fileSystem, ModManager modManager,
CollectionManager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog, CollectionManager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog,
@ -568,78 +571,49 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
public ModPriority Priority; public ModPriority Priority;
} }
private const StringComparison IgnoreCase = StringComparison.OrdinalIgnoreCase; private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods;
private LowerString _modFilter = LowerString.Empty;
private int _filterType = -1;
private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods;
private ChangedItemDrawer.ChangedItemIcon _slotFilter = 0;
private void SetFilterTooltip() private void SetFilterTooltip()
{ {
FilterTooltip = "Filter mods for those where their full paths or names contain the given substring.\n" FilterTooltip = "Filter mods for those where their full paths or names contain the given strings, split by spaces.\n"
+ "Enter c:[string] to filter for mods changing specific items.\n" + "Enter c:[string] to filter for mods changing specific items.\n"
+ "Enter t:[string] to filter for mods set to specific tags.\n" + "Enter t:[string] to filter for mods set to specific tags.\n"
+ "Enter n:[string] to filter only for mod names and no paths.\n" + "Enter n:[string] to filter only for mod names and no paths.\n"
+ "Enter a:[string] to filter for mods by specific authors.\n" + "Enter a:[string] to filter for mods by specific authors.\n"
+ $"Enter s:[string] to filter for mods by the categories of the items they change (1-{ChangedItemDrawer.NumCategories + 1} or partial category name).\n" + $"Enter s:[string] to filter for mods by the categories of the items they change (1-{ChangedItemDrawer.NumCategories + 1} or partial category name).\n\n"
+ "Use None as a placeholder value that only matches empty lists or names."; + "Use None as a placeholder value that only matches empty lists or names.\n"
+ "Regularly, a mod has to match all supplied criteria separately.\n"
+ "Put a - in front of a search token to search only for mods not matching the criterion.\n"
+ "Put a ? in front of a search token to search for mods matching at least one of the '?'-criteria.\n"
+ "Wrap spaces in \"[string with space]\" to match this exact combination of words.\n\n"
+ "Example: 't:Tag1 t:\"Tag 2\" -t:Tag3 -a:None s:Body -c:Hempen ?c:Camise ?n:Top' will match any mod that\n"
+ " - contains the tags 'tag1' and 'tag 2'\n"
+ " - does not contain the tag 'tag3'\n"
+ " - has any author set (negating None means Any)\n"
+ " - changes an item of the 'Body' category\n"
+ " - and either contains a changed item with 'camise' in it's name, or has 'top' in the mod's name.";
} }
/// <summary> Appropriately identify and set the string filter and its type. </summary> /// <summary> Appropriately identify and set the string filter and its type. </summary>
protected override bool ChangeFilter(string filterValue) protected override bool ChangeFilter(string filterValue)
{ {
(_modFilter, _filterType) = filterValue.Length switch Filter.Parse(filterValue);
{
0 => (LowerString.Empty, -1),
> 1 when filterValue[1] == ':' =>
filterValue[0] switch
{
'n' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1),
'N' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1),
'a' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 2),
'A' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 2),
'c' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 3),
'C' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 3),
't' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 4),
'T' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 4),
's' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 5),
'S' => filterValue.Length == 2 ? (LowerString.Empty, -1) : ParseFilter(filterValue, 5),
_ => (new LowerString(filterValue), 0),
},
_ => (new LowerString(filterValue), 0),
};
return true; return true;
} }
private const int EmptyOffset = 128;
private (LowerString, int) ParseFilter(string value, int id)
{
value = value[2..];
var lower = new LowerString(value);
if (id == 5 && !ChangedItemDrawer.TryParsePartial(lower.Lower, out _slotFilter))
_slotFilter = 0;
return (lower, lower.Lower is "none" ? id + EmptyOffset : id);
}
/// <summary> /// <summary>
/// Check the state filter for a specific pair of has/has-not flags. /// Check the state filter for a specific pair of has/has-not flags.
/// Uses count == 0 to check for has-not and count != 0 for has. /// Uses count == 0 to check for has-not and count != 0 for has.
/// Returns true if it should be filtered and false if not. /// Returns true if it should be filtered and false if not.
/// </summary> /// </summary>
private bool CheckFlags(int count, ModFilter hasNoFlag, ModFilter hasFlag) private bool CheckFlags(int count, ModFilter hasNoFlag, ModFilter hasFlag)
{ => count switch
return count switch
{ {
0 when _stateFilter.HasFlag(hasNoFlag) => false, 0 when _stateFilter.HasFlag(hasNoFlag) => false,
0 => true, 0 => true,
_ when _stateFilter.HasFlag(hasFlag) => false, _ when _stateFilter.HasFlag(hasFlag) => false,
_ => true, _ => true,
}; };
}
/// <summary> /// <summary>
/// The overwritten filter method also computes the state. /// The overwritten filter method also computes the state.
@ -653,7 +627,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
{ {
state = default; state = default;
return ModFilterExtensions.UnfilteredStateMods != _stateFilter return ModFilterExtensions.UnfilteredStateMods != _stateFilter
|| FilterValue.Length > 0 && !f.FullName().Contains(FilterValue, IgnoreCase); || !Filter.IsVisible(f);
} }
return ApplyFiltersAndState((ModFileSystem.Leaf)path, out state); return ApplyFiltersAndState((ModFileSystem.Leaf)path, out state);
@ -661,23 +635,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
/// <summary> Apply the string filters. </summary> /// <summary> Apply the string filters. </summary>
private bool ApplyStringFilters(ModFileSystem.Leaf leaf, Mod mod) private bool ApplyStringFilters(ModFileSystem.Leaf leaf, Mod mod)
{ => !Filter.IsVisible(leaf);
return _filterType switch
{
-1 => false,
0 => !(leaf.FullName().Contains(_modFilter.Lower, IgnoreCase) || mod.Name.Contains(_modFilter)),
1 => !mod.Name.Contains(_modFilter),
2 => !mod.Author.Contains(_modFilter),
3 => !mod.LowerChangedItemsString.Contains(_modFilter.Lower),
4 => !mod.AllTagsLower.Contains(_modFilter.Lower),
5 => mod.ChangedItems.All(p => (ChangedItemDrawer.GetCategoryIcon(p.Key, p.Value) & _slotFilter) == 0),
2 + EmptyOffset => !mod.Author.IsEmpty,
3 + EmptyOffset => mod.LowerChangedItemsString.Length > 0,
4 + EmptyOffset => mod.AllTagsLower.Length > 0,
5 + EmptyOffset => mod.ChangedItems.Count == 0,
_ => false, // Should never happen
};
}
/// <summary> Only get the text color for a mod if no filters are set. </summary> /// <summary> Only get the text color for a mod if no filters are set. </summary>
private ColorId GetTextColor(Mod mod, ModSettings? settings, ModCollection collection) private ColorId GetTextColor(Mod mod, ModSettings? settings, ModCollection collection)

View file

@ -0,0 +1,138 @@
using OtterGui.Filesystem;
using OtterGui.Filesystem.Selector;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
namespace Penumbra.UI.ModsTab;
public enum ModSearchType : byte
{
Default = 0,
ChangedItem,
Tag,
Name,
Author,
Category,
}
public sealed class ModSearchStringSplitter : SearchStringSplitter<ModSearchType, FileSystem<Mod>.Leaf, ModSearchStringSplitter.Entry>
{
public readonly struct Entry : ISplitterEntry<ModSearchType, Entry>
{
public string Needle { get; init; }
public ModSearchType Type { get; init; }
public ChangedItemDrawer.ChangedItemIcon IconFilter { get; init; }
public bool Contains(Entry other)
{
if (Type != other.Type)
return false;
if (Type is ModSearchType.Category)
return IconFilter == other.IconFilter;
return Needle.Contains(other.Needle);
}
}
protected override bool ConvertToken(char token, out ModSearchType val)
{
val = token switch
{
'c' or 'C' => ModSearchType.ChangedItem,
't' or 'T' => ModSearchType.Tag,
'n' or 'N' => ModSearchType.Name,
'a' or 'A' => ModSearchType.Author,
's' or 'S' => ModSearchType.Category,
_ => ModSearchType.Default,
};
return val is not ModSearchType.Default;
}
protected override bool AllowsNone(ModSearchType val)
=> val switch
{
ModSearchType.Author => true,
ModSearchType.ChangedItem => true,
ModSearchType.Tag => true,
ModSearchType.Category => true,
_ => false,
};
protected override void PostProcessing()
{
base.PostProcessing();
HandleList(General);
HandleList(Forced);
HandleList(Negated);
return;
static void HandleList(List<Entry> list)
{
for (var i = 0; i < list.Count; ++i)
{
var entry = list[i];
if (entry.Type is not ModSearchType.Category)
continue;
if (ChangedItemDrawer.TryParsePartial(entry.Needle, out var icon))
list[i] = entry with
{
IconFilter = icon,
Needle = string.Empty,
};
else
list.RemoveAt(i--);
}
}
}
public bool IsVisible(ModFileSystem.Folder folder)
{
switch (State)
{
case FilterState.NoFilters: return true;
case FilterState.NoMatches: return false;
}
var fullName = folder.FullName();
return Forced.All(i => MatchesName(i, folder.Name, fullName))
&& !Negated.Any(i => MatchesName(i, folder.Name, fullName))
&& (General.Count == 0 || General.Any(i => MatchesName(i, folder.Name, fullName)));
}
protected override bool Matches(Entry entry, ModFileSystem.Leaf leaf)
=> entry.Type switch
{
ModSearchType.Default => leaf.FullName().AsSpan().Contains(entry.Needle, StringComparison.OrdinalIgnoreCase)
|| leaf.Value.Name.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal),
ModSearchType.ChangedItem => leaf.Value.LowerChangedItemsString.AsSpan().Contains(entry.Needle, StringComparison.Ordinal),
ModSearchType.Tag => leaf.Value.AllTagsLower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal),
ModSearchType.Name => leaf.Value.Name.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal),
ModSearchType.Author => leaf.Value.Author.Lower.AsSpan().Contains(entry.Needle, StringComparison.Ordinal),
ModSearchType.Category => leaf.Value.ChangedItems.Any(p
=> (ChangedItemDrawer.GetCategoryIcon(p.Key, p.Value) & entry.IconFilter) != 0),
_ => true,
};
protected override bool MatchesNone(ModSearchType type, bool negated, ModFileSystem.Leaf haystack)
=> type switch
{
ModSearchType.Author when negated => !haystack.Value.Author.IsEmpty,
ModSearchType.Author => haystack.Value.Author.IsEmpty,
ModSearchType.ChangedItem when negated => haystack.Value.LowerChangedItemsString.Length > 0,
ModSearchType.ChangedItem => haystack.Value.LowerChangedItemsString.Length == 0,
ModSearchType.Tag when negated => haystack.Value.AllTagsLower.Length > 0,
ModSearchType.Tag => haystack.Value.AllTagsLower.Length == 0,
ModSearchType.Category when negated => haystack.Value.ChangedItems.Count > 0,
ModSearchType.Category => haystack.Value.ChangedItems.Count == 0,
_ => true,
};
private static bool MatchesName(Entry entry, ReadOnlySpan<char> name, ReadOnlySpan<char> fullName)
=> entry.Type switch
{
ModSearchType.Default => fullName.Contains(entry.Needle, StringComparison.OrdinalIgnoreCase),
ModSearchType.Name => name.Contains(entry.Needle, StringComparison.OrdinalIgnoreCase),
_ => false,
};
}