diff --git a/OtterGui b/OtterGui index bc2afed8..866389b3 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit bc2afed8a873d1f9517eefe7a7296bc5b83e693b +Subproject commit 866389b3988d9c4926a786f6c78ac9d5265591ac diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index d7138434..2ec60f7e 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -20,7 +20,7 @@ public enum GroupDrawBehaviour public interface IModGroup { - public const int MaxMultiOptions = 63; + public const int MaxMultiOptions = 32; public Mod Mod { get; } public string Name { get; set; } diff --git a/Penumbra/UI/ModsTab/DescriptionEditPopup.cs b/Penumbra/UI/ModsTab/DescriptionEditPopup.cs new file mode 100644 index 00000000..c284afc3 --- /dev/null +++ b/Penumbra/UI/ModsTab/DescriptionEditPopup.cs @@ -0,0 +1,114 @@ +using Dalamud.Interface.Utility; +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Mods; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Manager; +using Penumbra.Mods.SubMods; + +namespace Penumbra.UI.ModsTab; + +public class DescriptionEditPopup(ModManager modManager) : IUiService +{ + private static ReadOnlySpan PopupId + => "PenumbraEditDescription"u8; + + private bool _hasBeenEdited; + private string _description = string.Empty; + + private object? _current; + private bool _opened; + + public void Open(Mod mod) + { + _current = mod; + _opened = true; + _hasBeenEdited = false; + _description = mod.Description; + } + + public void Open(IModGroup group) + { + _current = group; + _opened = true; + _hasBeenEdited = false; + _description = group.Description; + } + + public void Open(IModOption option) + { + _current = option; + _opened = true; + _hasBeenEdited = false; + _description = option.Description; + } + + public void Draw() + { + if (_current == null) + return; + + if (_opened) + { + _opened = false; + ImUtf8.OpenPopup(PopupId); + } + + var inputSize = ImGuiHelpers.ScaledVector2(800); + using var popup = ImUtf8.Popup(PopupId); + if (!popup) + return; + + if (ImGui.IsWindowAppearing()) + ImGui.SetKeyboardFocusHere(); + + ImUtf8.InputMultiLineOnDeactivated("##editDescription"u8, ref _description, inputSize); + _hasBeenEdited |= ImGui.IsItemEdited(); + UiHelpers.DefaultLineSpace(); + + var buttonSize = new Vector2(ImUtf8.GlobalScale * 100, 0); + + var width = 2 * buttonSize.X + + 4 * ImUtf8.FramePadding.X + + ImUtf8.ItemSpacing.X; + + ImGui.SetCursorPosX((inputSize.X - width) / 2); + DrawSaveButton(buttonSize); + ImGui.SameLine(); + DrawCancelButton(buttonSize); + } + + private void DrawSaveButton(Vector2 buttonSize) + { + if (!ImUtf8.ButtonEx("Save"u8, _hasBeenEdited ? [] : "No changes made yet."u8, buttonSize, !_hasBeenEdited)) + return; + + switch (_current) + { + case Mod mod: + modManager.DataEditor.ChangeModDescription(mod, _description); + break; + case IModGroup group: + modManager.OptionEditor.ChangeGroupDescription(group, _description); + break; + case IModOption option: + modManager.OptionEditor.ChangeOptionDescription(option, _description); + break; + } + + _description = string.Empty; + _hasBeenEdited = false; + ImGui.CloseCurrentPopup(); + } + + private void DrawCancelButton(Vector2 buttonSize) + { + if (!ImUtf8.Button("Cancel"u8, buttonSize) && !ImGui.IsKeyPressed(ImGuiKey.Escape)) + return; + + _description = string.Empty; + _hasBeenEdited = false; + ImGui.CloseCurrentPopup(); + } +} diff --git a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs index 6b62d5b8..5652fa98 100644 --- a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs @@ -1,11 +1,12 @@ using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; +using OtterGui.Text.EndObjects; using Penumbra.Mods; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; @@ -17,87 +18,79 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab; -public sealed class ModGroupEditDrawer(ModManager modManager, Configuration config, FilenameService filenames) : IUiService +public sealed class ModGroupEditDrawer( + ModManager modManager, + Configuration config, + FilenameService filenames, + DescriptionEditPopup descriptionPopup) : IUiService { + private static ReadOnlySpan DragDropLabel + => "##DragOption"u8; + private Vector2 _buttonSize; + private Vector2 _availableWidth; private float _priorityWidth; private float _groupNameWidth; + private float _optionNameWidth; private float _spacing; + private Vector2 _optionIdxSelectable; + private bool _deleteEnabled; private string? _currentGroupName; private ModPriority? _currentGroupPriority; private IModGroup? _currentGroupEdited; - private bool _isGroupNameValid; - private IModGroup? _deleteGroup; - private IModGroup? _moveGroup; - private int _moveTo; + private bool _isGroupNameValid = true; - private string? _currentOptionName; - private ModPriority? _currentOptionPriority; - private IModOption? _currentOptionEdited; - private IModOption? _deleteOption; + private string? _newOptionName; + private IModGroup? _newOptionGroup; + private readonly Queue _actionQueue = new(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SameLine() - => ImGui.SameLine(0, _spacing); + private IModGroup? _dragDropGroup; + private IModOption? _dragDropOption; public void Draw(Mod mod) { - _buttonSize = new Vector2(ImGui.GetFrameHeight()); - _priorityWidth = 50 * ImGuiHelpers.GlobalScale; - _groupNameWidth = 350f * ImGuiHelpers.GlobalScale; - _spacing = ImGui.GetStyle().ItemInnerSpacing.X; + PrepareStyle(); + using var id = ImUtf8.PushId("##GroupEdit"u8); + foreach (var (group, groupIdx) in mod.Groups.WithIndex()) + DrawGroup(group, groupIdx); - FinishGroupCleanup(); - } - - private void FinishGroupCleanup() - { - if (_deleteGroup != null) - { - modManager.OptionEditor.DeleteModGroup(_deleteGroup); - _deleteGroup = null; - } - - if (_deleteOption != null) - { - modManager.OptionEditor.DeleteOption(_deleteOption); - _deleteOption = null; - } - - if (_moveGroup != null) - { - modManager.OptionEditor.MoveModGroup(_moveGroup, _moveTo); - _moveGroup = null; - } + while (_actionQueue.TryDequeue(out var action)) + action.Invoke(); } private void DrawGroup(IModGroup group, int idx) { - using var id = ImRaii.PushId(idx); + using var id = ImUtf8.PushId(idx); using var frame = ImRaii.FramedGroup($"Group #{idx + 1}"); - DrawGroupNameRow(group); + DrawGroupNameRow(group, idx); switch (group) { case SingleModGroup s: - DrawSingleGroup(s, idx); + DrawSingleGroup(s); break; case MultiModGroup m: - DrawMultiGroup(m, idx); + DrawMultiGroup(m); break; case ImcModGroup i: - DrawImcGroup(i, idx); + DrawImcGroup(i); break; } } - private void DrawGroupNameRow(IModGroup group) + private void DrawGroupNameRow(IModGroup group, int idx) { DrawGroupName(group); - SameLine(); + ImUtf8.SameLineInner(); + DrawGroupMoveButtons(group, idx); + ImUtf8.SameLineInner(); + DrawGroupOpenFile(group, idx); + ImUtf8.SameLineInner(); + DrawGroupDescription(group); + ImUtf8.SameLineInner(); DrawGroupDelete(group); - SameLine(); + ImUtf8.SameLineInner(); DrawGroupPriority(group); } @@ -106,11 +99,11 @@ public sealed class ModGroupEditDrawer(ModManager modManager, Configuration conf var text = _currentGroupEdited == group ? _currentGroupName ?? group.Name : group.Name; ImGui.SetNextItemWidth(_groupNameWidth); using var border = ImRaii.PushFrameBorder(UiHelpers.ScaleX2, Colors.RegexWarningBorder, !_isGroupNameValid); - if (ImGui.InputText("##GroupName", ref text, 256)) + if (ImUtf8.InputText("##GroupName"u8, ref text)) { _currentGroupEdited = group; _currentGroupName = text; - _isGroupNameValid = ModGroupEditor.VerifyFileName(group.Mod, group, text, false); + _isGroupNameValid = text == group.Name || ModGroupEditor.VerifyFileName(group.Mod, group, text, false); } if (ImGui.IsItemDeactivated()) @@ -123,20 +116,21 @@ public sealed class ModGroupEditDrawer(ModManager modManager, Configuration conf } var tt = _isGroupNameValid - ? "Group Name" - : "Current name can not be used for this group."; - ImGuiUtil.HoverTooltip(tt); + ? "Change the Group name."u8 + : "Current name can not be used for this group."u8; + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, tt); } private void DrawGroupDelete(IModGroup group) { - var enabled = config.DeleteModModifier.IsActive(); - var tt = enabled - ? "Delete this option group." - : $"Delete this option group.\nHold {config.DeleteModModifier} while clicking to delete."; + if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) + _actionQueue.Enqueue(() => modManager.OptionEditor.DeleteModGroup(group)); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), _buttonSize, tt, !enabled, true)) - _deleteGroup = group; + if (_deleteEnabled) + ImUtf8.HoverTooltip("Delete this option group."u8); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + $"Delete this option group.\nHold {config.DeleteModModifier} while clicking to delete."); } private void DrawGroupPriority(IModGroup group) @@ -162,36 +156,41 @@ public sealed class ModGroupEditDrawer(ModManager modManager, Configuration conf ImGuiUtil.HoverTooltip("Group Priority"); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawGroupDescription(IModGroup group) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit group description."u8)) + descriptionPopup.Open(group); + } + private void DrawGroupMoveButtons(IModGroup group, int idx) { var isFirst = idx == 0; - var tt = isFirst ? "Can not move this group further upwards." : $"Move this group up to group {idx}."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize, tt, isFirst, true)) - { - _moveGroup = group; - _moveTo = idx - 1; - } + if (ImUtf8.IconButton(FontAwesomeIcon.ArrowUp, isFirst)) + _actionQueue.Enqueue(() => modManager.OptionEditor.MoveModGroup(group, idx - 1)); - SameLine(); + if (isFirst) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Can not move this group further upwards."u8); + else + ImUtf8.HoverTooltip($"Move this group up to group {idx}."); + + + ImUtf8.SameLineInner(); var isLast = idx == group.Mod.Groups.Count - 1; - tt = isLast - ? "Can not move this group further downwards." - : $"Move this group down to group {idx + 2}."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize, tt, isLast, true)) - { - _moveGroup = group; - _moveTo = idx + 1; - } + if (ImUtf8.IconButton(FontAwesomeIcon.ArrowDown, isLast)) + _actionQueue.Enqueue(() => modManager.OptionEditor.MoveModGroup(group, idx + 1)); + + if (isLast) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, "Can not move this group further downwards."u8); + else + ImUtf8.HoverTooltip($"Move this group down to group {idx + 2}."); } private void DrawGroupOpenFile(IModGroup group, int idx) { var fileName = filenames.OptionGroupFile(group.Mod, idx, config.ReplaceNonAsciiOnImport); var fileExists = File.Exists(fileName); - var tt = fileExists - ? $"Open the {group.Name} json file in the text editor of your choice." - : $"The {group.Name} json file does not exist."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileExport.ToIconString(), UiHelpers.IconButtonSize, tt, !fileExists, true)) + if (ImUtf8.IconButton(FontAwesomeIcon.FileExport, !fileExists)) try { Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); @@ -200,15 +199,274 @@ public sealed class ModGroupEditDrawer(ModManager modManager, Configuration conf { Penumbra.Messager.NotificationMessage(e, "Could not open editor.", NotificationType.Error); } + + if (fileExists) + ImUtf8.HoverTooltip($"Open the {group.Name} json file in the text editor of your choice."); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"The {group.Name} json file does not exist."); } + private void DrawSingleGroup(SingleModGroup group) + { + foreach (var (option, optionIdx) in group.OptionData.WithIndex()) + { + using var id = ImRaii.PushId(optionIdx); + DrawOptionPosition(group, option, optionIdx); - private void DrawSingleGroup(SingleModGroup group, int idx) - { } + ImUtf8.SameLineInner(); + DrawOptionDefaultSingleBehaviour(group, option, optionIdx); - private void DrawMultiGroup(MultiModGroup group, int idx) - { } + ImUtf8.SameLineInner(); + DrawOptionName(option); - private void DrawImcGroup(ImcModGroup group, int idx) - { } + ImUtf8.SameLineInner(); + DrawOptionDescription(option); + + ImUtf8.SameLineInner(); + DrawOptionDelete(option); + + ImUtf8.SameLineInner(); + ImGui.Dummy(new Vector2(_priorityWidth, 0)); + } + + DrawNewOption(group); + var convertible = group.Options.Count <= IModGroup.MaxMultiOptions; + if (ImUtf8.ButtonEx("Convert to Multi Group", _availableWidth, !convertible)) + _actionQueue.Enqueue(() => modManager.OptionEditor.SingleEditor.ChangeToMulti(group)); + if (!convertible) + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + "Can not convert to multi group since maximum number of options is exceeded."u8); + } + + private void DrawMultiGroup(MultiModGroup group) + { + foreach (var (option, optionIdx) in group.OptionData.WithIndex()) + { + using var id = ImRaii.PushId(optionIdx); + DrawOptionPosition(group, option, optionIdx); + + ImUtf8.SameLineInner(); + DrawOptionDefaultMultiBehaviour(group, option, optionIdx); + + ImUtf8.SameLineInner(); + DrawOptionName(option); + + ImUtf8.SameLineInner(); + DrawOptionDescription(option); + + ImUtf8.SameLineInner(); + DrawOptionDelete(option); + + ImUtf8.SameLineInner(); + DrawOptionPriority(option); + } + + DrawNewOption(group); + if (ImUtf8.Button("Convert to Single Group"u8, _availableWidth)) + _actionQueue.Enqueue(() => modManager.OptionEditor.MultiEditor.ChangeToSingle(group)); + } + + private void DrawImcGroup(ImcModGroup group) + { + // TODO + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx) + { + ImGui.AlignTextToFramePadding(); + ImUtf8.Selectable($"Option #{optionIdx + 1}", false, size: _optionIdxSelectable); + Target(group, optionIdx); + Source(option); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionDefaultSingleBehaviour(IModGroup group, IModOption option, int optionIdx) + { + var isDefaultOption = group.DefaultSettings.AsIndex == optionIdx; + if (ImUtf8.RadioButton("##default"u8, isDefaultOption)) + modManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx)); + ImUtf8.HoverTooltip($"Set {option.Name} as the default choice for this group."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionDefaultMultiBehaviour(IModGroup group, IModOption option, int optionIdx) + { + var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); + if (ImUtf8.Checkbox("##default"u8, ref isDefaultOption)) + modManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); + ImUtf8.HoverTooltip($"{(isDefaultOption ? "Disable"u8 : "Enable"u8)} {option.Name} per default in this group."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionDescription(IModOption option) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit option description."u8)) + descriptionPopup.Open(option); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionPriority(MultiSubMod option) + { + var priority = option.Priority.Value; + ImGui.SetNextItemWidth(_priorityWidth); + if (ImUtf8.InputScalarOnDeactivated("##Priority"u8, ref priority)) + modManager.OptionEditor.MultiEditor.ChangeOptionPriority(option, new ModPriority(priority)); + ImUtf8.HoverTooltip("Option priority inside the mod."u8); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionName(IModOption option) + { + var name = option.Name; + ImGui.SetNextItemWidth(_optionNameWidth); + if (ImUtf8.InputTextOnDeactivated("##Name"u8, ref name)) + modManager.OptionEditor.RenameOption(option, name); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawOptionDelete(IModOption option) + { + if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) + _actionQueue.Enqueue(() => modManager.OptionEditor.DeleteOption(option)); + + if (_deleteEnabled) + ImUtf8.HoverTooltip("Delete this option."u8); + else + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + $"Delete this option.\nHold {config.DeleteModModifier} while clicking to delete."); + } + + private void DrawNewOption(SingleModGroup group) + { + var count = group.Options.Count; + if (count >= int.MaxValue) + return; + + DrawNewOptionBase(group, count); + + var validName = _newOptionName?.Length > 0; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, !validName)) + { + modManager.OptionEditor.SingleEditor.AddOption(group, _newOptionName!); + _newOptionName = null; + } + } + + private void DrawNewOption(MultiModGroup group) + { + var count = group.Options.Count; + if (count >= IModGroup.MaxMultiOptions) + return; + + DrawNewOptionBase(group, count); + + var validName = _newOptionName?.Length > 0; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, !validName)) + { + modManager.OptionEditor.MultiEditor.AddOption(group, _newOptionName!); + _newOptionName = null; + } + } + + private void DrawNewOption(ImcModGroup group) + { + // TODO + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawNewOptionBase(IModGroup group, int count) + { + ImUtf8.Selectable($"Option #{count + 1}", false, size: _optionIdxSelectable); + Target(group, count); + + ImUtf8.SameLineInner(); + ImUtf8.IconDummy(); + + ImUtf8.SameLineInner(); + ImGui.SetNextItemWidth(_optionNameWidth); + var newName = _newOptionGroup == group + ? _newOptionName ?? string.Empty + : string.Empty; + if (ImUtf8.InputText("##newOption"u8, ref newName, "Add new option..."u8)) + { + _newOptionName = newName; + _newOptionGroup = group; + } + + ImUtf8.SameLineInner(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Source(IModOption option) + { + if (option.Group is not ITexToolsGroup) + return; + + using var source = ImUtf8.DragDropSource(); + if (!source) + return; + + if (!DragDropSource.SetPayload(DragDropLabel)) + { + _dragDropGroup = option.Group; + _dragDropOption = option; + } + + ImGui.TextUnformatted($"Dragging option {option.Name} from group {option.Group.Name}..."); + } + + private void Target(IModGroup group, int optionIdx) + { + if (group is not ITexToolsGroup) + return; + + if (_dragDropGroup != group && _dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }) + return; + + using var target = ImRaii.DragDropTarget(); + if (!target.Success || !DragDropTarget.CheckPayload(DragDropLabel)) + return; + + if (_dragDropGroup != null && _dragDropOption != null) + { + if (_dragDropGroup == group) + { + var sourceOption = _dragDropOption; + _actionQueue.Enqueue(() => modManager.OptionEditor.MoveOption(sourceOption, optionIdx)); + } + else + { + // Move from one group to another by deleting, then adding, then moving the option. + var sourceOption = _dragDropOption; + _actionQueue.Enqueue(() => + { + modManager.OptionEditor.DeleteOption(sourceOption); + if (modManager.OptionEditor.AddOption(group, sourceOption) is { } newOption) + modManager.OptionEditor.MoveOption(newOption, optionIdx); + }); + } + } + + _dragDropGroup = null; + _dragDropOption = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void PrepareStyle() + { + var totalWidth = 400f * ImUtf8.GlobalScale; + _buttonSize = new Vector2(ImUtf8.FrameHeight); + _priorityWidth = 50 * ImUtf8.GlobalScale; + _availableWidth = new Vector2(totalWidth + 3 * _spacing + 2 * _buttonSize.X + _priorityWidth, 0); + _groupNameWidth = totalWidth - 3 * (_buttonSize.X + _spacing); + _spacing = ImGui.GetStyle().ItemInnerSpacing.X; + _optionIdxSelectable = ImUtf8.CalcTextSize("Option #88."u8); + _optionNameWidth = totalWidth - _optionIdxSelectable.X - _buttonSize.X - 2 * _spacing; + _deleteEnabled = config.DeleteModModifier.IsActive(); + } } diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 862852fa..a5db15b6 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -1,21 +1,17 @@ using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Utility; using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using OtterGui.Classes; -using Penumbra.Api.Enums; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.AdvancedWindow; -using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; -using Penumbra.Mods.SubMods; using Penumbra.Mods.Manager.OptionEditor; namespace Penumbra.UI.ModsTab; @@ -30,15 +26,13 @@ public class ModPanelEditTab( FilenameService filenames, ModExportManager modExportManager, Configuration config, - PredefinedTagManager predefinedTagManager) + PredefinedTagManager predefinedTagManager, + ModGroupEditDrawer groupEditDrawer, + DescriptionEditPopup descriptionPopup) : ITab { - private readonly ModManager _modManager = modManager; - private readonly TagButtons _modTags = new(); - private Vector2 _cellPadding = Vector2.Zero; - private Vector2 _itemSpacing = Vector2.Zero; private ModFileSystem.Leaf _leaf = null!; private Mod _mod = null!; @@ -54,9 +48,6 @@ public class ModPanelEditTab( _leaf = selector.SelectedLeaf!; _mod = selector.Selected!; - _cellPadding = ImGui.GetStyle().CellPadding with { X = 2 * UiHelpers.Scale }; - _itemSpacing = ImGui.GetStyle().CellPadding with { X = 4 * UiHelpers.Scale }; - EditButtons(); EditRegularMeta(); UiHelpers.DefaultLineSpace(); @@ -77,21 +68,18 @@ public class ModPanelEditTab( 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); if (tagIdx >= 0) - _modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag); + modManager.DataEditor.ChangeModTag(_mod, tagIdx, editedTag); if (sharedTagsEnabled) 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) - EditGroup(groupIdx); - - EndActions(); - DescriptionEdit.DrawPopup(_modManager); + groupEditDrawer.Draw(_mod); + descriptionPopup.Draw(); } public void Reset() @@ -99,7 +87,6 @@ public class ModPanelEditTab( AddOptionGroup.Reset(); MoveDirectory.Reset(); Input.Reset(); - OptionTable.Reset(); } /// The general edit row for non-detailed mod edits. @@ -117,10 +104,10 @@ public class ModPanelEditTab( if (ImGuiUtil.DrawDisabledButton("Reload Mod", buttonSize, "Reload the current mod from its files.\n" + "If the mod directory or meta file do not exist anymore or if the new mod name is empty, the mod is deleted instead.", false)) - _modManager.ReloadMod(_mod); + modManager.ReloadMod(_mod); BackupButtons(buttonSize); - MoveDirectory.Draw(_modManager, _mod, buttonSize); + MoveDirectory.Draw(modManager, _mod, buttonSize); UiHelpers.DefaultLineSpace(); DrawUpdateBibo(buttonSize); @@ -169,7 +156,7 @@ public class ModPanelEditTab( : $"Exported mod \"{backup.Name}\" does not exist."; ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Restore From Export", buttonSize, tt, !backup.Exists || !config.DeleteModModifier.IsActive())) - backup.Restore(_modManager); + backup.Restore(modManager); if (backup.Exists) { ImGui.SameLine(); @@ -186,24 +173,24 @@ public class ModPanelEditTab( private void EditRegularMeta() { if (Input.Text("Name", Input.Name, Input.None, _mod.Name, out var newName, 256, UiHelpers.InputTextWidth.X)) - _modManager.DataEditor.ChangeModName(_mod, newName); + modManager.DataEditor.ChangeModName(_mod, newName); if (Input.Text("Author", Input.Author, Input.None, _mod.Author, out var newAuthor, 256, UiHelpers.InputTextWidth.X)) - _modManager.DataEditor.ChangeModAuthor(_mod, newAuthor); + modManager.DataEditor.ChangeModAuthor(_mod, newAuthor); if (Input.Text("Version", Input.Version, Input.None, _mod.Version, out var newVersion, 32, UiHelpers.InputTextWidth.X)) - _modManager.DataEditor.ChangeModVersion(_mod, newVersion); + modManager.DataEditor.ChangeModVersion(_mod, newVersion); if (Input.Text("Website", Input.Website, Input.None, _mod.Website, out var newWebsite, 256, UiHelpers.InputTextWidth.X)) - _modManager.DataEditor.ChangeModWebsite(_mod, newWebsite); + modManager.DataEditor.ChangeModWebsite(_mod, newWebsite); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3)); var reducedSize = new Vector2(UiHelpers.InputTextMinusButton3, 0); if (ImGui.Button("Edit Description", reducedSize)) - _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, Input.Description)); + descriptionPopup.Open(_mod); ImGui.SameLine(); var fileExists = File.Exists(filenames.ModMetaPath(_mod)); @@ -215,16 +202,6 @@ public class ModPanelEditTab( Process.Start(new ProcessStartInfo(filenames.ModMetaPath(_mod)) { UseShellExecute = true }); } - /// Do some edits outside of iterations. - private readonly Queue _delayedActions = new(); - - /// Delete a marked group or option outside of iteration. - private void EndActions() - { - while (_delayedActions.TryDequeue(out var action)) - action.Invoke(); - } - /// Text input to add a new option group at the end of the current groups. private static class AddOptionGroup { @@ -309,372 +286,6 @@ public class ModPanelEditTab( } } - /// Open a popup to edit a multi-line mod or option description. - private static class DescriptionEdit - { - private const string PopupName = "Edit Description"; - private static string _newDescription = string.Empty; - private static string _oldDescription = string.Empty; - private static int _newDescriptionIdx = -1; - private static int _newDescriptionOptionIdx = -1; - private static Mod? _mod; - - public static void OpenPopup(Mod mod, int groupIdx, int optionIdx = -1) - { - _newDescriptionIdx = groupIdx; - _newDescriptionOptionIdx = optionIdx; - _newDescription = groupIdx < 0 - ? mod.Description - : optionIdx < 0 - ? mod.Groups[groupIdx].Description - : mod.Groups[groupIdx].Options[optionIdx].Description; - _oldDescription = _newDescription; - - _mod = mod; - ImGui.OpenPopup(PopupName); - } - - public static void DrawPopup(ModManager modManager) - { - if (_mod == null) - return; - - using var popup = ImRaii.Popup(PopupName); - if (!popup) - return; - - if (ImGui.IsWindowAppearing()) - ImGui.SetKeyboardFocusHere(); - - ImGui.InputTextMultiline("##editDescription", ref _newDescription, 4096, ImGuiHelpers.ScaledVector2(800, 800)); - UiHelpers.DefaultLineSpace(); - - var buttonSize = ImGuiHelpers.ScaledVector2(100, 0); - var width = 2 * buttonSize.X - + 4 * ImGui.GetStyle().FramePadding.X - + ImGui.GetStyle().ItemSpacing.X; - ImGui.SetCursorPosX((800 * UiHelpers.Scale - width) / 2); - - var tooltip = _newDescription != _oldDescription ? string.Empty : "No changes made yet."; - - if (ImGuiUtil.DrawDisabledButton("Save", buttonSize, tooltip, tooltip.Length > 0)) - { - switch (_newDescriptionIdx) - { - case Input.Description: - modManager.DataEditor.ChangeModDescription(_mod, _newDescription); - break; - case >= 0: - if (_newDescriptionOptionIdx < 0) - modManager.OptionEditor.ChangeGroupDescription(_mod.Groups[_newDescriptionIdx], _newDescription); - else - modManager.OptionEditor.ChangeOptionDescription(_mod.Groups[_newDescriptionIdx].Options[_newDescriptionOptionIdx], - _newDescription); - - break; - } - - ImGui.CloseCurrentPopup(); - } - - ImGui.SameLine(); - if (!ImGui.Button("Cancel", buttonSize) - && !ImGui.IsKeyPressed(ImGuiKey.Escape)) - return; - - _newDescriptionIdx = Input.None; - _newDescription = string.Empty; - ImGui.CloseCurrentPopup(); - } - } - - private void EditGroup(int groupIdx) - { - var group = _mod.Groups[groupIdx]; - using var id = ImRaii.PushId(groupIdx); - using var frame = ImRaii.FramedGroup($"Group #{groupIdx + 1}"); - - using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, _cellPadding) - .Push(ImGuiStyleVar.ItemSpacing, _itemSpacing); - - if (Input.Text("##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, UiHelpers.InputTextWidth.X)) - _modManager.OptionEditor.RenameModGroup(group, newGroupName); - - ImGuiUtil.HoverTooltip("Group Name"); - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, - "Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.DeleteModGroup(group)); - - ImGui.SameLine(); - - if (Input.Priority("##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * UiHelpers.Scale)) - _modManager.OptionEditor.ChangeGroupPriority(group, priority); - - ImGuiUtil.HoverTooltip("Group Priority"); - - DrawGroupCombo(group, groupIdx); - ImGui.SameLine(); - - var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize, - tt, groupIdx == 0, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(group, groupIdx - 1)); - - ImGui.SameLine(); - tt = groupIdx == _mod.Groups.Count - 1 - ? "Can not move this group further downwards." - : $"Move this group down to group {groupIdx + 2}."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize, - tt, groupIdx == _mod.Groups.Count - 1, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(group, groupIdx + 1)); - - ImGui.SameLine(); - - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, - "Edit group description.", false, true)) - _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, groupIdx)); - - ImGui.SameLine(); - 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." - : $"The {group.Name} json file does not exist."; - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileExport.ToIconString(), UiHelpers.IconButtonSize, tt, !fileExists, true)) - Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); - - UiHelpers.DefaultLineSpace(); - - OptionTable.Draw(this, groupIdx); - } - - /// Draw the table displaying all options and the add new option line. - private static class OptionTable - { - private const string DragDropLabel = "##DragOption"; - - private static int _newOptionNameIdx = -1; - private static string _newOptionName = string.Empty; - private static IModGroup? _dragDropGroup; - private static IModOption? _dragDropOption; - - public static void Reset() - { - _newOptionNameIdx = -1; - _newOptionName = string.Empty; - _dragDropGroup = null; - _dragDropOption = null; - } - - public static void Draw(ModPanelEditTab panel, int groupIdx) - { - using var table = ImRaii.Table(string.Empty, 6, ImGuiTableFlags.SizingFixedFit); - if (!table) - return; - - var maxWidth = ImGui.CalcTextSize("Option #88.").X; - ImGui.TableSetupColumn("idx", ImGuiTableColumnFlags.WidthFixed, maxWidth); - ImGui.TableSetupColumn("default", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight()); - ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthFixed, - UiHelpers.InputTextWidth.X - maxWidth - 12 * UiHelpers.Scale - ImGui.GetFrameHeight() - UiHelpers.IconButtonSize.X); - ImGui.TableSetupColumn("description", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); - ImGui.TableSetupColumn("delete", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); - ImGui.TableSetupColumn("priority", ImGuiTableColumnFlags.WidthFixed, 50 * UiHelpers.Scale); - - switch (panel._mod.Groups[groupIdx]) - { - case SingleModGroup single: - for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx) - EditOption(panel, single, groupIdx, optionIdx); - break; - case MultiModGroup multi: - for (var optionIdx = 0; optionIdx < multi.OptionData.Count; ++optionIdx) - EditOption(panel, multi, groupIdx, optionIdx); - break; - } - - DrawNewOption(panel, groupIdx, UiHelpers.IconButtonSize); - } - - /// Draw a line for a single option. - private static void EditOption(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) - { - var option = group.Options[optionIdx]; - using var id = ImRaii.PushId(optionIdx); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Selectable($"Option #{optionIdx + 1}"); - Source(option); - Target(panel, group, optionIdx); - - ImGui.TableNextColumn(); - - - if (group.Type == GroupType.Single) - { - if (ImGui.RadioButton("##default", group.DefaultSettings.AsIndex == optionIdx)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx)); - - ImGuiUtil.HoverTooltip($"Set {option.Name} as the default choice for this group."); - } - else - { - var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); - if (ImGui.Checkbox("##default", ref isDefaultOption)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); - - ImGuiUtil.HoverTooltip($"{(isDefaultOption ? "Disable" : "Enable")} {option.Name} per default in this group."); - } - - ImGui.TableNextColumn(); - if (Input.Text("##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1)) - panel._modManager.OptionEditor.RenameOption(option, newOptionName); - - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit option description.", - false, true)) - panel._delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(panel._mod, groupIdx, optionIdx)); - - ImGui.TableNextColumn(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, - "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) - panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(option)); - - ImGui.TableNextColumn(); - if (option is not MultiSubMod multi) - return; - - if (Input.Priority("##Priority", groupIdx, optionIdx, multi.Priority, out var priority, - 50 * UiHelpers.Scale)) - panel._modManager.OptionEditor.MultiEditor.ChangeOptionPriority(multi, priority); - - ImGuiUtil.HoverTooltip("Option priority."); - } - - /// Draw the line to add a new option. - private static void DrawNewOption(ModPanelEditTab panel, int groupIdx, Vector2 iconButtonSize) - { - var mod = panel._mod; - var group = mod.Groups[groupIdx]; - var count = group switch - { - SingleModGroup single => single.OptionData.Count, - MultiModGroup multi => multi.OptionData.Count, - _ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."), - }; - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Selectable($"Option #{count + 1}"); - Target(panel, group, count); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(-1); - var tmp = _newOptionNameIdx == groupIdx ? _newOptionName : string.Empty; - if (ImGui.InputTextWithHint("##newOption", "Add new option...", ref tmp, 256)) - { - _newOptionName = tmp; - _newOptionNameIdx = groupIdx; - } - - ImGui.TableNextColumn(); - var canAddGroup = mod.Groups[groupIdx].Type != GroupType.Multi || count < IModGroup.MaxMultiOptions; - var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; - var tt = canAddGroup - ? validName ? "Add a new option to this group." : "Please enter a name for the new option." - : $"Can not add more than {IModGroup.MaxMultiOptions} options to a multi group."; - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconButtonSize, - tt, !(canAddGroup && validName), true)) - return; - - panel._modManager.OptionEditor.AddOption(group, _newOptionName); - _newOptionName = string.Empty; - } - - // Handle drag and drop to move options inside a group or into another group. - private static void Source(IModOption option) - { - if (option.Group is not ITexToolsGroup) - return; - - using var source = ImRaii.DragDropSource(); - if (!source) - return; - - if (ImGui.SetDragDropPayload(DragDropLabel, IntPtr.Zero, 0)) - { - _dragDropGroup = option.Group; - _dragDropOption = option; - } - - ImGui.TextUnformatted($"Dragging option {option.Name} from group {option.Group.Name}..."); - } - - private static void Target(ModPanelEditTab panel, IModGroup group, int optionIdx) - { - if (group is not ITexToolsGroup) - return; - - using var target = ImRaii.DragDropTarget(); - if (!target.Success || !ImGuiUtil.IsDropping(DragDropLabel)) - return; - - if (_dragDropGroup != null && _dragDropOption != null) - { - if (_dragDropGroup == group) - { - var sourceOption = _dragDropOption; - panel._delayedActions.Enqueue( - () => panel._modManager.OptionEditor.MoveOption(sourceOption, optionIdx)); - } - else - { - // Move from one group to another by deleting, then adding, then moving the option. - var sourceOption = _dragDropOption; - panel._delayedActions.Enqueue(() => - { - panel._modManager.OptionEditor.DeleteOption(sourceOption); - if (panel._modManager.OptionEditor.AddOption(group, sourceOption) is { } newOption) - panel._modManager.OptionEditor.MoveOption(newOption, optionIdx); - }); - } - } - - _dragDropGroup = null; - _dragDropOption = null; - } - } - - /// Draw a combo to select single or multi group and switch between them. - private void DrawGroupCombo(IModGroup group, int groupIdx) - { - ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - 2 * UiHelpers.IconButtonSize.X - 2 * ImGui.GetStyle().ItemSpacing.X); - using var combo = ImRaii.Combo("##GroupType", GroupTypeName(group.Type)); - if (!combo) - return; - - if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single) && group is MultiModGroup m) - _modManager.OptionEditor.MultiEditor.ChangeToSingle(m); - - var canSwitchToMulti = group.Options.Count <= IModGroup.MaxMultiOptions; - using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti); - if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti && group is SingleModGroup s) - _modManager.OptionEditor.SingleEditor.ChangeToMulti(s); - - style.Pop(); - if (!canSwitchToMulti) - ImGuiUtil.HoverTooltip($"Can not convert group to multi group since it has more than {IModGroup.MaxMultiOptions} options."); - return; - - static string GroupTypeName(GroupType type) - => type switch - { - GroupType.Single => "Single Group", - GroupType.Multi => "Multi Group", - _ => "Unknown", - }; - } - /// Handles input text and integers in separate fields without buffers for every single one. private static class Input { @@ -705,6 +316,7 @@ public class ModPanelEditTab( { var tmp = field == _currentField && option == _optionIndex ? _currentEdit ?? oldValue : oldValue; ImGui.SetNextItemWidth(width); + if (ImGui.InputText(label, ref tmp, maxLength)) { _currentEdit = tmp;