Change predefined tag handling.

This commit is contained in:
Ottermandias 2024-03-19 20:45:16 +01:00
parent 2e2d3e173e
commit 52c1708dd2
6 changed files with 112 additions and 175 deletions

View file

@ -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<string> ModTags { get; internal set; } = Array.Empty<string>();
public IReadOnlyList<string> ModTags { get; internal set; } = [];
// Local Data
public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();
public IReadOnlyList<string> LocalTags { get; internal set; } = Array.Empty<string>();
public IReadOnlyList<string> 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<IModGroup> Groups = new();
public readonly List<IModGroup> Groups = [];
ISubMod IMod.Default
=> Default;

View file

@ -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: ",

View file

@ -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);

View file

@ -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<string> _sharedTags = [];
private readonly List<string> _predefinedTags = [];
[JsonIgnore]
public IReadOnlyList<string> SharedTags
=> _sharedTags;
public IReadOnlyList<string> 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<string> localTags, IReadOnlyCollection<string> 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<string> localTags, IReadOnlyCollection<string> 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<string> localTags, IReadOnlyCollection<string> modTags, bool editLocal)
private bool DrawList(IReadOnlyCollection<string> localTags, IReadOnlyCollection<string> 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<string> localTags, IReadOnlyCollection<string> 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;
}

View file

@ -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();

View file

@ -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<ColorId>())
{
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)