mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Add more filter options.
This commit is contained in:
parent
2e9f184454
commit
50a7e7efb7
4 changed files with 171 additions and 76 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
|||
Subproject commit 5de708b27ed45c9cdead71742c7061ad9ce64323
|
||||
Subproject commit ac176daf068f42d0b04a77dbc149f68a425fd460
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Interop.Structs;
|
||||
|
|
|
|||
|
|
@ -23,17 +23,20 @@ namespace Penumbra.UI.ModsTab;
|
|||
|
||||
public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSystemSelector.ModState>
|
||||
{
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly MessageService _messager;
|
||||
private readonly Configuration _config;
|
||||
private readonly FileDialogService _fileDialog;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly TutorialService _tutorial;
|
||||
private readonly ModImportManager _modImportManager;
|
||||
private readonly IDragDropManager _dragDrop;
|
||||
public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty;
|
||||
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly MessageService _messager;
|
||||
private readonly Configuration _config;
|
||||
private readonly FileDialogService _fileDialog;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly TutorialService _tutorial;
|
||||
private readonly ModImportManager _modImportManager;
|
||||
private readonly IDragDropManager _dragDrop;
|
||||
private readonly ModSearchStringSplitter Filter = new();
|
||||
|
||||
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,
|
||||
CollectionManager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog,
|
||||
|
|
@ -568,78 +571,49 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
public ModPriority Priority;
|
||||
}
|
||||
|
||||
private const StringComparison IgnoreCase = StringComparison.OrdinalIgnoreCase;
|
||||
private LowerString _modFilter = LowerString.Empty;
|
||||
private int _filterType = -1;
|
||||
private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods;
|
||||
private ChangedItemDrawer.ChangedItemIcon _slotFilter = 0;
|
||||
private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods;
|
||||
|
||||
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 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 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"
|
||||
+ "Use None as a placeholder value that only matches empty lists or names.";
|
||||
+ $"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.\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>
|
||||
protected override bool ChangeFilter(string filterValue)
|
||||
{
|
||||
(_modFilter, _filterType) = filterValue.Length switch
|
||||
{
|
||||
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),
|
||||
};
|
||||
|
||||
Filter.Parse(filterValue);
|
||||
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>
|
||||
/// 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.
|
||||
/// Returns true if it should be filtered and false if not.
|
||||
/// </summary>
|
||||
private bool CheckFlags(int count, ModFilter hasNoFlag, ModFilter hasFlag)
|
||||
{
|
||||
return count switch
|
||||
=> count switch
|
||||
{
|
||||
0 when _stateFilter.HasFlag(hasNoFlag) => false,
|
||||
0 => true,
|
||||
_ when _stateFilter.HasFlag(hasFlag) => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The overwritten filter method also computes the state.
|
||||
|
|
@ -653,7 +627,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
{
|
||||
state = default;
|
||||
return ModFilterExtensions.UnfilteredStateMods != _stateFilter
|
||||
|| FilterValue.Length > 0 && !f.FullName().Contains(FilterValue, IgnoreCase);
|
||||
|| !Filter.IsVisible(f);
|
||||
}
|
||||
|
||||
return ApplyFiltersAndState((ModFileSystem.Leaf)path, out state);
|
||||
|
|
@ -661,23 +635,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
|
||||
/// <summary> Apply the string filters. </summary>
|
||||
private bool ApplyStringFilters(ModFileSystem.Leaf leaf, Mod mod)
|
||||
{
|
||||
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
|
||||
};
|
||||
}
|
||||
=> !Filter.IsVisible(leaf);
|
||||
|
||||
/// <summary> Only get the text color for a mod if no filters are set. </summary>
|
||||
private ColorId GetTextColor(Mod mod, ModSettings? settings, ModCollection collection)
|
||||
|
|
|
|||
138
Penumbra/UI/ModsTab/ModSearchStringSplitter.cs
Normal file
138
Penumbra/UI/ModsTab/ModSearchStringSplitter.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue