diff --git a/Penumbra.GameData b/Penumbra.GameData index 1b0b5469..595ac572 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 1b0b5469f792999e5b412d4f0c3ed77d9994d7b7 +Subproject commit 595ac5722c9c400bea36110503ed2ae7b02d1489 diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index a268ba0f..d7138434 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -12,16 +12,23 @@ public interface ITexToolsGroup public IReadOnlyList OptionData { get; } } +public enum GroupDrawBehaviour +{ + SingleSelection, + MultiSelection, +} + public interface IModGroup { public const int MaxMultiOptions = 63; - public Mod Mod { get; } - public string Name { get; set; } - public string Description { get; set; } - public GroupType Type { get; } - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; } + public string Name { get; set; } + public string Description { get; set; } + public GroupType Type { get; } + public GroupDrawBehaviour Behaviour { get; } + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public FullPath? FindBestMatch(Utf8GamePath gamePath); public IModOption? AddOption(string name, string description = ""); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index e233f82e..e58d855a 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -21,6 +21,9 @@ public class ImcModGroup(Mod mod) : IModGroup public GroupType Type => GroupType.Imc; + public GroupDrawBehaviour Behaviour + => GroupDrawBehaviour.MultiSelection; + public ModPriority Priority { get; set; } = ModPriority.Default; public Setting DefaultSettings { get; set; } = Setting.Zero; @@ -150,7 +153,6 @@ public class ImcModGroup(Mod mod) : IModGroup } jWriter.WriteEndArray(); - jWriter.WriteEndObject(); } public (int Redirections, int Swaps, int Manips) GetCounts() diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index efdcde09..ed3c8857 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -56,10 +56,10 @@ public readonly struct ModSaveGroup : ISavable { _basePath = (container.Mod as Mod)?.ModPath ?? throw new Exception("Invalid save group from default data container without base path."); // Should not happen. - _defaultMod = null; + _defaultMod = container as DefaultSubMod; _onlyAscii = onlyAscii; - _group = container.Group!; - _groupIdx = _group.GetIndex(); + _group = container.Group; + _groupIdx = _group?.GetIndex() ?? -1; } public string ToFilename(FilenameService fileNames) @@ -80,7 +80,6 @@ public readonly struct ModSaveGroup : ISavable public static void WriteJsonBase(JsonTextWriter jWriter, IModGroup group) { - jWriter.WriteStartObject(); jWriter.WritePropertyName(nameof(group.Name)); jWriter.WriteValue(group.Name); jWriter.WritePropertyName(nameof(group.Description)); diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index a0034be0..7495c4b4 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -17,6 +17,9 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup public GroupType Type => GroupType.Multi; + public GroupDrawBehaviour Behaviour + => GroupDrawBehaviour.MultiSelection; + public Mod Mod { get; } = mod; public string Name { get; set; } = "Group"; public string Description { get; set; } = "A non-exclusive group of settings."; @@ -127,7 +130,6 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup } jWriter.WriteEndArray(); - jWriter.WriteEndObject(); } public (int Redirections, int Swaps, int Manips) GetCounts() diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index 0776c2af..459cec4a 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -15,7 +15,10 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public GroupType Type => GroupType.Single; - public Mod Mod { get; } = mod; + public GroupDrawBehaviour Behaviour + => GroupDrawBehaviour.SingleSelection; + + public Mod Mod { get; } = mod; public string Name { get; set; } = "Option"; public string Description { get; set; } = "A mutually exclusive group of settings."; public ModPriority Priority { get; set; } @@ -89,7 +92,12 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup => ModGroup.GetIndex(this); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) - => OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); + { + if (!IsOption) + return; + + OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); + } public Setting FixSetting(Setting setting) => OptionData.Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(OptionData.Count - 1))); @@ -111,7 +119,6 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup } jWriter.WriteEndArray(); - jWriter.WriteEndObject(); } /// Create a group without a mod only for saving it in the creator. diff --git a/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs index c48d2d40..74362325 100644 --- a/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/MultiModGroupEditor.cs @@ -16,8 +16,8 @@ public sealed class MultiModGroupEditor(CommunicatorService communicator, SaveSe var idx = group.GetIndex(); var singleGroup = group.ConvertToSingle(); group.Mod.Groups[idx] = singleGroup; - SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, group.Mod, group, null, null, -1); + SaveService.QueueSave(new ModSaveGroup(singleGroup, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, singleGroup.Mod, singleGroup, null, null, -1); } /// Change the internal priority of the given option. diff --git a/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs index 556416c6..15a899a0 100644 --- a/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/SingleModGroupEditor.cs @@ -16,8 +16,8 @@ public sealed class SingleModGroupEditor(CommunicatorService communicator, SaveS var idx = group.GetIndex(); var multiGroup = group.ConvertToMulti(); group.Mod.Groups[idx] = multiGroup; - SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); - Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, group.Mod, multiGroup, null, null, -1); + SaveService.QueueSave(new ModSaveGroup(multiGroup, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, multiGroup.Mod, multiGroup, null, null, -1); } protected override SingleModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) diff --git a/Penumbra/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs index a50e397f..b984b570 100644 --- a/Penumbra/Mods/SubMods/SubMod.cs +++ b/Penumbra/Mods/SubMods/SubMod.cs @@ -106,7 +106,6 @@ public static class SubMod j.WriteEndObject(); j.WritePropertyName(nameof(data.Manipulations)); serializer.Serialize(j, data.Manipulations); - j.WriteEndObject(); } /// Write the data for a selectable mod option on a JsonWriter. diff --git a/Penumbra/Services/BackupService.cs b/Penumbra/Services/BackupService.cs index a542dab5..bd5b3bcc 100644 --- a/Penumbra/Services/BackupService.cs +++ b/Penumbra/Services/BackupService.cs @@ -29,6 +29,7 @@ public class BackupService : IAsyncService list.Add(new FileInfo(fileNames.ConfigFile)); list.Add(new FileInfo(fileNames.FilesystemFile)); list.Add(new FileInfo(fileNames.ActiveCollectionsFile)); + list.Add(new FileInfo(fileNames.PredefinedTagFile)); return list; } diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index 5fa1a848..19ae31a2 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -162,7 +162,6 @@ public static class StaticServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/UI/ModsTab/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/ModGroupDrawer.cs new file mode 100644 index 00000000..e9b0b396 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModGroupDrawer.cs @@ -0,0 +1,233 @@ +using Dalamud.Interface.Components; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Services; +using OtterGui.Widgets; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Mods; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; + +namespace Penumbra.UI.ModsTab; + +public sealed class ModGroupDrawer(Configuration config, CollectionManager collectionManager) : IUiService +{ + private readonly List<(IModGroup, int)> _blockGroupCache = []; + + public void Draw(Mod mod, ModSettings settings) + { + if (mod.Groups.Count <= 0) + return; + + _blockGroupCache.Clear(); + var useDummy = true; + foreach (var (group, idx) in mod.Groups.WithIndex()) + { + if (!group.IsOption) + continue; + + switch (group.Behaviour) + { + case GroupDrawBehaviour.SingleSelection when group.Options.Count <= config.SingleGroupRadioMax: + case GroupDrawBehaviour.MultiSelection: + _blockGroupCache.Add((group, idx)); + break; + + case GroupDrawBehaviour.SingleSelection: + ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); + useDummy = false; + DrawSingleGroupCombo(group, idx, settings == ModSettings.Empty ? group.DefaultSettings : settings.Settings[idx]); + break; + } + } + + useDummy = true; + foreach (var (group, idx) in _blockGroupCache) + { + ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); + useDummy = false; + var option = settings == ModSettings.Empty ? group.DefaultSettings : settings.Settings[idx]; + if (group.Behaviour is GroupDrawBehaviour.MultiSelection) + DrawMultiGroup(group, idx, option); + else + DrawSingleGroupRadio(group, idx, option); + } + } + + /// + /// Draw a single group selector as a combo box. + /// If a description is provided, add a help marker besides it. + /// + private void DrawSingleGroupCombo(IModGroup group, int groupIdx, Setting setting) + { + using var id = ImRaii.PushId(groupIdx); + var selectedOption = setting.AsIndex; + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); + var options = group.Options; + using (var combo = ImRaii.Combo(string.Empty, options[selectedOption].Name)) + { + if (combo) + for (var idx2 = 0; idx2 < options.Count; ++idx2) + { + id.Push(idx2); + var option = options[idx2]; + if (ImGui.Selectable(option.Name, idx2 == selectedOption)) + SetModSetting(group, groupIdx, Setting.Single(idx2)); + + if (option.Description.Length > 0) + ImGuiUtil.SelectableHelpMarker(option.Description); + + id.Pop(); + } + } + + ImGui.SameLine(); + if (group.Description.Length > 0) + ImGuiUtil.LabeledHelpMarker(group.Name, group.Description); + else + ImGui.TextUnformatted(group.Name); + } + + /// + /// Draw a single group selector as a set of radio buttons. + /// If a description is provided, add a help marker besides it. + /// + private void DrawSingleGroupRadio(IModGroup group, int groupIdx, Setting setting) + { + using var id = ImRaii.PushId(groupIdx); + var selectedOption = setting.AsIndex; + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var options = group.Options; + DrawCollapseHandling(options, minWidth, DrawOptions); + + Widget.EndFramedGroup(); + return; + + void DrawOptions() + { + for (var idx = 0; idx < group.Options.Count; ++idx) + { + using var i = ImRaii.PushId(idx); + var option = options[idx]; + if (ImGui.RadioButton(option.Name, selectedOption == idx)) + SetModSetting(group, groupIdx, Setting.Single(idx)); + + if (option.Description.Length <= 0) + continue; + + ImGui.SameLine(); + ImGuiComponents.HelpMarker(option.Description); + } + } + } + + /// + /// Draw a multi group selector as a bordered set of checkboxes. + /// If a description is provided, add a help marker in the title. + /// + private void DrawMultiGroup(IModGroup group, int groupIdx, Setting setting) + { + using var id = ImRaii.PushId(groupIdx); + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var options = group.Options; + DrawCollapseHandling(options, minWidth, DrawOptions); + + Widget.EndFramedGroup(); + var label = $"##multi{groupIdx}"; + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImGui.OpenPopup($"##multi{groupIdx}"); + + DrawMultiPopup(group, groupIdx, label); + return; + + void DrawOptions() + { + for (var idx = 0; idx < options.Count; ++idx) + { + using var i = ImRaii.PushId(idx); + var option = options[idx]; + var enabled = setting.HasFlag(idx); + + if (ImGui.Checkbox(option.Name, ref enabled)) + SetModSetting(group, groupIdx, setting.SetBit(idx, enabled)); + + if (option.Description.Length > 0) + { + ImGui.SameLine(); + ImGuiComponents.HelpMarker(option.Description); + } + } + } + } + + private void DrawMultiPopup(IModGroup group, int groupIdx, string label) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.PopupBorderSize, 1); + using var popup = ImRaii.Popup(label); + if (!popup) + return; + + ImGui.TextUnformatted(group.Name); + ImGui.Separator(); + if (ImGui.Selectable("Enable All")) + SetModSetting(group, groupIdx, Setting.AllBits(group.Options.Count)); + + if (ImGui.Selectable("Disable All")) + SetModSetting(group, groupIdx, Setting.Zero); + } + + private void DrawCollapseHandling(IReadOnlyList options, float minWidth, Action draw) + { + if (options.Count <= config.OptionGroupCollapsibleMin) + { + draw(); + } + else + { + var collapseId = ImGui.GetID("Collapse"); + var shown = ImGui.GetStateStorage().GetBool(collapseId, true); + var buttonTextShow = $"Show {options.Count} Options"; + var buttonTextHide = $"Hide {options.Count} Options"; + var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) + + 2 * ImGui.GetStyle().FramePadding.X; + minWidth = Math.Max(buttonWidth, minWidth); + if (shown) + { + var pos = ImGui.GetCursorPos(); + ImGui.Dummy(UiHelpers.IconButtonSize); + using (var _ = ImRaii.Group()) + { + draw(); + } + + + var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); + var endPos = ImGui.GetCursorPos(); + ImGui.SetCursorPos(pos); + if (ImGui.Button(buttonTextHide, new Vector2(width, 0))) + ImGui.GetStateStorage().SetBool(collapseId, !shown); + + ImGui.SetCursorPos(endPos); + } + else + { + var optionWidth = options.Max(o => ImGui.CalcTextSize(o.Name).X) + + ImGui.GetStyle().ItemInnerSpacing.X + + ImGui.GetFrameHeight() + + ImGui.GetStyle().FramePadding.X; + var width = Math.Max(optionWidth, minWidth); + if (ImGui.Button(buttonTextShow, new Vector2(width, 0))) + ImGui.GetStateStorage().SetBool(collapseId, !shown); + } + } + } + + private ModCollection Current + => collectionManager.Active.Current; + + private void SetModSetting(IModGroup group, int groupIdx, Setting setting) + => collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting); +} diff --git a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs new file mode 100644 index 00000000..6b62d5b8 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs @@ -0,0 +1,214 @@ +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 Penumbra.Mods; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModsTab; + +public sealed class ModGroupEditDrawer(ModManager modManager, Configuration config, FilenameService filenames) : IUiService +{ + private Vector2 _buttonSize; + private float _priorityWidth; + private float _groupNameWidth; + private float _spacing; + + private string? _currentGroupName; + private ModPriority? _currentGroupPriority; + private IModGroup? _currentGroupEdited; + private bool _isGroupNameValid; + private IModGroup? _deleteGroup; + private IModGroup? _moveGroup; + private int _moveTo; + + private string? _currentOptionName; + private ModPriority? _currentOptionPriority; + private IModOption? _currentOptionEdited; + private IModOption? _deleteOption; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SameLine() + => ImGui.SameLine(0, _spacing); + + public void Draw(Mod mod) + { + _buttonSize = new Vector2(ImGui.GetFrameHeight()); + _priorityWidth = 50 * ImGuiHelpers.GlobalScale; + _groupNameWidth = 350f * ImGuiHelpers.GlobalScale; + _spacing = ImGui.GetStyle().ItemInnerSpacing.X; + + + 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; + } + } + + private void DrawGroup(IModGroup group, int idx) + { + using var id = ImRaii.PushId(idx); + using var frame = ImRaii.FramedGroup($"Group #{idx + 1}"); + DrawGroupNameRow(group); + switch (group) + { + case SingleModGroup s: + DrawSingleGroup(s, idx); + break; + case MultiModGroup m: + DrawMultiGroup(m, idx); + break; + case ImcModGroup i: + DrawImcGroup(i, idx); + break; + } + } + + private void DrawGroupNameRow(IModGroup group) + { + DrawGroupName(group); + SameLine(); + DrawGroupDelete(group); + SameLine(); + DrawGroupPriority(group); + } + + private void DrawGroupName(IModGroup group) + { + 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)) + { + _currentGroupEdited = group; + _currentGroupName = text; + _isGroupNameValid = ModGroupEditor.VerifyFileName(group.Mod, group, text, false); + } + + if (ImGui.IsItemDeactivated()) + { + if (_currentGroupName != null && _isGroupNameValid) + modManager.OptionEditor.RenameModGroup(group, _currentGroupName); + _currentGroupName = null; + _currentGroupEdited = null; + _isGroupNameValid = true; + } + + var tt = _isGroupNameValid + ? "Group Name" + : "Current name can not be used for this group."; + ImGuiUtil.HoverTooltip(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 (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), _buttonSize, tt, !enabled, true)) + _deleteGroup = group; + } + + private void DrawGroupPriority(IModGroup group) + { + var priority = _currentGroupEdited == group + ? (_currentGroupPriority ?? group.Priority).Value + : group.Priority.Value; + ImGui.SetNextItemWidth(_priorityWidth); + if (ImGui.InputInt("##GroupPriority", ref priority, 0, 0)) + { + _currentGroupEdited = group; + _currentGroupPriority = new ModPriority(priority); + } + + if (ImGui.IsItemDeactivated()) + { + if (_currentGroupPriority.HasValue) + modManager.OptionEditor.ChangeGroupPriority(group, _currentGroupPriority.Value); + _currentGroupEdited = null; + _currentGroupPriority = null; + } + + ImGuiUtil.HoverTooltip("Group Priority"); + } + + 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; + } + + SameLine(); + 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; + } + } + + 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)) + try + { + Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); + } + catch (Exception e) + { + Penumbra.Messager.NotificationMessage(e, "Could not open editor.", NotificationType.Error); + } + } + + + private void DrawSingleGroup(SingleModGroup group, int idx) + { } + + private void DrawMultiGroup(MultiModGroup group, int idx) + { } + + private void DrawImcGroup(ImcModGroup group, int idx) + { } +} diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 1326a763..fc5311d9 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -1,51 +1,37 @@ using ImGuiNET; using OtterGui.Raii; using OtterGui; +using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; -using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.UI.Classes; -using Dalamud.Interface.Components; using Penumbra.Collections.Manager; using Penumbra.Mods.Manager; using Penumbra.Services; -using Penumbra.Mods.SubMods; -using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; namespace Penumbra.UI.ModsTab; -public class ModPanelSettingsTab : ITab +public class ModPanelSettingsTab( + CollectionManager collectionManager, + ModManager modManager, + ModFileSystemSelector selector, + TutorialService tutorial, + CommunicatorService communicator, + ModGroupDrawer modGroupDrawer) + : ITab, IUiService { - private readonly Configuration _config; - private readonly CommunicatorService _communicator; - private readonly CollectionManager _collectionManager; - private readonly ModFileSystemSelector _selector; - private readonly TutorialService _tutorial; - private readonly ModManager _modManager; - private bool _inherited; private ModSettings _settings = null!; private ModCollection _collection = null!; - private bool _empty; private int? _currentPriority; - public ModPanelSettingsTab(CollectionManager collectionManager, ModManager modManager, ModFileSystemSelector selector, - TutorialService tutorial, CommunicatorService communicator, Configuration config) - { - _collectionManager = collectionManager; - _communicator = communicator; - _modManager = modManager; - _selector = selector; - _tutorial = tutorial; - _config = config; - } - public ReadOnlySpan Label => "Settings"u8; public void DrawHeader() - => _tutorial.OpenTutorial(BasicTutorialSteps.ModOptions); + => tutorial.OpenTutorial(BasicTutorialSteps.ModOptions); public void Reset() => _currentPriority = null; @@ -56,53 +42,24 @@ public class ModPanelSettingsTab : ITab if (!child) return; - _settings = _selector.SelectedSettings; - _collection = _selector.SelectedSettingCollection; - _inherited = _collection != _collectionManager.Active.Current; - _empty = _settings == ModSettings.Empty; - + _settings = selector.SelectedSettings; + _collection = selector.SelectedSettingCollection; + _inherited = _collection != collectionManager.Active.Current; DrawInheritedWarning(); UiHelpers.DefaultLineSpace(); - _communicator.PreSettingsPanelDraw.Invoke(_selector.Selected!.Identifier); + communicator.PreSettingsPanelDraw.Invoke(selector.Selected!.Identifier); DrawEnabledInput(); - _tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods); + tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods); ImGui.SameLine(); DrawPriorityInput(); - _tutorial.OpenTutorial(BasicTutorialSteps.Priority); + tutorial.OpenTutorial(BasicTutorialSteps.Priority); DrawRemoveSettings(); - _communicator.PostEnabledDraw.Invoke(_selector.Selected!.Identifier); - - if (_selector.Selected!.Groups.Count > 0) - { - var useDummy = true; - foreach (var (group, idx) in _selector.Selected!.Groups.WithIndex() - .Where(g => g.Value.Type == GroupType.Single && g.Value.Options.Count > _config.SingleGroupRadioMax)) - { - ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); - useDummy = false; - DrawSingleGroupCombo(group, idx); - } - - useDummy = true; - foreach (var (group, idx) in _selector.Selected!.Groups.WithIndex().Where(g => g.Value.IsOption)) - { - ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); - useDummy = false; - switch (group.Type) - { - case GroupType.Multi: - DrawMultiGroup(group, idx); - break; - case GroupType.Single when group.Options.Count <= _config.SingleGroupRadioMax: - DrawSingleGroupRadio(group, idx); - break; - } - } - } + communicator.PostEnabledDraw.Invoke(selector.Selected!.Identifier); + modGroupDrawer.Draw(selector.Selected!, _settings); UiHelpers.DefaultLineSpace(); - _communicator.PostSettingsPanelDraw.Invoke(_selector.Selected!.Identifier); + communicator.PostSettingsPanelDraw.Invoke(selector.Selected!.Identifier); } /// Draw a big red bar if the current setting is inherited. @@ -113,8 +70,8 @@ public class ModPanelSettingsTab : ITab using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); - if (ImGui.Button($"These settings are inherited from {_collection.Name}.", width)) - _collectionManager.Editor.SetModInheritance(_collectionManager.Active.Current, _selector.Selected!, false); + if (ImUtf8.Button($"These settings are inherited from {_collection.Name}.", width)) + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selector.Selected!, false); ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n" + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."); @@ -127,8 +84,8 @@ public class ModPanelSettingsTab : ITab if (!ImGui.Checkbox("Enabled", ref enabled)) return; - _modManager.SetKnown(_selector.Selected!); - _collectionManager.Editor.SetModState(_collectionManager.Active.Current, _selector.Selected!, enabled); + modManager.SetKnown(selector.Selected!); + collectionManager.Editor.SetModState(collectionManager.Active.Current, selector.Selected!, enabled); } /// @@ -146,7 +103,8 @@ public class ModPanelSettingsTab : ITab if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { if (_currentPriority != _settings.Priority.Value) - _collectionManager.Editor.SetModPriority(_collectionManager.Active.Current, _selector.Selected!, new ModPriority(_currentPriority.Value)); + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selector.Selected!, + new ModPriority(_currentPriority.Value)); _currentPriority = null; } @@ -162,189 +120,15 @@ public class ModPanelSettingsTab : ITab private void DrawRemoveSettings() { const string text = "Inherit Settings"; - if (_inherited || _empty) + if (_inherited || _settings == ModSettings.Empty) return; var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); if (ImGui.Button(text)) - _collectionManager.Editor.SetModInheritance(_collectionManager.Active.Current, _selector.Selected!, true); + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selector.Selected!, true); ImGuiUtil.HoverTooltip("Remove current settings from this collection so that it can inherit them.\n" + "If no inherited collection has settings for this mod, it will be disabled."); } - - /// - /// Draw a single group selector as a combo box. - /// If a description is provided, add a help marker besides it. - /// - private void DrawSingleGroupCombo(IModGroup group, int groupIdx) - { - using var id = ImRaii.PushId(groupIdx); - var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; - ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); - var options = group.Options; - using (var combo = ImRaii.Combo(string.Empty, options[selectedOption].Name)) - { - if (combo) - for (var idx2 = 0; idx2 < options.Count; ++idx2) - { - id.Push(idx2); - var option = options[idx2]; - if (ImGui.Selectable(option.Name, idx2 == selectedOption)) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, - Setting.Single(idx2)); - - if (option.Description.Length > 0) - ImGuiUtil.SelectableHelpMarker(option.Description); - - id.Pop(); - } - } - - ImGui.SameLine(); - if (group.Description.Length > 0) - ImGuiUtil.LabeledHelpMarker(group.Name, group.Description); - else - ImGui.TextUnformatted(group.Name); - } - - // Draw a single group selector as a set of radio buttons. - // If a description is provided, add a help marker besides it. - private void DrawSingleGroupRadio(IModGroup group, int groupIdx) - { - using var id = ImRaii.PushId(groupIdx); - var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - var options = group.Options; - DrawCollapseHandling(options, minWidth, DrawOptions); - - Widget.EndFramedGroup(); - return; - - void DrawOptions() - { - for (var idx = 0; idx < group.Options.Count; ++idx) - { - using var i = ImRaii.PushId(idx); - var option = options[idx]; - if (ImGui.RadioButton(option.Name, selectedOption == idx)) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, - Setting.Single(idx)); - - if (option.Description.Length <= 0) - continue; - - ImGui.SameLine(); - ImGuiComponents.HelpMarker(option.Description); - } - } - } - - - private void DrawCollapseHandling(IReadOnlyList options, float minWidth, Action draw) - { - if (options.Count <= _config.OptionGroupCollapsibleMin) - { - draw(); - } - else - { - var collapseId = ImGui.GetID("Collapse"); - var shown = ImGui.GetStateStorage().GetBool(collapseId, true); - var buttonTextShow = $"Show {options.Count} Options"; - var buttonTextHide = $"Hide {options.Count} Options"; - var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) - + 2 * ImGui.GetStyle().FramePadding.X; - minWidth = Math.Max(buttonWidth, minWidth); - if (shown) - { - var pos = ImGui.GetCursorPos(); - ImGui.Dummy(UiHelpers.IconButtonSize); - using (var _ = ImRaii.Group()) - { - draw(); - } - - - var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); - var endPos = ImGui.GetCursorPos(); - ImGui.SetCursorPos(pos); - if (ImGui.Button(buttonTextHide, new Vector2(width, 0))) - ImGui.GetStateStorage().SetBool(collapseId, !shown); - - ImGui.SetCursorPos(endPos); - } - else - { - var optionWidth = options.Max(o => ImGui.CalcTextSize(o.Name).X) - + ImGui.GetStyle().ItemInnerSpacing.X - + ImGui.GetFrameHeight() - + ImGui.GetStyle().FramePadding.X; - var width = Math.Max(optionWidth, minWidth); - if (ImGui.Button(buttonTextShow, new Vector2(width, 0))) - ImGui.GetStateStorage().SetBool(collapseId, !shown); - } - } - } - - /// - /// Draw a multi group selector as a bordered set of checkboxes. - /// If a description is provided, add a help marker in the title. - /// - private void DrawMultiGroup(IModGroup group, int groupIdx) - { - using var id = ImRaii.PushId(groupIdx); - var flags = _empty ? group.DefaultSettings : _settings.Settings[groupIdx]; - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - var options = group.Options; - DrawCollapseHandling(options, minWidth, DrawOptions); - - Widget.EndFramedGroup(); - var label = $"##multi{groupIdx}"; - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - ImGui.OpenPopup($"##multi{groupIdx}"); - - DrawMultiPopup(group, groupIdx, label); - return; - - void DrawOptions() - { - for (var idx = 0; idx < options.Count; ++idx) - { - using var i = ImRaii.PushId(idx); - var option = options[idx]; - var setting = flags.HasFlag(idx); - - if (ImGui.Checkbox(option.Name, ref setting)) - { - flags = flags.SetBit(idx, setting); - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, flags); - } - - if (option.Description.Length > 0) - { - ImGui.SameLine(); - ImGuiComponents.HelpMarker(option.Description); - } - } - } - } - - private void DrawMultiPopup(IModGroup group, int groupIdx, string label) - { - using var style = ImRaii.PushStyle(ImGuiStyleVar.PopupBorderSize, 1); - using var popup = ImRaii.Popup(label); - if (!popup) - return; - - ImGui.TextUnformatted(group.Name); - ImGui.Separator(); - if (ImGui.Selectable("Enable All")) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, - Setting.AllBits(group.Options.Count)); - - if (ImGui.Selectable("Disable All")) - _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, Setting.Zero); - } }