Penumbra/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs

766 lines
No EOL
30 KiB
C#

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();
}
}
}
}