From 2532e73f9d7e62ab2e7216cbbcae6336523d3a56 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 8 Aug 2021 12:43:12 +0200 Subject: [PATCH] Change folder handling and introduce drag & drop for folders --- Penumbra/Configuration.cs | 3 + Penumbra/Mod/ModData.cs | 60 +- Penumbra/Mod/ModMeta.cs | 45 +- Penumbra/Mods/CollectionManager.cs | 8 +- Penumbra/Mods/ModCollection.cs | 61 +- Penumbra/Mods/ModCollectionCache.cs | 28 +- Penumbra/Mods/ModFileSystem.cs | 251 ++++ Penumbra/Mods/ModFolder.cs | 243 ++++ Penumbra/Mods/ModManager.cs | 56 +- Penumbra/Mods/ModManagerEditExtensions.cs | 23 +- Penumbra/Plugin.cs | 11 +- Penumbra/UI/MenuTabs/TabCollections.cs | 10 +- .../UI/MenuTabs/TabInstalled/ModFilter.cs | 46 + .../UI/MenuTabs/TabInstalled/ModListCache.cs | 262 +++++ .../TabInstalled/TabInstalledDetails.cs | 37 +- .../TabInstalled/TabInstalledDetailsEdit.cs | 39 +- .../TabInstalled/TabInstalledModPanel.cs | 44 +- .../TabInstalled/TabInstalledSelector.cs | 1046 +++++++++-------- Penumbra/UI/MenuTabs/TabSettings.cs | 13 + Penumbra/UI/SettingsInterface.cs | 25 +- Penumbra/UI/SettingsMenu.cs | 30 +- 21 files changed, 1690 insertions(+), 651 deletions(-) create mode 100644 Penumbra/Mods/ModFileSystem.cs create mode 100644 Penumbra/Mods/ModFolder.cs create mode 100644 Penumbra/UI/MenuTabs/TabInstalled/ModFilter.cs create mode 100644 Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 8424d974..7a7d2524 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -29,6 +29,9 @@ namespace Penumbra public string CurrentCollection { get; set; } = "Default"; public string DefaultCollection { get; set; } = "Default"; public string ForcedCollection { get; set; } = ""; + + public bool SortFoldersFirst { get; set; } = false; + public Dictionary< string, string > CharacterCollections { get; set; } = new(); public Dictionary< string, string > ModSortOrder { get; set; } = new(); diff --git a/Penumbra/Mod/ModData.cs b/Penumbra/Mod/ModData.cs index b9087872..769aca50 100644 --- a/Penumbra/Mod/ModData.cs +++ b/Penumbra/Mod/ModData.cs @@ -1,11 +1,51 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Plugin; +using Penumbra.Mods; using Penumbra.Util; namespace Penumbra.Mod { + public struct SortOrder : IComparable + { + public ModFolder ParentFolder { get; set; } + + private string _sortOrderName; + + public string SortOrderName + { + get => _sortOrderName; + set => _sortOrderName = value.Replace( '/', '\\' ); + } + + public string SortOrderPath + => ParentFolder.FullName; + + public string FullName + { + get + { + var path = SortOrderPath; + return path.Any() ? $"{path}/{SortOrderName}" : SortOrderName; + } + } + + + public SortOrder( ModFolder parentFolder, string name ) + { + ParentFolder = parentFolder; + _sortOrderName = name.Replace( '/', '\\' ); + } + + public string FullPath + => SortOrderPath.Any() ? $"{SortOrderPath}/{SortOrderName}" : SortOrderName; + + public int CompareTo( SortOrder other ) + => string.Compare(FullPath, other.FullPath, StringComparison.InvariantCultureIgnoreCase ); + } + // ModData contains all permanent information about a mod, // and is independent of collections or settings. // It only changes when the user actively changes the mod or their filesystem. @@ -14,17 +54,22 @@ namespace Penumbra.Mod public DirectoryInfo BasePath; public ModMeta Meta; public ModResources Resources; - public string SortOrder; + + public SortOrder SortOrder; + public SortedList< string, object? > ChangedItems { get; } = new(); + public string LowerChangedItemsString { get; private set; } = string.Empty; public FileInfo MetaFile { get; set; } - private ModData( DirectoryInfo basePath, ModMeta meta, ModResources resources ) + private ModData( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources ) { BasePath = basePath; Meta = meta; Resources = resources; MetaFile = MetaFileInfo( basePath ); - SortOrder = meta.Name.Replace( '/', '\\' ); + SortOrder = new SortOrder( parentFolder, Meta.Name ); + SortOrder.ParentFolder.AddMod( this ); + ComputeChangedItems(); } @@ -44,12 +89,14 @@ namespace Penumbra.Mod { identifier.Identify( ChangedItems, path ); } + + LowerChangedItemsString = string.Join( "\0", ChangedItems.Keys.Select( k => k.ToLowerInvariant() ) ); } public static FileInfo MetaFileInfo( DirectoryInfo basePath ) => new( Path.Combine( basePath.FullName, "meta.json" ) ); - public static ModData? LoadMod( DirectoryInfo basePath ) + public static ModData? LoadMod( ModFolder parentFolder, DirectoryInfo basePath ) { basePath.Refresh(); if( !basePath.Exists ) @@ -77,10 +124,13 @@ namespace Penumbra.Mod data.SetManipulations( meta, basePath ); } - return new ModData( basePath, meta, data ); + return new ModData( parentFolder, basePath, meta, data ); } public void SaveMeta() => Meta.SaveToFile( MetaFile ); + + public override string ToString() + => SortOrder.FullPath; } } \ No newline at end of file diff --git a/Penumbra/Mod/ModMeta.cs b/Penumbra/Mod/ModMeta.cs index a9c674e3..6b5e72e7 100644 --- a/Penumbra/Mod/ModMeta.cs +++ b/Penumbra/Mod/ModMeta.cs @@ -13,8 +13,37 @@ namespace Penumbra.Mod public class ModMeta { public uint FileVersion { get; set; } - public string Name { get; set; } = "Mod"; - public string Author { get; set; } = ""; + + public string Name + { + get => _name; + set + { + _name = value; + LowerName = value.ToLowerInvariant(); + } + } + + private string _name = "Mod"; + + [JsonIgnore] + public string LowerName { get; private set; } = "mod"; + + private string _author = ""; + + public string Author + { + get => _author; + set + { + _author = value; + LowerAuthor = value.ToLowerInvariant(); + } + } + + [JsonIgnore] + public string LowerAuthor { get; private set; } = ""; + public string Description { get; set; } = ""; public string Version { get; set; } = ""; public string Website { get; set; } = ""; @@ -66,8 +95,8 @@ namespace Penumbra.Mod new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } ); if( meta != null ) { - meta.FileHash = text.GetHashCode(); - meta.HasGroupsWithConfig = meta.Groups.Values.Any( g => g.SelectionType == SelectType.Multi || g.Options.Count > 1 ); + meta.FileHash = text.GetHashCode(); + meta.RefreshHasGroupsWithConfig(); } return meta; @@ -79,6 +108,14 @@ namespace Penumbra.Mod } } + public bool RefreshHasGroupsWithConfig() + { + var oldValue = HasGroupsWithConfig; + HasGroupsWithConfig = Groups.Values.Any( g => g.Options.Count > 1 || g.SelectionType == SelectType.Multi && g.Options.Count == 1 ); + return oldValue != HasGroupsWithConfig; + } + + public void SaveToFile( FileInfo filePath ) { try diff --git a/Penumbra/Mods/CollectionManager.cs b/Penumbra/Mods/CollectionManager.cs index 99cf8af7..eac6e7e7 100644 --- a/Penumbra/Mods/CollectionManager.cs +++ b/Penumbra/Mods/CollectionManager.cs @@ -38,7 +38,7 @@ namespace Penumbra.Mods { foreach( var collection in Collections.Values.Where( c => c.Cache != null ) ) { - collection.CreateCache( _manager.BasePath, _manager.Mods, false ); + collection.CreateCache( _manager.BasePath, _manager.StructuredMods.AllMods(_manager.Config.SortFoldersFirst) ); } } @@ -57,10 +57,6 @@ namespace Penumbra.Mods if( metaChanges ) { collection.UpdateSetting( mod ); - if( nameChange ) - { - collection.Cache?.SortMods(); - } } if( fileChanges.HasFlag( ResourceChange.Files ) @@ -143,7 +139,7 @@ namespace Penumbra.Mods { if( collection.Cache == null && collection.Name != string.Empty ) { - collection.CreateCache( _manager.BasePath, _manager.Mods, false ); + collection.CreateCache( _manager.BasePath, _manager.StructuredMods.AllMods(_manager.Config.SortFoldersFirst) ); } } diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs index c250d218..fcb0dee9 100644 --- a/Penumbra/Mods/ModCollection.cs +++ b/Penumbra/Mods/ModCollection.cs @@ -35,60 +35,55 @@ namespace Penumbra.Mods Settings = settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() ); } - private bool CleanUnavailableSettings( Dictionary< string, ModData > data ) + public Mod.Mod GetMod( ModData mod ) { - if( Settings.Count <= data.Count ) + if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) { - return false; + return new Mod.Mod( settings, mod ); } - List< string > removeList = new(); - foreach( var settingKvp in Settings ) - { - if( !data.ContainsKey( settingKvp.Key ) ) - { - removeList.Add( settingKvp.Key ); - } - } + var newSettings = ModSettings.DefaultSettings( mod.Meta ); + Settings.Add( mod.BasePath.Name, newSettings ); + Save( Service< DalamudPluginInterface >.Get() ); + return new Mod.Mod( newSettings, mod ); + } + + private bool CleanUnavailableSettings( Dictionary< string, ModData > data ) + { + var removeList = Settings.Where( settingKvp => !data.ContainsKey( settingKvp.Key ) ).ToArray(); foreach( var s in removeList ) { - Settings.Remove( s ); + Settings.Remove( s.Key ); } - return removeList.Count > 0; + return removeList.Length > 0; } - public void CreateCache( DirectoryInfo modDirectory, Dictionary< string, ModData > data, bool cleanUnavailable = false ) + public void CreateCache( DirectoryInfo modDirectory, IEnumerable< ModData > data ) { Cache = new ModCollectionCache( Name, modDirectory ); var changedSettings = false; - foreach( var modKvp in data ) + foreach( var mod in data ) { - if( Settings.TryGetValue( modKvp.Key, out var settings ) ) + if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) { - Cache.AvailableMods.Add( new Mod.Mod( settings, modKvp.Value ) ); + Cache.AddMod( settings, mod ); } else { changedSettings = true; - var newSettings = ModSettings.DefaultSettings( modKvp.Value.Meta ); - Settings.Add( modKvp.Key, newSettings ); - Cache.AvailableMods.Add( new Mod.Mod( newSettings, modKvp.Value ) ); + var newSettings = ModSettings.DefaultSettings( mod.Meta ); + Settings.Add( mod.BasePath.Name, newSettings ); + Cache.AddMod( newSettings, mod ); } } - if( cleanUnavailable ) - { - changedSettings |= CleanUnavailableSettings( data ); - } - if( changedSettings ) { Save( Service< DalamudPluginInterface >.Get() ); } - Cache.SortMods(); CalculateEffectiveFileList( modDirectory, true, false ); } @@ -129,6 +124,8 @@ namespace Penumbra.Mods public void CalculateEffectiveFileList( DirectoryInfo modDir, bool withMetaManipulations, bool activeCollection ) { + PluginLog.Verbose( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{IsActiveCollection}]", Name, + withMetaManipulations, activeCollection ); Cache ??= new ModCollectionCache( Name, modDir ); UpdateSettings(); Cache.CalculateEffectiveFileList(); @@ -233,14 +230,10 @@ namespace Penumbra.Mods return; } - if( Settings.TryGetValue( data.BasePath.Name, out var settings ) ) - { - Cache.AddMod( settings, data ); - } - else - { - Cache.AddMod( ModSettings.DefaultSettings( data.Meta ), data ); - } + Cache.AddMod( Settings.TryGetValue( data.BasePath.Name, out var settings ) + ? settings + : ModSettings.DefaultSettings( data.Meta ), + data ); } public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath ) diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index 95254a46..d178b78c 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Lumina.Data.Parsing; using Penumbra.GameData.Util; using Penumbra.Meta; using Penumbra.Mod; @@ -12,7 +13,7 @@ namespace Penumbra.Mods // It will only be setup if a collection gets activated in any way. public class ModCollectionCache { - public readonly List< Mod.Mod > AvailableMods = new(); + public readonly List AvailableMods = new(); public readonly Dictionary< GamePath, FileInfo > ResolvedFiles = new(); public readonly Dictionary< GamePath, GamePath > SwappedFiles = new(); @@ -21,12 +22,6 @@ namespace Penumbra.Mods public ModCollectionCache( string collectionName, DirectoryInfo modDir ) => MetaManipulations = new MetaManager( collectionName, ResolvedFiles, modDir ); - public void SortMods() - { - AvailableMods.Sort( ( m1, m2 ) - => string.Compare( m1.Data.SortOrder, m2.Data.SortOrder, StringComparison.InvariantCultureIgnoreCase ) ); - } - private void AddFiles( Dictionary< GamePath, Mod.Mod > registeredFiles, Mod.Mod mod ) { foreach( var file in mod.Data.Resources.ModFiles ) @@ -82,8 +77,7 @@ namespace Penumbra.Mods { MetaManipulations.Reset( false ); - foreach( var mod in AvailableMods.Where( m => m.Settings.Enabled && m.Data.Resources.MetaManipulations.Count > 0 ) - .OrderByDescending( m => m.Settings.Priority ) ) + foreach( var mod in AvailableMods.Where( m => m.Settings.Enabled && m.Data.Resources.MetaManipulations.Count > 0 ) ) { mod.Cache.ClearMetaConflicts(); AddManipulations( mod ); @@ -98,7 +92,7 @@ namespace Penumbra.Mods SwappedFiles.Clear(); var registeredFiles = new Dictionary< GamePath, Mod.Mod >(); - foreach( var mod in AvailableMods.Where( m => m.Settings.Enabled ).OrderByDescending( m => m.Settings.Priority ) ) + foreach( var mod in AvailableMods.Where( m => m.Settings.Enabled ) ) { mod.Cache.ClearFileConflicts(); AddFiles( registeredFiles, mod ); @@ -131,10 +125,20 @@ namespace Penumbra.Mods } } + private class PriorityComparer : IComparer< Mod.Mod > + { + public int Compare( Mod.Mod x, Mod.Mod y ) + => x.Settings.Priority.CompareTo( y.Settings.Priority ); + } + + private static readonly PriorityComparer Comparer = new(); + public void AddMod( ModSettings settings, ModData data ) { - AvailableMods.Add( new Mod.Mod( settings, data ) ); - SortMods(); + var newMod = new Mod.Mod( settings, data ); + var idx = AvailableMods.BinarySearch( newMod, Comparer ); + idx = idx < 0 ? ~idx : idx; + AvailableMods.Insert( idx, newMod ); if( settings.Enabled ) { CalculateEffectiveFileList(); diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs new file mode 100644 index 00000000..03b3a1fe --- /dev/null +++ b/Penumbra/Mods/ModFileSystem.cs @@ -0,0 +1,251 @@ +using System; +using System.Linq; +using Penumbra.Mod; +using Penumbra.Util; + +namespace Penumbra.Mods +{ + public static partial class ModFileSystem + { + // The root folder that should be used as the base for all structured mods. + public static ModFolder Root = ModFolder.CreateRoot(); + + // Find a specific mod folder by its path from Root. + // Returns true if the folder was found, and false if not. + // The out parameter will contain the furthest existing folder. + public static bool Find( string path, out ModFolder folder ) + { + var split = path.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries ); + folder = Root; + foreach( var part in split ) + { + if( !folder.FindSubFolder( part, out folder ) ) + { + return false; + } + } + + return true; + } + + // Rename the SortOrderName of a single mod. Slashes are replaced by Backslashes. + // Saves and returns true if anything changed. + public static bool Rename( this ModData mod, string newName ) + { + if( RenameNoSave( mod, newName ) ) + { + SaveMod( mod ); + return true; + } + + return false; + } + + // Rename the target folder, merging it and its subfolders if the new name already exists. + // Saves all mods manipulated thus, and returns true if anything changed. + public static bool Rename( this ModFolder target, string newName ) + { + if( RenameNoSave( target, newName ) ) + { + SaveModChildren( target ); + return true; + } + + return false; + } + + // Move a single mod to the target folder. + // Returns true and saves if anything changed. + public static bool Move( this ModData mod, ModFolder target ) + { + if( MoveNoSave( mod, target ) ) + { + SaveMod( mod ); + return true; + } + + return false; + } + + // Move a mod to the filesystem location specified by sortOrder and rename its SortOrderName. + // Creates all necessary Subfolders. + // Does NOT save. + public static void Move( this ModData mod, string sortOrder ) + { + var split = sortOrder.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries ); + var folder = Root; + for( var i = 0; i < split.Length - 1; ++i ) + { + folder = folder.FindOrCreateSubFolder( split[ i ] ).Item1; + } + + MoveNoSave( mod, folder ); + RenameNoSave( mod, split.Last() ); + } + + // Moves folder to target. + // If an identically named subfolder of target already exists, merges instead. + // Root is not movable. + public static bool Move( this ModFolder folder, ModFolder target ) + { + if( MoveNoSave( folder, target ) ) + { + SaveModChildren( target ); + return true; + } + + return false; + } + + // Merge source with target, moving all direct mod children of source to target, + // and moving all subfolders of source to target, or merging them with targets subfolders if they exist. + // Returns true and saves if anything changed. + public static bool Merge( this ModFolder source, ModFolder target ) + { + if( MergeNoSave( source, target ) ) + { + SaveModChildren( target ); + return true; + } + + return false; + } + } + + // Internal stuff. + public static partial class ModFileSystem + { + // Reset all sort orders for all descendants of the given folder. + // Assumes that it is not called on Root, and thus does not remove unnecessary SortOrder entries. + private static void SaveModChildren( ModFolder target ) + { + var config = Service< Configuration >.Get(); + foreach( var mod in target.AllMods( true ) ) + { + config.ModSortOrder[ mod.BasePath.Name ] = mod.SortOrder.FullName; + } + + config.Save(); + } + + // Sets and saves the sort order of a single mod, removing the entry if it is unnecessary. + private static void SaveMod( ModData mod ) + { + var config = Service< Configuration >.Get(); + if( ReferenceEquals( mod.SortOrder.ParentFolder, Root ) + && string.Equals( mod.SortOrder.SortOrderName, mod.Meta.Name.Replace( '/', '\\' ), StringComparison.InvariantCultureIgnoreCase ) ) + { + config.ModSortOrder.Remove( mod.BasePath.Name ); + } + else + { + config.ModSortOrder[ mod.BasePath.Name ] = mod.SortOrder.FullName; + } + + config.Save(); + } + + private static bool RenameNoSave( this ModFolder target, string newName ) + { + if( ReferenceEquals( target, Root ) ) + { + throw new InvalidOperationException( "Can not rename root." ); + } + + newName = newName.Replace( '/', '\\' ); + if( target.Name == newName ) + { + return false; + } + + if( target.Parent!.FindSubFolder( newName, out var preExisting ) ) + { + MergeNoSave( target, preExisting ); + } + else + { + var parent = target.Parent; + parent.RemoveFolderIgnoreEmpty( target ); + target.Name = newName; + parent.FindOrAddSubFolder( target ); + } + + return true; + } + + private static bool RenameNoSave( ModData mod, string newName ) + { + newName = newName.Replace( '/', '\\' ); + if( mod.SortOrder.SortOrderName == newName ) + { + return false; + } + + mod.SortOrder.ParentFolder.RemoveModIgnoreEmpty( mod ); + mod.SortOrder = new SortOrder( mod.SortOrder.ParentFolder, newName ); + mod.SortOrder.ParentFolder.AddMod( mod ); + return true; + } + + private static bool MoveNoSave( ModData mod, ModFolder target ) + { + var oldParent = mod.SortOrder.ParentFolder; + if( ReferenceEquals( target, oldParent ) ) + { + return false; + } + + oldParent.RemoveMod( mod ); + mod.SortOrder = new SortOrder( target, mod.SortOrder.SortOrderName ); + target.AddMod( mod ); + return true; + } + + private static bool MergeNoSave( ModFolder source, ModFolder target ) + { + if( ReferenceEquals( source, target ) ) + { + return false; + } + + var any = false; + while( source.SubFolders.Count > 0 ) + { + any |= MoveNoSave( source.SubFolders.First(), target ); + } + + while( source.Mods.Count > 0 ) + { + any |= MoveNoSave( source.Mods.First(), target ); + } + + source.Parent?.RemoveSubFolder( source ); + + return any || source.Parent != null; + } + + private static bool MoveNoSave( ModFolder folder, ModFolder target ) + { + // Moving a folder into itself is not permitted. + if( ReferenceEquals( folder, target ) ) + { + return false; + } + + if( ReferenceEquals( target, folder.Parent! ) ) + { + return false; + } + + folder.Parent!.RemoveSubFolder( folder ); + var subFolderIdx = target.FindOrAddSubFolder( folder ); + if( subFolderIdx > 0 ) + { + var main = target.SubFolders[ subFolderIdx ]; + MergeNoSave( folder, main ); + } + + return true; + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ModFolder.cs b/Penumbra/Mods/ModFolder.cs new file mode 100644 index 00000000..66e51544 --- /dev/null +++ b/Penumbra/Mods/ModFolder.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Penumbra.Mod; + +namespace Penumbra.Mods +{ + public partial class ModFolder + { + public ModFolder? Parent; + + public string FullName + { + get + { + var parentPath = Parent?.FullName ?? string.Empty; + return parentPath.Any() ? $"{parentPath}/{Name}" : Name; + } + } + + private string _name = string.Empty; + + public string Name + { + get => _name; + set => _name = value.Replace( '/', '\\' ); + } + + public List< ModFolder > SubFolders { get; } = new(); + public List< ModData > Mods { get; } = new(); + + public ModFolder( ModFolder parent, string name ) + { + Parent = parent; + Name = name; + } + + public override string ToString() + => FullName; + + public int TotalDescendantMods() + => Mods.Count + SubFolders.Sum( f => f.TotalDescendantMods() ); + + public int TotalDescendantFolders() + => SubFolders.Sum( f => f.TotalDescendantFolders() ); + + // Return all descendant mods in the specified order. + public IEnumerable< ModData > AllMods( bool foldersFirst ) + { + if( foldersFirst ) + { + return SubFolders.SelectMany( f => f.AllMods( foldersFirst ) ).Concat( Mods ); + } + + return GetSortedEnumerator().SelectMany( f => + { + if( f is ModFolder folder ) + { + return folder.AllMods( false ); + } + + return new[] { ( ModData )f }; + } ); + } + + // Return all descendant subfolders. + public IEnumerable< ModFolder > AllFolders() + => SubFolders.SelectMany( f => f.AllFolders() ).Prepend( this ); + + // Iterate through all descendants in the specified order, returning subfolders as well as mods. + public IEnumerable< object > GetItems( bool foldersFirst ) + => foldersFirst ? SubFolders.Cast< object >().Concat( Mods ) : GetSortedEnumerator(); + + // Find a subfolder by name. Returns true and sets folder to it if it exists. + public bool FindSubFolder( string name, out ModFolder folder ) + { + var subFolder = new ModFolder( this, name ); + var idx = SubFolders.BinarySearch( subFolder, FolderComparer ); + folder = idx >= 0 ? SubFolders[ idx ] : this; + return idx >= 0; + } + + // Checks if an equivalent subfolder as folder already exists and returns its index. + // If it does not exist, inserts folder as a subfolder and returns the new index. + // Also sets this as folders parent. + public int FindOrAddSubFolder( ModFolder folder ) + { + var idx = SubFolders.BinarySearch( folder, FolderComparer ); + if( idx >= 0 ) + { + return idx; + } + + idx = ~idx; + SubFolders.Insert( idx, folder ); + folder.Parent = this; + return idx; + } + + // Checks if a subfolder with the given name already exists and returns it and its index. + // If it does not exists, creates and inserts it and returns the new subfolder and its index. + public (ModFolder, int) FindOrCreateSubFolder( string name ) + { + var subFolder = new ModFolder( this, name ); + var idx = FindOrAddSubFolder( subFolder ); + return ( SubFolders[ idx ], idx ); + } + + // Remove folder as a subfolder if it exists. + // If this folder is empty afterwards, remove it from its parent. + public void RemoveSubFolder( ModFolder folder ) + { + RemoveFolderIgnoreEmpty( folder ); + CheckEmpty(); + } + + // Add the given mod as a child, if it is not already a child. + // Returns the index of the found or inserted mod. + public int AddMod( ModData mod ) + { + var idx = Mods.BinarySearch( mod, ModComparer ); + if( idx >= 0 ) + { + return idx; + } + + idx = ~idx; + Mods.Insert( idx, mod ); + + return idx; + } + + // Remove mod as a child if it exists. + // If this folder is empty afterwards, remove it from its parent. + public void RemoveMod( ModData mod ) + { + RemoveModIgnoreEmpty( mod ); + CheckEmpty(); + } + } + + // Internals + public partial class ModFolder + { + // Create a Root folder without parent. + internal static ModFolder CreateRoot() + => new( null!, string.Empty ); + + internal class ModFolderComparer : IComparer< ModFolder > + { + // Compare only the direct folder names since this is only used inside an enumeration of subfolders of one folder. + public int Compare( ModFolder x, ModFolder y ) + => ReferenceEquals( x, y ) + ? 0 + : string.Compare( x.Name, y.Name, StringComparison.InvariantCultureIgnoreCase ); + } + + internal class ModDataComparer : IComparer< ModData > + { + // Compare only the direct SortOrderNames since this is only used inside an enumeration of direct mod children of one folder. + // Since mod SortOrderNames do not have to be unique inside a folder, also compare their BasePaths (and thus their identity) if necessary. + public int Compare( ModData x, ModData y ) + { + if( ReferenceEquals( x, y ) ) + { + return 0; + } + + var cmp = string.Compare( x.SortOrder.SortOrderName, y.SortOrder.SortOrderName, StringComparison.InvariantCultureIgnoreCase ); + if( cmp != 0 ) + { + return cmp; + } + + return string.Compare( x.BasePath.Name, y.BasePath.Name, StringComparison.InvariantCulture ); + } + } + + private static readonly ModFolderComparer FolderComparer = new(); + private static readonly ModDataComparer ModComparer = new(); + + // Get an enumerator for actually sorted objects instead of folder-first objects. + private IEnumerable< object > GetSortedEnumerator() + { + var modIdx = 0; + foreach( var folder in SubFolders ) + { + var folderString = folder.Name; + for( ; modIdx < Mods.Count; ++modIdx ) + { + var mod = Mods[ modIdx ]; + var modString = mod.SortOrder.SortOrderName; + if( string.Compare( folderString, modString, StringComparison.InvariantCultureIgnoreCase ) > 0 ) + { + yield return mod; + } + else + { + break; + } + } + + yield return folder; + } + + for( ; modIdx < Mods.Count; ++modIdx ) + { + yield return Mods[ modIdx ]; + } + } + + private void CheckEmpty() + { + if( Mods.Count == 0 && SubFolders.Count == 0 ) + { + Parent?.RemoveSubFolder( this ); + } + } + + // Remove a subfolder but do not remove this folder from its parent if it is empty afterwards. + internal void RemoveFolderIgnoreEmpty( ModFolder folder ) + { + var idx = SubFolders.BinarySearch( folder, FolderComparer ); + if( idx < 0 ) + { + return; + } + + SubFolders[ idx ].Parent = null; + SubFolders.RemoveAt( idx ); + } + + // Remove a mod, but do not remove this folder from its parent if it is empty afterwards. + internal void RemoveModIgnoreEmpty( ModData mod ) + { + var idx = Mods.BinarySearch( mod, ModComparer ); + if( idx >= 0 ) + { + Mods.RemoveAt( idx ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index 804345f2..a385d2c1 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Plugin; +using ImGuiScene; using Penumbra.GameData.Util; using Penumbra.Meta; using Penumbra.Mod; @@ -17,6 +18,8 @@ namespace Penumbra.Mods public DirectoryInfo BasePath { get; private set; } = null!; public Dictionary< string, ModData > Mods { get; } = new(); + public ModFolder StructuredMods { get; } = ModFileSystem.Root; + public CollectionManager Collections { get; } public bool Valid { get; private set; } @@ -53,25 +56,45 @@ namespace Penumbra.Mods DiscoverMods(); } - private void SetModOrders( Configuration config ) + private bool SetSortOrderPath( ModData mod, string path ) + { + mod.Move( path ); + var fixedPath = mod.SortOrder.FullPath; + if( !fixedPath.Any() || string.Equals( fixedPath, mod.Meta.Name, StringComparison.InvariantCultureIgnoreCase ) ) + { + Config.ModSortOrder.Remove( mod.BasePath.Name ); + return true; + } + + if( path != fixedPath ) + { + Config.ModSortOrder[ mod.BasePath.Name ] = fixedPath; + return true; + } + + return false; + } + + private void SetModStructure() { var changes = false; - foreach( var kvp in config.ModSortOrder.ToArray() ) + + foreach( var kvp in Config.ModSortOrder.ToArray() ) { - if( Mods.TryGetValue( kvp.Key, out var mod ) ) + if( kvp.Value.Any() && Mods.TryGetValue( kvp.Key, out var mod ) ) { - mod.SortOrder = string.Join( "/", kvp.Value.Trim().Split( new[] { "/" }, StringSplitOptions.RemoveEmptyEntries ) ); + changes |= SetSortOrderPath( mod, kvp.Value ); } else { changes = true; - config.ModSortOrder.Remove( kvp.Key ); + Config.ModSortOrder.Remove( kvp.Key ); } } if( changes ) { - config.Save(); + Config.Save(); } } @@ -96,7 +119,7 @@ namespace Penumbra.Mods { foreach( var modFolder in BasePath.EnumerateDirectories() ) { - var mod = ModData.LoadMod( modFolder ); + var mod = ModData.LoadMod( StructuredMods, modFolder ); if( mod == null ) { continue; @@ -105,7 +128,7 @@ namespace Penumbra.Mods Mods.Add( modFolder.Name, mod ); } - SetModOrders( _plugin.Configuration ); + SetModStructure(); } Collections.RecreateCaches(); @@ -132,7 +155,7 @@ namespace Penumbra.Mods public bool AddMod( DirectoryInfo modFolder ) { - var mod = ModData.LoadMod( modFolder ); + var mod = ModData.LoadMod( StructuredMods, modFolder ); if( mod == null ) { return false; @@ -140,7 +163,10 @@ namespace Penumbra.Mods if( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) { - mod.SortOrder = sortOrder; + if( SetSortOrderPath( mod, sortOrder ) ) + { + Config.Save(); + } } if( Mods.ContainsKey( modFolder.Name ) ) @@ -173,11 +199,17 @@ namespace Penumbra.Mods mod.ComputeChangedItems(); if( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) { - mod.SortOrder = sortOrder; + mod.Move( sortOrder ); + var path = mod.SortOrder.FullPath; + if( path != sortOrder ) + { + Config.ModSortOrder[ mod.BasePath.Name ] = path; + Config.Save(); + } } else { - mod.SortOrder = mod.Meta.Name.Replace( '/', '\\' ); + mod.SortOrder = new SortOrder( StructuredMods, mod.Meta.Name ); } } diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs index c3fd1efe..b1bc3435 100644 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -22,42 +22,31 @@ namespace Penumbra.Mods mod.Meta.Name = newName; mod.SaveMeta(); - foreach( var collection in manager.Collections.Collections.Values.Where( c => c.Cache != null ) ) - { - collection.Cache!.SortMods(); - } return true; } public static bool ChangeSortOrder( this ModManager manager, ModData mod, string newSortOrder ) { - newSortOrder = string.Join( "/", newSortOrder.Trim().Split( new[] { "/" }, StringSplitOptions.RemoveEmptyEntries ) ); - - if( mod.SortOrder == newSortOrder ) + if( string.Equals(mod.SortOrder.FullPath, newSortOrder, StringComparison.InvariantCultureIgnoreCase ) ) { return false; } - var modName = mod.Meta.Name.Replace( '/', '\\' ); - if( newSortOrder == string.Empty || newSortOrder == modName ) + var inRoot = new SortOrder( manager.StructuredMods, mod.Meta.Name ); + if( newSortOrder == string.Empty || newSortOrder == inRoot.SortOrderName ) { - mod.SortOrder = modName; + mod.SortOrder = inRoot; manager.Config.ModSortOrder.Remove( mod.BasePath.Name ); } else { - mod.SortOrder = newSortOrder; - manager.Config.ModSortOrder[ mod.BasePath.Name ] = newSortOrder; + mod.Move( newSortOrder ); + manager.Config.ModSortOrder[ mod.BasePath.Name ] = mod.SortOrder.FullPath; } manager.Config.Save(); - foreach( var collection in manager.Collections.Collections.Values.Where( c => c.Cache != null ) ) - { - collection.Cache!.SortMods(); - } - return true; } diff --git a/Penumbra/Plugin.cs b/Penumbra/Plugin.cs index 58cf6491..2d66a160 100644 --- a/Penumbra/Plugin.cs +++ b/Penumbra/Plugin.cs @@ -39,13 +39,20 @@ namespace Penumbra private WebServer? _webServer; + public static void SaveConfiguration() + { + var pi = Service< DalamudPluginInterface >.Get(); + var config = Service< Configuration >.Get(); + pi.SavePluginConfig( config ); + } + public void Initialize( DalamudPluginInterface pluginInterface ) { PluginInterface = pluginInterface; Service< DalamudPluginInterface >.Set( PluginInterface ); GameData.GameData.GetIdentifier( PluginInterface ); - - Configuration = Configuration.Load( PluginInterface ); + Configuration = Configuration.Load( PluginInterface ); + Service< Configuration >.Set( Configuration ); SoundShit = new MusicManager( this ); SoundShit.DisableStreaming(); diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs index 6e880260..8c581117 100644 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -14,6 +14,7 @@ namespace Penumbra.UI { private class TabCollections { + public const string LabelCurrentCollection = "Current Collection"; private readonly Selector _selector; private readonly ModManager _manager; private string _collectionNames = null!; @@ -130,11 +131,11 @@ namespace Penumbra.UI } } - private void DrawCurrentCollectionSelector() + public void DrawCurrentCollectionSelector(bool tooltip) { var index = _currentCollectionIndex; - var combo = ImGui.Combo( "Current Collection", ref index, _collectionNames ); - if( ImGui.IsItemHovered() ) + var combo = ImGui.Combo( LabelCurrentCollection, ref index, _collectionNames ); + if( tooltip && ImGui.IsItemHovered() ) { ImGui.SetTooltip( "This collection will be modified when using the Installed Mods tab and making changes. It does not apply to anything by itself." ); @@ -145,6 +146,7 @@ namespace Penumbra.UI _manager.Collections.SetCurrentCollection( _collections[ index + 1 ] ); _currentCollectionIndex = index; _selector.ReloadSelection(); + _selector.Cache.ResetModList(); } } @@ -277,7 +279,7 @@ namespace Penumbra.UI return; } - DrawCurrentCollectionSelector(); + DrawCurrentCollectionSelector(true); ImGui.Dummy( new Vector2( 0, 10 ) ); DrawNewCollectionInput(); diff --git a/Penumbra/UI/MenuTabs/TabInstalled/ModFilter.cs b/Penumbra/UI/MenuTabs/TabInstalled/ModFilter.cs new file mode 100644 index 00000000..590ffff0 --- /dev/null +++ b/Penumbra/UI/MenuTabs/TabInstalled/ModFilter.cs @@ -0,0 +1,46 @@ +using System; + +namespace Penumbra.UI +{ + [Flags] + public enum ModFilter + { + Enabled = 1 << 0, + Disabled = 1 << 1, + NoConflict = 1 << 2, + SolvedConflict = 1 << 3, + UnsolvedConflict = 1 << 4, + HasNoMetaManipulations = 1 << 5, + HasMetaManipulations = 1 << 6, + HasNoFileSwaps = 1 << 7, + HasFileSwaps = 1 << 8, + HasConfig = 1 << 9, + HasNoConfig = 1 << 10, + HasNoFiles = 1 << 11, + HasFiles = 1 << 12, + }; + + public static class ModFilterExtensions + { + public const ModFilter UnfilteredStateMods = ( ModFilter )( ( 1 << 13 ) - 1 ); + + public static string ToName( this ModFilter filter ) + => filter switch + { + ModFilter.Enabled => "Enabled", + ModFilter.Disabled => "Disabled", + ModFilter.NoConflict => "No Conflicts", + ModFilter.SolvedConflict => "Solved Conflicts", + ModFilter.UnsolvedConflict => "Unsolved Conflicts", + ModFilter.HasNoMetaManipulations => "No Meta Manipulations", + ModFilter.HasMetaManipulations => "Meta Manipulations", + ModFilter.HasNoFileSwaps => "No File Swaps", + ModFilter.HasFileSwaps => "File Swaps", + ModFilter.HasNoConfig => "No Configuration", + ModFilter.HasConfig => "Configuration", + ModFilter.HasNoFiles => "No Files", + ModFilter.HasFiles => "Files", + _ => throw new ArgumentOutOfRangeException( nameof( filter ), filter, null ), + }; + } +} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs new file mode 100644 index 00000000..2b19e36f --- /dev/null +++ b/Penumbra/UI/MenuTabs/TabInstalled/ModListCache.cs @@ -0,0 +1,262 @@ +using System.Collections.Generic; +using System.Linq; +using Dalamud.Plugin; +using Penumbra.Mods; + +namespace Penumbra.UI +{ + public class ModListCache + { + public const uint DisabledModColor = 0xFF666666u; + public const uint ConflictingModColor = 0xFFAAAAFFu; + public const uint HandledConflictModColor = 0xFF88DDDDu; + + private readonly ModManager _manager; + + private readonly List< Mod.Mod > _modsInOrder = new(); + private readonly List< (bool visible, uint color) > _visibleMods = new(); + private readonly Dictionary< ModFolder, (bool visible, bool enabled) > _visibleFolders = new(); + + private string _modFilter = ""; + private string _modFilterChanges = ""; + private string _modFilterAuthor = ""; + private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; + + public ModFilter StateFilter + { + get => _stateFilter; + set + { + var diff = _stateFilter != value; + _stateFilter = value; + if( diff ) + { + ResetFilters(); + } + } + } + + public ModListCache( ModManager manager ) + { + _manager = manager; + ResetModList(); + } + + public int Count + => _modsInOrder.Count; + + public void RemoveMod( Mod.Mod mod ) + { + var idx = _modsInOrder.IndexOf( mod ); + if( idx >= 0 ) + { + _modsInOrder.RemoveAt( idx ); + _visibleMods.RemoveAt( idx ); + UpdateFolders(); + } + } + + private void UpdateFolders() + { + _visibleFolders.Clear(); + + for( var i = 0; i < _modsInOrder.Count; ++i ) + { + if( _visibleMods[ i ].visible ) + { + _visibleFolders[ _modsInOrder[ i ].Data.SortOrder.ParentFolder ] = ( true, true ); + } + } + } + + public void SetTextFilter( string filter ) + { + var lower = filter.ToLowerInvariant(); + if( lower.StartsWith( "c:" ) ) + { + _modFilterChanges = lower.Substring( 2 ); + _modFilter = string.Empty; + _modFilterAuthor = string.Empty; + } + else if( lower.StartsWith( "a:" ) ) + { + _modFilterAuthor = lower.Substring( 2 ); + _modFilter = string.Empty; + _modFilterChanges = string.Empty; + } + else + { + _modFilter = lower; + _modFilterAuthor = string.Empty; + _modFilterChanges = string.Empty; + } + + ResetFilters(); + } + + public void ResetModList() + { + _modsInOrder.Clear(); + _visibleMods.Clear(); + _visibleFolders.Clear(); + + PluginLog.Verbose( "Resetting mod selector list..." ); + if( !_modsInOrder.Any() ) + { + foreach( var modData in _manager.StructuredMods.AllMods( _manager.Config.SortFoldersFirst ) ) + { + var mod = _manager.Collections.CurrentCollection.GetMod( modData ); + _modsInOrder.Add( mod ); + _visibleMods.Add( CheckFilters( mod ) ); + } + } + } + + public void ResetFilters() + { + _visibleMods.Clear(); + _visibleFolders.Clear(); + PluginLog.Verbose( "Resetting mod selector filters..." ); + foreach( var mod in _modsInOrder ) + { + _visibleMods.Add( CheckFilters( mod ) ); + } + } + + public (Mod.Mod? mod, int idx) GetModByName( string name ) + { + for( var i = 0; i < Count; ++i ) + { + if( _modsInOrder[ i ].Data.Meta.Name == name ) + { + return ( _modsInOrder[ i ], i ); + } + } + + return ( null, 0 ); + } + + public (Mod.Mod? mod, int idx) GetModByBasePath( string basePath ) + { + for( var i = 0; i < Count; ++i ) + { + if( _modsInOrder[ i ].Data.BasePath.Name == basePath ) + { + return ( _modsInOrder[ i ], i ); + } + } + + return ( null, 0 ); + } + + public (bool visible, bool enabled) GetFolder( ModFolder folder ) + => _visibleFolders.TryGetValue( folder, out var ret ) ? ret : ( false, false ); + + public (Mod.Mod?, bool visible, uint color) GetMod( int idx ) + => idx >= 0 && idx < _modsInOrder.Count + ? ( _modsInOrder[ idx ], _visibleMods[ idx ].visible, _visibleMods[ idx ].color ) + : ( null, false, 0 ); + + private bool CheckFlags( int count, ModFilter hasNoFlag, ModFilter hasFlag ) + { + if( count == 0 ) + { + if( StateFilter.HasFlag( hasNoFlag ) ) + { + return false; + } + } + else if( StateFilter.HasFlag( hasFlag ) ) + { + return false; + } + + return true; + } + + private (bool, uint) CheckFilters( Mod.Mod mod ) + { + var ret = ( false, 0u ); + if( _modFilter.Any() && !mod.Data.Meta.LowerName.Contains( _modFilter ) ) + { + return ret; + } + + if( _modFilterAuthor.Any() && !mod.Data.Meta.LowerAuthor.Contains( _modFilterAuthor ) ) + { + return ret; + } + + if( _modFilterChanges.Any() && !mod.Data.LowerChangedItemsString.Contains( _modFilterChanges ) ) + { + return ret; + } + + if( CheckFlags( mod.Data.Resources.ModFiles.Count, ModFilter.HasNoFiles, ModFilter.HasFiles ) ) + { + return ret; + } + + if( CheckFlags( mod.Data.Meta.FileSwaps.Count, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps ) ) + { + return ret; + } + + if( CheckFlags( mod.Data.Resources.MetaManipulations.Count, ModFilter.HasNoMetaManipulations, + ModFilter.HasMetaManipulations ) ) + { + return ret; + } + + if( CheckFlags( mod.Data.Meta.HasGroupsWithConfig ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig ) ) + { + return ret; + } + + if( !mod.Settings.Enabled ) + { + if( !StateFilter.HasFlag( ModFilter.Disabled ) || !StateFilter.HasFlag( ModFilter.NoConflict ) ) + { + return ret; + } + + ret.Item2 = ret.Item2 == 0 ? DisabledModColor : ret.Item2; + } + + if( mod.Settings.Enabled && !StateFilter.HasFlag( ModFilter.Enabled ) ) + { + return ret; + } + + if( mod.Cache.Conflicts.Any() ) + { + if( mod.Cache.Conflicts.Keys.Any( m => m.Settings.Priority == mod.Settings.Priority ) ) + { + if( !StateFilter.HasFlag( ModFilter.UnsolvedConflict ) ) + { + return ret; + } + + ret.Item2 = ret.Item2 == 0 ? ConflictingModColor : ret.Item2; + } + else + { + if( !StateFilter.HasFlag( ModFilter.SolvedConflict ) ) + { + return ret; + } + + ret.Item2 = ret.Item2 == 0 ? HandledConflictModColor : ret.Item2; + } + } + else if( !StateFilter.HasFlag( ModFilter.NoConflict ) ) + { + return ret; + } + + ret.Item1 = true; + _visibleFolders[ mod.Data.SortOrder.ParentFolder ] = ( true, true ); + return ret; + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index 0756c9f6..4ff83b76 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -431,6 +431,19 @@ namespace Penumbra.UI if( changed ) { _selector.SaveCurrentMod(); + // Since files may have changed, we need to recompute effective files. + foreach( var collection in _modManager.Collections.Collections.Values + .Where( c => c.Cache != null && c.Settings[ Mod!.Data.BasePath.Name ].Enabled ) ) + { + collection.CalculateEffectiveFileList( _modManager.BasePath, false, + collection == _modManager.Collections.ActiveCollection ); + } + + // If the mod is enabled in the current collection, its conflicts may have changed. + if( Mod!.Settings.Enabled ) + { + _selector.Cache.ResetFilters(); + } } } @@ -558,14 +571,12 @@ namespace Penumbra.UI if( ImGui.Checkbox( label, ref enabled ) && oldEnabled != enabled ) { Mod.Settings.Settings[ group.GroupName ] ^= 1 << idx; - if( Mod.Settings.Enabled && _modManager.Collections.CurrentCollection.Cache != null ) - { - _modManager.Collections.CurrentCollection.CalculateEffectiveFileList( Mod.Data.BasePath, - Mod.Data.Resources.MetaManipulations.Count > 0, - _modManager.Collections.CurrentCollection == _modManager.Collections.ActiveCollection ); - } - Save(); + // If the mod is enabled, recalculate files and filters. + if( Mod.Settings.Enabled ) + { + _base.RecalculateCurrent( Mod.Data.Resources.MetaManipulations.Count > 0 ); + } } } @@ -599,14 +610,12 @@ namespace Penumbra.UI && code != Mod.Settings.Settings[ group.GroupName ] ) { Mod.Settings.Settings[ group.GroupName ] = code; - if( Mod.Settings.Enabled && _modManager.Collections.CurrentCollection.Cache != null ) - { - _modManager.Collections.CurrentCollection.CalculateEffectiveFileList( Mod.Data.BasePath, - Mod.Data.Resources.MetaManipulations.Count > 0, - _modManager.Collections.CurrentCollection == _modManager.Collections.ActiveCollection ); - } - Save(); + // If the mod is enabled, recalculate files and filters. + if( Mod.Settings.Enabled ) + { + _base.RecalculateCurrent( Mod.Data.Resources.MetaManipulations.Count > 0 ); + } } } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs index 0458a0a9..546b0f11 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs @@ -111,7 +111,10 @@ namespace Penumbra.UI var groupName = group.GroupName; if( Custom.ImGuiCustom.BeginFramedGroupEdit( ref groupName ) ) { - _modManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ); + if( _modManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) + { + _selector.Cache.ResetFilters(); + } } } @@ -127,6 +130,10 @@ namespace Penumbra.UI group.Options.Add( new Option() { OptionName = newOption, OptionDesc = "", OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >() } ); _selector.SaveCurrentMod(); + if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() ) + { + _selector.Cache.ResetFilters(); + } } } @@ -163,6 +170,11 @@ namespace Penumbra.UI { OptionName = newName, OptionDesc = opt.OptionDesc, OptionFiles = opt.OptionFiles }; _selector.SaveCurrentMod(); } + + if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() ) + { + _selector.Cache.ResetFilters(); + } } } @@ -176,7 +188,10 @@ namespace Penumbra.UI var groupName = group.GroupName; if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) { - _modManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ); + if( _modManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() ) + { + _selector.Cache.ResetFilters(); + } } } @@ -220,6 +235,11 @@ namespace Penumbra.UI } } } + + if( Mod.Data.Meta.RefreshHasGroupsWithConfig() ) + { + _selector.Cache.ResetFilters(); + } } if( code != oldSetting ) @@ -247,6 +267,7 @@ namespace Penumbra.UI ImGuiInputTextFlags.EnterReturnsTrue ) ) { _modManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Single ); + // Adds empty group, so can not change filters. } } @@ -259,6 +280,7 @@ namespace Penumbra.UI ImGuiInputTextFlags.EnterReturnsTrue ) ) { _modManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Multi ); + // Adds empty group, so can not change filters. } } @@ -298,7 +320,6 @@ namespace Penumbra.UI var arrowWidth = ImGui.CalcTextSize( arrow ).X; ImGui.PopFont(); - var width = ( ImGui.GetWindowWidth() - arrowWidth - 4 * ImGui.GetStyle().ItemSpacing.X ) / 2; for( var idx = 0; idx < swaps.Length + 1; ++idx ) { @@ -325,10 +346,8 @@ namespace Penumbra.UI } _selector.SaveCurrentMod(); - if( Mod.Settings.Enabled ) - { - _selector.ReloadCurrentMod(); - } + _selector.ReloadCurrentMod(); + _selector.Cache.ResetModList(); } } @@ -350,10 +369,8 @@ namespace Penumbra.UI { Meta.FileSwaps[ key ] = newValue; _selector.SaveCurrentMod(); - if( Mod.Settings.Enabled ) - { - _selector.ReloadCurrentMod(); - } + _selector.ReloadCurrentMod(); + _selector.Cache.ResetModList(); } } } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs index edec8a0e..e3707612 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs @@ -74,8 +74,11 @@ namespace Penumbra.UI var name = Meta!.Name; if( Custom.ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) && _modManager.RenameMod( name, Mod!.Data ) ) { - _selector.RenameCurrentModLower( name ); _selector.SelectModByDir( Mod.Data.BasePath.Name ); + if( !_modManager.Config.ModSortOrder.ContainsKey( Mod!.Data.BasePath.Name ) && Mod.Data.Rename( name ) ) + { + _selector.Cache.ResetModList(); + } } } @@ -119,6 +122,7 @@ namespace Penumbra.UI { Meta.Author = author; _selector.SaveCurrentMod(); + _selector.Cache.ResetFilters(); } ImGui.EndGroup(); @@ -200,13 +204,8 @@ namespace Penumbra.UI if( ImGui.InputInt( "Priority", ref priority, 0 ) && priority != Mod!.Settings.Priority ) { Mod.Settings.Priority = priority; - var collection = _modManager.Collections.CurrentCollection; - collection.Save( _base._plugin.PluginInterface! ); - if( collection.Cache != null ) - { - collection.CalculateEffectiveFileList( _modManager.BasePath, Mod.Data.Resources.MetaManipulations.Count > 0, - collection == _modManager.Collections.ActiveCollection ); - } + _selector.Cache.ResetFilters(); + _base.SaveCurrentCollection( Mod.Data.Resources.MetaManipulations.Count > 0 ); } if( ImGui.IsItemHovered() ) @@ -222,23 +221,19 @@ namespace Penumbra.UI if( ImGui.Checkbox( LabelModEnabled, ref enabled ) ) { Mod.Settings.Enabled = enabled; - var collection = _modManager.Collections.CurrentCollection; - collection.Save( _base._plugin.PluginInterface! ); - if( collection.Cache != null ) - { - collection.CalculateEffectiveFileList( _modManager.BasePath, Mod.Data.Resources.MetaManipulations.Count > 0, - collection == _modManager.Collections.ActiveCollection ); - } + _selector.Cache.ResetFilters(); + _base.SaveCurrentCollection( Mod.Data.Resources.MetaManipulations.Count > 0 ); } } public static bool DrawSortOrder( ModData mod, ModManager manager, Selector selector ) { - var currentSortOrder = mod.SortOrder; + var currentSortOrder = mod.SortOrder.FullPath; ImGui.SetNextItemWidth( 300 ); if( ImGui.InputText( "Sort Order", ref currentSortOrder, 256, ImGuiInputTextFlags.EnterReturnsTrue ) ) { manager.ChangeSortOrder( mod, currentSortOrder ); + selector.Cache.ResetModList(); selector.SelectModByDir( mod.BasePath.Name ); return true; } @@ -337,7 +332,6 @@ namespace Penumbra.UI { Service< ModManager >.Get()!.RenameModFolder( Mod.Data, newDir, false ); - _selector.ResetModNamesLower(); _selector.SelectModByDir( _newName ); closeParent = true; @@ -400,7 +394,6 @@ namespace Penumbra.UI } } - private void DrawRenameModFolderButton() { DrawRenameModFolderPopup(); @@ -452,7 +445,9 @@ namespace Penumbra.UI if( ImGui.IsItemHovered() ) { ImGui.SetTooltip( - "Force a recomputation of the metadata_manipulations.json file from all .meta files in the folder.\nAlso reloads the mod.\nBe aware that this removes all manually added metadata changes." ); + "Force a recomputation of the metadata_manipulations.json file from all .meta files in the folder.\n" + + "Also reloads the mod.\n" + + "Be aware that this removes all manually added metadata changes." ); } } @@ -507,11 +502,6 @@ namespace Penumbra.UI public void Draw() { - if( Mod == null ) - { - return; - } - try { var ret = ImGui.BeginChild( LabelModPanel, AutoFillSize, true ); @@ -520,6 +510,12 @@ namespace Penumbra.UI return; } + if( Mod == null ) + { + ImGui.EndChild(); + return; + } + DrawHeaderLine(); // Next line with fixed distance. diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index a0aa8230..606cb09c 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; +using System.Runtime.InteropServices; using Dalamud.Interface; using Dalamud.Plugin; using ImGuiNET; @@ -15,52 +16,16 @@ namespace Penumbra.UI { public partial class SettingsInterface { - private class Selector + // Constants + private partial class Selector { - [Flags] - private enum ModFilter - { - Enabled = 1 << 0, - Disabled = 1 << 1, - NoConflict = 1 << 2, - SolvedConflict = 1 << 3, - UnsolvedConflict = 1 << 4, - HasNoMetaManipulations = 1 << 5, - HasMetaManipulations = 1 << 6, - HasNoFileSwaps = 1 << 7, - HasFileSwaps = 1 << 8, - HasConfig = 1 << 9, - HasNoConfig = 1 << 10, - HasNoFiles = 1 << 11, - HasFiles = 1 << 12, - }; - - private const ModFilter UnfilteredStateMods = ( ModFilter )( ( 1 << 13 ) - 1 ); - - private static readonly Dictionary< ModFilter, string > ModFilterNames = new() - { - { ModFilter.Enabled, "Enabled" }, - { ModFilter.Disabled, "Disabled" }, - { ModFilter.NoConflict, "No Conflicts" }, - { ModFilter.SolvedConflict, "Solved Conflicts" }, - { ModFilter.UnsolvedConflict, "Unsolved Conflicts" }, - { ModFilter.HasNoMetaManipulations, "No Meta Manipulations" }, - { ModFilter.HasMetaManipulations, "Meta Manipulations" }, - { ModFilter.HasNoFileSwaps, "No File Swaps" }, - { ModFilter.HasFileSwaps, "File Swaps" }, - { ModFilter.HasNoConfig, "No Configuration" }, - { ModFilter.HasConfig, "Configuration" }, - { ModFilter.HasNoFiles, "No Files" }, - { ModFilter.HasFiles, "Files" }, - }; - private const string LabelSelectorList = "##availableModList"; private const string LabelModFilter = "##ModFilter"; private const string LabelAddModPopup = "AddModPopup"; private const string LabelModHelpPopup = "Help##Selector"; private const string TooltipModFilter = - "Filter mods for those containing the given substring.\nEnter c:[string] to filter for mods changing specific items.\n:Enter a:[string] to filter for mods by specific authors."; + "Filter mods for those containing the given substring.\nEnter c:[string] to filter for mods changing specific items.\nEnter a:[string] to filter for mods by specific authors."; private const string TooltipDelete = "Delete the selected mod"; private const string TooltipAdd = "Add an empty mod"; @@ -68,60 +33,23 @@ namespace Penumbra.UI private const string ButtonYesDelete = "Yes, delete it"; private const string ButtonNoDelete = "No, keep it"; - private const float SelectorPanelWidth = 240f; - private const uint DisabledModColor = 0xFF666666; - private const uint ConflictingModColor = 0xFFAAAAFF; - private const uint HandledConflictModColor = 0xFF88DDDD; + private const float SelectorPanelWidth = 240f; private static readonly Vector2 SelectorButtonSizes = new( 100, 0 ); private static readonly Vector2 HelpButtonSizes = new( 40, 0 ); + } - private readonly SettingsInterface _base; - private readonly ModManager _modManager; - private string _currentModGroup = ""; - - private List< Mod.Mod >? Mods - => _modManager.Collections.CurrentCollection.Cache?.AvailableMods; - - private float _selectorFactor = 1; - - public Mod.Mod? Mod { get; private set; } - private int _index; - private int? _deleteIndex; - private string _modFilterInput = ""; - private string _modFilter = ""; - private string _modFilterChanges = ""; - private string _modFilterAuthor = ""; - private string[] _modNamesLower; - private ModFilter _stateFilter = UnfilteredStateMods; - - public Selector( SettingsInterface ui ) - { - _base = ui; - _modNamesLower = Array.Empty< string >(); - _modManager = Service< ModManager >.Get(); - ResetModNamesLower(); - } - - public void ResetModNamesLower() - { - _modNamesLower = Mods?.Select( m => m.Data.Meta.Name.ToLowerInvariant() ).ToArray() - ?? Array.Empty< string >(); - } - - public void RenameCurrentModLower( string newName ) - { - if( _index >= 0 ) - { - _modNamesLower[ _index ] = newName.ToLowerInvariant(); - } - } + // Buttons + private partial class Selector + { + // === Delete === + private int? _deleteIndex; private void DrawModTrashButton() { ImGui.PushFont( UiBuilder.IconFont ); - if( ImGui.Button( FontAwesomeIcon.Trash.ToIconString(), SelectorButtonSizes * _selectorFactor ) && _index >= 0 ) + if( ImGui.Button( FontAwesomeIcon.Trash.ToIconString(), SelectorButtonSizes * _selectorScalingFactor ) && _index >= 0 ) { _deleteIndex = _index; } @@ -134,215 +62,6 @@ namespace Penumbra.UI } } - private bool _keyboardFocus = true; - - private void DrawModAddButton() - { - if( ImGui.BeginPopup( LabelAddModPopup ) ) - { - if( _keyboardFocus ) - { - ImGui.SetKeyboardFocusHere(); - _keyboardFocus = false; - } - - var newName = ""; - if( ImGui.InputTextWithHint( "##AddMod", "New Mod Name...", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) - { - try - { - var newDir = TexToolsImport.CreateModFolder( new DirectoryInfo( _base._plugin.Configuration!.ModDirectory ), - newName ); - var modMeta = new ModMeta - { - Author = "Unknown", - Name = newName.Replace( '/', '\\' ), - Description = string.Empty, - }; - - var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); - modMeta.SaveToFile( metaFile ); - _modManager.AddMod( newDir ); - SelectModByDir( newDir.Name ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not create directory for new Mod {newName}:\n{e}" ); - } - - ImGui.CloseCurrentPopup(); - } - - if( ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) - { - ImGui.CloseCurrentPopup(); - } - - ImGui.EndPopup(); - } - - ImGui.PushFont( UiBuilder.IconFont ); - - if( ImGui.Button( FontAwesomeIcon.Plus.ToIconString(), SelectorButtonSizes * _selectorFactor ) ) - { - _keyboardFocus = true; - ImGui.OpenPopup( LabelAddModPopup ); - } - - ImGui.PopFont(); - - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( TooltipAdd ); - } - } - - private void DrawModHelpButton() - { - ImGui.PushFont( UiBuilder.IconFont ); - if( ImGui.Button( FontAwesomeIcon.QuestionCircle.ToIconString(), HelpButtonSizes * _selectorFactor ) ) - { - ImGui.OpenPopup( LabelModHelpPopup ); - } - - ImGui.PopFont(); - } - - private void DrawModHelpPopup() - { - ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); - ImGui.SetNextWindowSize( new Vector2( 5 * SelectorPanelWidth, 29 * ImGui.GetTextLineHeightWithSpacing() ), - ImGuiCond.Appearing ); - var _ = true; - if( !ImGui.BeginPopupModal( LabelModHelpPopup, ref _, ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove ) ) - { - return; - } - - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); - ImGui.Text( "Mod Selector" ); - ImGui.BulletText( "Select a mod to obtain more information." ); - ImGui.BulletText( "Mod names are colored according to their current state in the collection:" ); - ImGui.Indent(); - ImGui.Bullet(); - ImGui.SameLine(); - ImGui.Text( "Enabled in the current collection." ); - ImGui.Bullet(); - ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( DisabledModColor ), "Disabled in the current collection." ); - ImGui.Bullet(); - ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( HandledConflictModColor ), - "Enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)." ); - ImGui.Bullet(); - ImGui.SameLine(); - ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ConflictingModColor ), - "Enabled and conflicting with another enabled Mod on the same priority." ); - ImGui.Unindent(); - ImGui.BulletText( "Right-click a mod to enter its sort order, which is its name by default." ); - ImGui.Indent(); - ImGui.BulletText( "A sort order differing from the mods name will not be displayed, it will just be used for ordering." ); - ImGui.BulletText( - "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into collapsible folders that can group mods." ); - ImGui.BulletText( - "Collapsible folders can contain further collapsible folders, so \"folder1/folder2/folder3/1\" will produce 3 folders\n\t\t[folder1] -> [folder2] -> [folder3] -> [ModName],\nwhere ModName will be sorted as if it was the string '1'." ); - ImGui.Unindent(); - ImGui.BulletText( "Use the Filter Mods... input at the top to filter the list for mods with names containing the text." ); - ImGui.Indent(); - ImGui.BulletText( "You can enter c:[string] to filter for Changed Items instead." ); - ImGui.BulletText( "You can enter a:[string] to filter for Mod Authors instead." ); - ImGui.Unindent(); - ImGui.BulletText( "Use the expandable menu beside the input to filter for mods fulfilling specific criteria." ); - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); - ImGui.Text( "Mod Management" ); - ImGui.BulletText( "You can delete the currently selected mod with the trashcan button." ); - ImGui.BulletText( "You can add a completely empty mod with the plus button." ); - ImGui.BulletText( "You can import TTMP-based mods in the import tab." ); - ImGui.BulletText( - "You can import penumbra-based mods by moving the corresponding folder into your mod directory in a file explorer, then rediscovering mods." ); - ImGui.BulletText( - "If you enable Advanced Options in the Settings tab, you can toggle Edit Mode to manipulate your selected mod even further." ); - ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); - ImGui.Dummy( Vector2.UnitX * 2 * SelectorPanelWidth ); - ImGui.SameLine(); - if( ImGui.Button( "Understood", Vector2.UnitX * SelectorPanelWidth ) ) - { - ImGui.CloseCurrentPopup(); - } - - ImGui.EndPopup(); - } - - private void DrawModsSelectorFilter() - { - ImGui.SetNextItemWidth( SelectorPanelWidth * _selectorFactor - 22 ); - if( ImGui.InputTextWithHint( LabelModFilter, "Filter Mods...", ref _modFilterInput, 256 ) ) - { - var lower = _modFilterInput.ToLowerInvariant(); - if( lower.StartsWith( "c:" ) ) - { - _modFilterChanges = lower.Substring( 2 ); - _modFilter = string.Empty; - _modFilterAuthor = string.Empty; - } - else if( lower.StartsWith( "a:" ) ) - { - _modFilterAuthor = lower.Substring( 2 ); - _modFilter = string.Empty; - _modFilterChanges = string.Empty; - } - else - { - _modFilter = lower; - _modFilterAuthor = string.Empty; - _modFilterChanges = string.Empty; - } - } - - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( TooltipModFilter ); - } - - ImGui.SameLine(); - if( ImGui.BeginCombo( "##ModStateFilter", "", - ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest ) ) - { - var flags = ( int )_stateFilter; - foreach( ModFilter flag in Enum.GetValues( typeof( ModFilter ) ) ) - { - ImGui.CheckboxFlags( ModFilterNames[ flag ], ref flags, ( int )flag ); - } - - _stateFilter = ( ModFilter )flags; - - ImGui.EndCombo(); - } - - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( "Filter mods for their activation status." ); - } - } - - private void DrawModsSelectorButtons() - { - // Selector controls - ImGui.PushStyleVar( ImGuiStyleVar.WindowPadding, ZeroVector ); - ImGui.PushStyleVar( ImGuiStyleVar.FrameRounding, 0 ); - - DrawModAddButton(); - ImGui.SameLine(); - DrawModHelpButton(); - ImGui.SameLine(); - DrawModTrashButton(); - - - ImGui.PopStyleVar( 3 ); - - DrawModHelpPopup(); - } - private void DrawDeleteModal() { if( _deleteIndex == null ) @@ -369,7 +88,6 @@ namespace Penumbra.UI } ImGui.Text( "Are you sure you want to delete the following mod:" ); - // todo: why the fuck does this become null?????? ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); ImGui.TextColored( new Vector4( 0.7f, 0.1f, 0.1f, 1 ), Mod.Data.Meta.Name ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() ) / 2 ); @@ -379,7 +97,6 @@ namespace Penumbra.UI { ImGui.CloseCurrentPopup(); _modManager.DeleteMod( Mod.Data.BasePath ); - ResetModNamesLower(); ClearSelection(); } @@ -394,229 +111,318 @@ namespace Penumbra.UI ImGui.EndPopup(); } - private bool CheckFlags( int count, ModFilter hasNoFlag, ModFilter hasFlag ) + // === Add === + private bool _modAddKeyboardFocus = true; + + private void DrawModAddButton() { - if( count == 0 ) + ImGui.PushFont( UiBuilder.IconFont ); + + if( ImGui.Button( FontAwesomeIcon.Plus.ToIconString(), SelectorButtonSizes * _selectorScalingFactor ) ) { - if( _stateFilter.HasFlag( hasNoFlag ) ) - { - return false; - } - } - else if( _stateFilter.HasFlag( hasFlag ) ) - { - return false; + _modAddKeyboardFocus = true; + ImGui.OpenPopup( LabelAddModPopup ); } - return true; + ImGui.PopFont(); + + if( ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( TooltipAdd ); + } + + DrawModAddPopup(); } - private bool CheckFilters( Mod.Mod mod, int modIndex ) - => ( _modFilter.Length == 0 || _modNamesLower[ modIndex ].Contains( _modFilter ) ) - && ( _modFilterAuthor.Length == 0 || mod.Data.Meta.Author.ToLowerInvariant().Contains( _modFilterAuthor ) ) - && ( _modFilterChanges.Length == 0 - || mod.Data.ChangedItems.Any( s => s.Key.ToLowerInvariant().Contains( _modFilterChanges ) ) ) - && !CheckFlags( mod.Data.Resources.ModFiles.Count, ModFilter.HasNoFiles, ModFilter.HasFiles ) - && !CheckFlags( mod.Data.Meta.FileSwaps.Count, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps ) - && !CheckFlags( mod.Data.Resources.MetaManipulations.Count, ModFilter.HasNoMetaManipulations, ModFilter.HasMetaManipulations ) - && !CheckFlags( mod.Data.Meta.HasGroupsWithConfig ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig ); - - private void DrawModOrderPopup( string popupName, Mod.Mod mod, bool firstOpen ) + private void DrawModAddPopup() { - if( !ImGui.BeginPopup( popupName ) ) + if( !ImGui.BeginPopup( LabelAddModPopup ) ) { return; } - if( ModPanel.DrawSortOrder( mod.Data, _modManager, this ) ) + if( _modAddKeyboardFocus ) { + ImGui.SetKeyboardFocusHere(); + _modAddKeyboardFocus = false; + } + + var newName = ""; + if( ImGui.InputTextWithHint( "##AddMod", "New Mod Name...", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) + { + try + { + var newDir = TexToolsImport.CreateModFolder( new DirectoryInfo( _base._plugin.Configuration!.ModDirectory ), + newName ); + var modMeta = new ModMeta + { + Author = "Unknown", + Name = newName.Replace( '/', '\\' ), + Description = string.Empty, + }; + + var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); + modMeta.SaveToFile( metaFile ); + _modManager.AddMod( newDir ); + SelectModByDir( newDir.Name ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not create directory for new Mod {newName}:\n{e}" ); + } + ImGui.CloseCurrentPopup(); } - if( firstOpen ) + if( ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) { - ImGui.SetKeyboardFocusHere( mod.Data.SortOrder.Length - 1 ); + ImGui.CloseCurrentPopup(); } ImGui.EndPopup(); } - private void DrawMod( Mod.Mod mod, int modIndex ) + // === Help === + private void DrawModHelpButton() { - var changedColour = false; - if( !mod.Settings.Enabled ) + ImGui.PushFont( UiBuilder.IconFont ); + if( ImGui.Button( FontAwesomeIcon.QuestionCircle.ToIconString(), HelpButtonSizes * _selectorScalingFactor ) ) { - if( !_stateFilter.HasFlag( ModFilter.Disabled ) || !_stateFilter.HasFlag( ModFilter.NoConflict ) ) - { - return; - } - - ImGui.PushStyleColor( ImGuiCol.Text, DisabledModColor ); - changedColour = true; - } - else - { - if( !_stateFilter.HasFlag( ModFilter.Enabled ) ) - { - return; - } - - if( mod.Cache.Conflicts.Any() ) - { - if( mod.Cache.Conflicts.Keys.Any( m => m.Settings.Priority == mod.Settings.Priority ) ) - { - if( !_stateFilter.HasFlag( ModFilter.UnsolvedConflict ) ) - { - return; - } - - ImGui.PushStyleColor( ImGuiCol.Text, ConflictingModColor ); - } - else - { - if( !_stateFilter.HasFlag( ModFilter.SolvedConflict ) ) - { - return; - } - - ImGui.PushStyleColor( ImGuiCol.Text, HandledConflictModColor ); - } - - changedColour = true; - } - else if( !_stateFilter.HasFlag( ModFilter.NoConflict ) ) - { - return; - } + ImGui.OpenPopup( LabelModHelpPopup ); } - var selected = ImGui.Selectable( $"{mod.Data.Meta.Name}##{modIndex}", modIndex == _index ); - - if( changedColour ) - { - ImGui.PopStyleColor(); - } - - var popupName = $"##SortOrderPopup{modIndex}"; - var firstOpen = false; - if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) - { - ImGui.OpenPopup( popupName ); - firstOpen = true; - } - - DrawModOrderPopup( popupName, mod, firstOpen ); - - if( selected ) - { - SetSelection( modIndex, mod ); - } + ImGui.PopFont(); } - private bool DrawModGroup( Mod.Mod mod, ref int modIndex ) + private static void DrawModHelpPopup() { - if( !CheckFilters( mod, modIndex ) ) - { - return true; - } - - if( !mod.Data.SortOrder.StartsWith( _currentModGroup ) ) - { - var lastFolder = _currentModGroup.LastIndexOf( '/', _currentModGroup.Length - 2 ); - _currentModGroup = lastFolder == -1 ? string.Empty : _currentModGroup.Substring( 0, lastFolder + 1 ); - ImGui.TreePop(); - return false; - } - - var nextFolder = mod.Data.SortOrder.IndexOf( '/', _currentModGroup.Length ); - if( nextFolder == -1 ) - { - DrawMod( mod, modIndex ); - } - else - { - var mods = Mods!; - var folderLabel = - $"{mod.Data.SortOrder.Substring( _currentModGroup.Length, nextFolder - _currentModGroup.Length )}##{_currentModGroup}"; - _currentModGroup = mod.Data.SortOrder.Substring( 0, nextFolder + 1 ); - - if( ImGui.TreeNodeEx( folderLabel ) ) - { - for( ; modIndex < mods.Count; ++modIndex ) - { - if( !DrawModGroup( mods[ modIndex ], ref modIndex ) ) - { - return false; - } - } - } - else - { - ImGui.TreePush(); - for( ; modIndex < mods.Count; ++modIndex ) - { - if( !mods[ modIndex ].Data.SortOrder.StartsWith( _currentModGroup ) ) - { - return false; - } - } - } - - if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) - { } - } - - return true; - } - - private void CleanUpLastGroup() - { - var numFolders = _currentModGroup.Count( c => c == '/' ); - while( numFolders-- > 0 ) - { - ImGui.TreePop(); - } - - _currentModGroup = string.Empty; - } - - public void Draw() - { - if( Mods == null ) + ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); + ImGui.SetNextWindowSize( new Vector2( 5 * SelectorPanelWidth, 33 * ImGui.GetTextLineHeightWithSpacing() ), + ImGuiCond.Appearing ); + var _ = true; + if( !ImGui.BeginPopupModal( LabelModHelpPopup, ref _, ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove ) ) { return; } - _selectorFactor = _base._plugin.Configuration.ScaleModSelector ? ImGui.GetWindowWidth() / SettingsMenu.MinSettingsSize.X : 1f; - // Selector pane - ImGui.BeginGroup(); - ImGui.PushStyleVar( ImGuiStyleVar.ItemSpacing, ZeroVector ); - - DrawModsSelectorFilter(); - // Inlay selector list - ImGui.BeginChild( LabelSelectorList, new Vector2( SelectorPanelWidth * _selectorFactor, -ImGui.GetFrameHeightWithSpacing() ), - true, ImGuiWindowFlags.HorizontalScrollbar ); - - ImGui.PushStyleVar( ImGuiStyleVar.IndentSpacing, 12.5f ); - for( var modIndex = 0; modIndex < Mods!.Count; ) + ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); + ImGui.Text( "Mod Selector" ); + ImGui.BulletText( "Select a mod to obtain more information." ); + ImGui.BulletText( "Mod names are colored according to their current state in the collection:" ); + ImGui.Indent(); + ImGui.Bullet(); + ImGui.SameLine(); + ImGui.Text( "Enabled in the current collection." ); + ImGui.Bullet(); + ImGui.SameLine(); + ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.DisabledModColor ), "Disabled in the current collection." ); + ImGui.Bullet(); + ImGui.SameLine(); + ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.HandledConflictModColor ), + "Enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)." ); + ImGui.Bullet(); + ImGui.SameLine(); + ImGui.TextColored( ImGui.ColorConvertU32ToFloat4( ModListCache.ConflictingModColor ), + "Enabled and conflicting with another enabled Mod on the same priority." ); + ImGui.Unindent(); + ImGui.BulletText( "Right-click a mod to enter its sort order, which is its name by default." ); + ImGui.Indent(); + ImGui.BulletText( "A sort order differing from the mods name will not be displayed, it will just be used for ordering." ); + ImGui.BulletText( + "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into collapsible folders that can group mods." ); + ImGui.BulletText( + "Collapsible folders can contain further collapsible folders, so \"folder1/folder2/folder3/1\" will produce 3 folders\n" + + "\t\t[folder1] -> [folder2] -> [folder3] -> [ModName],\n" + + "where ModName will be sorted as if it was the string '1'." ); + ImGui.Unindent(); + ImGui.BulletText( + "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod." ); + ImGui.BulletText( "Right-clicking a folder opens a context menu." ); + ImGui.Indent(); + ImGui.BulletText( + "You can rename folders in the context menu. Leave the text blank and press enter to merge the folder with its parent." ); + ImGui.BulletText( "You can also enable or disable all descendant mods of a folder." ); + ImGui.Unindent(); + ImGui.BulletText( "Use the Filter Mods... input at the top to filter the list for mods with names containing the text." ); + ImGui.Indent(); + ImGui.BulletText( "You can enter c:[string] to filter for Changed Items instead." ); + ImGui.BulletText( "You can enter a:[string] to filter for Mod Authors instead." ); + ImGui.Unindent(); + ImGui.BulletText( "Use the expandable menu beside the input to filter for mods fulfilling specific criteria." ); + ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); + ImGui.Text( "Mod Management" ); + ImGui.BulletText( "You can delete the currently selected mod with the trashcan button." ); + ImGui.BulletText( "You can add a completely empty mod with the plus button." ); + ImGui.BulletText( "You can import TTMP-based mods in the import tab." ); + ImGui.BulletText( + "You can import penumbra-based mods by moving the corresponding folder into your mod directory in a file explorer, then rediscovering mods." ); + ImGui.BulletText( + "If you enable Advanced Options in the Settings tab, you can toggle Edit Mode to manipulate your selected mod even further." ); + ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() ); + ImGui.Dummy( Vector2.UnitX * 2 * SelectorPanelWidth ); + ImGui.SameLine(); + if( ImGui.Button( "Understood", Vector2.UnitX * SelectorPanelWidth ) ) { - if( DrawModGroup( Mods[ modIndex ], ref modIndex ) ) + ImGui.CloseCurrentPopup(); + } + + ImGui.EndPopup(); + } + + // === Main === + private void DrawModsSelectorButtons() + { + // Selector controls + ImGui.PushStyleVar( ImGuiStyleVar.WindowPadding, ZeroVector ); + ImGui.PushStyleVar( ImGuiStyleVar.FrameRounding, 0 ); + + DrawModAddButton(); + ImGui.SameLine(); + DrawModHelpButton(); + ImGui.SameLine(); + DrawModTrashButton(); + + ImGui.PopStyleVar( 2 ); + } + } + + // Filters + private partial class Selector + { + private string _modFilterInput = ""; + + private void DrawTextFilter() + { + ImGui.SetNextItemWidth( SelectorPanelWidth * _selectorScalingFactor - 22 ); + var tmp = _modFilterInput; + if( ImGui.InputTextWithHint( LabelModFilter, "Filter Mods...", ref tmp, 256 ) && _modFilterInput != tmp ) + { + Cache.SetTextFilter( tmp ); + _modFilterInput = tmp; + } + + if( ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( TooltipModFilter ); + } + } + + private void DrawToggleFilter() + { + if( ImGui.BeginCombo( "##ModStateFilter", "", + ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest ) ) + { + var flags = ( int )Cache.StateFilter; + foreach( ModFilter flag in Enum.GetValues( typeof( ModFilter ) ) ) { - ++modIndex; + ImGui.CheckboxFlags( flag.ToName(), ref flags, ( int )flag ); + } + + Cache.StateFilter = ( ModFilter )flags; + + ImGui.EndCombo(); + } + + if( ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( "Filter mods for their activation status." ); + } + } + + private void DrawModsSelectorFilter() + { + DrawTextFilter(); + ImGui.SameLine(); + DrawToggleFilter(); + } + } + + // Drag'n Drop + private partial class Selector + { + private const string DraggedModLabel = "ModIndex"; + private const string DraggedFolderLabel = "FolderName"; + + private readonly IntPtr _dragDropPayload = Marshal.AllocHGlobal( 4 ); + + private static unsafe bool IsDropping( string name ) + => ImGui.AcceptDragDropPayload( name ).NativePtr != null; + + private void DragDropTarget( ModFolder folder ) + { + if( !ImGui.BeginDragDropTarget() ) + { + return; + } + + if( IsDropping( DraggedModLabel ) ) + { + var payload = ImGui.GetDragDropPayload(); + var modIndex = Marshal.ReadInt32( payload.Data ); + var mod = Cache.GetMod( modIndex ).Item1; + if( mod != null ) + { + if( mod.Data.Move( folder ) ) + { + Cache.ResetModList(); + } + } + } + else if( IsDropping( DraggedFolderLabel ) ) + { + var payload = ImGui.GetDragDropPayload(); + var folderName = Marshal.PtrToStringUni( payload.Data ); + if( ModFileSystem.Find( folderName!, out var droppedFolder ) + && !ReferenceEquals( droppedFolder, folder ) + && !folder.FullName.StartsWith( folderName, StringComparison.InvariantCultureIgnoreCase ) ) + { + if( droppedFolder.Move( folder ) ) + { + Cache.ResetModList(); + } } } - CleanUpLastGroup(); - ImGui.PopStyleVar(); - - ImGui.EndChild(); - - DrawModsSelectorButtons(); - ImGui.EndGroup(); - - DrawDeleteModal(); + ImGui.EndDragDropTarget(); } + private void DragDropSourceFolder( ModFolder folder ) + { + if( !ImGui.BeginDragDropSource() ) + { + return; + } + + var folderName = folder.FullName; + var ptr = Marshal.StringToHGlobalUni( folderName ); + ImGui.SetDragDropPayload( DraggedFolderLabel, ptr, ( uint )( folderName.Length + 1 ) * 2 ); + ImGui.Text( $"Moving {folderName}..." ); + ImGui.EndDragDropSource(); + } + + private void DragDropSourceMod( int modIndex, string modName ) + { + if( !ImGui.BeginDragDropSource() ) + { + return; + } + + Marshal.WriteInt32( _dragDropPayload, modIndex ); + ImGui.SetDragDropPayload( "ModIndex", _dragDropPayload, 4 ); + ImGui.Text( $"Moving {modName}..." ); + ImGui.EndDragDropSource(); + } + + ~Selector() + => Marshal.FreeHGlobal( _dragDropPayload ); + } + + // Selection + private partial class Selector + { + public Mod.Mod? Mod { get; private set; } + private int _index; + private void SetSelection( int idx, Mod.Mod? info ) { Mod = info; @@ -631,7 +437,7 @@ namespace Penumbra.UI private void SetSelection( int idx ) { - if( idx >= ( Mods?.Count ?? 0 ) ) + if( idx >= Cache.Count ) { idx = -1; } @@ -642,26 +448,26 @@ namespace Penumbra.UI } else { - SetSelection( idx, Mods![ idx ] ); + SetSelection( idx, Cache.GetMod( idx ).Item1 ); } } public void ReloadSelection() - => SetSelection( _index, Mods![ _index ] ); + => SetSelection( _index, Cache.GetMod( _index ).Item1 ); public void ClearSelection() => SetSelection( -1 ); public void SelectModByName( string name ) { - var idx = Mods?.FindIndex( mod => mod.Data.Meta.Name == name ) ?? -1; - SetSelection( idx ); + var (mod, idx) = Cache.GetModByName( name ); + SetSelection( idx, mod ); } public void SelectModByDir( string name ) { - var idx = Mods?.FindIndex( mod => mod.Data.BasePath.Name == name ) ?? -1; - SetSelection( idx ); + var (mod, idx) = Cache.GetModByBasePath( name ); + SetSelection( idx, mod ); } public void ReloadCurrentMod( bool reloadMeta = false, bool recomputeMeta = false ) @@ -673,7 +479,7 @@ namespace Penumbra.UI if( _index >= 0 && _modManager.UpdateMod( Mod.Data, reloadMeta, recomputeMeta ) ) { - ResetModNamesLower(); + Cache.ResetModList(); SelectModByDir( Mod.Data.BasePath.Name ); _base._menu.InstalledTab.ModPanel.Details.ResetState(); } @@ -682,5 +488,271 @@ namespace Penumbra.UI public void SaveCurrentMod() => Mod?.Data.SaveMeta(); } + + // Right-Clicks + private partial class Selector + { + // === Mod === + private void DrawModOrderPopup( string popupName, Mod.Mod mod, bool firstOpen ) + { + if( !ImGui.BeginPopup( popupName ) ) + { + return; + } + + if( ModPanel.DrawSortOrder( mod.Data, _modManager, this ) ) + { + ImGui.CloseCurrentPopup(); + } + + if( firstOpen ) + { + ImGui.SetKeyboardFocusHere( mod.Data.SortOrder.FullPath.Length - 1 ); + } + + ImGui.EndPopup(); + } + + // === Folder === + private string _newFolderName = string.Empty; + + private void ChangeStatusOfChildren( ModFolder folder, int currentIdx, bool toWhat ) + { + var change = false; + var metaManips = false; + foreach( var _ in folder.AllMods( _modManager.Config.SortFoldersFirst ) ) + { + var (mod, _, _) = Cache.GetMod( currentIdx++ ); + if( mod != null ) + { + change |= mod.Settings.Enabled != toWhat; + mod!.Settings.Enabled = toWhat; + metaManips |= mod.Data.Resources.MetaManipulations.Count > 0; + } + } + + if( !change ) + { + return; + } + + Cache.ResetFilters(); + var collection = _modManager.Collections.CurrentCollection; + if( collection.Cache != null ) + { + collection.CalculateEffectiveFileList( _modManager.BasePath, metaManips, + collection == _modManager.Collections.ActiveCollection ); + } + + collection.Save( _base._plugin.PluginInterface ); + } + + private void DrawRenameFolderInput( ModFolder folder ) + { + ImGui.SetNextItemWidth( 150 ); + if( !ImGui.InputTextWithHint( "##NewFolderName", "Rename Folder...", ref _newFolderName, 64, + ImGuiInputTextFlags.EnterReturnsTrue ) ) + { + return; + } + + bool changes; + if( _newFolderName.Any() ) + { + changes = folder.Rename( _newFolderName ); + _newFolderName = string.Empty; + } + else + { + changes = folder.Merge( folder.Parent! ); + } + + if( changes ) + { + Cache.ResetModList(); + } + } + + private void DrawFolderContextMenu( ModFolder folder, int currentIdx, string treeName ) + { + if( ImGui.BeginPopup( treeName ) ) + { + if( ImGui.MenuItem( "Enable All Descendants" ) ) + { + ChangeStatusOfChildren( folder, currentIdx, true ); + } + + if( ImGui.MenuItem( "Disable All Descendants" ) ) + { + ChangeStatusOfChildren( folder, currentIdx, false ); + } + + ImGui.Dummy( Vector2.UnitY * 10 ); + DrawRenameFolderInput( folder ); + + ImGui.EndPopup(); + } + } + } + + // Main-Interface + private partial class Selector + { + private readonly SettingsInterface _base; + private readonly ModManager _modManager; + public readonly ModListCache Cache; + + private float _selectorScalingFactor = 1; + + public Selector( SettingsInterface ui ) + { + _base = ui; + _modManager = Service< ModManager >.Get(); + Cache = new ModListCache( _modManager ); + } + + private void DrawHeaderBar() + { + const float size = 200; + DrawModsSelectorFilter(); + var textSize = ImGui.CalcTextSize( TabCollections.LabelCurrentCollection ).X + ImGui.GetStyle().ItemInnerSpacing.X; + var comboSize = size * ImGui.GetIO().FontGlobalScale; + var offset = comboSize + textSize; + ImGui.SameLine( ImGui.GetWindowContentRegionWidth() - offset ); + ImGui.SetNextItemWidth( comboSize ); + _base._menu.CollectionsTab.DrawCurrentCollectionSelector( false ); + } + + private void DrawFolderContent( ModFolder folder, ref int idx ) + { + // Collection may be manipulated. + foreach( var item in folder.GetItems( _modManager.Config.SortFoldersFirst ).ToArray() ) + { + if( item is ModFolder sub ) + { + var (visible, enabled) = Cache.GetFolder( sub ); + if( visible ) + { + DrawModFolder( sub, ref idx ); + } + else + { + idx += sub.TotalDescendantMods(); + } + } + else if( item is ModData _ ) + { + var (mod, visible, color) = Cache.GetMod( idx ); + if( mod != null && visible ) + { + DrawMod( mod, idx++, color ); + } + else + { + ++idx; + } + } + } + } + + private void DrawModFolder( ModFolder folder, ref int idx ) + { + var treeName = $"{folder.Name}##{folder.FullName}"; + var open = ImGui.TreeNodeEx( treeName ); + if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) + { + _newFolderName = string.Empty; + ImGui.OpenPopup( treeName ); + } + + DrawFolderContextMenu( folder, idx, treeName ); + DragDropTarget( folder ); + DragDropSourceFolder( folder ); + + if( open ) + { + DrawFolderContent( folder, ref idx ); + ImGui.TreePop(); + } + else + { + idx += folder.TotalDescendantMods(); + } + } + + private void DrawMod( Mod.Mod mod, int modIndex, uint color ) + { + if( color != 0 ) + { + ImGui.PushStyleColor( ImGuiCol.Text, color ); + } + + var selected = ImGui.Selectable( $"{mod.Data.Meta.Name}##{modIndex}", modIndex == _index ); + + if( color != 0 ) + { + ImGui.PopStyleColor(); + } + + var popupName = $"##SortOrderPopup{modIndex}"; + var firstOpen = false; + if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) + { + ImGui.OpenPopup( popupName ); + firstOpen = true; + } + + DragDropTarget( mod.Data.SortOrder.ParentFolder ); + DragDropSourceMod( modIndex, mod.Data.Meta.Name ); + + DrawModOrderPopup( popupName, mod, firstOpen ); + + if( selected ) + { + SetSelection( modIndex, mod ); + } + } + + public void Draw() + { + if( Cache.Count == 0 ) + { + return; + } + + ImGui.PushStyleVar( ImGuiStyleVar.ItemSpacing, ZeroVector ); + try + { + _selectorScalingFactor = _base._plugin.Configuration.ScaleModSelector + ? ImGui.GetWindowWidth() / SettingsMenu.MinSettingsSize.X + : 1f; + // Selector pane + DrawHeaderBar(); + ImGui.BeginGroup(); + // Inlay selector list + ImGui.BeginChild( LabelSelectorList, + new Vector2( SelectorPanelWidth * _selectorScalingFactor, -ImGui.GetFrameHeightWithSpacing() ), + true, ImGuiWindowFlags.HorizontalScrollbar ); + + ImGui.PushStyleVar( ImGuiStyleVar.IndentSpacing, 12.5f ); + var modIndex = 0; + DrawFolderContent( _modManager.StructuredMods, ref modIndex ); + ImGui.PopStyleVar(); + + ImGui.EndChild(); + + DrawModsSelectorButtons(); + ImGui.EndGroup(); + } + finally + { + ImGui.PopStyleVar(); + } + + DrawModHelpPopup(); + + DrawDeleteModal(); + } + } } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index 31efe8bd..12756558 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -22,6 +22,7 @@ namespace Penumbra.UI private const string LabelEnabled = "Enable Mods"; private const string LabelEnabledPlayerWatch = "Enable automatic Character Redraws"; private const string LabelWaitFrames = "Wait Frames"; + private const string LabelSortFoldersFirst = "Sort Mod Folders Before Mods"; private const string LabelScaleModSelector = "Scale Mod Selector With Window Size"; private const string LabelShowAdvanced = "Show Advanced Settings"; private const string LabelLogLoadedFiles = "Log all loaded files"; @@ -100,6 +101,17 @@ namespace Penumbra.UI } } + private void DrawSortFoldersFirstBox() + { + var foldersFirst = _config.SortFoldersFirst; + if( ImGui.Checkbox( LabelSortFoldersFirst, ref foldersFirst ) ) + { + _config.SortFoldersFirst = foldersFirst; + _base._menu.InstalledTab.Selector.Cache.ResetModList(); + _configChanged = true; + } + } + private void DrawScaleModSelectorBox() { var scaleModSelector = _config.ScaleModSelector; @@ -238,6 +250,7 @@ namespace Penumbra.UI Custom.ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); DrawScaleModSelectorBox(); + DrawSortFoldersFirstBox(); DrawShowAdvancedBox(); if( _config.ShowAdvanced ) diff --git a/Penumbra/UI/SettingsInterface.cs b/Penumbra/UI/SettingsInterface.cs index 09ef89f8..a387fd13 100644 --- a/Penumbra/UI/SettingsInterface.cs +++ b/Penumbra/UI/SettingsInterface.cs @@ -16,6 +16,7 @@ namespace Penumbra.UI private readonly ManageModsButton _manageModsButton; private readonly MenuBar _menuBar; private readonly SettingsMenu _menu; + private readonly ModManager _modManager; public SettingsInterface( Plugin plugin ) { @@ -23,6 +24,7 @@ namespace Penumbra.UI _manageModsButton = new ManageModsButton( this ); _menuBar = new MenuBar( this ); _menu = new SettingsMenu( this ); + _modManager = Service< ModManager >.Get(); } public void FlipVisibility() @@ -40,12 +42,27 @@ namespace Penumbra.UI private void ReloadMods() { - _menu.InstalledTab.Selector.ResetModNamesLower(); _menu.InstalledTab.Selector.ClearSelection(); + _modManager.DiscoverMods( _plugin.Configuration.ModDirectory ); + _menu.InstalledTab.Selector.Cache.ResetModList(); + } - var modManager = Service< ModManager >.Get(); - modManager.DiscoverMods( _plugin.Configuration.ModDirectory ); - _menu.InstalledTab.Selector.ResetModNamesLower(); + private void SaveCurrentCollection( bool recalculateMeta ) + { + var current = _modManager.Collections.CurrentCollection; + current.Save( _plugin.PluginInterface ); + RecalculateCurrent( recalculateMeta ); + } + + private void RecalculateCurrent( bool recalculateMeta ) + { + var current = _modManager.Collections.CurrentCollection; + if( current.Cache != null ) + { + current.CalculateEffectiveFileList( _modManager.BasePath, recalculateMeta, + current == _modManager.Collections.ActiveCollection ); + _menu.InstalledTab.Selector.Cache.ResetFilters(); + } } } } \ No newline at end of file diff --git a/Penumbra/UI/SettingsMenu.cs b/Penumbra/UI/SettingsMenu.cs index b1db0930..8e03c8d3 100644 --- a/Penumbra/UI/SettingsMenu.cs +++ b/Penumbra/UI/SettingsMenu.cs @@ -14,23 +14,23 @@ namespace Penumbra.UI public static readonly Vector2 MinSettingsSize = new( 800, 450 ); public static readonly Vector2 MaxSettingsSize = new( 69420, 42069 ); - private readonly SettingsInterface _base; - private readonly TabSettings _settingsTab; - private readonly TabImport _importTab; - private readonly TabBrowser _browserTab; - private readonly TabCollections _collectionsTab; - public readonly TabInstalled InstalledTab; - private readonly TabEffective _effectiveTab; + private readonly SettingsInterface _base; + private readonly TabSettings _settingsTab; + private readonly TabImport _importTab; + private readonly TabBrowser _browserTab; + internal readonly TabCollections CollectionsTab; + internal readonly TabInstalled InstalledTab; + private readonly TabEffective _effectiveTab; public SettingsMenu( SettingsInterface ui ) { - _base = ui; - _settingsTab = new TabSettings( _base ); - _importTab = new TabImport( _base ); - _browserTab = new TabBrowser(); - InstalledTab = new TabInstalled( _base ); - _collectionsTab = new TabCollections( InstalledTab.Selector ); - _effectiveTab = new TabEffective(); + _base = ui; + _settingsTab = new TabSettings( _base ); + _importTab = new TabImport( _base ); + _browserTab = new TabBrowser(); + InstalledTab = new TabInstalled( _base ); + CollectionsTab = new TabCollections( InstalledTab.Selector ); + _effectiveTab = new TabEffective(); } #if DEBUG @@ -62,7 +62,7 @@ namespace Penumbra.UI ImGui.BeginTabBar( PenumbraSettingsLabel ); _settingsTab.Draw(); - _collectionsTab.Draw(); + CollectionsTab.Draw(); _importTab.Draw(); if( Service< ModManager >.Get().Valid && !_importTab.IsImporting() )