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)