From fec7819bf2ac36ef41d83a4612f1a21698e12ecd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Dec 2025 16:23:14 +0100 Subject: [PATCH] Current State. --- Luna | 2 +- OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.GameData | 2 +- Penumbra/Api/Api/UiApi.cs | 11 +- .../Cache/CollectionCacheManager.cs | 38 +- .../Collections/Manager/ActiveCollections.cs | 12 +- .../Collections/ModCollection.Cache.Access.cs | 26 +- Penumbra/CommandHandler.cs | 17 +- Penumbra/Communication/CollectionChange.cs | 123 +-- Penumbra/Communication/ModDirectoryChanged.cs | 3 +- Penumbra/Communication/ResolvedFileChanged.cs | 128 ++- Penumbra/Configuration.cs | 43 +- Penumbra/EphemeralConfig.cs | 2 +- .../PostProcessing/PreBoneDeformerReplacer.cs | 2 +- .../Interop/ResourceTree/ResolveContext.cs | 1 + Penumbra/Interop/ResourceTree/ResourceNode.cs | 1 + Penumbra/Interop/ResourceTree/ResourceTree.cs | 3 +- .../ResourceTree/ResourceTreeApiHelper.cs | 1 + Penumbra/Meta/Manipulations/AtchIdentifier.cs | 2 +- Penumbra/Meta/Manipulations/AtrIdentifier.cs | 2 +- Penumbra/Meta/Manipulations/Eqdp.cs | 2 +- Penumbra/Meta/Manipulations/Eqp.cs | 2 +- Penumbra/Meta/Manipulations/Est.cs | 2 +- Penumbra/Meta/Manipulations/Gmp.cs | 2 +- Penumbra/Meta/Manipulations/Imc.cs | 6 +- Penumbra/Meta/Manipulations/ShpIdentifier.cs | 2 +- Penumbra/Mods/Manager/ModFileSystem.cs | 40 +- Penumbra/Mods/Manager/ModFileSystemSaver.cs | 3 + Penumbra/Mods/Manager/SortModes.cs | 28 - Penumbra/Penumbra.cs | 17 +- Penumbra/Penumbra.csproj | 2 +- Penumbra/Services/CommunicatorService.cs | 173 ++-- Penumbra/Services/ConfigMigrationService.cs | 855 +++++++++--------- Penumbra/Services/FilenameService.cs | 1 + Penumbra/Services/StainService.cs | 2 +- Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 8 +- .../Materials/MtrlTab.ColorTable.cs | 164 ++-- .../Materials/MtrlTab.CommonColorTable.cs | 25 +- .../UI/AdvancedWindow/Materials/MtrlTab.cs | 2 +- .../Materials/MtrlTabFactory.cs | 1 + .../UI/AdvancedWindow/ModEditWindow.Models.cs | 6 +- .../ModEditWindow.QuickImport.cs | 1 + .../AdvancedWindow/ModEditWindow.ShpkTab.cs | 1 + .../AdvancedWindow/ModEditWindow.Textures.cs | 6 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 4 +- .../UI/AdvancedWindow/ModEditWindowFactory.cs | 1 + Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 147 ++- .../UI/AdvancedWindow/OptionSelectCombo.cs | 2 +- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 2 +- .../ResourceTreeViewerFactory.cs | 1 + .../UI/{ => Classes}/ChangedItemDrawer.cs | 9 +- .../UI/{ => Classes}/ChangedItemIconFlag.cs | 2 +- Penumbra/UI/Classes/CollectionSelectHeader.cs | 28 +- Penumbra/UI/Classes/Colors.cs | 19 +- Penumbra/UI/Classes/Combos.cs | 6 +- .../UI/{ => Classes}/FileDialogService.cs | 2 +- Penumbra/UI/{ => Classes}/IncognitoService.cs | 3 +- .../{Mods/Manager => UI/Classes}/ModCombo.cs | 4 +- Penumbra/UI/Classes/StainCombo.cs | 118 +++ Penumbra/UI/Classes/TutorialService.cs | 148 +++ Penumbra/UI/CollectionTab/CollectionPanel.cs | 16 +- .../UI/CollectionTab/CollectionSelector.cs | 286 +++--- Penumbra/UI/CollectionTab/InheritanceUi.cs | 4 +- Penumbra/UI/GlobalModImporter.cs | 45 + Penumbra/UI/LaunchButton.cs | 16 +- Penumbra/UI/MainWindow/ChangedItemsTab.cs | 191 ++++ Penumbra/UI/MainWindow/EffectiveTab.cs | 179 ++++ .../UI/{Tabs => MainWindow}/MainTabBar.cs | 11 +- .../MainWindow.cs} | 6 +- .../UI/{Tabs => MainWindow}/MessagesTab.cs | 0 .../UI/{Tabs => MainWindow}/OnScreenTab.cs | 2 +- Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs | 42 +- .../Groups/CombiningModGroupEditDrawer.cs | 207 ++--- .../ModsTab/Groups/ImcModGroupEditDrawer.cs | 376 ++++---- .../UI/ModsTab/Groups/ModGroupEditDrawer.cs | 713 +++++++-------- .../Groups/SingleModGroupEditDrawer.cs | 133 ++- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 2 +- Penumbra/UI/ModsTab/ModFilter.cs | 4 +- Penumbra/UI/ModsTab/ModPanel.cs | 152 ++-- Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs | 271 +++--- Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 112 +-- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 2 +- Penumbra/UI/ModsTab/ModPanelHeader.cs | 527 ++++++----- Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 524 +++++------ Penumbra/UI/ModsTab/ModPanelTabBar.cs | 3 +- .../UI/ModsTab/ModSearchStringSplitter.cs | 1 + Penumbra/UI/ModsTab/MultiModPanel.cs | 60 +- .../UI/ModsTab/Selector/AddNewModButton.cs | 97 ++ .../ClearDefaultImportFolderButton.cs | 22 + .../Selector/ClearTemporarySettingsButton.cs | 16 + .../Selector/EnableDescendantsButton.cs | 27 + .../UI/ModsTab/Selector/ModFileSystemCache.cs | 15 + .../ModsTab/Selector/ModFileSystemDrawer.cs | 57 ++ .../Selector/SetDefaultImportFolderButton.cs | 22 + Penumbra/UI/ModsTab/Selector/SortModes.cs | 63 ++ .../ModsTab/Selector/ToggleFavoriteButton.cs | 17 + Penumbra/UI/ResourceWatcher/Record.cs | 65 +- .../UI/ResourceWatcher/ResourceWatcher.cs | 42 +- .../ResourceWatcher/ResourceWatcherTable.cs | 773 ++++++++++------ Penumbra/UI/Tabs/ChangedItemsTab.cs | 121 --- Penumbra/UI/Tabs/CollectionsTab.cs | 15 +- Penumbra/UI/Tabs/Debug/AtchDrawer.cs | 52 +- Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs | 84 +- Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs | 61 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 44 +- .../UI/Tabs/Debug/GlobalVariablesDrawer.cs | 152 ++-- Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs | 20 +- Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs | 20 +- Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs | 75 +- Penumbra/UI/Tabs/Debug/ShapeInspector.cs | 105 +-- Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs | 71 +- Penumbra/UI/Tabs/EffectiveTab.cs | 195 ---- Penumbra/UI/Tabs/ModsTab.cs | 86 +- Penumbra/UI/Tabs/ResourceTab.cs | 87 +- Penumbra/UI/Tabs/SettingsTab.cs | 385 +++----- Penumbra/UI/TutorialService.cs | 171 ---- Penumbra/UI/WindowSystem.cs | 17 +- 118 files changed, 4849 insertions(+), 4283 deletions(-) delete mode 100644 Penumbra/Mods/Manager/SortModes.cs rename Penumbra/UI/{ => Classes}/ChangedItemDrawer.cs (98%) rename Penumbra/UI/{ => Classes}/ChangedItemIconFlag.cs (97%) rename Penumbra/UI/{ => Classes}/FileDialogService.cs (99%) rename Penumbra/UI/{ => Classes}/IncognitoService.cs (95%) rename Penumbra/{Mods/Manager => UI/Classes}/ModCombo.cs (89%) create mode 100644 Penumbra/UI/Classes/StainCombo.cs create mode 100644 Penumbra/UI/Classes/TutorialService.cs create mode 100644 Penumbra/UI/GlobalModImporter.cs create mode 100644 Penumbra/UI/MainWindow/ChangedItemsTab.cs create mode 100644 Penumbra/UI/MainWindow/EffectiveTab.cs rename Penumbra/UI/{Tabs => MainWindow}/MainTabBar.cs (86%) rename Penumbra/UI/{ConfigWindow.cs => MainWindow/MainWindow.cs} (97%) rename Penumbra/UI/{Tabs => MainWindow}/MessagesTab.cs (100%) rename Penumbra/UI/{Tabs => MainWindow}/OnScreenTab.cs (93%) create mode 100644 Penumbra/UI/ModsTab/Selector/AddNewModButton.cs create mode 100644 Penumbra/UI/ModsTab/Selector/ClearDefaultImportFolderButton.cs create mode 100644 Penumbra/UI/ModsTab/Selector/ClearTemporarySettingsButton.cs create mode 100644 Penumbra/UI/ModsTab/Selector/EnableDescendantsButton.cs create mode 100644 Penumbra/UI/ModsTab/Selector/ModFileSystemCache.cs create mode 100644 Penumbra/UI/ModsTab/Selector/ModFileSystemDrawer.cs create mode 100644 Penumbra/UI/ModsTab/Selector/SetDefaultImportFolderButton.cs create mode 100644 Penumbra/UI/ModsTab/Selector/SortModes.cs create mode 100644 Penumbra/UI/ModsTab/Selector/ToggleFavoriteButton.cs delete mode 100644 Penumbra/UI/Tabs/ChangedItemsTab.cs delete mode 100644 Penumbra/UI/Tabs/EffectiveTab.cs delete mode 100644 Penumbra/UI/TutorialService.cs diff --git a/Luna b/Luna index cb294f47..950ebea5 160000 --- a/Luna +++ b/Luna @@ -1 +1 @@ -Subproject commit cb294f476476f7a3d8b56a0072dbd300b3d54c4f +Subproject commit 950ebea591f4ab7dc0900cce22415c5221df3685 diff --git a/OtterGui b/OtterGui index 18e62ab2..1459e2b8 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 18e62ab2d8b9ac7028a33707eb35f8f9c61f245a +Subproject commit 1459e2b8f5e1687f659836709e23571235d4206c diff --git a/Penumbra.Api b/Penumbra.Api index c23ee05c..09cfde3d 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit c23ee05c1e9fa103eaa52e6aa7e855ef568ee669 +Subproject commit 09cfde3dd43aa5afcfd147ccbe3ee61534556f12 diff --git a/Penumbra.GameData b/Penumbra.GameData index 0901f2b7..885e5362 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 0901f2b7075c280c5216198921a3c09209b667d8 +Subproject commit 885e536267f814fc4e62e11a70a82cdde7b4778d diff --git a/Penumbra/Api/Api/UiApi.cs b/Penumbra/Api/Api/UiApi.cs index f776990b..7926a8e9 100644 --- a/Penumbra/Api/Api/UiApi.cs +++ b/Penumbra/Api/Api/UiApi.cs @@ -4,19 +4,20 @@ using Penumbra.Communication; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; +using Penumbra.UI.MainWindow; namespace Penumbra.Api.Api; public class UiApi : IPenumbraApiUi, Luna.IApiService, IDisposable { private readonly CommunicatorService _communicator; - private readonly ConfigWindow _configWindow; + private readonly MainWindow _mainWindow; private readonly ModManager _modManager; - public UiApi(CommunicatorService communicator, ConfigWindow configWindow, ModManager modManager) + public UiApi(CommunicatorService communicator, MainWindow mainWindow, ModManager modManager) { _communicator = communicator; - _configWindow = configWindow; + _mainWindow = mainWindow; _modManager = modManager; _communicator.ChangedItemHover.Subscribe(OnChangedItemHover, ChangedItemHover.Priority.Default); _communicator.ChangedItemClick.Subscribe(OnChangedItemClick, ChangedItemClick.Priority.Default); @@ -57,7 +58,7 @@ public class UiApi : IPenumbraApiUi, Luna.IApiService, IDisposable public PenumbraApiEc OpenMainWindow(TabType tab, string modDirectory, string modName) { - _configWindow.IsOpen = true; + _mainWindow.IsOpen = true; if (!Enum.IsDefined(tab)) return PenumbraApiEc.InvalidArgument; @@ -77,7 +78,7 @@ public class UiApi : IPenumbraApiUi, Luna.IApiService, IDisposable } public void CloseMainWindow() - => _configWindow.IsOpen = false; + => _mainWindow.IsOpen = false; private void OnChangedItemClick(in ChangedItemClick.Arguments arguments) { diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 3a79c2b4..ad48e8b4 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -85,8 +85,8 @@ public class CollectionCacheManager : IDisposable, IService foreach (var collection in _storage) { - collection._cache?.Dispose(); - collection._cache = null; + collection.Cache?.Dispose(); + collection.Cache = null; } } @@ -115,10 +115,10 @@ public class CollectionCacheManager : IDisposable, IService if (collection.Identity.Index == ModCollection.Empty.Identity.Index) return false; - if (collection._cache != null) + if (collection.Cache != null) return false; - collection._cache = new CollectionCache(this, collection); + collection.Cache = new CollectionCache(this, collection); if (collection.Identity.Index > 0) Interlocked.Increment(ref _count); Penumbra.Log.Verbose($"Created new cache for collection {collection.Identity.AnonymizedName}."); @@ -146,10 +146,10 @@ public class CollectionCacheManager : IDisposable, IService Penumbra.Log.Error( $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, no cache exists."); } - else if (collection._cache!.Calculating != -1) + else if (collection.Cache!.Calculating != -1) { Penumbra.Log.Error( - $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, already in calculation on [{collection._cache!.Calculating}]."); + $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, already in calculation on [{collection.Cache!.Calculating}]."); } else { @@ -162,7 +162,7 @@ public class CollectionCacheManager : IDisposable, IService private void FullRecalculation(ModCollection collection) { - var cache = collection._cache; + var cache = collection.Cache; if (cache is not { Calculating: -1 }) return; @@ -235,11 +235,11 @@ public class CollectionCacheManager : IDisposable, IService case ModPathChangeType.Deleted: case ModPathChangeType.StartingReload: foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(index).Settings?.Enabled == true)) - collection._cache!.RemoveMod(arguments.Mod, true); + collection.Cache!.RemoveMod(arguments.Mod, true); break; case ModPathChangeType.Moved: foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(index).Settings?.Enabled == true)) - collection._cache!.ReloadMod(arguments.Mod, true); + collection.Cache!.ReloadMod(arguments.Mod, true); break; } } @@ -251,7 +251,7 @@ public class CollectionCacheManager : IDisposable, IService var index = arguments.Mod.Index; foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(index).Settings?.Enabled == true)) - collection._cache!.AddMod(arguments.Mod, true); + collection.Cache!.AddMod(arguments.Mod, true); } /// Apply a mod change to all collections with a cache. @@ -281,7 +281,7 @@ public class CollectionCacheManager : IDisposable, IService var index = arguments.Mod.Index; foreach (var collection in _storage.Where(collection => collection.HasCache && collection.GetActualSettings(index).Settings is { Enabled: true })) - collection._cache!.RemoveMod(arguments.Mod, false); + collection.Cache!.RemoveMod(arguments.Mod, false); } else { @@ -295,9 +295,9 @@ public class CollectionCacheManager : IDisposable, IService => collection.HasCache && collection.GetActualSettings(index).Settings is { Enabled: true })) { if (justAdd) - collection._cache!.AddMod(arguments.Mod, true); + collection.Cache!.AddMod(arguments.Mod, true); else - collection._cache!.ReloadMod(arguments.Mod, true); + collection.Cache!.ReloadMod(arguments.Mod, true); } } } @@ -316,7 +316,7 @@ public class CollectionCacheManager : IDisposable, IService if (!collection.HasCache) return; - var cache = collection._cache!; + var cache = collection.Cache!; switch (arguments.Type) { case ModSettingChange.Inheritance: cache.ReloadMod(arguments.Mod!, true); break; @@ -366,8 +366,8 @@ public class CollectionCacheManager : IDisposable, IService if (!collection.HasCache) return; - collection._cache!.Dispose(); - collection._cache = null; + collection.Cache!.Dispose(); + collection.Cache = null; if (collection.Identity.Index > 0) Interlocked.Decrement(ref _count); Penumbra.Log.Verbose($"Cleared cache of collection {collection.Identity.AnonymizedName}."); @@ -398,9 +398,9 @@ public class CollectionCacheManager : IDisposable, IService { foreach (var collection in Active) { - collection._cache!.ResolvedFiles.Clear(); - collection._cache!.Meta.Reset(); - collection._cache!.ConflictDict.Clear(); + collection.Cache!.ResolvedFiles.Clear(); + collection.Cache!.Meta.Reset(); + collection.Cache!.ConflictDict.Clear(); } } diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 9adee887..4e034de4 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -320,7 +320,7 @@ public class ActiveCollections : ISavable, IDisposable, IService if (!_storage.ByName(defaultName, out var defaultCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Identity.Name}.", + $"Last choice of {"Base Collection"} {defaultName} is not available, reset to {ModCollection.Empty.Identity.Name}.", NotificationType.Warning); Default = ModCollection.Empty; configChanged = true; @@ -335,7 +335,7 @@ public class ActiveCollections : ISavable, IDisposable, IService if (!_storage.ByName(interfaceName, out var interfaceCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Identity.Name}.", + $"Last choice of Interface Collection {interfaceName} is not available, reset to {ModCollection.Empty.Identity.Name}.", NotificationType.Warning); Interface = ModCollection.Empty; configChanged = true; @@ -350,7 +350,7 @@ public class ActiveCollections : ISavable, IDisposable, IService if (!_storage.ByName(currentName, out var currentCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.", + $"Last choice of Selected Collection {currentName} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.", NotificationType.Warning); Current = _storage.DefaultNamed; configChanged = true; @@ -395,7 +395,7 @@ public class ActiveCollections : ISavable, IDisposable, IService if (!_storage.ById(defaultId, out var defaultCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Identity.Name}.", + $"Last choice of {"Base Collection"} {defaultId} is not available, reset to {ModCollection.Empty.Identity.Name}.", NotificationType.Warning); Default = ModCollection.Empty; configChanged = true; @@ -410,7 +410,7 @@ public class ActiveCollections : ISavable, IDisposable, IService if (!_storage.ById(interfaceId, out var interfaceCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.InterfaceCollection} {interfaceId} is not available, reset to {ModCollection.Empty.Identity.Name}.", + $"Last choice of {"Interface Collection"} {interfaceId} is not available, reset to {ModCollection.Empty.Identity.Name}.", NotificationType.Warning); Interface = ModCollection.Empty; configChanged = true; @@ -425,7 +425,7 @@ public class ActiveCollections : ISavable, IDisposable, IService if (!_storage.ById(currentId, out var currentCollection)) { Penumbra.Messager.NotificationMessage( - $"Last choice of {TutorialService.SelectedCollection} {currentId} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.", + $"Last choice of Selected Collection {currentId} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.", NotificationType.Warning); Current = _storage.DefaultNamed; configChanged = true; diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index ba071d16..824b8ac9 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -10,48 +10,48 @@ namespace Penumbra.Collections; public partial class ModCollection { // Only active collections need to have a cache. - internal CollectionCache? _cache; + public CollectionCache? Cache; public bool HasCache - => _cache != null; + => Cache != null; // Handle temporary mods for this collection. public void Apply(TemporaryMod tempMod, bool created) { if (created) - _cache?.AddMod(tempMod, tempMod.TotalManipulations > 0); + Cache?.AddMod(tempMod, tempMod.TotalManipulations > 0); else - _cache?.ReloadMod(tempMod, tempMod.TotalManipulations > 0); + Cache?.ReloadMod(tempMod, tempMod.TotalManipulations > 0); } public void Remove(TemporaryMod tempMod) { - _cache?.RemoveMod(tempMod, tempMod.TotalManipulations > 0); + Cache?.RemoveMod(tempMod, tempMod.TotalManipulations > 0); } public IEnumerable ReverseResolvePath(FullPath path) - => _cache?.ReverseResolvePath(path) ?? Array.Empty(); + => Cache?.ReverseResolvePath(path) ?? []; public HashSet[] ReverseResolvePaths(IReadOnlyCollection paths) - => _cache?.ReverseResolvePaths(paths) ?? paths.Select(_ => new HashSet()).ToArray(); + => Cache?.ReverseResolvePaths(paths) ?? paths.Select(_ => new HashSet()).ToArray(); public FullPath? ResolvePath(Utf8GamePath path) - => _cache?.ResolvePath(path); + => Cache?.ResolvePath(path); // Obtain data from the cache. internal MetaCache? MetaCache - => _cache?.Meta; + => Cache?.Meta; internal IReadOnlyDictionary ResolvedFiles - => _cache?.ResolvedFiles ?? new ConcurrentDictionary(); + => Cache?.ResolvedFiles ?? new ConcurrentDictionary(); internal IReadOnlyDictionary, IIdentifiedObjectData)> ChangedItems - => _cache?.ChangedItems ?? new Dictionary, IIdentifiedObjectData)>(); + => Cache?.ChangedItems ?? new Dictionary, IIdentifiedObjectData)>(); internal IEnumerable> AllConflicts - => _cache?.AllConflicts ?? Array.Empty>(); + => Cache?.AllConflicts ?? []; internal SingleArray Conflicts(Mod mod) - => _cache?.Conflicts(mod) ?? new SingleArray(); + => Cache?.Conflicts(mod) ?? new SingleArray(); } diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 11e70b1c..2f641f7a 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -13,6 +13,7 @@ using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.UI; using Penumbra.UI.Knowledge; +using Penumbra.UI.MainWindow; namespace Penumbra; @@ -24,7 +25,7 @@ public class CommandHandler : IDisposable, IApiService private readonly RedrawService _redrawService; private readonly IChatGui _chat; private readonly Configuration _config; - private readonly ConfigWindow _configWindow; + private readonly MainWindow _mainWindow; private readonly ActorManager _actors; private readonly ModManager _modManager; private readonly CollectionManager _collectionManager; @@ -33,14 +34,14 @@ public class CommandHandler : IDisposable, IApiService private readonly KnowledgeWindow _knowledgeWindow; public CommandHandler(IFramework framework, ICommandManager commandManager, IChatGui chat, RedrawService redrawService, - Configuration config, ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors, + Configuration config, MainWindow mainWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors, Penumbra penumbra, CollectionEditor collectionEditor, KnowledgeWindow knowledgeWindow) { _commandManager = commandManager; _redrawService = redrawService; _config = config; - _configWindow = configWindow; + _mainWindow = mainWindow; _modManager = modManager; _collectionManager = collectionManager; _actors = actors; @@ -146,11 +147,11 @@ public class CommandHandler : IDisposable, IApiService private bool ToggleWindow(string arguments) { - var value = ParseTrueFalseToggle(arguments) ?? !_configWindow.IsOpen; - if (value == _configWindow.IsOpen) + var value = ParseTrueFalseToggle(arguments) ?? !_mainWindow.IsOpen; + if (value == _mainWindow.IsOpen) return false; - _configWindow.Toggle(); + _mainWindow.Toggle(); return true; } @@ -211,12 +212,12 @@ public class CommandHandler : IDisposable, IApiService if (value) { Print("Penumbra UI locked in place."); - _configWindow.Flags |= WindowFlags.NoMove | WindowFlags.NoResize; + _mainWindow.Flags |= WindowFlags.NoMove | WindowFlags.NoResize; } else { Print("Penumbra UI unlocked."); - _configWindow.Flags &= ~(WindowFlags.NoMove | WindowFlags.NoResize); + _mainWindow.Flags &= ~(WindowFlags.NoMove | WindowFlags.NoResize); } _config.Ephemeral.FixMainWindow = value; diff --git a/Penumbra/Communication/CollectionChange.cs b/Penumbra/Communication/CollectionChange.cs index e577e69d..32c07169 100644 --- a/Penumbra/Communication/CollectionChange.cs +++ b/Penumbra/Communication/CollectionChange.cs @@ -1,61 +1,68 @@ -using Luna; -using Penumbra.Collections; -using Penumbra.Collections.Manager; -using Penumbra.UI.CollectionTab; - -namespace Penumbra.Communication; - -/// Triggered whenever collection setup is changed. -public sealed class CollectionChange(Logger log) - : EventBase(nameof(CollectionChange), log) -{ - public enum Priority - { - /// - DalamudSubstitutionProvider = -3, - - /// - CollectionCacheManager = -2, - - /// - ActiveCollections = -1, - - /// - TempModManager = 0, - - /// - InheritanceManager = 0, - - /// - IdentifiedCollectionCache = 0, - - /// - ItemSwapTab = 0, - - /// - CollectionSelector = 0, - - /// - IndividualAssignmentUi = 0, - - /// - ModFileSystemSelector = 0, - - /// +using Luna; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.UI.CollectionTab; +using Penumbra.UI.MainWindow; + +namespace Penumbra.Communication; + +/// Triggered whenever collection setup is changed. +public sealed class CollectionChange(Logger log) + : EventBase(nameof(CollectionChange), log) +{ + public enum Priority + { + /// + EffectiveChangesCache = int.MinValue, + + /// + DalamudSubstitutionProvider = -3, + + /// + CollectionCacheManager = -2, + + /// + ActiveCollections = -1, + + /// + TempModManager = 0, + + /// + InheritanceManager = 0, + + /// + IdentifiedCollectionCache = 0, + + /// + ChangedItemsTabCache = 0, + + /// + ItemSwapTab = 0, + + /// + CollectionSelector = 0, + + /// + IndividualAssignmentUi = 0, + + /// + ModFileSystemSelector = 0, + + /// ModSelection = 10, /// - CollectionCombo = 15, - } - - /// The arguments for a collection change event. - /// The type of the changed collection ( or for additions or deletions). - /// The old collection, or null on additions. - /// The new collection, or null on deletions. - /// The display name for Individual collections or an empty string otherwise. - public readonly record struct Arguments( - CollectionType Type, - ModCollection? OldCollection, - ModCollection? NewCollection, - string DisplayName); -} + CollectionCombo = 15, + } + + /// The arguments for a collection change event. + /// The type of the changed collection ( or for additions or deletions). + /// The old collection, or null on additions. + /// The new collection, or null on deletions. + /// The display name for Individual collections or an empty string otherwise. + public readonly record struct Arguments( + CollectionType Type, + ModCollection? OldCollection, + ModCollection? NewCollection, + string DisplayName); +} diff --git a/Penumbra/Communication/ModDirectoryChanged.cs b/Penumbra/Communication/ModDirectoryChanged.cs index 2bea68a5..cb80248b 100644 --- a/Penumbra/Communication/ModDirectoryChanged.cs +++ b/Penumbra/Communication/ModDirectoryChanged.cs @@ -1,5 +1,6 @@ using Luna; using Penumbra.Api.Api; +using Penumbra.UI.Classes; namespace Penumbra.Communication; @@ -12,7 +13,7 @@ public sealed class ModDirectoryChanged(Logger log) /// Api = 0, - /// + /// FileDialogService = 0, } diff --git a/Penumbra/Communication/ResolvedFileChanged.cs b/Penumbra/Communication/ResolvedFileChanged.cs index b9c220ef..80109ed8 100644 --- a/Penumbra/Communication/ResolvedFileChanged.cs +++ b/Penumbra/Communication/ResolvedFileChanged.cs @@ -1,44 +1,84 @@ -using Luna; -using Penumbra.Collections; -using Penumbra.Mods.Editor; -using Penumbra.String.Classes; - -namespace Penumbra.Communication; - -/// Triggered whenever a redirection in a mod collection cache is manipulated. -public sealed class ResolvedFileChanged(Logger log) : EventBase( - nameof(ResolvedFileChanged), log) -{ - public enum Type - { - Added, - Removed, - Replaced, - FullRecomputeStart, - FullRecomputeFinished, - } - - public enum Priority - { - /// - DalamudSubstitutionProvider = 0, - - /// - SchedulerResourceManagementService = 0, - } - - /// The arguments for a ResolvedFileChanged event. - /// The type of the redirection change. - /// The collection with a changed cache. - /// The game path to be redirected or empty for FullRecompute - /// The new redirection path or empty for Removed or FullRecompute. - /// The old redirection path for Replaced, or empty. - /// The mod responsible for the new redirection if any. - public readonly record struct Arguments( - Type Type, - ModCollection Collection, - Utf8GamePath GamePath, - FullPath OldRedirection, - FullPath NewRedirection, - IMod? Mod); -} +using Luna; +using Penumbra.Collections; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.String.Classes; +using Penumbra.UI.MainWindow; + +namespace Penumbra.Communication; + +/// Triggered whenever a redirection in a mod collection cache is manipulated. +public sealed class ResolvedFileChanged(Logger log) : EventBase( + nameof(ResolvedFileChanged), log) +{ + public enum Type + { + Added, + Removed, + Replaced, + FullRecomputeStart, + FullRecomputeFinished, + } + + public enum Priority + { + /// + DalamudSubstitutionProvider = 0, + + /// + SchedulerResourceManagementService = 0, + + /// + EffectiveChangesCache = int.MinValue, + } + + /// The arguments for a ResolvedFileChanged event. + /// The type of the redirection change. + /// The collection with a changed cache. + /// The game path to be redirected or empty for FullRecompute + /// The new redirection path or empty for Removed or FullRecompute. + /// The old redirection path for Replaced, or empty. + /// The mod responsible for the new redirection if any. + public readonly record struct Arguments( + Type Type, + ModCollection Collection, + Utf8GamePath GamePath, + FullPath OldRedirection, + FullPath NewRedirection, + IMod? Mod); +} + +/// Triggered whenever a meta edit in a mod collection cache is manipulated. +public sealed class ResolvedMetaChanged(Logger log) : EventBase( + nameof(ResolvedMetaChanged), log) +{ + public enum Type + { + Added, + Removed, + Replaced, + FullRecomputeStart, + FullRecomputeFinished, + } + + public enum Priority + { + /// + EffectiveChangesCache = int.MinValue, + } + + /// The arguments for a ResolvedMetaChanged event. + /// The type of the meta edit change. + /// The collection with a changed cache. + /// The meta identifier changed, or invalid for recomputes. + /// The old value for the meta identifier if any, null otherwise. + /// The new value for the meta identifier if any, null otherwise. + /// The mod responsible for the new meta edit if any. + public readonly record struct Arguments( + Type Type, + ModCollection Collection, + IMetaIdentifier Identifier, + object? OldValue, + object? NewValue, + IMod? Mod); +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index e4e09cf8..b83bed55 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -2,14 +2,12 @@ using Dalamud.Configuration; using Dalamud.Interface.ImGuiNotification; using Luna; using Newtonsoft.Json; -using OtterGui.Filesystem; using Penumbra.Import.Structs; using Penumbra.Interop.Services; -using Penumbra.Mods; -using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.UI.ModsTab; +using Penumbra.UI.ModsTab.Selector; using Penumbra.UI.ResourceWatcher; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; @@ -94,7 +92,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService [JsonConverter(typeof(SortModeConverter))] [JsonProperty(Order = int.MaxValue)] - public ISortMode SortMode = ISortMode.FoldersFirst; + public ISortMode SortMode = ISortMode.FoldersFirst; public bool OpenFoldersByDefault { get; set; } = false; public int SingleGroupRadioMax { get; set; } = 2; @@ -178,40 +176,24 @@ public class Configuration : IPluginConfiguration, ISavable, IService public const int MinScaledSize = 5; public const int MinimumSizeX = 900; public const int MinimumSizeY = 675; - - public static readonly ISortMode[] ValidSortModes = - { - ISortMode.FoldersFirst, - ISortMode.Lexicographical, - new ModFileSystem.ImportDate(), - new ModFileSystem.InverseImportDate(), - ISortMode.InverseFoldersFirst, - ISortMode.InverseLexicographical, - ISortMode.FoldersLast, - ISortMode.InverseFoldersLast, - ISortMode.InternalOrder, - ISortMode.InverseInternalOrder, - }; } /// Convert SortMode Types to their name. - private class SortModeConverter : JsonConverter> + private class SortModeConverter : JsonConverter { - public override void WriteJson(JsonWriter writer, ISortMode? value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, ISortMode? value, JsonSerializer serializer) { - value ??= ISortMode.FoldersFirst; + value ??= ISortMode.FoldersFirst; serializer.Serialize(writer, value.GetType().Name); } - public override ISortMode ReadJson(JsonReader reader, Type objectType, ISortMode? existingValue, - bool hasExistingValue, - JsonSerializer serializer) + public override ISortMode ReadJson(JsonReader reader, Type objectType, ISortMode? existingValue, + bool hasExistingValue, JsonSerializer serializer) { - var name = serializer.Deserialize(reader); - if (name == null || !Constants.ValidSortModes.FindFirst(s => s.GetType().Name == name, out var mode)) - return existingValue ?? ISortMode.FoldersFirst; + if (serializer.Deserialize(reader) is { } name) + return ISortMode.Valid.GetValueOrDefault(name, existingValue ?? ISortMode.FoldersFirst); - return mode; + return existingValue ?? ISortMode.FoldersFirst; } } @@ -220,8 +202,9 @@ public class Configuration : IPluginConfiguration, ISavable, IService public void Save(StreamWriter writer) { - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + using var jWriter = new JsonTextWriter(writer); + jWriter.Formatting = Formatting.Indented; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; serializer.Serialize(jWriter, this); } diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index dba07d29..bfee3d68 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -1,12 +1,12 @@ using Dalamud.Interface.ImGuiNotification; using Luna; using Newtonsoft.Json; -using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.Enums; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; +using Penumbra.UI.Classes; using Penumbra.UI.ResourceWatcher; using Penumbra.UI.Tabs; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; diff --git a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs index 6bea8465..7269a39e 100644 --- a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs +++ b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs @@ -50,7 +50,7 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, Luna.IRequired private SafeResourceHandle GetPreBoneDeformerForCharacter(CharacterBase* drawObject) { var resolveData = _collectionResolver.IdentifyCollection(&drawObject->DrawObject, true); - if (resolveData.ModCollection._cache is not { } cache) + if (resolveData.ModCollection.Cache is not { } cache) return _resourceLoader.LoadResolvedSafeResource(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath.Path, resolveData); return cache.CustomResources.Get(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath, resolveData); diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs index eb5b9324..7ed4b8c8 100644 --- a/Penumbra/Interop/ResourceTree/ResolveContext.cs +++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs @@ -14,6 +14,7 @@ using Penumbra.Meta; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; +using Penumbra.UI.Classes; using static Penumbra.Interop.Structs.StructExtensions; using CharaBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; using SpanTextWriter = Luna.SpanTextWriter; diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 08dee818..e6d600ff 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -3,6 +3,7 @@ using Penumbra.Mods; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.UI; +using Penumbra.UI.Classes; namespace Penumbra.Interop.ResourceTree; diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs index 1ebfe53d..cda4486c 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTree.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs @@ -9,6 +9,7 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.UI; +using Penumbra.UI.Classes; using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData; using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex; using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType; @@ -192,7 +193,7 @@ public class ResourceTree( { var genericContext = globalContext.CreateContext(&human->CharacterBase); - var cache = globalContext.Collection._cache; + var cache = globalContext.Collection.Cache; if (cache is not null && cache.CustomResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle) && genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle) is { } pbdNode) diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs index 5383beab..43e43838 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs @@ -4,6 +4,7 @@ using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.String.Classes; using Penumbra.UI; +using Penumbra.UI.Classes; namespace Penumbra.Interop.ResourceTree; diff --git a/Penumbra/Meta/Manipulations/AtchIdentifier.cs b/Penumbra/Meta/Manipulations/AtchIdentifier.cs index c248c48b..a6e19392 100644 --- a/Penumbra/Meta/Manipulations/AtchIdentifier.cs +++ b/Penumbra/Meta/Manipulations/AtchIdentifier.cs @@ -29,7 +29,7 @@ public readonly record struct AtchIdentifier(AtchType Type, GenderRace GenderRac } public override string ToString() - => $"Atch - {Type.ToAbbreviation()} - {GenderRace.ToName()} - {EntryIndex}"; + => $"ATCH - {Type.ToAbbreviation()} - {GenderRace.ToName()} - {EntryIndex}"; public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { diff --git a/Penumbra/Meta/Manipulations/AtrIdentifier.cs b/Penumbra/Meta/Manipulations/AtrIdentifier.cs index ca65f6aa..79e61a3c 100644 --- a/Penumbra/Meta/Manipulations/AtrIdentifier.cs +++ b/Penumbra/Meta/Manipulations/AtrIdentifier.cs @@ -47,7 +47,7 @@ public readonly record struct AtrIdentifier(HumanSlot Slot, PrimaryId? Id, Shape public override string ToString() { var sb = new StringBuilder(64); - sb.Append("Shp - ") + sb.Append("ATR - ") .Append(Attribute); if (Slot is HumanSlot.Unknown) { diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs index c8423b92..250e589d 100644 --- a/Penumbra/Meta/Manipulations/Eqdp.cs +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -22,7 +22,7 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge => CharacterUtilityData.EqdpIdx(GenderRace, Slot.IsAccessory()); public override string ToString() - => $"Eqdp - {SetId} - {Slot.ToName()} - {GenderRace.ToName()}"; + => $"EQDP - {SetId} - {Slot.ToName()} - {GenderRace.ToName()}"; public bool Validate() { diff --git a/Penumbra/Meta/Manipulations/Eqp.cs b/Penumbra/Meta/Manipulations/Eqp.cs index 154aca40..1e762e25 100644 --- a/Penumbra/Meta/Manipulations/Eqp.cs +++ b/Penumbra/Meta/Manipulations/Eqp.cs @@ -15,7 +15,7 @@ public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : I => MetaIndex.Eqp; public override string ToString() - => $"Eqp - {SetId} - {Slot}"; + => $"EQP - {SetId} - {Slot}"; public bool Validate() { diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs index 46a275a5..b5192a9f 100644 --- a/Penumbra/Meta/Manipulations/Est.cs +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -54,7 +54,7 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende => (MetaIndex)Slot; public override string ToString() - => $"Est - {SetId} - {Slot} - {GenderRace.ToName()}"; + => $"EST - {SetId} - {Slot} - {GenderRace.ToName()}"; public bool Validate() { diff --git a/Penumbra/Meta/Manipulations/Gmp.cs b/Penumbra/Meta/Manipulations/Gmp.cs index 5bc81f26..474857d3 100644 --- a/Penumbra/Meta/Manipulations/Gmp.cs +++ b/Penumbra/Meta/Manipulations/Gmp.cs @@ -15,7 +15,7 @@ public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, => MetaIndex.Gmp; public override string ToString() - => $"Gmp - {SetId}"; + => $"GMP - {SetId}"; public bool Validate() // No known conditions. diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index fa726708..31d68268 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -62,9 +62,9 @@ public readonly record struct ImcIdentifier( public override string ToString() => ObjectType switch { - ObjectType.Equipment or ObjectType.Accessory => $"Imc - {PrimaryId} - {EquipSlot.ToName()} - {Variant}", - ObjectType.DemiHuman => $"Imc - {PrimaryId} - DemiHuman - {SecondaryId} - {EquipSlot.ToName()} - {Variant}", - _ => $"Imc - {PrimaryId} - {ObjectType.ToName()} - {SecondaryId} - {BodySlot} - {Variant}", + ObjectType.Equipment or ObjectType.Accessory => $"IMC - {PrimaryId} - {EquipSlot.ToName()} - {Variant}", + ObjectType.DemiHuman => $"IMC - {PrimaryId} - DemiHuman - {SecondaryId} - {EquipSlot.ToName()} - {Variant}", + _ => $"IMC - {PrimaryId} - {ObjectType.ToName()} - {SecondaryId} - {BodySlot} - {Variant}", }; diff --git a/Penumbra/Meta/Manipulations/ShpIdentifier.cs b/Penumbra/Meta/Manipulations/ShpIdentifier.cs index 0a5b71b7..3915122b 100644 --- a/Penumbra/Meta/Manipulations/ShpIdentifier.cs +++ b/Penumbra/Meta/Manipulations/ShpIdentifier.cs @@ -66,7 +66,7 @@ public readonly record struct ShpIdentifier( public override string ToString() { var sb = new StringBuilder(64); - sb.Append("Shp - ") + sb.Append("SHP - ") .Append(Shape); if (Slot is HumanSlot.Unknown) { diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index c1f8b96b..c1773427 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -1,12 +1,50 @@ using Dalamud.Interface.ImGuiNotification; +using ImSharp; using Luna; using OtterGui.Filesystem; +using Penumbra.Api.Enums; using Penumbra.Communication; using Penumbra.Services; +using Penumbra.UI.Classes; +using Penumbra.UI.ModsTab; +using Penumbra.UI.ModsTab.Selector; using FileSystemChangeType = OtterGui.Filesystem.FileSystemChangeType; namespace Penumbra.Mods.Manager; +public sealed class ModTab : TwoPanelLayout, ITab +{ + public override ReadOnlySpan Label + => "Mods2"u8; + + public ModTab(ModFileSystemDrawer drawer, ModPanel panel, CollectionSelectHeader collectionHeader) + { + LeftHeader = drawer.Header; + LeftFooter = drawer.Footer; + LeftPanel = drawer; + RightPanel = panel; + RightHeader = collectionHeader; + } + + public void DrawContent() + => Draw(); + + public TabType Identifier + => TabType.Mods; + + protected override void SetSize(Vector2 newSize) + { + base.SetSize(newSize); + ((ModFileSystemDrawer)LeftPanel).Config.Ephemeral.CurrentModSelectorWidth = newSize.X / Im.Style.GlobalScale; + } + + protected override float MinimumWidth + => ((ModFileSystemDrawer)LeftPanel).Footer.Buttons.Count * Im.Style.FrameHeight; + + protected override float MaximumWidth + => Im.Window.Width - 500 * Im.Style.GlobalScale; +} + public sealed class ModFileSystem2 : BaseFileSystem, IDisposable, IRequiredService { private readonly Configuration _config; @@ -14,7 +52,7 @@ public sealed class ModFileSystem2 : BaseFileSystem, IDisposable, IRequiredServi private readonly ModFileSystemSaver _saver; public ModFileSystem2(Configuration config, CommunicatorService communicator, SaveService saveService, Logger log, ModStorage modStorage) - : base("ModFileSystem", log) + : base("ModFileSystem", log, true) { _config = config; _communicator = communicator; diff --git a/Penumbra/Mods/Manager/ModFileSystemSaver.cs b/Penumbra/Mods/Manager/ModFileSystemSaver.cs index a88d73b9..9939f3bd 100644 --- a/Penumbra/Mods/Manager/ModFileSystemSaver.cs +++ b/Penumbra/Mods/Manager/ModFileSystemSaver.cs @@ -15,6 +15,9 @@ public sealed class ModFileSystemSaver(Logger log, BaseFileSystem fileSystem, Sa protected override string EmptyFoldersFile(FilenameService provider) => provider.FileSystemEmptyFolders; + protected override string SelectionFile(FilenameService provider) + => provider.FileSystemSelectedNodes; + protected override string MigrationFile(FilenameService provider) => provider.OldFilesystemFile; diff --git a/Penumbra/Mods/Manager/SortModes.cs b/Penumbra/Mods/Manager/SortModes.cs deleted file mode 100644 index 247c14a4..00000000 --- a/Penumbra/Mods/Manager/SortModes.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Luna; - -namespace Penumbra.Mods.Manager; - -public readonly struct ImportDate : ISortMode -{ - public ReadOnlySpan Name - => "Import Date (Older First)"u8; - - public ReadOnlySpan Description - => "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."u8; - - public IEnumerable GetChildren(IFileSystemFolder f) - => f.GetSubFolders().Cast().Concat(f.GetLeaves().OfType>().OrderBy(l => l.Value.ImportDate)); -} - -public readonly struct InverseImportDate : ISortMode -{ - public ReadOnlySpan Name - => "Import Date (Newer First)"u8; - - public ReadOnlySpan Description - => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."u8; - - public IEnumerable GetChildren(IFileSystemFolder f) - => f.GetSubFolders().Cast() - .Concat(f.GetLeaves().OfType>().OrderByDescending(l => l.Value.ImportDate)); -} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index ee9c0d75..f5c03447 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -3,8 +3,6 @@ using Dalamud.Plugin.Services; using ImSharp; using Lumina.Excel.Sheets; using Luna; -using OtterGui; -using OtterGui.Tasks; using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -21,6 +19,7 @@ using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; using Penumbra.UI.AdvancedWindow; +using Penumbra.UI.MainWindow; using Penumbra.UI.Tabs; using ChangedItemClick = Penumbra.Communication.ChangedItemClick; using ChangedItemHover = Penumbra.Communication.ChangedItemHover; @@ -131,7 +130,7 @@ public class Penumbra : IDalamudPlugin private void SetupInterface() { - AsyncTask.Run(() => + Task.Run(() => { var system = _services.GetService(); system.Window.Setup(this, _services.GetService()); @@ -144,12 +143,16 @@ public class Penumbra : IDalamudPlugin var mods = _services.GetService(); var editWindowFactory = _services.GetService(); foreach (var identifier in _config.Ephemeral.AdvancedEditingOpenForModPaths) + { if (mods.TryGetMod(identifier, out var mod)) editWindowFactory.OpenForMod(mod); + } } } else + { system.Dispose(); + } } ); } @@ -203,8 +206,8 @@ public class Penumbra : IDalamudPlugin "Ktisis", "Brio", "heliosphere-plugin", "VfxEditor", "IllusioVitae", "Aetherment", "DynamicBridge", "GagSpeak", "ProjectGagSpeak", "RoleplayingVoiceDalamud", "AQuestReborn", - "MareSynchronos", "LoporritSync", "KittenSync", "Snowcloak", "LightlessSync", "Sphene", "XivSync", "MareSempiterne" /* PlayerSync */, "AnatoliIliou", "LaciSynchroni" - + "MareSynchronos", "LoporritSync", "KittenSync", "Snowcloak", "LightlessSync", "Sphene", "XivSync", + "MareSempiterne" /* PlayerSync */, "AnatoliIliou", "LaciSynchroni", ]; var plugins = _services.GetService().InstalledPlugins .GroupBy(p => p.InternalName) @@ -238,7 +241,7 @@ public class Penumbra : IDalamudPlugin sb.Append( $"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}{(cloudSynced ? ", Cloud-Synced" : "")}\n"); sb.Append( - $"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n"); + $"> **`Free Drive Space: `** {(drive != null ? FormattingFunctions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n"); sb.Append($"> **`Game Data Files: `** {(_gameData.HasModifiedGameDataFiles ? "Modified" : "Pristine")}\n"); sb.Append($"> **`Auto-Deduplication: `** {_config.AutoDeduplicateOnImport}\n"); sb.Append($"> **`Auto-UI-Reduplication: `** {_config.AutoReduplicateUiOnImport}\n"); @@ -285,7 +288,7 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`{id[0].Incognito(name) + ':',-29}`** {collection.Identity.AnonymizedName}\n"); foreach (var collection in _collectionManager.Caches.Active) - PrintCollection(collection, collection._cache!); + PrintCollection(collection, collection.Cache!); return sb.ToString(); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index bf385f5c..dfc2a136 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -72,7 +72,7 @@ - + diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 2bdadb78..591956e7 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -1,85 +1,88 @@ -using Luna; -using Penumbra.Communication; - -namespace Penumbra.Services; - -public class CommunicatorService(ServiceManager services) : IService -{ - /// - public readonly CollectionChange CollectionChange = services.GetService(); - - /// - public readonly TemporaryGlobalModChange TemporaryGlobalModChange = services.GetService(); - - /// - public readonly CreatingCharacterBase CreatingCharacterBase = services.GetService(); - - /// - public readonly CreatedCharacterBase CreatedCharacterBase = services.GetService(); - - /// - public readonly MtrlLoaded MtrlLoaded = services.GetService(); - - /// - public readonly ModDataChanged ModDataChanged = services.GetService(); - - /// - public readonly ModOptionChanged ModOptionChanged = services.GetService(); - - /// - public readonly ModDiscoveryStarted ModDiscoveryStarted = services.GetService(); - - /// - public readonly ModDiscoveryFinished ModDiscoveryFinished = services.GetService(); - - /// - public readonly ModDirectoryChanged ModDirectoryChanged = services.GetService(); - - /// - public readonly ModFileChanged ModFileChanged = services.GetService(); - - /// - public readonly ModPathChanged ModPathChanged = services.GetService(); - - /// - public readonly ModSettingChanged ModSettingChanged = services.GetService(); - - /// - public readonly CollectionInheritanceChanged CollectionInheritanceChanged = services.GetService(); - - /// - public readonly EnabledChanged EnabledChanged = services.GetService(); - - /// - public readonly PreSettingsTabBarDraw PreSettingsTabBarDraw = services.GetService(); - - /// - public readonly PreSettingsPanelDraw PreSettingsPanelDraw = services.GetService(); - - /// - public readonly PostEnabledDraw PostEnabledDraw = services.GetService(); - - /// - public readonly PostSettingsPanelDraw PostSettingsPanelDraw = services.GetService(); - - /// - public readonly ChangedItemHover ChangedItemHover = services.GetService(); - - /// - public readonly ChangedItemClick ChangedItemClick = services.GetService(); - - /// - public readonly SelectTab SelectTab = services.GetService(); - - /// - public readonly ResolvedFileChanged ResolvedFileChanged = services.GetService(); - - /// - public readonly PcpCreation PcpCreation = services.GetService(); - - /// - public readonly PcpParsing PcpParsing = services.GetService(); - - /// - public readonly CharacterUtilityFinished LoadingFinished = services.GetService(); -} +using Luna; +using Penumbra.Communication; + +namespace Penumbra.Services; + +public class CommunicatorService(ServiceManager services) : IService +{ + /// + public readonly CollectionChange CollectionChange = services.GetService(); + + /// + public readonly TemporaryGlobalModChange TemporaryGlobalModChange = services.GetService(); + + /// + public readonly CreatingCharacterBase CreatingCharacterBase = services.GetService(); + + /// + public readonly CreatedCharacterBase CreatedCharacterBase = services.GetService(); + + /// + public readonly MtrlLoaded MtrlLoaded = services.GetService(); + + /// + public readonly ModDataChanged ModDataChanged = services.GetService(); + + /// + public readonly ModOptionChanged ModOptionChanged = services.GetService(); + + /// + public readonly ModDiscoveryStarted ModDiscoveryStarted = services.GetService(); + + /// + public readonly ModDiscoveryFinished ModDiscoveryFinished = services.GetService(); + + /// + public readonly ModDirectoryChanged ModDirectoryChanged = services.GetService(); + + /// + public readonly ModFileChanged ModFileChanged = services.GetService(); + + /// + public readonly ModPathChanged ModPathChanged = services.GetService(); + + /// + public readonly ModSettingChanged ModSettingChanged = services.GetService(); + + /// + public readonly CollectionInheritanceChanged CollectionInheritanceChanged = services.GetService(); + + /// + public readonly EnabledChanged EnabledChanged = services.GetService(); + + /// + public readonly PreSettingsTabBarDraw PreSettingsTabBarDraw = services.GetService(); + + /// + public readonly PreSettingsPanelDraw PreSettingsPanelDraw = services.GetService(); + + /// + public readonly PostEnabledDraw PostEnabledDraw = services.GetService(); + + /// + public readonly PostSettingsPanelDraw PostSettingsPanelDraw = services.GetService(); + + /// + public readonly ChangedItemHover ChangedItemHover = services.GetService(); + + /// + public readonly ChangedItemClick ChangedItemClick = services.GetService(); + + /// + public readonly SelectTab SelectTab = services.GetService(); + + /// + public readonly ResolvedFileChanged ResolvedFileChanged = services.GetService(); + + /// + public readonly ResolvedMetaChanged ResolvedMetaChanged = services.GetService(); + + /// + public readonly PcpCreation PcpCreation = services.GetService(); + + /// + public readonly PcpParsing PcpParsing = services.GetService(); + + /// + public readonly CharacterUtilityFinished LoadingFinished = services.GetService(); +} diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 36688027..ef197664 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -1,432 +1,431 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OtterGui.Filesystem; -using Penumbra.Api.Enums; -using Penumbra.Collections; -using Penumbra.Collections.Manager; -using Penumbra.Enums; -using Penumbra.Interop.Services; -using Penumbra.Mods; -using Penumbra.Mods.Editor; -using Penumbra.Mods.Manager; -using Penumbra.Mods.Settings; -using Penumbra.UI; -using Penumbra.UI.Classes; -using Penumbra.UI.ResourceWatcher; -using Penumbra.UI.Tabs; -using TabType = Penumbra.Api.Enums.TabType; - -namespace Penumbra.Services; - -/// -/// Contains everything to migrate from older versions of the config to the current, -/// including deprecated fields. -/// -public class ConfigMigrationService(SaveService saveService, BackupService backupService) : Luna.IService -{ - private Configuration _config = null!; - private JObject _data = null!; - - public string CurrentCollection = ModCollectionIdentity.DefaultCollectionName; - public string DefaultCollection = ModCollectionIdentity.DefaultCollectionName; - public string ForcedCollection = string.Empty; - public Dictionary CharacterCollections = []; - public Dictionary ModSortOrder = []; - public bool InvertModListOrder; - public bool SortFoldersFirst; - public SortModeV3 SortMode = SortModeV3.FoldersFirst; - - /// Add missing colors to the dictionary if necessary. - private static void AddColors(Configuration config, bool forceSave) - { - var save = false; - foreach (var color in Enum.GetValues()) - save |= config.Colors.TryAdd(color, color.Data().DefaultColor); - - if (save || forceSave) - config.Save(); - - Colors.SetColors(config); - } - - public void Migrate(CharacterUtility utility, Configuration config) - { - _config = config; - // Do this on every migration from now on for a while - // because it stayed alive for a bunch of people for some reason. - DeleteMetaTmp(); - - if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(saveService.FileNames.ConfigurationFile)) - { - AddColors(config, false); - return; - } - - _data = JObject.Parse(File.ReadAllText(saveService.FileNames.ConfigurationFile)); - CreateBackup(); - - Version0To1(); - Version1To2(utility); - Version2To3(); - Version3To4(); - Version4To5(); - Version5To6(); - Version6To7(); - Version7To8(); - Version8To9(); - Version9To10(); - AddColors(config, true); - } - - private void Version9To10() - { - if (_config.Version != 9) +using Luna; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Enums; +using Penumbra.Interop.Services; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Settings; +using Penumbra.UI; +using Penumbra.UI.Classes; +using Penumbra.UI.ResourceWatcher; +using Penumbra.UI.Tabs; +using TabType = Penumbra.Api.Enums.TabType; + +namespace Penumbra.Services; + +/// +/// Contains everything to migrate from older versions of the config to the current, +/// including deprecated fields. +/// +public class ConfigMigrationService(SaveService saveService, BackupService backupService) : IService +{ + private Configuration _config = null!; + private JObject _data = null!; + + public string CurrentCollection = ModCollectionIdentity.DefaultCollectionName; + public string DefaultCollection = ModCollectionIdentity.DefaultCollectionName; + public string ForcedCollection = string.Empty; + public Dictionary CharacterCollections = []; + public Dictionary ModSortOrder = []; + public bool InvertModListOrder; + public bool SortFoldersFirst; + public SortModeV3 SortMode = SortModeV3.FoldersFirst; + + /// Add missing colors to the dictionary if necessary. + private static void AddColors(Configuration config, bool forceSave) + { + var save = false; + foreach (var color in Enum.GetValues()) + save |= config.Colors.TryAdd(color, color.Data().DefaultColor); + + if (save || forceSave) + config.Save(); + + Colors.SetColors(config); + } + + public void Migrate(CharacterUtility utility, Configuration config) + { + _config = config; + // Do this on every migration from now on for a while + // because it stayed alive for a bunch of people for some reason. + DeleteMetaTmp(); + + if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(saveService.FileNames.ConfigurationFile)) + { + AddColors(config, false); + return; + } + + _data = JObject.Parse(File.ReadAllText(saveService.FileNames.ConfigurationFile)); + CreateBackup(); + + Version0To1(); + Version1To2(utility); + Version2To3(); + Version3To4(); + Version4To5(); + Version5To6(); + Version6To7(); + Version7To8(); + Version8To9(); + Version9To10(); + AddColors(config, true); + } + + private void Version9To10() + { + if (_config.Version != 9) return; - backupService.CreateMigrationBackup("pre_filesystem_update", saveService.FileNames.OldFilesystemFile); - _config.Version = 10; - _config.Ephemeral.Version = 10; + backupService.CreateMigrationBackup("pre_filesystem_update", saveService.FileNames.OldFilesystemFile); + _config.Version = 10; + _config.Ephemeral.Version = 10; _config.Save(); - _config.Ephemeral.Save(); + _config.Ephemeral.Save(); } - - // Migrate to ephemeral config. - private void Version8To9() - { - if (_config.Version != 8) - return; - - backupService.CreateMigrationBackup("pre_collection_identifiers"); - _config.Version = 9; - _config.Ephemeral.Version = 9; - _config.Save(); - _config.Ephemeral.Save(); - } - - // Migrate to ephemeral config. - private void Version7To8() - { - if (_config.Version != 7) - return; - - _config.Version = 8; - _config.Ephemeral.Version = 8; - - _config.Ephemeral.LastSeenVersion = _data["LastSeenVersion"]?.ToObject() ?? _config.Ephemeral.LastSeenVersion; - _config.Ephemeral.DebugSeparateWindow = _data["DebugSeparateWindow"]?.ToObject() ?? _config.Ephemeral.DebugSeparateWindow; - _config.Ephemeral.TutorialStep = _data["TutorialStep"]?.ToObject() ?? _config.Ephemeral.TutorialStep; - _config.Ephemeral.EnableResourceLogging = _data["EnableResourceLogging"]?.ToObject() ?? _config.Ephemeral.EnableResourceLogging; - _config.Ephemeral.ResourceLoggingFilter = _data["ResourceLoggingFilter"]?.ToObject() ?? _config.Ephemeral.ResourceLoggingFilter; - _config.Ephemeral.EnableResourceWatcher = _data["EnableResourceWatcher"]?.ToObject() ?? _config.Ephemeral.EnableResourceWatcher; - _config.Ephemeral.OnlyAddMatchingResources = - _data["OnlyAddMatchingResources"]?.ToObject() ?? _config.Ephemeral.OnlyAddMatchingResources; - _config.Ephemeral.ResourceWatcherResourceTypes = _data["ResourceWatcherResourceTypes"]?.ToObject() - ?? _config.Ephemeral.ResourceWatcherResourceTypes; - _config.Ephemeral.ResourceWatcherResourceCategories = _data["ResourceWatcherResourceCategories"]?.ToObject() - ?? _config.Ephemeral.ResourceWatcherResourceCategories; - _config.Ephemeral.ResourceWatcherRecordTypes = - _data["ResourceWatcherRecordTypes"]?.ToObject() ?? _config.Ephemeral.ResourceWatcherRecordTypes; - _config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject() ?? _config.Ephemeral.CollectionPanel; - _config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject() ?? _config.Ephemeral.SelectedTab; - _config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject() - ?? _config.Ephemeral.ChangedItemFilter; - _config.Ephemeral.FixMainWindow = _data["FixMainWindow"]?.ToObject() ?? _config.Ephemeral.FixMainWindow; - _config.Ephemeral.Save(); - } - - // Gendered special collections were added. - private void Version6To7() - { - if (_config.Version != 6) - return; - - ActiveCollectionMigration.MigrateUngenderedCollections(saveService.FileNames); - _config.Version = 7; - } - - - // A new tutorial step was inserted in the middle. - // The UI collection and a new tutorial for it was added. - // The migration for the UI collection itself happens in the ActiveCollections file. - private void Version5To6() - { - if (_config.Version != 5) - return; - - if (_config.Ephemeral.TutorialStep == 25) - _config.Ephemeral.TutorialStep = 27; - - _config.Version = 6; - } - - // Mod backup extension was changed from .zip to .pmp. - // Actual migration takes place in ModManager. - private void Version4To5() - { - if (_config.Version != 4) - return; - - ModBackup.MigrateModBackups = true; - _config.Version = 5; - } - - // SortMode was changed from an enum to a type. - private void Version3To4() - { - if (_config.Version != 3) - return; - - SortMode = _data[nameof(SortMode)]?.ToObject() ?? SortMode; - _config.SortMode = SortMode switch - { - SortModeV3.FoldersFirst => ISortMode.FoldersFirst, - SortModeV3.Lexicographical => ISortMode.Lexicographical, - SortModeV3.InverseFoldersFirst => ISortMode.InverseFoldersFirst, - SortModeV3.InverseLexicographical => ISortMode.InverseLexicographical, - SortModeV3.FoldersLast => ISortMode.FoldersLast, - SortModeV3.InverseFoldersLast => ISortMode.InverseFoldersLast, - SortModeV3.InternalOrder => ISortMode.InternalOrder, - SortModeV3.InternalOrderInverse => ISortMode.InverseInternalOrder, - _ => ISortMode.FoldersFirst, - }; - _config.Version = 4; - } - - // SortFoldersFirst was changed from a bool to the enum SortMode. - private void Version2To3() - { - if (_config.Version != 2) - return; - - SortFoldersFirst = _data[nameof(SortFoldersFirst)]?.ToObject() ?? false; - SortMode = SortFoldersFirst ? SortModeV3.FoldersFirst : SortModeV3.Lexicographical; - _config.Version = 3; - } - - // The forced collection was removed due to general inheritance. - // Sort Order was moved to a separate file and may contain empty folders. - // Active collections in general were moved to their own file. - // Delete the penumbrametatmp folder if it exists. - private void Version1To2(CharacterUtility utility) - { - if (_config.Version != 1) - return; - - // Ensure the right meta files are loaded. - DeleteMetaTmp(); - if (utility.Ready) - utility.LoadCharacterResources(); - ResettleSortOrder(); - ResettleCollectionSettings(); - ResettleForcedCollection(); - _config.Version = 2; - } - - private void DeleteMetaTmp() - { - var path = Path.Combine(_config.ModDirectory, "penumbrametatmp"); - if (!Directory.Exists(path)) - return; - - try - { - Directory.Delete(path, true); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not delete the outdated penumbrametatmp folder:\n{e}"); - } - } - - private void ResettleForcedCollection() - { - ForcedCollection = _data[nameof(ForcedCollection)]?.ToObject() ?? ForcedCollection; - if (ForcedCollection.Length <= 0) - return; - - // Add the previous forced collection to all current collections except itself as an inheritance. - foreach (var collection in saveService.FileNames.CollectionFiles) - { - try - { - var jObject = JObject.Parse(File.ReadAllText(collection.FullName)); - if (jObject["Name"]?.ToObject() == ForcedCollection) - continue; - - jObject[nameof(ModCollectionInheritance.DirectlyInheritsFrom)] = JToken.FromObject(new List { ForcedCollection }); - File.WriteAllText(collection.FullName, jObject.ToString()); - } - catch (Exception e) - { - Penumbra.Log.Error( - $"Could not transfer forced collection {ForcedCollection} to inheritance of collection {collection}:\n{e}"); - } - } - } - - // Move the current sort order to its own file. - private void ResettleSortOrder() - { - ModSortOrder = _data[nameof(ModSortOrder)]?.ToObject>() ?? ModSortOrder; - var file = saveService.FileNames.OldFilesystemFile; - using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); - using var writer = new StreamWriter(stream); - using var j = new JsonTextWriter(writer); - j.Formatting = Formatting.Indented; - j.WriteStartObject(); - j.WritePropertyName("Data"); - j.WriteStartObject(); - foreach (var (mod, path) in ModSortOrder.Where(kvp => Directory.Exists(Path.Combine(_config.ModDirectory, kvp.Key)))) - { - j.WritePropertyName(mod, true); - j.WriteValue(path); - } - - j.WriteEndObject(); - j.WritePropertyName("EmptyFolders"); - j.WriteStartArray(); - j.WriteEndArray(); - j.WriteEndObject(); - } - - // Move the active collections to their own file. - private void ResettleCollectionSettings() - { - CurrentCollection = _data[nameof(CurrentCollection)]?.ToObject() ?? CurrentCollection; - DefaultCollection = _data[nameof(DefaultCollection)]?.ToObject() ?? DefaultCollection; - CharacterCollections = _data[nameof(CharacterCollections)]?.ToObject>() ?? CharacterCollections; - SaveActiveCollectionsV0(DefaultCollection, CurrentCollection, DefaultCollection, - CharacterCollections.Select(kvp => (kvp.Key, kvp.Value)), Array.Empty<(CollectionType, string)>()); - } - - // Outdated saving using the Characters list. - private void SaveActiveCollectionsV0(string def, string ui, string current, IEnumerable<(string, string)> characters, - IEnumerable<(CollectionType, string)> special) - { - var file = saveService.FileNames.ActiveCollectionsFile; - try - { - using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); - using var writer = new StreamWriter(stream); - using var j = new JsonTextWriter(writer); - j.Formatting = Formatting.Indented; - j.WriteStartObject(); - j.WritePropertyName(nameof(ActiveCollectionData.Default)); - j.WriteValue(def); - j.WritePropertyName(nameof(ActiveCollectionData.Interface)); - j.WriteValue(ui); - j.WritePropertyName(nameof(ActiveCollectionData.Current)); - j.WriteValue(current); - foreach (var (type, collection) in special) - { - j.WritePropertyName(type.ToString()); - j.WriteValue(collection); - } - - j.WritePropertyName("Characters"); - j.WriteStartObject(); - foreach (var (character, collection) in characters) - { - j.WritePropertyName(character, true); - j.WriteValue(collection); - } - - j.WriteEndObject(); - j.WriteEndObject(); - Penumbra.Log.Verbose("Active Collections saved."); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not save active collections to file {file}:\n{e}"); - } - } - - // Collections were introduced and the previous CurrentCollection got put into ModDirectory. - private void Version0To1() - { - if (_config.Version != 0) - return; - - _config.ModDirectory = _data[nameof(CurrentCollection)]?.ToObject() ?? string.Empty; - _config.Version = 1; - ResettleCollectionJson(); - } - - /// Move the previous mod configurations to a new default collection file. - private void ResettleCollectionJson() - { - var collectionJson = new FileInfo(Path.Combine(_config.ModDirectory, "collection.json")); - if (!collectionJson.Exists) - return; - - var defaultCollectionFile = new FileInfo(saveService.FileNames.CollectionFile(ModCollectionIdentity.DefaultCollectionName)); - if (defaultCollectionFile.Exists) - return; - - try - { - var text = File.ReadAllText(collectionJson.FullName); - var data = JArray.Parse(text); - - var maxPriority = ModPriority.Default; - var dict = new Dictionary(); - foreach (var setting in data.Cast()) - { - var modName = setting["FolderName"]?.ToObject()!; - var enabled = setting["Enabled"]?.ToObject() ?? false; - var priority = setting["Priority"]?.ToObject() ?? ModPriority.Default; - var settings = setting["Settings"]!.ToObject>() - ?? setting["Conf"]!.ToObject>(); - - dict[modName] = new ModSettings.SavedSettings() - { - Enabled = enabled, - Priority = priority, - Settings = settings!, - }; - maxPriority = maxPriority.Max(priority); - } - - InvertModListOrder = _data[nameof(InvertModListOrder)]?.ToObject() ?? InvertModListOrder; - if (!InvertModListOrder) - dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); - - var emptyStorage = new ModStorage(); - // Only used for saving and immediately discarded, so the local collection id here is irrelevant. - var collection = ModCollection.CreateFromData(saveService, emptyStorage, ModCollectionIdentity.New(ModCollectionIdentity.DefaultCollectionName, LocalCollectionId.Zero, 1), 0, dict, []); - saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection)); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not migrate the old collection file to new collection files:\n{e}"); - throw; - } - } - - // Create a backup of the configuration file specifically. - private void CreateBackup() - { - var name = saveService.FileNames.ConfigurationFile; - var bakName = name + ".bak"; - try - { - File.Copy(name, bakName, true); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not create backup copy of config at {bakName}:\n{e}"); - } - } - - public enum SortModeV3 : byte - { - FoldersFirst = 0x00, - Lexicographical = 0x01, - InverseFoldersFirst = 0x02, - InverseLexicographical = 0x03, - FoldersLast = 0x04, - InverseFoldersLast = 0x05, - InternalOrder = 0x06, - InternalOrderInverse = 0x07, - } -} + + // Migrate to ephemeral config. + private void Version8To9() + { + if (_config.Version != 8) + return; + + backupService.CreateMigrationBackup("pre_collection_identifiers"); + _config.Version = 9; + _config.Ephemeral.Version = 9; + _config.Save(); + _config.Ephemeral.Save(); + } + + // Migrate to ephemeral config. + private void Version7To8() + { + if (_config.Version != 7) + return; + + _config.Version = 8; + _config.Ephemeral.Version = 8; + + _config.Ephemeral.LastSeenVersion = _data["LastSeenVersion"]?.ToObject() ?? _config.Ephemeral.LastSeenVersion; + _config.Ephemeral.DebugSeparateWindow = _data["DebugSeparateWindow"]?.ToObject() ?? _config.Ephemeral.DebugSeparateWindow; + _config.Ephemeral.TutorialStep = _data["TutorialStep"]?.ToObject() ?? _config.Ephemeral.TutorialStep; + _config.Ephemeral.EnableResourceLogging = _data["EnableResourceLogging"]?.ToObject() ?? _config.Ephemeral.EnableResourceLogging; + _config.Ephemeral.ResourceLoggingFilter = _data["ResourceLoggingFilter"]?.ToObject() ?? _config.Ephemeral.ResourceLoggingFilter; + _config.Ephemeral.EnableResourceWatcher = _data["EnableResourceWatcher"]?.ToObject() ?? _config.Ephemeral.EnableResourceWatcher; + _config.Ephemeral.OnlyAddMatchingResources = + _data["OnlyAddMatchingResources"]?.ToObject() ?? _config.Ephemeral.OnlyAddMatchingResources; + _config.Ephemeral.ResourceWatcherResourceTypes = _data["ResourceWatcherResourceTypes"]?.ToObject() + ?? _config.Ephemeral.ResourceWatcherResourceTypes; + _config.Ephemeral.ResourceWatcherResourceCategories = _data["ResourceWatcherResourceCategories"]?.ToObject() + ?? _config.Ephemeral.ResourceWatcherResourceCategories; + _config.Ephemeral.ResourceWatcherRecordTypes = + _data["ResourceWatcherRecordTypes"]?.ToObject() ?? _config.Ephemeral.ResourceWatcherRecordTypes; + _config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject() ?? _config.Ephemeral.CollectionPanel; + _config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject() ?? _config.Ephemeral.SelectedTab; + _config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject() + ?? _config.Ephemeral.ChangedItemFilter; + _config.Ephemeral.FixMainWindow = _data["FixMainWindow"]?.ToObject() ?? _config.Ephemeral.FixMainWindow; + _config.Ephemeral.Save(); + } + + // Gendered special collections were added. + private void Version6To7() + { + if (_config.Version != 6) + return; + + ActiveCollectionMigration.MigrateUngenderedCollections(saveService.FileNames); + _config.Version = 7; + } + + + // A new tutorial step was inserted in the middle. + // The UI collection and a new tutorial for it was added. + // The migration for the UI collection itself happens in the ActiveCollections file. + private void Version5To6() + { + if (_config.Version != 5) + return; + + if (_config.Ephemeral.TutorialStep == 25) + _config.Ephemeral.TutorialStep = 27; + + _config.Version = 6; + } + + // Mod backup extension was changed from .zip to .pmp. + // Actual migration takes place in ModManager. + private void Version4To5() + { + if (_config.Version != 4) + return; + + ModBackup.MigrateModBackups = true; + _config.Version = 5; + } + + // SortMode was changed from an enum to a type. + private void Version3To4() + { + if (_config.Version != 3) + return; + + SortMode = _data[nameof(SortMode)]?.ToObject() ?? SortMode; + _config.SortMode = SortMode switch + { + SortModeV3.FoldersFirst => ISortMode.FoldersFirst, + SortModeV3.Lexicographical => ISortMode.Lexicographical, + SortModeV3.InverseFoldersFirst => ISortMode.InverseFoldersFirst, + SortModeV3.InverseLexicographical => ISortMode.InverseLexicographical, + SortModeV3.FoldersLast => ISortMode.FoldersLast, + SortModeV3.InverseFoldersLast => ISortMode.InverseFoldersLast, + SortModeV3.InternalOrder => ISortMode.InternalOrder, + SortModeV3.InternalOrderInverse => ISortMode.InverseInternalOrder, + _ => ISortMode.FoldersFirst, + }; + _config.Version = 4; + } + + // SortFoldersFirst was changed from a bool to the enum SortMode. + private void Version2To3() + { + if (_config.Version != 2) + return; + + SortFoldersFirst = _data[nameof(SortFoldersFirst)]?.ToObject() ?? false; + SortMode = SortFoldersFirst ? SortModeV3.FoldersFirst : SortModeV3.Lexicographical; + _config.Version = 3; + } + + // The forced collection was removed due to general inheritance. + // Sort Order was moved to a separate file and may contain empty folders. + // Active collections in general were moved to their own file. + // Delete the penumbrametatmp folder if it exists. + private void Version1To2(CharacterUtility utility) + { + if (_config.Version != 1) + return; + + // Ensure the right meta files are loaded. + DeleteMetaTmp(); + if (utility.Ready) + utility.LoadCharacterResources(); + ResettleSortOrder(); + ResettleCollectionSettings(); + ResettleForcedCollection(); + _config.Version = 2; + } + + private void DeleteMetaTmp() + { + var path = Path.Combine(_config.ModDirectory, "penumbrametatmp"); + if (!Directory.Exists(path)) + return; + + try + { + Directory.Delete(path, true); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not delete the outdated penumbrametatmp folder:\n{e}"); + } + } + + private void ResettleForcedCollection() + { + ForcedCollection = _data[nameof(ForcedCollection)]?.ToObject() ?? ForcedCollection; + if (ForcedCollection.Length <= 0) + return; + + // Add the previous forced collection to all current collections except itself as an inheritance. + foreach (var collection in saveService.FileNames.CollectionFiles) + { + try + { + var jObject = JObject.Parse(File.ReadAllText(collection.FullName)); + if (jObject["Name"]?.ToObject() == ForcedCollection) + continue; + + jObject[nameof(ModCollectionInheritance.DirectlyInheritsFrom)] = JToken.FromObject(new List { ForcedCollection }); + File.WriteAllText(collection.FullName, jObject.ToString()); + } + catch (Exception e) + { + Penumbra.Log.Error( + $"Could not transfer forced collection {ForcedCollection} to inheritance of collection {collection}:\n{e}"); + } + } + } + + // Move the current sort order to its own file. + private void ResettleSortOrder() + { + ModSortOrder = _data[nameof(ModSortOrder)]?.ToObject>() ?? ModSortOrder; + var file = saveService.FileNames.OldFilesystemFile; + using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); + using var writer = new StreamWriter(stream); + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + j.WriteStartObject(); + j.WritePropertyName("Data"); + j.WriteStartObject(); + foreach (var (mod, path) in ModSortOrder.Where(kvp => Directory.Exists(Path.Combine(_config.ModDirectory, kvp.Key)))) + { + j.WritePropertyName(mod, true); + j.WriteValue(path); + } + + j.WriteEndObject(); + j.WritePropertyName("EmptyFolders"); + j.WriteStartArray(); + j.WriteEndArray(); + j.WriteEndObject(); + } + + // Move the active collections to their own file. + private void ResettleCollectionSettings() + { + CurrentCollection = _data[nameof(CurrentCollection)]?.ToObject() ?? CurrentCollection; + DefaultCollection = _data[nameof(DefaultCollection)]?.ToObject() ?? DefaultCollection; + CharacterCollections = _data[nameof(CharacterCollections)]?.ToObject>() ?? CharacterCollections; + SaveActiveCollectionsV0(DefaultCollection, CurrentCollection, DefaultCollection, + CharacterCollections.Select(kvp => (kvp.Key, kvp.Value)), Array.Empty<(CollectionType, string)>()); + } + + // Outdated saving using the Characters list. + private void SaveActiveCollectionsV0(string def, string ui, string current, IEnumerable<(string, string)> characters, + IEnumerable<(CollectionType, string)> special) + { + var file = saveService.FileNames.ActiveCollectionsFile; + try + { + using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); + using var writer = new StreamWriter(stream); + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + j.WriteStartObject(); + j.WritePropertyName(nameof(ActiveCollectionData.Default)); + j.WriteValue(def); + j.WritePropertyName(nameof(ActiveCollectionData.Interface)); + j.WriteValue(ui); + j.WritePropertyName(nameof(ActiveCollectionData.Current)); + j.WriteValue(current); + foreach (var (type, collection) in special) + { + j.WritePropertyName(type.ToString()); + j.WriteValue(collection); + } + + j.WritePropertyName("Characters"); + j.WriteStartObject(); + foreach (var (character, collection) in characters) + { + j.WritePropertyName(character, true); + j.WriteValue(collection); + } + + j.WriteEndObject(); + j.WriteEndObject(); + Penumbra.Log.Verbose("Active Collections saved."); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not save active collections to file {file}:\n{e}"); + } + } + + // Collections were introduced and the previous CurrentCollection got put into ModDirectory. + private void Version0To1() + { + if (_config.Version != 0) + return; + + _config.ModDirectory = _data[nameof(CurrentCollection)]?.ToObject() ?? string.Empty; + _config.Version = 1; + ResettleCollectionJson(); + } + + /// Move the previous mod configurations to a new default collection file. + private void ResettleCollectionJson() + { + var collectionJson = new FileInfo(Path.Combine(_config.ModDirectory, "collection.json")); + if (!collectionJson.Exists) + return; + + var defaultCollectionFile = new FileInfo(saveService.FileNames.CollectionFile(ModCollectionIdentity.DefaultCollectionName)); + if (defaultCollectionFile.Exists) + return; + + try + { + var text = File.ReadAllText(collectionJson.FullName); + var data = JArray.Parse(text); + + var maxPriority = ModPriority.Default; + var dict = new Dictionary(); + foreach (var setting in data.Cast()) + { + var modName = setting["FolderName"]?.ToObject()!; + var enabled = setting["Enabled"]?.ToObject() ?? false; + var priority = setting["Priority"]?.ToObject() ?? ModPriority.Default; + var settings = setting["Settings"]!.ToObject>() + ?? setting["Conf"]!.ToObject>(); + + dict[modName] = new ModSettings.SavedSettings + { + Enabled = enabled, + Priority = priority, + Settings = settings!, + }; + maxPriority = maxPriority.Max(priority); + } + + InvertModListOrder = _data[nameof(InvertModListOrder)]?.ToObject() ?? InvertModListOrder; + if (!InvertModListOrder) + dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); + + var emptyStorage = new ModStorage(); + // Only used for saving and immediately discarded, so the local collection id here is irrelevant. + var collection = ModCollection.CreateFromData(saveService, emptyStorage, + ModCollectionIdentity.New(ModCollectionIdentity.DefaultCollectionName, LocalCollectionId.Zero, 1), 0, dict, []); + saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection)); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not migrate the old collection file to new collection files:\n{e}"); + throw; + } + } + + // Create a backup of the configuration file specifically. + private void CreateBackup() + { + var name = saveService.FileNames.ConfigurationFile; + var bakName = name + ".bak"; + try + { + File.Copy(name, bakName, true); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not create backup copy of config at {bakName}:\n{e}"); + } + } + + public enum SortModeV3 : byte + { + FoldersFirst = 0x00, + Lexicographical = 0x01, + InverseFoldersFirst = 0x02, + InverseLexicographical = 0x03, + FoldersLast = 0x04, + InverseFoldersLast = 0x05, + InternalOrder = 0x06, + InternalOrderInverse = 0x07, + } +} diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 65362ee1..ba3ef826 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -17,6 +17,7 @@ public sealed class FilenameService(IDalamudPluginInterface pi) : BaseFilePathPr public readonly string FileSystemEmptyFolders = Path.Combine(pi.ConfigDirectory.FullName, "mod_filesystem", "empty_folders.json"); public readonly string FileSystemExpandedFolders = Path.Combine(pi.ConfigDirectory.FullName, "mod_filesystem", "expanded_folders.json"); public readonly string FileSystemLockedNodes = Path.Combine(pi.ConfigDirectory.FullName, "mod_filesystem", "locked_nodes.json"); + public readonly string FileSystemSelectedNodes = Path.Combine(pi.ConfigDirectory.FullName, "mod_filesystem", "selected_nodes.json"); public readonly string CrashHandlerExe = Path.Combine(pi.AssemblyLocation.DirectoryName!, "Penumbra.CrashHandler.exe"); diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 174e0d0f..97da119d 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -8,11 +8,11 @@ using Penumbra.GameData.Files.StainMapStructs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.UI.AdvancedWindow.Materials; +using FilterComboColors = Penumbra.UI.Classes.FilterComboColors; using MouseWheelType = OtterGui.Widgets.MouseWheelType; namespace Penumbra.Services; -// TODO //public sealed class StainTemplateCombo(FilterComboColors[] stainCombos, StmFile stmFile) : SimpleFilterCombo(SimpleFilterType.Text) // where TDyePack : unmanaged, IDyePack //{ diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 70a88c00..c5b3a311 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -565,7 +565,7 @@ public class ItemSwapTab : IDisposable, ITab Im.Line.Same(); ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Count - 1} other Items.", Vector2.Zero, - Colors.PressEnterWarningBg); + new Rgba32(Colors.PressEnterWarningBg).Color); if (Im.Item.Hovered()) ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Item.Name)) .Select(i => i.Name))); @@ -626,7 +626,7 @@ public class ItemSwapTab : IDisposable, ITab Im.Line.Same(); ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Count - 1} other Items.", Vector2.Zero, - Colors.PressEnterWarningBg); + new Rgba32(Colors.PressEnterWarningBg).Color); if (Im.Item.Hovered()) ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Item.Name)) .Select(i => i.Name))); @@ -723,7 +723,7 @@ public class ItemSwapTab : IDisposable, ITab ModelRace.AuRa, ModelRace.Hrothgar, ], - RaceEnumExtensions.ToName); + ModelRaceExtensions.ToName); } } @@ -750,7 +750,7 @@ public class ItemSwapTab : IDisposable, ITab private static void DrawSwap(Swap swap) { - var flags = swap.ChildSwaps.Count == 0 ? ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.DefaultOpen; + var flags = swap.ChildSwaps.Count is 0 ? ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.DefaultOpen; using var tree = ImRaii.TreeNode(SwapToString(swap), flags); if (!tree) return; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs index a6c36161..264cffd3 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -1,10 +1,6 @@ using Dalamud.Bindings.ImGui; -using Dalamud.Interface; using ImSharp; using Luna; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Text; using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Files.StainMapStructs; using Penumbra.Services; @@ -30,7 +26,7 @@ public partial class MtrlTab var framePadding = Im.Style.FramePadding; var buttonWidth = (Im.ContentRegion.Available.X - itemSpacing * 7.0f) * 0.125f; var frameHeight = Im.Style.FrameHeight; - var highlighterSize = ImEx.Icon.CalculateSize(FontAwesomeIcon.Crosshairs.Icon()) + framePadding * 2.0f; + var highlighterSize = ImEx.Icon.CalculateSize(LunaStyle.OnHoverIcon) + framePadding * 2.0f; using var font = Im.Font.PushMono(); using var alignment = ImStyleDouble.ButtonTextAlign.Push(new Vector2(0, 0.5f)); @@ -56,19 +52,19 @@ public partial class MtrlTab CtBlendRect( rcMin with { X = rcMax.X - frameHeight * 3 - itemInnerSpacing * 2 }, rcMax with { X = rcMax.X - (frameHeight + itemInnerSpacing) * 2 }, - ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].DiffuseColor)), - ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].DiffuseColor)) + PseudoSqrtRgb((Vector3)table[pairIndex << 1].DiffuseColor), + PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].DiffuseColor) ); CtBlendRect( rcMin with { X = rcMax.X - frameHeight * 2 - itemInnerSpacing }, rcMax with { X = rcMax.X - frameHeight - itemInnerSpacing }, - ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].SpecularColor)), - ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].SpecularColor)) + PseudoSqrtRgb((Vector3)table[pairIndex << 1].SpecularColor), + PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].SpecularColor) ); CtBlendRect( rcMin with { X = rcMax.X - frameHeight }, rcMax, - ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].EmissiveColor)), - ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].EmissiveColor)) + PseudoSqrtRgb((Vector3)table[pairIndex << 1].EmissiveColor), + PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].EmissiveColor) ); if (j < 7) Im.Line.Same(); @@ -95,95 +91,95 @@ public partial class MtrlTab var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Key; var dyePackA = _stainService.GudStmFile.GetValueOrNull(dyeA.Template, previewDyeA); var dyePackB = _stainService.GudStmFile.GetValueOrNull(dyeB.Template, previewDyeB); - using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + using (var columns = Im.Columns(2, "ColorTable"u8)) { - using (ImUtf8.PushId("RowHeaderA"u8)) + using (Im.Id.Push("RowHeaderA"u8)) { retA |= DrawRowHeader(rowAIdx, disabled); } columns.Next(); - using (ImUtf8.PushId("RowHeaderB"u8)) + using (Im.Id.Push("RowHeaderB"u8)) { retB |= DrawRowHeader(rowBIdx, disabled); } } DrawHeader(" Colors"u8); - using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + using (var columns = Im.Columns(2, "ColorTable"u8)) { - using var dis = ImRaii.Disabled(disabled); - using (ImUtf8.PushId("ColorsA"u8)) + using var dis = Im.Disabled(disabled); + using (Im.Id.Push("ColorsA"u8)) { retA |= DrawColors(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); - using (ImUtf8.PushId("ColorsB"u8)) + using (Im.Id.Push("ColorsB"u8)) { retB |= DrawColors(table, dyeTable, dyePackB, rowBIdx); } } DrawHeader(" Physical Parameters"u8); - using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + using (var columns = Im.Columns(2, "ColorTable"u8)) { - using var dis = ImRaii.Disabled(disabled); - using (ImUtf8.PushId("PbrA"u8)) + using var dis = Im.Disabled(disabled); + using (Im.Id.Push("PbrA"u8)) { retA |= DrawPbr(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); - using (ImUtf8.PushId("PbrB"u8)) + using (Im.Id.Push("PbrB"u8)) { retB |= DrawPbr(table, dyeTable, dyePackB, rowBIdx); } } DrawHeader(" Sheen Layer Parameters"u8); - using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + using (var columns = Im.Columns(2, "ColorTable"u8)) { - using var dis = ImRaii.Disabled(disabled); - using (ImUtf8.PushId("SheenA"u8)) + using var dis = Im.Disabled(disabled); + using (Im.Id.Push("SheenA"u8)) { retA |= DrawSheen(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); - using (ImUtf8.PushId("SheenB"u8)) + using (Im.Id.Push("SheenB"u8)) { retB |= DrawSheen(table, dyeTable, dyePackB, rowBIdx); } } DrawHeader(" Pair Blending"u8); - using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + using (var columns = Im.Columns(2, "ColorTable"u8)) { - using var dis = ImRaii.Disabled(disabled); - using (ImUtf8.PushId("BlendingA"u8)) + using var dis = Im.Disabled(disabled); + using (Im.Id.Push("BlendingA"u8)) { retA |= DrawBlending(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); - using (ImUtf8.PushId("BlendingB"u8)) + using (Im.Id.Push("BlendingB"u8)) { retB |= DrawBlending(table, dyeTable, dyePackB, rowBIdx); } } DrawHeader(" Material Template"u8); - using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + using (var columns = Im.Columns(2, "ColorTable"u8)) { - using var dis = ImRaii.Disabled(disabled); - using (ImUtf8.PushId("TemplateA"u8)) + using var dis = Im.Disabled(disabled); + using (Im.Id.Push("TemplateA"u8)) { retA |= DrawTemplate(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); - using (ImUtf8.PushId("TemplateB"u8)) + using (Im.Id.Push("TemplateB"u8)) { retB |= DrawTemplate(table, dyeTable, dyePackB, rowBIdx); } @@ -192,31 +188,31 @@ public partial class MtrlTab if (dyeTable != null) { DrawHeader(" Dye Properties"u8); - using var columns = ImUtf8.Columns(2, "ColorTable"u8); - using var dis = ImRaii.Disabled(disabled); - using (ImUtf8.PushId("DyeA"u8)) + using var columns = Im.Columns(2, "ColorTable"u8); + using var dis = Im.Disabled(disabled); + using (Im.Id.Push("DyeA"u8)) { retA |= DrawDye(dyeTable, dyePackA, rowAIdx); } columns.Next(); - using (ImUtf8.PushId("DyeB"u8)) + using (Im.Id.Push("DyeB"u8)) { retB |= DrawDye(dyeTable, dyePackB, rowBIdx); } } DrawHeader(" Further Content"u8); - using (var columns = ImUtf8.Columns(2, "ColorTable"u8)) + using (var columns = Im.Columns(2, "ColorTable"u8)) { - using var dis = ImRaii.Disabled(disabled); - using (ImUtf8.PushId("FurtherA"u8)) + using var dis = Im.Disabled(disabled); + using (Im.Id.Push("FurtherA"u8)) { retA |= DrawFurther(table, dyeTable, dyePackA, rowAIdx); } columns.Next(); - using (ImUtf8.PushId("FurtherB"u8)) + using (Im.Id.Push("FurtherB"u8)) { retB |= DrawFurther(table, dyeTable, dyePackB, rowBIdx); } @@ -235,7 +231,7 @@ public partial class MtrlTab { var headerColor = Im.Style[ImGuiColor.Header]; using var _ = ImGuiColor.HeaderHovered.Push(headerColor).Push(ImGuiColor.HeaderActive, headerColor); - ImUtf8.CollapsingHeader(label, ImGuiTreeNodeFlags.Leaf); + Im.Tree.Header(label, TreeNodeFlags.Leaf); } private bool DrawRowHeader(int rowIdx, bool disabled) @@ -276,7 +272,7 @@ public partial class MtrlTab ret |= CtColorPicker("Specular Color"u8, default, row.SpecularColor, c => table[rowIdx].SpecularColor = c); - if (dyeTable != null) + if (dyeTable is not null) { Im.Line.Same(dyeOffset); ret |= CtApplyStainCheckbox("##dyeSpecularColor"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor, @@ -287,7 +283,7 @@ public partial class MtrlTab ret |= CtColorPicker("Emissive Color"u8, default, row.EmissiveColor, c => table[rowIdx].EmissiveColor = c); - if (dyeTable != null) + if (dyeTable is not null) { Im.Line.Same(dyeOffset); ret |= CtApplyStainCheckbox("##dyeEmissiveColor"u8, "Apply Emissive Color on Dye"u8, dye.EmissiveColor, @@ -308,7 +304,7 @@ public partial class MtrlTab - Im.Style.FrameHeight - scalarSize; - var isRowB = (rowIdx & 1) != 0; + var isRowB = (rowIdx & 1) is not 0; var ret = false; ref var row = ref table[rowIdx]; @@ -317,7 +313,7 @@ public partial class MtrlTab Im.Item.SetNextWidth(scalarSize); ret |= CtDragHalf(isRowB ? "Field #19"u8 : "Anisotropy Degree"u8, default, row.Anisotropy, "%.2f"u8, 0.0f, HalfMaxValue, 0.1f, v => table[rowIdx].Anisotropy = v); - if (dyeTable != null) + if (dyeTable is not null) { Im.Line.Same(dyeOffset); ret |= CtApplyStainCheckbox("##dyeAnisotropy"u8, isRowB ? "Apply Field #19 on Dye"u8 : "Apply Anisotropy Degree on Dye"u8, @@ -347,37 +343,37 @@ public partial class MtrlTab ret |= CtDragScalar("Shader ID"u8, default, row.ShaderId, "%d"u8, (ushort)0, (ushort)255, 0.25f, v => table[rowIdx].ShaderId = v); - ImGui.Dummy(new Vector2(Im.Style.TextHeight / 2)); + Im.Dummy(new Vector2(Im.Style.TextHeight / 2)); Im.Item.SetNextWidth(scalarSize + itemSpacing + 64.0f); ret |= CtSphereMapIndexPicker("###SphereMapIndex"u8, default, row.SphereMapIndex, false, v => table[rowIdx].SphereMapIndex = v); Im.Line.SameInner(); - ImUtf8.Text("Sphere Map"u8); - if (dyeTable != null) + Im.Text("Sphere Map"u8); + if (dyeTable is not null) { - var textRectMin = ImGui.GetItemRectMin(); - var textRectMax = ImGui.GetItemRectMax(); + var textRectMin = Im.Item.UpperLeftCorner; + var textRectMax = Im.Item.LowerRightCorner; Im.Line.Same(dyeOffset); - var cursor = ImGui.GetCursorScreenPos(); - ImGui.SetCursorScreenPos(cursor with { Y = float.Lerp(textRectMin.Y, textRectMax.Y, 0.5f) - Im.Style.FrameHeight * 0.5f }); + var cursor = Im.Cursor.ScreenPosition; + Im.Cursor.ScreenPosition = cursor with { Y = float.Lerp(textRectMin.Y, textRectMax.Y, 0.5f) - Im.Style.FrameHeight * 0.5f }; ret |= CtApplyStainCheckbox("##dyeSphereMapIndex"u8, "Apply Sphere Map on Dye"u8, dye.SphereMapIndex, b => dyeTable[rowIdx].SphereMapIndex = b); Im.Line.SameInner(); - ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursor.Y }); + Im.Cursor.ScreenPosition = Im.Cursor.ScreenPosition with { Y = cursor.Y }; Im.Item.SetNextWidth(scalarSize + itemSpacing + 64.0f); - using var dis = ImRaii.Disabled(); + using var dis = Im.Disabled(); CtSphereMapIndexPicker("###SphereMapIndexDye"u8, "Dye Preview for Sphere Map"u8, dyePack?.SphereMapIndex ?? ushort.MaxValue, false, Nop); } - ImGui.Dummy(new Vector2(64.0f, 0.0f)); + Im.Dummy(new Vector2(64.0f, 0.0f)); Im.Line.Same(); Im.Item.SetNextWidth(scalarSize); ret |= CtDragScalar("Sphere Map Intensity"u8, default, (float)row.SphereMapMask * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, v => table[rowIdx].SphereMapMask = (Half)(v * 0.01f)); - if (dyeTable != null) + if (dyeTable is not null) { Im.Line.Same(dyeOffset); ret |= CtApplyStainCheckbox("##dyeSphereMapMask"u8, "Apply Sphere Map Intensity on Dye"u8, dye.SphereMapMask, @@ -387,25 +383,24 @@ public partial class MtrlTab CtDragScalar("##dyeSphereMapMask"u8, "Dye Preview for Sphere Map Intensity"u8, (float?)dyePack?.SphereMapMask * 100.0f, "%.0f%%"u8); } - ImGui.Dummy(new Vector2(Im.Style.TextHeight / 2)); + Im.Dummy(new Vector2(Im.Style.TextHeight / 2)); var leftLineHeight = 64.0f + Im.Style.FramePadding.Y * 2.0f; var rightLineHeight = 3.0f * Im.Style.FrameHeight + 2.0f * Im.Style.ItemSpacing.Y; var lineHeight = Math.Max(leftLineHeight, rightLineHeight); - var cursorPos = ImGui.GetCursorScreenPos(); - ImGui.SetCursorScreenPos(cursorPos + new Vector2(0.0f, (lineHeight - leftLineHeight) * 0.5f)); + var cursorPos = Im.Cursor.ScreenPosition; + Im.Cursor.ScreenPosition = cursorPos + new Vector2(0.0f, (lineHeight - leftLineHeight) * 0.5f); Im.Item.SetNextWidth(scalarSize + (itemSpacing + 64.0f) * 2.0f); ret |= CtTileIndexPicker("###TileIndex"u8, default, row.TileIndex, false, v => table[rowIdx].TileIndex = v); Im.Line.SameInner(); - ImUtf8.Text("Tile"u8); + Im.Text("Tile"u8); Im.Line.Same(subColWidth); - ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursorPos.Y + (lineHeight - rightLineHeight) * 0.5f }); - using (ImUtf8.Child("###TileProperties"u8, - new Vector2(Im.ContentRegion.Available.X, float.Lerp(rightLineHeight, lineHeight, 0.5f)))) + Im.Cursor.ScreenPosition = Im.Cursor.ScreenPosition with { Y = cursorPos.Y + (lineHeight - rightLineHeight) * 0.5f }; + using (Im.Child.Begin("###TileProperties"u8, Im.ContentRegion.Available with { Y = float.Lerp(rightLineHeight, lineHeight, 0.5f) })) { - ImGui.Dummy(new Vector2(scalarSize, 0.0f)); + Im.Dummy(new Vector2(scalarSize, 0.0f)); Im.Line.SameInner(); Im.Item.SetNextWidth(scalarSize); ret |= CtDragScalar("Tile Opacity"u8, default, (float)row.TileAlpha * 100.0f, "%.0f%%"u8, 0.0f, HalfMaxValue * 100.0f, 1.0f, @@ -414,9 +409,8 @@ public partial class MtrlTab ret |= CtTileTransformMatrix(row.TileTransform, scalarSize, true, m => table[rowIdx].TileTransform = m); Im.Line.SameInner(); - ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() - - new Vector2(0.0f, (Im.Style.FrameHeight + Im.Style.ItemSpacing.Y) * 0.5f)); - ImUtf8.Text("Tile Transform"u8); + Im.Cursor.ScreenPosition -= new Vector2(0.0f, (Im.Style.FrameHeight + Im.Style.ItemSpacing.Y) * 0.5f); + Im.Text("Tile Transform"u8); } return ret; @@ -440,7 +434,7 @@ public partial class MtrlTab ret |= CtDragScalar("Roughness"u8, default, (float)row.Roughness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, v => table[rowIdx].Roughness = (Half)(v * 0.01f)); - if (dyeTable != null) + if (dyeTable is not null) { Im.Line.Same(dyeOffset); ret |= CtApplyStainCheckbox("##dyeRoughness"u8, "Apply Roughness on Dye"u8, dye.Roughness, @@ -455,7 +449,7 @@ public partial class MtrlTab ret |= CtDragScalar("Metalness"u8, default, (float)row.Metalness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, v => table[rowIdx].Metalness = (Half)(v * 0.01f)); - if (dyeTable != null) + if (dyeTable is not null) { Im.Line.Same(subColWidth + dyeOffset); ret |= CtApplyStainCheckbox("##dyeMetalness"u8, "Apply Metalness on Dye"u8, dye.Metalness, @@ -485,7 +479,7 @@ public partial class MtrlTab Im.Item.SetNextWidth(scalarSize); ret |= CtDragScalar("Sheen"u8, default, (float)row.SheenRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, v => table[rowIdx].SheenRate = (Half)(v * 0.01f)); - if (dyeTable != null) + if (dyeTable is not null) { Im.Line.Same(dyeOffset); ret |= CtApplyStainCheckbox("##dyeSheenRate"u8, "Apply Sheen on Dye"u8, dye.SheenRate, @@ -500,7 +494,7 @@ public partial class MtrlTab ret |= CtDragScalar("Sheen Tint"u8, default, (float)row.SheenTintRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f, v => table[rowIdx].SheenTintRate = (Half)(v * 0.01f)); - if (dyeTable != null) + if (dyeTable is not null) { Im.Line.Same(subColWidth + dyeOffset); ret |= CtApplyStainCheckbox("##dyeSheenTintRate"u8, "Apply Sheen Tint on Dye"u8, dye.SheenTintRate, @@ -514,7 +508,7 @@ public partial class MtrlTab ret |= CtDragScalar("Sheen Roughness"u8, default, 100.0f / (float)row.SheenAperture, "%.0f%%"u8, 100.0f / HalfMaxValue, 100.0f / HalfEpsilon, 1.0f, v => table[rowIdx].SheenAperture = (Half)(100.0f / v)); - if (dyeTable != null) + if (dyeTable is not null) { Im.Line.Same(dyeOffset); ret |= CtApplyStainCheckbox("##dyeSheenRoughness"u8, "Apply Sheen Roughness on Dye"u8, dye.SheenAperture, @@ -545,7 +539,7 @@ public partial class MtrlTab Im.Item.SetNextWidth(scalarSize); ret |= CtDragHalf("Field #11"u8, default, row.Scalar11, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, v => table[rowIdx].Scalar11 = v); - if (dyeTable != null) + if (dyeTable is not null) { Im.Line.Same(dyeOffset); ret |= CtApplyStainCheckbox("##dyeScalar11"u8, "Apply Field #11 on Dye"u8, dye.Scalar3, @@ -555,7 +549,7 @@ public partial class MtrlTab CtDragHalf("##dyePreviewScalar11"u8, "Dye Preview for Field #11"u8, dyePack?.Scalar3, "%.2f"u8); } - ImGui.Dummy(new Vector2(Im.Style.TextHeight / 2)); + Im.Dummy(new Vector2(Im.Style.TextHeight / 2)); Im.Item.SetNextWidth(scalarSize); ret |= CtDragHalf("Field #3"u8, default, row.Scalar3, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f, @@ -594,7 +588,7 @@ public partial class MtrlTab private bool DrawDye(ColorDyeTable dyeTable, DyePack? dyePack, int rowIdx) { var scalarSize = ColorTableScalarSize * Im.Style.GlobalScale; - var applyButtonWidth = ImUtf8.CalcTextSize("Apply Preview Dye"u8).X + Im.Style.FramePadding.X * 2.0f; + var applyButtonWidth = Im.Font.CalculateSize("Apply Preview Dye"u8).X + Im.Style.FramePadding.X * 2.0f; var subColWidth = CalculateSubColumnWidth(2, applyButtonWidth); var ret = false; @@ -614,10 +608,10 @@ public partial class MtrlTab } Im.Line.SameInner(); - ImUtf8.Text("Dye Template"u8); + Im.Text("Dye Template"u8); Im.Line.Same(Im.ContentRegion.Available.X - applyButtonWidth + Im.Style.ItemSpacing.X); - using var dis = ImRaii.Disabled(!dyePack.HasValue); - if (ImUtf8.Button("Apply Preview Dye"u8)) + using var dis = Im.Disabled(!dyePack.HasValue); + if (Im.Button("Apply Preview Dye"u8)) ret |= Mtrl.ApplyDyeToRow(_stainService.GudStmFile, [ _stainService.StainCombo1.CurrentSelection.Key, _stainService.StainCombo2.CurrentSelection.Key, @@ -626,13 +620,13 @@ public partial class MtrlTab return ret; } - private static void CenteredTextInRest(string text) - => AlignedTextInRest(text, 0.5f); + private static void CenteredTextInRest(Utf8StringHandler text) + => AlignedTextInRest(ref text, 0.5f); - private static void AlignedTextInRest(string text, float alignment) + private static void AlignedTextInRest(ref Utf8StringHandler text, float alignment) { - var width = ImGui.CalcTextSize(text).X; - ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() + new Vector2((Im.ContentRegion.Available.X - width) * alignment, 0.0f)); + var width = Im.Font.CalculateSize(text).X; + Im.Cursor.ScreenPosition += new Vector2((Im.ContentRegion.Available.X - width) * alignment, 0.0f); Im.Text(text); } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index e4c7a5db..600ce44f 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -1,6 +1,5 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Utility; using ImSharp; using Penumbra.GameData.Files.MaterialStructs; using Penumbra.GameData.Files; @@ -26,8 +25,8 @@ public partial class MtrlTab if (!_shpkLoading && !TextureIds.Contains(ShpkFile.TableSamplerId) || Mtrl.Table == null) return false; - ImGui.Dummy(new Vector2(Im.Style.TextHeight / 2)); - if (!ImUtf8.CollapsingHeader("Color Table"u8, ImGuiTreeNodeFlags.DefaultOpen)) + Im.Dummy(new Vector2(Im.Style.TextHeight / 2)); + if (!Im.Tree.Header("Color Table"u8, TreeNodeFlags.DefaultOpen)) return false; ColorTableCopyAllClipboardButton(); @@ -91,7 +90,7 @@ public partial class MtrlTab { var (dyeId1, (name1, dyeColor1, gloss1)) = _stainService.StainCombo1.CurrentSelection; var (dyeId2, (name2, dyeColor2, gloss2)) = _stainService.StainCombo2.CurrentSelection; - var tt = dyeId1 == 0 && dyeId2 == 0 + var tt = dyeId1 is 0 && dyeId2 is 0 ? "Select a preview dye first."u8 : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."u8; if (ImUtf8.ButtonEx("Apply Preview Dye"u8, tt, disabled: disabled || dyeId1 == 0 && dyeId2 == 0)) @@ -303,31 +302,31 @@ public partial class MtrlTab CancelColorTableHighlight(); } - private static void CtBlendRect(Vector2 rcMin, Vector2 rcMax, uint topColor, uint bottomColor) + private static void CtBlendRect(Vector2 rcMin, Vector2 rcMax, Rgba32 topColor, Rgba32 bottomColor) { var frameRounding = Im.Style.FrameRounding; var frameThickness = Im.Style.FrameBorderThickness; var borderColor = ImGuiColor.Border.Get(); - var drawList = ImGui.GetWindowDrawList(); + var drawList = Im.Window.DrawList.Shape; if (topColor == bottomColor) { - drawList.AddRectFilled(rcMin, rcMax, topColor, frameRounding, ImDrawFlags.RoundCornersDefault); + drawList.RectangleFilled(rcMin, rcMax, topColor, frameRounding); } else { - drawList.AddRectFilled( + drawList.RectangleFilled( rcMin, rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) }, - topColor, frameRounding, ImDrawFlags.RoundCornersTopLeft | ImDrawFlags.RoundCornersTopRight); - drawList.AddRectFilledMultiColor( + topColor, frameRounding, ImDrawFlagsRectangle.RoundCornersTop); + drawList.RectangleMulticolor( rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) }, rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) }, topColor, topColor, bottomColor, bottomColor); - drawList.AddRectFilled( + drawList.RectangleFilled( rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) }, rcMax, - bottomColor, frameRounding, ImDrawFlags.RoundCornersBottomLeft | ImDrawFlags.RoundCornersBottomRight); + bottomColor, frameRounding, ImDrawFlagsRectangle.RoundCornersTop); } - drawList.AddRect(rcMin, rcMax, borderColor.Color, frameRounding, ImDrawFlags.RoundCornersDefault, frameThickness); + drawList.Rectangle(rcMin, rcMax, borderColor.Color, frameRounding, default, frameThickness); } private static bool CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor current, Action setter, diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs index 479ebd1c..28440146 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs @@ -77,7 +77,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable if (!ImUtf8.ButtonEx("Update MTRL Version to Dawntrail"u8, "Try using this if the material can not be loaded or should use legacy shaders.\n\nThis is not revertible."u8, - new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg)) + new Vector2(-0.1f, 0), false, 0, new Rgba32(Colors.PressEnterWarningBg).Color)) return false; Mtrl.MigrateToDawntrail(); diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs index 426188b1..88a57ae2 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs @@ -4,6 +4,7 @@ using Penumbra.GameData.Interop; using Penumbra.Interop.Hooks.Objects; using Penumbra.Interop.ResourceTree; using Penumbra.Services; +using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow.Materials; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 3f0f80e1..8d99af7f 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -91,7 +91,7 @@ public partial class ModEditWindow if (!ImUtf8.ButtonEx("Update MDL Version from V5 to V6"u8, "Try using this if the bone weights of a pre-Dawntrail model seem wrong.\n\nThis is not revertible."u8, - new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg)) + new Vector2(-0.1f, 0), false, 0, new Rgba32(Colors.PressEnterWarningBg).Color)) return; tab.Mdl.ConvertV5ToV6(); @@ -198,7 +198,7 @@ public partial class ModEditWindow var size = new Vector2(Im.ContentRegion.Available.X, 0); using var frame = ImRaii.FramedGroup("Exceptions", size, headerPreIcon: FontAwesomeIcon.TimesCircle, - borderColor: Colors.RegexWarningBorder); + borderColor: new Rgba32(Colors.RegexWarningBorder).Color); var spaceAvail = Im.ContentRegion.Available.X - Im.Style.ItemSpacing.X - 100; foreach (var (index, exception) in tab.IoExceptions.Index()) @@ -304,7 +304,7 @@ public partial class ModEditWindow private void DrawDocumentationLink(string address) { - const string text = "Documentation →"; + var text = "Documentation →"u8; var framePadding = Im.Style.FramePadding; var width = ImGui.CalcTextSize(text).X + framePadding.X * 2; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 975338c3..42e0462c 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -8,6 +8,7 @@ using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; +using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs index e81442bf..d5f3a664 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs @@ -6,6 +6,7 @@ using Penumbra.GameData.Files.ShaderStructs; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; using Penumbra.UI.AdvancedWindow.Materials; +using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 10533fba..69a7613e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -56,8 +56,8 @@ public partial class ModEditWindow { TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input"u8, "Import Image..."u8, "Can import game paths as well as your own files."u8, Mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); - if (_textureSelectCombo.Draw("##combo", - "Select the textures included in this mod on your drive or the ones they replace from the game files.", tex.Path, + if (_textureSelectCombo.Draw("##combo"u8, + "Select the textures included in this mod on your drive or the ones they replace from the game files."u8, tex.Path, Mod.ModPath.FullName.Length + 1, out var newPath) && newPath != tex.Path) tex.Load(_textures, newPath); @@ -199,7 +199,7 @@ public partial class ModEditWindow case TaskStatus.WaitingForActivation: case TaskStatus.WaitingToRun: case TaskStatus.Running: - ImGuiUtil.DrawTextButton("Computing...", -Vector2.UnitX, Colors.PressEnterWarningBg); + ImGuiUtil.DrawTextButton("Computing...", -Vector2.UnitX, new Rgba32(Colors.PressEnterWarningBg).Color); break; case TaskStatus.Canceled: case TaskStatus.Faulted: diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 8eeedfba..05246aba 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -407,9 +407,9 @@ public partial class ModEditWindow : IndexedWindow, IDisposable var width = ImGui.CalcTextSize("NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN ").X; table.SetupColumn("file"u8, TableColumnFlags.WidthStretch); - table.SetupColumn("size", TableColumnFlags.WidthFixed, ImGui.CalcTextSize("NNN.NNN "u8).X); + table.SetupColumn("size"u8, TableColumnFlags.WidthFixed, ImGui.CalcTextSize("NNN.NNN "u8).X); table.SetupColumn("hash"u8, TableColumnFlags.WidthFixed, - ImGui.GetWindowWidth() > 2 * width ? width : ImGui.CalcTextSize("NNNNNNNN... ").X); + ImGui.GetWindowWidth() > 2 * width ? width : Im.Font.CalculateSize("NNNNNNNN... "u8).X); foreach (var (set, size, hash) in _editor.Duplicates.Duplicates.Where(s => s.Paths.Length > 1)) { ImGui.TableNextColumn(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindowFactory.cs b/Penumbra/UI/AdvancedWindow/ModEditWindowFactory.cs index b281577f..3e422a33 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindowFactory.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindowFactory.cs @@ -11,6 +11,7 @@ using Penumbra.Mods.Editor; using Penumbra.Services; using Penumbra.UI.AdvancedWindow.Materials; using Penumbra.UI.AdvancedWindow.Meta; +using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index 31d38f6a..c9608247 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -1,17 +1,12 @@ -using Dalamud.Interface.Utility; -using Dalamud.Bindings.ImGui; using ImSharp; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Text; +using Luna; using Penumbra.Mods.Editor; -using Penumbra.Mods.Manager; using Penumbra.Mods.SubMods; using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; -public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Luna.IUiService +public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : IUiService { private string _newModName = string.Empty; @@ -20,25 +15,24 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu if (modMerger.MergeFromMod == null) return; - using var tab = ImRaii.TabItem("Merge Mods"); + using var tab = Im.TabBar.BeginItem("Merge Mods"u8); if (!tab) return; - ImGui.Dummy(Vector2.One); + Im.Dummy(Vector2.Zero); var size = 550 * Im.Style.GlobalScale; DrawMergeInto(size); Im.Line.Same(); DrawMergeIntoDesc(); - ImGui.Dummy(Vector2.One); + Im.Dummy(Vector2.Zero); Im.Separator(); - ImGui.Dummy(Vector2.One); + Im.Dummy(Vector2.Zero); DrawSplitOff(size); Im.Line.Same(); DrawSplitOffDesc(); - DrawError(); DrawWarnings(); } @@ -47,13 +41,13 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu { using var bigGroup = Im.Group(); var minComboSize = 300 * Im.Style.GlobalScale; - var textSize = ImUtf8.CalcTextSize($"Merge {modMerger.MergeFromMod!.Name} into ").X; + var textSize = Im.Font.CalculateSize($"Merge {modMerger.MergeFromMod!.Name} into ").X; - ImGui.AlignTextToFramePadding(); + Im.Cursor.FrameAlign(); using (Im.Group()) { - ImUtf8.Text("Merge "u8); + Im.Text("Merge "u8); Im.Line.Same(0, 0); if (size - textSize < minComboSize) { @@ -66,56 +60,56 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu } Im.Line.Same(0, 0); - ImUtf8.Text(" into"u8); + Im.Text(" into"u8); } Im.Line.Same(); - DrawCombo(size - ImGui.GetItemRectSize().X - Im.Style.ItemSpacing.X); + DrawCombo(size - Im.Item.Size.X - Im.Style.ItemSpacing.X); using (Im.Group()) { - using var disabled = ImRaii.Disabled(modMerger.MergeFromMod.HasOptions); + using var disabled = Im.Disabled(modMerger.MergeFromMod.HasOptions); var buttonWidth = (size - Im.Style.ItemSpacing.X) / 2; var group = modMerger.MergeToMod?.Groups.FirstOrDefault(g => g.Name == modMerger.OptionGroupName); var color = group != null || modMerger.OptionGroupName.Length is 0 && modMerger.OptionName.Length is 0 ? Colors.PressEnterWarningBg - : Colors.DiscordColor; + : LunaStyle.DiscordColor; using var style = ImStyleBorder.Frame.Push(color); Im.Item.SetNextWidth(buttonWidth); - ImGui.InputTextWithHint("##optionGroupInput", "Target Option Group", ref modMerger.OptionGroupName, 64); - ImGuiUtil.HoverTooltip( - "The name of the new or existing option group to find or create the option in. Leave both group and option name blank for the default option.\n" - + "A red border indicates an existing option group, a blue border indicates a new one."); + Im.Input.Text("##optionGroupInput"u8, ref modMerger.OptionGroupName, "Target Option Group"u8); + Im.Tooltip.OnHover( + "The name of the new or existing option group to find or create the option in. Leave both group and option name blank for the default option.\n"u8 + + "A red border indicates an existing option group, a blue border indicates a new one."u8); Im.Line.Same(); - color = color == Colors.DiscordColor - ? Colors.DiscordColor + color = color == LunaStyle.DiscordColor + ? LunaStyle.DiscordColor : group == null || group.Options.Any(o => o.Name == modMerger.OptionName) ? Colors.PressEnterWarningBg - : Colors.DiscordColor; + : LunaStyle.DiscordColor; style.Push(ImGuiColor.Border, color); Im.Item.SetNextWidth(buttonWidth); - ImGui.InputTextWithHint("##optionInput", "Target Option Name", ref modMerger.OptionName, 64); - ImGuiUtil.HoverTooltip( - "The name of the new or existing option to merge this mod into. Leave both group and option name blank for the default option.\n" - + "A red border indicates an existing option, a blue border indicates a new one."); + Im.Input.Text("##optionInput"u8, ref modMerger.OptionName, "Target Option Name"u8); + Im.Tooltip.OnHover( + "The name of the new or existing option to merge this mod into. Leave both group and option name blank for the default option.\n"u8 + + "A red border indicates an existing option, a blue border indicates a new one."u8); } if (modMerger.MergeFromMod.HasOptions) Im.Tooltip.OnHover("You can only specify a target option if the source mod has no true options itself."u8, HoveredFlags.AllowWhenDisabled); - if (ImGuiUtil.DrawDisabledButton("Merge", new Vector2(size, 0), - modMerger.CanMerge ? string.Empty : "Please select a target mod different from the current mod.", !modMerger.CanMerge)) + if (ImEx.Button("Merge"u8, new Vector2(size, 0), + modMerger.CanMerge ? StringU8.Empty : "Please select a target mod different from the current mod."u8, !modMerger.CanMerge)) modMerger.Merge(); } private void DrawMergeIntoDesc() { - ImGuiUtil.TextWrapped(modMerger.MergeFromMod!.HasOptions - ? "The currently selected mod has options.\n\nThis means, that all of those options will be merged into the target. If merging an option is not possible due to the redirections already existing in an existing option, it will revert all changes and break." - : "The currently selected mod has no true options.\n\nThis means that you can select an existing or new option to merge all its changes into in the target mod. On failure to merge into an existing option, all changes will be reverted."); + Im.TextWrapped(modMerger.MergeFromMod!.HasOptions + ? "The currently selected mod has options.\n\nThis means, that all of those options will be merged into the target. If merging an option is not possible due to the redirections already existing in an existing option, it will revert all changes and break."u8 + : "The currently selected mod has no true options.\n\nThis means that you can select an existing or new option to merge all its changes into in the target mod. On failure to merge into an existing option, all changes will be reverted."u8); } private void DrawCombo(float width) @@ -129,37 +123,37 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu { using var group = Im.Group(); Im.Item.SetNextWidth(size); - ImGui.InputTextWithHint("##newModInput", "New Mod Name...", ref _newModName, 64); - ImGuiUtil.HoverTooltip("Choose a name for the newly created mod. This does not need to be unique."); + Im.Input.Text("##newModInput"u8, ref _newModName, "New Mod Name..."u8); + Im.Tooltip.OnHover("Choose a name for the newly created mod. This does not need to be unique."u8); var tt = _newModName.Length == 0 - ? "Please enter a name for the newly created mod first." + ? "Please enter a name for the newly created mod first."u8 : modMerger.SelectedOptions.Count == 0 - ? "Please select at least one option to split off." - : string.Empty; - var buttonText = - $"Split Off {modMerger.SelectedOptions.Count} Option{(modMerger.SelectedOptions.Count > 1 ? "s" : string.Empty)}###SplitOff"; - if (ImGuiUtil.DrawDisabledButton(buttonText, new Vector2(size, 0), tt, tt.Length > 0)) + ? "Please select at least one option to split off."u8 + : StringU8.Empty; + if (ImEx.Button( + $"Split Off {modMerger.SelectedOptions.Count} Option{(modMerger.SelectedOptions.Count > 1 ? "s" : string.Empty)}###SplitOff", + new Vector2(size, 0), tt, tt.Length > 0)) modMerger.SplitIntoMod(_newModName); - ImGui.Dummy(Vector2.One); + Im.Dummy(Vector2.One); var buttonSize = new Vector2((size - 2 * Im.Style.ItemSpacing.X) / 3, 0); - if (ImGui.Button("Select All", buttonSize)) + if (Im.Button("Select All"u8, buttonSize)) modMerger.SelectedOptions.UnionWith(modMerger.MergeFromMod!.AllDataContainers); Im.Line.Same(); - if (ImGui.Button("Unselect All", buttonSize)) + if (Im.Button("Unselect All"u8, buttonSize)) modMerger.SelectedOptions.Clear(); Im.Line.Same(); - if (ImGui.Button("Invert Selection", buttonSize)) + if (Im.Button("Invert Selection"u8, buttonSize)) modMerger.SelectedOptions.SymmetricExceptWith(modMerger.MergeFromMod!.AllDataContainers); DrawOptionTable(size); } - private void DrawSplitOffDesc() + private static void DrawSplitOffDesc() { - ImGuiUtil.TextWrapped("Here you can create a copy or a partial copy of the currently selected mod.\n\n" - + "Select as many of the options you want to copy over, enter a new mod name and click Split Off.\n\n" - + "You can right-click option groups to select or unselect all options from that specific group, and use the three buttons above the table for quick manipulation of your selection.\n\n" - + "Only required files will be copied over to the new mod. The names of options and groups will be retained. If the Default option is not selected, the new mods default option will be empty."); + Im.TextWrapped("Here you can create a copy or a partial copy of the currently selected mod.\n\n"u8 + + "Select as many of the options you want to copy over, enter a new mod name and click Split Off.\n\n"u8 + + "You can right-click option groups to select or unselect all options from that specific group, and use the three buttons above the table for quick manipulation of your selection.\n\n"u8 + + "Only required files will be copied over to the new mod. The names of options and groups will be retained. If the Default option is not selected, the new mods default option will be empty."u8); } private void DrawOptionTable(float size) @@ -186,48 +180,47 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu table.SetupColumn("#Files"u8, TableColumnFlags.WidthFixed, 50 * Im.Style.GlobalScale); table.SetupColumn("#Swaps"u8, TableColumnFlags.WidthFixed, 50 * Im.Style.GlobalScale); table.SetupColumn("#Manips"u8, TableColumnFlags.WidthFixed, 50 * Im.Style.GlobalScale); - ImGui.TableHeadersRow(); + table.HeaderRow(); foreach (var (idx, option) in options.Index()) { - using var id = ImRaii.PushId(idx); + using var id = Im.Id.Push(idx); var selected = modMerger.SelectedOptions.Contains(option); - ImGui.TableNextColumn(); - if (ImGui.Checkbox("##check", ref selected)) + table.NextColumn(); + if (Im.Checkbox("##check"u8, ref selected)) Handle(option, selected); if (option.Group is not { } group) { - ImGuiUtil.DrawTableColumn(option.GetFullName()); - ImGui.TableNextColumn(); + table.DrawColumn(option.GetFullName()); + table.NextColumn(); } else { - ImGuiUtil.DrawTableColumn(option.GetName()); - - ImGui.TableNextColumn(); - ImGui.Selectable(group.Name, false); - if (ImGui.BeginPopupContextItem("##groupContext")) + table.DrawColumn(option.GetName()); + table.NextColumn(); + Im.Selectable(group.Name); + using var popup = Im.Popup.BeginContextItem("##groupContext"u8); + if (popup) { - if (ImGui.MenuItem("Select All")) + if (Im.Menu.Item("Select All"u8)) // ReSharper disable once PossibleMultipleEnumeration foreach (var opt in group.DataContainers) Handle(opt, true); - if (ImGui.MenuItem("Unselect All")) + if (Im.Menu.Item("Unselect All"u8)) // ReSharper disable once PossibleMultipleEnumeration foreach (var opt in group.DataContainers) Handle(opt, false); - ImGui.EndPopup(); } } - ImGui.TableNextColumn(); - ImGuiUtil.RightAlign(option.Files.Count.ToString(), 3 * Im.Style.GlobalScale); - ImGui.TableNextColumn(); - ImGuiUtil.RightAlign(option.FileSwaps.Count.ToString(), 3 * Im.Style.GlobalScale); - ImGui.TableNextColumn(); - ImGuiUtil.RightAlign(option.Manipulations.Count.ToString(), 3 * Im.Style.GlobalScale); + table.NextColumn(); + ImEx.TextRightAligned($"{option.Files.Count}", 3 * Im.Style.GlobalScale); + table.NextColumn(); + ImEx.TextRightAligned($"{option.FileSwaps.Count}", 3 * Im.Style.GlobalScale); + table.NextColumn(); + ImEx.TextRightAligned($"{option.Manipulations.Count}", 3 * Im.Style.GlobalScale); continue; void Handle(IModDataContainer option2, bool selected2) @@ -246,15 +239,15 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu return; Im.Separator(); - ImGui.Dummy(Vector2.One); + Im.Dummy(Vector2.One); using var color = ImGuiColor.Text.Push(Colors.TutorialBorder); foreach (var warning in modMerger.Warnings.SkipLast(1)) { - ImGuiUtil.TextWrapped(warning); + Im.TextWrapped(warning); Im.Separator(); } - ImGuiUtil.TextWrapped(modMerger.Warnings[^1]); + Im.TextWrapped(modMerger.Warnings[^1]); } private void DrawError() @@ -263,8 +256,8 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu return; Im.Separator(); - ImGui.Dummy(Vector2.One); + Im.Dummy(Vector2.One); using var color = ImGuiColor.Text.Push(Colors.RegexWarningBorder); - ImGuiUtil.TextWrapped(modMerger.Error.ToString()); + Im.TextWrapped(modMerger.Error.ToString()); } } diff --git a/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs b/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs index 8470e8ad..51c998ce 100644 --- a/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs +++ b/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs @@ -17,7 +17,7 @@ public sealed class OptionSelectCombo(ModEditor editor, ModEditWindow window) protected override void DrawCombo(string label, string preview, string tooltip, int currentSelected, float previewWidth, float itemHeight, ImGuiComboFlags flags) { - _border.PushBorder(ImStyleBorder.Frame, ColorId.FolderLine.Value()); + _border.Push(ImStyleBorder.Frame, ColorId.FolderLine.Value()); base.DrawCombo(label, preview, tooltip, currentSelected, previewWidth, itemHeight, flags); _border.Dispose(); } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 6c122a41..c9764cca 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -60,7 +60,7 @@ public class ResourceTreeViewer( if (!_task.IsCompleted) { Im.Line.New(); - Im.Text("Calculating character list..."); + Im.Text("Calculating character list..."u8); } else if (_task.Exception != null) { diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs index 5b1af9dc..6e94f72f 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs @@ -3,6 +3,7 @@ using Luna; using Penumbra.GameData.Files; using Penumbra.Interop.ResourceTree; using Penumbra.Services; +using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/Classes/ChangedItemDrawer.cs similarity index 98% rename from Penumbra/UI/ChangedItemDrawer.cs rename to Penumbra/UI/Classes/ChangedItemDrawer.cs index d6256158..6ccaef5b 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/Classes/ChangedItemDrawer.cs @@ -9,10 +9,9 @@ using Luna; using Penumbra.Communication; using Penumbra.GameData.Data; using Penumbra.Services; -using Penumbra.UI.Classes; using MouseButton = Penumbra.Api.Enums.MouseButton; -namespace Penumbra.UI; +namespace Penumbra.UI.Classes; public class ChangedItemDrawer : IDisposable, IUiService { @@ -119,7 +118,7 @@ public class ChangedItemDrawer : IDisposable, IUiService var ret = leftClicked ? MouseButton.Left : MouseButton.None; ret = Im.Item.RightClicked() ? MouseButton.Right : ret; ret = Im.Item.MiddleClicked() ? MouseButton.Middle : ret; - if (ret != MouseButton.None) + if (ret is not MouseButton.None) _communicator.ChangedItemClick.Invoke(new ChangedItemClick.Arguments(ret, data)); if (!Im.Item.Hovered()) return; @@ -139,7 +138,7 @@ public class ChangedItemDrawer : IDisposable, IUiService public static void DrawModelData(IIdentifiedObjectData data, float height) { var additionalData = data.AdditionalData; - if (additionalData.Length == 0) + if (additionalData.Length is 0) return; Im.Line.Same(); @@ -151,7 +150,7 @@ public class ChangedItemDrawer : IDisposable, IUiService /// Draw the model information, right-justified. public static void DrawModelData(ReadOnlySpan text, float height) { - if (text.Length == 0) + if (text.Length is 0) return; Im.Line.Same(); diff --git a/Penumbra/UI/ChangedItemIconFlag.cs b/Penumbra/UI/Classes/ChangedItemIconFlag.cs similarity index 97% rename from Penumbra/UI/ChangedItemIconFlag.cs rename to Penumbra/UI/Classes/ChangedItemIconFlag.cs index adb901b7..6cc0842f 100644 --- a/Penumbra/UI/ChangedItemIconFlag.cs +++ b/Penumbra/UI/Classes/ChangedItemIconFlag.cs @@ -1,7 +1,7 @@ using Penumbra.Api.Enums; using Penumbra.GameData.Enums; -namespace Penumbra.UI; +namespace Penumbra.UI.Classes; [Flags] public enum ChangedItemIconFlag : uint diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index a829d9e5..206352c2 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -19,7 +19,7 @@ public class CollectionSelectHeader( CollectionResolver resolver, Configuration config, CollectionCombo combo) - : IUiService + : IHeader { private readonly ActiveCollections _activeCollections = collectionManager.Active; private static readonly AwesomeIcon Icon = FontAwesomeIcon.Stopwatch; @@ -161,4 +161,30 @@ public class CollectionSelectHeader( _activeCollections.SetCollection(collection!, CollectionType.Current); Im.Line.Same(); } + + public bool Collapsed + => false; + + public void Draw(Vector2 size) + { + using var style = ImStyleDouble.ItemSpacing.Push(Vector2.Zero); + DrawTemporaryCheckbox(); + Im.Line.Same(); + var comboWidth = (size.X - Im.Style.FrameHeight) / 4f; + var buttonSize = new Vector2(comboWidth * 3f / 4f, 0f); + using (var _ = Im.Group()) + { + DrawCollectionButton(buttonSize, GetDefaultCollectionInfo(), 1); + DrawCollectionButton(buttonSize, GetInterfaceCollectionInfo(), 2); + DrawCollectionButton(buttonSize, GetPlayerCollectionInfo(), 3); + DrawCollectionButton(buttonSize, GetInheritedCollectionInfo(), 4); + + combo.Draw("##collectionSelector"u8, comboWidth, ColorId.SelectedCollection.Value()); + } + + tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); + + if (!_activeCollections.CurrentCollectionInUse) + ImEx.TextFramed("The currently selected collection is not used in any way."u8, -Vector2.UnitX, Colors.PressEnterWarningBg); + } } diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 16b3c4c0..9eadb706 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -1,5 +1,4 @@ using ImSharp; -using OtterGui.Custom; namespace Penumbra.UI.Classes; @@ -41,17 +40,13 @@ public enum ColorId : short public static class Colors { // These are written as 0xAABBGGRR. - public const uint PressEnterWarningBg = 0xFF202080; - public const uint RegexWarningBorder = 0xFF0000B0; - public const uint MetaInfoText = 0xAAFFFFFF; - public const uint RedTableBgTint = 0x40000080; - public const uint DiscordColor = CustomGui.DiscordColor; - public const uint FilterActive = 0x807070FF; - public const uint TutorialMarker = 0xFF20FFFF; - public const uint TutorialBorder = 0xD00000FF; - public const uint ReniColorButton = CustomGui.ReniColorButton; - public const uint ReniColorHovered = CustomGui.ReniColorHovered; - public const uint ReniColorActive = CustomGui.ReniColorActive; + public static readonly Vector4 PressEnterWarningBg = new(0.5f, 0.125f, 0.125f, 1); + public static readonly Vector4 RegexWarningBorder = new(0.7f, 0, 0, 1); + public static readonly Vector4 MetaInfoText = new(1, 1, 1, 2f / 3); + public const uint RedTableBgTint = 0x40000080; + public const uint FilterActive = 0x807070FF; + public const uint TutorialMarker = 0xFF20FFFF; + public const uint TutorialBorder = 0xD00000FF; public static (uint DefaultColor, string Name, string Description) Data(this ColorId color) => color switch diff --git a/Penumbra/UI/Classes/Combos.cs b/Penumbra/UI/Classes/Combos.cs index 326534dc..f1cb292e 100644 --- a/Penumbra/UI/Classes/Combos.cs +++ b/Penumbra/UI/Classes/Combos.cs @@ -9,10 +9,10 @@ public static class Combos { // Different combos to use with enums. public static bool Race(string label, ModelRace current, out ModelRace race, float unscaledWidth = 100) - => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out race, RaceEnumExtensions.ToName, 1); + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out race, ModelRaceExtensions.ToName, 1); public static bool Gender(string label, Gender current, out Gender gender, float unscaledWidth = 120) - => ImGuiUtil.GenericEnumCombo(label, unscaledWidth, current, out gender, RaceEnumExtensions.ToName, 1); + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth, current, out gender, GenderExtensions.ToName, 1); public static bool EqdpEquipSlot(string label, EquipSlot current, out EquipSlot slot, float unscaledWidth = 100) => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out slot, EquipSlotExtensions.EqdpSlots, @@ -27,7 +27,7 @@ public static class Combos EquipSlotExtensions.ToName); public static bool SubRace(string label, SubRace current, out SubRace subRace, float unscaledWidth = 150) - => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out subRace, RaceEnumExtensions.ToName, 1); + => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out subRace, SubRaceExtensions.ToName, 1); public static bool RspAttribute(string label, RspAttribute current, out RspAttribute attribute, float unscaledWidth = 200) => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out attribute, diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/Classes/FileDialogService.cs similarity index 99% rename from Penumbra/UI/FileDialogService.cs rename to Penumbra/UI/Classes/FileDialogService.cs index 00fcc189..0a6cb6d9 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/Classes/FileDialogService.cs @@ -6,7 +6,7 @@ using Luna; using Penumbra.Communication; using Penumbra.Services; -namespace Penumbra.UI; +namespace Penumbra.UI.Classes; public class FileDialogService : IDisposable, IUiService { diff --git a/Penumbra/UI/IncognitoService.cs b/Penumbra/UI/Classes/IncognitoService.cs similarity index 95% rename from Penumbra/UI/IncognitoService.cs rename to Penumbra/UI/Classes/IncognitoService.cs index 9d930790..e6bdaa78 100644 --- a/Penumbra/UI/IncognitoService.cs +++ b/Penumbra/UI/Classes/IncognitoService.cs @@ -1,8 +1,7 @@ using ImSharp; using Luna; -using Penumbra.UI.Classes; -namespace Penumbra.UI; +namespace Penumbra.UI.Classes; public class IncognitoService(TutorialService tutorial, Configuration config) : IUiService { diff --git a/Penumbra/Mods/Manager/ModCombo.cs b/Penumbra/UI/Classes/ModCombo.cs similarity index 89% rename from Penumbra/Mods/Manager/ModCombo.cs rename to Penumbra/UI/Classes/ModCombo.cs index 4cc57aaf..6937663b 100644 --- a/Penumbra/Mods/Manager/ModCombo.cs +++ b/Penumbra/UI/Classes/ModCombo.cs @@ -1,8 +1,10 @@ using ImSharp; using Luna; +using Penumbra.Mods; using Penumbra.Mods.Editor; +using Penumbra.Mods.Manager; -namespace Penumbra.Mods.Manager; +namespace Penumbra.UI.Classes; public class ModCombo(ModStorage modStorage) : SimpleFilterCombo(SimpleFilterType.Regex), IUiService { diff --git a/Penumbra/UI/Classes/StainCombo.cs b/Penumbra/UI/Classes/StainCombo.cs new file mode 100644 index 00000000..8fbcda0c --- /dev/null +++ b/Penumbra/UI/Classes/StainCombo.cs @@ -0,0 +1,118 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Utility; +using OtterGui; +using OtterGui.Extensions; +using OtterGui.Log; +using OtterGui.Raii; +using OtterGui.Widgets; + +namespace Penumbra.UI.Classes; + +public class FilterComboColors : FilterComboCache> +{ + private readonly float _comboWidth; + private readonly ImRaii.Color _color = new(); + private Vector2 _buttonSize; + private uint _currentColor; + private bool _currentGloss; + + protected override int UpdateCurrentSelected(int currentSelected) + { + if (CurrentSelection.Value.Color != _currentColor) + { + CurrentSelectionIdx = Items.IndexOf(c => c.Value.Color == _currentColor); + CurrentSelection = CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : default; + return base.UpdateCurrentSelected(CurrentSelectionIdx); + } + + return currentSelected; + } + + public FilterComboColors(float comboWidth, MouseWheelType allowMouseWheel, + Func>> colors, + Logger log) + : base(colors, allowMouseWheel, log) + { + _comboWidth = comboWidth; + SearchByParts = true; + } + + protected override float GetFilterWidth() + { + // Hack to not color the filter frame. + _color.Pop(); + return _buttonSize.X + ImGui.GetStyle().ScrollbarSize; + } + + protected override void DrawList(float width, float itemHeight) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.WindowPadding, Vector2.Zero) + .Push(ImGuiStyleVar.FrameRounding, 0); + _buttonSize = new Vector2(_comboWidth * ImGuiHelpers.GlobalScale, 0); + if (ImGui.GetScrollMaxY() > 0) + _buttonSize.X += ImGui.GetStyle().ScrollbarSize; + base.DrawList(width, itemHeight); + } + + protected override string ToString(KeyValuePair obj) + => obj.Value.Name; + + protected override bool DrawSelectable(int globalIdx, bool selected) + { + var (_, (name, color, gloss)) = Items[globalIdx]; + // Push the stain color to type and if it is too bright, turn the text color black. + var contrastColor = ImGuiUtil.ContrastColorBw(color); + using var colors = ImRaii.PushColor(ImGuiCol.Button, color, color != 0) + .Push(ImGuiCol.Text, contrastColor); + var ret = ImGui.Button(name, _buttonSize); + if (selected) + { + ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), 0xFF2020D0, 0, ImDrawFlags.None, + ImGuiHelpers.GlobalScale); + ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin() + new Vector2(ImGuiHelpers.GlobalScale), + ImGui.GetItemRectMax() - new Vector2(ImGuiHelpers.GlobalScale), contrastColor, 0, ImDrawFlags.None, ImGuiHelpers.GlobalScale); + } + + if (gloss) + ImGui.GetWindowDrawList().AddRectFilledMultiColor(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), 0x50FFFFFF, 0x50000000, + 0x50FFFFFF, 0x50000000); + + return ret; + } + + public virtual bool Draw(string label, uint color, string name, bool found, bool gloss, float previewWidth, + MouseWheelType mouseWheel = MouseWheelType.Control) + { + _currentColor = color; + _currentGloss = gloss; + var preview = found && ImGui.CalcTextSize(name).X <= previewWidth ? name : string.Empty; + + AllowMouseWheel = mouseWheel; + _color.Push(ImGuiCol.FrameBg, color, found && color != 0) + .Push(ImGuiCol.Text, ImGuiUtil.ContrastColorBw(color), preview.Length > 0); + var change = Draw(label, preview, found ? name : string.Empty, previewWidth, ImGui.GetFrameHeight(), ImGuiComboFlags.NoArrowButton); + return change; + } + + protected override void PostCombo(float previewWidth) + { + _color.Dispose(); + if (_currentGloss) + { + var min = ImGui.GetItemRectMin(); + ImGui.GetWindowDrawList().AddRectFilledMultiColor(min, new Vector2(min.X + previewWidth, ImGui.GetItemRectMax().Y), 0x50FFFFFF, + 0x50000000, 0x50FFFFFF, 0x50000000); + } + } + + protected override void OnMouseWheel(string preview, ref int index, int steps) + { + UpdateCurrentSelected(0); + base.OnMouseWheel(preview, ref index, steps); + } + + public bool Draw(string label, uint color, string name, bool found, bool gloss, + MouseWheelType mouseWheel = MouseWheelType.Control) + => Draw(label, color, name, found, gloss, ImGui.GetFrameHeight(), mouseWheel); +} diff --git a/Penumbra/UI/Classes/TutorialService.cs b/Penumbra/UI/Classes/TutorialService.cs new file mode 100644 index 00000000..7a6c6f6c --- /dev/null +++ b/Penumbra/UI/Classes/TutorialService.cs @@ -0,0 +1,148 @@ +using ImSharp; + +namespace Penumbra.UI.Classes; + +/// List of currently available tutorials. +public enum BasicTutorialSteps +{ + GeneralTooltips, + ModDirectory, + EnableMods, + Deprecated1, + GeneralSettings, + Collections, + EditingCollections, + CurrentCollection, + SimpleAssignments, + IndividualAssignments, + GroupAssignments, + CollectionDetails, + Incognito, + Deprecated2, + Mods, + ModImport, + AdvancedHelp, + ModFilters, + CollectionSelectors, + Redrawing, + EnablingMods, + Priority, + ModOptions, + Fin, + Deprecated3, + Faq1, + Faq2, + Favorites, + Tags, +} + +/// Service for the in-game tutorial. +public class TutorialService(EphemeralConfig config) : Luna.IUiService +{ + private readonly Luna.Tutorial _tutorial = new Luna.Tutorial + { + BorderColor = new Rgba32(Colors.TutorialBorder).ToVector(), + HighlightColor = new Rgba32(Colors.TutorialMarker).ToVector(), + PopupLabel = new StringU8("Settings Tutorial"u8), + } + .Register("General Tooltips"u8, "This symbol gives you further information about whatever setting it appears next to.\n\n"u8 + + "Hover over them when you are unsure what something does or how to do something."u8) + .Register("Initial Setup, Step 1: Mod Directory"u8, + "The first step is to set up your mod directory, which is where your mods are extracted to.\n\n"u8 + + "The mod directory should be a short path - like 'C:\\FFXIVMods' - on your fastest available drive. Faster drives improve performance.\n\n"u8 + + "The folder should be an empty folder no other applications write to."u8) + .Register("Initial Setup, Step 2: Enable Mods"u8, "Do not forget to enable your mods in case they are not."u8) + .Deprecated() + .Register("General Settings"u8, "Look through all of these settings before starting, they might help you a lot!\n\n"u8 + + "If you do not know what some of these do yet, return to this later!"u8) + .Register("Initial Setup, Step 3: Collections"u8, "Collections are lists of settings for your installed mods.\n\n"u8 + + "This is our next stop!\n\n"u8 + + "Go here after setting up your root folder to continue the tutorial!"u8) + .Register("Initial Setup, Step 4: Managing Collections"u8, + "On the left, we have the collection selector. Here, we can create new collections - either empty ones or by duplicating existing ones - and delete any collections not needed anymore.\n"u8 + + "There will always be one collection called \"Default\" that can not be deleted."u8) + .Register("Initial Setup, Step 5: Selected Collection"u8, + "The Selected Collection is the one we highlighted in the selector. It is the collection we are currently looking at and editing.\nAny changes we make in our mod settings later in the next tab will edit this collection.\n"u8 + + "We should already have the collection named \"Default\" selected, and for our simple setup, we do not need to do anything here.\n\n"u8) + .Register("Initial Setup, Step 6: Simple Assignments"u8, + "Aside from being a collection of settings, we can also assign collections to different functions. This is used to make different mods apply to different characters.\n"u8 + + "The Simple Assignments panel shows you the possible assignments that are enough for most people along with descriptions.\n"u8 + + "If you are just starting, you can see that the \"Default\" collection is currently assigned to Default and Interface.\n"u8 + + "You can also assign 'Use No Mods' instead of a collection by clicking on the function buttons."u8) + .Register("Individual Assignments"u8, + "In the Individual Assignments panel, you can manually create assignments for very specific characters or monsters, not just yourself or ones you can currently target."u8) + .Register("Group Assignments"u8, + "In the Group Assignments panel, you can create Assignments for more specific groups of characters based on race or age."u8) + .Register("Collection Details"u8, + "In the Collection Details panel, you can see a detailed overview over the usage of the currently selected collection, as well as remove outdated mod settings and setup inheritance.\n"u8 + + "Inheritance can be used to make one collection take the settings of another as long as it does not setup the mod in question itself."u8) + .Register("Incognito Mode"u8, + "This button can toggle Incognito Mode, which shortens all collection names to two letters and a number,\n"u8 + + "and all displayed individual character names to their initials and world, in case you want to share screenshots.\n"u8 + + "It is strongly recommended to not show your characters name in public screenshots when using Penumbra."u8) + .Deprecated() + .Register("Initial Setup, Step 7: Mods"u8, "Our last stop is the Mods tab, where you can import and setup your mods.\n\n"u8 + + "Please go there after verifying that your Selected Collection and Default Collection are setup to your liking."u8) + .Register("Initial Setup, Step 8: Mod Import"u8, + "Click this button to open a file selector with which to select TTMP mod files. You can select multiple at once.\n\n"u8 + + "It is not recommended to import huge mod packs of all your TexTools mods, but rather import the mods themselves, otherwise you lose out on a lot of Penumbra features!\n\n"u8 + + "A feature to import raw texture mods for Tattoos etc. is available under Advanced Editing, but is currently a work in progress."u8) + .Register("Advanced Help"u8, "Click this button to get detailed information on what you can do in the mod selector.\n\n"u8 + + "Import and select a mod now to continue."u8) + .Register("Mod Filters"u8, "You can filter the available mods by name, author, changed items or various attributes here."u8) + .Register("Collection Selectors"u8, "This row provides shortcuts to set your Selected Collection.\n\n"u8 + + "The first button sets it to your Base Collection (if any).\n\n"u8 + + "The second button sets it to the collection the settings of the currently selected mod are inherited from (if any).\n\n"u8 + + "The third is a regular collection selector to let you choose among all your collections."u8) + .Register("Redrawing"u8, + "Whenever you change your mod configuration, changes do not immediately take effect. You will need to force the game to reload the relevant files (or if this is not possible, restart the game).\n\n"u8 + + "For this, Penumbra has these buttons as well as the '/penumbra redraw' command, which redraws all actors at once. You can also use several modifiers described in the help marker instead.\n\n"u8 + + "Feel free to use these slash commands (e.g. '/penumbra redraw self') as a macro, too."u8) + .Register("Initial Setup, Step 9: Enabling Mods"u8, + "Enable a mod here. Disabled mods will not apply to anything in the current collection.\n\n"u8 + + "Mods can be enabled or disabled in a collection, or they can be unconfigured, in which case they will use Inheritance."u8) + .Register("Initial Setup, Step 10: Priority"u8, + "If two enabled mods in one collection change the same files, there is a conflict.\n\n"u8 + + "Conflicts can be solved by setting a priority. The mod with the higher number will be used for all the conflicting files.\n\n"u8 + + "Conflicts are not a problem, as long as they are correctly resolved with priorities. Negative priorities are possible."u8) + .Register("Mod Options"u8, "Many mods have options themselves. You can also choose those here.\n\n"u8 + + "Pulldown-options are mutually exclusive, whereas checkmark options can all be enabled separately."u8) + .Register("Initial Setup - Fin"u8, "Now you should have all information to get Penumbra running and working!\n\n"u8 + + "If there are further questions or you need more help for the advanced features, take a look at the guide linked in the settings page."u8) + .Deprecated() + .Register("FAQ 1"u8, + "It is advised to not use TexTools and Penumbra at the same time. Penumbra may refuse to work if TexTools broke your game indices."u8) + .Register("FAQ 2"u8, "Penumbra can change the skin material a mod uses. This is under advanced editing."u8) + .Register("Favorites"u8, + "You can now toggle mods as favorites using this button. You can filter for favorited mods in the mod selector. Favorites are stored locally, not within the mod, but independently of collections."u8) + .Register("Tags"u8, + "Mods can now have two types of tags:\n\n- Local Tags are those that you can set for yourself. They are stored locally and are not saved in any way in the mod directory itself.\n- Mod Tags are stored in the mod metadata, are set by the mod creator and are exported together with the mod, they can only be edited in the Edit Mod tab.\n\nIf a mod has a tag in its Mod Tags, this overwrites any identical Local Tags.\n\nYou can filter for tags in the mod selector via 't:text'."u8) + .EnsureSize(Enum.GetValues().Length); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OpenTutorial(BasicTutorialSteps step) + => _tutorial.Open((int)step, config.TutorialStep, v => + { + config.TutorialStep = v; + config.Save(); + }); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SkipTutorial(BasicTutorialSteps step) + => _tutorial.Skip((int)step, config.TutorialStep, v => + { + config.TutorialStep = v; + config.Save(); + }); + + /// Update the current tutorial step if tutorials have changed since last update. + public void UpdateTutorialStep() + { + var tutorial = _tutorial.CurrentEnabledId(config.TutorialStep); + if (tutorial != config.TutorialStep) + { + config.TutorialStep = tutorial; + config.Save(); + } + } +} diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index c4de27b9..bcc910ed 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -272,7 +272,7 @@ public sealed class CollectionPanel( if (!context) return; - using (ImGuiColor.Text.Push(Colors.DiscordColor)) + using (ImGuiColor.Text.Push(LunaStyle.DiscordColor)) { if (Im.Menu.Item("Use no mods."u8)) _active.SetCollection(ModCollection.Empty, type, _active.Individuals.GetGroup(identifier)); @@ -349,7 +349,7 @@ public sealed class CollectionPanel( return; using var target = Im.DragDrop.Target(); - if (!target || !target.IsDropping("DragIndividual")) + if (!target || !target.IsDropping("DragIndividual"u8)) return; var currentIdx = _active.Individuals.Index(id); @@ -376,7 +376,7 @@ public sealed class CollectionPanel( break; case CollectionType.MalePlayerCharacter: ImEx.TextMultiColored("Overruled by "u8) - .Then("Male Racial Player"u8, Colors.DiscordColor) + .Then("Male Racial Player"u8, LunaStyle.DiscordColor) .Then(", "u8) .Then("Your Character"u8, ColorId.HandledConflictMod.Value().Color) .Then(", or "u8) @@ -386,7 +386,7 @@ public sealed class CollectionPanel( break; case CollectionType.FemalePlayerCharacter: ImEx.TextMultiColored("Overruled by "u8) - .Then("Female Racial Player"u8, Colors.ReniColorActive) + .Then("Female Racial Player"u8, LunaStyle.ReniColorActive) .Then(", "u8) .Then("Your Character"u8, ColorId.HandledConflictMod.Value().Color) .Then(", or "u8) @@ -396,24 +396,24 @@ public sealed class CollectionPanel( break; case CollectionType.MaleNonPlayerCharacter: ImEx.TextMultiColored("Overruled by "u8) - .Then("Male Racial NPC"u8, Colors.DiscordColor) + .Then("Male Racial NPC"u8, LunaStyle.DiscordColor) .Then(", "u8) .Then("Children"u8, ColorId.FolderLine.Value().Color) .Then(", "u8) .Then("Elderly"u8, Colors.MetaInfoText) .Then(", or "u8) .Then("Individual "u8, ColorId.NewMod.Value().Color) - .Then("Assignments.") + .Then("Assignments."u8) .End(); break; case CollectionType.FemaleNonPlayerCharacter: ImEx.TextMultiColored("Overruled by "u8) - .Then("Female Racial NPC"u8, Colors.ReniColorActive) + .Then("Female Racial NPC"u8, LunaStyle.ReniColorActive) .Then(", "u8) .Then("Children"u8, ColorId.FolderLine.Value().Color) .Then(", "u8) .Then("Elderly"u8, Colors.MetaInfoText) - .Then(", or ") + .Then(", or "u8) .Then("Individual "u8, ColorId.NewMod.Value().Color) .Then("Assignments."u8) .End(); diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index a4d19e3d..8e339111 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -1,144 +1,142 @@ -using ImSharp; -using OtterGui; -using Penumbra.Collections; -using Penumbra.Collections.Manager; -using Penumbra.Communication; -using Penumbra.GameData.Actors; -using Penumbra.Services; -using Penumbra.UI.Classes; - -namespace Penumbra.UI.CollectionTab; - -public sealed class CollectionSelector : ItemSelector, IDisposable -{ - private readonly Configuration _config; - private readonly CommunicatorService _communicator; - private readonly CollectionStorage _storage; - private readonly ActiveCollections _active; - private readonly TutorialService _tutorial; - private readonly IncognitoService _incognito; - - private ModCollection? _dragging; - - public CollectionSelector(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active, - TutorialService tutorial, IncognitoService incognito) - : base([], Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter) - { - _config = config; - _communicator = communicator; - _storage = storage; - _active = active; - _tutorial = tutorial; - _incognito = incognito; - - _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionSelector); - // Set items. - OnCollectionChange(new CollectionChange.Arguments(CollectionType.Inactive, null, null, string.Empty)); - // Set selection. - OnCollectionChange(new CollectionChange.Arguments(CollectionType.Current, null, _active.Current, string.Empty)); - } - - protected override bool OnDelete(int idx) - { - if (idx < 0 || idx >= Items.Count) - return false; - - // Always return false since we handle the selection update ourselves. - _storage.RemoveCollection(Items[idx]); - return false; - } - - protected override bool DeleteButtonEnabled() - => _storage.DefaultNamed != Current && _config.DeleteModModifier.IsActive(); - - protected override string DeleteButtonTooltip() - => _storage.DefaultNamed == Current - ? $"The selected collection {Name(Current)} can not be deleted." - : $"Delete the currently selected collection {(Current != null ? Name(Current) : string.Empty)}. Hold {_config.DeleteModModifier} to delete."; - - protected override bool OnAdd(string name) - => _storage.AddCollection(name, null); - - protected override bool OnDuplicate(string name, int idx) - { - if (idx < 0 || idx >= Items.Count) - return false; - - return _storage.AddCollection(name, Items[idx]); - } - - protected override bool Filtered(int idx) - => !Items[idx].Identity.Name.Contains(Filter, StringComparison.OrdinalIgnoreCase); - - private const string PayloadString = "Collection"; - - protected override bool OnDraw(int idx) - { - using var color = ImGuiColor.Header.Push(ColorId.SelectedCollection.Value()); - var ret = Im.Selectable(Name(Items[idx]), idx == CurrentIdx); - using var source = Im.DragDrop.Source(); - - if (idx == CurrentIdx) - _tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection); - - if (source) - { - _dragging = Items[idx]; - source.SetPayload(PayloadString); - Im.Text($"Assigning {Name(_dragging)} to..."); - } - - if (ret) - _active.SetCollection(Items[idx], CollectionType.Current); - - return ret; - } - - public void DragTargetAssignment(CollectionType type, ActorIdentifier identifier) - { - using var target = Im.DragDrop.Target(); - if (!target.Success || _dragging is null || !target.IsDropping(PayloadString)) - return; - - _active.SetCollection(_dragging, type, _active.Individuals.GetGroup(identifier)); - _dragging = null; - } - - public void Dispose() - { - _communicator.CollectionChange.Unsubscribe(OnCollectionChange); - } - - private string Name(ModCollection collection) - => _incognito.IncognitoMode || collection.Identity.Name.Length == 0 ? collection.Identity.AnonymizedName : collection.Identity.Name; - - public void RestoreCollections() - { - Items.Clear(); - Items.Add(_storage.DefaultNamed); - foreach (var c in _storage.OrderBy(c => c.Identity.Name).Where(c => c != _storage.DefaultNamed)) - Items.Add(c); - SetFilterDirty(); - SetCurrent(_active.Current); - } - - private void OnCollectionChange(in CollectionChange.Arguments arguments) - { - switch (arguments.Type) - { - case CollectionType.Temporary: return; - case CollectionType.Current: - if (arguments.NewCollection is not null) - SetCurrent(arguments.NewCollection); - SetFilterDirty(); - return; - case CollectionType.Inactive: - RestoreCollections(); - SetFilterDirty(); - return; - default: - SetFilterDirty(); - return; - } - } -} +using ImSharp; +using OtterGui; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Communication; +using Penumbra.GameData.Actors; +using Penumbra.Services; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.CollectionTab; + +public sealed class CollectionSelector : ItemSelector, IDisposable +{ + private readonly Configuration _config; + private readonly CommunicatorService _communicator; + private readonly CollectionStorage _storage; + private readonly ActiveCollections _active; + private readonly TutorialService _tutorial; + private readonly IncognitoService _incognito; + + private ModCollection? _dragging; + + public CollectionSelector(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active, + TutorialService tutorial, IncognitoService incognito) + : base([], Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter) + { + _config = config; + _communicator = communicator; + _storage = storage; + _active = active; + _tutorial = tutorial; + _incognito = incognito; + + _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionSelector); + // Set items. + OnCollectionChange(new CollectionChange.Arguments(CollectionType.Inactive, null, null, string.Empty)); + // Set selection. + OnCollectionChange(new CollectionChange.Arguments(CollectionType.Current, null, _active.Current, string.Empty)); + } + + protected override bool OnDelete(int idx) + { + if (idx < 0 || idx >= Items.Count) + return false; + + // Always return false since we handle the selection update ourselves. + _storage.RemoveCollection(Items[idx]); + return false; + } + + protected override bool DeleteButtonEnabled() + => _storage.DefaultNamed != Current && _config.DeleteModModifier.IsActive(); + + protected override string DeleteButtonTooltip() + => _storage.DefaultNamed == Current + ? $"The selected collection {Name(Current)} can not be deleted." + : $"Delete the currently selected collection {(Current != null ? Name(Current) : string.Empty)}. Hold {_config.DeleteModModifier} to delete."; + + protected override bool OnAdd(string name) + => _storage.AddCollection(name, null); + + protected override bool OnDuplicate(string name, int idx) + { + if (idx < 0 || idx >= Items.Count) + return false; + + return _storage.AddCollection(name, Items[idx]); + } + + protected override bool Filtered(int idx) + => !Items[idx].Identity.Name.Contains(Filter, StringComparison.OrdinalIgnoreCase); + + protected override bool OnDraw(int idx) + { + using var color = ImGuiColor.Header.Push(ColorId.SelectedCollection.Value()); + var ret = Im.Selectable(Name(Items[idx]), idx == CurrentIdx); + using var source = Im.DragDrop.Source(); + + if (idx == CurrentIdx) + _tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection); + + if (source) + { + _dragging = Items[idx]; + source.SetPayload("Collection"u8); + Im.Text($"Assigning {Name(_dragging)} to..."); + } + + if (ret) + _active.SetCollection(Items[idx], CollectionType.Current); + + return ret; + } + + public void DragTargetAssignment(CollectionType type, ActorIdentifier identifier) + { + using var target = Im.DragDrop.Target(); + if (!target.Success || _dragging is null || !target.IsDropping("Collection"u8)) + return; + + _active.SetCollection(_dragging, type, _active.Individuals.GetGroup(identifier)); + _dragging = null; + } + + public void Dispose() + { + _communicator.CollectionChange.Unsubscribe(OnCollectionChange); + } + + private string Name(ModCollection collection) + => _incognito.IncognitoMode || collection.Identity.Name.Length == 0 ? collection.Identity.AnonymizedName : collection.Identity.Name; + + public void RestoreCollections() + { + Items.Clear(); + Items.Add(_storage.DefaultNamed); + foreach (var c in _storage.OrderBy(c => c.Identity.Name).Where(c => c != _storage.DefaultNamed)) + Items.Add(c); + SetFilterDirty(); + SetCurrent(_active.Current); + } + + private void OnCollectionChange(in CollectionChange.Arguments arguments) + { + switch (arguments.Type) + { + case CollectionType.Temporary: return; + case CollectionType.Current: + if (arguments.NewCollection is not null) + SetCurrent(arguments.NewCollection); + SetFilterDirty(); + return; + case CollectionType.Inactive: + RestoreCollections(); + SetFilterDirty(); + return; + default: + SetFilterDirty(); + return; + } + } +} diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index 13be56f4..9a5f2e95 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -221,7 +221,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService var tt = inheritance switch { InheritanceManager.ValidInheritance.Empty => "No valid collection to inherit from selected.", - InheritanceManager.ValidInheritance.Valid => $"Let the {TutorialService.SelectedCollection} inherit from this collection.", + InheritanceManager.ValidInheritance.Valid => $"Let the Selected Collection inherit from this collection.", InheritanceManager.ValidInheritance.Self => "The collection can not inherit from itself.", InheritanceManager.ValidInheritance.Contained => "Already inheriting from this collection.", InheritanceManager.ValidInheritance.Circle => "Inheriting from this collection would lead to cyclic inheritance.", @@ -307,7 +307,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService } Im.Tooltip.OnHover( - $"Control + Right-Click to switch the {TutorialService.SelectedCollection} to this one.{(withDelete ? "\nControl + Shift + Right-Click to remove this inheritance."u8 : StringU8.Empty)}"); + $"Control + Right-Click to switch the Selected Collection to this one.{(withDelete ? "\nControl + Shift + Right-Click to remove this inheritance."u8 : StringU8.Empty)}"); } private string Name(ModCollection collection) diff --git a/Penumbra/UI/GlobalModImporter.cs b/Penumbra/UI/GlobalModImporter.cs new file mode 100644 index 00000000..592131b7 --- /dev/null +++ b/Penumbra/UI/GlobalModImporter.cs @@ -0,0 +1,45 @@ +using System.Collections.Frozen; +using Dalamud.Interface.DragDrop; +using ImSharp; +using Luna; +using Penumbra.Mods.Manager; + +namespace Penumbra.UI; + +public sealed class GlobalModImporter : IRequiredService, IDisposable +{ + public const string DragDropId = "ModDragDrop"; + private readonly DragDropManager _dragDropManager; + private readonly ModImportManager _importManager; + + /// All default extensions for valid mod imports. + public static readonly FrozenSet ValidModExtensions = FrozenSet.Create(StringComparer.OrdinalIgnoreCase, + ".ttmp", ".ttmp2", ".pmp", ".pcp", ".zip", ".rar", ".7z" + ); + + public GlobalModImporter(DragDropManager dragDropManager, ModImportManager modImportManager) + { + _dragDropManager = dragDropManager; + _importManager = modImportManager; + dragDropManager.AddSource(DragDropId, ValidExtension, DragTooltip); + dragDropManager.AddTarget(DragDropId, ImportFiles); + } + + public void Dispose() + { + _dragDropManager.RemoveSource(DragDropId); + _dragDropManager.RemoveTarget(DragDropId); + } + + private void ImportFiles(IReadOnlyList files, IReadOnlyList _) + => _importManager.AddUnpack(files.Where(f => ValidModExtensions.Contains(Path.GetExtension(f)))); + + private static bool ValidExtension(IDragDropManager manager) + => manager.Extensions.Any(ValidModExtensions.Contains); + + private static bool DragTooltip(IDragDropManager manager) + { + Im.Text($"Dragging mods for import:\n\t{StringU8.Join("\n\t"u8, manager.Files.Select(Path.GetFileName))}"); + return true; + } +} diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index fb963fd9..ad4c117e 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -10,21 +10,21 @@ namespace Penumbra.UI; /// public class LaunchButton : IDisposable, Luna.IUiService { - private readonly ConfigWindow _configWindow; - private readonly IUiBuilder _uiBuilder; - private readonly ITitleScreenMenu _title; - private readonly string _fileName; - private readonly ITextureProvider _textureProvider; + private readonly MainWindow.MainWindow _mainWindow; + private readonly IUiBuilder _uiBuilder; + private readonly ITitleScreenMenu _title; + private readonly string _fileName; + private readonly ITextureProvider _textureProvider; private IReadOnlyTitleScreenMenuEntry? _entry; /// /// Register the launch button to be created on the next draw event. /// - public LaunchButton(IDalamudPluginInterface pi, ITitleScreenMenu title, ConfigWindow ui, ITextureProvider textureProvider) + public LaunchButton(IDalamudPluginInterface pi, ITitleScreenMenu title, MainWindow.MainWindow ui, ITextureProvider textureProvider) { _uiBuilder = pi.UiBuilder; - _configWindow = ui; + _mainWindow = ui; _textureProvider = textureProvider; _title = title; _entry = null; @@ -59,5 +59,5 @@ public class LaunchButton : IDisposable, Luna.IUiService } private void OnTriggered() - => _configWindow.Toggle(); + => _mainWindow.Toggle(); } diff --git a/Penumbra/UI/MainWindow/ChangedItemsTab.cs b/Penumbra/UI/MainWindow/ChangedItemsTab.cs new file mode 100644 index 00000000..71499ced --- /dev/null +++ b/Penumbra/UI/MainWindow/ChangedItemsTab.cs @@ -0,0 +1,191 @@ +using ImSharp; +using Luna; +using Penumbra.Api.Enums; +using Penumbra.Collections.Manager; +using Penumbra.Communication; +using Penumbra.GameData.Data; +using Penumbra.Mods; +using Penumbra.Mods.Editor; +using Penumbra.Services; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.MainWindow; + +public class UiState : ISavable, IService +{ + public string ChangedItemTabNameFilter = string.Empty; + public string ChangedItemTabModFilter = string.Empty; + public ChangedItemIconFlag ChangedItemTabCategoryFilter = ChangedItemFlagExtensions.DefaultFlags; + + public string ToFilePath(FilenameService fileNames) + => "uiState"; + + public void Save(StreamWriter writer) + { } +} + +public sealed class ChangedItemsTab( + CollectionManager collectionManager, + CollectionSelectHeader collectionHeader, + ChangedItemDrawer drawer, + CommunicatorService communicator) + : ITab +{ + public ReadOnlySpan Label + => "Changed Items"u8; + + public TabType Identifier + => TabType.ChangedItems; + + private Vector2 _buttonSize; + + private readonly ChangedItemFilter _filter = new(drawer, new UiState()); + + private sealed class ChangedItemFilter(ChangedItemDrawer drawer, UiState uiState) : IFilter + { + public bool WouldBeVisible(in Item item, int globalIndex) + => drawer.FilterChangedItem(item.Name, item.Data, uiState.ChangedItemTabNameFilter) + && (uiState.ChangedItemTabModFilter.Length is 0 + || item.Mods.Any(m => m.Name.Contains(uiState.ChangedItemTabModFilter, StringComparison.OrdinalIgnoreCase))); + + public event Action? FilterChanged; + + public bool DrawFilter(ReadOnlySpan label, Vector2 availableRegion) + { + var varWidth = Im.ContentRegion.Available.X + - 450 * Im.Style.GlobalScale + - Im.Style.ItemSpacing.X; + Im.Item.SetNextWidth(450 * Im.Style.GlobalScale); + var ret = Im.Input.Text("##changedItemsFilter"u8, ref uiState.ChangedItemTabNameFilter, "Filter Item..."u8); + Im.Line.Same(); + Im.Item.SetNextWidth(varWidth); + ret |= Im.Input.Text("##changedItemsModFilter"u8, ref uiState.ChangedItemTabModFilter, "Filter Mods..."u8); + if (!ret) + return false; + + FilterChanged?.Invoke(); + return true; + } + + public void Clear() + { + uiState.ChangedItemTabModFilter = string.Empty; + uiState.ChangedItemTabNameFilter = string.Empty; + uiState.ChangedItemTabCategoryFilter = ChangedItemFlagExtensions.DefaultFlags; + FilterChanged?.Invoke(); + } + } + + private readonly record struct Item(string Label, IIdentifiedObjectData Data, SingleArray Mods) + { + public readonly string Name = Data.ToName(Label); + public readonly StringU8 ItemName = new(Data.ToName(Label)); + public readonly StringU8 Mod = Mods.Count > 0 ? new StringU8(Mods[0].Name) : StringU8.Empty; + public readonly StringU8 ModelData = new(Data.AdditionalData); + public readonly ChangedItemIconFlag CategoryIcon = Data.GetIcon().ToFlag(); + + public readonly StringU8 Tooltip = Mods.Count > 1 + ? new StringU8($"Other mods affecting this item:\n{StringU8.Join((byte)'\n', Mods.Skip(1).Select(m => m.Name))}") + : StringU8.Empty; + } + + private sealed class Cache : BasicFilterCache + { + private readonly ActiveCollections _collections; + private readonly CollectionChange _collectionChange; + + public Cache(ActiveCollections collections, CommunicatorService communicator, IFilter filter) + : base(filter) + { + _collections = collections; + _collectionChange = communicator.CollectionChange; + _collectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ChangedItemsTabCache); + } + + + + private void OnCollectionChange(in CollectionChange.Arguments arguments) + => FilterDirty = true; + + protected override IEnumerable GetItems() + => _collections.Current.ChangedItems.Select(kvp => new Item(kvp.Key, kvp.Value.Item2, kvp.Value.Item1)); + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _collectionChange.Unsubscribe(OnCollectionChange); + } + } + + public void DrawContent() + { + collectionHeader.Draw(true); + drawer.DrawTypeFilter(); + _filter.DrawFilter("##Filter"u8, Im.ContentRegion.Available); + using var child = Im.Child.Begin("##changedItemsChild"u8, Im.ContentRegion.Available); + if (!child) + return; + + _buttonSize = new Vector2(Im.Style.ItemSpacing.Y + Im.Style.FrameHeight); + using var style = ImStyleDouble.CellPadding.Push(Vector2.Zero) + .Push(ImStyleDouble.ItemSpacing, Vector2.Zero) + .Push(ImStyleDouble.FramePadding, Vector2.Zero) + .Push(ImStyleDouble.SelectableTextAlign, new Vector2(0.01f, 0.5f)); + + using var table = Im.Table.Begin("##changedItems"u8, 3, TableFlags.RowBackground, Im.ContentRegion.Available); + if (!table) + return; + + var varWidth = Im.ContentRegion.Available.X + - 450 * Im.Style.GlobalScale + - Im.Style.ItemSpacing.X; + const TableColumnFlags flags = TableColumnFlags.NoResize | TableColumnFlags.WidthFixed; + table.SetupColumn("items"u8, flags, 450 * Im.Style.GlobalScale); + table.SetupColumn("mods"u8, flags, varWidth - 140 * Im.Style.GlobalScale); + table.SetupColumn("id"u8, flags, 140 * Im.Style.GlobalScale); + + var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current, + () => new Cache(collectionManager.Active, communicator, _filter)); + using var clipper = new Im.ListClipper(cache.Count, _buttonSize.Y); + foreach (var (idx, item) in clipper.Iterate(cache).Index()) + { + using var id = Im.Id.Push(idx); + DrawChangedItemColumn(table, item); + } + } + + /// Draw a full column for a changed item. + private void DrawChangedItemColumn(in Im.TableDisposable table, in Item item) + { + table.NextColumn(); + drawer.DrawCategoryIcon(item.CategoryIcon, _buttonSize.Y); + Im.Line.NoSpacing(); + var clicked = Im.Selectable(item.ItemName, false, SelectableFlags.None, _buttonSize with { X = 0 }); + drawer.ChangedItemHandling(item.Data, clicked); + + table.NextColumn(); + DrawModColumn(item); + + table.NextColumn(); + ChangedItemDrawer.DrawModelData(item.ModelData, _buttonSize.Y); + } + + private void DrawModColumn(in Item item) + { + if (item.Mods.Count <= 0) + return; + + if (Im.Selectable(item.Mod, false, SelectableFlags.None, _buttonSize with { X = 0 }) + && Im.Io.KeyControl + && item.Mods[0] is Mod mod) + communicator.SelectTab.Invoke(new SelectTab.Arguments(TabType.Mods, mod)); + + if (!Im.Item.Hovered()) + return; + + using var _ = Im.Tooltip.Begin(); + Im.Text("Hold Control and click to jump to mod.\n"u8); + if (!item.Tooltip.IsEmpty) + Im.Text(item.Tooltip); + } +} diff --git a/Penumbra/UI/MainWindow/EffectiveTab.cs b/Penumbra/UI/MainWindow/EffectiveTab.cs new file mode 100644 index 00000000..44244aef --- /dev/null +++ b/Penumbra/UI/MainWindow/EffectiveTab.cs @@ -0,0 +1,179 @@ +using Dalamud.Interface; +using ImSharp; +using Luna; +using Penumbra.Api.Enums; +using Penumbra.Collections.Manager; +using Penumbra.Communication; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Services; +using Penumbra.String.Classes; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.MainWindow; + +public sealed class EffectiveTab( + CollectionManager collectionManager, + CollectionSelectHeader collectionHeader, + CommunicatorService communicatorService) + : ITab +{ + public ReadOnlySpan Label + => "Effective Changes"u8; + + public void DrawContent() + { + collectionHeader.Draw(true); + var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current, () => new Cache(collectionManager, communicatorService, _filter)); + cache.Draw(); + } + + public TabType Identifier + => TabType.EffectiveChanges; + + private readonly PairFilter _filter = new(new GamePathFilter(), new FullPathFilter()); + + private sealed class Cache : BasicFilterCache, IPanel + { + private readonly CollectionManager _collectionManager; + private readonly CommunicatorService _communicator; + private float _arrowSize; + private float _gamePathSize; + private static readonly AwesomeIcon Arrow = FontAwesomeIcon.LongArrowAltLeft; + + private new PairFilter Filter + => (PairFilter)base.Filter; + + public override void Update() + { + if (FontDirty) + { + _arrowSize = ImEx.Icon.CalculateSize(Arrow).X; + _gamePathSize = 450 * Im.Style.GlobalScale; + Dirty &= ~IManagedCache.DirtyFlags.Font; + } + + base.Update(); + } + + public Cache(CollectionManager collectionManager, CommunicatorService communicator, IFilter filter) + : base(filter) + { + _collectionManager = collectionManager; + _communicator = communicator; + + _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.EffectiveChangesCache); + _communicator.ResolvedFileChanged.Subscribe(OnResolvedFileChange, ResolvedFileChanged.Priority.EffectiveChangesCache); + _communicator.ResolvedMetaChanged.Subscribe(OnResolvedMetaChange, ResolvedMetaChanged.Priority.EffectiveChangesCache); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _communicator.CollectionChange.Unsubscribe(OnCollectionChange); + _communicator.ResolvedFileChanged.Unsubscribe(OnResolvedFileChange); + _communicator.ResolvedMetaChanged.Unsubscribe(OnResolvedMetaChange); + } + + private void OnResolvedFileChange(in ResolvedFileChanged.Arguments arguments) + => Dirty |= IManagedCache.DirtyFlags.Custom; + + private void OnResolvedMetaChange(in ResolvedMetaChanged.Arguments arguments) + => Dirty |= IManagedCache.DirtyFlags.Custom; + + private void OnCollectionChange(in CollectionChange.Arguments arguments) + { + if (arguments.Type is CollectionType.Current) + Dirty |= IManagedCache.DirtyFlags.Custom; + } + + protected override IEnumerable GetItems() + => _collectionManager.Active.Current.Cache is null + ? [] + : _collectionManager.Active.Current.Cache.ResolvedFiles.Select(f => new Item(f.Value.Mod, f.Key.Path.Span, f.Value.Path)) + .OrderBy(i => i.GamePath.Utf16) + .Concat(_collectionManager.Active.Current.Cache.Meta.IdentifierSources.Select(s => new Item(s.Item2, s.Item1)) + .OrderBy(i => i.GamePath.Utf16)); + + public ReadOnlySpan Id + => "EC"u8; + + public void Draw() + { + DrawFilters(); + DrawTable(); + } + + private void DrawFilters() + { + using var style = ImStyleSingle.FrameRounding.Push(0).PushX(ImStyleDouble.ItemSpacing, 0); + + Filter.Filter1.DrawFilter("Filter game path..."u8, new Vector2(_gamePathSize + Im.Style.CellPadding.X, Im.Style.FrameHeight)); + Im.Line.Same(0, _arrowSize + 2 * Im.Style.CellPadding.X); + Filter.Filter2.DrawFilter("Filter file path..."u8, Im.ContentRegion.Available with { Y = Im.Style.FrameHeight }); + + } + + private void DrawTable() + { + using var table = Im.Table.Begin("t"u8, 3, TableFlags.RowBackground | TableFlags.ScrollY, Im.ContentRegion.Available); + if (!table) + return; + + table.SetupColumn("gp"u8, TableColumnFlags.WidthFixed, _gamePathSize); + table.SetupColumn("a"u8, TableColumnFlags.WidthFixed, _arrowSize); + table.SetupColumn("fp"u8, TableColumnFlags.WidthStretch); + + using var clipper = new Im.ListClipper(Count, Im.Style.TextHeightWithSpacing); + foreach (var item in clipper.Iterate(this)) + { + table.NextColumn(); + ImEx.CopyOnClickSelectable(item.GamePath.Utf8); + + table.NextColumn(); + ImEx.Icon.Draw(Arrow); + + table.NextColumn(); + ImEx.CopyOnClickSelectable(item.FilePath.InternalName.Span); + if (!item.IsMeta) + Im.Tooltip.OnHover($"\nChanged by {item.Mod.Name}."); + } + } + } + + private sealed class GamePathFilter : RegexFilterBase + { + protected override string ToFilterString(in Item item, int globalIndex) + => item.GamePath.Utf16; + } + + private sealed class FullPathFilter : RegexFilterBase + { + protected override string ToFilterString(in Item item, int globalIndex) + => item.FilePath.FullName; + } + + private sealed class Item + { + public IMod Mod; + public StringPair GamePath; + public FullPath FilePath; + public bool IsMeta; + + public Item(IMod mod, ReadOnlySpan gamePath, FullPath filePath) + { + Mod = mod; + GamePath = new StringPair(gamePath); + FilePath = filePath; + IsMeta = false; + } + + public Item(IMod mod, IMetaIdentifier identifier) + { + Mod = mod; + GamePath = new StringPair($"{identifier}"); + FilePath = new FullPath(mod.Name); + IsMeta = true; + } + } +} diff --git a/Penumbra/UI/Tabs/MainTabBar.cs b/Penumbra/UI/MainWindow/MainTabBar.cs similarity index 86% rename from Penumbra/UI/Tabs/MainTabBar.cs rename to Penumbra/UI/MainWindow/MainTabBar.cs index 60526d34..1bf73669 100644 --- a/Penumbra/UI/Tabs/MainTabBar.cs +++ b/Penumbra/UI/MainWindow/MainTabBar.cs @@ -1,21 +1,24 @@ using Luna; using Penumbra.Api.Enums; using Penumbra.Communication; +using Penumbra.Mods.Manager; using Penumbra.Services; +using Penumbra.UI.Tabs; using Penumbra.UI.Tabs.Debug; using Watcher = Penumbra.UI.ResourceWatcher.ResourceWatcher; -namespace Penumbra.UI.Tabs; +namespace Penumbra.UI.MainWindow; public sealed class MainTabBar : TabBar, IDisposable { - public readonly ModsTab Mods; + public readonly Tabs.ModsTab Mods; private readonly EphemeralConfig _config; private readonly SelectTab _selectTab; public MainTabBar(Logger log, SettingsTab settings, - ModsTab mods, + Tabs.ModsTab mods, + ModTab mods2, CollectionsTab collections, ChangedItemsTab changedItems, EffectiveTab effectiveChanges, @@ -24,7 +27,7 @@ public sealed class MainTabBar : TabBar, IDisposable Watcher watcher, OnScreenTab onScreen, MessagesTab messages, EphemeralConfig config, CommunicatorService communicator) - : base(nameof(MainTabBar), log, settings, collections, mods, changedItems, effectiveChanges, onScreen, + : base(nameof(MainTabBar), log, settings, collections, mods, mods2, changedItems, effectiveChanges, onScreen, resources, watcher, debug, messages) { Mods = mods; diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/MainWindow/MainWindow.cs similarity index 97% rename from Penumbra/UI/ConfigWindow.cs rename to Penumbra/UI/MainWindow/MainWindow.cs index f054a09b..7182d484 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/MainWindow/MainWindow.cs @@ -6,9 +6,9 @@ using Penumbra.UI.Classes; using Penumbra.UI.Tabs; using TabType = Penumbra.Api.Enums.TabType; -namespace Penumbra.UI; +namespace Penumbra.UI.MainWindow; -public sealed class ConfigWindow : Window +public sealed class MainWindow : Window { private readonly IDalamudPluginInterface _pluginInterface; private readonly Configuration _config; @@ -17,7 +17,7 @@ public sealed class ConfigWindow : Window private MainTabBar _configTabs = null!; private string? _lastException; - public ConfigWindow(IDalamudPluginInterface pi, Configuration config, ValidityChecker checker, + public MainWindow(IDalamudPluginInterface pi, Configuration config, ValidityChecker checker, TutorialService tutorial) : base(GetLabel(checker)) { diff --git a/Penumbra/UI/Tabs/MessagesTab.cs b/Penumbra/UI/MainWindow/MessagesTab.cs similarity index 100% rename from Penumbra/UI/Tabs/MessagesTab.cs rename to Penumbra/UI/MainWindow/MessagesTab.cs diff --git a/Penumbra/UI/Tabs/OnScreenTab.cs b/Penumbra/UI/MainWindow/OnScreenTab.cs similarity index 93% rename from Penumbra/UI/Tabs/OnScreenTab.cs rename to Penumbra/UI/MainWindow/OnScreenTab.cs index c39eaba8..2d1f4c62 100644 --- a/Penumbra/UI/Tabs/OnScreenTab.cs +++ b/Penumbra/UI/MainWindow/OnScreenTab.cs @@ -2,7 +2,7 @@ using Luna; using Penumbra.Api.Enums; using Penumbra.UI.AdvancedWindow; -namespace Penumbra.UI.Tabs; +namespace Penumbra.UI.MainWindow; public sealed class OnScreenTab(ResourceTreeViewerFactory resourceTreeViewerFactory) : ITab { diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs index ec0746ab..c8545c10 100644 --- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs @@ -1,6 +1,4 @@ -using Dalamud.Bindings.ImGui; using ImSharp; -using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -34,7 +32,7 @@ public class AddGroupDrawer : Luna.IUiService public void Draw(Mod mod, float width) { - var buttonWidth = new Vector2((width - ImUtf8.ItemInnerSpacing.X) / 2, 0); + var buttonWidth = new Vector2((width - Im.Style.ItemInnerSpacing.X) / 2, 0); DrawBasicGroups(mod, width, buttonWidth); DrawImcData(mod, buttonWidth); } @@ -42,7 +40,7 @@ public class AddGroupDrawer : Luna.IUiService private void DrawBasicGroups(Mod mod, float width, Vector2 buttonWidth) { Im.Item.SetNextWidth(width); - if (ImUtf8.InputText("##name"u8, ref _groupName, "Enter New Name..."u8)) + if (Im.Input.Text("##name"u8, ref _groupName, "Enter New Name..."u8)) _groupNameValid = ModGroupEditor.VerifyFileName(mod, null, _groupName, false); DrawSingleGroupButton(mod, buttonWidth); @@ -53,10 +51,9 @@ public class AddGroupDrawer : Luna.IUiService private void DrawSingleGroupButton(Mod mod, Vector2 width) { - if (!ImUtf8.ButtonEx("Add Single Group"u8, _groupNameValid - ? "Add a new single selection option group to this mod."u8 - : "Can not add a new group of this name."u8, - width, !_groupNameValid)) + if (!ImEx.Button("Add Single Group"u8, width, _groupNameValid + ? "Add a new single selection option group to this mod."u8 + : "Can not add a new group of this name."u8, !_groupNameValid)) return; _modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _groupName); @@ -66,10 +63,9 @@ public class AddGroupDrawer : Luna.IUiService private void DrawMultiGroupButton(Mod mod, Vector2 width) { - if (!ImUtf8.ButtonEx("Add Multi Group"u8, _groupNameValid - ? "Add a new multi selection option group to this mod."u8 - : "Can not add a new group of this name."u8, - width, !_groupNameValid)) + if (!ImEx.Button("Add Multi Group"u8, width, _groupNameValid + ? "Add a new multi selection option group to this mod."u8 + : "Can not add a new group of this name."u8, !_groupNameValid)) return; _modManager.OptionEditor.AddModGroup(mod, GroupType.Multi, _groupName); @@ -79,10 +75,9 @@ public class AddGroupDrawer : Luna.IUiService private void DrawCombiningGroupButton(Mod mod, Vector2 width) { - if (!ImUtf8.ButtonEx("Add Combining Group"u8, _groupNameValid - ? "Add a new combining option group to this mod."u8 - : "Can not add a new group of this name."u8, - width, !_groupNameValid)) + if (!ImEx.Button("Add Combining Group"u8, width, _groupNameValid + ? "Add a new combining option group to this mod."u8 + : "Can not add a new group of this name."u8, !_groupNameValid)) return; _modManager.OptionEditor.AddModGroup(mod, GroupType.Combining, _groupName); @@ -103,7 +98,7 @@ public class AddGroupDrawer : Luna.IUiService } else if (_imcIdentifier.ObjectType is ObjectType.DemiHuman) { - var quarterWidth = (width - ImUtf8.ItemInnerSpacing.X / Im.Style.GlobalScale) / 2; + var quarterWidth = (width - Im.Style.ItemInnerSpacing.X / Im.Style.GlobalScale) / 2; change |= ImcMetaDrawer.DrawSecondaryId(ref _imcIdentifier, width); Im.Line.SameInner(); change |= ImcMetaDrawer.DrawSlot(ref _imcIdentifier, quarterWidth); @@ -130,12 +125,11 @@ public class AddGroupDrawer : Luna.IUiService private void DrawImcButton(Mod mod, Vector2 width) { - if (ImUtf8.ButtonEx("Add IMC Group"u8, !_groupNameValid - ? "Can not add a new group of this name."u8 - : _entryInvalid - ? "The associated IMC entry is invalid."u8 - : "Add a new multi selection option group to this mod."u8, - width, !_groupNameValid || _entryInvalid)) + if (ImEx.Button("Add IMC Group"u8, width, !_groupNameValid + ? "Can not add a new group of this name."u8 + : _entryInvalid + ? "The associated IMC entry is invalid."u8 + : "Add a new multi selection option group to this mod."u8, !_groupNameValid || _entryInvalid)) { _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcIdentifier, _defaultEntry); _groupName = string.Empty; @@ -148,7 +142,7 @@ public class AddGroupDrawer : Luna.IUiService var text = _imcFileExists ? "IMC Entry Does Not Exist"u8 : "IMC File Does Not Exist"u8; - ImUtf8.TextFramed(text, Colors.PressEnterWarningBg, width); + ImEx.TextFramed(text, width, Colors.PressEnterWarningBg); } } diff --git a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs index b9e64f70..a6761afa 100644 --- a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs @@ -1,112 +1,95 @@ -using Dalamud.Interface; -using Dalamud.Bindings.ImGui; -using ImSharp; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Text; -using Penumbra.Mods.Groups; -using Penumbra.Mods.SubMods; - -namespace Penumbra.UI.ModsTab.Groups; - -public readonly struct CombiningModGroupEditDrawer(ModGroupEditDrawer editor, CombiningModGroup group) : IModGroupEditDrawer -{ - public void Draw() - { - foreach (var (optionIdx, option) in group.OptionData.Index()) - { - using var id = ImUtf8.PushId(optionIdx); - editor.DrawOptionPosition(group, option, optionIdx); - - Im.Line.SameInner(); - editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx); - - Im.Line.SameInner(); - editor.DrawOptionName(option); - - Im.Line.SameInner(); - editor.DrawOptionDescription(option); - - Im.Line.SameInner(); - editor.DrawOptionDelete(option); - } - - DrawNewOption(); - DrawContainerNames(); - } - - private void DrawNewOption() - { - var count = group.OptionData.Count; - if (count >= IModGroup.MaxCombiningOptions) - return; - - var name = editor.DrawNewOptionBase(group, count); - - var validName = name.Length > 0; - if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName - ? "Add a new option to this group."u8 - : "Please enter a name for the new option."u8, default, !validName)) - { - editor.ModManager.OptionEditor.CombiningEditor.AddOption(group, name); - editor.NewOptionName = null; - } - } - - private unsafe void DrawContainerNames() - { - if (ImUtf8.ButtonEx("Edit Container Names"u8, - "Add optional names to separate data containers of the combining group.\nThose are just for easier identification while editing the mod, and are not generally displayed to the user."u8, - new Vector2(400 * Im.Style.GlobalScale, 0))) - ImUtf8.OpenPopup("DataContainerNames"u8); - - var sizeX = group.OptionData.Count * (Im.Style.ItemInnerSpacing.X + Im.Style.FrameHeight) + 300 * Im.Style.GlobalScale; - ImGui.SetNextWindowSize(new Vector2(sizeX, Im.Style.FrameHeightWithSpacing * Math.Min(16, group.Data.Count) + 200 * Im.Style.GlobalScale)); - using var popup = ImUtf8.Popup("DataContainerNames"u8); - if (!popup) - return; - - foreach (var option in group.OptionData) - { - ImUtf8.RotatedText(option.Name, true); - Im.Line.SameInner(); - } - - Im.Line.New(); - Im.Separator(); - using var child = ImUtf8.Child("##Child"u8, Im.ContentRegion.Available); - ImGuiClip.ClippedDraw(group.Data, DrawRow, Im.Style.FrameHeightWithSpacing); - } - - private void DrawRow(CombinedDataContainer container, int index) - { - using var id = ImUtf8.PushId(index); - using (ImRaii.Disabled()) - { - for (var i = 0; i < group.OptionData.Count; ++i) - { - id.Push(i); - var check = (index & (1 << i)) != 0; - ImUtf8.Checkbox(""u8, ref check); - Im.Line.SameInner(); - id.Pop(); - } - } - - var name = editor.CombiningDisplayIndex == index ? editor.CombiningDisplayName ?? container.Name : container.Name; - if (ImUtf8.InputText("##Nothing"u8, ref name, "Optional Display Name..."u8)) - { - editor.CombiningDisplayIndex = index; - editor.CombiningDisplayName = name; - } - - if (ImGui.IsItemDeactivatedAfterEdit()) - editor.ModManager.OptionEditor.CombiningEditor.SetDisplayName(container, name); - - if (ImGui.IsItemDeactivated()) - { - editor.CombiningDisplayIndex = -1; - editor.CombiningDisplayName = null; - } - } -} +using ImSharp; +using Luna; +using Penumbra.Mods.Groups; +using Penumbra.Mods.SubMods; + +namespace Penumbra.UI.ModsTab.Groups; + +public readonly struct CombiningModGroupEditDrawer(ModGroupEditDrawer editor, CombiningModGroup group) : IModGroupEditDrawer +{ + public void Draw() + { + foreach (var (optionIdx, option) in group.OptionData.Index()) + { + using var id = Im.Id.Push(optionIdx); + editor.DrawOptionPosition(group, option, optionIdx); + + Im.Line.SameInner(); + editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx); + + Im.Line.SameInner(); + editor.DrawOptionName(option); + + Im.Line.SameInner(); + editor.DrawOptionDescription(option); + + Im.Line.SameInner(); + editor.DrawOptionDelete(option); + } + + DrawNewOption(); + DrawContainerNames(); + } + + private void DrawNewOption() + { + var count = group.OptionData.Count; + if (count >= IModGroup.MaxCombiningOptions) + return; + + var name = editor.DrawNewOptionBase(group, count); + + var validName = name.Length > 0; + if (ImEx.Icon.Button(LunaStyle.AddObjectIcon, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, !validName)) + { + editor.ModManager.OptionEditor.CombiningEditor.AddOption(group, name); + editor.NewOptionName = null; + } + } + + private void DrawContainerNames() + { + if (ImEx.Button("Edit Container Names"u8, new Vector2(400 * Im.Style.GlobalScale, 0), + "Add optional names to separate data containers of the combining group.\nThose are just for easier identification while editing the mod, and are not generally displayed to the user."u8)) + Im.Popup.Open("names"u8); + + var sizeX = group.OptionData.Count * (Im.Style.ItemInnerSpacing.X + Im.Style.FrameHeight) + 300 * Im.Style.GlobalScale; + Im.Window.SetNextSize(new Vector2(sizeX, + Im.Style.FrameHeightWithSpacing * Math.Min(16, group.Data.Count) + 200 * Im.Style.GlobalScale)); + using var popup = Im.Popup.Begin("names"u8); + if (!popup) + return; + + foreach (var option in group.OptionData) + { + ImEx.RotatedText(option.Name, true); + Im.Line.SameInner(); + } + + Im.Line.New(); + Im.Separator(); + using var child = Im.Child.Begin("##Child"u8, Im.ContentRegion.Available); + Im.ListClipper.Draw(group.Data, DrawRow, Im.Style.FrameHeightWithSpacing); + } + + private void DrawRow(CombinedDataContainer container, int index) + { + using var id = Im.Id.Push(index); + using (Im.Disabled()) + { + for (var i = 0; i < group.OptionData.Count; ++i) + { + id.Push(i); + var check = (index & (1 << i)) != 0; + Im.Checkbox(""u8, ref check); + Im.Line.SameInner(); + id.Pop(); + } + } + + if (ImEx.InputOnDeactivation.Text("##Nothing"u8, container.Name, out string newName, "Optional Display Name..."u8)) + editor.ModManager.OptionEditor.CombiningEditor.SetDisplayName(container, newName); + } +} diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs index f491e7bc..d53410b1 100644 --- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs @@ -1,187 +1,189 @@ -using Dalamud.Interface; -using Dalamud.Bindings.ImGui; -using ImSharp; -using OtterGui.Raii; -using OtterGui.Text; -using OtterGuiInternal.Utility; -using Penumbra.GameData.Structs; -using Penumbra.Meta; -using Penumbra.Mods.Groups; -using Penumbra.Mods.Manager.OptionEditor; -using Penumbra.Mods.SubMods; -using Penumbra.UI.AdvancedWindow.Meta; - -namespace Penumbra.UI.ModsTab.Groups; - -public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGroup group) : IModGroupEditDrawer -{ - public void Draw() - { - var identifier = group.Identifier; - var defaultEntry = ImcChecker.GetDefaultEntry(identifier, true).Entry; - var entry = group.DefaultEntry; - var changes = false; - - var width = editor.AvailableWidth.X - 3 * ImUtf8.ItemInnerSpacing.X - Im.Style.ItemSpacing.X - ImUtf8.CalcTextSize("All Variants"u8).X - ImUtf8.CalcTextSize("Only Attributes"u8).X - 2 * ImUtf8.FrameHeight; - ImEx.TextFramed(identifier.ToString(), new Vector2(width, 0), Rgba32.Transparent); - - Im.Line.SameInner(); - var allVariants = group.AllVariants; - if (ImUtf8.Checkbox("All Variants"u8, ref allVariants)) - editor.ModManager.OptionEditor.ImcEditor.ChangeAllVariants(group, allVariants); - Im.Tooltip.OnHover("Make this group overwrite all corresponding variants for this identifier, not just the one specified."u8); - - Im.Line.Same(); - var onlyAttributes = group.OnlyAttributes; - if (ImUtf8.Checkbox("Only Attributes"u8, ref onlyAttributes)) - editor.ModManager.OptionEditor.ImcEditor.ChangeOnlyAttributes(group, onlyAttributes); - Im.Tooltip.OnHover("Only overwrite the attribute flags and take all the other values from the game's default entry instead of the one configured here.\n\nMainly useful if used with All Variants to keep the material IDs for each variant."u8); - - using (ImUtf8.Group()) - { - ImUtf8.TextFrameAligned("Material ID"u8); - ImUtf8.TextFrameAligned("VFX ID"u8); - ImUtf8.TextFrameAligned("Decal ID"u8); - } - - Im.Line.Same(); - using (ImUtf8.Group()) - { - changes |= ImcMetaDrawer.DrawMaterialId(defaultEntry, ref entry, true); - changes |= ImcMetaDrawer.DrawVfxId(defaultEntry, ref entry, true); - changes |= ImcMetaDrawer.DrawDecalId(defaultEntry, ref entry, true); - } - - Im.Line.Same(0, editor.PriorityWidth); - using (ImUtf8.Group()) - { - ImUtf8.TextFrameAligned("Material Animation ID"u8); - ImUtf8.TextFrameAligned("Sound ID"u8); - ImUtf8.TextFrameAligned("Can Be Disabled"u8); - } - - Im.Line.Same(); - - using (ImUtf8.Group()) - { - changes |= ImcMetaDrawer.DrawMaterialAnimationId(defaultEntry, ref entry, true); - changes |= ImcMetaDrawer.DrawSoundId(defaultEntry, ref entry, true); - var canBeDisabled = group.CanBeDisabled; - if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) - editor.ModManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled); - } - - if (changes) - editor.ModManager.OptionEditor.ImcEditor.ChangeDefaultEntry(group, entry); - - ImGui.Dummy(Vector2.Zero); - DrawOptions(); - var attributeCache = new ImcAttributeCache(group); - DrawNewOption(attributeCache); - ImGui.Dummy(Vector2.Zero); - - - using (ImUtf8.Group()) - { - ImUtf8.TextFrameAligned("Default Attributes"u8); - foreach (var option in group.OptionData.Where(o => !o.IsDisableSubMod)) - ImUtf8.TextFrameAligned(option.Name); - } - - Im.Line.SameInner(); - using (ImUtf8.Group()) - { - DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); - foreach (var (idx, option) in group.OptionData.Index().Where(o => !o.Item.IsDisableSubMod)) - { - using var id = ImUtf8.PushId(idx); - DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option, - group.DefaultEntry.AttributeMask); - } - } - } - - private void DrawOptions() - { - foreach (var (optionIdx, option) in group.OptionData.Index()) - { - using var id = ImRaii.PushId(optionIdx); - editor.DrawOptionPosition(group, option, optionIdx); - - Im.Line.SameInner(); - editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx); - - Im.Line.SameInner(); - editor.DrawOptionName(option); - - Im.Line.SameInner(); - editor.DrawOptionDescription(option); - - if (!option.IsDisableSubMod) - { - Im.Line.SameInner(); - editor.DrawOptionDelete(option); - } - } - } - - private void DrawNewOption(in ImcAttributeCache cache) - { - var dis = cache.LowestUnsetMask == 0; - var name = editor.DrawNewOptionBase(group, group.Options.Count); - var validName = name.Length > 0; - var tt = dis - ? "No Free Attribute Slots for New Options..."u8 - : validName - ? "Add a new option to this group."u8 - : "Please enter a name for the new option."u8; - if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, default, !validName || dis)) - { - editor.ModManager.OptionEditor.ImcEditor.AddOption(group, cache, name); - editor.NewOptionName = null; - } - } - - private static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data, - ushort? defaultMask = null) - { - for (var i = 0; i < ImcEntry.NumAttributes; ++i) - { - using var id = ImRaii.PushId(i); - var flag = 1 << i; - var value = (mask & flag) != 0; - var inDefault = defaultMask.HasValue && (defaultMask & flag) != 0; - using (ImRaii.Disabled(defaultMask != null && !cache.CanChange(i))) - { - if (inDefault ? NegativeCheckbox.Instance.Draw(""u8, ref value) : ImUtf8.Checkbox(""u8, ref value)) - { - if (data is ImcModGroup g) - editor.ChangeDefaultAttribute(g, cache, i, value); - else - editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value); - } - } - - Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "ABCDEFGHIJ"u8.Slice(i, 1)); - if (i != 9) - Im.Line.SameInner(); - } - } - - private sealed class NegativeCheckbox : OtterGui.Text.Widget.MultiStateCheckbox - { - public static readonly NegativeCheckbox Instance = new(); - - protected override void RenderSymbol(bool value, Vector2 position, float size) - { - if (value) - SymbolHelpers.RenderCross(ImGui.GetWindowDrawList(), position, ImGuiColor.CheckMark.Get().Color, size); - } - - protected override bool NextValue(bool value) - => !value; - - protected override bool PreviousValue(bool value) - => !value; - } -} +using ImSharp; +using Luna; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.Mods.SubMods; +using Penumbra.UI.AdvancedWindow.Meta; + +namespace Penumbra.UI.ModsTab.Groups; + +public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGroup group) : IModGroupEditDrawer +{ + public void Draw() + { + var identifier = group.Identifier; + var defaultEntry = ImcChecker.GetDefaultEntry(identifier, true).Entry; + var entry = group.DefaultEntry; + var changes = false; + + var width = editor.AvailableWidth.X + - 3 * Im.Style.ItemInnerSpacing.X + - Im.Style.ItemSpacing.X + - Im.Font.CalculateSize("All Variants"u8).X + - Im.Font.CalculateSize("Only Attributes"u8).X + - 2 * Im.Style.FrameHeight; + ImEx.TextFramed(identifier.ToString(), new Vector2(width, 0), Rgba32.Transparent); + + Im.Line.SameInner(); + var allVariants = group.AllVariants; + if (Im.Checkbox("All Variants"u8, ref allVariants)) + editor.ModManager.OptionEditor.ImcEditor.ChangeAllVariants(group, allVariants); + Im.Tooltip.OnHover("Make this group overwrite all corresponding variants for this identifier, not just the one specified."u8); + + Im.Line.Same(); + var onlyAttributes = group.OnlyAttributes; + if (Im.Checkbox("Only Attributes"u8, ref onlyAttributes)) + editor.ModManager.OptionEditor.ImcEditor.ChangeOnlyAttributes(group, onlyAttributes); + Im.Tooltip.OnHover( + "Only overwrite the attribute flags and take all the other values from the game's default entry instead of the one configured here.\n\nMainly useful if used with All Variants to keep the material IDs for each variant."u8); + + using (Im.Group()) + { + ImEx.TextFrameAligned("Material ID"u8); + ImEx.TextFrameAligned("VFX ID"u8); + ImEx.TextFrameAligned("Decal ID"u8); + } + + Im.Line.Same(); + using (Im.Group()) + { + changes |= ImcMetaDrawer.DrawMaterialId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawVfxId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawDecalId(defaultEntry, ref entry, true); + } + + Im.Line.Same(0, editor.PriorityWidth); + using (Im.Group()) + { + ImEx.TextFrameAligned("Material Animation ID"u8); + ImEx.TextFrameAligned("Sound ID"u8); + ImEx.TextFrameAligned("Can Be Disabled"u8); + } + + Im.Line.Same(); + + using (Im.Group()) + { + changes |= ImcMetaDrawer.DrawMaterialAnimationId(defaultEntry, ref entry, true); + changes |= ImcMetaDrawer.DrawSoundId(defaultEntry, ref entry, true); + var canBeDisabled = group.CanBeDisabled; + if (Im.Checkbox("##disabled"u8, ref canBeDisabled)) + editor.ModManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled); + } + + if (changes) + editor.ModManager.OptionEditor.ImcEditor.ChangeDefaultEntry(group, entry); + + Im.Dummy(Vector2.Zero); + DrawOptions(); + var attributeCache = new ImcAttributeCache(group); + DrawNewOption(attributeCache); + Im.Dummy(Vector2.Zero); + + + using (Im.Group()) + { + ImEx.TextFrameAligned("Default Attributes"u8); + foreach (var option in group.OptionData.Where(o => !o.IsDisableSubMod)) + ImEx.TextFrameAligned(option.Name); + } + + Im.Line.SameInner(); + using (Im.Group()) + { + DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); + foreach (var (idx, option) in group.OptionData.Index().Where(o => !o.Item.IsDisableSubMod)) + { + using var id = Im.Id.Push(idx); + DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option, + group.DefaultEntry.AttributeMask); + } + } + } + + private void DrawOptions() + { + foreach (var (optionIdx, option) in group.OptionData.Index()) + { + using var id = Im.Id.Push(optionIdx); + editor.DrawOptionPosition(group, option, optionIdx); + + Im.Line.SameInner(); + editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx); + + Im.Line.SameInner(); + editor.DrawOptionName(option); + + Im.Line.SameInner(); + editor.DrawOptionDescription(option); + + if (!option.IsDisableSubMod) + { + Im.Line.SameInner(); + editor.DrawOptionDelete(option); + } + } + } + + private void DrawNewOption(in ImcAttributeCache cache) + { + var dis = cache.LowestUnsetMask is 0; + var name = editor.DrawNewOptionBase(group, group.Options.Count); + var validName = name.Length > 0; + var tt = dis + ? "No Free Attribute Slots for New Options..."u8 + : validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8; + if (ImEx.Icon.Button(LunaStyle.AddObjectIcon, tt, !validName || dis)) + { + editor.ModManager.OptionEditor.ImcEditor.AddOption(group, cache, name); + editor.NewOptionName = null; + } + } + + private static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data, + ushort? defaultMask = null) + { + for (var i = 0; i < ImcEntry.NumAttributes; ++i) + { + using var id = Im.Id.Push(i); + var flag = 1 << i; + var value = (mask & flag) is not 0; + var inDefault = defaultMask.HasValue && (defaultMask & flag) is not 0; + using (Im.Disabled(defaultMask is not null && !cache.CanChange(i))) + { + if (inDefault ? NegativeCheckbox.Instance.Draw(""u8, ref value) : Im.Checkbox(""u8, ref value)) + { + if (data is ImcModGroup g) + editor.ChangeDefaultAttribute(g, cache, i, value); + else + editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value); + } + } + + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "ABCDEFGHIJ"u8.Slice(i, 1)); + if (i != 9) + Im.Line.SameInner(); + } + } + + private sealed class NegativeCheckbox : OtterGui.Text.Widget.MultiStateCheckbox + { + public static readonly NegativeCheckbox Instance = new(); + + protected override void RenderSymbol(bool value, Vector2 position, float size) + { + if (value) + Im.Render.Cross(Im.Window.DrawList, position, ImGuiColor.CheckMark.Get(), size); + } + + protected override bool NextValue(bool value) + => !value; + + protected override bool PreviousValue(bool value) + => !value; + } +} diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs index 1aa58872..5ceb658e 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs @@ -1,370 +1,343 @@ -using Dalamud.Interface; -using Dalamud.Interface.ImGuiNotification; -using Dalamud.Bindings.ImGui; -using ImSharp; -using Luna; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Text; -using OtterGui.Text.EndObjects; -using Penumbra.Meta; -using Penumbra.Mods; -using Penumbra.Mods.Groups; -using Penumbra.Mods.Manager; -using Penumbra.Mods.Manager.OptionEditor; -using Penumbra.Mods.Settings; -using Penumbra.Mods.SubMods; -using Penumbra.Services; -using Penumbra.UI.Classes; - -namespace Penumbra.UI.ModsTab.Groups; - -public sealed class ModGroupEditDrawer( - ModManager modManager, - Configuration config, - FilenameService filenames, - DescriptionEditPopup descriptionPopup, - ImcChecker imcChecker) : IUiService -{ - private static ReadOnlySpan AcrossGroupsLabel - => "##DragOptionAcross"u8; - - private static ReadOnlySpan InsideGroupLabel - => "##DragOptionInside"u8; - - internal readonly ImcChecker ImcChecker = imcChecker; - internal readonly ModManager ModManager = modManager; - internal readonly Queue ActionQueue = new(); - - internal Vector2 OptionIdxSelectable; - internal Vector2 AvailableWidth; - internal float PriorityWidth; - - internal string? NewOptionName; - private IModGroup? _newOptionGroup; - - private Vector2 _buttonSize; - private float _groupNameWidth; - private float _optionNameWidth; - private float _spacing; - private bool _deleteEnabled; - - private string? _currentGroupName; - private ModPriority? _currentGroupPriority; - private IModGroup? _currentGroupEdited; - private bool _isGroupNameValid = true; - - private IModGroup? _dragDropGroup; - private IModOption? _dragDropOption; - private bool _draggingAcross; - - internal string? CombiningDisplayName; - internal int CombiningDisplayIndex; - - public void Draw(Mod mod) - { - PrepareStyle(); - - using var id = ImUtf8.PushId("##GroupEdit"u8); - foreach (var (groupIdx, group) in mod.Groups.Index()) - DrawGroup(group, groupIdx); - - while (ActionQueue.TryDequeue(out var action)) - action.Invoke(); - } - - private void DrawGroup(IModGroup group, int idx) - { - using var id = ImUtf8.PushId(idx); - using var frame = ImRaii.FramedGroup($"Group #{idx + 1}"); - DrawGroupNameRow(group, idx); - group.EditDrawer(this).Draw(); - } - - private void DrawGroupNameRow(IModGroup group, int idx) - { - DrawGroupName(group); - Im.Line.SameInner(); - DrawGroupMoveButtons(group, idx); - Im.Line.SameInner(); - DrawGroupOpenFile(group, idx); - Im.Line.SameInner(); - DrawGroupDescription(group); - Im.Line.SameInner(); - DrawGroupDelete(group); - Im.Line.SameInner(); - DrawGroupPriority(group); - } - - private void DrawGroupName(IModGroup group) - { - var text = _currentGroupEdited == group ? _currentGroupName ?? group.Name : group.Name; - Im.Item.SetNextWidth(_groupNameWidth); - using var border = ImRaii.PushFrameBorder(Im.Style.GlobalScale * 2, Colors.RegexWarningBorder, !_isGroupNameValid); - if (ImUtf8.InputText("##GroupName"u8, ref text)) - { - _currentGroupEdited = group; - _currentGroupName = text; - _isGroupNameValid = text == group.Name || ModGroupEditor.VerifyFileName(group.Mod, group, text, false); - } - - if (ImGui.IsItemDeactivated()) - { - if (_currentGroupName != null && _isGroupNameValid) - ModManager.OptionEditor.RenameModGroup(group, _currentGroupName); - _currentGroupName = null; - _currentGroupEdited = null; - _isGroupNameValid = true; - } - - var tt = _isGroupNameValid - ? "Change the Group name."u8 - : "Current name can not be used for this group."u8; - Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, tt); - } - - private void DrawGroupDelete(IModGroup group) - { - if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) - ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteModGroup(group)); - - if (_deleteEnabled) - Im.Tooltip.OnHover("Delete this option group."u8); - else - Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, - $"Delete this option group.\nHold {config.DeleteModModifier} while clicking to delete."); - } - - private void DrawGroupPriority(IModGroup group) - { - var priority = _currentGroupEdited == group - ? (_currentGroupPriority ?? group.Priority).Value - : group.Priority.Value; - Im.Item.SetNextWidth(PriorityWidth); - if (ImGui.InputInt("##GroupPriority", ref priority)) - { - _currentGroupEdited = group; - _currentGroupPriority = new ModPriority(priority); - } - - if (ImGui.IsItemDeactivated()) - { - if (_currentGroupPriority.HasValue) - ModManager.OptionEditor.ChangeGroupPriority(group, _currentGroupPriority.Value); - _currentGroupEdited = null; - _currentGroupPriority = null; - } - - ImGuiUtil.HoverTooltip("Group Priority"); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawGroupDescription(IModGroup group) - { - if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit group description."u8)) - descriptionPopup.Open(group); - } - - private void DrawGroupMoveButtons(IModGroup group, int idx) - { - var isFirst = idx == 0; - if (ImUtf8.IconButton(FontAwesomeIcon.ArrowUp, isFirst)) - ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx - 1)); - - if (isFirst) - Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Can not move this group further upwards."u8); - else - Im.Tooltip.OnHover($"Move this group up to group {idx}."); - - - Im.Line.SameInner(); - var isLast = idx == group.Mod.Groups.Count - 1; - if (ImUtf8.IconButton(FontAwesomeIcon.ArrowDown, isLast)) - ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx + 1)); - - if (isLast) - Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Can not move this group further downwards."u8); - else - Im.Tooltip.OnHover($"Move this group down to group {idx + 2}."); - } - - private void DrawGroupOpenFile(IModGroup group, int idx) - { - var fileName = filenames.OptionGroupFile(group.Mod, idx, config.ReplaceNonAsciiOnImport); - var fileExists = File.Exists(fileName); - if (ImUtf8.IconButton(FontAwesomeIcon.FileExport, !fileExists)) - try - { - Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); - } - catch (Exception e) - { - Penumbra.Messager.NotificationMessage(e, "Could not open editor.", NotificationType.Error); - } - - if (fileExists) - Im.Tooltip.OnHover($"Open the {group.Name} json file in the text editor of your choice."); - else - Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"The {group.Name} json file does not exist."); - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx) - { - ImGui.AlignTextToFramePadding(); - ImUtf8.Selectable($"Option #{optionIdx + 1}", false, size: OptionIdxSelectable); - Target(group, optionIdx); - Source(option); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void DrawOptionDefaultSingleBehaviour(IModGroup group, IModOption option, int optionIdx) - { - var isDefaultOption = group.DefaultSettings.AsIndex == optionIdx; - if (ImUtf8.RadioButton("##default"u8, isDefaultOption)) - ModManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx)); - Im.Tooltip.OnHover($"Set {option.Name} as the default choice for this group."); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void DrawOptionDefaultMultiBehaviour(IModGroup group, IModOption option, int optionIdx) - { - var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); - if (ImUtf8.Checkbox("##default"u8, ref isDefaultOption)) - ModManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); - Im.Tooltip.OnHover($"{(isDefaultOption ? "Disable"u8 : "Enable"u8)} {option.Name} per default in this group."); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void DrawOptionDescription(IModOption option) - { - if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit option description."u8)) - descriptionPopup.Open(option); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void DrawOptionPriority(MultiSubMod option) - { - var priority = option.Priority.Value; - Im.Item.SetNextWidth(PriorityWidth); - if (ImUtf8.InputScalarOnDeactivated("##Priority"u8, ref priority)) - ModManager.OptionEditor.MultiEditor.ChangeOptionPriority(option, new ModPriority(priority)); - Im.Tooltip.OnHover("Option priority inside the mod."u8); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void DrawOptionName(IModOption option) - { - var name = option.Name; - Im.Item.SetNextWidth(_optionNameWidth); - if (ImUtf8.InputTextOnDeactivated("##Name"u8, ref name)) - ModManager.OptionEditor.RenameOption(option, name); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void DrawOptionDelete(IModOption option) - { - if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled)) - ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteOption(option)); - - if (_deleteEnabled) - Im.Tooltip.OnHover("Delete this option."u8); - else - Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, - $"Delete this option.\nHold {config.DeleteModModifier} while clicking to delete."); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal string DrawNewOptionBase(IModGroup group, int count) - { - ImGui.AlignTextToFramePadding(); - ImUtf8.Selectable($"Option #{count + 1}", false, size: OptionIdxSelectable); - Target(group, count); - - Im.Line.SameInner(); - ImUtf8.IconDummy(); - - Im.Line.SameInner(); - Im.Item.SetNextWidth(_optionNameWidth); - var newName = _newOptionGroup == group - ? NewOptionName ?? string.Empty - : string.Empty; - if (ImUtf8.InputText("##newOption"u8, ref newName, "Add new option..."u8)) - { - NewOptionName = newName; - _newOptionGroup = group; - } - - Im.Line.SameInner(); - return newName; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Source(IModOption option) - { - using var source = ImUtf8.DragDropSource(); - if (!source) - return; - - var across = option.Group is ITexToolsGroup; - - if (!DragDropSource.SetPayload(across ? AcrossGroupsLabel : InsideGroupLabel)) - { - _dragDropGroup = option.Group; - _dragDropOption = option; - _draggingAcross = across; - } - - ImUtf8.Text($"Dragging option {option.Name} from group {option.Group.Name}..."); - } - - private void Target(IModGroup group, int optionIdx) - { - if (_dragDropGroup != group - && (!_draggingAcross || (_dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }))) - return; - - using var target = ImUtf8.DragDropTarget(); - if (!target.IsDropping(_draggingAcross ? AcrossGroupsLabel : InsideGroupLabel)) - return; - - if (_dragDropGroup != null && _dragDropOption != null) - { - if (_dragDropGroup == group) - { - var sourceOption = _dragDropOption; - ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveOption(sourceOption, optionIdx)); - } - else - { - // Move from one group to another by deleting, then adding, then moving the option. - var sourceOption = _dragDropOption; - ActionQueue.Enqueue(() => - { - ModManager.OptionEditor.DeleteOption(sourceOption); - if (ModManager.OptionEditor.AddOption(group, sourceOption) is { } newOption) - ModManager.OptionEditor.MoveOption(newOption, optionIdx); - }); - } - } - - _dragDropGroup = null; - _dragDropOption = null; - _draggingAcross = false; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void PrepareStyle() - { - var totalWidth = 400f * Im.Style.GlobalScale; - _buttonSize = new Vector2(ImUtf8.FrameHeight); - PriorityWidth = 50 * Im.Style.GlobalScale; - AvailableWidth = new Vector2(totalWidth + 3 * _spacing + 2 * _buttonSize.X + PriorityWidth, 0); - _groupNameWidth = totalWidth - 3 * (_buttonSize.X + _spacing); - _spacing = Im.Style.ItemInnerSpacing.X; - OptionIdxSelectable = ImUtf8.CalcTextSize("Option #88."u8); - _optionNameWidth = totalWidth - OptionIdxSelectable.X - _buttonSize.X - 2 * _spacing; - _deleteEnabled = config.DeleteModModifier.IsActive(); - } -} +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; +using ImSharp; +using Luna; +using OtterGui.Raii; +using Penumbra.Meta; +using Penumbra.Mods; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModsTab.Groups; + +public sealed class ModGroupEditDrawer( + ModManager modManager, + Configuration config, + FilenameService filenames, + DescriptionEditPopup descriptionPopup, + ImcChecker imcChecker) : IUiService +{ + private static ReadOnlySpan AcrossGroupsLabel + => "##DragOptionAcross"u8; + + private static ReadOnlySpan InsideGroupLabel + => "##DragOptionInside"u8; + + internal readonly ImcChecker ImcChecker = imcChecker; + internal readonly ModManager ModManager = modManager; + internal readonly Queue ActionQueue = new(); + + internal Vector2 OptionIdxSelectable; + internal Vector2 AvailableWidth; + internal float PriorityWidth; + + internal string? NewOptionName; + private IModGroup? _newOptionGroup; + + private Vector2 _buttonSize; + private float _groupNameWidth; + private float _optionNameWidth; + private float _spacing; + private bool _deleteEnabled; + + private string? _currentGroupName; + private IModGroup? _currentGroupEdited; + private bool _isGroupNameValid = true; + + private IModGroup? _dragDropGroup; + private IModOption? _dragDropOption; + private bool _draggingAcross; + + public void Draw(Mod mod) + { + PrepareStyle(); + + using var id = Im.Id.Push("ge"u8); + foreach (var (groupIdx, group) in mod.Groups.Index()) + DrawGroup(group, groupIdx); + + while (ActionQueue.TryDequeue(out var action)) + action.Invoke(); + } + + private void DrawGroup(IModGroup group, int idx) + { + using var id = Im.Id.Push(idx); + using var frame = ImRaii.FramedGroup($"Group #{idx + 1}"); + DrawGroupNameRow(group, idx); + group.EditDrawer(this).Draw(); + } + + private void DrawGroupNameRow(IModGroup group, int idx) + { + DrawGroupName(group); + Im.Line.SameInner(); + DrawGroupMoveButtons(group, idx); + Im.Line.SameInner(); + DrawGroupOpenFile(group, idx); + Im.Line.SameInner(); + DrawGroupDescription(group); + Im.Line.SameInner(); + DrawGroupDelete(group); + Im.Line.SameInner(); + DrawGroupPriority(group); + } + + private void DrawGroupName(IModGroup group) + { + var text = _currentGroupEdited == group ? _currentGroupName ?? group.Name : group.Name; + Im.Item.SetNextWidth(_groupNameWidth); + using var border = ImStyleBorder.Frame.Push(Colors.RegexWarningBorder, Im.Style.GlobalScale * 2, !_isGroupNameValid); + if (Im.Input.Text("##GroupName"u8, ref text)) + { + _currentGroupEdited = group; + _currentGroupName = text; + _isGroupNameValid = text == group.Name || ModGroupEditor.VerifyFileName(group.Mod, group, text, false); + } + + if (Im.Item.Deactivated) + { + if (_currentGroupName != null && _isGroupNameValid) + ModManager.OptionEditor.RenameModGroup(group, _currentGroupName); + _currentGroupName = null; + _currentGroupEdited = null; + _isGroupNameValid = true; + } + + var tt = _isGroupNameValid + ? "Change the Group name."u8 + : "Current name can not be used for this group."u8; + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, tt); + } + + private void DrawGroupDelete(IModGroup group) + { + if (ImEx.Icon.Button(LunaStyle.DeleteIcon, !_deleteEnabled)) + ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteModGroup(group)); + + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Delete this option group."u8); + if (!_deleteEnabled) + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteModModifier} while clicking to delete."); + } + + private void DrawGroupPriority(IModGroup group) + { + Im.Item.SetNextWidth(PriorityWidth); + if (ImEx.InputOnDeactivation.Scalar("##GroupPriority"u8, group.Priority.Value, out var newPriority)) + ModManager.OptionEditor.ChangeGroupPriority(group, new ModPriority(newPriority)); + Im.Tooltip.OnHover("Group Priority"u8); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawGroupDescription(IModGroup group) + { + if (ImEx.Icon.Button(LunaStyle.EditIcon, "Edit group description."u8)) + descriptionPopup.Open(group); + } + + private void DrawGroupMoveButtons(IModGroup group, int idx) + { + var isFirst = idx is 0; + if (ImEx.Icon.Button(FontAwesomeIcon.ArrowUp.Icon(), isFirst)) + ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx - 1)); + + if (isFirst) + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Can not move this group further upwards."u8); + else + Im.Tooltip.OnHover($"Move this group up to group {idx}."); + + + Im.Line.SameInner(); + var isLast = idx == group.Mod.Groups.Count - 1; + if (ImEx.Icon.Button(FontAwesomeIcon.ArrowDown.Icon(), isLast)) + ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx + 1)); + + if (isLast) + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Can not move this group further downwards."u8); + else + Im.Tooltip.OnHover($"Move this group down to group {idx + 2}."); + } + + private void DrawGroupOpenFile(IModGroup group, int idx) + { + var fileName = filenames.OptionGroupFile(group.Mod, idx, config.ReplaceNonAsciiOnImport); + var fileExists = File.Exists(fileName); + if (ImEx.Icon.Button(LunaStyle.OpenExternalIcon, !fileExists)) + try + { + Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); + } + catch (Exception e) + { + Penumbra.Messager.NotificationMessage(e, "Could not open editor.", NotificationType.Error); + } + + if (fileExists) + Im.Tooltip.OnHover($"Open the {group.Name} json file in the text editor of your choice."); + else + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"The {group.Name} json file does not exist."); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx) + { + Im.Cursor.FrameAlign(); + Im.Selectable($"Option #{optionIdx + 1}", size: OptionIdxSelectable); + Target(group, optionIdx); + Source(option); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionDefaultSingleBehaviour(IModGroup group, IModOption option, int optionIdx) + { + var isDefaultOption = group.DefaultSettings.AsIndex == optionIdx; + if (Im.RadioButton("##default"u8, isDefaultOption)) + ModManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx)); + Im.Tooltip.OnHover($"Set {option.Name} as the default choice for this group."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionDefaultMultiBehaviour(IModGroup group, IModOption option, int optionIdx) + { + var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); + if (Im.Checkbox("##default"u8, ref isDefaultOption)) + ModManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); + Im.Tooltip.OnHover($"{(isDefaultOption ? "Disable"u8 : "Enable"u8)} {option.Name} per default in this group."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionDescription(IModOption option) + { + if (ImEx.Icon.Button(LunaStyle.EditIcon, "Edit option description."u8)) + descriptionPopup.Open(option); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionPriority(MultiSubMod option) + { + Im.Item.SetNextWidth(PriorityWidth); + if (ImEx.InputOnDeactivation.Scalar("##Priority"u8, option.Priority.Value, out var newValue)) + ModManager.OptionEditor.MultiEditor.ChangeOptionPriority(option, new ModPriority(newValue)); + Im.Tooltip.OnHover("Option priority inside the mod."u8); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionName(IModOption option) + { + Im.Item.SetNextWidth(_optionNameWidth); + if (ImEx.InputOnDeactivation.Text("##Name"u8, option.Name, out string newName)) + ModManager.OptionEditor.RenameOption(option, newName); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void DrawOptionDelete(IModOption option) + { + if (ImEx.Icon.Button(LunaStyle.DeleteIcon, !_deleteEnabled)) + ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteOption(option)); + + if (_deleteEnabled) + Im.Tooltip.OnHover("Delete this option."u8); + else + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, + $"Delete this option.\nHold {config.DeleteModModifier} while clicking to delete."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal string DrawNewOptionBase(IModGroup group, int count) + { + Im.Cursor.FrameAlign(); + Im.Selectable($"Option #{count + 1}", size: OptionIdxSelectable); + Target(group, count); + + Im.Line.SameInner(); + Im.FrameDummy(); + + Im.Line.SameInner(); + Im.Item.SetNextWidth(_optionNameWidth); + var newName = _newOptionGroup == group + ? NewOptionName ?? string.Empty + : string.Empty; + if (Im.Input.Text("##newOption"u8, ref newName, "Add new option..."u8)) + { + NewOptionName = newName; + _newOptionGroup = group; + } + + Im.Line.SameInner(); + return newName; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Source(IModOption option) + { + using var source = Im.DragDrop.Source(); + if (!source) + return; + + var across = option.Group is ITexToolsGroup; + + if (!source.SetPayload(across ? AcrossGroupsLabel : InsideGroupLabel)) + { + _dragDropGroup = option.Group; + _dragDropOption = option; + _draggingAcross = across; + } + + Im.Text($"Dragging option {option.Name} from group {option.Group.Name}..."); + } + + private void Target(IModGroup group, int optionIdx) + { + if (_dragDropGroup != group + && (!_draggingAcross || _dragDropGroup is not null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions })) + return; + + using var target = Im.DragDrop.Target(); + if (!target.IsDropping(_draggingAcross ? AcrossGroupsLabel : InsideGroupLabel)) + return; + + if (_dragDropGroup is not null && _dragDropOption is not null) + { + if (_dragDropGroup == group) + { + var sourceOption = _dragDropOption; + ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveOption(sourceOption, optionIdx)); + } + else + { + // Move from one group to another by deleting, then adding, then moving the option. + var sourceOption = _dragDropOption; + ActionQueue.Enqueue(() => + { + ModManager.OptionEditor.DeleteOption(sourceOption); + if (ModManager.OptionEditor.AddOption(group, sourceOption) is { } newOption) + ModManager.OptionEditor.MoveOption(newOption, optionIdx); + }); + } + } + + _dragDropGroup = null; + _dragDropOption = null; + _draggingAcross = false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void PrepareStyle() + { + var totalWidth = 400f * Im.Style.GlobalScale; + _buttonSize = new Vector2(Im.Style.FrameHeight); + PriorityWidth = 50 * Im.Style.GlobalScale; + AvailableWidth = new Vector2(totalWidth + 3 * _spacing + 2 * _buttonSize.X + PriorityWidth, 0); + _groupNameWidth = totalWidth - 3 * (_buttonSize.X + _spacing); + _spacing = Im.Style.ItemInnerSpacing.X; + OptionIdxSelectable = Im.Font.CalculateSize("Option #88."u8); + _optionNameWidth = totalWidth - OptionIdxSelectable.X - _buttonSize.X - 2 * _spacing; + _deleteEnabled = config.DeleteModModifier.IsActive(); + } +} diff --git a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs index 12e6ccbf..60a48543 100644 --- a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs @@ -1,68 +1,65 @@ -using Dalamud.Interface; -using Dalamud.Bindings.ImGui; -using ImSharp; -using OtterGui.Raii; -using OtterGui.Text; -using Penumbra.Mods.Groups; - -namespace Penumbra.UI.ModsTab.Groups; - -public readonly struct SingleModGroupEditDrawer(ModGroupEditDrawer editor, SingleModGroup group) : IModGroupEditDrawer -{ - public void Draw() - { - foreach (var (optionIdx, option) in group.OptionData.Index()) - { - using var id = ImRaii.PushId(optionIdx); - editor.DrawOptionPosition(group, option, optionIdx); - - Im.Line.SameInner(); - editor.DrawOptionDefaultSingleBehaviour(group, option, optionIdx); - - Im.Line.SameInner(); - editor.DrawOptionName(option); - - Im.Line.SameInner(); - editor.DrawOptionDescription(option); - - Im.Line.SameInner(); - editor.DrawOptionDelete(option); - - Im.Line.SameInner(); - ImGui.Dummy(new Vector2(editor.PriorityWidth, 0)); - } - - DrawNewOption(); - DrawConvertButton(); - } - - private void DrawConvertButton() - { - var convertible = group.Options.Count <= IModGroup.MaxMultiOptions; - var g = group; - var e = editor.ModManager.OptionEditor.SingleEditor; - if (ImUtf8.ButtonEx("Convert to Multi Group", editor.AvailableWidth, !convertible)) - editor.ActionQueue.Enqueue(() => e.ChangeToMulti(g)); - if (!convertible) - Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, - "Can not convert to multi group since maximum number of options is exceeded."u8); - } - - private void DrawNewOption() - { - var count = group.Options.Count; - if (count >= int.MaxValue) - return; - - var name = editor.DrawNewOptionBase(group, count); - - var validName = name.Length > 0; - if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName - ? "Add a new option to this group."u8 - : "Please enter a name for the new option."u8, default, !validName)) - { - editor.ModManager.OptionEditor.SingleEditor.AddOption(group, name); - editor.NewOptionName = null; - } - } -} +using ImSharp; +using Luna; +using Penumbra.Mods.Groups; + +namespace Penumbra.UI.ModsTab.Groups; + +public readonly struct SingleModGroupEditDrawer(ModGroupEditDrawer editor, SingleModGroup group) : IModGroupEditDrawer +{ + public void Draw() + { + foreach (var (optionIdx, option) in group.OptionData.Index()) + { + using var id = Im.Id.Push(optionIdx); + editor.DrawOptionPosition(group, option, optionIdx); + + Im.Line.SameInner(); + editor.DrawOptionDefaultSingleBehaviour(group, option, optionIdx); + + Im.Line.SameInner(); + editor.DrawOptionName(option); + + Im.Line.SameInner(); + editor.DrawOptionDescription(option); + + Im.Line.SameInner(); + editor.DrawOptionDelete(option); + + Im.Line.SameInner(); + Im.Dummy(new Vector2(editor.PriorityWidth, 0)); + } + + DrawNewOption(); + DrawConvertButton(); + } + + private void DrawConvertButton() + { + var convertible = group.Options.Count <= IModGroup.MaxMultiOptions; + var g = group; + var e = editor.ModManager.OptionEditor.SingleEditor; + if (ImEx.Button("Convert to Multi Group"u8, editor.AvailableWidth, !convertible)) + editor.ActionQueue.Enqueue(() => e.ChangeToMulti(g)); + if (!convertible) + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, + "Can not convert to multi group since maximum number of options is exceeded."u8); + } + + private void DrawNewOption() + { + var count = group.Options.Count; + if (count >= int.MaxValue) + return; + + var name = editor.DrawNewOptionBase(group, count); + + var validName = name.Length > 0; + if (ImEx.Icon.Button(LunaStyle.AddObjectIcon, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, !validName)) + { + editor.ModManager.OptionEditor.SingleEditor.AddOption(group, name); + editor.NewOptionName = null; + } + } +} diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 242d0bce..533de9c1 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -184,7 +184,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector SortMode - => _config.SortMode; + => ISortMode.FoldersFirst; protected override uint ExpandedFolderColor => ColorId.FolderExpanded.Value().Color; diff --git a/Penumbra/UI/ModsTab/ModFilter.cs b/Penumbra/UI/ModsTab/ModFilter.cs index 5f120b7d..514408f4 100644 --- a/Penumbra/UI/ModsTab/ModFilter.cs +++ b/Penumbra/UI/ModsTab/ModFilter.cs @@ -31,7 +31,7 @@ public static class ModFilterExtensions { public const ModFilter UnfilteredStateMods = (ModFilter)((1 << 22) - 1); - public static IReadOnlyList<(ModFilter On, ModFilter Off, string Name)> TriStatePairs = + public static readonly IReadOnlyList<(ModFilter On, ModFilter Off, string Name)> TriStatePairs = [ (ModFilter.Enabled, ModFilter.Disabled, "Enabled"), (ModFilter.IsNew, ModFilter.NotNew, "Newly Imported"), @@ -43,7 +43,7 @@ public static class ModFilterExtensions (ModFilter.Temporary, ModFilter.NotTemporary, "Temporary"), ]; - public static IReadOnlyList> Groups = + public static readonly IReadOnlyList> Groups = [ [ (ModFilter.NoConflict, "Has No Conflicts"), diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs index 10083804..cfc55cd3 100644 --- a/Penumbra/UI/ModsTab/ModPanel.cs +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -1,74 +1,78 @@ -using Dalamud.Plugin; -using ImSharp; -using Penumbra.Mods; -using Penumbra.Services; - -namespace Penumbra.UI.ModsTab; - -public class ModPanel : IDisposable, Luna.IUiService -{ - private readonly MultiModPanel _multiModPanel; - private readonly ModSelection _selection; - private readonly ModPanelHeader _header; - private readonly ModPanelTabBar _tabs; - private bool _resetCursor; - - public ModPanel(IDalamudPluginInterface pi, ModSelection selection, ModPanelTabBar tabs, - MultiModPanel multiModPanel, CommunicatorService communicator) - { - _selection = selection; - _tabs = tabs; - _multiModPanel = multiModPanel; - _header = new ModPanelHeader(pi, communicator); - _selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModPanel); - OnSelectionChange(new ModSelection.Arguments(null, _selection.Mod)); - } - - public void Draw() - { - if (!_valid) - { - _multiModPanel.Draw(); - return; - } - - if (_resetCursor) - { - _resetCursor = false; - Im.Scroll.X = 0; - } - - _header.Draw(); - Im.Cursor.X += Im.Scroll.X; - using var child = Im.Child.Begin("Tabs"u8, - Im.ContentRegion.Available with { X = Im.Window.MaximumContentRegion.X - Im.Window.MinimumContentRegion.X }); - if (child) - _tabs.Draw(_mod); - } - - public void Dispose() - { - _selection.Unsubscribe(OnSelectionChange); - _header.Dispose(); - } - - private bool _valid; - private Mod _mod = null!; - - private void OnSelectionChange(in ModSelection.Arguments arguments) - { - _resetCursor = true; - if (arguments.NewSelection is null || _selection.Mod is null) - { - _valid = false; - } - else - { - _valid = true; - _mod = arguments.NewSelection; - _header.ChangeMod(_mod); - _tabs.Settings.Reset(); - _tabs.Edit.Reset(); - } - } -} +using Dalamud.Plugin; +using ImSharp; +using Luna; +using Penumbra.Mods; +using Penumbra.Services; + +namespace Penumbra.UI.ModsTab; + +public class ModPanel : IDisposable, IPanel +{ + private readonly MultiModPanel _multiModPanel; + private readonly ModSelection _selection; + private readonly ModPanelHeader _header; + private readonly ModPanelTabBar _tabs; + private bool _resetCursor; + + public ModPanel(IDalamudPluginInterface pi, ModSelection selection, ModPanelTabBar tabs, + MultiModPanel multiModPanel, CommunicatorService communicator) + { + _selection = selection; + _tabs = tabs; + _multiModPanel = multiModPanel; + _header = new ModPanelHeader(pi, communicator); + _selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModPanel); + OnSelectionChange(new ModSelection.Arguments(null, _selection.Mod)); + } + + public ReadOnlySpan Id + => "MP"u8; + + public void Draw() + { + if (!_valid) + { + _multiModPanel.Draw(); + return; + } + + if (_resetCursor) + { + _resetCursor = false; + Im.Scroll.X = 0; + } + + _header.Draw(); + Im.Cursor.X += Im.Scroll.X; + using var child = Im.Child.Begin("Tabs"u8, + Im.ContentRegion.Available with { X = Im.Window.MaximumContentRegion.X - Im.Window.MinimumContentRegion.X }); + if (child) + _tabs.Draw(_mod); + } + + public void Dispose() + { + _selection.Unsubscribe(OnSelectionChange); + _header.Dispose(); + } + + private bool _valid; + private Mod _mod = null!; + + private void OnSelectionChange(in ModSelection.Arguments arguments) + { + _resetCursor = true; + if (arguments.NewSelection is null || _selection.Mod is null) + { + _valid = false; + } + else + { + _valid = true; + _mod = arguments.NewSelection; + _header.ChangeMod(_mod); + _tabs.Settings.Reset(); + _tabs.Edit.Reset(); + } + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs index 52992deb..fcb5b956 100644 --- a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs @@ -1,138 +1,137 @@ -using Dalamud.Bindings.ImGui; -using ImSharp; -using Luna; -using Penumbra.Collections; -using Penumbra.Collections.Manager; -using Penumbra.Mods; -using Penumbra.UI.Classes; - -namespace Penumbra.UI.ModsTab; - -public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSelector selector) : ITab -{ - private enum ModState - { - Enabled, - Disabled, - Unconfigured, - } - - private readonly List<(ModCollection, ModCollection, uint, ModState)> _cache = []; - - public ReadOnlySpan Label - => "Collections"u8; +using ImSharp; +using Luna; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.Mods; +using Penumbra.UI.Classes; - public ModPanelTab Identifier +namespace Penumbra.UI.ModsTab; + +public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSelector selector) : ITab +{ + private enum ModState + { + Enabled, + Disabled, + Unconfigured, + } + + private readonly List<(ModCollection, ModCollection, uint, ModState)> _cache = []; + + public ReadOnlySpan Label + => "Collections"u8; + + public ModPanelTab Identifier => ModPanelTab.Collections; - - public void DrawContent() - { - var (direct, inherited) = CountUsage(selector.Selected!); - Im.Line.New(); - switch (direct) - { - case 1: Im.Text("This Mod is directly configured in 1 collection."u8); break; - case 0: Im.Text("This mod is entirely unused."u8, Colors.RegexWarningBorder); break; - default: Im.Text($"This Mod is directly configured in {direct} collections."); break; - } - if (inherited > 0) - Im.Text($"It is also implicitly used in {inherited} {(inherited == 1 ? "collection" : "collections")} through inheritance."); - - Im.Line.New(); - Im.Separator(); - Im.Line.New(); - using var table = Im.Table.Begin("##modCollections"u8, 3, TableFlags.SizingFixedFit | TableFlags.RowBackground); - if (!table) - return; - - var size = Im.Font.CalculateSize(ToText(ModState.Unconfigured)).X + 20 * Im.Style.GlobalScale; - var collectionSize = 200 * Im.Style.GlobalScale; - table.SetupColumn("Collection"u8, TableColumnFlags.WidthFixed, collectionSize); - table.SetupColumn("State"u8, TableColumnFlags.WidthFixed, size); - table.SetupColumn("Inherited From"u8, TableColumnFlags.WidthFixed, collectionSize); - - ImGui.TableHeadersRow(); - foreach (var (idx, (collection, parent, color, state)) in _cache.Index()) - { - using var id = Im.Id.Push(idx); - table.DrawColumn(collection.Identity.Name); - - table.NextColumn(); - Im.Text(ToText(state), color); - - using (var context = Im.Popup.BeginContextItem("Context"u8)) - { - if (context) - { - Im.Text(collection.Identity.Name); - Im.Separator(); - using (Im.Disabled(state is ModState.Enabled && parent == collection)) - { - if (Im.Menu.Item("Enable"u8)) - { - if (parent != collection) - manager.Editor.SetModInheritance(collection, selector.Selected!, false); - manager.Editor.SetModState(collection, selector.Selected!, true); - } - } - - using (Im.Disabled(state is ModState.Disabled && parent == collection)) - { - if (Im.Menu.Item("Disable"u8)) - { - if (parent != collection) - manager.Editor.SetModInheritance(collection, selector.Selected!, false); - manager.Editor.SetModState(collection, selector.Selected!, false); - } - } - - using (Im.Disabled(parent != collection)) - { - if (Im.Menu.Item("Inherit"u8)) - manager.Editor.SetModInheritance(collection, selector.Selected!, true); - } - } - } - - table.DrawColumn(parent == collection ? StringU8.Empty : parent.Identity.Name); - } - } - - private static ReadOnlySpan ToText(ModState state) - => state switch - { - ModState.Unconfigured => "Unconfigured"u8, - ModState.Enabled => "Enabled"u8, - ModState.Disabled => "Disabled"u8, - _ => "Unknown"u8, - }; - - private (int Direct, int Inherited) CountUsage(Mod mod) - { - _cache.Clear(); - var undefined = ColorId.UndefinedMod.Value(); - var enabled = ColorId.EnabledMod.Value(); - var inherited = ColorId.InheritedMod.Value(); - var disabled = ColorId.DisabledMod.Value(); - var disInherited = ColorId.InheritedDisabledMod.Value(); - var directCount = 0; - var inheritedCount = 0; - foreach (var collection in manager.Storage) - { - var (settings, parent) = collection.GetInheritedSettings(mod.Index); - var (color, text) = settings == null - ? (undefined, ModState.Unconfigured) - : settings.Enabled - ? (parent == collection ? enabled : inherited, ModState.Enabled) - : (parent == collection ? disabled : disInherited, ModState.Disabled); - _cache.Add((collection, parent, color.Color, text)); - - if (color == enabled) - ++directCount; - else if (color == inherited) - ++inheritedCount; - } - - return (directCount, inheritedCount); - } -} + + public void DrawContent() + { + var (direct, inherited) = CountUsage(selector.Selected!); + Im.Line.New(); + switch (direct) + { + case 1: Im.Text("This Mod is directly configured in 1 collection."u8); break; + case 0: Im.Text("This mod is entirely unused."u8, Colors.RegexWarningBorder); break; + default: Im.Text($"This Mod is directly configured in {direct} collections."); break; + } + if (inherited > 0) + Im.Text($"It is also implicitly used in {inherited} {(inherited == 1 ? "collection" : "collections")} through inheritance."); + + Im.Line.New(); + Im.Separator(); + Im.Line.New(); + using var table = Im.Table.Begin("##modCollections"u8, 3, TableFlags.SizingFixedFit | TableFlags.RowBackground); + if (!table) + return; + + var size = Im.Font.CalculateSize(ToText(ModState.Unconfigured)).X + 20 * Im.Style.GlobalScale; + var collectionSize = 200 * Im.Style.GlobalScale; + table.SetupColumn("Collection"u8, TableColumnFlags.WidthFixed, collectionSize); + table.SetupColumn("State"u8, TableColumnFlags.WidthFixed, size); + table.SetupColumn("Inherited From"u8, TableColumnFlags.WidthFixed, collectionSize); + table.HeaderRow(); + + foreach (var (idx, (collection, parent, color, state)) in _cache.Index()) + { + using var id = Im.Id.Push(idx); + table.DrawColumn(collection.Identity.Name); + + table.NextColumn(); + Im.Text(ToText(state), color); + + using (var context = Im.Popup.BeginContextItem("Context"u8)) + { + if (context) + { + Im.Text(collection.Identity.Name); + Im.Separator(); + using (Im.Disabled(state is ModState.Enabled && parent == collection)) + { + if (Im.Menu.Item("Enable"u8)) + { + if (parent != collection) + manager.Editor.SetModInheritance(collection, selector.Selected!, false); + manager.Editor.SetModState(collection, selector.Selected!, true); + } + } + + using (Im.Disabled(state is ModState.Disabled && parent == collection)) + { + if (Im.Menu.Item("Disable"u8)) + { + if (parent != collection) + manager.Editor.SetModInheritance(collection, selector.Selected!, false); + manager.Editor.SetModState(collection, selector.Selected!, false); + } + } + + using (Im.Disabled(parent != collection)) + { + if (Im.Menu.Item("Inherit"u8)) + manager.Editor.SetModInheritance(collection, selector.Selected!, true); + } + } + } + + table.DrawColumn(parent == collection ? StringU8.Empty : parent.Identity.Name); + } + } + + private static ReadOnlySpan ToText(ModState state) + => state switch + { + ModState.Unconfigured => "Unconfigured"u8, + ModState.Enabled => "Enabled"u8, + ModState.Disabled => "Disabled"u8, + _ => "Unknown"u8, + }; + + private (int Direct, int Inherited) CountUsage(Mod mod) + { + _cache.Clear(); + var undefined = ColorId.UndefinedMod.Value(); + var enabled = ColorId.EnabledMod.Value(); + var inherited = ColorId.InheritedMod.Value(); + var disabled = ColorId.DisabledMod.Value(); + var disInherited = ColorId.InheritedDisabledMod.Value(); + var directCount = 0; + var inheritedCount = 0; + foreach (var collection in manager.Storage) + { + var (settings, parent) = collection.GetInheritedSettings(mod.Index); + var (color, text) = settings == null + ? (undefined, ModState.Unconfigured) + : settings.Enabled + ? (parent == collection ? enabled : inherited, ModState.Enabled) + : (parent == collection ? disabled : disInherited, ModState.Disabled); + _cache.Add((collection, parent, color.Color, text)); + + if (color == enabled) + ++directCount; + else if (color == inherited) + ++inheritedCount; + } + + return (directCount, inheritedCount); + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs index a0fe226f..be4d741c 100644 --- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -1,58 +1,58 @@ -using Dalamud.Bindings.ImGui; -using ImSharp; -using Luna; -using OtterGui.Widgets; -using Penumbra.Mods.Manager; - -namespace Penumbra.UI.ModsTab; - -public class ModPanelDescriptionTab( - ModFileSystemSelector selector, - TutorialService tutorial, - ModManager modManager, - PredefinedTagManager predefinedTagsConfig) - : ITab -{ - private readonly TagButtons _localTags = new(); - private readonly TagButtons _modTags = new(); - - public ReadOnlySpan Label - => "Description"u8; +using ImSharp; +using Luna; +using OtterGui.Widgets; +using Penumbra.Mods.Manager; +using Penumbra.UI.Classes; - public ModPanelTab Identifier +namespace Penumbra.UI.ModsTab; + +public class ModPanelDescriptionTab( + ModFileSystemSelector selector, + TutorialService tutorial, + ModManager modManager, + PredefinedTagManager predefinedTagsConfig) + : ITab +{ + private readonly TagButtons _localTags = new(); + private readonly TagButtons _modTags = new(); + + public ReadOnlySpan Label + => "Description"u8; + + public ModPanelTab Identifier => ModPanelTab.Description; - - public void DrawContent() - { - using var child = Im.Child.Begin("##description"u8); - if (!child) - return; - - Im.ScaledDummy(2, 2); - Im.ScaledDummy(2, 2); - var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.Enabled - ? (true, Im.Style.FrameHeight + Im.Style.WindowPadding.X + (ImGui.GetScrollMaxY() > 0 ? Im.Style.ScrollbarSize : 0)) - : (false, 0); - var tagIdx = _localTags.Draw("Local Tags: ", - "Custom tags you can set personally that will not be exported to the mod data but only set for you.\n" - + "If the mod already contains a local tag in its own tags, the local tag will be ignored.", selector.Selected!.LocalTags, - out var editedTag, rightEndOffset: predefinedTagButtonOffset); - tutorial.OpenTutorial(BasicTutorialSteps.Tags); - if (tagIdx >= 0) - modManager.DataEditor.ChangeLocalTag(selector.Selected!, tagIdx, editedTag); - - if (predefinedTagsEnabled) - predefinedTagsConfig.DrawAddFromSharedTagsAndUpdateTags(selector.Selected!.LocalTags, selector.Selected!.ModTags, true, - selector.Selected!); - - if (selector.Selected!.ModTags.Count > 0) - _modTags.Draw("Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.", - selector.Selected!.ModTags, out _, false, - ImGui.CalcTextSize("Local ").X - ImGui.CalcTextSize("Mod ").X); - - Im.ScaledDummy(2, 2); - Im.Separator(); - - Im.TextWrapped(selector.Selected!.Description); - } -} + + public void DrawContent() + { + using var child = Im.Child.Begin("##description"u8); + if (!child) + return; + + Im.ScaledDummy(2, 2); + Im.ScaledDummy(2, 2); + var (predefinedTagsEnabled, predefinedTagButtonOffset) = predefinedTagsConfig.Enabled + ? (true, Im.Style.FrameHeight + Im.Style.WindowPadding.X + (Im.Scroll.MaximumY > 0 ? Im.Style.ScrollbarSize : 0)) + : (false, 0); + var tagIdx = _localTags.Draw("Local Tags: ", + "Custom tags you can set personally that will not be exported to the mod data but only set for you.\n" + + "If the mod already contains a local tag in its own tags, the local tag will be ignored.", selector.Selected!.LocalTags, + out var editedTag, rightEndOffset: predefinedTagButtonOffset); + tutorial.OpenTutorial(BasicTutorialSteps.Tags); + if (tagIdx >= 0) + modManager.DataEditor.ChangeLocalTag(selector.Selected!, tagIdx, editedTag); + + if (predefinedTagsEnabled) + predefinedTagsConfig.DrawAddFromSharedTagsAndUpdateTags(selector.Selected!.LocalTags, selector.Selected!.ModTags, true, + selector.Selected!); + + if (selector.Selected!.ModTags.Count > 0) + _modTags.Draw("Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.", + selector.Selected!.ModTags, out _, false, + Im.Font.CalculateSize("Local "u8).X - Im.Font.CalculateSize("Mod "u8).X); + + Im.ScaledDummy(2, 2); + Im.Separator(); + + Im.TextWrapped(selector.Selected!.Description); + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 42b9fee2..0e5f4668 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -182,7 +182,7 @@ public class ModPanelEditTab( var tt = fileExists ? "Open the metadata json file in the text editor of your choice."u8 : "The metadata json file does not exist."u8; - using (Im.Id.Push("meta")) + using (Im.Id.Push("meta"u8)) { if (ImEx.Icon.Button(LunaStyle.FileExportIcon, tt, !fileExists)) Process.Start(new ProcessStartInfo(filenames.ModMetaPath(_mod)) { UseShellExecute = true }); diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs index f946259d..f3e0bfec 100644 --- a/Penumbra/UI/ModsTab/ModPanelHeader.cs +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -1,269 +1,258 @@ -using Dalamud.Bindings.ImGui; -using Dalamud.Interface.GameFonts; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Plugin; -using ImSharp; -using OtterGui; -using OtterGui.Raii; -using Penumbra.Communication; -using Penumbra.Mods; -using Penumbra.Mods.Manager; -using Penumbra.Services; -using Penumbra.UI.Classes; - -namespace Penumbra.UI.ModsTab; - -public class ModPanelHeader : IDisposable -{ - /// We use a big, nice game font for the title. - private readonly IFontHandle _nameFont; - - private readonly CommunicatorService _communicator; - private float _lastPreSettingsHeight; - private bool _dirty = true; - - public ModPanelHeader(IDalamudPluginInterface pi, CommunicatorService communicator) - { - _communicator = communicator; - _nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); - _communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModPanelHeader); - } - - /// - /// Draw the header for the current mod, - /// consisting of its name, version, author and website, if they exist. - /// - public void Draw() - { - UpdateModData(); - var height = Im.ContentRegion.Available.Y; - var maxHeight = 3 * height / 4; - using var child = _lastPreSettingsHeight > maxHeight && _communicator.PreSettingsTabBarDraw.HasSubscribers - ? ImRaii.Child("HeaderChild", new Vector2(Im.ContentRegion.Available.X, maxHeight), false) - : null; - using (Im.Group()) - { - var offset = DrawModName(); - DrawVersion(offset); - DrawSecondRow(offset); - } - - _communicator.PreSettingsTabBarDraw.Invoke(new PreSettingsTabBarDraw.Arguments(_mod, ImGui.GetItemRectSize().X, _nameWidth)); - _lastPreSettingsHeight = ImGui.GetCursorPosY(); - } - - public void ChangeMod(Mod mod) - { - _mod = mod; - _dirty = true; - } - - /// - /// Update all mod header data. Should someone change frame padding or item spacing, - /// or his default font, this will break, but he will just have to select a different mod to restore. - /// - private void UpdateModData() - { - if (!_dirty) - return; - - _dirty = false; - _lastPreSettingsHeight = 0; - // Name - var name = $" {_mod.Name} "; - if (name != _modName) - { - using var f = _nameFont.Push(); - _modName = name; - _modNameWidth = ImGui.CalcTextSize(name).X + 2 * (Im.Style.FramePadding.X + 2 * Im.Style.GlobalScale); - } - - // Author - if (_mod.Author != _modAuthor) - { - var author = _mod.Author.Length is 0 ? string.Empty : $"by {_mod.Author}"; - _modAuthor = _mod.Author; - _modAuthorWidth = ImGui.CalcTextSize(author).X; - _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + Im.Style.ItemSpacing.X; - } - - // Version - var version = _mod.Version.Length > 0 ? $"({_mod.Version})" : string.Empty; - if (version != _modVersion) - { - _modVersion = version; - _modVersionWidth = ImGui.CalcTextSize(version).X; - } - - // Website - if (_modWebsite != _mod.Website) - { - _modWebsite = _mod.Website; - _websiteValid = Uri.TryCreate(_modWebsite, UriKind.Absolute, out var uriResult) - && (uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp); - _modWebsiteButton = _websiteValid ? "Open Website" : _modWebsite.Length == 0 ? string.Empty : $"from {_modWebsite}"; - _modWebsiteButtonWidth = _websiteValid - ? ImGui.CalcTextSize(_modWebsiteButton).X + 2 * Im.Style.FramePadding.X - : ImGui.CalcTextSize(_modWebsiteButton).X; - _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + Im.Style.ItemSpacing.X; - } - } - - public void Dispose() - { - _nameFont.Dispose(); - _communicator.ModDataChanged.Unsubscribe(OnModDataChange); - } - - // Header data. - private Mod _mod = null!; - private string _modName = string.Empty; - private string _modAuthor = string.Empty; - private string _modVersion = string.Empty; - private string _modWebsite = string.Empty; - private string _modWebsiteButton = string.Empty; - private bool _websiteValid; - - private float _modNameWidth; - private float _modAuthorWidth; - private float _modVersionWidth; - private float _modWebsiteButtonWidth; - private float _secondRowWidth; - - private float _nameWidth; - - /// - /// Draw the mod name in the game font with a 2px border, centered, - /// with at least the width of the version space to each side. - /// - private float DrawModName() - { - var decidingWidth = Math.Max(_secondRowWidth, ImGui.GetWindowWidth()); - var offsetWidth = (decidingWidth - _modNameWidth) / 2; - var offsetVersion = _modVersion.Length > 0 - ? _modVersionWidth + Im.Style.ItemSpacing.X + Im.Style.WindowPadding.X - : 0; - var offset = Math.Max(offsetWidth, offsetVersion); - if (offset > 0) - { - ImGui.SetCursorPosX(offset); - } - - using var style = ImStyleBorder.Frame.Push(Colors.MetaInfoText, 2 * Im.Style.GlobalScale); - using var f = _nameFont.Push(); - ImGuiUtil.DrawTextButton(_modName, Vector2.Zero, 0); - _nameWidth = ImGui.GetItemRectSize().X; - return offset; - } - - /// Draw the version in the top-right corner. - private void DrawVersion(float offset) - { - var oldPos = ImGui.GetCursorPos(); - ImGui.SetCursorPos(new Vector2(2 * offset + _modNameWidth - _modVersionWidth - Im.Style.WindowPadding.X, - Im.Style.FramePadding.Y)); - ImGuiUtil.TextColored(Colors.MetaInfoText, _modVersion); - ImGui.SetCursorPos(oldPos); - } - - /// - /// Draw author and website if they exist. The website is a button if it is valid. - /// Usually, author begins at the left boundary of the name, - /// and website ends at the right boundary of the name. - /// If their combined width is larger than the name, they are combined-centered. - /// - private void DrawSecondRow(float offset) - { - if (_modAuthor.Length == 0) - { - if (_modWebsiteButton.Length == 0) - { - Im.Line.New(); - return; - } - - offset += (_modNameWidth - _modWebsiteButtonWidth) / 2; - ImGui.SetCursorPosX(offset); - DrawWebsite(); - } - else if (_modWebsiteButton.Length == 0) - { - offset += (_modNameWidth - _modAuthorWidth) / 2; - ImGui.SetCursorPosX(offset); - DrawAuthor(); - } - else if (_secondRowWidth < _modNameWidth) - { - ImGui.SetCursorPosX(offset); - DrawAuthor(); - Im.Line.Same(offset + _modNameWidth - _modWebsiteButtonWidth); - DrawWebsite(); - } - else - { - offset -= (_secondRowWidth - _modNameWidth) / 2; - if (offset > 0) - { - ImGui.SetCursorPosX(offset); - } - - DrawAuthor(); - Im.Line.Same(); - DrawWebsite(); - } - } - - /// Draw the author text. - private void DrawAuthor() - { - using var style = ImStyleDouble.ItemSpacing.Push(Vector2.Zero); - ImGuiUtil.TextColored(Colors.MetaInfoText, "by "); - Im.Line.Same(); - style.Pop(); - Im.Text(_modAuthor); - } - - /// - /// Draw either a website button if the source is a valid website address, - /// or a source text if it is not. - /// - private void DrawWebsite() - { - if (_websiteValid) - { - if (ImGui.SmallButton(_modWebsiteButton)) - { - try - { - var process = new ProcessStartInfo(_modWebsite) - { - UseShellExecute = true, - }; - Process.Start(process); - } - catch - { - // ignored - } - } - - ImGuiUtil.HoverTooltip(_modWebsite); - } - else - { - using var style = ImStyleDouble.ItemSpacing.Push(Vector2.Zero); - ImGuiUtil.TextColored(Colors.MetaInfoText, "from "); - Im.Line.Same(); - style.Pop(); - Im.Text(_modWebsite); - } - } - - /// Just update the data when any relevant field changes. - private void OnModDataChange(in ModDataChanged.Arguments arguments) - { - const ModDataChangeType relevantChanges = - ModDataChangeType.Author | ModDataChangeType.Name | ModDataChangeType.Website | ModDataChangeType.Version; - _dirty = (arguments.Type & relevantChanges) is not 0; - } -} +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Plugin; +using ImSharp; +using Penumbra.Communication; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Services; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModsTab; + +public class ModPanelHeader : IDisposable +{ + /// We use a big, nice game font for the title. + private readonly IFontHandle _nameFont; + + private readonly CommunicatorService _communicator; + private float _lastPreSettingsHeight; + private bool _dirty = true; + + public ModPanelHeader(IDalamudPluginInterface pi, CommunicatorService communicator) + { + _communicator = communicator; + _nameFont = pi.UiBuilder.FontAtlas.NewGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); + _communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModPanelHeader); + } + + /// + /// Draw the header for the current mod, + /// consisting of its name, version, author and website, if they exist. + /// + public void Draw() + { + UpdateModData(); + var height = Im.ContentRegion.Available.Y; + var maxHeight = 3 * height / 4; + using var child = _lastPreSettingsHeight > maxHeight && _communicator.PreSettingsTabBarDraw.HasSubscribers + ? Im.Child.Begin("HeaderChild"u8, Im.ContentRegion.Available with { Y = maxHeight }) + : default; + using (Im.Group()) + { + var offset = DrawModName(); + DrawVersion(offset); + DrawSecondRow(offset); + } + + _communicator.PreSettingsTabBarDraw.Invoke(new PreSettingsTabBarDraw.Arguments(_mod, Im.Item.Size.X, _nameWidth)); + _lastPreSettingsHeight = Im.Cursor.Position.Y; + } + + public void ChangeMod(Mod mod) + { + _mod = mod; + _dirty = true; + } + + /// + /// Update all mod header data. Should someone change frame padding or item spacing, + /// or his default font, this will break, but he will just have to select a different mod to restore. + /// + private void UpdateModData() + { + if (!_dirty) + return; + + _dirty = false; + _lastPreSettingsHeight = 0; + // Name + var name = $" {_mod.Name} "; + if (name != _modName) + { + using var f = _nameFont.Push(); + _modName = name; + _modNameWidth = Im.Font.CalculateSize(name).X + 2 * (Im.Style.FramePadding.X + 2 * Im.Style.GlobalScale); + } + + // Author + if (_mod.Author != _modAuthor) + { + var author = _mod.Author.Length is 0 ? string.Empty : $"by {_mod.Author}"; + _modAuthor = _mod.Author; + _modAuthorWidth = Im.Font.CalculateSize(author).X; + _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + Im.Style.ItemSpacing.X; + } + + // Version + var version = _mod.Version.Length > 0 ? $"({_mod.Version})" : string.Empty; + if (version != _modVersion) + { + _modVersion = version; + _modVersionWidth = Im.Font.CalculateSize(version).X; + } + + // Website + if (_modWebsite != _mod.Website) + { + _modWebsite = _mod.Website; + _websiteValid = Uri.TryCreate(_modWebsite, UriKind.Absolute, out var uriResult) + && (uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp); + _modWebsiteButton = _websiteValid ? "Open Website" : _modWebsite.Length == 0 ? string.Empty : $"from {_modWebsite}"; + _modWebsiteButtonWidth = _websiteValid + ? Im.Font.CalculateSize(_modWebsiteButton).X + 2 * Im.Style.FramePadding.X + : Im.Font.CalculateSize(_modWebsiteButton).X; + _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + Im.Style.ItemSpacing.X; + } + } + + public void Dispose() + { + _nameFont.Dispose(); + _communicator.ModDataChanged.Unsubscribe(OnModDataChange); + } + + // Header data. + private Mod _mod = null!; + private string _modName = string.Empty; + private string _modAuthor = string.Empty; + private string _modVersion = string.Empty; + private string _modWebsite = string.Empty; + private string _modWebsiteButton = string.Empty; + private bool _websiteValid; + + private float _modNameWidth; + private float _modAuthorWidth; + private float _modVersionWidth; + private float _modWebsiteButtonWidth; + private float _secondRowWidth; + + private float _nameWidth; + + /// + /// Draw the mod name in the game font with a 2px border, centered, + /// with at least the width of the version space to each side. + /// + private float DrawModName() + { + var decidingWidth = Math.Max(_secondRowWidth, Im.Window.Width); + var offsetWidth = (decidingWidth - _modNameWidth) / 2; + var offsetVersion = _modVersion.Length > 0 + ? _modVersionWidth + Im.Style.ItemSpacing.X + Im.Style.WindowPadding.X + : 0; + var offset = Math.Max(offsetWidth, offsetVersion); + if (offset > 0) + Im.Cursor.X = offset; + + using var style = ImStyleBorder.Frame.Push(Colors.MetaInfoText, 2 * Im.Style.GlobalScale); + using var f = _nameFont.Push(); + ImEx.TextFramed(_modName, Vector2.Zero, 0); + _nameWidth = Im.Item.Size.X; + return offset; + } + + /// Draw the version in the top-right corner. + private void DrawVersion(float offset) + { + var oldPos = Im.Cursor.Position; + Im.Cursor.Position = new Vector2(2 * offset + _modNameWidth - _modVersionWidth - Im.Style.WindowPadding.X, + Im.Style.FramePadding.Y); + Im.Text(_modVersion, Colors.MetaInfoText); + Im.Cursor.Position = oldPos; + } + + /// + /// Draw author and website if they exist. The website is a button if it is valid. + /// Usually, author begins at the left boundary of the name, + /// and website ends at the right boundary of the name. + /// If their combined width is larger than the name, they are combined-centered. + /// + private void DrawSecondRow(float offset) + { + if (_modAuthor.Length == 0) + { + if (_modWebsiteButton.Length == 0) + { + Im.Line.New(); + return; + } + + offset += (_modNameWidth - _modWebsiteButtonWidth) / 2; + Im.Cursor.X = offset; + DrawWebsite(); + } + else if (_modWebsiteButton.Length == 0) + { + offset += (_modNameWidth - _modAuthorWidth) / 2; + Im.Cursor.X = offset; + DrawAuthor(); + } + else if (_secondRowWidth < _modNameWidth) + { + Im.Cursor.X = offset; + DrawAuthor(); + Im.Line.Same(offset + _modNameWidth - _modWebsiteButtonWidth); + DrawWebsite(); + } + else + { + offset -= (_secondRowWidth - _modNameWidth) / 2; + if (offset > 0) + Im.Cursor.X = offset; + + DrawAuthor(); + Im.Line.Same(); + DrawWebsite(); + } + } + + /// Draw the author text. + private void DrawAuthor() + { + Im.Text("by "u8, Colors.MetaInfoText); + Im.Line.NoSpacing(); + Im.Text(_modAuthor); + } + + /// + /// Draw either a website button if the source is a valid website address, + /// or a source text if it is not. + /// + private void DrawWebsite() + { + if (_websiteValid) + { + if (Im.SmallButton(_modWebsiteButton)) + { + try + { + var process = new ProcessStartInfo(_modWebsite) + { + UseShellExecute = true, + }; + Process.Start(process); + } + catch + { + // ignored + } + } + + Im.Tooltip.OnHover(_modWebsite); + } + else + { + Im.Text("from "u8, Colors.MetaInfoText); + Im.Line.NoSpacing(); + Im.Text(_modWebsite); + } + } + + /// Just update the data when any relevant field changes. + private void OnModDataChange(in ModDataChanged.Arguments arguments) + { + const ModDataChangeType relevantChanges = + ModDataChangeType.Author | ModDataChangeType.Name | ModDataChangeType.Website | ModDataChangeType.Version; + _dirty = (arguments.Type & relevantChanges) is not 0; + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 92343c63..7cf5cad5 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -1,264 +1,264 @@ -using Dalamud.Bindings.ImGui; -using ImSharp; -using Luna; -using OtterGui.Raii; -using OtterGui.Text; -using Penumbra.UI.Classes; -using Penumbra.Collections.Manager; -using Penumbra.Communication; -using Penumbra.Mods; -using Penumbra.Mods.Manager; -using Penumbra.Services; -using Penumbra.Mods.Settings; -using Penumbra.UI.ModsTab.Groups; - -namespace Penumbra.UI.ModsTab; - -public class ModPanelSettingsTab( - CollectionManager collectionManager, - ModManager modManager, - ModSelection selection, - TutorialService tutorial, - CommunicatorService communicator, - ModGroupDrawer modGroupDrawer, - Configuration config) - : ITab -{ - private bool _inherited; - private bool _temporary; - private bool _locked; - private int? _currentPriority; - - public ReadOnlySpan Label - => "Settings"u8; +using ImSharp; +using Luna; +using Penumbra.UI.Classes; +using Penumbra.Collections.Manager; +using Penumbra.Communication; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Services; +using Penumbra.Mods.Settings; +using Penumbra.UI.ModsTab.Groups; - public ModPanelTab Identifier +namespace Penumbra.UI.ModsTab; + +public class ModPanelSettingsTab( + CollectionManager collectionManager, + ModManager modManager, + ModSelection selection, + TutorialService tutorial, + CommunicatorService communicator, + ModGroupDrawer modGroupDrawer, + Configuration config) + : ITab +{ + private bool _inherited; + private bool _temporary; + private bool _locked; + private int? _currentPriority; + + public ReadOnlySpan Label + => "Settings"u8; + + public ModPanelTab Identifier => ModPanelTab.Settings; - - public void PostTabButton() - => tutorial.OpenTutorial(BasicTutorialSteps.ModOptions); - - public void Reset() - => _currentPriority = null; - - public void DrawContent() - { - using var table = Im.Table.Begin("##settings"u8, 1, TableFlags.ScrollY, Im.ContentRegion.Available); - if (!table) - return; - - _inherited = selection.Collection != collectionManager.Active.Current; - _temporary = selection.TemporarySettings != null; - _locked = (selection.TemporarySettings?.Lock ?? 0) > 0; - - ImGui.TableSetupScrollFreeze(0, 1); - ImGui.TableNextColumn(); - DrawTemporaryWarning(); - DrawInheritedWarning(); - ImGui.Dummy(Vector2.Zero); - communicator.PreSettingsPanelDraw.Invoke(new PreSettingsPanelDraw.Arguments(selection.Mod!)); - DrawEnabledInput(); - tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods); - Im.Line.Same(); - DrawPriorityInput(); - tutorial.OpenTutorial(BasicTutorialSteps.Priority); - DrawRemoveSettings(); - - ImGui.TableNextColumn(); - communicator.PostEnabledDraw.Invoke(new PostEnabledDraw.Arguments(selection.Mod!)); - - modGroupDrawer.Draw(selection.Mod!, selection.Settings, selection.TemporarySettings); - UiHelpers.DefaultLineSpace(); - communicator.PostSettingsPanelDraw.Invoke(new PostSettingsPanelDraw.Arguments(selection.Mod!)); - } - - /// Draw a big tinted bar if the current setting is temporary. - private void DrawTemporaryWarning() - { - if (!_temporary) - return; - - using var color = ImGuiColor.Button.Push(Rgba32.TintColor(Im.Style[ImGuiColor.Button], ColorId.TemporaryModSettingsTint.Value().ToVector())); - var width = new Vector2(Im.ContentRegion.Available.X, 0); - if (ImUtf8.ButtonEx($"These settings are temporarily set by {selection.TemporarySettings!.Source}{(_locked ? " and locked." : ".")}", - width, - _locked)) - collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, null); - - Im.Tooltip.OnHover("Changing settings in temporary settings will not save them across sessions.\n"u8 - + "You can click this button to remove the temporary settings and return to your normal settings."u8); - } - - /// Draw a big red bar if the current setting is inherited. - private void DrawInheritedWarning() - { - if (!_inherited) - return; - - using var color = ImGuiColor.Button.Push(Colors.PressEnterWarningBg); - var width = new Vector2(Im.ContentRegion.Available.X, 0); - if (ImUtf8.ButtonEx($"These settings are inherited from {selection.Collection.Identity.Name}.", width, _locked)) - { - if (_temporary) - { - selection.TemporarySettings!.ForceInherit = false; - collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, selection.TemporarySettings); - } - else - { - collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, false); - } - } - - Im.Tooltip.OnHover("You can click this button to copy the current settings to the current selection.\n"u8 - + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."u8); - } - - /// Draw a checkbox for the enabled status of the mod. - private void DrawEnabledInput() - { - var enabled = selection.Settings.Enabled; - using var disabled = ImRaii.Disabled(_locked); - if (!ImUtf8.Checkbox("Enabled"u8, ref enabled)) - return; - - modManager.SetKnown(selection.Mod!); - if (_temporary || config.DefaultTemporaryMode) - { - var temporarySettings = selection.TemporarySettings ?? new TemporaryModSettings(selection.Mod!, selection.Settings); - temporarySettings.ForceInherit = false; - temporarySettings.Enabled = enabled; - collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, temporarySettings); - } - else - { - collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod!, enabled); - } - } - - /// - /// Draw a priority input. - /// Priority is changed on deactivation of the input box. - /// - private void DrawPriorityInput() - { - using var group = ImUtf8.Group(); - var settings = selection.Settings; - var priority = _currentPriority ?? settings.Priority.Value; - Im.Item.SetNextWidth(50 * Im.Style.GlobalScale); - using var disabled = ImRaii.Disabled(_locked); - if (ImUtf8.InputScalar("##Priority"u8, ref priority)) - _currentPriority = priority; - if (new ModPriority(priority).IsHidden) - Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, - $"This priority is special-cased to hide this mod in conflict tabs ({ModPriority.HiddenMin}, {ModPriority.HiddenMax})."); - - - if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) - { - if (_currentPriority != settings.Priority.Value) - { - if (_temporary || config.DefaultTemporaryMode) - { - var temporarySettings = selection.TemporarySettings ?? new TemporaryModSettings(selection.Mod!, selection.Settings); - temporarySettings.ForceInherit = false; - temporarySettings.Priority = new ModPriority(_currentPriority.Value); - collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, - temporarySettings); - } - else - { - collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod!, - new ModPriority(_currentPriority.Value)); - } - } - - _currentPriority = null; - } - - ImUtf8.LabeledHelpMarker("Priority"u8, "Mods with a higher number here take precedence before Mods with a lower number.\n"u8 - + "That means, if Mod A should overwrite changes from Mod B, Mod A should have a higher priority number than Mod B."u8); - } - - /// - /// Draw a button to remove the current settings and inherit them instead - /// in the top-right corner of the window/tab. - /// - private void DrawRemoveSettings() - { - var drawInherited = !_inherited && !selection.Settings.IsEmpty; - var scroll = ImGui.GetScrollMaxY() > 0 ? Im.Style.ScrollbarSize + Im.Style.ItemInnerSpacing.X : 0; - var buttonSize = ImUtf8.CalcTextSize("Turn Permanent_"u8).X; - var offset = drawInherited - ? buttonSize + ImUtf8.CalcTextSize("Inherit Settings"u8).X + Im.Style.FramePadding.X * 4 + Im.Style.ItemSpacing.X - : buttonSize + Im.Style.FramePadding.X * 2; - Im.Line.Same(ImGui.GetWindowWidth() - offset - scroll); - var enabled = config.DeleteModModifier.IsActive(); - if (drawInherited) - { - var inherit = (enabled, _locked) switch - { - (true, false) => ImUtf8.ButtonEx("Inherit Settings"u8, - "Remove current settings from this collection so that it can inherit them.\n"u8 - + "If no inherited collection has settings for this mod, it will be disabled."u8), - (false, false) => ImUtf8.ButtonEx("Inherit Settings"u8, - $"Remove current settings from this collection so that it can inherit them.\nHold {config.DeleteModModifier} to inherit.", - default, true), - (_, true) => ImUtf8.ButtonEx("Inherit Settings"u8, - "Remove current settings from this collection so that it can inherit them.\nThe settings are currently locked and can not be changed."u8, - default, true), - }; - if (inherit) - { - if (_temporary || config.DefaultTemporaryMode) - { - var temporarySettings = selection.TemporarySettings ?? new TemporaryModSettings(selection.Mod!, selection.Settings); - temporarySettings.ForceInherit = true; - collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, - temporarySettings); - } - else - { - collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true); - } - } - - Im.Line.Same(); - } - - if (_temporary) - { - var overwrite = enabled - ? ImUtf8.ButtonEx("Turn Permanent"u8, - "Overwrite the actual settings for this mod in this collection with the current temporary settings."u8, - new Vector2(buttonSize, 0)) - : ImUtf8.ButtonEx("Turn Permanent"u8, - $"Overwrite the actual settings for this mod in this collection with the current temporary settings.\nHold {config.DeleteModModifier} to overwrite.", - new Vector2(buttonSize, 0), true); - if (overwrite) - { - var settings = collectionManager.Active.Current.GetTempSettings(selection.Mod!.Index)!; - if (settings.ForceInherit) - { - collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod, true); - } - else - { - collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod, settings.Enabled); - collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod, settings.Priority); - foreach (var (index, setting) in settings.Settings.Index()) - collectionManager.Editor.SetModSetting(collectionManager.Active.Current, selection.Mod, index, setting); - } - - collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod, null); - } - } - else - { - var actual = collectionManager.Active.Current.GetActualSettings(selection.Mod!.Index).Settings; - if (ImUtf8.ButtonEx("Turn Temporary"u8, "Copy the current settings over to temporary settings to experiment with them."u8)) - collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, - new TemporaryModSettings(selection.Mod!, actual)); - } - } -} + + public void PostTabButton() + => tutorial.OpenTutorial(BasicTutorialSteps.ModOptions); + + public void Reset() + => _currentPriority = null; + + public void DrawContent() + { + using var table = Im.Table.Begin("##settings"u8, 1, TableFlags.ScrollY, Im.ContentRegion.Available); + if (!table) + return; + + _inherited = selection.Collection != collectionManager.Active.Current; + _temporary = selection.TemporarySettings != null; + _locked = (selection.TemporarySettings?.Lock ?? 0) > 0; + + table.SetupScrollFreeze(0, 1); + table.NextColumn(); + DrawTemporaryWarning(); + DrawInheritedWarning(); + Im.Dummy(Vector2.Zero); + communicator.PreSettingsPanelDraw.Invoke(new PreSettingsPanelDraw.Arguments(selection.Mod!)); + DrawEnabledInput(); + tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods); + Im.Line.Same(); + DrawPriorityInput(); + tutorial.OpenTutorial(BasicTutorialSteps.Priority); + DrawRemoveSettings(); + + table.NextColumn(); + communicator.PostEnabledDraw.Invoke(new PostEnabledDraw.Arguments(selection.Mod!)); + + modGroupDrawer.Draw(selection.Mod!, selection.Settings, selection.TemporarySettings); + UiHelpers.DefaultLineSpace(); + communicator.PostSettingsPanelDraw.Invoke(new PostSettingsPanelDraw.Arguments(selection.Mod!)); + } + + /// Draw a big tinted bar if the current setting is temporary. + private void DrawTemporaryWarning() + { + if (!_temporary) + return; + + using var color = + ImGuiColor.Button.Push(Rgba32.TintColor(Im.Style[ImGuiColor.Button], ColorId.TemporaryModSettingsTint.Value().ToVector())); + var width = Im.ContentRegion.Available with { Y = 0 }; + if (ImEx.Button($"These settings are temporarily set by {selection.TemporarySettings!.Source}{(_locked ? " and locked." : ".")}", + width, _locked)) + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, null); + + Im.Tooltip.OnHover("Changing settings in temporary settings will not save them across sessions.\n"u8 + + "You can click this button to remove the temporary settings and return to your normal settings."u8); + } + + /// Draw a big red bar if the current setting is inherited. + private void DrawInheritedWarning() + { + if (!_inherited) + return; + + using var color = ImGuiColor.Button.Push(Colors.PressEnterWarningBg); + var width = Im.ContentRegion.Available with { Y = 0 }; + if (ImEx.Button($"These settings are inherited from {selection.Collection.Identity.Name}.", width, _locked)) + { + if (_temporary) + { + selection.TemporarySettings!.ForceInherit = false; + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, selection.TemporarySettings); + } + else + { + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, false); + } + } + + Im.Tooltip.OnHover("You can click this button to copy the current settings to the current selection.\n"u8 + + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."u8); + } + + /// Draw a checkbox for the enabled status of the mod. + private void DrawEnabledInput() + { + var enabled = selection.Settings.Enabled; + using var disabled = Im.Disabled(_locked); + if (!Im.Checkbox("Enabled"u8, ref enabled)) + return; + + modManager.SetKnown(selection.Mod!); + if (_temporary || config.DefaultTemporaryMode) + { + var temporarySettings = selection.TemporarySettings ?? new TemporaryModSettings(selection.Mod!, selection.Settings); + temporarySettings.ForceInherit = false; + temporarySettings.Enabled = enabled; + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, temporarySettings); + } + else + { + collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod!, enabled); + } + } + + /// + /// Draw a priority input. + /// Priority is changed on deactivation of the input box. + /// + private void DrawPriorityInput() + { + using var group = Im.Group(); + var settings = selection.Settings; + var priority = _currentPriority ?? settings.Priority.Value; + Im.Item.SetNextWidth(50 * Im.Style.GlobalScale); + using var disabled = Im.Disabled(_locked); + if (Im.Input.Scalar("##Priority"u8, ref priority)) + _currentPriority = priority; + if (new ModPriority(priority).IsHidden) + Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, + $"This priority is special-cased to hide this mod in conflict tabs ({ModPriority.HiddenMin}, {ModPriority.HiddenMax})."); + + + if (Im.Item.DeactivatedAfterEdit && _currentPriority.HasValue) + { + if (_currentPriority != settings.Priority.Value) + { + if (_temporary || config.DefaultTemporaryMode) + { + var temporarySettings = selection.TemporarySettings ?? new TemporaryModSettings(selection.Mod!, selection.Settings); + temporarySettings.ForceInherit = false; + temporarySettings.Priority = new ModPriority(_currentPriority.Value); + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, + temporarySettings); + } + else + { + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod!, + new ModPriority(_currentPriority.Value)); + } + } + + _currentPriority = null; + } + + var hovered = LunaStyle.DrawHelpMarker(); + Im.Line.SameInner(); + Im.Text("Priority"u8); + if (hovered || Im.Item.Hovered()) + Im.Tooltip.Set("Mods with a higher number here take precedence before Mods with a lower number.\n"u8 + + "That means, if Mod A should overwrite changes from Mod B, Mod A should have a higher priority number than Mod B."u8); + } + + /// + /// Draw a button to remove the current settings and inherit them instead + /// in the top-right corner of the window/tab. + /// + private void DrawRemoveSettings() + { + var drawInherited = !_inherited && !selection.Settings.IsEmpty; + var scroll = Im.Scroll.MaximumY > 0 ? Im.Style.ScrollbarSize + Im.Style.ItemInnerSpacing.X : 0; + var buttonSize = Im.Font.CalculateSize("Turn Permanent_"u8).X; + var offset = drawInherited + ? buttonSize + Im.Font.CalculateSize("Inherit Settings"u8).X + Im.Style.FramePadding.X * 4 + Im.Style.ItemSpacing.X + : buttonSize + Im.Style.FramePadding.X * 2; + Im.Line.Same(Im.Window.Width - offset - scroll); + var enabled = config.DeleteModModifier.IsActive(); + if (drawInherited) + { + var inherit = (enabled, _locked) switch + { + (true, false) => ImEx.Button("Inherit Settings"u8, + "Remove current settings from this collection so that it can inherit them.\n"u8 + + "If no inherited collection has settings for this mod, it will be disabled."u8), + (false, false) => ImEx.Button("Inherit Settings"u8, default, + $"Remove current settings from this collection so that it can inherit them.\nHold {config.DeleteModModifier} to inherit.", + true), + (_, true) => ImEx.Button("Inherit Settings"u8, default, + "Remove current settings from this collection so that it can inherit them.\nThe settings are currently locked and can not be changed."u8, + true), + }; + if (inherit) + { + if (_temporary || config.DefaultTemporaryMode) + { + var temporarySettings = selection.TemporarySettings ?? new TemporaryModSettings(selection.Mod!, selection.Settings); + temporarySettings.ForceInherit = true; + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, + temporarySettings); + } + else + { + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true); + } + } + + Im.Line.Same(); + } + + if (_temporary) + { + var overwrite = enabled + ? ImEx.Button("Turn Permanent"u8, new Vector2(buttonSize, 0), + "Overwrite the actual settings for this mod in this collection with the current temporary settings."u8) + : ImEx.Button("Turn Permanent"u8, new Vector2(buttonSize, 0), + $"Overwrite the actual settings for this mod in this collection with the current temporary settings.\nHold {config.DeleteModModifier} to overwrite.", + true); + if (overwrite) + { + var settings = collectionManager.Active.Current.GetTempSettings(selection.Mod!.Index)!; + if (settings.ForceInherit) + { + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod, true); + } + else + { + collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod, settings.Enabled); + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod, settings.Priority); + foreach (var (index, setting) in settings.Settings.Index()) + collectionManager.Editor.SetModSetting(collectionManager.Active.Current, selection.Mod, index, setting); + } + + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod, null); + } + } + else + { + var actual = collectionManager.Active.Current.GetActualSettings(selection.Mod!.Index).Settings; + if (ImEx.Button("Turn Temporary"u8, "Copy the current settings over to temporary settings to experiment with them."u8)) + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, + new TemporaryModSettings(selection.Mod!, actual)); + } + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 3ebe7999..9a7b6c19 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -3,6 +3,7 @@ using Luna; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.UI.AdvancedWindow; +using Penumbra.UI.Classes; using ImGuiColor = ImSharp.ImGuiColor; namespace Penumbra.UI.ModsTab; @@ -31,7 +32,7 @@ public class ModPanelTabBar : TabBar TutorialService tutorial, ModPanelCollectionsTab collections, Logger log) : base(nameof(ModPanelTabBar), log, settings, description, conflicts, changedItems, collections, edit) { - Flags = TabBarFlags.NoTooltip; + Flags = TabBarFlags.NoTooltip | TabBarFlags.FittingPolicyScroll; Settings = settings; Edit = edit; _modManager = modManager; diff --git a/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs b/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs index f542c940..f9f1b33f 100644 --- a/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs +++ b/Penumbra/UI/ModsTab/ModSearchStringSplitter.cs @@ -2,6 +2,7 @@ using OtterGui.Filesystem; using OtterGui.Filesystem.Selector; using Penumbra.Mods; using Penumbra.Mods.Manager; +using Penumbra.UI.Classes; namespace Penumbra.UI.ModsTab; diff --git a/Penumbra/UI/ModsTab/MultiModPanel.cs b/Penumbra/UI/ModsTab/MultiModPanel.cs index 83c9d63c..52f1eef1 100644 --- a/Penumbra/UI/ModsTab/MultiModPanel.cs +++ b/Penumbra/UI/ModsTab/MultiModPanel.cs @@ -1,16 +1,12 @@ -using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Utility; using ImSharp; using Luna; -using OtterGui.Raii; -using OtterGui.Text; using Penumbra.Mods; using Penumbra.Mods.Manager; namespace Penumbra.UI.ModsTab; -public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor, PredefinedTagManager tagManager) : Luna.IUiService +public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor, PredefinedTagManager tagManager) : IUiService { public void Draw() { @@ -18,7 +14,7 @@ public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor, return; Im.Line.New(); - var treeNodePos = ImGui.GetCursorPos(); + var treeNodePos = Im.Cursor.Position; var numLeaves = DrawModList(); DrawCounts(treeNodePos, numLeaves); DrawMultiTagger(); @@ -26,24 +22,24 @@ public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor, private void DrawCounts(Vector2 treeNodePos, int numLeaves) { - var startPos = ImGui.GetCursorPos(); + var startPos = Im.Cursor.Position; var numFolders = selector.SelectedPaths.Count - numLeaves; - var text = (numLeaves, numFolders) switch + Utf8StringHandler text = (numLeaves, numFolders) switch { - (0, 0) => string.Empty, // should not happen + (0, 0) => StringU8.Empty, // should not happen (> 0, 0) => $"{numLeaves} Mods", (0, > 0) => $"{numFolders} Folders", _ => $"{numLeaves} Mods, {numFolders} Folders", - }; - ImGui.SetCursorPos(treeNodePos); - ImUtf8.TextRightAligned(text); - ImGui.SetCursorPos(startPos); + }; + Im.Cursor.Position = treeNodePos; + ImEx.TextRightAligned(ref text); + Im.Cursor.Position = startPos; } private int DrawModList() { - using var tree = ImUtf8.TreeNode("Currently Selected Objects###Selected"u8, - ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.NoTreePushOnOpen); + using var tree = Im.Tree.Node("Currently Selected Objects###Selected"u8, + TreeNodeFlags.DefaultOpen | TreeNodeFlags.NoTreePushOnOpen); Im.Separator(); @@ -69,16 +65,16 @@ public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor, foreach (var (fullName, path) in selector.SelectedPaths.Select(p => (p.FullName(), p)) .OrderBy(p => p.Item1, StringComparer.OrdinalIgnoreCase)) { - using var id = ImRaii.PushId(i++); + using var id = Im.Id.Push(i++); var (icon, text) = path is ModFileSystem.Leaf l - ? (FontAwesomeIcon.FileCircleMinus, l.Value.Name) - : (FontAwesomeIcon.FolderMinus, string.Empty); - ImGui.TableNextColumn(); - if (ImUtf8.IconButton(icon, "Remove from selection."u8, sizeType)) + ? (FontAwesomeIcon.FileCircleMinus.Icon(), l.Value.Name) + : (FontAwesomeIcon.FolderMinus.Icon(), string.Empty); + table.NextColumn(); + if (ImEx.Icon.Button(icon, "Remove from selection."u8, false, sizeType)) selector.RemovePathFromMultiSelection(path); - - ImUtf8.DrawFrameColumn(text); - ImUtf8.DrawFrameColumn(fullName); + + table.DrawFrameColumn(text); + table.DrawFrameColumn(fullName); if (path is ModFileSystem.Leaf) ++leaves; } @@ -95,7 +91,7 @@ public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor, private void DrawMultiTagger() { var width = ImEx.ScaledVector(150, 0); - ImUtf8.TextFrameAligned("Multi Tagger:"u8); + ImEx.TextFrameAligned("Multi Tagger:"u8); Im.Line.Same(); var predefinedTagsEnabled = tagManager.Enabled; @@ -103,32 +99,32 @@ public class MultiModPanel(ModFileSystemSelector selector, ModDataEditor editor, ? Im.ContentRegion.Available.X - 2 * width.X - 3 * Im.Style.ItemInnerSpacing.X - Im.Style.FrameHeight : Im.ContentRegion.Available.X - 2 * (width.X + Im.Style.ItemInnerSpacing.X); Im.Item.SetNextWidth(inputWidth); - ImUtf8.InputText("##tag"u8, ref _tag, "Local Tag Name..."u8); + Im.Input.Text("##tag"u8, ref _tag, "Local Tag Name..."u8); UpdateTagCache(); - var label = _addMods.Count > 0 + Utf8StringHandler label = _addMods.Count > 0 ? $"Add to {_addMods.Count} Mods" : "Add"; - var tooltip = _addMods.Count == 0 - ? _tag.Length == 0 + Utf8StringHandler tooltip = _addMods.Count is 0 + ? _tag.Length is 0 ? "No tag specified." : $"All mods selected already contain the tag \"{_tag}\", either locally or as mod data." : $"Add the tag \"{_tag}\" to {_addMods.Count} mods as a local tag:\n\n\t{string.Join("\n\t", _addMods.Select(m => m.Name))}"; Im.Line.SameInner(); - if (ImUtf8.ButtonEx(label, tooltip, width, _addMods.Count == 0)) + if (ImEx.Button(label, width, tooltip, _addMods.Count is 0)) foreach (var mod in _addMods) editor.ChangeLocalTag(mod, mod.LocalTags.Count, _tag); label = _removeMods.Count > 0 ? $"Remove from {_removeMods.Count} Mods" : "Remove"; - tooltip = _removeMods.Count == 0 - ? _tag.Length == 0 + tooltip = _removeMods.Count is 0 + ? _tag.Length is 0 ? "No tag specified." : $"No selected mod contains the tag \"{_tag}\" locally." : $"Remove the local tag \"{_tag}\" from {_removeMods.Count} mods:\n\n\t{string.Join("\n\t", _removeMods.Select(m => m.Item1.Name))}"; Im.Line.SameInner(); - if (ImUtf8.ButtonEx(label, tooltip, width, _removeMods.Count == 0)) + if (ImEx.Button(label, width, tooltip, _removeMods.Count is 0)) foreach (var (mod, index) in _removeMods) editor.ChangeLocalTag(mod, index, string.Empty); diff --git a/Penumbra/UI/ModsTab/Selector/AddNewModButton.cs b/Penumbra/UI/ModsTab/Selector/AddNewModButton.cs new file mode 100644 index 00000000..1b6e8404 --- /dev/null +++ b/Penumbra/UI/ModsTab/Selector/AddNewModButton.cs @@ -0,0 +1,97 @@ +using ImSharp; +using Luna; + +namespace Penumbra.UI.ModsTab.Selector; + +/// The button to add a new, empty mod. +/// The file system drawer. +public sealed class AddNewModButton(ModFileSystemDrawer drawer) : BaseIconButton +{ + /// + public override AwesomeIcon Icon + => LunaStyle.AddObjectIcon; + + /// + public override bool HasTooltip + => true; + + /// + public override void DrawTooltip() + => Im.Text("Create a new, empty mod of a given name."u8); + + /// + public override void OnClick() + => Im.Popup.Open("Create New Mod"u8); + + /// + protected override void PostDraw() + { + if (!InputPopup.OpenName("Create New Mod"u8, out var newModName)) + return; + + if (drawer.ModManager.Creator.CreateEmptyMod(drawer.ModManager.BasePath, newModName) is { } directory) + drawer.ModManager.AddMod(directory, false); + } +} + +/// The button to import a mod. +/// The file system drawer. +public sealed class ImportModButton(ModFileSystemDrawer drawer) : BaseIconButton +{ + /// + public override AwesomeIcon Icon + => LunaStyle.AddObjectIcon; + + /// + public override bool HasTooltip + => true; + + /// + public override void DrawTooltip() + => Im.Text("Create a new, empty mod of a given name."u8); + + /// + public override void OnClick() + => Im.Popup.Open("Create New Mod"u8); + + /// + protected override void PostDraw() + { + if (!InputPopup.OpenName("Create New Mod"u8, out var newModName)) + return; + + if (drawer.ModManager.Creator.CreateEmptyMod(drawer.ModManager.BasePath, newModName) is { } directory) + drawer.ModManager.AddMod(directory, false); + } +} + +/// The button to import a mod. +/// The file system drawer. +public sealed class DeleteSelectionButton(ModFileSystemDrawer drawer) : BaseIconButton +{ + /// + public override AwesomeIcon Icon + => LunaStyle.AddObjectIcon; + + /// + public override bool HasTooltip + => true; + + /// + public override void DrawTooltip() + => Im.Text("Create a new, empty mod of a given name."u8); + + /// + public override void OnClick() + => Im.Popup.Open("Create New Mod"u8); + + /// + protected override void PostDraw() + { + if (!InputPopup.OpenName("Create New Mod"u8, out var newModName)) + return; + + if (drawer.ModManager.Creator.CreateEmptyMod(drawer.ModManager.BasePath, newModName) is { } directory) + drawer.ModManager.AddMod(directory, false); + } +} diff --git a/Penumbra/UI/ModsTab/Selector/ClearDefaultImportFolderButton.cs b/Penumbra/UI/ModsTab/Selector/ClearDefaultImportFolderButton.cs new file mode 100644 index 00000000..73421a03 --- /dev/null +++ b/Penumbra/UI/ModsTab/Selector/ClearDefaultImportFolderButton.cs @@ -0,0 +1,22 @@ +using Luna; + +namespace Penumbra.UI.ModsTab.Selector; + +/// The menu item to clear the default import folder. +/// The file system drawer. +public sealed class ClearDefaultImportFolderButton(ModFileSystemDrawer drawer) : BaseButton +{ + /// + public override ReadOnlySpan Label + => "Clear Default Import Folder"u8; + + /// + public override void OnClick() + { + if (drawer.Config.DefaultImportFolder.Length is 0) + return; + + drawer.Config.DefaultImportFolder = string.Empty; + drawer.Config.Save(); + } +} diff --git a/Penumbra/UI/ModsTab/Selector/ClearTemporarySettingsButton.cs b/Penumbra/UI/ModsTab/Selector/ClearTemporarySettingsButton.cs new file mode 100644 index 00000000..048e9004 --- /dev/null +++ b/Penumbra/UI/ModsTab/Selector/ClearTemporarySettingsButton.cs @@ -0,0 +1,16 @@ +using Luna; + +namespace Penumbra.UI.ModsTab.Selector; + +/// The menu item to clear all temporary settings of the current collection. +/// The file system drawer. +public sealed class ClearTemporarySettingsButton(ModFileSystemDrawer drawer) : BaseButton +{ + /// + public override ReadOnlySpan Label + => "Clear Temporary Settings"u8; + + /// + public override void OnClick() + => drawer.CollectionManager.Editor.ClearTemporarySettings(drawer.CollectionManager.Active.Current); +} diff --git a/Penumbra/UI/ModsTab/Selector/EnableDescendantsButton.cs b/Penumbra/UI/ModsTab/Selector/EnableDescendantsButton.cs new file mode 100644 index 00000000..8aab254c --- /dev/null +++ b/Penumbra/UI/ModsTab/Selector/EnableDescendantsButton.cs @@ -0,0 +1,27 @@ +using ImSharp; +using Luna; + +namespace Penumbra.UI.ModsTab.Selector; + +/// The menu items to set all descendants of a folder enabled or disabled. +/// The file system drawer. +/// Whether the drawer should enable or disable the descendants. +/// Whether the drawer should inherit all descendants instead of enabling or disabling them. +public sealed class SetDescendantsButton(ModFileSystemDrawer drawer, bool setTo, bool inherit = false) : BaseButton +{ + private readonly StringU8 _label = new((inherit, setTo) switch + { + (true, true) => "Inherit Descendants"u8, + (true, false) => "Stop Inheriting Descendants"u8, + (_, true) => "Enable Descendants"u8, + (_, false) => "Disable Descendants"u8, + }); + + /// + public override ReadOnlySpan Label(in IFileSystemFolder folder) + => _label; + + /// + public override void OnClick(in IFileSystemFolder folder) + => drawer.SetDescendants(folder, setTo, inherit); +} diff --git a/Penumbra/UI/ModsTab/Selector/ModFileSystemCache.cs b/Penumbra/UI/ModsTab/Selector/ModFileSystemCache.cs new file mode 100644 index 00000000..5518cc8b --- /dev/null +++ b/Penumbra/UI/ModsTab/Selector/ModFileSystemCache.cs @@ -0,0 +1,15 @@ +using Luna; + +namespace Penumbra.UI.ModsTab.Selector; + +public sealed class ModFileSystemCache(ModFileSystemDrawer parent) + : FileSystemCache(parent), IService +{ + public sealed class ModData : BaseFileSystemNodeCache; + + public override void Update() + { } + + protected override ModData ConvertNode(in IFileSystemNode node) + => new(); +} diff --git a/Penumbra/UI/ModsTab/Selector/ModFileSystemDrawer.cs b/Penumbra/UI/ModsTab/Selector/ModFileSystemDrawer.cs new file mode 100644 index 00000000..92f8eec8 --- /dev/null +++ b/Penumbra/UI/ModsTab/Selector/ModFileSystemDrawer.cs @@ -0,0 +1,57 @@ +using Luna; +using Penumbra.Collections.Manager; +using Penumbra.Mods; +using Penumbra.Mods.Manager; + +namespace Penumbra.UI.ModsTab.Selector; + +public sealed class ModFileSystemDrawer : FileSystemDrawer +{ + public readonly ModManager ModManager; + public readonly CollectionManager CollectionManager; + public readonly Configuration Config; + + public ModFileSystemDrawer(ModFileSystem2 fileSystem, ModManager modManager, CollectionManager collectionManager, Configuration config) + : base(fileSystem, null) + { + ModManager = modManager; + CollectionManager = collectionManager; + Config = config; + + MainContext.AddButton(new ClearTemporarySettingsButton(this), 105); + MainContext.AddButton(new ClearDefaultImportFolderButton(this), 10); + + FolderContext.AddButton(new SetDescendantsButton(this, true), 11); + FolderContext.AddButton(new SetDescendantsButton(this, false), 10); + FolderContext.AddButton(new SetDescendantsButton(this, true, true), 6); + FolderContext.AddButton(new SetDescendantsButton(this, false, true), 5); + FolderContext.AddButton(new SetDefaultImportFolderButton(this), -100); + + DataContext.AddButton(new ToggleFavoriteButton(this), 10); + + Footer.Buttons.AddButton(new AddNewModButton(this), 1000); + + } + + public override ReadOnlySpan Id + => "ModFileSystem"u8; + + protected override FileSystemCache CreateCache() + => new ModFileSystemCache(this); + + + public void SetDescendants(IFileSystemFolder folder, bool enabled, bool inherit = false) + { + var mods = folder.GetDescendants().OfType>().Select(l => + { + // Any mod handled here should not stay new. + ModManager.SetKnown(l.Value); + return l.Value; + }); + + if (inherit) + CollectionManager.Editor.SetMultipleModInheritances(CollectionManager.Active.Current, mods, enabled); + else + CollectionManager.Editor.SetMultipleModStates(CollectionManager.Active.Current, mods, enabled); + } +} diff --git a/Penumbra/UI/ModsTab/Selector/SetDefaultImportFolderButton.cs b/Penumbra/UI/ModsTab/Selector/SetDefaultImportFolderButton.cs new file mode 100644 index 00000000..42ddacfc --- /dev/null +++ b/Penumbra/UI/ModsTab/Selector/SetDefaultImportFolderButton.cs @@ -0,0 +1,22 @@ +using Luna; + +namespace Penumbra.UI.ModsTab.Selector; + +/// The menu item to set a given folder as default import folder. +/// The file system drawer. +public sealed class SetDefaultImportFolderButton(ModFileSystemDrawer drawer) : BaseButton +{ + /// + public override ReadOnlySpan Label(in IFileSystemFolder _) + => "Set As Default Import Folder"u8; + + /// + public override void OnClick(in IFileSystemFolder folder) + { + if (folder.FullPath == drawer.Config.DefaultImportFolder) + return; + + drawer.Config.DefaultImportFolder = folder.FullPath; + drawer.Config.Save(); + } +} diff --git a/Penumbra/UI/ModsTab/Selector/SortModes.cs b/Penumbra/UI/ModsTab/Selector/SortModes.cs new file mode 100644 index 00000000..8cd3feef --- /dev/null +++ b/Penumbra/UI/ModsTab/Selector/SortModes.cs @@ -0,0 +1,63 @@ +using System.Collections.Frozen; +using Luna; +using Penumbra.Mods; + +namespace Penumbra.UI.ModsTab.Selector; + +public readonly struct ImportDate : ISortMode +{ + public static readonly ImportDate Instance = new(); + + public ReadOnlySpan Name + => "Import Date (Older First)"u8; + + public ReadOnlySpan Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."u8; + + public IEnumerable GetChildren(IFileSystemFolder f) + => f.GetSubFolders().Cast().Concat(f.GetLeaves().OfType>().OrderBy(l => l.Value.ImportDate)); +} + +public readonly struct InverseImportDate : ISortMode +{ + public static readonly InverseImportDate Instance = new(); + + public ReadOnlySpan Name + => "Import Date (Newer First)"u8; + + public ReadOnlySpan Description + => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."u8; + + public IEnumerable GetChildren(IFileSystemFolder f) + => f.GetSubFolders().Cast() + .Concat(f.GetLeaves().OfType>().OrderByDescending(l => l.Value.ImportDate)); +} + +public static class SortModeExtensions +{ + private static readonly FrozenDictionary ValidSortModes = new Dictionary + { + [nameof(ISortMode.FoldersFirst)] = ISortMode.FoldersFirst, + [nameof(ISortMode.Lexicographical)] = ISortMode.Lexicographical, + [nameof(ImportDate)] = ISortMode.ImportDate, + [nameof(InverseImportDate)] = ISortMode.InverseImportDate, + [nameof(ISortMode.InverseFoldersFirst)] = ISortMode.InverseFoldersFirst, + [nameof(ISortMode.InverseLexicographical)] = ISortMode.InverseLexicographical, + [nameof(ISortMode.FoldersLast)] = ISortMode.FoldersLast, + [nameof(ISortMode.InverseFoldersLast)] = ISortMode.InverseFoldersLast, + [nameof(ISortMode.InternalOrder)] = ISortMode.InternalOrder, + [nameof(ISortMode.InverseInternalOrder)] = ISortMode.InverseInternalOrder, + }.ToFrozenDictionary(); + + extension(ISortMode) + { + public static ISortMode ImportDate + => ImportDate.Instance; + + public static ISortMode InverseImportDate + => InverseImportDate.Instance; + + public static IReadOnlyDictionary Valid + => ValidSortModes; + } +} diff --git a/Penumbra/UI/ModsTab/Selector/ToggleFavoriteButton.cs b/Penumbra/UI/ModsTab/Selector/ToggleFavoriteButton.cs new file mode 100644 index 00000000..141b67b1 --- /dev/null +++ b/Penumbra/UI/ModsTab/Selector/ToggleFavoriteButton.cs @@ -0,0 +1,17 @@ +using Luna; +using Penumbra.Mods; + +namespace Penumbra.UI.ModsTab.Selector; + +/// The menu item to set toggle a mod's favourite state. +/// The file system drawer. +public sealed class ToggleFavoriteButton(ModFileSystemDrawer drawer) : BaseButton +{ + /// + public override ReadOnlySpan Label(in IFileSystemData data) + => ((Mod)data.Value).Favorite ? "Remove Favorite"u8 : "Mark as Favorite"u8; + + /// + public override void OnClick(in IFileSystemData data) + => drawer.ModManager.DataEditor.ChangeModFavorite((Mod)data.Value, !((Mod)data.Value).Favorite); +} diff --git a/Penumbra/UI/ResourceWatcher/Record.cs b/Penumbra/UI/ResourceWatcher/Record.cs index 1821d90d..ec2e0f9b 100644 --- a/Penumbra/UI/ResourceWatcher/Record.cs +++ b/Penumbra/UI/ResourceWatcher/Record.cs @@ -1,4 +1,6 @@ +using ImSharp; using Luna; +using Luna.Generators; using Penumbra.Collections; using Penumbra.Enums; using Penumbra.Interop; @@ -9,21 +11,31 @@ using Penumbra.String.Classes; namespace Penumbra.UI.ResourceWatcher; [Flags] +[NamedEnum(Utf16: false)] public enum RecordType : byte { - Request = 0x01, - ResourceLoad = 0x02, - FileLoad = 0x04, - Destruction = 0x08, + [Name("REQ")] + Request = 0x01, + + [Name("LOAD")] + ResourceLoad = 0x02, + + [Name("FILE")] + FileLoad = 0x04, + + [Name("DEST")] + Destruction = 0x08, + + [Name("DONE")] ResourceComplete = 0x10, } -internal unsafe struct Record +internal unsafe struct Record() { public DateTime Time; - public CiByteString Path; - public CiByteString OriginalPath; - public string AssociatedGameObject; + public StringU8 Path; + public StringU8 OriginalPath; + public string AssociatedGameObject = string.Empty; public ModCollection? Collection; public ResourceHandle* Handle; public ResourceTypeFlag ResourceType; @@ -42,8 +54,8 @@ internal unsafe struct Record => new() { Time = DateTime.UtcNow, - Path = path.IsOwned ? path : path.Clone(), - OriginalPath = CiByteString.Empty, + Path = new StringU8(path.Span, false), + OriginalPath = StringU8.Empty, Collection = null, Handle = null, ResourceType = ResourceExtensions.Type(path).ToFlag(), @@ -63,8 +75,8 @@ internal unsafe struct Record => new() { Time = DateTime.UtcNow, - Path = fullPath.InternalName.IsOwned ? fullPath.InternalName : fullPath.InternalName.Clone(), - OriginalPath = path.IsOwned ? path : path.Clone(), + Path = new StringU8(fullPath.InternalName.Span, false), + OriginalPath = new StringU8(path.Span, false), Collection = resolve.Valid ? resolve.ModCollection : null, Handle = null, ResourceType = ResourceExtensions.Type(path).ToFlag(), @@ -82,12 +94,12 @@ internal unsafe struct Record public static Record CreateDefaultLoad(CiByteString path, ResourceHandle* handle, ModCollection collection, string associatedGameObject) { - path = path.IsOwned ? path : path.Clone(); + var p = new StringU8(path.Span, false); return new Record { Time = DateTime.UtcNow, - Path = path, - OriginalPath = path, + Path = p, + OriginalPath = p, Collection = collection, Handle = handle, ResourceType = handle->FileType.ToFlag(), @@ -109,8 +121,8 @@ internal unsafe struct Record => new() { Time = DateTime.UtcNow, - Path = path.InternalName.IsOwned ? path.InternalName : path.InternalName.Clone(), - OriginalPath = originalPath.IsOwned ? originalPath : originalPath.Clone(), + Path = new StringU8(path.InternalName.Span, false), + OriginalPath = new StringU8(originalPath.Span, false), Collection = collection, Handle = handle, ResourceType = handle->FileType.ToFlag(), @@ -128,12 +140,12 @@ internal unsafe struct Record public static Record CreateDestruction(ResourceHandle* handle) { - var path = handle->FileName().Clone(); + var path = new StringU8(handle->FileName().Span, false); return new Record { Time = DateTime.UtcNow, Path = path, - OriginalPath = CiByteString.Empty, + OriginalPath = StringU8.Empty, Collection = null, Handle = handle, ResourceType = handle->FileType.ToFlag(), @@ -154,8 +166,8 @@ internal unsafe struct Record => new() { Time = DateTime.UtcNow, - Path = path.IsOwned ? path : path.Clone(), - OriginalPath = CiByteString.Empty, + Path = new StringU8(path.Span, false), + OriginalPath = StringU8.Empty, Collection = null, Handle = handle, ResourceType = handle->FileType.ToFlag(), @@ -171,12 +183,13 @@ internal unsafe struct Record OsThreadId = ProcessThreadApi.GetCurrentThreadId(), }; - public static Record CreateResourceComplete(CiByteString path, ResourceHandle* handle, Utf8GamePath originalPath, ReadOnlySpan additionalData) + public static Record CreateResourceComplete(CiByteString path, ResourceHandle* handle, Utf8GamePath originalPath, + ReadOnlySpan additionalData) => new() { Time = DateTime.UtcNow, Path = CombinedPath(path, additionalData), - OriginalPath = originalPath.Path.IsOwned ? originalPath.Path : originalPath.Path.Clone(), + OriginalPath = new StringU8(originalPath.Path.Span, false), Collection = null, Handle = handle, ResourceType = handle->FileType.ToFlag(), @@ -192,16 +205,16 @@ internal unsafe struct Record OsThreadId = ProcessThreadApi.GetCurrentThreadId(), }; - private static CiByteString CombinedPath(CiByteString path, ReadOnlySpan additionalData) + private static StringU8 CombinedPath(CiByteString path, ReadOnlySpan additionalData) { if (additionalData.Length is 0) - return path.IsOwned ? path : path.Clone(); + return new StringU8(path.Span, false); fixed (byte* ptr = additionalData) { // If a path has additional data and is split, it is always in the form of |{additionalData}|{path}, // so we can just read from the start of additional data - 1 and sum their length +2 for the pipes. - return new CiByteString(new ReadOnlySpan(ptr - 1, additionalData.Length + 2 + path.Length)).Clone(); + return new StringU8(new ReadOnlySpan(ptr - 1, additionalData.Length + 2 + path.Length)); } } } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index ae43c0bd..d931799c 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -1,7 +1,7 @@ -using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using ImSharp; +using ImSharp.Containers; using Luna; using Penumbra.Api.Enums; using Penumbra.Collections; @@ -27,7 +27,7 @@ public sealed class ResourceWatcher : IDisposable, ITab private readonly ResourceLoader _loader; private readonly ResourceHandleDestructor _destructor; private readonly ActorManager _actors; - private readonly List _records = []; + private readonly ObservableList _records = []; private readonly ConcurrentQueue _newRecords = []; private readonly ResourceWatcherTable _table; private string _logFilter = string.Empty; @@ -43,7 +43,7 @@ public sealed class ResourceWatcher : IDisposable, ITab _resources = resources; _destructor = destructor; _loader = loader; - _table = new ResourceWatcherTable(config.Ephemeral, _records); + _table = new ResourceWatcherTable(new ResourceWatcherConfig(), _records); _resources.ResourceRequested += OnResourceRequested; _destructor.Subscribe(OnResourceDestroyed, ResourceHandleDestructor.Priority.ResourceWatcher); _loader.ResourceLoaded += OnResourceLoaded; @@ -71,7 +71,7 @@ public sealed class ResourceWatcher : IDisposable, ITab ? Record.CreateRequest(original.Path, false, _1.Value, _2) : Record.CreateRequest(original.Path, false); if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) - _newRecords.Enqueue(record); + Enqueue(record); } public unsafe void Dispose() @@ -90,7 +90,6 @@ public sealed class ResourceWatcher : IDisposable, ITab { _records.Clear(); _newRecords.Clear(); - _table.Reset(); } public ReadOnlySpan Label @@ -103,7 +102,7 @@ public sealed class ResourceWatcher : IDisposable, ITab { UpdateRecords(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + Im.Style.TextHeightWithSpacing / 2); + Im.Cursor.Y += Im.Style.TextHeightWithSpacing / 2; var isEnabled = _ephemeral.EnableResourceWatcher; if (Im.Checkbox("Enable"u8, ref isEnabled)) { @@ -138,7 +137,7 @@ public sealed class ResourceWatcher : IDisposable, ITab Im.Cursor.Y += Im.Style.TextHeightWithSpacing / 2; - _table.Draw(Im.Style.TextHeightWithSpacing); + _table.Draw(); } private void DrawFilterInput() @@ -184,8 +183,8 @@ public sealed class ResourceWatcher : IDisposable, ITab { Im.Item.SetNextWidthScaled(80); Im.Input.Scalar("Max. Entries"u8, ref _newMaxEntries); - var change = ImGui.IsItemDeactivatedAfterEdit(); - if (Im.Item.RightClicked() && ImGui.GetIO().KeyCtrl) + var change = Im.Item.DeactivatedAfterEdit; + if (Im.Item.RightClicked() && Im.Io.KeyControl) { change = true; _newMaxEntries = DefaultMaxEntries; @@ -193,7 +192,7 @@ public sealed class ResourceWatcher : IDisposable, ITab var maxEntries = _config.MaxResourceWatcherRecords; if (maxEntries != DefaultMaxEntries && Im.Item.Hovered()) - ImGui.SetTooltip($"CTRL + Right-Click to reset to default {DefaultMaxEntries}."); + Im.Tooltip.Set($"CTRL + Right-Click to reset to default {DefaultMaxEntries}."); if (!change) return; @@ -219,8 +218,6 @@ public sealed class ResourceWatcher : IDisposable, ITab if (_records.Count > _config.MaxResourceWatcherRecords) _records.RemoveRange(0, _records.Count - _config.MaxResourceWatcherRecords); - - _table.Reset(); } @@ -235,7 +232,7 @@ public sealed class ResourceWatcher : IDisposable, ITab var record = Record.CreateRequest(original.Path, sync); if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) - _newRecords.Enqueue(record); + Enqueue(record); } private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data) @@ -262,7 +259,7 @@ public sealed class ResourceWatcher : IDisposable, ITab ? Record.CreateDefaultLoad(path.Path, handle, data.ModCollection, Name(data)) : Record.CreateLoad(manipulatedPath.Value, path.Path, handle, data.ModCollection, Name(data)); if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) - _newRecords.Enqueue(record); + Enqueue(record); } private unsafe void OnResourceComplete(ResourceHandle* resource, CiByteString path, Utf8GamePath original, @@ -280,7 +277,7 @@ public sealed class ResourceWatcher : IDisposable, ITab var record = Record.CreateResourceComplete(path, resource, original, additionalData); if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) - _newRecords.Enqueue(record); + Enqueue(record); } private unsafe void OnFileLoaded(ResourceHandle* resource, CiByteString path, bool success, bool custom, ReadOnlySpan _) @@ -294,7 +291,7 @@ public sealed class ResourceWatcher : IDisposable, ITab var record = Record.CreateFileLoad(path, resource, success, custom); if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) - _newRecords.Enqueue(record); + Enqueue(record); } private unsafe void OnResourceDestroyed(in ResourceHandleDestructor.Arguments arguments) @@ -308,7 +305,7 @@ public sealed class ResourceWatcher : IDisposable, ITab var record = Record.CreateDestruction(arguments.ResourceHandle); if (!_ephemeral.OnlyAddMatchingResources || _table.WouldBeVisible(record)) - _newRecords.Enqueue(record); + Enqueue(record); } public unsafe string Name(ResolveData resolve, string none = "") @@ -336,4 +333,15 @@ public sealed class ResourceWatcher : IDisposable, ITab return $"0x{resolve.AssociatedGameObject:X}"; } + + private void Enqueue(Record record) + { + lock (_newRecords) + { + // Discard entries that exceed the number of records. + while (_newRecords.Count >= _config.MaxResourceWatcherRecords) + _newRecords.TryDequeue(out _); + _newRecords.Enqueue(record); + } + } } diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs index 52fd14b9..252dcbdb 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcherTable.cs @@ -1,57 +1,110 @@ -using Dalamud.Bindings.ImGui; using Dalamud.Interface; using ImSharp; +using ImSharp.Containers; +using ImSharp.Table; using Luna; -using OtterGui.Table; using Penumbra.Enums; using Penumbra.Interop.Structs; -using Penumbra.String; using Penumbra.UI.Classes; namespace Penumbra.UI.ResourceWatcher; -internal sealed class ResourceWatcherTable : Table +public class ResourceWatcherConfig { - public ResourceWatcherTable(EphemeralConfig config, IReadOnlyCollection records) - : base("##records", - records, - new PathColumn { Label = "Path" }, - new RecordTypeColumn(config) { Label = "Record" }, - new CollectionColumn { Label = "Collection" }, - new ObjectColumn { Label = "Game Object" }, - new CustomLoadColumn { Label = "Custom" }, - new SynchronousLoadColumn { Label = "Sync" }, - new OriginalPathColumn { Label = "Original Path" }, - new ResourceCategoryColumn(config) { Label = "Category" }, - new ResourceTypeColumn(config) { Label = "Type" }, - new HandleColumn { Label = "Resource" }, - new LoadStateColumn { Label = "State" }, - new RefCountColumn { Label = "#Ref" }, - new DateColumn { Label = "Time" }, - new Crc64Column { Label = "Crc64" }, - new OsThreadColumn { Label = "TID" } - ) + public int Version = 1; + public bool Enabled = false; + public int MaxEntries = 500; + public bool StoreOnlyMatching = true; + public bool WriteToLog = false; + public string LogFilter = string.Empty; + public string PathFilter = string.Empty; + public string CollectionFilter = string.Empty; + public string ObjectFilter = string.Empty; + public string OriginalPathFilter = string.Empty; + public string ResourceFilter = string.Empty; + public string CrcFilter = string.Empty; + public string RefFilter = string.Empty; + public string ThreadFilter = string.Empty; + public RecordType RecordFilter = Enum.GetValues().Or(); + public BoolEnum CustomFilter = BoolEnum.True | BoolEnum.False | BoolEnum.Unknown; + public BoolEnum SyncFilter = BoolEnum.True | BoolEnum.False | BoolEnum.Unknown; + public ResourceCategoryFlag CategoryFilter = ResourceExtensions.AllResourceCategories; + public ResourceTypeFlag TypeFilter = ResourceExtensions.AllResourceTypes; + public LoadStateFlag LoadStateFilter = Enum.GetValues().Or(); + + public void Save() { } +} - public void Reset() - => FilterDirty = true; +[Flags] +public enum BoolEnum : byte +{ + True = 0x01, + False = 0x02, + Unknown = 0x04, +} - private sealed class PathColumn : ColumnString +[Flags] +public enum LoadStateFlag : byte +{ + Success = 0x01, + Async = 0x02, + Failed = 0x04, + FailedSub = 0x08, + Unknown = 0x10, + None = 0xFF, +} + +internal sealed unsafe class CachedRecord(Record record) +{ + public readonly Record Record = record; + public readonly string PathU16 = record.Path.ToString(); + public readonly StringU8 TypeName = new(record.RecordType.ToName()); + public readonly StringU8 Time = new($"{record.Time.ToLongTimeString()}.{record.Time.Millisecond:D4}"); + public readonly StringPair Crc64 = new($"{record.Crc64:X16}"); + public readonly StringU8 Collection = record.Collection is null ? StringU8.Empty : new StringU8(record.Collection.Identity.Name); + public readonly StringU8 AssociatedGameObject = new(record.AssociatedGameObject); + public readonly string OriginalPath = record.OriginalPath.ToString(); + public readonly StringU8 ResourceCategory = new($"{record.Category}"); + public readonly StringU8 ResourceType = new(record.ResourceType.ToString().ToLowerInvariant()); + public readonly string HandleU16 = $"0x{(nint)record.Handle:X}"; + public readonly SizedStringPair Thread = new($"{record.OsThreadId}"); + public readonly SizedStringPair RefCount = new($"{record.RefCount}"); +} + +internal sealed class ResourceWatcherTable : TableBase> +{ + private readonly IReadOnlyList _records; + + public bool WouldBeVisible(Record record) { - public override float Width - => 300 * Im.Style.GlobalScale; - - public override string ToName(Record item) - => item.Path.ToString(); - - public override int Compare(Record lhs, Record rhs) - => lhs.Path.CompareTo(rhs.Path); - - public override void DrawColumn(Record item, int _) - => DrawByteString(item.Path, 280 * Im.Style.GlobalScale); + var cached = new CachedRecord(record); + return Columns.All(c => c.WouldBeVisible(cached, -1)); } - private static void DrawByteString(CiByteString path, float length) + public ResourceWatcherTable(ResourceWatcherConfig config, IReadOnlyList records) + : base(new StringU8("##records"u8), + new PathColumn(config) { Label = new StringU8("Path"u8) }, + new RecordTypeColumn(config) { Label = new StringU8("Record"u8) }, + new CollectionColumn(config) { Label = new StringU8("Collection"u8) }, + new ObjectColumn(config) { Label = new StringU8("Game Object"u8) }, + new CustomLoadColumn(config) { Label = new StringU8("Custom"u8) }, + new SynchronousLoadColumn(config) { Label = new StringU8("Sync"u8) }, + new OriginalPathColumn(config) { Label = new StringU8("Original Path"u8) }, + new ResourceCategoryColumn(config) { Label = new StringU8("Category"u8) }, + new ResourceTypeColumn(config) { Label = new StringU8("Type"u8) }, + new HandleColumn(config) { Label = new StringU8("Resource"u8) }, + new LoadStateColumn(config) { Label = new StringU8("State"u8) }, + new RefCountColumn(config) { Label = new StringU8("#Ref"u8) }, + new DateColumn { Label = new StringU8("Time"u8) }, + new Crc64Column(config) { Label = new StringU8("Crc64"u8) }, + new OsThreadColumn(config) { Label = new StringU8("TID"u8) } + ) + { + _records = records; + } + + private static void DrawByteString(StringU8 path, float length) { if (path.IsEmpty) return; @@ -64,24 +117,24 @@ internal sealed class ResourceWatcherTable : Table } else { - var fileName = path.LastIndexOf((byte)'/'); + var fileName = path.Span.LastIndexOf((byte)'/'); using (Im.Group()) { - CiByteString shortPath; - var icon = FontAwesomeIcon.EllipsisH.Icon(); + ReadOnlySpan shortPath; + var icon = FontAwesomeIcon.EllipsisH.Icon(); if (fileName is not -1) { using var font = AwesomeIcon.Font.Push(); clicked = Im.Selectable(icon.Span); Im.Line.SameInner(); - shortPath = path.Substring(fileName, path.Length - fileName); + shortPath = path.Span.Slice(fileName, path.Length - fileName); } else { shortPath = path; } - clicked |= Im.Selectable(shortPath.Span, false, SelectableFlags.AllowOverlap); + clicked |= Im.Selectable(shortPath, false, SelectableFlags.AllowOverlap); } Im.Tooltip.OnHover(path.Span); @@ -91,378 +144,496 @@ internal sealed class ResourceWatcherTable : Table Im.Clipboard.Set(path.Span); } - private sealed class RecordTypeColumn : ColumnFlags - { - private readonly EphemeralConfig _config; - public RecordTypeColumn(EphemeralConfig config) + private sealed class PathColumn : TextColumn + { + private readonly ResourceWatcherConfig _config; + + public PathColumn(ResourceWatcherConfig config) { - AllFlags = ResourceWatcher.AllRecords; - _config = config; + _config = config; + UnscaledWidth = 300; + Filter.Set(config.PathFilter); + Filter.FilterChanged += OnFilterChanged; } - public override float Width - => 80 * Im.Style.GlobalScale; - - public override bool FilterFunc(Record item) - => FilterValue.HasFlag(item.RecordType); - - public override RecordType FilterValue - => _config.ResourceWatcherRecordTypes; - - protected override void SetValue(RecordType value, bool enable) + private void OnFilterChanged() { - if (enable) - _config.ResourceWatcherRecordTypes |= value; - else - _config.ResourceWatcherRecordTypes &= ~value; - + _config.PathFilter = Filter.Text; _config.Save(); } - public override void DrawColumn(Record item, int idx) + public override void DrawColumn(in CachedRecord item, int globalIndex) { - Im.Text(item.RecordType switch - { - RecordType.Request => "REQ"u8, - RecordType.ResourceLoad => "LOAD"u8, - RecordType.FileLoad => "FILE"u8, - RecordType.Destruction => "DEST"u8, - RecordType.ResourceComplete => "DONE"u8, - _ => StringU8.Empty, - }); + DrawByteString(item.Record.Path, 290 * Im.Style.GlobalScale); } + + protected override string ComparisonText(in CachedRecord item, int globalIndex) + => item.PathU16; + + protected override StringU8 DisplayText(in CachedRecord item, int globalIndex) + => item.Record.Path; } - private sealed class DateColumn : Column + private sealed class RecordTypeColumn : FlagColumn { - public override float Width - => 80 * Im.Style.GlobalScale; + private readonly ResourceWatcherConfig _config; - public override int Compare(Record lhs, Record rhs) - => lhs.Time.CompareTo(rhs.Time); - - public override void DrawColumn(Record item, int _) - => Im.Text($"{item.Time.ToLongTimeString()}.{item.Time.Millisecond:D4}"); - } - - private sealed class Crc64Column : ColumnString - { - public override float Width - => UiBuilder.MonoFont.GetCharAdvance('0') * 17; - - public override string ToName(Record item) - => item.Crc64 is not 0 ? $"{item.Crc64:X16}" : string.Empty; - - public override unsafe void DrawColumn(Record item, int _) + public RecordTypeColumn(ResourceWatcherConfig config) { - using var font = item.Handle is null ? null : Im.Font.PushMono(); - Im.Text(ToName(item)); + _config = config; + UnscaledWidth = 80; + Filter.LoadValue(config.RecordFilter); + Filter.FilterChanged += OnFilterChanged; } + + private void OnFilterChanged() + { + _config.RecordFilter = Filter.FilterValue; + _config.Save(); + } + + protected override StringU8 DisplayString(in CachedRecord item, int globalIndex) + => item.TypeName; + + protected override IReadOnlyList<(RecordType Value, StringU8 Name)> EnumData + => Enum.GetValues().Select(t => (t, new StringU8(t.ToName()))).ToArray(); + + protected override RecordType GetValue(in CachedRecord item, int globalIndex) + => item.Record.RecordType; + } + + private sealed class DateColumn : BasicColumn + { + public DateColumn() + => UnscaledWidth = 80; + + public override int Compare(in CachedRecord lhs, int lhsGlobalIndex, in CachedRecord rhs, int rhsGlobalIndex) + => lhs.Record.Time.CompareTo(rhs.Record.Time); + + public override void DrawColumn(in CachedRecord item, int globalIndex) + => Im.Text(item.Time); + } + + private sealed class Crc64Column : TextColumn + { + private readonly ResourceWatcherConfig _config; + + public Crc64Column(ResourceWatcherConfig config) + { + _config = config; + UnscaledWidth = 17 * 8; + Filter.Set(config.CrcFilter); + Filter.FilterChanged += OnFilterChanged; + } + + private void OnFilterChanged() + { + _config.CrcFilter = Filter.Text; + _config.Save(); + } + + public override int Compare(in CachedRecord lhs, int lhsGlobalIndex, in CachedRecord rhs, int rhsGlobalIndex) + => lhs.Record.Crc64.CompareTo(rhs.Record.Crc64); + + public override void DrawColumn(in CachedRecord item, int globalIndex) + { + if (item.Record.Crc64 is 0) + return; + + using var font = Im.Font.PushMono(); + base.DrawColumn(in item, globalIndex); + } + + protected override string ComparisonText(in CachedRecord item, int globalIndex) + => item.Crc64; + + protected override StringU8 DisplayText(in CachedRecord item, int globalIndex) + => item.Crc64; } - private sealed class CollectionColumn : ColumnString + private sealed class CollectionColumn : TextColumn { - public override float Width - => 80 * Im.Style.GlobalScale; + private readonly ResourceWatcherConfig _config; - public override string ToName(Record item) - => (item.Collection != null ? item.Collection.Identity.Name : null) ?? string.Empty; + public CollectionColumn(ResourceWatcherConfig config) + { + _config = config; + UnscaledWidth = 80; + Filter.Set(config.CollectionFilter); + Filter.FilterChanged += OnFilterChanged; + } + + private void OnFilterChanged() + { + _config.CollectionFilter = Filter.Text; + _config.Save(); + } + + protected override string ComparisonText(in CachedRecord item, int globalIndex) + => item.Record.Collection?.Identity.Name ?? string.Empty; + + protected override StringU8 DisplayText(in CachedRecord item, int globalIndex) + => item.Collection; } - private sealed class ObjectColumn : ColumnString + private sealed class ObjectColumn : TextColumn { - public override float Width - => 200 * Im.Style.GlobalScale; + private readonly ResourceWatcherConfig _config; - public override string ToName(Record item) + public ObjectColumn(ResourceWatcherConfig config) + { + _config = config; + UnscaledWidth = 150; + Filter.Set(config.ObjectFilter); + Filter.FilterChanged += OnFilterChanged; + } + + private void OnFilterChanged() + { + _config.ObjectFilter = Filter.Text; + _config.Save(); + } + + protected override string ComparisonText(in CachedRecord item, int globalIndex) + => item.Record.AssociatedGameObject; + + protected override StringU8 DisplayText(in CachedRecord item, int globalIndex) => item.AssociatedGameObject; } - private sealed class OriginalPathColumn : ColumnString + private sealed class OriginalPathColumn : TextColumn { - public override float Width - => 200 * Im.Style.GlobalScale; + private readonly ResourceWatcherConfig _config; - public override string ToName(Record item) - => item.OriginalPath.ToString(); - - public override int Compare(Record lhs, Record rhs) - => lhs.OriginalPath.CompareTo(rhs.OriginalPath); - - public override void DrawColumn(Record item, int _) - => DrawByteString(item.OriginalPath, 190 * Im.Style.GlobalScale); - } - - private sealed class ResourceCategoryColumn : ColumnFlags - { - private readonly EphemeralConfig _config; - - public ResourceCategoryColumn(EphemeralConfig config) + public OriginalPathColumn(ResourceWatcherConfig config) { - _config = config; - AllFlags = ResourceExtensions.AllResourceCategories; + _config = config; + UnscaledWidth = 200; + Filter.Set(config.OriginalPathFilter); + Filter.FilterChanged += OnFilterChanged; } - public override float Width - => 80 * Im.Style.GlobalScale; - - public override bool FilterFunc(Record item) - => FilterValue.HasFlag(item.Category); - - public override ResourceCategoryFlag FilterValue - => _config.ResourceWatcherResourceCategories; - - protected override void SetValue(ResourceCategoryFlag value, bool enable) + private void OnFilterChanged() { - if (enable) - _config.ResourceWatcherResourceCategories |= value; - else - _config.ResourceWatcherResourceCategories &= ~value; - + _config.OriginalPathFilter = Filter.Text; _config.Save(); } - public override void DrawColumn(Record item, int idx) + public override void DrawColumn(in CachedRecord item, int globalIndex) { - Im.Text($"{item.Category}"); + DrawByteString(item.Record.OriginalPath, 190 * Im.Style.GlobalScale); } + + protected override string ComparisonText(in CachedRecord item, int globalIndex) + => item.OriginalPath; + + protected override StringU8 DisplayText(in CachedRecord item, int globalIndex) + => item.Record.OriginalPath; } - private sealed class ResourceTypeColumn : ColumnFlags + private sealed class ResourceCategoryColumn : FlagColumn { - private readonly EphemeralConfig _config; + private readonly ResourceWatcherConfig _config; - public ResourceTypeColumn(EphemeralConfig config) + public ResourceCategoryColumn(ResourceWatcherConfig config) { - _config = config; - AllFlags = Enum.GetValues().Aggregate((v, f) => v | f); - for (var i = 0; i < Names.Length; ++i) - Names[i] = Names[i].ToLowerInvariant(); + _config = config; + UnscaledWidth = 80; + Filter.LoadValue(config.CategoryFilter); + Filter.FilterChanged += OnFilterChanged; } - public override float Width - => 50 * Im.Style.GlobalScale; - - public override bool FilterFunc(Record item) - => FilterValue.HasFlag(item.ResourceType); - - public override ResourceTypeFlag FilterValue - => _config.ResourceWatcherResourceTypes; - - protected override void SetValue(ResourceTypeFlag value, bool enable) + private void OnFilterChanged() { - if (enable) - _config.ResourceWatcherResourceTypes |= value; - else - _config.ResourceWatcherResourceTypes &= ~value; - + _config.CategoryFilter = Filter.FilterValue; _config.Save(); } - public override void DrawColumn(Record item, int idx) - { - Im.Text($"{item.ResourceType.ToString().ToLowerInvariant()}"); - } + protected override StringU8 DisplayString(in CachedRecord item, int globalIndex) + => item.ResourceCategory; + + protected override IReadOnlyList<(ResourceCategoryFlag Value, StringU8 Name)> EnumData { get; } = + Enum.GetValues().Select(r => (r, new StringU8($"{r}"))).ToArray(); + + protected override ResourceCategoryFlag GetValue(in CachedRecord item, int globalIndex) + => item.Record.Category; } - private sealed class LoadStateColumn : ColumnFlags + private sealed class ResourceTypeColumn : FlagColumn { - public override float Width - => 50 * Im.Style.GlobalScale; + private readonly ResourceWatcherConfig _config; - [Flags] - public enum LoadStateFlag : byte + public ResourceTypeColumn(ResourceWatcherConfig config) { - Success = 0x01, - Async = 0x02, - Failed = 0x04, - FailedSub = 0x08, - Unknown = 0x10, - None = 0xFF, + _config = config; + UnscaledWidth = 50; + Filter.LoadValue(config.TypeFilter); + Filter.FilterChanged += OnFilterChanged; } - protected override string[] Names - => new[] - { - "Loaded", - "Loading", - "Failed", - "Dependency Failed", - "Unknown", - "None", - }; - - public LoadStateColumn() + private void OnFilterChanged() { - AllFlags = Enum.GetValues().Aggregate((v, f) => v | f); - _filterValue = AllFlags; + _config.TypeFilter = Filter.FilterValue; + _config.Save(); } - private LoadStateFlag _filterValue; + protected override IReadOnlyList<(ResourceTypeFlag Value, StringU8 Name)> EnumData { get; } = + Enum.GetValues().Select(r => (r, new StringU8(r.ToString().ToLowerInvariant()))).ToArray(); - public override LoadStateFlag FilterValue - => _filterValue; + protected override StringU8 DisplayString(in CachedRecord item, int globalIndex) + => item.ResourceType; - protected override void SetValue(LoadStateFlag value, bool enable) + protected override ResourceTypeFlag GetValue(in CachedRecord item, int globalIndex) + => item.Record.ResourceType; + } + + private sealed class LoadStateColumn : FlagColumn + { + private readonly ResourceWatcherConfig _config; + + public LoadStateColumn(ResourceWatcherConfig config) { - if (enable) - _filterValue |= value; - else - _filterValue &= ~value; + _config = config; + UnscaledWidth = 50; + Filter.LoadValue(config.LoadStateFilter); + Filter.FilterChanged += OnFilterChanged; } - public override bool FilterFunc(Record item) - => item.LoadState switch - { - LoadState.None => FilterValue.HasFlag(LoadStateFlag.None), - LoadState.Success => FilterValue.HasFlag(LoadStateFlag.Success), - LoadState.FailedSubResource => FilterValue.HasFlag(LoadStateFlag.FailedSub), - <= LoadState.Constructed => FilterValue.HasFlag(LoadStateFlag.Unknown), - < LoadState.Success => FilterValue.HasFlag(LoadStateFlag.Async), - > LoadState.Success => FilterValue.HasFlag(LoadStateFlag.Failed), - }; - - public override void DrawColumn(Record item, int _) + private void OnFilterChanged() { - if (item.LoadState == LoadState.None) + _config.LoadStateFilter = Filter.FilterValue; + _config.Save(); + } + + public override void DrawColumn(in CachedRecord item, int globalIndex) + { + if (item.Record.LoadState == LoadState.None) return; - var (icon, color, tt) = item.LoadState switch + var (icon, color, tt) = item.Record.LoadState switch { LoadState.Success => (FontAwesomeIcon.CheckCircle, ColorId.IncreasedMetaValue.Value(), - new StringU8($"Successfully loaded ({(byte)item.LoadState}).")), + new StringU8($"Successfully loaded ({(byte)item.Record.LoadState}).")), LoadState.FailedSubResource => (FontAwesomeIcon.ExclamationCircle, ColorId.DecreasedMetaValue.Value(), - new StringU8($"Dependencies failed to load ({(byte)item.LoadState}).")), + new StringU8($"Dependencies failed to load ({(byte)item.Record.LoadState}).")), <= LoadState.Constructed => (FontAwesomeIcon.QuestionCircle, ColorId.UndefinedMod.Value(), - new StringU8($"Not yet loaded ({(byte)item.LoadState}).")), - < LoadState.Success => (FontAwesomeIcon.Clock, ColorId.FolderLine.Value(), new StringU8($"Loading asynchronously ({(byte)item.LoadState}).")), + new StringU8($"Not yet loaded ({(byte)item.Record.LoadState}).")), + < LoadState.Success => (FontAwesomeIcon.Clock, ColorId.FolderLine.Value(), + new StringU8($"Loading asynchronously ({(byte)item.Record.LoadState}).")), > LoadState.Success => (FontAwesomeIcon.Times, ColorId.DecreasedMetaValue.Value(), - new StringU8($"Failed to load ({(byte)item.LoadState}).")), + new StringU8($"Failed to load ({(byte)item.Record.LoadState}).")), }; ImEx.Icon.Draw(icon.Icon(), color); Im.Tooltip.OnHover(tt); } - public override int Compare(Record lhs, Record rhs) - => lhs.LoadState.CompareTo(rhs.LoadState); - } + public override int Compare(in CachedRecord lhs, int lhsGlobalIndex, in CachedRecord rhs, int rhsGlobalIndex) + => lhs.Record.LoadState.CompareTo(rhs.Record.LoadState); - private sealed class HandleColumn : ColumnString - { - public override float Width - => 120 * Im.Style.GlobalScale; + protected override StringU8 DisplayString(in CachedRecord item, int globalIndex) + => StringU8.Empty; - public override unsafe string ToName(Record item) - => item.Handle == null ? string.Empty : $"0x{(ulong)item.Handle:X}"; + protected override IReadOnlyList<(LoadStateFlag Value, StringU8 Name)> EnumData { get; } + = + [ + (LoadStateFlag.Success, new StringU8("Loaded"u8)), + (LoadStateFlag.Async, new StringU8("Loading"u8)), + (LoadStateFlag.Failed, new StringU8("Failed"u8)), + (LoadStateFlag.FailedSub, new StringU8("Dependency Failed"u8)), + (LoadStateFlag.Unknown, new StringU8("Unknown"u8)), + (LoadStateFlag.None, new StringU8("None"u8)), + ]; - public override unsafe void DrawColumn(Record item, int _) - { - using var font = item.Handle is null ? null : Im.Font.PushMono(); - ImEx.TextRightAligned(ToName(item)); - } - } - - [Flags] - private enum BoolEnum : byte - { - True = 0x01, - False = 0x02, - Unknown = 0x04, - } - - private class OptBoolColumn : ColumnFlags - { - private BoolEnum _filter; - - public OptBoolColumn() - { - AllFlags = BoolEnum.True | BoolEnum.False | BoolEnum.Unknown; - _filter = AllFlags; - Flags &= ~ImGuiTableColumnFlags.NoSort; - } - - protected bool FilterFunc(OptionalBool b) - => b.Value switch + protected override LoadStateFlag GetValue(in CachedRecord item, int globalIndex) + => item.Record.LoadState switch { - null => _filter.HasFlag(BoolEnum.Unknown), - true => _filter.HasFlag(BoolEnum.True), - false => _filter.HasFlag(BoolEnum.False), + LoadState.None => LoadStateFlag.None, + LoadState.Success => LoadStateFlag.Success, + LoadState.FailedSubResource => LoadStateFlag.FailedSub, + <= LoadState.Constructed => LoadStateFlag.Unknown, + < LoadState.Success => LoadStateFlag.Async, + > LoadState.Success => LoadStateFlag.Failed, }; + } - public override BoolEnum FilterValue - => _filter; + private sealed class HandleColumn : TextColumn + { + private readonly ResourceWatcherConfig _config; - protected override void SetValue(BoolEnum value, bool enable) + public HandleColumn(ResourceWatcherConfig config) { - if (enable) - _filter |= value; - else - _filter &= ~value; + _config = config; + UnscaledWidth = 120; + Filter.Set(config.ResourceFilter); + Filter.FilterChanged += OnFilterChanged; } - protected static void DrawColumn(OptionalBool b) + private void OnFilterChanged() { - if (!b.HasValue) + _config.ResourceFilter = Filter.Text; + _config.Save(); + } + + public override unsafe void DrawColumn(in CachedRecord item, int globalIndex) + { + if (item.Record.RecordType is RecordType.Request) return; - ImEx.Icon.Draw(b.Value switch - { - true => FontAwesomeIcon.Check.Icon(), - _ => FontAwesomeIcon.Times.Icon(), - }); + Penumbra.Dynamis.DrawPointer(item.Record.Handle); } + + protected override string ComparisonText(in CachedRecord item, int globalIndex) + => item.HandleU16; + + protected override StringU8 DisplayText(in CachedRecord item, int globalIndex) + => StringU8.Empty; + } + + + private abstract class OptBoolColumn : FlagColumn + { + protected OptBoolColumn(float width) + { + UnscaledWidth = width; + Flags &= ~TableColumnFlags.NoSort; + } + + public override void DrawColumn(in CachedRecord item, int globalIndex) + { + var value = GetValue(item, globalIndex); + if (value is BoolEnum.Unknown) + return; + + ImEx.Icon.Draw(value is BoolEnum.True + ? FontAwesomeIcon.Check.Icon() + : FontAwesomeIcon.Times.Icon() + ); + } + + protected override IReadOnlyList<(BoolEnum Value, StringU8 Name)> EnumData { get; } = + [ + (BoolEnum.True, new StringU8("True"u8)), + (BoolEnum.False, new StringU8("False"u8)), + (BoolEnum.Unknown, new StringU8("Unknown"u8)), + ]; + + protected override StringU8 DisplayString(in CachedRecord item, int globalIndex) + => StringU8.Empty; + + protected static BoolEnum ToValue(OptionalBool value) + => value.Value switch + { + true => BoolEnum.True, + false => BoolEnum.False, + null => BoolEnum.Unknown, + }; } private sealed class CustomLoadColumn : OptBoolColumn { - public override float Width - => 60 * Im.Style.GlobalScale; + private readonly ResourceWatcherConfig _config; - public override bool FilterFunc(Record item) - => FilterFunc(item.CustomLoad); + public CustomLoadColumn(ResourceWatcherConfig config) + : base(60f) + { + _config = config; + Filter.LoadValue(config.CustomFilter); + Filter.FilterChanged += OnFilterChanged; + } - public override void DrawColumn(Record item, int idx) - => DrawColumn(item.CustomLoad); + private void OnFilterChanged() + { + _config.CustomFilter = Filter.FilterValue; + _config.Save(); + } + + protected override BoolEnum GetValue(in CachedRecord item, int globalIndex) + => ToValue(item.Record.CustomLoad); } private sealed class SynchronousLoadColumn : OptBoolColumn { - public override float Width - => 45 * Im.Style.GlobalScale; + private readonly ResourceWatcherConfig _config; - public override bool FilterFunc(Record item) - => FilterFunc(item.Synchronously); + public SynchronousLoadColumn(ResourceWatcherConfig config) + : base(45) + { + _config = config; + Filter.LoadValue(config.SyncFilter); + Filter.FilterChanged += OnFilterChanged; + } - public override void DrawColumn(Record item, int idx) - => DrawColumn(item.Synchronously); + private void OnFilterChanged() + { + _config.SyncFilter = Filter.FilterValue; + _config.Save(); + } + + protected override BoolEnum GetValue(in CachedRecord item, int globalIndex) + => ToValue(item.Record.Synchronously); } - private sealed class RefCountColumn : Column + private sealed class RefCountColumn : NumberColumn { - public override float Width - => 30 * Im.Style.GlobalScale; + private readonly ResourceWatcherConfig _config; - public override void DrawColumn(Record item, int _) - => ImEx.TextRightAligned($"{item.RefCount}"); + public RefCountColumn(ResourceWatcherConfig config) + { + _config = config; + UnscaledWidth = 60; + Filter.Set(config.RefFilter); + Filter.FilterChanged += OnFilterChanged; + } - public override int Compare(Record lhs, Record rhs) - => lhs.RefCount.CompareTo(rhs.RefCount); + private void OnFilterChanged() + { + _config.RefFilter = Filter.Text; + _config.Save(); + } + + public override uint ToValue(in CachedRecord item, int globalIndex) + => item.Record.RefCount; + + protected override SizedString DisplayNumber(in CachedRecord item, int globalIndex) + => item.RefCount; + + protected override string ComparisonText(in CachedRecord item, int globalIndex) + => item.RefCount; } - private sealed class OsThreadColumn : ColumnString + private sealed class OsThreadColumn : NumberColumn { - public override float Width - => 60 * Im.Style.GlobalScale; + private readonly ResourceWatcherConfig _config; - public override string ToName(Record item) - => item.OsThreadId.ToString(); + public OsThreadColumn(ResourceWatcherConfig config) + { + _config = config; + UnscaledWidth = 60; + Filter.Set(config.ThreadFilter); + Filter.FilterChanged += OnFilterChanged; + } - public override void DrawColumn(Record item, int _) - => ImEx.TextRightAligned($"{item.OsThreadId}"); + private void OnFilterChanged() + { + _config.ThreadFilter = Filter.Text; + _config.Save(); + } - public override int Compare(Record lhs, Record rhs) - => lhs.OsThreadId.CompareTo(rhs.OsThreadId); + public override uint ToValue(in CachedRecord item, int globalIndex) + => item.Record.OsThreadId; + + protected override SizedString DisplayNumber(in CachedRecord item, int globalIndex) + => item.Thread; + + protected override string ComparisonText(in CachedRecord item, int globalIndex) + => item.Thread; } + + public override IEnumerable GetItems() + => new CacheListAdapter(_records, arg => new CachedRecord(arg)); + + protected override TableCache CreateCache() + => new(this); } diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs deleted file mode 100644 index 3ff58171..00000000 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ /dev/null @@ -1,121 +0,0 @@ -using Dalamud.Bindings.ImGui; -using ImSharp; -using Luna; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Text; -using Penumbra.Api.Enums; -using Penumbra.Collections.Manager; -using Penumbra.Communication; -using Penumbra.GameData.Data; -using Penumbra.Mods; -using Penumbra.Mods.Editor; -using Penumbra.Services; -using Penumbra.UI.Classes; - -namespace Penumbra.UI.Tabs; - -public sealed class ChangedItemsTab( - CollectionManager collectionManager, - CollectionSelectHeader collectionHeader, - ChangedItemDrawer drawer, - CommunicatorService communicator) - : ITab -{ - public ReadOnlySpan Label - => "Changed Items"u8; - - public TabType Identifier - => TabType.ChangedItems; - - private string _changedItemFilter = string.Empty; - private string _changedItemModFilter = string.Empty; - private Vector2 _buttonSize; - - public void DrawContent() - { - collectionHeader.Draw(true); - drawer.DrawTypeFilter(); - var varWidth = DrawFilters(); - using var child = ImUtf8.Child("##changedItemsChild"u8, -Vector2.One); - if (!child) - return; - - _buttonSize = new Vector2(Im.Style.ItemSpacing.Y + Im.Style.FrameHeight); - using var style = ImStyleDouble.CellPadding.Push(Vector2.Zero) - .Push(ImStyleDouble.ItemSpacing, Vector2.Zero) - .Push(ImStyleDouble.FramePadding, Vector2.Zero) - .Push(ImStyleDouble.SelectableTextAlign, new Vector2(0.01f, 0.5f)); - - var skips = ImGuiClip.GetNecessarySkips(_buttonSize.Y); - using var table = Im.Table.Begin("##changedItems"u8, 3, TableFlags.RowBackground, -Vector2.One); - if (!table) - return; - - const TableColumnFlags flags = TableColumnFlags.NoResize | TableColumnFlags.WidthFixed; - table.SetupColumn("items"u8, flags, 450 * Im.Style.GlobalScale); - table.SetupColumn("mods"u8, flags, varWidth - 140 * Im.Style.GlobalScale); - table.SetupColumn("id"u8, flags, 140 * Im.Style.GlobalScale); - - var items = collectionManager.Active.Current.ChangedItems; - var rest = ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn); - ImGuiClip.DrawEndDummy(rest, _buttonSize.Y); - } - - /// Draw a pair of filters and return the variable width of the flexible column. - private float DrawFilters() - { - var varWidth = Im.ContentRegion.Available.X - - 450 * Im.Style.GlobalScale - - Im.Style.ItemSpacing.X; - Im.Item.SetNextWidth(450 * Im.Style.GlobalScale); - Im.Input.Text("##changedItemsFilter"u8, ref _changedItemFilter, "Filter Item..."u8); - Im.Line.Same(); - Im.Item.SetNextWidth(varWidth); - Im.Input.Text("##changedItemsModFilter"u8, ref _changedItemModFilter, "Filter Mods..."u8); - return varWidth; - } - - /// Apply the current filters. - private bool FilterChangedItem(KeyValuePair, IIdentifiedObjectData)> item) - => drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter) - && (_changedItemModFilter.Length is 0 - || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter, StringComparison.OrdinalIgnoreCase))); - - /// Draw a full column for a changed item. - private void DrawChangedItemColumn(KeyValuePair, IIdentifiedObjectData)> item) - { - ImGui.TableNextColumn(); - drawer.DrawCategoryIcon(item.Value.Item2, _buttonSize.Y); - Im.Line.Same(0, 0); - var name = item.Value.Item2.ToName(item.Key); - var clicked = ImUtf8.Selectable(name, false, ImGuiSelectableFlags.None, _buttonSize with { X = 0 }); - drawer.ChangedItemHandling(item.Value.Item2, clicked); - - ImGui.TableNextColumn(); - DrawModColumn(item.Value.Item1); - - ImGui.TableNextColumn(); - ChangedItemDrawer.DrawModelData(item.Value.Item2, _buttonSize.Y); - } - - private void DrawModColumn(Luna.SingleArray mods) - { - if (mods.Count <= 0) - return; - - var first = mods[0]; - if (ImUtf8.Selectable(first.Name, false, ImGuiSelectableFlags.None, _buttonSize with { X = 0 }) - && ImGui.GetIO().KeyCtrl - && first is Mod mod) - communicator.SelectTab.Invoke(new SelectTab.Arguments(Api.Enums.TabType.Mods, mod)); - - if (!Im.Item.Hovered()) - return; - - using var _ = ImRaii.Tooltip(); - ImUtf8.Text("Hold Control and click to jump to mod.\n"u8); - if (mods.Count > 1) - ImUtf8.Text("Other mods affecting this item:\n" + string.Join("\n", mods.Skip(1).Select(m => m.Name))); - } -} diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 8af56cf7..a98ae98d 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -1,14 +1,13 @@ -using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Objects; using Dalamud.Plugin; using ImSharp; using Luna; -using OtterGui.Raii; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.Mods.Manager; using Penumbra.Services; +using Penumbra.UI.Classes; using Penumbra.UI.CollectionTab; namespace Penumbra.UI.Tabs; @@ -63,7 +62,7 @@ public sealed class CollectionsTab : ITab, IDisposable public void DrawContent() { - var width = ImGui.CalcTextSize("nnnnnnnnnnnnnnnnnnnnnnnnnn").X; + var width = Im.Font.CalculateSize("nnnnnnnnnnnnnnnnnnnnnnnnnn"u8).X; using (Im.Group()) { _selector.Draw(width); @@ -93,28 +92,28 @@ public sealed class CollectionsTab : ITab, IDisposable using var _ = Im.Group(); var tabSelectedColor = Im.Style[ImGuiColor.TabSelected]; using var color = ImGuiColor.Button.Push(tabSelectedColor, Mode is PanelMode.SimpleAssignment); - if (ImGui.Button("Simple Assignments", buttonSize)) + if (Im.Button("Simple Assignments"u8, buttonSize)) Mode = PanelMode.SimpleAssignment; color.Pop(); _tutorial.OpenTutorial(BasicTutorialSteps.SimpleAssignments); Im.Line.Same(); color.Push(ImGuiColor.Button, tabSelectedColor, Mode is PanelMode.IndividualAssignment); - if (ImGui.Button("Individual Assignments", buttonSize)) + if (Im.Button("Individual Assignments"u8, buttonSize)) Mode = PanelMode.IndividualAssignment; color.Pop(); _tutorial.OpenTutorial(BasicTutorialSteps.IndividualAssignments); Im.Line.Same(); color.Push(ImGuiColor.Button, tabSelectedColor, Mode is PanelMode.GroupAssignment); - if (ImGui.Button("Group Assignments", buttonSize)) + if (Im.Button("Group Assignments"u8, buttonSize)) Mode = PanelMode.GroupAssignment; color.Pop(); _tutorial.OpenTutorial(BasicTutorialSteps.GroupAssignments); Im.Line.Same(); color.Push(ImGuiColor.Button, tabSelectedColor, Mode is PanelMode.Details); - if (ImGui.Button("Collection Details", buttonSize)) + if (Im.Button("Collection Details"u8, buttonSize)) Mode = PanelMode.Details; color.Pop(); _tutorial.OpenTutorial(BasicTutorialSteps.CollectionDetails); @@ -126,7 +125,7 @@ public sealed class CollectionsTab : ITab, IDisposable private void DrawPanel() { using var style = ImStyleDouble.ItemSpacing.Push(Vector2.Zero); - using var child = ImRaii.Child("##CollectionSettings", Im.ContentRegion.Available with { Y = 0 }, true); + using var child = Im.Child.Begin("##CollectionSettings"U8, Im.ContentRegion.Available with { Y = 0 }, true); if (!child) return; diff --git a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs index 759795f5..cde6e3bb 100644 --- a/Penumbra/UI/Tabs/Debug/AtchDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/AtchDrawer.cs @@ -1,6 +1,4 @@ -using Dalamud.Bindings.ImGui; using ImSharp; -using OtterGui.Text; using Penumbra.GameData.Files; using Penumbra.GameData.Files.AtchStructs; @@ -10,47 +8,47 @@ public static class AtchDrawer { public static void Draw(AtchFile file) { - using (ImUtf8.Group()) + using (Im.Group()) { - ImUtf8.Text("Entries: "u8); - ImUtf8.Text("States: "u8); + Im.Text("Entries: "u8); + Im.Text("States: "u8); } Im.Line.Same(); - using (ImUtf8.Group()) + using (Im.Group()) { - ImUtf8.Text($"{file.Points.Count}"); + Im.Text($"{file.Points.Count}"); if (file.Points.Count == 0) { - ImUtf8.Text("0"u8); + Im.Text("0"u8); return; } - ImUtf8.Text($"{file.Points[0].Entries.Length}"); + Im.Text($"{file.Points[0].Entries.Length}"); } foreach (var (index, entry) in file.Points.Index()) { - using var id = ImUtf8.PushId(index); - using var tree = ImUtf8.TreeNode($"{index:D3}: {entry.Type.ToName()}"); - if (tree) - { - ImUtf8.TreeNode(entry.Accessory ? "Accessory"u8 : "Weapon"u8, ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); - foreach (var (i, state) in entry.Entries.Index()) - { - id.Push(i); - using var t = ImUtf8.TreeNode(state.Bone); - if (t) - { - ImUtf8.TreeNode($"Scale: {state.Scale}", ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); - ImUtf8.TreeNode($"Offset: {state.Offset.X} | {state.Offset.Y} | {state.Offset.Z}", - ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); - ImUtf8.TreeNode($"Rotation: {state.Rotation.X} | {state.Rotation.Y} | {state.Rotation.Z}", - ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); - } + using var id = Im.Id.Push(index); + using var tree = Im.Tree.Node($"{index:D3}: {entry.Type.ToName()}"); + if (!tree) + continue; - id.Pop(); + Im.Tree.Node(entry.Accessory ? "Accessory"u8 : "Weapon"u8, TreeNodeFlags.Bullet | TreeNodeFlags.Leaf).Dispose(); + foreach (var (i, state) in entry.Entries.Index()) + { + id.Push(i); + using var t = Im.Tree.Node(state.Bone); + if (t) + { + Im.Tree.Node($"Scale: {state.Scale}", TreeNodeFlags.Bullet | TreeNodeFlags.Leaf).Dispose(); + Im.Tree.Node($"Offset: {state.Offset.X} | {state.Offset.Y} | {state.Offset.Z}", + TreeNodeFlags.Bullet | TreeNodeFlags.Leaf).Dispose(); + Im.Tree.Node($"Rotation: {state.Rotation.X} | {state.Rotation.Y} | {state.Rotation.Z}", + TreeNodeFlags.Bullet | TreeNodeFlags.Leaf).Dispose(); } + + id.Pop(); } } } diff --git a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs index 053df2e5..cf40f2fa 100644 --- a/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs +++ b/Penumbra/UI/Tabs/Debug/CrashDataExtensions.cs @@ -1,7 +1,4 @@ -using Dalamud.Bindings.ImGui; using ImSharp; -using OtterGui; -using OtterGui.Raii; using Penumbra.CrashHandler; namespace Penumbra.UI.Tabs.Debug; @@ -12,16 +9,16 @@ public static class CrashDataExtensions { using (Im.Group()) { - Im.Text(nameof(data.Mode)); - Im.Text(nameof(data.CrashTime)); + Im.Text("Mode"u8); + Im.Text("Crash Time"u8); Im.Text("Current Age"u8); - Im.Text(nameof(data.Version)); - Im.Text(nameof(data.GameVersion)); - Im.Text(nameof(data.ExitCode)); - Im.Text(nameof(data.ProcessId)); - Im.Text(nameof(data.TotalModdedFilesLoaded)); - Im.Text(nameof(data.TotalCharactersLoaded)); - Im.Text(nameof(data.TotalVFXFuncsInvoked)); + Im.Text("Version"u8); + Im.Text("Game Version"u8); + Im.Text("Exit Code"u8); + Im.Text("Process ID"u8); + Im.Text("Total Modded Files Loaded"u8); + Im.Text("Total Characters Loaded"u8); + Im.Text("Total VFX Functions Invoked"u8); } Im.Line.Same(); @@ -42,7 +39,7 @@ public static class CrashDataExtensions public static void DrawCharacters(this CrashData data) { - using var tree = ImRaii.TreeNode("Last Characters"); + using var tree = Im.Tree.Node("Last Characters"u8); if (!tree) return; @@ -51,20 +48,21 @@ public static class CrashDataExtensions if (!table) return; - ImGuiClip.ClippedDraw(data.LastCharactersLoaded, character => + using var clipper = new Im.ListClipper(data.LastCharactersLoaded.Count, Im.Style.TextHeightWithSpacing); + foreach (var character in clipper.Iterate(data.LastCharactersLoaded)) { - ImGuiUtil.DrawTableColumn(character.Age.ToString(CultureInfo.InvariantCulture)); - ImGuiUtil.DrawTableColumn(character.ThreadId.ToString()); - ImGuiUtil.DrawTableColumn(character.CharacterName); - ImGuiUtil.DrawTableColumn(character.CollectionId.ToString()); - ImGuiUtil.DrawTableColumn(character.CharacterAddress); - ImGuiUtil.DrawTableColumn(character.Timestamp.ToString()); - }, Im.Style.TextHeightWithSpacing); + table.DrawColumn($"{character.Age}"); + table.DrawColumn($"{character.ThreadId}"); + table.DrawColumn(character.CharacterName); + table.DrawColumn($"{character.CollectionId}"); + table.DrawColumn(character.CharacterAddress); + table.DrawColumn($"{character.Timestamp}"); + } } public static void DrawFiles(this CrashData data) { - using var tree = ImRaii.TreeNode("Last Files"); + using var tree = Im.Tree.Node("Last Files"u8); if (!tree) return; @@ -73,22 +71,23 @@ public static class CrashDataExtensions if (!table) return; - ImGuiClip.ClippedDraw(data.LastModdedFilesLoaded, file => + using var clipper = new Im.ListClipper(data.LastModdedFilesLoaded.Count, Im.Style.TextHeightWithSpacing); + foreach (var file in clipper.Iterate(data.LastModdedFilesLoaded)) { - ImGuiUtil.DrawTableColumn(file.Age.ToString(CultureInfo.InvariantCulture)); - ImGuiUtil.DrawTableColumn(file.ThreadId.ToString()); - ImGuiUtil.DrawTableColumn(file.ActualFileName); - ImGuiUtil.DrawTableColumn(file.RequestedFileName); - ImGuiUtil.DrawTableColumn(file.CharacterName); - ImGuiUtil.DrawTableColumn(file.CollectionId.ToString()); - ImGuiUtil.DrawTableColumn(file.CharacterAddress); - ImGuiUtil.DrawTableColumn(file.Timestamp.ToString()); - }, Im.Style.TextHeightWithSpacing); + table.DrawColumn($"{file.Age}"); + table.DrawColumn($"{file.ThreadId}"); + table.DrawColumn(file.ActualFileName); + table.DrawColumn(file.RequestedFileName); + table.DrawColumn(file.CharacterName); + table.DrawColumn($"{file.CollectionId}"); + table.DrawColumn(file.CharacterAddress); + table.DrawColumn($"{file.Timestamp}"); + } } public static void DrawVfxInvocations(this CrashData data) { - using var tree = ImRaii.TreeNode("Last VFX Invocations"); + using var tree = Im.Tree.Node("Last VFX Invocations"u8); if (!tree) return; @@ -97,15 +96,16 @@ public static class CrashDataExtensions if (!table) return; - ImGuiClip.ClippedDraw(data.LastVFXFuncsInvoked, vfx => + using var clipper = new Im.ListClipper(data.LastVFXFuncsInvoked.Count, Im.Style.TextHeightWithSpacing); + foreach (var vfx in clipper.Iterate(data.LastVFXFuncsInvoked)) { - ImGuiUtil.DrawTableColumn(vfx.Age.ToString(CultureInfo.InvariantCulture)); - ImGuiUtil.DrawTableColumn(vfx.ThreadId.ToString()); - ImGuiUtil.DrawTableColumn(vfx.InvocationType); - ImGuiUtil.DrawTableColumn(vfx.CharacterName); - ImGuiUtil.DrawTableColumn(vfx.CollectionId.ToString()); - ImGuiUtil.DrawTableColumn(vfx.CharacterAddress); - ImGuiUtil.DrawTableColumn(vfx.Timestamp.ToString()); - }, Im.Style.TextHeightWithSpacing); + table.DrawColumn($"{vfx.Age}"); + table.DrawColumn($"{vfx.ThreadId}"); + table.DrawColumn(vfx.InvocationType); + table.DrawColumn(vfx.CharacterName); + table.DrawColumn($"{vfx.CollectionId}"); + table.DrawColumn(vfx.CharacterAddress); + table.DrawColumn($"{vfx.Timestamp}"); + } } } diff --git a/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs b/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs index 579489a8..15f08aeb 100644 --- a/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs +++ b/Penumbra/UI/Tabs/Debug/CrashHandlerPanel.cs @@ -1,9 +1,6 @@ using System.Text.Json; -using Dalamud.Bindings.ImGui; using Dalamud.Interface.DragDrop; using ImSharp; -using OtterGui; -using OtterGui.Raii; using Penumbra.CrashHandler; using Penumbra.Services; @@ -26,14 +23,14 @@ public class CrashHandlerPanel(CrashHandlerService service, Configuration config private void DrawData() { using var _ = Im.Group(); - using var header = ImRaii.CollapsingHeader("Crash Handler"); + using var header = Im.Tree.HeaderId("Crash Handler"u8); if (!header) return; DrawButtons(); DrawMainData(); - DrawObject("Last Manual Dump", _lastDump, null); - DrawObject(_lastLoadedFile.Length > 0 ? $"Loaded File ({_lastLoadedFile})###Loaded File" : "Loaded File", _lastLoad, + DrawObject("Last Manual Dump"u8, _lastDump, null); + DrawObject(_lastLoadedFile.Length > 0 ? $"Loaded File ({_lastLoadedFile})###Loaded File" : "Loaded File"u8, _lastLoad, _lastLoadException); } @@ -43,38 +40,46 @@ public class CrashHandlerPanel(CrashHandlerService service, Configuration config if (!table) return; - PrintValue("Enabled", config.UseCrashHandler); - PrintValue("Copied Executable Path", service.CopiedExe); - PrintValue("Original Executable Path", service.OriginalExe); - PrintValue("Log File Path", service.LogPath); - PrintValue("XIV Process ID", service.ProcessId.ToString()); - PrintValue("Crash Handler Running", service.IsRunning.ToString()); - PrintValue("Crash Handler Process ID", service.ChildProcessId.ToString()); - PrintValue("Crash Handler Exit Code", service.ChildExitCode.ToString()); + table.DrawColumn("Enabled"u8); + table.DrawColumn($"{config.UseCrashHandler}"); + table.DrawColumn("Copied Executable Path"u8); + table.DrawColumn(service.CopiedExe); + table.DrawColumn("Original Executable Path"u8); + table.DrawColumn(service.OriginalExe); + table.DrawColumn("Log File Path"u8); + table.DrawColumn(service.LogPath); + table.DrawColumn("XIV Process ID"u8); + table.DrawColumn($"{service.ProcessId}"); + table.DrawColumn("Crash Handler Running"u8); + table.DrawColumn($"{service.IsRunning}"); + table.DrawColumn("Crash Handler Process ID"u8); + table.DrawColumn($"{service.ChildProcessId}"); + table.DrawColumn("Crash Handler Exit Code"u8); + table.DrawColumn($"{service.ChildExitCode}"); } private void DrawButtons() { - if (ImGui.Button("Dump Crash Handler Memory")) + if (Im.Button("Dump Crash Handler Memory"u8)) _lastDump = service.Dump()?.Deserialize(); - if (ImGui.Button("Enable")) + if (Im.Button("Enable"u8)) service.Enable(); Im.Line.Same(); - if (ImGui.Button("Disable")) + if (Im.Button("Disable"u8)) service.Disable(); - if (ImGui.Button("Shutdown Crash Handler")) + if (Im.Button("Shutdown Crash Handler"u8)) service.CloseCrashHandler(); Im.Line.Same(); - if (ImGui.Button("Relaunch Crash Handler")) + if (Im.Button("Relaunch Crash Handler"u8)) service.LaunchCrashHandler(); } private void DrawDropSource() { - dragDrop.CreateImGuiSource("LogDragDrop", m => m.Files.Any(f => f.EndsWith("Penumbra.log")), m => + dragDrop.CreateImGuiSource("LogDragDrop", m => m.Files.Any(f => f.EndsWith("Penumbra.log")), _ => { Im.Text("Dragging Penumbra.log for import."u8); return true; @@ -104,19 +109,19 @@ public class CrashHandlerPanel(CrashHandlerService service, Configuration config } } - private static void DrawObject(string name, CrashData? data, Exception? ex) + private static void DrawObject(Utf8StringHandler name, CrashData? data, Exception? ex) { - using var tree = ImRaii.TreeNode(name); + using var tree = Im.Tree.Node(name); if (!tree) return; - if (ex != null) + if (ex is not null) { - ImGuiUtil.TextWrapped(ex.ToString()); + Im.TextWrapped($"{ex}"); return; } - if (data == null) + if (data is null) { Im.Text("Nothing loaded."u8); return; @@ -127,10 +132,4 @@ public class CrashHandlerPanel(CrashHandlerService service, Configuration config data.DrawCharacters(); data.DrawVfxInvocations(); } - - private static void PrintValue(string label, in T data) - { - ImGuiUtil.DrawTableColumn(label); - ImGuiUtil.DrawTableColumn(data?.ToString() ?? "NULL"); - } } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index d62328f5..2819a365 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -31,7 +31,6 @@ using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.String; using Penumbra.UI.Classes; -using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; using ImGuiClip = OtterGui.ImGuiClip; using Penumbra.Api.IpcTester; using Penumbra.GameData.Data; @@ -94,7 +93,6 @@ public sealed class DebugTab : Window, ITab private readonly DictEmote _emotes; private readonly Diagnostics _diagnostics; private readonly ObjectManager _objects; - private readonly IClientState _clientState; private readonly IDataManager _dataManager; private readonly IpcTester _ipcTester; private readonly CrashHandlerPanel _crashHandlerPanel; @@ -107,9 +105,9 @@ public sealed class DebugTab : Window, ITab private readonly ModMigratorDebug _modMigratorDebug; private readonly ShapeInspector _shapeInspector; private readonly FileWatcher.FileWatcherDrawer _fileWatcherDrawer; + private readonly DragDropManager _dragDropManager; - public DebugTab(Configuration config, CollectionManager collectionManager, ObjectManager objects, - IClientState clientState, IDataManager dataManager, + public DebugTab(Configuration config, CollectionManager collectionManager, ObjectManager objects, IDataManager dataManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, ResourceManagerService resourceManager, ResourceLoader resourceLoader, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, @@ -118,7 +116,8 @@ public sealed class DebugTab : Window, ITab Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer, - ModMigratorDebug modMigratorDebug, ShapeInspector shapeInspector, FileWatcher.FileWatcherDrawer fileWatcherDrawer) + ModMigratorDebug modMigratorDebug, ShapeInspector shapeInspector, FileWatcher.FileWatcherDrawer fileWatcherDrawer, + DragDropManager dragDropManager) : base("Penumbra Debug Window", WindowFlags.NoCollapse) { IsOpen = true; @@ -161,9 +160,9 @@ public sealed class DebugTab : Window, ITab _renderTargetDrawer = renderTargetDrawer; _modMigratorDebug = modMigratorDebug; _shapeInspector = shapeInspector; - _fileWatcherDrawer = fileWatcherDrawer; + _fileWatcherDrawer = fileWatcherDrawer; + _dragDropManager = dragDropManager; _objects = objects; - _clientState = clientState; _dataManager = dataManager; } @@ -209,6 +208,7 @@ public sealed class DebugTab : Window, ITab _globalVariablesDrawer.Draw(); DrawCloudApi(); DrawDebugTabIpc(); + _dragDropManager.DrawDebugInfo(); } @@ -265,7 +265,7 @@ public sealed class DebugTab : Window, ITab using (var resourceNode = ImUtf8.TreeNode("Custom Resources"u8)) { if (resourceNode) - foreach (var (path, resource) in collection._cache!.CustomResources) + foreach (var (path, resource) in collection.Cache!.CustomResources) { ImUtf8.TreeNode($"{path} -> 0x{(ulong)resource.ResourceHandle:X}", ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose(); @@ -274,7 +274,7 @@ public sealed class DebugTab : Window, ITab using var modNode = ImUtf8.TreeNode("Enabled Mods"u8); if (modNode) - foreach (var (mod, paths, manips) in collection._cache!.ModData.Data.OrderBy(t => t.Item1.Name)) + foreach (var (mod, paths, manips) in collection.Cache!.ModData.Data.OrderBy(t => t.Item1.Name)) { using var id = mod is TemporaryMod t ? Im.Id.Push(t.Priority.Value) : Im.Id.Push(((Mod)mod).ModPath.Name); using var node2 = Im.Tree.Node(mod.Name); @@ -318,9 +318,9 @@ public sealed class DebugTab : Window, ITab { PrintValue("Penumbra Version", $"{_validityChecker.Version} {DebugVersionString}"); PrintValue("Git Commit Hash", _validityChecker.CommitHash); - PrintValue(TutorialService.SelectedCollection, _collectionManager.Active.Current.Identity.Name); + PrintValue("Selected Collection", _collectionManager.Active.Current.Identity.Name); PrintValue(" has Cache", _collectionManager.Active.Current.HasCache.ToString()); - PrintValue(TutorialService.DefaultCollection, _collectionManager.Active.Default.Identity.Name); + PrintValue("Base Collection", _collectionManager.Active.Default.Identity.Name); PrintValue(" has Cache", _collectionManager.Active.Default.HasCache.ToString()); PrintValue("Mod Manager BasePath", _modManager.BasePath.Name); PrintValue("Mod Manager BasePath-Full", _modManager.BasePath.FullName); @@ -1032,15 +1032,15 @@ public sealed class DebugTab : Window, ITab /// Draw information about the models, materials and resources currently loaded by the local player. private unsafe void DrawPlayerModelInfo() { - var player = _clientState.LocalPlayer; - var name = player?.Name.ToString() ?? "NULL"; - if (!ImGui.CollapsingHeader($"Player Model Info: {name}##Draw") || player == null) + var player = _objects[0]; + var name = player.Valid ? player.StoredName() : "NULL"u8; + if (!ImGui.CollapsingHeader($"Player Model Info: {name}##Draw") || !player.Valid) return; DrawCopyableAddress("PlayerCharacter"u8, player.Address); - var model = (CharacterBase*)((Character*)player.Address)->GameObject.GetDrawObject(); - if (model == null) + var model = player.Model; + if (!model.IsCharacterBase) return; DrawCopyableAddress("CharacterBase"u8, model); @@ -1050,11 +1050,11 @@ public sealed class DebugTab : Window, ITab if (t1) { ImGuiUtil.DrawTableColumn("Flags"); - ImGuiUtil.DrawTableColumn($"{model->StateFlags}"); + ImGuiUtil.DrawTableColumn($"{model.AsCharacterBase->StateFlags}"); ImGuiUtil.DrawTableColumn("Has Model In Slot Loaded"); - ImGuiUtil.DrawTableColumn($"{model->HasModelInSlotLoaded:X8}"); + ImGuiUtil.DrawTableColumn($"{model.AsCharacterBase->HasModelInSlotLoaded:X8}"); ImGuiUtil.DrawTableColumn("Has Model Files In Slot Loaded"); - ImGuiUtil.DrawTableColumn($"{model->HasModelFilesInSlotLoaded:X8}"); + ImGuiUtil.DrawTableColumn($"{model.AsCharacterBase->HasModelFilesInSlotLoaded:X8}"); } } @@ -1073,9 +1073,9 @@ public sealed class DebugTab : Window, ITab ImGui.TableNextColumn(); ImGui.TableHeader("Model File"); - for (var i = 0; i < model->SlotCount; ++i) + for (var i = 0; i < model.AsCharacterBase->SlotCount; ++i) { - var imc = (ResourceHandle*)model->IMCArray[i]; + var imc = (ResourceHandle*)model.AsCharacterBase->IMCArray[i]; ImGui.TableNextRow(); ImGui.TableNextColumn(); Im.Text($"Slot {i}"); @@ -1085,7 +1085,7 @@ public sealed class DebugTab : Window, ITab if (imc is not null) Im.Text(imc->FileName().Span); - var mdl = (RenderModel*)model->Models[i]; + var mdl = (RenderModel*)model.AsCharacterBase->Models[i]; ImGui.TableNextColumn(); Penumbra.Dynamis.DrawPointer((nint)mdl); if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) diff --git a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs index ad00c156..9bd8df8d 100644 --- a/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/GlobalVariablesDrawer.cs @@ -1,10 +1,8 @@ -using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.Scheduler; using FFXIVClientStructs.FFXIV.Client.System.Scheduler.Resource; using FFXIVClientStructs.STD; using ImSharp; -using OtterGui.Text; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.String; @@ -20,13 +18,13 @@ public unsafe class GlobalVariablesDrawer( /// Draw information about some game global variables. public void Draw() { - var header = ImUtf8.CollapsingHeader("Global Variables"u8); + var header = Im.Tree.Header("Global Variables"u8); Im.Tooltip.OnHover("Draw information about global variables. Can provide useful starting points for a memory viewer."u8); if (!header) return; var actionManager = (ActionTimelineManager**)ActionTimelineManager.Instance(); - using (ImUtf8.Group()) + using (Im.Group()) { Penumbra.Dynamis.DrawPointer(characterUtility.Address); Penumbra.Dynamis.DrawPointer(residentResources.Address); @@ -39,16 +37,16 @@ public unsafe class GlobalVariablesDrawer( } Im.Line.Same(); - using (ImUtf8.Group()) + using (Im.Group()) { - ImUtf8.Text("CharacterUtility"u8); - ImUtf8.Text("ResidentResourceManager"u8); - ImUtf8.Text("ScheduleManagement"u8); - ImUtf8.Text("ActionTimelineManager*"u8); - ImUtf8.Text("ActionTimelineManager"u8); - ImUtf8.Text("SchedulerResourceManagement*"u8); - ImUtf8.Text("SchedulerResourceManagement"u8); - ImUtf8.Text("Device"u8); + Im.Text("CharacterUtility"u8); + Im.Text("ResidentResourceManager"u8); + Im.Text("ScheduleManagement"u8); + Im.Text("ActionTimelineManager*"u8); + Im.Text("ActionTimelineManager"u8); + Im.Text("SchedulerResourceManagement*"u8); + Im.Text("SchedulerResourceManagement"u8); + Im.Text("Device"u8); } DrawCharacterUtility(); @@ -64,7 +62,7 @@ public unsafe class GlobalVariablesDrawer( /// private void DrawCharacterUtility() { - using var tree = ImUtf8.TreeNode("Character Utility"u8); + using var tree = Im.Tree.Node("Character Utility"u8); if (!tree) return; @@ -76,30 +74,30 @@ public unsafe class GlobalVariablesDrawer( { var intern = CharacterUtility.ReverseIndices[idx]; var resource = characterUtility.Address->Resource(idx); - ImUtf8.DrawTableColumn($"[{idx}]"); - ImGui.TableNextColumn(); + table.DrawColumn($"[{idx}]"); + table.NextColumn(); Penumbra.Dynamis.DrawPointer(resource); - if (resource == null) + if (resource is null) { - ImGui.TableNextRow(); + table.NextRow(); continue; } - ImUtf8.DrawTableColumn(resource->CsHandle.FileName.AsSpan()); - ImGui.TableNextColumn(); + table.DrawColumn(resource->CsHandle.FileName.AsSpan()); + table.NextColumn(); var data = (nint)resource->CsHandle.GetData(); var length = resource->CsHandle.GetLength(); Penumbra.Dynamis.DrawPointer(data); - ImUtf8.DrawTableColumn(length.ToString()); - ImGui.TableNextColumn(); - if (intern.Value != -1) + table.DrawColumn($"{length}"); + table.NextColumn(); + if (intern.Value is -1) { Penumbra.Dynamis.DrawPointer(characterUtility.DefaultResource(intern).Address); - ImUtf8.DrawTableColumn($"{characterUtility.DefaultResource(intern).Size}"); + table.DrawColumn($"{characterUtility.DefaultResource(intern).Size}"); } else { - ImGui.TableNextColumn(); + table.NextColumn(); } } } @@ -107,11 +105,11 @@ public unsafe class GlobalVariablesDrawer( /// Draw information about the resident resource files. private void DrawResidentResources() { - using var tree = ImUtf8.TreeNode("Resident Resources"u8); + using var tree = Im.Tree.Node("Resident Resources"u8); if (!tree) return; - if (residentResources.Address == null || residentResources.Address->NumResources == 0) + if (residentResources.Address is null || residentResources.Address->NumResources is 0) return; using var table = Im.Table.Begin("##ResidentResources"u8, 5, TableFlags.RowBackground | TableFlags.SizingFixedFit, @@ -122,45 +120,40 @@ public unsafe class GlobalVariablesDrawer( for (var idx = 0; idx < residentResources.Address->NumResources; ++idx) { var resource = residentResources.Address->ResourceList[idx]; - ImUtf8.DrawTableColumn($"[{idx}]"); - ImGui.TableNextColumn(); + table.DrawColumn($"[{idx}]"); + table.NextColumn(); Penumbra.Dynamis.DrawPointer(resource); - if (resource == null) + if (resource is null) { - ImGui.TableNextRow(); + table.NextRow(); continue; } - ImUtf8.DrawTableColumn(resource->CsHandle.FileName.AsSpan()); - ImGui.TableNextColumn(); + table.DrawColumn(resource->CsHandle.FileName.AsSpan()); + table.NextColumn(); var data = (nint)resource->CsHandle.GetData(); var length = resource->CsHandle.GetLength(); Penumbra.Dynamis.DrawPointer(data); - ImUtf8.DrawTableColumn(length.ToString()); + table.DrawColumn($"{length}"); } } - private string _schedulerFilterList = string.Empty; - private string _schedulerFilterMap = string.Empty; - private CiByteString _schedulerFilterListU8 = CiByteString.Empty; - private CiByteString _schedulerFilterMapU8 = CiByteString.Empty; - private int _shownResourcesList = 0; - private int _shownResourcesMap = 0; + private StringU8 _schedulerFilterList = StringU8.Empty; + private StringU8 _schedulerFilterMap = StringU8.Empty; + private int _shownResourcesList; + private int _shownResourcesMap; private void DrawSchedulerResourcesMap() { - using var tree = ImUtf8.TreeNode("Scheduler Resources (Map)"u8); + using var tree = Im.Tree.Node("Scheduler Resources (Map)"u8); if (!tree) return; - if (scheduler.Address == null || scheduler.Scheduler == null) + if (scheduler.Address is null || scheduler.Scheduler is null) return; - if (ImUtf8.InputText("##SchedulerMapFilter"u8, ref _schedulerFilterMap, "Filter..."u8)) - _schedulerFilterMapU8 = CiByteString.FromString(_schedulerFilterMap, out var t, MetaDataComputation.All, false) - ? t - : CiByteString.Empty; - ImUtf8.Text($"{_shownResourcesMap} / {scheduler.Scheduler->Resources.LongCount}"); + Im.Input.Text("##SchedulerMapFilter"u8, ref _schedulerFilterMap, "Filter..."u8); + Im.Text($"{_shownResourcesMap} / {scheduler.Scheduler->Resources.LongCount}"); using var table = Im.Table.Begin("##SchedulerMapResources"u8, 10, TableFlags.RowBackground | TableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) @@ -170,27 +163,27 @@ public unsafe class GlobalVariablesDrawer( var map = (StdMap>*)&scheduler.Scheduler->Resources; var total = 0; _shownResourcesMap = 0; - foreach (var (key, resourcePtr) in *map) + foreach (var (_, resourcePtr) in *map) { var resource = resourcePtr.Value; - if (_schedulerFilterMap.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterMapU8.Span) >= 0) + if (_schedulerFilterMap.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterMap.Span) >= 0) { - ImUtf8.DrawTableColumn($"[{total:D4}]"); - ImUtf8.DrawTableColumn($"{resource->Name.Unk1}"); - ImUtf8.DrawTableColumn(new CiByteString(resource->Name.Buffer, MetaDataComputation.None).Span); - ImUtf8.DrawTableColumn($"{resource->Consumers}"); - ImUtf8.DrawTableColumn($"{resource->Unk1}"); // key - ImGui.TableNextColumn(); + table.DrawColumn($"[{total:D4}]"); + table.DrawColumn($"{resource->Name.Unk1}"); + table.DrawColumn(new CiByteString(resource->Name.Buffer).Span); + table.DrawColumn($"{resource->Consumers}"); + table.DrawColumn($"{resource->Unk1}"); // key + table.NextColumn(); Penumbra.Dynamis.DrawPointer(resource); - ImGui.TableNextColumn(); + table.NextColumn(); var resourceHandle = *((ResourceHandle**)resource + 3); Penumbra.Dynamis.DrawPointer(resourceHandle); - ImGui.TableNextColumn(); - ImUtf8.CopyOnClickSelectable(resourceHandle->FileName().Span); - ImGui.TableNextColumn(); - uint dataLength = 0; + table.NextColumn(); + ImEx.CopyOnClickSelectable(resourceHandle->FileName().Span); + table.NextColumn(); + var dataLength = 0u; Penumbra.Dynamis.DrawPointer(resource->GetResourceData(&dataLength)); - ImUtf8.DrawTableColumn($"{dataLength}"); + table.DrawColumn($"{dataLength}"); ++_shownResourcesMap; } @@ -200,18 +193,15 @@ public unsafe class GlobalVariablesDrawer( private void DrawSchedulerResourcesList() { - using var tree = ImUtf8.TreeNode("Scheduler Resources (List)"u8); + using var tree = Im.Tree.Node("Scheduler Resources (List)"u8); if (!tree) return; - if (scheduler.Address == null || scheduler.Scheduler == null) + if (scheduler.Address is null || scheduler.Scheduler is null) return; - if (ImUtf8.InputText("##SchedulerListFilter"u8, ref _schedulerFilterList, "Filter..."u8)) - _schedulerFilterListU8 = CiByteString.FromString(_schedulerFilterList, out var t, MetaDataComputation.All, false) - ? t - : CiByteString.Empty; - ImUtf8.Text($"{_shownResourcesList} / {scheduler.Scheduler->Resources.LongCount}"); + Im.Input.Text("##SchedulerListFilter"u8, ref _schedulerFilterList, "Filter..."u8); + Im.Text($"{_shownResourcesList} / {scheduler.Scheduler->Resources.LongCount}"); using var table = Im.Table.Begin("##SchedulerListResources"u8, 10, TableFlags.RowBackground | TableFlags.SizingFixedFit, -Vector2.UnitX); if (!table) @@ -220,26 +210,26 @@ public unsafe class GlobalVariablesDrawer( var resource = scheduler.Scheduler->Begin; var total = 0; _shownResourcesList = 0; - while (resource != null && total < scheduler.Scheduler->Resources.Count) + while (resource is not null && total < scheduler.Scheduler->Resources.Count) { - if (_schedulerFilterList.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterListU8.Span) >= 0) + if (_schedulerFilterList.Length is 0 || resource->Name.Buffer.IndexOf(_schedulerFilterList.Span) >= 0) { - ImUtf8.DrawTableColumn($"[{total:D4}]"); - ImUtf8.DrawTableColumn($"{resource->Name.Unk1}"); - ImUtf8.DrawTableColumn(new CiByteString(resource->Name.Buffer, MetaDataComputation.None).Span); - ImUtf8.DrawTableColumn($"{resource->Consumers}"); - ImUtf8.DrawTableColumn($"{resource->Unk1}"); // key - ImGui.TableNextColumn(); + table.DrawColumn($"[{total:D4}]"); + table.DrawColumn($"{resource->Name.Unk1}"); + table.DrawColumn(new CiByteString(resource->Name.Buffer).Span); + table.DrawColumn($"{resource->Consumers}"); + table.DrawColumn($"{resource->Unk1}"); // key + table.NextColumn(); Penumbra.Dynamis.DrawPointer(resource); - ImGui.TableNextColumn(); + table.NextColumn(); var resourceHandle = *((ResourceHandle**)resource + 3); Penumbra.Dynamis.DrawPointer(resourceHandle); - ImGui.TableNextColumn(); - ImUtf8.CopyOnClickSelectable(resourceHandle->FileName().Span); - ImGui.TableNextColumn(); + table.NextColumn(); + ImEx.CopyOnClickSelectable(resourceHandle->FileName().Span); + table.NextColumn(); uint dataLength = 0; Penumbra.Dynamis.DrawPointer(resource->GetResourceData(&dataLength)); - ImUtf8.DrawTableColumn($"{dataLength}"); + table.DrawColumn($"{dataLength}"); ++_shownResourcesList; } diff --git a/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs b/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs index fb290fda..cf42540b 100644 --- a/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/HookOverrideDrawer.cs @@ -1,7 +1,5 @@ -using Dalamud.Bindings.ImGui; using Dalamud.Plugin; using ImSharp; -using OtterGui.Text; using Penumbra.Interop.Hooks; namespace Penumbra.UI.Tabs.Debug; @@ -12,19 +10,19 @@ public class HookOverrideDrawer(IDalamudPluginInterface pluginInterface) : Luna. public void Draw() { - using var header = ImUtf8.CollapsingHeaderId("Generate Hook Override"u8); + using var header = Im.Tree.HeaderId("Generate Hook Override"u8); if (!header) return; _overrides ??= HookOverrides.Instance.Clone(); - if (ImUtf8.Button("Save"u8)) + if (Im.Button("Save"u8)) _overrides.Write(pluginInterface); Im.Line.Same(); var path = Path.Combine(pluginInterface.GetPluginConfigDirectory(), HookOverrides.FileName); var exists = File.Exists(path); - if (ImUtf8.ButtonEx("Delete"u8, disabled: !exists, tooltip: exists ? ""u8 : "File does not exist."u8)) + if (ImEx.Button("Delete"u8, disabled: !exists, tooltip: exists ? ""u8 : "File does not exist."u8)) try { File.Delete(path); @@ -36,23 +34,23 @@ public class HookOverrideDrawer(IDalamudPluginInterface pluginInterface) : Luna. bool? allVisible = null; Im.Line.Same(); - if (ImUtf8.Button("Disable All Visible Hooks"u8)) + if (Im.Button("Disable All Visible Hooks"u8)) allVisible = true; Im.Line.Same(); - if (ImUtf8.Button("Enable All VisibleHooks"u8)) + if (Im.Button("Enable All VisibleHooks"u8)) allVisible = false; bool? all = null; Im.Line.Same(); - if (ImUtf8.Button("Disable All Hooks")) + if (Im.Button("Disable All Hooks"u8)) all = true; Im.Line.Same(); - if (ImUtf8.Button("Enable All Hooks")) + if (Im.Button("Enable All Hooks"u8)) all = false; foreach (var propertyField in typeof(HookOverrides).GetFields().Where(f => f is { IsStatic: false, FieldType.IsValueType: true })) { - using var tree = ImUtf8.TreeNode(propertyField.Name); + using var tree = Im.Tree.Node(propertyField.Name); if (!tree) { if (all.HasValue) @@ -72,7 +70,7 @@ public class HookOverrideDrawer(IDalamudPluginInterface pluginInterface) : Luna. foreach (var valueField in propertyField.FieldType.GetFields()) { var value = valueField.GetValue(property) as bool? ?? false; - if (ImUtf8.Checkbox($"Disable {valueField.Name}", ref value) || allVisible.HasValue) + if (Im.Checkbox($"Disable {valueField.Name}", ref value) || allVisible.HasValue) { valueField.SetValue(property, allVisible ?? value); propertyField.SetValue(_overrides, property); diff --git a/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs b/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs index 7e985a93..3c447364 100644 --- a/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs +++ b/Penumbra/UI/Tabs/Debug/ModMigratorDebug.cs @@ -1,6 +1,4 @@ -using Dalamud.Bindings.ImGui; using ImSharp; -using OtterGui.Text; using Penumbra.Services; namespace Penumbra.UI.Tabs.Debug; @@ -14,14 +12,14 @@ public class ModMigratorDebug(ModMigrator migrator) : Luna.IUiService public void Draw() { - if (!ImUtf8.CollapsingHeaderId("Mod Migrator"u8)) + if (!Im.Tree.HeaderId("Mod Migrator"u8)) return; - ImUtf8.InputText("##input"u8, ref _inputPath, "Input Path..."u8); - ImUtf8.InputText("##output"u8, ref _outputPath, "Output Path..."u8); + Im.Input.Text("##input"u8, ref _inputPath, "Input Path..."u8); + Im.Input.Text("##output"u8, ref _outputPath, "Output Path..."u8); - if (ImUtf8.ButtonEx("Create Index Texture"u8, "Requires input to be a path to a normal texture."u8, default, _inputPath.Length == 0 - || _outputPath.Length == 0 + if (ImEx.Button("Create Index Texture"u8, default, "Requires input to be a path to a normal texture."u8, _inputPath.Length is 0 + || _outputPath.Length is 0 || _indexTask is { IsCompleted: false, @@ -31,11 +29,11 @@ public class ModMigratorDebug(ModMigrator migrator) : Luna.IUiService if (_indexTask is not null) { Im.Line.Same(); - ImUtf8.TextFrameAligned($"{_indexTask.Status}"); + ImEx.TextFrameAligned($"{_indexTask.Status}"); } - if (ImUtf8.ButtonEx("Update Model File"u8, "Requires input to be a path to a mdl."u8, default, _inputPath.Length == 0 - || _outputPath.Length == 0 + if (ImEx.Button("Update Model File"u8, default, "Requires input to be a path to a mdl."u8, _inputPath.Length is 0 + || _outputPath.Length is 0 || _mdlTask is { IsCompleted: false, @@ -49,7 +47,7 @@ public class ModMigratorDebug(ModMigrator migrator) : Luna.IUiService if (_mdlTask is not null) { Im.Line.Same(); - ImUtf8.TextFrameAligned($"{_mdlTask.Status}"); + ImEx.TextFrameAligned($"{_mdlTask.Status}"); } } } diff --git a/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs b/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs index 1696d7d1..3f3e994f 100644 --- a/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/RenderTargetDrawer.cs @@ -1,65 +1,64 @@ -using Dalamud.Bindings.ImGui; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.Graphics.Render; using ImSharp; -using OtterGui; -using OtterGui.Text; using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Services; namespace Penumbra.UI.Tabs.Debug; -public class RenderTargetDrawer(RenderTargetHdrEnabler renderTargetHdrEnabler, DalamudConfigService dalamudConfig, Configuration config) : Luna.IUiService +public class RenderTargetDrawer(RenderTargetHdrEnabler renderTargetHdrEnabler, DalamudConfigService dalamudConfig, Configuration config) + : Luna.IUiService { private void DrawStatistics() { - using (ImUtf8.Group()) + using (Im.Group()) { - ImUtf8.Text("Wait For Plugins (Now)"); - ImUtf8.Text("Wait For Plugins (First Launch)"); + Im.Text("Wait For Plugins (Now)"u8); + Im.Text("Wait For Plugins (First Launch)"u8); - ImUtf8.Text("HDR Enabled (Now)"); - ImUtf8.Text("HDR Enabled (First Launch)"); + Im.Text("HDR Enabled (Now)"u8); + Im.Text("HDR Enabled (First Launch)"u8); - ImUtf8.Text("HDR Hook Overriden (Now)"); - ImUtf8.Text("HDR Hook Overriden (First Launch)"); + Im.Text("HDR Hook Overriden (Now)"u8); + Im.Text("HDR Hook Overriden (First Launch)"u8); - ImUtf8.Text("HDR Detour Called"); - ImUtf8.Text("Penumbra Reload Count"); + Im.Text("HDR Detour Called"u8); + Im.Text("Penumbra Reload Count"u8); } + Im.Line.Same(); - using (ImUtf8.Group()) + using (Im.Group()) { - ImUtf8.Text($"{(dalamudConfig.GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool w) ? w.ToString() : "Unknown")}"); - ImUtf8.Text($"{renderTargetHdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"}"); + Im.Text($"{(dalamudConfig.GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool w) ? w.ToString() : "Unknown")}"); + Im.Text($"{renderTargetHdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"}"); - ImUtf8.Text($"{config.HdrRenderTargets}"); - ImUtf8.Text($"{renderTargetHdrEnabler.FirstLaunchHdrState}"); + Im.Text($"{config.HdrRenderTargets}"); + Im.Text($"{renderTargetHdrEnabler.FirstLaunchHdrState}"); - ImUtf8.Text($"{HookOverrides.Instance.PostProcessing.RenderTargetManagerInitialize}"); - ImUtf8.Text($"{!renderTargetHdrEnabler.FirstLaunchHdrHookOverrideState}"); + Im.Text($"{HookOverrides.Instance.PostProcessing.RenderTargetManagerInitialize}"); + Im.Text($"{!renderTargetHdrEnabler.FirstLaunchHdrHookOverrideState}"); - ImUtf8.Text($"{renderTargetHdrEnabler.HdrEnabledSuccess}"); - ImUtf8.Text($"{renderTargetHdrEnabler.PenumbraReloadCount}"); + Im.Text($"{renderTargetHdrEnabler.HdrEnabledSuccess}"); + Im.Text($"{renderTargetHdrEnabler.PenumbraReloadCount}"); } } /// Draw information about render targets. public unsafe void Draw() { - if (!ImUtf8.CollapsingHeader("Render Targets"u8)) + if (!Im.Tree.Header("Render Targets"u8)) return; DrawStatistics(); - ImUtf8.Dummy(0); + Im.Dummy(0); Im.Separator(); - ImUtf8.Dummy(0); + Im.Dummy(0); var report = renderTargetHdrEnabler.TextureReport; - if (report == null) + if (report is null) { - ImUtf8.Text("The RenderTargetManager report has not been gathered."u8); - ImUtf8.Text("Please restart the game with Debug Mode and Wait for Plugins on Startup enabled to fill this section."u8); + Im.Text("The RenderTargetManager report has not been gathered."u8); + Im.Text("Please restart the game with Debug Mode and Wait for Plugins on Startup enabled to fill this section."u8); return; } @@ -72,27 +71,27 @@ public class RenderTargetDrawer(RenderTargetHdrEnabler renderTargetHdrEnabler, D table.SetupColumn("Original Texture Format"u8, TableColumnFlags.WidthStretch, 0.2f); table.SetupColumn("Current Texture Format"u8, TableColumnFlags.WidthStretch, 0.2f); table.SetupColumn("Comment"u8, TableColumnFlags.WidthStretch, 0.3f); - ImGui.TableHeadersRow(); + table.HeaderRow(); foreach (var record in report) { - ImUtf8.DrawTableColumn($"0x{record.Offset:X}"); - ImUtf8.DrawTableColumn($"{record.CreationOrder}"); - ImUtf8.DrawTableColumn($"{record.OriginalTextureFormat}"); - ImGui.TableNextColumn(); + table.DrawColumn($"0x{record.Offset:X}"); + table.DrawColumn($"{record.CreationOrder}"); + table.DrawColumn($"{record.OriginalTextureFormat}"); + table.NextColumn(); var texture = *(Texture**)((nint)RenderTargetManager.Instance() + record.Offset); - if (texture != null) + if (texture is not null) { - using var color = ImGuiColor.Text.Push(ImGuiUtil.HalfBlendText(0xFF), + using var color = ImGuiColor.Text.Push(ImGuiColor.Text.Get().HalfBlend(Rgba32.Red), texture->TextureFormat != record.OriginalTextureFormat); - ImUtf8.Text($"{texture->TextureFormat}"); + Im.Text($"{texture->TextureFormat}"); } - ImGui.TableNextColumn(); + table.NextColumn(); var forcedConfig = RenderTargetHdrEnabler.GetForcedTextureConfig(record.CreationOrder); if (forcedConfig.HasValue) - ImUtf8.Text(forcedConfig.Value.Comment); + Im.Text(forcedConfig.Value.Comment); } } } diff --git a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs index d0f9c4c4..b09f63bd 100644 --- a/Penumbra/UI/Tabs/Debug/ShapeInspector.cs +++ b/Penumbra/UI/Tabs/Debug/ShapeInspector.cs @@ -1,8 +1,5 @@ -using Dalamud.Bindings.ImGui; using Dalamud.Interface; -using Dalamud.Interface.Utility.Raii; using ImSharp; -using OtterGui.Text; using Penumbra.Collections.Cache; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; @@ -18,18 +15,18 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) public void Draw() { - ImUtf8.InputScalar("Object Index"u8, ref _objectIndex); + Im.Input.Scalar("Object Index"u8, ref _objectIndex); var actor = objects[0]; if (!actor.IsCharacter) { - ImUtf8.Text("No valid character."u8); + Im.Text("No valid character."u8); return; } var human = actor.Model; if (!human.IsHuman) { - ImUtf8.Text("No valid character."u8); + Im.Text("No valid character."u8); return; } @@ -42,7 +39,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) private unsafe void DrawCollectionAttributeCache(Actor actor) { var data = resolver.IdentifyCollection(actor.AsObject, true); - using var treeNode1 = ImUtf8.TreeNode($"Collection Attribute Cache ({data.ModCollection})"); + using var treeNode1 = Im.Tree.Node($"Collection Attribute Cache ({data.ModCollection})"); if (!treeNode1.Success || !data.ModCollection.HasCache) return; @@ -52,19 +49,19 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) table.SetupColumn("Attribute"u8, TableColumnFlags.WidthFixed, 150 * Im.Style.GlobalScale); table.SetupColumn("State"u8, TableColumnFlags.WidthStretch); + table.HeaderRow(); - ImGui.TableHeadersRow(); foreach (var (attribute, set) in data.ModCollection.MetaCache!.Atr.Data.OrderBy(a => a.Key)) { - ImUtf8.DrawTableColumn(attribute.AsSpan); - DrawValues(attribute, set); + table.DrawColumn(attribute.AsSpan); + DrawValues(table, attribute, set); } } private unsafe void DrawCollectionShapeCache(Actor actor) { var data = resolver.IdentifyCollection(actor.AsObject, true); - using var treeNode1 = ImUtf8.TreeNode($"Collection Shape Cache ({data.ModCollection})"); + using var treeNode1 = Im.Tree.Node($"Collection Shape Cache ({data.ModCollection})"); if (!treeNode1.Success || !data.ModCollection.HasCache) return; @@ -75,27 +72,27 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) table.SetupColumn("Condition"u8, TableColumnFlags.WidthFixed, 150 * Im.Style.GlobalScale); table.SetupColumn("Shape"u8, TableColumnFlags.WidthFixed, 150 * Im.Style.GlobalScale); table.SetupColumn("State"u8, TableColumnFlags.WidthStretch); + table.HeaderRow(); - ImGui.TableHeadersRow(); foreach (var condition in Enum.GetValues()) { foreach (var (shape, set) in data.ModCollection.MetaCache!.Shp.State(condition).OrderBy(shp => shp.Key)) { - ImUtf8.DrawTableColumn(condition.ToString()); - ImUtf8.DrawTableColumn(shape.AsSpan); - DrawValues(shape, set); + table.DrawColumn($"{condition}"); + table.DrawColumn(shape.AsSpan); + DrawValues(table, shape, set); } } } - private static void DrawValues(in ShapeAttributeString shapeAttribute, ShapeAttributeHashSet set) + private static void DrawValues(in Im.TableDisposable table, in ShapeAttributeString _, ShapeAttributeHashSet set) { - ImGui.TableNextColumn(); + table.NextColumn(); var disabledColor = Im.Style[ImGuiColor.TextDisabled]; if (set.All is { } value) { using var color = ImGuiColor.Text.Push(disabledColor, !value); - ImUtf8.Text("All, "u8); + Im.Text("All, "u8); Im.Line.Same(0, 0); } @@ -105,7 +102,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) continue; using var color = ImGuiColor.Text.Push(disabledColor, !value2); - ImUtf8.Text($"All {slot.ToName()}, "); + Im.Text($"All {slot.ToNameU8()}, "); Im.Line.Same(0, 0); } @@ -114,7 +111,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (set[gr] is { } value3) { using var color = ImGuiColor.Text.Push(disabledColor, !value3); - ImUtf8.Text($"All {gr.ToName()}, "); + Im.Text($"All {gr.ToNameU8()}, "); Im.Line.Same(0, 0); } else @@ -125,7 +122,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) continue; using var color = ImGuiColor.Text.Push(disabledColor, !value4); - ImUtf8.Text($"All {gr.ToName()} {slot.ToName()}, "); + Im.Text($"All {gr.ToNameU8()} {slot.ToNameU8()}, "); Im.Line.Same(0, 0); } } @@ -140,7 +137,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (set[slot, GenderRace.Unknown] != enabled) { using var color = ImGuiColor.Text.Push(disabledColor, !enabled); - ImUtf8.Text($"{slot.ToName()} {id.Id:D4}, "); + Im.Text($"{slot.ToNameU8()} {id.Id:D4}, "); Im.Line.Same(0, 0); } } @@ -155,7 +152,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) if (set[slot, gr] != enabled) { using var color = ImGuiColor.Text.Push(disabledColor, !enabled); - ImUtf8.Text($"{gr.ToName()} {slot.ToName()} #{id.Id:D4}, "); + Im.Text($"{gr.ToNameU8()} {slot.ToNameU8()} #{id.Id:D4}, "); Im.Line.Same(0, 0); } @@ -169,7 +166,7 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) private unsafe void DrawCharacterShapes(Model human) { - using var treeNode2 = ImUtf8.TreeNode("Character Model Shapes"u8); + using var treeNode2 = Im.Tree.Node("Character Model Shapes"u8); if (!treeNode2) return; @@ -184,49 +181,48 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) table.SetupColumn("ID"u8, TableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 4); table.SetupColumn("Count"u8, TableColumnFlags.WidthFixed, 30 * Im.Style.GlobalScale); table.SetupColumn("Shapes"u8, TableColumnFlags.WidthStretch); - - ImGui.TableHeadersRow(); + table.HeaderRow(); var disabledColor = Im.Style[ImGuiColor.TextDisabled]; for (var i = 0; i < human.AsHuman->SlotCount; ++i) { - ImUtf8.DrawTableColumn($"{(uint)i:D2}"); - ImUtf8.DrawTableColumn(((HumanSlot)i).ToName()); + table.DrawColumn($"{(uint)i:D2}"); + table.DrawColumn(((HumanSlot)i).ToNameU8()); - ImGui.TableNextColumn(); + table.NextColumn(); var model = human.AsHuman->Models[i]; Penumbra.Dynamis.DrawPointer((nint)model); if (model is not null) { var mask = model->EnabledShapeKeyIndexMask; - ImUtf8.DrawTableColumn($"{mask:X8}"); - ImUtf8.DrawTableColumn($"{human.GetModelId((HumanSlot)i):D4}"); - ImUtf8.DrawTableColumn($"{model->ModelResourceHandle->Shapes.Count}"); - ImGui.TableNextColumn(); + table.DrawColumn($"{mask:X8}"); + table.DrawColumn($"{human.GetModelId((HumanSlot)i):D4}"); + table.DrawColumn($"{model->ModelResourceHandle->Shapes.Count}"); + table.NextColumn(); foreach (var (idx, (shape, flag)) in model->ModelResourceHandle->Shapes.Index()) { var disabled = (mask & (1u << flag)) is 0; using var color = ImGuiColor.Text.Push(disabledColor, disabled); - ImUtf8.Text(shape.AsSpan()); + Im.Text(shape.AsSpan()); Im.Line.Same(0, 0); - ImUtf8.Text(", "u8); + Im.Text(", "u8); if (idx % 8 < 7) Im.Line.Same(0, 0); } } else { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); + table.NextColumn(); + table.NextColumn(); + table.NextColumn(); + table.NextColumn(); } } } private unsafe void DrawCharacterAttributes(Model human) { - using var treeNode2 = ImUtf8.TreeNode("Character Model Attributes"u8); + using var treeNode2 = Im.Tree.Node("Character Model Attributes"u8); if (!treeNode2) return; @@ -241,42 +237,41 @@ public class ShapeInspector(ObjectManager objects, CollectionResolver resolver) table.SetupColumn("ID"u8, TableColumnFlags.WidthFixed, UiBuilder.MonoFont.GetCharAdvance('0') * 4); table.SetupColumn("Count"u8, TableColumnFlags.WidthFixed, 30 * Im.Style.GlobalScale); table.SetupColumn("Attributes"u8, TableColumnFlags.WidthStretch); - - ImGui.TableHeadersRow(); + table.HeaderRow(); var disabledColor = Im.Style[ImGuiColor.TextDisabled]; for (var i = 0; i < human.AsHuman->SlotCount; ++i) { - ImUtf8.DrawTableColumn($"{(uint)i:D2}"); - ImUtf8.DrawTableColumn(((HumanSlot)i).ToName()); + table.DrawColumn($"{(uint)i:D2}"); + table.DrawColumn(((HumanSlot)i).ToNameU8()); - ImGui.TableNextColumn(); + table.NextColumn(); var model = human.AsHuman->Models[i]; Penumbra.Dynamis.DrawPointer((nint)model); if (model is not null) { var mask = model->EnabledAttributeIndexMask; - ImUtf8.DrawTableColumn($"{mask:X8}"); - ImUtf8.DrawTableColumn($"{human.GetModelId((HumanSlot)i):D4}"); - ImUtf8.DrawTableColumn($"{model->ModelResourceHandle->Attributes.Count}"); - ImGui.TableNextColumn(); + table.DrawColumn($"{mask:X8}"); + table.DrawColumn($"{human.GetModelId((HumanSlot)i):D4}"); + table.DrawColumn($"{model->ModelResourceHandle->Attributes.Count}"); + table.NextColumn(); foreach (var (idx, (attribute, flag)) in model->ModelResourceHandle->Attributes.Index()) { var disabled = (mask & (1u << flag)) is 0; using var color = ImGuiColor.Text.Push(disabledColor, disabled); - ImUtf8.Text(attribute.AsSpan()); + Im.Text(attribute.AsSpan()); Im.Line.Same(0, 0); - ImUtf8.Text(", "u8); + Im.Text(", "u8); if (idx % 8 < 7) Im.Line.Same(0, 0); } } else { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); + table.NextColumn(); + table.NextColumn(); + table.NextColumn(); + table.NextColumn(); } } } diff --git a/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs b/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs index 642a2f15..f47c44cb 100644 --- a/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/TexHeaderDrawer.cs @@ -1,9 +1,6 @@ -using Dalamud.Bindings.ImGui; using Dalamud.Interface.DragDrop; -using Dalamud.Interface.Utility.Raii; using ImSharp; using Lumina.Data.Files; -using OtterGui.Text; using Penumbra.UI.Classes; namespace Penumbra.UI.Tabs.Debug; @@ -17,7 +14,7 @@ public class TexHeaderDrawer(IDragDropManager dragDrop) : Luna.IUiService public void Draw() { - using var header = ImUtf8.CollapsingHeaderId("Tex Header"u8); + using var header = Im.Tree.HeaderId("Tex Header"u8); if (!header) return; @@ -29,11 +26,11 @@ public class TexHeaderDrawer(IDragDropManager dragDrop) : Luna.IUiService { dragDrop.CreateImGuiSource("TexFileDragDrop", m => m.Files.Count == 1 && m.Extensions.Contains(".tex"), m => { - ImUtf8.Text($"Dragging {m.Files[0]}..."); + Im.Text($"Dragging {m.Files[0]}..."); return true; }); - ImUtf8.Button("Drag .tex here..."); + Im.Button("Drag .tex here..."u8); if (dragDrop.CreateImGuiTarget("TexFileDragDrop", out var files, out _)) ReadTex(files[0]); } @@ -43,13 +40,13 @@ public class TexHeaderDrawer(IDragDropManager dragDrop) : Luna.IUiService if (_path == null) return; - ImUtf8.TextFramed(_path, 0, borderColor: 0xFFFFFFFF); + ImEx.TextFramed(_path, default, 0, borderColor: 0xFFFFFFFF); if (_exception != null) { using var color = ImGuiColor.Text.Push(Colors.RegexWarningBorder); - ImUtf8.TextWrapped($"Failure to load file:\n{_exception}"); + Im.TextWrapped($"Failure to load file:\n{_exception}"); } else if (_tex != null) { @@ -57,45 +54,37 @@ public class TexHeaderDrawer(IDragDropManager dragDrop) : Luna.IUiService if (!table) return; - TableLine("Format"u8, _header.Format); - TableLine("Width"u8, _header.Width); - TableLine("Height"u8, _header.Height); - TableLine("Depth"u8, _header.Depth); - TableLine("Mip Levels"u8, _header.MipCount); - TableLine("Array Size"u8, _header.ArraySize); - TableLine("Type"u8, _header.Type); - TableLine("Mip Flag"u8, _header.MipUnknownFlag); - TableLine("Byte Size"u8, _tex.Length); + table.DrawDataPair("Format"u8, _header.Format); + table.DrawDataPair("Width"u8, _header.Width); + table.DrawDataPair("Height"u8, _header.Height); + table.DrawDataPair("Depth"u8, _header.Depth); + table.DrawDataPair("Mip Levels"u8, _header.MipCount); + table.DrawDataPair("Array Size"u8, _header.ArraySize); + table.DrawDataPair("Type"u8, _header.Type); + table.DrawDataPair("Mip Flag"u8, _header.MipUnknownFlag); + table.DrawDataPair("Byte Size"u8, _tex.Length); unsafe { - TableLine("LoD Offset 0"u8, _header.LodOffset[0]); - TableLine("LoD Offset 1"u8, _header.LodOffset[1]); - TableLine("LoD Offset 2"u8, _header.LodOffset[2]); - TableLine("LoD Offset 0"u8, _header.OffsetToSurface[0]); - TableLine("LoD Offset 1"u8, _header.OffsetToSurface[1]); - TableLine("LoD Offset 2"u8, _header.OffsetToSurface[2]); - TableLine("LoD Offset 3"u8, _header.OffsetToSurface[3]); - TableLine("LoD Offset 4"u8, _header.OffsetToSurface[4]); - TableLine("LoD Offset 5"u8, _header.OffsetToSurface[5]); - TableLine("LoD Offset 6"u8, _header.OffsetToSurface[6]); - TableLine("LoD Offset 7"u8, _header.OffsetToSurface[7]); - TableLine("LoD Offset 8"u8, _header.OffsetToSurface[8]); - TableLine("LoD Offset 9"u8, _header.OffsetToSurface[9]); - TableLine("LoD Offset 10"u8, _header.OffsetToSurface[10]); - TableLine("LoD Offset 11"u8, _header.OffsetToSurface[11]); - TableLine("LoD Offset 12"u8, _header.OffsetToSurface[12]); + table.DrawDataPair("LoD Offset 0"u8, _header.LodOffset[0]); + table.DrawDataPair("LoD Offset 1"u8, _header.LodOffset[1]); + table.DrawDataPair("LoD Offset 2"u8, _header.LodOffset[2]); + table.DrawDataPair("LoD Offset 0"u8, _header.OffsetToSurface[0]); + table.DrawDataPair("LoD Offset 1"u8, _header.OffsetToSurface[1]); + table.DrawDataPair("LoD Offset 2"u8, _header.OffsetToSurface[2]); + table.DrawDataPair("LoD Offset 3"u8, _header.OffsetToSurface[3]); + table.DrawDataPair("LoD Offset 4"u8, _header.OffsetToSurface[4]); + table.DrawDataPair("LoD Offset 5"u8, _header.OffsetToSurface[5]); + table.DrawDataPair("LoD Offset 6"u8, _header.OffsetToSurface[6]); + table.DrawDataPair("LoD Offset 7"u8, _header.OffsetToSurface[7]); + table.DrawDataPair("LoD Offset 8"u8, _header.OffsetToSurface[8]); + table.DrawDataPair("LoD Offset 9"u8, _header.OffsetToSurface[9]); + table.DrawDataPair("LoD Offset 10"u8, _header.OffsetToSurface[10]); + table.DrawDataPair("LoD Offset 11"u8, _header.OffsetToSurface[11]); + table.DrawDataPair("LoD Offset 12"u8, _header.OffsetToSurface[12]); } } } - private static void TableLine(ReadOnlySpan text, T value) - { - ImGui.TableNextColumn(); - ImUtf8.Text(text); - ImGui.TableNextColumn(); - ImUtf8.Text($"{value}"); - } - private unsafe void ReadTex(string path) { try diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs deleted file mode 100644 index 98b53c11..00000000 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ /dev/null @@ -1,195 +0,0 @@ -using Dalamud.Bindings.ImGui; -using Dalamud.Interface; -using ImSharp; -using Luna; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Text; -using Penumbra.Api.Enums; -using Penumbra.Collections; -using Penumbra.Collections.Cache; -using Penumbra.Collections.Manager; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Editor; -using Penumbra.String.Classes; -using Penumbra.UI.Classes; - -namespace Penumbra.UI.Tabs; - -public sealed class EffectiveTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader) - : ITab -{ - public ReadOnlySpan Label - => "Effective Changes"u8; - - public TabType Identifier - => TabType.EffectiveChanges; - - public void DrawContent() - { - SetupEffectiveSizes(); - collectionHeader.Draw(true); - DrawFilters(); - using var child = ImRaii.Child("##EffectiveChangesTab", Im.ContentRegion.Available, false); - if (!child) - return; - - var height = Im.Style.TextHeightWithSpacing + 2 * Im.Style.CellPadding.Y; - var skips = ImGuiClip.GetNecessarySkips(height); - using var table = Im.Table.Begin("##EffectiveChangesTable"u8, 3, TableFlags.RowBackground); - if (!table) - return; - - table.SetupColumn("##gamePath"u8, TableColumnFlags.WidthFixed, _effectiveLeftTextLength); - table.SetupColumn(StringU8.Empty, TableColumnFlags.WidthFixed, _effectiveArrowLength); - table.SetupColumn("##file"u8, TableColumnFlags.WidthFixed, _effectiveRightTextLength); - - DrawEffectiveRows(collectionManager.Active.Current, skips, height, - _effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0); - } - - // Sizes - private float _effectiveLeftTextLength; - private float _effectiveRightTextLength; - private float _effectiveUnscaledArrowLength; - private float _effectiveArrowLength; - - // Filters - private string _effectiveGamePathFilter = string.Empty; - private string _effectiveFilePathFilter = string.Empty; - - /// Setup table sizes. - private void SetupEffectiveSizes() - { - if (_effectiveUnscaledArrowLength == 0) - { - using var font = ImRaii.PushFont(UiBuilder.IconFont); - _effectiveUnscaledArrowLength = - ImGui.CalcTextSize(FontAwesomeIcon.LongArrowAltLeft.ToIconString()).X / Im.Style.GlobalScale; - } - - _effectiveArrowLength = _effectiveUnscaledArrowLength * Im.Style.GlobalScale; - _effectiveLeftTextLength = 450 * Im.Style.GlobalScale; - _effectiveRightTextLength = ImGui.GetWindowSize().X - _effectiveArrowLength - _effectiveLeftTextLength; - } - - /// Draw the header line for filters. - private void DrawFilters() - { - var tmp = _effectiveGamePathFilter; - Im.Item.SetNextWidth(_effectiveLeftTextLength); - if (ImGui.InputTextWithHint("##gamePathFilter", "Filter game path...", ref tmp, 256)) - _effectiveGamePathFilter = tmp; - - Im.Line.Same(_effectiveArrowLength + _effectiveLeftTextLength + 3 * Im.Style.ItemSpacing.X); - Im.Item.SetNextWidth(-1); - tmp = _effectiveFilePathFilter; - if (ImGui.InputTextWithHint("##fileFilter", "Filter file path...", ref tmp, 256)) - _effectiveFilePathFilter = tmp; - } - - /// Draw all rows for one collection respecting filters and using clipping. - private void DrawEffectiveRows(ModCollection active, int skips, float height, bool hasFilters) - { - // We can use the known counts if no filters are active. - var stop = hasFilters - ? ImGuiClip.FilteredClippedDraw(active.ResolvedFiles, skips, CheckFilters, DrawLine) - : ImGuiClip.ClippedDraw(active.ResolvedFiles, skips, DrawLine, active.ResolvedFiles.Count); - - var m = active.MetaCache; - // If no meta manipulations are active, we can just draw the end dummy. - if (m is { Count: > 0 }) - { - // Filters mean we can not use the known counts. - if (hasFilters) - { - var it2 = m.IdentifierSources.Select(p => (p.Item1.ToString(), p.Item2.Name)); - if (stop >= 0) - { - ImGuiClip.DrawEndDummy(stop + it2.Count(CheckFilters), height); - } - else - { - stop = ImGuiClip.FilteredClippedDraw(it2, skips, CheckFilters, DrawLine, ~stop); - ImGuiClip.DrawEndDummy(stop, height); - } - } - else - { - if (stop >= 0) - { - ImGuiClip.DrawEndDummy(stop + m.Count, height); - } - else - { - stop = ImGuiClip.ClippedDraw(m.IdentifierSources, skips, DrawLine, m.Count, ~stop); - ImGuiClip.DrawEndDummy(stop, height); - } - } - } - else - { - ImGuiClip.DrawEndDummy(stop, height); - } - } - - /// Draw a line for a game path and its redirected file. - private static void DrawLine(KeyValuePair pair) - { - var (path, name) = pair; - ImGui.TableNextColumn(); - ImUtf8.CopyOnClickSelectable(path.Path.Span); - - ImGui.TableNextColumn(); - ImGuiUtil.PrintIcon(FontAwesomeIcon.LongArrowAltLeft); - ImGui.TableNextColumn(); - ImUtf8.CopyOnClickSelectable(name.Path.InternalName.Span); - ImGuiUtil.HoverTooltip($"\nChanged by {name.Mod.Name}."); - } - - /// Draw a line for a path and its name. - private static void DrawLine((string, string) pair) - { - var (path, name) = pair; - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(path); - - ImGui.TableNextColumn(); - ImGuiUtil.PrintIcon(FontAwesomeIcon.LongArrowAltLeft); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(name); - } - - /// Draw a line for a unfiltered/unconverted manipulation and mod-index pair. - private static void DrawLine((IMetaIdentifier, IMod) pair) - { - var (manipulation, mod) = pair; - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(manipulation.ToString()); - - ImGui.TableNextColumn(); - ImGuiUtil.PrintIcon(FontAwesomeIcon.LongArrowAltLeft); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(mod.Name); - } - - /// Check filters for file replacements. - private bool CheckFilters(KeyValuePair kvp) - { - var (gamePath, fullPath) = kvp; - if (_effectiveGamePathFilter.Length > 0 && !gamePath.ToString().Contains(_effectiveGamePathFilter, StringComparison.OrdinalIgnoreCase)) - return false; - - return _effectiveFilePathFilter.Length == 0 || fullPath.Path.FullName.Contains(_effectiveFilePathFilter, StringComparison.OrdinalIgnoreCase); - } - - /// Check filters for meta manipulations. - private bool CheckFilters((string, string) kvp) - { - var (name, path) = kvp; - if (_effectiveGamePathFilter.Length > 0 && !name.Contains(_effectiveGamePathFilter, StringComparison.OrdinalIgnoreCase)) - return false; - - return _effectiveFilePathFilter.Length == 0 || path.Contains(_effectiveFilePathFilter, StringComparison.OrdinalIgnoreCase); - } -} diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 53ff8121..11b3e577 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -1,10 +1,7 @@ -using Dalamud.Bindings.ImGui; using Dalamud.Game.ClientState.Objects; using OtterGui; -using OtterGui.Raii; using Penumbra.UI.Classes; using Dalamud.Interface; -using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using ImSharp; using Luna; @@ -27,7 +24,6 @@ public sealed class ModsTab( TutorialService tutorial, RedrawService redrawService, Configuration config, - IClientState clientState, CollectionSelectHeader collectionHeader, ITargetManager targets, ObjectManager objects) @@ -58,14 +54,14 @@ public sealed class ModsTab( { selector.Draw(); Im.Line.Same(); - ImGui.SetCursorPosX(MathF.Round(ImGui.GetCursorPosX())); + Im.Cursor.X = MathF.Round(Im.Cursor.X); using var group = Im.Group(); collectionHeader.Draw(false); using var style = ImStyleDouble.ItemSpacing.Push(Vector2.Zero); - using (var child = ImRaii.Child("##ModsTabMod", + using (var child = Im.Child.Begin("##ModsTabMod"u8, Im.ContentRegion.Available with { Y = config.HideRedrawBar ? 0 : -Im.Style.FrameHeight }, - true, ImGuiWindowFlags.HorizontalScrollbar)) + true, WindowFlags.HorizontalScrollbar)) { style.Pop(); if (child) @@ -99,58 +95,67 @@ public sealed class ModsTab( } var frameHeight = new Vector2(0, Im.Style.FrameHeight); - var frameColor = ImGuiColor.FrameBackground.Get().Color; + var frameColor = Im.Style[ImGuiColor.FrameBackground]; using (Im.Group()) { - using (ImRaii.PushFont(UiBuilder.IconFont)) + using (AwesomeIcon.Font.Push()) { - ImGuiUtil.DrawTextButton(FontAwesomeIcon.InfoCircle.ToIconString(), frameHeight, frameColor); - Im.Line.Same(); + ImEx.TextFramed(LunaStyle.HelpMarker.Span, frameHeight, frameColor); } - - ImGuiUtil.DrawTextButton("Redraw: ", frameHeight, frameColor); + Im.Line.Same(); + ImEx.TextFramed("Redraw: "u8, frameHeight, frameColor); } var hovered = Im.Item.Hovered(); tutorial.OpenTutorial(BasicTutorialSteps.Redrawing); if (hovered) - ImGui.SetTooltip($"The supported modifiers for '/penumbra redraw' are:\n{TutorialService.SupportedRedrawModifiers}"); + { + using var _ = Im.Tooltip.Begin(); + Im.Text("The supported modifiers for '/penumbra redraw' are:"u8); + Im.BulletText("nothing, to redraw all characters\n"u8); + Im.BulletText("'self' or '': your own character\n"u8); + Im.BulletText("'target' or '': your target\n"u8); + Im.BulletText("'focus' or ': your focus target\n"u8); + Im.BulletText("'mouseover' or '': the actor you are currently hovering over\n"u8); + Im.BulletText("'furniture': most indoor furniture, does not currently work outdoors\n"u8); + Im.BulletText("any specific actor name to redraw all actors of that exactly matching name."u8); + } - using var id = ImRaii.PushId("Redraw"); - using var disabled = ImRaii.Disabled(clientState.LocalPlayer == null); + using var id = Im.Id.Push("Redraw"u8); + using var disabled = Im.Disabled(!objects[0].Valid); Im.Line.Same(); var buttonWidth = frameHeight with { X = Im.ContentRegion.Available.X / 5 }; var tt = !objects[0].Valid - ? "\nCan only be used when you are logged in and your character is available." - : string.Empty; - DrawButton(buttonWidth, "All", string.Empty, tt); + ? "Can only be used when you are logged in and your character is available."u8 + : StringU8.Empty; + DrawButton(buttonWidth, "All"u8, string.Empty, tt); Im.Line.Same(); - DrawButton(buttonWidth, "Self", "self", tt); + DrawButton(buttonWidth, "Self"u8, "self", tt); Im.Line.Same(); - tt = targets.Target == null && targets.GPoseTarget == null - ? "\nCan only be used when you have a target." - : string.Empty; - DrawButton(buttonWidth, "Target", "target", tt); + tt = targets.Target is null && targets.GPoseTarget is null + ? "Can only be used when you have a target."u8 + : StringU8.Empty; + DrawButton(buttonWidth, "Target"u8, "target", tt); Im.Line.Same(); - tt = targets.FocusTarget == null - ? "\nCan only be used when you have a focus target." - : string.Empty; - DrawButton(buttonWidth, "Focus", "focus", tt); + tt = targets.FocusTarget is null + ? "Can only be used when you have a focus target."u8 + : StringU8.Empty; + DrawButton(buttonWidth, "Focus"u8, "focus", tt); Im.Line.Same(); tt = !IsIndoors() - ? "\nCan currently only be used for indoor furniture." - : string.Empty; - DrawButton(frameHeight with { X = Im.ContentRegion.Available.X - 1 }, "Furniture", "furniture", tt); + ? "Can currently only be used for indoor furniture."u8 + : StringU8.Empty; + DrawButton(frameHeight with { X = Im.ContentRegion.Available.X - 1 }, "Furniture"u8, "furniture", tt); return; - void DrawButton(Vector2 size, string label, string lower, string additionalTooltip) + void DrawButton(Vector2 size, ReadOnlySpan label, string lower, ReadOnlySpan additionalTooltip) { - using (_ = ImRaii.Disabled(additionalTooltip.Length > 0)) + using (Im.Disabled(additionalTooltip.Length > 0)) { - if (ImGui.Button(label, size)) + if (Im.Button(label, size)) { if (lower.Length > 0) redrawService.RedrawObject(lower, RedrawType.Redraw); @@ -159,9 +164,16 @@ public sealed class ModsTab( } } - Im.Tooltip.OnHover(lower.Length > 0 - ? $"Execute '/penumbra redraw {lower}'.{additionalTooltip}" - : $"Execute '/penumbra redraw'.{additionalTooltip}", HoveredFlags.AllowWhenDisabled); + if (!Im.Item.Hovered(HoveredFlags.AllowWhenDisabled)) + return; + + using var _ = Im.Tooltip.Begin(); + if (lower.Length > 0) + Im.Text($"Execute '/penumbra redraw {lower}'."); + else + Im.Text("Execute '/penumbra redraw'."u8); + if (additionalTooltip.Length > 0) + Im.Text(additionalTooltip); } } diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs index 909ff406..cbd6347a 100644 --- a/Penumbra/UI/Tabs/ResourceTab.cs +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -1,19 +1,14 @@ -using Dalamud.Bindings.ImGui; -using Dalamud.Game; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.STD; using ImSharp; using Luna; -using OtterGui; -using OtterGui.Raii; using Penumbra.Api.Enums; using Penumbra.Interop.Hooks.ResourceLoading; -using Penumbra.String.Classes; namespace Penumbra.UI.Tabs; -public sealed class ResourceTab(Configuration config, ResourceManagerService resourceManager, ISigScanner sigScanner) +public sealed class ResourceTab(Configuration config, ResourceManagerService resourceManager) : ITab { public TabType Identifier @@ -25,35 +20,35 @@ public sealed class ResourceTab(Configuration config, ResourceManagerService res public bool IsVisible => config.DebugMode; + public readonly TextFilter Filter = new(); + /// Draw a tab to iterate over the main resource maps and see what resources are currently loaded. - public void DrawContent() + public unsafe void DrawContent() { // Filter for resources containing the input string. - Im.Item.SetNextWidth(-1); - ImGui.InputTextWithHint("##resourceFilter", "Filter...", ref _resourceManagerFilter, Utf8GamePath.MaxGamePathLength); - - using var child = ImRaii.Child("##ResourceManagerTab", -Vector2.One); + Filter.DrawFilter("##ResourceFilter"u8, Im.ContentRegion.Available); + using var child = Im.Child.Begin("##ResourceManagerTab"u8, Im.ContentRegion.Available); if (!child) return; - unsafe - { - resourceManager.IterateGraphs(DrawCategoryContainer); - } + resourceManager.IterateGraphs(DrawCategoryContainer); Im.Line.New(); - unsafe - { - Im.Text( - $"Static Address: 0x{(ulong)resourceManager.ResourceManagerAddress:X} (+0x{(ulong)resourceManager.ResourceManagerAddress - (ulong)sigScanner.Module.BaseAddress:X})"); - Im.Text($"Actual Address: 0x{(ulong)resourceManager.ResourceManager:X}"); - } + using var table = Im.Table.Begin("##t"u8, 2, TableFlags.SizingFixedFit); + if (!table) + return; + + table.DrawColumn("Static Address:"u8); + table.NextColumn(); + Penumbra.Dynamis.DrawPointer(resourceManager.ResourceManagerAddress); + table.DrawColumn("Actual Address:"u8); + table.NextColumn(); + Penumbra.Dynamis.DrawPointer(resourceManager.ResourceManager); } private float _hashColumnWidth; private float _pathColumnWidth; private float _refsColumnWidth; - private string _resourceManagerFilter = string.Empty; /// Draw a single resource map. private unsafe void DrawResourceMap(ResourceCategory category, uint ext, @@ -63,7 +58,7 @@ public sealed class ResourceTab(Configuration config, ResourceManagerService res return; var label = GetNodeLabel((uint)category, ext, map->Count); - using var tree = ImRaii.TreeNode(label); + using var tree = Im.Tree.Node(label); if (!tree || map->Count == 0) return; @@ -75,37 +70,35 @@ public sealed class ResourceTab(Configuration config, ResourceManagerService res table.SetupColumn("Ptr"u8, TableColumnFlags.WidthFixed, _hashColumnWidth); table.SetupColumn("Path"u8, TableColumnFlags.WidthFixed, _pathColumnWidth); table.SetupColumn("Refs"u8, TableColumnFlags.WidthFixed, _refsColumnWidth); - ImGui.TableHeadersRow(); + Im.Table.HeaderRow(); resourceManager.IterateResourceMap(map, (hash, r) => { // Filter unwanted names. - if (_resourceManagerFilter.Length != 0 - && !r->FileName.ToString().Contains(_resourceManagerFilter, StringComparison.OrdinalIgnoreCase)) + if (Filter.Text.Length > 0 && Filter.WouldBeVisible(r->FileName.ToString(), -1)) return; - var address = $"0x{(ulong)r:X}"; - ImGuiUtil.DrawTableColumn($"0x{hash:X8}"); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable(address); - + Im.Table.DrawColumn($"0x{hash:X8}"); + Im.Table.NextColumn(); + Penumbra.Dynamis.DrawPointer(r); var resource = (Interop.Structs.ResourceHandle*)r; - ImGui.TableNextColumn(); - Im.Text(resource->FileName().Span); - if (ImGui.IsItemClicked()) + Im.Table.DrawColumn(resource->FileName().Span); + if (Im.Item.Clicked()) + Im.Clipboard.Set(resource->FileName().Span); + else if (Im.Item.RightClicked()) { var data = resource->CsHandle.GetData(); if (data != null) { var length = (int)resource->CsHandle.GetLength(); - ImGui.SetClipboardText(string.Join(" ", + Im.Clipboard.Set(StringU8.Join((byte) ' ', new ReadOnlySpan(data, length).ToArray().Select(b => b.ToString("X2")))); } } - ImGuiUtil.HoverTooltip("Click to copy byte-wise file data to clipboard, if any."); + Im.Tooltip.OnHover("Click to copy the file name to clipboard.\nRight-Click to copy byte-wise file data to clipboard, if any."u8); - ImGuiUtil.DrawTableColumn(r->RefCount.ToString()); + Im.Table.DrawColumn($"{r->RefCount}"); }); } @@ -113,22 +106,22 @@ public sealed class ResourceTab(Configuration config, ResourceManagerService res private unsafe void DrawCategoryContainer(ResourceCategory category, StdMap>>>* map, int idx) { - if (map == null) + if (map is null) return; - using var tree = ImRaii.TreeNode($"({(uint)category:D2}) {category} (Ex {idx}) - {map->Count}###{(uint)category}_{idx}"); - if (tree) - { - SetTableWidths(); - resourceManager.IterateExtMap(map, (ext, m) => DrawResourceMap(category, ext, m)); - } + using var tree = Im.Tree.Node($"({(uint)category:D2}) {category} (Ex {idx}) - {map->Count}###{(uint)category}_{idx}"); + if (!tree) + return; + + SetTableWidths(); + resourceManager.IterateExtMap(map, (ext, m) => DrawResourceMap(category, ext, m)); } /// Obtain a label for an extension node. - private static string GetNodeLabel(uint label, uint type, int count) + private static Utf8StringHandler GetNodeLabel(uint label, uint type, int count) { - var (lowest, mid1, mid2, highest) = Functions.SplitBytes(type); - return highest == 0 + var (lowest, mid1, mid2, highest) = BitFunctions.SplitBytes(type); + return highest is 0 ? $"({type:X8}) {(char)mid2}{(char)mid1}{(char)lowest} - {count}###{label}{type}" : $"({type:X8}) {(char)highest}{(char)mid2}{(char)mid1}{(char)lowest} - {count}###{label}{type}"; } diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index f1407bad..cc6f0add 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -1,4 +1,3 @@ -using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Plugin; @@ -7,7 +6,6 @@ using Dalamud.Utility; using ImSharp; using Luna; using OtterGui; -using OtterGui.Raii; using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api; @@ -20,6 +18,7 @@ using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.UI.ModsTab; +using Penumbra.UI.ModsTab.Selector; namespace Penumbra.UI.Tabs; @@ -58,9 +57,6 @@ public sealed class SettingsTab : ITab private readonly AttributeHook _attributeHook; private readonly PcpService _pcpService; - private int _minimumX = int.MaxValue; - private int _minimumY = int.MaxValue; - private readonly TagButtons _sharedTags = new(); private string _lastCloudSyncTestedPath = string.Empty; @@ -103,119 +99,43 @@ public sealed class SettingsTab : ITab _pcpService = pcpService; } - public void DrawHeader() + public void PostTabButton() { _tutorial.OpenTutorial(BasicTutorialSteps.Fin); _tutorial.OpenTutorial(BasicTutorialSteps.Faq1); _tutorial.OpenTutorial(BasicTutorialSteps.Faq2); } - public sealed class TestFlattened : IFlattenedTreeNode - { - public int ParentIndex { get; set; } - public int StartsLineTo { get; set; } - public int IndentationDepth { get; set; } - - public void Draw() - { - Im.Tree.Node("", - TreeNodeFlags.DefaultOpen - | TreeNodeFlags.NoTreePushOnOpen); //Im.Selectable($"{ParentIndex} {StartsLineTo} {IndentationDepth}")); - } - } - - private static readonly List List = - [ - new() - { - IndentationDepth = 0, - ParentIndex = -1, - StartsLineTo = 2, - }, - new() - { - IndentationDepth = 1, - ParentIndex = 0, - StartsLineTo = -1, - }, - new() - { - IndentationDepth = 1, - ParentIndex = 0, - StartsLineTo = -1, - }, - new() - { - IndentationDepth = 0, - ParentIndex = -1, - StartsLineTo = 8, - }, - new() - { - IndentationDepth = 1, - ParentIndex = 3, - StartsLineTo = 6, - }, - new() - { - IndentationDepth = 2, - ParentIndex = 4, - StartsLineTo = -1, - }, - new() - { - IndentationDepth = 2, - ParentIndex = 4, - StartsLineTo = 7, - }, - new() - { - IndentationDepth = 3, - ParentIndex = 6, - StartsLineTo = -1, - }, - new() - { - IndentationDepth = 1, - ParentIndex = 3, - StartsLineTo = -1, - }, - ]; - public void DrawContent() { - using var child = ImRaii.Child("##SettingsTab", -Vector2.One, false); + using var child = Im.Child.Begin("##SettingsTab"u8, -Vector2.One); if (!child) return; - using var c2 = ImRaii.Child("a", new Vector2(300, 5 * Im.Style.TextHeightWithSpacing), true, - ImGuiWindowFlags.AlwaysVerticalScrollbar); - TreeLine.Draw(List, 0xFFFFFFFF); + DrawEnabledBox(); + EphemeralCheckbox("Lock Main Window", "Prevent the main window from being resized or moved.", _config.Ephemeral.FixMainWindow, + v => _config.Ephemeral.FixMainWindow = v); - //DrawEnabledBox(); - //EphemeralCheckbox("Lock Main Window", "Prevent the main window from being resized or moved.", _config.Ephemeral.FixMainWindow, - // v => _config.Ephemeral.FixMainWindow = v); - // - //Im.Line.New(); - //DrawRootFolder(); - //DrawDirectoryButtons(); - //Im.Line.New(); - //Im.Line.New(); - // - //DrawGeneralSettings(); - //_migrationDrawer.Draw(); - //DrawColorSettings(); - //DrawPredefinedTagsSection(); - //DrawAdvancedSettings(); - //DrawSupportButtons(); + Im.Line.New(); + DrawRootFolder(); + DrawDirectoryButtons(); + Im.Line.New(); + Im.Line.New(); + + DrawGeneralSettings(); + _migrationDrawer.Draw(); + DrawColorSettings(); + DrawPredefinedTagsSection(); + DrawAdvancedSettings(); + DrawSupportButtons(); } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void Checkbox(string label, string tooltip, bool current, Action setter) { - using var id = ImRaii.PushId(label); + using var id = Im.Id.Push(label); var tmp = current; - if (ImGui.Checkbox(string.Empty, ref tmp) && tmp != current) + if (Im.Checkbox(StringU8.Empty, ref tmp) && tmp != current) { setter(tmp); _config.Save(); @@ -228,9 +148,9 @@ public sealed class SettingsTab : ITab [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void EphemeralCheckbox(string label, string tooltip, bool current, Action setter) { - using var id = ImRaii.PushId(label); + using var id = Im.Id.Push(label); var tmp = current; - if (ImGui.Checkbox(string.Empty, ref tmp) && tmp != current) + if (Im.Checkbox(StringU8.Empty, ref tmp) && tmp != current) { setter(tmp); _config.Ephemeral.Save(); @@ -252,21 +172,12 @@ public sealed class SettingsTab : ITab var w = new Vector2(width, 0); var (text, valid) = CheckRootDirectoryPath(newName, old, selected); - return (ImGui.Button(text, w) || saved) && valid; + return (Im.Button(text, w) || saved) && valid; } /// Check a potential new root directory for validity and return the button text and whether it is valid. private (string Text, bool Valid) CheckRootDirectoryPath(string newName, string old, bool selected) { - static bool IsSubPathOf(string basePath, string subPath) - { - if (basePath.Length == 0) - return false; - - var rel = Path.GetRelativePath(basePath, subPath); - return rel == "." || !rel.StartsWith('.') && !Path.IsPathRooted(rel); - } - if (newName.Length > RootDirectoryMaxLength) return ($"Path is too long. The maximum length is {RootDirectoryMaxLength}.", false); @@ -305,6 +216,15 @@ public sealed class SettingsTab : ITab return selected ? ($"Press Enter or Click Here to Save (Current Directory: {old})", true) : ($"Click Here to Save (Current Directory: {old})", true); + + static bool IsSubPathOf(string basePath, string subPath) + { + if (basePath.Length == 0) + return false; + + var rel = Path.GetRelativePath(basePath, subPath); + return rel == "." || !rel.StartsWith('.') && !Path.IsPathRooted(rel); + } } /// Changing the base mod directory. @@ -316,8 +236,7 @@ public sealed class SettingsTab : ITab /// private void DrawDirectoryPickerButton() { - if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Folder.ToIconString(), UiHelpers.IconButtonSize, - "Select a directory via dialog.", false, true)) + if (!ImEx.Icon.Button(LunaStyle.FolderIcon, "Select a directory via dialog."u8)) return; _newModDirectory ??= _config.ModDirectory; @@ -348,11 +267,11 @@ public sealed class SettingsTab : ITab using (var color = ImStyleBorder.Frame.Push(Colors.RegexWarningBorder, Im.Style.GlobalScale, !_modManager.Valid)) { color.Push(ImGuiColor.TextDisabled, Colors.RegexWarningBorder, !_modManager.Valid); - save = ImGui.InputTextWithHint("##rootDirectory", "Enter Root Directory here (MANDATORY)...", ref _newModDirectory, - RootDirectoryMaxLength, ImGuiInputTextFlags.EnterReturnsTrue); + save = Im.Input.Text("##rootDirectory"u8, ref _newModDirectory, "Enter Root Directory here (MANDATORY)..."u8, + InputTextFlags.EnterReturnsTrue, RootDirectoryMaxLength); } - selected = ImGui.IsItemActive(); + selected = Im.Item.Active; using var style = ImStyleDouble.ItemSpacing.Push(new Vector2(Im.Style.GlobalScale * 3, 0)); Im.Line.Same(); DrawDirectoryPickerButton(); @@ -374,11 +293,11 @@ public sealed class SettingsTab : ITab _tutorial.OpenTutorial(BasicTutorialSteps.ModDirectory); Im.Line.Same(); - var pos = ImGui.GetCursorPosX(); + var pos = Im.Cursor.Y; Im.Line.New(); if (_config.ModDirectory != _newModDirectory - && _newModDirectory.Length != 0 + && _newModDirectory.Length is not 0 && DrawPressEnterWarning(_newModDirectory, _config.ModDirectory, pos, save, selected)) _modManager.DiscoverMods(_newModDirectory, out _newModDirectory); } @@ -399,7 +318,7 @@ public sealed class SettingsTab : ITab private void DrawEnabledBox() { var enabled = _config.EnableMods; - if (ImGui.Checkbox("Enable Mods", ref enabled)) + if (Im.Checkbox("Enable Mods"u8, ref enabled)) _penumbra.SetEnabled(enabled); _tutorial.OpenTutorial(BasicTutorialSteps.EnableMods); @@ -412,7 +331,7 @@ public sealed class SettingsTab : ITab /// Draw all settings pertaining to the Mod Selector. private void DrawGeneralSettings() { - if (!ImGui.CollapsingHeader("General")) + if (!Im.Tree.Header("General"u8)) { _tutorial.OpenTutorial(BasicTutorialSteps.GeneralSettings); return; @@ -439,27 +358,15 @@ public sealed class SettingsTab : ITab Im.Line.New(); } - private int _singleGroupRadioMax = int.MaxValue; - /// Draw a selection for the maximum number of single select options displayed as a radio toggle. private void DrawSingleSelectRadioMax() { - if (_singleGroupRadioMax == int.MaxValue) - _singleGroupRadioMax = _config.SingleGroupRadioMax; - Im.Item.SetNextWidth(UiHelpers.InputTextWidth.X); - if (ImGui.DragInt("##SingleSelectRadioMax", ref _singleGroupRadioMax, 0.01f, 1)) - _singleGroupRadioMax = Math.Max(1, _singleGroupRadioMax); - - if (ImGui.IsItemDeactivated()) + if (ImEx.InputOnDeactivation.Drag("##SingleSelectRadioMax"u8, _config.SingleGroupRadioMax, out var newValue, 1, null, 0.01f, + SliderFlags.AlwaysClamp)) { - if (_singleGroupRadioMax != _config.SingleGroupRadioMax) - { - _config.SingleGroupRadioMax = _singleGroupRadioMax; - _config.Save(); - } - - _singleGroupRadioMax = int.MaxValue; + _config.SingleGroupRadioMax = newValue; + _config.Save(); } ImGuiUtil.LabeledHelpMarker("Upper Limit for Single-Selection Group Radio Buttons", @@ -467,27 +374,15 @@ public sealed class SettingsTab : ITab + "All other Single-Selection Groups will be displayed as a set of Radio-Buttons."); } - private int _collapsibleGroupMin = int.MaxValue; - /// Draw a selection for the minimum number of options after which a group is drawn as collapsible. private void DrawCollapsibleGroupMin() { - if (_collapsibleGroupMin == int.MaxValue) - _collapsibleGroupMin = _config.OptionGroupCollapsibleMin; - Im.Item.SetNextWidth(UiHelpers.InputTextWidth.X); - if (ImGui.DragInt("##CollapsibleGroupMin", ref _collapsibleGroupMin, 0.01f, 1)) - _collapsibleGroupMin = Math.Max(2, _collapsibleGroupMin); - - if (ImGui.IsItemDeactivated()) + if (ImEx.InputOnDeactivation.Drag("##CollapsibleGroupMin"u8, _config.OptionGroupCollapsibleMin, out var newValue, 2, null, 0.01f, + SliderFlags.AlwaysClamp)) { - if (_collapsibleGroupMin != _config.OptionGroupCollapsibleMin) - { - _config.OptionGroupCollapsibleMin = _collapsibleGroupMin; - _config.Save(); - } - - _collapsibleGroupMin = int.MaxValue; + _config.OptionGroupCollapsibleMin = newValue; + _config.Save(); } ImGuiUtil.LabeledHelpMarker("Collapsible Option Group Limit", @@ -575,24 +470,24 @@ public sealed class SettingsTab : ITab Checkbox("Use Interface Collection for other Plugin UIs", "Use the collection assigned to your interface for other plugins requesting UI-textures and icons through Dalamud.", _dalamudSubstitutionProvider.Enabled, _dalamudSubstitutionProvider.Set); - Checkbox($"Use {TutorialService.AssignedCollections} in Lobby", + Checkbox($"Use {"Assigned Collections"} in Lobby", "If this is disabled, no mods are applied to characters in the lobby or at the aesthetician.", _config.ShowModsInLobby, v => _config.ShowModsInLobby = v); - Checkbox($"Use {TutorialService.AssignedCollections} in Character Window", + Checkbox($"Use {"Assigned Collections"} in Character Window", "Use the individual collection for your characters name or the Your Character collection in your main character window, if it is set.", _config.UseCharacterCollectionInMainWindow, v => _config.UseCharacterCollectionInMainWindow = v); - Checkbox($"Use {TutorialService.AssignedCollections} in Adventurer Cards", + Checkbox($"Use {"Assigned Collections"} in Adventurer Cards", "Use the appropriate individual collection for the adventurer card you are currently looking at, based on the adventurer's name.", _config.UseCharacterCollectionsInCards, v => _config.UseCharacterCollectionsInCards = v); - Checkbox($"Use {TutorialService.AssignedCollections} in Try-On Window", + Checkbox($"Use {"Assigned Collections"} in Try-On Window", "Use the individual collection for your character's name in your try-on, dye preview or glamour plate window, if it is set.", _config.UseCharacterCollectionInTryOn, v => _config.UseCharacterCollectionInTryOn = v); Checkbox("Use No Mods in Inspect Windows", "Use the empty collection for characters you are inspecting, regardless of the character.\n" + "Takes precedence before the next option.", _config.UseNoModsInInspect, v => _config.UseNoModsInInspect = v); - Checkbox($"Use {TutorialService.AssignedCollections} in Inspect Windows", + Checkbox($"Use {"Assigned Collections"} in Inspect Windows", "Use the appropriate individual collection for the character you are currently inspecting, based on their name.", _config.UseCharacterCollectionInInspect, v => _config.UseCharacterCollectionInInspect = v); - Checkbox($"Use {TutorialService.AssignedCollections} based on Ownership", + Checkbox($"Use {"Assigned Collections"} based on Ownership", "Use the owner's name to determine the appropriate individual collection for mounts, companions, accessories and combat pets.", _config.UseOwnerNameForCharacterCollection, v => _config.UseOwnerNameForCharacterCollection = v); } @@ -605,9 +500,9 @@ public sealed class SettingsTab : ITab using (var combo = ImUtf8.Combo("##sortMode", sortMode.Name)) { if (combo) - foreach (var val in Configuration.Constants.ValidSortModes) + foreach (var val in ISortMode.Valid.Values) { - if (ImUtf8.Selectable(val.Name, val.GetType() == sortMode.GetType()) && val.GetType() != sortMode.GetType()) + if (Im.Selectable(val.Name, val.Equals(sortMode)) && !val.Equals(sortMode)) { _config.SortMode = val; _selector.SetFilterDirty(); @@ -624,13 +519,13 @@ public sealed class SettingsTab : ITab private void DrawRenameSettings() { Im.Item.SetNextWidth(UiHelpers.InputTextWidth.X); - using (var combo = ImRaii.Combo("##renameSettings", _config.ShowRename.GetData().Name)) + using (var combo = Im.Combo.Begin("##renameSettings"u8, _config.ShowRename.GetData().Name)) { if (combo) foreach (var value in Enum.GetValues()) { var (name, desc) = value.GetData(); - if (ImGui.Selectable(name, _config.ShowRename == value)) + if (Im.Selectable(name, _config.ShowRename == value)) { _config.ShowRename = value; _selector.SetRenameSearchPath(value); @@ -741,16 +636,15 @@ public sealed class SettingsTab : ITab /// Draw input for the default import path for a mod. private void DrawDefaultModImportPath() { - var tmp = _config.DefaultModImportPath; var spacing = new Vector2(Im.Style.GlobalScale * 3); using var style = ImStyleDouble.ItemSpacing.Push(spacing); Im.Item.SetNextWidth(UiHelpers.InputTextMinusButton3); - if (ImGui.InputText("##defaultModImport", ref tmp, 256)) - _config.DefaultModImportPath = tmp; - - if (ImGui.IsItemDeactivatedAfterEdit()) + if (ImEx.InputOnDeactivation.Text("##defaultModImport"u8, _config.DefaultModImportPath, out string newDirectory)) + { + _config.DefaultModImportPath = newDirectory; _config.Save(); + } Im.Line.Same(); if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Folder.ToIconString()}##import", UiHelpers.IconButtonSize, @@ -777,20 +671,14 @@ public sealed class SettingsTab : ITab "Set the directory that gets opened when using the file picker to import mods for the first time."); } - private string _tempExportDirectory = string.Empty; - /// Draw input for the default export/backup path for mods. private void DrawDefaultModExportPath() { - var tmp = _config.ExportDirectory; var spacing = new Vector2(Im.Style.GlobalScale * 3); using var style = ImStyleDouble.ItemSpacing.Push(spacing); Im.Item.SetNextWidth(UiHelpers.InputTextMinusButton3); - if (ImGui.InputText("##defaultModExport", ref tmp, 256)) - _tempExportDirectory = tmp; - - if (ImGui.IsItemDeactivatedAfterEdit()) - _modExportManager.UpdateExportDirectory(_tempExportDirectory); + if (ImEx.InputOnDeactivation.Text("##defaultModExport"u8, _config.ExportDirectory, out string newDirectory)) + _modExportManager.UpdateExportDirectory(newDirectory); Im.Line.Same(); if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Folder.ToIconString()}##export", UiHelpers.IconButtonSize, @@ -814,28 +702,18 @@ public sealed class SettingsTab : ITab + "Keep this empty to use the root directory."); } - private string? _tempWatchDirectory; - /// Draw input for the Automatic Mod import path. private void DrawFileWatcherPath() { - var tmp = _tempWatchDirectory ?? _config.WatchDirectory; + using var id = Im.Id.Push("fw"u8); var spacing = new Vector2(Im.Style.GlobalScale * 3); using var style = ImStyleDouble.ItemSpacing.Push(spacing); Im.Item.SetNextWidth(UiHelpers.InputTextMinusButton3); - if (ImGui.InputText("##fileWatchPath", ref tmp, 256)) - _tempWatchDirectory = tmp; - - if (ImGui.IsItemDeactivated() && _tempWatchDirectory is not null) - { - if (ImGui.IsItemDeactivatedAfterEdit()) - _fileWatcher.UpdateDirectory(_tempWatchDirectory); - _tempWatchDirectory = null; - } + if (ImEx.InputOnDeactivation.Text("##path"u8, _config.WatchDirectory, out string newDirectory, maxLength: 256)) + _fileWatcher.UpdateDirectory(newDirectory); Im.Line.Same(); - if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Folder.ToIconString()}##fileWatch", UiHelpers.IconButtonSize, - "Select a directory via dialog.", false, true)) + if (ImEx.Icon.Button(LunaStyle.FolderIcon, "Select a directory via dialog."u8)) { var startDir = _config.WatchDirectory.Length > 0 && Directory.Exists(_config.WatchDirectory) ? _config.WatchDirectory @@ -857,13 +735,12 @@ public sealed class SettingsTab : ITab /// Draw input for the default name to input as author into newly generated mods. private void DrawDefaultModAuthor() { - var tmp = _config.DefaultModAuthor; Im.Item.SetNextWidth(UiHelpers.InputTextWidth.X); - if (ImGui.InputText("##defaultAuthor", ref tmp, 64)) - _config.DefaultModAuthor = tmp; - - if (ImGui.IsItemDeactivatedAfterEdit()) + if (ImEx.InputOnDeactivation.Text("##author"u8, _config.DefaultModAuthor, out string newAuthor)) + { + _config.DefaultModAuthor = newAuthor; _config.Save(); + } ImGuiUtil.LabeledHelpMarker("Default Mod Author", "Set the default author stored for newly created mods."); } @@ -871,13 +748,12 @@ public sealed class SettingsTab : ITab /// Draw input for the default folder to sort put newly imported mods into. private void DrawDefaultModImportFolder() { - var tmp = _config.DefaultImportFolder; Im.Item.SetNextWidth(UiHelpers.InputTextWidth.X); - if (ImGui.InputText("##defaultImportFolder", ref tmp, 64)) - _config.DefaultImportFolder = tmp; - - if (ImGui.IsItemDeactivatedAfterEdit()) + if (ImEx.InputOnDeactivation.Text("##importFolder"u8, _config.DefaultImportFolder, out string newFolder)) + { + _config.DefaultImportFolder = newFolder; _config.Save(); + } ImGuiUtil.LabeledHelpMarker("Default Mod Import Organizational Folder", "Set the default Penumbra mod folder to place newly imported mods into.\nLeave blank to import into Root."); @@ -886,13 +762,12 @@ public sealed class SettingsTab : ITab /// Draw input for the default folder to sort put newly imported mods into. private void DrawPcpFolder() { - var tmp = _config.PcpSettings.FolderName; Im.Item.SetNextWidth(UiHelpers.InputTextWidth.X); - if (ImUtf8.InputText("##pcpFolder"u8, ref tmp)) - _config.PcpSettings.FolderName = tmp; - - if (ImGui.IsItemDeactivatedAfterEdit()) + if (ImEx.InputOnDeactivation.Text("##pcpFolder"u8, _config.PcpSettings.FolderName, out string newFolder)) + { + _config.PcpSettings.FolderName = newFolder; _config.Save(); + } ImGuiUtil.LabeledHelpMarker("Default PCP Organizational Folder", "The folder any penumbra character packs are moved to on import.\nLeave blank to import into Root."); @@ -900,16 +775,15 @@ public sealed class SettingsTab : ITab private void DrawPcpExtension() { - var tmp = _config.PcpSettings.PcpExtension; Im.Item.SetNextWidth(UiHelpers.InputTextWidth.X); - if (ImUtf8.InputText("##pcpExtension"u8, ref tmp)) - _config.PcpSettings.PcpExtension = tmp; - - if (ImGui.IsItemDeactivatedAfterEdit()) + if (ImEx.InputOnDeactivation.Text("##pcpExtension"u8, _config.PcpSettings.PcpExtension, out string newExtension)) + { + _config.PcpSettings.PcpExtension = newExtension; _config.Save(); + } Im.Line.SameInner(); - if (ImEx.Button("Reset##pcpExtension"u8, Vector2.Zero, "Reset the extension to its default value of \".pcp\".", + if (ImEx.Button("Reset##pcpExtension"u8, Vector2.Zero, "Reset the extension to its default value of \".pcp\"."u8, _config.PcpSettings.PcpExtension is ".pcp")) { _config.PcpSettings.PcpExtension = ".pcp"; @@ -934,7 +808,7 @@ public sealed class SettingsTab : ITab /// Draw the entire Color subsection. private void DrawColorSettings() { - if (!ImGui.CollapsingHeader("Colors")) + if (!Im.Tree.Header("Colors"u8)) return; foreach (var color in Enum.GetValues()) @@ -953,7 +827,7 @@ public sealed class SettingsTab : ITab /// Draw all advanced settings. private void DrawAdvancedSettings() { - var header = ImGui.CollapsingHeader("Advanced"); + var header = Im.Tree.Header("Advanced"u8); if (!header) return; @@ -1029,7 +903,7 @@ public sealed class SettingsTab : ITab if (_compactor.MassCompactRunning) { - ImGui.ProgressBar((float)_compactor.CurrentIndex / _compactor.TotalFiles, + Im.ProgressBar((float)_compactor.CurrentIndex / _compactor.TotalFiles, new Vector2(Im.ContentRegion.Available.X - Im.Style.ItemSpacing.X - UiHelpers.IconButtonSize.X, Im.Style.FrameHeight), _compactor.CurrentFile?.FullName[(_modManager.BasePath.FullName.Length + 1)..] ?? "Gathering Files..."); @@ -1040,60 +914,51 @@ public sealed class SettingsTab : ITab } else { - ImGui.Dummy(UiHelpers.IconButtonSize); + Im.FrameDummy(); } } /// Draw two integral inputs for minimum dimensions of this window. private void DrawMinimumDimensionConfig() { - var x = _minimumX == int.MaxValue ? (int)_config.MinimumSize.X : _minimumX; - var y = _minimumY == int.MaxValue ? (int)_config.MinimumSize.Y : _minimumY; - - var warning = x < Configuration.Constants.MinimumSizeX - ? y < Configuration.Constants.MinimumSizeY - ? "Size is smaller than default: This may look undesirable." - : "Width is smaller than default: This may look undesirable." - : y < Configuration.Constants.MinimumSizeY - ? "Height is smaller than default: This may look undesirable." - : string.Empty; + var warning = _config.MinimumSize.X < Configuration.Constants.MinimumSizeX + ? _config.MinimumSize.Y < Configuration.Constants.MinimumSizeY + ? "Size is smaller than default: This may look undesirable."u8 + : "Width is smaller than default: This may look undesirable."u8 + : _config.MinimumSize.Y < Configuration.Constants.MinimumSizeY + ? "Height is smaller than default: This may look undesirable."u8 + : StringU8.Empty; var buttonWidth = UiHelpers.InputTextWidth.X / 2.5f; Im.Item.SetNextWidth(buttonWidth); - if (ImGui.DragInt("##xMinSize", ref x, 0.1f, 500, 1500)) - _minimumX = x; - var edited = ImGui.IsItemDeactivatedAfterEdit(); - + if (ImEx.InputOnDeactivation.Drag("##xMinSize"u8, (int)_config.MinimumSize.X, out var newX, 500, 1500, 0.1f)) + { + _config.MinimumSize.X = newX; + _config.Save(); + } Im.Line.Same(); Im.Item.SetNextWidth(buttonWidth); - if (ImGui.DragInt("##yMinSize", ref y, 0.1f, 300, 1500)) - _minimumY = y; - edited |= ImGui.IsItemDeactivatedAfterEdit(); + if (ImEx.InputOnDeactivation.Drag("##yMinSize"u8, (int)_config.MinimumSize.Y, out var newY, 300, 1500, 0.1f)) + { + _config.MinimumSize.Y = newY; + _config.Save(); + } Im.Line.Same(); if (ImGuiUtil.DrawDisabledButton("Reset##resetMinSize", new Vector2(buttonWidth / 2 - Im.Style.ItemSpacing.X * 2, 0), $"Reset minimum dimensions to ({Configuration.Constants.MinimumSizeX}, {Configuration.Constants.MinimumSizeY}).", - x == Configuration.Constants.MinimumSizeX && y == Configuration.Constants.MinimumSizeY)) + _config.MinimumSize is { X: Configuration.Constants.MinimumSizeX, Y: Configuration.Constants.MinimumSizeY })) { - x = Configuration.Constants.MinimumSizeX; - y = Configuration.Constants.MinimumSizeY; - edited = true; + _config.MinimumSize = new Vector2(Configuration.Constants.MinimumSizeX, Configuration.Constants.MinimumSizeY); + _config.Save(); } ImGuiUtil.LabeledHelpMarker("Minimum Window Dimensions", "Set the minimum dimensions for resizing this window. Reducing these dimensions may cause the window to look bad or more confusing and is not recommended."); if (warning.Length > 0) - ImGuiUtil.DrawTextButton(warning, UiHelpers.InputTextWidth, Colors.PressEnterWarningBg); + ImEx.TextFramed(warning, UiHelpers.InputTextWidth, Colors.PressEnterWarningBg); else Im.Line.New(); - - if (!edited) - return; - - _config.MinimumSize = new Vector2(x, y); - _minimumX = int.MaxValue; - _minimumY = int.MaxValue; - _config.Save(); } private void DrawHdrRenderTargets() @@ -1127,7 +992,7 @@ public sealed class SettingsTab : ITab private void DrawEnableHttpApiBox() { var http = _config.EnableHttpApi; - if (ImGui.Checkbox("##http", ref http)) + if (Im.Checkbox("##http"u8, ref http)) { if (http) _httpApi.CreateWebServer(); @@ -1147,7 +1012,7 @@ public sealed class SettingsTab : ITab private void DrawEnableDebugModeBox() { var tmp = _config.DebugMode; - if (ImGui.Checkbox("##debugMode", ref tmp) && tmp != _config.DebugMode) + if (Im.Checkbox("##debugMode"u8, ref tmp) && tmp != _config.DebugMode) { _config.DebugMode = tmp; _config.Save(); @@ -1217,7 +1082,7 @@ public sealed class SettingsTab : ITab { if (!_dalamudConfig.GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool value)) { - using var disabled = ImRaii.Disabled(); + using var disabled = Im.Disabled(); Checkbox("Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)", string.Empty, false, _ => { }); } else @@ -1237,39 +1102,39 @@ public sealed class SettingsTab : ITab /// Draw the support button group on the right-hand side of the window. private void DrawSupportButtons() { - var width = ImGui.CalcTextSize(UiHelpers.SupportInfoButtonText).X + Im.Style.FramePadding.X * 2; - var xPos = ImGui.GetWindowWidth() - width; + var width = Im.Font.CalculateSize(UiHelpers.SupportInfoButtonText).X + Im.Style.FramePadding.X * 2; + var xPos = Im.Window.Width - width; // Respect the scroll bar width. - if (ImGui.GetScrollMaxY() > 0) + if (Im.Scroll.MaximumY> 0) xPos -= Im.Style.ScrollbarSize + Im.Style.FramePadding.X; - ImGui.SetCursorPos(new Vector2(xPos, Im.Style.FrameHeightWithSpacing)); + Im.Cursor.Position = new Vector2(xPos, Im.Style.FrameHeightWithSpacing); UiHelpers.DrawSupportButton(_penumbra); - ImGui.SetCursorPos(new Vector2(xPos, 0)); + Im.Cursor.Position = new Vector2(xPos, 0); SupportButton.Discord(Penumbra.Messager, width); - ImGui.SetCursorPos(new Vector2(xPos, 2 * Im.Style.FrameHeightWithSpacing)); + Im.Cursor.Position = new Vector2(xPos, 2 * Im.Style.FrameHeightWithSpacing); SupportButton.ReniGuide(Penumbra.Messager, width); - ImGui.SetCursorPos(new Vector2(xPos, 3 * Im.Style.FrameHeightWithSpacing)); - if (ImGui.Button("Restart Tutorial", new Vector2(width, 0))) + Im.Cursor.Position = new Vector2(xPos, 3 * Im.Style.FrameHeightWithSpacing); + if (Im.Button("Restart Tutorial"u8, new Vector2(width, 0))) { _config.Ephemeral.TutorialStep = 0; _config.Ephemeral.Save(); } - ImGui.SetCursorPos(new Vector2(xPos, 4 * Im.Style.FrameHeightWithSpacing)); - if (ImGui.Button("Show Changelogs", new Vector2(width, 0))) + Im.Cursor.Position = new Vector2(xPos, 4 * Im.Style.FrameHeightWithSpacing); + if (Im.Button("Show Changelogs"u8, new Vector2(width, 0))) _penumbra.ForceChangelogOpen(); - ImGui.SetCursorPos(new Vector2(xPos, 5 * Im.Style.FrameHeightWithSpacing)); + Im.Cursor.Position = new Vector2(xPos, 5 * Im.Style.FrameHeightWithSpacing); SupportButton.KoFiPatreon(Penumbra.Messager, new Vector2(width, 0)); } private void DrawPredefinedTagsSection() { - if (!ImGui.CollapsingHeader("Tags")) + if (!Im.Tree.Header("Tags"u8)) return; var tagIdx = _sharedTags.Draw("Predefined Tags: ", diff --git a/Penumbra/UI/TutorialService.cs b/Penumbra/UI/TutorialService.cs deleted file mode 100644 index 7471611a..00000000 --- a/Penumbra/UI/TutorialService.cs +++ /dev/null @@ -1,171 +0,0 @@ -using OtterGui.Widgets; -using Penumbra.Collections; -using Penumbra.Collections.Manager; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -/// List of currently available tutorials. -public enum BasicTutorialSteps -{ - GeneralTooltips, - ModDirectory, - EnableMods, - Deprecated1, - GeneralSettings, - Collections, - EditingCollections, - CurrentCollection, - SimpleAssignments, - IndividualAssignments, - GroupAssignments, - CollectionDetails, - Incognito, - Deprecated2, - Mods, - ModImport, - AdvancedHelp, - ModFilters, - CollectionSelectors, - Redrawing, - EnablingMods, - Priority, - ModOptions, - Fin, - Deprecated3, - Faq1, - Faq2, - Favorites, - Tags, -} - -/// Service for the in-game tutorial. -public class TutorialService : Luna.IUiService -{ - public const string SelectedCollection = "Selected Collection"; - public const string DefaultCollection = "Base Collection"; - public const string InterfaceCollection = "Interface Collection"; - public const string AssignedCollections = "Assigned Collections"; - - public const string SupportedRedrawModifiers = " - nothing, to redraw all characters\n" - + " - 'self' or '': your own character\n" - + " - 'target' or '': your target\n" - + " - 'focus' or ': your focus target\n" - + " - 'mouseover' or '': the actor you are currently hovering over\n" - + " - 'furniture': most indoor furniture, does not currently work outdoors\n" - + " - any specific actor name to redraw all actors of that exactly matching name."; - - private readonly EphemeralConfig _config; - private readonly Tutorial _tutorial; - - public TutorialService(EphemeralConfig config) - { - _config = config; - _tutorial = new Tutorial() - { - BorderColor = Colors.TutorialBorder, - HighlightColor = Colors.TutorialMarker, - PopupLabel = "Settings Tutorial", - } - .Register("General Tooltips", "This symbol gives you further information about whatever setting it appears next to.\n\n" - + "Hover over them when you are unsure what something does or how to do something.") - .Register("Initial Setup, Step 1: Mod Directory", - "The first step is to set up your mod directory, which is where your mods are extracted to.\n\n" - + "The mod directory should be a short path - like 'C:\\FFXIVMods' - on your fastest available drive. Faster drives improve performance.\n\n" - + "The folder should be an empty folder no other applications write to.") - .Register("Initial Setup, Step 2: Enable Mods", "Do not forget to enable your mods in case they are not.") - .Deprecated() - .Register("General Settings", "Look through all of these settings before starting, they might help you a lot!\n\n" - + "If you do not know what some of these do yet, return to this later!") - .Register("Initial Setup, Step 3: Collections", "Collections are lists of settings for your installed mods.\n\n" - + "This is our next stop!\n\n" - + "Go here after setting up your root folder to continue the tutorial!") - .Register("Initial Setup, Step 4: Managing Collections", - "On the left, we have the collection selector. Here, we can create new collections - either empty ones or by duplicating existing ones - and delete any collections not needed anymore.\n" - + $"There will always be one collection called {ModCollectionIdentity.DefaultCollectionName} that can not be deleted.") - .Register($"Initial Setup, Step 5: {SelectedCollection}", - $"The {SelectedCollection} is the one we highlighted in the selector. It is the collection we are currently looking at and editing.\nAny changes we make in our mod settings later in the next tab will edit this collection.\n" - + $"We should already have the collection named {ModCollectionIdentity.DefaultCollectionName} selected, and for our simple setup, we do not need to do anything here.\n\n") - .Register("Initial Setup, Step 6: Simple Assignments", - "Aside from being a collection of settings, we can also assign collections to different functions. This is used to make different mods apply to different characters.\n" - + "The Simple Assignments panel shows you the possible assignments that are enough for most people along with descriptions.\n" - + $"If you are just starting, you can see that the {ModCollectionIdentity.DefaultCollectionName} is currently assigned to {CollectionType.Default.ToName()} and {CollectionType.Interface.ToName()}.\n" - + "You can also assign 'Use No Mods' instead of a collection by clicking on the function buttons.") - .Register("Individual Assignments", - "In the Individual Assignments panel, you can manually create assignments for very specific characters or monsters, not just yourself or ones you can currently target.") - .Register("Group Assignments", - "In the Group Assignments panel, you can create Assignments for more specific groups of characters based on race or age.") - .Register("Collection Details", - "In the Collection Details panel, you can see a detailed overview over the usage of the currently selected collection, as well as remove outdated mod settings and setup inheritance.\n" - + "Inheritance can be used to make one collection take the settings of another as long as it does not setup the mod in question itself.") - .Register("Incognito Mode", - "This button can toggle Incognito Mode, which shortens all collection names to two letters and a number,\n" - + "and all displayed individual character names to their initials and world, in case you want to share screenshots.\n" - + "It is strongly recommended to not show your characters name in public screenshots when using Penumbra.") - .Deprecated() - .Register("Initial Setup, Step 7: Mods", "Our last stop is the Mods tab, where you can import and setup your mods.\n\n" - + $"Please go there after verifying that your {SelectedCollection} and {DefaultCollection} are setup to your liking.") - .Register("Initial Setup, Step 8: Mod Import", - "Click this button to open a file selector with which to select TTMP mod files. You can select multiple at once.\n\n" - + "It is not recommended to import huge mod packs of all your TexTools mods, but rather import the mods themselves, otherwise you lose out on a lot of Penumbra features!\n\n" - + "A feature to import raw texture mods for Tattoos etc. is available under Advanced Editing, but is currently a work in progress.") - .Register("Advanced Help", "Click this button to get detailed information on what you can do in the mod selector.\n\n" - + "Import and select a mod now to continue.") - .Register("Mod Filters", "You can filter the available mods by name, author, changed items or various attributes here.") - .Register("Collection Selectors", $"This row provides shortcuts to set your {SelectedCollection}.\n\n" - + $"The first button sets it to your {DefaultCollection} (if any).\n\n" - + "The second button sets it to the collection the settings of the currently selected mod are inherited from (if any).\n\n" - + "The third is a regular collection selector to let you choose among all your collections.") - .Register("Redrawing", - "Whenever you change your mod configuration, changes do not immediately take effect. You will need to force the game to reload the relevant files (or if this is not possible, restart the game).\n\n" - + "For this, Penumbra has these buttons as well as the '/penumbra redraw' command, which redraws all actors at once. You can also use several modifiers described in the help marker instead.\n\n" - + "Feel free to use these slash commands (e.g. '/penumbra redraw self') as a macro, too.") - .Register("Initial Setup, Step 9: Enabling Mods", - "Enable a mod here. Disabled mods will not apply to anything in the current collection.\n\n" - + "Mods can be enabled or disabled in a collection, or they can be unconfigured, in which case they will use Inheritance.") - .Register("Initial Setup, Step 10: Priority", - "If two enabled mods in one collection change the same files, there is a conflict.\n\n" - + "Conflicts can be solved by setting a priority. The mod with the higher number will be used for all the conflicting files.\n\n" - + "Conflicts are not a problem, as long as they are correctly resolved with priorities. Negative priorities are possible.") - .Register("Mod Options", "Many mods have options themselves. You can also choose those here.\n\n" - + "Pulldown-options are mutually exclusive, whereas checkmark options can all be enabled separately.") - .Register("Initial Setup - Fin", "Now you should have all information to get Penumbra running and working!\n\n" - + "If there are further questions or you need more help for the advanced features, take a look at the guide linked in the settings page.") - .Deprecated() - .Register("FAQ 1", - "It is advised to not use TexTools and Penumbra at the same time. Penumbra may refuse to work if TexTools broke your game indices.") - .Register("FAQ 2", "Penumbra can change the skin material a mod uses. This is under advanced editing.") - .Register("Favorites", - "You can now toggle mods as favorites using this button. You can filter for favorited mods in the mod selector. Favorites are stored locally, not within the mod, but independently of collections.") - .Register("Tags", - "Mods can now have two types of tags:\n\n- Local Tags are those that you can set for yourself. They are stored locally and are not saved in any way in the mod directory itself.\n- Mod Tags are stored in the mod metadata, are set by the mod creator and are exported together with the mod, they can only be edited in the Edit Mod tab.\n\nIf a mod has a tag in its Mod Tags, this overwrites any identical Local Tags.\n\nYou can filter for tags in the mod selector via 't:text'.") - .EnsureSize(Enum.GetValues().Length); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void OpenTutorial(BasicTutorialSteps step) - => _tutorial.Open((int)step, _config.TutorialStep, v => - { - _config.TutorialStep = v; - _config.Save(); - }); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SkipTutorial(BasicTutorialSteps step) - => _tutorial.Skip((int)step, _config.TutorialStep, v => - { - _config.TutorialStep = v; - _config.Save(); - }); - - /// Update the current tutorial step if tutorials have changed since last update. - public void UpdateTutorialStep() - { - var tutorial = _tutorial.CurrentEnabledId(_config.TutorialStep); - if (tutorial != _config.TutorialStep) - { - _config.TutorialStep = tutorial; - _config.Save(); - } - } -} diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 32f790c4..68253505 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -3,6 +3,7 @@ using Dalamud.Plugin; using Luna; using Penumbra.Interop.Services; using Penumbra.UI.AdvancedWindow; +using Penumbra.UI.Classes; using Penumbra.UI.Knowledge; using Penumbra.UI.Tabs.Debug; @@ -10,15 +11,15 @@ namespace Penumbra.UI; public class PenumbraWindowSystem : IDisposable, IUiService { - private readonly IUiBuilder _uiBuilder; - private readonly WindowSystem _windowSystem; - private readonly FileDialogService _fileDialog; - private readonly TextureArraySlicer _textureArraySlicer; - public readonly ConfigWindow Window; - public readonly PenumbraChangelog Changelog; - public readonly KnowledgeWindow KnowledgeWindow; + private readonly IUiBuilder _uiBuilder; + private readonly WindowSystem _windowSystem; + private readonly FileDialogService _fileDialog; + private readonly TextureArraySlicer _textureArraySlicer; + public readonly MainWindow.MainWindow Window; + public readonly PenumbraChangelog Changelog; + public readonly KnowledgeWindow KnowledgeWindow; - public PenumbraWindowSystem(IDalamudPluginInterface pi, Configuration config, PenumbraChangelog changelog, ConfigWindow window, + public PenumbraWindowSystem(IDalamudPluginInterface pi, Configuration config, PenumbraChangelog changelog, MainWindow.MainWindow window, LaunchButton _, ModEditWindowFactory editWindowFactory, FileDialogService fileDialog, ImportPopup importPopup, DebugTab debugTab, KnowledgeWindow knowledgeWindow, TextureArraySlicer textureArraySlicer, WindowSystem windowSystem) {