Rename Mod BasePath to ModPath, add simple Directory Renaming and Reloading, some fixes, Cleanup EditWindow.

This commit is contained in:
Ottermandias 2022-05-02 16:19:24 +02:00
parent c416d044a4
commit 65bbece9cf
17 changed files with 636 additions and 368 deletions

View file

@ -20,9 +20,9 @@ public class ModsController : WebApiController
{
x.Second?.Enabled,
x.Second?.Priority,
FolderName = x.First.BasePath.Name,
FolderName = x.First.ModPath.Name,
x.First.Name,
BasePath = x.First.BasePath.FullName,
BasePath = x.First.ModPath.FullName,
Files = x.First.AllFiles,
} );
}

View file

@ -239,6 +239,9 @@ public partial class ModCollection
collection.Save();
}
OnModChangedActive( mod.TotalManipulations > 0, mod.Index );
break;
case ModPathChangeType.Reloaded:
OnModChangedActive( mod.TotalManipulations > 0, mod.Index );
break;
default: throw new ArgumentOutOfRangeException( nameof( type ), type, null );

View file

@ -47,7 +47,7 @@ public partial class ModCollection
var settings = _settings[ i ];
if( settings != null )
{
j.WritePropertyName( Penumbra.ModManager[ i ].BasePath.Name );
j.WritePropertyName( Penumbra.ModManager[ i ].ModPath.Name );
x.Serialize( j, new ModSettings.SavedSettings( settings, Penumbra.ModManager[ i ] ) );
}
}

View file

@ -92,11 +92,11 @@ public partial class ModCollection
// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion.
private bool AddMod( Mod mod )
{
if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var save ) )
if( _unusedSettings.TryGetValue( mod.ModPath.Name, out var save ) )
{
var ret = save.ToSettings( mod, out var settings );
_settings.Add( settings );
_unusedSettings.Remove( mod.BasePath.Name );
_unusedSettings.Remove( mod.ModPath.Name );
return ret;
}
@ -110,7 +110,7 @@ public partial class ModCollection
var settings = _settings[ idx ];
if( settings != null )
{
_unusedSettings.Add( mod.BasePath.Name, new ModSettings.SavedSettings( settings, mod ) );
_unusedSettings.Add( mod.ModPath.Name, new ModSettings.SavedSettings( settings, mod ) );
}
_settings.RemoveAt( idx );
@ -131,7 +131,7 @@ public partial class ModCollection
{
foreach( var (mod, setting) in Penumbra.ModManager.Zip( _settings ).Where( s => s.Second != null ) )
{
_unusedSettings[ mod.BasePath.Name ] = new ModSettings.SavedSettings( setting!, mod );
_unusedSettings[ mod.ModPath.Name ] = new ModSettings.SavedSettings( setting!, mod );
}
_settings.Clear();

View file

@ -41,7 +41,7 @@ public partial class Mod
var dict = new Dictionary< Utf8GamePath, FullPath >( UnusedFiles.Count );
foreach( var file in UnusedFiles )
{
var gamePath = file.ToGamePath( _mod.BasePath, out var g ) ? g : Utf8GamePath.Empty;
var gamePath = file.ToGamePath( _mod.ModPath, out var g ) ? g : Utf8GamePath.Empty;
if( !gamePath.IsEmpty && !dict.ContainsKey( gamePath ) )
{
dict.Add( gamePath, file );
@ -105,7 +105,7 @@ public partial class Mod
private static List<(FullPath, long)> GetAvailablePaths( Mod mod )
=> mod.BasePath.EnumerateDirectories()
=> mod.ModPath.EnumerateDirectories()
.SelectMany( d => d.EnumerateFiles( "*.*", SearchOption.AllDirectories ).Select( f => (new FullPath( f ), f.Length) ) )
.OrderBy( p => -p.Length ).ToList();
}

View file

@ -16,10 +16,86 @@ public partial class Mod
// Rename/Move a mod directory.
// Updates all collection settings and sort order settings.
public void MoveModDirectory( Index idx, DirectoryInfo newDirectory )
public void MoveModDirectory( int idx, string newName )
{
var mod = this[ idx ];
// TODO
var mod = this[ idx ];
var oldName = mod.Name;
var oldDirectory = mod.ModPath;
switch( NewDirectoryValid( oldDirectory.Name, newName, out var dir ) )
{
case NewDirectoryState.NonExisting:
// Nothing to do
break;
case NewDirectoryState.ExistsEmpty:
try
{
Directory.Delete( dir!.FullName );
}
catch( Exception e )
{
PluginLog.Error( $"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}" );
return;
}
break;
// Should be caught beforehand.
case NewDirectoryState.ExistsNonEmpty:
case NewDirectoryState.ExistsAsFile:
case NewDirectoryState.ContainsInvalidSymbols:
// Nothing to do at all.
case NewDirectoryState.Identical:
default:
return;
}
try
{
Directory.Move( oldDirectory.FullName, dir!.FullName );
}
catch( Exception e )
{
PluginLog.Error( $"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}" );
return;
}
dir.Refresh();
mod.ModPath = dir;
if( !mod.Reload( out var metaChange ) )
{
PluginLog.Error( $"Error reloading moved mod {mod.Name}." );
return;
}
ModPathChanged.Invoke( ModPathChangeType.Moved, mod, oldDirectory, BasePath );
if( metaChange != MetaChangeType.None )
{
ModMetaChanged?.Invoke( metaChange, mod, oldName );
}
}
// Reload a mod without changing its base directory.
// If the base directory does not exist anymore, the mod will be deleted.
public void ReloadMod( int idx )
{
var mod = this[ idx ];
var oldName = mod.Name;
if( !mod.Reload( out var metaChange ) )
{
PluginLog.Warning( mod.Name.Length == 0
? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead."
: $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead." );
DeleteMod( idx );
return;
}
ModPathChanged.Invoke( ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath );
if( metaChange != MetaChangeType.None )
{
ModMetaChanged?.Invoke( metaChange, mod, oldName );
}
}
// Delete a mod by its index.
@ -28,16 +104,16 @@ public partial class Mod
public void DeleteMod( int idx )
{
var mod = this[ idx ];
if( Directory.Exists( mod.BasePath.FullName ) )
if( Directory.Exists( mod.ModPath.FullName ) )
{
try
{
Directory.Delete( mod.BasePath.FullName, true );
PluginLog.Debug( "Deleted directory {Directory:l} for {Name:l}.", mod.BasePath.FullName, mod.Name );
Directory.Delete( mod.ModPath.FullName, true );
PluginLog.Debug( "Deleted directory {Directory:l} for {Name:l}.", mod.ModPath.FullName, mod.Name );
}
catch( Exception e )
{
PluginLog.Error( $"Could not delete the mod {mod.BasePath.Name}:\n{e}" );
PluginLog.Error( $"Could not delete the mod {mod.ModPath.Name}:\n{e}" );
}
}
@ -47,14 +123,14 @@ public partial class Mod
--remainingMod.Index;
}
ModPathChanged.Invoke( ModPathChangeType.Deleted, mod, mod.BasePath, null );
ModPathChanged.Invoke( ModPathChangeType.Deleted, mod, mod.ModPath, null );
PluginLog.Debug( "Deleted mod {Name:l}.", mod.Name );
}
// Load a new mod and add it to the manager if successful.
public void AddMod( DirectoryInfo modFolder )
{
if( _mods.Any( m => m.BasePath.Name == modFolder.Name ) )
if( _mods.Any( m => m.ModPath.Name == modFolder.Name ) )
{
return;
}
@ -67,10 +143,61 @@ public partial class Mod
mod.Index = _mods.Count;
_mods.Add( mod );
ModPathChanged.Invoke( ModPathChangeType.Added, mod, null, mod.BasePath );
PluginLog.Debug( "Added new mod {Name:l} from {Directory:l}.", mod.Name, modFolder.FullName );
ModPathChanged.Invoke( ModPathChangeType.Added, mod, null, mod.ModPath );
PluginLog.Debug( "Added new mod {Name:l} from {Directory:l}.", mod.Name, modFolder.FullName );
}
public enum NewDirectoryState
{
NonExisting,
ExistsEmpty,
ExistsNonEmpty,
ExistsAsFile,
ContainsInvalidSymbols,
Identical,
Empty,
}
// Return the state of the new potential name of a directory.
public static NewDirectoryState NewDirectoryValid( string oldName, string newName, out DirectoryInfo? directory )
{
directory = null;
if( newName.Length == 0 )
{
return NewDirectoryState.Empty;
}
if( oldName == newName )
{
return NewDirectoryState.Identical;
}
var fixedNewName = ReplaceBadXivSymbols( newName );
if( fixedNewName != newName )
{
return NewDirectoryState.ContainsInvalidSymbols;
}
directory = new DirectoryInfo( Path.Combine( Penumbra.ModManager.BasePath.FullName, fixedNewName ) );
if( File.Exists( directory.FullName ) )
{
return NewDirectoryState.ExistsAsFile;
}
if( !Directory.Exists( directory.FullName ) )
{
return NewDirectoryState.NonExisting;
}
if( directory.EnumerateFileSystemInfos().Any() )
{
return NewDirectoryState.ExistsNonEmpty;
}
return NewDirectoryState.ExistsEmpty;
}
// Add new mods to NewMods and remove deleted mods from NewMods.
private void OnModPathChange( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
DirectoryInfo? newDirectory )

View file

@ -55,7 +55,7 @@ public sealed partial class Mod
return;
}
group.DeleteFile( mod.BasePath, groupIdx );
group.DeleteFile( mod.ModPath, groupIdx );
var _ = group switch
{
@ -86,7 +86,7 @@ public sealed partial class Mod
{
var group = mod._groups[ groupIdx ];
mod._groups.RemoveAt( groupIdx );
group.DeleteFile( mod.BasePath, groupIdx );
group.DeleteFile( mod.ModPath, groupIdx );
ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1 );
}
@ -426,7 +426,7 @@ public sealed partial class Mod
}
else
{
IModGroup.SaveModGroup( mod._groups[ groupIdx ], mod.BasePath, groupIdx );
IModGroup.SaveModGroup( mod._groups[ groupIdx ], mod.ModPath, groupIdx );
}
}

View file

@ -8,37 +8,53 @@ public enum ModPathChangeType
Added,
Deleted,
Moved,
Reloaded,
}
public partial class Mod
{
public DirectoryInfo BasePath { get; private set; }
public DirectoryInfo ModPath { get; private set; }
public int Index { get; private set; } = -1;
private Mod( DirectoryInfo basePath )
=> BasePath = basePath;
private Mod( DirectoryInfo modPath )
=> ModPath = modPath;
private static Mod? LoadMod( DirectoryInfo basePath )
private static Mod? LoadMod( DirectoryInfo modPath )
{
basePath.Refresh();
if( !basePath.Exists )
modPath.Refresh();
if( !modPath.Exists )
{
PluginLog.Error( $"Supplied mod directory {basePath} does not exist." );
PluginLog.Error( $"Supplied mod directory {modPath} does not exist." );
return null;
}
var mod = new Mod( basePath );
mod.LoadMeta();
if( mod.Name.Length == 0 )
var mod = new Mod( modPath );
if( !mod.Reload(out _) )
{
PluginLog.Error( $"Mod at {basePath} without name is not supported." );
// Can not be base path not existing because that is checked before.
PluginLog.Error( $"Mod at {modPath} without name is not supported." );
}
mod.LoadDefaultOption();
mod.LoadAllGroups();
mod.ComputeChangedItems();
mod.SetCounts();
return mod;
}
private bool Reload(out MetaChangeType metaChange)
{
metaChange = MetaChangeType.Deletion;
ModPath.Refresh();
if( !ModPath.Exists )
return false;
metaChange = LoadMeta();
if( metaChange.HasFlag(MetaChangeType.Deletion) || Name.Length == 0 )
{
return false;
}
LoadDefaultOption();
LoadAllGroups();
ComputeChangedItems();
SetCounts();
return true;
}
}

View file

@ -141,7 +141,7 @@ public partial class Mod
// XIV can not deal with non-ascii symbols in a path,
// and the path must obviously be valid itself.
private static string ReplaceBadXivSymbols( string s, string replacement = "_" )
public static string ReplaceBadXivSymbols( string s, string replacement = "_" )
{
StringBuilder sb = new(s.Length);
foreach( var c in s )

View file

@ -59,12 +59,12 @@ public partial class Mod
.Select( p => p.Value );
public IEnumerable< FileInfo > GroupFiles
=> BasePath.EnumerateFiles( "group_*.json" );
=> ModPath.EnumerateFiles( "group_*.json" );
public List< FullPath > FindUnusedFiles()
{
var modFiles = AllFiles.ToHashSet();
return BasePath.EnumerateDirectories()
return ModPath.EnumerateDirectories()
.SelectMany( f => f.EnumerateFiles( "*", SearchOption.AllDirectories ) )
.Select( f => new FullPath( f ) )
.Where( f => !modFiles.Contains( f ) )
@ -107,7 +107,7 @@ public partial class Mod
_groups.Clear();
foreach( var file in GroupFiles )
{
var group = LoadModGroup( file, BasePath );
var group = LoadModGroup( file, ModPath );
if( group != null )
{
_groups.Add( group );
@ -136,7 +136,7 @@ public partial class Mod
foreach( var (group, index) in _groups.WithIndex() )
{
IModGroup.SaveModGroup( group, BasePath, index );
IModGroup.SaveModGroup( group, ModPath, index );
}
}
}

View file

@ -66,7 +66,7 @@ public sealed partial class Mod
foreach( var unusedFile in mod.FindUnusedFiles().Where( f => !seenMetaFiles.Contains( f ) ) )
{
if( unusedFile.ToGamePath( mod.BasePath, out var gamePath )
if( unusedFile.ToGamePath( mod.ModPath, out var gamePath )
&& !mod._default.FileData.TryAdd( gamePath, unusedFile ) )
{
PluginLog.Error( $"Could not add {gamePath} because it already points to {mod._default.FileData[ gamePath ]}." );
@ -80,10 +80,10 @@ public sealed partial class Mod
mod._default.FileSwapData.Add( gamePath, swapPath );
}
mod._default.IncorporateMetaChanges( mod.BasePath, true );
mod._default.IncorporateMetaChanges( mod.ModPath, true );
foreach( var (group, index) in mod.Groups.WithIndex() )
{
IModGroup.SaveModGroup( group, mod.BasePath, index );
IModGroup.SaveModGroup( group, mod.ModPath, index );
}
// Delete meta files.
@ -100,7 +100,7 @@ public sealed partial class Mod
}
// Delete old meta files.
var oldMetaFile = Path.Combine( mod.BasePath.FullName, "metadata_manipulations.json" );
var oldMetaFile = Path.Combine( mod.ModPath.FullName, "metadata_manipulations.json" );
if( File.Exists( oldMetaFile ) )
{
try
@ -141,14 +141,14 @@ public sealed partial class Mod
mod._groups.Add( newMultiGroup );
foreach( var option in group.Options )
{
newMultiGroup.PrioritizedOptions.Add( ( SubModFromOption( mod.BasePath, option, seenMetaFiles ), optionPriority++ ) );
newMultiGroup.PrioritizedOptions.Add( ( SubModFromOption( mod.ModPath, option, seenMetaFiles ), optionPriority++ ) );
}
break;
case SelectType.Single:
if( group.Options.Count == 1 )
{
AddFilesToSubMod( mod._default, mod.BasePath, group.Options[ 0 ], seenMetaFiles );
AddFilesToSubMod( mod._default, mod.ModPath, group.Options[ 0 ], seenMetaFiles );
return;
}
@ -161,7 +161,7 @@ public sealed partial class Mod
mod._groups.Add( newSingleGroup );
foreach( var option in group.Options )
{
newSingleGroup.OptionData.Add( SubModFromOption( mod.BasePath, option, seenMetaFiles ) );
newSingleGroup.OptionData.Add( SubModFromOption( mod.ModPath, option, seenMetaFiles ) );
}
break;

View file

@ -34,14 +34,14 @@ public sealed partial class Mod
public long ImportDate { get; private set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
internal FileInfo MetaFile
=> new(Path.Combine( BasePath.FullName, "meta.json" ));
=> new(Path.Combine( ModPath.FullName, "meta.json" ));
private MetaChangeType LoadMeta()
{
var metaFile = MetaFile;
if( !File.Exists( metaFile.FullName ) )
{
PluginLog.Debug( "No mod meta found for {ModLocation}.", BasePath.Name );
PluginLog.Debug( "No mod meta found for {ModLocation}.", ModPath.Name );
return MetaChangeType.Deletion;
}

View file

@ -102,12 +102,15 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable
case ModPathChangeType.Moved:
Save();
break;
case ModPathChangeType.Reloaded:
// Nothing
break;
}
}
// Used for saving and loading.
private static string ModToIdentifier( Mod mod )
=> mod.BasePath.Name;
=> mod.ModPath.Name;
private static string ModToName( Mod mod )
=> mod.Name.Text.FixName();

View file

@ -14,7 +14,7 @@ namespace Penumbra.Mods;
public partial class Mod
{
internal string DefaultFile
=> Path.Combine( BasePath.FullName, "default_mod.json" );
=> Path.Combine( ModPath.FullName, "default_mod.json" );
// The default mod contains setting-independent sets of file replacements, file swaps and meta changes.
// Every mod has an default mod, though it may be empty.
@ -33,7 +33,7 @@ public partial class Mod
{
Formatting = Formatting.Indented,
};
ISubMod.WriteSubMod( j, serializer, _default, BasePath, 0 );
ISubMod.WriteSubMod( j, serializer, _default, ModPath, 0 );
}
private void LoadDefaultOption()
@ -43,11 +43,11 @@ public partial class Mod
{
if( !File.Exists( defaultFile ) )
{
_default.Load( BasePath, new JObject(), out _ );
_default.Load( ModPath, new JObject(), out _ );
}
else
{
_default.Load( BasePath, JObject.Parse( File.ReadAllText( defaultFile ) ), out _ );
_default.Load( ModPath, JObject.Parse( File.ReadAllText( defaultFile ) ), out _ );
}
}
catch( Exception e )

View file

@ -147,7 +147,7 @@ public class ModEditWindow : Window, IDisposable
foreach( var (set, size, hash) in _editor.Duplicates.Where( s => s.Paths.Length > 1 ) )
{
ImGui.TableNextColumn();
using var tree = ImRaii.TreeNode( set[ 0 ].FullName[ ( _mod!.BasePath.FullName.Length + 1 ).. ],
using var tree = ImRaii.TreeNode( set[ 0 ].FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ],
ImGuiTreeNodeFlags.NoTreePushOnOpen );
ImGui.TableNextColumn();
ImGuiUtil.RightAlign( Functions.HumanReadableSize( size ) );
@ -174,7 +174,7 @@ public class ModEditWindow : Window, IDisposable
{
ImGui.TableNextColumn();
ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 );
using var node = ImRaii.TreeNode( duplicate.FullName[ ( _mod!.BasePath.FullName.Length + 1 ).. ], ImGuiTreeNodeFlags.Leaf );
using var node = ImRaii.TreeNode( duplicate.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ], ImGuiTreeNodeFlags.Leaf );
ImGui.TableNextColumn();
ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 );
ImGui.TableNextColumn();

View file

@ -137,6 +137,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
{
var newDir = Mod.CreateModFolder( Penumbra.ModManager.BasePath, _newModName );
Mod.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty );
Mod.CreateDefaultFiles( newDir );
Penumbra.ModManager.AddMod( newDir );
_newModName = string.Empty;
}
@ -341,7 +342,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
private void StoreCurrentSelection()
{
_lastSelectedDirectory = Selected?.BasePath.FullName ?? string.Empty;
_lastSelectedDirectory = Selected?.ModPath.FullName ?? string.Empty;
ClearSelection();
}
@ -350,7 +351,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
if( _lastSelectedDirectory.Length > 0 )
{
base.SelectedLeaf = ( ModFileSystem.Leaf? )FileSystem.Root.GetAllDescendants( SortMode.Lexicographical )
.FirstOrDefault( l => l is ModFileSystem.Leaf m && m.Value.BasePath.FullName == _lastSelectedDirectory );
.FirstOrDefault( l => l is ModFileSystem.Leaf m && m.Value.ModPath.FullName == _lastSelectedDirectory );
OnSelectionChange( null, base.SelectedLeaf?.Value, default );
_lastSelectedDirectory = string.Empty;
}

View file

@ -42,13 +42,14 @@ public partial class ConfigWindow
EditRegularMeta();
ImGui.Dummy( _window._defaultSpace );
if( TextInput( "Mod Path", PathFieldIdx, NoFieldIdx, _leaf.FullName(), out var newPath, 256, _window._inputTextWidth.X ) )
if( Input.Text( "Mod Path", Input.Path, Input.None, _leaf.FullName(), out var newPath, 256,
_window._inputTextWidth.X ) )
{
_window._penumbra.ModFileSystem.RenameAndMove( _leaf, newPath );
}
ImGui.Dummy( _window._defaultSpace );
DrawAddOptionGroupInput();
AddOptionGroup.Draw( _window, _mod );
ImGui.Dummy( _window._defaultSpace );
for( var groupIdx = 0; groupIdx < _mod.Groups.Count; ++groupIdx )
@ -57,110 +58,59 @@ public partial class ConfigWindow
}
EndActions();
EditDescriptionPopup();
}
// Do some edits outside of iterations.
private readonly Queue< Action > _delayedActions = new();
// Text input to add a new option group at the end of the current groups.
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(), _window._iconButtonSize,
tt, !nameValid, true ) )
{
Penumbra.ModManager.AddModGroup( _mod, SelectType.Single, _newGroupName );
_newGroupName = string.Empty;
}
}
private string _materialSuffixFrom = string.Empty;
private string _materialSuffixTo = string.Empty;
// A row of three buttonSizes and a help marker that can be used for material suffix changing.
private void DrawChangeMaterialSuffix( Vector2 buttonSize )
{
ImGui.SetNextItemWidth( buttonSize.X );
ImGui.InputTextWithHint( "##suffixFrom", "From...", ref _materialSuffixFrom, 32 );
ImGui.SameLine();
var disabled = !ModelChanger.ValidStrings( _materialSuffixFrom, _materialSuffixTo );
var tt = _materialSuffixTo.Length == 0 ? "Please enter a target suffix."
: _materialSuffixFrom == _materialSuffixTo ? "The source and target are identical."
: disabled ? "The suffices are not valid suffices."
: _materialSuffixFrom.Length == 0 ? "Convert all skin material suffices to the target."
: $"Convert all skin material suffices that are currently {_materialSuffixFrom} to {_materialSuffixTo}.";
if( ImGuiUtil.DrawDisabledButton( "Change Material Suffix", buttonSize, tt, disabled ) )
{
ModelChanger.ChangeModMaterials( _mod, _materialSuffixFrom, _materialSuffixTo );
}
ImGui.SameLine();
ImGui.SetNextItemWidth( buttonSize.X );
ImGui.InputTextWithHint( "##suffixTo", "To...", ref _materialSuffixTo, 32 );
ImGui.SameLine();
ImGuiComponents.HelpMarker(
"Model files refer to the skin material they should use. This skin material is always the same, but modders have started using different suffices to differentiate between body types.\n"
+ "This option allows you to switch the suffix of all model files to another. This changes the files, so you do this on your own risk.\n"
+ "If you do not know what the currently used suffix of this mod is, you can leave 'From' blank and it will replace all suffices with 'To', instead of only the matching ones." );
DescriptionEdit.DrawPopup( _window );
}
// The general edit row for non-detailed mod edits.
private void EditButtons()
{
var buttonSize = new Vector2( 150 * ImGuiHelpers.GlobalScale, 0 );
var folderExists = Directory.Exists( _mod.BasePath.FullName );
var folderExists = Directory.Exists( _mod.ModPath.FullName );
var tt = folderExists
? $"Open \"{_mod.BasePath.FullName}\" in the file explorer of your choice."
: $"Mod directory \"{_mod.BasePath.FullName}\" does not exist.";
? $"Open \"{_mod.ModPath.FullName}\" in the file explorer of your choice."
: $"Mod directory \"{_mod.ModPath.FullName}\" does not exist.";
if( ImGuiUtil.DrawDisabledButton( "Open Mod Directory", buttonSize, tt, !folderExists ) )
{
Process.Start( new ProcessStartInfo( _mod.BasePath.FullName ) { UseShellExecute = true } );
Process.Start( new ProcessStartInfo( _mod.ModPath.FullName ) { UseShellExecute = true } );
}
ImGui.SameLine();
if( ImGuiUtil.DrawDisabledButton( "Reload Mod", buttonSize, "Reload the current mod from its files.\n"
+ "If the mod directory or meta file do not exist anymore or if the new mod name is empty, the mod is deleted instead.",
false ) )
{
Penumbra.ModManager.ReloadMod( _mod.Index );
}
ImGui.SameLine();
ImGuiUtil.DrawDisabledButton( "Rename Mod Directory", buttonSize, "Not implemented yet", true );
ImGui.SameLine();
ImGuiUtil.DrawDisabledButton( "Reload Mod", buttonSize, "Not implemented yet", true );
MoveDirectory.Draw( _mod, buttonSize );
DrawChangeMaterialSuffix( buttonSize );
MaterialSuffix.Draw( _mod, buttonSize );
ImGui.Dummy( _window._defaultSpace );
}
// 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;
// Anything about editing the regular meta information about the mod.
private void EditRegularMeta()
{
if( TextInput( "Name", NameFieldIdx, NoFieldIdx, _mod.Name, out var newName, 256, _window._inputTextWidth.X ) )
if( Input.Text( "Name", Input.Name, Input.None, _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 ) )
if( Input.Text( "Author", Input.Author, Input.None, _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 ) )
if( Input.Text( "Version", Input.Version, Input.None, _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 ) )
if( Input.Text( "Website", Input.Website, Input.None, _mod.Website, out var newWebsite, 256,
_window._inputTextWidth.X ) )
{
Penumbra.ModManager.ChangeModWebsite( _mod.Index, newWebsite );
}
@ -171,7 +121,7 @@ public partial class ConfigWindow
var reducedSize = new Vector2( _window._inputTextWidth.X - _window._iconButtonSize.X - spacing.X, 0 );
if( ImGui.Button( "Edit Description", reducedSize ) )
{
_delayedActions.Enqueue( () => OpenEditDescriptionPopup( DescriptionFieldIdx ) );
_delayedActions.Enqueue( () => DescriptionEdit.OpenPopup( _mod, Input.Description ) );
}
ImGui.SameLine();
@ -204,18 +154,199 @@ public partial class ConfigWindow
}
}
// Do some edits outside of iterations.
private readonly Queue< Action > _delayedActions = new();
// Temporary strings
private string? _currentEdit;
private int? _currentGroupPriority;
private int _currentField = -1;
private int _optionIndex = -1;
// Delete a marked group or option outside of iteration.
private void EndActions()
{
while( _delayedActions.TryDequeue( out var action ) )
{
action.Invoke();
}
}
private int _newOptionNameIdx = -1;
private string _newGroupName = string.Empty;
private string _newOptionName = string.Empty;
private string _newDescription = string.Empty;
private int _newDescriptionIdx = -1;
// Text input to add a new option group at the end of the current groups.
private static class AddOptionGroup
{
private static string _newGroupName = string.Empty;
public static void Draw( ConfigWindow window, Mod mod )
{
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(), window._iconButtonSize,
tt, !nameValid, true ) )
{
Penumbra.ModManager.AddModGroup( mod, SelectType.Single, _newGroupName );
_newGroupName = string.Empty;
}
}
}
// A row of three buttonSizes and a help marker that can be used for material suffix changing.
private static class MaterialSuffix
{
private static string _materialSuffixFrom = string.Empty;
private static string _materialSuffixTo = string.Empty;
public static void Draw( Mod mod, Vector2 buttonSize )
{
ImGui.SetNextItemWidth( buttonSize.X );
ImGui.InputTextWithHint( "##suffixFrom", "From...", ref _materialSuffixFrom, 32 );
ImGui.SameLine();
ImGui.SetNextItemWidth( buttonSize.X );
ImGui.InputTextWithHint( "##suffixTo", "To...", ref _materialSuffixTo, 32 );
ImGui.SameLine();
var disabled = !ModelChanger.ValidStrings( _materialSuffixFrom, _materialSuffixTo );
var tt = _materialSuffixTo.Length == 0 ? "Please enter a target suffix."
: _materialSuffixFrom == _materialSuffixTo ? "The source and target are identical."
: disabled ? "The suffices are not valid suffices."
: _materialSuffixFrom.Length == 0 ? "Convert all skin material suffices to the target."
: $"Convert all skin material suffices that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'.";
if( ImGuiUtil.DrawDisabledButton( "Change Material Suffix", buttonSize, tt, disabled ) )
{
ModelChanger.ChangeModMaterials( mod, _materialSuffixFrom, _materialSuffixTo );
}
ImGui.SameLine();
ImGuiComponents.HelpMarker(
"Model files refer to the skin material they should use. This skin material is always the same, but modders have started using different suffices to differentiate between body types.\n"
+ "This option allows you to switch the suffix of all model files to another. This changes the files, so you do this on your own risk.\n"
+ "If you do not know what the currently used suffix of this mod is, you can leave 'From' blank and it will replace all suffices with 'To', instead of only the matching ones." );
}
}
// A text input for the new directory name and a button to apply the move.
private static class MoveDirectory
{
private static string? _currentModDirectory;
private static Mod? _modForDirectory;
private static Mod.Manager.NewDirectoryState _state = Mod.Manager.NewDirectoryState.Identical;
public static void Draw( Mod mod, Vector2 buttonSize )
{
ImGui.SetNextItemWidth( buttonSize.X * 2 + ImGui.GetStyle().ItemSpacing.X );
var tmp = _currentModDirectory ?? mod.ModPath.Name;
if( mod != _modForDirectory )
{
tmp = mod.ModPath.Name;
_currentModDirectory = null;
_state = Mod.Manager.NewDirectoryState.Identical;
}
if( ImGui.InputText( "##newModMove", ref tmp, 64 ) )
{
_currentModDirectory = tmp;
_modForDirectory = mod;
_state = Mod.Manager.NewDirectoryValid( mod.ModPath.Name, _currentModDirectory, out _ );
}
var (disabled, tt) = _state switch
{
Mod.Manager.NewDirectoryState.Identical => ( true, "Current directory name is identical to new one." ),
Mod.Manager.NewDirectoryState.Empty => ( true, "Please enter a new directory name first." ),
Mod.Manager.NewDirectoryState.NonExisting => ( false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}." ),
Mod.Manager.NewDirectoryState.ExistsEmpty => ( false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}." ),
Mod.Manager.NewDirectoryState.ExistsNonEmpty => ( true, $"{_currentModDirectory} already exists and is not empty." ),
Mod.Manager.NewDirectoryState.ExistsAsFile => ( true, $"{_currentModDirectory} exists as a file." ),
Mod.Manager.NewDirectoryState.ContainsInvalidSymbols => ( true,
$"{_currentModDirectory} contains invalid symbols for FFXIV." ),
_ => ( true, "Unknown error." ),
};
ImGui.SameLine();
if( ImGuiUtil.DrawDisabledButton( "Rename Mod Directory", buttonSize, tt, disabled ) && _currentModDirectory != null )
{
Penumbra.ModManager.MoveModDirectory( mod.Index, _currentModDirectory );
_currentModDirectory = null;
_state = Mod.Manager.NewDirectoryState.Identical;
}
ImGui.SameLine();
ImGuiComponents.HelpMarker(
"The mod directory name is used to correspond stored settings and sort orders, otherwise it has no influence on anything that is displayed.\n"
+ "This can currently not be used on pre-existing folders and does not support merges or overwriting." );
}
}
// Open a popup to edit a multi-line mod or option description.
private static class DescriptionEdit
{
private const string PopupName = "Edit Description";
private static string _newDescription = string.Empty;
private static int _newDescriptionIdx = -1;
private static Mod? _mod;
public static void OpenPopup( Mod mod, int groupIdx )
{
_newDescriptionIdx = groupIdx;
_newDescription = groupIdx < 0 ? mod.Description : mod.Groups[ groupIdx ].Description;
_mod = mod;
ImGui.OpenPopup( PopupName );
}
public static void DrawPopup( ConfigWindow window )
{
if( _mod == null )
{
return;
}
using var popup = ImRaii.Popup( PopupName );
if( !popup )
{
return;
}
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 == Input.Description
? _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 ) )
{
switch( _newDescriptionIdx )
{
case Input.Description:
Penumbra.ModManager.ChangeModDescription( _mod.Index, _newDescription );
break;
case >= 0:
Penumbra.ModManager.ChangeGroupDescription( _mod, _newDescriptionIdx, _newDescription );
break;
}
ImGui.CloseCurrentPopup();
}
ImGui.SameLine();
if( ImGui.Button( "Cancel", buttonSize )
|| ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) )
{
_newDescriptionIdx = Input.None;
_newDescription = string.Empty;
ImGui.CloseCurrentPopup();
}
}
}
private void EditGroup( int groupIdx )
{
@ -226,7 +357,7 @@ public partial class ConfigWindow
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 ) )
if( Input.Text( "##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, _window._inputTextWidth.X ) )
{
Penumbra.ModManager.RenameModGroup( _mod, groupIdx, newGroupName );
}
@ -244,33 +375,19 @@ public partial class ConfigWindow
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), _window._iconButtonSize,
"Edit group description.", false, true ) )
{
_delayedActions.Enqueue( () => OpenEditDescriptionPopup( groupIdx ) );
_delayedActions.Enqueue( () => DescriptionEdit.OpenPopup( _mod, groupIdx ) );
}
ImGui.SameLine();
if( PriorityInput( "##Priority", groupIdx, NoFieldIdx, group.Priority, out var priority, 50 * ImGuiHelpers.GlobalScale ) )
if( Input.Priority( "##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * ImGuiHelpers.GlobalScale ) )
{
Penumbra.ModManager.ChangeGroupPriority( _mod, groupIdx, priority );
}
ImGuiUtil.HoverTooltip( "Group Priority" );
ImGui.SetNextItemWidth( _window._inputTextWidth.X - 3 * _window._iconButtonSize.X - 12 * 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 );
}
}
}
}
DrawGroupCombo( group, groupIdx );
ImGui.SameLine();
var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}.";
@ -291,7 +408,7 @@ public partial class ConfigWindow
}
ImGui.SameLine();
var fileName = group.FileName( _mod.BasePath, groupIdx );
var fileName = group.FileName( _mod.ModPath, groupIdx );
var fileExists = File.Exists( fileName );
tt = fileExists
? $"Open the {group.Name} json file in the text editor of your choice."
@ -303,19 +420,92 @@ public partial class ConfigWindow
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, _window._iconButtonSize.X );
ImGui.TableSetupColumn( "edit", ImGuiTableColumnFlags.WidthFixed, _window._iconButtonSize.X );
ImGui.TableSetupColumn( "priority", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale );
if( table )
OptionTable.Draw( this, groupIdx );
}
// Draw the table displaying all options and the add new option line.
private static class OptionTable
{
private const string DragDropLabel = "##DragOption";
private static int _newOptionNameIdx = -1;
private static string _newOptionName = string.Empty;
private static int _dragDropGroupIdx = -1;
private static int _dragDropOptionIdx = -1;
public static void Draw( ModPanel panel, int groupIdx )
{
for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx )
using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.SizingFixedFit );
if( !table )
{
EditOption( group, groupIdx, optionIdx );
return;
}
ImGui.TableSetupColumn( "idx", ImGuiTableColumnFlags.WidthFixed, 60 * ImGuiHelpers.GlobalScale );
ImGui.TableSetupColumn( "name", ImGuiTableColumnFlags.WidthFixed,
panel._window._inputTextWidth.X - 62 * ImGuiHelpers.GlobalScale );
ImGui.TableSetupColumn( "delete", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X );
ImGui.TableSetupColumn( "edit", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X );
ImGui.TableSetupColumn( "priority", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale );
var group = panel._mod.Groups[ groupIdx ];
for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx )
{
EditOption( panel, group, groupIdx, optionIdx );
}
DrawNewOption( panel._mod, groupIdx, panel._window._iconButtonSize );
}
// Draw a line for a single option.
private static void EditOption( ModPanel panel, 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}" );
Source( group, groupIdx, optionIdx );
Target( panel, group, groupIdx, optionIdx );
ImGui.TableNextColumn();
if( Input.Text( "##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1 ) )
{
Penumbra.ModManager.RenameOption( panel._mod, groupIdx, optionIdx, newOptionName );
}
ImGui.TableNextColumn();
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), panel._window._iconButtonSize,
"Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true ) )
{
panel._delayedActions.Enqueue( () => Penumbra.ModManager.DeleteOption( panel._mod, groupIdx, optionIdx ) );
}
ImGui.TableNextColumn();
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), panel._window._iconButtonSize,
"Edit this option.", false, true ) )
{
panel._window.ModEditPopup.ChangeMod( panel._mod );
panel._window.ModEditPopup.ChangeOption( groupIdx, optionIdx );
panel._window.ModEditPopup.IsOpen = true;
}
ImGui.TableNextColumn();
if( group.Type == SelectType.Multi )
{
if( Input.Priority( "##Priority", groupIdx, optionIdx, group.OptionPriority( optionIdx ), out var priority,
50 * ImGuiHelpers.GlobalScale ) )
{
Penumbra.ModManager.ChangeOptionPriority( panel._mod, groupIdx, optionIdx, priority );
}
ImGuiUtil.HoverTooltip( "Option priority." );
}
}
// Draw the line to add a new option.
private static void DrawNewOption( Mod mod, int groupIdx, Vector2 iconButtonSize )
{
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.SetNextItemWidth( -1 );
@ -327,233 +517,161 @@ public partial class ConfigWindow
}
ImGui.TableNextColumn();
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), _window._iconButtonSize,
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconButtonSize,
"Add a new option to this group.", _newOptionName.Length == 0 || _newOptionNameIdx != groupIdx, true ) )
{
Penumbra.ModManager.AddOption( _mod, groupIdx, _newOptionName );
Penumbra.ModManager.AddOption( mod, groupIdx, _newOptionName );
_newOptionName = string.Empty;
}
}
}
private static string GroupTypeName( SelectType type )
=> type switch
// Handle drag and drop to move options inside a group or into another group.
private static void Source( IModGroup group, int groupIdx, int optionIdx )
{
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 )
using var source = ImRaii.DragDropSource();
if( !source )
{
if( ImGui.SetDragDropPayload( label, IntPtr.Zero, 0 ) )
{
_dragDropGroupIdx = groupIdx;
_dragDropOptionIdx = optionIdx;
}
ImGui.TextUnformatted( $"Dragging option {group[ optionIdx ].Name} from group {group.Name}..." );
return;
}
if( ImGui.SetDragDropPayload( DragDropLabel, IntPtr.Zero, 0 ) )
{
_dragDropGroupIdx = groupIdx;
_dragDropOptionIdx = optionIdx;
}
ImGui.TextUnformatted( $"Dragging option {group[ optionIdx ].Name} from group {group.Name}..." );
}
// TODO drag options to other groups without options.
using( var target = ImRaii.DragDropTarget() )
private static void Target( ModPanel panel, IModGroup group, int groupIdx, int optionIdx )
{
if( target.Success && ImGuiUtil.IsDropping( label ) )
// TODO drag options to other groups without options.
using var target = ImRaii.DragDropTarget();
if( !target.Success || !ImGuiUtil.IsDropping( DragDropLabel ) )
{
if( _dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0 )
return;
}
if( _dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0 )
{
if( _dragDropGroupIdx == groupIdx )
{
if( _dragDropGroupIdx == groupIdx )
var sourceOption = _dragDropOptionIdx;
panel._delayedActions.Enqueue( () => Penumbra.ModManager.MoveOption( panel._mod, groupIdx, sourceOption, optionIdx ) );
}
else
{
// Move from one group to another by deleting, then adding the option.
var sourceGroup = _dragDropGroupIdx;
var sourceOption = _dragDropOptionIdx;
var option = @group[ _dragDropOptionIdx ];
var priority = @group.OptionPriority( _dragDropGroupIdx );
panel._delayedActions.Enqueue( () =>
{
var sourceOption = _dragDropOptionIdx;
_delayedActions.Enqueue( () => Penumbra.ModManager.MoveOption( _mod, groupIdx, sourceOption, optionIdx ) );
}
else
{
// Move from one group to another by deleting, then adding the option.
var sourceGroup = _dragDropGroupIdx;
var sourceOption = _dragDropOptionIdx;
var option = group[ _dragDropOptionIdx ];
var priority = group.OptionPriority( _dragDropGroupIdx );
_delayedActions.Enqueue( () =>
{
Penumbra.ModManager.DeleteOption( _mod, sourceGroup, sourceOption );
Penumbra.ModManager.AddOption( _mod, groupIdx, option, priority );
} );
}
Penumbra.ModManager.DeleteOption( panel._mod, sourceGroup, sourceOption );
Penumbra.ModManager.AddOption( panel._mod, groupIdx, option, priority );
} );
}
_dragDropGroupIdx = -1;
_dragDropOptionIdx = -1;
}
_dragDropGroupIdx = -1;
_dragDropOptionIdx = -1;
}
}
private void EditOption( IModGroup group, int groupIdx, int optionIdx )
// Draw a combo to select single or multi group and switch between them.
private void DrawGroupCombo( IModGroup group, int groupIdx )
{
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(), _window._iconButtonSize,
"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(), _window._iconButtonSize,
"Edit this option.", false, true ) )
{
_window.ModEditPopup.ChangeMod( _mod );
_window.ModEditPopup.ChangeOption( groupIdx, optionIdx );
_window.ModEditPopup.IsOpen = true;
}
ImGui.TableNextColumn();
if( group.Type == SelectType.Multi )
{
if( PriorityInput( "##Priority", groupIdx, optionIdx, group.OptionPriority( optionIdx ), out var priority,
50 * ImGuiHelpers.GlobalScale ) )
static string GroupTypeName( SelectType type )
=> type switch
{
Penumbra.ModManager.ChangeOptionPriority( _mod, groupIdx, optionIdx, priority );
}
SelectType.Single => "Single Group",
SelectType.Multi => "Multi Group",
_ => "Unknown",
};
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 ) )
ImGui.SetNextItemWidth( _window._inputTextWidth.X - 3 * _window._iconButtonSize.X - 12 * ImGuiHelpers.GlobalScale );
using var combo = ImRaii.Combo( "##GroupType", GroupTypeName( group.Type ) );
if( !combo )
{
_currentEdit = tmp;
_optionIndex = option;
_currentField = field;
return;
}
if( ImGui.IsItemDeactivatedAfterEdit() && _currentEdit != null )
foreach( var type in new[] { SelectType.Single, SelectType.Multi } )
{
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() )
if( ImGui.Selectable( GroupTypeName( type ), @group.Type == type ) )
{
ImGui.SetKeyboardFocusHere();
Penumbra.ModManager.ChangeModGroupType( _mod, groupIdx, type );
}
}
}
ImGui.InputTextMultiline( "##editDescription", ref _newDescription, 4096, ImGuiHelpers.ScaledVector2( 800, 800 ) );
ImGui.Dummy( _window._defaultSpace );
// Handles input text and integers in separate fields without buffers for every single one.
private static class Input
{
// Special field indices to reuse the same string buffer.
public const int None = -1;
public const int Name = -2;
public const int Author = -3;
public const int Version = -4;
public const int Website = -5;
public const int Path = -6;
public const int Description = -7;
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 );
// Temporary strings
private static string? _currentEdit;
private static int? _currentGroupPriority;
private static int _currentField = -1;
private static int _optionIndex = -1;
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 ) )
public static bool Text( 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 ) )
{
if( _newDescriptionIdx == DescriptionFieldIdx )
{
Penumbra.ModManager.ChangeModDescription( _mod.Index, _newDescription );
}
else if( _newDescriptionIdx >= 0 )
{
Penumbra.ModManager.ChangeGroupDescription( _mod, _newDescriptionIdx, _newDescription );
}
ImGui.CloseCurrentPopup();
_currentEdit = tmp;
_optionIndex = option;
_currentField = field;
}
ImGui.SameLine();
if( ImGui.Button( "Cancel", buttonSize )
|| ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) )
if( ImGui.IsItemDeactivatedAfterEdit() && _currentEdit != null )
{
_newDescriptionIdx = NoFieldIdx;
_newDescription = string.Empty;
ImGui.CloseCurrentPopup();
var ret = _currentEdit != oldValue;
value = _currentEdit;
_currentEdit = null;
_currentField = None;
_optionIndex = None;
return ret;
}
value = string.Empty;
return false;
}
public static bool Priority( 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 = None;
_optionIndex = None;
return ret;
}
value = 0;
return false;
}
}
}