diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 2d90a6dd..1ed590cf 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -31,7 +31,6 @@ public class ModMerger : IDisposable public string OptionGroupName = "Merges"; public string OptionName = string.Empty; - private readonly Dictionary _fileToFile = new(); private readonly HashSet _createdDirectories = new(); private readonly HashSet _createdGroups = new(); diff --git a/Penumbra/Services/SaveService.cs b/Penumbra/Services/SaveService.cs index 557cc4fc..66695b40 100644 --- a/Penumbra/Services/SaveService.cs +++ b/Penumbra/Services/SaveService.cs @@ -29,7 +29,11 @@ public interface ISavable public class SaveService { - private static readonly TimeSpan StandardDelay = TimeSpan.FromSeconds(30); +#if DEBUG + private static readonly TimeSpan StandardDelay = TimeSpan.FromSeconds(2); +#else + private static readonly TimeSpan StandardDelay = TimeSpan.FromSeconds(10); +#endif private readonly Logger _log; private readonly FrameworkManager _framework; diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index d1a5ec95..f7490c93 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -1,13 +1,14 @@ +using System; using System.Linq; using System.Numerics; using Dalamud.Interface; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.UI.Classes; -using SixLabors.ImageSharp.ColorSpaces; namespace Penumbra.UI.AdvancedWindow; @@ -33,90 +34,225 @@ public class ModMergeTab if (!tab) return; - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted($"Merge {_modMerger.MergeFromMod.Name} into "); + ImGui.Dummy(Vector2.One); + var size = 550 * ImGuiHelpers.GlobalScale; + DrawMergeInto(size); ImGui.SameLine(); - DrawCombo(); - - using (var disabled = ImRaii.Disabled(_modMerger.MergeFromMod.HasOptions)) - { - ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); - ImGui.InputTextWithHint("##optionGroupInput", "Target Option Group", ref _modMerger.OptionGroupName, 64); - ImGui.SameLine(); - ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); - ImGui.InputTextWithHint("##optionInput", "Target Option Name", ref _modMerger.OptionName, 64); - } - - if (ImGuiUtil.DrawDisabledButton("Merge", Vector2.Zero, string.Empty, !_modMerger.CanMerge)) - _modMerger.Merge(); + DrawMergeIntoDesc(); ImGui.Dummy(Vector2.One); ImGui.Separator(); ImGui.Dummy(Vector2.One); - using (var table = ImRaii.Table("##options", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY, - ImGui.GetContentRegionAvail() with { Y = 6 * ImGui.GetFrameHeightWithSpacing() })) - { - foreach (var (option, idx) in _modMerger.MergeFromMod.AllSubMods.WithIndex()) - { - using var id = ImRaii.PushId(idx); - var selected = _modMerger.SelectedOptions.Contains(option); - ImGui.TableNextColumn(); - if (ImGui.Checkbox("##check", ref selected)) - { - if (selected) - _modMerger.SelectedOptions.Add(option); - else - _modMerger.SelectedOptions.Remove(option); - } - if (option.IsDefault) - { - ImGuiUtil.DrawTableColumn(option.FullName); - ImGui.TableNextColumn(); - } - else - { - ImGuiUtil.DrawTableColumn(option.ParentMod.Groups[option.GroupIdx].Name); - ImGuiUtil.DrawTableColumn(option.Name); - } + DrawSplitOff(size); + ImGui.SameLine(); + DrawSplitOffDesc(); - ImGuiUtil.DrawTableColumn(option.FileData.Count.ToString()); - ImGuiUtil.DrawTableColumn(option.FileSwapData.Count.ToString()); - ImGuiUtil.DrawTableColumn(option.Manipulations.Count.ToString()); - } - } - ImGui.InputTextWithHint("##newModInput", "New Mod Name...", ref _newModName, 64); - if (ImGuiUtil.DrawDisabledButton("Split Off", Vector2.Zero, string.Empty, - _newModName.Length == 0 || _modMerger.SelectedOptions.Count == 0)) - _modMerger.SplitIntoMod(_newModName); - - if (_modMerger.Warnings.Count > 0) - { - ImGui.Separator(); - ImGui.Dummy(Vector2.One); - using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.TutorialBorder); - foreach (var warning in _modMerger.Warnings.SkipLast(1)) - { - ImGuiUtil.TextWrapped(warning); - ImGui.Separator(); - } - ImGuiUtil.TextWrapped(_modMerger.Warnings[^1]); - } - - if (_modMerger.Error != null) - { - ImGui.Separator(); - ImGui.Dummy(Vector2.One); - using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); - ImGuiUtil.TextWrapped(_modMerger.Error.ToString()); - } + DrawError(); + DrawWarnings(); } - private void DrawCombo() + private void DrawMergeInto(float size) { - _modCombo.Draw("##ModSelection", _modCombo.CurrentSelection?.Name.Text ?? string.Empty, string.Empty, - 200 * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeight()); + using var bigGroup = ImRaii.Group(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted($"Merge {_modMerger.MergeFromMod!.Name} into "); + ImGui.SameLine(); + DrawCombo(size - ImGui.GetItemRectSize().X - ImGui.GetStyle().ItemSpacing.X); + + var width = ImGui.GetItemRectSize(); + using (var g = ImRaii.Group()) + { + using var disabled = ImRaii.Disabled(_modMerger.MergeFromMod.HasOptions); + var buttonWidth = (size - ImGui.GetStyle().ItemSpacing.X) / 2; + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 1); + var group = _modMerger.MergeToMod?.Groups.FirstOrDefault(g => g.Name == _modMerger.OptionGroupName); + var color = group != null || _modMerger.OptionGroupName.Length == 0 && _modMerger.OptionName.Length == 0 + ? Colors.PressEnterWarningBg + : Colors.DiscordColor; + using var c = ImRaii.PushColor(ImGuiCol.Border, color); + ImGui.SetNextItemWidth(buttonWidth); + ImGui.InputTextWithHint("##optionGroupInput", "Target Option Group", ref _modMerger.OptionGroupName, 64); + ImGuiUtil.HoverTooltip( + "The name of the new or existing option group to find or create the option in. Leave both group and option name blank for the default option.\n" + + "A red border indicates an existing option group, a blue border indicates a new one."); + ImGui.SameLine(); + + + color = color == Colors.DiscordColor + ? Colors.DiscordColor + : group == null || group.Any(o => o.Name == _modMerger.OptionName) + ? Colors.PressEnterWarningBg + : Colors.DiscordColor; + c.Push(ImGuiCol.Border, color); + ImGui.SetNextItemWidth(buttonWidth); + ImGui.InputTextWithHint("##optionInput", "Target Option Name", ref _modMerger.OptionName, 64); + ImGuiUtil.HoverTooltip( + "The name of the new or existing option to merge this mod into. Leave both group and option name blank for the default option.\n" + + "A red border indicates an existing option, a blue border indicates a new one."); + } + + if (_modMerger.MergeFromMod.HasOptions) + ImGuiUtil.HoverTooltip("You can only specify a target option if the source mod has no true options itself.", + ImGuiHoveredFlags.AllowWhenDisabled); + + if (ImGuiUtil.DrawDisabledButton("Merge", new Vector2(size, 0), + _modMerger.CanMerge ? string.Empty : "Please select a target mod different from the current mod.", !_modMerger.CanMerge)) + _modMerger.Merge(); + } + + private void DrawMergeIntoDesc() + { + ImGuiUtil.TextWrapped(_modMerger.MergeFromMod!.HasOptions + ? "The currently selected mod has options.\n\nThis means, that all of those options will be merged into the target. If merging an option is not possible due to the redirections already existing in an existing option, it will revert all changes and break." + : "The currently selected mod has no true options.\n\nThis means that you can select an existing or new option to merge all its changes into in the target mod. On failure to merge into an existing option, all changes will be reverted."); + } + + private void DrawCombo(float width) + { + _modCombo.Draw("##ModSelection", _modCombo.CurrentSelection?.Name.Text ?? "Select the target Mod...", string.Empty, width, + ImGui.GetTextLineHeight()); _modMerger.MergeToMod = _modCombo.CurrentSelection; } + + private void DrawSplitOff(float size) + { + using var group = ImRaii.Group(); + ImGui.SetNextItemWidth(size); + ImGui.InputTextWithHint("##newModInput", "New Mod Name...", ref _newModName, 64); + ImGuiUtil.HoverTooltip("Choose a name for the newly created mod. This does not need to be unique."); + var tt = _newModName.Length == 0 + ? "Please enter a name for the newly created mod first." + : _modMerger.SelectedOptions.Count == 0 + ? "Please select at least one option to split off." + : string.Empty; + var buttonText = + $"Split Off {_modMerger.SelectedOptions.Count} Option{(_modMerger.SelectedOptions.Count > 1 ? "s" : string.Empty)}###SplitOff"; + if (ImGuiUtil.DrawDisabledButton(buttonText, new Vector2(size, 0), tt, tt.Length > 0)) + _modMerger.SplitIntoMod(_newModName); + + ImGui.Dummy(Vector2.One); + var buttonSize = new Vector2((size - 2 * ImGui.GetStyle().ItemSpacing.X) / 3, 0); + if (ImGui.Button("Select All", buttonSize)) + _modMerger.SelectedOptions.UnionWith(_modMerger.MergeFromMod!.AllSubMods); + ImGui.SameLine(); + if (ImGui.Button("Unselect All", buttonSize)) + _modMerger.SelectedOptions.Clear(); + ImGui.SameLine(); + if (ImGui.Button("Invert Selection", buttonSize)) + _modMerger.SelectedOptions.SymmetricExceptWith(_modMerger.MergeFromMod!.AllSubMods); + DrawOptionTable(size); + } + + private void DrawSplitOffDesc() + { + ImGuiUtil.TextWrapped("Here you can create a copy or a partial copy of the currently selected mod.\n\n" + + "Select as many of the options you want to copy over, enter a new mod name and click Split Off.\n\n" + + "You can right-click option groups to select or unselect all options from that specific group, and use the three buttons above the table for quick manipulation of your selection.\n\n" + + "Only required files will be copied over to the new mod. The names of options and groups will be retained. If the Default option is not selected, the new mods default option will be empty."); + } + + private void DrawOptionTable(float size) + { + var options = _modMerger.MergeFromMod!.AllSubMods.ToList(); + var height = _modMerger.Warnings.Count == 0 && _modMerger.Error == null + ? ImGui.GetContentRegionAvail().Y - 3 * ImGui.GetFrameHeightWithSpacing() + : 8 * ImGui.GetFrameHeightWithSpacing(); + height = Math.Min(height, (options.Count + 1) * ImGui.GetFrameHeightWithSpacing()); + var tableSize = new Vector2(size, height); + using var table = ImRaii.Table("##options", 6, + ImGuiTableFlags.RowBg + | ImGuiTableFlags.SizingFixedFit + | ImGuiTableFlags.ScrollY + | ImGuiTableFlags.BordersOuterV + | ImGuiTableFlags.BordersOuterH, + tableSize); + if (!table) + return; + + ImGui.TableSetupColumn("##Selected", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight()); + ImGui.TableSetupColumn("Option", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Option Group", ImGuiTableColumnFlags.WidthFixed, 120 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("#Files", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("#Swaps", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("#Manips", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale); + ImGui.TableHeadersRow(); + foreach (var (option, idx) in options.WithIndex()) + { + using var id = ImRaii.PushId(idx); + var selected = _modMerger.SelectedOptions.Contains(option); + + void Handle(SubMod option2, bool selected2) + { + if (selected2) + _modMerger.SelectedOptions.Add(option2); + else + _modMerger.SelectedOptions.Remove(option2); + } + + ImGui.TableNextColumn(); + if (ImGui.Checkbox("##check", ref selected)) + Handle(option, selected); + + if (option.IsDefault) + { + ImGuiUtil.DrawTableColumn(option.FullName); + ImGui.TableNextColumn(); + } + else + { + ImGuiUtil.DrawTableColumn(option.Name); + var group = option.ParentMod.Groups[option.GroupIdx]; + ImGui.TableNextColumn(); + ImGui.Selectable(group.Name, false); + if (ImGui.BeginPopupContextItem("##groupContext")) + { + if (ImGui.MenuItem("Select All")) + foreach (var opt in group) + Handle((SubMod)opt, true); + + if (ImGui.MenuItem("Unselect All")) + foreach (var opt in group) + Handle((SubMod)opt, false); + ImGui.EndPopup(); + } + } + + ImGui.TableNextColumn(); + ImGuiUtil.RightAlign(option.FileData.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); + ImGui.TableNextColumn(); + ImGuiUtil.RightAlign(option.FileSwapData.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); + ImGui.TableNextColumn(); + ImGuiUtil.RightAlign(option.Manipulations.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); + } + } + + private void DrawWarnings() + { + if (_modMerger.Warnings.Count == 0) + return; + + ImGui.Separator(); + ImGui.Dummy(Vector2.One); + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.TutorialBorder); + foreach (var warning in _modMerger.Warnings.SkipLast(1)) + { + ImGuiUtil.TextWrapped(warning); + ImGui.Separator(); + } + + ImGuiUtil.TextWrapped(_modMerger.Warnings[^1]); + } + + private void DrawError() + { + if (_modMerger.Error == null) + return; + + ImGui.Separator(); + ImGui.Dummy(Vector2.One); + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); + ImGuiUtil.TextWrapped(_modMerger.Error.ToString()); + } }