From 7128326ab9f30e2fb2a3449953ed9d6a4c0e0ba1 Mon Sep 17 00:00:00 2001 From: AeAstralis Date: Fri, 1 Mar 2024 17:10:33 -0500 Subject: [PATCH 1/9] Add shared tag system for tagging individual mods Adds a new system of shared tags that are saved in the Penumbra config, and can then be 1-click added or removed to/from mods via a popup menu. The use case for this new system is to allow users to more easily re-use tags and to allow them to quickly tag individual mods. Shared tags can be added/removed/modified via a new Tags section of the main Penumbra Settings tab. Once any shared tags have been saved, they can be added via a new tags button that shows up in the Description and Edit Mod tabs, to the right of the existing + button that already existed for typing in new tags. Shared tags have the same restrictions as regular mod tags, and the application of shared tags should respect the same limits as application of normal tags. Signed-off-by: AeAstralis --- Penumbra/Configuration.cs | 2 + Penumbra/Services/ServiceManagerA.cs | 3 +- Penumbra/UI/Classes/Colors.cs | 4 + Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 29 +++- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 30 +++- Penumbra/UI/SharedTagManager.cs | 160 ++++++++++++++++++ Penumbra/UI/Tabs/SettingsTab.cs | 26 ++- 7 files changed, 248 insertions(+), 6 deletions(-) create mode 100644 Penumbra/UI/SharedTagManager.cs diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 188be65d..43253223 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -87,6 +87,8 @@ public class Configuration : IPluginConfiguration, ISavable public Dictionary Colors { get; set; } = Enum.GetValues().ToDictionary(c => c, c => c.Data().DefaultColor); + public IReadOnlyList SharedTags { get; set; } + /// /// Load the current configuration. /// Includes adding new colors and migrating from old versions. diff --git a/Penumbra/Services/ServiceManagerA.cs b/Penumbra/Services/ServiceManagerA.cs index f25aac7c..b0ecdcf0 100644 --- a/Penumbra/Services/ServiceManagerA.cs +++ b/Penumbra/Services/ServiceManagerA.cs @@ -103,7 +103,8 @@ public static class ServiceManagerA private static ServiceManager AddConfiguration(this ServiceManager services) => services.AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static ServiceManager AddCollections(this ServiceManager services) => services.AddSingleton() diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 93d7e091..50096696 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -28,6 +28,8 @@ public enum ColorId ResTreePlayer, ResTreeNetworked, ResTreeNonNetworked, + SharedTagAdd, + SharedTagRemove } public static class Colors @@ -73,6 +75,8 @@ public static class Colors ColorId.ResTreePlayer => ( 0xFFC0FFC0, "On-Screen: Other Players", "Other players and what they own, in the On-Screen tab." ), ColorId.ResTreeNetworked => ( 0xFFFFFFFF, "On-Screen: Non-Players (Networked)", "Non-player entities handled by the game server, in the On-Screen tab." ), ColorId.ResTreeNonNetworked => ( 0xFFC0C0FF, "On-Screen: Non-Players (Local)", "Non-player entities handled locally, in the On-Screen tab." ), + ColorId.SharedTagAdd => ( 0xFF44AA44, "Shared Tags: Add Tag", "A shared tag that is not present on the current mod and can be added." ), + ColorId.SharedTagRemove => ( 0xFF2222AA, "Shared Tags: Remove Tag", "A shared tag that is already present on the current mod and can be removed." ), _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 3cc59661..7da13966 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -12,14 +12,16 @@ public class ModPanelDescriptionTab : ITab private readonly ModFileSystemSelector _selector; private readonly TutorialService _tutorial; private readonly ModManager _modManager; + private readonly SharedTagManager _sharedTagManager; private readonly TagButtons _localTags = new(); private readonly TagButtons _modTags = new(); - public ModPanelDescriptionTab(ModFileSystemSelector selector, TutorialService tutorial, ModManager modManager) + public ModPanelDescriptionTab(ModFileSystemSelector selector, TutorialService tutorial, ModManager modManager, SharedTagManager sharedTagsConfig) { _selector = selector; _tutorial = tutorial; _modManager = modManager; + _sharedTagManager = sharedTagsConfig; } public ReadOnlySpan Label @@ -34,14 +36,37 @@ public class ModPanelDescriptionTab : ITab ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); + var sharedTagsEnabled = _sharedTagManager.SharedTags.Count() > 0; + var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _localTags.Draw("Local Tags: ", "Custom tags you can set personally that will not be exported to the mod data but only set for you.\n" + "If the mod already contains a local tag in its own tags, the local tag will be ignored.", _selector.Selected!.LocalTags, - out var editedTag); + out var editedTag, rightEndOffset: sharedTagButtonOffset); _tutorial.OpenTutorial(BasicTutorialSteps.Tags); if (tagIdx >= 0) _modManager.DataEditor.ChangeLocalTag(_selector.Selected!, tagIdx, editedTag); + if (sharedTagsEnabled) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetFrameHeightWithSpacing()); + ImGui.SetCursorPosX(ImGui.GetWindowWidth() - ImGui.GetFrameHeight() - ImGui.GetStyle().FramePadding.X); + var sharedTag = _sharedTagManager.DrawAddFromSharedTags(_selector.Selected!.LocalTags, _selector.Selected!.ModTags, true); + if (sharedTag.Length > 0) + { + var index = _selector.Selected!.LocalTags.IndexOf(sharedTag); + if (index < 0) + { + index = _selector.Selected!.LocalTags.Count; + _modManager.DataEditor.ChangeLocalTag(_selector.Selected, index, sharedTag); + } + else + { + _modManager.DataEditor.ChangeLocalTag(_selector.Selected, index, string.Empty); + } + + } + } + if (_selector.Selected!.ModTags.Count > 0) _modTags.Draw("Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.", _selector.Selected!.ModTags, out var _, false, diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 20da8fde..3620c7ac 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -28,6 +28,7 @@ public class ModPanelEditTab : ITab private readonly ModEditWindow _editWindow; private readonly ModEditor _editor; private readonly Configuration _config; + private readonly SharedTagManager _sharedTagManager; private readonly TagButtons _modTags = new(); @@ -37,7 +38,8 @@ public class ModPanelEditTab : ITab private Mod _mod = null!; public ModPanelEditTab(ModManager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, Services.MessageService messager, - ModEditWindow editWindow, ModEditor editor, FilenameService filenames, ModExportManager modExportManager, Configuration config) + ModEditWindow editWindow, ModEditor editor, FilenameService filenames, ModExportManager modExportManager, Configuration config, + SharedTagManager sharedTagManager) { _modManager = modManager; _selector = selector; @@ -48,6 +50,7 @@ public class ModPanelEditTab : ITab _filenames = filenames; _modExportManager = modExportManager; _config = config; + _sharedTagManager = sharedTagManager; } public ReadOnlySpan Label @@ -80,11 +83,34 @@ public class ModPanelEditTab : ITab } UiHelpers.DefaultLineSpace(); + var sharedTagsEnabled = _sharedTagManager.SharedTags.Count() > 0; + var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, - out var editedTag); + out var editedTag, rightEndOffset: sharedTagButtonOffset); if (tagIdx >= 0) _modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag); + if (sharedTagsEnabled) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetFrameHeightWithSpacing()); + ImGui.SetCursorPosX(ImGui.GetWindowWidth() - ImGui.GetFrameHeight() - ImGui.GetStyle().FramePadding.X); + var sharedTag = _sharedTagManager.DrawAddFromSharedTags(_selector.Selected!.LocalTags, _selector.Selected!.ModTags, false); + if (sharedTag.Length > 0) + { + var index = _selector.Selected!.ModTags.IndexOf(sharedTag); + if (index < 0) + { + index = _selector.Selected!.ModTags.Count; + _modManager.DataEditor.ChangeModTag(_selector.Selected, index, sharedTag); + } + else + { + _modManager.DataEditor.ChangeModTag(_selector.Selected, index, string.Empty); + } + + } + } + UiHelpers.DefaultLineSpace(); AddOptionGroup.Draw(_filenames, _modManager, _mod, _config.ReplaceNonAsciiOnImport); UiHelpers.DefaultLineSpace(); diff --git a/Penumbra/UI/SharedTagManager.cs b/Penumbra/UI/SharedTagManager.cs new file mode 100644 index 00000000..9562b24c --- /dev/null +++ b/Penumbra/UI/SharedTagManager.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; +public sealed class SharedTagManager +{ + private static uint _tagButtonAddColor = ColorId.SharedTagAdd.Value(); + private static uint _tagButtonRemoveColor = ColorId.SharedTagRemove.Value(); + + private static float _minTagButtonWidth = 15; + + private const string PopupContext = "SharedTagsPopup"; + private bool _isPopupOpen = false; + + + public IReadOnlyList SharedTags { get; internal set; } = Array.Empty(); + + public SharedTagManager() + { + } + + public void ChangeSharedTag(int tagIdx, string tag) + { + if (tagIdx < 0 || tagIdx > SharedTags.Count) + return; + + if (tagIdx == SharedTags.Count) // Adding a new tag + { + SharedTags = SharedTags.Append(tag).Distinct().Where(tag => tag.Length > 0).OrderBy(a => a).ToArray(); + } + else // Editing an existing tag + { + var tmpTags = SharedTags.ToArray(); + tmpTags[tagIdx] = tag; + SharedTags = tmpTags.Distinct().Where(tag => tag.Length > 0).OrderBy(a => a).ToArray(); + } + } + + public string DrawAddFromSharedTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal) + { + var tagToAdd = ""; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Tags.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Add Shared Tag... (Right-click to close popup)", + false, true) || _isPopupOpen) + return DrawSharedTagsPopup(localTags, modTags, editLocal); + + + return tagToAdd; + } + + private string DrawSharedTagsPopup(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal) + { + var selected = ""; + if (!ImGui.IsPopupOpen(PopupContext)) + { + ImGui.OpenPopup(PopupContext); + _isPopupOpen = true; + } + + var display = ImGui.GetIO().DisplaySize; + var height = Math.Min(display.Y / 4, 10 * ImGui.GetFrameHeightWithSpacing()); + var width = display.X / 6; + var size = new Vector2(width, height); + ImGui.SetNextWindowSize(size); + using var popup = ImRaii.Popup(PopupContext); + if (!popup) + return selected; + + ImGui.Text("Shared Tags"); + ImGuiUtil.HoverTooltip("Right-click to close popup"); + ImGui.Separator(); + + foreach (var tag in SharedTags) + { + if (DrawColoredButton(localTags, modTags, tag, editLocal)) + { + selected = tag; + return selected; + } + ImGui.SameLine(); + } + + if (ImGui.IsMouseClicked(ImGuiMouseButton.Right)) + { + _isPopupOpen = false; + } + + return selected; + } + + private static bool DrawColoredButton(IReadOnlyCollection localTags, IReadOnlyCollection modTags, string buttonLabel, bool editLocal) + { + var isLocalTagPresent = localTags.Contains(buttonLabel); + var isModTagPresent = modTags.Contains(buttonLabel); + + var buttonWidth = CalcTextButtonWidth(buttonLabel); + // Would prefer to be able to fit at least 2 buttons per line so the popup doesn't look sparse with lots of long tags. Thus long tags will be trimmed. + var maxButtonWidth = (ImGui.GetContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) * 0.5f - ImGui.GetStyle().ItemSpacing.X; + var displayedLabel = buttonLabel; + if (buttonWidth >= maxButtonWidth) + { + displayedLabel = TrimButtonTextToWidth(buttonLabel, maxButtonWidth); + buttonWidth = CalcTextButtonWidth(displayedLabel); + } + + // Prevent adding a new tag past the right edge of the popup + if (buttonWidth + ImGui.GetStyle().ItemSpacing.X >= ImGui.GetContentRegionAvail().X) + ImGui.NewLine(); + + // Trimmed tag names can collide, but the full tags are guaranteed distinct so use the full tag as the ID to avoid an ImGui moment. + ImRaii.PushId(buttonLabel); + + if (editLocal && isModTagPresent || !editLocal && isLocalTagPresent) + { + using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f); + ImGui.Button(displayedLabel); + alpha.Pop(); + return false; + } + + using (ImRaii.PushColor(ImGuiCol.Button, isLocalTagPresent || isModTagPresent ? _tagButtonRemoveColor : _tagButtonAddColor)) + { + return ImGui.Button(displayedLabel); + } + } + + private static string TrimButtonTextToWidth(string fullText, float maxWidth) + { + var trimmedText = fullText; + + while (trimmedText.Length > _minTagButtonWidth) + { + var nextTrim = trimmedText.Substring(0, Math.Max(trimmedText.Length - 1, 0)); + + // An ellipsis will be used to indicate trimmed tags + if (CalcTextButtonWidth(nextTrim + "...") < maxWidth) + { + return nextTrim + "..."; + } + trimmedText = nextTrim; + } + + return trimmedText; + } + + private static float CalcTextButtonWidth(string text) + { + return ImGui.CalcTextSize(text).X + 2 * ImGui.GetStyle().FramePadding.X; + } + +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index a03e7b87..f37c2c81 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -41,15 +41,18 @@ public class SettingsTab : ITab private readonly DalamudConfigService _dalamudConfig; private readonly DalamudPluginInterface _pluginInterface; private readonly IDataManager _gameData; + private readonly SharedTagManager _sharedTagManager; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; + private readonly TagButtons _sharedTags = new(); + public SettingsTab(DalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, - IDataManager gameData) + IDataManager gameData, SharedTagManager sharedTagConfig) { _pluginInterface = pluginInterface; _config = config; @@ -69,6 +72,9 @@ public class SettingsTab : ITab _gameData = gameData; if (_compactor.CanCompact) _compactor.Enabled = _config.UseFileSystemCompression; + _sharedTagManager = sharedTagConfig; + if (sharedTagConfig.SharedTags.Count == 0 && _config.SharedTags != null) + sharedTagConfig.SharedTags = _config.SharedTags; } public void DrawHeader() @@ -96,6 +102,7 @@ public class SettingsTab : ITab DrawGeneralSettings(); DrawColorSettings(); DrawAdvancedSettings(); + DrawSharedTagsSection(); DrawSupportButtons(); } @@ -902,4 +909,21 @@ public class SettingsTab : ITab if (ImGui.Button("Show Changelogs", new Vector2(width, 0))) _penumbra.ForceChangelogOpen(); } + + private void DrawSharedTagsSection() + { + if (!ImGui.CollapsingHeader("Tags")) + return; + + var tagIdx = _sharedTags.Draw("Shared Tags: ", + "Tags that can be added/removed from mods with 1 click.", _sharedTagManager.SharedTags, + out var editedTag); + + if (tagIdx >= 0) + { + _sharedTagManager.ChangeSharedTag(tagIdx, editedTag); + _config.SharedTags = _sharedTagManager.SharedTags; + _config.Save(); + } + } } From 334be441f88dc554461cdddbb81ba658abe5b958 Mon Sep 17 00:00:00 2001 From: AeAstralis Date: Fri, 1 Mar 2024 18:04:03 -0500 Subject: [PATCH 2/9] Update OtterGui to 97ac353 Updates OtterGui to 97ac3538536a17e980027f783ec5e5167b371f71 to include change that xivdev/Penumbra#399 is dependent on. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 1a187f75..97ac3538 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 1a187f756f2e8823197bd43db1c3383231f5eaff +Subproject commit 97ac3538536a17e980027f783ec5e5167b371f71 From 282f6d48551e0208d4ad06da18167ad74c48d9e2 Mon Sep 17 00:00:00 2001 From: AeAstralis Date: Fri, 1 Mar 2024 21:03:34 -0500 Subject: [PATCH 3/9] Migrate shared tag to own config, address comments Migrates the configuration for shared tags to a separate config file, and addresses CR feedback. --- Penumbra/Configuration.cs | 2 - Penumbra/Services/FilenameService.cs | 1 + Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 20 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 20 +- Penumbra/UI/SharedTagManager.cs | 171 ++++++++++++++---- Penumbra/UI/Tabs/SettingsTab.cs | 6 +- 6 files changed, 143 insertions(+), 77 deletions(-) diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 43253223..188be65d 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -87,8 +87,6 @@ public class Configuration : IPluginConfiguration, ISavable public Dictionary Colors { get; set; } = Enum.GetValues().ToDictionary(c => c, c => c.Data().DefaultColor); - public IReadOnlyList SharedTags { get; set; } - /// /// Load the current configuration. /// Includes adding new colors and migrating from old versions. diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 5f918a90..23694ebc 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -14,6 +14,7 @@ public class FilenameService(DalamudPluginInterface pi) : IService 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"); + public readonly string SharedTagFile = Path.Combine(pi.ConfigDirectory.FullName, "shared_tags.json"); /// Obtain the path of a collection file given its name. public string CollectionFile(ModCollection collection) diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 7da13966..5f2687c3 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -36,7 +36,7 @@ public class ModPanelDescriptionTab : ITab ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); - var sharedTagsEnabled = _sharedTagManager.SharedTags.Count() > 0; + var sharedTagsEnabled = _sharedTagManager.SharedTags.Count > 0; var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _localTags.Draw("Local Tags: ", "Custom tags you can set personally that will not be exported to the mod data but only set for you.\n" @@ -48,23 +48,7 @@ public class ModPanelDescriptionTab : ITab if (sharedTagsEnabled) { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetFrameHeightWithSpacing()); - ImGui.SetCursorPosX(ImGui.GetWindowWidth() - ImGui.GetFrameHeight() - ImGui.GetStyle().FramePadding.X); - var sharedTag = _sharedTagManager.DrawAddFromSharedTags(_selector.Selected!.LocalTags, _selector.Selected!.ModTags, true); - if (sharedTag.Length > 0) - { - var index = _selector.Selected!.LocalTags.IndexOf(sharedTag); - if (index < 0) - { - index = _selector.Selected!.LocalTags.Count; - _modManager.DataEditor.ChangeLocalTag(_selector.Selected, index, sharedTag); - } - else - { - _modManager.DataEditor.ChangeLocalTag(_selector.Selected, index, string.Empty); - } - - } + _sharedTagManager.DrawAddFromSharedTagsAndUpdateTags(_selector.Selected!.LocalTags, _selector.Selected!.ModTags, true, _selector.Selected!); } if (_selector.Selected!.ModTags.Count > 0) diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 3620c7ac..9b4a582f 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -83,7 +83,7 @@ public class ModPanelEditTab : ITab } UiHelpers.DefaultLineSpace(); - var sharedTagsEnabled = _sharedTagManager.SharedTags.Count() > 0; + var sharedTagsEnabled = _sharedTagManager.SharedTags.Count > 0; var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, out var editedTag, rightEndOffset: sharedTagButtonOffset); @@ -92,23 +92,7 @@ public class ModPanelEditTab : ITab if (sharedTagsEnabled) { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetFrameHeightWithSpacing()); - ImGui.SetCursorPosX(ImGui.GetWindowWidth() - ImGui.GetFrameHeight() - ImGui.GetStyle().FramePadding.X); - var sharedTag = _sharedTagManager.DrawAddFromSharedTags(_selector.Selected!.LocalTags, _selector.Selected!.ModTags, false); - if (sharedTag.Length > 0) - { - var index = _selector.Selected!.ModTags.IndexOf(sharedTag); - if (index < 0) - { - index = _selector.Selected!.ModTags.Count; - _modManager.DataEditor.ChangeModTag(_selector.Selected, index, sharedTag); - } - else - { - _modManager.DataEditor.ChangeModTag(_selector.Selected, index, string.Empty); - } - - } + _sharedTagManager.DrawAddFromSharedTagsAndUpdateTags(_selector.Selected!.LocalTags, _selector.Selected!.ModTags, false, _selector.Selected!); } UiHelpers.DefaultLineSpace(); diff --git a/Penumbra/UI/SharedTagManager.cs b/Penumbra/UI/SharedTagManager.cs index 9562b24c..23196319 100644 --- a/Penumbra/UI/SharedTagManager.cs +++ b/Penumbra/UI/SharedTagManager.cs @@ -1,19 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Dalamud.Interface; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Utility; using ImGuiNET; +using Newtonsoft.Json; using OtterGui; +using OtterGui.Classes; using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Services; using Penumbra.UI.Classes; +using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra.UI; -public sealed class SharedTagManager +public sealed class SharedTagManager : ISavable { + private readonly ModManager _modManager; + private readonly SaveService _saveService; + private static uint _tagButtonAddColor = ColorId.SharedTagAdd.Value(); private static uint _tagButtonRemoveColor = ColorId.SharedTagRemove.Value(); @@ -22,11 +25,66 @@ public sealed class SharedTagManager private const string PopupContext = "SharedTagsPopup"; private bool _isPopupOpen = false; + // Operations on this list assume that it is sorted and will keep it sorted if that is the case. + // The list also gets re-sorted when first loaded from config in case the config was modified. + [JsonRequired] + private readonly List _sharedTags = []; + [JsonIgnore] + public IReadOnlyList SharedTags => _sharedTags; - public IReadOnlyList SharedTags { get; internal set; } = Array.Empty(); + public int ConfigVersion = 1; - public SharedTagManager() + public SharedTagManager(ModManager modManager, SaveService saveService) { + _modManager = modManager; + _saveService = saveService; + Load(); + } + + public string ToFilename(FilenameService fileNames) + { + return fileNames.SharedTagFile; + } + + public void Save(StreamWriter writer) + { + using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + serializer.Serialize(jWriter, this); + } + + public void Save() + => _saveService.DelaySave(this, TimeSpan.FromSeconds(5)); + + private void Load() + { + static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) + { + Penumbra.Log.Error( + $"Error parsing shared tags Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); + errorArgs.ErrorContext.Handled = true; + } + + if (!File.Exists(_saveService.FileNames.SharedTagFile)) + return; + + try + { + var text = File.ReadAllText(_saveService.FileNames.SharedTagFile); + JsonConvert.PopulateObject(text, this, new JsonSerializerSettings + { + Error = HandleDeserializationError, + }); + + // Any changes to this within this class should keep it sorted, but in case someone went in and manually changed the JSON, run a sort on initial load. + _sharedTags.Sort(); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage(ex, + "Error reading shared tags Configuration, reverting to default.", + "Error reading shared tags Configuration", NotificationType.Error); + } } public void ChangeSharedTag(int tagIdx, string tag) @@ -34,32 +92,74 @@ public sealed class SharedTagManager if (tagIdx < 0 || tagIdx > SharedTags.Count) return; - if (tagIdx == SharedTags.Count) // Adding a new tag + // In the case of editing a tag, remove what's there prior to doing an insert. + if (tagIdx != SharedTags.Count) { - SharedTags = SharedTags.Append(tag).Distinct().Where(tag => tag.Length > 0).OrderBy(a => a).ToArray(); + _sharedTags.RemoveAt(tagIdx); } - else // Editing an existing tag + + if (!string.IsNullOrEmpty(tag)) { - var tmpTags = SharedTags.ToArray(); - tmpTags[tagIdx] = tag; - SharedTags = tmpTags.Distinct().Where(tag => tag.Length > 0).OrderBy(a => a).ToArray(); + // Taking advantage of the fact that BinarySearch returns the complement of the correct sorted position for the tag. + var existingIdx = _sharedTags.BinarySearch(tag); + if (existingIdx < 0) + _sharedTags.Insert(~existingIdx, tag); + } + + Save(); + } + + public void DrawAddFromSharedTagsAndUpdateTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal, Mods.Mod mod) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetFrameHeightWithSpacing()); + ImGui.SetCursorPosX(ImGui.GetWindowWidth() - ImGui.GetFrameHeight() - ImGui.GetStyle().FramePadding.X); + + var sharedTag = DrawAddFromSharedTags(localTags, modTags, editLocal); + + if (sharedTag.Length > 0) + { + var index = editLocal ? mod.LocalTags.IndexOf(sharedTag) : mod.ModTags.IndexOf(sharedTag); + + if (editLocal) + { + if (index < 0) + { + index = mod.LocalTags.Count; + _modManager.DataEditor.ChangeLocalTag(mod, index, sharedTag); + } + else + { + _modManager.DataEditor.ChangeLocalTag(mod, index, string.Empty); + } + } else + { + if (index < 0) + { + index = mod.ModTags.Count; + _modManager.DataEditor.ChangeModTag(mod, index, sharedTag); + } + else + { + _modManager.DataEditor.ChangeModTag(mod, index, string.Empty); + } + } + } } public string DrawAddFromSharedTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal) { - var tagToAdd = ""; + var tagToAdd = string.Empty; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Tags.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Add Shared Tag... (Right-click to close popup)", false, true) || _isPopupOpen) return DrawSharedTagsPopup(localTags, modTags, editLocal); - return tagToAdd; } private string DrawSharedTagsPopup(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal) { - var selected = ""; + var selected = string.Empty; if (!ImGui.IsPopupOpen(PopupContext)) { ImGui.OpenPopup(PopupContext); @@ -75,16 +175,15 @@ public sealed class SharedTagManager if (!popup) return selected; - ImGui.Text("Shared Tags"); + ImGui.TextUnformatted("Shared Tags"); ImGuiUtil.HoverTooltip("Right-click to close popup"); ImGui.Separator(); - foreach (var tag in SharedTags) + foreach (var (tag, idx) in SharedTags.WithIndex()) { - if (DrawColoredButton(localTags, modTags, tag, editLocal)) + if (DrawColoredButton(localTags, modTags, tag, editLocal, idx)) { selected = tag; - return selected; } ImGui.SameLine(); } @@ -97,8 +196,10 @@ public sealed class SharedTagManager return selected; } - private static bool DrawColoredButton(IReadOnlyCollection localTags, IReadOnlyCollection modTags, string buttonLabel, bool editLocal) + private static bool DrawColoredButton(IReadOnlyCollection localTags, IReadOnlyCollection modTags, string buttonLabel, bool editLocal, int index) { + var ret = false; + var isLocalTagPresent = localTags.Contains(buttonLabel); var isModTagPresent = modTags.Contains(buttonLabel); @@ -116,21 +217,24 @@ public sealed class SharedTagManager if (buttonWidth + ImGui.GetStyle().ItemSpacing.X >= ImGui.GetContentRegionAvail().X) ImGui.NewLine(); - // Trimmed tag names can collide, but the full tags are guaranteed distinct so use the full tag as the ID to avoid an ImGui moment. - ImRaii.PushId(buttonLabel); + // Trimmed tag names can collide, and while tag names are currently distinct this may not always be the case. As such use the index to avoid an ImGui moment. + using var id = ImRaii.PushId(index); if (editLocal && isModTagPresent || !editLocal && isLocalTagPresent) { using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f); ImGui.Button(displayedLabel); - alpha.Pop(); - return false; + } + else + { + using (ImRaii.PushColor(ImGuiCol.Button, isLocalTagPresent || isModTagPresent ? _tagButtonRemoveColor : _tagButtonAddColor)) + { + if (ImGui.Button(displayedLabel)) + ret = true; + } } - using (ImRaii.PushColor(ImGuiCol.Button, isLocalTagPresent || isModTagPresent ? _tagButtonRemoveColor : _tagButtonAddColor)) - { - return ImGui.Button(displayedLabel); - } + return ret; } private static string TrimButtonTextToWidth(string fullText, float maxWidth) @@ -156,5 +260,4 @@ public sealed class SharedTagManager { return ImGui.CalcTextSize(text).X + 2 * ImGui.GetStyle().FramePadding.X; } - } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index f37c2c81..71f108c2 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -73,8 +73,6 @@ public class SettingsTab : ITab if (_compactor.CanCompact) _compactor.Enabled = _config.UseFileSystemCompression; _sharedTagManager = sharedTagConfig; - if (sharedTagConfig.SharedTags.Count == 0 && _config.SharedTags != null) - sharedTagConfig.SharedTags = _config.SharedTags; } public void DrawHeader() @@ -916,14 +914,12 @@ public class SettingsTab : ITab return; var tagIdx = _sharedTags.Draw("Shared Tags: ", - "Tags that can be added/removed from mods with 1 click.", _sharedTagManager.SharedTags, + "Predefined tags that can be added or removed from mods with a single click.", _sharedTagManager.SharedTags, out var editedTag); if (tagIdx >= 0) { _sharedTagManager.ChangeSharedTag(tagIdx, editedTag); - _config.SharedTags = _sharedTagManager.SharedTags; - _config.Save(); } } } From 038c230427f6bf8c7bd2283939b343fcc60c6d4e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Mar 2024 13:59:40 +0100 Subject: [PATCH 4/9] Rename to Predefined. --- Penumbra/Services/FilenameService.cs | 6 +- Penumbra/Services/ServiceManagerA.cs | 2 +- Penumbra/UI/Classes/Colors.cs | 8 +- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 42 ++++------ Penumbra/UI/ModsTab/ModPanelEditTab.cs | 83 ++++++++----------- ...dTagManager.cs => PredefinedTagManager.cs} | 71 ++++++++-------- Penumbra/UI/Tabs/SettingsTab.cs | 22 +++-- 7 files changed, 102 insertions(+), 132 deletions(-) rename Penumbra/UI/{SharedTagManager.cs => PredefinedTagManager.cs} (83%) diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 20794f12..2de4bff0 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -14,7 +14,7 @@ public class FilenameService(DalamudPluginInterface pi) : IService 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"); - public readonly string SharedTagFile = Path.Combine(pi.ConfigDirectory.FullName, "shared_tags.json"); + public readonly string PredefinedTagFile = Path.Combine(pi.ConfigDirectory.FullName, "predefined_tags.json"); public readonly string CrashHandlerExe = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Penumbra.CrashHandler.exe"); @@ -44,7 +44,7 @@ public class FilenameService(DalamudPluginInterface pi) : IService get { var directory = new DirectoryInfo(CollectionDirectory); - return directory.Exists ? directory.EnumerateFiles("*.json") : Array.Empty(); + return directory.Exists ? directory.EnumerateFiles("*.json") : []; } } @@ -54,7 +54,7 @@ public class FilenameService(DalamudPluginInterface pi) : IService get { var directory = new DirectoryInfo(LocalDataDirectory); - return directory.Exists ? directory.EnumerateFiles("*.json") : Array.Empty(); + return directory.Exists ? directory.EnumerateFiles("*.json") : []; } } diff --git a/Penumbra/Services/ServiceManagerA.cs b/Penumbra/Services/ServiceManagerA.cs index cb2032a2..39ef0560 100644 --- a/Penumbra/Services/ServiceManagerA.cs +++ b/Penumbra/Services/ServiceManagerA.cs @@ -105,7 +105,7 @@ public static class ServiceManagerA private static ServiceManager AddConfiguration(this ServiceManager services) => services.AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton(); private static ServiceManager AddCollections(this ServiceManager services) => services.AddSingleton() diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 50096696..4d0c62af 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -28,8 +28,8 @@ public enum ColorId ResTreePlayer, ResTreeNetworked, ResTreeNonNetworked, - SharedTagAdd, - SharedTagRemove + PredefinedTagAdd, + PredefinedTagRemove, } public static class Colors @@ -75,8 +75,8 @@ public static class Colors ColorId.ResTreePlayer => ( 0xFFC0FFC0, "On-Screen: Other Players", "Other players and what they own, in the On-Screen tab." ), ColorId.ResTreeNetworked => ( 0xFFFFFFFF, "On-Screen: Non-Players (Networked)", "Non-player entities handled by the game server, in the On-Screen tab." ), ColorId.ResTreeNonNetworked => ( 0xFFC0C0FF, "On-Screen: Non-Players (Local)", "Non-player entities handled locally, in the On-Screen tab." ), - ColorId.SharedTagAdd => ( 0xFF44AA44, "Shared Tags: Add Tag", "A shared tag that is not present on the current mod and can be added." ), - ColorId.SharedTagRemove => ( 0xFF2222AA, "Shared Tags: Remove Tag", "A shared tag that is already present on the current mod and can be removed." ), + ColorId.PredefinedTagAdd => ( 0xFF44AA44, "Predefined Tags: Add Tag", "A predefined tag that is not present on the current mod and can be added." ), + ColorId.PredefinedTagRemove => ( 0xFF2222AA, "Predefined Tags: Remove Tag", "A predefined tag that is already present on the current mod and can be removed." ), _ => throw new ArgumentOutOfRangeException( nameof( color ), color, null ), // @formatter:on }; diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 5f2687c3..4c5e68ff 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -7,22 +7,15 @@ using Penumbra.Mods.Manager; namespace Penumbra.UI.ModsTab; -public class ModPanelDescriptionTab : ITab +public class ModPanelDescriptionTab( + ModFileSystemSelector selector, + TutorialService tutorial, + ModManager modManager, + PredefinedTagManager predefinedTagsConfig) + : ITab { - private readonly ModFileSystemSelector _selector; - private readonly TutorialService _tutorial; - private readonly ModManager _modManager; - private readonly SharedTagManager _sharedTagManager; - private readonly TagButtons _localTags = new(); - private readonly TagButtons _modTags = new(); - - public ModPanelDescriptionTab(ModFileSystemSelector selector, TutorialService tutorial, ModManager modManager, SharedTagManager sharedTagsConfig) - { - _selector = selector; - _tutorial = tutorial; - _modManager = modManager; - _sharedTagManager = sharedTagsConfig; - } + private readonly TagButtons _localTags = new(); + private readonly TagButtons _modTags = new(); public ReadOnlySpan Label => "Description"u8; @@ -36,29 +29,28 @@ public class ModPanelDescriptionTab : ITab ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); - var sharedTagsEnabled = _sharedTagManager.SharedTags.Count > 0; + var sharedTagsEnabled = predefinedTagsConfig.SharedTags.Count > 0; var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _localTags.Draw("Local Tags: ", "Custom tags you can set personally that will not be exported to the mod data but only set for you.\n" - + "If the mod already contains a local tag in its own tags, the local tag will be ignored.", _selector.Selected!.LocalTags, + + "If the mod already contains a local tag in its own tags, the local tag will be ignored.", selector.Selected!.LocalTags, out var editedTag, rightEndOffset: sharedTagButtonOffset); - _tutorial.OpenTutorial(BasicTutorialSteps.Tags); + tutorial.OpenTutorial(BasicTutorialSteps.Tags); if (tagIdx >= 0) - _modManager.DataEditor.ChangeLocalTag(_selector.Selected!, tagIdx, editedTag); + modManager.DataEditor.ChangeLocalTag(selector.Selected!, tagIdx, editedTag); if (sharedTagsEnabled) - { - _sharedTagManager.DrawAddFromSharedTagsAndUpdateTags(_selector.Selected!.LocalTags, _selector.Selected!.ModTags, true, _selector.Selected!); - } + predefinedTagsConfig.DrawAddFromSharedTagsAndUpdateTags(selector.Selected!.LocalTags, selector.Selected!.ModTags, true, + selector.Selected!); - if (_selector.Selected!.ModTags.Count > 0) + if (selector.Selected!.ModTags.Count > 0) _modTags.Draw("Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.", - _selector.Selected!.ModTags, out var _, false, + selector.Selected!.ModTags, out _, false, ImGui.CalcTextSize("Local ").X - ImGui.CalcTextSize("Mod ").X); ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); ImGui.Separator(); - ImGuiUtil.TextWrapped(_selector.Selected!.Description); + ImGuiUtil.TextWrapped(selector.Selected!.Description); } } diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 9b4a582f..275c89ef 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -17,18 +17,20 @@ using Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.ModsTab; -public class ModPanelEditTab : ITab +public class ModPanelEditTab( + ModManager modManager, + ModFileSystemSelector selector, + ModFileSystem fileSystem, + Services.MessageService messager, + ModEditWindow editWindow, + ModEditor editor, + FilenameService filenames, + ModExportManager modExportManager, + Configuration config, + PredefinedTagManager predefinedTagManager) + : ITab { - private readonly Services.MessageService _messager; - private readonly FilenameService _filenames; - private readonly ModManager _modManager; - private readonly ModExportManager _modExportManager; - private readonly ModFileSystem _fileSystem; - private readonly ModFileSystemSelector _selector; - private readonly ModEditWindow _editWindow; - private readonly ModEditor _editor; - private readonly Configuration _config; - private readonly SharedTagManager _sharedTagManager; + private readonly ModManager _modManager = modManager; private readonly TagButtons _modTags = new(); @@ -37,22 +39,6 @@ public class ModPanelEditTab : ITab private ModFileSystem.Leaf _leaf = null!; private Mod _mod = null!; - public ModPanelEditTab(ModManager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, Services.MessageService messager, - ModEditWindow editWindow, ModEditor editor, FilenameService filenames, ModExportManager modExportManager, Configuration config, - SharedTagManager sharedTagManager) - { - _modManager = modManager; - _selector = selector; - _fileSystem = fileSystem; - _messager = messager; - _editWindow = editWindow; - _editor = editor; - _filenames = filenames; - _modExportManager = modExportManager; - _config = config; - _sharedTagManager = sharedTagManager; - } - public ReadOnlySpan Label => "Edit Mod"u8; @@ -62,8 +48,8 @@ public class ModPanelEditTab : ITab if (!child) return; - _leaf = _selector.SelectedLeaf!; - _mod = _selector.Selected!; + _leaf = selector.SelectedLeaf!; + _mod = selector.Selected!; _cellPadding = ImGui.GetStyle().CellPadding with { X = 2 * UiHelpers.Scale }; _itemSpacing = ImGui.GetStyle().CellPadding with { X = 4 * UiHelpers.Scale }; @@ -75,15 +61,15 @@ public class ModPanelEditTab : ITab if (Input.Text("Mod Path", Input.Path, Input.None, _leaf.FullName(), out var newPath, 256, UiHelpers.InputTextWidth.X)) try { - _fileSystem.RenameAndMove(_leaf, newPath); + fileSystem.RenameAndMove(_leaf, newPath); } catch (Exception e) { - _messager.NotificationMessage(e.Message, NotificationType.Warning, false); + messager.NotificationMessage(e.Message, NotificationType.Warning, false); } UiHelpers.DefaultLineSpace(); - var sharedTagsEnabled = _sharedTagManager.SharedTags.Count > 0; + var sharedTagsEnabled = predefinedTagManager.SharedTags.Count > 0; var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, out var editedTag, rightEndOffset: sharedTagButtonOffset); @@ -91,12 +77,11 @@ public class ModPanelEditTab : ITab _modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag); if (sharedTagsEnabled) - { - _sharedTagManager.DrawAddFromSharedTagsAndUpdateTags(_selector.Selected!.LocalTags, _selector.Selected!.ModTags, false, _selector.Selected!); - } + predefinedTagManager.DrawAddFromSharedTagsAndUpdateTags(selector.Selected!.LocalTags, selector.Selected!.ModTags, false, + selector.Selected!); UiHelpers.DefaultLineSpace(); - AddOptionGroup.Draw(_filenames, _modManager, _mod, _config.ReplaceNonAsciiOnImport); + AddOptionGroup.Draw(filenames, _modManager, _mod, config.ReplaceNonAsciiOnImport); UiHelpers.DefaultLineSpace(); for (var groupIdx = 0; groupIdx < _mod.Groups.Count; ++groupIdx) @@ -144,11 +129,11 @@ public class ModPanelEditTab : ITab { if (ImGui.Button("Update Bibo Material", buttonSize)) { - _editor.LoadMod(_mod); - _editor.MdlMaterialEditor.ReplaceAllMaterials("bibo", "b"); - _editor.MdlMaterialEditor.ReplaceAllMaterials("bibopube", "c"); - _editor.MdlMaterialEditor.SaveAllModels(_editor.Compactor); - _editWindow.UpdateModels(); + editor.LoadMod(_mod); + editor.MdlMaterialEditor.ReplaceAllMaterials("bibo", "b"); + editor.MdlMaterialEditor.ReplaceAllMaterials("bibopube", "c"); + editor.MdlMaterialEditor.SaveAllModels(editor.Compactor); + editWindow.UpdateModels(); } ImGuiUtil.HoverTooltip( @@ -160,7 +145,7 @@ public class ModPanelEditTab : ITab private void BackupButtons(Vector2 buttonSize) { - var backup = new ModBackup(_modExportManager, _mod); + var backup = new ModBackup(modExportManager, _mod); var tt = ModBackup.CreatingBackup ? "Already exporting a mod." : backup.Exists @@ -171,16 +156,16 @@ public class ModPanelEditTab : ITab ImGui.SameLine(); tt = backup.Exists - ? $"Delete existing mod export \"{backup.Name}\" (hold {_config.DeleteModModifier} while clicking)." + ? $"Delete existing mod export \"{backup.Name}\" (hold {config.DeleteModModifier} while clicking)." : $"Exported mod \"{backup.Name}\" does not exist."; - if (ImGuiUtil.DrawDisabledButton("Delete Export", buttonSize, tt, !backup.Exists || !_config.DeleteModModifier.IsActive())) + if (ImGuiUtil.DrawDisabledButton("Delete Export", buttonSize, tt, !backup.Exists || !config.DeleteModModifier.IsActive())) backup.Delete(); tt = backup.Exists - ? $"Restore mod from exported file \"{backup.Name}\" (hold {_config.DeleteModModifier} while clicking)." + ? $"Restore mod from exported file \"{backup.Name}\" (hold {config.DeleteModModifier} while clicking)." : $"Exported mod \"{backup.Name}\" does not exist."; ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton("Restore From Export", buttonSize, tt, !backup.Exists || !_config.DeleteModModifier.IsActive())) + if (ImGuiUtil.DrawDisabledButton("Restore From Export", buttonSize, tt, !backup.Exists || !config.DeleteModModifier.IsActive())) backup.Restore(_modManager); if (backup.Exists) { @@ -218,13 +203,13 @@ public class ModPanelEditTab : ITab _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, Input.Description)); ImGui.SameLine(); - var fileExists = File.Exists(_filenames.ModMetaPath(_mod)); + var fileExists = File.Exists(filenames.ModMetaPath(_mod)); var tt = fileExists ? "Open the metadata json file in the text editor of your choice." : "The metadata json file does not exist."; if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##metaFile", UiHelpers.IconButtonSize, tt, !fileExists, true)) - Process.Start(new ProcessStartInfo(_filenames.ModMetaPath(_mod)) { UseShellExecute = true }); + Process.Start(new ProcessStartInfo(filenames.ModMetaPath(_mod)) { UseShellExecute = true }); } /// Do some edits outside of iterations. @@ -448,7 +433,7 @@ public class ModPanelEditTab : ITab _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, groupIdx)); ImGui.SameLine(); - var fileName = _filenames.OptionGroupFile(_mod, groupIdx, _config.ReplaceNonAsciiOnImport); + var fileName = filenames.OptionGroupFile(_mod, groupIdx, config.ReplaceNonAsciiOnImport); var fileExists = File.Exists(fileName); tt = fileExists ? $"Open the {group.Name} json file in the text editor of your choice." diff --git a/Penumbra/UI/SharedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs similarity index 83% rename from Penumbra/UI/SharedTagManager.cs rename to Penumbra/UI/PredefinedTagManager.cs index 23196319..fafca101 100644 --- a/Penumbra/UI/SharedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -1,6 +1,5 @@ using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Utility; using ImGuiNET; using Newtonsoft.Json; using OtterGui; @@ -12,44 +11,45 @@ using Penumbra.UI.Classes; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra.UI; -public sealed class SharedTagManager : ISavable + +public sealed class PredefinedTagManager : ISavable { - private readonly ModManager _modManager; + private readonly ModManager _modManager; private readonly SaveService _saveService; - private static uint _tagButtonAddColor = ColorId.SharedTagAdd.Value(); - private static uint _tagButtonRemoveColor = ColorId.SharedTagRemove.Value(); + private static uint _tagButtonAddColor = ColorId.PredefinedTagAdd.Value(); + private static uint _tagButtonRemoveColor = ColorId.PredefinedTagRemove.Value(); private static float _minTagButtonWidth = 15; private const string PopupContext = "SharedTagsPopup"; - private bool _isPopupOpen = false; + private bool _isPopupOpen = false; // Operations on this list assume that it is sorted and will keep it sorted if that is the case. // The list also gets re-sorted when first loaded from config in case the config was modified. [JsonRequired] private readonly List _sharedTags = []; + [JsonIgnore] - public IReadOnlyList SharedTags => _sharedTags; + public IReadOnlyList SharedTags + => _sharedTags; public int ConfigVersion = 1; - public SharedTagManager(ModManager modManager, SaveService saveService) + public PredefinedTagManager(ModManager modManager, SaveService saveService) { - _modManager = modManager; + _modManager = modManager; _saveService = saveService; Load(); } public string ToFilename(FilenameService fileNames) - { - return fileNames.SharedTagFile; - } + => fileNames.PredefinedTagFile; public void Save(StreamWriter writer) { - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; serializer.Serialize(jWriter, this); } @@ -65,12 +65,12 @@ public sealed class SharedTagManager : ISavable errorArgs.ErrorContext.Handled = true; } - if (!File.Exists(_saveService.FileNames.SharedTagFile)) + if (!File.Exists(_saveService.FileNames.PredefinedTagFile)) return; try { - var text = File.ReadAllText(_saveService.FileNames.SharedTagFile); + var text = File.ReadAllText(_saveService.FileNames.PredefinedTagFile); JsonConvert.PopulateObject(text, this, new JsonSerializerSettings { Error = HandleDeserializationError, @@ -94,9 +94,7 @@ public sealed class SharedTagManager : ISavable // In the case of editing a tag, remove what's there prior to doing an insert. if (tagIdx != SharedTags.Count) - { _sharedTags.RemoveAt(tagIdx); - } if (!string.IsNullOrEmpty(tag)) { @@ -109,7 +107,8 @@ public sealed class SharedTagManager : ISavable Save(); } - public void DrawAddFromSharedTagsAndUpdateTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal, Mods.Mod mod) + public void DrawAddFromSharedTagsAndUpdateTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal, + Mods.Mod mod) { ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetFrameHeightWithSpacing()); ImGui.SetCursorPosX(ImGui.GetWindowWidth() - ImGui.GetFrameHeight() - ImGui.GetStyle().FramePadding.X); @@ -131,7 +130,8 @@ public sealed class SharedTagManager : ISavable { _modManager.DataEditor.ChangeLocalTag(mod, index, string.Empty); } - } else + } + else { if (index < 0) { @@ -143,15 +143,16 @@ public sealed class SharedTagManager : ISavable _modManager.DataEditor.ChangeModTag(mod, index, string.Empty); } } - } } public string DrawAddFromSharedTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal) { var tagToAdd = string.Empty; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Tags.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Add Shared Tag... (Right-click to close popup)", - false, true) || _isPopupOpen) + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Tags.ToIconString(), new Vector2(ImGui.GetFrameHeight()), + "Add Shared Tag... (Right-click to close popup)", + false, true) + || _isPopupOpen) return DrawSharedTagsPopup(localTags, modTags, editLocal); return tagToAdd; @@ -167,9 +168,9 @@ public sealed class SharedTagManager : ISavable } var display = ImGui.GetIO().DisplaySize; - var height = Math.Min(display.Y / 4, 10 * ImGui.GetFrameHeightWithSpacing()); - var width = display.X / 6; - var size = new Vector2(width, height); + var height = Math.Min(display.Y / 4, 10 * ImGui.GetFrameHeightWithSpacing()); + var width = display.X / 6; + var size = new Vector2(width, height); ImGui.SetNextWindowSize(size); using var popup = ImRaii.Popup(PopupContext); if (!popup) @@ -182,26 +183,23 @@ public sealed class SharedTagManager : ISavable foreach (var (tag, idx) in SharedTags.WithIndex()) { if (DrawColoredButton(localTags, modTags, tag, editLocal, idx)) - { selected = tag; - } ImGui.SameLine(); } if (ImGui.IsMouseClicked(ImGuiMouseButton.Right)) - { _isPopupOpen = false; - } return selected; } - private static bool DrawColoredButton(IReadOnlyCollection localTags, IReadOnlyCollection modTags, string buttonLabel, bool editLocal, int index) + private static bool DrawColoredButton(IReadOnlyCollection localTags, IReadOnlyCollection modTags, string buttonLabel, + bool editLocal, int index) { var ret = false; var isLocalTagPresent = localTags.Contains(buttonLabel); - var isModTagPresent = modTags.Contains(buttonLabel); + var isModTagPresent = modTags.Contains(buttonLabel); var buttonWidth = CalcTextButtonWidth(buttonLabel); // Would prefer to be able to fit at least 2 buttons per line so the popup doesn't look sparse with lots of long tags. Thus long tags will be trimmed. @@ -210,7 +208,7 @@ public sealed class SharedTagManager : ISavable if (buttonWidth >= maxButtonWidth) { displayedLabel = TrimButtonTextToWidth(buttonLabel, maxButtonWidth); - buttonWidth = CalcTextButtonWidth(displayedLabel); + buttonWidth = CalcTextButtonWidth(displayedLabel); } // Prevent adding a new tag past the right edge of the popup @@ -247,9 +245,8 @@ public sealed class SharedTagManager : ISavable // An ellipsis will be used to indicate trimmed tags if (CalcTextButtonWidth(nextTrim + "...") < maxWidth) - { return nextTrim + "..."; - } + trimmedText = nextTrim; } @@ -257,7 +254,5 @@ public sealed class SharedTagManager : ISavable } private static float CalcTextButtonWidth(string text) - { - return ImGui.CalcTextSize(text).X + 2 * ImGui.GetStyle().FramePadding.X; - } + => ImGui.CalcTextSize(text).X + 2 * ImGui.GetStyle().FramePadding.X; } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index b311bb93..c36c63b2 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -42,7 +42,7 @@ public class SettingsTab : ITab private readonly DalamudConfigService _dalamudConfig; private readonly DalamudPluginInterface _pluginInterface; private readonly IDataManager _gameData; - private readonly SharedTagManager _sharedTagManager; + private readonly PredefinedTagManager _predefinedTagManager; private int _minimumX = int.MaxValue; private int _minimumY = int.MaxValue; @@ -53,7 +53,7 @@ public class SettingsTab : ITab Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, - IDataManager gameData, SharedTagManager sharedTagConfig) + IDataManager gameData, PredefinedTagManager predefinedTagConfig) { _pluginInterface = pluginInterface; _config = config; @@ -73,7 +73,7 @@ public class SettingsTab : ITab _gameData = gameData; if (_compactor.CanCompact) _compactor.Enabled = _config.UseFileSystemCompression; - _sharedTagManager = sharedTagConfig; + _predefinedTagManager = predefinedTagConfig; } public void DrawHeader() @@ -101,7 +101,7 @@ public class SettingsTab : ITab DrawGeneralSettings(); DrawColorSettings(); DrawAdvancedSettings(); - DrawSharedTagsSection(); + DrawPredefinedTagsSection(); DrawSupportButtons(); } @@ -239,7 +239,7 @@ public class SettingsTab : ITab } var selected = ImGui.IsItemActive(); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0)); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0)); ImGui.SameLine(); DrawDirectoryPickerButton(); style.Pop(); @@ -388,7 +388,7 @@ public class SettingsTab : ITab "Hide the Penumbra main window when you manually hide the in-game user interface.", _config.HideUiWhenUiHidden, v => { - _config.HideUiWhenUiHidden = v; + _config.HideUiWhenUiHidden = v; _pluginInterface.UiBuilder.DisableUserUiHide = !v; }); Checkbox("Hide Config Window when in Cutscenes", @@ -917,18 +917,16 @@ public class SettingsTab : ITab _penumbra.ForceChangelogOpen(); } - private void DrawSharedTagsSection() + private void DrawPredefinedTagsSection() { if (!ImGui.CollapsingHeader("Tags")) return; - var tagIdx = _sharedTags.Draw("Shared Tags: ", - "Predefined tags that can be added or removed from mods with a single click.", _sharedTagManager.SharedTags, + var tagIdx = _sharedTags.Draw("Predefined Tags: ", + "Predefined tags that can be added or removed from mods with a single click.", _predefinedTagManager.SharedTags, out var editedTag); if (tagIdx >= 0) - { - _sharedTagManager.ChangeSharedTag(tagIdx, editedTag); - } + _predefinedTagManager.ChangeSharedTag(tagIdx, editedTag); } } From 814aa92e19e7a971ef12c2950c0cff023d3e0b8d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Mar 2024 14:08:14 +0100 Subject: [PATCH 5/9] Move tags before advanced. --- Penumbra/UI/Tabs/SettingsTab.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index c36c63b2..60c18d5f 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -100,8 +100,8 @@ public class SettingsTab : ITab DrawGeneralSettings(); DrawColorSettings(); - DrawAdvancedSettings(); DrawPredefinedTagsSection(); + DrawAdvancedSettings(); DrawSupportButtons(); } From 19526dd92d4361fe7eebbeb1bc6fe0844be7a8c0 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 17 Mar 2024 14:09:01 +0100 Subject: [PATCH 6/9] Help marker position. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index 1be9365d..cf42043c 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 1be9365d048bf1da3700e8cf1df9acbe42523f5c +Subproject commit cf42043c2b0e76b59919688dc250a762fe52d4b1 From 0f89e243779334d3c916698290e9157fd787775c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 18 Mar 2024 17:12:30 +0100 Subject: [PATCH 7/9] Improve tag button positioning. --- OtterGui | 2 +- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 9 +++++---- Penumbra/UI/PredefinedTagManager.cs | 4 +--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/OtterGui b/OtterGui index 5a2e12a1..c59b1c09 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5a2e12a1acd6760a3a592447a92215135e79197c +Subproject commit c59b1c09ff7b8093d3a70c45957f9c41341dd3a4 diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 4c5e68ff..e1b80b23 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -29,17 +29,18 @@ public class ModPanelDescriptionTab( ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); - var sharedTagsEnabled = predefinedTagsConfig.SharedTags.Count > 0; - var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; + var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.SharedTags.Count > 0 + ? (true, ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.X + (ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0)) + : (false, 0); var tagIdx = _localTags.Draw("Local Tags: ", "Custom tags you can set personally that will not be exported to the mod data but only set for you.\n" + "If the mod already contains a local tag in its own tags, the local tag will be ignored.", selector.Selected!.LocalTags, - out var editedTag, rightEndOffset: sharedTagButtonOffset); + out var editedTag, rightEndOffset: predefinedTagButtonOffset); tutorial.OpenTutorial(BasicTutorialSteps.Tags); if (tagIdx >= 0) modManager.DataEditor.ChangeLocalTag(selector.Selected!, tagIdx, editedTag); - if (sharedTagsEnabled) + if (predefinedTagsEnabled) predefinedTagsConfig.DrawAddFromSharedTagsAndUpdateTags(selector.Selected!.LocalTags, selector.Selected!.ModTags, true, selector.Selected!); diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index fafca101..b85b5dea 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -110,9 +110,7 @@ public sealed class PredefinedTagManager : ISavable public void DrawAddFromSharedTagsAndUpdateTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal, Mods.Mod mod) { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetFrameHeightWithSpacing()); - ImGui.SetCursorPosX(ImGui.GetWindowWidth() - ImGui.GetFrameHeight() - ImGui.GetStyle().FramePadding.X); - + ImGui.SameLine(ImGui.GetContentRegionMax().X - ImGui.GetFrameHeight() - ImGui.GetStyle().WindowPadding.X); var sharedTag = DrawAddFromSharedTags(localTags, modTags, editLocal); if (sharedTag.Length > 0) From 52c1708dd270ec46a3de242d57a42116a9de094c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Mar 2024 20:45:16 +0100 Subject: [PATCH 8/9] Change predefined tag handling. --- Penumbra/Mods/Mod.cs | 6 +- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 2 +- Penumbra/UI/PredefinedTagManager.cs | 210 ++++++------------ Penumbra/UI/Tabs/ModsTab.cs | 2 +- Penumbra/UI/Tabs/SettingsTab.cs | 65 +++--- 6 files changed, 112 insertions(+), 175 deletions(-) diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index a9ef22cb..c5e671af 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -45,19 +45,19 @@ public sealed class Mod : IMod public string Description { get; internal set; } = string.Empty; public string Version { get; internal set; } = string.Empty; public string Website { get; internal set; } = string.Empty; - public IReadOnlyList ModTags { get; internal set; } = Array.Empty(); + public IReadOnlyList ModTags { get; internal set; } = []; // Local Data public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); - public IReadOnlyList LocalTags { get; internal set; } = Array.Empty(); + public IReadOnlyList LocalTags { get; internal set; } = []; public string Note { get; internal set; } = string.Empty; public bool Favorite { get; internal set; } = false; // Options public readonly SubMod Default; - public readonly List Groups = new(); + public readonly List Groups = []; ISubMod IMod.Default => Default; diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index e1b80b23..4ad30a6f 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -29,7 +29,7 @@ public class ModPanelDescriptionTab( ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); - var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.SharedTags.Count > 0 + var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.PredefinedTags.Count > 0 ? (true, ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.X + (ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0)) : (false, 0); var tagIdx = _localTags.Draw("Local Tags: ", diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 275c89ef..a0e32c22 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -69,7 +69,7 @@ public class ModPanelEditTab( } UiHelpers.DefaultLineSpace(); - var sharedTagsEnabled = predefinedTagManager.SharedTags.Count > 0; + var sharedTagsEnabled = predefinedTagManager.PredefinedTags.Count > 0; var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, out var editedTag, rightEndOffset: sharedTagButtonOffset); diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index b85b5dea..63be42de 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -17,22 +17,19 @@ public sealed class PredefinedTagManager : ISavable private readonly ModManager _modManager; private readonly SaveService _saveService; - private static uint _tagButtonAddColor = ColorId.PredefinedTagAdd.Value(); - private static uint _tagButtonRemoveColor = ColorId.PredefinedTagRemove.Value(); + private bool _isListOpen = false; + private uint _enabledColor; + private uint _disabledColor; - private static float _minTagButtonWidth = 15; - - private const string PopupContext = "SharedTagsPopup"; - private bool _isPopupOpen = false; // Operations on this list assume that it is sorted and will keep it sorted if that is the case. // The list also gets re-sorted when first loaded from config in case the config was modified. [JsonRequired] - private readonly List _sharedTags = []; + private readonly List _predefinedTags = []; [JsonIgnore] - public IReadOnlyList SharedTags - => _sharedTags; + public IReadOnlyList PredefinedTags + => _predefinedTags; public int ConfigVersion = 1; @@ -48,8 +45,9 @@ public sealed class PredefinedTagManager : ISavable public void Save(StreamWriter writer) { - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + using var jWriter = new JsonTextWriter(writer); + jWriter.Formatting = Formatting.Indented; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; serializer.Serialize(jWriter, this); } @@ -58,13 +56,6 @@ public sealed class PredefinedTagManager : ISavable private void Load() { - static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) - { - Penumbra.Log.Error( - $"Error parsing shared tags Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); - errorArgs.ErrorContext.Handled = true; - } - if (!File.Exists(_saveService.FileNames.PredefinedTagFile)) return; @@ -77,7 +68,7 @@ public sealed class PredefinedTagManager : ISavable }); // Any changes to this within this class should keep it sorted, but in case someone went in and manually changed the JSON, run a sort on initial load. - _sharedTags.Sort(); + _predefinedTags.Sort(); } catch (Exception ex) { @@ -85,23 +76,32 @@ public sealed class PredefinedTagManager : ISavable "Error reading shared tags Configuration, reverting to default.", "Error reading shared tags Configuration", NotificationType.Error); } + + return; + + static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) + { + Penumbra.Log.Error( + $"Error parsing shared tags Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); + errorArgs.ErrorContext.Handled = true; + } } public void ChangeSharedTag(int tagIdx, string tag) { - if (tagIdx < 0 || tagIdx > SharedTags.Count) + if (tagIdx < 0 || tagIdx > PredefinedTags.Count) return; // In the case of editing a tag, remove what's there prior to doing an insert. - if (tagIdx != SharedTags.Count) - _sharedTags.RemoveAt(tagIdx); + if (tagIdx != PredefinedTags.Count) + _predefinedTags.RemoveAt(tagIdx); if (!string.IsNullOrEmpty(tag)) { // Taking advantage of the fact that BinarySearch returns the complement of the correct sorted position for the tag. - var existingIdx = _sharedTags.BinarySearch(tag); + var existingIdx = _predefinedTags.BinarySearch(tag); if (existingIdx < 0) - _sharedTags.Insert(~existingIdx, tag); + _predefinedTags.Insert(~existingIdx, tag); } Save(); @@ -110,147 +110,83 @@ public sealed class PredefinedTagManager : ISavable public void DrawAddFromSharedTagsAndUpdateTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal, Mods.Mod mod) { - ImGui.SameLine(ImGui.GetContentRegionMax().X - ImGui.GetFrameHeight() - ImGui.GetStyle().WindowPadding.X); - var sharedTag = DrawAddFromSharedTags(localTags, modTags, editLocal); + DrawToggleButton(); + if (!DrawList(localTags, modTags, editLocal, out var changedTag, out var index)) + return; - if (sharedTag.Length > 0) - { - var index = editLocal ? mod.LocalTags.IndexOf(sharedTag) : mod.ModTags.IndexOf(sharedTag); - - if (editLocal) - { - if (index < 0) - { - index = mod.LocalTags.Count; - _modManager.DataEditor.ChangeLocalTag(mod, index, sharedTag); - } - else - { - _modManager.DataEditor.ChangeLocalTag(mod, index, string.Empty); - } - } - else - { - if (index < 0) - { - index = mod.ModTags.Count; - _modManager.DataEditor.ChangeModTag(mod, index, sharedTag); - } - else - { - _modManager.DataEditor.ChangeModTag(mod, index, string.Empty); - } - } - } + if (editLocal) + _modManager.DataEditor.ChangeLocalTag(mod, index, changedTag); + else + _modManager.DataEditor.ChangeModTag(mod, index, changedTag); } - public string DrawAddFromSharedTags(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal) + private void DrawToggleButton() { - var tagToAdd = string.Empty; + ImGui.SameLine(ImGui.GetContentRegionMax().X + - ImGui.GetFrameHeight() + - (ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ItemInnerSpacing.X : 0)); + using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.ButtonActive), _isListOpen); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Tags.ToIconString(), new Vector2(ImGui.GetFrameHeight()), - "Add Shared Tag... (Right-click to close popup)", - false, true) - || _isPopupOpen) - return DrawSharedTagsPopup(localTags, modTags, editLocal); - - return tagToAdd; + "Add Predefined Tags...", false, true)) + _isListOpen = !_isListOpen; } - private string DrawSharedTagsPopup(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal) + private bool DrawList(IReadOnlyCollection localTags, IReadOnlyCollection modTags, bool editLocal, out string changedTag, + out int changedIndex) { - var selected = string.Empty; - if (!ImGui.IsPopupOpen(PopupContext)) - { - ImGui.OpenPopup(PopupContext); - _isPopupOpen = true; - } + changedTag = string.Empty; + changedIndex = -1; - var display = ImGui.GetIO().DisplaySize; - var height = Math.Min(display.Y / 4, 10 * ImGui.GetFrameHeightWithSpacing()); - var width = display.X / 6; - var size = new Vector2(width, height); - ImGui.SetNextWindowSize(size); - using var popup = ImRaii.Popup(PopupContext); - if (!popup) - return selected; + if (!_isListOpen) + return false; - ImGui.TextUnformatted("Shared Tags"); - ImGuiUtil.HoverTooltip("Right-click to close popup"); + ImGui.TextUnformatted("Predefined Tags"); ImGui.Separator(); - foreach (var (tag, idx) in SharedTags.WithIndex()) + var ret = false; + _enabledColor = ColorId.PredefinedTagAdd.Value(); + _disabledColor = ColorId.PredefinedTagRemove.Value(); + var (edited, others) = editLocal ? (localTags, modTags) : (modTags, localTags); + foreach (var (tag, idx) in PredefinedTags.WithIndex()) { - if (DrawColoredButton(localTags, modTags, tag, editLocal, idx)) - selected = tag; + var tagIdx = edited.IndexOf(tag); + var inOther = tagIdx < 0 && others.IndexOf(tag) >= 0; + if (DrawColoredButton(tag, idx, tagIdx, inOther)) + { + (changedTag, changedIndex) = tagIdx >= 0 ? (string.Empty, tagIdx) : (tag, edited.Count); + ret = true; + } + ImGui.SameLine(); } - if (ImGui.IsMouseClicked(ImGuiMouseButton.Right)) - _isPopupOpen = false; - - return selected; + ImGui.NewLine(); + ImGui.Separator(); + return ret; } - private static bool DrawColoredButton(IReadOnlyCollection localTags, IReadOnlyCollection modTags, string buttonLabel, - bool editLocal, int index) + private bool DrawColoredButton(string buttonLabel, int index, int tagIdx, bool inOther) { - var ret = false; - - var isLocalTagPresent = localTags.Contains(buttonLabel); - var isModTagPresent = modTags.Contains(buttonLabel); - - var buttonWidth = CalcTextButtonWidth(buttonLabel); - // Would prefer to be able to fit at least 2 buttons per line so the popup doesn't look sparse with lots of long tags. Thus long tags will be trimmed. - var maxButtonWidth = (ImGui.GetContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) * 0.5f - ImGui.GetStyle().ItemSpacing.X; - var displayedLabel = buttonLabel; - if (buttonWidth >= maxButtonWidth) - { - displayedLabel = TrimButtonTextToWidth(buttonLabel, maxButtonWidth); - buttonWidth = CalcTextButtonWidth(displayedLabel); - } - + using var id = ImRaii.PushId(index); + var buttonWidth = CalcTextButtonWidth(buttonLabel); // Prevent adding a new tag past the right edge of the popup if (buttonWidth + ImGui.GetStyle().ItemSpacing.X >= ImGui.GetContentRegionAvail().X) ImGui.NewLine(); - // Trimmed tag names can collide, and while tag names are currently distinct this may not always be the case. As such use the index to avoid an ImGui moment. - using var id = ImRaii.PushId(index); + bool ret; + using (ImRaii.Disabled(inOther)) + { + using var color = ImRaii.PushColor(ImGuiCol.Button, tagIdx >= 0 || inOther ? _disabledColor : _enabledColor); + ret = ImGui.Button(buttonLabel); + } + + if (inOther && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip("This tag is already present in the other set of tags."); - if (editLocal && isModTagPresent || !editLocal && isLocalTagPresent) - { - using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f); - ImGui.Button(displayedLabel); - } - else - { - using (ImRaii.PushColor(ImGuiCol.Button, isLocalTagPresent || isModTagPresent ? _tagButtonRemoveColor : _tagButtonAddColor)) - { - if (ImGui.Button(displayedLabel)) - ret = true; - } - } return ret; } - private static string TrimButtonTextToWidth(string fullText, float maxWidth) - { - var trimmedText = fullText; - - while (trimmedText.Length > _minTagButtonWidth) - { - var nextTrim = trimmedText.Substring(0, Math.Max(trimmedText.Length - 1, 0)); - - // An ellipsis will be used to indicate trimmed tags - if (CalcTextButtonWidth(nextTrim + "...") < maxWidth) - return nextTrim + "..."; - - trimmedText = nextTrim; - } - - return trimmedText; - } - private static float CalcTextButtonWidth(string text) => ImGui.CalcTextSize(text).X + 2 * ImGui.GetStyle().FramePadding.X; } diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index d111c465..bb8856b3 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -110,7 +110,7 @@ public class ModsTab( var frameColor = ImGui.GetColorU32(ImGuiCol.FrameBg); using (var _ = ImRaii.Group()) { - using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + using (ImRaii.PushFont(UiBuilder.IconFont)) { ImGuiUtil.DrawTextButton(FontAwesomeIcon.InfoCircle.ToIconString(), frameHeight, frameColor); ImGui.SameLine(); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 60c18d5f..9f8ffb38 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -227,37 +227,38 @@ public class SettingsTab : ITab if (_newModDirectory.IsNullOrEmpty()) _newModDirectory = _config.ModDirectory; - using var group = ImRaii.Group(); - ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); - bool save; - using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, !_modManager.Valid)) - { - using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder) - .Push(ImGuiCol.TextDisabled, Colors.RegexWarningBorder, !_modManager.Valid); - save = ImGui.InputTextWithHint("##rootDirectory", "Enter Root Directory here (MANDATORY)...", ref _newModDirectory, - RootDirectoryMaxLength, ImGuiInputTextFlags.EnterReturnsTrue); + bool save, selected; + using (ImRaii.Group()) + { + ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); + using (ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, !_modManager.Valid)) + { + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder) + .Push(ImGuiCol.TextDisabled, Colors.RegexWarningBorder, !_modManager.Valid); + save = ImGui.InputTextWithHint("##rootDirectory", "Enter Root Directory here (MANDATORY)...", ref _newModDirectory, + RootDirectoryMaxLength, ImGuiInputTextFlags.EnterReturnsTrue); + } + + selected = ImGui.IsItemActive(); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0)); + ImGui.SameLine(); + DrawDirectoryPickerButton(); + style.Pop(); + ImGui.SameLine(); + + const string tt = "This is where Penumbra will store your extracted mod files.\n" + + "TTMP files are not copied, just extracted.\n" + + "This directory needs to be accessible and you need write access here.\n" + + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" + + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" + + "Definitely do not place it in your Dalamud directory or any sub-directory thereof."; + ImGuiComponents.HelpMarker(tt); + _tutorial.OpenTutorial(BasicTutorialSteps.GeneralTooltips); + ImGui.SameLine(); + ImGui.TextUnformatted("Root Directory"); + ImGuiUtil.HoverTooltip(tt); } - var selected = ImGui.IsItemActive(); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0)); - ImGui.SameLine(); - DrawDirectoryPickerButton(); - style.Pop(); - ImGui.SameLine(); - - const string tt = "This is where Penumbra will store your extracted mod files.\n" - + "TTMP files are not copied, just extracted.\n" - + "This directory needs to be accessible and you need write access here.\n" - + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" - + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" - + "Definitely do not place it in your Dalamud directory or any sub-directory thereof."; - ImGuiComponents.HelpMarker(tt); - _tutorial.OpenTutorial(BasicTutorialSteps.GeneralTooltips); - ImGui.SameLine(); - ImGui.TextUnformatted("Root Directory"); - ImGuiUtil.HoverTooltip(tt); - - group.Dispose(); _tutorial.OpenTutorial(BasicTutorialSteps.ModDirectory); ImGui.SameLine(); var pos = ImGui.GetCursorPosX(); @@ -685,7 +686,7 @@ public class SettingsTab : ITab foreach (var color in Enum.GetValues()) { var (defaultColor, name, description) = color.Data(); - var currentColor = _config.Colors.TryGetValue(color, out var current) ? current : defaultColor; + var currentColor = _config.Colors.GetValueOrDefault(color, defaultColor); if (Widget.ColorPicker(name, description, currentColor, c => _config.Colors[color] = c, defaultColor)) _config.Save(); } @@ -871,7 +872,7 @@ public class SettingsTab : ITab if (!_dalamudConfig.GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool value)) { using var disabled = ImRaii.Disabled(); - Checkbox("Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)", string.Empty, false, v => { }); + Checkbox("Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)", string.Empty, false, _ => { }); } else { @@ -923,7 +924,7 @@ public class SettingsTab : ITab return; var tagIdx = _sharedTags.Draw("Predefined Tags: ", - "Predefined tags that can be added or removed from mods with a single click.", _predefinedTagManager.SharedTags, + "Predefined tags that can be added or removed from mods with a single click.", _predefinedTagManager.PredefinedTags, out var editedTag); if (tagIdx >= 0) From 0e50a8a9e5155423ab8db2334a07861d83cf44db Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 19 Mar 2024 21:01:48 +0100 Subject: [PATCH 9/9] More future proof structure for tags. --- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 2 +- Penumbra/UI/PredefinedTagManager.cs | 86 ++++++++++--------- Penumbra/UI/Tabs/SettingsTab.cs | 2 +- 4 files changed, 47 insertions(+), 45 deletions(-) diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index 4ad30a6f..ed6340ab 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -29,7 +29,7 @@ public class ModPanelDescriptionTab( ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); - var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.PredefinedTags.Count > 0 + var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.Count > 0 ? (true, ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.X + (ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0)) : (false, 0); var tagIdx = _localTags.Draw("Local Tags: ", diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index a0e32c22..eb79869e 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -69,7 +69,7 @@ public class ModPanelEditTab( } UiHelpers.DefaultLineSpace(); - var sharedTagsEnabled = predefinedTagManager.PredefinedTags.Count > 0; + var sharedTagsEnabled = predefinedTagManager.Count > 0; var sharedTagButtonOffset = sharedTagsEnabled ? ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X : 0; var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, out var editedTag, rightEndOffset: sharedTagButtonOffset); diff --git a/Penumbra/UI/PredefinedTagManager.cs b/Penumbra/UI/PredefinedTagManager.cs index 63be42de..17e8432b 100644 --- a/Penumbra/UI/PredefinedTagManager.cs +++ b/Penumbra/UI/PredefinedTagManager.cs @@ -2,6 +2,7 @@ using Dalamud.Interface.Internal.Notifications; using ImGuiNET; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; @@ -12,8 +13,13 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; namespace Penumbra.UI; -public sealed class PredefinedTagManager : ISavable +public sealed class PredefinedTagManager : ISavable, IReadOnlyList { + public const int Version = 1; + + public record struct TagData + { } + private readonly ModManager _modManager; private readonly SaveService _saveService; @@ -21,17 +27,7 @@ public sealed class PredefinedTagManager : ISavable private uint _enabledColor; private uint _disabledColor; - - // Operations on this list assume that it is sorted and will keep it sorted if that is the case. - // The list also gets re-sorted when first loaded from config in case the config was modified. - [JsonRequired] - private readonly List _predefinedTags = []; - - [JsonIgnore] - public IReadOnlyList PredefinedTags - => _predefinedTags; - - public int ConfigVersion = 1; + private readonly SortedList _predefinedTags = []; public PredefinedTagManager(ModManager modManager, SaveService saveService) { @@ -47,8 +43,12 @@ public sealed class PredefinedTagManager : ISavable { using var jWriter = new JsonTextWriter(writer); jWriter.Formatting = Formatting.Indented; - var serializer = new JsonSerializer { Formatting = Formatting.Indented }; - serializer.Serialize(jWriter, this); + var jObj = new JObject() + { + ["Version"] = Version, + ["Tags"] = JObject.FromObject(_predefinedTags), + }; + jObj.WriteTo(jWriter); } public void Save() @@ -61,48 +61,38 @@ public sealed class PredefinedTagManager : ISavable try { - var text = File.ReadAllText(_saveService.FileNames.PredefinedTagFile); - JsonConvert.PopulateObject(text, this, new JsonSerializerSettings + var text = File.ReadAllText(_saveService.FileNames.PredefinedTagFile); + var jObj = JObject.Parse(text); + var version = jObj["Version"]?.ToObject() ?? 0; + switch (version) { - Error = HandleDeserializationError, - }); - - // Any changes to this within this class should keep it sorted, but in case someone went in and manually changed the JSON, run a sort on initial load. - _predefinedTags.Sort(); + case 1: + var tags = jObj["Tags"]?.ToObject>() ?? []; + foreach (var (tag, data) in tags) + _predefinedTags.TryAdd(tag, data); + break; + default: + throw new Exception($"Invalid version {version}."); + } } catch (Exception ex) { Penumbra.Messager.NotificationMessage(ex, - "Error reading shared tags Configuration, reverting to default.", - "Error reading shared tags Configuration", NotificationType.Error); - } - - return; - - static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs) - { - Penumbra.Log.Error( - $"Error parsing shared tags Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); - errorArgs.ErrorContext.Handled = true; + "Error reading predefined tags Configuration, reverting to default.", + "Error reading predefined tags Configuration", NotificationType.Error); } } public void ChangeSharedTag(int tagIdx, string tag) { - if (tagIdx < 0 || tagIdx > PredefinedTags.Count) + if (tagIdx < 0 || tagIdx > _predefinedTags.Count) return; - // In the case of editing a tag, remove what's there prior to doing an insert. - if (tagIdx != PredefinedTags.Count) + if (tagIdx != _predefinedTags.Count) _predefinedTags.RemoveAt(tagIdx); if (!string.IsNullOrEmpty(tag)) - { - // Taking advantage of the fact that BinarySearch returns the complement of the correct sorted position for the tag. - var existingIdx = _predefinedTags.BinarySearch(tag); - if (existingIdx < 0) - _predefinedTags.Insert(~existingIdx, tag); - } + _predefinedTags.TryAdd(tag, default); Save(); } @@ -147,7 +137,7 @@ public sealed class PredefinedTagManager : ISavable _enabledColor = ColorId.PredefinedTagAdd.Value(); _disabledColor = ColorId.PredefinedTagRemove.Value(); var (edited, others) = editLocal ? (localTags, modTags) : (modTags, localTags); - foreach (var (tag, idx) in PredefinedTags.WithIndex()) + foreach (var (tag, idx) in _predefinedTags.Keys.WithIndex()) { var tagIdx = edited.IndexOf(tag); var inOther = tagIdx < 0 && others.IndexOf(tag) >= 0; @@ -189,4 +179,16 @@ public sealed class PredefinedTagManager : ISavable private static float CalcTextButtonWidth(string text) => ImGui.CalcTextSize(text).X + 2 * ImGui.GetStyle().FramePadding.X; + + public IEnumerator GetEnumerator() + => _predefinedTags.Keys.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _predefinedTags.Count; + + public string this[int index] + => _predefinedTags.Keys[index]; } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 9f8ffb38..80fe6fb6 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -924,7 +924,7 @@ public class SettingsTab : ITab return; var tagIdx = _sharedTags.Draw("Predefined Tags: ", - "Predefined tags that can be added or removed from mods with a single click.", _predefinedTagManager.PredefinedTags, + "Predefined tags that can be added or removed from mods with a single click.", _predefinedTagManager, out var editedTag); if (tagIdx >= 0)