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(); + } + } }