diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs
index e525c1f4..cf99c6c8 100644
--- a/Penumbra/Services/FilenameService.cs
+++ b/Penumbra/Services/FilenameService.cs
@@ -5,26 +5,15 @@ using Penumbra.Mods;
namespace Penumbra.Services;
-public class FilenameService
+public class FilenameService(DalamudPluginInterface pi)
{
- public readonly string ConfigDirectory;
- public readonly string CollectionDirectory;
- public readonly string LocalDataDirectory;
- public readonly string ConfigFile;
- public readonly string EphemeralConfigFile;
- public readonly string FilesystemFile;
- public readonly string ActiveCollectionsFile;
-
- public FilenameService(DalamudPluginInterface pi)
- {
- ConfigDirectory = pi.ConfigDirectory.FullName;
- CollectionDirectory = Path.Combine(pi.ConfigDirectory.FullName, "collections");
- LocalDataDirectory = Path.Combine(pi.ConfigDirectory.FullName, "mod_data");
- ConfigFile = pi.ConfigFile.FullName;
- FilesystemFile = Path.Combine(pi.ConfigDirectory.FullName, "sort_order.json");
- ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json");
- EphemeralConfigFile = Path.Combine(pi.ConfigDirectory.FullName, "ephemeral_config.json");
- }
+ public readonly string ConfigDirectory = pi.ConfigDirectory.FullName;
+ public readonly string CollectionDirectory = Path.Combine(pi.ConfigDirectory.FullName, "collections");
+ public readonly string LocalDataDirectory = Path.Combine(pi.ConfigDirectory.FullName, "mod_data");
+ public readonly string ConfigFile = pi.ConfigFile.FullName;
+ public readonly string EphemeralConfigFile = Path.Combine(pi.ConfigDirectory.FullName, "ephemeral_config.json");
+ public readonly string FilesystemFile = Path.Combine(pi.ConfigDirectory.FullName, "sort_order.json");
+ public readonly string ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json");
/// Obtain the path of a collection file given its name.
public string CollectionFile(ModCollection collection)
@@ -34,7 +23,6 @@ public class FilenameService
public string CollectionFile(string collectionName)
=> Path.Combine(CollectionDirectory, $"{collectionName}.json");
-
/// Obtain the path of the local data file given a mod directory. Returns an empty string if the mod is temporary.
public string LocalDataFile(Mod mod)
=> LocalDataFile(mod.ModPath.FullName);
diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs
index 227f65d7..73be8834 100644
--- a/Penumbra/Services/ServiceManager.cs
+++ b/Penumbra/Services/ServiceManager.cs
@@ -149,6 +149,7 @@ public static class ServiceManager
.AddSingleton()
.AddSingleton()
.AddSingleton()
+ .AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs
index 59c9d279..15961ff3 100644
--- a/Penumbra/UI/ModsTab/ModPanel.cs
+++ b/Penumbra/UI/ModsTab/ModPanel.cs
@@ -1,26 +1,24 @@
-using Dalamud.Interface;
using Dalamud.Plugin;
-using ImGuiNET;
-using OtterGui;
-using OtterGui.Raii;
using Penumbra.Mods;
-using Penumbra.Mods.Manager;
using Penumbra.UI.AdvancedWindow;
namespace Penumbra.UI.ModsTab;
public class ModPanel : IDisposable
{
+ private readonly MultiModPanel _multiModPanel;
private readonly ModFileSystemSelector _selector;
private readonly ModEditWindow _editWindow;
private readonly ModPanelHeader _header;
private readonly ModPanelTabBar _tabs;
- public ModPanel(DalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs)
+ public ModPanel(DalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs,
+ MultiModPanel multiModPanel)
{
_selector = selector;
_editWindow = editWindow;
_tabs = tabs;
+ _multiModPanel = multiModPanel;
_header = new ModPanelHeader(pi);
_selector.SelectionChanged += OnSelectionChange;
}
@@ -29,7 +27,7 @@ public class ModPanel : IDisposable
{
if (!_valid)
{
- DrawMultiSelection();
+ _multiModPanel.Draw();
return;
}
@@ -43,45 +41,6 @@ public class ModPanel : IDisposable
_header.Dispose();
}
- private void DrawMultiSelection()
- {
- if (_selector.SelectedPaths.Count == 0)
- return;
-
- var sizeType = ImGui.GetFrameHeight();
- var availableSizePercent = (ImGui.GetContentRegionAvail().X - sizeType - 4 * ImGui.GetStyle().CellPadding.X) / 100;
- var sizeMods = availableSizePercent * 35;
- var sizeFolders = availableSizePercent * 65;
-
- ImGui.NewLine();
- ImGui.TextUnformatted("Currently Selected Objects");
- ImGui.Separator();
- using var table = ImRaii.Table("mods", 3, ImGuiTableFlags.RowBg);
- ImGui.TableSetupColumn("type", ImGuiTableColumnFlags.WidthFixed, sizeType);
- ImGui.TableSetupColumn("mod", ImGuiTableColumnFlags.WidthFixed, sizeMods);
- ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthFixed, sizeFolders);
-
- var i = 0;
- foreach (var (fullName, path) in _selector.SelectedPaths.Select(p => (p.FullName(), p))
- .OrderBy(p => p.Item1, StringComparer.OrdinalIgnoreCase))
- {
- using var id = ImRaii.PushId(i++);
- ImGui.TableNextColumn();
- var icon = (path is ModFileSystem.Leaf ? FontAwesomeIcon.FileCircleMinus : FontAwesomeIcon.FolderMinus).ToIconString();
- if (ImGuiUtil.DrawDisabledButton(icon, new Vector2(sizeType), "Remove from selection.", false, true))
- _selector.RemovePathFromMultiselection(path);
-
- ImGui.TableNextColumn();
- ImGui.AlignTextToFramePadding();
- ImGui.TextUnformatted(path is ModFileSystem.Leaf l ? l.Value.Name : string.Empty);
-
- ImGui.TableNextColumn();
- ImGui.AlignTextToFramePadding();
- ImGui.TextUnformatted(fullName);
- }
- }
-
-
private bool _valid;
private Mod _mod = null!;
diff --git a/Penumbra/UI/ModsTab/MultiModPanel.cs b/Penumbra/UI/ModsTab/MultiModPanel.cs
new file mode 100644
index 00000000..1e4117ec
--- /dev/null
+++ b/Penumbra/UI/ModsTab/MultiModPanel.cs
@@ -0,0 +1,125 @@
+using Dalamud.Interface;
+using Dalamud.Interface.Utility;
+using ImGuiNET;
+using OtterGui;
+using OtterGui.Raii;
+using Penumbra.Mods;
+using Penumbra.Mods.Manager;
+
+namespace Penumbra.UI.ModsTab;
+
+public class MultiModPanel(ModFileSystemSelector _selector, ModDataEditor _editor)
+{
+ public void Draw()
+ {
+ if (_selector.SelectedPaths.Count == 0)
+ return;
+
+ ImGui.NewLine();
+ DrawModList();
+ DrawMultiTagger();
+ }
+
+ private void DrawModList()
+ {
+ using var tree = ImRaii.TreeNode("Currently Selected Objects", ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.NoTreePushOnOpen);
+ ImGui.Separator();
+ if (!tree)
+ return;
+
+ var sizeType = ImGui.GetFrameHeight();
+ var availableSizePercent = (ImGui.GetContentRegionAvail().X - sizeType - 4 * ImGui.GetStyle().CellPadding.X) / 100;
+ var sizeMods = availableSizePercent * 35;
+ var sizeFolders = availableSizePercent * 65;
+
+ using (var table = ImRaii.Table("mods", 3, ImGuiTableFlags.RowBg))
+ {
+ if (!table)
+ return;
+
+ ImGui.TableSetupColumn("type", ImGuiTableColumnFlags.WidthFixed, sizeType);
+ ImGui.TableSetupColumn("mod", ImGuiTableColumnFlags.WidthFixed, sizeMods);
+ ImGui.TableSetupColumn("path", ImGuiTableColumnFlags.WidthFixed, sizeFolders);
+
+ var i = 0;
+ foreach (var (fullName, path) in _selector.SelectedPaths.Select(p => (p.FullName(), p))
+ .OrderBy(p => p.Item1, StringComparer.OrdinalIgnoreCase))
+ {
+ using var id = ImRaii.PushId(i++);
+ ImGui.TableNextColumn();
+ var icon = (path is ModFileSystem.Leaf ? FontAwesomeIcon.FileCircleMinus : FontAwesomeIcon.FolderMinus).ToIconString();
+ if (ImGuiUtil.DrawDisabledButton(icon, new Vector2(sizeType), "Remove from selection.", false, true))
+ _selector.RemovePathFromMultiselection(path);
+
+ ImGui.TableNextColumn();
+ ImGui.AlignTextToFramePadding();
+ ImGui.TextUnformatted(path is ModFileSystem.Leaf l ? l.Value.Name : string.Empty);
+
+ ImGui.TableNextColumn();
+ ImGui.AlignTextToFramePadding();
+ ImGui.TextUnformatted(fullName);
+ }
+ }
+
+ ImGui.Separator();
+ }
+
+ private string _tag = string.Empty;
+ private readonly List _addMods = [];
+ private readonly List<(Mod, int)> _removeMods = [];
+
+ private void DrawMultiTagger()
+ {
+ var width = ImGuiHelpers.ScaledVector2(150, 0);
+ ImGui.AlignTextToFramePadding();
+ ImGui.TextUnformatted("Multi Tagger:");
+ ImGui.SameLine();
+ ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 2 * (width.X + ImGui.GetStyle().ItemSpacing.X));
+ ImGui.InputTextWithHint("##tag", "Local Tag Name...", ref _tag, 128);
+
+ UpdateTagCache();
+ var label = _addMods.Count > 0
+ ? $"Add to {_addMods.Count} Mods"
+ : "Add";
+ var tooltip = _addMods.Count == 0
+ ? _tag.Length == 0
+ ? "No tag specified."
+ : $"All mods selected already contain the tag \"{_tag}\", either locally or as mod data."
+ : $"Add the tag \"{_tag}\" to {_addMods.Count} mods as a local tag:\n\n\t{string.Join("\n\t", _addMods.Select(m => m.Name.Text))}";
+ ImGui.SameLine();
+ if (ImGuiUtil.DrawDisabledButton(label, width, tooltip, _addMods.Count == 0))
+ foreach (var mod in _addMods)
+ _editor.ChangeLocalTag(mod, mod.LocalTags.Count, _tag);
+
+ label = _removeMods.Count > 0
+ ? $"Remove from {_removeMods.Count} Mods"
+ : "Remove";
+ tooltip = _removeMods.Count == 0
+ ? _tag.Length == 0
+ ? "No tag specified."
+ : $"No selected mod contains the tag \"{_tag}\" locally."
+ : $"Remove the local tag \"{_tag}\" from {_removeMods.Count} mods:\n\n\t{string.Join("\n\t", _removeMods.Select(m => m.Item1.Name.Text))}";
+ ImGui.SameLine();
+ if (ImGuiUtil.DrawDisabledButton(label, width, tooltip, _removeMods.Count == 0))
+ foreach (var (mod, index) in _removeMods)
+ _editor.ChangeLocalTag(mod, index, string.Empty);
+ ImGui.Separator();
+ }
+
+ private void UpdateTagCache()
+ {
+ _addMods.Clear();
+ _removeMods.Clear();
+ if (_tag.Length == 0)
+ return;
+
+ foreach (var leaf in _selector.SelectedPaths.OfType())
+ {
+ var index = leaf.Value.LocalTags.IndexOf(_tag);
+ if (index >= 0)
+ _removeMods.Add((leaf.Value, index));
+ else if (!leaf.Value.ModTags.Contains(_tag))
+ _addMods.Add(leaf.Value);
+ }
+ }
+}