diff --git a/Penumbra/Models/ModMeta.cs b/Penumbra/Models/ModMeta.cs index c027c467..1ab6d31e 100644 --- a/Penumbra/Models/ModMeta.cs +++ b/Penumbra/Models/ModMeta.cs @@ -1,5 +1,8 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using Newtonsoft.Json; +using System.Linq; +using System.IO; +using System; namespace Penumbra.Models { @@ -22,5 +25,21 @@ namespace Penumbra.Models [JsonIgnore] public bool HasGroupWithConfig { get; set; } = false; + + public static ModMeta LoadFromFile(string filePath) + { + try + { + var meta = JsonConvert.DeserializeObject< ModMeta >( File.ReadAllText( filePath ) ); + meta.HasGroupWithConfig = meta.Groups != null && meta.Groups.Count > 0 + && meta.Groups.Values.Any( G => G.SelectionType == SelectType.Multi || G.Options.Count > 1); + return meta; + } + catch( Exception) + { + return null; + // todo: handle broken mods properly + } + } } } \ No newline at end of file diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs index f3e828a9..10bf35fd 100644 --- a/Penumbra/Mods/ModCollection.cs +++ b/Penumbra/Mods/ModCollection.cs @@ -64,21 +64,7 @@ namespace Penumbra.Mods continue; } - ModMeta meta; - - try - { - meta = JsonConvert.DeserializeObject< ModMeta >( File.ReadAllText( metaFile.FullName ) ); - meta.HasGroupWithConfig = meta.Groups != null && meta.Groups.Count > 0 - && meta.Groups.Values.Any( G => G.SelectionType == SelectType.Multi || G.Options.Count > 1); - } - catch( Exception e ) - { - PluginLog.Error( e, "failed to parse mod info, attempting basic load for: {FilePath}", metaFile.FullName ); - continue; - - // todo: handle broken mods properly - } + var meta = ModMeta.LoadFromFile(metaFile.FullName); var mod = new ResourceMod { diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index 9b2013be..164b715a 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -108,6 +108,9 @@ namespace Penumbra.Mods { ResolvedFiles.Clear(); SwappedFiles.Clear(); + + if (!_plugin.Configuration.IsEnabled) + return; var registeredFiles = new Dictionary< string, string >(); diff --git a/Penumbra/Plugin.cs b/Penumbra/Plugin.cs index 38cf4a9e..1bf4bf39 100644 --- a/Penumbra/Plugin.cs +++ b/Penumbra/Plugin.cs @@ -124,7 +124,7 @@ namespace Penumbra return; } - SettingsInterface.Visible = !SettingsInterface.Visible; + SettingsInterface.FlipVisibility(); } } } diff --git a/Penumbra/UI/ImGuiFramedGroup.cs b/Penumbra/UI/ImGuiFramedGroup.cs new file mode 100644 index 00000000..01262878 --- /dev/null +++ b/Penumbra/UI/ImGuiFramedGroup.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using ImGuiNET; + +namespace Penumbra.UI +{ + public static partial class ImGuiCustom + { + public static void BeginFramedGroup(string label) => BeginFramedGroupInternal(ref label, ZeroVector, false); + public static void BeginFramedGroup(string label, Vector2 minSize) => BeginFramedGroupInternal(ref label, minSize, false); + + public static bool BeginFramedGroupEdit(ref string label) => BeginFramedGroupInternal(ref label, ZeroVector, true); + public static bool BeginFramedGroupEdit(ref string label, Vector2 minSize) => BeginFramedGroupInternal(ref label, minSize, true); + + private static bool BeginFramedGroupInternal(ref string label, Vector2 minSize, bool edit) + { + var itemSpacing = ImGui.GetStyle().ItemSpacing; + var frameHeight = ImGui.GetFrameHeight(); + var halfFrameHeight = new Vector2(ImGui.GetFrameHeight() / 2, 0); + + ImGui.BeginGroup(); // First group + + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, ZeroVector); + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, ZeroVector); + + ImGui.BeginGroup(); // Second group + + var effectiveSize = minSize; + if (effectiveSize.X < 0) + effectiveSize.X = ImGui.GetContentRegionAvail().X; + + // Ensure width. + ImGui.Dummy(new(effectiveSize.X, 0)); + // Ensure left half boundary width/distance. + ImGui.Dummy(halfFrameHeight); + + ImGui.SameLine(); + ImGui.BeginGroup(); // Third group. + // Ensure right half of boundary width/distance + ImGui.Dummy(halfFrameHeight); + + // Label block + ImGui.SameLine(); + var ret = false; + if (edit) + ret = ImGuiCustom.ResizingTextInput(ref label, 1024); + else + ImGui.TextUnformatted(label); + + var labelMin = ImGui.GetItemRectMin(); + var labelMax = ImGui.GetItemRectMax(); + ImGui.SameLine(); + // Ensure height and distance to label. + ImGui.Dummy(new Vector2(0, frameHeight + itemSpacing.Y)); + + ImGui.BeginGroup(); // Fourth Group. + + ImGui.PopStyleVar(2); + + ImGui.SetWindowSize(new Vector2(ImGui.GetWindowSize().X - frameHeight, ImGui.GetWindowSize().Y)); + + var itemWidth = ImGui.CalcItemWidth(); + ImGui.PushItemWidth(Math.Max(0f, itemWidth - frameHeight)); + + labelStack.Add((labelMin, labelMax)); + return ret; + } + + private static void DrawClippedRect(Vector2 clipMin, Vector2 clipMax, Vector2 drawMin, Vector2 drawMax, uint color, float thickness) + { + ImGui.PushClipRect(clipMin, clipMax, true); + ImGui.GetWindowDrawList().AddRect(drawMin, drawMax, color, thickness); + ImGui.PopClipRect(); + } + + public static void EndFramedGroup() + { + uint borderColor = ImGui.ColorConvertFloat4ToU32(ImGui.GetStyle().Colors[(int)ImGuiCol.Border]); + Vector2 itemSpacing = ImGui.GetStyle().ItemSpacing; + float frameHeight = ImGui.GetFrameHeight(); + Vector2 halfFrameHeight = new(ImGui.GetFrameHeight() / 2, 0); + + ImGui.PopItemWidth(); + + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, ZeroVector); + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, ZeroVector); + + ImGui.EndGroup(); // Close fourth group + ImGui.EndGroup(); // Close third group + + ImGui.SameLine(); + // Ensure right distance. + ImGui.Dummy(halfFrameHeight); + // Ensure bottom distance + ImGui.Dummy(new Vector2(0, frameHeight/2 - itemSpacing.Y)); + ImGui.EndGroup(); // Close second group + + var itemMin = ImGui.GetItemRectMin(); + var itemMax = ImGui.GetItemRectMax(); + var (currentLabelMin, currentLabelMax) = labelStack[labelStack.Count - 1]; + labelStack.RemoveAt(labelStack.Count - 1); + + var halfFrame = new Vector2(frameHeight / 8, frameHeight / 2); + currentLabelMin.X -= itemSpacing.X; + currentLabelMax.X += itemSpacing.X; + var frameMin = itemMin + halfFrame; + var frameMax = itemMax - new Vector2(halfFrame.X, 0); + + // Left + DrawClippedRect(new(-float.MaxValue , -float.MaxValue ), new(currentLabelMin.X, float.MaxValue ), frameMin, frameMax, borderColor, halfFrame.X); + // Right + DrawClippedRect(new(currentLabelMax.X, -float.MaxValue ), new(float.MaxValue , float.MaxValue ), frameMin, frameMax, borderColor, halfFrame.X); + // Top + DrawClippedRect(new(currentLabelMin.X, -float.MaxValue ), new(currentLabelMax.X, currentLabelMin.Y), frameMin, frameMax, borderColor, halfFrame.X); + // Bottom + DrawClippedRect(new(currentLabelMin.X, currentLabelMax.Y), new(currentLabelMax.X, float.MaxValue ), frameMin, frameMax, borderColor, halfFrame.X); + + ImGui.PopStyleVar(2); + ImGui.SetWindowSize(new Vector2(ImGui.GetWindowSize().X + frameHeight, ImGui.GetWindowSize().Y)); + ImGui.Dummy(ZeroVector); + + ImGui.EndGroup(); // Close first group + } + + private static readonly Vector2 ZeroVector = new(0, 0); + + private static List<(Vector2, Vector2)> labelStack = new(); + } +} \ No newline at end of file diff --git a/Penumbra/UI/ImGuiRenameableCombo.cs b/Penumbra/UI/ImGuiRenameableCombo.cs new file mode 100644 index 00000000..4fa0509b --- /dev/null +++ b/Penumbra/UI/ImGuiRenameableCombo.cs @@ -0,0 +1,43 @@ +using ImGuiNET; + +namespace Penumbra.UI +{ + public static partial class ImGuiCustom + { + public static bool RenameableCombo(string label, ref int currentItem, ref string newName, string[] items, int numItems) + { + var ret = false; + newName = ""; + var newOption = ""; + if (ImGui.BeginCombo(label, (numItems > 0) ? items[currentItem] : newOption)) + { + for (var i = 0; i < numItems; ++i) + { + var isSelected = i == currentItem; + ImGui.SetNextItemWidth(-1); + if (ImGui.InputText($"##{label}_{i}", ref items[i], 64, ImGuiInputTextFlags.EnterReturnsTrue)) + { + currentItem = i; + newName = items[i]; + ret = true; + ImGui.CloseCurrentPopup(); + } + if (isSelected) + ImGui.SetItemDefaultFocus(); + } + ImGui.SetNextItemWidth(-1); + if (ImGui.InputText($"##{label}_new", ref newOption, 64, ImGuiInputTextFlags.EnterReturnsTrue)) + { + currentItem = numItems; + newName = newOption; + ret = true; + ImGui.CloseCurrentPopup(); + } + if (numItems == 0) + ImGui.SetItemDefaultFocus(); + ImGui.EndCombo(); + } + return ret; + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ImGuiResizingTextInput.cs b/Penumbra/UI/ImGuiResizingTextInput.cs new file mode 100644 index 00000000..8ec12d66 --- /dev/null +++ b/Penumbra/UI/ImGuiResizingTextInput.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using ImGuiNET; + +namespace Penumbra.UI +{ + public static partial class ImGuiCustom + { + public static bool InputOrText(bool editable, string label, ref string text, uint maxLength) + { + if (editable) + return ResizingTextInput(label, ref text, maxLength); + + ImGui.Text(text); + return false; + } + + public static bool ResizingTextInput(string label, ref string input, uint maxLength) => ResizingTextInputIntern(label, ref input, maxLength).Item1; + public static bool ResizingTextInput(ref string input, uint maxLength) + { + var (ret, id) = ResizingTextInputIntern($"##{input}", ref input, maxLength); + if (ret) + _textInputWidths.Remove(id); + return ret; + } + + private static (bool, uint) ResizingTextInputIntern(string label, ref string input, uint maxLength) + { + var id = ImGui.GetID(label); + if (!_textInputWidths.TryGetValue(id, out var width)) + width = ImGui.CalcTextSize(input).X + 10; + + ImGui.SetNextItemWidth(width); + var ret = ImGui.InputText(label, ref input, maxLength, ImGuiInputTextFlags.EnterReturnsTrue); + _textInputWidths[id] = ImGui.CalcTextSize(input).X + 10; + return (ret, id); + } + + private static readonly Dictionary _textInputWidths = new(); + } +} \ No newline at end of file diff --git a/Penumbra/UI/ImGuiUtil.cs b/Penumbra/UI/ImGuiUtil.cs new file mode 100644 index 00000000..58a00a33 --- /dev/null +++ b/Penumbra/UI/ImGuiUtil.cs @@ -0,0 +1,26 @@ +using ImGuiNET; + +namespace Penumbra.UI +{ + public static partial class ImGuiCustom + { + public static void VerticalDistance(float distance) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + distance); + } + + public static void RightJustifiedText(float pos, string text) + { + ImGui.SetCursorPosX(pos - ImGui.CalcTextSize(text).X - 2 * ImGui.GetStyle().ItemSpacing.X); + ImGui.Text(text); + } + + public static void RightJustifiedLabel(float pos, string text) + { + ImGui.SetCursorPosX(pos - ImGui.CalcTextSize(text).X - ImGui.GetStyle().ItemSpacing.X / 2); + ImGui.Text(text); + ImGui.SameLine(pos); + } + + } +} \ No newline at end of file diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs new file mode 100644 index 00000000..cd61c6de --- /dev/null +++ b/Penumbra/UI/LaunchButton.cs @@ -0,0 +1,56 @@ +using System.Numerics; +using ImGuiNET; + +namespace Penumbra.UI +{ + public partial class SettingsInterface + { + private class LaunchButton + { + // magic numbers + private const int Padding = 50; + private const int Width = 200; + private const int Height = 45; + private const string MenuButtonsName = "Penumbra Menu Buttons"; + private const string MenuButtonLabel = "Manage Mods"; + + private static readonly Vector2 WindowSize = new(Width, Height); + private static readonly Vector2 WindowPosOffset = new(Padding + Width, Padding + Height); + + private readonly ImGuiWindowFlags ButtonFlags = ImGuiWindowFlags.AlwaysAutoResize + | ImGuiWindowFlags.NoBackground + | ImGuiWindowFlags.NoDecoration + | ImGuiWindowFlags.NoMove + | ImGuiWindowFlags.NoScrollbar + | ImGuiWindowFlags.NoResize + | ImGuiWindowFlags.NoSavedSettings; + + private readonly SettingsInterface _base; + private readonly Dalamud.Game.ClientState.Condition _condition; + + public LaunchButton(SettingsInterface ui) + { + _base = ui; + _condition = ui._plugin.PluginInterface.ClientState.Condition; + } + + public void Draw() + { + if( !_condition.Any() && !_base._menu.Visible ) + { + var ss = ImGui.GetIO().DisplaySize; + + ImGui.SetNextWindowPos( ss - WindowPosOffset, ImGuiCond.Always ); + + if( ImGui.Begin(MenuButtonsName, ButtonFlags) ) + { + if( ImGui.Button( MenuButtonLabel, WindowSize ) ) + _base.FlipVisibility(); + + ImGui.End(); + } + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/MenuBar.cs b/Penumbra/UI/MenuBar.cs new file mode 100644 index 00000000..aa6e929b --- /dev/null +++ b/Penumbra/UI/MenuBar.cs @@ -0,0 +1,43 @@ +using ImGuiNET; + +namespace Penumbra.UI +{ + public partial class SettingsInterface + { + private class MenuBar + { + private const string MenuLabel = "Penumbra"; + private const string MenuItemToggle = "Toggle UI"; + private const string SlashCommand = "/penumbra"; + private const string MenuItemRediscover = "Rediscover Mods"; + +#if DEBUG + private const bool _showDebugBar = true; +#else + private const bool _showDebugBar = false; +#endif + + private readonly SettingsInterface _base; + public MenuBar(SettingsInterface ui) => _base = ui; + + public void Draw() + { + if( _showDebugBar && ImGui.BeginMainMenuBar() ) + { + if( ImGui.BeginMenu( MenuLabel ) ) + { + if( ImGui.MenuItem( MenuItemToggle, SlashCommand, _base._menu.Visible ) ) + _base.FlipVisibility(); + + if( ImGui.MenuItem( MenuItemRediscover ) ) + _base.ReloadMods(); + + ImGui.EndMenu(); + } + + ImGui.EndMainMenuBar(); + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/SettingsInterface.cs b/Penumbra/UI/SettingsInterface.cs index 54bf55bb..00a2cde6 100644 --- a/Penumbra/UI/SettingsInterface.cs +++ b/Penumbra/UI/SettingsInterface.cs @@ -1,871 +1,46 @@ -using System; -using System.ComponentModel.Design; -using System.Diagnostics; using System.IO; -using System.Linq; using System.Numerics; -using System.Threading.Tasks; -using System.Windows.Forms; -using Dalamud.Interface; -using Dalamud.Plugin; -using ImGuiNET; -using Newtonsoft.Json; -using Penumbra.Importer; -using Penumbra.Models; namespace Penumbra.UI -{ - public class SettingsInterface - { - private readonly Plugin _plugin; - - public bool Visible; - public bool ShowDebugBar; - - private static readonly Vector2 AutoFillSize = new Vector2( -1, -1 ); - private static readonly Vector2 ModListSize = new Vector2( 200, -1 ); - - private static readonly Vector2 MinSettingsSize = new Vector2( 800, 450 ); - private static readonly Vector2 MaxSettingsSize = new Vector2( 69420, 42069 ); - - private const string DialogDeleteMod = "PenumbraDeleteMod"; - - private int _selectedModIndex; - private int? _selectedModDeleteIndex; - private ModInfo _selectedMod; - - private bool _isImportRunning; - private TexToolsImport _texToolsImport = null!; - +{ + public partial class SettingsInterface + { + private const float DefaultVerticalSpace = 20f; + + private static readonly Vector2 AutoFillSize = new(-1, -1); + private static readonly Vector2 ZeroVector = new( 0, 0); + + private readonly Plugin _plugin; + + private readonly LaunchButton _launchButton; + private readonly MenuBar _menuBar; + private readonly SettingsMenu _menu; + public SettingsInterface( Plugin plugin ) - { - _plugin = plugin; -#if DEBUG - Visible = true; - ShowDebugBar = true; -#endif - } - + { + _plugin = plugin; + _launchButton = new(this); + _menuBar = new(this); + _menu = new(this); + } + + public void FlipVisibility() => _menu.Visible = !_menu.Visible; + public void Draw() { - if( ShowDebugBar && ImGui.BeginMainMenuBar() ) - { - if( ImGui.BeginMenu( "Penumbra" ) ) - { - if( ImGui.MenuItem( "Toggle UI", "/penumbra", Visible ) ) - { - Visible = !Visible; - } - - if( ImGui.MenuItem( "Rediscover Mods" ) ) - { - ReloadMods(); - } - -// ImGui.Separator(); -// #if DEBUG -// ImGui.Text( _plugin.PluginDebugTitleStr ); -// #else -// ImGui.Text( _plugin.Name ); -// #endif - - ImGui.EndMenu(); - } - - ImGui.EndMainMenuBar(); - } - - if( !_plugin.PluginInterface.ClientState.Condition.Any() && !Visible ) - { - // draw mods button on da menu :DDD - var ss = ImGui.GetIO().DisplaySize; - var padding = 50; - var width = 200; - var height = 45; - - // magic numbers - ImGui.SetNextWindowPos( new Vector2( ss.X - padding - width, ss.Y - padding - height ), ImGuiCond.Always ); - - if( - ImGui.Begin( - "Penumbra Menu Buttons", - ImGuiWindowFlags.AlwaysAutoResize | - ImGuiWindowFlags.NoBackground | - ImGuiWindowFlags.NoDecoration | - ImGuiWindowFlags.NoMove | - ImGuiWindowFlags.NoScrollbar | - ImGuiWindowFlags.NoResize | - ImGuiWindowFlags.NoSavedSettings - ) - ) - { - if( ImGui.Button( "Manage Mods", new Vector2( width, height ) ) ) - { - Visible = !Visible; - } - - ImGui.End(); - } - } - - if( !Visible ) - { - return; - } - - ImGui.SetNextWindowSizeConstraints( MinSettingsSize, MaxSettingsSize ); -#if DEBUG - var ret = ImGui.Begin( _plugin.PluginDebugTitleStr, ref Visible ); -#else - var ret = ImGui.Begin( _plugin.Name, ref Visible ); -#endif - if( !ret ) - { - return; - } - - ImGui.BeginTabBar( "PenumbraSettings" ); - - DrawSettingsTab(); - DrawImportTab(); - - - if( !_isImportRunning ) - { - DrawModBrowser(); - - DrawInstalledMods(); - - if( _plugin.Configuration.ShowAdvanced ) - { - DrawEffectiveFileList(); - } - - DrawDeleteModal(); - } - - ImGui.EndTabBar(); - - ImGui.End(); - } - - void DrawImportTab() - { - var ret = ImGui.BeginTabItem( "Import Mods" ); - if( !ret ) - { - return; - } - - if( !_isImportRunning ) - { - if( ImGui.Button( "Import TexTools Modpacks" ) ) - { - _isImportRunning = true; - - Task.Run( async () => - { - var picker = new OpenFileDialog - { - Multiselect = true, - Filter = "TexTools TTMP Modpack (*.ttmp2)|*.ttmp*|All files (*.*)|*.*", - CheckFileExists = true, - Title = "Pick one or more modpacks." - }; - - var result = await picker.ShowDialogAsync(); - - if( result == DialogResult.OK ) - { - foreach( var fileName in picker.FileNames ) - { - PluginLog.Log( "-> {0} START", fileName ); - - try - { - _texToolsImport = new TexToolsImport( new DirectoryInfo( _plugin.Configuration.CurrentCollection ) ); - _texToolsImport.ImportModPack( new FileInfo( fileName ) ); - } - catch( Exception ex ) - { - PluginLog.LogError( ex, "Could not import one or more modpacks." ); - } - - PluginLog.Log( "-> {0} OK!", fileName ); - } - - _texToolsImport = null; - ReloadMods(); - } - - _isImportRunning = false; - } ); - } - } - else - { - ImGui.Button( "Import in progress..." ); - - if( _texToolsImport != null ) - { - switch( _texToolsImport.State ) - { - case ImporterState.None: - break; - case ImporterState.WritingPackToDisk: - ImGui.Text( "Writing modpack to disk before extracting..." ); - break; - case ImporterState.ExtractingModFiles: - { - var str = - $"{_texToolsImport.CurrentModPack} - {_texToolsImport.CurrentProgress} of {_texToolsImport.TotalProgress} files"; - - ImGui.ProgressBar( _texToolsImport.Progress, new Vector2( -1, 0 ), str ); - break; - } - case ImporterState.Done: - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - } - - ImGui.EndTabItem(); - } - - [Conditional( "DEBUG" )] - void DrawModBrowser() - { - var ret = ImGui.BeginTabItem( "Available Mods" ); - if( !ret ) - { - return; - } - - ImGui.Text( "woah" ); - - ImGui.EndTabItem(); - } - - void DrawSettingsTab() - { - var ret = ImGui.BeginTabItem( "Settings" ); - if( !ret ) - { - return; - } - - bool dirty = false; - - // FUCKKKKK - var basePath = _plugin.Configuration.CurrentCollection; - if( ImGui.InputText( "Root Folder", ref basePath, 255 ) ) - { - _plugin.Configuration.CurrentCollection = basePath; - dirty = true; - } - - if( ImGui.Button( "Rediscover Mods" ) ) - { - ReloadMods(); - _selectedModIndex = 0; - _selectedMod = null; - } - - ImGui.SameLine(); - - if( ImGui.Button( "Open Mods Folder" ) ) - { - Process.Start( _plugin.Configuration.CurrentCollection ); - } - - ImGui.SetCursorPosY( ImGui.GetCursorPosY() + 20 ); - - var invertOrder = _plugin.Configuration.InvertModListOrder; - if( ImGui.Checkbox( "Invert mod load order (mods are loaded bottom up)", ref invertOrder ) ) - { - _plugin.Configuration.InvertModListOrder = invertOrder; - dirty = true; - - ReloadMods(); - } - - ImGui.SetCursorPosY( ImGui.GetCursorPosY() + 20 ); - - var showAdvanced = _plugin.Configuration.ShowAdvanced; - if( ImGui.Checkbox( "Show Advanced Settings", ref showAdvanced ) ) - { - _plugin.Configuration.ShowAdvanced = showAdvanced; - dirty = true; - } - - if( _plugin.Configuration.ShowAdvanced ) - { - if( _plugin.ResourceLoader != null ) - { - ImGui.Checkbox( "Log all loaded files", ref _plugin.ResourceLoader.LogAllFiles ); - } - - var fswatch = _plugin.Configuration.DisableFileSystemNotifications; - if( ImGui.Checkbox( "Disable filesystem change notifications", ref fswatch ) ) - { - _plugin.Configuration.DisableFileSystemNotifications = fswatch; - dirty = true; - } - - var http = _plugin.Configuration.EnableHttpApi; - if( ImGui.Checkbox( "Enable HTTP API", ref http ) ) - { - if( http ) - { - _plugin.CreateWebServer(); - } - else - { - _plugin.ShutdownWebServer(); - } - - _plugin.Configuration.EnableHttpApi = http; - dirty = true; - } - - if( ImGui.Button( "Reload Player Resource" ) ) - { - _plugin.GameUtils.ReloadPlayerResources(); - } - } - - if( dirty ) - { - _plugin.Configuration.Save(); - } - - ImGui.EndTabItem(); - } - - void DrawModsSelector() - { - // Selector pane - ImGui.BeginGroup(); - ImGui.PushStyleVar( ImGuiStyleVar.ItemSpacing, new Vector2( 0, 0 ) ); - - // Inlay selector list - ImGui.BeginChild( "availableModList", new Vector2( 240, -ImGui.GetFrameHeightWithSpacing() ), true ); - - if( _plugin.ModManager.Mods != null ) - { - for( var modIndex = 0; modIndex < _plugin.ModManager.Mods.ModSettings.Count; modIndex++ ) - { - var settings = _plugin.ModManager.Mods.ModSettings[ modIndex ]; - - var changedColour = false; - if( !settings.Enabled ) - { - ImGui.PushStyleColor( ImGuiCol.Text, 0xFF666666 ); - changedColour = true; - } - else if( settings.Mod.FileConflicts.Any() ) - { - ImGui.PushStyleColor( ImGuiCol.Text, 0xFFAAAAFF ); - changedColour = true; - } - -#if DEBUG - var selected = ImGui.Selectable( - $"id={modIndex} {settings.Mod.Meta.Name}", - modIndex == _selectedModIndex - ); -#else - var selected = ImGui.Selectable( settings.Mod.Meta.Name, modIndex == _selectedModIndex ); -#endif - - if( changedColour ) - { - ImGui.PopStyleColor(); - } - - if( selected ) - { - _selectedModIndex = modIndex; - _selectedMod = settings; - } - } - } - - ImGui.EndChild(); - - // Selector controls - ImGui.PushStyleVar( ImGuiStyleVar.WindowPadding, new Vector2( 0, 0 ) ); - ImGui.PushStyleVar( ImGuiStyleVar.FrameRounding, 0 ); - ImGui.PushFont( UiBuilder.IconFont ); - if( _selectedModIndex != 0 ) - { - if( ImGui.Button( FontAwesomeIcon.ArrowUp.ToIconString(), new Vector2( 60, 0 ) ) ) - { - _plugin.ModManager.ChangeModPriority( _selectedMod ); - _selectedModIndex -= 1; - } - } - else - { - ImGui.PushStyleVar( ImGuiStyleVar.Alpha, 0.5f ); - ImGui.Button( FontAwesomeIcon.ArrowUp.ToIconString(), new Vector2( 60, 0 ) ); - ImGui.PopStyleVar(); - } - - ImGui.PopFont(); - - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( - _plugin.Configuration.InvertModListOrder - ? "Move the selected mod down in priority" - : "Move the selected mod up in priority" - ); - } - - ImGui.PushFont( UiBuilder.IconFont ); - - ImGui.SameLine(); - - if( _selectedModIndex != _plugin.ModManager.Mods?.ModSettings.Count - 1 ) - { - if( ImGui.Button( FontAwesomeIcon.ArrowDown.ToIconString(), new Vector2( 60, 0 ) ) ) - { - _plugin.ModManager.ChangeModPriority( _selectedMod, true ); - _selectedModIndex += 1; - } - } - else - { - ImGui.PushStyleVar( ImGuiStyleVar.Alpha, 0.5f ); - ImGui.Button( FontAwesomeIcon.ArrowDown.ToIconString(), new Vector2( 60, 0 ) ); - ImGui.PopStyleVar(); - } - - - ImGui.PopFont(); - - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( - _plugin.Configuration.InvertModListOrder - ? "Move the selected mod up in priority" - : "Move the selected mod down in priority" - ); - } - - ImGui.PushFont( UiBuilder.IconFont ); - - ImGui.SameLine(); - - if( ImGui.Button( FontAwesomeIcon.Trash.ToIconString(), new Vector2( 60, 0 ) ) ) - { - _selectedModDeleteIndex = _selectedModIndex; - } - - ImGui.PopFont(); - - if( ImGui.IsItemHovered() ) - ImGui.SetTooltip( "Delete the selected mod" ); - - ImGui.PushFont( UiBuilder.IconFont ); - - ImGui.SameLine(); - - if( ImGui.Button( FontAwesomeIcon.Plus.ToIconString(), new Vector2( 60, 0 ) ) ) - { - } - - ImGui.PopFont(); - - if( ImGui.IsItemHovered() ) - ImGui.SetTooltip( "Add an empty mod" ); - - ImGui.PopStyleVar( 3 ); - - ImGui.EndGroup(); - } - - void DrawDeleteModal() - { - if( _selectedModDeleteIndex != null ) - ImGui.OpenPopup( DialogDeleteMod ); - - var ret = ImGui.BeginPopupModal( DialogDeleteMod ); - if( !ret ) - { - return; - } - - if( _selectedMod?.Mod == null ) - { - ImGui.CloseCurrentPopup(); - ImGui.EndPopup(); - } - - ImGui.Text( "Are you sure you want to delete the following mod:" ); - // todo: why the fuck does this become null?????? - ImGui.Text( _selectedMod?.Mod?.Meta?.Name ); - - if( ImGui.Button( "Yes, delete it" ) ) - { - ImGui.CloseCurrentPopup(); - _plugin.ModManager.DeleteMod( _selectedMod.Mod ); - _selectedMod = null; - _selectedModIndex = 0; - _selectedModDeleteIndex = null; - } - - ImGui.SameLine(); - - if( ImGui.Button( "No, keep it" ) ) - { - ImGui.CloseCurrentPopup(); - _selectedModDeleteIndex = null; - } - - ImGui.EndPopup(); - } - - // Website button with On-Hover address if valid http(s), otherwise text. - private void DrawWebsiteText() - { - if( ( _selectedMod.Mod.Meta.Website?.Length ?? 0 ) <= 0 ) - { - return; - } - - var validUrl = Uri.TryCreate( _selectedMod.Mod.Meta.Website, UriKind.Absolute, out Uri uriResult ) - && ( uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp ); - ImGui.SameLine(); - if( validUrl ) - { - if( ImGui.SmallButton( "Open Website" ) ) - { - Process.Start( _selectedMod.Mod.Meta.Website ); - } - - if( ImGui.IsItemHovered() ) - ImGui.SetTooltip( _selectedMod.Mod.Meta.Website ); - } - else - { - ImGui.TextColored( new Vector4( 1f, 1f, 1f, 0.66f ), "from" ); - ImGui.SameLine(); - ImGui.Text( _selectedMod.Mod.Meta.Website ); - } - } - - // Create Mod-Handling buttons. - private void DrawEditButtons() - { - ImGui.SameLine(); - if( ImGui.Button( "Open Mod Folder" ) ) - { - Process.Start( _selectedMod.Mod.ModBasePath.FullName ); - } - if( ImGui.IsItemHovered() ) - ImGui.SetTooltip( "Open the directory containing this mod in your default file explorer." ); - - ImGui.SameLine(); - if( ImGui.Button( "Edit JSON" ) ) - { - var metaPath = Path.Combine( _selectedMod.Mod.ModBasePath.FullName, "meta.json" ); - File.WriteAllText( metaPath, JsonConvert.SerializeObject( _selectedMod.Mod.Meta, Formatting.Indented ) ); - Process.Start( metaPath ); - } - if( ImGui.IsItemHovered() ) - ImGui.SetTooltip( "Open the JSON configuration file in your default application for .json." ); - - ImGui.SameLine(); - if( ImGui.Button( "Reload JSON" ) ) - { - ReloadMods(); - } - if( ImGui.IsItemHovered() ) - ImGui.SetTooltip( "Reload the configuration of all mods." ); - - ImGui.SameLine(); - if( ImGui.Button( "Deduplicate" ) ) - { - new Deduplicator(_selectedMod.Mod.ModBasePath, _selectedMod.Mod.Meta).Run(); - var metaPath = Path.Combine( _selectedMod.Mod.ModBasePath.FullName, "meta.json" ); - File.WriteAllText( metaPath, JsonConvert.SerializeObject( _selectedMod.Mod.Meta, Formatting.Indented ) ); - ReloadMods(); - } - if( ImGui.IsItemHovered() ) - ImGui.SetTooltip( "Try to find identical files and remove duplicate occurences to reduce the mods disk size." ); - } - - private void DrawGroupSelectors() - { - //Spahetti code time - var mod = _plugin.SettingsInterface._selectedMod; - var conf = mod.Conf; - var settings = mod.Mod.Meta.Groups; - foreach( var g in settings ) - { - switch( g.Value.SelectionType ) - { - case SelectType.Multi: - { - var i = 0; - var flag = conf[g.Key]; - foreach( var opt in g.Value.Options ) - { - var enab = ( flag & 1 << i ) != 0; - if( ImGui.Checkbox( g.Value.GroupName + " - " + opt.OptionName, ref enab ) ) - { - flag = flag ^= 1 << i; - conf[g.Key] = flag; - _plugin.ModManager.Mods.Save(); - _plugin.ModManager.CalculateEffectiveFileList(); - } - i++; - } - break; - } - case SelectType.Single: - { - var code = conf[g.Key]; - if( g.Value.Options.Count > 1 ) - { - if( ImGui.Combo( g.Value.GroupName, ref code, g.Value.Options.Select( x => x.OptionName ).ToArray(), g.Value.Options.ToArray().Length ) ) - { - conf[g.Key] = code; - _plugin.ModManager.Mods.Save(); - _plugin.ModManager.CalculateEffectiveFileList(); - } - } - break; - } - } - } - } - - void DrawInstalledMods() - { - var ret = ImGui.BeginTabItem( "Installed Mods" ); - if( !ret ) - { - return; - } - - if( _plugin.ModManager.Mods == null ) - { - ImGui.Text( "You don't have any mods :(" ); - ImGui.SetCursorPosY( ImGui.GetCursorPosY() + 20 ); - ImGui.Text( "You'll need to install them first by creating a folder close to the root of your drive (preferably an SSD)." ); - ImGui.Text( "For example: D:/ffxiv/mods/" ); - ImGui.Text( "And pasting that path into the settings tab and clicking the 'Rediscover Mods' button." ); - ImGui.Text( "You can return to this tab once you've done that." ); - ImGui.EndTabItem(); - return; - } - - DrawModsSelector(); - - ImGui.SameLine(); - - if( _selectedMod != null ) - { - try - { - ImGui.BeginChild( "selectedModInfo", AutoFillSize, true ); - - ImGui.Text( _selectedMod.Mod.Meta.Name ); - - // (Version ...) or nothing. - if( ( _selectedMod.Mod.Meta.Version?.Length ?? 0 ) > 0 ) - { - ImGui.SameLine(); - ImGui.Text( $"(Version {_selectedMod.Mod.Meta.Version})" ); - } - - // by Author or Unknown. - ImGui.SameLine(); - ImGui.TextColored( new Vector4( 1f, 1f, 1f, 0.66f ), "by" ); - ImGui.SameLine(); - if( ( _selectedMod.Mod.Meta.Author?.Length ?? 0 ) > 0 ) - ImGui.Text( _selectedMod.Mod.Meta.Author ); - else - ImGui.Text( "Unknown" ); - - DrawWebsiteText(); - - ImGui.SetCursorPosY( ImGui.GetCursorPosY() + 10 ); - - var enabled = _selectedMod.Enabled; - if( ImGui.Checkbox( "Enabled", ref enabled ) ) - { - _selectedMod.Enabled = enabled; - _plugin.ModManager.Mods.Save(); - _plugin.ModManager.CalculateEffectiveFileList(); - } - - DrawEditButtons(); - - ImGui.BeginTabBar( "PenumbraPluginDetails" ); - - if( _selectedMod.Mod.Meta.Description?.Length > 0 && ImGui.BeginTabItem( "About" ) ) - { - ImGui.TextWrapped( _selectedMod.Mod.Meta.Description ); - ImGui.EndTabItem(); - } - - if( ( _selectedMod.Mod.Meta.ChangedItems?.Count ?? 0 ) > 0 ) - { - if( ImGui.BeginTabItem( "Changed Items" ) ) - { - ImGui.SetNextItemWidth( -1 ); - if( ImGui.ListBoxHeader( "###", AutoFillSize ) ) - foreach( var item in _selectedMod.Mod.Meta.ChangedItems ) - ImGui.Selectable( item ); - ImGui.ListBoxFooter(); - ImGui.EndTabItem(); - } - } - if(_selectedMod.Mod.Meta.HasGroupWithConfig) { - if(ImGui.BeginTabItem( "Configuration" )) - { - DrawGroupSelectors(); - ImGui.EndTabItem(); - } - } - if( ImGui.BeginTabItem( "Files" ) ) - { - ImGui.SetNextItemWidth( -1 ); - if( ImGui.ListBoxHeader( "##", AutoFillSize ) ) - foreach( var file in _selectedMod.Mod.ModFiles ) - ImGui.Selectable( file.FullName ); - - ImGui.ListBoxFooter(); - ImGui.EndTabItem(); - } - - if( _selectedMod.Mod.Meta.FileSwaps.Any() ) - { - if( ImGui.BeginTabItem( "File Swaps" ) ) - { - ImGui.SetNextItemWidth( -1 ); - if( ImGui.ListBoxHeader( "##", AutoFillSize ) ) - { - foreach( var file in _selectedMod.Mod.Meta.FileSwaps ) - { - // todo: fucking gross alloc every frame * items - ImGui.Selectable( $"{file.Key} -> {file.Value}" ); - } - } - - ImGui.ListBoxFooter(); - ImGui.EndTabItem(); - } - } - - if( _selectedMod.Mod.FileConflicts.Any() ) - { - if( ImGui.BeginTabItem( "File Conflicts" ) ) - { - ImGui.SetNextItemWidth( -1 ); - if( ImGui.ListBoxHeader( "##", AutoFillSize ) ) - { - foreach( var kv in _selectedMod.Mod.FileConflicts ) - { - var mod = kv.Key; - var files = kv.Value; - - if( ImGui.Selectable( mod ) ) - { - SelectModByName( mod ); - } - - ImGui.Indent( 15 ); - foreach( var file in files ) - { - ImGui.Selectable( file ); - } - - ImGui.Unindent( 15 ); - } - } - - ImGui.ListBoxFooter(); - ImGui.EndTabItem(); - } - } - - ImGui.EndTabBar(); - ImGui.EndChild(); - } - catch( Exception ex ) - { - PluginLog.LogError( ex, "fuck" ); - } - } - - ImGui.EndTabItem(); - } - - void SelectModByName( string name ) - { - for( var modIndex = 0; modIndex < _plugin.ModManager.Mods.ModSettings.Count; modIndex++ ) - { - var mod = _plugin.ModManager.Mods.ModSettings[ modIndex ]; - - if( mod.Mod.Meta.Name != name ) - { - continue; - } - - _selectedMod = mod; - _selectedModIndex = modIndex; - return; - } - } - - void DrawEffectiveFileList() - { - var ret = ImGui.BeginTabItem( "Effective File List" ); - if( !ret ) - { - return; - } - - if( ImGui.ListBoxHeader( "##", AutoFillSize ) ) - { - // todo: virtualise this - foreach( var file in _plugin.ModManager.ResolvedFiles ) - { - ImGui.Selectable( file.Value.FullName + " -> " + file.Key ); - } - } - - ImGui.ListBoxFooter(); - - ImGui.EndTabItem(); + _menuBar.Draw(); + _launchButton.Draw(); + _menu.Draw(); } private void ReloadMods() - { - _selectedMod = null; - + { + _menu._installedTab._selector.ClearSelection(); // create the directory if it doesn't exist Directory.CreateDirectory( _plugin.Configuration.CurrentCollection ); _plugin.ModManager.DiscoverMods( _plugin.Configuration.CurrentCollection ); - - // May select a different mod than before if mods were added or deleted, but will not crash. - if( _selectedModIndex < _plugin.ModManager.Mods.ModSettings.Count ) - { - _selectedMod = _plugin.ModManager.Mods.ModSettings[ _selectedModIndex ]; - } - else - { - _selectedModIndex = 0; - _selectedMod = null; - } - } + _menu._effectiveTab.RebuildFileList(_plugin.Configuration.ShowAdvanced); + } } } \ No newline at end of file diff --git a/Penumbra/UI/SettingsMenu.cs b/Penumbra/UI/SettingsMenu.cs new file mode 100644 index 00000000..6e28097b --- /dev/null +++ b/Penumbra/UI/SettingsMenu.cs @@ -0,0 +1,72 @@ +using System.Numerics; +using ImGuiNET; + +namespace Penumbra.UI +{ + public partial class SettingsInterface + { + private partial class SettingsMenu + { + private const string PenumbraSettingsLabel = "PenumbraSettings"; + + private static readonly Vector2 MinSettingsSize = new( 800, 450 ); + private static readonly Vector2 MaxSettingsSize = new( 69420, 42069 ); + + private readonly SettingsInterface _base; + public readonly TabSettings _settingsTab; + public readonly TabImport _importTab; + public readonly TabBrowser _browserTab; + public readonly TabInstalled _installedTab; + public readonly TabEffective _effectiveTab; + + public SettingsMenu(SettingsInterface ui) + { + _base = ui; + _settingsTab = new(_base); + _importTab = new(_base); + _browserTab = new(); + _installedTab = new(_base); + _effectiveTab = new(_base); + } + +#if DEBUG + private const bool DefaultVisibility = true; +#else + private const bool DefaultVisibility = false; +#endif + public bool Visible = DefaultVisibility; + + public void Draw() + { + if( !Visible ) + return; + + ImGui.SetNextWindowSizeConstraints( MinSettingsSize, MaxSettingsSize ); +#if DEBUG + var ret = ImGui.Begin( _base._plugin.PluginDebugTitleStr, ref Visible ); +#else + var ret = ImGui.Begin( _base._plugin.Name, ref Visible ); +#endif + if( !ret ) + return; + + ImGui.BeginTabBar( PenumbraSettingsLabel ); + + _settingsTab.Draw(); + _importTab.Draw(); + + if( !_importTab.IsImporting() ) + { + _browserTab.Draw(); + _installedTab.Draw(); + + if( _base._plugin.Configuration.ShowAdvanced ) + _effectiveTab.Draw(); + } + + ImGui.EndTabBar(); + ImGui.End(); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/TabBrowser.cs b/Penumbra/UI/TabBrowser.cs new file mode 100644 index 00000000..8201d17a --- /dev/null +++ b/Penumbra/UI/TabBrowser.cs @@ -0,0 +1,22 @@ +using System.Diagnostics; +using ImGuiNET; + +namespace Penumbra.UI +{ + public partial class SettingsInterface + { + private class TabBrowser + { + [Conditional( "DEBUG" )] + public void Draw() + { + var ret = ImGui.BeginTabItem( "Available Mods" ); + if( !ret ) + return; + + ImGui.Text( "woah" ); + ImGui.EndTabItem(); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/TabEffective.cs b/Penumbra/UI/TabEffective.cs new file mode 100644 index 00000000..90de944e --- /dev/null +++ b/Penumbra/UI/TabEffective.cs @@ -0,0 +1,66 @@ +using System.Linq; +using ImGuiNET; +using Penumbra.Mods; + +namespace Penumbra.UI +{ + public partial class SettingsInterface + { + private class TabEffective + { + private const string LabelTab = "Effective File List"; + private const float TextSizePadding = 5f; + + private ModManager _mods; + private (string, string)[] _fileList = null; + private float _maxGamePath = 0f; + + public TabEffective(SettingsInterface ui) + { + _mods = ui._plugin.ModManager; + RebuildFileList(ui._plugin.Configuration.ShowAdvanced); + } + + public void RebuildFileList(bool advanced) + { + if (advanced) + { + _fileList = _mods.ResolvedFiles.Select( P => (P.Value.FullName, P.Key) ).ToArray(); + _maxGamePath = ((_fileList.Length > 0) ? _fileList.Max( P => ImGui.CalcTextSize(P.Item2).X ) : 0f) + TextSizePadding; + } + else + { + _fileList = null; + _maxGamePath = 0f; + } + } + + private void DrawFileLine((string, string) file) + { + ImGui.Selectable(file.Item2); + ImGui.SameLine(); + ImGui.SetCursorPosX(_maxGamePath); + ImGui.TextUnformatted(" <-- "); + ImGui.SameLine(); + ImGui.Selectable(file.Item1); + } + + public void Draw() + { + var ret = ImGui.BeginTabItem( LabelTab ); + if( !ret ) + return; + + if( ImGui.ListBoxHeader( "##effective_files", AutoFillSize ) ) + { + foreach( var file in _fileList ) + DrawFileLine(file); + + ImGui.ListBoxFooter(); + } + + ImGui.EndTabItem(); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/TabImport.cs b/Penumbra/UI/TabImport.cs new file mode 100644 index 00000000..9254146f --- /dev/null +++ b/Penumbra/UI/TabImport.cs @@ -0,0 +1,126 @@ +using ImGuiNET; +using System.Threading.Tasks; +using System.Windows.Forms; +using System.IO; +using System; +using Penumbra.Importer; +using Dalamud.Plugin; +using System.Numerics; + +namespace Penumbra.UI +{ + public partial class SettingsInterface + { + private class TabImport + { + private const string LabelTab = "Import Mods"; + private const string LabelImportButton = "Import TexTools Modpacks"; + private const string FileTypeFilter = "TexTools TTMP Modpack (*.ttmp2)|*.ttmp*|All files (*.*)|*.*"; + private const string LabelFileDialog = "Pick one or more modpacks."; + private const string LabelFileImportRunning = "Import in progress..."; + private const string TooltipModpack1 = "Writing modpack to disk before extracting..."; + + private static readonly Vector2 ImportBarSize = new( -1, 0 ); + + private bool _isImportRunning = false; + private TexToolsImport _texToolsImport = null!; + private readonly SettingsInterface _base; + + public TabImport(SettingsInterface ui) => _base = ui; + + public bool IsImporting() => _isImportRunning; + + private void RunImportTask() + { + _isImportRunning = true; + Task.Run( async () => + { + var picker = new OpenFileDialog + { + Multiselect = true, + Filter = FileTypeFilter, + CheckFileExists = true, + Title = LabelFileDialog + }; + + var result = await picker.ShowDialogAsync(); + + if( result == DialogResult.OK ) + { + foreach( var fileName in picker.FileNames ) + { + PluginLog.Log( $"-> {fileName} START"); + + try + { + _texToolsImport = new TexToolsImport( new DirectoryInfo( _base._plugin.Configuration.CurrentCollection ) ); + _texToolsImport.ImportModPack( new FileInfo( fileName ) ); + } + catch( Exception ex ) + { + PluginLog.LogError( ex, "Could not import one or more modpacks." ); + } + + PluginLog.Log( $"-> {fileName} OK!"); + } + + _texToolsImport = null; + _base.ReloadMods(); + } + _isImportRunning = false; + } ); + } + + private void DrawImportButton() + { + if( ImGui.Button( LabelImportButton ) ) + { + RunImportTask(); + } + } + + private void DrawImportProgress() + { + ImGui.Button( LabelFileImportRunning ); + + if( _texToolsImport != null ) + { + switch( _texToolsImport.State ) + { + case ImporterState.None: + break; + case ImporterState.WritingPackToDisk: + ImGui.Text( TooltipModpack1 ); + break; + case ImporterState.ExtractingModFiles: + { + var str = + $"{_texToolsImport.CurrentModPack} - {_texToolsImport.CurrentProgress} of {_texToolsImport.TotalProgress} files"; + + ImGui.ProgressBar( _texToolsImport.Progress, ImportBarSize, str ); + break; + } + case ImporterState.Done: + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + public void Draw() + { + var ret = ImGui.BeginTabItem( LabelTab ); + if( !ret ) + return; + + if( !_isImportRunning ) + DrawImportButton(); + else + DrawImportProgress(); + + ImGui.EndTabItem(); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/TabInstalled.cs b/Penumbra/UI/TabInstalled.cs new file mode 100644 index 00000000..18831069 --- /dev/null +++ b/Penumbra/UI/TabInstalled.cs @@ -0,0 +1,53 @@ +using ImGuiNET; + +namespace Penumbra.UI +{ + public partial class SettingsInterface + { + private partial class TabInstalled + { + private const string LabelTab = "Installed Mods"; + + private readonly SettingsInterface _base; + public readonly Selector _selector; + public readonly ModPanel _modPanel; + + public TabInstalled(SettingsInterface ui) + { + _base = ui; + _selector = new(_base); + _modPanel = new(_base, _selector); + } + + private void DrawNoModsAvailable() + { + ImGui.Text( "You don't have any mods :(" ); + ImGuiCustom.VerticalDistance(20f); + ImGui.Text( "You'll need to install them first by creating a folder close to the root of your drive (preferably an SSD)." ); + ImGui.Text( "For example: D:/ffxiv/mods/" ); + ImGui.Text( "And pasting that path into the settings tab and clicking the 'Rediscover Mods' button." ); + ImGui.Text( "You can return to this tab once you've done that." ); + } + + public void Draw() + { + var ret = ImGui.BeginTabItem( LabelTab ); + if( !ret ) + return; + + if (_base._plugin.ModManager.Mods != null) + { + _selector.Draw(); + ImGui.SameLine(); + _modPanel.Draw(); + } + else + DrawNoModsAvailable(); + + ImGui.EndTabItem(); + + return; + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/TabInstalledDetails.cs b/Penumbra/UI/TabInstalledDetails.cs new file mode 100644 index 00000000..3160e1d9 --- /dev/null +++ b/Penumbra/UI/TabInstalledDetails.cs @@ -0,0 +1,806 @@ +using Penumbra.Models; +using ImGuiNET; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Penumbra.UI +{ + internal static class Extension + { + // Remove the entry at idx from the list if the new string is empty, otherwise replace it. + public static void RemoveOrChange(this List list, string newString, int idx) + { + if (newString?.Length == 0) + list.RemoveAt(idx); + else + list[idx] = newString; + } + } + + public partial class SettingsInterface + { + private class PluginDetails + { + #region ========== Literals =============== + private const string LabelPluginDetails = "PenumbraPluginDetails"; + private const string LabelAboutTab = "About"; + private const string TooltipAboutEdit = "Use Ctrl+Enter for newlines."; + private const string LabelDescEdit = "##descedit"; + private const string LabelChangedItemsTab = "Changed Items"; + private const string LabelChangedItemsHeader = "##changedItems"; + private const string LabelChangedItemIdx = "##citem_"; + private const string LabelChangedItemNew = "##citem_new"; + private const string LabelConflictsTab = "File Conflicts"; + private const string LabelConflictsHeader = "##conflicts"; + private const string LabelFileSwapTab = "File Swaps"; + private const string LabelFileSwapHeader = "##fileSwaps"; + private const string LabelFileListTab = "Files"; + private const string LabelFileListHeader = "##fileList"; + private const string TooltipFilesTab = "Green files replace their standard game path counterpart (not in any option) or are in all options of a Single-Select option.\nYellow files are restricted to some options."; + private const string ButtonAddToGroup = "Add to Group"; + private const string ButtonRemoveFromGroup = "Remove from Group"; + private const string LabelGroupSelect = "##groupSelect"; + private const string LabelOptionSelect = "##optionSelect"; + private const string TextNoOptionAvailable = "[No Option Available]"; + private const string LabelConfigurationTab = "Configuration"; + private const string LabelNewSingleGroup = "New Single Group"; + private const string LabelNewSingleGroupEdit = "##newSingleGroup"; + private const string LabelNewMultiGroup = "New Multi Group"; + private const string TextDefaultGamePath = "default"; + private const string LabelGamePathsEdit = "Game Paths"; + private const string LabelGamePathsEditBox = "##gamePathsEdit"; + private const string TooltipGamePathText = "Click to copy to clipboard."; + private static readonly string TooltipGamePathsEdit = $"Enter all game paths to add or remove, separated by '{GamePathsSeparator}'.\nUse '{TextDefaultGamePath}' to add the original file path."; + private static readonly string TooltipFilesTabEdit = $"{TooltipFilesTab}\nRed Files are replaced in another group or a different option in this group, but not contained in the current option."; + + private const char GamePathsSeparator = ';'; + private const float TextSizePadding = 5f; + private const float OptionSelectionWidth = 140f; + private const float CheckMarkSize = 50f; + private const float MultiEditBoxWidth = 300f; + private const uint ColorGreen = 0xFF00C800; + private const uint ColorYellow = 0xFF00C8C8; + private const uint ColorRed = 0xFF0000C8; + #endregion + + #region ========== State ================== + private bool _editMode = false; + private int _selectedGroupIndex = 0; + private InstallerInfo? _selectedGroup = null; + private int _selectedOptionIndex = 0; + private Option? _selectedOption = null; + private (string label, string name)[] _changedItemsList = null; + private float? _fileSwapOffset = null; + private string _currentGamePaths = ""; + + private (string name, bool selected, uint color, string relName)[] _fullFilenameList = null; + + public void SelectGroup(int idx) + { + _selectedGroupIndex = idx; + if (_selectedGroupIndex >= Meta?.Groups?.Count) + _selectedGroupIndex = 0; + if (Meta?.Groups?.Count > 0) + _selectedGroup = Meta.Groups.ElementAt(_selectedGroupIndex).Value; + else + _selectedGroup = null; + } + public void SelectGroup() => SelectGroup(_selectedGroupIndex); + + public void SelectOption(int idx) + { + _selectedOptionIndex = idx; + if (_selectedOptionIndex >= _selectedGroup?.Options.Count) + _selectedOptionIndex = 0; + if (_selectedGroup?.Options.Count > 0) + _selectedOption = ((InstallerInfo) _selectedGroup).Options[_selectedOptionIndex]; + else + _selectedOption = null; + } + public void SelectOption() => SelectOption(_selectedOptionIndex); + + public void ResetState() + { + _changedItemsList = null; + _fileSwapOffset = null; + _fullFilenameList = null; + SelectGroup(); + SelectOption(); + } + + + private readonly Selector _selector; + private readonly SettingsInterface _base; + public PluginDetails(SettingsInterface ui, Selector s) + { + _base = ui; + _selector = s; + ResetState(); + } + + private ModInfo Mod { get{ return _selector.Mod(); } } + private ModMeta Meta { get{ return Mod?.Mod?.Meta; } } + + private void Save() + { + _base._plugin.ModManager.Mods.Save(); + _base._plugin.ModManager.CalculateEffectiveFileList(); + _base._menu._effectiveTab.RebuildFileList(_base._plugin.Configuration.ShowAdvanced); + } + + #endregion + + #region ========== Tabs =================== + private void DrawAboutTab() + { + if (!_editMode && Meta.Description?.Length == 0) + return; + + if(ImGui.BeginTabItem( LabelAboutTab ) ) + { + var desc = Meta.Description; + var flags = _editMode + ? ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CtrlEnterForNewLine + : ImGuiInputTextFlags.ReadOnly; + + if (ImGui.InputTextMultiline(LabelDescEdit, ref desc, 1 << 16, AutoFillSize, flags)) + { + Meta.Description = desc; + _selector.SaveCurrentMod(); + } + if (_editMode && ImGui.IsItemHovered()) + ImGui.SetTooltip( TooltipAboutEdit ); + + + ImGui.EndTabItem(); + } + } + + private void DrawChangedItemsTab() + { + if (!_editMode && Meta.ChangedItems?.Count == 0) + return; + + var flags = _editMode + ? ImGuiInputTextFlags.EnterReturnsTrue + : ImGuiInputTextFlags.ReadOnly; + + if( ImGui.BeginTabItem( LabelChangedItemsTab ) ) + { + ImGui.SetNextItemWidth( -1 ); + if( ImGui.ListBoxHeader( LabelChangedItemsHeader, AutoFillSize ) ) + { + if (_changedItemsList == null) + _changedItemsList = Meta.ChangedItems.Select( (I, index) => ($"{LabelChangedItemIdx}{index}", I) ).ToArray(); + for (var i = 0; i < Meta.ChangedItems.Count; ++i) + { + ImGui.SetNextItemWidth(-1); + if ( ImGui.InputText(_changedItemsList[i].label, ref _changedItemsList[i].name, 128, flags) ) + { + Meta.ChangedItems.RemoveOrChange(_changedItemsList[i].name, i); + _selector.SaveCurrentMod(); + } + } + var newItem = ""; + if ( _editMode ) + { + ImGui.SetNextItemWidth(-1); + if ( ImGui.InputText( LabelChangedItemNew, ref newItem, 128, flags) ) + { + if (newItem.Length > 0) + { + if (Meta.ChangedItems == null) + Meta.ChangedItems = new(){ newItem }; + else + Meta.ChangedItems.Add(newItem); + _selector.SaveCurrentMod(); + } + } + } + ImGui.ListBoxFooter(); + } + ImGui.EndTabItem(); + } + else + _changedItemsList = null; + } + + private void DrawConflictTab() + { + if( Mod.Mod.FileConflicts.Any() ) + { + if( ImGui.BeginTabItem( LabelConflictsTab ) ) + { + ImGui.SetNextItemWidth( -1 ); + if( ImGui.ListBoxHeader( LabelConflictsHeader, AutoFillSize ) ) + { + foreach( var kv in Mod.Mod.FileConflicts ) + { + var mod = kv.Key; + if( ImGui.Selectable( mod ) ) + _selector.SelectModByName( mod ); + + ImGui.Indent( 15 ); + foreach( var file in kv.Value ) + ImGui.Selectable( file ); + ImGui.Unindent( 15 ); + } + ImGui.ListBoxFooter(); + } + + ImGui.EndTabItem(); + } + } + } + + private void DrawFileSwapTab() + { + if( Meta.FileSwaps.Any() ) + { + if( ImGui.BeginTabItem( LabelFileSwapTab ) ) + { + if (_fileSwapOffset == null) + _fileSwapOffset = Meta.FileSwaps.Max( P => ImGui.CalcTextSize(P.Key).X) + TextSizePadding; + ImGui.SetNextItemWidth( -1 ); + if( ImGui.ListBoxHeader( LabelFileSwapHeader, AutoFillSize ) ) + { + foreach( var file in Meta.FileSwaps ) + { + ImGui.Selectable(file.Key); + ImGui.SameLine(_fileSwapOffset ?? 0); + ImGui.TextUnformatted(" -> "); + ImGui.Selectable(file.Value); + } + ImGui.ListBoxFooter(); + } + ImGui.EndTabItem(); + } + else + _fileSwapOffset = null; + } + } + #endregion + + #region ========== FileList =============== + private void UpdateFilenameList() + { + if (_fullFilenameList == null) + { + var len = Mod.Mod.ModBasePath.FullName.Length; + _fullFilenameList = Mod.Mod.ModFiles.Select( F => (F.FullName, false, ColorGreen, "") ).ToArray(); + + if(Meta.Groups?.Count == 0) + return; + + for (var i = 0; i < Mod.Mod.ModFiles.Count; ++i) + { + _fullFilenameList[i].relName = _fullFilenameList[i].name.Substring(len).TrimStart('\\'); + foreach (var Group in Meta.Groups.Values) + { + var inAll = true; + foreach (var Option in Group.Options) + { + if (Option.OptionFiles.ContainsKey(_fullFilenameList[i].relName)) + _fullFilenameList[i].color = ColorYellow; + else + inAll = false; + } + if (inAll && Group.SelectionType == SelectType.Single) + _fullFilenameList[i].color = ColorGreen; + } + } + } + } + + private void DrawFileListTab() + { + if( ImGui.BeginTabItem( LabelFileListTab ) ) + { + if (ImGui.IsItemHovered()) + ImGui.SetTooltip( TooltipFilesTab ); + + ImGui.SetNextItemWidth( -1 ); + if( ImGui.ListBoxHeader( LabelFileListHeader, AutoFillSize ) ) + { + UpdateFilenameList(); + foreach(var file in _fullFilenameList) + { + ImGui.PushStyleColor(ImGuiCol.Text, file.color); + ImGui.Selectable(file.name); + ImGui.PopStyleColor(); + } + ImGui.ListBoxFooter(); + } + else + _fullFilenameList = null; + ImGui.EndTabItem(); + } + } + + private void HandleSelectedFilesButton(bool remove) + { + if (_selectedOption == null) + return; + var option = (Option) _selectedOption; + + var gamePaths = _currentGamePaths.Split(';'); + if (gamePaths.Length == 0 || gamePaths[0].Length == 0) + return; + + int? defaultIndex = null; + for (var i = 0; i < gamePaths.Length; ++i) + { + if (gamePaths[i] == TextDefaultGamePath ) + { + defaultIndex = i; + break; + } + } + + var baseLength = Mod.Mod.ModBasePath.FullName.Length; + var changed = false; + for (var i = 0; i < Mod.Mod.ModFiles.Count; ++i) + { + if (!_fullFilenameList[i].selected) + continue; + + var fileName = _fullFilenameList[i].relName; + if (defaultIndex != null) + gamePaths[(int)defaultIndex] = fileName.Replace('\\', '/'); + + if (remove && option.OptionFiles.TryGetValue(fileName, out var setPaths)) + { + if (setPaths.RemoveWhere( P => gamePaths.Contains(P)) > 0) + changed = true; + if (setPaths.Count == 0 && option.OptionFiles.Remove(fileName)) + changed = true; + } + else + { + foreach(var gamePath in gamePaths) + changed |= option.AddFile(fileName, gamePath); + } + } + if (changed) + _selector.SaveCurrentMod(); + } + + private void DrawAddToGroupButton() + { + if (ImGui.Button( ButtonAddToGroup ) ) + HandleSelectedFilesButton(false); + } + + private void DrawRemoveFromGroupButton() + { + if (ImGui.Button( ButtonRemoveFromGroup ) ) + HandleSelectedFilesButton(true); + } + + private void DrawEditGroupSelector() + { + ImGui.SetNextItemWidth( OptionSelectionWidth ); + if (Meta.Groups.Count == 0) + { + ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex, TextNoOptionAvailable, 1); + } + else + { + if (ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex, Meta.Groups.Values.Select( G => G.GroupName ).ToArray(), Meta.Groups.Count)) + { + SelectGroup(); + SelectOption(0); + } + } + } + + private void DrawEditOptionSelector() + { + ImGui.SameLine(); + ImGui.SetNextItemWidth( OptionSelectionWidth ); + if (_selectedGroup?.Options.Count == 0) + { + ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, TextNoOptionAvailable, 1); + return; + } + + var group = (InstallerInfo) _selectedGroup; + if (ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, group.Options.Select(O => O.OptionName).ToArray(), group.Options.Count)) + SelectOption(); + } + + private void DrawGamePathInput() + { + ImGui.TextUnformatted( LabelGamePathsEdit ); + ImGui.SameLine(); + ImGui.SetNextItemWidth(-1); + ImGui.InputText(LabelGamePathsEditBox, ref _currentGamePaths, 128); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(TooltipGamePathsEdit); + } + + private void DrawGroupRow() + { + if (_selectedGroup == null) + SelectGroup(); + if (_selectedOption == null) + SelectOption(); + + DrawEditGroupSelector(); + ImGui.SameLine(); + DrawEditOptionSelector(); + ImGui.SameLine(); + DrawAddToGroupButton(); + ImGui.SameLine(); + DrawRemoveFromGroupButton(); + ImGui.SameLine(); + DrawGamePathInput(); + } + + private void DrawFileAndGamePaths(int idx) + { + void Selectable(uint colorNormal, uint colorReplace) + { + var loc = _fullFilenameList[idx].color; + if (loc == colorNormal) + loc = colorReplace; + ImGui.PushStyleColor(ImGuiCol.Text, loc); + ImGui.Selectable( _fullFilenameList[idx].name, ref _fullFilenameList[idx].selected ); + ImGui.PopStyleColor(); + } + + const float indent = 30f; + if (_selectedOption == null) + { + Selectable(0, ColorGreen); + return; + } + + var fileName = _fullFilenameList[idx].relName; + if (((Option) _selectedOption).OptionFiles.TryGetValue(fileName, out var gamePaths)) + { + Selectable(0, ColorGreen); + + ImGui.Indent(indent); + foreach (var gamePath in gamePaths) + { + ImGui.Text(gamePath); + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText(gamePath); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip( TooltipGamePathText ); + } + ImGui.Unindent(indent); + } + else + Selectable(ColorYellow, ColorRed); + } + + private void DrawFileListTabEdit() + { + if( ImGui.BeginTabItem( LabelFileListTab ) ) + { + UpdateFilenameList(); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip( _editMode ? TooltipFilesTabEdit : TooltipFilesTab ); + + ImGui.SetNextItemWidth( -1 ); + if( ImGui.ListBoxHeader( LabelFileListHeader, AutoFillSize - new Vector2(0, 1.5f * ImGui.GetTextLineHeight()) ) ) + for(var i = 0; i < Mod.Mod.ModFiles.Count; ++i) + DrawFileAndGamePaths(i); + + ImGui.ListBoxFooter(); + + DrawGroupRow(); + ImGui.EndTabItem(); + } + else + _fullFilenameList = null; + } + #endregion + + #region ========== Configuration ========== + #region ========== MultiSelectorEdit ========== + private bool DrawMultiSelectorEditBegin(InstallerInfo group) + { + var groupName = group.GroupName; + if (ImGuiCustom.BeginFramedGroupEdit(ref groupName) + && groupName != group.GroupName && !Meta.Groups.ContainsKey(groupName)) + { + var oldConf = Mod.Conf[group.GroupName]; + Meta.Groups.Remove(group.GroupName); + Mod.Conf.Remove(group.GroupName); + if (groupName.Length > 0) + { + Meta.Groups[groupName] = new(){ GroupName = groupName, SelectionType = SelectType.Multi, Options = group.Options }; + Mod.Conf[groupName] = oldConf; + } + return true; + } + return false; + } + private void DrawMultiSelectorEditAdd(InstallerInfo group, float nameBoxStart) + { + var newOption = ""; + ImGui.SetCursorPosX(nameBoxStart); + ImGui.SetNextItemWidth(MultiEditBoxWidth); + if (ImGui.InputText($"##new_{group.GroupName}_l", ref newOption, 64, ImGuiInputTextFlags.EnterReturnsTrue)) + { + if (newOption.Length != 0) + { + group.Options.Add(new(){ OptionName = newOption, OptionDesc = "", OptionFiles = new() }); + _selector.SaveCurrentMod(); + } + } + } + + private void DrawMultiSelectorEdit(InstallerInfo group) + { + var nameBoxStart = CheckMarkSize; + var flag = Mod.Conf[group.GroupName]; + + var modChanged = DrawMultiSelectorEditBegin(group); + + for (var i = 0; i < group.Options.Count; ++i) + { + var opt = group.Options[i]; + var label = $"##{opt.OptionName}_{group.GroupName}"; + DrawMultiSelectorCheckBox(group, i, flag, label); + + ImGui.SameLine(); + var newName = opt.OptionName; + + if (nameBoxStart == CheckMarkSize) + nameBoxStart = ImGui.GetCursorPosX(); + + ImGui.SetNextItemWidth(MultiEditBoxWidth); + if (ImGui.InputText($"{label}_l", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue)) + { + if (newName.Length == 0) + { + group.Options.RemoveAt(i); + var bitmaskFront = (1 << i) - 1; + Mod.Conf[group.GroupName] = (flag & bitmaskFront) | ((flag & ~bitmaskFront) >> 1); + modChanged = true; + } + else if (newName != opt.OptionName) + { + group.Options[i] = new(){ OptionName = newName, OptionDesc = opt.OptionDesc, OptionFiles = opt.OptionFiles }; + _selector.SaveCurrentMod(); + } + } + } + + DrawMultiSelectorEditAdd(group, nameBoxStart); + + if (modChanged) + { + _selector.SaveCurrentMod(); + Save(); + } + + ImGuiCustom.EndFramedGroup(); + } + #endregion + + #region ========== SingleSelectorEdit ========== + private bool DrawSingleSelectorEditGroup(InstallerInfo group) + { + var groupName = group.GroupName; + if (ImGui.InputText($"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue) + && !Meta.Groups.ContainsKey(groupName)) + { + var oldConf = Mod.Conf[group.GroupName]; + if (groupName != group.GroupName) + { + Meta.Groups.Remove(group.GroupName); + Mod.Conf.Remove(group.GroupName); + } + if (groupName.Length > 0) + { + Meta.Groups.Add(groupName, new InstallerInfo(){ GroupName = groupName, Options = group.Options, SelectionType = SelectType.Single } ); + Mod.Conf[groupName] = oldConf; + } + return true; + } + return false; + } + + private float DrawSingleSelectorEdit(InstallerInfo group) + { + var code = Mod.Conf[group.GroupName]; + var selectionChanged = false; + var modChanged = false; + var newName = ""; + if (ImGuiCustom.RenameableCombo($"##{group.GroupName}", ref code, ref newName, group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count)) + { + if (code == group.Options.Count) + { + if (newName.Length > 0) + { + selectionChanged = true; + modChanged = true; + Mod.Conf[group.GroupName] = code; + group.Options.Add(new(){ OptionName = newName, OptionDesc = "", OptionFiles = new()}); + } + } + else + { + if (newName.Length == 0) + { + modChanged = true; + group.Options.RemoveAt(code); + if (code >= group.Options.Count) + code = 0; + } + else if (newName != group.Options[code].OptionName) + { + modChanged = true; + group.Options[code] = new Option(){ OptionName = newName, OptionDesc = group.Options[code].OptionDesc, OptionFiles = group.Options[code].OptionFiles}; + } + if (Mod.Conf[group.GroupName] != code) + { + selectionChanged = true; + Mod.Conf[group.GroupName] = code; + } + } + } + + ImGui.SameLine(); + var labelEditPos = ImGui.GetCursorPosX(); + modChanged |= DrawSingleSelectorEditGroup(group); + + if (modChanged) + _selector.SaveCurrentMod(); + + if (selectionChanged) + Save(); + + return labelEditPos; + } + #endregion + private void AddNewGroup(string newGroup, SelectType selectType) + { + if (!Meta.Groups.ContainsKey(newGroup) && newGroup.Length > 0) + { + Meta.Groups[newGroup] = new () + { + GroupName = newGroup, + SelectionType = selectType, + Options = new() + } ; + + Mod.Conf[newGroup] = 0; + _selector.SaveCurrentMod(); + Save(); + } + } + + private void DrawAddSingleGroupField(float labelEditPos) + { + var newGroup = ""; + if(labelEditPos == CheckMarkSize) + { + ImGui.SetCursorPosX(CheckMarkSize); + ImGui.SetNextItemWidth(MultiEditBoxWidth); + if (ImGui.InputText(LabelNewSingleGroup, ref newGroup, 64, ImGuiInputTextFlags.EnterReturnsTrue)) + AddNewGroup(newGroup, SelectType.Single); + } + else + { + ImGuiCustom.RightJustifiedLabel(labelEditPos, LabelNewSingleGroup ); + if (ImGui.InputText(LabelNewSingleGroupEdit, ref newGroup, 64, ImGuiInputTextFlags.EnterReturnsTrue)) + AddNewGroup(newGroup, SelectType.Single); + } + } + + private void DrawAddMultiGroupField() + { + var newGroup = ""; + ImGui.SetCursorPosX(CheckMarkSize); + ImGui.SetNextItemWidth(MultiEditBoxWidth); + if (ImGui.InputText(LabelNewMultiGroup, ref newGroup, 64, ImGuiInputTextFlags.EnterReturnsTrue)) + AddNewGroup(newGroup, SelectType.Multi); + } + + private void DrawGroupSelectorsEdit() + { + var labelEditPos = CheckMarkSize; + foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ) ) + labelEditPos = DrawSingleSelectorEdit(g); + DrawAddSingleGroupField(labelEditPos); + + foreach(var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi )) + DrawMultiSelectorEdit(g); + DrawAddMultiGroupField(); + } + + #region Non-Edit + + private void DrawMultiSelectorCheckBox(InstallerInfo group, int idx, int flag, string label) + { + var opt = group.Options[idx]; + var enabled = ( flag & (1 << idx)) != 0; + var oldEnabled = enabled; + if (ImGui.Checkbox(label, ref enabled)) + { + if (oldEnabled != enabled) + { + Mod.Conf[group.GroupName] ^= (1 << idx); + Save(); + } + } + } + + private void DrawMultiSelector(InstallerInfo group) + { + if (group.Options.Count == 0) + return; + + ImGuiCustom.BeginFramedGroup(group.GroupName); + for(var i = 0; i < group.Options.Count; ++i) + DrawMultiSelectorCheckBox(group, i, Mod.Conf[group.GroupName], $"{group.Options[i].OptionName}##{group.GroupName}"); + + ImGuiCustom.EndFramedGroup(); + } + + private void DrawSingleSelector(InstallerInfo group) + { + if (group.Options.Count < 2) + return; + var code = Mod.Conf[group.GroupName]; + if( ImGui.Combo( group.GroupName, ref code, group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) ) + { + Mod.Conf[group.GroupName] = code; + Save(); + } + } + + private void DrawGroupSelectors() + { + foreach(var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ) ) + DrawSingleSelector(g); + foreach(var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi )) + DrawMultiSelector(g); + return; + } + #endregion + + + private void DrawConfigurationTab() + { + if (!_editMode && !Meta.HasGroupWithConfig) + return; + + if(ImGui.BeginTabItem( LabelConfigurationTab ) ) + { + if (_editMode) + DrawGroupSelectorsEdit(); + else + DrawGroupSelectors(); + ImGui.EndTabItem(); + } + } + #endregion + + public void Draw(bool editMode) + { + _editMode = editMode; + ImGui.BeginTabBar( LabelPluginDetails ); + + DrawAboutTab(); + DrawChangedItemsTab(); + DrawConfigurationTab(); + if (_editMode) + DrawFileListTabEdit(); + else + DrawFileListTab(); + DrawFileSwapTab(); + DrawConflictTab(); + + ImGui.EndTabBar(); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/TabInstalledModPanel.cs b/Penumbra/UI/TabInstalledModPanel.cs new file mode 100644 index 00000000..253274ab --- /dev/null +++ b/Penumbra/UI/TabInstalledModPanel.cs @@ -0,0 +1,287 @@ +using ImGuiNET; +using Dalamud.Plugin; +using System; +using System.Numerics; +using System.Diagnostics; +using Penumbra.Models; + +namespace Penumbra.UI +{ + public partial class SettingsInterface + { + private class ModPanel + { + private const string LabelModPanel = "selectedModInfo"; + private const string LabelEditName = "##editName"; + private const string LabelEditVersion = "##editVersion"; + private const string LabelEditAuthor = "##editAuthor"; + private const string LabelEditWebsite = "##editWebsite"; + private const string ButtonOpenWebsite = "Open Website"; + private const string LabelModEnabled = "Enabled"; + private const string LabelEditingEnabled = "Enable Editing"; + private const string ButtonOpenModFolder = "Open Mod Folder"; + private const string TooltipOpenModFolder = "Open the directory containing this mod in your default file explorer."; + private const string ButtonEditJson = "Edit JSON"; + private const string TooltipEditJson = "Open the JSON configuration file in your default application for .json."; + private const string ButtonReloadJson = "Reload JSON"; + private const string TooltipReloadJson = "Reload the configuration of all mods."; + private const string ButtonDeduplicate = "Deduplicate"; + private const string TooltipDeduplicate = "Try to find identical files and remove duplicate occurences to reduce the mods disk size. Introduces an invisible single-option Group \"Duplicates\"."; + + private const float HeaderLineDistance = 10f; + private static readonly Vector4 GreyColor = new( 1f, 1f, 1f, 0.66f ); + + private readonly SettingsInterface _base; + private readonly Selector _selector; + public readonly PluginDetails _details; + + private bool _editMode = false; + private string _currentWebsite; + private bool _validWebsite; + + public ModPanel(SettingsInterface ui, Selector s) + { + _base = ui; + _selector = s; + _details = new(_base, _selector); + _currentWebsite = Meta?.Website; + } + + private ModInfo Mod { get{ return _selector.Mod(); } } + private ModMeta Meta { get{ return Mod?.Mod.Meta; } } + + #region Header Line Functions + private void DrawName() + { + var name = Meta.Name; + if (ImGuiCustom.InputOrText(_editMode, LabelEditName, ref name, 64) + && name.Length > 0 && name != Meta.Name) + { + Meta.Name = name; + _selector.SaveCurrentMod(); + } + } + + private void DrawVersion() + { + if (_editMode) + { + ImGui.BeginGroup(); + ImGui.Text("(Version "); + + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, ZeroVector); + ImGui.SameLine(); + var version = Meta.Version ?? ""; + if (ImGuiCustom.ResizingTextInput( LabelEditVersion, ref version, 16) + && version != Meta.Version) + { + Meta.Version = version.Length > 0 ? version : null; + _selector.SaveCurrentMod(); + } + + ImGui.SameLine(); + ImGui.Text(")"); + ImGui.PopStyleVar(); + ImGui.EndGroup(); + } + else if ((Meta.Version?.Length ?? 0) > 0) + { + ImGui.Text( $"(Version {Meta.Version})" ); + } + } + + private void DrawAuthor() + { + ImGui.BeginGroup(); + ImGui.TextColored( GreyColor, "by" ); + + ImGui.SameLine(); + var author = Meta.Author ?? ""; + if (ImGuiCustom.InputOrText(_editMode, LabelEditAuthor, ref author, 64) + && author != Meta.Author) + { + Meta.Author = author.Length > 0 ? author : null; + _selector.SaveCurrentMod(); + } + ImGui.EndGroup(); + } + + private void DrawWebsite() + { + ImGui.BeginGroup(); + if (_editMode) + { + ImGui.TextColored( GreyColor, "from" ); + ImGui.SameLine(); + var website = Meta.Website ?? ""; + if (ImGuiCustom.ResizingTextInput(LabelEditWebsite, ref website, 512) + && website != Meta.Website) + { + Meta.Website = website.Length > 0 ? website : null; + _selector.SaveCurrentMod(); + } + } + else if (( Meta.Website?.Length ?? 0 ) > 0) + { + if (_currentWebsite != Meta.Website) + { + _currentWebsite = Meta.Website; + _validWebsite = Uri.TryCreate( Meta.Website, UriKind.Absolute, out var uriResult ) + && ( uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp ); + } + if( _validWebsite ) + { + if( ImGui.SmallButton( ButtonOpenWebsite ) ) + { + try + { + var process = new ProcessStartInfo(Meta.Website) + { + UseShellExecute = true + }; + Process.Start(process); + } + catch(System.ComponentModel.Win32Exception) + { + // Do nothing. + } + } + + if( ImGui.IsItemHovered() ) + ImGui.SetTooltip( Meta.Website ); + } + else + { + ImGui.TextColored( GreyColor, "from" ); + ImGui.SameLine(); + ImGui.Text( Meta.Website ); + } + } + ImGui.EndGroup(); + } + + private void DrawHeaderLine() + { + DrawName(); + ImGui.SameLine(); + DrawVersion(); + ImGui.SameLine(); + DrawAuthor(); + ImGui.SameLine(); + DrawWebsite(); + } + #endregion + + #region Enabled Checkmarks + private void DrawEnabledMark() + { + var enabled = Mod.Enabled; + if( ImGui.Checkbox( LabelModEnabled, ref enabled ) ) + { + Mod.Enabled = enabled; + _base._plugin.ModManager.Mods.Save(); + _base._plugin.ModManager.CalculateEffectiveFileList(); + _base._menu._effectiveTab.RebuildFileList(_base._plugin.Configuration.ShowAdvanced); + } + } + + private void DrawEditableMark() + { + ImGui.Checkbox( LabelEditingEnabled, ref _editMode); + } + #endregion + + #region Edit Line Functions + private void DrawOpenModFolderButton() + { + if( ImGui.Button( ButtonOpenModFolder ) ) + { + Process.Start( Mod.Mod.ModBasePath.FullName ); + } + if( ImGui.IsItemHovered() ) + ImGui.SetTooltip( TooltipOpenModFolder ); + } + + private void DrawEditJsonButton() + { + if( ImGui.Button( ButtonEditJson ) ) + { + Process.Start( _selector.SaveCurrentMod() ); + } + if( ImGui.IsItemHovered() ) + ImGui.SetTooltip( TooltipEditJson ); + } + + private void DrawReloadJsonButton() + { + if( ImGui.Button( ButtonReloadJson ) ) + { + _selector.ReloadCurrentMod(); + } + if( ImGui.IsItemHovered() ) + ImGui.SetTooltip( TooltipReloadJson ); + } + + private void DrawDeduplicateButton() + { + if( ImGui.Button( ButtonDeduplicate ) ) + { + new Deduplicator(Mod.Mod.ModBasePath, Meta).Run(); + _selector.SaveCurrentMod(); + _base._menu._effectiveTab.RebuildFileList(_base._plugin.Configuration.ShowAdvanced); + } + if( ImGui.IsItemHovered() ) + ImGui.SetTooltip( TooltipDeduplicate ); + } + + private void DrawEditLine() + { + DrawOpenModFolderButton(); + ImGui.SameLine(); + DrawEditJsonButton(); + ImGui.SameLine(); + DrawReloadJsonButton(); + ImGui.SameLine(); + DrawDeduplicateButton(); + } + #endregion + + public void Draw() + { + if( Mod != null ) + { + try + { + var ret = ImGui.BeginChild( LabelModPanel, AutoFillSize, true ); + if (!ret) + return; + + DrawHeaderLine(); + + // Next line with fixed distance. + ImGuiCustom.VerticalDistance(HeaderLineDistance); + + DrawEnabledMark(); + if (_base._plugin.Configuration.ShowAdvanced) + { + ImGui.SameLine(); + DrawEditableMark(); + } + + // Next line, if editable. + if (_editMode) + DrawEditLine(); + + _details.Draw(_editMode); + + ImGui.EndChild(); + } + catch( Exception ex ) + { + PluginLog.LogError( ex, "fuck" ); + } + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/TabInstalledSelector.cs b/Penumbra/UI/TabInstalledSelector.cs new file mode 100644 index 00000000..34b80a6d --- /dev/null +++ b/Penumbra/UI/TabInstalledSelector.cs @@ -0,0 +1,275 @@ +using System.Numerics; +using System.Linq; +using System.IO; +using Newtonsoft.Json; +using ImGuiNET; +using Penumbra.Mods; +using Penumbra.Models; +using Dalamud.Interface; + +namespace Penumbra.UI +{ + public partial class SettingsInterface + { + private class Selector + { + private const string LabelSelectorList = "##availableModList"; + private const string TooltipMoveDown = "Move the selected mod down in priority"; + private const string TooltipMoveUp = "Move the selected mod up in priority"; + private const string TooltipDelete = "Delete the selected mod"; + private const string TooltipAdd = "Add an empty mod"; + private const string DialogDeleteMod = "PenumbraDeleteMod"; + private const string ButtonYesDelete = "Yes, delete it"; + private const string ButtonNoDelete = "No, keep it"; + private const float SelectorPanelWidth = 240f; + private const uint DisabledModColor = 0xFF666666; + private const uint ConflictingModColor = 0xFFAAAAFF; + + private static readonly Vector2 SelectorButtonSizes = new(60, 0); + private static readonly string ArrowUpString = FontAwesomeIcon.ArrowUp.ToIconString(); + private static readonly string ArrowDownString = FontAwesomeIcon.ArrowDown.ToIconString(); + + private readonly SettingsInterface _base; + private ModCollection Mods{ get{ return _base._plugin.ModManager.Mods; } } + + private ModInfo _mod = null; + private int _index = 0; + private int? _deleteIndex = null; + + public Selector(SettingsInterface ui) + { + _base = ui; + } + + private void DrawPriorityChangeButton(string iconString, bool up, int unavailableWhen) + { + ImGui.PushFont( UiBuilder.IconFont ); + if( _index != unavailableWhen ) + { + if( ImGui.Button( iconString, SelectorButtonSizes ) ) + { + SetSelection(_index); + _base._plugin.ModManager.ChangeModPriority( _mod, up ); + _index += up ? 1 : -1; + } + } + else + { + ImGui.PushStyleVar( ImGuiStyleVar.Alpha, 0.5f ); + ImGui.Button( iconString, SelectorButtonSizes ); + ImGui.PopStyleVar(); + } + + ImGui.PopFont(); + + if( ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( + _base._plugin.Configuration.InvertModListOrder ^ up ? TooltipMoveDown : TooltipMoveUp + ); + } + } + + private void DrawModTrashButton() + { + ImGui.PushFont( UiBuilder.IconFont ); + + if( ImGui.Button( FontAwesomeIcon.Trash.ToIconString(), SelectorButtonSizes ) ) + { + _deleteIndex = _index; + } + + ImGui.PopFont(); + + if( ImGui.IsItemHovered() ) + ImGui.SetTooltip( TooltipDelete ); + } + + private void DrawModAddButton() + { + ImGui.PushFont( UiBuilder.IconFont ); + + if( ImGui.Button( FontAwesomeIcon.Plus.ToIconString(), SelectorButtonSizes ) ) + { + // Do nothing. YEAH. #TODO. + } + + ImGui.PopFont(); + + if( ImGui.IsItemHovered() ) + ImGui.SetTooltip( TooltipAdd ); + } + + private void DrawModsSelectorButtons() + { + // Selector controls + ImGui.PushStyleVar( ImGuiStyleVar.WindowPadding, ZeroVector ); + ImGui.PushStyleVar( ImGuiStyleVar.FrameRounding, 0 ); + + DrawPriorityChangeButton(ArrowUpString, false, 0); + ImGui.SameLine(); + DrawPriorityChangeButton(ArrowDownString, true, Mods?.ModSettings.Count - 1 ?? 0); + ImGui.SameLine(); + DrawModTrashButton(); + ImGui.SameLine(); + DrawModAddButton(); + + ImGui.PopStyleVar( 3 ); + } + + void DrawDeleteModal() + { + if( _deleteIndex != null ) + ImGui.OpenPopup( DialogDeleteMod ); + + var ret = ImGui.BeginPopupModal( DialogDeleteMod ); + if( !ret ) + return; + + if( _mod?.Mod == null ) + { + ImGui.CloseCurrentPopup(); + ImGui.EndPopup(); + } + + ImGui.Text( "Are you sure you want to delete the following mod:" ); + // todo: why the fuck does this become null?????? + ImGui.Text( _mod?.Mod?.Meta?.Name ); + + if( ImGui.Button( ButtonYesDelete ) ) + { + ImGui.CloseCurrentPopup(); + _base._plugin.ModManager.DeleteMod( _mod.Mod ); + ClearSelection(); + _base.ReloadMods(); + } + + ImGui.SameLine(); + + if( ImGui.Button( ButtonNoDelete ) ) + { + ImGui.CloseCurrentPopup(); + _deleteIndex = null; + } + + ImGui.EndPopup(); + } + + + public void Draw() + { + if (Mods == null) + return; + + // Selector pane + ImGui.BeginGroup(); + ImGui.PushStyleVar( ImGuiStyleVar.ItemSpacing, ZeroVector ); + + // Inlay selector list + ImGui.BeginChild( LabelSelectorList, new Vector2(SelectorPanelWidth, -ImGui.GetFrameHeightWithSpacing() ), true ); + + for( var modIndex = 0; modIndex < Mods.ModSettings.Count; modIndex++ ) + { + var settings = Mods.ModSettings[ modIndex ]; + + var changedColour = false; + if( !settings.Enabled ) + { + ImGui.PushStyleColor( ImGuiCol.Text, DisabledModColor ); + changedColour = true; + } + else if( settings.Mod.FileConflicts.Any() ) + { + ImGui.PushStyleColor( ImGuiCol.Text, ConflictingModColor ); + changedColour = true; + } + +#if DEBUG + var selected = ImGui.Selectable( + $"id={modIndex} {settings.Mod.Meta.Name}", + modIndex == _index + ); +#else + var selected = ImGui.Selectable( settings.Mod.Meta.Name, modIndex == _index ); +#endif + + if( changedColour ) + ImGui.PopStyleColor(); + + if( selected ) + SetSelection(modIndex, settings); + } + + ImGui.EndChild(); + + DrawModsSelectorButtons(); + ImGui.EndGroup(); + + DrawDeleteModal(); + } + + public ModInfo Mod() => _mod; + + private void SetSelection(int idx, ModInfo info) + { + _mod = info; + if (idx != _index) + _base._menu._installedTab._modPanel._details.ResetState(); + _index = idx; + _deleteIndex = null; + } + + public void SetSelection(int idx) + { + if (idx >= (Mods?.ModSettings?.Count ?? 0)) + idx = -1; + if (idx < 0) + SetSelection(0, null); + else + SetSelection(idx, Mods.ModSettings[idx]); + } + + public void ClearSelection() => SetSelection(-1); + + public void SelectModByName( string name ) + { + for( var modIndex = 0; modIndex < Mods.ModSettings.Count; modIndex++ ) + { + var mod = Mods.ModSettings[ modIndex ]; + + if( mod.Mod.Meta.Name != name ) + continue; + + SetSelection(modIndex, mod); + return; + } + } + + private string GetCurrentModMetaFile() + { + if( _mod == null ) + return ""; + return Path.Combine( _mod.Mod.ModBasePath.FullName, "meta.json" ); + } + + public void ReloadCurrentMod() + { + var metaPath = GetCurrentModMetaFile(); + if (metaPath.Length > 0 && File.Exists(metaPath)) + { + _mod.Mod.Meta = ModMeta.LoadFromFile(metaPath) ?? _mod.Mod.Meta; + _base._menu._installedTab._modPanel._details.ResetState(); + } + } + + public string SaveCurrentMod() + { + var metaPath = GetCurrentModMetaFile(); + if (metaPath.Length > 0) + File.WriteAllText( metaPath, JsonConvert.SerializeObject( _mod.Mod.Meta, Formatting.Indented ) ); + _base._menu._installedTab._modPanel._details.ResetState(); + return metaPath; + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/TabSettings.cs b/Penumbra/UI/TabSettings.cs new file mode 100644 index 00000000..3e1372df --- /dev/null +++ b/Penumbra/UI/TabSettings.cs @@ -0,0 +1,174 @@ +using System.Diagnostics; +using ImGuiNET; + +namespace Penumbra.UI +{ + public partial class SettingsInterface + { + private class TabSettings + { + private const string LabelTab = "Settings"; + private const string LabelRootFolder = "Root Folder"; + private const string LabelRediscoverButton = "Rediscover Mods"; + private const string LabelOpenFolder = "Open Mods Folder"; + private const string LabelEnabled = "Enable Mods"; + private const string LabelInvertModOrder = "Invert mod load order (mods are loaded bottom up)"; + private const string LabelShowAdvanced = "Show Advanced Settings"; + private const string LabelLogLoadedFiles = "Log all loaded files"; + private const string LabelDisableNotifications = "Disable filesystem change notifications"; + private const string LabelEnableHttpApi = "Enable HTTP API"; + private const string LabelReloadResource = "Reload Player Resource"; + + private readonly SettingsInterface _base; + private readonly Configuration _config; + private bool _configChanged; + + public TabSettings(SettingsInterface ui) + { + _base = ui; + _config = _base._plugin.Configuration; + _configChanged = false; + } + + private void DrawRootFolder() + { + var basePath = _config.CurrentCollection; + if( ImGui.InputText( LabelRootFolder, ref basePath, 255 ) && _config.CurrentCollection != basePath ) + { + _config.CurrentCollection = basePath; + _configChanged = true; + } + } + + private void DrawRediscoverButton() + { + if( ImGui.Button( LabelRediscoverButton ) ) + { + _base.ReloadMods(); + _base._menu._installedTab._selector.ClearSelection(); + } + } + + private void DrawOpenModsButton() + { + if( ImGui.Button( LabelOpenFolder ) ) + { + Process.Start( _config.CurrentCollection ); + } + } + + private void DrawEnabledBox() + { + var enabled = _config.IsEnabled; + if( ImGui.Checkbox( LabelEnabled, ref enabled ) ) + { + _config.IsEnabled = enabled; + _base.ReloadMods(); + _configChanged = true; + } + } + + private void DrawInvertModOrderBox() + { + var invertOrder = _config.InvertModListOrder; + if( ImGui.Checkbox( LabelInvertModOrder, ref invertOrder ) ) + { + _config.InvertModListOrder = invertOrder; + _base.ReloadMods(); + _configChanged = true; + } + } + + private void DrawShowAdvancedBox() + { + var showAdvanced = _config.ShowAdvanced; + if( ImGui.Checkbox( LabelShowAdvanced, ref showAdvanced ) ) + { + _config.ShowAdvanced = showAdvanced; + _configChanged = true; + _base._menu._effectiveTab.RebuildFileList(showAdvanced); + } + } + + private void DrawLogLoadedFilesBox() + { + if( _base._plugin.ResourceLoader != null ) + ImGui.Checkbox( LabelLogLoadedFiles, ref _base._plugin.ResourceLoader.LogAllFiles ); + } + + private void DrawDisableNotificationsBox() + { + var fswatch = _config.DisableFileSystemNotifications; + if( ImGui.Checkbox( LabelDisableNotifications, ref fswatch ) ) + { + _config.DisableFileSystemNotifications = fswatch; + _configChanged = true; + } + } + + private void DrawEnableHttpApiBox() + { + var http = _config.EnableHttpApi; + if( ImGui.Checkbox( LabelEnableHttpApi, ref http ) ) + { + if( http ) + _base._plugin.CreateWebServer(); + else + _base._plugin.ShutdownWebServer(); + + _config.EnableHttpApi = http; + _configChanged = true; + } + } + + private void DrawReloadResourceButton() + { + if( ImGui.Button( LabelReloadResource ) ) + { + _base._plugin.GameUtils.ReloadPlayerResources(); + } + } + + private void DrawAdvancedSettings() + { + DrawLogLoadedFilesBox(); + DrawDisableNotificationsBox(); + DrawEnableHttpApiBox(); + DrawReloadResourceButton(); + } + + public void Draw() + { + var ret = ImGui.BeginTabItem( LabelTab ); + if( !ret ) + return; + + DrawRootFolder(); + + DrawRediscoverButton(); + ImGui.SameLine(); + DrawOpenModsButton(); + + ImGuiCustom.VerticalDistance(DefaultVerticalSpace); + DrawEnabledBox(); + + ImGuiCustom.VerticalDistance(DefaultVerticalSpace); + DrawInvertModOrderBox(); + + ImGuiCustom.VerticalDistance(DefaultVerticalSpace); + DrawShowAdvancedBox(); + + if( _config.ShowAdvanced ) + DrawAdvancedSettings(); + + if( _configChanged ) + { + _config.Save(); + _configChanged = false; + } + + ImGui.EndTabItem(); + } + } + } +} \ No newline at end of file