A lot of interface stuff, some more cleanup and fixes. Main functionality should be mostly fine, importing works. Missing a lot of mod edit options.

This commit is contained in:
Ottermandias 2022-04-26 21:35:09 +02:00
parent 8dd681bdda
commit dbb9931189
77 changed files with 3332 additions and 2066 deletions

View file

@ -5,6 +5,7 @@ using System.Numerics;
using System.Runtime.InteropServices;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Filesystem;
using OtterGui.Raii;
using Penumbra.Collections;
@ -21,7 +22,7 @@ public partial class ModFileSystemSelector
}
private const StringComparison IgnoreCase = StringComparison.InvariantCultureIgnoreCase;
private readonly IReadOnlySet< Mod2 > _newMods = new HashSet< Mod2 >();
private readonly IReadOnlySet< Mod > _newMods = new HashSet< Mod >();
private LowerString _modFilter = LowerString.Empty;
private int _filterType = -1;
private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods;
@ -75,7 +76,7 @@ public partial class ModFileSystemSelector
// Folders have default state and are filtered out on the direct string instead of the other options.
// If any filter is set, they should be hidden by default unless their children are visible,
// or they contain the path search string.
protected override bool ApplyFiltersAndState( FileSystem< Mod2 >.IPath path, out ModState state )
protected override bool ApplyFiltersAndState( FileSystem< Mod >.IPath path, out ModState state )
{
if( path is ModFileSystem.Folder f )
{
@ -88,7 +89,7 @@ public partial class ModFileSystemSelector
}
// Apply the string filters.
private bool ApplyStringFilters( ModFileSystem.Leaf leaf, Mod2 mod )
private bool ApplyStringFilters( ModFileSystem.Leaf leaf, Mod mod )
{
return _filterType switch
{
@ -102,7 +103,7 @@ public partial class ModFileSystemSelector
}
// Only get the text color for a mod if no filters are set.
private uint GetTextColor( Mod2 mod, ModSettings2? settings, ModCollection collection )
private uint GetTextColor( Mod mod, ModSettings? settings, ModCollection collection )
{
if( _newMods.Contains( mod ) )
{
@ -119,7 +120,7 @@ public partial class ModFileSystemSelector
return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedDisabledMod.Value() : ColorId.DisabledMod.Value();
}
var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index ).ToList();
var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index );
if( conflicts.Count == 0 )
{
return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedMod.Value() : ColorId.EnabledMod.Value();
@ -130,7 +131,7 @@ public partial class ModFileSystemSelector
: ColorId.HandledConflictMod.Value();
}
private bool CheckStateFilters( Mod2 mod, ModSettings2? settings, ModCollection collection, ref ModState state )
private bool CheckStateFilters( Mod mod, ModSettings? settings, ModCollection collection, ref ModState state )
{
var isNew = _newMods.Contains( mod );
// Handle mod details.
@ -188,7 +189,7 @@ public partial class ModFileSystemSelector
}
// Conflicts can only be relevant if the mod is enabled.
var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index ).ToList();
var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index );
if( conflicts.Count > 0 )
{
if( conflicts.Any( c => !c.Solved ) )

View file

@ -1,23 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Logging;
using ImGuiNET;
using OtterGui;
using OtterGui.Filesystem;
using OtterGui.FileSystem.Selector;
using OtterGui.Raii;
using Penumbra.Collections;
using Penumbra.Import;
using Penumbra.Mods;
namespace Penumbra.UI.Classes;
public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, ModFileSystemSelector.ModState >
public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, ModFileSystemSelector.ModState >
{
public ModSettings2 SelectedSettings { get; private set; } = ModSettings2.Empty;
private readonly FileDialogManager _fileManager = new();
private TexToolsImporter? _import;
public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty;
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
public ModFileSystemSelector( ModFileSystem fileSystem, IReadOnlySet< Mod2 > newMods )
public ModFileSystemSelector( ModFileSystem fileSystem, IReadOnlySet< Mod > newMods )
: base( fileSystem )
{
_newMods = newMods;
@ -26,6 +33,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
SubscribeRightClickFolder( InheritDescendants, 15 );
SubscribeRightClickFolder( OwnDescendants, 15 );
AddButton( AddNewModButton, 0 );
AddButton( AddImportModButton, 1 );
AddButton( DeleteModButton, 1000 );
SetFilterTooltip();
@ -33,6 +41,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
Penumbra.CollectionManager.CollectionChanged += OnCollectionChange;
Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange;
Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange;
Penumbra.ModManager.ModMetaChanged += OnModMetaChange;
Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection;
Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection;
OnCollectionChange( ModCollection.Type.Current, null, Penumbra.CollectionManager.Current, null );
@ -43,6 +52,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
base.Dispose();
Penumbra.ModManager.ModDiscoveryStarted -= StoreCurrentSelection;
Penumbra.ModManager.ModDiscoveryFinished -= RestoreLastSelection;
Penumbra.ModManager.ModMetaChanged -= OnModMetaChange;
Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange;
Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange;
Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange;
@ -64,10 +74,11 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
protected override uint FolderLineColor
=> ColorId.FolderLine.Value();
protected override void DrawLeafName( FileSystem< Mod2 >.Leaf leaf, in ModState state, bool selected )
protected override void DrawLeafName( FileSystem< Mod >.Leaf leaf, in ModState state, bool selected )
{
var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags;
using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color );
using var id = ImRaii.PushId( leaf.Value.Index );
using var _ = ImRaii.TreeNode( leaf.Value.Name, flags );
}
@ -107,17 +118,90 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
// Add custom buttons.
private static void AddNewModButton( Vector2 size )
private string _newModName = string.Empty;
private void AddNewModButton( Vector2 size )
{
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", false, true ) )
{ }
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", !Penumbra.ModManager.Valid, true ) )
{
ImGui.OpenPopup( "Create New Mod" );
}
if( ImGuiUtil.OpenNameField( "Create New Mod", ref _newModName ) )
{
try
{
var newDir = Mod.CreateModFolder( Penumbra.ModManager.BasePath, _newModName );
Mod.CreateMeta( newDir, _newModName, string.Empty, string.Empty, "1.0", string.Empty );
Penumbra.ModManager.AddMod( newDir );
_newModName = string.Empty;
}
catch( Exception e )
{
PluginLog.Error( $"Could not create directory for new Mod {_newModName}:\n{e}" );
}
}
}
// Add an import mods button that opens a file selector.
private void AddImportModButton( Vector2 size )
{
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), size,
"Import one or multiple mods from Tex Tools Mod Pack Files.", !Penumbra.ModManager.Valid, true ) )
{
_fileManager.OpenFileDialog( "Import Mod Pack", "TexTools Mod Packs{.ttmp,.ttmp2}", ( s, f ) =>
{
if( s )
{
_import = new TexToolsImporter( Penumbra.ModManager.BasePath, f.Count, f.Select( file => new FileInfo( file ) ) );
ImGui.OpenPopup( "Import Status" );
}
}, 0, Penumbra.Config.ModDirectory );
}
_fileManager.Draw();
DrawInfoPopup();
}
// Draw the progress information for import.
private void DrawInfoPopup()
{
var display = ImGui.GetIO().DisplaySize;
ImGui.SetNextWindowSize( display / 4 );
ImGui.SetNextWindowPos( 3 * display / 8 );
using var popup = ImRaii.Popup( "Import Status", ImGuiWindowFlags.Modal );
if( _import != null && popup.Success )
{
_import.DrawProgressInfo( ImGuiHelpers.ScaledVector2( -1, ImGui.GetFrameHeight() ) );
if( _import.State == ImporterState.Done )
{
ImGui.SetCursorPosY( ImGui.GetWindowHeight() - ImGui.GetFrameHeight() * 2 );
if( ImGui.Button( "Close", -Vector2.UnitX ) )
{
_import = null;
ImGui.CloseCurrentPopup();
}
}
}
}
private void DeleteModButton( Vector2 size )
{
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size,
"Delete the currently selected mod entirely from your drive.", SelectedLeaf == null, true ) )
{ }
var keys = ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyShift;
var tt = SelectedLeaf == null
? "No mod selected."
: "Delete the currently selected mod entirely from your drive.\n"
+ "This can not be undone.";
if( !keys )
{
tt += "\nHold Control and Shift while clicking to delete the mod.";
}
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true )
&& Selected != null )
{
Penumbra.ModManager.DeleteMod( Selected.Index );
}
}
@ -146,6 +230,17 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
}
}
private void OnModMetaChange( MetaChangeType type, Mod mod, string? oldName )
{
switch( type )
{
case MetaChangeType.Name:
case MetaChangeType.Author:
SetFilterDirty();
break;
}
}
private void OnInheritanceChange( bool _ )
{
SetFilterDirty();
@ -175,17 +270,17 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, Mo
OnSelectionChange( Selected, Selected, default );
}
private void OnSelectionChange( Mod2? _1, Mod2? newSelection, in ModState _2 )
private void OnSelectionChange( Mod? _1, Mod? newSelection, in ModState _2 )
{
if( newSelection == null )
{
SelectedSettings = ModSettings2.Empty;
SelectedSettings = ModSettings.Empty;
SelectedSettingCollection = ModCollection.Empty;
}
else
{
( var settings, SelectedSettingCollection ) = Penumbra.CollectionManager.Current[ newSelection.Index ];
SelectedSettings = settings ?? ModSettings2.Empty;
SelectedSettings = settings ?? ModSettings.Empty;
}
}

View file

@ -0,0 +1,225 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Interface.Windowing;
using ImGuiNET;
using OtterGui.Raii;
using Penumbra.GameData.ByteString;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.UI.Classes;
public class SubModEditWindow : Window
{
private const string WindowBaseLabel = "###SubModEdit";
private Mod? _mod;
private int _groupIdx = -1;
private int _optionIdx = -1;
private IModGroup? _group;
private ISubMod? _subMod;
private readonly List< FilePathInfo > _availableFiles = new();
private readonly struct FilePathInfo
{
public readonly FullPath File;
public readonly Utf8RelPath RelFile;
public readonly long Size;
public readonly List< (int, int, Utf8GamePath) > SubMods;
public FilePathInfo( FileInfo file, Mod mod )
{
File = new FullPath( file );
RelFile = Utf8RelPath.FromFile( File, mod.BasePath, out var f ) ? f : Utf8RelPath.Empty;
Size = file.Length;
SubMods = new List< (int, int, Utf8GamePath) >();
var path = File;
foreach( var (group, groupIdx) in mod.Groups.WithIndex() )
{
foreach( var (subMod, optionIdx) in group.WithIndex() )
{
SubMods.AddRange( subMod.Files.Where( kvp => kvp.Value.Equals( path ) ).Select( kvp => ( groupIdx, optionIdx, kvp.Key ) ) );
}
}
SubMods.AddRange( mod.Default.Files.Where( kvp => kvp.Value.Equals( path ) ).Select( kvp => (-1, 0, kvp.Key) ) );
}
}
private readonly HashSet< MetaManipulation > _manipulations = new();
private readonly Dictionary< Utf8GamePath, FullPath > _files = new();
private readonly Dictionary< Utf8GamePath, FullPath > _fileSwaps = new();
public void Activate( Mod mod, int groupIdx, int optionIdx )
{
IsOpen = true;
_mod = mod;
_groupIdx = groupIdx;
_group = groupIdx >= 0 ? mod.Groups[ groupIdx ] : null;
_optionIdx = optionIdx;
_subMod = groupIdx >= 0 ? _group![ optionIdx ] : _mod.Default;
_availableFiles.Clear();
_availableFiles.AddRange( mod.BasePath.EnumerateDirectories()
.SelectMany( d => d.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
.Select( f => new FilePathInfo( f, _mod ) ) );
_manipulations.Clear();
_manipulations.UnionWith( _subMod.Manipulations );
_files.SetTo( _subMod.Files );
_fileSwaps.SetTo( _subMod.FileSwaps );
WindowName = $"{_mod.Name}: {(_group != null ? $"{_group.Name} - " : string.Empty)}{_subMod.Name}";
}
public override bool DrawConditions()
=> _subMod != null;
public override void Draw()
{
using var tabBar = ImRaii.TabBar( "##tabs" );
if( !tabBar )
{
return;
}
DrawFileTab();
DrawMetaTab();
DrawSwapTab();
}
private void Save()
{
if( _mod != null )
{
Penumbra.ModManager.OptionUpdate( _mod, _groupIdx, _optionIdx, _files, _manipulations, _fileSwaps );
}
}
public override void OnClose()
{
_subMod = null;
}
private void DrawFileTab()
{
using var tab = ImRaii.TabItem( "File Redirections" );
if( !tab )
{
return;
}
using var list = ImRaii.Table( "##files", 3 );
if( !list )
{
return;
}
foreach( var file in _availableFiles )
{
ImGui.TableNextColumn();
ConfigWindow.Text( file.RelFile.Path );
ImGui.TableNextColumn();
ImGui.Text( file.Size.ToString() );
ImGui.TableNextColumn();
if( file.SubMods.Count == 0 )
{
ImGui.Text( "Unused" );
}
foreach( var (groupIdx, optionIdx, gamePath) in file.SubMods )
{
ImGui.TableNextColumn();
ImGui.TableNextColumn();
var group = groupIdx >= 0 ? _mod!.Groups[ groupIdx ] : null;
var option = groupIdx >= 0 ? group![ optionIdx ] : _mod!.Default;
var text = groupIdx >= 0
? $"{group!.Name} - {option.Name}"
: option.Name;
ImGui.Text( text );
ImGui.TableNextColumn();
ConfigWindow.Text( gamePath.Path );
}
}
ImGui.TableNextRow();
foreach( var (gamePath, fullPath) in _files )
{
ImGui.TableNextColumn();
ConfigWindow.Text( gamePath.Path );
ImGui.TableNextColumn();
ImGui.Text( fullPath.FullName );
ImGui.TableNextColumn();
}
}
private void DrawMetaTab()
{
using var tab = ImRaii.TabItem( "Meta Manipulations" );
if( !tab )
{
return;
}
using var list = ImRaii.Table( "##meta", 3 );
if( !list )
{
return;
}
foreach( var manip in _manipulations )
{
ImGui.TableNextColumn();
ImGui.Text( manip.ManipulationType.ToString() );
ImGui.TableNextColumn();
ImGui.Text( manip.ManipulationType switch
{
MetaManipulation.Type.Imc => manip.Imc.ToString(),
MetaManipulation.Type.Eqdp => manip.Eqdp.ToString(),
MetaManipulation.Type.Eqp => manip.Eqp.ToString(),
MetaManipulation.Type.Est => manip.Est.ToString(),
MetaManipulation.Type.Gmp => manip.Gmp.ToString(),
MetaManipulation.Type.Rsp => manip.Rsp.ToString(),
_ => string.Empty,
} );
ImGui.TableNextColumn();
ImGui.Text( manip.ManipulationType switch
{
MetaManipulation.Type.Imc => manip.Imc.Entry.ToString(),
MetaManipulation.Type.Eqdp => manip.Eqdp.Entry.ToString(),
MetaManipulation.Type.Eqp => manip.Eqp.Entry.ToString(),
MetaManipulation.Type.Est => manip.Est.Entry.ToString(),
MetaManipulation.Type.Gmp => manip.Gmp.Entry.ToString(),
MetaManipulation.Type.Rsp => manip.Rsp.Entry.ToString(),
_ => string.Empty,
} );
}
}
private void DrawSwapTab()
{
using var tab = ImRaii.TabItem( "File Swaps" );
if( !tab )
{
return;
}
using var list = ImRaii.Table( "##swaps", 3 );
if( !list )
{
return;
}
foreach( var (from, to) in _fileSwaps )
{
ImGui.TableNextColumn();
ConfigWindow.Text( from.Path );
ImGui.TableNextColumn();
ImGui.Text( to.FullName );
ImGui.TableNextColumn();
}
}
public SubModEditWindow()
: base( WindowBaseLabel )
{ }
}

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Numerics;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
namespace Penumbra.UI;

View file

@ -4,6 +4,7 @@ using System.Numerics;
using Dalamud.Interface;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.Collections;
using Penumbra.GameData.ByteString;

View file

@ -0,0 +1,444 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Numerics;
using Dalamud.Interface;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.Mods;
namespace Penumbra.UI;
public partial class ConfigWindow
{
private partial class ModPanel
{
public readonly Queue< Action > _delayedActions = new();
private void DrawAddOptionGroupInput()
{
ImGui.SetNextItemWidth( _window._inputTextWidth.X );
ImGui.InputTextWithHint( "##newGroup", "Add new option group...", ref _newGroupName, 256 );
ImGui.SameLine();
var nameValid = Mod.Manager.VerifyFileName( _mod, null, _newGroupName, false );
var tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name.";
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
tt, !nameValid, true ) )
{
Penumbra.ModManager.AddModGroup( _mod, SelectType.Single, _newGroupName );
_newGroupName = string.Empty;
}
}
private Vector2 _cellPadding = Vector2.Zero;
private Vector2 _itemSpacing = Vector2.Zero;
private void DrawEditModTab()
{
using var tab = DrawTab( EditModTabHeader, Tabs.Edit );
if( !tab )
{
return;
}
using var child = ImRaii.Child( "##editChild", -Vector2.One );
if( !child )
{
return;
}
_cellPadding = ImGui.GetStyle().CellPadding with { X = 2 * ImGuiHelpers.GlobalScale };
_itemSpacing = ImGui.GetStyle().CellPadding with { X = 4 * ImGuiHelpers.GlobalScale };
EditRegularMeta();
ImGui.Dummy( _window._defaultSpace );
if( TextInput( "Mod Path", PathFieldIdx, NoFieldIdx, _leaf.FullName(), out var newPath, 256, _window._inputTextWidth.X ) )
{
_window._penumbra.ModFileSystem.RenameAndMove( _leaf, newPath );
}
ImGui.Dummy( _window._defaultSpace );
DrawAddOptionGroupInput();
ImGui.Dummy( _window._defaultSpace );
for( var groupIdx = 0; groupIdx < _mod.Groups.Count; ++groupIdx )
{
EditGroup( groupIdx );
}
EndActions();
EditDescriptionPopup();
}
// Special field indices to reuse the same string buffer.
private const int NoFieldIdx = -1;
private const int NameFieldIdx = -2;
private const int AuthorFieldIdx = -3;
private const int VersionFieldIdx = -4;
private const int WebsiteFieldIdx = -5;
private const int PathFieldIdx = -6;
private const int DescriptionFieldIdx = -7;
private void EditRegularMeta()
{
if( TextInput( "Name", NameFieldIdx, NoFieldIdx, _mod.Name, out var newName, 256, _window._inputTextWidth.X ) )
{
Penumbra.ModManager.ChangeModName( _mod.Index, newName );
}
if( TextInput( "Author", AuthorFieldIdx, NoFieldIdx, _mod.Author, out var newAuthor, 256, _window._inputTextWidth.X ) )
{
Penumbra.ModManager.ChangeModAuthor( _mod.Index, newAuthor );
}
if( TextInput( "Version", VersionFieldIdx, NoFieldIdx, _mod.Version, out var newVersion, 32, _window._inputTextWidth.X ) )
{
Penumbra.ModManager.ChangeModVersion( _mod.Index, newVersion );
}
if( TextInput( "Website", WebsiteFieldIdx, NoFieldIdx, _mod.Website, out var newWebsite, 256, _window._inputTextWidth.X ) )
{
Penumbra.ModManager.ChangeModWebsite( _mod.Index, newWebsite );
}
if( ImGui.Button( "Edit Description", _window._inputTextWidth ) )
{
_delayedActions.Enqueue( () => OpenEditDescriptionPopup( DescriptionFieldIdx ) );
}
if( ImGui.Button( "Edit Default Mod", _window._inputTextWidth ) )
{
_window.SubModPopup.Activate( _mod, -1, 0 );
}
}
// Temporary strings
private string? _currentEdit;
private int? _currentGroupPriority;
private int _currentField = -1;
private int _optionIndex = -1;
private string _newGroupName = string.Empty;
private string _newOptionName = string.Empty;
private string _newDescription = string.Empty;
private int _newDescriptionIdx = -1;
private void EditGroup( int groupIdx )
{
var group = _mod.Groups[ groupIdx ];
using var id = ImRaii.PushId( groupIdx );
using var frame = ImRaii.FramedGroup( $"Group #{groupIdx + 1}" );
using var style = ImRaii.PushStyle( ImGuiStyleVar.CellPadding, _cellPadding )
.Push( ImGuiStyleVar.ItemSpacing, _itemSpacing );
if( TextInput( "##Name", groupIdx, NoFieldIdx, group.Name, out var newGroupName, 256, _window._inputTextWidth.X ) )
{
Penumbra.ModManager.RenameModGroup( _mod, groupIdx, newGroupName );
}
ImGuiUtil.HoverTooltip( "Group Name" );
ImGui.SameLine();
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
"Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true ) )
{
_delayedActions.Enqueue( () => Penumbra.ModManager.DeleteModGroup( _mod, groupIdx ) );
}
ImGui.SameLine();
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
"Edit group description.", false, true ) )
{
_delayedActions.Enqueue( () => OpenEditDescriptionPopup( groupIdx ) );
}
ImGui.SameLine();
if( PriorityInput( "##Priority", groupIdx, NoFieldIdx, group.Priority, out var priority, 50 * ImGuiHelpers.GlobalScale ) )
{
Penumbra.ModManager.ChangeGroupPriority( _mod, groupIdx, priority );
}
ImGuiUtil.HoverTooltip( "Group Priority" );
ImGui.SetNextItemWidth( _window._inputTextWidth.X - 2 * ImGui.GetFrameHeight() - 8 * ImGuiHelpers.GlobalScale );
using( var combo = ImRaii.Combo( "##GroupType", GroupTypeName( group.Type ) ) )
{
if( combo )
{
foreach( var type in new[] { SelectType.Single, SelectType.Multi } )
{
if( ImGui.Selectable( GroupTypeName( type ), group.Type == type ) )
{
Penumbra.ModManager.ChangeModGroupType( _mod, groupIdx, type );
}
}
}
}
ImGui.SameLine();
var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}.";
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.ArrowUp.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
tt, groupIdx == 0, true ) )
{
_delayedActions.Enqueue( () => Penumbra.ModManager.MoveModGroup( _mod, groupIdx, groupIdx - 1 ) );
}
ImGui.SameLine();
tt = groupIdx == _mod.Groups.Count - 1
? "Can not move this group further downwards."
: $"Move this group down to group {groupIdx + 2}.";
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.ArrowDown.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
tt, groupIdx == _mod.Groups.Count - 1, true ) )
{
_delayedActions.Enqueue( () => Penumbra.ModManager.MoveModGroup( _mod, groupIdx, groupIdx + 1 ) );
}
ImGui.Dummy( _window._defaultSpace );
using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.SizingFixedFit );
ImGui.TableSetupColumn( "idx", ImGuiTableColumnFlags.WidthFixed, 60 * ImGuiHelpers.GlobalScale );
ImGui.TableSetupColumn( "name", ImGuiTableColumnFlags.WidthFixed, _window._inputTextWidth.X - 62 * ImGuiHelpers.GlobalScale );
ImGui.TableSetupColumn( "delete", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight() );
ImGui.TableSetupColumn( "edit", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight() );
ImGui.TableSetupColumn( "priority", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale );
if( table )
{
for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx )
{
EditOption( group, groupIdx, optionIdx );
}
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.SetNextItemWidth( -1 );
ImGui.InputTextWithHint( "##newOption", "Add new option...", ref _newOptionName, 256 );
ImGui.TableNextColumn();
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
"Add a new option to this group.", _newOptionName.Length == 0, true ) )
{
Penumbra.ModManager.AddOption( _mod, groupIdx, _newOptionName );
_newOptionName = string.Empty;
}
}
}
private static string GroupTypeName( SelectType type )
=> type switch
{
SelectType.Single => "Single Group",
SelectType.Multi => "Multi Group",
_ => "Unknown",
};
private int _dragDropGroupIdx = -1;
private int _dragDropOptionIdx = -1;
private void OptionDragDrop( IModGroup group, int groupIdx, int optionIdx )
{
const string label = "##DragOption";
using( var source = ImRaii.DragDropSource() )
{
if( source )
{
if( ImGui.SetDragDropPayload( label, IntPtr.Zero, 0 ) )
{
_dragDropGroupIdx = groupIdx;
_dragDropOptionIdx = optionIdx;
}
ImGui.Text( $"Dragging option {group[ optionIdx ].Name} from group {group.Name}..." );
}
}
using( var target = ImRaii.DragDropTarget() )
{
if( target.Success && ImGuiUtil.IsDropping( label ) )
{
if( _dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0 )
{
if( _dragDropGroupIdx == groupIdx )
{
// TODO
Dalamud.Chat.Print(
$"Dropped {_mod.Groups[ _dragDropGroupIdx ][ _dragDropOptionIdx ].Name} onto {_mod.Groups[ groupIdx ][ optionIdx ].Name}" );
}
else
{
Dalamud.Chat.Print(
$"Dropped {_mod.Groups[ _dragDropGroupIdx ][ _dragDropOptionIdx ].Name} onto {_mod.Groups[ groupIdx ][ optionIdx ].Name}" );
}
}
_dragDropGroupIdx = -1;
_dragDropOptionIdx = -1;
}
}
}
private void EditOption( IModGroup group, int groupIdx, int optionIdx )
{
var option = group[ optionIdx ];
using var id = ImRaii.PushId( optionIdx );
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.Selectable( $"Option #{optionIdx + 1}" );
OptionDragDrop( group, groupIdx, optionIdx );
ImGui.TableNextColumn();
if( TextInput( "##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1 ) )
{
Penumbra.ModManager.RenameOption( _mod, groupIdx, optionIdx, newOptionName );
}
ImGui.TableNextColumn();
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
"Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true ) )
{
_delayedActions.Enqueue( () => Penumbra.ModManager.DeleteOption( _mod, groupIdx, optionIdx ) );
}
ImGui.TableNextColumn();
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
"Edit this option.", false, true ) )
{
_window.SubModPopup.Activate( _mod, groupIdx, optionIdx );
}
ImGui.TableNextColumn();
if( group.Type == SelectType.Multi )
{
if( PriorityInput( "##Priority", groupIdx, optionIdx, group.OptionPriority( optionIdx ), out var priority,
50 * ImGuiHelpers.GlobalScale ) )
{
Penumbra.ModManager.ChangeOptionPriority( _mod, groupIdx, optionIdx, priority );
}
ImGuiUtil.HoverTooltip( "Option priority." );
}
}
private bool TextInput( string label, int field, int option, string oldValue, out string value, uint maxLength, float width )
{
var tmp = field == _currentField && option == _optionIndex ? _currentEdit ?? oldValue : oldValue;
ImGui.SetNextItemWidth( width );
if( ImGui.InputText( label, ref tmp, maxLength ) )
{
_currentEdit = tmp;
_optionIndex = option;
_currentField = field;
}
if( ImGui.IsItemDeactivatedAfterEdit() && _currentEdit != null )
{
var ret = _currentEdit != oldValue;
value = _currentEdit;
_currentEdit = null;
_currentField = NoFieldIdx;
_optionIndex = NoFieldIdx;
return ret;
}
value = string.Empty;
return false;
}
private bool PriorityInput( string label, int field, int option, int oldValue, out int value, float width )
{
var tmp = field == _currentField && option == _optionIndex ? _currentGroupPriority ?? oldValue : oldValue;
ImGui.SetNextItemWidth( width );
if( ImGui.InputInt( label, ref tmp, 0, 0 ) )
{
_currentGroupPriority = tmp;
_optionIndex = option;
_currentField = field;
}
if( ImGui.IsItemDeactivatedAfterEdit() && _currentGroupPriority != null )
{
var ret = _currentGroupPriority != oldValue;
value = _currentGroupPriority.Value;
_currentGroupPriority = null;
_currentField = NoFieldIdx;
_optionIndex = NoFieldIdx;
return ret;
}
value = 0;
return false;
}
// Delete a marked group or option outside of iteration.
private void EndActions()
{
while( _delayedActions.TryDequeue( out var action ) )
{
action.Invoke();
}
}
private void OpenEditDescriptionPopup( int groupIdx )
{
_newDescriptionIdx = groupIdx;
_newDescription = groupIdx < 0 ? _mod.Description : _mod.Groups[ groupIdx ].Description;
ImGui.OpenPopup( "Edit Description" );
}
private void EditDescriptionPopup()
{
using var popup = ImRaii.Popup( "Edit Description" );
if( popup )
{
if( ImGui.IsWindowAppearing() )
{
ImGui.SetKeyboardFocusHere();
}
ImGui.InputTextMultiline( "##editDescription", ref _newDescription, 4096, ImGuiHelpers.ScaledVector2( 800, 800 ) );
ImGui.Dummy( _window._defaultSpace );
var buttonSize = ImGuiHelpers.ScaledVector2( 100, 0 );
var width = 2 * buttonSize.X
+ 4 * ImGui.GetStyle().FramePadding.X
+ ImGui.GetStyle().ItemSpacing.X;
ImGui.SetCursorPosX( ( 800 * ImGuiHelpers.GlobalScale - width ) / 2 );
var oldDescription = _newDescriptionIdx == DescriptionFieldIdx
? _mod.Description
: _mod.Groups[ _newDescriptionIdx ].Description;
var tooltip = _newDescription != oldDescription ? string.Empty : "No changes made yet.";
if( ImGuiUtil.DrawDisabledButton( "Save", buttonSize, tooltip, tooltip.Length > 0 ) )
{
if( _newDescriptionIdx == DescriptionFieldIdx )
{
Penumbra.ModManager.ChangeModDescription( _mod.Index, _newDescription );
}
else if( _newDescriptionIdx >= 0 )
{
Penumbra.ModManager.ChangeGroupDescription( _mod, _newDescriptionIdx, _newDescription );
}
ImGui.CloseCurrentPopup();
}
ImGui.SameLine();
if( ImGui.Button( "Cancel", buttonSize )
|| ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) )
{
_newDescriptionIdx = NoFieldIdx;
_newDescription = string.Empty;
ImGui.CloseCurrentPopup();
}
}
}
}
}

View file

@ -0,0 +1,214 @@
using System;
using System.Diagnostics;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.GameFonts;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.UI.Classes;
namespace Penumbra.UI;
public partial class ConfigWindow
{
private partial class ModPanel : IDisposable
{
// We use a big, nice game font for the title.
private readonly GameFontHandle _nameFont =
Dalamud.PluginInterface.UiBuilder.GetGameFontHandle( new GameFontStyle( GameFontFamilyAndSize.Jupiter23 ) );
public void Dispose()
{
_nameFont.Dispose();
}
// Header data.
private string _modName = string.Empty;
private string _modAuthor = string.Empty;
private string _modVersion = string.Empty;
private string _modWebsite = string.Empty;
private string _modWebsiteButton = string.Empty;
private bool _websiteValid;
private float _modNameWidth;
private float _modAuthorWidth;
private float _modVersionWidth;
private float _modWebsiteButtonWidth;
private float _secondRowWidth;
// Draw the header for the current mod,
// consisting of its name, version, author and website, if they exist.
private void DrawModHeader()
{
var offset = DrawModName();
DrawVersion( offset );
DrawSecondRow( offset );
}
// Draw the mod name in the game font with a 2px border, centered,
// with at least the width of the version space to each side.
private float DrawModName()
{
var decidingWidth = Math.Max( _secondRowWidth, ImGui.GetWindowWidth() );
var offsetWidth = ( decidingWidth - _modNameWidth ) / 2;
var offsetVersion = _modVersion.Length > 0
? _modVersionWidth + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X
: 0;
var offset = Math.Max( offsetWidth, offsetVersion );
if( offset > 0 )
{
ImGui.SetCursorPosX( offset );
}
using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.MetaInfoText );
using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale );
using var font = ImRaii.PushFont( _nameFont.ImFont, _nameFont.Available );
ImGuiUtil.DrawTextButton( _modName, Vector2.Zero, 0 );
return offset;
}
// Draw the version in the top-right corner.
private void DrawVersion( float offset )
{
var oldPos = ImGui.GetCursorPos();
ImGui.SetCursorPos( new Vector2( 2 * offset + _modNameWidth - _modVersionWidth - ImGui.GetStyle().WindowPadding.X,
ImGui.GetStyle().FramePadding.Y ) );
ImGuiUtil.TextColored( Colors.MetaInfoText, _modVersion );
ImGui.SetCursorPos( oldPos );
}
// Draw author and website if they exist. The website is a button if it is valid.
// Usually, author begins at the left boundary of the name,
// and website ends at the right boundary of the name.
// If their combined width is larger than the name, they are combined-centered.
private void DrawSecondRow( float offset )
{
if( _modAuthor.Length == 0 )
{
if( _modWebsiteButton.Length == 0 )
{
ImGui.NewLine();
return;
}
offset += ( _modNameWidth - _modWebsiteButtonWidth ) / 2;
ImGui.SetCursorPosX( offset );
DrawWebsite();
}
else if( _modWebsiteButton.Length == 0 )
{
offset += ( _modNameWidth - _modAuthorWidth ) / 2;
ImGui.SetCursorPosX( offset );
DrawAuthor();
}
else if( _secondRowWidth < _modNameWidth )
{
ImGui.SetCursorPosX( offset );
DrawAuthor();
ImGui.SameLine( offset + _modNameWidth - _modWebsiteButtonWidth );
DrawWebsite();
}
else
{
offset -= ( _secondRowWidth - _modNameWidth ) / 2;
if( offset > 0 )
{
ImGui.SetCursorPosX( offset );
}
DrawAuthor();
ImGui.SameLine();
DrawWebsite();
}
}
// Draw the author text.
private void DrawAuthor()
{
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero );
ImGuiUtil.TextColored( Colors.MetaInfoText, "by " );
ImGui.SameLine();
style.Pop();
ImGui.Text( _mod.Author );
}
// Draw either a website button if the source is a valid website address,
// or a source text if it is not.
private void DrawWebsite()
{
if( _websiteValid )
{
if( ImGui.SmallButton( _modWebsiteButton ) )
{
try
{
var process = new ProcessStartInfo( _modWebsite )
{
UseShellExecute = true,
};
Process.Start( process );
}
catch
{
// ignored
}
}
ImGuiUtil.HoverTooltip( _modWebsite );
}
else
{
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero );
ImGuiUtil.TextColored( Colors.MetaInfoText, "from " );
ImGui.SameLine();
style.Pop();
ImGui.Text( _mod.Website );
}
}
// Update all mod header data. Should someone change frame padding or item spacing,
// or his default font, this will break, but he will just have to select a different mod to restore.
private void UpdateModData()
{
// Name
var name = $" {_mod.Name} ";
if( name != _modName )
{
using var font = ImRaii.PushFont( _nameFont.ImFont, _nameFont.Available );
_modName = name;
_modNameWidth = ImGui.CalcTextSize( name ).X + 2 * ( ImGui.GetStyle().FramePadding.X + 2 * ImGuiHelpers.GlobalScale );
}
// Author
var author = _mod.Author.IsEmpty ? string.Empty : $"by {_mod.Author}";
if( author != _modAuthor )
{
_modAuthor = author;
_modAuthorWidth = ImGui.CalcTextSize( author ).X;
_secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X;
}
// Version
var version = _mod.Version.Length > 0 ? $"({_mod.Version})" : string.Empty;
if( version != _modVersion )
{
_modVersion = version;
_modVersionWidth = ImGui.CalcTextSize( version ).X;
}
// Website
if( _modWebsite != _mod.Website )
{
_modWebsite = _mod.Website;
_websiteValid = Uri.TryCreate( _modWebsite, UriKind.Absolute, out var uriResult )
&& ( uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp );
_modWebsiteButton = _websiteValid ? "Open Website" : _modWebsite.Length == 0 ? string.Empty : $"from {_modWebsite}";
_modWebsiteButtonWidth = _websiteValid
? ImGui.CalcTextSize( _modWebsiteButton ).X + 2 * ImGui.GetStyle().FramePadding.X
: ImGui.CalcTextSize( _modWebsiteButton ).X;
_secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X;
}
}
}
}

View file

@ -0,0 +1,207 @@
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Collections;
using Penumbra.Mods;
using Penumbra.UI.Classes;
namespace Penumbra.UI;
public partial class ConfigWindow
{
private partial class ModPanel
{
private ModSettings _settings = null!;
private ModCollection _collection = null!;
private bool _emptySetting;
private bool _inherited;
private SubList< ConflictCache.Conflict > _conflicts = SubList< ConflictCache.Conflict >.Empty;
private int? _currentPriority;
private void UpdateSettingsData( ModFileSystemSelector selector )
{
_settings = selector.SelectedSettings;
_collection = selector.SelectedSettingCollection;
_emptySetting = _settings == ModSettings.Empty;
_inherited = _collection != Penumbra.CollectionManager.Current;
_conflicts = Penumbra.CollectionManager.Current.ModConflicts( _mod.Index );
}
// Draw the whole settings tab as well as its contents.
private void DrawSettingsTab()
{
using var tab = DrawTab( SettingsTabHeader, Tabs.Settings );
if( !tab )
{
return;
}
using var child = ImRaii.Child( "##settings" );
if( !child )
{
return;
}
DrawInheritedWarning();
ImGui.Dummy( _window._defaultSpace );
DrawEnabledInput();
ImGui.SameLine();
DrawPriorityInput();
DrawRemoveSettings();
ImGui.Dummy( _window._defaultSpace );
for( var idx = 0; idx < _mod.Groups.Count; ++idx )
{
DrawSingleGroup( _mod.Groups[ idx ], idx );
}
for( var idx = 0; idx < _mod.Groups.Count; ++idx )
{
DrawMultiGroup( _mod.Groups[ idx ], idx );
}
}
// Draw a big red bar if the current setting is inherited.
private void DrawInheritedWarning()
{
if( !_inherited )
{
return;
}
using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg );
var width = new Vector2( ImGui.GetContentRegionAvail().X, 0 );
if( ImGui.Button( $"These settings are inherited from {_collection.Name}.", width ) )
{
Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, false );
}
ImGuiUtil.HoverTooltip( "You can click this button to copy the current settings to the current selection.\n"
+ "You can also just change any setting, which will copy the settings with the single setting changed to the current selection." );
}
// Draw a checkbox for the enabled status of the mod.
private void DrawEnabledInput()
{
var enabled = _settings.Enabled;
if( ImGui.Checkbox( "Enabled", ref enabled ) )
{
Penumbra.CollectionManager.Current.SetModState( _mod.Index, enabled );
}
}
// Draw a priority input.
// Priority is changed on deactivation of the input box.
private void DrawPriorityInput()
{
var priority = _currentPriority ?? _settings.Priority;
ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale );
if( ImGui.InputInt( "##Priority", ref priority, 0, 0 ) )
{
_currentPriority = priority;
}
if( ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue )
{
if( _currentPriority != _settings.Priority )
{
Penumbra.CollectionManager.Current.SetModPriority( _mod.Index, _currentPriority.Value );
}
_currentPriority = null;
}
ImGuiUtil.LabeledHelpMarker( "Priority", "Mods with higher priority take precedence before Mods with lower priority.\n"
+ "That means, if Mod A should overwrite changes from Mod B, Mod A should have higher priority than Mod B." );
}
// Draw a button to remove the current settings and inherit them instead
// on the top-right corner of the window/tab.
private void DrawRemoveSettings()
{
const string text = "Remove Settings";
if( _inherited || _emptySetting )
{
return;
}
var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0;
ImGui.SameLine( ImGui.GetWindowWidth() - ImGui.CalcTextSize( text ).X - ImGui.GetStyle().FramePadding.X * 2 - scroll);
if( ImGui.Button( text ) )
{
Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, true );
}
ImGuiUtil.HoverTooltip( "Remove current settings from this collection so that it can inherit them.\n"
+ "If no inherited collection has settings for this mod, it will be disabled." );
}
// Draw a single group selector as a combo box.
// If a description is provided, add a help marker besides it.
private void DrawSingleGroup( IModGroup group, int groupIdx )
{
if( group.Type != SelectType.Single || !group.IsOption )
{
return;
}
using var id = ImRaii.PushId( groupIdx );
var selectedOption = _emptySetting ? 0 : ( int )_settings.Settings[ groupIdx ];
ImGui.SetNextItemWidth( _window._inputTextWidth.X * 3 / 4 );
using var combo = ImRaii.Combo( string.Empty, group[ selectedOption ].Name );
if( combo )
{
for( var idx2 = 0; idx2 < group.Count; ++idx2 )
{
if( ImGui.Selectable( group[ idx2 ].Name, idx2 == selectedOption ) )
{
Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, ( uint )idx2 );
}
}
}
combo.Dispose();
ImGui.SameLine();
if( group.Description.Length > 0 )
{
ImGuiUtil.LabeledHelpMarker( group.Name, group.Description );
}
else
{
ImGui.Text( group.Name );
}
}
// Draw a multi group selector as a bordered set of checkboxes.
// If a description is provided, add a help marker in the title.
private void DrawMultiGroup( IModGroup group, int groupIdx )
{
if( group.Type != SelectType.Multi || !group.IsOption )
{
return;
}
using var id = ImRaii.PushId( groupIdx );
var flags = _emptySetting ? 0u : _settings.Settings[ groupIdx ];
Widget.BeginFramedGroup( group.Name, group.Description );
for( var idx2 = 0; idx2 < group.Count; ++idx2 )
{
var flag = 1u << idx2;
var setting = ( flags & flag ) != 0;
if( ImGui.Checkbox( group[ idx2 ].Name, ref setting ) )
{
flags = setting ? flags | flag : flags & ~flag;
Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, flags );
}
}
Widget.EndFramedGroup();
}
}
}

View file

@ -0,0 +1,178 @@
using System;
using System.ComponentModel.Design;
using System.Linq;
using System.Numerics;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.GameData.ByteString;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.UI.Classes;
namespace Penumbra.UI;
public partial class ConfigWindow
{
private partial class ModPanel
{
[Flags]
private enum Tabs
{
Description = 0x01,
Settings = 0x02,
ChangedItems = 0x04,
Conflicts = 0x08,
Edit = 0x10,
};
// We want to keep the preferred tab selected even if switching through mods.
private Tabs _preferredTab = Tabs.Settings;
private Tabs _availableTabs = 0;
// Required to use tabs that can not be closed but have a flag to set them open.
private static readonly Utf8String ConflictTabHeader = Utf8String.FromStringUnsafe( "Conflicts", false );
private static readonly Utf8String DescriptionTabHeader = Utf8String.FromStringUnsafe( "Description", false );
private static readonly Utf8String SettingsTabHeader = Utf8String.FromStringUnsafe( "Settings", false );
private static readonly Utf8String ChangedItemsTabHeader = Utf8String.FromStringUnsafe( "Changed Items", false );
private static readonly Utf8String EditModTabHeader = Utf8String.FromStringUnsafe( "Edit Mod", false );
private void DrawTabBar()
{
ImGui.Dummy( _window._defaultSpace );
using var tabBar = ImRaii.TabBar( "##ModTabs" );
if( !tabBar )
{
return;
}
_availableTabs = Tabs.Settings
| ( _mod.ChangedItems.Count > 0 ? Tabs.ChangedItems : 0 )
| ( _mod.Description.Length > 0 ? Tabs.Description : 0 )
| ( _conflicts.Count > 0 ? Tabs.Conflicts : 0 )
| ( Penumbra.Config.ShowAdvanced ? Tabs.Edit : 0 );
DrawSettingsTab();
DrawDescriptionTab();
DrawChangedItemsTab();
DrawConflictsTab();
DrawEditModTab();
}
// Just a simple text box with the wrapped description, if it exists.
private void DrawDescriptionTab()
{
using var tab = DrawTab( DescriptionTabHeader, Tabs.Description );
if( !tab )
{
return;
}
using var child = ImRaii.Child( "##description" );
if( !child )
{
return;
}
ImGui.TextWrapped( _mod.Description );
}
// A simple clipped list of changed items.
private void DrawChangedItemsTab()
{
using var tab = DrawTab( ChangedItemsTabHeader, Tabs.ChangedItems );
if( !tab )
{
return;
}
using var list = ImRaii.ListBox( "##changedItems", -Vector2.One );
if( !list )
{
return;
}
var zipList = ZipList.FromSortedList( _mod.ChangedItems );
var height = ImGui.GetTextLineHeight();
ImGuiClip.ClippedDraw( zipList, kvp => _window.DrawChangedItem( kvp.Item1, kvp.Item2 ), height );
}
// If any conflicts exist, show them in this tab.
private void DrawConflictsTab()
{
using var tab = DrawTab( ConflictTabHeader, Tabs.Conflicts );
if( !tab )
{
return;
}
using var box = ImRaii.ListBox( "##conflicts" );
if( !box )
{
return;
}
var conflicts = Penumbra.CollectionManager.Current.ModConflicts( _mod.Index );
Mod? oldBadMod = null;
using var indent = ImRaii.PushIndent( 0f );
foreach( var conflict in conflicts )
{
var badMod = Penumbra.ModManager[ conflict.Mod2 ];
if( badMod != oldBadMod )
{
if( oldBadMod != null )
{
indent.Pop( 30f );
}
if( ImGui.Selectable( badMod.Name ) )
{
_window._selector.SelectByValue( badMod );
}
ImGui.SameLine();
using var color = ImRaii.PushColor( ImGuiCol.Text, conflict.Mod1Priority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() );
ImGui.Text( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2 ].Settings!.Priority})" );
indent.Push( 30f );
}
if( conflict.Data is Utf8GamePath p )
{
unsafe
{
ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero );
}
}
else if( conflict.Data is MetaManipulation m )
{
ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty );
}
oldBadMod = badMod;
}
}
// Draw a tab by given name if it is available, and deal with changing the preferred tab.
private ImRaii.IEndObject DrawTab( Utf8String name, Tabs flag )
{
if( !_availableTabs.HasFlag( flag ) )
{
return ImRaii.IEndObject.Empty;
}
var flags = _preferredTab == flag ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None;
unsafe
{
var tab = ImRaii.TabItem( name.Path, flags );
if( ImGui.IsItemClicked() )
{
_preferredTab = flag;
}
return tab;
}
}
}
}

View file

@ -1,386 +1,15 @@
using System;
using System.Diagnostics;
using System.Numerics;
using Dalamud.Interface;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Collections;
using Penumbra.Mods;
using Penumbra.UI.Classes;
namespace Penumbra.UI;
public partial class ConfigWindow
{
private class ModPanel
{
private readonly ConfigWindow _window;
private bool _valid;
private bool _emptySetting;
private bool _inherited;
private ModFileSystem.Leaf _leaf = null!;
private Mod2 _mod = null!;
private ModSettings2 _settings = null!;
private ModCollection _collection = null!;
private string _lastWebsite = string.Empty;
private bool _websiteValid;
private string? _currentSortOrderPath;
private int? _currentPriority;
public ModPanel( ConfigWindow window )
=> _window = window;
private void Init( ModFileSystemSelector selector )
{
_valid = selector.Selected != null;
if( !_valid )
{
return;
}
_leaf = selector.SelectedLeaf!;
_mod = selector.Selected!;
_settings = selector.SelectedSettings;
_collection = selector.SelectedSettingCollection;
_emptySetting = _settings == ModSettings2.Empty;
_inherited = _collection != Penumbra.CollectionManager.Current;
}
public void Draw( ModFileSystemSelector selector )
{
Init( selector );
if( !_valid )
{
return;
}
DrawInheritedWarning();
DrawHeaderLine();
DrawFilesystemPath();
DrawEnabledInput();
ImGui.SameLine();
DrawPriorityInput();
DrawRemoveSettings();
DrawTabBar();
}
private void DrawDescriptionTab()
{
if( _mod.Description.Length == 0 )
{
return;
}
using var tab = ImRaii.TabItem( "Description" );
if( !tab )
{
return;
}
using var child = ImRaii.Child( "##tab" );
if( !child )
{
return;
}
ImGui.TextWrapped( _mod.Description );
}
private void DrawSettingsTab()
{
if( !_mod.HasOptions )
{
return;
}
using var tab = ImRaii.TabItem( "Settings" );
if( !tab )
{
return;
}
using var child = ImRaii.Child( "##tab" );
if( !child )
{
return;
}
for( var idx = 0; idx < _mod.Groups.Count; ++idx )
{
var group = _mod.Groups[ idx ];
if( group.Type == SelectType.Single && group.IsOption )
{
using var id = ImRaii.PushId( idx );
var selectedOption = _emptySetting ? 0 : ( int )_settings.Settings[ idx ];
ImGui.SetNextItemWidth( _window._inputTextWidth.X );
using var combo = ImRaii.Combo( string.Empty, group[ selectedOption ].Name );
if( combo )
{
for( var idx2 = 0; idx2 < group.Count; ++idx2 )
{
if( ImGui.Selectable( group[ idx2 ].Name, idx2 == selectedOption ) )
{
Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, idx, ( uint )idx2 );
}
}
}
combo.Dispose();
ImGui.SameLine();
if( group.Description.Length > 0 )
{
ImGuiUtil.LabeledHelpMarker( group.Name, group.Description );
}
else
{
ImGui.Text( group.Name );
}
}
}
// TODO add description
for( var idx = 0; idx < _mod.Groups.Count; ++idx )
{
var group = _mod.Groups[ idx ];
if( group.Type == SelectType.Multi && group.IsOption )
{
using var id = ImRaii.PushId( idx );
var flags = _emptySetting ? 0u : _settings.Settings[ idx ];
Widget.BeginFramedGroup( group.Name );
for( var idx2 = 0; idx2 < group.Count; ++idx2 )
{
var flag = 1u << idx2;
var setting = ( flags & flag ) != 0;
if( ImGui.Checkbox( group[ idx2 ].Name, ref setting ) )
{
flags = setting ? flags | flag : flags & ~flag;
Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, idx, flags );
}
}
Widget.EndFramedGroup();
}
}
}
private void DrawChangedItemsTab()
{
if( _mod.ChangedItems.Count == 0 )
{
return;
}
using var tab = ImRaii.TabItem( "Changed Items" );
if( !tab )
{
return;
}
using var list = ImRaii.ListBox( "##changedItems", -Vector2.One );
if( !list )
{
return;
}
foreach( var (name, data) in _mod.ChangedItems )
{
_window.DrawChangedItem( name, data );
}
}
private void DrawTabBar()
{
using var tabBar = ImRaii.TabBar( "##ModTabs" );
if( !tabBar )
{
return;
}
DrawDescriptionTab();
DrawSettingsTab();
DrawChangedItemsTab();
}
private void DrawInheritedWarning()
{
if( _inherited )
{
using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg );
var w = new Vector2( ImGui.GetContentRegionAvail().X, 0 );
if( ImGui.Button( $"These settings are inherited from {_collection.Name}.", w ) )
{
Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, false );
}
}
}
private void DrawPriorityInput()
{
var priority = _currentPriority ?? _settings.Priority;
ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale );
if( ImGui.InputInt( "Priority", ref priority, 0, 0 ) )
{
_currentPriority = priority;
}
if( ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue )
{
if( _currentPriority != _settings.Priority )
{
Penumbra.CollectionManager.Current.SetModPriority( _mod.Index, _currentPriority.Value );
}
_currentPriority = null;
}
}
private void DrawRemoveSettings()
{
if( _inherited )
{
return;
}
ImGui.SameLine();
if( ImGui.Button( "Remove Settings" ) )
{
Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, true );
}
ImGuiUtil.HoverTooltip( "Remove current settings from this collection so that it can inherit them.\n"
+ "If no inherited collection has settings for this mod, it will be disabled." );
}
private void DrawEnabledInput()
{
var enabled = _settings.Enabled;
if( ImGui.Checkbox( "Enabled", ref enabled ) )
{
Penumbra.CollectionManager.Current.SetModState( _mod.Index, enabled );
}
}
private void DrawFilesystemPath()
{
var fullName = _leaf.FullName();
var path = _currentSortOrderPath ?? fullName;
ImGui.SetNextItemWidth( 300 * ImGuiHelpers.GlobalScale );
if( ImGui.InputText( "Sort Order", ref path, 256 ) )
{
_currentSortOrderPath = path;
}
if( ImGui.IsItemDeactivatedAfterEdit() && _currentSortOrderPath != null )
{
if( _currentSortOrderPath != fullName )
{
_window._penumbra.ModFileSystem.RenameAndMove( _leaf, _currentSortOrderPath );
}
_currentSortOrderPath = null;
}
}
// Draw the first info line for the mod panel,
// containing all basic meta information.
private void DrawHeaderLine()
{
DrawName();
ImGui.SameLine();
DrawVersion();
ImGui.SameLine();
DrawAuthor();
ImGui.SameLine();
DrawWebsite();
}
// Draw the mod name.
private void DrawName()
{
ImGui.Text( _mod.Name.Text );
}
// Draw the author of the mod, if any.
private void DrawAuthor()
{
using var group = ImRaii.Group();
ImGuiUtil.TextColored( Colors.MetaInfoText, "by" );
ImGui.SameLine();
ImGui.Text( _mod.Author.IsEmpty ? "Unknown" : _mod.Author.Text );
}
// Draw the mod version, if any.
private void DrawVersion()
{
if( _mod.Version.Length > 0 )
{
ImGui.Text( $"(Version {_mod.Version})" );
}
else
{
ImGui.Dummy( Vector2.Zero );
}
}
// Update the last seen website and check for validity.
private void UpdateWebsite( string newWebsite )
{
if( _lastWebsite == newWebsite )
{
return;
}
_lastWebsite = newWebsite;
_websiteValid = Uri.TryCreate( _lastWebsite, UriKind.Absolute, out var uriResult )
&& ( uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp );
}
// Draw the website source either as a button to open the site,
// if it is a valid http website, or as pure text.
private void DrawWebsite()
{
UpdateWebsite( _mod.Website );
if( _lastWebsite.Length == 0 )
{
ImGui.Dummy( Vector2.Zero );
return;
}
using var group = ImRaii.Group();
if( _websiteValid )
{
if( ImGui.Button( "Open Website" ) )
{
try
{
var process = new ProcessStartInfo( _lastWebsite )
{
UseShellExecute = true,
};
Process.Start( process );
}
catch
{
// ignored
}
}
ImGuiUtil.HoverTooltip( _lastWebsite );
}
else
{
ImGuiUtil.TextColored( Colors.MetaInfoText, "from" );
ImGui.SameLine();
ImGui.Text( _lastWebsite );
}
}
}
}
public partial class ConfigWindow
{
public void DrawModsTab()
@ -401,14 +30,13 @@ public partial class ConfigWindow
using var group = ImRaii.Group();
DrawHeaderLine();
using var child = ImRaii.Child( "##ModsTabMod", -Vector2.One, true );
using var child = ImRaii.Child( "##ModsTabMod", -Vector2.One, true, ImGuiWindowFlags.HorizontalScrollbar );
if( child )
{
_modPanel.Draw( _selector );
}
}
// Draw the header line that can quick switch between collections.
private void DrawHeaderLine()
{
@ -466,4 +94,46 @@ public partial class ConfigWindow
? absoluteSize
: Math.Max( absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100 );
}
// The basic setup for the mod panel.
// Details are in other files.
private partial class ModPanel
{
private readonly ConfigWindow _window;
private bool _valid;
private ModFileSystem.Leaf _leaf = null!;
private Mod _mod = null!;
public ModPanel( ConfigWindow window )
{
_window = window;
}
public void Draw( ModFileSystemSelector selector )
{
Init( selector );
if( !_valid )
{
return;
}
DrawModHeader();
DrawTabBar();
}
private void Init( ModFileSystemSelector selector )
{
_valid = selector.Selected != null;
if( !_valid )
{
return;
}
_leaf = selector.SelectedLeaf!;
_mod = selector.Selected!;
UpdateSettingsData( selector );
UpdateModData();
}
}
}

View file

@ -47,7 +47,7 @@ public partial class ConfigWindow
DrawAdvancedSettings();
}
private string? _settingsNewModDirectory;
private string? _newModDirectory;
private readonly FileDialogManager _dialogManager = new();
private bool _dialogOpen;
@ -70,12 +70,18 @@ public partial class ConfigWindow
}
else
{
// TODO
//_dialogManager.OpenFolderDialog( "Choose Mod Directory", ( b, s ) =>
//{
// _newModDirectory = b ? s : _newModDirectory;
// _dialogOpen = false;
//}, _newModDirectory, false);
_newModDirectory ??= Penumbra.Config.ModDirectory;
var startDir = Directory.Exists( _newModDirectory )
? _newModDirectory
: Directory.Exists( Penumbra.Config.ModDirectory )
? Penumbra.Config.ModDirectory
: ".";
_dialogManager.OpenFolderDialog( "Choose Mod Directory", ( b, s ) =>
{
_newModDirectory = b ? s : _newModDirectory;
_dialogOpen = false;
}, startDir );
_dialogOpen = true;
}
}
@ -99,12 +105,12 @@ public partial class ConfigWindow
private void DrawRootFolder()
{
_settingsNewModDirectory ??= Penumbra.Config.ModDirectory;
_newModDirectory ??= Penumbra.Config.ModDirectory;
var spacing = 3 * ImGuiHelpers.GlobalScale;
using var group = ImRaii.Group();
ImGui.SetNextItemWidth( _window._inputTextWidth.X - spacing - ImGui.GetFrameHeight() );
var save = ImGui.InputText( "##rootDirectory", ref _settingsNewModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue );
var save = ImGui.InputText( "##rootDirectory", ref _newModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue );
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( spacing, 0 ) );
ImGui.SameLine();
DrawDirectoryPickerButton();
@ -121,14 +127,14 @@ public partial class ConfigWindow
var pos = ImGui.GetCursorPosX();
ImGui.NewLine();
if( Penumbra.Config.ModDirectory == _settingsNewModDirectory || _settingsNewModDirectory.Length == 0 )
if( Penumbra.Config.ModDirectory == _newModDirectory || _newModDirectory.Length == 0 )
{
return;
}
if( save || DrawPressEnterWarning( Penumbra.Config.ModDirectory, pos ) )
{
Penumbra.ModManager.DiscoverMods( _settingsNewModDirectory );
Penumbra.ModManager.DiscoverMods( _newModDirectory );
}
}
@ -137,12 +143,13 @@ public partial class ConfigWindow
{
DrawOpenDirectoryButton( 0, Penumbra.ModManager.BasePath, Penumbra.ModManager.Valid );
ImGui.SameLine();
if( ImGui.Button( "Rediscover Mods" ) )
var tt = Penumbra.ModManager.Valid
? "Force Penumbra to completely re-scan your root directory as if it was restarted."
: "The currently selected folder is not valid. Please select a different folder.";
if( ImGuiUtil.DrawDisabledButton( "Rediscover Mods", Vector2.Zero, tt, !Penumbra.ModManager.Valid ) )
{
Penumbra.ModManager.DiscoverMods();
}
ImGuiUtil.HoverTooltip( "Force Penumbra to completely re-scan your root directory as if it was restarted." );
}
private void DrawEnabledBox()

View file

@ -21,13 +21,14 @@ public sealed partial class ConfigWindow : Window, IDisposable
private readonly EffectiveTab _effectiveTab;
private readonly DebugTab _debugTab;
private readonly ResourceTab _resourceTab;
public readonly SubModEditWindow SubModPopup = new();
public ConfigWindow( Penumbra penumbra )
: base( GetLabel() )
{
_penumbra = penumbra;
_settingsTab = new SettingsTab( this );
_selector = new ModFileSystemSelector( _penumbra.ModFileSystem, new HashSet< Mod2 >() ); // TODO
_selector = new ModFileSystemSelector( _penumbra.ModFileSystem, new HashSet< Mod >() ); // TODO
_modPanel = new ModPanel( this );
_collectionsTab = new CollectionsTab( this );
_effectiveTab = new EffectiveTab();
@ -61,6 +62,7 @@ public sealed partial class ConfigWindow : Window, IDisposable
public void Dispose()
{
_selector.Dispose();
_modPanel.Dispose();
}
private static string GetLabel()
@ -70,10 +72,12 @@ public sealed partial class ConfigWindow : Window, IDisposable
private Vector2 _defaultSpace;
private Vector2 _inputTextWidth;
private Vector2 _iconButtonSize;
private void SetupSizes()
{
_defaultSpace = new Vector2( 0, 10 * ImGuiHelpers.GlobalScale );
_inputTextWidth = new Vector2( 350f * ImGuiHelpers.GlobalScale, 0 );
_iconButtonSize = new Vector2( ImGui.GetFrameHeight() );
}
}

View file

@ -9,7 +9,7 @@ namespace Penumbra.UI;
// using the Dalamud-provided collapsible submenu.
public class LaunchButton : IDisposable
{
private readonly ConfigWindow _configWindow;
private readonly ConfigWindow _configWindow;
private readonly TextureWrap? _icon;
private readonly TitleScreenMenu.TitleScreenMenuEntry? _entry;