using System; using System.IO; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using System.Windows.Forms.VisualStyles; using Dalamud.Interface; using Dalamud.Logging; using ImGuiNET; using Penumbra.Importer; using Penumbra.Mod; using Penumbra.Mods; using Penumbra.UI.Custom; using Penumbra.Util; namespace Penumbra.UI { public partial class SettingsInterface { // Constants private partial class Selector { private const string LabelSelectorList = "##availableModList"; private const string LabelModFilter = "##ModFilter"; private const string LabelAddModPopup = "AddModPopup"; private const string LabelModHelpPopup = "Help##Selector"; private const string TooltipModFilter = "Filter mods for those containing the given substring.\nEnter c:[string] to filter for mods changing specific items.\nEnter a:[string] to filter for mods by specific authors."; 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 static readonly Vector2 SelectorButtonSizes = new( 100, 0 ); private static readonly Vector2 HelpButtonSizes = new( 40, 0 ); private static readonly Vector4 DeleteModNameColor = new( 0.7f, 0.1f, 0.1f, 1 ); } // Buttons private partial class Selector { // === Delete === private int? _deleteIndex; private void DrawModTrashButton() { using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); if( ImGui.Button( FontAwesomeIcon.Trash.ToIconString(), SelectorButtonSizes * _selectorScalingFactor ) && _index >= 0 ) { _deleteIndex = _index; } raii.Pop(); ImGuiCustom.HoverTooltip( TooltipDelete ); } private void DrawDeleteModal() { if( _deleteIndex == null ) { return; } ImGui.OpenPopup( DialogDeleteMod ); var _ = true; ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); var ret = ImGui.BeginPopupModal( DialogDeleteMod, ref _, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration ); if( !ret ) { return; } using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); if( Mod == null ) { _deleteIndex = null; ImGui.CloseCurrentPopup(); return; } ImGui.Text( "Are you sure you want to delete the following mod:" ); var halfLine = new Vector2( ImGui.GetTextLineHeight() / 2 ); ImGui.Dummy( halfLine ); ImGui.TextColored( DeleteModNameColor, Mod.Data.Meta.Name ); ImGui.Dummy( halfLine ); var buttonSize = ImGuiHelpers.ScaledVector2( 120, 0 ); if( ImGui.Button( ButtonYesDelete, buttonSize ) ) { ImGui.CloseCurrentPopup(); var mod = Mod; Cache.RemoveMod( mod ); _modManager.DeleteMod( mod.Data.BasePath ); ModFileSystem.InvokeChange(); ClearSelection(); } ImGui.SameLine(); if( ImGui.Button( ButtonNoDelete, buttonSize ) ) { ImGui.CloseCurrentPopup(); _deleteIndex = null; } } // === Add === private bool _modAddKeyboardFocus = true; private void DrawModAddButton() { using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); if( ImGui.Button( FontAwesomeIcon.Plus.ToIconString(), SelectorButtonSizes * _selectorScalingFactor ) ) { _modAddKeyboardFocus = true; ImGui.OpenPopup( LabelAddModPopup ); } raii.Pop(); ImGuiCustom.HoverTooltip( TooltipAdd ); DrawModAddPopup(); } private void DrawModAddPopup() { if( !ImGui.BeginPopup( LabelAddModPopup ) ) { return; } using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); if( _modAddKeyboardFocus ) { ImGui.SetKeyboardFocusHere(); _modAddKeyboardFocus = false; } var newName = ""; if( ImGui.InputTextWithHint( "##AddMod", "New Mod Name...", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) { try { var newDir = TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config!.ModDirectory ), newName ); var modMeta = new ModMeta { Author = "Unknown", Name = newName.Replace( '/', '\\' ), Description = string.Empty, }; var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); modMeta.SaveToFile( metaFile ); _modManager.AddMod( newDir ); ModFileSystem.InvokeChange(); SelectModOnUpdate( newDir.Name ); } catch( Exception e ) { PluginLog.Error( $"Could not create directory for new Mod {newName}:\n{e}" ); } ImGui.CloseCurrentPopup(); } if( ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) { ImGui.CloseCurrentPopup(); } } // === Help === private void DrawModHelpButton() { using var raii = ImGuiRaii.PushFont( UiBuilder.IconFont ); if( ImGui.Button( FontAwesomeIcon.QuestionCircle.ToIconString(), HelpButtonSizes * _selectorScalingFactor ) ) { ImGui.OpenPopup( LabelModHelpPopup ); } } private static void DrawModHelpPopup() { ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); ImGui.SetNextWindowSize( new Vector2( 5 * SelectorPanelWidth, 33 * ImGui.GetTextLineHeightWithSpacing() ), ImGuiCond.Appearing ); var _ = true; if( !ImGui.BeginPopupModal( LabelModHelpPopup, ref _, ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove ) ) { return; } using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); ImGui.Text( "Mod Selector" ); ImGui.BulletText( "Select a mod to obtain more information." ); ImGui.BulletText( "Mod names are colored according to their current state in the collection:" ); ImGui.Indent(); ImGui.Bullet(); ImGui.SameLine(); ImGui.Text( "Enabled in the current collection." ); ImGui.Bullet(); ImGui.SameLine(); ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.DisabledModColor ), "Disabled in the current collection." ); ImGui.Bullet(); ImGui.SameLine(); ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.HandledConflictModColor ), "Enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)." ); ImGui.Bullet(); ImGui.SameLine(); ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.ConflictingModColor ), "Enabled and conflicting with another enabled Mod on the same priority." ); ImGui.Unindent(); ImGui.BulletText( "Right-click a mod to enter its sort order, which is its name by default." ); ImGui.Indent(); ImGui.BulletText( "A sort order differing from the mods name will not be displayed, it will just be used for ordering." ); ImGui.BulletText( "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into collapsible folders that can group mods." ); ImGui.BulletText( "Collapsible folders can contain further collapsible folders, so \"folder1/folder2/folder3/1\" will produce 3 folders\n" + "\t\t[folder1] -> [folder2] -> [folder3] -> [ModName],\n" + "where ModName will be sorted as if it was the string '1'." ); ImGui.Unindent(); ImGui.BulletText( "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod." ); ImGui.BulletText( "Right-clicking a folder opens a context menu." ); ImGui.Indent(); ImGui.BulletText( "You can rename folders in the context menu. Leave the text blank and press enter to merge the folder with its parent." ); ImGui.BulletText( "You can also enable or disable all descendant mods of a folder." ); ImGui.Unindent(); ImGui.BulletText( "Use the Filter Mods... input at the top to filter the list for mods with names containing the text." ); ImGui.Indent(); ImGui.BulletText( "You can enter c:[string] to filter for Changed Items instead." ); ImGui.BulletText( "You can enter a:[string] to filter for Mod Authors instead." ); ImGui.Unindent(); ImGui.BulletText( "Use the expandable menu beside the input to filter for mods fulfilling specific criteria." ); ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); ImGui.Text( "Mod Management" ); ImGui.BulletText( "You can delete the currently selected mod with the trashcan button." ); ImGui.BulletText( "You can add a completely empty mod with the plus button." ); ImGui.BulletText( "You can import TTMP-based mods in the import tab." ); ImGui.BulletText( "You can import penumbra-based mods by moving the corresponding folder into your mod directory in a file explorer, then rediscovering mods." ); ImGui.BulletText( "If you enable Advanced Options in the Settings tab, you can toggle Edit Mode to manipulate your selected mod even further." ); ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); ImGui.Dummy( Vector2.UnitX * 2 * SelectorPanelWidth ); ImGui.SameLine(); if( ImGui.Button( "Understood", Vector2.UnitX * SelectorPanelWidth ) ) { ImGui.CloseCurrentPopup(); } } // === Main === private void DrawModsSelectorButtons() { // Selector controls using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.WindowPadding, ZeroVector ) .Push( ImGuiStyleVar.FrameRounding, 0 ); DrawModAddButton(); ImGui.SameLine(); DrawModHelpButton(); ImGui.SameLine(); DrawModTrashButton(); } } // Filters private partial class Selector { private string _modFilterInput = ""; private void DrawTextFilter() { ImGui.SetNextItemWidth( SelectorPanelWidth * _selectorScalingFactor - 22 * ImGuiHelpers.GlobalScale ); var tmp = _modFilterInput; if( ImGui.InputTextWithHint( LabelModFilter, "Filter Mods...", ref tmp, 256 ) && _modFilterInput != tmp ) { Cache.SetTextFilter( tmp ); _modFilterInput = tmp; } ImGuiCustom.HoverTooltip( TooltipModFilter ); } private void DrawToggleFilter() { if( ImGui.BeginCombo( "##ModStateFilter", "", ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest ) ) { using var raii = ImGuiRaii.DeferredEnd( ImGui.EndCombo ); var flags = ( int )Cache.StateFilter; foreach( ModFilter flag in Enum.GetValues( typeof( ModFilter ) ) ) { ImGui.CheckboxFlags( flag.ToName(), ref flags, ( int )flag ); } Cache.StateFilter = ( ModFilter )flags; } ImGuiCustom.HoverTooltip( "Filter mods for their activation status." ); } private void DrawModsSelectorFilter() { using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ZeroVector ); DrawTextFilter(); ImGui.SameLine(); DrawToggleFilter(); } } // Drag'n Drop private partial class Selector { private const string DraggedModLabel = "ModIndex"; private const string DraggedFolderLabel = "FolderName"; private readonly IntPtr _dragDropPayload = Marshal.AllocHGlobal( 4 ); private static unsafe bool IsDropping( string name ) => ImGui.AcceptDragDropPayload( name ).NativePtr != null; private void DragDropTarget( ModFolder folder ) { if( !ImGui.BeginDragDropTarget() ) { return; } using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropTarget ); if( IsDropping( DraggedModLabel ) ) { var payload = ImGui.GetDragDropPayload(); var modIndex = Marshal.ReadInt32( payload.Data ); var mod = Cache.GetMod( modIndex ).Item1; mod?.Data.Move( folder ); } else if( IsDropping( DraggedFolderLabel ) ) { var payload = ImGui.GetDragDropPayload(); var folderName = Marshal.PtrToStringUni( payload.Data ); if( ModFileSystem.Find( folderName!, out var droppedFolder ) && !ReferenceEquals( droppedFolder, folder ) && !folder.FullName.StartsWith( folderName!, StringComparison.InvariantCultureIgnoreCase ) ) { droppedFolder.Move( folder ); } } } private void DragDropSourceFolder( ModFolder folder ) { if( !ImGui.BeginDragDropSource() ) { return; } using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropSource ); var folderName = folder.FullName; var ptr = Marshal.StringToHGlobalUni( folderName ); ImGui.SetDragDropPayload( DraggedFolderLabel, ptr, ( uint )( folderName.Length + 1 ) * 2 ); ImGui.Text( $"Moving {folderName}..." ); } private void DragDropSourceMod( int modIndex, string modName ) { if( !ImGui.BeginDragDropSource() ) { return; } using var raii = ImGuiRaii.DeferredEnd( ImGui.EndDragDropSource ); Marshal.WriteInt32( _dragDropPayload, modIndex ); ImGui.SetDragDropPayload( "ModIndex", _dragDropPayload, 4 ); ImGui.Text( $"Moving {modName}..." ); } ~Selector() => Marshal.FreeHGlobal( _dragDropPayload ); } // Selection private partial class Selector { public Mod.Mod? Mod { get; private set; } private int _index; private string _nextDir = string.Empty; private void SetSelection( int idx, Mod.Mod? info ) { Mod = info; if( idx != _index ) { _base._menu.InstalledTab.ModPanel.Details.ResetState(); } _index = idx; _deleteIndex = null; } private void SetSelection( int idx ) { if( idx >= Cache.Count ) { idx = -1; } if( idx < 0 ) { SetSelection( 0, null ); } else { SetSelection( idx, Cache.GetMod( idx ).Item1 ); } } public void ReloadSelection() => SetSelection( _index, Cache.GetMod( _index ).Item1 ); public void ClearSelection() => SetSelection( -1 ); public void SelectModOnUpdate( string directory ) => _nextDir = directory; public void SelectModByDir( string name ) { var (mod, idx) = Cache.GetModByBasePath( name ); SetSelection( idx, mod ); } public void ReloadCurrentMod( bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) { if( Mod == null ) { return; } if( _index >= 0 && _modManager.UpdateMod( Mod.Data, reloadMeta, recomputeMeta, force ) ) { SelectModOnUpdate( Mod.Data.BasePath.Name ); _base._menu.InstalledTab.ModPanel.Details.ResetState(); } } public void SaveCurrentMod() => Mod?.Data.SaveMeta(); } // Right-Clicks private partial class Selector { // === Mod === private void DrawModOrderPopup( string popupName, Mod.Mod mod, bool firstOpen ) { if( !ImGui.BeginPopup( popupName ) ) { return; } using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); if( ModPanel.DrawSortOrder( mod.Data, _modManager, this ) ) { ImGui.CloseCurrentPopup(); } if( firstOpen ) { ImGui.SetKeyboardFocusHere( mod.Data.SortOrder.FullPath.Length - 1 ); } } // === Folder === private string _newFolderName = string.Empty; private void ChangeStatusOfChildren( ModFolder folder, int currentIdx, bool toWhat ) { var change = false; var metaManips = false; foreach( var _ in folder.AllMods( _modManager.Config.SortFoldersFirst ) ) { var (mod, _, _) = Cache.GetMod( currentIdx++ ); if( mod != null ) { change |= mod.Settings.Enabled != toWhat; mod!.Settings.Enabled = toWhat; metaManips |= mod.Data.Resources.MetaManipulations.Count > 0; } } if( !change ) { return; } Cache.TriggerFilterReset(); var collection = _modManager.Collections.CurrentCollection; if( collection.Cache != null ) { collection.CalculateEffectiveFileList( _modManager.TempPath, metaManips, collection == _modManager.Collections.ActiveCollection ); } collection.Save(); } private void DrawRenameFolderInput( ModFolder folder ) { ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale ); if( !ImGui.InputTextWithHint( "##NewFolderName", "Rename Folder...", ref _newFolderName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) { return; } if( _newFolderName.Any() ) { folder.Rename( _newFolderName ); } else { folder.Merge( folder.Parent! ); } _newFolderName = string.Empty; } private void DrawFolderContextMenu( ModFolder folder, int currentIdx, string treeName ) { if( !ImGui.BeginPopup( treeName ) ) { return; } using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); if( ImGui.MenuItem( "Enable All Descendants" ) ) { ChangeStatusOfChildren( folder, currentIdx, true ); } if( ImGui.MenuItem( "Disable All Descendants" ) ) { ChangeStatusOfChildren( folder, currentIdx, false ); } ImGuiHelpers.ScaledDummy( 0, 10 ); DrawRenameFolderInput( folder ); } } // Main-Interface private partial class Selector { private readonly SettingsInterface _base; private readonly ModManager _modManager; public readonly ModListCache Cache; private float _selectorScalingFactor = 1; public Selector( SettingsInterface ui ) { _base = ui; _modManager = Service< ModManager >.Get(); Cache = new ModListCache( _modManager ); } private void DrawCollectionButton( string label, string tooltipLabel, float size, ModCollection collection ) { if( collection == ModCollection.Empty || collection == _modManager.Collections.CurrentCollection ) { using var _ = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f ); ImGui.Button( label, Vector2.UnitX * size ); } else if( ImGui.Button( label, Vector2.UnitX * size ) ) { _base._menu.CollectionsTab.SetCurrentCollection( collection ); } ImGuiCustom.HoverTooltip( $"Switches to the currently set {tooltipLabel} collection, if it is not set to None and it is not the current collection already." ); } private void DrawHeaderBar() { const float size = 200; DrawModsSelectorFilter(); var textSize = ImGui.CalcTextSize( TabCollections.LabelCurrentCollection ).X + ImGui.GetStyle().ItemInnerSpacing.X; var comboSize = size * ImGui.GetIO().FontGlobalScale; var offset = comboSize + textSize; var buttonSize = Math.Max( ( ImGui.GetWindowContentRegionWidth() - offset - SelectorPanelWidth * _selectorScalingFactor - 4 * ImGui.GetStyle().ItemSpacing.X ) / 2, 5f ); ImGui.SameLine(); DrawCollectionButton( "Default", "default", buttonSize, _modManager.Collections.DefaultCollection ); ImGui.SameLine(); DrawCollectionButton( "Forced", "forced", buttonSize, _modManager.Collections.ForcedCollection ); ImGui.SameLine(); ImGui.SetNextItemWidth( comboSize ); using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); _base._menu.CollectionsTab.DrawCurrentCollectionSelector( false ); } private void DrawFolderContent( ModFolder folder, ref int idx ) { // Collection may be manipulated. foreach( var item in folder.GetItems( _modManager.Config.SortFoldersFirst ).ToArray() ) { if( item is ModFolder sub ) { var (visible, _) = Cache.GetFolder( sub ); if( visible ) { DrawModFolder( sub, ref idx ); } else { idx += sub.TotalDescendantMods(); } } else if( item is ModData _ ) { var (mod, visible, color) = Cache.GetMod( idx ); if( mod != null && visible ) { DrawMod( mod, idx++, color ); } else { ++idx; } } } } private void DrawModFolder( ModFolder folder, ref int idx ) { var treeName = $"{folder.Name}##{folder.FullName}"; var open = ImGui.TreeNodeEx( treeName ); using var raii = ImGuiRaii.DeferredEnd( ImGui.TreePop, open ); if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) { _newFolderName = string.Empty; ImGui.OpenPopup( treeName ); } DrawFolderContextMenu( folder, idx, treeName ); DragDropTarget( folder ); DragDropSourceFolder( folder ); if( open ) { DrawFolderContent( folder, ref idx ); } else { idx += folder.TotalDescendantMods(); } } private void DrawMod( Mod.Mod mod, int modIndex, uint color ) { using var colorRaii = ImGuiRaii.PushColor( ImGuiCol.Text, color, color != 0 ); var selected = ImGui.Selectable( $"{mod.Data.Meta.Name}##{modIndex}", modIndex == _index ); colorRaii.Pop(); var popupName = $"##SortOrderPopup{modIndex}"; var firstOpen = false; if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) { ImGui.OpenPopup( popupName ); firstOpen = true; } DragDropTarget( mod.Data.SortOrder.ParentFolder ); DragDropSourceMod( modIndex, mod.Data.Meta.Name ); DrawModOrderPopup( popupName, mod, firstOpen ); if( selected ) { SetSelection( modIndex, mod ); } } public void Draw() { if( Cache.Update() ) { if( _nextDir.Any() ) { SelectModByDir( _nextDir ); _nextDir = string.Empty; } else if( Mod != null ) { SelectModByDir( Mod.Data.BasePath.Name ); } } _selectorScalingFactor = ImGuiHelpers.GlobalScale * ( Penumbra.Config.ScaleModSelector ? ImGui.GetWindowWidth() / SettingsMenu.MinSettingsSize.X : 1f ); // Selector pane DrawHeaderBar(); using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); ImGui.BeginGroup(); using var raii = ImGuiRaii.DeferredEnd( ImGui.EndGroup ) .Push( ImGui.EndChild ); // Inlay selector list if( ImGui.BeginChild( LabelSelectorList, new Vector2( SelectorPanelWidth * _selectorScalingFactor, -ImGui.GetFrameHeightWithSpacing() ), true, ImGuiWindowFlags.HorizontalScrollbar ) ) { style.Push( ImGuiStyleVar.IndentSpacing, 12.5f ); var modIndex = 0; DrawFolderContent( _modManager.StructuredMods, ref modIndex ); style.Pop(); } raii.Pop(); DrawModsSelectorButtons(); style.Pop(); DrawModHelpPopup(); DrawDeleteModal(); } } } }