From 651c7410acda6b39cbae76f565fe6c407f59c808 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 18 Mar 2023 21:39:59 +0100 Subject: [PATCH] Wow, I accidentally the whole UI --- OtterGui | 2 +- Penumbra/Api/IpcTester.cs | 29 +- Penumbra/Api/PenumbraApi.cs | 291 ++++--- .../Collections/CollectionManager.Active.cs | 6 +- Penumbra/Collections/CollectionManager.cs | 112 ++- Penumbra/Collections/CollectionType.cs | 87 ++ Penumbra/Configuration.cs | 118 +-- Penumbra/Import/TexToolsImporter.Gui.cs | 4 +- Penumbra/Import/Textures/Texture.cs | 17 +- Penumbra/Interop/RedrawService.cs | 4 +- .../Resolver/IdentifiedCollectionCache.cs | 4 +- .../Resolver/PathResolver.AnimationState.cs | 16 +- .../Resolver/PathResolver.DrawObjectState.cs | 6 +- .../Resolver/PathResolver.Identification.cs | 4 +- Penumbra/Interop/Resolver/PathResolver.cs | 4 +- Penumbra/Meta/Files/ImcFile.cs | 6 +- Penumbra/Mods/ItemSwap/ItemSwap.cs | 2 +- Penumbra/Mods/ItemSwap/Swaps.cs | 2 +- Penumbra/PenumbraNew.cs | 54 +- Penumbra/Services/DalamudServices.cs | 78 +- Penumbra/Services/ServiceWrapper.cs | 9 +- Penumbra/UI/Classes/Colors.cs | 11 +- Penumbra/UI/Classes/Combos.cs | 18 +- Penumbra/UI/Classes/ItemSwapWindow.cs | 14 +- .../UI/Classes/ModEditWindow.FileEditor.cs | 188 ++--- Penumbra/UI/Classes/ModEditWindow.Files.cs | 350 ++++---- .../ModEditWindow.Materials.ColorSet.cs | 4 +- .../ModEditWindow.Materials.MtrlTab.cs | 6 +- .../Classes/ModEditWindow.Materials.Shpk.cs | 508 +++++------- .../UI/Classes/ModEditWindow.Materials.cs | 10 +- Penumbra/UI/Classes/ModEditWindow.Meta.cs | 38 +- .../Classes/ModEditWindow.ShaderPackages.cs | 30 +- Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs | 134 ++- Penumbra/UI/Classes/ModEditWindow.Textures.cs | 198 ++--- Penumbra/UI/Classes/ModEditWindow.cs | 9 +- .../Classes/ModFileSystemSelector.Filters.cs | 313 ------- Penumbra/UI/Classes/ModFileSystemSelector.cs | 478 ----------- Penumbra/UI/Classes/ModFilter.cs | 59 -- .../Collections.CollectionSelector.cs | 34 + .../Collections.IndividualCollectionUi.cs | 357 ++++++++ .../Collections.InheritanceUi.cs | 302 +++++++ .../UI/CollectionTab/Collections.NpcCombo.cs | 31 + .../CollectionTab/Collections.SpecialCombo.cs | 42 + .../CollectionTab/Collections.WorldCombo.cs | 24 + Penumbra/UI/ConfigWindow.ChangedItemsTab.cs | 101 --- .../ConfigWindow.CollectionsTab.Individual.cs | 367 --------- ...ConfigWindow.CollectionsTab.Inheritance.cs | 317 ------- Penumbra/UI/ConfigWindow.CollectionsTab.cs | 307 ------- Penumbra/UI/ConfigWindow.DebugTab.cs | 644 --------------- Penumbra/UI/ConfigWindow.EffectiveTab.cs | 204 ----- Penumbra/UI/ConfigWindow.Misc.cs | 181 ---- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 776 ------------------ Penumbra/UI/ConfigWindow.ModPanel.Header.cs | 210 ----- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 359 -------- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 251 ------ Penumbra/UI/ConfigWindow.ModPanel.cs | 79 -- Penumbra/UI/ConfigWindow.ModsTab.cs | 205 ----- Penumbra/UI/ConfigWindow.ResourceTab.cs | 152 ---- .../UI/ConfigWindow.SettingsTab.Advanced.cs | 111 --- .../UI/ConfigWindow.SettingsTab.General.cs | 351 -------- Penumbra/UI/ConfigWindow.SettingsTab.cs | 374 --------- Penumbra/UI/ConfigWindow.Tutorial.cs | 167 ---- Penumbra/UI/ConfigWindow.cs | 189 ++--- Penumbra/UI/FileDialogService.cs | 154 ++++ Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 773 +++++++++++++++++ Penumbra/UI/ModsTab/ModFilter.cs | 59 ++ Penumbra/UI/ModsTab/ModPanel.cs | 60 ++ .../UI/ModsTab/ModPanelChangedItemsTab.cs | 40 + Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 69 ++ Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs | 57 ++ Penumbra/UI/ModsTab/ModPanelEditTab.cs | 718 ++++++++++++++++ Penumbra/UI/ModsTab/ModPanelHeader.cs | 224 +++++ Penumbra/UI/ModsTab/ModPanelSettingsTab.cs | 348 ++++++++ Penumbra/UI/ModsTab/ModPanelTabBar.cs | 152 ++++ .../ResourceWatcher/ResourceWatcher.Table.cs | 28 +- .../UI/ResourceWatcher/ResourceWatcher.cs | 4 +- Penumbra/UI/Tabs/ChangedItemsTab.cs | 100 +++ Penumbra/UI/Tabs/CollectionsTab.cs | 298 +++++++ Penumbra/UI/Tabs/ConfigTabBar.cs | 67 ++ Penumbra/UI/Tabs/DebugTab.cs | 604 ++++++++++++++ Penumbra/UI/Tabs/EffectiveTab.cs | 194 +++++ Penumbra/UI/Tabs/ModsTab.cs | 208 +++++ Penumbra/UI/Tabs/ResourceTab.cs | 152 ++++ Penumbra/UI/Tabs/SettingsTab.cs | 751 +++++++++++++++++ Penumbra/UI/TutorialService.cs | 180 ++++ Penumbra/UI/UiHelpers.cs | 236 ++++++ Penumbra/UI/WindowSystem.cs | 17 +- 87 files changed, 7571 insertions(+), 7280 deletions(-) delete mode 100644 Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs delete mode 100644 Penumbra/UI/Classes/ModFileSystemSelector.cs delete mode 100644 Penumbra/UI/Classes/ModFilter.cs create mode 100644 Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs create mode 100644 Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs create mode 100644 Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs create mode 100644 Penumbra/UI/CollectionTab/Collections.NpcCombo.cs create mode 100644 Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs create mode 100644 Penumbra/UI/CollectionTab/Collections.WorldCombo.cs delete mode 100644 Penumbra/UI/ConfigWindow.ChangedItemsTab.cs delete mode 100644 Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs delete mode 100644 Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs delete mode 100644 Penumbra/UI/ConfigWindow.CollectionsTab.cs delete mode 100644 Penumbra/UI/ConfigWindow.DebugTab.cs delete mode 100644 Penumbra/UI/ConfigWindow.EffectiveTab.cs delete mode 100644 Penumbra/UI/ConfigWindow.Misc.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModPanel.Edit.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModPanel.Header.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModPanel.Settings.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModPanel.cs delete mode 100644 Penumbra/UI/ConfigWindow.ModsTab.cs delete mode 100644 Penumbra/UI/ConfigWindow.ResourceTab.cs delete mode 100644 Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs delete mode 100644 Penumbra/UI/ConfigWindow.SettingsTab.General.cs delete mode 100644 Penumbra/UI/ConfigWindow.SettingsTab.cs delete mode 100644 Penumbra/UI/ConfigWindow.Tutorial.cs create mode 100644 Penumbra/UI/FileDialogService.cs create mode 100644 Penumbra/UI/ModsTab/ModFileSystemSelector.cs create mode 100644 Penumbra/UI/ModsTab/ModFilter.cs create mode 100644 Penumbra/UI/ModsTab/ModPanel.cs create mode 100644 Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs create mode 100644 Penumbra/UI/ModsTab/ModPanelConflictsTab.cs create mode 100644 Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs create mode 100644 Penumbra/UI/ModsTab/ModPanelEditTab.cs create mode 100644 Penumbra/UI/ModsTab/ModPanelHeader.cs create mode 100644 Penumbra/UI/ModsTab/ModPanelSettingsTab.cs create mode 100644 Penumbra/UI/ModsTab/ModPanelTabBar.cs create mode 100644 Penumbra/UI/Tabs/ChangedItemsTab.cs create mode 100644 Penumbra/UI/Tabs/CollectionsTab.cs create mode 100644 Penumbra/UI/Tabs/ConfigTabBar.cs create mode 100644 Penumbra/UI/Tabs/DebugTab.cs create mode 100644 Penumbra/UI/Tabs/EffectiveTab.cs create mode 100644 Penumbra/UI/Tabs/ModsTab.cs create mode 100644 Penumbra/UI/Tabs/ResourceTab.cs create mode 100644 Penumbra/UI/Tabs/SettingsTab.cs create mode 100644 Penumbra/UI/TutorialService.cs create mode 100644 Penumbra/UI/UiHelpers.cs diff --git a/OtterGui b/OtterGui index df1cd8b0..e06d547c 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit df1cd8b02d729b2e7f585c301105b37c70d81c3e +Subproject commit e06d547c1690212c1ed3d471b0f9798101f06145 diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 7e7fac3b..7565c8b3 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -16,8 +16,9 @@ using Penumbra.Collections; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Meta.Manipulations; -using Penumbra.Services; - +using Penumbra.Services; +using Penumbra.UI; + namespace Penumbra.Api; public class IpcTester : IDisposable @@ -450,7 +451,7 @@ public class IpcTester : IDisposable } DrawIntro( Ipc.RedrawObjectByName.Label, "Redraw by Name" ); - ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 100 * UiHelpers.Scale ); ImGui.InputTextWithHint( "##redrawName", "Name...", ref _redrawName, 32 ); ImGui.SameLine(); if( ImGui.Button( "Redraw##Name" ) ) @@ -459,17 +460,17 @@ public class IpcTester : IDisposable } DrawIntro( Ipc.RedrawObject.Label, "Redraw Player Character" ); - if( ImGui.Button( "Redraw##pc" ) && DalamudServices.ClientState.LocalPlayer != null ) + if( ImGui.Button( "Redraw##pc" ) && DalamudServices.SClientState.LocalPlayer != null ) { - Ipc.RedrawObject.Subscriber( _pi ).Invoke( DalamudServices.ClientState.LocalPlayer, RedrawType.Redraw ); + Ipc.RedrawObject.Subscriber( _pi ).Invoke( DalamudServices.SClientState.LocalPlayer, RedrawType.Redraw ); } DrawIntro( Ipc.RedrawObjectByIndex.Label, "Redraw by Index" ); var tmp = _redrawIndex; - ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); - if( ImGui.DragInt( "##redrawIndex", ref tmp, 0.1f, 0, DalamudServices.Objects.Length ) ) + ImGui.SetNextItemWidth( 100 * UiHelpers.Scale ); + if( ImGui.DragInt( "##redrawIndex", ref tmp, 0.1f, 0, DalamudServices.SObjects.Length ) ) { - _redrawIndex = Math.Clamp( tmp, 0, DalamudServices.Objects.Length ); + _redrawIndex = Math.Clamp( tmp, 0, DalamudServices.SObjects.Length ); } ImGui.SameLine(); @@ -490,12 +491,12 @@ public class IpcTester : IDisposable private void SetLastRedrawn( IntPtr address, int index ) { - if( index < 0 || index > DalamudServices.Objects.Length || address == IntPtr.Zero || DalamudServices.Objects[ index ]?.Address != address ) + if( index < 0 || index > DalamudServices.SObjects.Length || address == IntPtr.Zero || DalamudServices.SObjects[ index ]?.Address != address ) { _lastRedrawnString = "Invalid"; } - _lastRedrawnString = $"{DalamudServices.Objects[ index ]!.Name} (0x{address:X}, {index})"; + _lastRedrawnString = $"{DalamudServices.SObjects[ index ]!.Name} (0x{address:X}, {index})"; } } @@ -794,7 +795,7 @@ public class IpcTester : IDisposable DrawIntro( Ipc.GetInterfaceCollectionName.Label, "Interface Collection" ); ImGui.TextUnformatted( Ipc.GetInterfaceCollectionName.Subscriber( _pi ).Invoke() ); DrawIntro( Ipc.GetCharacterCollectionName.Label, "Character" ); - ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 200 * UiHelpers.Scale ); ImGui.InputTextWithHint( "##characterCollectionName", "Character Name...", ref _characterCollectionName, 64 ); var (c, s) = Ipc.GetCharacterCollectionName.Subscriber( _pi ).Invoke( _characterCollectionName ); ImGui.SameLine(); @@ -832,7 +833,7 @@ public class IpcTester : IDisposable } DrawIntro( Ipc.GetChangedItems.Label, "Changed Item List" ); - ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 200 * UiHelpers.Scale ); ImGui.InputTextWithHint( "##changedCollection", "Collection Name...", ref _changedItemCollection, 64 ); ImGui.SameLine(); if( ImGui.Button( "Get" ) ) @@ -1182,7 +1183,7 @@ public class IpcTester : IDisposable } DrawIntro( Ipc.TrySetModPriority.Label, "Set Priority" ); - ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 200 * UiHelpers.Scale ); ImGui.DragInt( "##Priority", ref _settingsPriority ); ImGui.SameLine(); if( ImGui.Button( "Set##Priority" ) ) @@ -1222,7 +1223,7 @@ public class IpcTester : IDisposable } } - ImGui.SetNextItemWidth( 200 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 200 * UiHelpers.Scale ); using( var c = ImRaii.Combo( "##group", preview ) ) { if( c ) diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 1b581f0f..a225a626 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -16,6 +16,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using Penumbra.Api.Enums; using Penumbra.GameData.Actors; +using Penumbra.Interop.Loader; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Services; @@ -27,10 +28,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi public (int, int) ApiVersion => (4, 19); - private CommunicatorService? _communicator; - private Penumbra? _penumbra; - private Lumina.GameData? _lumina; - private readonly Dictionary _delegates = new(); public event Action? PreSettingsPanelDraw; @@ -83,34 +80,70 @@ public class PenumbraApi : IDisposable, IPenumbraApi public bool Valid => _penumbra != null; - public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra) + private CommunicatorService _communicator; + private Penumbra _penumbra; + private Lumina.GameData? _lumina; + + private Mod.Manager _modManager; + private ResourceLoader _resourceLoader; + private Configuration _config; + private ModCollection.Manager _collectionManager; + private DalamudServices _dalamud; + private TempCollectionManager _tempCollections; + private TempModManager _tempMods; + private ActorService _actors; + + public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, Mod.Manager modManager, ResourceLoader resourceLoader, + Configuration config, ModCollection.Manager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, + TempModManager tempMods, ActorService actors) { - _communicator = communicator; - _penumbra = penumbra; - _lumina = (Lumina.GameData?)DalamudServices.GameData.GetType() + _communicator = communicator; + _penumbra = penumbra; + _modManager = modManager; + _resourceLoader = resourceLoader; + _config = config; + _collectionManager = collectionManager; + _dalamud = dalamud; + _tempCollections = tempCollections; + _tempMods = tempMods; + _actors = actors; + + _lumina = (Lumina.GameData?)_dalamud.GameData.GetType() .GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic) - ?.GetValue(DalamudServices.GameData); - foreach (var collection in Penumbra.CollectionManager) + ?.GetValue(_dalamud.GameData); + foreach (var collection in _collectionManager) SubscribeToCollection(collection); - _communicator.CollectionChange.Event += SubscribeToNewCollections; - Penumbra.ResourceLoader.ResourceLoaded += OnResourceLoaded; - Penumbra.ModManager.ModPathChanged += ModPathChangeSubscriber; + _communicator.CollectionChange.Event += SubscribeToNewCollections; + _resourceLoader.ResourceLoaded += OnResourceLoaded; + _modManager.ModPathChanged += ModPathChangeSubscriber; } public unsafe void Dispose() { - Penumbra.ResourceLoader.ResourceLoaded -= OnResourceLoaded; - _communicator!.CollectionChange.Event -= SubscribeToNewCollections; - Penumbra.ModManager.ModPathChanged -= ModPathChangeSubscriber; - _penumbra = null; - _lumina = null; - _communicator = null; - foreach (var collection in Penumbra.CollectionManager) + if (!Valid) + return; + + foreach (var collection in _collectionManager) { if (_delegates.TryGetValue(collection, out var del)) collection.ModSettingChanged -= del; } + + _resourceLoader.ResourceLoaded -= OnResourceLoaded; + _communicator.CollectionChange.Event -= SubscribeToNewCollections; + _modManager.ModPathChanged -= ModPathChangeSubscriber; + _lumina = null; + _communicator = null!; + _penumbra = null!; + _modManager = null!; + _resourceLoader = null!; + _config = null!; + _collectionManager = null!; + _dalamud = null!; + _tempCollections = null!; + _tempMods = null!; + _actors = null!; } public event ChangedItemClick? ChangedItemClicked; @@ -118,7 +151,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public string GetModDirectory() { CheckInitialized(); - return Penumbra.Config.ModDirectory; + return _config.ModDirectory; } private unsafe void OnResourceLoaded(ResourceHandle* _, Utf8GamePath originalPath, FullPath? manipulatedPath, @@ -134,17 +167,17 @@ public class PenumbraApi : IDisposable, IPenumbraApi add { CheckInitialized(); - Penumbra.ModManager.ModDirectoryChanged += value; + _modManager.ModDirectoryChanged += value; } remove { CheckInitialized(); - Penumbra.ModManager.ModDirectoryChanged -= value; + _modManager.ModDirectoryChanged -= value; } } public bool GetEnabledState() - => Penumbra.Config.EnableMods; + => _config.EnableMods; public event Action? EnabledChange { @@ -163,7 +196,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public string GetConfiguration() { CheckInitialized(); - return JsonConvert.SerializeObject(Penumbra.Config, Formatting.Indented); + return JsonConvert.SerializeObject(_config, Formatting.Indented); } public event ChangedItemHover? ChangedItemTooltip; @@ -181,11 +214,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.InvalidArgument; if (tab != TabType.None) - _penumbra!.ConfigWindow.SelectTab = tab; + _penumbra!.ConfigWindow.SelectTab(tab); if (tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0)) { - if (Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (_modManager.TryGetMod(modDirectory, modName, out var mod)) _penumbra!.ConfigWindow.SelectMod(mod); else return PenumbraApiEc.ModMissing; @@ -230,19 +263,19 @@ public class PenumbraApi : IDisposable, IPenumbraApi public string ResolveDefaultPath(string path) { CheckInitialized(); - return ResolvePath(path, Penumbra.ModManager, Penumbra.CollectionManager.Default); + return ResolvePath(path, _modManager, _collectionManager.Default); } public string ResolveInterfacePath(string path) { CheckInitialized(); - return ResolvePath(path, Penumbra.ModManager, Penumbra.CollectionManager.Interface); + return ResolvePath(path, _modManager, _collectionManager.Interface); } public string ResolvePlayerPath(string path) { CheckInitialized(); - return ResolvePath(path, Penumbra.ModManager, PathResolver.PlayerCollection()); + return ResolvePath(path, _modManager, PathResolver.PlayerCollection()); } // TODO: cleanup when incrementing API level @@ -253,14 +286,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); AssociatedCollection(gameObjectIdx, out var collection); - return ResolvePath(path, Penumbra.ModManager, collection); + return ResolvePath(path, _modManager, collection); } public string ResolvePath(string path, string characterName, ushort worldId) { CheckInitialized(); - return ResolvePath(path, Penumbra.ModManager, - Penumbra.CollectionManager.Individual(NameToIdentifier(characterName, worldId))); + return ResolvePath(path, _modManager, + _collectionManager.Individual(NameToIdentifier(characterName, worldId))); } // TODO: cleanup when incrementing API level @@ -270,20 +303,20 @@ public class PenumbraApi : IDisposable, IPenumbraApi public string[] ReverseResolvePath(string path, string characterName, ushort worldId) { CheckInitialized(); - if (!Penumbra.Config.EnableMods) + if (!_config.EnableMods) return new[] { path, }; - var ret = Penumbra.CollectionManager.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path)); + var ret = _collectionManager.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path)); return ret.Select(r => r.ToString()).ToArray(); } public string[] ReverseResolveGameObjectPath(string path, int gameObjectIdx) { CheckInitialized(); - if (!Penumbra.Config.EnableMods) + if (!_config.EnableMods) return new[] { path, @@ -297,7 +330,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public string[] ReverseResolvePlayerPath(string path) { CheckInitialized(); - if (!Penumbra.Config.EnableMods) + if (!_config.EnableMods) return new[] { path, @@ -310,14 +343,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi public (string[], string[][]) ResolvePlayerPaths(string[] forward, string[] reverse) { CheckInitialized(); - if (!Penumbra.Config.EnableMods) + if (!_config.EnableMods) return (forward, reverse.Select(p => new[] { p, }).ToArray()); var playerCollection = PathResolver.PlayerCollection(); - var resolved = forward.Select(p => ResolvePath(p, Penumbra.ModManager, playerCollection)).ToArray(); + var resolved = forward.Select(p => ResolvePath(p, _modManager, playerCollection)).ToArray(); var reverseResolved = playerCollection.ReverseResolvePaths(reverse); return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray()); } @@ -333,7 +366,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); try { - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) collection = ModCollection.Empty; if (collection.HasCache) @@ -355,7 +388,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!Enum.IsDefined(type)) return string.Empty; - var collection = Penumbra.CollectionManager.ByType((CollectionType)type); + var collection = _collectionManager.ByType((CollectionType)type); return collection?.Name ?? string.Empty; } @@ -366,7 +399,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!Enum.IsDefined(type)) return (PenumbraApiEc.InvalidArgument, string.Empty); - var oldCollection = Penumbra.CollectionManager.ByType((CollectionType)type)?.Name ?? string.Empty; + var oldCollection = _collectionManager.ByType((CollectionType)type)?.Name ?? string.Empty; if (collectionName.Length == 0) { @@ -376,11 +409,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface) return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); - Penumbra.CollectionManager.RemoveSpecialCollection((CollectionType)type); + _collectionManager.RemoveSpecialCollection((CollectionType)type); return (PenumbraApiEc.Success, oldCollection); } - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) return (PenumbraApiEc.CollectionMissing, oldCollection); if (oldCollection.Length == 0) @@ -388,14 +421,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!allowCreateNew) return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); - Penumbra.CollectionManager.CreateSpecialCollection((CollectionType)type); + _collectionManager.CreateSpecialCollection((CollectionType)type); } else if (oldCollection == collection.Name) { return (PenumbraApiEc.NothingChanged, oldCollection); } - Penumbra.CollectionManager.SetCollection(collection, (CollectionType)type); + _collectionManager.SetCollection(collection, (CollectionType)type); return (PenumbraApiEc.Success, oldCollection); } @@ -404,9 +437,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); var id = AssociatedIdentifier(gameObjectIdx); if (!id.IsValid) - return (false, false, Penumbra.CollectionManager.Default.Name); + return (false, false, _collectionManager.Default.Name); - if (Penumbra.CollectionManager.Individuals.Individuals.TryGetValue(id, out var collection)) + if (_collectionManager.Individuals.Individuals.TryGetValue(id, out var collection)) return (true, true, collection.Name); AssociatedCollection(gameObjectIdx, out collection); @@ -419,9 +452,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); var id = AssociatedIdentifier(gameObjectIdx); if (!id.IsValid) - return (PenumbraApiEc.InvalidIdentifier, Penumbra.CollectionManager.Default.Name); + return (PenumbraApiEc.InvalidIdentifier, _collectionManager.Default.Name); - var oldCollection = Penumbra.CollectionManager.Individuals.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty; + var oldCollection = _collectionManager.Individuals.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty; if (collectionName.Length == 0) { @@ -431,12 +464,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!allowDelete) return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); - var idx = Penumbra.CollectionManager.Individuals.Index(id); - Penumbra.CollectionManager.RemoveIndividualCollection(idx); + var idx = _collectionManager.Individuals.Index(id); + _collectionManager.RemoveIndividualCollection(idx); return (PenumbraApiEc.Success, oldCollection); } - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) return (PenumbraApiEc.CollectionMissing, oldCollection); if (oldCollection.Length == 0) @@ -444,40 +477,40 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!allowCreateNew) return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); - var ids = Penumbra.CollectionManager.Individuals.GetGroup(id); - Penumbra.CollectionManager.CreateIndividualCollection(ids); + var ids = _collectionManager.Individuals.GetGroup(id); + _collectionManager.CreateIndividualCollection(ids); } else if (oldCollection == collection.Name) { return (PenumbraApiEc.NothingChanged, oldCollection); } - Penumbra.CollectionManager.SetCollection(collection, CollectionType.Individual, Penumbra.CollectionManager.Individuals.Index(id)); + _collectionManager.SetCollection(collection, CollectionType.Individual, _collectionManager.Individuals.Index(id)); return (PenumbraApiEc.Success, oldCollection); } public IList GetCollections() { CheckInitialized(); - return Penumbra.CollectionManager.Select(c => c.Name).ToArray(); + return _collectionManager.Select(c => c.Name).ToArray(); } public string GetCurrentCollection() { CheckInitialized(); - return Penumbra.CollectionManager.Current.Name; + return _collectionManager.Current.Name; } public string GetDefaultCollection() { CheckInitialized(); - return Penumbra.CollectionManager.Default.Name; + return _collectionManager.Default.Name; } public string GetInterfaceCollection() { CheckInitialized(); - return Penumbra.CollectionManager.Interface.Name; + return _collectionManager.Interface.Name; } // TODO: cleanup when incrementing API level @@ -487,9 +520,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi public (string, bool) GetCharacterCollection(string characterName, ushort worldId) { CheckInitialized(); - return Penumbra.CollectionManager.Individuals.TryGetCollection(NameToIdentifier(characterName, worldId), out var collection) + return _collectionManager.Individuals.TryGetCollection(NameToIdentifier(characterName, worldId), out var collection) ? (collection.Name, true) - : (Penumbra.CollectionManager.Default.Name, false); + : (_collectionManager.Default.Name, false); } public (IntPtr, string) GetDrawObjectInfo(IntPtr drawObject) @@ -508,13 +541,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi public IList<(string, string)> GetModList() { CheckInitialized(); - return Penumbra.ModManager.Select(m => (m.ModPath.Name, m.Name.Text)).ToArray(); + return _modManager.Select(m => (m.ModPath.Name, m.Name.Text)).ToArray(); } public IDictionary, GroupType)>? GetAvailableModSettings(string modDirectory, string modName) { CheckInitialized(); - return Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod) + return _modManager.TryGetMod(modDirectory, modName, out var mod) ? mod.Groups.ToDictionary(g => g.Name, g => ((IList)g.Select(o => o.Name).ToList(), g.Type)) : null; } @@ -523,10 +556,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi string modDirectory, string modName, bool allowInheritance) { CheckInitialized(); - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) return (PenumbraApiEc.CollectionMissing, null); - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return (PenumbraApiEc.ModMissing, null); var settings = allowInheritance ? collection.Settings[mod.Index] : collection[mod.Index].Settings; @@ -541,31 +574,31 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc ReloadMod(string modDirectory, string modName) { CheckInitialized(); - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; - Penumbra.ModManager.ReloadMod(mod.Index); + _modManager.ReloadMod(mod.Index); return PenumbraApiEc.Success; } public PenumbraApiEc AddMod(string modDirectory) { CheckInitialized(); - var dir = new DirectoryInfo(Path.Join(Penumbra.ModManager.BasePath.FullName, Path.GetFileName(modDirectory))); + var dir = new DirectoryInfo(Path.Join(_modManager.BasePath.FullName, Path.GetFileName(modDirectory))); if (!dir.Exists) return PenumbraApiEc.FileMissing; - Penumbra.ModManager.AddMod(dir); + _modManager.AddMod(dir); return PenumbraApiEc.Success; } public PenumbraApiEc DeleteMod(string modDirectory, string modName) { CheckInitialized(); - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.NothingChanged; - Penumbra.ModManager.DeleteMod(mod.Index); + _modManager.DeleteMod(mod.Index); return PenumbraApiEc.Success; } @@ -593,7 +626,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public (PenumbraApiEc, string, bool) GetModPath(string modDirectory, string modName) { CheckInitialized(); - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod) || !_penumbra!.ModFileSystem.FindLeaf(mod, out var leaf)) return (PenumbraApiEc.ModMissing, string.Empty, false); @@ -608,7 +641,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (newPath.Length == 0) return PenumbraApiEc.InvalidArgument; - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod) || !_penumbra!.ModFileSystem.FindLeaf(mod, out var leaf)) return PenumbraApiEc.ModMissing; @@ -626,10 +659,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc TryInheritMod(string collectionName, string modDirectory, string modName, bool inherit) { CheckInitialized(); - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; @@ -639,10 +672,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc TrySetMod(string collectionName, string modDirectory, string modName, bool enabled) { CheckInitialized(); - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; return collection.SetModState(mod.Index, enabled) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; @@ -651,10 +684,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc TrySetModPriority(string collectionName, string modDirectory, string modName, int priority) { CheckInitialized(); - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; return collection.SetModPriority(mod.Index, priority) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; @@ -664,10 +697,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi string optionName) { CheckInitialized(); - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); @@ -687,10 +720,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi IReadOnlyList optionNames) { CheckInitialized(); - if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; - if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) return PenumbraApiEc.ModMissing; var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); @@ -728,16 +761,16 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); - var sourceModIdx = Penumbra.ModManager + var sourceModIdx = _modManager .FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase))?.Index ?? -1; - var targetModIdx = Penumbra.ModManager + var targetModIdx = _modManager .FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase))?.Index ?? -1; if (string.IsNullOrEmpty(collectionName)) - foreach (var collection in Penumbra.CollectionManager) + foreach (var collection in _collectionManager) collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo); - else if (Penumbra.CollectionManager.ByName(collectionName, out var collection)) + else if (_collectionManager.ByName(collectionName, out var collection)) collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo); else return PenumbraApiEc.CollectionMissing; @@ -756,8 +789,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!identifier.IsValid) return (PenumbraApiEc.InvalidArgument, string.Empty); - if (!forceOverwriteCharacter && Penumbra.CollectionManager.Individuals.Individuals.ContainsKey(identifier) - || Penumbra.TempCollections.Collections.Individuals.ContainsKey(identifier)) + if (!forceOverwriteCharacter && _collectionManager.Individuals.Individuals.ContainsKey(identifier) + || _tempCollections.Collections.Individuals.ContainsKey(identifier)) return (PenumbraApiEc.CharacterCollectionExists, string.Empty); var name = $"{tag}_{character}"; @@ -765,10 +798,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (ret != PenumbraApiEc.Success) return (ret, name); - if (Penumbra.TempCollections.AddIdentifier(name, identifier)) + if (_tempCollections.AddIdentifier(name, identifier)) return (PenumbraApiEc.Success, name); - Penumbra.TempCollections.RemoveTemporaryCollection(name); + _tempCollections.RemoveTemporaryCollection(name); return (PenumbraApiEc.UnknownError, string.Empty); } @@ -778,7 +811,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (name.Length == 0 || Mod.Creator.ReplaceBadXivSymbols(name) != name) return PenumbraApiEc.InvalidArgument; - return Penumbra.TempCollections.CreateTemporaryCollection(name).Length > 0 + return _tempCollections.CreateTemporaryCollection(name).Length > 0 ? PenumbraApiEc.Success : PenumbraApiEc.CollectionExists; } @@ -787,23 +820,26 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); - if (actorIndex < 0 || actorIndex >= DalamudServices.Objects.Length) + if (!_actors.Valid) + return PenumbraApiEc.SystemDisposed; + + if (actorIndex < 0 || actorIndex >= DalamudServices.SObjects.Length) return PenumbraApiEc.InvalidArgument; - var identifier = Penumbra.Actors.FromObject(DalamudServices.Objects[actorIndex], false, false, true); + var identifier = _actors.AwaitedService.FromObject(DalamudServices.SObjects[actorIndex], false, false, true); if (!identifier.IsValid) return PenumbraApiEc.InvalidArgument; - if (!Penumbra.TempCollections.CollectionByName(collectionName, out var collection)) + if (!_tempCollections.CollectionByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; if (!forceAssignment - && (Penumbra.TempCollections.Collections.Individuals.ContainsKey(identifier) - || Penumbra.CollectionManager.Individuals.Individuals.ContainsKey(identifier))) + && (_tempCollections.Collections.Individuals.ContainsKey(identifier) + || _collectionManager.Individuals.Individuals.ContainsKey(identifier))) return PenumbraApiEc.CharacterCollectionExists; - var group = Penumbra.TempCollections.Collections.GetGroup(identifier); - return Penumbra.TempCollections.AddIdentifier(collection, group) + var group = _tempCollections.Collections.GetGroup(identifier); + return _tempCollections.AddIdentifier(collection, group) ? PenumbraApiEc.Success : PenumbraApiEc.UnknownError; } @@ -811,7 +847,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc RemoveTemporaryCollection(string character) { CheckInitialized(); - return Penumbra.TempCollections.RemoveByCharacterName(character) + return _tempCollections.RemoveByCharacterName(character) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } @@ -819,7 +855,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc RemoveTemporaryCollectionByName(string name) { CheckInitialized(); - return Penumbra.TempCollections.RemoveTemporaryCollection(name) + return _tempCollections.RemoveTemporaryCollection(name) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; } @@ -833,7 +869,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!ConvertManips(manipString, out var m)) return PenumbraApiEc.InvalidManipulation; - return Penumbra.TempMods.Register(tag, null, p, m, priority) switch + return _tempMods.Register(tag, null, p, m, priority) switch { RedirectResult.Success => PenumbraApiEc.Success, _ => PenumbraApiEc.UnknownError, @@ -844,8 +880,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi int priority) { CheckInitialized(); - if (!Penumbra.TempCollections.CollectionByName(collectionName, out var collection) - && !Penumbra.CollectionManager.ByName(collectionName, out collection)) + if (!_tempCollections.CollectionByName(collectionName, out var collection) + && !_collectionManager.ByName(collectionName, out collection)) return PenumbraApiEc.CollectionMissing; if (!ConvertPaths(paths, out var p)) @@ -854,7 +890,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!ConvertManips(manipString, out var m)) return PenumbraApiEc.InvalidManipulation; - return Penumbra.TempMods.Register(tag, collection, p, m, priority) switch + return _tempMods.Register(tag, collection, p, m, priority) switch { RedirectResult.Success => PenumbraApiEc.Success, _ => PenumbraApiEc.UnknownError, @@ -864,7 +900,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc RemoveTemporaryModAll(string tag, int priority) { CheckInitialized(); - return Penumbra.TempMods.Unregister(tag, null, priority) switch + return _tempMods.Unregister(tag, null, priority) switch { RedirectResult.Success => PenumbraApiEc.Success, RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, @@ -875,11 +911,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc RemoveTemporaryMod(string tag, string collectionName, int priority) { CheckInitialized(); - if (!Penumbra.TempCollections.CollectionByName(collectionName, out var collection) - && !Penumbra.CollectionManager.ByName(collectionName, out collection)) + if (!_tempCollections.CollectionByName(collectionName, out var collection) + && !_collectionManager.ByName(collectionName, out collection)) return PenumbraApiEc.CollectionMissing; - return Penumbra.TempMods.Unregister(tag, collection, priority) switch + return _tempMods.Unregister(tag, collection, priority) switch { RedirectResult.Success => PenumbraApiEc.Success, RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged, @@ -903,9 +939,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); var identifier = NameToIdentifier(characterName, worldId); - var collection = Penumbra.TempCollections.Collections.TryGetCollection(identifier, out var c) + var collection = _tempCollections.Collections.TryGetCollection(identifier, out var c) ? c - : Penumbra.CollectionManager.Individual(identifier); + : _collectionManager.Individual(identifier); var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(); return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } @@ -938,13 +974,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi // Return the collection associated to a current game object. If it does not exist, return the default collection. // If the index is invalid, returns false and the default collection. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection) + private unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection) { - collection = Penumbra.CollectionManager.Default; - if (gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length) + collection = _collectionManager.Default; + if (gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.SObjects.Length) return false; - var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx); + var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.SObjects.GetObjectAddress(gameObjectIdx); var data = PathResolver.IdentifyCollection(ptr, false); if (data.Valid) collection = data.ModCollection; @@ -953,20 +989,20 @@ public class PenumbraApi : IDisposable, IPenumbraApi } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static unsafe ActorIdentifier AssociatedIdentifier(int gameObjectIdx) + private unsafe ActorIdentifier AssociatedIdentifier(int gameObjectIdx) { - if (gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length) + if (gameObjectIdx < 0 || gameObjectIdx >= _dalamud.Objects.Length || !_actors.Valid) return ActorIdentifier.Invalid; - var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx); - return Penumbra.Actors.FromObject(ptr, out _, false, true, true); + var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.SObjects.GetObjectAddress(gameObjectIdx); + return _actors.AwaitedService.FromObject(ptr, out _, false, true, true); } // Resolve a path given by string for a specific collection. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private static string ResolvePath(string path, Mod.Manager _, ModCollection collection) + private string ResolvePath(string path, Mod.Manager _, ModCollection collection) { - if (!Penumbra.Config.EnableMods) + if (!_config.EnableMods) return path; var gamePath = Utf8GamePath.FromString(path, out var p, true) ? p : Utf8GamePath.Empty; @@ -983,7 +1019,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (Path.IsPathRooted(resolvedPath)) return _lumina?.GetFileFromDisk(resolvedPath); - return DalamudServices.GameData.GetFile(resolvedPath); + return _dalamud.GameData.GetFile(resolvedPath); } catch (Exception e) { @@ -1054,7 +1090,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi var name = c.Name; void Del(ModSettingChange type, int idx, int _, int _2, bool inherited) - => ModSettingChanged?.Invoke(type, name, idx >= 0 ? Penumbra.ModManager[idx].ModPath.Name : string.Empty, inherited); + => ModSettingChanged?.Invoke(type, name, idx >= 0 ? _modManager[idx].ModPath.Name : string.Empty, inherited); _delegates[c] = Del; c.ModSettingChanged += Del; @@ -1079,10 +1115,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi => PostSettingsPanelDraw?.Invoke(modDirectory); // TODO: replace all usages with ActorIdentifier stuff when incrementing API - private static ActorIdentifier NameToIdentifier(string name, ushort worldId) + private ActorIdentifier NameToIdentifier(string name, ushort worldId) { + if (!_actors.Valid) + return ActorIdentifier.Invalid; + // Verified to be valid name beforehand. var b = ByteString.FromStringUnsafe(name, false); - return Penumbra.Actors.CreatePlayer(b, worldId); + return _actors.AwaitedService.CreatePlayer(b, worldId); } } diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 5f2042dd..61a37961 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -193,7 +193,7 @@ public partial class ModCollection if (defaultIdx < 0) { Penumbra.ChatService.NotificationMessage( - $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure", + $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure", NotificationType.Warning); Default = Empty; configChanged = true; @@ -209,7 +209,7 @@ public partial class ModCollection if (interfaceIdx < 0) { Penumbra.ChatService.NotificationMessage( - $"Last choice of {ConfigWindow.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.", + $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.", "Load Failure", NotificationType.Warning); Interface = Empty; configChanged = true; @@ -225,7 +225,7 @@ public partial class ModCollection if (currentIdx < 0) { Penumbra.ChatService.NotificationMessage( - $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.", + $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.", "Load Failure", NotificationType.Warning); Current = DefaultName; configChanged = true; diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index f65be1ad..f0368e5a 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Penumbra.Api; +using Penumbra.GameData.Actors; using Penumbra.Interop; using Penumbra.Interop.Services; using Penumbra.Services; @@ -76,7 +77,7 @@ public partial class ModCollection _communicator.TemporaryGlobalModChange.Event += OnGlobalModChange; ReadCollections(files); LoadCollections(files); - UpdateCurrentCollectionInUse(); + UpdateCurrentCollectionInUse(); CreateNecessaryCaches(); } @@ -131,8 +132,8 @@ public partial class ModCollection var newCollection = duplicate?.Duplicate(name) ?? CreateNewEmpty(name); newCollection.Index = _collections.Count; - _collections.Add(newCollection); - + _collections.Add(newCollection); + Penumbra.SaveService.ImmediateSave(newCollection); Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}."); _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); @@ -180,7 +181,7 @@ public partial class ModCollection // Clear own inheritances. foreach (var inheritance in collection.Inheritance) collection.ClearSubscriptions(inheritance); - + Penumbra.SaveService.ImmediateDelete(collection); _collections.RemoveAt(idx); @@ -346,7 +347,7 @@ public partial class ModCollection // Duplicate collection files are not deleted, just not added here. private void ReadCollections(FilenameService files) { - var inheritances = new List>(); + var inheritances = new List>(); foreach (var file in files.CollectionFiles) { var collection = LoadFromFile(file, out var inheritance); @@ -371,5 +372,106 @@ public partial class ModCollection AddDefaultCollection(); ApplyInheritances(inheritances); } + + public string RedundancyCheck(CollectionType type, ActorIdentifier id) + { + var checkAssignment = ByType(type, id); + if (checkAssignment == null) + return string.Empty; + + switch (type) + { + // Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap. + case CollectionType.Individual: + switch (id.Type) + { + case IdentifierType.Player when id.HomeWorld != ushort.MaxValue: + { + var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue)); + return global?.Index == checkAssignment.Index + ? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it." + : string.Empty; + } + case IdentifierType.Owned: + if (id.HomeWorld != ushort.MaxValue) + { + var global = ByType(CollectionType.Individual, + Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId)); + if (global?.Index == checkAssignment.Index) + return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."; + } + + var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId)); + return unowned?.Index == checkAssignment.Index + ? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it." + : string.Empty; + } + break; + // The group of all Characters is redundant if they are all equal to Default or unassigned. + case CollectionType.MalePlayerCharacter: + case CollectionType.MaleNonPlayerCharacter: + case CollectionType.FemalePlayerCharacter: + case CollectionType.FemaleNonPlayerCharacter: + var first = ByType(CollectionType.MalePlayerCharacter) ?? Default; + var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default; + var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default; + var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default; + if (first.Index == second.Index + && first.Index == third.Index + && first.Index == fourth.Index + && first.Index == Default.Index) + return + "Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n" + + "You can keep just the Default Assignment."; + + break; + // Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default. + case CollectionType.NonPlayerChild: + case CollectionType.NonPlayerElderly: + var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter); + var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter); + var collection1 = CollectionType.MaleNonPlayerCharacter; + var collection2 = CollectionType.FemaleNonPlayerCharacter; + if (maleNpc == null) + { + maleNpc = Default; + if (maleNpc.Index != checkAssignment.Index) + return string.Empty; + + collection1 = CollectionType.Default; + } + + if (femaleNpc == null) + { + femaleNpc = Default; + if (femaleNpc.Index != checkAssignment.Index) + return string.Empty; + + collection2 = CollectionType.Default; + } + + return collection1 == collection2 + ? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them." + : $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them."; + + // For other assignments, check the inheritance order, unassigned means fall-through, + // assigned needs identical assignments to be redundant. + default: + var group = type.InheritanceOrder(); + foreach (var parentType in group) + { + var assignment = ByType(parentType); + if (assignment == null) + continue; + + if (assignment.Index == checkAssignment.Index) + return $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it."; + } + + break; + } + + return string.Empty; + } } } diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs index de9d80c4..91fcc5a1 100644 --- a/Penumbra/Collections/CollectionType.cs +++ b/Penumbra/Collections/CollectionType.cs @@ -1,6 +1,8 @@ using Penumbra.GameData.Enums; using System; +using System.Collections.Generic; using System.Linq; +using System.Reflection.Metadata.Ecma335; namespace Penumbra.Collections; @@ -132,6 +134,91 @@ public static class CollectionTypeExtensions _ => CollectionType.Inactive, }; } + + // @formatter:off + private static readonly IReadOnlyList DefaultList = new[] { CollectionType.Default }; + private static readonly IReadOnlyList MalePlayerList = new[] { CollectionType.MalePlayerCharacter, CollectionType.Default }; + private static readonly IReadOnlyList FemalePlayerList = new[] { CollectionType.FemalePlayerCharacter, CollectionType.Default }; + private static readonly IReadOnlyList MaleNpcList = new[] { CollectionType.MaleNonPlayerCharacter, CollectionType.Default }; + private static readonly IReadOnlyList FemaleNpcList = new[] { CollectionType.FemaleNonPlayerCharacter, CollectionType.Default }; + // @formatter:on + + /// A list of definite redundancy possibilities. + public static IReadOnlyList InheritanceOrder(this CollectionType collectionType) + => collectionType switch + { + CollectionType.Yourself => DefaultList, + CollectionType.MalePlayerCharacter => DefaultList, + CollectionType.FemalePlayerCharacter => DefaultList, + CollectionType.MaleNonPlayerCharacter => DefaultList, + CollectionType.FemaleNonPlayerCharacter => DefaultList, + CollectionType.MaleMidlander => MalePlayerList, + CollectionType.FemaleMidlander => FemalePlayerList, + CollectionType.MaleHighlander => MalePlayerList, + CollectionType.FemaleHighlander => FemalePlayerList, + CollectionType.MaleWildwood => MalePlayerList, + CollectionType.FemaleWildwood => FemalePlayerList, + CollectionType.MaleDuskwight => MalePlayerList, + CollectionType.FemaleDuskwight => FemalePlayerList, + CollectionType.MalePlainsfolk => MalePlayerList, + CollectionType.FemalePlainsfolk => FemalePlayerList, + CollectionType.MaleDunesfolk => MalePlayerList, + CollectionType.FemaleDunesfolk => FemalePlayerList, + CollectionType.MaleSeekerOfTheSun => MalePlayerList, + CollectionType.FemaleSeekerOfTheSun => FemalePlayerList, + CollectionType.MaleKeeperOfTheMoon => MalePlayerList, + CollectionType.FemaleKeeperOfTheMoon => FemalePlayerList, + CollectionType.MaleSeawolf => MalePlayerList, + CollectionType.FemaleSeawolf => FemalePlayerList, + CollectionType.MaleHellsguard => MalePlayerList, + CollectionType.FemaleHellsguard => FemalePlayerList, + CollectionType.MaleRaen => MalePlayerList, + CollectionType.FemaleRaen => FemalePlayerList, + CollectionType.MaleXaela => MalePlayerList, + CollectionType.FemaleXaela => FemalePlayerList, + CollectionType.MaleHelion => MalePlayerList, + CollectionType.FemaleHelion => FemalePlayerList, + CollectionType.MaleLost => MalePlayerList, + CollectionType.FemaleLost => FemalePlayerList, + CollectionType.MaleRava => MalePlayerList, + CollectionType.FemaleRava => FemalePlayerList, + CollectionType.MaleVeena => MalePlayerList, + CollectionType.FemaleVeena => FemalePlayerList, + CollectionType.MaleMidlanderNpc => MaleNpcList, + CollectionType.FemaleMidlanderNpc => FemaleNpcList, + CollectionType.MaleHighlanderNpc => MaleNpcList, + CollectionType.FemaleHighlanderNpc => FemaleNpcList, + CollectionType.MaleWildwoodNpc => MaleNpcList, + CollectionType.FemaleWildwoodNpc => FemaleNpcList, + CollectionType.MaleDuskwightNpc => MaleNpcList, + CollectionType.FemaleDuskwightNpc => FemaleNpcList, + CollectionType.MalePlainsfolkNpc => MaleNpcList, + CollectionType.FemalePlainsfolkNpc => FemaleNpcList, + CollectionType.MaleDunesfolkNpc => MaleNpcList, + CollectionType.FemaleDunesfolkNpc => FemaleNpcList, + CollectionType.MaleSeekerOfTheSunNpc => MaleNpcList, + CollectionType.FemaleSeekerOfTheSunNpc => FemaleNpcList, + CollectionType.MaleKeeperOfTheMoonNpc => MaleNpcList, + CollectionType.FemaleKeeperOfTheMoonNpc => FemaleNpcList, + CollectionType.MaleSeawolfNpc => MaleNpcList, + CollectionType.FemaleSeawolfNpc => FemaleNpcList, + CollectionType.MaleHellsguardNpc => MaleNpcList, + CollectionType.FemaleHellsguardNpc => FemaleNpcList, + CollectionType.MaleRaenNpc => MaleNpcList, + CollectionType.FemaleRaenNpc => FemaleNpcList, + CollectionType.MaleXaelaNpc => MaleNpcList, + CollectionType.FemaleXaelaNpc => FemaleNpcList, + CollectionType.MaleHelionNpc => MaleNpcList, + CollectionType.FemaleHelionNpc => FemaleNpcList, + CollectionType.MaleLostNpc => MaleNpcList, + CollectionType.FemaleLostNpc => FemaleNpcList, + CollectionType.MaleRavaNpc => MaleNpcList, + CollectionType.FemaleRavaNpc => FemaleNpcList, + CollectionType.MaleVeenaNpc => MaleNpcList, + CollectionType.FemaleVeenaNpc => FemaleNpcList, + CollectionType.Individual => DefaultList, + _ => Array.Empty(), + }; public static CollectionType FromParts( SubRace race, Gender gender, bool npc ) { diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 9499ae30..48b3c29e 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -11,7 +11,7 @@ using OtterGui.Widgets; using Penumbra.GameData.Enums; using Penumbra.Import; using Penumbra.Mods; -using Penumbra.Services; +using Penumbra.Services; using Penumbra.UI; using Penumbra.UI.Classes; using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs; @@ -29,25 +29,26 @@ public class Configuration : IPluginConfiguration public int Version { get; set; } = Constants.CurrentVersion; - public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; + public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; public ChangeLogDisplayType ChangeLogDisplayType { get; set; } = ChangeLogDisplayType.New; - public bool EnableMods { get; set; } = true; - public string ModDirectory { get; set; } = string.Empty; + public bool EnableMods { get; set; } = true; + public string ModDirectory { get; set; } = string.Empty; public string ExportDirectory { get; set; } = string.Empty; - public bool HideUiInGPose { get; set; } = false; - public bool HideUiInCutscenes { get; set; } = true; + public bool HideUiInGPose { get; set; } = false; + public bool HideUiInCutscenes { get; set; } = true; public bool HideUiWhenUiHidden { get; set; } = false; public bool UseCharacterCollectionInMainWindow { get; set; } = true; - public bool UseCharacterCollectionsInCards { get; set; } = true; - public bool UseCharacterCollectionInInspect { get; set; } = true; - public bool UseCharacterCollectionInTryOn { get; set; } = true; + public bool UseCharacterCollectionsInCards { get; set; } = true; + public bool UseCharacterCollectionInInspect { get; set; } = true; + public bool UseCharacterCollectionInTryOn { get; set; } = true; public bool UseOwnerNameForCharacterCollection { get; set; } = true; - public bool UseNoModsInInspect { get; set; } = false; + public bool UseNoModsInInspect { get; set; } = false; - public bool HideRedrawBar { get; set; } = false; + public bool HideRedrawBar { get; set; } = false; + public int OptionGroupCollapsibleMin { get; set; } = 5; #if DEBUG public bool DebugMode { get; set; } = true; @@ -63,35 +64,35 @@ public class Configuration : IPluginConfiguration public bool OnlyAddMatchingResources { get; set; } = true; public int MaxResourceWatcherRecords { get; set; } = ResourceWatcher.DefaultMaxEntries; - public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; - public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; - public ResourceWatcher.RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; + public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; + public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; + public ResourceWatcher.RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; - [JsonConverter( typeof( SortModeConverter ) )] - [JsonProperty( Order = int.MaxValue )] - public ISortMode< Mod > SortMode = ISortMode< Mod >.FoldersFirst; + [JsonConverter(typeof(SortModeConverter))] + [JsonProperty(Order = int.MaxValue)] + public ISortMode SortMode = ISortMode.FoldersFirst; - public bool ScaleModSelector { get; set; } = false; - public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize; - public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; - public bool OpenFoldersByDefault { get; set; } = false; - public int SingleGroupRadioMax { get; set; } = 2; - public string DefaultImportFolder { get; set; } = string.Empty; - public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); + public bool ScaleModSelector { get; set; } = false; + public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize; + public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; + public bool OpenFoldersByDefault { get; set; } = false; + public int SingleGroupRadioMax { get; set; } = 2; + public string DefaultImportFolder { get; set; } = string.Empty; + public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); public bool PrintSuccessfulCommandsToChat { get; set; } = true; - public bool FixMainWindow { get; set; } = false; - public bool AutoDeduplicateOnImport { get; set; } = true; - public bool EnableHttpApi { get; set; } = true; + public bool FixMainWindow { get; set; } = false; + public bool AutoDeduplicateOnImport { get; set; } = true; + public bool EnableHttpApi { get; set; } = true; - public string DefaultModImportPath { get; set; } = string.Empty; - public bool AlwaysOpenDefaultImport { get; set; } = false; - public bool KeepDefaultMetaChanges { get; set; } = false; - public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author; + public string DefaultModImportPath { get; set; } = string.Empty; + public bool AlwaysOpenDefaultImport { get; set; } = false; + public bool KeepDefaultMetaChanges { get; set; } = false; + public string DefaultModAuthor { get; set; } = DefaultTexToolsData.Author; - public Dictionary< ColorId, uint > Colors { get; set; } - = Enum.GetValues< ColorId >().ToDictionary( c => c, c => c.Data().DefaultColor ); + public Dictionary Colors { get; set; } + = Enum.GetValues().ToDictionary(c => c, c => c.Data().DefaultColor); /// /// Load the current configuration. @@ -121,6 +122,7 @@ public class Configuration : IPluginConfiguration Error = HandleDeserializationError, }); } + migrator.Migrate(this); } @@ -129,17 +131,17 @@ public class Configuration : IPluginConfiguration { try { - var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - File.WriteAllText( _fileName, text ); + var text = JsonConvert.SerializeObject(this, Formatting.Indented); + File.WriteAllText(_fileName, text); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not save plugin configuration:\n{e}" ); + Penumbra.Log.Error($"Could not save plugin configuration:\n{e}"); } } public void Save() - => _framework.RegisterDelayed( nameof( SaveConfiguration ), SaveConfiguration ); + => _framework.RegisterDelayed(nameof(SaveConfiguration), SaveConfiguration); /// Contains some default values or boundaries for config values. public static class Constants @@ -152,41 +154,39 @@ public class Configuration : IPluginConfiguration public const int DefaultScaledSize = 20; public const int MinScaledSize = 5; - public static readonly ISortMode< Mod >[] ValidSortModes = + public static readonly ISortMode[] ValidSortModes = { - ISortMode< Mod >.FoldersFirst, - ISortMode< Mod >.Lexicographical, + ISortMode.FoldersFirst, + ISortMode.Lexicographical, new ModFileSystem.ImportDate(), new ModFileSystem.InverseImportDate(), - ISortMode< Mod >.InverseFoldersFirst, - ISortMode< Mod >.InverseLexicographical, - ISortMode< Mod >.FoldersLast, - ISortMode< Mod >.InverseFoldersLast, - ISortMode< Mod >.InternalOrder, - ISortMode< Mod >.InverseInternalOrder, + ISortMode.InverseFoldersFirst, + ISortMode.InverseLexicographical, + ISortMode.FoldersLast, + ISortMode.InverseFoldersLast, + ISortMode.InternalOrder, + ISortMode.InverseInternalOrder, }; } /// Convert SortMode Types to their name. - private class SortModeConverter : JsonConverter< ISortMode< Mod > > + private class SortModeConverter : JsonConverter> { - public override void WriteJson( JsonWriter writer, ISortMode< Mod >? value, JsonSerializer serializer ) + public override void WriteJson(JsonWriter writer, ISortMode? value, JsonSerializer serializer) { - value ??= ISortMode< Mod >.FoldersFirst; - serializer.Serialize( writer, value.GetType().Name ); + value ??= ISortMode.FoldersFirst; + serializer.Serialize(writer, value.GetType().Name); } - public override ISortMode< Mod > ReadJson( JsonReader reader, Type objectType, ISortMode< Mod >? existingValue, + public override ISortMode ReadJson(JsonReader reader, Type objectType, ISortMode? existingValue, bool hasExistingValue, - JsonSerializer serializer ) + JsonSerializer serializer) { - var name = serializer.Deserialize< string >( reader ); - if( name == null || !Constants.ValidSortModes.FindFirst( s => s.GetType().Name == name, out var mode ) ) - { - return existingValue ?? ISortMode< Mod >.FoldersFirst; - } + var name = serializer.Deserialize(reader); + if (name == null || !Constants.ValidSortModes.FindFirst(s => s.GetType().Name == name, out var mode)) + return existingValue ?? ISortMode.FoldersFirst; return mode; } } -} \ No newline at end of file +} diff --git a/Penumbra/Import/TexToolsImporter.Gui.cs b/Penumbra/Import/TexToolsImporter.Gui.cs index 6e365e1b..bf997c2c 100644 --- a/Penumbra/Import/TexToolsImporter.Gui.cs +++ b/Penumbra/Import/TexToolsImporter.Gui.cs @@ -93,12 +93,12 @@ public partial class TexToolsImporter ImGui.TableNextColumn(); if( ex == null ) { - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.FolderExpanded.Value() ); + using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.FolderExpanded.Value(Penumbra.Config) ); ImGui.TextUnformatted( dir?.FullName[ ( _baseDirectory.FullName.Length + 1 ).. ] ?? "Unknown Directory" ); } else { - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ConflictingMod.Value() ); + using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ConflictingMod.Value(Penumbra.Config) ); ImGui.TextUnformatted( ex.Message ); ImGuiUtil.HoverTooltip( ex.ToString() ); } diff --git a/Penumbra/Import/Textures/Texture.cs b/Penumbra/Import/Textures/Texture.cs index f6bb9866..669f81d7 100644 --- a/Penumbra/Import/Textures/Texture.cs +++ b/Penumbra/Import/Textures/Texture.cs @@ -10,8 +10,9 @@ using Lumina.Data.Files; using OtterGui; using OtterGui.Raii; using OtterTex; -using Penumbra.Services; +using Penumbra.Services; using Penumbra.String.Classes; +using Penumbra.UI; using Penumbra.UI.Classes; using SixLabors.ImageSharp.PixelFormats; using Image = SixLabors.ImageSharp.Image; @@ -196,7 +197,7 @@ public sealed class Texture : IDisposable return File.OpenRead( Path ); } - var file = DalamudServices.GameData.GetFile( Path ); + var file = DalamudServices.SGameData.GetFile( Path ); return file != null ? new MemoryStream( file.Data ) : throw new Exception( $"Unable to obtain \"{Path}\" from game files." ); } @@ -216,7 +217,7 @@ public sealed class Texture : IDisposable { if( game ) { - if( !DalamudServices.GameData.FileExists( path ) ) + if( !DalamudServices.SGameData.FileExists( path ) ) { continue; } @@ -227,7 +228,7 @@ public sealed class Texture : IDisposable } using var id = ImRaii.PushId( idx ); - using( var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.FolderExpanded.Value(), game ) ) + using( var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.FolderExpanded.Value(Penumbra.Config), game ) ) { var p = game ? $"--> {path}" : path[ skipPrefix.. ]; if( ImGui.Selectable( p, path == startPath ) && path != startPath ) @@ -245,12 +246,12 @@ public sealed class Texture : IDisposable ImGuiUtil.HoverTooltip( tooltip ); } - public void PathInputBox( string label, string hint, string tooltip, string startPath, FileDialogManager manager ) + public void PathInputBox( string label, string hint, string tooltip, string startPath, FileDialogService fileDialog ) { _tmpPath ??= Path; using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, - new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); - ImGui.SetNextItemWidth( -2 * ImGui.GetFrameHeight() - 7 * ImGuiHelpers.GlobalScale ); + new Vector2( UiHelpers.ScaleX3, ImGui.GetStyle().ItemSpacing.Y ) ); + ImGui.SetNextItemWidth( -2 * ImGui.GetFrameHeight() - 7 * UiHelpers.Scale ); ImGui.InputTextWithHint( label, hint, ref _tmpPath, Utf8GamePath.MaxGamePathLength ); if( ImGui.IsItemDeactivatedAfterEdit() ) { @@ -277,7 +278,7 @@ public sealed class Texture : IDisposable } } - manager.OpenFileDialog( "Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath ); + fileDialog.OpenFilePicker( "Open Image...", "Textures{.png,.dds,.tex}", UpdatePath, 1, startPath, false ); } ImGui.SameLine(); diff --git a/Penumbra/Interop/RedrawService.cs b/Penumbra/Interop/RedrawService.cs index f738320c..6739e95e 100644 --- a/Penumbra/Interop/RedrawService.cs +++ b/Penumbra/Interop/RedrawService.cs @@ -295,8 +295,8 @@ public sealed unsafe partial class RedrawService : IDisposable private static GameObject? GetLocalPlayer() { - var gPosePlayer = DalamudServices.Objects[GPosePlayerIdx]; - return gPosePlayer ?? DalamudServices.Objects[0]; + var gPosePlayer = DalamudServices.SObjects[GPosePlayerIdx]; + return gPosePlayer ?? DalamudServices.SObjects[0]; } public bool GetName(string lowerName, out GameObject? actor) diff --git a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs index 7135eebb..fa09f64d 100644 --- a/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/Resolver/IdentifiedCollectionCache.cs @@ -30,7 +30,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(IntPtr return; _communicator.CollectionChange.Event += CollectionChangeClear; - DalamudServices.ClientState.TerritoryChanged += TerritoryClear; + DalamudServices.SClientState.TerritoryChanged += TerritoryClear; _events.CharacterDestructor += OnCharacterDestruct; _enabled = true; } @@ -41,7 +41,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(IntPtr return; _communicator.CollectionChange.Event -= CollectionChangeClear; - DalamudServices.ClientState.TerritoryChanged -= TerritoryClear; + DalamudServices.SClientState.TerritoryChanged -= TerritoryClear; _events.CharacterDestructor -= OnCharacterDestruct; _enabled = false; } diff --git a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs index 65d032de..e39b85f4 100644 --- a/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.AnimationState.cs @@ -138,9 +138,9 @@ public unsafe partial class PathResolver { var getGameObjectIdx = ( ( delegate* unmanaged< IntPtr, int >** )timeline )[ 0 ][ Offsets.GetGameObjectIdxVfunc ]; var idx = getGameObjectIdx( timeline ); - if( idx >= 0 && idx < DalamudServices.Objects.Length ) + if( idx >= 0 && idx < DalamudServices.SObjects.Length ) { - var obj = DalamudServices.Objects[ idx ]; + var obj = DalamudServices.SObjects[ idx ]; return obj != null ? IdentifyCollection( ( GameObject* )obj.Address, true ) : ResolveData.Invalid; } } @@ -204,9 +204,9 @@ public unsafe partial class PathResolver if( timelinePtr != IntPtr.Zero ) { var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 ); - if( actorIdx >= 0 && actorIdx < DalamudServices.Objects.Length ) + if( actorIdx >= 0 && actorIdx < DalamudServices.SObjects.Length ) { - _animationLoadData = IdentifyCollection( ( GameObject* )( DalamudServices.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ), true ); + _animationLoadData = IdentifyCollection( ( GameObject* )( DalamudServices.SObjects[ actorIdx ]?.Address ?? IntPtr.Zero ), true ); } } @@ -234,14 +234,14 @@ public unsafe partial class PathResolver private global::Dalamud.Game.ClientState.Objects.Types.GameObject? GetOwnedObject( uint id ) { - var owner = DalamudServices.Objects.SearchById( id ); + var owner = DalamudServices.SObjects.SearchById( id ); if( owner == null ) { return null; } var idx = ( ( GameObject* )owner.Address )->ObjectIndex; - return DalamudServices.Objects[ idx + 1 ]; + return DalamudServices.SObjects[ idx + 1 ]; } private IntPtr LoadCharacterVfxDetour( byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4 ) @@ -252,8 +252,8 @@ public unsafe partial class PathResolver { var obj = vfxParams->GameObjectType switch { - 0 => DalamudServices.Objects.SearchById( vfxParams->GameObjectId ), - 2 => DalamudServices.Objects[ ( int )vfxParams->GameObjectId ], + 0 => DalamudServices.SObjects.SearchById( vfxParams->GameObjectId ), + 2 => DalamudServices.SObjects[ ( int )vfxParams->GameObjectId ], 4 => GetOwnedObject( vfxParams->GameObjectId ), _ => null, }; diff --git a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs index edfe566b..983dcdb6 100644 --- a/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs +++ b/Penumbra/Interop/Resolver/PathResolver.DrawObjectState.cs @@ -112,7 +112,7 @@ public unsafe partial class PathResolver // Check that a linked DrawObject still corresponds to the correct actor and that it still exists, otherwise remove it. private bool VerifyEntry(IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject) { - gameObject = (GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx); + gameObject = (GameObject*)DalamudServices.SObjects.GetObjectAddress(gameObjectIdx); var draw = (DrawObject*)drawObject; if (gameObject != null && (gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject)) @@ -244,9 +244,9 @@ public unsafe partial class PathResolver // We do not iterate the Dalamud table because it does not work when not logged in. private void InitializeDrawObjects() { - for (var i = 0; i < DalamudServices.Objects.Length; ++i) + for (var i = 0; i < DalamudServices.SObjects.Length; ++i) { - var ptr = (GameObject*)DalamudServices.Objects.GetObjectAddress(i); + var ptr = (GameObject*)DalamudServices.SObjects.GetObjectAddress(i); if (ptr != null && ptr->IsCharacter() && ptr->DrawObject != null) _drawObjectToObject[(IntPtr)ptr->DrawObject] = (IdentifyCollection(ptr, false), ptr->ObjectIndex); } diff --git a/Penumbra/Interop/Resolver/PathResolver.Identification.cs b/Penumbra/Interop/Resolver/PathResolver.Identification.cs index 516d2a2b..a619755a 100644 --- a/Penumbra/Interop/Resolver/PathResolver.Identification.cs +++ b/Penumbra/Interop/Resolver/PathResolver.Identification.cs @@ -38,7 +38,7 @@ public unsafe partial class PathResolver // Login screen. Names are populated after actors are drawn, // so it is not possible to fetch names from the ui list. // Actors are also not named. So use Yourself > Players > Racial > Default. - if( !DalamudServices.ClientState.IsLoggedIn ) + if( !DalamudServices.SClientState.IsLoggedIn ) { var collection2 = Penumbra.CollectionManager.ByType( CollectionType.Yourself ) ?? CollectionByAttributes( gameObject ) @@ -87,7 +87,7 @@ public unsafe partial class PathResolver public static ModCollection PlayerCollection() { using var performance = Penumbra.Performance.Measure( PerformanceType.IdentifyCollection ); - var gameObject = ( GameObject* )DalamudServices.Objects.GetObjectAddress( 0 ); + var gameObject = ( GameObject* )DalamudServices.SObjects.GetObjectAddress( 0 ); if( gameObject == null ) { return Penumbra.CollectionManager.ByType( CollectionType.Yourself ) diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index edcb76d5..059a0550 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -29,7 +29,7 @@ public partial class PathResolver : IDisposable private readonly CommunicatorService _communicator; private readonly ResourceLoader _loader; - private static readonly CutsceneCharacters Cutscenes = new(DalamudServices.Objects, Penumbra.GameEvents); // TODO + private static readonly CutsceneCharacters Cutscenes = new(DalamudServices.SObjects, Penumbra.GameEvents); // TODO private static DrawObjectState _drawObjects = null!; // TODO private static readonly BitArray ValidHumanModels; internal static IdentifiedCollectionCache IdentifiedCache = null!; // TODO @@ -39,7 +39,7 @@ public partial class PathResolver : IDisposable private readonly SubfileHelper _subFiles; static PathResolver() - => ValidHumanModels = GetValidHumanModels(DalamudServices.GameData); + => ValidHumanModels = GetValidHumanModels(DalamudServices.SGameData); public unsafe PathResolver(StartTracker timer, CommunicatorService communicator, GameEventManager events, ResourceLoader loader) { diff --git a/Penumbra/Meta/Files/ImcFile.cs b/Penumbra/Meta/Files/ImcFile.cs index cc3add2d..32a9c925 100644 --- a/Penumbra/Meta/Files/ImcFile.cs +++ b/Penumbra/Meta/Files/ImcFile.cs @@ -141,7 +141,7 @@ public unsafe class ImcFile : MetaBaseFile public override void Reset() { - var file = DalamudServices.GameData.GetFile( Path.ToString() ); + var file = DalamudServices.SGameData.GetFile( Path.ToString() ); fixed( byte* ptr = file!.Data ) { MemoryUtility.MemCpyUnchecked( Data, ptr, file.Data.Length ); @@ -153,7 +153,7 @@ public unsafe class ImcFile : MetaBaseFile : base( 0 ) { Path = manip.GamePath(); - var file = DalamudServices.GameData.GetFile( Path.ToString() ); + var file = DalamudServices.SGameData.GetFile( Path.ToString() ); if( file == null ) { throw new ImcException( manip, Path ); @@ -172,7 +172,7 @@ public unsafe class ImcFile : MetaBaseFile public static ImcEntry GetDefault( string path, EquipSlot slot, int variantIdx, out bool exists ) { - var file = DalamudServices.GameData.GetFile( path ); + var file = DalamudServices.SGameData.GetFile( path ); exists = false; if( file == null ) { diff --git a/Penumbra/Mods/ItemSwap/ItemSwap.cs b/Penumbra/Mods/ItemSwap/ItemSwap.cs index 8358b97d..daefee5c 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwap.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwap.cs @@ -39,7 +39,7 @@ public static class ItemSwap return true; } - var file = DalamudServices.GameData.GetFile( path.InternalName.ToString() ); + var file = DalamudServices.SGameData.GetFile( path.InternalName.ToString() ); if( file != null ) { data = file.Data; diff --git a/Penumbra/Mods/ItemSwap/Swaps.cs b/Penumbra/Mods/ItemSwap/Swaps.cs index 6cca0356..8249c189 100644 --- a/Penumbra/Mods/ItemSwap/Swaps.cs +++ b/Penumbra/Mods/ItemSwap/Swaps.cs @@ -140,7 +140,7 @@ public sealed class FileSwap : Swap } swap.SwapToModded = redirections( swap.SwapToRequestPath ); - swap.SwapToModdedExistsInGame = !swap.SwapToModded.IsRooted && DalamudServices.GameData.FileExists( swap.SwapToModded.InternalName.ToString() ); + swap.SwapToModdedExistsInGame = !swap.SwapToModded.IsRooted && DalamudServices.SGameData.FileExists( swap.SwapToModded.InternalName.ToString() ); swap.SwapToModdedEqualsOriginal = !swap.SwapToModded.IsRooted && swap.SwapToModded.InternalName.Equals( swap.SwapFromRequestPath.Path ); swap.FileData = type switch diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 004fd284..9e196424 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -16,7 +16,10 @@ using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI; using Penumbra.UI.Classes; +using Penumbra.UI.ModTab; +using Penumbra.UI.Tabs; using Penumbra.Util; +using ModFileSystemSelector = Penumbra.UI.ModTab.ModFileSystemSelector; namespace Penumbra; @@ -28,7 +31,7 @@ public class PenumbraNew public static readonly Logger Log = new(); public readonly ServiceProvider Services; - public PenumbraNew(Penumbra pnumb, DalamudPluginInterface pi) + public PenumbraNew(Penumbra penumbra, DalamudPluginInterface pi) { var startTimer = new StartTracker(); using var time = startTimer.Measure(StartTimeType.Total); @@ -48,7 +51,7 @@ public class PenumbraNew // Add Dalamud services var dalamud = new DalamudServices(pi); dalamud.AddServices(services); - services.AddSingleton(pnumb); + services.AddSingleton(penumbra); // Add Game Data services.AddSingleton() @@ -84,28 +87,49 @@ public class PenumbraNew // Add Mod Services services.AddSingleton() .AddSingleton() - .AddSingleton(); - + .AddSingleton(); + // Add main services services.AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton(); // Add Interface - services.AddSingleton() + services.AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); - - // Add API - services.AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); - Services = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); + // Add API + services.AddSingleton() + .AddSingleton(x => x.GetRequiredService()) + .AddSingleton() + .AddSingleton(); + + Services = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); } public void Dispose() diff --git a/Penumbra/Services/DalamudServices.cs b/Penumbra/Services/DalamudServices.cs index a3227b92..7320822d 100644 --- a/Penumbra/Services/DalamudServices.cs +++ b/Penumbra/Services/DalamudServices.cs @@ -13,9 +13,9 @@ using Dalamud.Plugin; using System.Linq; using System.Reflection; using Microsoft.Extensions.DependencyInjection; - -// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local - + +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local + namespace Penumbra.Services; public class DalamudServices @@ -25,39 +25,37 @@ public class DalamudServices pluginInterface.Inject(this); try { - var serviceType = typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "Service`1" && t.IsGenericType); - var configType = typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "DalamudConfiguration"); + var serviceType = + typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "Service`1" && t.IsGenericType); + var configType = typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "DalamudConfiguration"); var interfaceType = typeof(DalamudPluginInterface).Assembly.DefinedTypes.FirstOrDefault(t => t.Name == "DalamudInterface"); if (serviceType == null || configType == null || interfaceType == null) - { return; - } - var configService = serviceType.MakeGenericType(configType); + var configService = serviceType.MakeGenericType(configType); var interfaceService = serviceType.MakeGenericType(interfaceType); - var configGetter = configService.GetMethod("Get", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + var configGetter = configService.GetMethod("Get", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); _interfaceGetter = interfaceService.GetMethod("Get", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (configGetter == null || _interfaceGetter == null) - { return; - } _dalamudConfig = configGetter.Invoke(null, null); if (_dalamudConfig != null) { - _saveDalamudConfig = _dalamudConfig.GetType().GetMethod("Save", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + _saveDalamudConfig = _dalamudConfig.GetType() + .GetMethod("Save", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (_saveDalamudConfig == null) { - _dalamudConfig = null; + _dalamudConfig = null; _interfaceGetter = null; } } } catch { - _dalamudConfig = null; + _dalamudConfig = null; _saveDalamudConfig = null; - _interfaceGetter = null; + _interfaceGetter = null; } } @@ -65,64 +63,70 @@ public class DalamudServices { services.AddSingleton(PluginInterface); services.AddSingleton(Commands); - services.AddSingleton(GameData); - services.AddSingleton(ClientState); + services.AddSingleton(SGameData); + services.AddSingleton(SClientState); services.AddSingleton(Chat); services.AddSingleton(Framework); services.AddSingleton(Conditions); services.AddSingleton(Targets); - services.AddSingleton(Objects); + services.AddSingleton(SObjects); services.AddSingleton(TitleScreenMenu); services.AddSingleton(GameGui); services.AddSingleton(KeyState); services.AddSingleton(SigScanner); services.AddSingleton(this); } - + // TODO remove static // @formatter:off [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static DataManager SGameData { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ClientState SClientState { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static Condition Conditions { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; - [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; + [PluginService][RequiredVersion("1.0")] public static ObjectTable SObjects { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static KeyState KeyState { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static SigScanner SigScanner { get; private set; } = null!; // @formatter:on + public UiBuilder UiBuilder + => PluginInterface.UiBuilder; + + public ObjectTable Objects + => SObjects; + + public ClientState ClientState + => SClientState; + + public DataManager GameData + => SGameData; + public const string WaitingForPluginsOption = "IsResumeGameAfterPluginLoad"; - private readonly object? _dalamudConfig; + private readonly object? _dalamudConfig; private readonly MethodInfo? _interfaceGetter; - private readonly MethodInfo? _saveDalamudConfig; - + private readonly MethodInfo? _saveDalamudConfig; + public bool GetDalamudConfig(string fieldName, out T? value) { value = default; try { if (_dalamudConfig == null) - { return false; - } var getter = _dalamudConfig.GetType().GetProperty(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (getter == null) - { return false; - } var result = getter.GetValue(_dalamudConfig); if (result is not T v) - { return false; - } value = v; return true; @@ -139,22 +143,20 @@ public class DalamudServices try { if (_dalamudConfig == null) - { return false; - } var getter = _dalamudConfig.GetType().GetProperty(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (getter == null) - { return false; - } getter.SetValue(_dalamudConfig, value); if (windowFieldName != null) { var inter = _interfaceGetter!.Invoke(null, null); - var settingsWindow = inter?.GetType().GetField("settingsWindow", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(inter); - settingsWindow?.GetType().GetField(windowFieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.SetValue(settingsWindow, value); + var settingsWindow = inter?.GetType() + .GetField("settingsWindow", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(inter); + settingsWindow?.GetType().GetField(windowFieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + ?.SetValue(settingsWindow, value); } _saveDalamudConfig!.Invoke(_dalamudConfig, null); @@ -166,4 +168,4 @@ public class DalamudServices return false; } } -} \ No newline at end of file +} diff --git a/Penumbra/Services/ServiceWrapper.cs b/Penumbra/Services/ServiceWrapper.cs index 403626b6..1adec97f 100644 --- a/Penumbra/Services/ServiceWrapper.cs +++ b/Penumbra/Services/ServiceWrapper.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using OtterGui.Classes; using Penumbra.Util; namespace Penumbra.Services; @@ -50,7 +49,7 @@ public abstract class AsyncServiceWrapper : IServiceWrapper { get { - _task.Wait(); + _task?.Wait(); return Service!; } } @@ -58,8 +57,8 @@ public abstract class AsyncServiceWrapper : IServiceWrapper public bool Valid => Service != null && !_isDisposed; - public event Action? FinishedCreation; - private readonly Task _task; + public event Action? FinishedCreation; + private Task? _task; private bool _isDisposed; @@ -99,6 +98,7 @@ public abstract class AsyncServiceWrapper : IServiceWrapper { Service = service; Penumbra.Log.Verbose($"[{Name}] Created."); + _task = null; FinishedCreation?.Invoke(); } }); @@ -110,6 +110,7 @@ public abstract class AsyncServiceWrapper : IServiceWrapper return; _isDisposed = true; + _task = null; if (Service is IDisposable d) d.Dispose(); Penumbra.Log.Verbose($"[{Name}] Disposed."); diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index a41dc28b..3edc0e9c 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -30,8 +30,11 @@ public static class Colors public const uint FilterActive = 0x807070FF; public const uint TutorialMarker = 0xFF20FFFF; public const uint TutorialBorder = 0xD00000FF; + public const uint ReniColorButton = 0xFFCC648D; + public const uint ReniColorHovered = 0xFFB070B0; + public const uint ReniColorActive = 0xFF9070E0; - public static (uint DefaultColor, string Name, string Description) Data( this ColorId color ) + public static (uint DefaultColor, string Name, string Description) Data(this ColorId color) => color switch { // @formatter:off @@ -53,6 +56,6 @@ public static class Colors // @formatter:on }; - public static uint Value( this ColorId color ) - => Penumbra.Config.Colors.TryGetValue( color, out var value ) ? value : color.Data().DefaultColor; -} \ No newline at end of file + public static uint Value(this ColorId color, Configuration config) + => config.Colors.TryGetValue(color, out var value) ? value : color.Data().DefaultColor; +} diff --git a/Penumbra/UI/Classes/Combos.cs b/Penumbra/UI/Classes/Combos.cs index 0f56cd77..26f747b7 100644 --- a/Penumbra/UI/Classes/Combos.cs +++ b/Penumbra/UI/Classes/Combos.cs @@ -12,34 +12,34 @@ public static class Combos => Race( label, 100, current, out race ); public static bool Race( string label, float unscaledWidth, ModelRace current, out ModelRace race ) - => ImGuiUtil.GenericEnumCombo( label, unscaledWidth * ImGuiHelpers.GlobalScale, current, out race, RaceEnumExtensions.ToName, 1 ); + => ImGuiUtil.GenericEnumCombo( label, unscaledWidth * UiHelpers.Scale, current, out race, RaceEnumExtensions.ToName, 1 ); public static bool Gender( string label, Gender current, out Gender gender ) => Gender( label, 120, current, out gender ); public static bool Gender( string label, float unscaledWidth, Gender current, out Gender gender ) - => ImGuiUtil.GenericEnumCombo( label, unscaledWidth * ImGuiHelpers.GlobalScale, current, out gender, RaceEnumExtensions.ToName, 1 ); + => ImGuiUtil.GenericEnumCombo( label, unscaledWidth * UiHelpers.Scale, current, out gender, RaceEnumExtensions.ToName, 1 ); public static bool EqdpEquipSlot( string label, EquipSlot current, out EquipSlot slot ) - => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EqdpSlots, EquipSlotExtensions.ToName ); + => ImGuiUtil.GenericEnumCombo( label, 100 * UiHelpers.Scale, current, out slot, EquipSlotExtensions.EqdpSlots, EquipSlotExtensions.ToName ); public static bool EqpEquipSlot( string label, float width, EquipSlot current, out EquipSlot slot ) - => ImGuiUtil.GenericEnumCombo( label, width * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EquipmentSlots, EquipSlotExtensions.ToName ); + => ImGuiUtil.GenericEnumCombo( label, width * UiHelpers.Scale, current, out slot, EquipSlotExtensions.EquipmentSlots, EquipSlotExtensions.ToName ); public static bool AccessorySlot( string label, EquipSlot current, out EquipSlot slot ) - => ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.AccessorySlots, EquipSlotExtensions.ToName ); + => ImGuiUtil.GenericEnumCombo( label, 100 * UiHelpers.Scale, current, out slot, EquipSlotExtensions.AccessorySlots, EquipSlotExtensions.ToName ); public static bool SubRace( string label, SubRace current, out SubRace subRace ) - => ImGuiUtil.GenericEnumCombo( label, 150 * ImGuiHelpers.GlobalScale, current, out subRace, RaceEnumExtensions.ToName, 1 ); + => ImGuiUtil.GenericEnumCombo( label, 150 * UiHelpers.Scale, current, out subRace, RaceEnumExtensions.ToName, 1 ); public static bool RspAttribute( string label, RspAttribute current, out RspAttribute attribute ) - => ImGuiUtil.GenericEnumCombo( label, 200 * ImGuiHelpers.GlobalScale, current, out attribute, + => ImGuiUtil.GenericEnumCombo( label, 200 * UiHelpers.Scale, current, out attribute, RspAttributeExtensions.ToFullString, 0, 1 ); public static bool EstSlot( string label, EstManipulation.EstType current, out EstManipulation.EstType attribute ) - => ImGuiUtil.GenericEnumCombo( label, 200 * ImGuiHelpers.GlobalScale, current, out attribute ); + => ImGuiUtil.GenericEnumCombo( label, 200 * UiHelpers.Scale, current, out attribute ); public static bool ImcType( string label, ObjectType current, out ObjectType type ) - => ImGuiUtil.GenericEnumCombo( label, 110 * ImGuiHelpers.GlobalScale, current, out type, ObjectTypeExtensions.ValidImcTypes, + => ImGuiUtil.GenericEnumCombo( label, 110 * UiHelpers.Scale, current, out type, ObjectTypeExtensions.ValidImcTypes, ObjectTypeExtensions.ToName ); } \ No newline at end of file diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index 9abeb3f5..ce3b2dd3 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -332,7 +332,7 @@ public class ItemSwapWindow : IDisposable CreateMod(); ImGui.SameLine(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * UiHelpers.Scale); ImGui.Checkbox("Use File Swaps", ref _useFileSwaps); ImGuiUtil.HoverTooltip("Instead of writing every single non-default file to the newly created mod or option,\n" + "even those available from game files, use File Swaps to default game files where possible."); @@ -356,7 +356,7 @@ public class ItemSwapWindow : IDisposable CreateOption(); ImGui.SameLine(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * UiHelpers.Scale); _dirty |= ImGui.Checkbox("Use Entire Collection", ref _useCurrentCollection); ImGuiUtil.HoverTooltip( "Use all applied mods from the Selected Collection with their current settings and respecting the enabled state of mods and inheritance,\n" @@ -415,7 +415,7 @@ public class ItemSwapWindow : IDisposable ImGui.TextUnformatted( $"Take {article1}" ); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 100 * UiHelpers.Scale ); using( var combo = ImRaii.Combo( "##fromType", _slotFrom is EquipSlot.Head ? "Hat" : _slotFrom.ToName() ) ) { if( combo ) @@ -444,7 +444,7 @@ public class ItemSwapWindow : IDisposable ImGui.TextUnformatted( $"and put {article2} on {article1}" ); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 100 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 100 * UiHelpers.Scale ); using( var combo = ImRaii.Combo( "##toType", _slotTo.ToName() ) ) { if( combo ) @@ -636,7 +636,7 @@ public class ItemSwapWindow : IDisposable ImGui.TextUnformatted(text); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(InputWidth * ImGuiHelpers.GlobalScale); + ImGui.SetNextItemWidth(InputWidth * UiHelpers.Scale); if (ImGui.InputInt("##targetId", ref _targetId, 0, 0)) _targetId = Math.Clamp(_targetId, 0, byte.MaxValue); @@ -650,7 +650,7 @@ public class ItemSwapWindow : IDisposable ImGui.TextUnformatted(text); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(InputWidth * ImGuiHelpers.GlobalScale); + ImGui.SetNextItemWidth(InputWidth * UiHelpers.Scale); if (ImGui.InputInt("##sourceId", ref _sourceId, 0, 0)) _sourceId = Math.Clamp(_sourceId, 0, byte.MaxValue); @@ -714,7 +714,7 @@ public class ItemSwapWindow : IDisposable return; ImGui.NewLine(); - DrawHeaderLine(300 * ImGuiHelpers.GlobalScale); + DrawHeaderLine(300 * UiHelpers.Scale); ImGui.NewLine(); DrawSwapBar(); diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs index 1e0b609b..1d722927 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs +++ b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs @@ -5,26 +5,27 @@ using System.Numerics; using System.Reflection; using Dalamud.Interface; using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Interface.Internal.Notifications; using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.GameData.Files; using Penumbra.Mods; -using Penumbra.Services; +using Penumbra.Services; using Penumbra.String.Classes; namespace Penumbra.UI.Classes; public partial class ModEditWindow { - private class FileEditor< T > where T : class, IWritable + private class FileEditor where T : class, IWritable { - private readonly string _tabName; - private readonly string _fileType; - private readonly Func< IReadOnlyList< Mod.Editor.FileRegistry > > _getFiles; - private readonly Func< T, bool, bool > _drawEdit; - private readonly Func< string > _getInitialPath; - private readonly Func< byte[], T? > _parseFile; + private readonly string _tabName; + private readonly string _fileType; + private readonly Func> _getFiles; + private readonly Func _drawEdit; + private readonly Func _getInitialPath; + private readonly Func _parseFile; private Mod.Editor.FileRegistry? _currentPath; private T? _currentFile; @@ -36,29 +37,28 @@ public partial class ModEditWindow private T? _defaultFile; private Exception? _defaultException; - private IReadOnlyList< Mod.Editor.FileRegistry > _list = null!; + private IReadOnlyList _list = null!; - private readonly FileDialogManager _fileDialog = ConfigWindow.SetupFileManager(); + private readonly FileDialogService _fileDialog; - public FileEditor( string tabName, string fileType, Func< IReadOnlyList< Mod.Editor.FileRegistry > > getFiles, - Func< T, bool, bool > drawEdit, Func< string > getInitialPath, Func< byte[], T? >? parseFile ) + public FileEditor(string tabName, string fileType, FileDialogService fileDialog, Func> getFiles, + Func drawEdit, Func getInitialPath, Func? parseFile) { _tabName = tabName; _fileType = fileType; _getFiles = getFiles; _drawEdit = drawEdit; _getInitialPath = getInitialPath; + _fileDialog = fileDialog; _parseFile = parseFile ?? DefaultParseFile; } public void Draw() { _list = _getFiles(); - using var tab = ImRaii.TabItem( _tabName ); - if( !tab ) - { + using var tab = ImRaii.TabItem(_tabName); + if (!tab) return; - } ImGui.NewLine(); DrawFileSelectCombo(); @@ -67,35 +67,35 @@ public partial class ModEditWindow ResetButton(); ImGui.SameLine(); DefaultInput(); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); DrawFilePanel(); } private void DefaultInput() { - using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale } ); - ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X - 3 * ImGuiHelpers.GlobalScale - ImGui.GetFrameHeight() ); - ImGui.InputTextWithHint( "##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength ); + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * UiHelpers.Scale }); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 3 * UiHelpers.Scale - ImGui.GetFrameHeight()); + ImGui.InputTextWithHint("##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength); _inInput = ImGui.IsItemActive(); - if( ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0 ) + if (ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0) { _fileDialog.Reset(); try { - var file = DalamudServices.GameData.GetFile( _defaultPath ); - if( file != null ) + var file = DalamudServices.SGameData.GetFile(_defaultPath); + if (file != null) { _defaultException = null; - _defaultFile = _parseFile( file.Data ); + _defaultFile = _parseFile(file.Data); } else { _defaultFile = null; - _defaultException = new Exception( "File does not exist." ); + _defaultException = new Exception("File does not exist."); } } - catch( Exception e ) + catch (Exception e) { _defaultFile = null; _defaultException = e; @@ -103,25 +103,23 @@ public partial class ModEditWindow } ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), "Export this file.", _defaultFile == null, true ) ) - { - _fileDialog.SaveFileDialog( $"Export {_defaultPath} to...", _fileType, Path.GetFileNameWithoutExtension( _defaultPath ), _fileType, ( success, name ) => - { - if( !success ) + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Export this file.", + _defaultFile == null, true)) + _fileDialog.OpenSavePicker($"Export {_defaultPath} to...", _fileType, Path.GetFileNameWithoutExtension(_defaultPath), _fileType, + (success, name) => { - return; - } + if (!success) + return; - try - { - File.WriteAllBytes( name, _defaultFile?.Write() ?? throw new Exception( "File invalid." ) ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not export {_defaultPath}:\n{e}" ); - } - }, _getInitialPath() ); - } + try + { + File.WriteAllBytes(name, _defaultFile?.Write() ?? throw new Exception("File invalid.")); + } + catch (Exception e) + { + Penumbra.ChatService.NotificationMessage($"Could not export {_defaultPath}:\n{e}", "Error", NotificationType.Error); + } + }, _getInitialPath(), false); _fileDialog.Draw(); } @@ -136,64 +134,58 @@ public partial class ModEditWindow private void DrawFileSelectCombo() { - ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); - using var combo = ImRaii.Combo( "##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File..." ); - if( !combo ) - { + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + using var combo = ImRaii.Combo("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File..."); + if (!combo) return; - } - foreach( var file in _list ) + foreach (var file in _list) { - if( ImGui.Selectable( file.RelPath.ToString(), ReferenceEquals( file, _currentPath ) ) ) - { - UpdateCurrentFile( file ); - } + if (ImGui.Selectable(file.RelPath.ToString(), ReferenceEquals(file, _currentPath))) + UpdateCurrentFile(file); - if( ImGui.IsItemHovered() ) + if (ImGui.IsItemHovered()) { using var tt = ImRaii.Tooltip(); - ImGui.TextUnformatted( "All Game Paths" ); + ImGui.TextUnformatted("All Game Paths"); ImGui.Separator(); - using var t = ImRaii.Table( "##Tooltip", 2, ImGuiTableFlags.SizingFixedFit ); - foreach( var (option, gamePath) in file.SubModUsage ) + using var t = ImRaii.Table("##Tooltip", 2, ImGuiTableFlags.SizingFixedFit); + foreach (var (option, gamePath) in file.SubModUsage) { ImGui.TableNextColumn(); - ConfigWindow.Text( gamePath.Path ); + UiHelpers.Text(gamePath.Path); ImGui.TableNextColumn(); - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ItemId.Value() ); - ImGui.TextUnformatted( option.FullName ); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(Penumbra.Config)); + ImGui.TextUnformatted(option.FullName); } } - if( file.SubModUsage.Count > 0 ) + if (file.SubModUsage.Count > 0) { ImGui.SameLine(); - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ItemId.Value() ); - ImGuiUtil.RightAlign( file.SubModUsage[ 0 ].Item2.Path.ToString() ); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(Penumbra.Config)); + ImGuiUtil.RightAlign(file.SubModUsage[0].Item2.Path.ToString()); } } } - private static T? DefaultParseFile( byte[] bytes ) - => Activator.CreateInstance( typeof( T ), bytes ) as T; + private static T? DefaultParseFile(byte[] bytes) + => Activator.CreateInstance(typeof(T), bytes) as T; - private void UpdateCurrentFile( Mod.Editor.FileRegistry path ) + private void UpdateCurrentFile(Mod.Editor.FileRegistry path) { - if( ReferenceEquals( _currentPath, path ) ) - { + if (ReferenceEquals(_currentPath, path)) return; - } _changed = false; _currentPath = path; _currentException = null; try { - var bytes = File.ReadAllBytes( _currentPath.File.FullName ); - _currentFile = _parseFile( bytes ); + var bytes = File.ReadAllBytes(_currentPath.File.FullName); + _currentFile = _parseFile(bytes); } - catch( Exception e ) + catch (Exception e) { _currentFile = null; _currentException = e; @@ -202,76 +194,74 @@ public partial class ModEditWindow private void SaveButton() { - if( ImGuiUtil.DrawDisabledButton( "Save to File", Vector2.Zero, - $"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed ) ) + if (ImGuiUtil.DrawDisabledButton("Save to File", Vector2.Zero, + $"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed)) { - File.WriteAllBytes( _currentPath!.File.FullName, _currentFile!.Write() ); + File.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); _changed = false; } } private void ResetButton() { - if( ImGuiUtil.DrawDisabledButton( "Reset Changes", Vector2.Zero, - $"Reset all changes made to the {_fileType} file.", !_changed ) ) + if (ImGuiUtil.DrawDisabledButton("Reset Changes", Vector2.Zero, + $"Reset all changes made to the {_fileType} file.", !_changed)) { var tmp = _currentPath; _currentPath = null; - UpdateCurrentFile( tmp! ); + UpdateCurrentFile(tmp!); } } private void DrawFilePanel() { - using var child = ImRaii.Child( "##filePanel", -Vector2.One, true ); - if( !child ) - { + using var child = ImRaii.Child("##filePanel", -Vector2.One, true); + if (!child) return; - } - if( _currentPath != null ) + if (_currentPath != null) { - if( _currentFile == null ) + if (_currentFile == null) { - ImGui.TextUnformatted( $"Could not parse selected {_fileType} file." ); - if( _currentException != null ) + ImGui.TextUnformatted($"Could not parse selected {_fileType} file."); + if (_currentException != null) { using var tab = ImRaii.PushIndent(); - ImGuiUtil.TextWrapped( _currentException.ToString() ); + ImGuiUtil.TextWrapped(_currentException.ToString()); } } else { - using var id = ImRaii.PushId( 0 ); - _changed |= _drawEdit( _currentFile, false ); + using var id = ImRaii.PushId(0); + _changed |= _drawEdit(_currentFile, false); } } - if( !_inInput && _defaultPath.Length > 0 ) + if (!_inInput && _defaultPath.Length > 0) { - if( _currentPath != null ) + if (_currentPath != null) { ImGui.NewLine(); ImGui.NewLine(); - ImGui.TextUnformatted( $"Preview of {_defaultPath}:" ); + ImGui.TextUnformatted($"Preview of {_defaultPath}:"); ImGui.Separator(); } - if( _defaultFile == null ) + if (_defaultFile == null) { - ImGui.TextUnformatted( $"Could not parse provided {_fileType} game file:\n" ); - if( _defaultException != null ) + ImGui.TextUnformatted($"Could not parse provided {_fileType} game file:\n"); + if (_defaultException != null) { using var tab = ImRaii.PushIndent(); - ImGuiUtil.TextWrapped( _defaultException.ToString() ); + ImGuiUtil.TextWrapped(_defaultException.ToString()); } } else { - using var id = ImRaii.PushId( 1 ); - _drawEdit( _defaultFile, true ); + using var id = ImRaii.PushId(1); + _drawEdit(_defaultFile, true); } } } } -} \ No newline at end of file +} diff --git a/Penumbra/UI/Classes/ModEditWindow.Files.cs b/Penumbra/UI/Classes/ModEditWindow.Files.cs index 18f17711..469b10a8 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Files.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Files.cs @@ -14,184 +14,156 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow { - private readonly HashSet< Mod.Editor.FileRegistry > _selectedFiles = new(256); - private LowerString _fileFilter = LowerString.Empty; - private bool _showGamePaths = true; - private string _gamePathEdit = string.Empty; - private int _fileIdx = -1; - private int _pathIdx = -1; - private int _folderSkip = 0; - private bool _overviewMode = false; - private LowerString _fileOverviewFilter1 = LowerString.Empty; - private LowerString _fileOverviewFilter2 = LowerString.Empty; - private LowerString _fileOverviewFilter3 = LowerString.Empty; + private readonly HashSet _selectedFiles = new(256); + private LowerString _fileFilter = LowerString.Empty; + private bool _showGamePaths = true; + private string _gamePathEdit = string.Empty; + private int _fileIdx = -1; + private int _pathIdx = -1; + private int _folderSkip = 0; + private bool _overviewMode = false; + private LowerString _fileOverviewFilter1 = LowerString.Empty; + private LowerString _fileOverviewFilter2 = LowerString.Empty; + private LowerString _fileOverviewFilter3 = LowerString.Empty; - private bool CheckFilter( Mod.Editor.FileRegistry registry ) - => _fileFilter.IsEmpty || registry.File.FullName.Contains( _fileFilter.Lower, StringComparison.OrdinalIgnoreCase ); + private bool CheckFilter(Mod.Editor.FileRegistry registry) + => _fileFilter.IsEmpty || registry.File.FullName.Contains(_fileFilter.Lower, StringComparison.OrdinalIgnoreCase); - private bool CheckFilter( (Mod.Editor.FileRegistry, int) p ) - => CheckFilter( p.Item1 ); + private bool CheckFilter((Mod.Editor.FileRegistry, int) p) + => CheckFilter(p.Item1); private void DrawFileTab() { - using var tab = ImRaii.TabItem( "File Redirections" ); - if( !tab ) - { + using var tab = ImRaii.TabItem("File Redirections"); + if (!tab) return; - } DrawOptionSelectHeader(); DrawButtonHeader(); - if( _overviewMode ) - { + if (_overviewMode) DrawFileManagementOverview(); - } else - { DrawFileManagementNormal(); - } - using var child = ImRaii.Child( "##files", -Vector2.One, true ); - if( !child ) - { + using var child = ImRaii.Child("##files", -Vector2.One, true); + if (!child) return; - } - if( _overviewMode ) - { + if (_overviewMode) DrawFilesOverviewMode(); - } else - { DrawFilesNormalMode(); - } } private void DrawFilesOverviewMode() { var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - var skips = ImGuiClip.GetNecessarySkips( height ); + var skips = ImGuiClip.GetNecessarySkips(height); - using var list = ImRaii.Table( "##table", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV, -Vector2.One ); + using var list = ImRaii.Table("##table", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV, -Vector2.One); - if( !list ) - { + if (!list) return; - } var width = ImGui.GetContentRegionAvail().X / 8; - ImGui.TableSetupColumn( "##file", ImGuiTableColumnFlags.WidthFixed, width * 3 ); - ImGui.TableSetupColumn( "##path", ImGuiTableColumnFlags.WidthFixed, width * 3 + ImGui.GetStyle().FrameBorderSize ); - ImGui.TableSetupColumn( "##option", ImGuiTableColumnFlags.WidthFixed, width * 2 ); + ImGui.TableSetupColumn("##file", ImGuiTableColumnFlags.WidthFixed, width * 3); + ImGui.TableSetupColumn("##path", ImGuiTableColumnFlags.WidthFixed, width * 3 + ImGui.GetStyle().FrameBorderSize); + ImGui.TableSetupColumn("##option", ImGuiTableColumnFlags.WidthFixed, width * 2); var idx = 0; - var files = _editor!.AvailableFiles.SelectMany( f => + var files = _editor!.AvailableFiles.SelectMany(f => { var file = f.RelPath.ToString(); return f.SubModUsage.Count == 0 - ? Enumerable.Repeat( ( file, "Unused", string.Empty, 0x40000080u ), 1 ) - : f.SubModUsage.Select( s => ( file, s.Item2.ToString(), s.Item1.FullName, - _editor.CurrentOption == s.Item1 && _mod!.HasOptions ? 0x40008000u : 0u ) ); - } ); + ? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1) + : f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.FullName, + _editor.CurrentOption == s.Item1 && _mod!.HasOptions ? 0x40008000u : 0u)); + }); - void DrawLine( (string, string, string, uint) data ) + void DrawLine((string, string, string, uint) data) { - using var id = ImRaii.PushId( idx++ ); + using var id = ImRaii.PushId(idx++); ImGui.TableNextColumn(); - if( data.Item4 != 0 ) - { - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, data.Item4 ); - } + if (data.Item4 != 0) + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, data.Item4); - ImGuiUtil.CopyOnClickSelectable( data.Item1 ); + ImGuiUtil.CopyOnClickSelectable(data.Item1); ImGui.TableNextColumn(); - if( data.Item4 != 0 ) - { - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, data.Item4 ); - } + if (data.Item4 != 0) + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, data.Item4); - ImGuiUtil.CopyOnClickSelectable( data.Item2 ); + ImGuiUtil.CopyOnClickSelectable(data.Item2); ImGui.TableNextColumn(); - if( data.Item4 != 0 ) - { - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, data.Item4 ); - } + if (data.Item4 != 0) + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, data.Item4); - ImGuiUtil.CopyOnClickSelectable( data.Item3 ); + ImGuiUtil.CopyOnClickSelectable(data.Item3); } - bool Filter( (string, string, string, uint) data ) - => _fileOverviewFilter1.IsContained( data.Item1 ) - && _fileOverviewFilter2.IsContained( data.Item2 ) - && _fileOverviewFilter3.IsContained( data.Item3 ); + bool Filter((string, string, string, uint) data) + => _fileOverviewFilter1.IsContained(data.Item1) + && _fileOverviewFilter2.IsContained(data.Item2) + && _fileOverviewFilter3.IsContained(data.Item3); - var end = ImGuiClip.FilteredClippedDraw( files, skips, Filter, DrawLine ); - ImGuiClip.DrawEndDummy( end, height ); + var end = ImGuiClip.FilteredClippedDraw(files, skips, Filter, DrawLine); + ImGuiClip.DrawEndDummy(end, height); } private void DrawFilesNormalMode() { - using var list = ImRaii.Table( "##table", 1 ); + using var list = ImRaii.Table("##table", 1); - if( !list ) - { + if (!list) return; - } - foreach( var (registry, i) in _editor!.AvailableFiles.WithIndex().Where( CheckFilter ) ) + foreach (var (registry, i) in _editor!.AvailableFiles.WithIndex().Where(CheckFilter)) { - using var id = ImRaii.PushId( i ); + using var id = ImRaii.PushId(i); ImGui.TableNextColumn(); - DrawSelectable( registry ); + DrawSelectable(registry); - if( !_showGamePaths ) - { + if (!_showGamePaths) continue; - } - using var indent = ImRaii.PushIndent( 50f ); - for( var j = 0; j < registry.SubModUsage.Count; ++j ) + using var indent = ImRaii.PushIndent(50f); + for (var j = 0; j < registry.SubModUsage.Count; ++j) { - var (subMod, gamePath) = registry.SubModUsage[ j ]; - if( subMod != _editor.CurrentOption ) - { + var (subMod, gamePath) = registry.SubModUsage[j]; + if (subMod != _editor.CurrentOption) continue; - } - PrintGamePath( i, j, registry, subMod, gamePath ); + PrintGamePath(i, j, registry, subMod, gamePath); } - PrintNewGamePath( i, registry, _editor.CurrentOption ); + PrintNewGamePath(i, registry, _editor.CurrentOption); } } - private static string DrawFileTooltip( Mod.Editor.FileRegistry registry, ColorId color ) + private static string DrawFileTooltip(Mod.Editor.FileRegistry registry, ColorId color) { (string, int) GetMulti() { - var groups = registry.SubModUsage.GroupBy( s => s.Item1 ).ToArray(); - return ( string.Join( "\n", groups.Select( g => g.Key.Name ) ), groups.Length ); + var groups = registry.SubModUsage.GroupBy(s => s.Item1).ToArray(); + return (string.Join("\n", groups.Select(g => g.Key.Name)), groups.Length); } var (text, groupCount) = color switch { - ColorId.ConflictingMod => ( string.Empty, 0 ), - ColorId.NewMod => ( registry.SubModUsage[ 0 ].Item1.Name, 1 ), + ColorId.ConflictingMod => (string.Empty, 0), + ColorId.NewMod => (registry.SubModUsage[0].Item1.Name, 1), ColorId.InheritedMod => GetMulti(), - _ => ( string.Empty, 0 ), + _ => (string.Empty, 0), }; - if( text.Length > 0 && ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( text ); - } + if (text.Length > 0 && ImGui.IsItemHovered()) + ImGui.SetTooltip(text); - return ( groupCount, registry.SubModUsage.Count ) switch + return (groupCount, registry.SubModUsage.Count) switch { (0, 0) => "(unused)", (1, 1) => "(used 1 time)", @@ -200,95 +172,91 @@ public partial class ModEditWindow }; } - private void DrawSelectable( Mod.Editor.FileRegistry registry ) + private void DrawSelectable(Mod.Editor.FileRegistry registry) { - var selected = _selectedFiles.Contains( registry ); - var color = registry.SubModUsage.Count == 0 ? ColorId.ConflictingMod : - registry.CurrentUsage == registry.SubModUsage.Count ? ColorId.NewMod : ColorId.InheritedMod; - using var c = ImRaii.PushColor( ImGuiCol.Text, color.Value() ); - if( ConfigWindow.Selectable( registry.RelPath.Path, selected ) ) + var selected = _selectedFiles.Contains(registry); + var color = registry.SubModUsage.Count == 0 ? ColorId.ConflictingMod : + registry.CurrentUsage == registry.SubModUsage.Count ? ColorId.NewMod : ColorId.InheritedMod; + using var c = ImRaii.PushColor(ImGuiCol.Text, color.Value(Penumbra.Config)); + if (UiHelpers.Selectable(registry.RelPath.Path, selected)) { - if( selected ) - { - _selectedFiles.Remove( registry ); - } + if (selected) + _selectedFiles.Remove(registry); else - { - _selectedFiles.Add( registry ); - } + _selectedFiles.Add(registry); } - var rightText = DrawFileTooltip( registry, color ); + var rightText = DrawFileTooltip(registry, color); ImGui.SameLine(); - ImGuiUtil.RightAlign( rightText ); + ImGuiUtil.RightAlign(rightText); } - private void PrintGamePath( int i, int j, Mod.Editor.FileRegistry registry, ISubMod subMod, Utf8GamePath gamePath ) + private void PrintGamePath(int i, int j, Mod.Editor.FileRegistry registry, ISubMod subMod, Utf8GamePath gamePath) { - using var id = ImRaii.PushId( j ); + using var id = ImRaii.PushId(j); ImGui.TableNextColumn(); var tmp = _fileIdx == i && _pathIdx == j ? _gamePathEdit : gamePath.ToString(); var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight(); - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength ) ) + ImGui.SetNextItemWidth(-1); + if (ImGui.InputText(string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength)) { _fileIdx = i; _pathIdx = j; _gamePathEdit = tmp; } - ImGuiUtil.HoverTooltip( "Clear completely to remove the path from this mod." ); + ImGuiUtil.HoverTooltip("Clear completely to remove the path from this mod."); - if( ImGui.IsItemDeactivatedAfterEdit() ) + if (ImGui.IsItemDeactivatedAfterEdit()) { - if( Utf8GamePath.FromString( _gamePathEdit, out var path, false ) ) - { - _editor!.SetGamePath( _fileIdx, _pathIdx, path ); - } + if (Utf8GamePath.FromString(_gamePathEdit, out var path, false)) + _editor!.SetGamePath(_fileIdx, _pathIdx, path); _fileIdx = -1; _pathIdx = -1; } - else if( _fileIdx == i && _pathIdx == j && ( !Utf8GamePath.FromString( _gamePathEdit, out var path, false ) - || !path.IsEmpty && !path.Equals( gamePath ) && !_editor!.CanAddGamePath( path )) ) + else if (_fileIdx == i + && _pathIdx == j + && (!Utf8GamePath.FromString(_gamePathEdit, out var path, false) + || !path.IsEmpty && !path.Equals(gamePath) && !_editor!.CanAddGamePath(path))) { ImGui.SameLine(); - ImGui.SetCursorPosX( pos ); - using var font = ImRaii.PushFont( UiBuilder.IconFont ); - ImGuiUtil.TextColored( 0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString() ); + ImGui.SetCursorPosX(pos); + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString()); } } - private void PrintNewGamePath( int i, Mod.Editor.FileRegistry registry, ISubMod subMod ) + private void PrintNewGamePath(int i, Mod.Editor.FileRegistry registry, ISubMod subMod) { var tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty; var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight(); - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputTextWithHint( "##new", "Add New Path...", ref tmp, Utf8GamePath.MaxGamePathLength ) ) + ImGui.SetNextItemWidth(-1); + if (ImGui.InputTextWithHint("##new", "Add New Path...", ref tmp, Utf8GamePath.MaxGamePathLength)) { _fileIdx = i; _pathIdx = -1; _gamePathEdit = tmp; } - if( ImGui.IsItemDeactivatedAfterEdit() ) + if (ImGui.IsItemDeactivatedAfterEdit()) { - if( Utf8GamePath.FromString( _gamePathEdit, out var path, false ) && !path.IsEmpty ) - { - _editor!.SetGamePath( _fileIdx, _pathIdx, path ); - } + if (Utf8GamePath.FromString(_gamePathEdit, out var path, false) && !path.IsEmpty) + _editor!.SetGamePath(_fileIdx, _pathIdx, path); _fileIdx = -1; _pathIdx = -1; } - else if( _fileIdx == i && _pathIdx == -1 && (!Utf8GamePath.FromString( _gamePathEdit, out var path, false ) - || !path.IsEmpty && !_editor!.CanAddGamePath( path )) ) + else if (_fileIdx == i + && _pathIdx == -1 + && (!Utf8GamePath.FromString(_gamePathEdit, out var path, false) + || !path.IsEmpty && !_editor!.CanAddGamePath(path))) { ImGui.SameLine(); - ImGui.SetCursorPosX( pos ); - using var font = ImRaii.PushFont( UiBuilder.IconFont ); - ImGuiUtil.TextColored( 0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString() ); + ImGui.SetCursorPosX(pos); + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString()); } } @@ -296,115 +264,97 @@ public partial class ModEditWindow { ImGui.NewLine(); - using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale, 0 ) ); - ImGui.SetNextItemWidth( 30 * ImGuiHelpers.GlobalScale ); - ImGui.DragInt( "##skippedFolders", ref _folderSkip, 0.01f, 0, 10 ); - ImGuiUtil.HoverTooltip( "Skip the first N folders when automatically constructing the game path from the file path." ); + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(3 * UiHelpers.Scale, 0)); + ImGui.SetNextItemWidth(30 * UiHelpers.Scale); + ImGui.DragInt("##skippedFolders", ref _folderSkip, 0.01f, 0, 10); + ImGuiUtil.HoverTooltip("Skip the first N folders when automatically constructing the game path from the file path."); ImGui.SameLine(); spacing.Pop(); - if( ImGui.Button( "Add Paths" ) ) - { - _editor!.AddPathsToSelected( _editor!.AvailableFiles.Where( _selectedFiles.Contains ), _folderSkip ); - } + if (ImGui.Button("Add Paths")) + _editor!.AddPathsToSelected(_editor!.AvailableFiles.Where(_selectedFiles.Contains), _folderSkip); ImGuiUtil.HoverTooltip( - "Add the file path converted to a game path to all selected files for the current option, optionally skipping the first N folders." ); + "Add the file path converted to a game path to all selected files for the current option, optionally skipping the first N folders."); ImGui.SameLine(); - if( ImGui.Button( "Remove Paths" ) ) - { - _editor!.RemovePathsFromSelected( _editor!.AvailableFiles.Where( _selectedFiles.Contains ) ); - } + if (ImGui.Button("Remove Paths")) + _editor!.RemovePathsFromSelected(_editor!.AvailableFiles.Where(_selectedFiles.Contains)); - ImGuiUtil.HoverTooltip( "Remove all game paths associated with the selected files in the current option." ); + ImGuiUtil.HoverTooltip("Remove all game paths associated with the selected files in the current option."); ImGui.SameLine(); - if( ImGui.Button( "Delete Selected Files" ) ) - { - _editor!.DeleteFiles( _editor!.AvailableFiles.Where( _selectedFiles.Contains ) ); - } + if (ImGui.Button("Delete Selected Files")) + _editor!.DeleteFiles(_editor!.AvailableFiles.Where(_selectedFiles.Contains)); ImGuiUtil.HoverTooltip( - "Delete all selected files entirely from your filesystem, but not their file associations in the mod, if there are any.\n!!!This can not be reverted!!!" ); + "Delete all selected files entirely from your filesystem, but not their file associations in the mod, if there are any.\n!!!This can not be reverted!!!"); ImGui.SameLine(); var changes = _editor!.FileChanges; var tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made."; - if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, !changes ) ) + if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes)) { var failedFiles = _editor!.ApplyFiles(); - if( failedFiles > 0 ) - { - Penumbra.Log.Information( $"Failed to apply {failedFiles} file redirections to {_editor.CurrentOption.FullName}." ); - } + if (failedFiles > 0) + Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.CurrentOption.FullName}."); } ImGui.SameLine(); var label = changes ? "Revert Changes" : "Reload Files"; - var length = new Vector2( ImGui.CalcTextSize( "Revert Changes" ).X, 0 ); - if( ImGui.Button( label, length ) ) - { + var length = new Vector2(ImGui.CalcTextSize("Revert Changes").X, 0); + if (ImGui.Button(label, length)) _editor!.RevertFiles(); - } - ImGuiUtil.HoverTooltip( "Revert all revertible changes since the last file or option reload or data refresh." ); + ImGuiUtil.HoverTooltip("Revert all revertible changes since the last file or option reload or data refresh."); ImGui.SameLine(); - ImGui.Checkbox( "Overview Mode", ref _overviewMode ); + ImGui.Checkbox("Overview Mode", ref _overviewMode); } private void DrawFileManagementNormal() { - ImGui.SetNextItemWidth( 250 * ImGuiHelpers.GlobalScale ); - LowerString.InputWithHint( "##filter", "Filter paths...", ref _fileFilter, Utf8GamePath.MaxGamePathLength ); + ImGui.SetNextItemWidth(250 * UiHelpers.Scale); + LowerString.InputWithHint("##filter", "Filter paths...", ref _fileFilter, Utf8GamePath.MaxGamePathLength); ImGui.SameLine(); - ImGui.Checkbox( "Show Game Paths", ref _showGamePaths ); + ImGui.Checkbox("Show Game Paths", ref _showGamePaths); ImGui.SameLine(); - if( ImGui.Button( "Unselect All" ) ) - { + if (ImGui.Button("Unselect All")) _selectedFiles.Clear(); - } ImGui.SameLine(); - if( ImGui.Button( "Select Visible" ) ) - { - _selectedFiles.UnionWith( _editor!.AvailableFiles.Where( CheckFilter ) ); - } + if (ImGui.Button("Select Visible")) + _selectedFiles.UnionWith(_editor!.AvailableFiles.Where(CheckFilter)); ImGui.SameLine(); - if( ImGui.Button( "Select Unused" ) ) - { - _selectedFiles.UnionWith( _editor!.AvailableFiles.Where( f => f.SubModUsage.Count == 0 ) ); - } + if (ImGui.Button("Select Unused")) + _selectedFiles.UnionWith(_editor!.AvailableFiles.Where(f => f.SubModUsage.Count == 0)); ImGui.SameLine(); - if( ImGui.Button( "Select Used Here" ) ) - { - _selectedFiles.UnionWith( _editor!.AvailableFiles.Where( f => f.CurrentUsage > 0 ) ); - } + if (ImGui.Button("Select Used Here")) + _selectedFiles.UnionWith(_editor!.AvailableFiles.Where(f => f.CurrentUsage > 0)); ImGui.SameLine(); - ImGuiUtil.RightAlign( $"{_selectedFiles.Count} / {_editor!.AvailableFiles.Count} Files Selected" ); + ImGuiUtil.RightAlign($"{_selectedFiles.Count} / {_editor!.AvailableFiles.Count} Files Selected"); } private void DrawFileManagementOverview() { - using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameRounding, 0 ) - .Push( ImGuiStyleVar.ItemSpacing, Vector2.Zero ) - .Push( ImGuiStyleVar.FrameBorderSize, ImGui.GetStyle().ChildBorderSize ); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0) + .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FrameBorderSize, ImGui.GetStyle().ChildBorderSize); var width = ImGui.GetContentRegionAvail().X / 8; - ImGui.SetNextItemWidth( width * 3 ); - LowerString.InputWithHint( "##fileFilter", "Filter file...", ref _fileOverviewFilter1, Utf8GamePath.MaxGamePathLength ); + ImGui.SetNextItemWidth(width * 3); + LowerString.InputWithHint("##fileFilter", "Filter file...", ref _fileOverviewFilter1, Utf8GamePath.MaxGamePathLength); ImGui.SameLine(); - ImGui.SetNextItemWidth( width * 3 ); - LowerString.InputWithHint( "##pathFilter", "Filter path...", ref _fileOverviewFilter2, Utf8GamePath.MaxGamePathLength ); + ImGui.SetNextItemWidth(width * 3); + LowerString.InputWithHint("##pathFilter", "Filter path...", ref _fileOverviewFilter2, Utf8GamePath.MaxGamePathLength); ImGui.SameLine(); - ImGui.SetNextItemWidth( width * 2 ); - LowerString.InputWithHint( "##optionFilter", "Filter option...", ref _fileOverviewFilter3, Utf8GamePath.MaxGamePathLength ); + ImGui.SetNextItemWidth(width * 2); + LowerString.InputWithHint("##optionFilter", "Filter option...", ref _fileOverviewFilter3, Utf8GamePath.MaxGamePathLength); } -} \ No newline at end of file +} diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs index bdfae0b8..d259e300 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.ColorSet.cs @@ -229,8 +229,8 @@ public partial class ModEditWindow var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; var hasDye = file.ColorDyeSets.Length > colorSetIdx; var dye = hasDye ? file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); - var floatSize = 70 * ImGuiHelpers.GlobalScale; - var intSize = 45 * ImGuiHelpers.GlobalScale; + var floatSize = 70 * UiHelpers.Scale; + var intSize = 45 * UiHelpers.Scale; ImGui.TableNextColumn(); ColorSetCopyClipboardButton( row, dye ); ImGui.SameLine(); diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs index 24887288..7c121ca6 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs @@ -81,7 +81,7 @@ public partial class ModEditWindow LoadedShpkPath = path; var data = LoadedShpkPath.IsRooted ? File.ReadAllBytes( LoadedShpkPath.FullName ) - : DalamudServices.GameData.GetFile( LoadedShpkPath.InternalName.ToString() )?.Data; + : DalamudServices.SGameData.GetFile( LoadedShpkPath.InternalName.ToString() )?.Data; AssociatedShpk = data?.Length > 0 ? new ShpkFile( data ) : throw new Exception( "Failure to load file data." ); LoadedShpkPathName = path.ToPath(); } @@ -100,7 +100,7 @@ public partial class ModEditWindow { var samplers = Mtrl.GetSamplersByTexture( AssociatedShpk ); TextureLabels.Clear(); - TextureLabelWidth = 50f * ImGuiHelpers.GlobalScale; + TextureLabelWidth = 50f * UiHelpers.Scale; using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) { for( var i = 0; i < Mtrl.Textures.Length; ++i ) @@ -112,7 +112,7 @@ public partial class ModEditWindow } } - TextureLabelWidth = TextureLabelWidth / ImGuiHelpers.GlobalScale + 4; + TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4; } public void UpdateShaderKeyLabels() diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs index 5238d9a0..92e4db6d 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.Shpk.cs @@ -17,36 +17,35 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow { - private readonly FileDialogManager _materialFileDialog = ConfigWindow.SetupFileManager(); + private readonly FileDialogService _fileDialog; - private bool DrawPackageNameInput( MtrlTab tab, bool disabled ) + private bool DrawPackageNameInput(MtrlTab tab, bool disabled) { var ret = false; - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputText( "Shader Package Name", ref tab.Mtrl.ShaderPackage.Name, 63, disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + if (ImGui.InputText("Shader Package Name", ref tab.Mtrl.ShaderPackage.Name, 63, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) { ret = true; tab.AssociatedShpk = null; tab.LoadedShpkPath = FullPath.Empty; } - if( ImGui.IsItemDeactivatedAfterEdit() ) - { - tab.LoadShpk( tab.FindAssociatedShpk( out _, out _ ) ); - } + if (ImGui.IsItemDeactivatedAfterEdit()) + tab.LoadShpk(tab.FindAssociatedShpk(out _, out _)); return ret; } - private static bool DrawShaderFlagsInput( MtrlFile file, bool disabled ) + private static bool DrawShaderFlagsInput(MtrlFile file, bool disabled) { var ret = false; - var shpkFlags = ( int )file.ShaderPackage.Flags; - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputInt( "Shader Package Flags", ref shpkFlags, 0, 0, - ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) + var shpkFlags = (int)file.ShaderPackage.Flags; + ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + if (ImGui.InputInt("Shader Package Flags", ref shpkFlags, 0, 0, + ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) { - file.ShaderPackage.Flags = ( uint )shpkFlags; + file.ShaderPackage.Flags = (uint)shpkFlags; ret = true; } @@ -57,86 +56,72 @@ public partial class ModEditWindow /// Show the currently associated shpk file, if any, and the buttons to associate /// a specific shpk from your drive, the modded shpk by path or the default shpk. /// - private void DrawCustomAssociations( MtrlTab tab ) + private void DrawCustomAssociations(MtrlTab tab) { var text = tab.AssociatedShpk == null ? "Associated .shpk file: None" : $"Associated .shpk file: {tab.LoadedShpkPathName}"; - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); - if( ImGui.Selectable( text ) ) - { - ImGui.SetClipboardText( tab.LoadedShpkPathName ); - } + if (ImGui.Selectable(text)) + ImGui.SetClipboardText(tab.LoadedShpkPathName); - ImGuiUtil.HoverTooltip( "Click to copy file path to clipboard." ); + ImGuiUtil.HoverTooltip("Click to copy file path to clipboard."); - if( ImGui.Button( "Associate Custom .shpk File" ) ) - { - _materialFileDialog.OpenFileDialog( "Associate Custom .shpk File...", ".shpk", ( success, name ) => + if (ImGui.Button("Associate Custom .shpk File")) + _fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) => { - if( !success ) - { - return; - } + if (success) + tab.LoadShpk(new FullPath(name[0])); + }, 1, _mod!.ModPath.FullName, false); - tab.LoadShpk( new FullPath( name ) ); - } ); - } - - var moddedPath = tab.FindAssociatedShpk( out var defaultPath, out var gamePath ); + var moddedPath = tab.FindAssociatedShpk(out var defaultPath, out var gamePath); ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), moddedPath.Equals( tab.LoadedShpkPath ) ) ) - { - tab.LoadShpk( moddedPath ); - } + if (ImGuiUtil.DrawDisabledButton("Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(), + moddedPath.Equals(tab.LoadedShpkPath))) + tab.LoadShpk(moddedPath); - if( !gamePath.Path.Equals( moddedPath.InternalName ) ) + if (!gamePath.Path.Equals(moddedPath.InternalName)) { ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Associate Unmodded .shpk File", Vector2.Zero, defaultPath, gamePath.Path.Equals( tab.LoadedShpkPath.InternalName ) ) ) - { - tab.LoadShpk( new FullPath( gamePath ) ); - } + if (ImGuiUtil.DrawDisabledButton("Associate Unmodded .shpk File", Vector2.Zero, defaultPath, + gamePath.Path.Equals(tab.LoadedShpkPath.InternalName))) + tab.LoadShpk(new FullPath(gamePath)); } - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); } - private static bool DrawShaderKey( MtrlTab tab, bool disabled, ref int idx ) + private static bool DrawShaderKey(MtrlTab tab, bool disabled, ref int idx) { var ret = false; - using var t2 = ImRaii.TreeNode( tab.ShaderKeyLabels[ idx ], disabled ? ImGuiTreeNodeFlags.Leaf : 0 ); - if( !t2 || disabled ) - { + using var t2 = ImRaii.TreeNode(tab.ShaderKeyLabels[idx], disabled ? ImGuiTreeNodeFlags.Leaf : 0); + if (!t2 || disabled) return ret; - } - var key = tab.Mtrl.ShaderPackage.ShaderKeys[ idx ]; - var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById( key.Category ); - if( shpkKey.HasValue ) + var key = tab.Mtrl.ShaderPackage.ShaderKeys[idx]; + var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category); + if (shpkKey.HasValue) { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - using var c = ImRaii.Combo( "Value", $"0x{key.Value:X8}" ); - if( c ) - { - foreach( var value in shpkKey.Value.Values ) + ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + using var c = ImRaii.Combo("Value", $"0x{key.Value:X8}"); + if (c) + foreach (var value in shpkKey.Value.Values) { - if( ImGui.Selectable( $"0x{value:X8}", value == key.Value ) ) + if (ImGui.Selectable($"0x{value:X8}", value == key.Value)) { - tab.Mtrl.ShaderPackage.ShaderKeys[ idx ].Value = value; - ret = true; + tab.Mtrl.ShaderPackage.ShaderKeys[idx].Value = value; + ret = true; tab.UpdateShaderKeyLabels(); } } - } } - if( ImGui.Button( "Remove Key" ) ) + if (ImGui.Button("Remove Key")) { - tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.RemoveItems( idx-- ); + tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.RemoveItems(idx--); ret = true; tab.UpdateShaderKeyLabels(); } @@ -144,19 +129,18 @@ public partial class ModEditWindow return ret; } - private static bool DrawNewShaderKey( MtrlTab tab ) + private static bool DrawNewShaderKey(MtrlTab tab) { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); var ret = false; - using( var c = ImRaii.Combo( "##NewConstantId", $"ID: 0x{tab.NewKeyId:X8}" ) ) + using (var c = ImRaii.Combo("##NewConstantId", $"ID: 0x{tab.NewKeyId:X8}")) { - if( c ) - { - foreach( var idx in tab.MissingShaderKeyIndices ) + if (c) + foreach (var idx in tab.MissingShaderKeyIndices) { - var key = tab.AssociatedShpk!.MaterialKeys[ idx ]; + var key = tab.AssociatedShpk!.MaterialKeys[idx]; - if( ImGui.Selectable( $"ID: 0x{key.Id:X8}", key.Id == tab.NewKeyId ) ) + if (ImGui.Selectable($"ID: 0x{key.Id:X8}", key.Id == tab.NewKeyId)) { tab.NewKeyDefault = key.DefaultValue; tab.NewKeyId = key.Id; @@ -164,17 +148,16 @@ public partial class ModEditWindow tab.UpdateShaderKeyLabels(); } } - } } ImGui.SameLine(); - if( ImGui.Button( "Add Key" ) ) + if (ImGui.Button("Add Key")) { - tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.AddItem( new ShaderKey + tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.AddItem(new ShaderKey { Category = tab.NewKeyId, Value = tab.NewKeyDefault, - } ); + }); ret = true; tab.UpdateShaderKeyLabels(); } @@ -182,70 +165,59 @@ public partial class ModEditWindow return ret; } - private static bool DrawMaterialShaderKeys( MtrlTab tab, bool disabled ) + private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled) { - if( tab.Mtrl.ShaderPackage.ShaderKeys.Length <= 0 && ( disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialKeys.Length <= 0 ) ) - { + if (tab.Mtrl.ShaderPackage.ShaderKeys.Length <= 0 + && (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialKeys.Length <= 0)) return false; - } - using var t = ImRaii.TreeNode( "Shader Keys" ); - if( !t ) - { + using var t = ImRaii.TreeNode("Shader Keys"); + if (!t) return false; - } var ret = false; - for( var idx = 0; idx < tab.Mtrl.ShaderPackage.ShaderKeys.Length; ++idx ) - { - ret |= DrawShaderKey( tab, disabled, ref idx ); - } + for (var idx = 0; idx < tab.Mtrl.ShaderPackage.ShaderKeys.Length; ++idx) + ret |= DrawShaderKey(tab, disabled, ref idx); - if( !disabled && tab.AssociatedShpk != null && tab.MissingShaderKeyIndices.Count != 0 ) - { - ret |= DrawNewShaderKey( tab ); - } + if (!disabled && tab.AssociatedShpk != null && tab.MissingShaderKeyIndices.Count != 0) + ret |= DrawNewShaderKey(tab); return ret; } - private static void DrawMaterialShaders( MtrlTab tab ) + private static void DrawMaterialShaders(MtrlTab tab) { - if( tab.AssociatedShpk == null ) - { + if (tab.AssociatedShpk == null) return; - } - ImRaii.TreeNode( tab.VertexShaders, ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( tab.PixelShaders, ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode(tab.VertexShaders, ImGuiTreeNodeFlags.Leaf).Dispose(); + ImRaii.TreeNode(tab.PixelShaders, ImGuiTreeNodeFlags.Leaf).Dispose(); } - private static bool DrawMaterialConstantValues( MtrlTab tab, bool disabled, ref int idx ) + private static bool DrawMaterialConstantValues(MtrlTab tab, bool disabled, ref int idx) { - var (name, componentOnly, paramValueOffset) = tab.MaterialConstants[ idx ]; - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - using var t2 = ImRaii.TreeNode( name ); - if( !t2 ) - { + var (name, componentOnly, paramValueOffset) = tab.MaterialConstants[idx]; + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var t2 = ImRaii.TreeNode(name); + if (!t2) return false; - } font.Dispose(); - var constant = tab.Mtrl.ShaderPackage.Constants[ idx ]; + var constant = tab.Mtrl.ShaderPackage.Constants[idx]; var ret = false; - var values = tab.Mtrl.GetConstantValues( constant ); - if( values.Length > 0 ) + var values = tab.Mtrl.GetConstantValues(constant); + if (values.Length > 0) { var valueOffset = constant.ByteOffset >> 2; - for( var valueIdx = 0; valueIdx < values.Length; ++valueIdx ) + for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx) { - var paramName = MaterialParamName( componentOnly, paramValueOffset + valueIdx ) ?? $"#{valueIdx}"; - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputFloat( $"{paramName} (at 0x{( valueOffset + valueIdx ) << 2:X4})", ref values[ valueIdx ], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + var paramName = MaterialParamName(componentOnly, paramValueOffset + valueIdx) ?? $"#{valueIdx}"; + ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + if (ImGui.InputFloat($"{paramName} (at 0x{(valueOffset + valueIdx) << 2:X4})", ref values[valueIdx], 0.0f, 0.0f, "%.3f", + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) { ret = true; tab.UpdateConstantLabels(); @@ -254,24 +226,23 @@ public partial class ModEditWindow } else { - ImRaii.TreeNode( $"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); - ImRaii.TreeNode( $"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode($"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf).Dispose(); + ImRaii.TreeNode($"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf).Dispose(); } - if( !disabled - && !tab.HasMalformedMaterialConstants - && tab.OrphanedMaterialValues.Count == 0 - && tab.AliasedMaterialValueCount == 0 - && ImGui.Button( "Remove Constant" ) ) + if (!disabled + && !tab.HasMalformedMaterialConstants + && tab.OrphanedMaterialValues.Count == 0 + && tab.AliasedMaterialValueCount == 0 + && ImGui.Button("Remove Constant")) { - tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.RemoveItems( constant.ByteOffset >> 2, constant.ByteSize >> 2 ); - tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.RemoveItems( idx-- ); - for( var i = 0; i < tab.Mtrl.ShaderPackage.Constants.Length; ++i ) + tab.Mtrl.ShaderPackage.ShaderValues = + tab.Mtrl.ShaderPackage.ShaderValues.RemoveItems(constant.ByteOffset >> 2, constant.ByteSize >> 2); + tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.RemoveItems(idx--); + for (var i = 0; i < tab.Mtrl.ShaderPackage.Constants.Length; ++i) { - if( tab.Mtrl.ShaderPackage.Constants[ i ].ByteOffset >= constant.ByteOffset ) - { - tab.Mtrl.ShaderPackage.Constants[ i ].ByteOffset -= constant.ByteSize; - } + if (tab.Mtrl.ShaderPackage.Constants[i].ByteOffset >= constant.ByteOffset) + tab.Mtrl.ShaderPackage.Constants[i].ByteOffset -= constant.ByteSize; } ret = true; @@ -281,21 +252,19 @@ public partial class ModEditWindow return ret; } - private static bool DrawMaterialOrphans( MtrlTab tab, bool disabled ) + private static bool DrawMaterialOrphans(MtrlTab tab, bool disabled) { - using var t2 = ImRaii.TreeNode( $"Orphan Values ({tab.OrphanedMaterialValues.Count})" ); - if( !t2 ) - { + using var t2 = ImRaii.TreeNode($"Orphan Values ({tab.OrphanedMaterialValues.Count})"); + if (!t2) return false; - } var ret = false; - foreach( var idx in tab.OrphanedMaterialValues ) + foreach (var idx in tab.OrphanedMaterialValues) { - ImGui.SetNextItemWidth( ImGui.GetFontSize() * 10.0f ); - if( ImGui.InputFloat( $"#{idx} (at 0x{idx << 2:X4})", - ref tab.Mtrl.ShaderPackage.ShaderValues[ idx ], 0.0f, 0.0f, "%.3f", - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) + ImGui.SetNextItemWidth(ImGui.GetFontSize() * 10.0f); + if (ImGui.InputFloat($"#{idx} (at 0x{idx << 2:X4})", + ref tab.Mtrl.ShaderPackage.ShaderValues[idx], 0.0f, 0.0f, "%.3f", + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) { ret = true; tab.UpdateConstantLabels(); @@ -305,36 +274,34 @@ public partial class ModEditWindow return ret; } - private static bool DrawNewMaterialParam( MtrlTab tab ) + private static bool DrawNewMaterialParam(MtrlTab tab) { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); - using( var font = ImRaii.PushFont( UiBuilder.MonoFont ) ) + ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f); + using (var font = ImRaii.PushFont(UiBuilder.MonoFont)) { - using var c = ImRaii.Combo( "##NewConstantId", tab.MissingMaterialConstants[ tab.NewConstantIdx ].Name ); - if( c ) - { - foreach( var (constant, idx) in tab.MissingMaterialConstants.WithIndex() ) + using var c = ImRaii.Combo("##NewConstantId", tab.MissingMaterialConstants[tab.NewConstantIdx].Name); + if (c) + foreach (var (constant, idx) in tab.MissingMaterialConstants.WithIndex()) { - if( ImGui.Selectable( constant.Name, constant.Id == tab.NewConstantId ) ) + if (ImGui.Selectable(constant.Name, constant.Id == tab.NewConstantId)) { tab.NewConstantIdx = idx; tab.NewConstantId = constant.Id; } } - } } ImGui.SameLine(); - if( ImGui.Button( "Add Constant" ) ) + if (ImGui.Button("Add Constant")) { - var (_, _, byteSize) = tab.MissingMaterialConstants[ tab.NewConstantIdx ]; - tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.AddItem( new MtrlFile.Constant + var (_, _, byteSize) = tab.MissingMaterialConstants[tab.NewConstantIdx]; + tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.AddItem(new MtrlFile.Constant { Id = tab.NewConstantId, - ByteOffset = ( ushort )( tab.Mtrl.ShaderPackage.ShaderValues.Length << 2 ), + ByteOffset = (ushort)(tab.Mtrl.ShaderPackage.ShaderValues.Length << 2), ByteSize = byteSize, - } ); - tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.AddItem( 0.0f, byteSize >> 2 ); + }); + tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.AddItem(0.0f, byteSize >> 2); tab.UpdateConstantLabels(); return true; } @@ -342,92 +309,76 @@ public partial class ModEditWindow return false; } - private static bool DrawMaterialConstants( MtrlTab tab, bool disabled ) + private static bool DrawMaterialConstants(MtrlTab tab, bool disabled) { - if( tab.Mtrl.ShaderPackage.Constants.Length == 0 - && tab.Mtrl.ShaderPackage.ShaderValues.Length == 0 - && ( disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialParams.Length == 0 ) ) - { + if (tab.Mtrl.ShaderPackage.Constants.Length == 0 + && tab.Mtrl.ShaderPackage.ShaderValues.Length == 0 + && (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialParams.Length == 0)) return false; - } - using var font = ImRaii.PushFont( UiBuilder.MonoFont ); - using var t = ImRaii.TreeNode( tab.MaterialConstantLabel ); - if( !t ) - { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var t = ImRaii.TreeNode(tab.MaterialConstantLabel); + if (!t) return false; - } font.Dispose(); var ret = false; - for( var idx = 0; idx < tab.Mtrl.ShaderPackage.Constants.Length; ++idx ) - { - ret |= DrawMaterialConstantValues( tab, disabled, ref idx ); - } + for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Constants.Length; ++idx) + ret |= DrawMaterialConstantValues(tab, disabled, ref idx); - if( tab.OrphanedMaterialValues.Count > 0 ) - { - ret |= DrawMaterialOrphans( tab, disabled ); - } - else if( !disabled && !tab.HasMalformedMaterialConstants && tab.MissingMaterialConstants.Count > 0 ) - { - ret |= DrawNewMaterialParam( tab ); - } + if (tab.OrphanedMaterialValues.Count > 0) + ret |= DrawMaterialOrphans(tab, disabled); + else if (!disabled && !tab.HasMalformedMaterialConstants && tab.MissingMaterialConstants.Count > 0) + ret |= DrawNewMaterialParam(tab); return ret; } - private static bool DrawMaterialSampler( MtrlTab tab, bool disabled, ref int idx ) + private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, ref int idx) { - var (label, filename) = tab.Samplers[ idx ]; - using var tree = ImRaii.TreeNode( label ); - if( !tree ) - { + var (label, filename) = tab.Samplers[idx]; + using var tree = ImRaii.TreeNode(label); + if (!tree) return false; - } - ImRaii.TreeNode( filename, ImGuiTreeNodeFlags.Leaf ).Dispose(); + ImRaii.TreeNode(filename, ImGuiTreeNodeFlags.Leaf).Dispose(); var ret = false; - var sampler = tab.Mtrl.ShaderPackage.Samplers[ idx ]; + var sampler = tab.Mtrl.ShaderPackage.Samplers[idx]; // FIXME this probably doesn't belong here - static unsafe bool InputHexUInt16( string label, ref ushort v, ImGuiInputTextFlags flags ) + static unsafe bool InputHexUInt16(string label, ref ushort v, ImGuiInputTextFlags flags) { - fixed( ushort* v2 = &v ) + fixed (ushort* v2 = &v) { - return ImGui.InputScalar( label, ImGuiDataType.U16, ( nint )v2, IntPtr.Zero, IntPtr.Zero, "%04X", flags ); + return ImGui.InputScalar(label, ImGuiDataType.U16, (nint)v2, IntPtr.Zero, IntPtr.Zero, "%04X", flags); } } - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( InputHexUInt16( "Texture Flags", ref tab.Mtrl.Textures[ sampler.TextureIndex ].Flags, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) - { + ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + if (InputHexUInt16("Texture Flags", ref tab.Mtrl.Textures[sampler.TextureIndex].Flags, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)) ret = true; + + var samplerFlags = (int)sampler.Flags; + ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f); + if (ImGui.InputInt("Sampler Flags", ref samplerFlags, 0, 0, + ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))) + { + tab.Mtrl.ShaderPackage.Samplers[idx].Flags = (uint)samplerFlags; + ret = true; } - var samplerFlags = ( int )sampler.Flags; - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); - if( ImGui.InputInt( "Sampler Flags", ref samplerFlags, 0, 0, - ImGuiInputTextFlags.CharsHexadecimal | ( disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) ) ) + if (!disabled + && tab.OrphanedSamplers.Count == 0 + && tab.AliasedSamplerCount == 0 + && ImGui.Button("Remove Sampler")) { - tab.Mtrl.ShaderPackage.Samplers[ idx ].Flags = ( uint )samplerFlags; - ret = true; - } - - if( !disabled - && tab.OrphanedSamplers.Count == 0 - && tab.AliasedSamplerCount == 0 - && ImGui.Button( "Remove Sampler" ) ) - { - tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems( sampler.TextureIndex ); - tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.RemoveItems( idx-- ); - for( var i = 0; i < tab.Mtrl.ShaderPackage.Samplers.Length; ++i ) + tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems(sampler.TextureIndex); + tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.RemoveItems(idx--); + for (var i = 0; i < tab.Mtrl.ShaderPackage.Samplers.Length; ++i) { - if( tab.Mtrl.ShaderPackage.Samplers[ i ].TextureIndex >= sampler.TextureIndex ) - { - --tab.Mtrl.ShaderPackage.Samplers[ i ].TextureIndex; - } + if (tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex) + --tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex; } ret = true; @@ -438,114 +389,99 @@ public partial class ModEditWindow return ret; } - private static bool DrawMaterialNewSampler( MtrlTab tab ) + private static bool DrawMaterialNewSampler(MtrlTab tab) { - var (name, id) = tab.MissingSamplers[ tab.NewSamplerIdx ]; - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 450.0f ); - using( var c = ImRaii.Combo( "##NewSamplerId", $"{name} (ID: 0x{id:X8})" ) ) + var (name, id) = tab.MissingSamplers[tab.NewSamplerIdx]; + ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f); + using (var c = ImRaii.Combo("##NewSamplerId", $"{name} (ID: 0x{id:X8})")) { - if( c ) - { - foreach( var (sampler, idx) in tab.MissingSamplers.WithIndex() ) + if (c) + foreach (var (sampler, idx) in tab.MissingSamplers.WithIndex()) { - if( ImGui.Selectable( $"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == tab.NewSamplerId ) ) + if (ImGui.Selectable($"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == tab.NewSamplerId)) { tab.NewSamplerIdx = idx; tab.NewSamplerId = sampler.Id; } } - } } ImGui.SameLine(); - if( !ImGui.Button( "Add Sampler" ) ) - { + if (!ImGui.Button("Add Sampler")) return false; - } - tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem( new Sampler + tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem(new Sampler { SamplerId = tab.NewSamplerId, - TextureIndex = ( byte )tab.Mtrl.Textures.Length, + TextureIndex = (byte)tab.Mtrl.Textures.Length, Flags = 0, - } ); - tab.Mtrl.Textures = tab.Mtrl.Textures.AddItem( new MtrlFile.Texture + }); + tab.Mtrl.Textures = tab.Mtrl.Textures.AddItem(new MtrlFile.Texture { Path = string.Empty, Flags = 0, - } ); + }); tab.UpdateSamplers(); tab.UpdateTextureLabels(); return true; } - private static bool DrawMaterialSamplers( MtrlTab tab, bool disabled ) + private static bool DrawMaterialSamplers(MtrlTab tab, bool disabled) { - if( tab.Mtrl.ShaderPackage.Samplers.Length == 0 - && tab.Mtrl.Textures.Length == 0 - && ( disabled || ( tab.AssociatedShpk?.Samplers.All( sampler => sampler.Slot != 2 ) ?? false ) ) ) - { + if (tab.Mtrl.ShaderPackage.Samplers.Length == 0 + && tab.Mtrl.Textures.Length == 0 + && (disabled || (tab.AssociatedShpk?.Samplers.All(sampler => sampler.Slot != 2) ?? false))) return false; - } - using var t = ImRaii.TreeNode( "Samplers" ); - if( !t ) - { + using var t = ImRaii.TreeNode("Samplers"); + if (!t) return false; - } var ret = false; - for( var idx = 0; idx < tab.Mtrl.ShaderPackage.Samplers.Length; ++idx ) - { - ret |= DrawMaterialSampler( tab, disabled, ref idx ); - } + for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Samplers.Length; ++idx) + ret |= DrawMaterialSampler(tab, disabled, ref idx); - if( tab.OrphanedSamplers.Count > 0 ) + if (tab.OrphanedSamplers.Count > 0) { - using var t2 = ImRaii.TreeNode( $"Orphan Textures ({tab.OrphanedSamplers.Count})" ); - if( t2 ) - { - foreach( var idx in tab.OrphanedSamplers ) + using var t2 = ImRaii.TreeNode($"Orphan Textures ({tab.OrphanedSamplers.Count})"); + if (t2) + foreach (var idx in tab.OrphanedSamplers) { - ImRaii.TreeNode( $"#{idx}: {Path.GetFileName( tab.Mtrl.Textures[ idx ].Path )} - {tab.Mtrl.Textures[ idx ].Flags:X4}", ImGuiTreeNodeFlags.Leaf ) - .Dispose(); + ImRaii.TreeNode($"#{idx}: {Path.GetFileName(tab.Mtrl.Textures[idx].Path)} - {tab.Mtrl.Textures[idx].Flags:X4}", + ImGuiTreeNodeFlags.Leaf) + .Dispose(); } - } } - else if( !disabled && tab.MissingSamplers.Count > 0 && tab.AliasedSamplerCount == 0 && tab.Mtrl.Textures.Length < 255 ) + else if (!disabled && tab.MissingSamplers.Count > 0 && tab.AliasedSamplerCount == 0 && tab.Mtrl.Textures.Length < 255) { - ret |= DrawMaterialNewSampler( tab ); + ret |= DrawMaterialNewSampler(tab); } return ret; } - private bool DrawMaterialShaderResources( MtrlTab tab, bool disabled ) + private bool DrawMaterialShaderResources(MtrlTab tab, bool disabled) { var ret = false; - if( !ImGui.CollapsingHeader( "Advanced Shader Resources" ) ) - { + if (!ImGui.CollapsingHeader("Advanced Shader Resources")) return ret; - } - ret |= DrawPackageNameInput( tab, disabled ); - ret |= DrawShaderFlagsInput( tab.Mtrl, disabled ); - DrawCustomAssociations( tab ); - ret |= DrawMaterialShaderKeys( tab, disabled ); - DrawMaterialShaders( tab ); - ret |= DrawMaterialConstants( tab, disabled ); - ret |= DrawMaterialSamplers( tab, disabled ); + ret |= DrawPackageNameInput(tab, disabled); + ret |= DrawShaderFlagsInput(tab.Mtrl, disabled); + DrawCustomAssociations(tab); + ret |= DrawMaterialShaderKeys(tab, disabled); + DrawMaterialShaders(tab); + ret |= DrawMaterialConstants(tab, disabled); + ret |= DrawMaterialSamplers(tab, disabled); return ret; } - private static string? MaterialParamName( bool componentOnly, int offset ) + private static string? MaterialParamName(bool componentOnly, int offset) { - if( offset < 0 ) - { + if (offset < 0) return null; - } - return ( componentOnly, offset & 0x3 ) switch + return (componentOnly, offset & 0x3) switch { (true, 0) => "x", (true, 1) => "y", @@ -559,10 +495,10 @@ public partial class ModEditWindow }; } - private static (string? Name, bool ComponentOnly) MaterialParamRangeName( string prefix, int valueOffset, int valueLength ) + private static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength) { - static string VectorSwizzle( int firstComponent, int lastComponent ) - => ( firstComponent, lastComponent ) switch + static string VectorSwizzle(int firstComponent, int lastComponent) + => (firstComponent, lastComponent) switch { (0, 4) => " ", (0, 0) => ".x ", @@ -578,28 +514,22 @@ public partial class ModEditWindow _ => string.Empty, }; - if( valueLength == 0 || valueOffset < 0 ) - { - return ( null, false ); - } + if (valueLength == 0 || valueOffset < 0) + return (null, false); - var firstVector = valueOffset >> 2; - var lastVector = ( valueOffset + valueLength - 1 ) >> 2; - var firstComponent = valueOffset & 0x3; - var lastComponent = ( valueOffset + valueLength - 1 ) & 0x3; - if( firstVector == lastVector ) - { - return ( $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, lastComponent )}", true ); - } + var firstVector = valueOffset >> 2; + var lastVector = (valueOffset + valueLength - 1) >> 2; + var firstComponent = valueOffset & 0x3; + var lastComponent = (valueOffset + valueLength - 1) & 0x3; + if (firstVector == lastVector) + return ($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, lastComponent)}", true); - var sb = new StringBuilder( 128 ); - sb.Append( $"{prefix}[{firstVector}]{VectorSwizzle( firstComponent, 3 ).TrimEnd()}" ); - for( var i = firstVector + 1; i < lastVector; ++i ) - { - sb.Append( $", [{i}]" ); - } + var sb = new StringBuilder(128); + sb.Append($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, 3).TrimEnd()}"); + for (var i = firstVector + 1; i < lastVector; ++i) + sb.Append($", [{i}]"); - sb.Append( $", [{lastVector}]{VectorSwizzle( 0, lastComponent )}" ); - return ( sb.ToString(), false ); + sb.Append($", [{lastVector}]{VectorSwizzle(0, lastComponent)}"); + return (sb.ToString(), false); } -} \ No newline at end of file +} diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs index 2cf5cd26..7a170803 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -29,8 +29,6 @@ public partial class ModEditWindow ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); DrawOtherMaterialDetails( tab.Mtrl, disabled ); - _materialFileDialog.Draw(); - return !disabled && ret; } @@ -39,7 +37,7 @@ public partial class ModEditWindow var ret = false; using var table = ImRaii.Table( "##Textures", 2 ); ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthStretch ); - ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale ); for( var i = 0; i < tab.Mtrl.Textures.Length; ++i ) { using var _ = ImRaii.PushId( i ); @@ -80,7 +78,7 @@ public partial class ModEditWindow ret = true; } - ImGui.SameLine( 200 * ImGuiHelpers.GlobalScale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X ); + ImGui.SameLine( 200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X ); tmp = ( file.ShaderPackage.Flags & backfaceBit ) != 0; if( ImGui.Checkbox( "Hide Backfaces", ref tmp ) ) { @@ -171,7 +169,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.TextUnformatted( info.Path.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ] ); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 400 * UiHelpers.Scale ); var tmp = info.CurrentMaterials[ 0 ]; if( ImGui.InputText( "##0", ref tmp, 64 ) ) { @@ -184,7 +182,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); + ImGui.SetNextItemWidth( 400 * UiHelpers.Scale ); tmp = info.CurrentMaterials[ i ]; if( ImGui.InputText( $"##{i}", ref tmp, 64 ) ) { diff --git a/Penumbra/UI/Classes/ModEditWindow.Meta.cs b/Penumbra/UI/Classes/ModEditWindow.Meta.cs index 66ac4a87..e1f348a5 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Meta.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Meta.cs @@ -117,7 +117,7 @@ public partial class ModEditWindow private static EqpManipulation _new = new(Eqp.DefaultEntry, EquipSlot.Head, 1); private static float IdWidth - => 100 * ImGuiHelpers.GlobalScale; + => 100 * UiHelpers.Scale; public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { @@ -154,7 +154,7 @@ public partial class ModEditWindow using var disabled = ImRaii.Disabled(); ImGui.TableNextColumn(); using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, - new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); + new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) ); foreach( var flag in Eqp.EqpAttributes[ _new.Slot ] ) { var value = defaultEntry.HasFlag( flag ); @@ -184,7 +184,7 @@ public partial class ModEditWindow // Values ImGui.TableNextColumn(); using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, - new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); + new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) ); var idx = 0; foreach( var flag in Eqp.EqpAttributes[ meta.Slot ] ) { @@ -209,7 +209,7 @@ public partial class ModEditWindow private static EqdpManipulation _new = new(EqdpEntry.Invalid, EquipSlot.Head, Gender.Male, ModelRace.Midlander, 1); private static float IdWidth - => 100 * ImGuiHelpers.GlobalScale; + => 100 * UiHelpers.Scale; public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { @@ -321,10 +321,10 @@ public partial class ModEditWindow private static ImcManipulation _new = new(EquipSlot.Head, 1, 1, new ImcEntry()); private static float IdWidth - => 80 * ImGuiHelpers.GlobalScale; + => 80 * UiHelpers.Scale; private static float SmallIdWidth - => 45 * ImGuiHelpers.GlobalScale; + => 45 * UiHelpers.Scale; // Convert throwing to null-return if the file does not exist. private static ImcEntry? GetDefault( ImcManipulation imc ) @@ -380,7 +380,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( PrimaryIdTooltip ); using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, - new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); + new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) ); ImGui.TableNextColumn(); // Equipment and accessories are slightly different imcs than other types. @@ -406,7 +406,7 @@ public partial class ModEditWindow } else { - if( IdInput( "##imcId2", 100 * ImGuiHelpers.GlobalScale, _new.SecondaryId, out var setId2, 0, ushort.MaxValue, false ) ) + if( IdInput( "##imcId2", 100 * UiHelpers.Scale, _new.SecondaryId, out var setId2, 0, ushort.MaxValue, false ) ) { _new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, setId2, _new.Variant, _new.EquipSlot, _new.Entry ).Copy( GetDefault( _new ) ?? new ImcEntry() ); @@ -435,7 +435,7 @@ public partial class ModEditWindow } else { - ImGui.Dummy( new Vector2( 70 * ImGuiHelpers.GlobalScale, 0 ) ); + ImGui.Dummy( new Vector2( 70 * UiHelpers.Scale, 0 ) ); } ImGuiUtil.HoverTooltip( VariantIdTooltip ); @@ -511,7 +511,7 @@ public partial class ModEditWindow // Values using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, - new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) ); + new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) ); ImGui.TableNextColumn(); var defaultEntry = GetDefault( meta ) ?? new ImcEntry(); if( IntDragInput( "##imcMaterialId", $"Material ID\nDefault Value: {defaultEntry.MaterialId}", SmallIdWidth, meta.Entry.MaterialId, @@ -572,7 +572,7 @@ public partial class ModEditWindow private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstManipulation.EstType.Body, 1, 0); private static float IdWidth - => 100 * ImGuiHelpers.GlobalScale; + => 100 * UiHelpers.Scale; public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { @@ -669,13 +669,13 @@ public partial class ModEditWindow private static GmpManipulation _new = new(GmpEntry.Default, 1); private static float RotationWidth - => 75 * ImGuiHelpers.GlobalScale; + => 75 * UiHelpers.Scale; private static float UnkWidth - => 50 * ImGuiHelpers.GlobalScale; + => 50 * UiHelpers.Scale; private static float IdWidth - => 100 * ImGuiHelpers.GlobalScale; + => 100 * UiHelpers.Scale; public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { @@ -787,7 +787,7 @@ public partial class ModEditWindow private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, 1f); private static float FloatWidth - => 150 * ImGuiHelpers.GlobalScale; + => 150 * UiHelpers.Scale; public static void DrawNew( Mod.Editor editor, Vector2 iconSize ) { @@ -847,7 +847,7 @@ public partial class ModEditWindow var value = meta.Entry; ImGui.SetNextItemWidth( FloatWidth ); using var color = ImRaii.PushColor( ImGuiCol.FrameBg, - def < value ? ColorId.IncreasedMetaValue.Value() : ColorId.DecreasedMetaValue.Value(), + def < value ? ColorId.IncreasedMetaValue.Value(Penumbra.Config) : ColorId.DecreasedMetaValue.Value(Penumbra.Config), def != value ); if( ImGui.DragFloat( "##rspValue", ref value, 0.001f, 0.01f, 8f ) && value is >= 0.01f and <= 8f ) { @@ -864,7 +864,7 @@ public partial class ModEditWindow { int tmp = currentId; ImGui.SetNextItemWidth( width ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, border ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border ); using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.RegexWarningBorder, border ); if( ImGui.InputInt( label, ref tmp, 0 ) ) { @@ -880,7 +880,7 @@ public partial class ModEditWindow private static bool Checkmark( string label, string tooltip, bool currentValue, bool defaultValue, out bool newValue ) { using var color = ImRaii.PushColor( ImGuiCol.FrameBg, - defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), defaultValue != currentValue ); + defaultValue ? ColorId.DecreasedMetaValue.Value(Penumbra.Config) : ColorId.IncreasedMetaValue.Value(Penumbra.Config), defaultValue != currentValue ); newValue = currentValue; ImGui.Checkbox( label, ref newValue ); ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled ); @@ -894,7 +894,7 @@ public partial class ModEditWindow { newValue = currentValue; using var color = ImRaii.PushColor( ImGuiCol.FrameBg, - defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), + defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value(Penumbra.Config) : ColorId.IncreasedMetaValue.Value(Penumbra.Config), defaultValue != currentValue ); ImGui.SetNextItemWidth( width ); if( ImGui.DragInt( label, ref newValue, speed, minValue, maxValue ) ) diff --git a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs index cef13a4c..bce1c61f 100644 --- a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs @@ -67,7 +67,7 @@ public partial class ModEditWindow }; var blob = shader.Blob; - tab.FileDialog.SaveFileDialog( $"Export {objectName} #{idx} Program Blob to...", tab.Extension, defaultName, tab.Extension, ( success, name ) => + tab.FileDialog.OpenSavePicker( $"Export {objectName} #{idx} Program Blob to...", tab.Extension, defaultName, tab.Extension, ( success, name ) => { if( !success ) { @@ -87,7 +87,7 @@ public partial class ModEditWindow Penumbra.ChatService.NotificationMessage( $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success ); - } ); + }, null, false ); } private static void DrawShaderImportButton( ShpkTab tab, string objectName, Shader[] shaders, int idx ) @@ -97,7 +97,7 @@ public partial class ModEditWindow return; } - tab.FileDialog.OpenFileDialog( $"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", ( success, name ) => + tab.FileDialog.OpenFilePicker( $"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", ( success, name ) => { if( !success ) { @@ -106,7 +106,7 @@ public partial class ModEditWindow try { - shaders[ idx ].Blob = File.ReadAllBytes( name ); + shaders[ idx ].Blob = File.ReadAllBytes(name[0] ); } catch( Exception e ) { @@ -128,7 +128,7 @@ public partial class ModEditWindow } tab.Shpk.SetChanged(); - } ); + }, 1, null, false ); } private static unsafe void DrawRawDisassembly( Shader shader ) @@ -193,7 +193,7 @@ public partial class ModEditWindow var ret = false; if( !disabled ) { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 150.0f ); + ImGui.SetNextItemWidth( UiHelpers.Scale * 150.0f ); if( ImGuiUtil.InputUInt16( $"{char.ToUpper( slotLabel[ 0 ] )}{slotLabel[ 1.. ].ToLower()}", ref resource.Slot, ImGuiInputTextFlags.None ) ) { ret = true; @@ -285,11 +285,11 @@ public partial class ModEditWindow return false; } - ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, 25 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "x", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "y", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "z", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "w", ImGuiTableColumnFlags.WidthFixed, 100 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, 25 * UiHelpers.Scale ); + ImGui.TableSetupColumn( "x", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); + ImGui.TableSetupColumn( "y", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); + ImGui.TableSetupColumn( "z", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); + ImGui.TableSetupColumn( "w", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale ); ImGui.TableHeadersRow(); var textColorStart = ImGui.GetColorU32( ImGuiCol.Text ); @@ -362,7 +362,7 @@ public partial class ModEditWindow using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing ); using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 400 ); + ImGui.SetNextItemWidth( UiHelpers.Scale * 400 ); using var c = ImRaii.Combo( "##Start", tab.Orphans[ tab.NewMaterialParamStart ].Name ); if( c ) { @@ -385,7 +385,7 @@ public partial class ModEditWindow using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing ); using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) { - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 400 ); + ImGui.SetNextItemWidth( UiHelpers.Scale * 400 ); using var c = ImRaii.Combo( "##End", tab.Orphans[ tab.NewMaterialParamEnd ].Name ); if( c ) { @@ -420,7 +420,7 @@ public partial class ModEditWindow DrawShaderPackageStartCombo( tab ); DrawShaderPackageEndCombo( tab ); - ImGui.SetNextItemWidth( ImGuiHelpers.GlobalScale * 400 ); + ImGui.SetNextItemWidth( UiHelpers.Scale * 400 ); if( ImGui.InputText( "Name", ref tab.NewMaterialParamName, 63 ) ) { tab.NewMaterialParamId = Crc32.Get( tab.NewMaterialParamName, 0xFFFFFFFFu ); @@ -429,7 +429,7 @@ public partial class ModEditWindow var tooltip = tab.UsedIds.Contains( tab.NewMaterialParamId ) ? "The ID is already in use. Please choose a different name." : string.Empty; - if( !ImGuiUtil.DrawDisabledButton( $"Add ID 0x{tab.NewMaterialParamId:X8}", new Vector2( 400 * ImGuiHelpers.GlobalScale, ImGui.GetFrameHeight() ), tooltip, + if( !ImGuiUtil.DrawDisabledButton( $"Add ID 0x{tab.NewMaterialParamId:X8}", new Vector2( 400 * UiHelpers.Scale, ImGui.GetFrameHeight() ), tooltip, tooltip.Length > 0 ) ) { return false; diff --git a/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs b/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs index 448d8e35..0bcac7a3 100644 --- a/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs +++ b/Penumbra/UI/Classes/ModEditWindow.ShpkTab.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Utility; using Lumina.Misc; using OtterGui; @@ -16,19 +15,20 @@ public partial class ModEditWindow public readonly ShpkFile Shpk; public string NewMaterialParamName = string.Empty; - public uint NewMaterialParamId = Crc32.Get( string.Empty, 0xFFFFFFFFu ); + public uint NewMaterialParamId = Crc32.Get(string.Empty, 0xFFFFFFFFu); public short NewMaterialParamStart; public short NewMaterialParamEnd; - public readonly FileDialogManager FileDialog = ConfigWindow.SetupFileManager(); + public readonly FileDialogService FileDialog; public readonly string Header; public readonly string Extension; - public ShpkTab( byte[] bytes ) + public ShpkTab(FileDialogService fileDialog, byte[] bytes) { - Shpk = new ShpkFile( bytes, true ); - Header = $"Shader Package for DirectX {( int )Shpk.DirectXVersion}"; + FileDialog = fileDialog; + Shpk = new ShpkFile(bytes, true); + Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}"; Extension = Shpk.DirectXVersion switch { ShpkFile.DxVersion.DirectX9 => ".cso", @@ -47,134 +47,130 @@ public partial class ModEditWindow } public (string Name, string Tooltip, short Index, ColorType Color)[,] Matrix = null!; - public readonly List< string > MalformedParameters = new(); - public readonly HashSet< uint > UsedIds = new(16); - public readonly List< (string Name, short Index) > Orphans = new(16); + public readonly List MalformedParameters = new(); + public readonly HashSet UsedIds = new(16); + public readonly List<(string Name, short Index)> Orphans = new(16); public void Update() { - var materialParams = Shpk.GetConstantById( ShpkFile.MaterialParamsConstantId ); - var numParameters = ( ( Shpk.MaterialParamsSize + 0xFu ) & ~0xFu ) >> 4; + var materialParams = Shpk.GetConstantById(ShpkFile.MaterialParamsConstantId); + var numParameters = ((Shpk.MaterialParamsSize + 0xFu) & ~0xFu) >> 4; Matrix = new (string Name, string Tooltip, short Index, ColorType Color)[numParameters, 4]; MalformedParameters.Clear(); UsedIds.Clear(); - foreach( var (param, idx) in Shpk.MaterialParams.WithIndex() ) + foreach (var (param, idx) in Shpk.MaterialParams.WithIndex()) { - UsedIds.Add( param.Id ); + UsedIds.Add(param.Id); var iStart = param.ByteOffset >> 4; - var jStart = ( param.ByteOffset >> 2 ) & 3; - var iEnd = ( param.ByteOffset + param.ByteSize - 1 ) >> 4; - var jEnd = ( ( param.ByteOffset + param.ByteSize - 1 ) >> 2 ) & 3; - if( ( param.ByteOffset & 0x3 ) != 0 || ( param.ByteSize & 0x3 ) != 0 ) + var jStart = (param.ByteOffset >> 2) & 3; + var iEnd = (param.ByteOffset + param.ByteSize - 1) >> 4; + var jEnd = ((param.ByteOffset + param.ByteSize - 1) >> 2) & 3; + if ((param.ByteOffset & 0x3) != 0 || (param.ByteSize & 0x3) != 0) { - MalformedParameters.Add( $"ID: 0x{param.Id:X8}, offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}" ); + MalformedParameters.Add($"ID: 0x{param.Id:X8}, offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}"); continue; } - if( iEnd >= numParameters ) + if (iEnd >= numParameters) { MalformedParameters.Add( - $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 )} (ID: 0x{param.Id:X8})" ); + $"{MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2)} (ID: 0x{param.Id:X8})"); continue; } - for( var i = iStart; i <= iEnd; ++i ) + for (var i = iStart; i <= iEnd; ++i) { var end = i == iEnd ? jEnd : 3; - for( var j = i == iStart ? jStart : 0; j <= end; ++j ) + for (var j = i == iStart ? jStart : 0; j <= end; ++j) { - var tt = $"{MaterialParamRangeName( materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2 ).Item1} (ID: 0x{param.Id:X8})"; - Matrix[ i, j ] = ( $"0x{param.Id:X8}", tt, ( short )idx, 0 ); + var tt = + $"{MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2).Item1} (ID: 0x{param.Id:X8})"; + Matrix[i, j] = ($"0x{param.Id:X8}", tt, (short)idx, 0); } } } - UpdateOrphans( materialParams ); - UpdateColors( materialParams ); + UpdateOrphans(materialParams); + UpdateColors(materialParams); } - public void UpdateOrphanStart( int orphanStart ) + public void UpdateOrphanStart(int orphanStart) { - var oldEnd = Orphans.Count > 0 ? Orphans[ NewMaterialParamEnd ].Index : -1; - UpdateOrphanStart( orphanStart, oldEnd ); + var oldEnd = Orphans.Count > 0 ? Orphans[NewMaterialParamEnd].Index : -1; + UpdateOrphanStart(orphanStart, oldEnd); } - private void UpdateOrphanStart( int orphanStart, int oldEnd ) + private void UpdateOrphanStart(int orphanStart, int oldEnd) { - var count = Math.Min( NewMaterialParamEnd - NewMaterialParamStart + orphanStart + 1, Orphans.Count ); - NewMaterialParamStart = ( short )orphanStart; - var current = Orphans[ NewMaterialParamStart ].Index; - for( var i = NewMaterialParamStart; i < count; ++i ) + var count = Math.Min(NewMaterialParamEnd - NewMaterialParamStart + orphanStart + 1, Orphans.Count); + NewMaterialParamStart = (short)orphanStart; + var current = Orphans[NewMaterialParamStart].Index; + for (var i = NewMaterialParamStart; i < count; ++i) { - var next = Orphans[ i ].Index; - if( current++ != next ) + var next = Orphans[i].Index; + if (current++ != next) { - NewMaterialParamEnd = ( short )( i - 1 ); + NewMaterialParamEnd = (short)(i - 1); return; } - if( next == oldEnd ) + if (next == oldEnd) { NewMaterialParamEnd = i; return; } } - NewMaterialParamEnd = ( short )( count - 1 ); + NewMaterialParamEnd = (short)(count - 1); } - private void UpdateOrphans( ShpkFile.Resource? materialParams ) + private void UpdateOrphans(ShpkFile.Resource? materialParams) { - var oldStart = Orphans.Count > 0 ? Orphans[ NewMaterialParamStart ].Index : -1; - var oldEnd = Orphans.Count > 0 ? Orphans[ NewMaterialParamEnd ].Index : -1; + var oldStart = Orphans.Count > 0 ? Orphans[NewMaterialParamStart].Index : -1; + var oldEnd = Orphans.Count > 0 ? Orphans[NewMaterialParamEnd].Index : -1; Orphans.Clear(); short newMaterialParamStart = 0; - for( var i = 0; i < Matrix.GetLength( 0 ); ++i ) - for( var j = 0; j < 4; ++j ) + for (var i = 0; i < Matrix.GetLength(0); ++i) { - if( !Matrix[ i, j ].Name.IsNullOrEmpty() ) + for (var j = 0; j < 4; ++j) { - continue; - } + if (!Matrix[i, j].Name.IsNullOrEmpty()) + continue; - Matrix[ i, j ] = ( "(none)", string.Empty, -1, 0 ); - var linear = ( short )( 4 * i + j ); - if( oldStart == linear ) - { - newMaterialParamStart = ( short )Orphans.Count; - } + Matrix[i, j] = ("(none)", string.Empty, -1, 0); + var linear = (short)(4 * i + j); + if (oldStart == linear) + newMaterialParamStart = (short)Orphans.Count; - Orphans.Add( ( $"{materialParams?.Name ?? string.Empty}{MaterialParamName( false, linear )}", linear ) ); + Orphans.Add(($"{materialParams?.Name ?? string.Empty}{MaterialParamName(false, linear)}", linear)); + } } - if( Orphans.Count == 0 ) - { + if (Orphans.Count == 0) return; - } - UpdateOrphanStart( newMaterialParamStart, oldEnd ); + UpdateOrphanStart(newMaterialParamStart, oldEnd); } - private void UpdateColors( ShpkFile.Resource? materialParams ) + private void UpdateColors(ShpkFile.Resource? materialParams) { var lastIndex = -1; - for( var i = 0; i < Matrix.GetLength( 0 ); ++i ) + for (var i = 0; i < Matrix.GetLength(0); ++i) { - var usedComponents = ( materialParams?.Used?[ i ] ?? DisassembledShader.VectorComponents.All ) | ( materialParams?.UsedDynamically ?? 0 ); - for( var j = 0; j < 4; ++j ) + var usedComponents = (materialParams?.Used?[i] ?? DisassembledShader.VectorComponents.All) + | (materialParams?.UsedDynamically ?? 0); + for (var j = 0; j < 4; ++j) { - var color = ( ( byte )usedComponents & ( 1 << j ) ) != 0 + var color = ((byte)usedComponents & (1 << j)) != 0 ? ColorType.Used : 0; - if( Matrix[ i, j ].Index == lastIndex || Matrix[ i, j ].Index < 0 ) - { + if (Matrix[i, j].Index == lastIndex || Matrix[i, j].Index < 0) color |= ColorType.Continuation; - } - lastIndex = Matrix[ i, j ].Index; - Matrix[ i, j ].Color = color; + lastIndex = Matrix[i, j].Index; + Matrix[i, j].Color = color; } } } @@ -185,4 +181,4 @@ public partial class ModEditWindow public byte[] Write() => Shpk.Write(); } -} \ No newline at end of file +} diff --git a/Penumbra/UI/Classes/ModEditWindow.Textures.cs b/Penumbra/UI/Classes/ModEditWindow.Textures.cs index c3ab2a14..05a878e9 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Textures.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Textures.cs @@ -2,12 +2,9 @@ using System; using System.IO; using System.Linq; using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using OtterTex; using Penumbra.Import.Textures; namespace Penumbra.UI.Classes; @@ -18,210 +15,181 @@ public partial class ModEditWindow private readonly Texture _right = new(); private readonly CombinedTexture _center; - private readonly FileDialogManager _dialogManager = ConfigWindow.SetupFileManager(); - private bool _overlayCollapsed = true; + private bool _overlayCollapsed = true; - private bool _addMipMaps = true; - private int _currentSaveAs = 0; + private bool _addMipMaps = true; + private int _currentSaveAs; private static readonly (string, string)[] SaveAsStrings = { - ( "As Is", "Save the current texture with its own format without additional conversion or compression, if possible." ), - ( "RGBA (Uncompressed)", - "Save the current texture as an uncompressed BGRA bitmap. This requires the most space but technically offers the best quality." ), - ( "BC3 (Simple Compression)", - "Save the current texture compressed via BC3/DXT5 compression. This offers a 4:1 compression ratio and is quick with acceptable quality." ), - ( "BC7 (Complex Compression)", - "Save the current texture compressed via BC7 compression. This offers a 4:1 compression ratio and has almost indistinguishable quality, but may take a while." ), + ("As Is", "Save the current texture with its own format without additional conversion or compression, if possible."), + ("RGBA (Uncompressed)", + "Save the current texture as an uncompressed BGRA bitmap. This requires the most space but technically offers the best quality."), + ("BC3 (Simple Compression)", + "Save the current texture compressed via BC3/DXT5 compression. This offers a 4:1 compression ratio and is quick with acceptable quality."), + ("BC7 (Complex Compression)", + "Save the current texture compressed via BC7 compression. This offers a 4:1 compression ratio and has almost indistinguishable quality, but may take a while."), }; - private void DrawInputChild( string label, Texture tex, Vector2 size, Vector2 imageSize ) + private void DrawInputChild(string label, Texture tex, Vector2 size, Vector2 imageSize) { - using var child = ImRaii.Child( label, size, true ); - if( !child ) - { + using var child = ImRaii.Child(label, size, true); + if (!child) return; - } - using var id = ImRaii.PushId( label ); - ImGuiUtil.DrawTextButton( label, new Vector2( -1, 0 ), ImGui.GetColorU32( ImGuiCol.FrameBg ) ); + using var id = ImRaii.PushId(label); + ImGuiUtil.DrawTextButton(label, new Vector2(-1, 0), ImGui.GetColorU32(ImGuiCol.FrameBg)); ImGui.NewLine(); - tex.PathInputBox( "##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName, - _dialogManager ); - var files = _editor!.TexFiles.SelectMany( f => f.SubModUsage.Select( p => (p.Item2.ToString(), true) ) - .Prepend( (f.File.FullName, false ))); - tex.PathSelectBox( "##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.", - files, _mod.ModPath.FullName.Length + 1 ); + tex.PathInputBox("##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName, + _fileDialog); + var files = _editor!.TexFiles.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true)) + .Prepend((f.File.FullName, false))); + tex.PathSelectBox("##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.", + files, _mod.ModPath.FullName.Length + 1); - if( tex == _left ) - { - _center.DrawMatrixInputLeft( size.X ); - } + if (tex == _left) + _center.DrawMatrixInputLeft(size.X); else - { - _center.DrawMatrixInputRight( size.X ); - } + _center.DrawMatrixInputRight(size.X); ImGui.NewLine(); - using var child2 = ImRaii.Child( "image" ); - if( child2 ) - { - tex.Draw( imageSize ); - } + using var child2 = ImRaii.Child("image"); + if (child2) + tex.Draw(imageSize); } private void SaveAsCombo() { - var (text, desc) = SaveAsStrings[ _currentSaveAs ]; - ImGui.SetNextItemWidth( -ImGui.GetFrameHeight() - ImGui.GetStyle().ItemSpacing.X ); - using var combo = ImRaii.Combo( "##format", text ); - ImGuiUtil.HoverTooltip( desc ); - if( !combo ) - { + var (text, desc) = SaveAsStrings[_currentSaveAs]; + ImGui.SetNextItemWidth(-ImGui.GetFrameHeight() - ImGui.GetStyle().ItemSpacing.X); + using var combo = ImRaii.Combo("##format", text); + ImGuiUtil.HoverTooltip(desc); + if (!combo) return; - } - foreach( var ((newText, newDesc), idx) in SaveAsStrings.WithIndex() ) + foreach (var ((newText, newDesc), idx) in SaveAsStrings.WithIndex()) { - if( ImGui.Selectable( newText, idx == _currentSaveAs ) ) - { + if (ImGui.Selectable(newText, idx == _currentSaveAs)) _currentSaveAs = idx; - } - ImGuiUtil.HoverTooltip( newDesc ); + ImGuiUtil.HoverTooltip(newDesc); } } private void MipMapInput() { - ImGui.Checkbox( "##mipMaps", ref _addMipMaps ); + ImGui.Checkbox("##mipMaps", ref _addMipMaps); ImGuiUtil.HoverTooltip( - "Add the appropriate number of MipMaps to the file." ); + "Add the appropriate number of MipMaps to the file."); } - private void DrawOutputChild( Vector2 size, Vector2 imageSize ) + private void DrawOutputChild(Vector2 size, Vector2 imageSize) { - using var child = ImRaii.Child( "Output", size, true ); - if( !child ) - { + using var child = ImRaii.Child("Output", size, true); + if (!child) return; - } - if( _center.IsLoaded ) + if (_center.IsLoaded) { SaveAsCombo(); ImGui.SameLine(); MipMapInput(); - if( ImGui.Button( "Save as TEX", -Vector2.UnitX ) ) + if (ImGui.Button("Save as TEX", -Vector2.UnitX)) { - var fileName = Path.GetFileNameWithoutExtension( _left.Path.Length > 0 ? _left.Path : _right.Path ); - _dialogManager.SaveFileDialog( "Save Texture as TEX...", ".tex", fileName, ".tex", ( a, b ) => + var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); + _fileDialog.OpenSavePicker("Save Texture as TEX...", ".tex", fileName, ".tex", (a, b) => { - if( a ) - { - _center.SaveAsTex( b, ( CombinedTexture.TextureSaveType )_currentSaveAs, _addMipMaps ); - } - }, _mod!.ModPath.FullName ); + if (a) + _center.SaveAsTex(b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + }, _mod!.ModPath.FullName, false); } - if( ImGui.Button( "Save as DDS", -Vector2.UnitX ) ) + if (ImGui.Button("Save as DDS", -Vector2.UnitX)) { - var fileName = Path.GetFileNameWithoutExtension( _right.Path.Length > 0 ? _right.Path : _left.Path ); - _dialogManager.SaveFileDialog( "Save Texture as DDS...", ".dds", fileName, ".dds", ( a, b ) => + var fileName = Path.GetFileNameWithoutExtension(_right.Path.Length > 0 ? _right.Path : _left.Path); + _fileDialog.OpenSavePicker("Save Texture as DDS...", ".dds", fileName, ".dds", (a, b) => { - if( a ) - { - _center.SaveAsDds( b, ( CombinedTexture.TextureSaveType )_currentSaveAs, _addMipMaps ); - } - }, _mod!.ModPath.FullName ); + if (a) + _center.SaveAsDds(b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + }, _mod!.ModPath.FullName, false); } ImGui.NewLine(); - if( ImGui.Button( "Save as PNG", -Vector2.UnitX ) ) + if (ImGui.Button("Save as PNG", -Vector2.UnitX)) { - var fileName = Path.GetFileNameWithoutExtension( _right.Path.Length > 0 ? _right.Path : _left.Path ); - _dialogManager.SaveFileDialog( "Save Texture as PNG...", ".png", fileName, ".png", ( a, b ) => + var fileName = Path.GetFileNameWithoutExtension(_right.Path.Length > 0 ? _right.Path : _left.Path); + _fileDialog.OpenSavePicker("Save Texture as PNG...", ".png", fileName, ".png", (a, b) => { - if( a ) - { - _center.SaveAsPng( b ); - } - }, _mod!.ModPath.FullName ); + if (a) + _center.SaveAsPng(b); + }, _mod!.ModPath.FullName, false); } ImGui.NewLine(); } - if( _center.SaveException != null ) + if (_center.SaveException != null) { - ImGui.TextUnformatted( "Could not save file:" ); - using var color = ImRaii.PushColor( ImGuiCol.Text, 0xFF0000FF ); - ImGuiUtil.TextWrapped( _center.SaveException.ToString() ); + ImGui.TextUnformatted("Could not save file:"); + using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF); + ImGuiUtil.TextWrapped(_center.SaveException.ToString()); } - using var child2 = ImRaii.Child( "image" ); - if( child2 ) - { - _center.Draw( imageSize ); - } + using var child2 = ImRaii.Child("image"); + if (child2) + _center.Draw(imageSize); } private Vector2 GetChildWidth() { var windowWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - ImGui.GetTextLineHeight(); - if( _overlayCollapsed ) + if (_overlayCollapsed) { var width = windowWidth - ImGui.GetStyle().FramePadding.X * 3; - return new Vector2( width / 2, -1 ); + return new Vector2(width / 2, -1); } - return new Vector2( ( windowWidth - ImGui.GetStyle().FramePadding.X * 5 ) / 3, -1 ); + return new Vector2((windowWidth - ImGui.GetStyle().FramePadding.X * 5) / 3, -1); } private void DrawTextureTab() { - _dialogManager.Draw(); - - using var tab = ImRaii.TabItem( "Texture Import/Export" ); - if( !tab ) - { + using var tab = ImRaii.TabItem("Texture Import/Export"); + if (!tab) return; - } try { var childWidth = GetChildWidth(); - var imageSize = new Vector2( childWidth.X - ImGui.GetStyle().FramePadding.X * 2 ); - DrawInputChild( "Input Texture", _left, childWidth, imageSize ); + var imageSize = new Vector2(childWidth.X - ImGui.GetStyle().FramePadding.X * 2); + DrawInputChild("Input Texture", _left, childWidth, imageSize); ImGui.SameLine(); - DrawOutputChild( childWidth, imageSize ); - if( !_overlayCollapsed ) + DrawOutputChild(childWidth, imageSize); + if (!_overlayCollapsed) { ImGui.SameLine(); - DrawInputChild( "Overlay Texture", _right, childWidth, imageSize ); + DrawInputChild("Overlay Texture", _right, childWidth, imageSize); } ImGui.SameLine(); DrawOverlayCollapseButton(); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Unknown Error while drawing textures:\n{e}" ); + Penumbra.Log.Error($"Unknown Error while drawing textures:\n{e}"); } } private void DrawOverlayCollapseButton() { var (label, tooltip) = _overlayCollapsed - ? ( ">", "Show a third panel in which you can import an additional texture as an overlay for the primary texture." ) - : ( "<", "Hide the overlay texture panel and clear the currently loaded overlay texture, if any." ); - if( ImGui.Button( label, new Vector2( ImGui.GetTextLineHeight(), ImGui.GetContentRegionAvail().Y ) ) ) - { + ? (">", "Show a third panel in which you can import an additional texture as an overlay for the primary texture.") + : ("<", "Hide the overlay texture panel and clear the currently loaded overlay texture, if any."); + if (ImGui.Button(label, new Vector2(ImGui.GetTextLineHeight(), ImGui.GetContentRegionAvail().Y))) _overlayCollapsed = !_overlayCollapsed; - } - ImGuiUtil.HoverTooltip( tooltip ); + ImGuiUtil.HoverTooltip(tooltip); } -} \ No newline at end of file +} diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 7c4946a0..d6ad92db 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -477,21 +477,22 @@ public partial class ModEditWindow : Window, IDisposable return new FullPath(path); } - public ModEditWindow(CommunicatorService communicator) + public ModEditWindow(CommunicatorService communicator, FileDialogService fileDialog) : base(WindowBaseLabel) { + _fileDialog = fileDialog; _swapWindow = new ItemSwapWindow(communicator); - _materialTab = new FileEditor("Materials", ".mtrl", + _materialTab = new FileEditor("Materials", ".mtrl", _fileDialog, () => _editor?.MtrlFiles ?? Array.Empty(), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, bytes => new MtrlTab(this, new MtrlFile(bytes))); - _modelTab = new FileEditor("Models", ".mdl", + _modelTab = new FileEditor("Models", ".mdl", _fileDialog, () => _editor?.MdlFiles ?? Array.Empty(), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, null); - _shaderPackageTab = new FileEditor("Shader Packages", ".shpk", + _shaderPackageTab = new FileEditor("Shader Packages", ".shpk", _fileDialog, () => _editor?.ShpkFiles ?? Array.Empty(), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs deleted file mode 100644 index 93a3e9cd..00000000 --- a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs +++ /dev/null @@ -1,313 +0,0 @@ -using System; -using System.Linq; -using System.Numerics; -using System.Runtime.InteropServices; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Filesystem; -using OtterGui.Raii; -using Penumbra.Collections; -using Penumbra.Mods; - -namespace Penumbra.UI.Classes; - -public partial class ModFileSystemSelector -{ - [StructLayout( LayoutKind.Sequential, Pack = 1 )] - public struct ModState - { - public ColorId Color; - } - - private const StringComparison IgnoreCase = StringComparison.OrdinalIgnoreCase; - private LowerString _modFilter = LowerString.Empty; - private int _filterType = -1; - private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; - - private void SetFilterTooltip() - { - FilterTooltip = "Filter mods for those where their full paths or names contain the given substring.\n" - + "Enter c:[string] to filter for mods changing specific items.\n" - + "Enter t:[string] to filter for mods set to specific tags.\n" - + "Enter n:[string] to filter only for mod names and no paths.\n" - + "Enter a:[string] to filter for mods by specific authors."; - } - - // Appropriately identify and set the string filter and its type. - protected override bool ChangeFilter( string filterValue ) - { - ( _modFilter, _filterType ) = filterValue.Length switch - { - 0 => ( LowerString.Empty, -1 ), - > 1 when filterValue[ 1 ] == ':' => - filterValue[ 0 ] switch - { - 'n' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 1 ), - 'N' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 1 ), - 'a' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 2 ), - 'A' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 2 ), - 'c' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 3 ), - 'C' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 3 ), - 't' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 4 ), - 'T' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 4 ), - _ => ( new LowerString( filterValue ), 0 ), - }, - _ => ( new LowerString( filterValue ), 0 ), - }; - - return true; - } - - // Check the state filter for a specific pair of has/has-not flags. - // Uses count == 0 to check for has-not and count != 0 for has. - // Returns true if it should be filtered and false if not. - private bool CheckFlags( int count, ModFilter hasNoFlag, ModFilter hasFlag ) - { - return count switch - { - 0 when _stateFilter.HasFlag( hasNoFlag ) => false, - 0 => true, - _ when _stateFilter.HasFlag( hasFlag ) => false, - _ => true, - }; - } - - // The overwritten filter method also computes the state. - // Folders have default state and are filtered out on the direct string instead of the other options. - // If any filter is set, they should be hidden by default unless their children are visible, - // or they contain the path search string. - protected override bool ApplyFiltersAndState( FileSystem< Mod >.IPath path, out ModState state ) - { - if( path is ModFileSystem.Folder f ) - { - state = default; - return ModFilterExtensions.UnfilteredStateMods != _stateFilter - || FilterValue.Length > 0 && !f.FullName().Contains( FilterValue, IgnoreCase ); - } - - return ApplyFiltersAndState( ( ModFileSystem.Leaf )path, out state ); - } - - // Apply the string filters. - private bool ApplyStringFilters( ModFileSystem.Leaf leaf, Mod mod ) - { - return _filterType switch - { - -1 => false, - 0 => !( leaf.FullName().Contains( _modFilter.Lower, IgnoreCase ) || mod.Name.Contains( _modFilter ) ), - 1 => !mod.Name.Contains( _modFilter ), - 2 => !mod.Author.Contains( _modFilter ), - 3 => !mod.LowerChangedItemsString.Contains( _modFilter.Lower ), - 4 => !mod.AllTagsLower.Contains( _modFilter.Lower ), - _ => false, // Should never happen - }; - } - - // Only get the text color for a mod if no filters are set. - private static ColorId GetTextColor( Mod mod, ModSettings? settings, ModCollection collection ) - { - if( Penumbra.ModManager.NewMods.Contains( mod ) ) - { - return ColorId.NewMod; - } - - if( settings == null ) - { - return ColorId.UndefinedMod; - } - - if( !settings.Enabled ) - { - return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedDisabledMod : ColorId.DisabledMod; - } - - var conflicts = Penumbra.CollectionManager.Current.Conflicts( mod ); - if( conflicts.Count == 0 ) - { - return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedMod : ColorId.EnabledMod; - } - - return conflicts.Any( c => !c.Solved ) - ? ColorId.ConflictingMod - : ColorId.HandledConflictMod; - } - - private bool CheckStateFilters( Mod mod, ModSettings? settings, ModCollection collection, ref ModState state ) - { - var isNew = Penumbra.ModManager.NewMods.Contains( mod ); - // Handle mod details. - if( CheckFlags( mod.TotalFileCount, ModFilter.HasNoFiles, ModFilter.HasFiles ) - || CheckFlags( mod.TotalSwapCount, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps ) - || CheckFlags( mod.TotalManipulations, ModFilter.HasNoMetaManipulations, ModFilter.HasMetaManipulations ) - || CheckFlags( mod.HasOptions ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig ) - || CheckFlags( isNew ? 1 : 0, ModFilter.NotNew, ModFilter.IsNew ) ) - { - return true; - } - - // Handle Favoritism - if( !_stateFilter.HasFlag( ModFilter.Favorite ) && mod.Favorite - || !_stateFilter.HasFlag( ModFilter.NotFavorite ) && !mod.Favorite ) - { - return true; - } - - // Handle Inheritance - if( collection == Penumbra.CollectionManager.Current ) - { - if( !_stateFilter.HasFlag( ModFilter.Uninherited ) ) - { - return true; - } - } - else - { - state.Color = ColorId.InheritedMod; - if( !_stateFilter.HasFlag( ModFilter.Inherited ) ) - { - return true; - } - } - - // Handle settings. - if( settings == null ) - { - state.Color = ColorId.UndefinedMod; - if( !_stateFilter.HasFlag( ModFilter.Undefined ) - || !_stateFilter.HasFlag( ModFilter.Disabled ) - || !_stateFilter.HasFlag( ModFilter.NoConflict ) ) - { - return true; - } - } - else if( !settings.Enabled ) - { - state.Color = collection == Penumbra.CollectionManager.Current ? ColorId.DisabledMod : ColorId.InheritedDisabledMod; - if( !_stateFilter.HasFlag( ModFilter.Disabled ) - || !_stateFilter.HasFlag( ModFilter.NoConflict ) ) - { - return true; - } - } - else - { - if( !_stateFilter.HasFlag( ModFilter.Enabled ) ) - { - return true; - } - - // Conflicts can only be relevant if the mod is enabled. - var conflicts = Penumbra.CollectionManager.Current.Conflicts( mod ); - if( conflicts.Count > 0 ) - { - if( conflicts.Any( c => !c.Solved ) ) - { - if( !_stateFilter.HasFlag( ModFilter.UnsolvedConflict ) ) - { - return true; - } - - state.Color = ColorId.ConflictingMod; - } - else - { - if( !_stateFilter.HasFlag( ModFilter.SolvedConflict ) ) - { - return true; - } - - state.Color = ColorId.HandledConflictMod; - } - } - else if( !_stateFilter.HasFlag( ModFilter.NoConflict ) ) - { - return true; - } - } - - // isNew color takes precedence before other colors. - if( isNew ) - { - state.Color = ColorId.NewMod; - } - - return false; - } - - // Combined wrapper for handling all filters and setting state. - private bool ApplyFiltersAndState( ModFileSystem.Leaf leaf, out ModState state ) - { - state = new ModState { Color = ColorId.EnabledMod }; - var mod = leaf.Value; - var (settings, collection) = Penumbra.CollectionManager.Current[ mod.Index ]; - - if( ApplyStringFilters( leaf, mod ) ) - { - return true; - } - - if( _stateFilter != ModFilterExtensions.UnfilteredStateMods ) - { - return CheckStateFilters( mod, settings, collection, ref state ); - } - - state.Color = GetTextColor( mod, settings, collection ); - return false; - } - - private void DrawFilterCombo( ref bool everything ) - { - using var combo = ImRaii.Combo( "##filterCombo", string.Empty, - ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest ); - if( combo ) - { - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, - ImGui.GetStyle().ItemSpacing with { Y = 3 * ImGuiHelpers.GlobalScale } ); - var flags = ( int )_stateFilter; - - - if( ImGui.Checkbox( "Everything", ref everything ) ) - { - _stateFilter = everything ? ModFilterExtensions.UnfilteredStateMods : 0; - SetFilterDirty(); - } - - ImGui.Dummy( new Vector2( 0, 5 * ImGuiHelpers.GlobalScale ) ); - foreach( ModFilter flag in Enum.GetValues( typeof( ModFilter ) ) ) - { - if( ImGui.CheckboxFlags( flag.ToName(), ref flags, ( int )flag ) ) - { - _stateFilter = ( ModFilter )flags; - SetFilterDirty(); - } - } - } - } - - // Add the state filter combo-button to the right of the filter box. - protected override float CustomFilters( float width ) - { - var pos = ImGui.GetCursorPos(); - var remainingWidth = width - ImGui.GetFrameHeight(); - var comboPos = new Vector2( pos.X + remainingWidth, pos.Y ); - - var everything = _stateFilter == ModFilterExtensions.UnfilteredStateMods; - - ImGui.SetCursorPos( comboPos ); - // Draw combo button - using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.FilterActive, !everything ); - DrawFilterCombo( ref everything ); - ConfigWindow.OpenTutorial( ConfigWindow.BasicTutorialSteps.ModFilters ); - if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) - { - _stateFilter = ModFilterExtensions.UnfilteredStateMods; - SetFilterDirty(); - } - - ImGuiUtil.HoverTooltip( "Filter mods for their activation status.\nRight-Click to clear all filters." ); - ImGui.SetCursorPos( pos ); - return remainingWidth; - } -} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs deleted file mode 100644 index c9ba7b13..00000000 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ /dev/null @@ -1,478 +0,0 @@ -using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; -using ImGuiNET; -using OtterGui; -using OtterGui.Filesystem; -using OtterGui.FileSystem.Selector; -using OtterGui.Raii; -using Penumbra.Collections; -using Penumbra.Import; -using Penumbra.Mods; -using System; -using System.Collections.Concurrent; -using System.IO; -using System.Linq; -using System.Numerics; -using Penumbra.Api.Enums; -using Penumbra.Services; - -namespace Penumbra.UI.Classes; - -public sealed partial class ModFileSystemSelector : FileSystemSelector -{ - private readonly CommunicatorService _communicator; - private readonly FileDialogManager _fileManager = ConfigWindow.SetupFileManager(); - private TexToolsImporter? _import; - public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; - public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; - - public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem) - : base(fileSystem, DalamudServices.KeyState) - { - _communicator = communicator; - SubscribeRightClickFolder(EnableDescendants, 10); - SubscribeRightClickFolder(DisableDescendants, 10); - SubscribeRightClickFolder(InheritDescendants, 15); - SubscribeRightClickFolder(OwnDescendants, 15); - SubscribeRightClickFolder(SetDefaultImportFolder, 100); - SubscribeRightClickLeaf(ToggleLeafFavorite, 0); - SubscribeRightClickMain(ClearDefaultImportFolder, 100); - AddButton(AddNewModButton, 0); - AddButton(AddImportModButton, 1); - AddButton(AddHelpButton, 2); - AddButton(DeleteModButton, 1000); - SetFilterTooltip(); - - SelectionChanged += OnSelectionChange; - _communicator.CollectionChange.Event += OnCollectionChange; - Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; - Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange; - Penumbra.ModManager.ModDataChanged += OnModDataChange; - Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection; - Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection; - OnCollectionChange(CollectionType.Current, null, Penumbra.CollectionManager.Current, ""); - } - - public override void Dispose() - { - base.Dispose(); - Penumbra.ModManager.ModDiscoveryStarted -= StoreCurrentSelection; - Penumbra.ModManager.ModDiscoveryFinished -= RestoreLastSelection; - Penumbra.ModManager.ModDataChanged -= OnModDataChange; - Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; - Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange; - _communicator.CollectionChange.Event -= OnCollectionChange; - _import?.Dispose(); - _import = null; - } - - public new ModFileSystem.Leaf? SelectedLeaf - => base.SelectedLeaf; - - // Customization points. - public override ISortMode SortMode - => Penumbra.Config.SortMode; - - protected override uint ExpandedFolderColor - => ColorId.FolderExpanded.Value(); - - protected override uint CollapsedFolderColor - => ColorId.FolderCollapsed.Value(); - - protected override uint FolderLineColor - => ColorId.FolderLine.Value(); - - protected override bool FoldersDefaultOpen - => Penumbra.Config.OpenFoldersByDefault; - - protected override void DrawPopups() - { - _fileManager.Draw(); - DrawHelpPopup(); - DrawInfoPopup(); - - if (ImGuiUtil.OpenNameField("Create New Mod", ref _newModName)) - try - { - var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName); - Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty); - Mod.Creator.CreateDefaultFiles(newDir); - Penumbra.ModManager.AddMod(newDir); - _newModName = string.Empty; - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not create directory for new Mod {_newModName}:\n{e}"); - } - - while (_modsToAdd.TryDequeue(out var dir)) - { - Penumbra.ModManager.AddMod(dir); - var mod = Penumbra.ModManager.LastOrDefault(); - if (mod != null) - { - MoveModToDefaultDirectory(mod); - SelectByValue(mod); - } - } - } - - protected override void DrawLeafName(FileSystem.Leaf leaf, in ModState state, bool selected) - { - var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; - using var c = ImRaii.PushColor(ImGuiCol.Text, state.Color.Value()) - .Push(ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite); - using var id = ImRaii.PushId(leaf.Value.Index); - ImRaii.TreeNode(leaf.Value.Name, flags).Dispose(); - } - - - // Add custom context menu items. - private static void EnableDescendants(ModFileSystem.Folder folder) - { - if (ImGui.MenuItem("Enable Descendants")) - SetDescendants(folder, true); - } - - private static void DisableDescendants(ModFileSystem.Folder folder) - { - if (ImGui.MenuItem("Disable Descendants")) - SetDescendants(folder, false); - } - - private static void InheritDescendants(ModFileSystem.Folder folder) - { - if (ImGui.MenuItem("Inherit Descendants")) - SetDescendants(folder, true, true); - } - - private static void OwnDescendants(ModFileSystem.Folder folder) - { - if (ImGui.MenuItem("Stop Inheriting Descendants")) - SetDescendants(folder, false, true); - } - - private static void ToggleLeafFavorite(FileSystem.Leaf mod) - { - if (ImGui.MenuItem(mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite")) - Penumbra.ModManager.ChangeModFavorite(mod.Value.Index, !mod.Value.Favorite); - } - - private static void SetDefaultImportFolder(ModFileSystem.Folder folder) - { - if (ImGui.MenuItem("Set As Default Import Folder")) - { - var newName = folder.FullName(); - if (newName != Penumbra.Config.DefaultImportFolder) - { - Penumbra.Config.DefaultImportFolder = newName; - Penumbra.Config.Save(); - } - } - } - - private static void ClearDefaultImportFolder() - { - if (ImGui.MenuItem("Clear Default Import Folder") && Penumbra.Config.DefaultImportFolder.Length > 0) - { - Penumbra.Config.DefaultImportFolder = string.Empty; - Penumbra.Config.Save(); - } - } - - - // Add custom buttons. - private string _newModName = string.Empty; - - private static void AddNewModButton(Vector2 size) - { - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", - !Penumbra.ModManager.Valid, true)) - ImGui.OpenPopup("Create New Mod"); - } - - // Add an import mods button that opens a file selector. - // Only set the initial directory once. - private bool _hasSetFolder; - - private void AddImportModButton(Vector2 size) - { - var button = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), size, - "Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files.", !Penumbra.ModManager.Valid, true); - ConfigWindow.OpenTutorial(ConfigWindow.BasicTutorialSteps.ModImport); - if (!button) - return; - - var modPath = _hasSetFolder && !Penumbra.Config.AlwaysOpenDefaultImport ? null - : Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath - : Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null; - _hasSetFolder = true; - - _fileManager.OpenFileDialog("Import Mod Pack", - "Mod Packs{.ttmp,.ttmp2,.pmp},TexTools Mod Packs{.ttmp,.ttmp2},Penumbra Mod Packs{.pmp},Archives{.zip,.7z,.rar}", (s, f) => - { - if (s) - { - _import = new TexToolsImporter(Penumbra.ModManager.BasePath, f.Count, f.Select(file => new FileInfo(file)), - AddNewMod); - ImGui.OpenPopup("Import Status"); - } - }, 0, modPath); - } - - // Draw the progress information for import. - private void DrawInfoPopup() - { - var display = ImGui.GetIO().DisplaySize; - var height = Math.Max(display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing()); - var width = display.X / 8; - var size = new Vector2(width * 2, height); - ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2); - ImGui.SetNextWindowSize(size); - using var popup = ImRaii.Popup("Import Status", ImGuiWindowFlags.Modal); - if (_import == null || !popup.Success) - return; - - using (var child = ImRaii.Child("##import", new Vector2(-1, size.Y - ImGui.GetFrameHeight() * 2))) - { - if (child) - _import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight())); - } - - if (_import.State == ImporterState.Done && ImGui.Button("Close", -Vector2.UnitX) - || _import.State != ImporterState.Done && _import.DrawCancelButton(-Vector2.UnitX)) - { - _import?.Dispose(); - _import = null; - ImGui.CloseCurrentPopup(); - } - } - - // Mods need to be added thread-safely outside of iteration. - private readonly ConcurrentQueue _modsToAdd = new(); - - // Clean up invalid directory if necessary. - // Add successfully extracted mods. - private void AddNewMod(FileInfo file, DirectoryInfo? dir, Exception? error) - { - if (error != null) - { - if (dir != null && Directory.Exists(dir.FullName)) - try - { - Directory.Delete(dir.FullName, true); - } - catch (Exception e) - { - Penumbra.Log.Error($"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}"); - } - - if (error is not OperationCanceledException) - Penumbra.Log.Error($"Error extracting {file.FullName}, mod skipped:\n{error}"); - } - else if (dir != null) - { - _modsToAdd.Enqueue(dir); - } - } - - private void DeleteModButton(Vector2 size) - { - var keys = Penumbra.Config.DeleteModModifier.IsActive(); - var tt = SelectedLeaf == null - ? "No mod selected." - : "Delete the currently selected mod entirely from your drive.\n" - + "This can not be undone."; - if (!keys) - tt += $"\nHold {Penumbra.Config.DeleteModModifier} while clicking to delete the mod."; - - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true) - && Selected != null) - Penumbra.ModManager.DeleteMod(Selected.Index); - } - - private static void AddHelpButton(Vector2 size) - { - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.QuestionCircle.ToIconString(), size, "Open extended help.", false, true)) - ImGui.OpenPopup("ExtendedHelp"); - - ConfigWindow.OpenTutorial(ConfigWindow.BasicTutorialSteps.AdvancedHelp); - } - - // Helpers. - private static void SetDescendants(ModFileSystem.Folder folder, bool enabled, bool inherit = false) - { - var mods = folder.GetAllDescendants(ISortMode.Lexicographical).OfType().Select(l => - { - // Any mod handled here should not stay new. - Penumbra.ModManager.NewMods.Remove(l.Value); - return l.Value; - }); - - if (inherit) - Penumbra.CollectionManager.Current.SetMultipleModInheritances(mods, enabled); - else - Penumbra.CollectionManager.Current.SetMultipleModStates(mods, enabled); - } - - // Automatic cache update functions. - private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited) - { - // TODO: maybe make more efficient - SetFilterDirty(); - if (modIdx == Selected?.Index) - OnSelectionChange(Selected, Selected, default); - } - - private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName) - { - switch (type) - { - case ModDataChangeType.Name: - case ModDataChangeType.Author: - case ModDataChangeType.ModTags: - case ModDataChangeType.LocalTags: - case ModDataChangeType.Favorite: - SetFilterDirty(); - break; - } - } - - private void OnInheritanceChange(bool _) - { - SetFilterDirty(); - OnSelectionChange(Selected, Selected, default); - } - - private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _) - { - if (collectionType != CollectionType.Current || oldCollection == newCollection) - return; - - if (oldCollection != null) - { - oldCollection.ModSettingChanged -= OnSettingChange; - oldCollection.InheritanceChanged -= OnInheritanceChange; - } - - if (newCollection != null) - { - newCollection.ModSettingChanged += OnSettingChange; - newCollection.InheritanceChanged += OnInheritanceChange; - } - - SetFilterDirty(); - OnSelectionChange(Selected, Selected, default); - } - - private void OnSelectionChange(Mod? _1, Mod? newSelection, in ModState _2) - { - if (newSelection == null) - { - SelectedSettings = ModSettings.Empty; - SelectedSettingCollection = ModCollection.Empty; - } - else - { - (var settings, SelectedSettingCollection) = Penumbra.CollectionManager.Current[newSelection.Index]; - SelectedSettings = settings ?? ModSettings.Empty; - } - } - - // Keep selections across rediscoveries if possible. - private string _lastSelectedDirectory = string.Empty; - - private void StoreCurrentSelection() - { - _lastSelectedDirectory = Selected?.ModPath.FullName ?? string.Empty; - ClearSelection(); - } - - private void RestoreLastSelection() - { - if (_lastSelectedDirectory.Length > 0) - { - var leaf = (ModFileSystem.Leaf?)FileSystem.Root.GetAllDescendants(ISortMode.Lexicographical) - .FirstOrDefault(l => l is ModFileSystem.Leaf m && m.Value.ModPath.FullName == _lastSelectedDirectory); - Select(leaf); - _lastSelectedDirectory = string.Empty; - } - } - - // If a default import folder is setup, try to move the given mod in there. - // If the folder does not exist, create it if possible. - private void MoveModToDefaultDirectory(Mod mod) - { - if (Penumbra.Config.DefaultImportFolder.Length == 0) - return; - - try - { - var leaf = FileSystem.Root.GetChildren(ISortMode.Lexicographical) - .FirstOrDefault(f => f is FileSystem.Leaf l && l.Value == mod); - if (leaf == null) - throw new Exception("Mod was not found at root."); - - var folder = FileSystem.FindOrCreateAllFolders(Penumbra.Config.DefaultImportFolder); - FileSystem.Move(leaf, folder); - } - catch (Exception e) - { - Penumbra.Log.Warning( - $"Could not move newly imported mod {mod.Name} to default import folder {Penumbra.Config.DefaultImportFolder}:\n{e}"); - } - } - - private static void DrawHelpPopup() - { - ImGuiUtil.HelpPopup("ExtendedHelp", new Vector2(1000 * ImGuiHelpers.GlobalScale, 34.5f * ImGui.GetTextLineHeightWithSpacing()), () => - { - ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); - ImGui.TextUnformatted("Mod Management"); - ImGui.BulletText("You can create empty mods or import mods with the buttons in this row."); - using var indent = ImRaii.PushIndent(); - ImGui.BulletText("Supported formats for import are: .ttmp, .ttmp2, .pmp."); - ImGui.BulletText( - "You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata."); - indent.Pop(1); - ImGui.BulletText("You can also create empty mod folders and delete mods."); - ImGui.BulletText("For further editing of mods, select them and use the Edit Mod tab in the panel or the Advanced Editing popup."); - ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); - ImGui.TextUnformatted("Mod Selector"); - ImGui.BulletText("Select a mod to obtain more information or change settings."); - ImGui.BulletText("Names are colored according to your config and their current state in the collection:"); - indent.Push(); - ImGuiUtil.BulletTextColored(ColorId.EnabledMod.Value(), "enabled in the current collection."); - ImGuiUtil.BulletTextColored(ColorId.DisabledMod.Value(), "disabled in the current collection."); - ImGuiUtil.BulletTextColored(ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection."); - ImGuiUtil.BulletTextColored(ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection."); - ImGuiUtil.BulletTextColored(ColorId.UndefinedMod.Value(), "unconfigured in all inherited collections."); - ImGuiUtil.BulletTextColored(ColorId.NewMod.Value(), - "newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded."); - ImGuiUtil.BulletTextColored(ColorId.HandledConflictMod.Value(), - "enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)."); - ImGuiUtil.BulletTextColored(ColorId.ConflictingMod.Value(), - "enabled and conflicting with another enabled Mod on the same priority."); - ImGuiUtil.BulletTextColored(ColorId.FolderExpanded.Value(), "expanded mod folder."); - ImGuiUtil.BulletTextColored(ColorId.FolderCollapsed.Value(), "collapsed mod folder"); - indent.Pop(1); - ImGui.BulletText("Right-click a mod to enter its sort order, which is its name by default, possibly with a duplicate number."); - indent.Push(); - ImGui.BulletText("A sort order differing from the mods name will not be displayed, it will just be used for ordering."); - ImGui.BulletText( - "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically."); - indent.Pop(1); - ImGui.BulletText( - "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod."); - ImGui.BulletText("Right-clicking a folder opens a context menu."); - ImGui.BulletText("Right-clicking empty space allows you to expand or collapse all folders at once."); - ImGui.BulletText("Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text."); - indent.Push(); - ImGui.BulletText("You can enter n:[string] to filter only for names, without path."); - ImGui.BulletText("You can enter c:[string] to filter for Changed Items instead."); - ImGui.BulletText("You can enter a:[string] to filter for Mod Authors instead."); - indent.Pop(1); - ImGui.BulletText("Use the expandable menu beside the input to filter for mods fulfilling specific criteria."); - }); - } -} diff --git a/Penumbra/UI/Classes/ModFilter.cs b/Penumbra/UI/Classes/ModFilter.cs deleted file mode 100644 index 3c68f15c..00000000 --- a/Penumbra/UI/Classes/ModFilter.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; - -namespace Penumbra.UI.Classes; - -[Flags] -public enum ModFilter -{ - Enabled = 1 << 0, - Disabled = 1 << 1, - Favorite = 1 << 2, - NotFavorite = 1 << 3, - NoConflict = 1 << 4, - SolvedConflict = 1 << 5, - UnsolvedConflict = 1 << 6, - HasNoMetaManipulations = 1 << 7, - HasMetaManipulations = 1 << 8, - HasNoFileSwaps = 1 << 9, - HasFileSwaps = 1 << 10, - HasConfig = 1 << 11, - HasNoConfig = 1 << 12, - HasNoFiles = 1 << 13, - HasFiles = 1 << 14, - IsNew = 1 << 15, - NotNew = 1 << 16, - Inherited = 1 << 17, - Uninherited = 1 << 18, - Undefined = 1 << 19, -}; - -public static class ModFilterExtensions -{ - public const ModFilter UnfilteredStateMods = ( ModFilter )( ( 1 << 20 ) - 1 ); - - public static string ToName( this ModFilter filter ) - => filter switch - { - ModFilter.Enabled => "Enabled", - ModFilter.Disabled => "Disabled", - ModFilter.Favorite => "Favorite", - ModFilter.NotFavorite => "No Favorite", - ModFilter.NoConflict => "No Conflicts", - ModFilter.SolvedConflict => "Solved Conflicts", - ModFilter.UnsolvedConflict => "Unsolved Conflicts", - ModFilter.HasNoMetaManipulations => "No Meta Manipulations", - ModFilter.HasMetaManipulations => "Meta Manipulations", - ModFilter.HasNoFileSwaps => "No File Swaps", - ModFilter.HasFileSwaps => "File Swaps", - ModFilter.HasNoConfig => "No Configuration", - ModFilter.HasConfig => "Configuration", - ModFilter.HasNoFiles => "No Files", - ModFilter.HasFiles => "Files", - ModFilter.IsNew => "Newly Imported", - ModFilter.NotNew => "Not Newly Imported", - ModFilter.Inherited => "Inherited Configuration", - ModFilter.Uninherited => "Own Configuration", - ModFilter.Undefined => "Not Configured", - _ => throw new ArgumentOutOfRangeException( nameof( filter ), filter, null ), - }; -} \ No newline at end of file diff --git a/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs b/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs new file mode 100644 index 00000000..bd9d3e35 --- /dev/null +++ b/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using ImGuiNET; +using OtterGui.Widgets; +using Penumbra.Collections; +using Penumbra.GameData.Actors; + +namespace Penumbra.UI.CollectionTab; + +public sealed class CollectionSelector : FilterComboCache +{ + private readonly ModCollection.Manager _collectionManager; + + public CollectionSelector(ModCollection.Manager manager, Func> items) + : base(items) + => _collectionManager = manager; + + public void Draw(string label, float width, int individualIdx) + { + var (_, collection) = _collectionManager.Individuals[individualIdx]; + if (Draw(label, collection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()) && CurrentSelection != null) + _collectionManager.SetCollection(CurrentSelection, CollectionType.Individual, individualIdx); + } + + public void Draw(string label, float width, CollectionType type) + { + var current = _collectionManager.ByType(type, ActorIdentifier.Invalid); + if (Draw(label, current?.Name ?? string.Empty, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()) && CurrentSelection != null) + _collectionManager.SetCollection(CurrentSelection, type); + } + + protected override string ToString(ModCollection obj) + => obj.Name; +} diff --git a/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs b/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs new file mode 100644 index 00000000..a29ebb25 --- /dev/null +++ b/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs @@ -0,0 +1,357 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Collections; +using Penumbra.GameData.Actors; +using Penumbra.Services; + +namespace Penumbra.UI.CollectionTab; + +public class IndividualCollectionUi +{ + private readonly ActorService _actorService; + private readonly ModCollection.Manager _collectionManager; + private readonly CollectionSelector _withEmpty; + + public IndividualCollectionUi(ActorService actors, ModCollection.Manager collectionManager, CollectionSelector withEmpty) + { + _actorService = actors; + _collectionManager = collectionManager; + _withEmpty = withEmpty; + if (_actorService.Valid) + SetupCombos(); + else + _actorService.FinishedCreation += SetupCombos; + } + + /// Draw all individual assignments as well as the options to create a new one. + public void Draw() + { + if (!_ready) + return; + + using var _ = ImRaii.Group(); + using var mainId = ImRaii.PushId("Individual"); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted($"Individual {TutorialService.ConditionalIndividual}s"); + ImGui.SameLine(); + ImGuiComponents.HelpMarker("Individual Collections apply specifically to individual game objects that fulfill the given criteria.\n" + + $"More general {TutorialService.GroupAssignment} or the {TutorialService.DefaultCollection} do not apply if an Individual Collection takes effect.\n" + + "Certain related actors - like the ones in cutscenes or preview windows - will try to use appropriate individual collections."); + ImGui.Separator(); + for (var i = 0; i < _collectionManager.Individuals.Count; ++i) + { + DrawIndividualAssignment(i); + } + + UiHelpers.DefaultLineSpace(); + DrawNewIndividualCollection(); + } + + public void UpdateIdentifiers(CollectionType type, ModCollection? _1, ModCollection? _2, string _3) + { + if (type == CollectionType.Individual) + UpdateIdentifiers(); + } + + // Input Selections. + private string _newCharacterName = string.Empty; + private ObjectKind _newKind = ObjectKind.BattleNpc; + + private WorldCombo _worldCombo = null!; + private NpcCombo _mountCombo = null!; + private NpcCombo _companionCombo = null!; + private NpcCombo _ornamentCombo = null!; + private NpcCombo _bnpcCombo = null!; + private NpcCombo _enpcCombo = null!; + + private const string NewPlayerTooltipEmpty = "Please enter a valid player name and choose an available world or 'Any World'."; + private const string NewRetainerTooltipEmpty = "Please enter a valid retainer name."; + private const string NewPlayerTooltipInvalid = "The entered name is not a valid name for a player character."; + private const string NewRetainerTooltipInvalid = "The entered name is not a valid name for a retainer."; + private const string AlreadyAssigned = "The Individual you specified has already been assigned a collection."; + private const string NewNpcTooltipEmpty = "Please select a valid NPC from the drop down menu first."; + + private ActorIdentifier[] _newPlayerIdentifiers = Array.Empty(); + private string _newPlayerTooltip = NewPlayerTooltipEmpty; + private ActorIdentifier[] _newRetainerIdentifiers = Array.Empty(); + private string _newRetainerTooltip = NewRetainerTooltipEmpty; + private ActorIdentifier[] _newNpcIdentifiers = Array.Empty(); + private string _newNpcTooltip = NewNpcTooltipEmpty; + private ActorIdentifier[] _newOwnedIdentifiers = Array.Empty(); + private string _newOwnedTooltip = NewPlayerTooltipEmpty; + + private bool _ready; + + /// Create combos when ready. + private void SetupCombos() + { + _worldCombo = new WorldCombo(_actorService.AwaitedService.Data.Worlds); + _mountCombo = new NpcCombo("##mountCombo", _actorService.AwaitedService.Data.Mounts); + _companionCombo = new NpcCombo("##companionCombo", _actorService.AwaitedService.Data.Companions); + _ornamentCombo = new NpcCombo("##ornamentCombo", _actorService.AwaitedService.Data.Ornaments); + _bnpcCombo = new NpcCombo("##bnpcCombo", _actorService.AwaitedService.Data.BNpcs); + _enpcCombo = new NpcCombo("##enpcCombo", _actorService.AwaitedService.Data.ENpcs); + _ready = true; + _actorService.FinishedCreation -= SetupCombos; + } + + + private static readonly IReadOnlyList ObjectKinds = new[] + { + ObjectKind.BattleNpc, + ObjectKind.EventNpc, + ObjectKind.Companion, + ObjectKind.MountType, + ObjectKind.Ornament, + }; + + /// Draw the Object Kind Selector. + private bool DrawNewObjectKindOptions(float width) + { + ImGui.SetNextItemWidth(width); + using var combo = ImRaii.Combo("##newKind", _newKind.ToName()); + if (!combo) + return false; + + var ret = false; + foreach (var kind in ObjectKinds) + { + if (!ImGui.Selectable(kind.ToName(), _newKind == kind)) + continue; + + _newKind = kind; + ret = true; + } + + return ret; + } + + private int _individualDragDropIdx = -1; + + /// Draw a single individual assignment. + private void DrawIndividualAssignment(int idx) + { + var (name, _) = _collectionManager.Individuals[idx]; + using var id = ImRaii.PushId(idx); + _withEmpty.Draw("##IndividualCombo", UiHelpers.InputTextWidth.X, idx); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty, + false, true)) + _collectionManager.RemoveIndividualCollection(idx); + + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.Selectable(name); + using (var source = ImRaii.DragDropSource()) + { + if (source) + { + ImGui.SetDragDropPayload("Individual", nint.Zero, 0); + _individualDragDropIdx = idx; + } + } + + using var target = ImRaii.DragDropTarget(); + if (!target.Success || !ImGuiUtil.IsDropping("Individual")) + return; + + if (_individualDragDropIdx >= 0) + _collectionManager.MoveIndividualCollection(_individualDragDropIdx, idx); + + _individualDragDropIdx = -1; + } + + private bool DrawNewPlayerCollection(Vector2 buttonWidth, float width) + { + var change = _worldCombo.Draw(width); + ImGui.SameLine(); + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X - width); + change |= ImGui.InputTextWithHint("##NewCharacter", "Character Name...", ref _newCharacterName, 32); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Assign Player", buttonWidth, _newPlayerTooltip, + _newPlayerTooltip.Length > 0 || _newPlayerIdentifiers.Length == 0)) + { + _collectionManager.CreateIndividualCollection(_newPlayerIdentifiers); + change = true; + } + + return change; + } + + private bool DrawNewNpcCollection(NpcCombo combo, Vector2 buttonWidth, float width) + { + var comboWidth = UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X - width; + var change = DrawNewObjectKindOptions(width); + ImGui.SameLine(); + change |= combo.Draw(comboWidth); + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Assign NPC", buttonWidth, _newNpcTooltip, + _newNpcIdentifiers.Length == 0 || _newNpcTooltip.Length > 0)) + { + _collectionManager.CreateIndividualCollection(_newNpcIdentifiers); + change = true; + } + + return change; + } + + private bool DrawNewOwnedCollection(Vector2 buttonWidth) + { + if (!ImGuiUtil.DrawDisabledButton("Assign Owned NPC", buttonWidth, _newOwnedTooltip, + _newOwnedIdentifiers.Length == 0 || _newOwnedTooltip.Length > 0)) + return false; + + _collectionManager.CreateIndividualCollection(_newOwnedIdentifiers); + return true; + + } + + private bool DrawNewRetainerCollection(Vector2 buttonWidth) + { + if (!ImGuiUtil.DrawDisabledButton("Assign Bell Retainer", buttonWidth, _newRetainerTooltip, + _newRetainerIdentifiers.Length == 0 || _newRetainerTooltip.Length > 0)) + return false; + + _collectionManager.CreateIndividualCollection(_newRetainerIdentifiers); + return true; + + } + + private NpcCombo GetNpcCombo(ObjectKind kind) + => kind switch + { + ObjectKind.BattleNpc => _bnpcCombo, + ObjectKind.EventNpc => _enpcCombo, + ObjectKind.MountType => _mountCombo, + ObjectKind.Companion => _companionCombo, + ObjectKind.Ornament => _ornamentCombo, + _ => throw new NotImplementedException(), + }; + + private void DrawNewIndividualCollection() + { + var width = (UiHelpers.InputTextWidth.X - 2 * ImGui.GetStyle().ItemSpacing.X) / 3; + + var buttonWidth1 = new Vector2(90 * UiHelpers.Scale, 0); + var buttonWidth2 = new Vector2(120 * UiHelpers.Scale, 0); + + var assignWidth = new Vector2((UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + var change = DrawNewCurrentPlayerCollection(assignWidth); + ImGui.SameLine(); + change |= DrawNewTargetCollection(assignWidth); + + change |= DrawNewPlayerCollection(buttonWidth1, width); + ImGui.SameLine(); + change |= DrawNewRetainerCollection(buttonWidth2); + + var combo = GetNpcCombo(_newKind); + change |= DrawNewNpcCollection(combo, buttonWidth1, width); + ImGui.SameLine(); + change |= DrawNewOwnedCollection(buttonWidth2); + + if (change) + UpdateIdentifiers(); + } + + private bool DrawNewCurrentPlayerCollection(Vector2 width) + { + var player = _actorService.AwaitedService.GetCurrentPlayer(); + var result = _collectionManager.Individuals.CanAdd(player); + var tt = result switch + { + IndividualCollections.AddResult.Valid => $"Assign a collection to {player}.", + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + IndividualCollections.AddResult.Invalid => "No logged-in character detected.", + _ => string.Empty, + }; + + + if (!ImGuiUtil.DrawDisabledButton("Assign Current Player", width, tt, result != IndividualCollections.AddResult.Valid)) + return false; + + _collectionManager.CreateIndividualCollection(player); + return true; + + } + + private bool DrawNewTargetCollection(Vector2 width) + { + var target = _actorService.AwaitedService.FromObject(DalamudServices.Targets.Target, false, true, true); + var result = _collectionManager.Individuals.CanAdd(target); + var tt = result switch + { + IndividualCollections.AddResult.Valid => $"Assign a collection to {target}.", + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + IndividualCollections.AddResult.Invalid => "No valid character in target detected.", + _ => string.Empty, + }; + if (ImGuiUtil.DrawDisabledButton("Assign Current Target", width, tt, result != IndividualCollections.AddResult.Valid)) + { + _collectionManager.CreateIndividualCollection(_collectionManager.Individuals.GetGroup(target)); + return true; + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "- Bell Retainers also apply to Mannequins named after them, but not to outdoor retainers, since they only carry their owners name.\n" + + "- Some NPCs are available as Battle- and Event NPCs and need to be setup for both if desired.\n" + + "- Battle- and Event NPCs may apply to more than one ID if they share the same name. This is language dependent. If you change your clients language, verify that your collections are still correctly assigned."); + + return false; + } + + private void UpdateIdentifiers() + { + var combo = GetNpcCombo(_newKind); + _newPlayerTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Player, _newCharacterName, + _worldCombo.CurrentSelection.Key, ObjectKind.None, + Array.Empty(), out _newPlayerIdentifiers) switch + { + _ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty, + IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid, + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + _newRetainerTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None, + Array.Empty(), out _newRetainerIdentifiers) switch + { + _ when _newCharacterName.Length == 0 => NewRetainerTooltipEmpty, + IndividualCollections.AddResult.Invalid => NewRetainerTooltipInvalid, + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + if (combo.CurrentSelection.Ids != null) + { + _newNpcTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind, + combo.CurrentSelection.Ids, out _newNpcIdentifiers) switch + { + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + _newOwnedTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Owned, _newCharacterName, + _worldCombo.CurrentSelection.Key, _newKind, + combo.CurrentSelection.Ids, out _newOwnedIdentifiers) switch + { + _ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty, + IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid, + IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, + _ => string.Empty, + }; + } + else + { + _newNpcTooltip = NewNpcTooltipEmpty; + _newOwnedTooltip = NewNpcTooltipEmpty; + _newNpcIdentifiers = Array.Empty(); + _newOwnedIdentifiers = Array.Empty(); + } + } +} diff --git a/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs b/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs new file mode 100644 index 00000000..dc3bc52f --- /dev/null +++ b/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs @@ -0,0 +1,302 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Collections; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.CollectionTab; + +public class InheritanceUi +{ + private const int InheritedCollectionHeight = 9; + private const string InheritanceDragDropLabel = "##InheritanceMove"; + + private readonly ModCollection.Manager _collectionManager; + + public InheritanceUi(ModCollection.Manager collectionManager) + => _collectionManager = collectionManager; + + /// Draw the whole inheritance block. + public void Draw() + { + using var group = ImRaii.Group(); + using var id = ImRaii.PushId("##Inheritance"); + ImGui.TextUnformatted($"The {TutorialService.SelectedCollection} inherits from:"); + DrawCurrentCollectionInheritance(); + DrawInheritanceTrashButton(); + DrawNewInheritanceSelection(); + DelayedActions(); + } + + // Keep for reuse. + private readonly HashSet _seenInheritedCollections = new(32); + + // Execute changes only outside of loops. + private ModCollection? _newInheritance; + private ModCollection? _movedInheritance; + private (int, int)? _inheritanceAction; + private ModCollection? _newCurrentCollection; + + /// + /// If an inherited collection is expanded, + /// draw all its flattened, distinct children in order with a tree-line. + /// + private void DrawInheritedChildren(ModCollection collection) + { + using var id = ImRaii.PushId(collection.Index); + using var indent = ImRaii.PushIndent(); + + // Get start point for the lines (top of the selector). + // Tree line stuff. + var lineStart = ImGui.GetCursorScreenPos(); + var offsetX = -ImGui.GetStyle().IndentSpacing + ImGui.GetTreeNodeToLabelSpacing() / 2; + var drawList = ImGui.GetWindowDrawList(); + var lineSize = Math.Max(0, ImGui.GetStyle().IndentSpacing - 9 * UiHelpers.Scale); + lineStart.X += offsetX; + lineStart.Y -= 2 * UiHelpers.Scale; + var lineEnd = lineStart; + + // Skip the collection itself. + foreach (var inheritance in collection.GetFlattenedInheritance().Skip(1)) + { + // Draw the child, already seen collections are colored as conflicts. + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(Penumbra.Config), + _seenInheritedCollections.Contains(inheritance)); + _seenInheritedCollections.Add(inheritance); + + ImRaii.TreeNode(inheritance.Name, ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet); + var (minRect, maxRect) = (ImGui.GetItemRectMin(), ImGui.GetItemRectMax()); + DrawInheritanceTreeClicks(inheritance, false); + + // Tree line stuff. + if (minRect.X == 0) + continue; + + // Draw the notch and increase the line length. + var midPoint = (minRect.Y + maxRect.Y) / 2f - 1f; + drawList.AddLine(new Vector2(lineStart.X, midPoint), new Vector2(lineStart.X + lineSize, midPoint), Colors.MetaInfoText, + UiHelpers.Scale); + lineEnd.Y = midPoint; + } + + // Finally, draw the folder line. + drawList.AddLine(lineStart, lineEnd, Colors.MetaInfoText, UiHelpers.Scale); + } + + /// Draw a single primary inherited collection. + private void DrawInheritance(ModCollection collection) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(Penumbra.Config), + _seenInheritedCollections.Contains(collection)); + _seenInheritedCollections.Add(collection); + using var tree = ImRaii.TreeNode(collection.Name, ImGuiTreeNodeFlags.NoTreePushOnOpen); + color.Pop(); + DrawInheritanceTreeClicks(collection, true); + DrawInheritanceDropSource(collection); + DrawInheritanceDropTarget(collection); + + if (tree) + DrawInheritedChildren(collection); + else + // We still want to keep track of conflicts. + _seenInheritedCollections.UnionWith(collection.GetFlattenedInheritance()); + } + + /// Draw the list box containing the current inheritance information. + private void DrawCurrentCollectionInheritance() + { + using var list = ImRaii.ListBox("##inheritanceList", + new Vector2(UiHelpers.InputTextMinusButton, ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight)); + if (!list) + return; + + _seenInheritedCollections.Clear(); + _seenInheritedCollections.Add(_collectionManager.Current); + foreach (var collection in _collectionManager.Current.Inheritance.ToList()) + DrawInheritance(collection); + } + + /// Draw a drag and drop button to delete. + private void DrawInheritanceTrashButton() + { + ImGui.SameLine(); + var size = UiHelpers.IconButtonSize with { Y = ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight }; + var buttonColor = ImGui.GetColorU32(ImGuiCol.Button); + // Prevent hovering from highlighting the button. + using var color = ImRaii.PushColor(ImGuiCol.ButtonActive, buttonColor) + .Push(ImGuiCol.ButtonHovered, buttonColor); + ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size, + "Drag primary inheritance here to remove it from the list.", false, true); + + using var target = ImRaii.DragDropTarget(); + if (target.Success && ImGuiUtil.IsDropping(InheritanceDragDropLabel)) + _inheritanceAction = (_collectionManager.Current.Inheritance.IndexOf(_movedInheritance!), -1); + } + + /// + /// Set the current collection, or delete or move an inheritance if the action was triggered during iteration. + /// Can not be done during iteration to keep collections unchanged. + /// + private void DelayedActions() + { + if (_newCurrentCollection != null) + { + _collectionManager.SetCollection(_newCurrentCollection, CollectionType.Current); + _newCurrentCollection = null; + } + + if (_inheritanceAction == null) + return; + + if (_inheritanceAction.Value.Item1 >= 0) + { + if (_inheritanceAction.Value.Item2 == -1) + _collectionManager.Current.RemoveInheritance(_inheritanceAction.Value.Item1); + else + _collectionManager.Current.MoveInheritance(_inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2); + } + + _inheritanceAction = null; + } + + /// + /// Draw the selector to add new inheritances. + /// The add button is only available if the selected collection can actually be added. + /// + private void DrawNewInheritanceSelection() + { + DrawNewInheritanceCombo(); + ImGui.SameLine(); + var inheritance = _collectionManager.Current.CheckValidInheritance(_newInheritance); + var tt = inheritance switch + { + ModCollection.ValidInheritance.Empty => "No valid collection to inherit from selected.", + ModCollection.ValidInheritance.Valid => $"Let the {TutorialService.SelectedCollection} inherit from this collection.", + ModCollection.ValidInheritance.Self => "The collection can not inherit from itself.", + ModCollection.ValidInheritance.Contained => "Already inheriting from this collection.", + ModCollection.ValidInheritance.Circle => "Inheriting from this collection would lead to cyclic inheritance.", + _ => string.Empty, + }; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, tt, + inheritance != ModCollection.ValidInheritance.Valid, true) + && _collectionManager.Current.AddInheritance(_newInheritance!, true)) + _newInheritance = null; + + if (inheritance != ModCollection.ValidInheritance.Valid) + _newInheritance = null; + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.QuestionCircle.ToIconString(), UiHelpers.IconButtonSize, "What is Inheritance?", + false, true)) + ImGui.OpenPopup("InheritanceHelp"); + + ImGuiUtil.HelpPopup("InheritanceHelp", new Vector2(1000 * UiHelpers.Scale, 21 * ImGui.GetTextLineHeightWithSpacing()), () => + { + ImGui.NewLine(); + ImGui.TextWrapped( + "Inheritance is a way to use a baseline of mods across multiple collections, without needing to change all those collections if you want to add a single mod."); + ImGui.NewLine(); + ImGui.TextUnformatted("Every mod in a collection can have three basic states: 'Enabled', 'Disabled' and 'Unconfigured'."); + ImGui.BulletText("If the mod is 'Enabled' or 'Disabled', it does not matter if the collection inherits from other collections."); + ImGui.BulletText( + "If the mod is unconfigured, those inherited-from collections are checked in the order displayed here, including sub-inheritances."); + ImGui.BulletText( + "If a collection is found in which the mod is either 'Enabled' or 'Disabled', the settings from this collection will be used."); + ImGui.BulletText("If no such collection is found, the mod will be treated as disabled."); + ImGui.BulletText( + "Highlighted collections in the left box are never reached because they are already checked in a sub-inheritance before."); + ImGui.NewLine(); + ImGui.TextUnformatted("Example"); + ImGui.BulletText("Collection A has the Bibo+ body and a Hempen Camise mod enabled."); + ImGui.BulletText( + "Collection B inherits from A, leaves Bibo+ unconfigured, but has the Hempen Camise enabled with different settings than A."); + ImGui.BulletText("Collection C also inherits from A, has Bibo+ explicitly disabled and the Hempen Camise unconfigured."); + ImGui.BulletText("Collection D inherits from C and then B and leaves everything unconfigured."); + using var indent = ImRaii.PushIndent(); + ImGui.BulletText("B uses Bibo+ settings from A and its own Hempen Camise settings."); + ImGui.BulletText("C has Bibo+ disabled and uses A's Hempen Camise settings."); + ImGui.BulletText( + "D has Bibo+ disabled and uses A's Hempen Camise settings, not B's. It traversed the collections in Order D -> (C -> A) -> (B -> A)."); + }); + } + + /// + /// Draw the combo to select new potential inheritances. + /// Only valid inheritances are drawn in the preview, or nothing if no inheritance is available. + /// + private void DrawNewInheritanceCombo() + { + ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton); + _newInheritance ??= _collectionManager.FirstOrDefault(c + => c != _collectionManager.Current && !_collectionManager.Current.Inheritance.Contains(c)) + ?? ModCollection.Empty; + using var combo = ImRaii.Combo("##newInheritance", _newInheritance.Name); + if (!combo) + return; + + foreach (var collection in _collectionManager + .Where(c => _collectionManager.Current.CheckValidInheritance(c) == ModCollection.ValidInheritance.Valid) + .OrderBy(c => c.Name)) + { + if (ImGui.Selectable(collection.Name, _newInheritance == collection)) + _newInheritance = collection; + } + } + + /// + /// Move an inherited collection when dropped onto another. + /// Move is delayed due to collection changes. + /// + private void DrawInheritanceDropTarget(ModCollection collection) + { + using var target = ImRaii.DragDropTarget(); + if (!target.Success || !ImGuiUtil.IsDropping(InheritanceDragDropLabel)) + return; + + if (_movedInheritance != null) + { + var idx1 = _collectionManager.Current.Inheritance.IndexOf(_movedInheritance); + var idx2 = _collectionManager.Current.Inheritance.IndexOf(collection); + if (idx1 >= 0 && idx2 >= 0) + _inheritanceAction = (idx1, idx2); + } + + _movedInheritance = null; + } + + /// Move an inherited collection. + private void DrawInheritanceDropSource(ModCollection collection) + { + using var source = ImRaii.DragDropSource(); + if (!source) + return; + + ImGui.SetDragDropPayload(InheritanceDragDropLabel, nint.Zero, 0); + _movedInheritance = collection; + ImGui.TextUnformatted($"Moving {_movedInheritance?.Name ?? "Unknown"}..."); + } + + /// + /// Ctrl + Right-Click -> Switch current collection to this (for all). + /// Ctrl + Shift + Right-Click -> Delete this inheritance (only if withDelete). + /// Deletion is delayed due to collection changes. + /// + private void DrawInheritanceTreeClicks(ModCollection collection, bool withDelete) + { + if (ImGui.GetIO().KeyCtrl && ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + if (withDelete && ImGui.GetIO().KeyShift) + _inheritanceAction = (_collectionManager.Current.Inheritance.IndexOf(collection), -1); + else + _newCurrentCollection = collection; + } + + ImGuiUtil.HoverTooltip($"Control + Right-Click to switch the {TutorialService.SelectedCollection} to this one." + + (withDelete ? "\nControl + Shift + Right-Click to remove this inheritance." : string.Empty)); + } +} diff --git a/Penumbra/UI/CollectionTab/Collections.NpcCombo.cs b/Penumbra/UI/CollectionTab/Collections.NpcCombo.cs new file mode 100644 index 00000000..9e0ffe14 --- /dev/null +++ b/Penumbra/UI/CollectionTab/Collections.NpcCombo.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using ImGuiNET; +using OtterGui.Widgets; + +namespace Penumbra.UI.CollectionTab; + +public sealed class NpcCombo : FilterComboCache<(string Name, uint[] Ids)> +{ + private readonly string _label; + + public NpcCombo(string label, IReadOnlyDictionary names) + : base(() => names.GroupBy(kvp => kvp.Value).Select(g => (g.Key, g.Select(g => g.Key).ToArray())).OrderBy(g => g.Key).ToList()) + => _label = label; + + protected override string ToString((string Name, uint[] Ids) obj) + => obj.Name; + + protected override bool DrawSelectable(int globalIdx, bool selected) + { + var (name, ids) = Items[globalIdx]; + var ret = ImGui.Selectable(name, selected); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join('\n', ids.Select(i => i.ToString()))); + + return ret; + } + + public bool Draw(float width) + => Draw(_label, CurrentSelection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); +} diff --git a/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs b/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs new file mode 100644 index 00000000..59461f42 --- /dev/null +++ b/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs @@ -0,0 +1,42 @@ +using ImGuiNET; +using OtterGui.Classes; +using OtterGui.Widgets; +using Penumbra.Collections; + +namespace Penumbra.UI.CollectionTab; + +public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, string)> +{ + private readonly ModCollection.Manager _collectionManager; + + public (CollectionType, string, string)? CurrentType + => CollectionTypeExtensions.Special[CurrentIdx]; + + public int CurrentIdx; + private readonly float _unscaledWidth; + private readonly string _label; + + public SpecialCombo(ModCollection.Manager collectionManager, string label, float unscaledWidth) + : base(CollectionTypeExtensions.Special, false) + { + _collectionManager = collectionManager; + _label = label; + _unscaledWidth = unscaledWidth; + } + + public void Draw() + { + var preview = CurrentIdx >= 0 ? Items[CurrentIdx].Item2 : string.Empty; + Draw(_label, preview, string.Empty, ref CurrentIdx, _unscaledWidth * UiHelpers.Scale, + ImGui.GetTextLineHeightWithSpacing()); + } + + protected override string ToString((CollectionType, string, string) obj) + => obj.Item2; + + protected override bool IsVisible(int globalIdx, LowerString filter) + { + var obj = Items[globalIdx]; + return filter.IsContained(obj.Item2) && _collectionManager.ByType(obj.Item1) == null; + } +} \ No newline at end of file diff --git a/Penumbra/UI/CollectionTab/Collections.WorldCombo.cs b/Penumbra/UI/CollectionTab/Collections.WorldCombo.cs new file mode 100644 index 00000000..5441dbaa --- /dev/null +++ b/Penumbra/UI/CollectionTab/Collections.WorldCombo.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; +using ImGuiNET; +using OtterGui.Widgets; + +namespace Penumbra.UI.CollectionTab; + +public sealed class WorldCombo : FilterComboCache> +{ + private static readonly KeyValuePair AllWorldPair = new(ushort.MaxValue, "Any World"); + + public WorldCombo(IReadOnlyDictionary worlds) + : base(worlds.OrderBy(kvp => kvp.Value).Prepend(AllWorldPair)) + { + CurrentSelection = AllWorldPair; + CurrentSelectionIdx = 0; + } + + protected override string ToString(KeyValuePair obj) + => obj.Value; + + public bool Draw(float width) + => Draw("##worldCombo", CurrentSelection.Value, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()); +} diff --git a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs deleted file mode 100644 index c55fd6d4..00000000 --- a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Mods; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - // Draw a simple clipped table containing all changed items. - public class ChangedItemsTab : ITab - { - private readonly ConfigWindow _config; - - public ChangedItemsTab( ConfigWindow config ) - => _config = config; - - public ReadOnlySpan Label - => "Changed Items"u8; - - private LowerString _changedItemFilter = LowerString.Empty; - private LowerString _changedItemModFilter = LowerString.Empty; - - public void DrawContent() - { - // Draw filters. - var varWidth = ImGui.GetContentRegionAvail().X - - 400 * ImGuiHelpers.GlobalScale - - ImGui.GetStyle().ItemSpacing.X; - ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); - LowerString.InputWithHint( "##changedItemsFilter", "Filter Item...", ref _changedItemFilter, 128 ); - ImGui.SameLine(); - ImGui.SetNextItemWidth( varWidth ); - LowerString.InputWithHint( "##changedItemsModFilter", "Filter Mods...", ref _changedItemModFilter, 128 ); - - using var child = ImRaii.Child( "##changedItemsChild", -Vector2.One ); - if( !child ) - { - return; - } - - // Draw table of changed items. - var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - var skips = ImGuiClip.GetNecessarySkips( height ); - using var list = ImRaii.Table( "##changedItems", 3, ImGuiTableFlags.RowBg, -Vector2.One ); - if( !list ) - { - return; - } - - const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed; - ImGui.TableSetupColumn( "items", flags, 400 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "mods", flags, varWidth - 120 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "id", flags, 120 * ImGuiHelpers.GlobalScale ); - - var items = Penumbra.CollectionManager.Current.ChangedItems; - var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty - ? ImGuiClip.ClippedDraw( items, skips, DrawChangedItemColumn, items.Count ) - : ImGuiClip.FilteredClippedDraw( items, skips, FilterChangedItem, DrawChangedItemColumn ); - ImGuiClip.DrawEndDummy( rest, height ); - } - - - // Functions in here for less pollution. - private bool FilterChangedItem( KeyValuePair< string, (SingleArray< IMod >, object?) > item ) - => ( _changedItemFilter.IsEmpty - || ChangedItemName( item.Key, item.Value.Item2 ) - .Contains( _changedItemFilter.Lower, StringComparison.OrdinalIgnoreCase ) ) - && ( _changedItemModFilter.IsEmpty || item.Value.Item1.Any( m => m.Name.Contains( _changedItemModFilter ) ) ); - - private void DrawChangedItemColumn( KeyValuePair< string, (SingleArray< IMod >, object?) > item ) - { - ImGui.TableNextColumn(); - _config.DrawChangedItem( item.Key, item.Value.Item2, false ); - ImGui.TableNextColumn(); - if( item.Value.Item1.Count > 0 ) - { - ImGui.TextUnformatted( item.Value.Item1[ 0 ].Name ); - if( item.Value.Item1.Count > 1 ) - { - ImGuiUtil.HoverTooltip( string.Join( "\n", item.Value.Item1.Skip( 1 ).Select( m => m.Name ) ) ); - } - } - - ImGui.TableNextColumn(); - if( DrawChangedItemObject( item.Value.Item2, out var text ) ) - { - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ItemId.Value() ); - ImGuiUtil.RightAlign( text ); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs deleted file mode 100644 index d89d554e..00000000 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Individual.cs +++ /dev/null @@ -1,367 +0,0 @@ -using System; -using System.Collections.Generic; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.Collections; -using System.Linq; -using System.Numerics; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Interface.Components; -using OtterGui.Widgets; -using Penumbra.GameData.Actors; -using Penumbra.Services; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class CollectionsTab - { - private sealed class WorldCombo : FilterComboCache< KeyValuePair< ushort, string > > - { - private static readonly KeyValuePair< ushort, string > AllWorldPair = new(ushort.MaxValue, "Any World"); - - public WorldCombo( IReadOnlyDictionary< ushort, string > worlds ) - : base( worlds.OrderBy( kvp => kvp.Value ).Prepend( AllWorldPair ) ) - { - CurrentSelection = AllWorldPair; - CurrentSelectionIdx = 0; - } - - protected override string ToString( KeyValuePair< ushort, string > obj ) - => obj.Value; - - public bool Draw( float width ) - => Draw( "##worldCombo", CurrentSelection.Value, string.Empty, width, ImGui.GetTextLineHeightWithSpacing() ); - } - - private sealed class NpcCombo : FilterComboCache< (string Name, uint[] Ids) > - { - private readonly string _label; - - public NpcCombo( string label, IReadOnlyDictionary< uint, string > names ) - : base( () => names.GroupBy( kvp => kvp.Value ).Select( g => ( g.Key, g.Select( g => g.Key ).ToArray() ) ).OrderBy( g => g.Key ).ToList() ) - => _label = label; - - protected override string ToString( (string Name, uint[] Ids) obj ) - => obj.Name; - - protected override bool DrawSelectable( int globalIdx, bool selected ) - { - var (name, ids) = Items[ globalIdx ]; - var ret = ImGui.Selectable( name, selected ); - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( string.Join( '\n', ids.Select( i => i.ToString() ) ) ); - } - - return ret; - } - - public bool Draw( float width ) - => Draw( _label, CurrentSelection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing() ); - } - - - // Input Selections. - private string _newCharacterName = string.Empty; - private ObjectKind _newKind = ObjectKind.BattleNpc; - - private readonly WorldCombo _worldCombo = new(Penumbra.Actors.Data.Worlds); - private readonly NpcCombo _mountCombo = new("##mountCombo", Penumbra.Actors.Data.Mounts); - private readonly NpcCombo _companionCombo = new("##companionCombo", Penumbra.Actors.Data.Companions); - private readonly NpcCombo _ornamentCombo = new("##ornamentCombo", Penumbra.Actors.Data.Ornaments); - private readonly NpcCombo _bnpcCombo = new("##bnpcCombo", Penumbra.Actors.Data.BNpcs); - private readonly NpcCombo _enpcCombo = new("##enpcCombo", Penumbra.Actors.Data.ENpcs); - - private const string NewPlayerTooltipEmpty = "Please enter a valid player name and choose an available world or 'Any World'."; - private const string NewRetainerTooltipEmpty = "Please enter a valid retainer name."; - private const string NewPlayerTooltipInvalid = "The entered name is not a valid name for a player character."; - private const string NewRetainerTooltipInvalid = "The entered name is not a valid name for a retainer."; - private const string AlreadyAssigned = "The Individual you specified has already been assigned a collection."; - private const string NewNpcTooltipEmpty = "Please select a valid NPC from the drop down menu first."; - - private ActorIdentifier[] _newPlayerIdentifiers = Array.Empty< ActorIdentifier >(); - private string _newPlayerTooltip = NewPlayerTooltipEmpty; - private ActorIdentifier[] _newRetainerIdentifiers = Array.Empty< ActorIdentifier >(); - private string _newRetainerTooltip = NewRetainerTooltipEmpty; - private ActorIdentifier[] _newNpcIdentifiers = Array.Empty< ActorIdentifier >(); - private string _newNpcTooltip = NewNpcTooltipEmpty; - private ActorIdentifier[] _newOwnedIdentifiers = Array.Empty< ActorIdentifier >(); - private string _newOwnedTooltip = NewPlayerTooltipEmpty; - - private bool DrawNewObjectKindOptions( float width ) - { - ImGui.SetNextItemWidth( width ); - using var combo = ImRaii.Combo( "##newKind", _newKind.ToName() ); - if( !combo ) - { - return false; - } - - var ret = false; - foreach( var kind in new[] { ObjectKind.BattleNpc, ObjectKind.EventNpc, ObjectKind.Companion, ObjectKind.MountType, ( ObjectKind )15 } ) // TODO: CS Update - { - if( ImGui.Selectable( kind.ToName(), _newKind == kind ) ) - { - _newKind = kind; - ret = true; - } - } - - return ret; - } - - private int _individualDragDropIdx = -1; - - private void DrawIndividualAssignments() - { - using var _ = ImRaii.Group(); - using var mainId = ImRaii.PushId( "Individual" ); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( $"Individual {ConditionalIndividual}s" ); - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "Individual Collections apply specifically to individual game objects that fulfill the given criteria.\n" - + $"More general {GroupAssignment} or the {DefaultCollection} do not apply if an Individual Collection takes effect.\n" - + "Certain related actors - like the ones in cutscenes or preview windows - will try to use appropriate individual collections." ); - ImGui.Separator(); - for( var i = 0; i < Penumbra.CollectionManager.Individuals.Count; ++i ) - { - var (name, _) = Penumbra.CollectionManager.Individuals[ i ]; - using var id = ImRaii.PushId( i ); - CollectionsWithEmpty.Draw( "##IndividualCombo", _window._inputTextWidth.X, i ); - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, - false, true ) ) - { - Penumbra.CollectionManager.RemoveIndividualCollection( i ); - } - - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.Selectable( name ); - using( var source = ImRaii.DragDropSource() ) - { - if( source ) - { - ImGui.SetDragDropPayload( "Individual", IntPtr.Zero, 0 ); - _individualDragDropIdx = i; - } - } - - using( var target = ImRaii.DragDropTarget() ) - { - if( !target.Success || !ImGuiUtil.IsDropping( "Individual" ) ) - { - continue; - } - - if( _individualDragDropIdx >= 0 ) - { - Penumbra.CollectionManager.MoveIndividualCollection( _individualDragDropIdx, i ); - } - - _individualDragDropIdx = -1; - } - } - - ImGui.Dummy( _window._defaultSpace ); - DrawNewIndividualCollection(); - } - - private bool DrawNewPlayerCollection( Vector2 buttonWidth, float width ) - { - var change = _worldCombo.Draw( width ); - ImGui.SameLine(); - ImGui.SetNextItemWidth( _window._inputTextWidth.X - ImGui.GetStyle().ItemSpacing.X - width ); - change |= ImGui.InputTextWithHint( "##NewCharacter", "Character Name...", ref _newCharacterName, 32 ); - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Assign Player", buttonWidth, _newPlayerTooltip, _newPlayerTooltip.Length > 0 || _newPlayerIdentifiers.Length == 0 ) ) - { - Penumbra.CollectionManager.CreateIndividualCollection( _newPlayerIdentifiers ); - change = true; - } - - return change; - } - - private bool DrawNewNpcCollection( NpcCombo combo, Vector2 buttonWidth, float width ) - { - var comboWidth = _window._inputTextWidth.X - ImGui.GetStyle().ItemSpacing.X - width; - var change = DrawNewObjectKindOptions( width ); - ImGui.SameLine(); - change |= combo.Draw( comboWidth ); - - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Assign NPC", buttonWidth, _newNpcTooltip, _newNpcIdentifiers.Length == 0 || _newNpcTooltip.Length > 0 ) ) - { - Penumbra.CollectionManager.CreateIndividualCollection( _newNpcIdentifiers ); - change = true; - } - - return change; - } - - private bool DrawNewOwnedCollection( Vector2 buttonWidth ) - { - if( ImGuiUtil.DrawDisabledButton( "Assign Owned NPC", buttonWidth, _newOwnedTooltip, _newOwnedIdentifiers.Length == 0 || _newOwnedTooltip.Length > 0 ) ) - { - Penumbra.CollectionManager.CreateIndividualCollection( _newOwnedIdentifiers ); - return true; - } - - return false; - } - - private bool DrawNewRetainerCollection( Vector2 buttonWidth ) - { - if( ImGuiUtil.DrawDisabledButton( "Assign Bell Retainer", buttonWidth, _newRetainerTooltip, _newRetainerIdentifiers.Length == 0 || _newRetainerTooltip.Length > 0 ) ) - { - Penumbra.CollectionManager.CreateIndividualCollection( _newRetainerIdentifiers ); - return true; - } - - return false; - } - - private NpcCombo GetNpcCombo( ObjectKind kind ) - => kind switch - { - ObjectKind.BattleNpc => _bnpcCombo, - ObjectKind.EventNpc => _enpcCombo, - ObjectKind.MountType => _mountCombo, - ObjectKind.Companion => _companionCombo, - ( ObjectKind )15 => _ornamentCombo, // TODO: CS update - _ => throw new NotImplementedException(), - }; - - private void DrawNewIndividualCollection() - { - var width = ( _window._inputTextWidth.X - 2 * ImGui.GetStyle().ItemSpacing.X ) / 3; - - var buttonWidth1 = new Vector2( 90 * ImGuiHelpers.GlobalScale, 0 ); - var buttonWidth2 = new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ); - - var assignWidth = new Vector2( ( _window._inputTextWidth.X - ImGui.GetStyle().ItemSpacing.X ) / 2, 0 ); - var change = DrawNewCurrentPlayerCollection( assignWidth ); - ImGui.SameLine(); - change |= DrawNewTargetCollection( assignWidth ); - - change |= DrawNewPlayerCollection( buttonWidth1, width ); - ImGui.SameLine(); - change |= DrawNewRetainerCollection( buttonWidth2 ); - - var combo = GetNpcCombo( _newKind ); - change |= DrawNewNpcCollection( combo, buttonWidth1, width ); - ImGui.SameLine(); - change |= DrawNewOwnedCollection( buttonWidth2 ); - - if( change ) - { - UpdateIdentifiers(); - } - } - - private static bool DrawNewCurrentPlayerCollection( Vector2 width ) - { - var player = Penumbra.Actors.GetCurrentPlayer(); - var result = Penumbra.CollectionManager.Individuals.CanAdd( player ); - var tt = result switch - { - IndividualCollections.AddResult.Valid => $"Assign a collection to {player}.", - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - IndividualCollections.AddResult.Invalid => "No logged-in character detected.", - _ => string.Empty, - }; - - - if( ImGuiUtil.DrawDisabledButton( "Assign Current Player", width, tt, result != IndividualCollections.AddResult.Valid ) ) - { - Penumbra.CollectionManager.CreateIndividualCollection( player ); - return true; - } - - return false; - } - - private static bool DrawNewTargetCollection( Vector2 width ) - { - var target = Penumbra.Actors.FromObject( DalamudServices.Targets.Target, false, true, true ); - var result = Penumbra.CollectionManager.Individuals.CanAdd( target ); - var tt = result switch - { - IndividualCollections.AddResult.Valid => $"Assign a collection to {target}.", - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - IndividualCollections.AddResult.Invalid => "No valid character in target detected.", - _ => string.Empty, - }; - if( ImGuiUtil.DrawDisabledButton( "Assign Current Target", width, tt, result != IndividualCollections.AddResult.Valid ) ) - { - Penumbra.CollectionManager.CreateIndividualCollection( Penumbra.CollectionManager.Individuals.GetGroup( target ) ); - return true; - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( "- Bell Retainers also apply to Mannequins named after them, but not to outdoor retainers, since they only carry their owners name.\n" - + "- Some NPCs are available as Battle- and Event NPCs and need to be setup for both if desired.\n" - + "- Battle- and Event NPCs may apply to more than one ID if they share the same name. This is language dependent. If you change your clients language, verify that your collections are still correctly assigned." ); - - return false; - } - - private void UpdateIdentifiers() - { - var combo = GetNpcCombo( _newKind ); - _newPlayerTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Player, _newCharacterName, _worldCombo.CurrentSelection.Key, ObjectKind.None, - Array.Empty< uint >(), out _newPlayerIdentifiers ) switch - { - _ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty, - IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid, - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - _ => string.Empty, - }; - _newRetainerTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None, - Array.Empty< uint >(), out _newRetainerIdentifiers ) switch - { - _ when _newCharacterName.Length == 0 => NewRetainerTooltipEmpty, - IndividualCollections.AddResult.Invalid => NewRetainerTooltipInvalid, - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - _ => string.Empty, - }; - if( combo.CurrentSelection.Ids != null ) - { - _newNpcTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind, - combo.CurrentSelection.Ids, out _newNpcIdentifiers ) switch - { - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - _ => string.Empty, - }; - _newOwnedTooltip = Penumbra.CollectionManager.Individuals.CanAdd( IdentifierType.Owned, _newCharacterName, _worldCombo.CurrentSelection.Key, _newKind, - combo.CurrentSelection.Ids, out _newOwnedIdentifiers ) switch - { - _ when _newCharacterName.Length == 0 => NewPlayerTooltipEmpty, - IndividualCollections.AddResult.Invalid => NewPlayerTooltipInvalid, - IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, - _ => string.Empty, - }; - } - else - { - _newNpcTooltip = NewNpcTooltipEmpty; - _newOwnedTooltip = NewNpcTooltipEmpty; - _newNpcIdentifiers = Array.Empty< ActorIdentifier >(); - _newOwnedIdentifiers = Array.Empty< ActorIdentifier >(); - } - } - - private void UpdateIdentifiers( CollectionType type, ModCollection? _1, ModCollection? _2, string _3 ) - { - if( type == CollectionType.Individual ) - { - UpdateIdentifiers(); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs deleted file mode 100644 index fd64093f..00000000 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.Inheritance.cs +++ /dev/null @@ -1,317 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.Collections; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class CollectionsTab - { - private const int InheritedCollectionHeight = 9; - private const string InheritanceDragDropLabel = "##InheritanceMove"; - - // Keep for reuse. - private readonly HashSet< ModCollection > _seenInheritedCollections = new(32); - - // Execute changes only outside of loops. - private ModCollection? _newInheritance; - private ModCollection? _movedInheritance; - private (int, int)? _inheritanceAction; - private ModCollection? _newCurrentCollection; - - // Draw the whole inheritance block. - private void DrawInheritanceBlock() - { - using var group = ImRaii.Group(); - using var id = ImRaii.PushId( "##Inheritance" ); - ImGui.TextUnformatted( $"The {SelectedCollection} inherits from:" ); - DrawCurrentCollectionInheritance(); - DrawInheritanceTrashButton(); - DrawNewInheritanceSelection(); - DelayedActions(); - } - - // If an inherited collection is expanded, - // draw all its flattened, distinct children in order with a tree-line. - private void DrawInheritedChildren( ModCollection collection ) - { - using var id = ImRaii.PushId( collection.Index ); - using var indent = ImRaii.PushIndent(); - - // Get start point for the lines (top of the selector). - // Tree line stuff. - var lineStart = ImGui.GetCursorScreenPos(); - var offsetX = -ImGui.GetStyle().IndentSpacing + ImGui.GetTreeNodeToLabelSpacing() / 2; - var drawList = ImGui.GetWindowDrawList(); - var lineSize = Math.Max( 0, ImGui.GetStyle().IndentSpacing - 9 * ImGuiHelpers.GlobalScale ); - lineStart.X += offsetX; - lineStart.Y -= 2 * ImGuiHelpers.GlobalScale; - var lineEnd = lineStart; - - // Skip the collection itself. - foreach( var inheritance in collection.GetFlattenedInheritance().Skip( 1 ) ) - { - // Draw the child, already seen collections are colored as conflicts. - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.HandledConflictMod.Value(), - _seenInheritedCollections.Contains( inheritance ) ); - _seenInheritedCollections.Add( inheritance ); - - ImRaii.TreeNode( inheritance.Name, ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ); - var (minRect, maxRect) = ( ImGui.GetItemRectMin(), ImGui.GetItemRectMax() ); - DrawInheritanceTreeClicks( inheritance, false ); - - // Tree line stuff. - if( minRect.X == 0 ) - { - continue; - } - - // Draw the notch and increase the line length. - var midPoint = ( minRect.Y + maxRect.Y ) / 2f - 1f; - drawList.AddLine( new Vector2( lineStart.X, midPoint ), new Vector2( lineStart.X + lineSize, midPoint ), Colors.MetaInfoText, - ImGuiHelpers.GlobalScale ); - lineEnd.Y = midPoint; - } - - // Finally, draw the folder line. - drawList.AddLine( lineStart, lineEnd, Colors.MetaInfoText, ImGuiHelpers.GlobalScale ); - } - - // Draw a single primary inherited collection. - private void DrawInheritance( ModCollection collection ) - { - using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.HandledConflictMod.Value(), - _seenInheritedCollections.Contains( collection ) ); - _seenInheritedCollections.Add( collection ); - using var tree = ImRaii.TreeNode( collection.Name, ImGuiTreeNodeFlags.NoTreePushOnOpen ); - color.Pop(); - DrawInheritanceTreeClicks( collection, true ); - DrawInheritanceDropSource( collection ); - DrawInheritanceDropTarget( collection ); - - if( tree ) - { - DrawInheritedChildren( collection ); - } - else - { - // We still want to keep track of conflicts. - _seenInheritedCollections.UnionWith( collection.GetFlattenedInheritance() ); - } - } - - // Draw the list box containing the current inheritance information. - private void DrawCurrentCollectionInheritance() - { - using var list = ImRaii.ListBox( "##inheritanceList", - new Vector2( _window._inputTextWidth.X - _window._iconButtonSize.X - ImGui.GetStyle().ItemSpacing.X, - ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight ) ); - if( !list ) - { - return; - } - - _seenInheritedCollections.Clear(); - _seenInheritedCollections.Add( Penumbra.CollectionManager.Current ); - foreach( var collection in Penumbra.CollectionManager.Current.Inheritance.ToList() ) - { - DrawInheritance( collection ); - } - } - - // Draw a drag and drop button to delete. - private void DrawInheritanceTrashButton() - { - ImGui.SameLine(); - var size = new Vector2( _window._iconButtonSize.X, ImGui.GetTextLineHeightWithSpacing() * InheritedCollectionHeight ); - var buttonColor = ImGui.GetColorU32( ImGuiCol.Button ); - // Prevent hovering from highlighting the button. - using var color = ImRaii.PushColor( ImGuiCol.ButtonActive, buttonColor ) - .Push( ImGuiCol.ButtonHovered, buttonColor ); - ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size, - "Drag primary inheritance here to remove it from the list.", false, true ); - - using var target = ImRaii.DragDropTarget(); - if( target.Success && ImGuiUtil.IsDropping( InheritanceDragDropLabel ) ) - { - _inheritanceAction = ( Penumbra.CollectionManager.Current.Inheritance.IndexOf( _movedInheritance! ), -1 ); - } - } - - // Set the current collection, or delete or move an inheritance if the action was triggered during iteration. - // Can not be done during iteration to keep collections unchanged. - private void DelayedActions() - { - if( _newCurrentCollection != null ) - { - Penumbra.CollectionManager.SetCollection( _newCurrentCollection, CollectionType.Current ); - _newCurrentCollection = null; - } - - if( _inheritanceAction == null ) - { - return; - } - - if( _inheritanceAction.Value.Item1 >= 0 ) - { - if( _inheritanceAction.Value.Item2 == -1 ) - { - Penumbra.CollectionManager.Current.RemoveInheritance( _inheritanceAction.Value.Item1 ); - } - else - { - Penumbra.CollectionManager.Current.MoveInheritance( _inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2 ); - } - } - - _inheritanceAction = null; - } - - // Draw the selector to add new inheritances. - // The add button is only available if the selected collection can actually be added. - private void DrawNewInheritanceSelection() - { - DrawNewInheritanceCombo(); - ImGui.SameLine(); - var inheritance = Penumbra.CollectionManager.Current.CheckValidInheritance( _newInheritance ); - var tt = inheritance switch - { - ModCollection.ValidInheritance.Empty => "No valid collection to inherit from selected.", - ModCollection.ValidInheritance.Valid => $"Let the {SelectedCollection} inherit from this collection.", - ModCollection.ValidInheritance.Self => "The collection can not inherit from itself.", - ModCollection.ValidInheritance.Contained => "Already inheriting from this collection.", - ModCollection.ValidInheritance.Circle => "Inheriting from this collection would lead to cyclic inheritance.", - _ => string.Empty, - }; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), _window._iconButtonSize, tt, - inheritance != ModCollection.ValidInheritance.Valid, true ) - && Penumbra.CollectionManager.Current.AddInheritance( _newInheritance!, true ) ) - { - _newInheritance = null; - } - - if( inheritance != ModCollection.ValidInheritance.Valid ) - { - _newInheritance = null; - } - - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.QuestionCircle.ToIconString(), _window._iconButtonSize, "What is Inheritance?", - false, true ) ) - { - ImGui.OpenPopup( "InheritanceHelp" ); - } - - ImGuiUtil.HelpPopup( "InheritanceHelp", new Vector2( 1000 * ImGuiHelpers.GlobalScale, 21 * ImGui.GetTextLineHeightWithSpacing() ), () => - { - ImGui.NewLine(); - ImGui.TextWrapped( "Inheritance is a way to use a baseline of mods across multiple collections, without needing to change all those collections if you want to add a single mod." ); - ImGui.NewLine(); - ImGui.TextUnformatted( "Every mod in a collection can have three basic states: 'Enabled', 'Disabled' and 'Unconfigured'." ); - ImGui.BulletText( "If the mod is 'Enabled' or 'Disabled', it does not matter if the collection inherits from other collections." ); - ImGui.BulletText( "If the mod is unconfigured, those inherited-from collections are checked in the order displayed here, including sub-inheritances." ); - ImGui.BulletText( "If a collection is found in which the mod is either 'Enabled' or 'Disabled', the settings from this collection will be used." ); - ImGui.BulletText( "If no such collection is found, the mod will be treated as disabled." ); - ImGui.BulletText( "Highlighted collections in the left box are never reached because they are already checked in a sub-inheritance before." ); - ImGui.NewLine(); - ImGui.TextUnformatted( "Example" ); - ImGui.BulletText( "Collection A has the Bibo+ body and a Hempen Camise mod enabled." ); - ImGui.BulletText( "Collection B inherits from A, leaves Bibo+ unconfigured, but has the Hempen Camise enabled with different settings than A." ); - ImGui.BulletText( "Collection C also inherits from A, has Bibo+ explicitly disabled and the Hempen Camise unconfigured." ); - ImGui.BulletText( "Collection D inherits from C and then B and leaves everything unconfigured." ); - using var indent = ImRaii.PushIndent(); - ImGui.BulletText( "B uses Bibo+ settings from A and its own Hempen Camise settings." ); - ImGui.BulletText( "C has Bibo+ disabled and uses A's Hempen Camise settings." ); - ImGui.BulletText( "D has Bibo+ disabled and uses A's Hempen Camise settings, not B's. It traversed the collections in Order D -> (C -> A) -> (B -> A)." ); - } ); - } - - // Draw the combo to select new potential inheritances. - // Only valid inheritances are drawn in the preview, or nothing if no inheritance is available. - private void DrawNewInheritanceCombo() - { - ImGui.SetNextItemWidth( _window._inputTextWidth.X - _window._iconButtonSize.X - ImGui.GetStyle().ItemSpacing.X ); - _newInheritance ??= Penumbra.CollectionManager.FirstOrDefault( c - => c != Penumbra.CollectionManager.Current && !Penumbra.CollectionManager.Current.Inheritance.Contains( c ) ) - ?? ModCollection.Empty; - using var combo = ImRaii.Combo( "##newInheritance", _newInheritance.Name ); - if( !combo ) - { - return; - } - - foreach( var collection in Penumbra.CollectionManager - .Where( c => Penumbra.CollectionManager.Current.CheckValidInheritance( c ) == ModCollection.ValidInheritance.Valid ) - .OrderBy( c => c.Name )) - { - if( ImGui.Selectable( collection.Name, _newInheritance == collection ) ) - { - _newInheritance = collection; - } - } - } - - // Move an inherited collection when dropped onto another. - // Move is delayed due to collection changes. - private void DrawInheritanceDropTarget( ModCollection collection ) - { - using var target = ImRaii.DragDropTarget(); - if( target.Success && ImGuiUtil.IsDropping( InheritanceDragDropLabel ) ) - { - if( _movedInheritance != null ) - { - var idx1 = Penumbra.CollectionManager.Current.Inheritance.IndexOf( _movedInheritance ); - var idx2 = Penumbra.CollectionManager.Current.Inheritance.IndexOf( collection ); - if( idx1 >= 0 && idx2 >= 0 ) - { - _inheritanceAction = ( idx1, idx2 ); - } - } - - _movedInheritance = null; - } - } - - // Move an inherited collection. - private void DrawInheritanceDropSource( ModCollection collection ) - { - using var source = ImRaii.DragDropSource(); - if( source ) - { - ImGui.SetDragDropPayload( InheritanceDragDropLabel, IntPtr.Zero, 0 ); - _movedInheritance = collection; - ImGui.TextUnformatted( $"Moving {_movedInheritance?.Name ?? "Unknown"}..." ); - } - } - - // Ctrl + Right-Click -> Switch current collection to this (for all). - // Ctrl + Shift + Right-Click -> Delete this inheritance (only if withDelete). - // Deletion is delayed due to collection changes. - private void DrawInheritanceTreeClicks( ModCollection collection, bool withDelete ) - { - if( ImGui.GetIO().KeyCtrl && ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) - { - if( withDelete && ImGui.GetIO().KeyShift ) - { - _inheritanceAction = ( Penumbra.CollectionManager.Current.Inheritance.IndexOf( collection ), -1 ); - } - else - { - _newCurrentCollection = collection; - } - } - - ImGuiUtil.HoverTooltip( $"Control + Right-Click to switch the {SelectedCollection} to this one." - + ( withDelete ? "\nControl + Shift + Right-Click to remove this inheritance." : string.Empty ) ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs deleted file mode 100644 index bac726a2..00000000 --- a/Penumbra/UI/ConfigWindow.CollectionsTab.cs +++ /dev/null @@ -1,307 +0,0 @@ -using System; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using ImGuiNET; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Collections; -using Penumbra.Services; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - // Encapsulate for less pollution. - private partial class CollectionsTab : IDisposable, ITab - { - private readonly CommunicatorService _communicator; - private readonly ConfigWindow _window; - - public CollectionsTab( CommunicatorService communicator, ConfigWindow window ) - { - _window = window; - _communicator = communicator; - - _communicator.CollectionChange.Event += UpdateIdentifiers; - } - - public ReadOnlySpan Label - => "Collections"u8; - - public void Dispose() - => _communicator.CollectionChange.Event -= UpdateIdentifiers; - - public void DrawHeader() - => OpenTutorial( BasicTutorialSteps.Collections ); - - public void DrawContent() - { - using var child = ImRaii.Child( "##collections", -Vector2.One ); - if( child ) - { - DrawActiveCollectionSelectors(); - DrawMainSelectors(); - } - } - - // Input text fields. - private string _newCollectionName = string.Empty; - private bool _canAddCollection; - - // Create a new collection that is either empty or a duplicate of the current collection. - // Resets the new collection name. - private void CreateNewCollection( bool duplicate ) - { - if( Penumbra.CollectionManager.AddCollection( _newCollectionName, duplicate ? Penumbra.CollectionManager.Current : null ) ) - { - _newCollectionName = string.Empty; - } - } - - // Only gets drawn when actually relevant. - private static void DrawCleanCollectionButton( Vector2 width ) - { - if( Penumbra.CollectionManager.Current.HasUnusedSettings ) - { - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( - $"Clean {Penumbra.CollectionManager.Current.NumUnusedSettings} Unused Settings###CleanSettings", width - , "Remove all stored settings for mods not currently available and fix invalid settings.\n\nUse at own risk." - , false ) ) - { - Penumbra.CollectionManager.Current.CleanUnavailableSettings(); - } - } - } - - // Draw the new collection input as well as its buttons. - private void DrawNewCollectionInput( Vector2 width ) - { - // Input for new collection name. Also checks for validity when changed. - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - if( ImGui.InputTextWithHint( "##New Collection", "New Collection Name...", ref _newCollectionName, 64 ) ) - { - _canAddCollection = Penumbra.CollectionManager.CanAddCollection( _newCollectionName, out _ ); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "A collection is a set of settings for your installed mods, including their enabled status, their priorities and their mod-specific configuration.\n" - + "You can use multiple collections to quickly switch between sets of enabled mods." ); - - // Creation buttons. - var tt = _canAddCollection - ? string.Empty - : "Please enter a unique name only consisting of symbols valid in a path but no '|' before creating a collection."; - if( ImGuiUtil.DrawDisabledButton( "Create Empty Collection", width, tt, !_canAddCollection ) ) - { - CreateNewCollection( false ); - } - - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( $"Duplicate {SelectedCollection}", width, tt, !_canAddCollection ) ) - { - CreateNewCollection( true ); - } - } - - private void DrawCurrentCollectionSelector( Vector2 width ) - { - using var group = ImRaii.Group(); - DrawCollectionSelector( "##current", _window._inputTextWidth.X, CollectionType.Current, false ); - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( SelectedCollection, - "This collection will be modified when using the Installed Mods tab and making changes.\nIt is not automatically assigned to anything." ); - - // Deletion conditions. - var deleteCondition = Penumbra.CollectionManager.Current.Name != ModCollection.DefaultCollection; - var modifierHeld = Penumbra.Config.DeleteModModifier.IsActive(); - var tt = deleteCondition - ? modifierHeld ? string.Empty : $"Hold {Penumbra.Config.DeleteModModifier} while clicking to delete the collection." - : $"You can not delete the collection {ModCollection.DefaultCollection}."; - - if( ImGuiUtil.DrawDisabledButton( $"Delete {SelectedCollection}", width, tt, !deleteCondition || !modifierHeld ) ) - { - Penumbra.CollectionManager.RemoveCollection( Penumbra.CollectionManager.Current ); - } - - DrawCleanCollectionButton( width ); - } - - private void DrawDefaultCollectionSelector() - { - using var group = ImRaii.Group(); - DrawCollectionSelector( "##default", _window._inputTextWidth.X, CollectionType.Default, true ); - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( DefaultCollection, - $"Mods in the {DefaultCollection} are loaded for anything that is not associated with the user interface or a character in the game," - + "as well as any character for whom no more specific conditions from below apply." ); - } - - private void DrawInterfaceCollectionSelector() - { - using var group = ImRaii.Group(); - DrawCollectionSelector( "##interface", _window._inputTextWidth.X, CollectionType.Interface, true ); - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( InterfaceCollection, - $"Mods in the {InterfaceCollection} are loaded for any file that the game categorizes as an UI file. This is mostly icons as well as the tiles that generate the user interface windows themselves." ); - } - - private sealed class SpecialCombo : FilterComboBase< (CollectionType, string, string) > - { - public (CollectionType, string, string)? CurrentType - => CollectionTypeExtensions.Special[ CurrentIdx ]; - - public int CurrentIdx; - private readonly float _unscaledWidth; - private readonly string _label; - - public SpecialCombo( string label, float unscaledWidth ) - : base( CollectionTypeExtensions.Special, false ) - { - _label = label; - _unscaledWidth = unscaledWidth; - } - - public void Draw() - { - var preview = CurrentIdx >= 0 ? Items[ CurrentIdx ].Item2 : string.Empty; - Draw( _label, preview, string.Empty, ref CurrentIdx, _unscaledWidth * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeightWithSpacing() ); - } - - protected override string ToString( (CollectionType, string, string) obj ) - => obj.Item2; - - protected override bool IsVisible( int globalIdx, LowerString filter ) - { - var obj = Items[ globalIdx ]; - return filter.IsContained( obj.Item2 ) && Penumbra.CollectionManager.ByType( obj.Item1 ) == null; - } - } - - private readonly SpecialCombo _specialCollectionCombo = new("##NewSpecial", 350); - - private const string CharacterGroupDescription = $"{CharacterGroups} apply to certain types of characters based on a condition.\n" - + $"All of them take precedence before the {DefaultCollection},\n" - + $"but all {IndividualAssignments} take precedence before them."; - - - // We do not check for valid character names. - private void DrawNewSpecialCollection() - { - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - if( _specialCollectionCombo.CurrentIdx == -1 - || Penumbra.CollectionManager.ByType( _specialCollectionCombo.CurrentType!.Value.Item1 ) != null ) - { - _specialCollectionCombo.ResetFilter(); - _specialCollectionCombo.CurrentIdx = CollectionTypeExtensions.Special - .IndexOf( t => Penumbra.CollectionManager.ByType( t.Item1 ) == null ); - } - - if( _specialCollectionCombo.CurrentType == null ) - { - return; - } - - _specialCollectionCombo.Draw(); - ImGui.SameLine(); - var disabled = _specialCollectionCombo.CurrentType == null; - var tt = disabled - ? $"Please select a condition for a {GroupAssignment} before creating the collection.\n\n" + CharacterGroupDescription - : CharacterGroupDescription; - if( ImGuiUtil.DrawDisabledButton( $"Assign {ConditionalGroup}", new Vector2( 120 * ImGuiHelpers.GlobalScale, 0 ), tt, disabled ) ) - { - Penumbra.CollectionManager.CreateSpecialCollection( _specialCollectionCombo.CurrentType!.Value.Item1 ); - _specialCollectionCombo.CurrentIdx = -1; - } - } - - private void DrawSpecialCollections() - { - foreach( var (type, name, desc) in CollectionTypeExtensions.Special ) - { - var collection = Penumbra.CollectionManager.ByType( type ); - if( collection != null ) - { - using var id = ImRaii.PushId( ( int )type ); - DrawCollectionSelector( "##SpecialCombo", _window._inputTextWidth.X, type, true ); - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, - false, true ) ) - { - Penumbra.CollectionManager.RemoveSpecialCollection( type ); - _specialCollectionCombo.ResetFilter(); - } - - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGuiUtil.LabeledHelpMarker( name, desc ); - } - } - } - - private void DrawSpecialAssignments() - { - using var _ = ImRaii.Group(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted( CharacterGroups ); - ImGuiComponents.HelpMarker( CharacterGroupDescription ); - ImGui.Separator(); - DrawSpecialCollections(); - ImGui.Dummy( Vector2.Zero ); - DrawNewSpecialCollection(); - } - - private void DrawActiveCollectionSelectors() - { - ImGui.Dummy( _window._defaultSpace ); - var open = ImGui.CollapsingHeader( ActiveCollections, ImGuiTreeNodeFlags.DefaultOpen ); - OpenTutorial( BasicTutorialSteps.ActiveCollections ); - if( !open ) - { - return; - } - - ImGui.Dummy( _window._defaultSpace ); - DrawDefaultCollectionSelector(); - OpenTutorial( BasicTutorialSteps.DefaultCollection ); - DrawInterfaceCollectionSelector(); - OpenTutorial( BasicTutorialSteps.InterfaceCollection ); - ImGui.Dummy( _window._defaultSpace ); - - DrawSpecialAssignments(); - OpenTutorial( BasicTutorialSteps.SpecialCollections1 ); - - ImGui.Dummy( _window._defaultSpace ); - - DrawIndividualAssignments(); - OpenTutorial( BasicTutorialSteps.SpecialCollections2 ); - - ImGui.Dummy( _window._defaultSpace ); - } - - private void DrawMainSelectors() - { - ImGui.Dummy( _window._defaultSpace ); - var open = ImGui.CollapsingHeader( "Collection Settings", ImGuiTreeNodeFlags.DefaultOpen ); - OpenTutorial( BasicTutorialSteps.EditingCollections ); - if( !open ) - { - return; - } - - var width = new Vector2( ( _window._inputTextWidth.X - ImGui.GetStyle().ItemSpacing.X ) / 2, 0 ); - ImGui.Dummy( _window._defaultSpace ); - DrawCurrentCollectionSelector( width ); - OpenTutorial( BasicTutorialSteps.CurrentCollection ); - ImGui.Dummy( _window._defaultSpace ); - DrawNewCollectionInput( width ); - ImGui.Dummy( _window._defaultSpace ); - DrawInheritanceBlock(); - OpenTutorial( BasicTutorialSteps.Inheritance ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs deleted file mode 100644 index 0a59b517..00000000 --- a/Penumbra/UI/ConfigWindow.DebugTab.cs +++ /dev/null @@ -1,644 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Numerics; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Game.Group; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using ImGuiNET; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Widgets; -using Penumbra.GameData.Actors; -using Penumbra.GameData.Files; -using Penumbra.Interop.Loader; -using Penumbra.Interop.Resolver; -using Penumbra.Interop.Structs; -using Penumbra.Services; -using Penumbra.String; -using Penumbra.Util; -using static OtterGui.Raii.ImRaii; -using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; -using CharacterUtility = Penumbra.Interop.CharacterUtility; -using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private class DebugTab : ITab - { - private readonly StartTracker _timer; - private readonly ConfigWindow _window; - - public DebugTab( ConfigWindow window, StartTracker timer) - { - _window = window; - _timer = timer; - } - - public ReadOnlySpan Label - => "Debug"u8; - - public bool IsVisible - => Penumbra.Config.DebugMode; - -#if DEBUG - private const string DebugVersionString = "(Debug)"; -#else - private const string DebugVersionString = "(Release)"; -#endif - - public void DrawContent() - { - using var child = Child( "##DebugTab", -Vector2.One ); - if( !child ) - { - return; - } - - DrawDebugTabGeneral(); - DrawPerformanceTab(); - ImGui.NewLine(); - DrawPathResolverDebug(); - ImGui.NewLine(); - DrawActorsDebug(); - ImGui.NewLine(); - DrawDebugCharacterUtility(); - ImGui.NewLine(); - DrawStainTemplates(); - ImGui.NewLine(); - DrawDebugTabMetaLists(); - ImGui.NewLine(); - DrawDebugResidentResources(); - ImGui.NewLine(); - DrawResourceProblems(); - ImGui.NewLine(); - DrawPlayerModelInfo(); - ImGui.NewLine(); - DrawDebugTabIpc(); - ImGui.NewLine(); - } - - // Draw general information about mod and collection state. - private void DrawDebugTabGeneral() - { - if( !ImGui.CollapsingHeader( "General" ) ) - { - return; - } - - using var table = Table( "##DebugGeneralTable", 2, ImGuiTableFlags.SizingFixedFit, - new Vector2( -1, ImGui.GetTextLineHeightWithSpacing() * 1 ) ); - if( !table ) - { - return; - } - - var manager = Penumbra.ModManager; - PrintValue( "Penumbra Version", $"{Penumbra.Version} {DebugVersionString}" ); - PrintValue( "Git Commit Hash", Penumbra.CommitHash ); - PrintValue( SelectedCollection, Penumbra.CollectionManager.Current.Name ); - PrintValue( " has Cache", Penumbra.CollectionManager.Current.HasCache.ToString() ); - PrintValue( DefaultCollection, Penumbra.CollectionManager.Default.Name ); - PrintValue( " has Cache", Penumbra.CollectionManager.Default.HasCache.ToString() ); - PrintValue( "Mod Manager BasePath", manager.BasePath.Name ); - PrintValue( "Mod Manager BasePath-Full", manager.BasePath.FullName ); - PrintValue( "Mod Manager BasePath IsRooted", Path.IsPathRooted( Penumbra.Config.ModDirectory ).ToString() ); - PrintValue( "Mod Manager BasePath Exists", Directory.Exists( manager.BasePath.FullName ).ToString() ); - PrintValue( "Mod Manager Valid", manager.Valid.ToString() ); - PrintValue( "Path Resolver Enabled", _window._penumbra.PathResolver.Enabled.ToString() ); - PrintValue( "Web Server Enabled", _window._penumbra.HttpApi.Enabled.ToString() ); - } - - private void DrawPerformanceTab() - { - ImGui.NewLine(); - if( ImGui.CollapsingHeader( "Performance" ) ) - { - return; - } - - using( var start = TreeNode( "Startup Performance", ImGuiTreeNodeFlags.DefaultOpen ) ) - { - if( start ) - { - _timer.Draw( "##startTimer", TimingExtensions.ToName ); - ImGui.NewLine(); - } - } - - Penumbra.Performance.Draw( "##performance", "Enable Runtime Performance Tracking", TimingExtensions.ToName ); - } - - private static unsafe void DrawActorsDebug() - { - if( !ImGui.CollapsingHeader( "Actors" ) ) - { - return; - } - - using var table = Table( "##actors", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, - -Vector2.UnitX ); - if( !table ) - { - return; - } - - static void DrawSpecial( string name, ActorIdentifier id ) - { - if( !id.IsValid ) - { - return; - } - - ImGuiUtil.DrawTableColumn( name ); - ImGuiUtil.DrawTableColumn( string.Empty ); - ImGuiUtil.DrawTableColumn( Penumbra.Actors.ToString( id ) ); - ImGuiUtil.DrawTableColumn( string.Empty ); - } - - DrawSpecial( "Current Player", Penumbra.Actors.GetCurrentPlayer() ); - DrawSpecial( "Current Inspect", Penumbra.Actors.GetInspectPlayer() ); - DrawSpecial( "Current Card", Penumbra.Actors.GetCardPlayer() ); - DrawSpecial( "Current Glamour", Penumbra.Actors.GetGlamourPlayer() ); - - foreach( var obj in DalamudServices.Objects ) - { - ImGuiUtil.DrawTableColumn( $"{( ( GameObject* )obj.Address )->ObjectIndex}" ); - ImGuiUtil.DrawTableColumn( $"0x{obj.Address:X}" ); - var identifier = Penumbra.Actors.FromObject( obj, false, true, false ); - ImGuiUtil.DrawTableColumn( Penumbra.Actors.ToString( identifier ) ); - var id = obj.ObjectKind == ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.DataId}" : identifier.DataId.ToString(); - ImGuiUtil.DrawTableColumn( id ); - } - } - - // Draw information about which draw objects correspond to which game objects - // and which paths are due to be loaded by which collection. - private unsafe void DrawPathResolverDebug() - { - if( !ImGui.CollapsingHeader( "Path Resolver" ) ) - { - return; - } - - ImGui.TextUnformatted( - $"Last Game Object: 0x{_window._penumbra.PathResolver.LastGameObject:X} ({_window._penumbra.PathResolver.LastGameObjectData.ModCollection.Name})" ); - using( var drawTree = TreeNode( "Draw Object to Object" ) ) - { - if( drawTree ) - { - using var table = Table( "###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit ); - if( table ) - { - foreach( var (ptr, (c, idx)) in _window._penumbra.PathResolver.DrawObjectMap ) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted( ptr.ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( idx.ToString() ); - ImGui.TableNextColumn(); - var obj = ( GameObject* )DalamudServices.Objects.GetObjectAddress( idx ); - var (address, name) = - obj != null ? ( $"0x{( ulong )obj:X}", new ByteString( obj->Name ).ToString() ) : ( "NULL", "NULL" ); - ImGui.TextUnformatted( address ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( name ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( c.ModCollection.Name ); - } - } - } - } - - using( var pathTree = TreeNode( "Path Collections" ) ) - { - if( pathTree ) - { - using var table = Table( "###PathCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit ); - if( table ) - { - foreach( var (path, collection) in _window._penumbra.PathResolver.PathCollections ) - { - ImGui.TableNextColumn(); - ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( collection.ModCollection.Name ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( collection.AssociatedGameObject.ToString( "X" ) ); - } - } - } - } - - using( var resourceTree = TreeNode( "Subfile Collections" ) ) - { - if( resourceTree ) - { - using var table = Table( "###ResourceCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit ); - if( table ) - { - ImGuiUtil.DrawTableColumn( "Current Mtrl Data" ); - ImGuiUtil.DrawTableColumn( _window._penumbra.PathResolver.CurrentMtrlData.ModCollection.Name ); - ImGuiUtil.DrawTableColumn( $"0x{_window._penumbra.PathResolver.CurrentMtrlData.AssociatedGameObject:X}" ); - - ImGuiUtil.DrawTableColumn( "Current Avfx Data" ); - ImGuiUtil.DrawTableColumn( _window._penumbra.PathResolver.CurrentAvfxData.ModCollection.Name ); - ImGuiUtil.DrawTableColumn( $"0x{_window._penumbra.PathResolver.CurrentAvfxData.AssociatedGameObject:X}" ); - - ImGuiUtil.DrawTableColumn( "Current Resources" ); - ImGuiUtil.DrawTableColumn( _window._penumbra.PathResolver.SubfileCount.ToString() ); - ImGui.TableNextColumn(); - - foreach( var (resource, resolve) in _window._penumbra.PathResolver.ResourceCollections ) - { - ImGuiUtil.DrawTableColumn( $"0x{resource:X}" ); - ImGuiUtil.DrawTableColumn( resolve.ModCollection.Name ); - ImGuiUtil.DrawTableColumn( $"0x{resolve.AssociatedGameObject:X}" ); - } - } - } - } - - using( var identifiedTree = TreeNode( "Identified Collections" ) ) - { - if( identifiedTree ) - { - using var table = Table( "##PathCollectionsIdentifiedTable", 3, ImGuiTableFlags.SizingFixedFit ); - if( table ) - { - foreach( var (address, identifier, collection) in PathResolver.IdentifiedCache ) - { - ImGuiUtil.DrawTableColumn( $"0x{address:X}" ); - ImGuiUtil.DrawTableColumn( identifier.ToString() ); - ImGuiUtil.DrawTableColumn( collection.Name ); - } - } - } - } - - using( var cutsceneTree = TreeNode( "Cutscene Actors" ) ) - { - if( cutsceneTree ) - { - using var table = Table( "###PCutsceneResolverTable", 2, ImGuiTableFlags.SizingFixedFit ); - if( table ) - { - foreach( var (idx, actor) in _window._penumbra.PathResolver.CutsceneActors ) - { - ImGuiUtil.DrawTableColumn( $"Cutscene Actor {idx}" ); - ImGuiUtil.DrawTableColumn( actor.Name.ToString() ); - } - } - } - } - - using( var groupTree = TreeNode( "Group" ) ) - { - if( groupTree ) - { - using var table = Table( "###PGroupTable", 2, ImGuiTableFlags.SizingFixedFit ); - if( table ) - { - ImGuiUtil.DrawTableColumn( "Group Members" ); - ImGuiUtil.DrawTableColumn( GroupManager.Instance()->MemberCount.ToString() ); - for( var i = 0; i < 8; ++i ) - { - ImGuiUtil.DrawTableColumn( $"Member #{i}" ); - var member = GroupManager.Instance()->GetPartyMemberByIndex( i ); - ImGuiUtil.DrawTableColumn( member == null ? "NULL" : new ByteString( member->Name ).ToString() ); - } - } - } - } - - using( var bannerTree = TreeNode( "Party Banner" ) ) - { - if( bannerTree ) - { - var agent = &AgentBannerParty.Instance()->AgentBannerInterface; - if( agent->Data == null ) - { - agent = &AgentBannerMIP.Instance()->AgentBannerInterface; - } - - if( agent->Data != null ) - { - using var table = Table( "###PBannerTable", 2, ImGuiTableFlags.SizingFixedFit ); - if( table ) - { - for( var i = 0; i < 8; ++i ) - { - var c = agent->Character( i ); - ImGuiUtil.DrawTableColumn( $"Character {i}" ); - var name = c->Name1.ToString(); - ImGuiUtil.DrawTableColumn( name.Length == 0 ? "NULL" : $"{name} ({c->WorldId})" ); - } - } - } - else - { - ImGui.TextUnformatted( "INACTIVE" ); - } - } - } - } - - private static unsafe void DrawStainTemplates() - { - if( !ImGui.CollapsingHeader( "Staining Templates" ) ) - { - return; - } - - foreach( var (key, data) in Penumbra.StainService.StmFile.Entries ) - { - using var tree = TreeNode( $"Template {key}" ); - if( !tree ) - { - continue; - } - - using var table = Table( "##table", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); - if( !table ) - { - continue; - } - - for( var i = 0; i < StmFile.StainingTemplateEntry.NumElements; ++i ) - { - var (r, g, b) = data.DiffuseEntries[ i ]; - ImGuiUtil.DrawTableColumn( $"{r:F6} | {g:F6} | {b:F6}" ); - - ( r, g, b ) = data.SpecularEntries[ i ]; - ImGuiUtil.DrawTableColumn( $"{r:F6} | {g:F6} | {b:F6}" ); - - ( r, g, b ) = data.EmissiveEntries[ i ]; - ImGuiUtil.DrawTableColumn( $"{r:F6} | {g:F6} | {b:F6}" ); - - var a = data.SpecularPowerEntries[ i ]; - ImGuiUtil.DrawTableColumn( $"{a:F6}" ); - - a = data.GlossEntries[ i ]; - ImGuiUtil.DrawTableColumn( $"{a:F6}" ); - } - } - } - - // Draw information about the character utility class from SE, - // displaying all files, their sizes, the default files and the default sizes. - public static unsafe void DrawDebugCharacterUtility() - { - if( !ImGui.CollapsingHeader( "Character Utility" ) ) - { - return; - } - - using var table = Table( "##CharacterUtility", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, - -Vector2.UnitX ); - if( !table ) - { - return; - } - - for( var i = 0; i < CharacterUtility.RelevantIndices.Length; ++i ) - { - var idx = CharacterUtility.RelevantIndices[ i ]; - var intern = new CharacterUtility.InternalIndex( i ); - var resource = ( ResourceHandle* )Penumbra.CharacterUtility.Address->Resource( idx ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"0x{( ulong )resource:X}" ); - ImGui.TableNextColumn(); - Text( resource ); - ImGui.TableNextColumn(); - ImGui.Selectable( $"0x{resource->GetData().Data:X}" ); - if( ImGui.IsItemClicked() ) - { - var (data, length) = resource->GetData(); - if( data != IntPtr.Zero && length > 0 ) - { - ImGui.SetClipboardText( string.Join( "\n", - new ReadOnlySpan< byte >( ( byte* )data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); - } - } - - ImGuiUtil.HoverTooltip( "Click to copy bytes to clipboard." ); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"{resource->GetData().Length}" ); - ImGui.TableNextColumn(); - ImGui.Selectable( $"0x{Penumbra.CharacterUtility.DefaultResource( intern ).Address:X}" ); - if( ImGui.IsItemClicked() ) - { - ImGui.SetClipboardText( string.Join( "\n", - new ReadOnlySpan< byte >( ( byte* )Penumbra.CharacterUtility.DefaultResource( intern ).Address, - Penumbra.CharacterUtility.DefaultResource( intern ).Size ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); - } - - ImGuiUtil.HoverTooltip( "Click to copy bytes to clipboard." ); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"{Penumbra.CharacterUtility.DefaultResource( intern ).Size}" ); - } - } - - private static void DrawDebugTabMetaLists() - { - if( !ImGui.CollapsingHeader( "Metadata Changes" ) ) - { - return; - } - - using var table = Table( "##DebugMetaTable", 3, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { - return; - } - - foreach( var list in Penumbra.CharacterUtility.Lists ) - { - ImGuiUtil.DrawTableColumn( list.GlobalIndex.ToString() ); - ImGuiUtil.DrawTableColumn( list.Entries.Count.ToString() ); - ImGuiUtil.DrawTableColumn( string.Join( ", ", list.Entries.Select( e => $"0x{e.Data:X}" ) ) ); - } - } - - // Draw information about the resident resource files. - public unsafe void DrawDebugResidentResources() - { - if( !ImGui.CollapsingHeader( "Resident Resources" ) ) - { - return; - } - - if( Penumbra.ResidentResources.Address == null || Penumbra.ResidentResources.Address->NumResources == 0 ) - { - return; - } - - using var table = Table( "##ResidentResources", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, - -Vector2.UnitX ); - if( !table ) - { - return; - } - - for( var i = 0; i < Penumbra.ResidentResources.Address->NumResources; ++i ) - { - var resource = Penumbra.ResidentResources.Address->ResourceList[ i ]; - ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"0x{( ulong )resource:X}" ); - ImGui.TableNextColumn(); - Text( resource ); - } - } - - // Draw information about the models, materials and resources currently loaded by the local player. - private static unsafe void DrawPlayerModelInfo() - { - var player = DalamudServices.ClientState.LocalPlayer; - var name = player?.Name.ToString() ?? "NULL"; - if( !ImGui.CollapsingHeader( $"Player Model Info: {name}##Draw" ) || player == null ) - { - return; - } - - var model = ( CharacterBase* )( ( Character* )player.Address )->GameObject.GetDrawObject(); - if( model == null ) - { - return; - } - - using( var t1 = Table( "##table", 2, ImGuiTableFlags.SizingFixedFit ) ) - { - if( t1 ) - { - ImGuiUtil.DrawTableColumn( "Flags" ); - ImGuiUtil.DrawTableColumn( $"{model->UnkFlags_01:X2}" ); - ImGuiUtil.DrawTableColumn( "Has Model In Slot Loaded" ); - ImGuiUtil.DrawTableColumn( $"{model->HasModelInSlotLoaded:X8}" ); - ImGuiUtil.DrawTableColumn( "Has Model Files In Slot Loaded" ); - ImGuiUtil.DrawTableColumn( $"{model->HasModelFilesInSlotLoaded:X8}" ); - } - } - - using var table = Table( $"##{name}DrawTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { - return; - } - - ImGui.TableNextColumn(); - ImGui.TableHeader( "Slot" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Imc Ptr" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Imc File" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Model Ptr" ); - ImGui.TableNextColumn(); - ImGui.TableHeader( "Model File" ); - - for( var i = 0; i < model->SlotCount; ++i ) - { - var imc = ( ResourceHandle* )model->IMCArray[ i ]; - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( $"Slot {i}" ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( imc == null ? "NULL" : $"0x{( ulong )imc:X}" ); - ImGui.TableNextColumn(); - if( imc != null ) - { - Text( imc ); - } - - var mdl = ( RenderModel* )model->ModelArray[ i ]; - ImGui.TableNextColumn(); - ImGui.TextUnformatted( mdl == null ? "NULL" : $"0x{( ulong )mdl:X}" ); - if( mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara ) - { - continue; - } - - ImGui.TableNextColumn(); - { - Text( mdl->ResourceHandle ); - } - } - } - - // Draw resources with unusual reference count. - private static unsafe void DrawResourceProblems() - { - var header = ImGui.CollapsingHeader( "Resource Problems" ); - ImGuiUtil.HoverTooltip( "Draw resources with unusually high reference count to detect overflows." ); - if( !header ) - { - return; - } - - using var table = Table( "##ProblemsTable", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { - return; - } - - Penumbra.ResourceManagerService.IterateResources( ( _, r ) => - { - if( r->RefCount < 10000 ) - { - return; - } - - ImGui.TableNextColumn(); - ImGui.TextUnformatted( r->Category.ToString() ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( r->FileType.ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( r->Id.ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( ( ( ulong )r ).ToString( "X" ) ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( r->RefCount.ToString() ); - ImGui.TableNextColumn(); - ref var name = ref r->FileName; - if( name.Capacity > 15 ) - { - ImGuiNative.igTextUnformatted( name.BufferPtr, name.BufferPtr + name.Length ); - } - else - { - fixed( byte* ptr = name.Buffer ) - { - ImGuiNative.igTextUnformatted( ptr, ptr + name.Length ); - } - } - } ); - } - - - // Draw information about IPC options and availability. - private void DrawDebugTabIpc() - { - if( !ImGui.CollapsingHeader( "IPC" ) ) - { - _window._penumbra.IpcProviders.Tester.UnsubscribeEvents(); - return; - } - - _window._penumbra.IpcProviders.Tester.Draw(); - } - - // Helper to print a property and its value in a 2-column table. - private static void PrintValue( string name, string value ) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted( name ); - ImGui.TableNextColumn(); - ImGui.TextUnformatted( value ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs deleted file mode 100644 index a90df004..00000000 --- a/Penumbra/UI/ConfigWindow.EffectiveTab.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Collections; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods; -using Penumbra.String.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private class EffectiveTab : ITab - { - public ReadOnlySpan Label - => "Effective Changes"u8; - - public void DrawContent() - { - SetupEffectiveSizes(); - DrawFilters(); - using var child = ImRaii.Child( "##EffectiveChangesTab", -Vector2.One, false ); - if( !child ) - { - return; - } - - var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - var skips = ImGuiClip.GetNecessarySkips( height ); - using var table = ImRaii.Table( "##EffectiveChangesTable", 3, ImGuiTableFlags.RowBg ); - if( !table ) - { - return; - } - - ImGui.TableSetupColumn( "##gamePath", ImGuiTableColumnFlags.WidthFixed, _effectiveLeftTextLength ); - ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, _effectiveArrowLength ); - ImGui.TableSetupColumn( "##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength ); - - DrawEffectiveRows( Penumbra.CollectionManager.Current, skips, height, - _effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0 ); - } - - // Sizes - private float _effectiveLeftTextLength; - private float _effectiveRightTextLength; - private float _effectiveUnscaledArrowLength; - private float _effectiveArrowLength; - - // Filters - private LowerString _effectiveGamePathFilter = LowerString.Empty; - private LowerString _effectiveFilePathFilter = LowerString.Empty; - - // Setup table sizes. - private void SetupEffectiveSizes() - { - if( _effectiveUnscaledArrowLength == 0 ) - { - using var font = ImRaii.PushFont( UiBuilder.IconFont ); - _effectiveUnscaledArrowLength = - ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltLeft.ToIconString() ).X / ImGuiHelpers.GlobalScale; - } - - _effectiveArrowLength = _effectiveUnscaledArrowLength * ImGuiHelpers.GlobalScale; - _effectiveLeftTextLength = 450 * ImGuiHelpers.GlobalScale; - _effectiveRightTextLength = ImGui.GetWindowSize().X - _effectiveArrowLength - _effectiveLeftTextLength; - } - - // Draw the header line for filters - private void DrawFilters() - { - var tmp = _effectiveGamePathFilter.Text; - ImGui.SetNextItemWidth( _effectiveLeftTextLength ); - if( ImGui.InputTextWithHint( "##gamePathFilter", "Filter game path...", ref tmp, 256 ) ) - { - _effectiveGamePathFilter = tmp; - } - - ImGui.SameLine( _effectiveArrowLength + _effectiveLeftTextLength + 3 * ImGui.GetStyle().ItemSpacing.X ); - ImGui.SetNextItemWidth( -1 ); - tmp = _effectiveFilePathFilter.Text; - if( ImGui.InputTextWithHint( "##fileFilter", "Filter file path...", ref tmp, 256 ) ) - { - _effectiveFilePathFilter = tmp; - } - } - - // Draw all rows 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.Select( p => ( p.Key.ToString(), p.Value.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, 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< Utf8GamePath, ModPath > pair ) - { - var (path, name) = pair; - ImGui.TableNextColumn(); - CopyOnClickSelectable( path.Path ); - - ImGui.TableNextColumn(); - ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); - ImGui.TableNextColumn(); - CopyOnClickSelectable( name.Path.InternalName ); - ImGuiUtil.HoverTooltip( $"\nChanged by {name.Mod.Name}." ); - } - - // Draw a line for a path and its name. - private static void DrawLine( (string, LowerString) 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( KeyValuePair< MetaManipulation, IMod > pair ) - { - var (manipulation, mod) = pair; - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable( manipulation.ToString() ?? string.Empty ); - - ImGui.TableNextColumn(); - ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable( mod.Name ); - } - - // Check filters for file replacements. - private bool CheckFilters( KeyValuePair< Utf8GamePath, ModPath > kvp ) - { - var (gamePath, fullPath) = kvp; - if( _effectiveGamePathFilter.Length > 0 && !gamePath.ToString().Contains( _effectiveGamePathFilter.Lower ) ) - { - return false; - } - - return _effectiveFilePathFilter.Length == 0 || fullPath.Path.FullName.ToLowerInvariant().Contains( _effectiveFilePathFilter.Lower ); - } - - // Check filters for meta manipulations. - private bool CheckFilters( (string, LowerString) kvp ) - { - var (name, path) = kvp; - if( _effectiveGamePathFilter.Length > 0 && !name.ToLowerInvariant().Contains( _effectiveGamePathFilter.Lower ) ) - { - return false; - } - - return _effectiveFilePathFilter.Length == 0 || path.Contains( _effectiveFilePathFilter.Lower ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs deleted file mode 100644 index b683b75e..00000000 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using ImGuiNET; -using Lumina.Data.Parsing; -using Lumina.Excel.GeneratedSheets; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Api.Enums; -using Penumbra.Collections; -using Penumbra.GameData.Actors; -using Penumbra.GameData.Enums; -using Penumbra.Interop.Structs; -using Penumbra.String; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - // Draw text given by a ByteString. - internal static unsafe void Text( ByteString s ) - => ImGuiNative.igTextUnformatted( s.Path, s.Path + s.Length ); - - // Draw text given by a byte pointer. - private static unsafe void Text( byte* s, int length ) - => ImGuiNative.igTextUnformatted( s, s + length ); - - // Draw the name of a resource file. - private static unsafe void Text( ResourceHandle* resource ) - => Text( resource->FileName().Path, resource->FileNameLength ); - - // Draw a ByteString as a selectable. - internal static unsafe bool Selectable( ByteString s, bool selected ) - { - var tmp = ( byte )( selected ? 1 : 0 ); - return ImGuiNative.igSelectable_Bool( s.Path, tmp, ImGuiSelectableFlags.None, Vector2.Zero ) != 0; - } - - // Apply Changed Item Counters to the Name if necessary. - private static string ChangedItemName( string name, object? data ) - => data is int counter ? $"{counter} Files Manipulating {name}s" : name; - - // Draw a changed item, invoking the Api-Events for clicks and tooltips. - // Also draw the item Id in grey if requested - private void DrawChangedItem( string name, object? data, bool drawId ) - { - name = ChangedItemName( name, data ); - var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; - ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret; - ret = ImGui.IsItemClicked( ImGuiMouseButton.Middle ) ? MouseButton.Middle : ret; - - if( ret != MouseButton.None ) - { - _penumbra.Api.InvokeClick( ret, data ); - } - - if( _penumbra.Api.HasTooltip && ImGui.IsItemHovered() ) - { - // We can not be sure that any subscriber actually prints something in any case. - // Circumvent ugly blank tooltip with less-ugly useless tooltip. - using var tt = ImRaii.Tooltip(); - using var group = ImRaii.Group(); - _penumbra.Api.InvokeTooltip( data ); - group.Dispose(); - if( ImGui.GetItemRectSize() == Vector2.Zero ) - { - ImGui.TextUnformatted( "No actions available." ); - } - } - - if( drawId && DrawChangedItemObject( data, out var text ) ) - { - ImGui.SameLine( ImGui.GetContentRegionAvail().X ); - ImGuiUtil.RightJustify( text, ColorId.ItemId.Value() ); - } - } - - private static bool DrawChangedItemObject( object? obj, out string text ) - { - switch( obj ) - { - case Item it: - var quad = ( Quad )it.ModelMain; - text = quad.C == 0 ? $"({quad.A}-{quad.B})" : $"({quad.A}-{quad.B}-{quad.C})"; - return true; - case ModelChara m: - text = $"({( ( CharacterBase.ModelType )m.Type ).ToName()} {m.Model}-{m.Base}-{m.Variant})"; - return true; - default: - text = string.Empty; - return false; - } - } - - // A selectable that copies its text to clipboard on selection and provides a on-hover tooltip about that, - // using an ByteString. - private static unsafe void CopyOnClickSelectable( ByteString text ) - { - if( ImGuiNative.igSelectable_Bool( text.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ) != 0 ) - { - ImGuiNative.igSetClipboardText( text.Path ); - } - - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( "Click to copy to clipboard." ); - } - } - - private sealed class CollectionSelector : FilterComboCache< ModCollection > - { - public CollectionSelector( Func> items ) - : base( items ) - { } - - public void Draw( string label, float width, int individualIdx ) - { - var (_, collection) = Penumbra.CollectionManager.Individuals[ individualIdx ]; - if( Draw( label, collection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing() ) && CurrentSelection != null ) - { - Penumbra.CollectionManager.SetCollection( CurrentSelection, CollectionType.Individual, individualIdx ); - } - } - - public void Draw( string label, float width, CollectionType type ) - { - var current = Penumbra.CollectionManager.ByType( type, ActorIdentifier.Invalid ); - if( Draw( label, current?.Name ?? string.Empty, string.Empty, width, ImGui.GetTextLineHeightWithSpacing() ) && CurrentSelection != null ) - { - Penumbra.CollectionManager.SetCollection( CurrentSelection, type ); - } - } - - protected override string ToString( ModCollection obj ) - => obj.Name; - } - - private static readonly CollectionSelector CollectionsWithEmpty = new(() => Penumbra.CollectionManager.OrderBy( c => c.Name ).Prepend( ModCollection.Empty ).ToList()); - private static readonly CollectionSelector Collections = new(() => Penumbra.CollectionManager.OrderBy( c => c.Name ).ToList()); - - // Draw a collection selector of a certain width for a certain type. - private static void DrawCollectionSelector( string label, float width, CollectionType collectionType, bool withEmpty ) - => ( withEmpty ? CollectionsWithEmpty : Collections ).Draw( label, width, collectionType ); - - // Set up the file selector with the right flags and custom side bar items. - public static FileDialogManager SetupFileManager() - { - var fileManager = new FileDialogManager - { - AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking, - }; - - if( Functions.GetDownloadsFolder( out var downloadsFolder ) ) - { - fileManager.CustomSideBarItems.Add( ( "Downloads", downloadsFolder, FontAwesomeIcon.Download, -1 ) ); - } - - if( Functions.GetQuickAccessFolders( out var folders ) ) - { - foreach( var ((name, path), idx) in folders.WithIndex() ) - { - fileManager.CustomSideBarItems.Add( ( $"{name}##{idx}", path, FontAwesomeIcon.Folder, -1 ) ); - } - } - - // Add Penumbra Root. This is not updated if the root changes right now. - fileManager.CustomSideBarItems.Add( ( "Root Directory", Penumbra.Config.ModDirectory, FontAwesomeIcon.Gamepad, 0 ) ); - - // Remove Videos and Music. - fileManager.CustomSideBarItems.Add( ( "Videos", string.Empty, 0, -1 ) ); - fileManager.CustomSideBarItems.Add( ( "Music", string.Empty, 0, -1 ) ); - - return fileManager; - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs deleted file mode 100644 index ca5de44b..00000000 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ /dev/null @@ -1,776 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.Api.Enums; -using Penumbra.Mods; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class ModPanel - { - private Vector2 _cellPadding = Vector2.Zero; - private Vector2 _itemSpacing = Vector2.Zero; - - // Draw the edit tab that contains all things concerning editing the mod. - private void DrawEditModTab() - { - using var tab = DrawTab( EditModTabHeader, Tabs.Edit ); - if( !tab ) - { - return; - } - - using var child = ImRaii.Child( "##editChild", -Vector2.One ); - if( !child ) - { - return; - } - - _cellPadding = ImGui.GetStyle().CellPadding with { X = 2 * ImGuiHelpers.GlobalScale }; - _itemSpacing = ImGui.GetStyle().CellPadding with { X = 4 * ImGuiHelpers.GlobalScale }; - - EditButtons(); - EditRegularMeta(); - ImGui.Dummy( _window._defaultSpace ); - - if( Input.Text( "Mod Path", Input.Path, Input.None, _leaf.FullName(), out var newPath, 256, - _window._inputTextWidth.X ) ) - { - try - { - _window._penumbra.ModFileSystem.RenameAndMove( _leaf, newPath ); - } - catch( Exception e ) - { - Penumbra.Log.Warning( e.Message ); - } - } - - ImGui.Dummy( _window._defaultSpace ); - var tagIdx = _modTags.Draw( "Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, out var editedTag ); - if( tagIdx >= 0 ) - { - Penumbra.ModManager.ChangeModTag( _mod.Index, tagIdx, editedTag ); - } - - ImGui.Dummy( _window._defaultSpace ); - AddOptionGroup.Draw( _window, _mod ); - ImGui.Dummy( _window._defaultSpace ); - - for( var groupIdx = 0; groupIdx < _mod.Groups.Count; ++groupIdx ) - { - EditGroup( groupIdx ); - } - - EndActions(); - DescriptionEdit.DrawPopup( _window ); - } - - // The general edit row for non-detailed mod edits. - private void EditButtons() - { - var buttonSize = new Vector2( 150 * ImGuiHelpers.GlobalScale, 0 ); - var folderExists = Directory.Exists( _mod.ModPath.FullName ); - var tt = folderExists - ? $"Open \"{_mod.ModPath.FullName}\" in the file explorer of your choice." - : $"Mod directory \"{_mod.ModPath.FullName}\" does not exist."; - if( ImGuiUtil.DrawDisabledButton( "Open Mod Directory", buttonSize, tt, !folderExists ) ) - { - Process.Start( new ProcessStartInfo( _mod.ModPath.FullName ) { UseShellExecute = true } ); - } - - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Reload Mod", buttonSize, "Reload the current mod from its files.\n" - + "If the mod directory or meta file do not exist anymore or if the new mod name is empty, the mod is deleted instead.", - false ) ) - { - Penumbra.ModManager.ReloadMod( _mod.Index ); - } - - BackupButtons( buttonSize ); - MoveDirectory.Draw( _mod, buttonSize ); - - ImGui.Dummy( _window._defaultSpace ); - DrawUpdateBibo( buttonSize ); - - ImGui.Dummy( _window._defaultSpace ); - } - - private void DrawUpdateBibo( Vector2 buttonSize ) - { - if( ImGui.Button( "Update Bibo Material", buttonSize ) ) - { - var editor = new Mod.Editor( _mod, null ); - editor.ReplaceAllMaterials( "bibo", "b" ); - editor.ReplaceAllMaterials( "bibopube", "c" ); - editor.SaveAllModels(); - _window.ModEditPopup.UpdateModels(); - } - - ImGuiUtil.HoverTooltip( - "For every model in this mod, change all material names that end in a _b or _c suffix to a _bibo or _bibopube suffix respectively.\n" - + "Does nothing if the mod does not contain any such models or no model contains such materials.\n" - + "Use this for outdated mods made for old Bibo bodies.\n" - + "Go to Advanced Editing for more fine-tuned control over material assignment." ); - } - - private void BackupButtons( Vector2 buttonSize ) - { - var backup = new ModBackup( _mod ); - var tt = ModBackup.CreatingBackup - ? "Already exporting a mod." - : backup.Exists - ? $"Overwrite current exported mod \"{backup.Name}\" with current mod." - : $"Create exported archive of current mod at \"{backup.Name}\"."; - if( ImGuiUtil.DrawDisabledButton( "Export Mod", buttonSize, tt, ModBackup.CreatingBackup ) ) - { - backup.CreateAsync(); - } - - ImGui.SameLine(); - tt = backup.Exists - ? $"Delete existing mod export \"{backup.Name}\"." - : $"Exported mod \"{backup.Name}\" does not exist."; - if( ImGuiUtil.DrawDisabledButton( "Delete Export", buttonSize, tt, !backup.Exists ) ) - { - backup.Delete(); - } - - tt = backup.Exists - ? $"Restore mod from exported file \"{backup.Name}\"." - : $"Exported mod \"{backup.Name}\" does not exist."; - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Restore From Export", buttonSize, tt, !backup.Exists ) ) - { - backup.Restore(); - } - } - - // Anything about editing the regular meta information about the mod. - private void EditRegularMeta() - { - if( Input.Text( "Name", Input.Name, Input.None, _mod.Name, out var newName, 256, _window._inputTextWidth.X ) ) - { - Penumbra.ModManager.ChangeModName( _mod.Index, newName ); - } - - if( Input.Text( "Author", Input.Author, Input.None, _mod.Author, out var newAuthor, 256, _window._inputTextWidth.X ) ) - { - Penumbra.ModManager.ChangeModAuthor( _mod.Index, newAuthor ); - } - - if( Input.Text( "Version", Input.Version, Input.None, _mod.Version, out var newVersion, 32, - _window._inputTextWidth.X ) ) - { - Penumbra.ModManager.ChangeModVersion( _mod.Index, newVersion ); - } - - if( Input.Text( "Website", Input.Website, Input.None, _mod.Website, out var newWebsite, 256, - _window._inputTextWidth.X ) ) - { - Penumbra.ModManager.ChangeModWebsite( _mod.Index, newWebsite ); - } - - var spacing = new Vector2( 3 * ImGuiHelpers.GlobalScale ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, spacing ); - - var reducedSize = new Vector2( _window._inputTextWidth.X - _window._iconButtonSize.X - spacing.X, 0 ); - if( ImGui.Button( "Edit Description", reducedSize ) ) - { - _delayedActions.Enqueue( () => DescriptionEdit.OpenPopup( _mod, Input.Description ) ); - } - - ImGui.SameLine(); - var fileExists = File.Exists( _mod.MetaFile.FullName ); - var tt = fileExists - ? "Open the metadata json file in the text editor of your choice." - : "The metadata json file does not exist."; - if( ImGuiUtil.DrawDisabledButton( $"{FontAwesomeIcon.FileExport.ToIconString()}##metaFile", _window._iconButtonSize, tt, - !fileExists, true ) ) - { - Process.Start( new ProcessStartInfo( _mod.MetaFile.FullName ) { UseShellExecute = true } ); - } - } - - // Do some edits outside of iterations. - private readonly Queue< Action > _delayedActions = new(); - - // Delete a marked group or option outside of iteration. - private void EndActions() - { - while( _delayedActions.TryDequeue( out var action ) ) - { - action.Invoke(); - } - } - - // Text input to add a new option group at the end of the current groups. - private static class AddOptionGroup - { - private static string _newGroupName = string.Empty; - - public static void Reset() - => _newGroupName = string.Empty; - - public static void Draw( ConfigWindow window, Mod mod ) - { - using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 3 * ImGuiHelpers.GlobalScale ) ); - ImGui.SetNextItemWidth( window._inputTextWidth.X - window._iconButtonSize.X - 3 * ImGuiHelpers.GlobalScale ); - ImGui.InputTextWithHint( "##newGroup", "Add new option group...", ref _newGroupName, 256 ); - ImGui.SameLine(); - var fileExists = File.Exists( mod.DefaultFile ); - var tt = fileExists - ? "Open the default option json file in the text editor of your choice." - : "The default option json file does not exist."; - if( ImGuiUtil.DrawDisabledButton( $"{FontAwesomeIcon.FileExport.ToIconString()}##defaultFile", window._iconButtonSize, tt, - !fileExists, true ) ) - { - Process.Start( new ProcessStartInfo( mod.DefaultFile ) { UseShellExecute = true } ); - } - - ImGui.SameLine(); - - var nameValid = Penumbra.ModManager.VerifyFileName( mod, null, _newGroupName, false ); - tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), window._iconButtonSize, - tt, !nameValid, true ) ) - { - Penumbra.ModManager.AddModGroup( mod, GroupType.Single, _newGroupName ); - Reset(); - } - } - } - - // A text input for the new directory name and a button to apply the move. - private static class MoveDirectory - { - private static string? _currentModDirectory; - private static Mod.Manager.NewDirectoryState _state = Mod.Manager.NewDirectoryState.Identical; - - public static void Reset() - { - _currentModDirectory = null; - _state = Mod.Manager.NewDirectoryState.Identical; - } - - public static void Draw( Mod mod, Vector2 buttonSize ) - { - ImGui.SetNextItemWidth( buttonSize.X * 2 + ImGui.GetStyle().ItemSpacing.X ); - var tmp = _currentModDirectory ?? mod.ModPath.Name; - if( ImGui.InputText( "##newModMove", ref tmp, 64 ) ) - { - _currentModDirectory = tmp; - _state = Penumbra.ModManager.NewDirectoryValid( mod.ModPath.Name, _currentModDirectory, out _ ); - } - - var (disabled, tt) = _state switch - { - Mod.Manager.NewDirectoryState.Identical => ( true, "Current directory name is identical to new one." ), - Mod.Manager.NewDirectoryState.Empty => ( true, "Please enter a new directory name first." ), - Mod.Manager.NewDirectoryState.NonExisting => ( false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}." ), - Mod.Manager.NewDirectoryState.ExistsEmpty => ( false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}." ), - Mod.Manager.NewDirectoryState.ExistsNonEmpty => ( true, $"{_currentModDirectory} already exists and is not empty." ), - Mod.Manager.NewDirectoryState.ExistsAsFile => ( true, $"{_currentModDirectory} exists as a file." ), - Mod.Manager.NewDirectoryState.ContainsInvalidSymbols => ( true, - $"{_currentModDirectory} contains invalid symbols for FFXIV." ), - _ => ( true, "Unknown error." ), - }; - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Rename Mod Directory", buttonSize, tt, disabled ) && _currentModDirectory != null ) - { - Penumbra.ModManager.MoveModDirectory( mod.Index, _currentModDirectory ); - Reset(); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "The mod directory name is used to correspond stored settings and sort orders, otherwise it has no influence on anything that is displayed.\n" - + "This can currently not be used on pre-existing folders and does not support merges or overwriting." ); - } - } - - // Open a popup to edit a multi-line mod or option description. - private static class DescriptionEdit - { - private const string PopupName = "Edit Description"; - private static string _newDescription = string.Empty; - private static int _newDescriptionIdx = -1; - private static int _newDesriptionOptionIdx = -1; - private static Mod? _mod; - - public static void OpenPopup( Mod mod, int groupIdx, int optionIdx = -1 ) - { - _newDescriptionIdx = groupIdx; - _newDesriptionOptionIdx = optionIdx; - _newDescription = groupIdx < 0 - ? mod.Description - : optionIdx < 0 - ? mod.Groups[ groupIdx ].Description - : mod.Groups[ groupIdx ][ optionIdx ].Description; - - _mod = mod; - ImGui.OpenPopup( PopupName ); - } - - public static void DrawPopup( ConfigWindow window ) - { - if( _mod == null ) - { - return; - } - - using var popup = ImRaii.Popup( PopupName ); - if( !popup ) - { - return; - } - - if( ImGui.IsWindowAppearing() ) - { - ImGui.SetKeyboardFocusHere(); - } - - ImGui.InputTextMultiline( "##editDescription", ref _newDescription, 4096, ImGuiHelpers.ScaledVector2( 800, 800 ) ); - ImGui.Dummy( window._defaultSpace ); - - var buttonSize = ImGuiHelpers.ScaledVector2( 100, 0 ); - var width = 2 * buttonSize.X - + 4 * ImGui.GetStyle().FramePadding.X - + ImGui.GetStyle().ItemSpacing.X; - ImGui.SetCursorPosX( ( 800 * ImGuiHelpers.GlobalScale - width ) / 2 ); - - var oldDescription = _newDescriptionIdx == Input.Description - ? _mod.Description - : _mod.Groups[ _newDescriptionIdx ].Description; - - var tooltip = _newDescription != oldDescription ? string.Empty : "No changes made yet."; - - if( ImGuiUtil.DrawDisabledButton( "Save", buttonSize, tooltip, tooltip.Length > 0 ) ) - { - switch( _newDescriptionIdx ) - { - case Input.Description: - Penumbra.ModManager.ChangeModDescription( _mod.Index, _newDescription ); - break; - case >= 0: - if( _newDesriptionOptionIdx < 0 ) - { - Penumbra.ModManager.ChangeGroupDescription( _mod, _newDescriptionIdx, _newDescription ); - } - else - { - Penumbra.ModManager.ChangeOptionDescription( _mod, _newDescriptionIdx, _newDesriptionOptionIdx, _newDescription ); - } - - break; - } - - ImGui.CloseCurrentPopup(); - } - - ImGui.SameLine(); - if( ImGui.Button( "Cancel", buttonSize ) - || ImGui.IsKeyPressed( ImGuiKey.Escape ) ) - { - _newDescriptionIdx = Input.None; - _newDescription = string.Empty; - ImGui.CloseCurrentPopup(); - } - } - } - - private void EditGroup( int groupIdx ) - { - var group = _mod.Groups[ groupIdx ]; - using var id = ImRaii.PushId( groupIdx ); - using var frame = ImRaii.FramedGroup( $"Group #{groupIdx + 1}" ); - - using var style = ImRaii.PushStyle( ImGuiStyleVar.CellPadding, _cellPadding ) - .Push( ImGuiStyleVar.ItemSpacing, _itemSpacing ); - - if( Input.Text( "##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, _window._inputTextWidth.X ) ) - { - Penumbra.ModManager.RenameModGroup( _mod, groupIdx, newGroupName ); - } - - ImGuiUtil.HoverTooltip( "Group Name" ); - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, - "Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true ) ) - { - _delayedActions.Enqueue( () => Penumbra.ModManager.DeleteModGroup( _mod, groupIdx ) ); - } - - ImGui.SameLine(); - - if( Input.Priority( "##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * ImGuiHelpers.GlobalScale ) ) - { - Penumbra.ModManager.ChangeGroupPriority( _mod, groupIdx, priority ); - } - - ImGuiUtil.HoverTooltip( "Group Priority" ); - - DrawGroupCombo( group, groupIdx ); - ImGui.SameLine(); - - var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}."; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.ArrowUp.ToIconString(), _window._iconButtonSize, - tt, groupIdx == 0, true ) ) - { - _delayedActions.Enqueue( () => Penumbra.ModManager.MoveModGroup( _mod, groupIdx, groupIdx - 1 ) ); - } - - ImGui.SameLine(); - tt = groupIdx == _mod.Groups.Count - 1 - ? "Can not move this group further downwards." - : $"Move this group down to group {groupIdx + 2}."; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.ArrowDown.ToIconString(), _window._iconButtonSize, - tt, groupIdx == _mod.Groups.Count - 1, true ) ) - { - _delayedActions.Enqueue( () => Penumbra.ModManager.MoveModGroup( _mod, groupIdx, groupIdx + 1 ) ); - } - - ImGui.SameLine(); - - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), _window._iconButtonSize, - "Edit group description.", false, true ) ) - { - _delayedActions.Enqueue( () => DescriptionEdit.OpenPopup( _mod, groupIdx ) ); - } - - ImGui.SameLine(); - var fileName = group.FileName( _mod.ModPath, groupIdx ); - var fileExists = File.Exists( fileName ); - tt = fileExists - ? $"Open the {group.Name} json file in the text editor of your choice." - : $"The {group.Name} json file does not exist."; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileExport.ToIconString(), _window._iconButtonSize, tt, !fileExists, true ) ) - { - Process.Start( new ProcessStartInfo( fileName ) { UseShellExecute = true } ); - } - - ImGui.Dummy( _window._defaultSpace ); - - OptionTable.Draw( this, groupIdx ); - } - - // Draw the table displaying all options and the add new option line. - private static class OptionTable - { - private const string DragDropLabel = "##DragOption"; - - private static int _newOptionNameIdx = -1; - private static string _newOptionName = string.Empty; - private static int _dragDropGroupIdx = -1; - private static int _dragDropOptionIdx = -1; - - public static void Reset() - { - _newOptionNameIdx = -1; - _newOptionName = string.Empty; - _dragDropGroupIdx = -1; - _dragDropOptionIdx = -1; - } - - public static void Draw( ModPanel panel, int groupIdx ) - { - using var table = ImRaii.Table( string.Empty, 6, ImGuiTableFlags.SizingFixedFit ); - if( !table ) - { - return; - } - - ImGui.TableSetupColumn( "idx", ImGuiTableColumnFlags.WidthFixed, 60 * ImGuiHelpers.GlobalScale ); - ImGui.TableSetupColumn( "default", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight() ); - ImGui.TableSetupColumn( "name", ImGuiTableColumnFlags.WidthFixed, - panel._window._inputTextWidth.X - 72 * ImGuiHelpers.GlobalScale - ImGui.GetFrameHeight() - panel._window._iconButtonSize.X ); - ImGui.TableSetupColumn( "description", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X ); - ImGui.TableSetupColumn( "delete", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X ); - ImGui.TableSetupColumn( "priority", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale ); - - var group = panel._mod.Groups[ groupIdx ]; - for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx ) - { - EditOption( panel, group, groupIdx, optionIdx ); - } - - DrawNewOption( panel, groupIdx, panel._window._iconButtonSize ); - } - - // Draw a line for a single option. - private static void EditOption( ModPanel panel, IModGroup group, int groupIdx, int optionIdx ) - { - var option = group[ optionIdx ]; - using var id = ImRaii.PushId( optionIdx ); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Selectable( $"Option #{optionIdx + 1}" ); - Source( group, groupIdx, optionIdx ); - Target( panel, group, groupIdx, optionIdx ); - - ImGui.TableNextColumn(); - - - if( group.Type == GroupType.Single ) - { - if( ImGui.RadioButton( "##default", group.DefaultSettings == optionIdx ) ) - { - Penumbra.ModManager.ChangeModGroupDefaultOption( panel._mod, groupIdx, ( uint )optionIdx ); - } - - ImGuiUtil.HoverTooltip( $"Set {option.Name} as the default choice for this group." ); - } - else - { - var isDefaultOption = ( ( group.DefaultSettings >> optionIdx ) & 1 ) != 0; - if( ImGui.Checkbox( "##default", ref isDefaultOption ) ) - { - Penumbra.ModManager.ChangeModGroupDefaultOption( panel._mod, groupIdx, isDefaultOption - ? group.DefaultSettings | ( 1u << optionIdx ) - : group.DefaultSettings & ~( 1u << optionIdx ) ); - } - - ImGuiUtil.HoverTooltip( $"{( isDefaultOption ? "Disable" : "Enable" )} {option.Name} per default in this group." ); - } - - ImGui.TableNextColumn(); - if( Input.Text( "##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1 ) ) - { - Penumbra.ModManager.RenameOption( panel._mod, groupIdx, optionIdx, newOptionName ); - } - - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), panel._window._iconButtonSize, "Edit option description.", false, true ) ) - { - panel._delayedActions.Enqueue( () => DescriptionEdit.OpenPopup( panel._mod, groupIdx, optionIdx ) ); - } - - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), panel._window._iconButtonSize, - "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true ) ) - { - panel._delayedActions.Enqueue( () => Penumbra.ModManager.DeleteOption( panel._mod, groupIdx, optionIdx ) ); - } - - ImGui.TableNextColumn(); - if( group.Type == GroupType.Multi ) - { - if( Input.Priority( "##Priority", groupIdx, optionIdx, group.OptionPriority( optionIdx ), out var priority, - 50 * ImGuiHelpers.GlobalScale ) ) - { - Penumbra.ModManager.ChangeOptionPriority( panel._mod, groupIdx, optionIdx, priority ); - } - - ImGuiUtil.HoverTooltip( "Option priority." ); - } - } - - // Draw the line to add a new option. - private static void DrawNewOption( ModPanel panel, int groupIdx, Vector2 iconButtonSize ) - { - var mod = panel._mod; - var group = mod.Groups[ groupIdx ]; - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Selectable( $"Option #{group.Count + 1}" ); - Target( panel, group, groupIdx, group.Count ); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( -1 ); - var tmp = _newOptionNameIdx == groupIdx ? _newOptionName : string.Empty; - if( ImGui.InputTextWithHint( "##newOption", "Add new option...", ref tmp, 256 ) ) - { - _newOptionName = tmp; - _newOptionNameIdx = groupIdx; - } - - ImGui.TableNextColumn(); - var canAddGroup = mod.Groups[ groupIdx ].Type != GroupType.Multi || mod.Groups[ groupIdx ].Count < IModGroup.MaxMultiOptions; - var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; - var tt = canAddGroup - ? validName ? "Add a new option to this group." : "Please enter a name for the new option." - : $"Can not add more than {IModGroup.MaxMultiOptions} options to a multi group."; - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconButtonSize, - tt, !( canAddGroup && validName ), true ) ) - { - Penumbra.ModManager.AddOption( mod, groupIdx, _newOptionName ); - _newOptionName = string.Empty; - } - } - - // Handle drag and drop to move options inside a group or into another group. - private static void Source( IModGroup group, int groupIdx, int optionIdx ) - { - using var source = ImRaii.DragDropSource(); - if( !source ) - { - return; - } - - if( ImGui.SetDragDropPayload( DragDropLabel, IntPtr.Zero, 0 ) ) - { - _dragDropGroupIdx = groupIdx; - _dragDropOptionIdx = optionIdx; - } - - ImGui.TextUnformatted( $"Dragging option {group[ optionIdx ].Name} from group {group.Name}..." ); - } - - private static void Target( ModPanel panel, IModGroup group, int groupIdx, int optionIdx ) - { - using var target = ImRaii.DragDropTarget(); - if( !target.Success || !ImGuiUtil.IsDropping( DragDropLabel ) ) - { - return; - } - - if( _dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0 ) - { - if( _dragDropGroupIdx == groupIdx ) - { - var sourceOption = _dragDropOptionIdx; - panel._delayedActions.Enqueue( () => Penumbra.ModManager.MoveOption( panel._mod, groupIdx, sourceOption, optionIdx ) ); - } - else - { - // Move from one group to another by deleting, then adding, then moving the option. - var sourceGroupIdx = _dragDropGroupIdx; - var sourceOption = _dragDropOptionIdx; - var sourceGroup = panel._mod.Groups[ sourceGroupIdx ]; - var currentCount = group.Count; - var option = sourceGroup[ sourceOption ]; - var priority = sourceGroup.OptionPriority( _dragDropOptionIdx ); - panel._delayedActions.Enqueue( () => - { - Penumbra.ModManager.DeleteOption( panel._mod, sourceGroupIdx, sourceOption ); - Penumbra.ModManager.AddOption( panel._mod, groupIdx, option, priority ); - Penumbra.ModManager.MoveOption( panel._mod, groupIdx, currentCount, optionIdx ); - } ); - } - } - - _dragDropGroupIdx = -1; - _dragDropOptionIdx = -1; - } - } - - // Draw a combo to select single or multi group and switch between them. - private void DrawGroupCombo( IModGroup group, int groupIdx ) - { - static string GroupTypeName( GroupType type ) - => type switch - { - GroupType.Single => "Single Group", - GroupType.Multi => "Multi Group", - _ => "Unknown", - }; - - ImGui.SetNextItemWidth( _window._inputTextWidth.X - 3 * _window._iconButtonSize.X - 12 * ImGuiHelpers.GlobalScale ); - using var combo = ImRaii.Combo( "##GroupType", GroupTypeName( group.Type ) ); - if( !combo ) - { - return; - } - - if( ImGui.Selectable( GroupTypeName( GroupType.Single ), group.Type == GroupType.Single ) ) - { - Penumbra.ModManager.ChangeModGroupType( _mod, groupIdx, GroupType.Single ); - } - - var canSwitchToMulti = group.Count <= IModGroup.MaxMultiOptions; - using var style = ImRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti ); - if( ImGui.Selectable( GroupTypeName( GroupType.Multi ), group.Type == GroupType.Multi ) && canSwitchToMulti ) - { - Penumbra.ModManager.ChangeModGroupType( _mod, groupIdx, GroupType.Multi ); - } - - style.Pop(); - if( !canSwitchToMulti ) - { - ImGuiUtil.HoverTooltip( $"Can not convert group to multi group since it has more than {IModGroup.MaxMultiOptions} options." ); - } - } - - // Handles input text and integers in separate fields without buffers for every single one. - private static class Input - { - // Special field indices to reuse the same string buffer. - public const int None = -1; - public const int Name = -2; - public const int Author = -3; - public const int Version = -4; - public const int Website = -5; - public const int Path = -6; - public const int Description = -7; - - // Temporary strings - private static string? _currentEdit; - private static int? _currentGroupPriority; - private static int _currentField = None; - private static int _optionIndex = None; - - public static void Reset() - { - _currentEdit = null; - _currentGroupPriority = null; - _currentField = None; - _optionIndex = None; - } - - public static bool Text( string label, int field, int option, string oldValue, out string value, uint maxLength, float width ) - { - var tmp = field == _currentField && option == _optionIndex ? _currentEdit ?? oldValue : oldValue; - ImGui.SetNextItemWidth( width ); - if( ImGui.InputText( label, ref tmp, maxLength ) ) - { - _currentEdit = tmp; - _optionIndex = option; - _currentField = field; - } - - if( ImGui.IsItemDeactivatedAfterEdit() && _currentEdit != null ) - { - var ret = _currentEdit != oldValue; - value = _currentEdit; - Reset(); - return ret; - } - - value = string.Empty; - return false; - } - - public static bool Priority( string label, int field, int option, int oldValue, out int value, float width ) - { - var tmp = field == _currentField && option == _optionIndex ? _currentGroupPriority ?? oldValue : oldValue; - ImGui.SetNextItemWidth( width ); - if( ImGui.InputInt( label, ref tmp, 0, 0 ) ) - { - _currentGroupPriority = tmp; - _optionIndex = option; - _currentField = field; - } - - if( ImGui.IsItemDeactivatedAfterEdit() && _currentGroupPriority != null ) - { - var ret = _currentGroupPriority != oldValue; - value = _currentGroupPriority.Value; - Reset(); - return ret; - } - - value = 0; - return false; - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Header.cs b/Penumbra/UI/ConfigWindow.ModPanel.Header.cs deleted file mode 100644 index 6297e807..00000000 --- a/Penumbra/UI/ConfigWindow.ModPanel.Header.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Diagnostics; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.GameFonts; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.Services; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class ModPanel - { - // We use a big, nice game font for the title. - private readonly GameFontHandle _nameFont = - DalamudServices.PluginInterface.UiBuilder.GetGameFontHandle( new GameFontStyle( GameFontFamilyAndSize.Jupiter23 ) ); - - // Header data. - private string _modName = string.Empty; - private string _modAuthor = string.Empty; - private string _modVersion = string.Empty; - private string _modWebsite = string.Empty; - private string _modWebsiteButton = string.Empty; - private bool _websiteValid; - - private float _modNameWidth; - private float _modAuthorWidth; - private float _modVersionWidth; - private float _modWebsiteButtonWidth; - private float _secondRowWidth; - - // Draw the header for the current mod, - // consisting of its name, version, author and website, if they exist. - private void DrawModHeader() - { - var offset = DrawModName(); - DrawVersion( offset ); - DrawSecondRow( offset ); - } - - // Draw the mod name in the game font with a 2px border, centered, - // with at least the width of the version space to each side. - private float DrawModName() - { - var decidingWidth = Math.Max( _secondRowWidth, ImGui.GetWindowWidth() ); - var offsetWidth = ( decidingWidth - _modNameWidth ) / 2; - var offsetVersion = _modVersion.Length > 0 - ? _modVersionWidth + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X - : 0; - var offset = Math.Max( offsetWidth, offsetVersion ); - if( offset > 0 ) - { - ImGui.SetCursorPosX( offset ); - } - - using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.MetaInfoText ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale ); - using var font = ImRaii.PushFont( _nameFont.ImFont, _nameFont.Available ); - ImGuiUtil.DrawTextButton( _modName, Vector2.Zero, 0 ); - return offset; - } - - // Draw the version in the top-right corner. - private void DrawVersion( float offset ) - { - var oldPos = ImGui.GetCursorPos(); - ImGui.SetCursorPos( new Vector2( 2 * offset + _modNameWidth - _modVersionWidth - ImGui.GetStyle().WindowPadding.X, - ImGui.GetStyle().FramePadding.Y ) ); - ImGuiUtil.TextColored( Colors.MetaInfoText, _modVersion ); - ImGui.SetCursorPos( oldPos ); - } - - // Draw author and website if they exist. The website is a button if it is valid. - // Usually, author begins at the left boundary of the name, - // and website ends at the right boundary of the name. - // If their combined width is larger than the name, they are combined-centered. - private void DrawSecondRow( float offset ) - { - if( _modAuthor.Length == 0 ) - { - if( _modWebsiteButton.Length == 0 ) - { - ImGui.NewLine(); - return; - } - - offset += ( _modNameWidth - _modWebsiteButtonWidth ) / 2; - ImGui.SetCursorPosX( offset ); - DrawWebsite(); - } - else if( _modWebsiteButton.Length == 0 ) - { - offset += ( _modNameWidth - _modAuthorWidth ) / 2; - ImGui.SetCursorPosX( offset ); - DrawAuthor(); - } - else if( _secondRowWidth < _modNameWidth ) - { - ImGui.SetCursorPosX( offset ); - DrawAuthor(); - ImGui.SameLine( offset + _modNameWidth - _modWebsiteButtonWidth ); - DrawWebsite(); - } - else - { - offset -= ( _secondRowWidth - _modNameWidth ) / 2; - if( offset > 0 ) - { - ImGui.SetCursorPosX( offset ); - } - - DrawAuthor(); - ImGui.SameLine(); - DrawWebsite(); - } - } - - // Draw the author text. - private void DrawAuthor() - { - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - ImGuiUtil.TextColored( Colors.MetaInfoText, "by " ); - ImGui.SameLine(); - style.Pop(); - ImGui.TextUnformatted( _mod.Author ); - } - - // Draw either a website button if the source is a valid website address, - // or a source text if it is not. - private void DrawWebsite() - { - if( _websiteValid ) - { - if( ImGui.SmallButton( _modWebsiteButton ) ) - { - try - { - var process = new ProcessStartInfo( _modWebsite ) - { - UseShellExecute = true, - }; - Process.Start( process ); - } - catch - { - // ignored - } - } - - ImGuiUtil.HoverTooltip( _modWebsite ); - } - else - { - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - ImGuiUtil.TextColored( Colors.MetaInfoText, "from " ); - ImGui.SameLine(); - style.Pop(); - ImGui.TextUnformatted( _mod.Website ); - } - } - - // Update all mod header data. Should someone change frame padding or item spacing, - // or his default font, this will break, but he will just have to select a different mod to restore. - private void UpdateModData() - { - // Name - var name = $" {_mod.Name} "; - if( name != _modName ) - { - using var font = ImRaii.PushFont( _nameFont.ImFont, _nameFont.Available ); - _modName = name; - _modNameWidth = ImGui.CalcTextSize( name ).X + 2 * ( ImGui.GetStyle().FramePadding.X + 2 * ImGuiHelpers.GlobalScale ); - } - - // Author - var author = _mod.Author.IsEmpty ? string.Empty : $"by {_mod.Author}"; - if( author != _modAuthor ) - { - _modAuthor = author; - _modAuthorWidth = ImGui.CalcTextSize( author ).X; - _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X; - } - - // Version - var version = _mod.Version.Length > 0 ? $"({_mod.Version})" : string.Empty; - if( version != _modVersion ) - { - _modVersion = version; - _modVersionWidth = ImGui.CalcTextSize( version ).X; - } - - // Website - if( _modWebsite != _mod.Website ) - { - _modWebsite = _mod.Website; - _websiteValid = Uri.TryCreate( _modWebsite, UriKind.Absolute, out var uriResult ) - && ( uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp ); - _modWebsiteButton = _websiteValid ? "Open Website" : _modWebsite.Length == 0 ? string.Empty : $"from {_modWebsite}"; - _modWebsiteButtonWidth = _websiteValid - ? ImGui.CalcTextSize( _modWebsiteButton ).X + 2 * ImGui.GetStyle().FramePadding.X - : ImGui.CalcTextSize( _modWebsiteButton ).X; - _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X; - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs deleted file mode 100644 index 10cac9ba..00000000 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ /dev/null @@ -1,359 +0,0 @@ -using System; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using ImGuiNET; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Api.Enums; -using Penumbra.Collections; -using Penumbra.Mods; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class ModPanel - { - private ModSettings _settings = null!; - private ModCollection _collection = null!; - private bool _emptySetting; - private bool _inherited; - private SingleArray< ModConflicts > _conflicts = new(); - - private int? _currentPriority; - - private void UpdateSettingsData( ModFileSystemSelector selector ) - { - _settings = selector.SelectedSettings; - _collection = selector.SelectedSettingCollection; - _emptySetting = _settings == ModSettings.Empty; - _inherited = _collection != Penumbra.CollectionManager.Current; - _conflicts = Penumbra.CollectionManager.Current.Conflicts( _mod ); - } - - // Draw the whole settings tab as well as its contents. - private void DrawSettingsTab() - { - using var tab = DrawTab( SettingsTabHeader, Tabs.Settings ); - OpenTutorial( BasicTutorialSteps.ModOptions ); - if( !tab ) - { - return; - } - - using var child = ImRaii.Child( "##settings" ); - if( !child ) - { - return; - } - - DrawInheritedWarning(); - ImGui.Dummy( _window._defaultSpace ); - _window._penumbra.Api.InvokePreSettingsPanel( _mod.ModPath.Name ); - DrawEnabledInput(); - OpenTutorial( BasicTutorialSteps.EnablingMods ); - ImGui.SameLine(); - DrawPriorityInput(); - OpenTutorial( BasicTutorialSteps.Priority ); - DrawRemoveSettings(); - - if( _mod.Groups.Count > 0 ) - { - var useDummy = true; - foreach( var (group, idx) in _mod.Groups.WithIndex().Where( g => g.Value.Type == GroupType.Single && g.Value.Count > Penumbra.Config.SingleGroupRadioMax ) ) - { - ImGuiUtil.Dummy( _window._defaultSpace, useDummy ); - useDummy = false; - DrawSingleGroupCombo( group, idx ); - } - - useDummy = true; - foreach( var (group, idx) in _mod.Groups.WithIndex().Where( g => g.Value.IsOption ) ) - { - ImGuiUtil.Dummy( _window._defaultSpace, useDummy ); - useDummy = false; - switch( group.Type ) - { - case GroupType.Multi: - DrawMultiGroup( group, idx ); - break; - case GroupType.Single when group.Count <= Penumbra.Config.SingleGroupRadioMax: - DrawSingleGroupRadio( group, idx ); - break; - } - } - } - - ImGui.Dummy( _window._defaultSpace ); - _window._penumbra.Api.InvokePostSettingsPanel( _mod.ModPath.Name ); - } - - - // Draw a big red bar if the current setting is inherited. - private void DrawInheritedWarning() - { - if( !_inherited ) - { - return; - } - - using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg ); - var width = new Vector2( ImGui.GetContentRegionAvail().X, 0 ); - if( ImGui.Button( $"These settings are inherited from {_collection.Name}.", width ) ) - { - Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, false ); - } - - ImGuiUtil.HoverTooltip( "You can click this button to copy the current settings to the current selection.\n" - + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection." ); - } - - // Draw a checkbox for the enabled status of the mod. - private void DrawEnabledInput() - { - var enabled = _settings.Enabled; - if( ImGui.Checkbox( "Enabled", ref enabled ) ) - { - Penumbra.ModManager.NewMods.Remove( _mod ); - Penumbra.CollectionManager.Current.SetModState( _mod.Index, enabled ); - } - } - - // Draw a priority input. - // Priority is changed on deactivation of the input box. - private void DrawPriorityInput() - { - using var group = ImRaii.Group(); - var priority = _currentPriority ?? _settings.Priority; - ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); - if( ImGui.InputInt( "##Priority", ref priority, 0, 0 ) ) - { - _currentPriority = priority; - } - - if( ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue ) - { - if( _currentPriority != _settings.Priority ) - { - Penumbra.CollectionManager.Current.SetModPriority( _mod.Index, _currentPriority.Value ); - } - - _currentPriority = null; - } - - ImGuiUtil.LabeledHelpMarker( "Priority", "Mods with a higher number here take precedence before Mods with a lower number.\n" - + "That means, if Mod A should overwrite changes from Mod B, Mod A should have a higher priority number than Mod B." ); - } - - // Draw a button to remove the current settings and inherit them instead - // on the top-right corner of the window/tab. - private void DrawRemoveSettings() - { - const string text = "Inherit Settings"; - if( _inherited || _emptySetting ) - { - return; - } - - var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; - ImGui.SameLine( ImGui.GetWindowWidth() - ImGui.CalcTextSize( text ).X - ImGui.GetStyle().FramePadding.X * 2 - scroll ); - if( ImGui.Button( text ) ) - { - Penumbra.CollectionManager.Current.SetModInheritance( _mod.Index, true ); - } - - ImGuiUtil.HoverTooltip( "Remove current settings from this collection so that it can inherit them.\n" - + "If no inherited collection has settings for this mod, it will be disabled." ); - } - - - // Draw a single group selector as a combo box. - // If a description is provided, add a help marker besides it. - private void DrawSingleGroupCombo( IModGroup group, int groupIdx ) - { - using var id = ImRaii.PushId( groupIdx ); - var selectedOption = _emptySetting ? ( int )group.DefaultSettings : ( int )_settings.Settings[ groupIdx ]; - ImGui.SetNextItemWidth( _window._inputTextWidth.X * 3 / 4 ); - using( var combo = ImRaii.Combo( string.Empty, group[ selectedOption ].Name ) ) - { - if( combo ) - { - for( var idx2 = 0; idx2 < group.Count; ++idx2 ) - { - id.Push( idx2 ); - var option = group[ idx2 ]; - if( ImGui.Selectable( option.Name, idx2 == selectedOption ) ) - { - Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, ( uint )idx2 ); - } - - if( option.Description.Length > 0 ) - { - var hovered = ImGui.IsItemHovered(); - ImGui.SameLine(); - using( var font = ImRaii.PushFont( UiBuilder.IconFont ) ) - { - using var color = ImRaii.PushColor( ImGuiCol.Text, ImGui.GetColorU32( ImGuiCol.TextDisabled ) ); - ImGuiUtil.RightAlign( FontAwesomeIcon.InfoCircle.ToIconString(), ImGui.GetStyle().ItemSpacing.X ); - } - - if( hovered ) - { - using var tt = ImRaii.Tooltip(); - ImGui.TextUnformatted( option.Description ); - } - } - - id.Pop(); - } - } - } - - ImGui.SameLine(); - if( group.Description.Length > 0 ) - { - ImGuiUtil.LabeledHelpMarker( group.Name, group.Description ); - } - else - { - ImGui.TextUnformatted( group.Name ); - } - } - - // Draw a single group selector as a set of radio buttons. - // If a description is provided, add a help marker besides it. - private void DrawSingleGroupRadio( IModGroup group, int groupIdx ) - { - using var id = ImRaii.PushId( groupIdx ); - var selectedOption = _emptySetting ? ( int )group.DefaultSettings : ( int )_settings.Settings[ groupIdx ]; - Widget.BeginFramedGroup( group.Name, group.Description ); - - void DrawOptions() - { - for( var idx = 0; idx < group.Count; ++idx ) - { - using var i = ImRaii.PushId( idx ); - var option = group[ idx ]; - if( ImGui.RadioButton( option.Name, selectedOption == idx ) ) - { - Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, ( uint )idx ); - } - - if( option.Description.Length > 0 ) - { - ImGui.SameLine(); - ImGuiComponents.HelpMarker( option.Description ); - } - } - } - - DrawCollapseHandling( group, DrawOptions ); - - Widget.EndFramedGroup(); - } - - private static void DrawCollapseHandling( IModGroup group, Action draw ) - { - if( group.Count <= 5 ) - { - draw(); - } - else - { - var collapseId = ImGui.GetID( "Collapse" ); - var shown = ImGui.GetStateStorage().GetBool( collapseId, true ); - if( shown ) - { - var pos = ImGui.GetCursorPos(); - ImGui.Dummy( new Vector2( ImGui.GetFrameHeight() ) ); - using( var _ = ImRaii.Group() ) - { - draw(); - } - - var width = ImGui.GetItemRectSize().X; - var endPos = ImGui.GetCursorPos(); - ImGui.SetCursorPos( pos ); - if( ImGui.Button( $"Hide {group.Count} Options", new Vector2( width, 0 ) ) ) - { - ImGui.GetStateStorage().SetBool( collapseId, !shown ); - } - - ImGui.SetCursorPos( endPos ); - } - else - { - var max = group.Max( o => ImGui.CalcTextSize( o.Name ).X ) + ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X; - if( ImGui.Button( $"Show {group.Count} Options", new Vector2( max, 0 ) ) ) - { - ImGui.GetStateStorage().SetBool( collapseId, !shown ); - } - } - } - } - - // Draw a multi group selector as a bordered set of checkboxes. - // If a description is provided, add a help marker in the title. - private void DrawMultiGroup( IModGroup group, int groupIdx ) - { - using var id = ImRaii.PushId( groupIdx ); - var flags = _emptySetting ? group.DefaultSettings : _settings.Settings[ groupIdx ]; - Widget.BeginFramedGroup( group.Name, group.Description ); - - void DrawOptions() - { - for( var idx = 0; idx < group.Count; ++idx ) - { - using var i = ImRaii.PushId( idx ); - var option = group[ idx ]; - var flag = 1u << idx; - var setting = ( flags & flag ) != 0; - - if( ImGui.Checkbox( option.Name, ref setting ) ) - { - flags = setting ? flags | flag : flags & ~flag; - Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, flags ); - } - - if( option.Description.Length > 0 ) - { - ImGui.SameLine(); - ImGuiComponents.HelpMarker( option.Description ); - } - } - } - - DrawCollapseHandling( group, DrawOptions ); - - Widget.EndFramedGroup(); - var label = $"##multi{groupIdx}"; - if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) - { - ImGui.OpenPopup( $"##multi{groupIdx}" ); - } - - using var style = ImRaii.PushStyle( ImGuiStyleVar.PopupBorderSize, 1 ); - using var popup = ImRaii.Popup( label ); - if( popup ) - { - ImGui.TextUnformatted( group.Name ); - ImGui.Separator(); - if( ImGui.Selectable( "Enable All" ) ) - { - flags = group.Count == 32 ? uint.MaxValue : ( 1u << group.Count ) - 1u; - Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, flags ); - } - - if( ImGui.Selectable( "Disable All" ) ) - { - Penumbra.CollectionManager.Current.SetModSetting( _mod.Index, groupIdx, 0 ); - } - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs deleted file mode 100644 index 28015b79..00000000 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ /dev/null @@ -1,251 +0,0 @@ -using System; -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Classes; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods; -using Penumbra.String; -using Penumbra.String.Classes; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class ModPanel - { - [Flags] - private enum Tabs - { - Description = 0x01, - Settings = 0x02, - ChangedItems = 0x04, - Conflicts = 0x08, - Edit = 0x10, - }; - - // We want to keep the preferred tab selected even if switching through mods. - private Tabs _preferredTab = Tabs.Settings; - private Tabs _availableTabs = 0; - - // Required to use tabs that can not be closed but have a flag to set them open. - private static readonly ByteString ConflictTabHeader = ByteString.FromSpanUnsafe( "Conflicts"u8, true, false, true ); - private static readonly ByteString DescriptionTabHeader = ByteString.FromSpanUnsafe( "Description"u8, true, false, true ); - private static readonly ByteString SettingsTabHeader = ByteString.FromSpanUnsafe( "Settings"u8, true, false, true ); - private static readonly ByteString ChangedItemsTabHeader = ByteString.FromSpanUnsafe( "Changed Items"u8, true, false, true ); - private static readonly ByteString EditModTabHeader = ByteString.FromSpanUnsafe( "Edit Mod"u8, true, false, true ); - - private readonly TagButtons _modTags = new(); - - private void DrawTabBar() - { - var tabBarHeight = ImGui.GetCursorPosY(); - using var tabBar = ImRaii.TabBar( "##ModTabs" ); - if( !tabBar ) - { - return; - } - - _availableTabs = Tabs.Settings - | ( _mod.ChangedItems.Count > 0 ? Tabs.ChangedItems : 0 ) - | Tabs.Description - | ( _conflicts.Count > 0 ? Tabs.Conflicts : 0 ) - | Tabs.Edit; - - DrawSettingsTab(); - DrawDescriptionTab(); - DrawChangedItemsTab(); - DrawConflictsTab(); - DrawEditModTab(); - DrawAdvancedEditingButton(); - DrawFavoriteButton( tabBarHeight ); - } - - private void DrawAdvancedEditingButton() - { - if( ImGui.TabItemButton( "Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) ) - { - _window.ModEditPopup.ChangeMod( _mod ); - _window.ModEditPopup.ChangeOption( _mod.Default ); - _window.ModEditPopup.IsOpen = true; - } - - ImGuiUtil.HoverTooltip( - "Clicking this will open a new window in which you can\nedit the following things per option for this mod:\n\n" - + "\t\t- file redirections\n" - + "\t\t- file swaps\n" - + "\t\t- metadata manipulations\n" - + "\t\t- model materials\n" - + "\t\t- duplicates\n" - + "\t\t- textures" ); - } - - private void DrawFavoriteButton( float height ) - { - var oldPos = ImGui.GetCursorPos(); - - using( var font = ImRaii.PushFont( UiBuilder.IconFont ) ) - { - var size = ImGui.CalcTextSize( FontAwesomeIcon.Star.ToIconString() ) + ImGui.GetStyle().FramePadding * 2; - var newPos = new Vector2( ImGui.GetWindowWidth() - size.X - ImGui.GetStyle().ItemSpacing.X, height ); - if( ImGui.GetScrollMaxX() > 0 ) - { - newPos.X += ImGui.GetScrollX(); - } - - var rectUpper = ImGui.GetWindowPos() + newPos; - var color = ImGui.IsMouseHoveringRect( rectUpper, rectUpper + size ) ? ImGui.GetColorU32( ImGuiCol.Text ) : - _mod.Favorite ? 0xFF00FFFF : ImGui.GetColorU32( ImGuiCol.TextDisabled ); - using var c = ImRaii.PushColor( ImGuiCol.Text, color ) - .Push( ImGuiCol.Button, 0 ) - .Push( ImGuiCol.ButtonHovered, 0 ) - .Push( ImGuiCol.ButtonActive, 0 ); - - ImGui.SetCursorPos( newPos ); - if( ImGui.Button( FontAwesomeIcon.Star.ToIconString() ) ) - { - Penumbra.ModManager.ChangeModFavorite( _mod.Index, !_mod.Favorite ); - } - } - - var hovered = ImGui.IsItemHovered(); - OpenTutorial( BasicTutorialSteps.Favorites ); - - if( hovered ) - { - ImGui.SetTooltip( "Favorite" ); - } - } - - - // Just a simple text box with the wrapped description, if it exists. - private void DrawDescriptionTab() - { - using var tab = DrawTab( DescriptionTabHeader, Tabs.Description ); - if( !tab ) - { - return; - } - - using var child = ImRaii.Child( "##description" ); - if( !child ) - { - return; - } - - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 2 ) ); - - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 2 ) ); - 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.", _mod.LocalTags, - out var editedTag ); - OpenTutorial( BasicTutorialSteps.Tags ); - if( tagIdx >= 0 ) - { - Penumbra.ModManager.ChangeLocalTag( _mod.Index, tagIdx, editedTag ); - } - - if( _mod.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.", _mod.ModTags, out var _, false, - ImGui.CalcTextSize( "Local " ).X - ImGui.CalcTextSize( "Mod " ).X ); - } - - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 2 ) ); - ImGui.Separator(); - - ImGuiUtil.TextWrapped( _mod.Description ); - } - - // A simple clipped list of changed items. - private void DrawChangedItemsTab() - { - using var tab = DrawTab( ChangedItemsTabHeader, Tabs.ChangedItems ); - if( !tab ) - { - return; - } - - using var list = ImRaii.ListBox( "##changedItems", -Vector2.One ); - if( !list ) - { - return; - } - - var zipList = ZipList.FromSortedList( _mod.ChangedItems ); - var height = ImGui.GetTextLineHeight(); - ImGuiClip.ClippedDraw( zipList, kvp => _window.DrawChangedItem( kvp.Item1, kvp.Item2, true ), height ); - } - - // If any conflicts exist, show them in this tab. - private unsafe void DrawConflictsTab() - { - using var tab = DrawTab( ConflictTabHeader, Tabs.Conflicts ); - if( !tab ) - { - return; - } - - using var box = ImRaii.ListBox( "##conflicts", -Vector2.One ); - if( !box ) - { - return; - } - - foreach( var conflict in Penumbra.CollectionManager.Current.Conflicts( _mod ) ) - { - if( ImGui.Selectable( conflict.Mod2.Name ) && conflict.Mod2 is Mod mod ) - { - _window._selector.SelectByValue( mod ); - } - - ImGui.SameLine(); - using( var color = ImRaii.PushColor( ImGuiCol.Text, - conflict.HasPriority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() ) ) - { - var priority = conflict.Mod2.Index < 0 - ? conflict.Mod2.Priority - : Penumbra.CollectionManager.Current[ conflict.Mod2.Index ].Settings!.Priority; - ImGui.TextUnformatted( $"(Priority {priority})" ); - } - - using var indent = ImRaii.PushIndent( 30f ); - foreach( var data in conflict.Conflicts ) - { - var _ = data switch - { - Utf8GamePath p => ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ) > 0, - MetaManipulation m => ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty ), - _ => false, - }; - } - } - } - - - // Draw a tab by given name if it is available, and deal with changing the preferred tab. - private ImRaii.IEndObject DrawTab( ByteString name, Tabs flag ) - { - if( !_availableTabs.HasFlag( flag ) ) - { - return ImRaii.IEndObject.Empty; - } - - var flags = _preferredTab == flag ? ImGuiTabItemFlags.SetSelected : ImGuiTabItemFlags.None; - unsafe - { - var tab = ImRaii.TabItem( name.Path, flags ); - if( ImGui.IsItemClicked() ) - { - _preferredTab = flag; - } - - return tab; - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.cs b/Penumbra/UI/ConfigWindow.ModPanel.cs deleted file mode 100644 index d9b1f7ac..00000000 --- a/Penumbra/UI/ConfigWindow.ModPanel.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using OtterGui.Widgets; -using Penumbra.Mods; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - - // The basic setup for the mod panel. - // Details are in other files. - private partial class ModPanel : IDisposable - { - private readonly ConfigWindow _window; - - private bool _valid; - private ModFileSystem.Leaf _leaf = null!; - private Mod _mod = null!; - private readonly TagButtons _localTags = new(); - - public ModPanel( ConfigWindow window ) - => _window = window; - - public void Dispose() - { - _nameFont.Dispose(); - } - - public void Draw( ModFileSystemSelector selector ) - { - Init( selector ); - if( !_valid ) - { - return; - } - - DrawModHeader(); - DrawTabBar(); - } - - private void Init( ModFileSystemSelector selector ) - { - _valid = selector.Selected != null; - if( !_valid ) - { - return; - } - - _leaf = selector.SelectedLeaf!; - _mod = selector.Selected!; - UpdateSettingsData( selector ); - UpdateModData(); - } - - public void OnSelectionChange( Mod? old, Mod? mod, in ModFileSystemSelector.ModState _ ) - { - if( old == mod ) - { - return; - } - - if( mod == null ) - { - _window.ModEditPopup.IsOpen = false; - } - else if( _window.ModEditPopup.IsOpen ) - { - _window.ModEditPopup.ChangeMod( mod ); - } - - _currentPriority = null; - MoveDirectory.Reset(); - OptionTable.Reset(); - Input.Reset(); - AddOptionGroup.Reset(); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs deleted file mode 100644 index 5be9c8e0..00000000 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ /dev/null @@ -1,205 +0,0 @@ -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.Collections; -using Penumbra.UI.Classes; -using System; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using OtterGui.Widgets; -using Penumbra.Api.Enums; -using Penumbra.Services; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private class ModsTab : ITab - { - private readonly ModFileSystemSelector _selector; - private readonly ModPanel _panel; - private readonly Penumbra _penumbra; - - public ModsTab(ModFileSystemSelector selector, ModPanel panel, Penumbra penumbra) - { - _selector = selector; - _panel = panel; - _penumbra = penumbra; - } - - public bool IsVisible - => Penumbra.ModManager.Valid; - - public ReadOnlySpan Label - => "Mods"u8; - - public void DrawHeader() - => OpenTutorial( BasicTutorialSteps.Mods ); - - public void DrawContent() - { - try - { - _selector.Draw( GetModSelectorSize() ); - ImGui.SameLine(); - using var group = ImRaii.Group(); - DrawHeaderLine(); - - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - - using( var child = ImRaii.Child( "##ModsTabMod", new Vector2( -1, Penumbra.Config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight() ), - true, ImGuiWindowFlags.HorizontalScrollbar ) ) - { - style.Pop(); - if( child ) - { - _panel.Draw( _selector ); - } - - style.Push( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - } - - style.Push( ImGuiStyleVar.FrameRounding, 0 ); - DrawRedrawLine(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Exception thrown during ModPanel Render:\n{e}" ); - Penumbra.Log.Error( $"{Penumbra.ModManager.Count} Mods\n" - + $"{Penumbra.CollectionManager.Current.AnonymizedName} Current Collection\n" - + $"{Penumbra.CollectionManager.Current.Settings.Count} Settings\n" - + $"{_selector.SortMode.Name} Sort Mode\n" - + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" - + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join( ", ", Penumbra.CollectionManager.Current.Inheritance.Select( c => c.AnonymizedName ) )} Inheritances\n" - + $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n" ); - } - } - - private void DrawRedrawLine() - { - if( Penumbra.Config.HideRedrawBar ) - { - SkipTutorial( BasicTutorialSteps.Redrawing ); - return; - } - - var frameHeight = new Vector2( 0, ImGui.GetFrameHeight() ); - var frameColor = ImGui.GetColorU32( ImGuiCol.FrameBg ); - using( var _ = ImRaii.Group() ) - { - using( var font = ImRaii.PushFont( UiBuilder.IconFont ) ) - { - ImGuiUtil.DrawTextButton( FontAwesomeIcon.InfoCircle.ToIconString(), frameHeight, frameColor ); - ImGui.SameLine(); - } - - ImGuiUtil.DrawTextButton( "Redraw: ", frameHeight, frameColor ); - } - - var hovered = ImGui.IsItemHovered(); - OpenTutorial( BasicTutorialSteps.Redrawing ); - if( hovered ) - { - ImGui.SetTooltip( $"The supported modifiers for '/penumbra redraw' are:\n{SupportedRedrawModifiers}" ); - } - - void DrawButton( Vector2 size, string label, string lower ) - { - if( ImGui.Button( label, size ) ) - { - if( lower.Length > 0 ) - { - _penumbra.RedrawService.RedrawObject( lower, RedrawType.Redraw ); - } - else - { - _penumbra.RedrawService.RedrawAll( RedrawType.Redraw ); - } - } - - ImGuiUtil.HoverTooltip( lower.Length > 0 ? $"Execute '/penumbra redraw {lower}'." : $"Execute '/penumbra redraw'." ); - } - - using var disabled = ImRaii.Disabled( DalamudServices.ClientState.LocalPlayer == null ); - ImGui.SameLine(); - var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 4 }; - DrawButton( buttonWidth, "All", string.Empty ); - ImGui.SameLine(); - DrawButton( buttonWidth, "Self", "self" ); - ImGui.SameLine(); - DrawButton( buttonWidth, "Target", "target" ); - ImGui.SameLine(); - DrawButton( frameHeight with { X = ImGui.GetContentRegionAvail().X - 1 }, "Focus", "focus" ); - } - - // Draw the header line that can quick switch between collections. - private void DrawHeaderLine() - { - using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameRounding, 0 ).Push( ImGuiStyleVar.ItemSpacing, Vector2.Zero ); - var buttonSize = new Vector2( ImGui.GetContentRegionAvail().X / 8f, 0 ); - - using( var _ = ImRaii.Group() ) - { - DrawDefaultCollectionButton( 3 * buttonSize ); - ImGui.SameLine(); - DrawInheritedCollectionButton( 3 * buttonSize ); - ImGui.SameLine(); - DrawCollectionSelector( "##collectionSelector", 2 * buttonSize.X, CollectionType.Current, false ); - } - - OpenTutorial( BasicTutorialSteps.CollectionSelectors ); - - if( !Penumbra.CollectionManager.CurrentCollectionInUse ) - { - ImGuiUtil.DrawTextButton( "The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg ); - } - } - - private static void DrawDefaultCollectionButton( Vector2 width ) - { - var name = $"{DefaultCollection} ({Penumbra.CollectionManager.Default.Name})"; - var isCurrent = Penumbra.CollectionManager.Default == Penumbra.CollectionManager.Current; - var isEmpty = Penumbra.CollectionManager.Default == ModCollection.Empty; - var tt = isCurrent ? $"The current collection is already the configured {DefaultCollection}." - : isEmpty ? $"The {DefaultCollection} is configured to be empty." - : $"Set the {SelectedCollection} to the configured {DefaultCollection}."; - if( ImGuiUtil.DrawDisabledButton( name, width, tt, isCurrent || isEmpty ) ) - { - Penumbra.CollectionManager.SetCollection( Penumbra.CollectionManager.Default, CollectionType.Current ); - } - } - - private void DrawInheritedCollectionButton( Vector2 width ) - { - var noModSelected = _selector.Selected == null; - var collection = _selector.SelectedSettingCollection; - var modInherited = collection != Penumbra.CollectionManager.Current; - var (name, tt) = (noModSelected, modInherited) switch - { - (true, _ ) => ("Inherited Collection", "No mod selected."), - (false, true ) => ($"Inherited Collection ({collection.Name})", - "Set the current collection to the collection the selected mod inherits its settings from."), - (false, false ) => ("Not Inherited", "The selected mod does not inherit its settings."), - }; - if( ImGuiUtil.DrawDisabledButton( name, width, tt, noModSelected || !modInherited ) ) - { - Penumbra.CollectionManager.SetCollection( collection, CollectionType.Current ); - } - } - - // Get the correct size for the mod selector based on current config. - private static float GetModSelectorSize() - { - var absoluteSize = Math.Clamp( Penumbra.Config.ModSelectorAbsoluteSize, Configuration.Constants.MinAbsoluteSize, - Math.Min( Configuration.Constants.MaxAbsoluteSize, ImGui.GetContentRegionAvail().X - 100 ) ); - var relativeSize = Penumbra.Config.ScaleModSelector - ? Math.Clamp( Penumbra.Config.ModSelectorScaledSize, Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize ) - : 0; - return !Penumbra.Config.ScaleModSelector - ? absoluteSize - : Math.Max( absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100 ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs deleted file mode 100644 index b6d2e46c..00000000 --- a/Penumbra/UI/ConfigWindow.ResourceTab.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; -using FFXIVClientStructs.Interop; -using FFXIVClientStructs.STD; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Services; -using Penumbra.String.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private class ResourceTab : ITab - { - public ReadOnlySpan Label - => "Resource Manager"u8; - - public bool IsVisible - => Penumbra.Config.DebugMode; - - private float _hashColumnWidth; - private float _pathColumnWidth; - private float _refsColumnWidth; - private string _resourceManagerFilter = string.Empty; - - // Draw a tab to iterate over the main resource maps and see what resources are currently loaded. - public void DrawContent() - { - // Filter for resources containing the input string. - ImGui.SetNextItemWidth( -1 ); - ImGui.InputTextWithHint( "##resourceFilter", "Filter...", ref _resourceManagerFilter, Utf8GamePath.MaxGamePathLength ); - - using var child = ImRaii.Child( "##ResourceManagerTab", -Vector2.One ); - if( !child ) - { - return; - } - - unsafe - { - Penumbra.ResourceManagerService.IterateGraphs( DrawCategoryContainer ); - } - ImGui.NewLine(); - unsafe - { - ImGui.TextUnformatted( $"Static Address: 0x{( ulong )Penumbra.ResourceManagerService.ResourceManagerAddress:X} (+0x{( ulong )Penumbra.ResourceManagerService.ResourceManagerAddress - ( ulong )DalamudServices.SigScanner.Module.BaseAddress:X})" ); - ImGui.TextUnformatted( $"Actual Address: 0x{( ulong )Penumbra.ResourceManagerService.ResourceManager:X}" ); - } - } - - private unsafe void DrawResourceMap( ResourceCategory category, uint ext, StdMap< uint, Pointer< ResourceHandle > >* map ) - { - if( map == null ) - { - return; - } - - var label = GetNodeLabel( ( uint )category, ext, map->Count ); - using var tree = ImRaii.TreeNode( label ); - if( !tree || map->Count == 0 ) - { - return; - } - - using var table = ImRaii.Table( "##table", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg ); - if( !table ) - { - return; - } - - ImGui.TableSetupColumn( "Hash", ImGuiTableColumnFlags.WidthFixed, _hashColumnWidth ); - ImGui.TableSetupColumn( "Ptr", ImGuiTableColumnFlags.WidthFixed, _hashColumnWidth ); - ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthFixed, _pathColumnWidth ); - ImGui.TableSetupColumn( "Refs", ImGuiTableColumnFlags.WidthFixed, _refsColumnWidth ); - ImGui.TableHeadersRow(); - - Penumbra.ResourceManagerService.IterateResourceMap( map, ( hash, r ) => - { - // Filter unwanted names. - if( _resourceManagerFilter.Length != 0 - && !r->FileName.ToString().Contains( _resourceManagerFilter, StringComparison.OrdinalIgnoreCase ) ) - { - return; - } - - var address = $"0x{( ulong )r:X}"; - ImGuiUtil.TextNextColumn( $"0x{hash:X8}" ); - ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable( address ); - - var resource = ( Interop.Structs.ResourceHandle* )r; - ImGui.TableNextColumn(); - Text( resource ); - if( ImGui.IsItemClicked() ) - { - var data = Interop.Structs.ResourceHandle.GetData( resource ); - if( data != null ) - { - var length = ( int )Interop.Structs.ResourceHandle.GetLength( resource ); - ImGui.SetClipboardText( string.Join( " ", - new ReadOnlySpan< byte >( data, length ).ToArray().Select( b => b.ToString( "X2" ) ) ) ); - } - } - - ImGuiUtil.HoverTooltip( "Click to copy byte-wise file data to clipboard, if any." ); - - ImGuiUtil.TextNextColumn( r->RefCount.ToString() ); - } ); - } - - // Draw a full category for the resource manager. - private unsafe void DrawCategoryContainer( ResourceCategory category, - StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* map, int idx ) - { - if( map == null ) - { - return; - } - - using var tree = ImRaii.TreeNode( $"({( uint )category:D2}) {category} (Ex {idx}) - {map->Count}###{( uint )category}_{idx}" ); - if( tree ) - { - SetTableWidths(); - Penumbra.ResourceManagerService.IterateExtMap( map, ( ext, m ) => DrawResourceMap( category, ext, m ) ); - } - } - - // Obtain a label for an extension node. - private static string GetNodeLabel( uint label, uint type, ulong count ) - { - var (lowest, mid1, mid2, highest) = Functions.SplitBytes( type ); - return highest == 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}"; - } - - // Set the widths for a resource table. - private void SetTableWidths() - { - _hashColumnWidth = 100 * ImGuiHelpers.GlobalScale; - _pathColumnWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - 300 * ImGuiHelpers.GlobalScale; - _refsColumnWidth = 30 * ImGuiHelpers.GlobalScale; - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs b/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs deleted file mode 100644 index 56a757b3..00000000 --- a/Penumbra/UI/ConfigWindow.SettingsTab.Advanced.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System.Numerics; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using Penumbra.Interop; -using Penumbra.Services; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class SettingsTab - { - private void DrawAdvancedSettings() - { - var header = ImGui.CollapsingHeader( "Advanced" ); - OpenTutorial( BasicTutorialSteps.AdvancedSettings ); - - if( !header ) - { - return; - } - - Checkbox( "Auto Deduplicate on Import", - "Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files.", - Penumbra.Config.AutoDeduplicateOnImport, v => Penumbra.Config.AutoDeduplicateOnImport = v ); - Checkbox( "Keep Default Metadata Changes on Import", - "Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. " - + "Toggle this to keep them, for example if an option in a mod is supposed to disable a metadata change from a prior option.", - Penumbra.Config.KeepDefaultMetaChanges, v => Penumbra.Config.KeepDefaultMetaChanges = v ); - DrawWaitForPluginsReflection(); - DrawEnableHttpApiBox(); - DrawEnableDebugModeBox(); - DrawReloadResourceButton(); - DrawReloadFontsButton(); - ImGui.NewLine(); - } - - // Creates and destroys the web server when toggled. - private void DrawEnableHttpApiBox() - { - var http = Penumbra.Config.EnableHttpApi; - if( ImGui.Checkbox( "##http", ref http ) ) - { - if( http ) - { - _window._penumbra.HttpApi.CreateWebServer(); - } - else - { - _window._penumbra.HttpApi.ShutdownWebServer(); - } - - Penumbra.Config.EnableHttpApi = http; - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Enable HTTP API", - "Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws." ); - } - - // Should only be used for debugging. - private static void DrawEnableDebugModeBox() - { - var tmp = Penumbra.Config.DebugMode; - if( ImGui.Checkbox( "##debugMode", ref tmp ) && tmp != Penumbra.Config.DebugMode ) - { - Penumbra.Config.DebugMode = tmp; - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Enable Debug Mode", - "[DEBUG] Enable the Debug Tab and Resource Manager Tab as well as some additional data collection. Also open the config window on plugin load." ); - } - - private static void DrawReloadResourceButton() - { - if( ImGui.Button( "Reload Resident Resources" ) && Penumbra.CharacterUtility.Ready ) - { - Penumbra.ResidentResources.Reload(); - } - - ImGuiUtil.HoverTooltip( "Reload some specific files that the game keeps in memory at all times.\n" - + "You usually should not need to do this." ); - } - - private void DrawReloadFontsButton() - { - if( ImGuiUtil.DrawDisabledButton( "Reload Fonts", Vector2.Zero, "Force the game to reload its font files.", !_fontReloader.Valid ) ) - { - _fontReloader.Reload(); - } - } - - private static void DrawWaitForPluginsReflection() - { - if( !Penumbra.Dalamud.GetDalamudConfig( DalamudServices.WaitingForPluginsOption, out bool value ) ) - { - using var disabled = ImRaii.Disabled(); - Checkbox( "Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)", string.Empty, false, v => { } ); - } - else - { - Checkbox( "Wait for Plugins on Startup", "This changes a setting in the Dalamud Configuration found at /xlsettings -> General.", value, - v => Penumbra.Dalamud.SetDalamudConfig( DalamudServices.WaitingForPluginsOption, v, "doWaitForPluginsOnStartup" ) ); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs b/Penumbra/UI/ConfigWindow.SettingsTab.General.cs deleted file mode 100644 index 6995d7a3..00000000 --- a/Penumbra/UI/ConfigWindow.SettingsTab.General.cs +++ /dev/null @@ -1,351 +0,0 @@ -using System; -using System.IO; -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Services; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class SettingsTab - { - private static void Checkbox( string label, string tooltip, bool current, Action< bool > setter ) - { - using var id = ImRaii.PushId( label ); - var tmp = current; - if( ImGui.Checkbox( string.Empty, ref tmp ) && tmp != current ) - { - setter( tmp ); - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( label, tooltip ); - } - - private static int _singleGroupRadioMax = int.MaxValue; - - private void DrawSingleSelectRadioMax() - { - if( _singleGroupRadioMax == int.MaxValue ) - { - _singleGroupRadioMax = Penumbra.Config.SingleGroupRadioMax; - } - - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - if( ImGui.DragInt( "##SingleSelectRadioMax", ref _singleGroupRadioMax, 0.01f, 1 ) ) - { - _singleGroupRadioMax = Math.Max( 1, _singleGroupRadioMax ); - } - - if( ImGui.IsItemDeactivated() ) - { - if( _singleGroupRadioMax != Penumbra.Config.SingleGroupRadioMax ) - { - Penumbra.Config.SingleGroupRadioMax = _singleGroupRadioMax; - Penumbra.Config.Save(); - } - - _singleGroupRadioMax = int.MaxValue; - } - - ImGuiUtil.LabeledHelpMarker( "Upper Limit for Single-Selection Group Radio Buttons", - "All Single-Selection Groups with more options than specified here will be displayed as Combo-Boxes at the top.\n" - + "All other Single-Selection Groups will be displayed as a set of Radio-Buttons." ); - } - - private void DrawModSelectorSettings() - { -#if DEBUG - ImGui.NewLine(); // Due to the timing button. -#endif - if( !ImGui.CollapsingHeader( "General" ) ) - { - OpenTutorial( BasicTutorialSteps.GeneralSettings ); - return; - } - - OpenTutorial( BasicTutorialSteps.GeneralSettings ); - - Checkbox( "Hide Config Window when UI is Hidden", - "Hide the penumbra main window when you manually hide the in-game user interface.", Penumbra.Config.HideUiWhenUiHidden, - v => - { - Penumbra.Config.HideUiWhenUiHidden = v; - DalamudServices.PluginInterface.UiBuilder.DisableUserUiHide = !v; - } ); - Checkbox( "Hide Config Window when in Cutscenes", - "Hide the penumbra main window when you are currently watching a cutscene.", Penumbra.Config.HideUiInCutscenes, - v => - { - Penumbra.Config.HideUiInCutscenes = v; - DalamudServices.PluginInterface.UiBuilder.DisableCutsceneUiHide = !v; - } ); - Checkbox( "Hide Config Window when in GPose", - "Hide the penumbra main window when you are currently in GPose mode.", Penumbra.Config.HideUiInGPose, - v => - { - Penumbra.Config.HideUiInGPose = v; - DalamudServices.PluginInterface.UiBuilder.DisableGposeUiHide = !v; - } ); - ImGui.Dummy( _window._defaultSpace ); - - Checkbox( "Print Chat Command Success Messages to Chat", - "Chat Commands usually print messages on failure but also on success to confirm your action. You can disable this here.", - Penumbra.Config.PrintSuccessfulCommandsToChat, v => Penumbra.Config.PrintSuccessfulCommandsToChat = v ); - Checkbox( "Hide Redraw Bar in Mod Panel", "Hides the lower redraw buttons in the mod panel in your Mods tab.", - Penumbra.Config.HideRedrawBar, v => Penumbra.Config.HideRedrawBar = v ); - ImGui.Dummy( _window._defaultSpace ); - Checkbox( $"Use {AssignedCollections} 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.", - Penumbra.Config.UseCharacterCollectionInMainWindow, v => Penumbra.Config.UseCharacterCollectionInMainWindow = v ); - Checkbox( $"Use {AssignedCollections} in Adventurer Cards", - "Use the appropriate individual collection for the adventurer card you are currently looking at, based on the adventurer's name.", - Penumbra.Config.UseCharacterCollectionsInCards, v => Penumbra.Config.UseCharacterCollectionsInCards = v ); - Checkbox( $"Use {AssignedCollections} 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.", - Penumbra.Config.UseCharacterCollectionInTryOn, v => Penumbra.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.", Penumbra.Config.UseNoModsInInspect, v => Penumbra.Config.UseNoModsInInspect = v ); - Checkbox( $"Use {AssignedCollections} in Inspect Windows", - "Use the appropriate individual collection for the character you are currently inspecting, based on their name.", - Penumbra.Config.UseCharacterCollectionInInspect, v => Penumbra.Config.UseCharacterCollectionInInspect = v ); - Checkbox( $"Use {AssignedCollections} based on Ownership", - "Use the owner's name to determine the appropriate individual collection for mounts, companions, accessories and combat pets.", - Penumbra.Config.UseOwnerNameForCharacterCollection, v => Penumbra.Config.UseOwnerNameForCharacterCollection = v ); - ImGui.Dummy( _window._defaultSpace ); - DrawSingleSelectRadioMax(); - DrawFolderSortType(); - DrawAbsoluteSizeSelector(); - DrawRelativeSizeSelector(); - Checkbox( "Open Folders by Default", "Whether to start with all folders collapsed or expanded in the mod selector.", - Penumbra.Config.OpenFoldersByDefault, v => - { - Penumbra.Config.OpenFoldersByDefault = v; - _window._selector.SetFilterDirty(); - } ); - - Widget.DoubleModifierSelector( "Mod Deletion Modifier", - "A modifier you need to hold while clicking the Delete Mod button for it to take effect.", _window._inputTextWidth.X, - Penumbra.Config.DeleteModModifier, - v => - { - Penumbra.Config.DeleteModModifier = v; - Penumbra.Config.Save(); - } ); - ImGui.Dummy( _window._defaultSpace ); - Checkbox( "Always Open Import at Default Directory", - "Open the import window at the location specified here every time, forgetting your previous path.", - Penumbra.Config.AlwaysOpenDefaultImport, v => Penumbra.Config.AlwaysOpenDefaultImport = v ); - DrawDefaultModImportPath(); - DrawDefaultModAuthor(); - DrawDefaultModImportFolder(); - DrawDefaultModExportPath(); - - ImGui.NewLine(); - } - - // Store separately to use IsItemDeactivatedAfterEdit. - private float _absoluteSelectorSize = Penumbra.Config.ModSelectorAbsoluteSize; - private int _relativeSelectorSize = Penumbra.Config.ModSelectorScaledSize; - - // Different supported sort modes as a combo. - private void DrawFolderSortType() - { - var sortMode = Penumbra.Config.SortMode; - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - using var combo = ImRaii.Combo( "##sortMode", sortMode.Name ); - if( combo ) - { - foreach( var val in Configuration.Constants.ValidSortModes ) - { - if( ImGui.Selectable( val.Name, val.GetType() == sortMode.GetType() ) && val.GetType() != sortMode.GetType() ) - { - Penumbra.Config.SortMode = val; - _window._selector.SetFilterDirty(); - Penumbra.Config.Save(); - } - - ImGuiUtil.HoverTooltip( val.Description ); - } - } - - combo.Dispose(); - ImGuiUtil.LabeledHelpMarker( "Sort Mode", "Choose the sort mode for the mod selector in the mods tab." ); - } - - // Absolute size in pixels. - private void DrawAbsoluteSizeSelector() - { - if( ImGuiUtil.DragFloat( "##absoluteSize", ref _absoluteSelectorSize, _window._inputTextWidth.X, 1, - Configuration.Constants.MinAbsoluteSize, Configuration.Constants.MaxAbsoluteSize, "%.0f" ) - && _absoluteSelectorSize != Penumbra.Config.ModSelectorAbsoluteSize ) - { - Penumbra.Config.ModSelectorAbsoluteSize = _absoluteSelectorSize; - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Mod Selector Absolute Size", - "The minimal absolute size of the mod selector in the mod tab in pixels." ); - } - - // Relative size toggle and percentage. - private void DrawRelativeSizeSelector() - { - var scaleModSelector = Penumbra.Config.ScaleModSelector; - if( ImGui.Checkbox( "Scale Mod Selector With Window Size", ref scaleModSelector ) ) - { - Penumbra.Config.ScaleModSelector = scaleModSelector; - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - if( ImGuiUtil.DragInt( "##relativeSize", ref _relativeSelectorSize, _window._inputTextWidth.X - ImGui.GetCursorPosX(), 0.1f, - Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize, "%i%%" ) - && _relativeSelectorSize != Penumbra.Config.ModSelectorScaledSize ) - { - Penumbra.Config.ModSelectorScaledSize = _relativeSelectorSize; - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - ImGuiUtil.LabeledHelpMarker( "Mod Selector Relative Size", - "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window." ); - } - - private void DrawDefaultModImportPath() - { - var tmp = Penumbra.Config.DefaultModImportPath; - var spacing = new Vector2( 3 * ImGuiHelpers.GlobalScale ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, spacing ); - ImGui.SetNextItemWidth( _window._inputTextWidth.X - _window._iconButtonSize.X - spacing.X ); - if( ImGui.InputText( "##defaultModImport", ref tmp, 256 ) ) - { - Penumbra.Config.DefaultModImportPath = tmp; - } - - if( ImGui.IsItemDeactivatedAfterEdit() ) - { - Penumbra.Config.Save(); - } - - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( $"{FontAwesomeIcon.Folder.ToIconString()}##import", _window._iconButtonSize, - "Select a directory via dialog.", false, true ) ) - { - if( _dialogOpen ) - { - _dialogManager.Reset(); - _dialogOpen = false; - } - else - { - var startDir = Directory.Exists( Penumbra.Config.ModDirectory ) ? Penumbra.Config.ModDirectory : "."; - - _dialogManager.OpenFolderDialog( "Choose Default Import Directory", ( b, s ) => - { - Penumbra.Config.DefaultModImportPath = b ? s : Penumbra.Config.DefaultModImportPath; - Penumbra.Config.Save(); - _dialogOpen = false; - }, startDir ); - _dialogOpen = true; - } - } - - style.Pop(); - ImGuiUtil.LabeledHelpMarker( "Default Mod Import Directory", - "Set the directory that gets opened when using the file picker to import mods for the first time." ); - } - - private string _tempExportDirectory = string.Empty; - - private void DrawDefaultModExportPath() - { - var tmp = Penumbra.Config.ExportDirectory; - var spacing = new Vector2( 3 * ImGuiHelpers.GlobalScale ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, spacing ); - ImGui.SetNextItemWidth( _window._inputTextWidth.X - _window._iconButtonSize.X - spacing.X ); - if( ImGui.InputText( "##defaultModExport", ref tmp, 256 ) ) - { - _tempExportDirectory = tmp; - } - - if( ImGui.IsItemDeactivatedAfterEdit() ) - { - Penumbra.ModManager.UpdateExportDirectory( _tempExportDirectory, true ); - } - - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( $"{FontAwesomeIcon.Folder.ToIconString()}##export", _window._iconButtonSize, - "Select a directory via dialog.", false, true ) ) - { - if( _dialogOpen ) - { - _dialogManager.Reset(); - _dialogOpen = false; - } - else - { - var startDir = Penumbra.Config.ExportDirectory.Length > 0 && Directory.Exists( Penumbra.Config.ExportDirectory ) - ? Penumbra.Config.ExportDirectory - : Directory.Exists( Penumbra.Config.ModDirectory ) - ? Penumbra.Config.ModDirectory - : "."; - - _dialogManager.OpenFolderDialog( "Choose Default Export Directory", ( b, s ) => - { - Penumbra.ModManager.UpdateExportDirectory( b ? s : Penumbra.Config.ExportDirectory, true ); - _dialogOpen = false; - }, startDir ); - _dialogOpen = true; - } - } - - style.Pop(); - ImGuiUtil.LabeledHelpMarker( "Default Mod Export Directory", - "Set the directory mods get saved to when using the export function or loaded from when reimporting backups.\n" - + "Keep this empty to use the root directory." ); - } - - private void DrawDefaultModAuthor() - { - var tmp = Penumbra.Config.DefaultModAuthor; - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - if( ImGui.InputText( "##defaultAuthor", ref tmp, 64 ) ) - { - Penumbra.Config.DefaultModAuthor = tmp; - } - - if( ImGui.IsItemDeactivatedAfterEdit() ) - { - Penumbra.Config.Save(); - } - - ImGuiUtil.LabeledHelpMarker( "Default Mod Author", "Set the default author stored for newly created mods." ); - } - - private void DrawDefaultModImportFolder() - { - var tmp = Penumbra.Config.DefaultImportFolder; - ImGui.SetNextItemWidth( _window._inputTextWidth.X ); - if( ImGui.InputText( "##defaultImportFolder", ref tmp, 64 ) ) - { - Penumbra.Config.DefaultImportFolder = tmp; - } - - if( ImGui.IsItemDeactivatedAfterEdit() ) - { - Penumbra.Config.Save(); - } - - ImGuiUtil.LabeledHelpMarker( "Default Mod Import Folder", - "Set the default Penumbra mod folder to place newly imported mods into.\nLeave blank to import into Root." ); - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs deleted file mode 100644 index e6948019..00000000 --- a/Penumbra/UI/ConfigWindow.SettingsTab.cs +++ /dev/null @@ -1,374 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Numerics; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using Dalamud.Interface.ImGuiFileDialog; -using Dalamud.Utility; -using ImGuiNET; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Widgets; -using Penumbra.Interop.Services; -using Penumbra.Services; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - private partial class SettingsTab : ITab - { - public const int RootDirectoryMaxLength = 64; - private readonly ConfigWindow _window; - private readonly FontReloader _fontReloader; - public ReadOnlySpan Label - => "Settings"u8; - public SettingsTab( ConfigWindow window, FontReloader fontReloader ) - { - _window = window; - _fontReloader = fontReloader; - } - - public void DrawHeader() - { - OpenTutorial( BasicTutorialSteps.Fin ); - OpenTutorial( BasicTutorialSteps.Faq1 ); - OpenTutorial( BasicTutorialSteps.Faq2 ); - OpenTutorial( BasicTutorialSteps.Faq3 ); - } - - public void DrawContent() - { - using var child = ImRaii.Child( "##SettingsTab", -Vector2.One, false ); - if( !child ) - { - return; - } - - DrawEnabledBox(); - Checkbox( "Lock Main Window", "Prevent the main window from being resized or moved.", Penumbra.Config.FixMainWindow, v => - { - Penumbra.Config.FixMainWindow = v; - _window.Flags = v - ? _window.Flags | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize - : _window.Flags & ~( ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize ); - } ); - - ImGui.NewLine(); - DrawRootFolder(); - DrawRediscoverButton(); - ImGui.NewLine(); - - DrawModSelectorSettings(); - DrawColorSettings(); - DrawAdvancedSettings(); - - _dialogManager.Draw(); - DrawSupportButtons(); - } - - // Changing the base mod directory. - private string? _newModDirectory; - private readonly FileDialogManager _dialogManager = SetupFileManager(); - private bool _dialogOpen; // For toggling on/off. - - // Do not change the directory without explicitly pressing enter or this button. - // Shows up only if the current input does not correspond to the current directory. - private static bool DrawPressEnterWarning( string newName, string old, float width, bool saved ) - { - using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg ); - var w = new Vector2( width, 0 ); - var (text, valid) = CheckPath( newName, old ); - - return ( ImGui.Button( text, w ) || saved ) && valid; - } - - private static (string Text, bool Valid) CheckPath( string newName, string old ) - { - 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 ); - } - - if( Path.GetDirectoryName( newName ) == null ) - { - return ( "Path is not allowed to be a drive root. Please add a directory.", false ); - } - - var desktop = Environment.GetFolderPath( Environment.SpecialFolder.Desktop ); - if( IsSubPathOf( desktop, newName ) ) - { - return ( "Path is not allowed to be on your Desktop.", false ); - } - - var programFiles = Environment.GetFolderPath( Environment.SpecialFolder.ProgramFiles ); - var programFilesX86 = Environment.GetFolderPath( Environment.SpecialFolder.ProgramFilesX86 ); - if( IsSubPathOf( programFiles, newName ) || IsSubPathOf( programFilesX86, newName ) ) - { - return ( "Path is not allowed to be in ProgramFiles.", false ); - } - - var dalamud = DalamudServices.PluginInterface.ConfigDirectory.Parent!.Parent!; - if( IsSubPathOf( dalamud.FullName, newName ) ) - { - return ( "Path is not allowed to be inside your Dalamud directories.", false ); - } - - if( Functions.GetDownloadsFolder( out var downloads ) && IsSubPathOf( downloads, newName ) ) - { - return ( "Path is not allowed to be inside your Downloads folder.", false ); - } - - var gameDir = DalamudServices.GameData.GameData.DataPath.Parent!.Parent!.FullName; - if( IsSubPathOf( gameDir, newName ) ) - { - return ( "Path is not allowed to be inside your game folder.", false ); - } - - return ( $"Press Enter or Click Here to Save (Current Directory: {old})", true ); - } - - // Draw a directory picker button that toggles the directory picker. - // Selecting a directory does behave the same as writing in the text input, i.e. needs to be saved. - private void DrawDirectoryPickerButton() - { - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Folder.ToIconString(), _window._iconButtonSize, - "Select a directory via dialog.", false, true ) ) - { - if( _dialogOpen ) - { - _dialogManager.Reset(); - _dialogOpen = false; - } - else - { - _newModDirectory ??= Penumbra.Config.ModDirectory; - // Use the current input as start directory if it exists, - // otherwise the current mod directory, otherwise the current application directory. - var startDir = Directory.Exists( _newModDirectory ) - ? _newModDirectory - : Directory.Exists( Penumbra.Config.ModDirectory ) - ? Penumbra.Config.ModDirectory - : "."; - - _dialogManager.OpenFolderDialog( "Choose Mod Directory", ( b, s ) => - { - _newModDirectory = b ? s : _newModDirectory; - _dialogOpen = false; - }, startDir ); - _dialogOpen = true; - } - } - } - - private static void DrawOpenDirectoryButton( int id, DirectoryInfo directory, bool condition ) - { - using var _ = ImRaii.PushId( id ); - var ret = ImGui.Button( "Open Directory" ); - ImGuiUtil.HoverTooltip( "Open this directory in your configured file explorer." ); - if( ret && condition && Directory.Exists( directory.FullName ) ) - { - Process.Start( new ProcessStartInfo( directory.FullName ) - { - UseShellExecute = true, - } ); - } - } - - // Draw the text input for the mod directory, - // as well as the directory picker button and the enter warning. - private void DrawRootFolder() - { - if( _newModDirectory.IsNullOrEmpty() ) - { - _newModDirectory = Penumbra.Config.ModDirectory; - } - - var spacing = 3 * ImGuiHelpers.GlobalScale; - using var group = ImRaii.Group(); - ImGui.SetNextItemWidth( _window._inputTextWidth.X - spacing - _window._iconButtonSize.X ); - var save = ImGui.InputText( "##rootDirectory", ref _newModDirectory, 64, ImGuiInputTextFlags.EnterReturnsTrue ); - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( spacing, 0 ) ); - ImGui.SameLine(); - DrawDirectoryPickerButton(); - style.Pop(); - ImGui.SameLine(); - - const string tt = "This is where Penumbra will store your extracted mod files.\n" - + "TTMP files are not copied, just extracted.\n" - + "This directory needs to be accessible and you need write access here.\n" - + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" - + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" - + "Definitely do not place it in your Dalamud directory or any sub-directory thereof."; - ImGuiComponents.HelpMarker( tt ); - OpenTutorial( BasicTutorialSteps.GeneralTooltips ); - ImGui.SameLine(); - ImGui.TextUnformatted( "Root Directory" ); - ImGuiUtil.HoverTooltip( tt ); - - group.Dispose(); - OpenTutorial( BasicTutorialSteps.ModDirectory ); - ImGui.SameLine(); - var pos = ImGui.GetCursorPosX(); - ImGui.NewLine(); - - if( Penumbra.Config.ModDirectory != _newModDirectory - && _newModDirectory.Length != 0 - && DrawPressEnterWarning( _newModDirectory, Penumbra.Config.ModDirectory, pos, save ) ) - { - Penumbra.ModManager.DiscoverMods( _newModDirectory ); - } - } - - private static void DrawRediscoverButton() - { - DrawOpenDirectoryButton( 0, Penumbra.ModManager.BasePath, Penumbra.ModManager.Valid ); - ImGui.SameLine(); - var tt = Penumbra.ModManager.Valid - ? "Force Penumbra to completely re-scan your root directory as if it was restarted." - : "The currently selected folder is not valid. Please select a different folder."; - if( ImGuiUtil.DrawDisabledButton( "Rediscover Mods", Vector2.Zero, tt, !Penumbra.ModManager.Valid ) ) - { - Penumbra.ModManager.DiscoverMods(); - } - } - - private void DrawEnabledBox() - { - var enabled = Penumbra.Config.EnableMods; - if( ImGui.Checkbox( "Enable Mods", ref enabled ) ) - { - _window._penumbra.SetEnabled( enabled ); - } - - OpenTutorial( BasicTutorialSteps.EnableMods ); - } - - private static void DrawColorSettings() - { - if( !ImGui.CollapsingHeader( "Colors" ) ) - { - return; - } - - foreach( var color in Enum.GetValues< ColorId >() ) - { - var (defaultColor, name, description) = color.Data(); - var currentColor = Penumbra.Config.Colors.TryGetValue( color, out var current ) ? current : defaultColor; - if( Widget.ColorPicker( name, description, currentColor, c => Penumbra.Config.Colors[ color ] = c, defaultColor ) ) - { - Penumbra.Config.Save(); - } - } - - ImGui.NewLine(); - } - - public static void DrawDiscordButton( float width ) - { - const string address = @"https://discord.gg/kVva7DHV4r"; - using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.DiscordColor ); - if( ImGui.Button( "Join Discord for Support", new Vector2( width, 0 ) ) ) - { - try - { - var process = new ProcessStartInfo( address ) - { - UseShellExecute = true, - }; - Process.Start( process ); - } - catch - { - // ignored - } - } - - ImGuiUtil.HoverTooltip( $"Open {address}" ); - } - - private const string SupportInfoButtonText = "Copy Support Info to Clipboard"; - - public static void DrawSupportButton(Penumbra penumbra) - { - if( ImGui.Button( SupportInfoButtonText ) ) - { - var text = penumbra.GatherSupportInformation(); - ImGui.SetClipboardText( text ); - } - } - - private static void DrawGuideButton( float width ) - { - const string address = @"https://reniguide.info/"; - using var color = ImRaii.PushColor( ImGuiCol.Button, 0xFFCC648D ) - .Push( ImGuiCol.ButtonHovered, 0xFFB070B0 ) - .Push( ImGuiCol.ButtonActive, 0xFF9070E0 ); - if( ImGui.Button( "Beginner's Guides", new Vector2( width, 0 ) ) ) - { - try - { - var process = new ProcessStartInfo( address ) - { - UseShellExecute = true, - }; - Process.Start( process ); - } - catch - { - // ignored - } - } - - ImGuiUtil.HoverTooltip( - $"Open {address}\nImage and text based guides for most functionality of Penumbra made by Serenity.\n" - + "Not directly affiliated and potentially, but not usually out of date." ); - } - - private void DrawSupportButtons() - { - var width = ImGui.CalcTextSize( SupportInfoButtonText ).X + ImGui.GetStyle().FramePadding.X * 2; - var xPos = ImGui.GetWindowWidth() - width; - if( ImGui.GetScrollMaxY() > 0 ) - { - xPos -= ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().FramePadding.X; - } - - ImGui.SetCursorPos( new Vector2( xPos, ImGui.GetFrameHeightWithSpacing() ) ); - DrawSupportButton(_window._penumbra); - - ImGui.SetCursorPos( new Vector2( xPos, 0 ) ); - DrawDiscordButton( width ); - - ImGui.SetCursorPos( new Vector2( xPos, 2 * ImGui.GetFrameHeightWithSpacing() ) ); - DrawGuideButton( width ); - - ImGui.SetCursorPos( new Vector2( xPos, 3 * ImGui.GetFrameHeightWithSpacing() ) ); - if( ImGui.Button( "Restart Tutorial", new Vector2( width, 0 ) ) ) - { - Penumbra.Config.TutorialStep = 0; - Penumbra.Config.Save(); - } - - ImGui.SetCursorPos( new Vector2( xPos, 4 * ImGui.GetFrameHeightWithSpacing() ) ); - if( ImGui.Button( "Show Changelogs", new Vector2( width, 0 ) ) ) - { - _window._penumbra.ForceChangelogOpen(); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.Tutorial.cs b/Penumbra/UI/ConfigWindow.Tutorial.cs deleted file mode 100644 index 6c10f740..00000000 --- a/Penumbra/UI/ConfigWindow.Tutorial.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using OtterGui.Widgets; -using Penumbra.Collections; -using Penumbra.UI.Classes; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - public const string SelectedCollection = "Selected Collection"; - public const string DefaultCollection = "Base Collection"; - public const string InterfaceCollection = "Interface Collection"; - public const string ActiveCollections = "Active Collections"; - public const string AssignedCollections = "Assigned Collections"; - public const string GroupAssignment = "Group Assignment"; - public const string CharacterGroups = "Character Groups"; - public const string ConditionalGroup = "Group"; - public const string ConditionalIndividual = "Character"; - public const string IndividualAssignments = "Individual Assignments"; - - 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" - + " - any specific actor name to redraw all actors of that exactly matching name."; - - private static void UpdateTutorialStep() - { - var tutorial = Tutorial.CurrentEnabledId( Penumbra.Config.TutorialStep ); - if( tutorial != Penumbra.Config.TutorialStep ) - { - Penumbra.Config.TutorialStep = tutorial; - Penumbra.Config.Save(); - } - } - - [MethodImpl( MethodImplOptions.AggressiveInlining )] - public static void OpenTutorial( BasicTutorialSteps step ) - => Tutorial.Open( ( int )step, Penumbra.Config.TutorialStep, v => - { - Penumbra.Config.TutorialStep = v; - Penumbra.Config.Save(); - } ); - - [MethodImpl( MethodImplOptions.AggressiveInlining )] - public static void SkipTutorial( BasicTutorialSteps step ) - => Tutorial.Skip( ( int )step, Penumbra.Config.TutorialStep, v => - { - Penumbra.Config.TutorialStep = v; - Penumbra.Config.Save(); - } ); - - public enum BasicTutorialSteps - { - GeneralTooltips, - ModDirectory, - EnableMods, - AdvancedSettings, - GeneralSettings, - Collections, - EditingCollections, - CurrentCollection, - Inheritance, - ActiveCollections, - DefaultCollection, - InterfaceCollection, - SpecialCollections1, - SpecialCollections2, - Mods, - ModImport, - AdvancedHelp, - ModFilters, - CollectionSelectors, - Redrawing, - EnablingMods, - Priority, - ModOptions, - Fin, - Faq1, - Faq2, - Faq3, - Favorites, - Tags, - } - - public static readonly Tutorial 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: Editing Collections", "First, we need to open the Collection Settings.\n\n" - + "In here, we can create new collections, delete collections, or make them inherit from each other." ) - .Register( $"Initial Setup, Step 5: {SelectedCollection}", - $"The {SelectedCollection} is the one we are currently editing. Any changes we make in our mod settings later in the next tab will edit this collection." - + $"We should already have a collection named {ModCollection.DefaultCollection} selected, and for our simple setup, we do not need to do anything here.\n\n" ) - .Register( "Inheritance", - "This is a more advanced feature. Click the help button for more information, but we will ignore this for now." ) - .Register( $"Initial Setup, Step 6: {ActiveCollections}", - $"{ActiveCollections} are those that are actually assigned to conditions at the moment.\n\n" - + "Any collection assigned here will apply to the game under certain conditions.\n\n" - + $"The {SelectedCollection} is also active for technical reasons, while not necessarily being assigned to anything.\n\n" - + "Open this now to continue." ) - .Register( $"Initial Setup, Step 7: {DefaultCollection}", - $"The {DefaultCollection} - which should currently be set to a collection named {ModCollection.DefaultCollection} - is the main one.\n\n" - + $"As long as no more specific conditions apply to an object in the game, the mods from the {DefaultCollection} will be used.\n\n" - + "This is also the collection you need to use for all mods that are not directly associated with any character in the game or the user interface, like music mods." ) - .Register( "Interface Collection", - $"The {InterfaceCollection} - which should currently be set to None - is used exclusively for files categorized as 'UI' files by the game, which is mostly icons and the backgrounds for different UI windows etc.\n\n" - + $"If you have mods manipulating your interface, they should be enabled in the collection assigned to this slot. You can of course assign the same collection you assigned to the {DefaultCollection} to the {InterfaceCollection}, too, and enable all your UI mods in this one." ) - .Register( GroupAssignment + 's', - "Collections assigned here are used for groups of characters for which specific conditions are met.\n\n" - + "The more specific the condition, the higher its priority (i.e. Your Character > Player Characters > Race).\n\n" - + $"{IndividualAssignments} always take precedence before groups." ) - .Register( IndividualAssignments, - "Collections assigned here are used only for individual players or NPCs that fulfill the given criteria.\n\n" - + "They may also apply to objects 'owned' by those characters implicitly, e.g. minions or mounts - see the general settings for options on this.\n\n" ) - .Register( "Initial Setup, Step 8: 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 9: 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." ) // TODO - .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 11: 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 12: 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." ) - .Register( "FAQ 1", "Penumbra can not easily change which items a mod applies to." ) - .Register( "FAQ 2", - "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 3", "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< BasicTutorialSteps >().Length ); -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index ec149ce2..5036e706 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -1,144 +1,131 @@ using System; using System.Numerics; -using Dalamud.Interface; using Dalamud.Interface.Windowing; +using Dalamud.Plugin; using ImGuiNET; using OtterGui; using OtterGui.Raii; -using OtterGui.Widgets; using Penumbra.Api.Enums; -using Penumbra.Interop.Services; using Penumbra.Mods; -using Penumbra.Services; using Penumbra.UI.Classes; +using Penumbra.UI.Tabs; using Penumbra.Util; namespace Penumbra.UI; -public sealed partial class ConfigWindow : Window, IDisposable +public sealed class ConfigWindow : Window { - private readonly Penumbra _penumbra; - private readonly ModFileSystemSelector _selector; - private readonly ModPanel _modPanel; - public readonly ModEditWindow ModEditPopup; - private readonly Configuration _config; + private readonly DalamudPluginInterface _pluginInterface; + private readonly Configuration _config; + private readonly PerformanceTracker _tracker; + private readonly ValidityChecker _validityChecker; + private readonly Penumbra _penumbra; + private readonly ConfigTabBar _configTabs; + private string? _lastException; - private readonly SettingsTab _settingsTab; - private readonly CollectionsTab _collectionsTab; - private readonly ModsTab _modsTab; - private readonly ChangedItemsTab _changedItemsTab; - private readonly EffectiveTab _effectiveTab; - private readonly DebugTab _debugTab; - private readonly ResourceTab _resourceTab; - private readonly ResourceWatcher _resourceWatcher; + public void SelectTab(TabType tab) + => _configTabs.SelectTab = tab; - public TabType SelectTab = TabType.None; public void SelectMod(Mod mod) - => _selector.SelectByValue(mod); + => _configTabs.Mods.SelectMod = mod; - public ConfigWindow(Configuration config, CommunicatorService communicator, StartTracker timer, FontReloader fontReloader, - Penumbra penumbra, ResourceWatcher watcher) - : base(GetLabel()) + + public ConfigWindow(PerformanceTracker tracker, DalamudPluginInterface pi, Configuration config, ValidityChecker checker, + TutorialService tutorial, Penumbra penumbra, ConfigTabBar configTabs) + : base(GetLabel(checker)) { - _penumbra = penumbra; + _pluginInterface = pi; _config = config; - _resourceWatcher = watcher; + _tracker = tracker; + _validityChecker = checker; + _penumbra = penumbra; + _configTabs = configTabs; - ModEditPopup = new ModEditWindow(communicator); - _settingsTab = new SettingsTab(this, fontReloader); - _selector = new ModFileSystemSelector(communicator, _penumbra.ModFileSystem); - _modPanel = new ModPanel(this); - _modsTab = new ModsTab(_selector, _modPanel, _penumbra); - _selector.SelectionChanged += _modPanel.OnSelectionChange; - _collectionsTab = new CollectionsTab(communicator, this); - _changedItemsTab = new ChangedItemsTab(this); - _effectiveTab = new EffectiveTab(); - _debugTab = new DebugTab(this, timer); - _resourceTab = new ResourceTab(); - if (Penumbra.Config.FixMainWindow) - Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove; - - DalamudServices.PluginInterface.UiBuilder.DisableGposeUiHide = !Penumbra.Config.HideUiInGPose; - DalamudServices.PluginInterface.UiBuilder.DisableCutsceneUiHide = !Penumbra.Config.HideUiInCutscenes; - DalamudServices.PluginInterface.UiBuilder.DisableUserUiHide = !Penumbra.Config.HideUiWhenUiHidden; - RespectCloseHotkey = true; + RespectCloseHotkey = true; SizeConstraints = new WindowSizeConstraints() { MinimumSize = new Vector2(800, 600), MaximumSize = new Vector2(4096, 2160), }; - UpdateTutorialStep(); + tutorial.UpdateTutorialStep(); IsOpen = _config.DebugMode; } - private ReadOnlySpan ToLabel(TabType type) - => type switch - { - TabType.Settings => _settingsTab.Label, - TabType.Mods => _modsTab.Label, - TabType.Collections => _collectionsTab.Label, - TabType.ChangedItems => _changedItemsTab.Label, - TabType.EffectiveChanges => _effectiveTab.Label, - TabType.ResourceWatcher => _resourceWatcher.Label, - TabType.Debug => _debugTab.Label, - TabType.ResourceManager => _resourceTab.Label, - _ => ReadOnlySpan.Empty, - }; + public override void PreDraw() + { + if (_config.FixMainWindow) + Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove; + else + Flags &= ~(ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove); + } public override void Draw() { - using var performance = Penumbra.Performance.Measure(PerformanceType.UiMainWindow); - + using var timer = _tracker.Measure(PerformanceType.UiMainWindow); + UiHelpers.SetupCommonSizes(); try { if (Penumbra.ValidityChecker.ImcExceptions.Count > 0) { - DrawProblemWindow(_penumbra, - $"There were {Penumbra.ValidityChecker.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n" + DrawProblemWindow( + $"There were {_validityChecker.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n" + "This usually means that your game installation was corrupted by updating the game while having TexTools mods still active.\n" + "It is recommended to not use TexTools and Penumbra (or other Lumina-based tools) at the same time.\n\n" - + "Please use the Launcher's Repair Game Files function to repair your client installation.", true); + + "Please use the Launcher's Repair Game Files function to repair your client installation."); + DrawImcExceptions(); } else if (!Penumbra.ValidityChecker.IsValidSourceRepo) { - DrawProblemWindow(_penumbra, - $"You are loading a release version of Penumbra from the repository \"{DalamudServices.PluginInterface.SourceRepository}\" instead of the official repository.\n" + DrawProblemWindow( + $"You are loading a release version of Penumbra from the repository \"{_pluginInterface.SourceRepository}\" instead of the official repository.\n" + $"Please use the official repository at {ValidityChecker.Repository}.\n\n" - + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false); + + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it."); } else if (Penumbra.ValidityChecker.IsNotInstalledPenumbra) { - DrawProblemWindow(_penumbra, - $"You are loading a release version of Penumbra from \"{DalamudServices.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n" + DrawProblemWindow( + $"You are loading a release version of Penumbra from \"{_pluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n" + "You should not install Penumbra manually, but rather add the plugin repository under settings and then install it via the plugin installer.\n\n" + "If you do not know how to do this, please take a look at the readme in Penumbras github repository or join us in discord.\n" - + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false); + + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it."); } else if (Penumbra.ValidityChecker.DevPenumbraExists) { - DrawProblemWindow(_penumbra, - $"You are loading a installed version of Penumbra from \"{DalamudServices.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", " + DrawProblemWindow( + $"You are loading a installed version of Penumbra from \"{_pluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", " + "but also still have some remnants of a custom install of Penumbra in your devPlugins folder.\n\n" + "This can cause some issues, so please go to your \"%%appdata%%\\XIVLauncher\\devPlugins\" folder and delete the Penumbra folder from there.\n\n" - + "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode.", - false); + + "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode."); } else { - SetupSizes(); - if (TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel(SelectTab), _settingsTab, _modsTab, _collectionsTab, - _changedItemsTab, _effectiveTab, _resourceWatcher, _debugTab, _resourceTab)) - SelectTab = TabType.None; + _configTabs.Draw(); } + + _lastException = null; } catch (Exception e) { - Penumbra.Log.Error($"Exception thrown during UI Render:\n{e}"); + if (_lastException != null) + { + var text = e.ToString(); + if (text == _lastException) + return; + + _lastException = text; + } + + Penumbra.Log.Error($"Exception thrown during UI Render:\n{_lastException}"); } } - private static void DrawProblemWindow(Penumbra penumbra, string text, bool withExceptions) + private static string GetLabel(ValidityChecker checker) + => checker.Version.Length == 0 + ? "Penumbra###PenumbraConfigWindow" + : $"Penumbra v{Penumbra.Version}###PenumbraConfigWindow"; + + private void DrawProblemWindow(string text) { using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); ImGui.NewLine(); @@ -148,47 +135,23 @@ public sealed partial class ConfigWindow : Window, IDisposable ImGui.NewLine(); ImGui.NewLine(); - SettingsTab.DrawDiscordButton(0); + UiHelpers.DrawDiscordButton(0); ImGui.SameLine(); - SettingsTab.DrawSupportButton(penumbra); + UiHelpers.DrawSupportButton(_penumbra); ImGui.NewLine(); ImGui.NewLine(); + } - if (withExceptions) + private void DrawImcExceptions() + { + ImGui.TextUnformatted("Exceptions"); + ImGui.Separator(); + using var box = ImRaii.ListBox("##Exceptions", new Vector2(-1, -1)); + foreach (var exception in _validityChecker.ImcExceptions) { - ImGui.TextUnformatted("Exceptions"); + ImGuiUtil.TextWrapped(exception.ToString()); ImGui.Separator(); - using var box = ImRaii.ListBox("##Exceptions", new Vector2(-1, -1)); - foreach (var exception in Penumbra.ValidityChecker.ImcExceptions) - { - ImGuiUtil.TextWrapped(exception.ToString()); - ImGui.Separator(); - ImGui.NewLine(); - } + ImGui.NewLine(); } } - - public void Dispose() - { - _selector.Dispose(); - _modPanel.Dispose(); - _collectionsTab.Dispose(); - ModEditPopup.Dispose(); - } - - private static string GetLabel() - => Penumbra.Version.Length == 0 - ? "Penumbra###PenumbraConfigWindow" - : $"Penumbra v{Penumbra.Version}###PenumbraConfigWindow"; - - private Vector2 _defaultSpace; - private Vector2 _inputTextWidth; - private Vector2 _iconButtonSize; - - private void SetupSizes() - { - _defaultSpace = new Vector2(0, 10 * ImGuiHelpers.GlobalScale); - _inputTextWidth = new Vector2(350f * ImGuiHelpers.GlobalScale, 0); - _iconButtonSize = new Vector2(ImGui.GetFrameHeight()); - } } diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs new file mode 100644 index 00000000..2d16668f --- /dev/null +++ b/Penumbra/UI/FileDialogService.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; +using Dalamud.Utility; +using ImGuiNET; +using OtterGui; +using Penumbra.Mods; + +namespace Penumbra.UI; + +public class FileDialogService : IDisposable +{ + private readonly Mod.Manager _mods; + private readonly FileDialogManager _manager; + private readonly ConcurrentDictionary _startPaths = new(); + private bool _isOpen; + + public FileDialogService(Mod.Manager mods, Configuration config) + { + _mods = mods; + _manager = SetupFileManager(config.ModDirectory); + + _mods.ModDirectoryChanged += OnModDirectoryChange; + } + + public void OpenFilePicker(string title, string filters, Action> callback, int selectionCountMax, string? startPath, + bool forceStartPath) + { + _isOpen = true; + _manager.OpenFileDialog(title, filters, CreateCallback(title, callback), selectionCountMax, + GetStartPath(title, startPath, forceStartPath)); + } + + public void OpenFolderPicker(string title, Action callback, string? startPath, bool forceStartPath) + { + _isOpen = true; + _manager.OpenFolderDialog(title, CreateCallback(title, callback), GetStartPath(title, startPath, forceStartPath)); + } + + public void OpenSavePicker(string title, string filters, string defaultFileName, string defaultExtension, Action callback, + string? startPath, + bool forceStartPath) + { + _isOpen = true; + _manager.SaveFileDialog(title, filters, defaultFileName, defaultExtension, CreateCallback(title, callback), + GetStartPath(title, startPath, forceStartPath)); + } + + public void Close() + { + _isOpen = false; + } + + public void Reset() + { + _isOpen = false; + _manager.Reset(); + } + + public void Draw() + { + if (_isOpen) + _manager.Draw(); + } + + public void Dispose() + { + _startPaths.Clear(); + _manager.Reset(); + _mods.ModDirectoryChanged -= OnModDirectoryChange; + } + + private string? GetStartPath(string title, string? startPath, bool forceStartPath) + { + var path = !forceStartPath && _startPaths.TryGetValue(title, out var p) ? p : startPath; + if (!path.IsNullOrEmpty() && !Directory.Exists(path)) + path = null; + return path; + } + + private Action> CreateCallback(string title, Action> callback) + { + return (valid, list) => + { + _isOpen = false; + _startPaths[title] = GetCurrentLocation(); + callback(valid, list); + }; + } + + private Action CreateCallback(string title, Action callback) + { + return (valid, list) => + { + _isOpen = false; + var loc = GetCurrentLocation(); + if (loc.Length == 2) + loc += '\\'; + _startPaths[title] = loc; + callback(valid, list); + }; + } + + // TODO: maybe change this from reflection when its public. + private string GetCurrentLocation() + => (_manager.GetType().GetField("dialog", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(_manager) as FileDialog) + ?.GetCurrentPath() + ?? "."; + + /// Set up the file selector with the right flags and custom side bar items. + private static FileDialogManager SetupFileManager(string modDirectory) + { + var fileManager = new FileDialogManager + { + AddedWindowFlags = ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoDocking, + }; + + if (Functions.GetDownloadsFolder(out var downloadsFolder)) + fileManager.CustomSideBarItems.Add(("Downloads", downloadsFolder, FontAwesomeIcon.Download, -1)); + + if (Functions.GetQuickAccessFolders(out var folders)) + foreach (var ((name, path), idx) in folders.WithIndex()) + fileManager.CustomSideBarItems.Add(($"{name}##{idx}", path, FontAwesomeIcon.Folder, -1)); + + // Add Penumbra Root. This is not updated if the root changes right now. + fileManager.CustomSideBarItems.Add(("Root Directory", modDirectory, FontAwesomeIcon.Gamepad, 0)); + + // Remove Videos and Music. + fileManager.CustomSideBarItems.Add(("Videos", string.Empty, 0, -1)); + fileManager.CustomSideBarItems.Add(("Music", string.Empty, 0, -1)); + + return fileManager; + } + + /// Update the Root Directory link on changes. + private void OnModDirectoryChange(string directory, bool valid) + { + var idx = _manager.CustomSideBarItems.IndexOf(t => t.Name == "Root Directory"); + if (idx >= 0) + _manager.CustomSideBarItems.RemoveAt(idx); + + if (!valid) + return; + + if (idx >= 0) + _manager.CustomSideBarItems.Insert(idx, ("Root Directory", directory, FontAwesomeIcon.Gamepad, 0)); + else + _manager.CustomSideBarItems.Add(("Root Directory", directory, FontAwesomeIcon.Gamepad, 0)); + } +} diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs new file mode 100644 index 00000000..c6384fd0 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -0,0 +1,773 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Filesystem; +using OtterGui.FileSystem.Selector; +using OtterGui.Raii; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Import; +using Penumbra.Mods; +using Penumbra.Services; +using Penumbra.UI.Classes; +using Penumbra.Util; + +namespace Penumbra.UI.ModTab; + +public sealed partial class ModFileSystemSelector : FileSystemSelector +{ + private readonly CommunicatorService _communicator; + private readonly ChatService _chat; + private readonly Configuration _config; + private readonly FileDialogService _fileDialog; + private readonly Mod.Manager _modManager; + private readonly ModCollection.Manager _collectionManager; + private readonly TutorialService _tutorial; + + private TexToolsImporter? _import; + public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; + public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; + + public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem, Mod.Manager modManager, + ModCollection.Manager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog, ChatService chat) + : base(fileSystem, DalamudServices.KeyState) + { + _communicator = communicator; + _modManager = modManager; + _collectionManager = collectionManager; + _config = config; + _tutorial = tutorial; + _fileDialog = fileDialog; + _chat = chat; + + SubscribeRightClickFolder(EnableDescendants, 10); + SubscribeRightClickFolder(DisableDescendants, 10); + SubscribeRightClickFolder(InheritDescendants, 15); + SubscribeRightClickFolder(OwnDescendants, 15); + SubscribeRightClickFolder(SetDefaultImportFolder, 100); + SubscribeRightClickLeaf(ToggleLeafFavorite); + SubscribeRightClickMain(ClearDefaultImportFolder, 100); + AddButton(AddNewModButton, 0); + AddButton(AddImportModButton, 1); + AddButton(AddHelpButton, 2); + AddButton(DeleteModButton, 1000); + SetFilterTooltip(); + + SelectionChanged += OnSelectionChange; + _communicator.CollectionChange.Event += OnCollectionChange; + _collectionManager.Current.ModSettingChanged += OnSettingChange; + _collectionManager.Current.InheritanceChanged += OnInheritanceChange; + _modManager.ModDataChanged += OnModDataChange; + _modManager.ModDiscoveryStarted += StoreCurrentSelection; + _modManager.ModDiscoveryFinished += RestoreLastSelection; + OnCollectionChange(CollectionType.Current, null, _collectionManager.Current, ""); + } + + public override void Dispose() + { + base.Dispose(); + _modManager.ModDiscoveryStarted -= StoreCurrentSelection; + _modManager.ModDiscoveryFinished -= RestoreLastSelection; + _modManager.ModDataChanged -= OnModDataChange; + _collectionManager.Current.ModSettingChanged -= OnSettingChange; + _collectionManager.Current.InheritanceChanged -= OnInheritanceChange; + _communicator.CollectionChange.Event -= OnCollectionChange; + _import?.Dispose(); + _import = null; + } + + public new ModFileSystem.Leaf? SelectedLeaf + => base.SelectedLeaf; + + #region Interface + + // Customization points. + public override ISortMode SortMode + => Penumbra.Config.SortMode; + + protected override uint ExpandedFolderColor + => ColorId.FolderExpanded.Value(_config); + + protected override uint CollapsedFolderColor + => ColorId.FolderCollapsed.Value(_config); + + protected override uint FolderLineColor + => ColorId.FolderLine.Value(_config); + + protected override bool FoldersDefaultOpen + => Penumbra.Config.OpenFoldersByDefault; + + protected override void DrawPopups() + { + DrawHelpPopup(); + DrawInfoPopup(); + + if (ImGuiUtil.OpenNameField("Create New Mod", ref _newModName)) + try + { + var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName); + Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty); + Mod.Creator.CreateDefaultFiles(newDir); + Penumbra.ModManager.AddMod(newDir); + _newModName = string.Empty; + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not create directory for new Mod {_newModName}:\n{e}"); + } + + while (_modsToAdd.TryDequeue(out var dir)) + { + Penumbra.ModManager.AddMod(dir); + var mod = Penumbra.ModManager.LastOrDefault(); + if (mod != null) + { + MoveModToDefaultDirectory(mod); + SelectByValue(mod); + } + } + } + + protected override void DrawLeafName(FileSystem.Leaf leaf, in ModState state, bool selected) + { + var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; + using var c = ImRaii.PushColor(ImGuiCol.Text, state.Color.Value(_config)) + .Push(ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite); + using var id = ImRaii.PushId(leaf.Value.Index); + ImRaii.TreeNode(leaf.Value.Name, flags).Dispose(); + } + + + // Add custom context menu items. + private void EnableDescendants(ModFileSystem.Folder folder) + { + if (ImGui.MenuItem("Enable Descendants")) + SetDescendants(folder, true); + } + + private void DisableDescendants(ModFileSystem.Folder folder) + { + if (ImGui.MenuItem("Disable Descendants")) + SetDescendants(folder, false); + } + + private void InheritDescendants(ModFileSystem.Folder folder) + { + if (ImGui.MenuItem("Inherit Descendants")) + SetDescendants(folder, true, true); + } + + private void OwnDescendants(ModFileSystem.Folder folder) + { + if (ImGui.MenuItem("Stop Inheriting Descendants")) + SetDescendants(folder, false, true); + } + + private void ToggleLeafFavorite(FileSystem.Leaf mod) + { + if (ImGui.MenuItem(mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite")) + _modManager.ChangeModFavorite(mod.Value.Index, !mod.Value.Favorite); + } + + private void SetDefaultImportFolder(ModFileSystem.Folder folder) + { + if (ImGui.MenuItem("Set As Default Import Folder")) + { + var newName = folder.FullName(); + if (newName != _config.DefaultImportFolder) + { + _config.DefaultImportFolder = newName; + _config.Save(); + } + } + } + + private void ClearDefaultImportFolder() + { + if (ImGui.MenuItem("Clear Default Import Folder") && _config.DefaultImportFolder.Length > 0) + { + _config.DefaultImportFolder = string.Empty; + _config.Save(); + } + } + + private string _newModName = string.Empty; + + private void AddNewModButton(Vector2 size) + { + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", + !_modManager.Valid, true)) + ImGui.OpenPopup("Create New Mod"); + } + + /// Add an import mods button that opens a file selector. + private void AddImportModButton(Vector2 size) + { + var button = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), size, + "Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files.", !Penumbra.ModManager.Valid, true); + _tutorial.OpenTutorial(BasicTutorialSteps.ModImport); + if (!button) + return; + + var modPath = !_config.AlwaysOpenDefaultImport ? null + : _config.DefaultModImportPath.Length > 0 ? _config.DefaultModImportPath + : _config.ModDirectory.Length > 0 ? _config.ModDirectory : null; + + _fileDialog.OpenFilePicker("Import Mod Pack", + "Mod Packs{.ttmp,.ttmp2,.pmp},TexTools Mod Packs{.ttmp,.ttmp2},Penumbra Mod Packs{.pmp},Archives{.zip,.7z,.rar}", (s, f) => + { + if (!s) + return; + + _import = new TexToolsImporter(_modManager.BasePath, f.Count, f.Select(file => new FileInfo(file)), + AddNewMod); + ImGui.OpenPopup("Import Status"); + }, 0, modPath, _config.AlwaysOpenDefaultImport); + } + + /// Draw the progress information for import. + private void DrawInfoPopup() + { + var display = ImGui.GetIO().DisplaySize; + var height = Math.Max(display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing()); + var width = display.X / 8; + var size = new Vector2(width * 2, height); + ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2); + ImGui.SetNextWindowSize(size); + using var popup = ImRaii.Popup("Import Status", ImGuiWindowFlags.Modal); + if (_import == null || !popup.Success) + return; + + using (var child = ImRaii.Child("##import", new Vector2(-1, size.Y - ImGui.GetFrameHeight() * 2))) + { + if (child) + _import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight())); + } + + if (_import.State == ImporterState.Done && ImGui.Button("Close", -Vector2.UnitX) + || _import.State != ImporterState.Done && _import.DrawCancelButton(-Vector2.UnitX)) + { + _import?.Dispose(); + _import = null; + ImGui.CloseCurrentPopup(); + } + } + + /// Mods need to be added thread-safely outside of iteration. + private readonly ConcurrentQueue _modsToAdd = new(); + + /// + /// Clean up invalid directory if necessary. + /// Add successfully extracted mods. + /// + private void AddNewMod(FileInfo file, DirectoryInfo? dir, Exception? error) + { + if (error != null) + { + if (dir != null && Directory.Exists(dir.FullName)) + try + { + Directory.Delete(dir.FullName, true); + } + catch (Exception e) + { + Penumbra.Log.Error($"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}"); + } + + if (error is not OperationCanceledException) + Penumbra.Log.Error($"Error extracting {file.FullName}, mod skipped:\n{error}"); + } + else if (dir != null) + { + _modsToAdd.Enqueue(dir); + } + } + + private void DeleteModButton(Vector2 size) + { + var keys = _config.DeleteModModifier.IsActive(); + var tt = SelectedLeaf == null + ? "No mod selected." + : "Delete the currently selected mod entirely from your drive.\n" + + "This can not be undone."; + if (!keys) + tt += $"\nHold {_config.DeleteModModifier} while clicking to delete the mod."; + + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true) + && Selected != null) + _modManager.DeleteMod(Selected.Index); + } + + private void AddHelpButton(Vector2 size) + { + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.QuestionCircle.ToIconString(), size, "Open extended help.", false, true)) + ImGui.OpenPopup("ExtendedHelp"); + + _tutorial.OpenTutorial(BasicTutorialSteps.AdvancedHelp); + } + + private void SetDescendants(ModFileSystem.Folder folder, bool enabled, bool inherit = false) + { + var mods = folder.GetAllDescendants(ISortMode.Lexicographical).OfType().Select(l => + { + // Any mod handled here should not stay new. + _modManager.NewMods.Remove(l.Value); + return l.Value; + }); + + if (inherit) + _collectionManager.Current.SetMultipleModInheritances(mods, enabled); + else + _collectionManager.Current.SetMultipleModStates(mods, enabled); + } + + /// + /// If a default import folder is setup, try to move the given mod in there. + /// If the folder does not exist, create it if possible. + /// + /// + private void MoveModToDefaultDirectory(Mod mod) + { + if (_config.DefaultImportFolder.Length == 0) + return; + + try + { + var leaf = FileSystem.Root.GetChildren(ISortMode.Lexicographical) + .FirstOrDefault(f => f is FileSystem.Leaf l && l.Value == mod); + if (leaf == null) + throw new Exception("Mod was not found at root."); + + var folder = FileSystem.FindOrCreateAllFolders(Penumbra.Config.DefaultImportFolder); + FileSystem.Move(leaf, folder); + } + catch (Exception e) + { + _chat.NotificationMessage( + $"Could not move newly imported mod {mod.Name} to default import folder {_config.DefaultImportFolder}:\n{e}", "Warning", + NotificationType.Warning); + } + } + + private void DrawHelpPopup() + { + ImGuiUtil.HelpPopup("ExtendedHelp", new Vector2(1000 * UiHelpers.Scale, 34.5f * ImGui.GetTextLineHeightWithSpacing()), () => + { + ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); + ImGui.TextUnformatted("Mod Management"); + ImGui.BulletText("You can create empty mods or import mods with the buttons in this row."); + using var indent = ImRaii.PushIndent(); + ImGui.BulletText("Supported formats for import are: .ttmp, .ttmp2, .pmp."); + ImGui.BulletText( + "You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata."); + indent.Pop(1); + ImGui.BulletText("You can also create empty mod folders and delete mods."); + ImGui.BulletText("For further editing of mods, select them and use the Edit Mod tab in the panel or the Advanced Editing popup."); + ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight()); + ImGui.TextUnformatted("Mod Selector"); + ImGui.BulletText("Select a mod to obtain more information or change settings."); + ImGui.BulletText("Names are colored according to your config and their current state in the collection:"); + indent.Push(); + ImGuiUtil.BulletTextColored(ColorId.EnabledMod.Value(_config), "enabled in the current collection."); + ImGuiUtil.BulletTextColored(ColorId.DisabledMod.Value(_config), "disabled in the current collection."); + ImGuiUtil.BulletTextColored(ColorId.InheritedMod.Value(_config), "enabled due to inheritance from another collection."); + ImGuiUtil.BulletTextColored(ColorId.InheritedDisabledMod.Value(_config), "disabled due to inheritance from another collection."); + ImGuiUtil.BulletTextColored(ColorId.UndefinedMod.Value(_config), "unconfigured in all inherited collections."); + ImGuiUtil.BulletTextColored(ColorId.NewMod.Value(_config), + "newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded."); + ImGuiUtil.BulletTextColored(ColorId.HandledConflictMod.Value(_config), + "enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)."); + ImGuiUtil.BulletTextColored(ColorId.ConflictingMod.Value(_config), + "enabled and conflicting with another enabled Mod on the same priority."); + ImGuiUtil.BulletTextColored(ColorId.FolderExpanded.Value(_config), "expanded mod folder."); + ImGuiUtil.BulletTextColored(ColorId.FolderCollapsed.Value(_config), "collapsed mod folder"); + indent.Pop(1); + ImGui.BulletText("Right-click a mod to enter its sort order, which is its name by default, possibly with a duplicate number."); + indent.Push(); + ImGui.BulletText("A sort order differing from the mods name will not be displayed, it will just be used for ordering."); + ImGui.BulletText( + "If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically."); + indent.Pop(1); + ImGui.BulletText( + "You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod."); + ImGui.BulletText("Right-clicking a folder opens a context menu."); + ImGui.BulletText("Right-clicking empty space allows you to expand or collapse all folders at once."); + ImGui.BulletText("Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text."); + indent.Push(); + ImGui.BulletText("You can enter n:[string] to filter only for names, without path."); + ImGui.BulletText("You can enter c:[string] to filter for Changed Items instead."); + ImGui.BulletText("You can enter a:[string] to filter for Mod Authors instead."); + indent.Pop(1); + ImGui.BulletText("Use the expandable menu beside the input to filter for mods fulfilling specific criteria."); + }); + } + + #endregion + + #region Automatic cache update functions. + + private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited) + { + // TODO: maybe make more efficient + SetFilterDirty(); + if (modIdx == Selected?.Index) + OnSelectionChange(Selected, Selected, default); + } + + private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName) + { + switch (type) + { + case ModDataChangeType.Name: + case ModDataChangeType.Author: + case ModDataChangeType.ModTags: + case ModDataChangeType.LocalTags: + case ModDataChangeType.Favorite: + SetFilterDirty(); + break; + } + } + + private void OnInheritanceChange(bool _) + { + SetFilterDirty(); + OnSelectionChange(Selected, Selected, default); + } + + private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _) + { + if (collectionType != CollectionType.Current || oldCollection == newCollection) + return; + + if (oldCollection != null) + { + oldCollection.ModSettingChanged -= OnSettingChange; + oldCollection.InheritanceChanged -= OnInheritanceChange; + } + + if (newCollection != null) + { + newCollection.ModSettingChanged += OnSettingChange; + newCollection.InheritanceChanged += OnInheritanceChange; + } + + SetFilterDirty(); + OnSelectionChange(Selected, Selected, default); + } + + private void OnSelectionChange(Mod? _1, Mod? newSelection, in ModState _2) + { + if (newSelection == null) + { + SelectedSettings = ModSettings.Empty; + SelectedSettingCollection = ModCollection.Empty; + } + else + { + (var settings, SelectedSettingCollection) = _collectionManager.Current[newSelection.Index]; + SelectedSettings = settings ?? ModSettings.Empty; + } + } + + // Keep selections across rediscoveries if possible. + private string _lastSelectedDirectory = string.Empty; + + private void StoreCurrentSelection() + { + _lastSelectedDirectory = Selected?.ModPath.FullName ?? string.Empty; + ClearSelection(); + } + + private void RestoreLastSelection() + { + if (_lastSelectedDirectory.Length <= 0) + return; + + var leaf = (ModFileSystem.Leaf?)FileSystem.Root.GetAllDescendants(ISortMode.Lexicographical) + .FirstOrDefault(l => l is ModFileSystem.Leaf m && m.Value.ModPath.FullName == _lastSelectedDirectory); + Select(leaf); + _lastSelectedDirectory = string.Empty; + } + + #endregion + + #region Filters + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ModState + { + public ColorId Color; + } + + private const StringComparison IgnoreCase = StringComparison.OrdinalIgnoreCase; + private LowerString _modFilter = LowerString.Empty; + private int _filterType = -1; + private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; + + private void SetFilterTooltip() + { + FilterTooltip = "Filter mods for those where their full paths or names contain the given substring.\n" + + "Enter c:[string] to filter for mods changing specific items.\n" + + "Enter t:[string] to filter for mods set to specific tags.\n" + + "Enter n:[string] to filter only for mod names and no paths.\n" + + "Enter a:[string] to filter for mods by specific authors."; + } + + /// Appropriately identify and set the string filter and its type. + protected override bool ChangeFilter(string filterValue) + { + (_modFilter, _filterType) = filterValue.Length switch + { + 0 => (LowerString.Empty, -1), + > 1 when filterValue[1] == ':' => + filterValue[0] switch + { + 'n' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1), + 'N' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 1), + 'a' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 2), + 'A' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 2), + 'c' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 3), + 'C' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 3), + 't' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 4), + 'T' => filterValue.Length == 2 ? (LowerString.Empty, -1) : (new LowerString(filterValue[2..]), 4), + _ => (new LowerString(filterValue), 0), + }, + _ => (new LowerString(filterValue), 0), + }; + + return true; + } + + /// + /// Check the state filter for a specific pair of has/has-not flags. + /// Uses count == 0 to check for has-not and count != 0 for has. + /// Returns true if it should be filtered and false if not. + /// + private bool CheckFlags(int count, ModFilter hasNoFlag, ModFilter hasFlag) + { + return count switch + { + 0 when _stateFilter.HasFlag(hasNoFlag) => false, + 0 => true, + _ when _stateFilter.HasFlag(hasFlag) => false, + _ => true, + }; + } + + /// + /// The overwritten filter method also computes the state. + /// Folders have default state and are filtered out on the direct string instead of the other options. + /// If any filter is set, they should be hidden by default unless their children are visible, + /// or they contain the path search string. + /// + protected override bool ApplyFiltersAndState(FileSystem.IPath path, out ModState state) + { + if (path is ModFileSystem.Folder f) + { + state = default; + return ModFilterExtensions.UnfilteredStateMods != _stateFilter + || FilterValue.Length > 0 && !f.FullName().Contains(FilterValue, IgnoreCase); + } + + return ApplyFiltersAndState((ModFileSystem.Leaf)path, out state); + } + + /// Apply the string filters. + private bool ApplyStringFilters(ModFileSystem.Leaf leaf, Mod mod) + { + return _filterType switch + { + -1 => false, + 0 => !(leaf.FullName().Contains(_modFilter.Lower, IgnoreCase) || mod.Name.Contains(_modFilter)), + 1 => !mod.Name.Contains(_modFilter), + 2 => !mod.Author.Contains(_modFilter), + 3 => !mod.LowerChangedItemsString.Contains(_modFilter.Lower), + 4 => !mod.AllTagsLower.Contains(_modFilter.Lower), + _ => false, // Should never happen + }; + } + + /// Only get the text color for a mod if no filters are set. + private ColorId GetTextColor(Mod mod, ModSettings? settings, ModCollection collection) + { + if (Penumbra.ModManager.NewMods.Contains(mod)) + return ColorId.NewMod; + + if (settings == null) + return ColorId.UndefinedMod; + + if (!settings.Enabled) + return collection != _collectionManager.Current ? ColorId.InheritedDisabledMod : ColorId.DisabledMod; + + var conflicts = _collectionManager.Current.Conflicts(mod); + if (conflicts.Count == 0) + return collection != _collectionManager.Current ? ColorId.InheritedMod : ColorId.EnabledMod; + + return conflicts.Any(c => !c.Solved) + ? ColorId.ConflictingMod + : ColorId.HandledConflictMod; + } + + private bool CheckStateFilters(Mod mod, ModSettings? settings, ModCollection collection, ref ModState state) + { + var isNew = _modManager.NewMods.Contains(mod); + // Handle mod details. + if (CheckFlags(mod.TotalFileCount, ModFilter.HasNoFiles, ModFilter.HasFiles) + || CheckFlags(mod.TotalSwapCount, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps) + || CheckFlags(mod.TotalManipulations, ModFilter.HasNoMetaManipulations, ModFilter.HasMetaManipulations) + || CheckFlags(mod.HasOptions ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig) + || CheckFlags(isNew ? 1 : 0, ModFilter.NotNew, ModFilter.IsNew)) + return true; + + // Handle Favoritism + if (!_stateFilter.HasFlag(ModFilter.Favorite) && mod.Favorite + || !_stateFilter.HasFlag(ModFilter.NotFavorite) && !mod.Favorite) + return true; + + // Handle Inheritance + if (collection == _collectionManager.Current) + { + if (!_stateFilter.HasFlag(ModFilter.Uninherited)) + return true; + } + else + { + state.Color = ColorId.InheritedMod; + if (!_stateFilter.HasFlag(ModFilter.Inherited)) + return true; + } + + // Handle settings. + if (settings == null) + { + state.Color = ColorId.UndefinedMod; + if (!_stateFilter.HasFlag(ModFilter.Undefined) + || !_stateFilter.HasFlag(ModFilter.Disabled) + || !_stateFilter.HasFlag(ModFilter.NoConflict)) + return true; + } + else if (!settings.Enabled) + { + state.Color = collection == _collectionManager.Current ? ColorId.DisabledMod : ColorId.InheritedDisabledMod; + if (!_stateFilter.HasFlag(ModFilter.Disabled) + || !_stateFilter.HasFlag(ModFilter.NoConflict)) + return true; + } + else + { + if (!_stateFilter.HasFlag(ModFilter.Enabled)) + return true; + + // Conflicts can only be relevant if the mod is enabled. + var conflicts = _collectionManager.Current.Conflicts(mod); + if (conflicts.Count > 0) + { + if (conflicts.Any(c => !c.Solved)) + { + if (!_stateFilter.HasFlag(ModFilter.UnsolvedConflict)) + return true; + + state.Color = ColorId.ConflictingMod; + } + else + { + if (!_stateFilter.HasFlag(ModFilter.SolvedConflict)) + return true; + + state.Color = ColorId.HandledConflictMod; + } + } + else if (!_stateFilter.HasFlag(ModFilter.NoConflict)) + { + return true; + } + } + + // isNew color takes precedence before other colors. + if (isNew) + state.Color = ColorId.NewMod; + + return false; + } + + /// Combined wrapper for handling all filters and setting state. + private bool ApplyFiltersAndState(ModFileSystem.Leaf leaf, out ModState state) + { + state = new ModState { Color = ColorId.EnabledMod }; + var mod = leaf.Value; + var (settings, collection) = _collectionManager.Current[mod.Index]; + + if (ApplyStringFilters(leaf, mod)) + return true; + + if (_stateFilter != ModFilterExtensions.UnfilteredStateMods) + return CheckStateFilters(mod, settings, collection, ref state); + + state.Color = GetTextColor(mod, settings, collection); + return false; + } + + private void DrawFilterCombo(ref bool everything) + { + using var combo = ImRaii.Combo("##filterCombo", string.Empty, + ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest); + if (!combo) + return; + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, + ImGui.GetStyle().ItemSpacing with { Y = 3 * UiHelpers.Scale }); + var flags = (int)_stateFilter; + + + if (ImGui.Checkbox("Everything", ref everything)) + { + _stateFilter = everything ? ModFilterExtensions.UnfilteredStateMods : 0; + SetFilterDirty(); + } + + ImGui.Dummy(new Vector2(0, 5 * UiHelpers.Scale)); + foreach (ModFilter flag in Enum.GetValues(typeof(ModFilter))) + { + if (ImGui.CheckboxFlags(flag.ToName(), ref flags, (int)flag)) + { + _stateFilter = (ModFilter)flags; + SetFilterDirty(); + } + } + } + + /// Add the state filter combo-button to the right of the filter box. + protected override float CustomFilters(float width) + { + var pos = ImGui.GetCursorPos(); + var remainingWidth = width - ImGui.GetFrameHeight(); + var comboPos = new Vector2(pos.X + remainingWidth, pos.Y); + + var everything = _stateFilter == ModFilterExtensions.UnfilteredStateMods; + + ImGui.SetCursorPos(comboPos); + // Draw combo button + using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.FilterActive, !everything); + DrawFilterCombo(ref everything); + _tutorial.OpenTutorial(BasicTutorialSteps.ModFilters); + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _stateFilter = ModFilterExtensions.UnfilteredStateMods; + SetFilterDirty(); + } + + ImGuiUtil.HoverTooltip("Filter mods for their activation status.\nRight-Click to clear all filters."); + ImGui.SetCursorPos(pos); + return remainingWidth; + } + + #endregion +} diff --git a/Penumbra/UI/ModsTab/ModFilter.cs b/Penumbra/UI/ModsTab/ModFilter.cs new file mode 100644 index 00000000..4c221b21 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModFilter.cs @@ -0,0 +1,59 @@ +using System; + +namespace Penumbra.UI.ModTab; + +[Flags] +public enum ModFilter +{ + Enabled = 1 << 0, + Disabled = 1 << 1, + Favorite = 1 << 2, + NotFavorite = 1 << 3, + NoConflict = 1 << 4, + SolvedConflict = 1 << 5, + UnsolvedConflict = 1 << 6, + HasNoMetaManipulations = 1 << 7, + HasMetaManipulations = 1 << 8, + HasNoFileSwaps = 1 << 9, + HasFileSwaps = 1 << 10, + HasConfig = 1 << 11, + HasNoConfig = 1 << 12, + HasNoFiles = 1 << 13, + HasFiles = 1 << 14, + IsNew = 1 << 15, + NotNew = 1 << 16, + Inherited = 1 << 17, + Uninherited = 1 << 18, + Undefined = 1 << 19, +}; + +public static class ModFilterExtensions +{ + public const ModFilter UnfilteredStateMods = (ModFilter)((1 << 20) - 1); + + public static string ToName(this ModFilter filter) + => filter switch + { + ModFilter.Enabled => "Enabled", + ModFilter.Disabled => "Disabled", + ModFilter.Favorite => "Favorite", + ModFilter.NotFavorite => "No Favorite", + ModFilter.NoConflict => "No Conflicts", + ModFilter.SolvedConflict => "Solved Conflicts", + ModFilter.UnsolvedConflict => "Unsolved Conflicts", + ModFilter.HasNoMetaManipulations => "No Meta Manipulations", + ModFilter.HasMetaManipulations => "Meta Manipulations", + ModFilter.HasNoFileSwaps => "No File Swaps", + ModFilter.HasFileSwaps => "File Swaps", + ModFilter.HasNoConfig => "No Configuration", + ModFilter.HasConfig => "Configuration", + ModFilter.HasNoFiles => "No Files", + ModFilter.HasFiles => "Files", + ModFilter.IsNew => "Newly Imported", + ModFilter.NotNew => "Not Newly Imported", + ModFilter.Inherited => "Inherited Configuration", + ModFilter.Uninherited => "Own Configuration", + ModFilter.Undefined => "Not Configured", + _ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null), + }; +} \ No newline at end of file diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs new file mode 100644 index 00000000..e6999412 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanel.cs @@ -0,0 +1,60 @@ +using System; +using Dalamud.Plugin; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModTab; + +public class ModPanel : IDisposable +{ + private readonly ModFileSystemSelector _selector; + private readonly ModEditWindow _editWindow; + private readonly ModPanelHeader _header; + private readonly ModPanelTabBar _tabs; + + public ModPanel(DalamudPluginInterface pi, ModFileSystemSelector selector, ModEditWindow editWindow, ModPanelTabBar tabs) + { + _selector = selector; + _editWindow = editWindow; + _tabs = tabs; + _header = new ModPanelHeader(pi); + _selector.SelectionChanged += OnSelectionChange; + } + + public void Draw() + { + if (!_valid) + return; + + _header.Draw(); + _tabs.Draw(_mod); + } + + public void Dispose() + { + _selector.SelectionChanged -= OnSelectionChange; + _header.Dispose(); + } + + private bool _valid; + private Mod _mod = null!; + + private void OnSelectionChange(Mod? old, Mod? mod, in ModFileSystemSelector.ModState _) + { + if (mod == null || _selector.Selected == null) + { + _editWindow.IsOpen = false; + _valid = false; + } + else + { + if (_editWindow.IsOpen) + _editWindow.ChangeMod(mod); + _valid = true; + _mod = mod; + _header.UpdateModData(_mod); + _tabs.Settings.Reset(); + _tabs.Edit.Reset(); + } + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs new file mode 100644 index 00000000..49f5d8cf --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -0,0 +1,40 @@ +using System; +using System.Numerics; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Api; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModTab; + +public class ModPanelChangedItemsTab : ITab +{ + private readonly ModFileSystemSelector _selector; + private readonly PenumbraApi _api; + + public ReadOnlySpan Label + => "Changed Items"u8; + + public ModPanelChangedItemsTab(PenumbraApi api, ModFileSystemSelector selector) + { + _api = api; + _selector = selector; + } + + public bool IsVisible + => _selector.Selected!.ChangedItems.Count > 0; + + public void DrawContent() + { + using var list = ImRaii.ListBox("##changedItems", -Vector2.One); + if (!list) + return; + + var zipList = ZipList.FromSortedList(_selector.Selected!.ChangedItems); + var height = ImGui.GetTextLineHeight(); + ImGuiClip.ClippedDraw(zipList, kvp => UiHelpers.DrawChangedItem(_api, kvp.Item1, kvp.Item2, true), height); + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs new file mode 100644 index 00000000..bcdb0f16 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -0,0 +1,69 @@ +using System; +using System.Numerics; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Collections; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using Penumbra.String.Classes; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModTab; + +public class ModPanelConflictsTab : ITab +{ + private readonly ModFileSystemSelector _selector; + private readonly ModCollection.Manager _collectionManager; + + public ModPanelConflictsTab(ModCollection.Manager collectionManager, ModFileSystemSelector selector) + { + _collectionManager = collectionManager; + _selector = selector; + } + + public ReadOnlySpan Label + => "Conflicts"u8; + + public bool IsVisible + => _collectionManager.Current.Conflicts(_selector.Selected!).Count > 0; + + public void DrawContent() + { + using var box = ImRaii.ListBox("##conflicts", -Vector2.One); + if (!box) + return; + + // Can not be null because otherwise the tab bar is never drawn. + var mod = _selector.Selected!; + foreach (var conflict in Penumbra.CollectionManager.Current.Conflicts(mod)) + { + if (ImGui.Selectable(conflict.Mod2.Name) && conflict.Mod2 is Mod otherMod) + _selector.SelectByValue(otherMod); + + ImGui.SameLine(); + using (var color = ImRaii.PushColor(ImGuiCol.Text, + conflict.HasPriority ? ColorId.HandledConflictMod.Value(Penumbra.Config) : ColorId.ConflictingMod.Value(Penumbra.Config))) + { + var priority = conflict.Mod2.Index < 0 + ? conflict.Mod2.Priority + : _collectionManager.Current[conflict.Mod2.Index].Settings!.Priority; + ImGui.TextUnformatted($"(Priority {priority})"); + } + + using var indent = ImRaii.PushIndent(30f); + foreach (var data in conflict.Conflicts) + { + unsafe + { + var _ = data switch + { + Utf8GamePath p => ImGuiNative.igSelectable_Bool(p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) > 0, + MetaManipulation m => ImGui.Selectable(m.Manipulation?.ToString() ?? string.Empty), + _ => false, + }; + } + } + } + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs new file mode 100644 index 00000000..3cc770a2 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs @@ -0,0 +1,57 @@ +using System; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui; +using OtterGui.Widgets; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModTab; + +public class ModPanelDescriptionTab : ITab +{ + private readonly ModFileSystemSelector _selector; + private readonly TutorialService _tutorial; + private readonly Mod.Manager _modManager; + private readonly TagButtons _localTags = new(); + private readonly TagButtons _modTags = new(); + + public ModPanelDescriptionTab(ModFileSystemSelector selector, TutorialService tutorial, Mod.Manager modManager) + { + _selector = selector; + _tutorial = tutorial; + _modManager = modManager; + } + + public ReadOnlySpan Label + => "Description"u8; + + public void DrawContent() + { + using var child = ImRaii.Child("##description"); + if (!child) + return; + + ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); + + ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); + 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); + _tutorial.OpenTutorial(BasicTutorialSteps.Tags); + if (tagIdx >= 0) + _modManager.ChangeLocalTag(_selector.Selected!.Index, tagIdx, editedTag); + + 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 var _, false, + ImGui.CalcTextSize("Local ").X - ImGui.CalcTextSize("Mod ").X); + + ImGui.Dummy(ImGuiHelpers.ScaledVector2(2)); + ImGui.Separator(); + + ImGuiUtil.TextWrapped(_selector.Selected!.Description); + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs new file mode 100644 index 00000000..89bdff92 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -0,0 +1,718 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.Internal.Notifications; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Api.Enums; +using Penumbra.Mods; +using Penumbra.UI.Classes; +using Penumbra.Util; + +namespace Penumbra.UI.ModTab; + +public class ModPanelEditTab : ITab +{ + private readonly ChatService _chat; + private readonly Mod.Manager _modManager; + private readonly ModFileSystem _fileSystem; + private readonly ModFileSystemSelector _selector; + private readonly ModEditWindow _editWindow; + + private readonly TagButtons _modTags = new(); + + private Vector2 _cellPadding = Vector2.Zero; + private Vector2 _itemSpacing = Vector2.Zero; + private ModFileSystem.Leaf _leaf = null!; + private Mod _mod = null!; + + public ModPanelEditTab(Mod.Manager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat, + ModEditWindow editWindow) + { + _modManager = modManager; + _selector = selector; + _fileSystem = fileSystem; + _chat = chat; + _editWindow = editWindow; + } + + public ReadOnlySpan Label + => "Edit Mod"u8; + + public void DrawContent() + { + using var child = ImRaii.Child("##editChild", -Vector2.One); + if (!child) + return; + + _leaf = _selector.SelectedLeaf!; + _mod = _selector.Selected!; + + _cellPadding = ImGui.GetStyle().CellPadding with { X = 2 * UiHelpers.Scale }; + _itemSpacing = ImGui.GetStyle().CellPadding with { X = 4 * UiHelpers.Scale }; + + EditButtons(); + EditRegularMeta(); + UiHelpers.DefaultLineSpace(); + + if (Input.Text("Mod Path", Input.Path, Input.None, _leaf.FullName(), out var newPath, 256, UiHelpers.InputTextWidth.X)) + try + { + _fileSystem.RenameAndMove(_leaf, newPath); + } + catch (Exception e) + { + _chat.NotificationMessage(e.Message, "Warning", NotificationType.Warning); + } + + UiHelpers.DefaultLineSpace(); + var tagIdx = _modTags.Draw("Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, + out var editedTag); + if (tagIdx >= 0) + _modManager.ChangeModTag(_mod.Index, tagIdx, editedTag); + + UiHelpers.DefaultLineSpace(); + AddOptionGroup.Draw(_modManager, _mod); + UiHelpers.DefaultLineSpace(); + + for (var groupIdx = 0; groupIdx < _mod.Groups.Count; ++groupIdx) + EditGroup(groupIdx); + + EndActions(); + DescriptionEdit.DrawPopup(_modManager); + } + + public void Reset() + { + AddOptionGroup.Reset(); + MoveDirectory.Reset(); + Input.Reset(); + OptionTable.Reset(); + } + + /// The general edit row for non-detailed mod edits. + private void EditButtons() + { + var buttonSize = new Vector2(150 * UiHelpers.Scale, 0); + var folderExists = Directory.Exists(_mod.ModPath.FullName); + var tt = folderExists + ? $"Open \"{_mod.ModPath.FullName}\" in the file explorer of your choice." + : $"Mod directory \"{_mod.ModPath.FullName}\" does not exist."; + if (ImGuiUtil.DrawDisabledButton("Open Mod Directory", buttonSize, tt, !folderExists)) + Process.Start(new ProcessStartInfo(_mod.ModPath.FullName) { UseShellExecute = true }); + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Reload Mod", buttonSize, "Reload the current mod from its files.\n" + + "If the mod directory or meta file do not exist anymore or if the new mod name is empty, the mod is deleted instead.", + false)) + _modManager.ReloadMod(_mod.Index); + + BackupButtons(buttonSize); + MoveDirectory.Draw(_modManager, _mod, buttonSize); + + UiHelpers.DefaultLineSpace(); + DrawUpdateBibo(buttonSize); + + UiHelpers.DefaultLineSpace(); + } + + private void DrawUpdateBibo(Vector2 buttonSize) + { + if (ImGui.Button("Update Bibo Material", buttonSize)) + { + var editor = new Mod.Editor(_mod, null); + editor.ReplaceAllMaterials("bibo", "b"); + editor.ReplaceAllMaterials("bibopube", "c"); + editor.SaveAllModels(); + _editWindow.UpdateModels(); + } + + ImGuiUtil.HoverTooltip( + "For every model in this mod, change all material names that end in a _b or _c suffix to a _bibo or _bibopube suffix respectively.\n" + + "Does nothing if the mod does not contain any such models or no model contains such materials.\n" + + "Use this for outdated mods made for old Bibo bodies.\n" + + "Go to Advanced Editing for more fine-tuned control over material assignment."); + } + + private void BackupButtons(Vector2 buttonSize) + { + var backup = new ModBackup(_mod); + var tt = ModBackup.CreatingBackup + ? "Already exporting a mod." + : backup.Exists + ? $"Overwrite current exported mod \"{backup.Name}\" with current mod." + : $"Create exported archive of current mod at \"{backup.Name}\"."; + if (ImGuiUtil.DrawDisabledButton("Export Mod", buttonSize, tt, ModBackup.CreatingBackup)) + backup.CreateAsync(); + + ImGui.SameLine(); + tt = backup.Exists + ? $"Delete existing mod export \"{backup.Name}\"." + : $"Exported mod \"{backup.Name}\" does not exist."; + if (ImGuiUtil.DrawDisabledButton("Delete Export", buttonSize, tt, !backup.Exists)) + backup.Delete(); + + tt = backup.Exists + ? $"Restore mod from exported file \"{backup.Name}\"." + : $"Exported mod \"{backup.Name}\" does not exist."; + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Restore From Export", buttonSize, tt, !backup.Exists)) + backup.Restore(); + } + + /// Anything about editing the regular meta information about the mod. + private void EditRegularMeta() + { + if (Input.Text("Name", Input.Name, Input.None, _mod.Name, out var newName, 256, UiHelpers.InputTextWidth.X)) + _modManager.ChangeModName(_mod.Index, newName); + + if (Input.Text("Author", Input.Author, Input.None, _mod.Author, out var newAuthor, 256, UiHelpers.InputTextWidth.X)) + Penumbra.ModManager.ChangeModAuthor(_mod.Index, newAuthor); + + if (Input.Text("Version", Input.Version, Input.None, _mod.Version, out var newVersion, 32, + UiHelpers.InputTextWidth.X)) + _modManager.ChangeModVersion(_mod.Index, newVersion); + + if (Input.Text("Website", Input.Website, Input.None, _mod.Website, out var newWebsite, 256, + UiHelpers.InputTextWidth.X)) + _modManager.ChangeModWebsite(_mod.Index, newWebsite); + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3)); + + var reducedSize = new Vector2(UiHelpers.InputTextMinusButton3, 0); + if (ImGui.Button("Edit Description", reducedSize)) + _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, Input.Description)); + + ImGui.SameLine(); + var fileExists = File.Exists(_mod.MetaFile.FullName); + var tt = fileExists + ? "Open the metadata json file in the text editor of your choice." + : "The metadata json file does not exist."; + if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##metaFile", UiHelpers.IconButtonSize, tt, + !fileExists, true)) + Process.Start(new ProcessStartInfo(_mod.MetaFile.FullName) { UseShellExecute = true }); + } + + /// Do some edits outside of iterations. + private readonly Queue _delayedActions = new(); + + /// Delete a marked group or option outside of iteration. + private void EndActions() + { + while (_delayedActions.TryDequeue(out var action)) + action.Invoke(); + } + + /// Text input to add a new option group at the end of the current groups. + private static class AddOptionGroup + { + private static string _newGroupName = string.Empty; + + public static void Reset() + => _newGroupName = string.Empty; + + public static void Draw(Mod.Manager modManager, Mod mod) + { + using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, UiHelpers.ScaleX3); + ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); + ImGui.InputTextWithHint("##newGroup", "Add new option group...", ref _newGroupName, 256); + ImGui.SameLine(); + var fileExists = File.Exists(mod.DefaultFile); + var tt = fileExists + ? "Open the default option json file in the text editor of your choice." + : "The default option json file does not exist."; + if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.FileExport.ToIconString()}##defaultFile", UiHelpers.IconButtonSize, tt, + !fileExists, true)) + Process.Start(new ProcessStartInfo(mod.DefaultFile) { UseShellExecute = true }); + + ImGui.SameLine(); + + var nameValid = modManager.VerifyFileName(mod, null, _newGroupName, false); + tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, + tt, !nameValid, true)) + return; + + modManager.AddModGroup(mod, GroupType.Single, _newGroupName); + Reset(); + } + } + + /// A text input for the new directory name and a button to apply the move. + private static class MoveDirectory + { + private static string? _currentModDirectory; + private static Mod.Manager.NewDirectoryState _state = Mod.Manager.NewDirectoryState.Identical; + + public static void Reset() + { + _currentModDirectory = null; + _state = Mod.Manager.NewDirectoryState.Identical; + } + + public static void Draw(Mod.Manager modManager, Mod mod, Vector2 buttonSize) + { + ImGui.SetNextItemWidth(buttonSize.X * 2 + ImGui.GetStyle().ItemSpacing.X); + var tmp = _currentModDirectory ?? mod.ModPath.Name; + if (ImGui.InputText("##newModMove", ref tmp, 64)) + { + _currentModDirectory = tmp; + _state = modManager.NewDirectoryValid(mod.ModPath.Name, _currentModDirectory, out _); + } + + var (disabled, tt) = _state switch + { + Mod.Manager.NewDirectoryState.Identical => (true, "Current directory name is identical to new one."), + Mod.Manager.NewDirectoryState.Empty => (true, "Please enter a new directory name first."), + Mod.Manager.NewDirectoryState.NonExisting => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), + Mod.Manager.NewDirectoryState.ExistsEmpty => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), + Mod.Manager.NewDirectoryState.ExistsNonEmpty => (true, $"{_currentModDirectory} already exists and is not empty."), + Mod.Manager.NewDirectoryState.ExistsAsFile => (true, $"{_currentModDirectory} exists as a file."), + Mod.Manager.NewDirectoryState.ContainsInvalidSymbols => (true, + $"{_currentModDirectory} contains invalid symbols for FFXIV."), + _ => (true, "Unknown error."), + }; + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton("Rename Mod Directory", buttonSize, tt, disabled) && _currentModDirectory != null) + { + modManager.MoveModDirectory(mod.Index, _currentModDirectory); + Reset(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "The mod directory name is used to correspond stored settings and sort orders, otherwise it has no influence on anything that is displayed.\n" + + "This can currently not be used on pre-existing folders and does not support merges or overwriting."); + } + } + + /// Open a popup to edit a multi-line mod or option description. + private static class DescriptionEdit + { + private const string PopupName = "Edit Description"; + private static string _newDescription = string.Empty; + private static int _newDescriptionIdx = -1; + private static int _newDescriptionOptionIdx = -1; + private static Mod? _mod; + + public static void OpenPopup(Mod mod, int groupIdx, int optionIdx = -1) + { + _newDescriptionIdx = groupIdx; + _newDescriptionOptionIdx = optionIdx; + _newDescription = groupIdx < 0 + ? mod.Description + : optionIdx < 0 + ? mod.Groups[groupIdx].Description + : mod.Groups[groupIdx][optionIdx].Description; + + _mod = mod; + ImGui.OpenPopup(PopupName); + } + + public static void DrawPopup(Mod.Manager modManager) + { + if (_mod == null) + return; + + using var popup = ImRaii.Popup(PopupName); + if (!popup) + return; + + if (ImGui.IsWindowAppearing()) + ImGui.SetKeyboardFocusHere(); + + ImGui.InputTextMultiline("##editDescription", ref _newDescription, 4096, ImGuiHelpers.ScaledVector2(800, 800)); + UiHelpers.DefaultLineSpace(); + + var buttonSize = ImGuiHelpers.ScaledVector2(100, 0); + var width = 2 * buttonSize.X + + 4 * ImGui.GetStyle().FramePadding.X + + ImGui.GetStyle().ItemSpacing.X; + ImGui.SetCursorPosX((800 * UiHelpers.Scale - width) / 2); + + var oldDescription = _newDescriptionIdx == Input.Description + ? _mod.Description + : _mod.Groups[_newDescriptionIdx].Description; + + var tooltip = _newDescription != oldDescription ? string.Empty : "No changes made yet."; + + if (ImGuiUtil.DrawDisabledButton("Save", buttonSize, tooltip, tooltip.Length > 0)) + { + switch (_newDescriptionIdx) + { + case Input.Description: + modManager.ChangeModDescription(_mod.Index, _newDescription); + break; + case >= 0: + if (_newDescriptionOptionIdx < 0) + modManager.ChangeGroupDescription(_mod, _newDescriptionIdx, _newDescription); + else + modManager.ChangeOptionDescription(_mod, _newDescriptionIdx, _newDescriptionOptionIdx, _newDescription); + + break; + } + + ImGui.CloseCurrentPopup(); + } + + ImGui.SameLine(); + if (!ImGui.Button("Cancel", buttonSize) + && !ImGui.IsKeyPressed(ImGuiKey.Escape)) + return; + + _newDescriptionIdx = Input.None; + _newDescription = string.Empty; + ImGui.CloseCurrentPopup(); + } + } + + private void EditGroup(int groupIdx) + { + var group = _mod.Groups[groupIdx]; + using var id = ImRaii.PushId(groupIdx); + using var frame = ImRaii.FramedGroup($"Group #{groupIdx + 1}"); + + using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, _cellPadding) + .Push(ImGuiStyleVar.ItemSpacing, _itemSpacing); + + if (Input.Text("##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, UiHelpers.InputTextWidth.X)) + _modManager.RenameModGroup(_mod, groupIdx, newGroupName); + + ImGuiUtil.HoverTooltip("Group Name"); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, + "Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) + _delayedActions.Enqueue(() => _modManager.DeleteModGroup(_mod, groupIdx)); + + ImGui.SameLine(); + + if (Input.Priority("##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * UiHelpers.Scale)) + _modManager.ChangeGroupPriority(_mod, groupIdx, priority); + + ImGuiUtil.HoverTooltip("Group Priority"); + + DrawGroupCombo(group, groupIdx); + ImGui.SameLine(); + + var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}."; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize, + tt, groupIdx == 0, true)) + _delayedActions.Enqueue(() => _modManager.MoveModGroup(_mod, groupIdx, groupIdx - 1)); + + ImGui.SameLine(); + tt = groupIdx == _mod.Groups.Count - 1 + ? "Can not move this group further downwards." + : $"Move this group down to group {groupIdx + 2}."; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize, + tt, groupIdx == _mod.Groups.Count - 1, true)) + _delayedActions.Enqueue(() => _modManager.MoveModGroup(_mod, groupIdx, groupIdx + 1)); + + ImGui.SameLine(); + + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, + "Edit group description.", false, true)) + _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, groupIdx)); + + ImGui.SameLine(); + var fileName = group.FileName(_mod.ModPath, groupIdx); + var fileExists = File.Exists(fileName); + tt = fileExists + ? $"Open the {group.Name} json file in the text editor of your choice." + : $"The {group.Name} json file does not exist."; + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileExport.ToIconString(), UiHelpers.IconButtonSize, tt, !fileExists, true)) + Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true }); + + UiHelpers.DefaultLineSpace(); + + OptionTable.Draw(this, groupIdx); + } + + /// Draw the table displaying all options and the add new option line. + private static class OptionTable + { + private const string DragDropLabel = "##DragOption"; + + private static int _newOptionNameIdx = -1; + private static string _newOptionName = string.Empty; + private static int _dragDropGroupIdx = -1; + private static int _dragDropOptionIdx = -1; + + public static void Reset() + { + _newOptionNameIdx = -1; + _newOptionName = string.Empty; + _dragDropGroupIdx = -1; + _dragDropOptionIdx = -1; + } + + public static void Draw(ModPanelEditTab panel, int groupIdx) + { + using var table = ImRaii.Table(string.Empty, 6, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImGui.TableSetupColumn("idx", ImGuiTableColumnFlags.WidthFixed, 60 * UiHelpers.Scale); + ImGui.TableSetupColumn("default", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight()); + ImGui.TableSetupColumn("name", ImGuiTableColumnFlags.WidthFixed, + UiHelpers.InputTextWidth.X - 72 * UiHelpers.Scale - ImGui.GetFrameHeight() - UiHelpers.IconButtonSize.X); + ImGui.TableSetupColumn("description", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); + ImGui.TableSetupColumn("delete", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); + ImGui.TableSetupColumn("priority", ImGuiTableColumnFlags.WidthFixed, 50 * UiHelpers.Scale); + + var group = panel._mod.Groups[groupIdx]; + for (var optionIdx = 0; optionIdx < group.Count; ++optionIdx) + EditOption(panel, group, groupIdx, optionIdx); + + DrawNewOption(panel, groupIdx, UiHelpers.IconButtonSize); + } + + /// Draw a line for a single option. + private static void EditOption(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) + { + var option = group[optionIdx]; + using var id = ImRaii.PushId(optionIdx); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Selectable($"Option #{optionIdx + 1}"); + Source(group, groupIdx, optionIdx); + Target(panel, group, groupIdx, optionIdx); + + ImGui.TableNextColumn(); + + + if (group.Type == GroupType.Single) + { + if (ImGui.RadioButton("##default", group.DefaultSettings == optionIdx)) + panel._modManager.ChangeModGroupDefaultOption(panel._mod, groupIdx, (uint)optionIdx); + + ImGuiUtil.HoverTooltip($"Set {option.Name} as the default choice for this group."); + } + else + { + var isDefaultOption = ((group.DefaultSettings >> optionIdx) & 1) != 0; + if (ImGui.Checkbox("##default", ref isDefaultOption)) + panel._modManager.ChangeModGroupDefaultOption(panel._mod, groupIdx, isDefaultOption + ? group.DefaultSettings | (1u << optionIdx) + : group.DefaultSettings & ~(1u << optionIdx)); + + ImGuiUtil.HoverTooltip($"{(isDefaultOption ? "Disable" : "Enable")} {option.Name} per default in this group."); + } + + ImGui.TableNextColumn(); + if (Input.Text("##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1)) + panel._modManager.RenameOption(panel._mod, groupIdx, optionIdx, newOptionName); + + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit option description.", + false, true)) + panel._delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(panel._mod, groupIdx, optionIdx)); + + ImGui.TableNextColumn(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, + "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) + panel._delayedActions.Enqueue(() => panel._modManager.DeleteOption(panel._mod, groupIdx, optionIdx)); + + ImGui.TableNextColumn(); + if (group.Type != GroupType.Multi) + return; + + if (Input.Priority("##Priority", groupIdx, optionIdx, group.OptionPriority(optionIdx), out var priority, + 50 * UiHelpers.Scale)) + panel._modManager.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); + + ImGuiUtil.HoverTooltip("Option priority."); + } + + /// Draw the line to add a new option. + private static void DrawNewOption(ModPanelEditTab panel, int groupIdx, Vector2 iconButtonSize) + { + var mod = panel._mod; + var group = mod.Groups[groupIdx]; + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Selectable($"Option #{group.Count + 1}"); + Target(panel, group, groupIdx, group.Count); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(-1); + var tmp = _newOptionNameIdx == groupIdx ? _newOptionName : string.Empty; + if (ImGui.InputTextWithHint("##newOption", "Add new option...", ref tmp, 256)) + { + _newOptionName = tmp; + _newOptionNameIdx = groupIdx; + } + + ImGui.TableNextColumn(); + var canAddGroup = mod.Groups[groupIdx].Type != GroupType.Multi || mod.Groups[groupIdx].Count < IModGroup.MaxMultiOptions; + var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; + var tt = canAddGroup + ? validName ? "Add a new option to this group." : "Please enter a name for the new option." + : $"Can not add more than {IModGroup.MaxMultiOptions} options to a multi group."; + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconButtonSize, + tt, !(canAddGroup && validName), true)) + return; + + panel._modManager.AddOption(mod, groupIdx, _newOptionName); + _newOptionName = string.Empty; + } + + // Handle drag and drop to move options inside a group or into another group. + private static void Source(IModGroup group, int groupIdx, int optionIdx) + { + using var source = ImRaii.DragDropSource(); + if (!source) + return; + + if (ImGui.SetDragDropPayload(DragDropLabel, IntPtr.Zero, 0)) + { + _dragDropGroupIdx = groupIdx; + _dragDropOptionIdx = optionIdx; + } + + ImGui.TextUnformatted($"Dragging option {group[optionIdx].Name} from group {group.Name}..."); + } + + private static void Target(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) + { + using var target = ImRaii.DragDropTarget(); + if (!target.Success || !ImGuiUtil.IsDropping(DragDropLabel)) + return; + + if (_dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0) + { + if (_dragDropGroupIdx == groupIdx) + { + var sourceOption = _dragDropOptionIdx; + panel._delayedActions.Enqueue(() => panel._modManager.MoveOption(panel._mod, groupIdx, sourceOption, optionIdx)); + } + else + { + // Move from one group to another by deleting, then adding, then moving the option. + var sourceGroupIdx = _dragDropGroupIdx; + var sourceOption = _dragDropOptionIdx; + var sourceGroup = panel._mod.Groups[sourceGroupIdx]; + var currentCount = group.Count; + var option = sourceGroup[sourceOption]; + var priority = sourceGroup.OptionPriority(_dragDropOptionIdx); + panel._delayedActions.Enqueue(() => + { + panel._modManager.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); + panel._modManager.AddOption(panel._mod, groupIdx, option, priority); + panel._modManager.MoveOption(panel._mod, groupIdx, currentCount, optionIdx); + }); + } + } + + _dragDropGroupIdx = -1; + _dragDropOptionIdx = -1; + } + } + + /// Draw a combo to select single or multi group and switch between them. + private void DrawGroupCombo(IModGroup group, int groupIdx) + { + static string GroupTypeName(GroupType type) + => type switch + { + GroupType.Single => "Single Group", + GroupType.Multi => "Multi Group", + _ => "Unknown", + }; + + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - 3 * (UiHelpers.IconButtonSize.X - 4 * UiHelpers.Scale)); + using var combo = ImRaii.Combo("##GroupType", GroupTypeName(group.Type)); + if (!combo) + return; + + if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single)) + _modManager.ChangeModGroupType(_mod, groupIdx, GroupType.Single); + + var canSwitchToMulti = group.Count <= IModGroup.MaxMultiOptions; + using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti); + if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti) + _modManager.ChangeModGroupType(_mod, groupIdx, GroupType.Multi); + + style.Pop(); + if (!canSwitchToMulti) + ImGuiUtil.HoverTooltip($"Can not convert group to multi group since it has more than {IModGroup.MaxMultiOptions} options."); + } + + /// Handles input text and integers in separate fields without buffers for every single one. + private static class Input + { + // Special field indices to reuse the same string buffer. + public const int None = -1; + public const int Name = -2; + public const int Author = -3; + public const int Version = -4; + public const int Website = -5; + public const int Path = -6; + public const int Description = -7; + + // Temporary strings + private static string? _currentEdit; + private static int? _currentGroupPriority; + private static int _currentField = None; + private static int _optionIndex = None; + + public static void Reset() + { + _currentEdit = null; + _currentGroupPriority = null; + _currentField = None; + _optionIndex = None; + } + + public static bool Text(string label, int field, int option, string oldValue, out string value, uint maxLength, float width) + { + var tmp = field == _currentField && option == _optionIndex ? _currentEdit ?? oldValue : oldValue; + ImGui.SetNextItemWidth(width); + if (ImGui.InputText(label, ref tmp, maxLength)) + { + _currentEdit = tmp; + _optionIndex = option; + _currentField = field; + } + + if (ImGui.IsItemDeactivatedAfterEdit() && _currentEdit != null) + { + var ret = _currentEdit != oldValue; + value = _currentEdit; + Reset(); + return ret; + } + + value = string.Empty; + return false; + } + + public static bool Priority(string label, int field, int option, int oldValue, out int value, float width) + { + var tmp = field == _currentField && option == _optionIndex ? _currentGroupPriority ?? oldValue : oldValue; + ImGui.SetNextItemWidth(width); + if (ImGui.InputInt(label, ref tmp, 0, 0)) + { + _currentGroupPriority = tmp; + _optionIndex = option; + _currentField = field; + } + + if (ImGui.IsItemDeactivatedAfterEdit() && _currentGroupPriority != null) + { + var ret = _currentGroupPriority != oldValue; + value = _currentGroupPriority.Value; + Reset(); + return ret; + } + + value = 0; + return false; + } + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelHeader.cs b/Penumbra/UI/ModsTab/ModPanelHeader.cs new file mode 100644 index 00000000..6c0a7efa --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanelHeader.cs @@ -0,0 +1,224 @@ +using System; +using System.Diagnostics; +using System.Numerics; +using Dalamud.Interface.GameFonts; +using Dalamud.Plugin; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModTab; + +public class ModPanelHeader : IDisposable +{ + /// We use a big, nice game font for the title. + private readonly GameFontHandle _nameFont; + + public ModPanelHeader(DalamudPluginInterface pi) + => _nameFont = pi.UiBuilder.GetGameFontHandle(new GameFontStyle(GameFontFamilyAndSize.Jupiter23)); + + /// + /// Draw the header for the current mod, + /// consisting of its name, version, author and website, if they exist. + /// + public void Draw() + { + var offset = DrawModName(); + DrawVersion(offset); + DrawSecondRow(offset); + } + + /// + /// 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. + /// + public void UpdateModData(Mod mod) + { + // Name + var name = $" {mod.Name} "; + if (name != _modName) + { + using var font = ImRaii.PushFont(_nameFont.ImFont, _nameFont.Available); + _modName = name; + _modNameWidth = ImGui.CalcTextSize(name).X + 2 * (ImGui.GetStyle().FramePadding.X + 2 * UiHelpers.Scale); + } + + // Author + var author = mod.Author.IsEmpty ? string.Empty : $"by {mod.Author}"; + if (author != _modAuthor) + { + _modAuthor = author; + _modAuthorWidth = ImGui.CalcTextSize(author).X; + _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X; + } + + // Version + var version = mod.Version.Length > 0 ? $"({mod.Version})" : string.Empty; + if (version != _modVersion) + { + _modVersion = version; + _modVersionWidth = ImGui.CalcTextSize(version).X; + } + + // Website + if (_modWebsite != mod.Website) + { + _modWebsite = mod.Website; + _websiteValid = Uri.TryCreate(_modWebsite, UriKind.Absolute, out var uriResult) + && (uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp); + _modWebsiteButton = _websiteValid ? "Open Website" : _modWebsite.Length == 0 ? string.Empty : $"from {_modWebsite}"; + _modWebsiteButtonWidth = _websiteValid + ? ImGui.CalcTextSize(_modWebsiteButton).X + 2 * ImGui.GetStyle().FramePadding.X + : ImGui.CalcTextSize(_modWebsiteButton).X; + _secondRowWidth = _modAuthorWidth + _modWebsiteButtonWidth + ImGui.GetStyle().ItemSpacing.X; + } + } + + public void Dispose() + { + _nameFont.Dispose(); + } + + // Header data. + private string _modName = string.Empty; + private string _modAuthor = string.Empty; + private string _modVersion = string.Empty; + private string _modWebsite = string.Empty; + private string _modWebsiteButton = string.Empty; + private bool _websiteValid; + + private float _modNameWidth; + private float _modAuthorWidth; + private float _modVersionWidth; + private float _modWebsiteButtonWidth; + private float _secondRowWidth; + + /// + /// Draw the mod name in the game font with a 2px border, centered, + /// with at least the width of the version space to each side. + /// + private float DrawModName() + { + var decidingWidth = Math.Max(_secondRowWidth, ImGui.GetWindowWidth()); + var offsetWidth = (decidingWidth - _modNameWidth) / 2; + var offsetVersion = _modVersion.Length > 0 + ? _modVersionWidth + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X + : 0; + var offset = Math.Max(offsetWidth, offsetVersion); + if (offset > 0) + { + ImGui.SetCursorPosX(offset); + } + + using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * UiHelpers.Scale); + using var font = ImRaii.PushFont(_nameFont.ImFont, _nameFont.Available); + ImGuiUtil.DrawTextButton(_modName, Vector2.Zero, 0); + return offset; + } + + /// Draw the version in the top-right corner. + private void DrawVersion(float offset) + { + var oldPos = ImGui.GetCursorPos(); + ImGui.SetCursorPos(new Vector2(2 * offset + _modNameWidth - _modVersionWidth - ImGui.GetStyle().WindowPadding.X, + ImGui.GetStyle().FramePadding.Y)); + ImGuiUtil.TextColored(Colors.MetaInfoText, _modVersion); + ImGui.SetCursorPos(oldPos); + } + + /// + /// Draw author and website if they exist. The website is a button if it is valid. + /// Usually, author begins at the left boundary of the name, + /// and website ends at the right boundary of the name. + /// If their combined width is larger than the name, they are combined-centered. + /// + private void DrawSecondRow(float offset) + { + if (_modAuthor.Length == 0) + { + if (_modWebsiteButton.Length == 0) + { + ImGui.NewLine(); + return; + } + + offset += (_modNameWidth - _modWebsiteButtonWidth) / 2; + ImGui.SetCursorPosX(offset); + DrawWebsite(); + } + else if (_modWebsiteButton.Length == 0) + { + offset += (_modNameWidth - _modAuthorWidth) / 2; + ImGui.SetCursorPosX(offset); + DrawAuthor(); + } + else if (_secondRowWidth < _modNameWidth) + { + ImGui.SetCursorPosX(offset); + DrawAuthor(); + ImGui.SameLine(offset + _modNameWidth - _modWebsiteButtonWidth); + DrawWebsite(); + } + else + { + offset -= (_secondRowWidth - _modNameWidth) / 2; + if (offset > 0) + { + ImGui.SetCursorPosX(offset); + } + + DrawAuthor(); + ImGui.SameLine(); + DrawWebsite(); + } + } + + /// Draw the author text. + private void DrawAuthor() + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + ImGuiUtil.TextColored(Colors.MetaInfoText, "by "); + ImGui.SameLine(); + style.Pop(); + ImGui.TextUnformatted(_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 = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + ImGuiUtil.TextColored(Colors.MetaInfoText, "from "); + ImGui.SameLine(); + style.Pop(); + ImGui.TextUnformatted(_modWebsite); + } + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs new file mode 100644 index 00000000..1f528904 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -0,0 +1,348 @@ +using System; +using System.Linq; +using System.Numerics; +using ImGuiNET; +using OtterGui.Raii; +using OtterGui; +using OtterGui.Widgets; +using Penumbra.Api; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.Mods; +using Penumbra.UI.Classes; +using Dalamud.Interface.Components; +using Dalamud.Interface; + +namespace Penumbra.UI.ModTab; + +public class ModPanelSettingsTab : ITab +{ + private readonly Configuration _config; + private readonly ModCollection.Manager _collectionManager; + private readonly ModFileSystemSelector _selector; + private readonly TutorialService _tutorial; + private readonly PenumbraApi _api; + private readonly Mod.Manager _modManager; + + private bool _inherited; + private ModSettings _settings = null!; + private ModCollection _collection = null!; + private bool _empty; + private int? _currentPriority = null; + + public ModPanelSettingsTab(ModCollection.Manager collectionManager, Mod.Manager modManager, ModFileSystemSelector selector, + TutorialService tutorial, PenumbraApi api, Configuration config) + { + _collectionManager = collectionManager; + _modManager = modManager; + _selector = selector; + _tutorial = tutorial; + _api = api; + _config = config; + } + + public ReadOnlySpan Label + => "Settings"u8; + + public void DrawHeader() + => _tutorial.OpenTutorial(BasicTutorialSteps.ModOptions); + + public void Reset() + => _currentPriority = null; + + public void DrawContent() + { + using var child = ImRaii.Child("##settings"); + if (!child) + return; + + _settings = _selector.SelectedSettings; + _collection = _selector.SelectedSettingCollection; + _inherited = _collection != _collectionManager.Current; + _empty = _settings == ModSettings.Empty; + + DrawInheritedWarning(); + UiHelpers.DefaultLineSpace(); + _api.InvokePreSettingsPanel(_selector.Selected!.ModPath.Name); + DrawEnabledInput(); + _tutorial.OpenTutorial(BasicTutorialSteps.EnablingMods); + ImGui.SameLine(); + DrawPriorityInput(); + _tutorial.OpenTutorial(BasicTutorialSteps.Priority); + DrawRemoveSettings(); + + if (_selector.Selected!.Groups.Count > 0) + { + var useDummy = true; + foreach (var (group, idx) in _selector.Selected!.Groups.WithIndex() + .Where(g => g.Value.Type == GroupType.Single && g.Value.Count > _config.SingleGroupRadioMax)) + { + ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); + useDummy = false; + DrawSingleGroupCombo(group, idx); + } + + useDummy = true; + foreach (var (group, idx) in _selector.Selected!.Groups.WithIndex().Where(g => g.Value.IsOption)) + { + ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); + useDummy = false; + switch (group.Type) + { + case GroupType.Multi: + DrawMultiGroup(group, idx); + break; + case GroupType.Single when group.Count <= _config.SingleGroupRadioMax: + DrawSingleGroupRadio(group, idx); + break; + } + } + } + + UiHelpers.DefaultLineSpace(); + _api.InvokePostSettingsPanel(_selector.Selected!.ModPath.Name); + } + + /// Draw a big red bar if the current setting is inherited. + private void DrawInheritedWarning() + { + if (!_inherited) + return; + + using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); + var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); + if (ImGui.Button($"These settings are inherited from {_collection.Name}.", width)) + _collectionManager.Current.SetModInheritance(_selector.Selected!.Index, false); + + ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n" + + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."); + } + + /// Draw a checkbox for the enabled status of the mod. + private void DrawEnabledInput() + { + var enabled = _settings.Enabled; + if (ImGui.Checkbox("Enabled", ref enabled)) + { + _modManager.NewMods.Remove(_selector.Selected!); + _collectionManager.Current.SetModState(_selector.Selected!.Index, enabled); + } + } + + /// + /// Draw a priority input. + /// Priority is changed on deactivation of the input box. + /// + private void DrawPriorityInput() + { + using var group = ImRaii.Group(); + var priority = _currentPriority ?? _settings.Priority; + ImGui.SetNextItemWidth(50 * UiHelpers.Scale); + if (ImGui.InputInt("##Priority", ref priority, 0, 0)) + _currentPriority = priority; + + if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) + { + if (_currentPriority != _settings.Priority) + _collectionManager.Current.SetModPriority(_selector.Selected!.Index, _currentPriority.Value); + + _currentPriority = null; + } + + ImGuiUtil.LabeledHelpMarker("Priority", "Mods with a higher number here take precedence before Mods with a lower number.\n" + + "That means, if Mod A should overwrite changes from Mod B, Mod A should have a higher priority number than Mod B."); + } + + /// + /// Draw a button to remove the current settings and inherit them instead + /// on the top-right corner of the window/tab. + /// + private void DrawRemoveSettings() + { + const string text = "Inherit Settings"; + if (_inherited || _empty) + return; + + var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; + ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); + if (ImGui.Button(text)) + _collectionManager.Current.SetModInheritance(_selector.Selected!.Index, true); + + ImGuiUtil.HoverTooltip("Remove current settings from this collection so that it can inherit them.\n" + + "If no inherited collection has settings for this mod, it will be disabled."); + } + + /// + /// Draw a single group selector as a combo box. + /// If a description is provided, add a help marker besides it. + /// + private void DrawSingleGroupCombo(IModGroup group, int groupIdx) + { + using var id = ImRaii.PushId(groupIdx); + var selectedOption = _empty ? (int)group.DefaultSettings : (int)_settings.Settings[groupIdx]; + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); + using (var combo = ImRaii.Combo(string.Empty, group[selectedOption].Name)) + { + if (combo) + for (var idx2 = 0; idx2 < group.Count; ++idx2) + { + id.Push(idx2); + var option = group[idx2]; + if (ImGui.Selectable(option.Name, idx2 == selectedOption)) + _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx2); + + if (option.Description.Length > 0) + { + var hovered = ImGui.IsItemHovered(); + ImGui.SameLine(); + using (var _ = ImRaii.PushFont(UiBuilder.IconFont)) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGui.GetColorU32(ImGuiCol.TextDisabled)); + ImGuiUtil.RightAlign(FontAwesomeIcon.InfoCircle.ToIconString(), ImGui.GetStyle().ItemSpacing.X); + } + + if (hovered) + { + using var tt = ImRaii.Tooltip(); + ImGui.TextUnformatted(option.Description); + } + } + + id.Pop(); + } + } + + ImGui.SameLine(); + if (group.Description.Length > 0) + ImGuiUtil.LabeledHelpMarker(group.Name, group.Description); + else + ImGui.TextUnformatted(group.Name); + } + + // Draw a single group selector as a set of radio buttons. + // If a description is provided, add a help marker besides it. + private void DrawSingleGroupRadio(IModGroup group, int groupIdx) + { + using var id = ImRaii.PushId(groupIdx); + var selectedOption = _empty ? (int)group.DefaultSettings : (int)_settings.Settings[groupIdx]; + Widget.BeginFramedGroup(group.Name, group.Description); + + void DrawOptions() + { + for (var idx = 0; idx < group.Count; ++idx) + { + using var i = ImRaii.PushId(idx); + var option = group[idx]; + if (ImGui.RadioButton(option.Name, selectedOption == idx)) + _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx); + + if (option.Description.Length > 0) + { + ImGui.SameLine(); + ImGuiComponents.HelpMarker(option.Description); + } + } + } + + DrawCollapseHandling(group, DrawOptions); + + Widget.EndFramedGroup(); + } + + + private void DrawCollapseHandling(IModGroup group, Action draw) + { + if (group.Count <= _config.OptionGroupCollapsibleMin) + { + draw(); + } + else + { + var collapseId = ImGui.GetID("Collapse"); + var shown = ImGui.GetStateStorage().GetBool(collapseId, true); + if (shown) + { + var pos = ImGui.GetCursorPos(); + ImGui.Dummy(UiHelpers.IconButtonSize); + using (var _ = ImRaii.Group()) + { + draw(); + } + + var width = ImGui.GetItemRectSize().X; + var endPos = ImGui.GetCursorPos(); + ImGui.SetCursorPos(pos); + if (ImGui.Button($"Hide {group.Count} Options", new Vector2(width, 0))) + ImGui.GetStateStorage().SetBool(collapseId, !shown); + + ImGui.SetCursorPos(endPos); + } + else + { + var max = group.Max(o => ImGui.CalcTextSize(o.Name).X) + + ImGui.GetStyle().ItemInnerSpacing.X + + ImGui.GetFrameHeight() + + ImGui.GetStyle().FramePadding.X; + if (ImGui.Button($"Show {group.Count} Options", new Vector2(max, 0))) + ImGui.GetStateStorage().SetBool(collapseId, !shown); + } + } + } + + /// + /// Draw a multi group selector as a bordered set of checkboxes. + /// If a description is provided, add a help marker in the title. + /// + private void DrawMultiGroup(IModGroup group, int groupIdx) + { + using var id = ImRaii.PushId(groupIdx); + var flags = _empty ? group.DefaultSettings : _settings.Settings[groupIdx]; + Widget.BeginFramedGroup(group.Name, group.Description); + + void DrawOptions() + { + for (var idx = 0; idx < group.Count; ++idx) + { + using var i = ImRaii.PushId(idx); + var option = group[idx]; + var flag = 1u << idx; + var setting = (flags & flag) != 0; + + if (ImGui.Checkbox(option.Name, ref setting)) + { + flags = setting ? flags | flag : flags & ~flag; + _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags); + } + + if (option.Description.Length > 0) + { + ImGui.SameLine(); + ImGuiComponents.HelpMarker(option.Description); + } + } + } + + DrawCollapseHandling(group, DrawOptions); + + Widget.EndFramedGroup(); + var label = $"##multi{groupIdx}"; + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImGui.OpenPopup($"##multi{groupIdx}"); + + using var style = ImRaii.PushStyle(ImGuiStyleVar.PopupBorderSize, 1); + using var popup = ImRaii.Popup(label); + if (!popup) + return; + + ImGui.TextUnformatted(group.Name); + ImGui.Separator(); + if (ImGui.Selectable("Enable All")) + { + flags = group.Count == 32 ? uint.MaxValue : (1u << group.Count) - 1u; + _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags); + } + + if (ImGui.Selectable("Disable All")) + _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, 0); + } +} diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs new file mode 100644 index 00000000..a946a916 --- /dev/null +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -0,0 +1,152 @@ +using System; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModTab; + +public class ModPanelTabBar +{ + private enum ModPanelTabType + { + Description, + Settings, + ChangedItems, + Conflicts, + Edit, + }; + + public readonly ModPanelSettingsTab Settings; + public readonly ModPanelDescriptionTab Description; + public readonly ModPanelConflictsTab Conflicts; + public readonly ModPanelChangedItemsTab ChangedItems; + public readonly ModPanelEditTab Edit; + private readonly ModEditWindow _modEditWindow; + private readonly Mod.Manager _modManager; + private readonly TutorialService _tutorial; + + public readonly ITab[] Tabs; + private ModPanelTabType _preferredTab = 0; + private Mod? _lastMod = null; + + public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, ModPanelDescriptionTab description, + ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, Mod.Manager modManager, + TutorialService tutorial) + { + _modEditWindow = modEditWindow; + Settings = settings; + Description = description; + Conflicts = conflicts; + ChangedItems = changedItems; + Edit = edit; + _modManager = modManager; + _tutorial = tutorial; + + Tabs = new ITab[] + { + Settings, + Description, + Conflicts, + ChangedItems, + Edit, + }; + } + + public void Draw(Mod mod) + { + var tabBarHeight = ImGui.GetCursorPosY(); + if (_lastMod != mod) + { + _lastMod = mod; + TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel(_preferredTab), out _, () => DrawAdvancedEditingButton(mod), Tabs); + } + else + { + TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ReadOnlySpan.Empty, out var label, () => DrawAdvancedEditingButton(mod), + Tabs); + _preferredTab = ToType(label); + } + + DrawFavoriteButton(mod, tabBarHeight); + } + + private ReadOnlySpan ToLabel(ModPanelTabType type) + => type switch + { + ModPanelTabType.Description => Description.Label, + ModPanelTabType.Settings => Settings.Label, + ModPanelTabType.ChangedItems => ChangedItems.Label, + ModPanelTabType.Conflicts => Conflicts.Label, + ModPanelTabType.Edit => Edit.Label, + _ => ReadOnlySpan.Empty, + }; + + private ModPanelTabType ToType(ReadOnlySpan label) + { + if (label == Description.Label) + return ModPanelTabType.Description; + if (label == Settings.Label) + return ModPanelTabType.Settings; + if (label == ChangedItems.Label) + return ModPanelTabType.ChangedItems; + if (label == Conflicts.Label) + return ModPanelTabType.Conflicts; + if (label == Edit.Label) + return ModPanelTabType.Edit; + + return 0; + } + + private void DrawAdvancedEditingButton(Mod mod) + { + if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip)) + { + _modEditWindow.ChangeMod(mod); + _modEditWindow.ChangeOption(mod.Default); + _modEditWindow.IsOpen = true; + } + + ImGuiUtil.HoverTooltip( + "Clicking this will open a new window in which you can\nedit the following things per option for this mod:\n\n" + + "\t\t- file redirections\n" + + "\t\t- file swaps\n" + + "\t\t- metadata manipulations\n" + + "\t\t- model materials\n" + + "\t\t- duplicates\n" + + "\t\t- textures"); + } + + private void DrawFavoriteButton(Mod mod, float height) + { + using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + { + var size = ImGui.CalcTextSize(FontAwesomeIcon.Star.ToIconString()) + ImGui.GetStyle().FramePadding * 2; + var newPos = new Vector2(ImGui.GetWindowWidth() - size.X - ImGui.GetStyle().ItemSpacing.X, height); + if (ImGui.GetScrollMaxX() > 0) + newPos.X += ImGui.GetScrollX(); + + var rectUpper = ImGui.GetWindowPos() + newPos; + var color = ImGui.IsMouseHoveringRect(rectUpper, rectUpper + size) ? ImGui.GetColorU32(ImGuiCol.Text) : + mod.Favorite ? 0xFF00FFFF : ImGui.GetColorU32(ImGuiCol.TextDisabled); + using var c = ImRaii.PushColor(ImGuiCol.Text, color) + .Push(ImGuiCol.Button, 0) + .Push(ImGuiCol.ButtonHovered, 0) + .Push(ImGuiCol.ButtonActive, 0); + + ImGui.SetCursorPos(newPos); + if (ImGui.Button(FontAwesomeIcon.Star.ToIconString())) + _modManager.ChangeModFavorite(mod.Index, !mod.Favorite); + } + + var hovered = ImGui.IsItemHovered(); + _tutorial.OpenTutorial(BasicTutorialSteps.Favorites); + + if (hovered) + ImGui.SetTooltip("Favorite"); + } +} diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs index 0f97d883..1cb787ef 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.Table.cs @@ -42,7 +42,7 @@ public partial class ResourceWatcher private sealed class PathColumn : ColumnString< Record > { public override float Width - => 300 * ImGuiHelpers.GlobalScale; + => 300 * UiHelpers.Scale; public override string ToName( Record item ) => item.Path.ToString(); @@ -51,7 +51,7 @@ public partial class ResourceWatcher => lhs.Path.CompareTo( rhs.Path ); public override void DrawColumn( Record item, int _ ) - => DrawByteString( item.Path, 290 * ImGuiHelpers.GlobalScale ); + => DrawByteString( item.Path, 290 * UiHelpers.Scale ); } private static unsafe void DrawByteString( ByteString path, float length ) @@ -68,7 +68,7 @@ public partial class ResourceWatcher ByteString shortPath; if( fileName != -1 ) { - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 2 * ImGuiHelpers.GlobalScale ) ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, new Vector2( 2 * UiHelpers.Scale ) ); using var font = ImRaii.PushFont( UiBuilder.IconFont ); ImGui.TextUnformatted( FontAwesomeIcon.EllipsisH.ToIconString() ); ImGui.SameLine(); @@ -98,7 +98,7 @@ public partial class ResourceWatcher => AllFlags = AllRecords; public override float Width - => 80 * ImGuiHelpers.GlobalScale; + => 80 * UiHelpers.Scale; public override bool FilterFunc( Record item ) => FilterValue.HasFlag( item.RecordType ); @@ -136,7 +136,7 @@ public partial class ResourceWatcher private sealed class DateColumn : Column< Record > { public override float Width - => 80 * ImGuiHelpers.GlobalScale; + => 80 * UiHelpers.Scale; public override int Compare( Record lhs, Record rhs ) => lhs.Time.CompareTo( rhs.Time ); @@ -149,7 +149,7 @@ public partial class ResourceWatcher private sealed class CollectionColumn : ColumnString< Record > { public override float Width - => 80 * ImGuiHelpers.GlobalScale; + => 80 * UiHelpers.Scale; public override string ToName( Record item ) => item.Collection?.Name ?? string.Empty; @@ -158,7 +158,7 @@ public partial class ResourceWatcher private sealed class OriginalPathColumn : ColumnString< Record > { public override float Width - => 200 * ImGuiHelpers.GlobalScale; + => 200 * UiHelpers.Scale; public override string ToName( Record item ) => item.OriginalPath.ToString(); @@ -167,7 +167,7 @@ public partial class ResourceWatcher => lhs.OriginalPath.CompareTo( rhs.OriginalPath ); public override void DrawColumn( Record item, int _ ) - => DrawByteString( item.OriginalPath, 190 * ImGuiHelpers.GlobalScale ); + => DrawByteString( item.OriginalPath, 190 * UiHelpers.Scale ); } private sealed class ResourceCategoryColumn : ColumnFlags< ResourceCategoryFlag, Record > @@ -176,7 +176,7 @@ public partial class ResourceWatcher => AllFlags = ResourceExtensions.AllResourceCategories; public override float Width - => 80 * ImGuiHelpers.GlobalScale; + => 80 * UiHelpers.Scale; public override bool FilterFunc( Record item ) => FilterValue.HasFlag( item.Category ); @@ -216,7 +216,7 @@ public partial class ResourceWatcher } public override float Width - => 50 * ImGuiHelpers.GlobalScale; + => 50 * UiHelpers.Scale; public override bool FilterFunc( Record item ) => FilterValue.HasFlag( item.ResourceType ); @@ -247,7 +247,7 @@ public partial class ResourceWatcher private sealed class HandleColumn : ColumnString< Record > { public override float Width - => 120 * ImGuiHelpers.GlobalScale; + => 120 * UiHelpers.Scale; public override unsafe string ToName( Record item ) => item.Handle == null ? string.Empty : $"0x{( ulong )item.Handle:X}"; @@ -316,7 +316,7 @@ public partial class ResourceWatcher private sealed class CustomLoadColumn : OptBoolColumn { public override float Width - => 60 * ImGuiHelpers.GlobalScale; + => 60 * UiHelpers.Scale; public override bool FilterFunc( Record item ) => FilterFunc( item.CustomLoad ); @@ -328,7 +328,7 @@ public partial class ResourceWatcher private sealed class SynchronousLoadColumn : OptBoolColumn { public override float Width - => 45 * ImGuiHelpers.GlobalScale; + => 45 * UiHelpers.Scale; public override bool FilterFunc( Record item ) => FilterFunc( item.Synchronously ); @@ -340,7 +340,7 @@ public partial class ResourceWatcher private sealed class RefCountColumn : Column< Record > { public override float Width - => 30 * ImGuiHelpers.GlobalScale; + => 30 * UiHelpers.Scale; public override void DrawColumn( Record item, int _ ) => ImGuiUtil.RightAlign( item.RefCount.ToString() ); diff --git a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs index e73bba63..27abb6dc 100644 --- a/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs +++ b/Penumbra/UI/ResourceWatcher/ResourceWatcher.cs @@ -115,7 +115,7 @@ public partial class ResourceWatcher : IDisposable, ITab var tmp = _logFilter; var invalidRegex = _logRegex == null && _logFilter.Length > 0; using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.RegexWarningBorder, invalidRegex); - using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, invalidRegex); + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, invalidRegex); if (ImGui.InputTextWithHint("##logFilter", "If path matches this Regex...", ref tmp, 256)) UpdateFilter(tmp, true); } @@ -151,7 +151,7 @@ public partial class ResourceWatcher : IDisposable, ITab private void DrawMaxEntries() { - ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale); + ImGui.SetNextItemWidth(80 * UiHelpers.Scale); ImGui.InputInt("Max. Entries", ref _newMaxEntries, 0, 0); var change = ImGui.IsItemDeactivatedAfterEdit(); if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs new file mode 100644 index 00000000..22eecf7e --- /dev/null +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Api; +using Penumbra.Collections; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.Tabs; + +public class ChangedItemsTab : ITab +{ + private readonly ModCollection.Manager _collectionManager; + private readonly PenumbraApi _api; + + public ChangedItemsTab(ModCollection.Manager collectionManager, PenumbraApi api) + { + _collectionManager = collectionManager; + _api = api; + } + + public ReadOnlySpan Label + => "Changed Items"u8; + + private LowerString _changedItemFilter = LowerString.Empty; + private LowerString _changedItemModFilter = LowerString.Empty; + + public void DrawContent() + { + var varWidth = DrawFilters(); + using var child = ImRaii.Child("##changedItemsChild", -Vector2.One); + if (!child) + return; + + var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; + var skips = ImGuiClip.GetNecessarySkips(height); + using var list = ImRaii.Table("##changedItems", 3, ImGuiTableFlags.RowBg, -Vector2.One); + if (!list) + return; + + const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed; + ImGui.TableSetupColumn("items", flags, 400 * UiHelpers.Scale); + ImGui.TableSetupColumn("mods", flags, varWidth - 120 * UiHelpers.Scale); + ImGui.TableSetupColumn("id", flags, 120 * UiHelpers.Scale); + + var items = _collectionManager.Current.ChangedItems; + var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty + ? ImGuiClip.ClippedDraw(items, skips, DrawChangedItemColumn, items.Count) + : ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn); + ImGuiClip.DrawEndDummy(rest, height); + } + + /// Draw a pair of filters and return the variable width of the flexible column. + private float DrawFilters() + { + var varWidth = ImGui.GetContentRegionAvail().X + - 400 * UiHelpers.Scale + - ImGui.GetStyle().ItemSpacing.X; + ImGui.SetNextItemWidth(400 * UiHelpers.Scale); + LowerString.InputWithHint("##changedItemsFilter", "Filter Item...", ref _changedItemFilter, 128); + ImGui.SameLine(); + ImGui.SetNextItemWidth(varWidth); + LowerString.InputWithHint("##changedItemsModFilter", "Filter Mods...", ref _changedItemModFilter, 128); + return varWidth; + } + + /// Apply the current filters. + private bool FilterChangedItem(KeyValuePair, object?)> item) + => (_changedItemFilter.IsEmpty + || UiHelpers.ChangedItemName(item.Key, item.Value.Item2) + .Contains(_changedItemFilter.Lower, StringComparison.OrdinalIgnoreCase)) + && (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter))); + + /// Draw a full column for a changed item. + private void DrawChangedItemColumn(KeyValuePair, object?)> item) + { + ImGui.TableNextColumn(); + UiHelpers.DrawChangedItem(_api, item.Key, item.Value.Item2, false); + ImGui.TableNextColumn(); + if (item.Value.Item1.Count > 0) + { + ImGui.TextUnformatted(item.Value.Item1[0].Name); + if (item.Value.Item1.Count > 1 && ImGui.IsItemHovered()) + ImGui.SetTooltip(string.Join("\n", item.Value.Item1.Skip(1).Select(m => m.Name))); + } + + ImGui.TableNextColumn(); + if (!UiHelpers.GetChangedItemObject(item.Value.Item2, out var text)) + return; + + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(Penumbra.Config)); + ImGuiUtil.RightAlign(text); + } +} diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs new file mode 100644 index 00000000..4825b4aa --- /dev/null +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -0,0 +1,298 @@ +using System; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Collections; +using Penumbra.Services; +using Penumbra.UI.CollectionTab; + +namespace Penumbra.UI.Tabs; + +public class CollectionsTab : IDisposable, ITab +{ + private readonly CommunicatorService _communicator; + private readonly Configuration _config; + private readonly ModCollection.Manager _collectionManager; + private readonly TutorialService _tutorial; + private readonly SpecialCombo _specialCollectionCombo; + + private readonly CollectionSelector _collectionsWithEmpty; + private readonly CollectionSelector _collectionSelector; + private readonly InheritanceUi _inheritance; + private readonly IndividualCollectionUi _individualCollections; + + public CollectionsTab(ActorService actorService, CommunicatorService communicator, ModCollection.Manager collectionManager, + TutorialService tutorial, Configuration config) + { + _communicator = communicator; + _collectionManager = collectionManager; + _tutorial = tutorial; + _config = config; + _specialCollectionCombo = new SpecialCombo(_collectionManager, "##NewSpecial", 350); + _collectionsWithEmpty = new CollectionSelector(_collectionManager, + () => _collectionManager.OrderBy(c => c.Name).Prepend(ModCollection.Empty).ToList()); + _collectionSelector = new CollectionSelector(_collectionManager, () => _collectionManager.OrderBy(c => c.Name).ToList()); + _inheritance = new InheritanceUi(_collectionManager); + _individualCollections = new IndividualCollectionUi(actorService, _collectionManager, _collectionsWithEmpty); + + _communicator.CollectionChange.Event += _individualCollections.UpdateIdentifiers; + } + + public ReadOnlySpan Label + => "Collections"u8; + + /// Draw a collection selector of a certain width for a certain type. + public void DrawCollectionSelector(string label, float width, CollectionType collectionType, bool withEmpty) + => (withEmpty ? _collectionsWithEmpty : _collectionSelector).Draw(label, width, collectionType); + + public void Dispose() + => _communicator.CollectionChange.Event -= _individualCollections.UpdateIdentifiers; + + /// Draw a tutorial step regardless of tab selection. + public void DrawHeader() + => _tutorial.OpenTutorial(BasicTutorialSteps.Collections); + + public void DrawContent() + { + using var child = ImRaii.Child("##collections", -Vector2.One); + if (child) + { + DrawActiveCollectionSelectors(); + DrawMainSelectors(); + } + } + + #region New Collections + + // Input text fields. + private string _newCollectionName = string.Empty; + private bool _canAddCollection; + + /// + /// Create a new collection that is either empty or a duplicate of the current collection. + /// Resets the new collection name. + /// + private void CreateNewCollection(bool duplicate) + { + if (_collectionManager.AddCollection(_newCollectionName, duplicate ? _collectionManager.Current : null)) + _newCollectionName = string.Empty; + } + + /// Draw the Clean Unused Settings button if there are any. + private void DrawCleanCollectionButton(Vector2 width) + { + if (!_collectionManager.Current.HasUnusedSettings) + return; + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton( + $"Clean {_collectionManager.Current.NumUnusedSettings} Unused Settings###CleanSettings", width + , "Remove all stored settings for mods not currently available and fix invalid settings.\n\nUse at own risk." + , false)) + _collectionManager.Current.CleanUnavailableSettings(); + } + + /// Draw the new collection input as well as its buttons. + private void DrawNewCollectionInput(Vector2 width) + { + // Input for new collection name. Also checks for validity when changed. + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + if (ImGui.InputTextWithHint("##New Collection", "New Collection Name...", ref _newCollectionName, 64)) + _canAddCollection = _collectionManager.CanAddCollection(_newCollectionName, out _); + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "A collection is a set of settings for your installed mods, including their enabled status, their priorities and their mod-specific configuration.\n" + + "You can use multiple collections to quickly switch between sets of enabled mods."); + + // Creation buttons. + var tt = _canAddCollection + ? string.Empty + : "Please enter a unique name only consisting of symbols valid in a path but no '|' before creating a collection."; + if (ImGuiUtil.DrawDisabledButton("Create Empty Collection", width, tt, !_canAddCollection)) + CreateNewCollection(false); + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton($"Duplicate {TutorialService.SelectedCollection}", width, tt, !_canAddCollection)) + CreateNewCollection(true); + } + + #endregion + + #region Collection Selection + + /// Draw all collection assignment selections. + private void DrawActiveCollectionSelectors() + { + UiHelpers.DefaultLineSpace(); + var open = ImGui.CollapsingHeader(TutorialService.ActiveCollections, ImGuiTreeNodeFlags.DefaultOpen); + _tutorial.OpenTutorial(BasicTutorialSteps.ActiveCollections); + if (!open) + return; + + UiHelpers.DefaultLineSpace(); + + DrawDefaultCollectionSelector(); + _tutorial.OpenTutorial(BasicTutorialSteps.DefaultCollection); + DrawInterfaceCollectionSelector(); + _tutorial.OpenTutorial(BasicTutorialSteps.InterfaceCollection); + UiHelpers.DefaultLineSpace(); + + DrawSpecialAssignments(); + _tutorial.OpenTutorial(BasicTutorialSteps.SpecialCollections1); + UiHelpers.DefaultLineSpace(); + + _individualCollections.Draw(); + _tutorial.OpenTutorial(BasicTutorialSteps.SpecialCollections2); + UiHelpers.DefaultLineSpace(); + } + + private void DrawCurrentCollectionSelector(Vector2 width) + { + using var group = ImRaii.Group(); + DrawCollectionSelector("##current", UiHelpers.InputTextWidth.X, CollectionType.Current, false); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker(TutorialService.SelectedCollection, + "This collection will be modified when using the Installed Mods tab and making changes.\nIt is not automatically assigned to anything."); + + // Deletion conditions. + var deleteCondition = _collectionManager.Current.Name != ModCollection.DefaultCollection; + var modifierHeld = Penumbra.Config.DeleteModModifier.IsActive(); + var tt = deleteCondition + ? modifierHeld ? string.Empty : $"Hold {_config.DeleteModModifier} while clicking to delete the collection." + : $"You can not delete the collection {ModCollection.DefaultCollection}."; + + if (ImGuiUtil.DrawDisabledButton($"Delete {TutorialService.SelectedCollection}", width, tt, !deleteCondition || !modifierHeld)) + _collectionManager.RemoveCollection(_collectionManager.Current); + + DrawCleanCollectionButton(width); + } + + /// Draw the selector for the default collection assignment. + private void DrawDefaultCollectionSelector() + { + using var group = ImRaii.Group(); + DrawCollectionSelector("##default", UiHelpers.InputTextWidth.X, CollectionType.Default, true); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker(TutorialService.DefaultCollection, + $"Mods in the {TutorialService.DefaultCollection} are loaded for anything that is not associated with the user interface or a character in the game," + + "as well as any character for whom no more specific conditions from below apply."); + } + + /// Draw the selector for the interface collection assignment. + private void DrawInterfaceCollectionSelector() + { + using var group = ImRaii.Group(); + DrawCollectionSelector("##interface", UiHelpers.InputTextWidth.X, CollectionType.Interface, true); + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker(TutorialService.InterfaceCollection, + $"Mods in the {TutorialService.InterfaceCollection} are loaded for any file that the game categorizes as an UI file. This is mostly icons as well as the tiles that generate the user interface windows themselves."); + } + + /// Description for character groups used in multiple help markers. + private const string CharacterGroupDescription = + $"{TutorialService.CharacterGroups} apply to certain types of characters based on a condition.\n" + + $"All of them take precedence before the {TutorialService.DefaultCollection},\n" + + $"but all {TutorialService.IndividualAssignments} take precedence before them."; + + /// Draw the entire group assignment section. + private void DrawSpecialAssignments() + { + using var _ = ImRaii.Group(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(TutorialService.CharacterGroups); + ImGuiComponents.HelpMarker(CharacterGroupDescription); + ImGui.Separator(); + DrawSpecialCollections(); + ImGui.Dummy(Vector2.Zero); + DrawNewSpecialCollection(); + } + + /// Draw a new combo to select special collections as well as button to create it. + private void DrawNewSpecialCollection() + { + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + if (_specialCollectionCombo.CurrentIdx == -1 + || _collectionManager.ByType(_specialCollectionCombo.CurrentType!.Value.Item1) != null) + { + _specialCollectionCombo.ResetFilter(); + _specialCollectionCombo.CurrentIdx = CollectionTypeExtensions.Special + .IndexOf(t => _collectionManager.ByType(t.Item1) == null); + } + + if (_specialCollectionCombo.CurrentType == null) + return; + + _specialCollectionCombo.Draw(); + ImGui.SameLine(); + var disabled = _specialCollectionCombo.CurrentType == null; + var tt = disabled + ? $"Please select a condition for a {TutorialService.GroupAssignment} before creating the collection.\n\n" + + CharacterGroupDescription + : CharacterGroupDescription; + if (!ImGuiUtil.DrawDisabledButton($"Assign {TutorialService.ConditionalGroup}", new Vector2(120 * UiHelpers.Scale, 0), tt, disabled)) + return; + + _collectionManager.CreateSpecialCollection(_specialCollectionCombo.CurrentType!.Value.Item1); + _specialCollectionCombo.CurrentIdx = -1; + } + + #endregion + + #region Current Collection Editing + + /// Draw the current collection selection, the creation of new collections and the inheritance block. + private void DrawMainSelectors() + { + UiHelpers.DefaultLineSpace(); + var open = ImGui.CollapsingHeader("Collection Settings", ImGuiTreeNodeFlags.DefaultOpen); + _tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections); + if (!open) + return; + + var width = new Vector2((UiHelpers.InputTextWidth.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0); + UiHelpers.DefaultLineSpace(); + + DrawCurrentCollectionSelector(width); + _tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection); + UiHelpers.DefaultLineSpace(); + + DrawNewCollectionInput(width); + UiHelpers.DefaultLineSpace(); + + _inheritance.Draw(); + _tutorial.OpenTutorial(BasicTutorialSteps.Inheritance); + } + + /// Draw all currently set special collections. + private void DrawSpecialCollections() + { + foreach (var (type, name, desc) in CollectionTypeExtensions.Special) + { + var collection = _collectionManager.ByType(type); + if (collection == null) + continue; + + using var id = ImRaii.PushId((int)type); + DrawCollectionSelector("##SpecialCombo", UiHelpers.InputTextWidth.X, type, true); + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty, + false, true)) + { + _collectionManager.RemoveSpecialCollection(type); + _specialCollectionCombo.ResetFilter(); + } + + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGuiUtil.LabeledHelpMarker(name, desc); + } + } + + #endregion +} diff --git a/Penumbra/UI/Tabs/ConfigTabBar.cs b/Penumbra/UI/Tabs/ConfigTabBar.cs new file mode 100644 index 00000000..0f91f3dc --- /dev/null +++ b/Penumbra/UI/Tabs/ConfigTabBar.cs @@ -0,0 +1,67 @@ +using System; +using ImGuiNET; +using OtterGui.Widgets; +using Penumbra.Api.Enums; + +namespace Penumbra.UI.Tabs; + +public class ConfigTabBar +{ + public readonly SettingsTab Settings; + public readonly ModsTab Mods; + public readonly CollectionsTab Collections; + public readonly ChangedItemsTab ChangedItems; + public readonly EffectiveTab Effective; + public readonly DebugTab Debug; + public readonly ResourceTab Resource; + public readonly ResourceWatcher Watcher; + + public readonly ITab[] Tabs; + + /// The tab to select on the next Draw call, if any. + public TabType SelectTab = TabType.None; + + public ConfigTabBar(SettingsTab settings, ModsTab mods, CollectionsTab collections, ChangedItemsTab changedItems, EffectiveTab effective, + DebugTab debug, ResourceTab resource, ResourceWatcher watcher) + { + Settings = settings; + Mods = mods; + Collections = collections; + ChangedItems = changedItems; + Effective = effective; + Debug = debug; + Resource = resource; + Watcher = watcher; + Tabs = new ITab[] + { + Settings, + Mods, + Collections, + ChangedItems, + Effective, + Debug, + Resource, + Watcher, + }; + } + + public void Draw() + { + if (TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel(SelectTab), out _, () => { }, Tabs)) + SelectTab = TabType.None; + } + + private ReadOnlySpan ToLabel(TabType type) + => type switch + { + TabType.Settings => Settings.Label, + TabType.Mods => Mods.Label, + TabType.Collections => Collections.Label, + TabType.ChangedItems => ChangedItems.Label, + TabType.EffectiveChanges => Effective.Label, + TabType.ResourceWatcher => Watcher.Label, + TabType.Debug => Debug.Label, + TabType.ResourceManager => Resource.Label, + _ => ReadOnlySpan.Empty, + }; +} diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs new file mode 100644 index 00000000..17e3ccea --- /dev/null +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -0,0 +1,604 @@ +using System; +using System.IO; +using System.Linq; +using System.Numerics; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Group; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using ImGuiNET; +using OtterGui; +using OtterGui.Widgets; +using Penumbra.Api; +using Penumbra.Collections; +using Penumbra.GameData.Actors; +using Penumbra.GameData.Files; +using Penumbra.Interop.Loader; +using Penumbra.Interop.Resolver; +using Penumbra.Interop.Structs; +using Penumbra.Mods; +using Penumbra.Services; +using Penumbra.String; +using Penumbra.Util; +using static OtterGui.Raii.ImRaii; +using CharacterBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase; +using CharacterUtility = Penumbra.Interop.CharacterUtility; +using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; +using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; + +namespace Penumbra.UI.Tabs; + +public class DebugTab : ITab +{ + private readonly StartTracker _timer; + private readonly PerformanceTracker _performance; + private readonly Configuration _config; + private readonly ModCollection.Manager _collectionManager; + private readonly Mod.Manager _modManager; + private readonly ValidityChecker _validityChecker; + private readonly HttpApi _httpApi; + private readonly PathResolver _pathResolver; + private readonly ActorService _actorService; + private readonly DalamudServices _dalamud; + private readonly StainService _stains; + private readonly CharacterUtility _characterUtility; + private readonly ResidentResourceManager _residentResources; + private readonly ResourceManagerService _resourceManager; + private readonly PenumbraIpcProviders _ipc; + + public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager, + ValidityChecker validityChecker, Mod.Manager modManager, HttpApi httpApi, PathResolver pathResolver, ActorService actorService, + DalamudServices dalamud, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, + ResourceManagerService resourceManager, PenumbraIpcProviders ipc) + { + _timer = timer; + _performance = performance; + _config = config; + _collectionManager = collectionManager; + _validityChecker = validityChecker; + _modManager = modManager; + _httpApi = httpApi; + _pathResolver = pathResolver; + _actorService = actorService; + _dalamud = dalamud; + _stains = stains; + _characterUtility = characterUtility; + _residentResources = residentResources; + _resourceManager = resourceManager; + _ipc = ipc; + } + + public ReadOnlySpan Label + => "Debug"u8; + + public bool IsVisible + => _config.DebugMode; + +#if DEBUG + private const string DebugVersionString = "(Debug)"; +#else + private const string DebugVersionString = "(Release)"; +#endif + + public void DrawContent() + { + using var child = Child("##DebugTab", -Vector2.One); + if (!child) + return; + + DrawDebugTabGeneral(); + DrawPerformanceTab(); + ImGui.NewLine(); + DrawPathResolverDebug(); + ImGui.NewLine(); + DrawActorsDebug(); + ImGui.NewLine(); + DrawDebugCharacterUtility(); + ImGui.NewLine(); + DrawStainTemplates(); + ImGui.NewLine(); + DrawDebugTabMetaLists(); + ImGui.NewLine(); + DrawDebugResidentResources(); + ImGui.NewLine(); + DrawResourceProblems(); + ImGui.NewLine(); + DrawPlayerModelInfo(); + ImGui.NewLine(); + DrawDebugTabIpc(); + ImGui.NewLine(); + } + + /// Draw general information about mod and collection state. + private void DrawDebugTabGeneral() + { + if (!ImGui.CollapsingHeader("General")) + return; + + using var table = Table("##DebugGeneralTable", 2, ImGuiTableFlags.SizingFixedFit, + new Vector2(-1, ImGui.GetTextLineHeightWithSpacing() * 1)); + if (!table) + return; + + PrintValue("Penumbra Version", $"{_validityChecker.Version} {DebugVersionString}"); + PrintValue("Git Commit Hash", _validityChecker.CommitHash); + PrintValue(TutorialService.SelectedCollection, _collectionManager.Current.Name); + PrintValue(" has Cache", _collectionManager.Current.HasCache.ToString()); + PrintValue(TutorialService.DefaultCollection, _collectionManager.Default.Name); + PrintValue(" has Cache", _collectionManager.Default.HasCache.ToString()); + PrintValue("Mod Manager BasePath", _modManager.BasePath.Name); + PrintValue("Mod Manager BasePath-Full", _modManager.BasePath.FullName); + PrintValue("Mod Manager BasePath IsRooted", Path.IsPathRooted(_config.ModDirectory).ToString()); + PrintValue("Mod Manager BasePath Exists", Directory.Exists(_modManager.BasePath.FullName).ToString()); + PrintValue("Mod Manager Valid", _modManager.Valid.ToString()); + PrintValue("Path Resolver Enabled", _pathResolver.Enabled.ToString()); + PrintValue("Web Server Enabled", _httpApi.Enabled.ToString()); + } + + private void DrawPerformanceTab() + { + ImGui.NewLine(); + if (ImGui.CollapsingHeader("Performance")) + return; + + using (var start = TreeNode("Startup Performance", ImGuiTreeNodeFlags.DefaultOpen)) + { + if (start) + { + _timer.Draw("##startTimer", TimingExtensions.ToName); + ImGui.NewLine(); + } + } + + _performance.Draw("##performance", "Enable Runtime Performance Tracking", TimingExtensions.ToName); + } + + private unsafe void DrawActorsDebug() + { + if (!ImGui.CollapsingHeader("Actors")) + return; + + using var table = Table("##actors", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + void DrawSpecial(string name, ActorIdentifier id) + { + if (!id.IsValid) + return; + + ImGuiUtil.DrawTableColumn(name); + ImGuiUtil.DrawTableColumn(string.Empty); + ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(id)); + ImGuiUtil.DrawTableColumn(string.Empty); + } + + DrawSpecial("Current Player", _actorService.AwaitedService.GetCurrentPlayer()); + DrawSpecial("Current Inspect", _actorService.AwaitedService.GetInspectPlayer()); + DrawSpecial("Current Card", _actorService.AwaitedService.GetCardPlayer()); + DrawSpecial("Current Glamour", _actorService.AwaitedService.GetGlamourPlayer()); + + foreach (var obj in DalamudServices.SObjects) + { + ImGuiUtil.DrawTableColumn($"{((GameObject*)obj.Address)->ObjectIndex}"); + ImGuiUtil.DrawTableColumn($"0x{obj.Address:X}"); + var identifier = _actorService.AwaitedService.FromObject(obj, false, true, false); + ImGuiUtil.DrawTableColumn(_actorService.AwaitedService.ToString(identifier)); + var id = obj.ObjectKind == ObjectKind.BattleNpc ? $"{identifier.DataId} | {obj.DataId}" : identifier.DataId.ToString(); + ImGuiUtil.DrawTableColumn(id); + } + } + + /// + /// Draw information about which draw objects correspond to which game objects + /// and which paths are due to be loaded by which collection. + /// + private unsafe void DrawPathResolverDebug() + { + if (!ImGui.CollapsingHeader("Path Resolver")) + return; + + ImGui.TextUnformatted( + $"Last Game Object: 0x{_pathResolver.LastGameObject:X} ({_pathResolver.LastGameObjectData.ModCollection.Name})"); + using (var drawTree = TreeNode("Draw Object to Object")) + { + if (drawTree) + { + using var table = Table("###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit); + if (table) + foreach (var (ptr, (c, idx)) in _pathResolver.DrawObjectMap) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(ptr.ToString("X")); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(idx.ToString()); + ImGui.TableNextColumn(); + var obj = (GameObject*)_dalamud.Objects.GetObjectAddress(idx); + var (address, name) = + obj != null ? ($"0x{(ulong)obj:X}", new ByteString(obj->Name).ToString()) : ("NULL", "NULL"); + ImGui.TextUnformatted(address); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(name); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(c.ModCollection.Name); + } + } + } + + using (var pathTree = TreeNode("Path Collections")) + { + if (pathTree) + { + using var table = Table("###PathCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit); + if (table) + foreach (var (path, collection) in _pathResolver.PathCollections) + { + ImGui.TableNextColumn(); + ImGuiNative.igTextUnformatted(path.Path, path.Path + path.Length); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(collection.ModCollection.Name); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(collection.AssociatedGameObject.ToString("X")); + } + } + } + + using (var resourceTree = TreeNode("Subfile Collections")) + { + if (resourceTree) + { + using var table = Table("###ResourceCollectionResolverTable", 3, ImGuiTableFlags.SizingFixedFit); + if (table) + { + ImGuiUtil.DrawTableColumn("Current Mtrl Data"); + ImGuiUtil.DrawTableColumn(_pathResolver.CurrentMtrlData.ModCollection.Name); + ImGuiUtil.DrawTableColumn($"0x{_pathResolver.CurrentMtrlData.AssociatedGameObject:X}"); + + ImGuiUtil.DrawTableColumn("Current Avfx Data"); + ImGuiUtil.DrawTableColumn(_pathResolver.CurrentAvfxData.ModCollection.Name); + ImGuiUtil.DrawTableColumn($"0x{_pathResolver.CurrentAvfxData.AssociatedGameObject:X}"); + + ImGuiUtil.DrawTableColumn("Current Resources"); + ImGuiUtil.DrawTableColumn(_pathResolver.SubfileCount.ToString()); + ImGui.TableNextColumn(); + + foreach (var (resource, resolve) in _pathResolver.ResourceCollections) + { + ImGuiUtil.DrawTableColumn($"0x{resource:X}"); + ImGuiUtil.DrawTableColumn(resolve.ModCollection.Name); + ImGuiUtil.DrawTableColumn($"0x{resolve.AssociatedGameObject:X}"); + } + } + } + } + + using (var identifiedTree = TreeNode("Identified Collections")) + { + if (identifiedTree) + { + using var table = Table("##PathCollectionsIdentifiedTable", 3, ImGuiTableFlags.SizingFixedFit); + if (table) + foreach (var (address, identifier, collection) in PathResolver.IdentifiedCache) + { + ImGuiUtil.DrawTableColumn($"0x{address:X}"); + ImGuiUtil.DrawTableColumn(identifier.ToString()); + ImGuiUtil.DrawTableColumn(collection.Name); + } + } + } + + using (var cutsceneTree = TreeNode("Cutscene Actors")) + { + if (cutsceneTree) + { + using var table = Table("###PCutsceneResolverTable", 2, ImGuiTableFlags.SizingFixedFit); + if (table) + foreach (var (idx, actor) in _pathResolver.CutsceneActors) + { + ImGuiUtil.DrawTableColumn($"Cutscene Actor {idx}"); + ImGuiUtil.DrawTableColumn(actor.Name.ToString()); + } + } + } + + using (var groupTree = TreeNode("Group")) + { + if (groupTree) + { + using var table = Table("###PGroupTable", 2, ImGuiTableFlags.SizingFixedFit); + if (table) + { + ImGuiUtil.DrawTableColumn("Group Members"); + ImGuiUtil.DrawTableColumn(GroupManager.Instance()->MemberCount.ToString()); + for (var i = 0; i < 8; ++i) + { + ImGuiUtil.DrawTableColumn($"Member #{i}"); + var member = GroupManager.Instance()->GetPartyMemberByIndex(i); + ImGuiUtil.DrawTableColumn(member == null ? "NULL" : new ByteString(member->Name).ToString()); + } + } + } + } + + using (var bannerTree = TreeNode("Party Banner")) + { + if (bannerTree) + { + var agent = &AgentBannerParty.Instance()->AgentBannerInterface; + if (agent->Data == null) + agent = &AgentBannerMIP.Instance()->AgentBannerInterface; + + if (agent->Data != null) + { + using var table = Table("###PBannerTable", 2, ImGuiTableFlags.SizingFixedFit); + if (table) + for (var i = 0; i < 8; ++i) + { + var c = agent->Character(i); + ImGuiUtil.DrawTableColumn($"Character {i}"); + var name = c->Name1.ToString(); + ImGuiUtil.DrawTableColumn(name.Length == 0 ? "NULL" : $"{name} ({c->WorldId})"); + } + } + else + { + ImGui.TextUnformatted("INACTIVE"); + } + } + } + } + + private void DrawStainTemplates() + { + if (!ImGui.CollapsingHeader("Staining Templates")) + return; + + foreach (var (key, data) in _stains.StmFile.Entries) + { + using var tree = TreeNode($"Template {key}"); + if (!tree) + continue; + + using var table = Table("##table", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + continue; + + for (var i = 0; i < StmFile.StainingTemplateEntry.NumElements; ++i) + { + var (r, g, b) = data.DiffuseEntries[i]; + ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); + + (r, g, b) = data.SpecularEntries[i]; + ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); + + (r, g, b) = data.EmissiveEntries[i]; + ImGuiUtil.DrawTableColumn($"{r:F6} | {g:F6} | {b:F6}"); + + var a = data.SpecularPowerEntries[i]; + ImGuiUtil.DrawTableColumn($"{a:F6}"); + + a = data.GlossEntries[i]; + ImGuiUtil.DrawTableColumn($"{a:F6}"); + } + } + } + + /// + /// Draw information about the character utility class from SE, + /// displaying all files, their sizes, the default files and the default sizes. + /// + private unsafe void DrawDebugCharacterUtility() + { + if (!ImGui.CollapsingHeader("Character Utility")) + return; + + using var table = Table("##CharacterUtility", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + for (var i = 0; i < CharacterUtility.RelevantIndices.Length; ++i) + { + var idx = CharacterUtility.RelevantIndices[i]; + var intern = new CharacterUtility.InternalIndex(i); + var resource = _characterUtility.Address->Resource(idx); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"0x{(ulong)resource:X}"); + ImGui.TableNextColumn(); + UiHelpers.Text(resource); + ImGui.TableNextColumn(); + ImGui.Selectable($"0x{resource->GetData().Data:X}"); + if (ImGui.IsItemClicked()) + { + var (data, length) = resource->GetData(); + if (data != nint.Zero && length > 0) + ImGui.SetClipboardText(string.Join("\n", + new ReadOnlySpan((byte*)data, length).ToArray().Select(b => b.ToString("X2")))); + } + + ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{resource->GetData().Length}"); + ImGui.TableNextColumn(); + ImGui.Selectable($"0x{_characterUtility.DefaultResource(intern).Address:X}"); + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText(string.Join("\n", + new ReadOnlySpan((byte*)_characterUtility.DefaultResource(intern).Address, + _characterUtility.DefaultResource(intern).Size).ToArray().Select(b => b.ToString("X2")))); + + ImGuiUtil.HoverTooltip("Click to copy bytes to clipboard."); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{_characterUtility.DefaultResource(intern).Size}"); + } + } + + private void DrawDebugTabMetaLists() + { + if (!ImGui.CollapsingHeader("Metadata Changes")) + return; + + using var table = Table("##DebugMetaTable", 3, ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + foreach (var list in _characterUtility.Lists) + { + ImGuiUtil.DrawTableColumn(list.GlobalIndex.ToString()); + ImGuiUtil.DrawTableColumn(list.Entries.Count.ToString()); + ImGuiUtil.DrawTableColumn(string.Join(", ", list.Entries.Select(e => $"0x{e.Data:X}"))); + } + } + + /// Draw information about the resident resource files. + private unsafe void DrawDebugResidentResources() + { + if (!ImGui.CollapsingHeader("Resident Resources")) + return; + + if (_residentResources.Address == null || _residentResources.Address->NumResources == 0) + return; + + using var table = Table("##ResidentResources", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, + -Vector2.UnitX); + if (!table) + return; + + for (var i = 0; i < _residentResources.Address->NumResources; ++i) + { + var resource = _residentResources.Address->ResourceList[i]; + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"0x{(ulong)resource:X}"); + ImGui.TableNextColumn(); + UiHelpers.Text(resource); + } + } + + /// Draw information about the models, materials and resources currently loaded by the local player. + private unsafe void DrawPlayerModelInfo() + { + var player = _dalamud.ClientState.LocalPlayer; + var name = player?.Name.ToString() ?? "NULL"; + if (!ImGui.CollapsingHeader($"Player Model Info: {name}##Draw") || player == null) + return; + + var model = (CharacterBase*)((Character*)player.Address)->GameObject.GetDrawObject(); + if (model == null) + return; + + using (var t1 = Table("##table", 2, ImGuiTableFlags.SizingFixedFit)) + { + if (t1) + { + ImGuiUtil.DrawTableColumn("Flags"); + ImGuiUtil.DrawTableColumn($"{model->UnkFlags_01:X2}"); + ImGuiUtil.DrawTableColumn("Has Model In Slot Loaded"); + ImGuiUtil.DrawTableColumn($"{model->HasModelInSlotLoaded:X8}"); + ImGuiUtil.DrawTableColumn("Has Model Files In Slot Loaded"); + ImGuiUtil.DrawTableColumn($"{model->HasModelFilesInSlotLoaded:X8}"); + } + } + + using var table = Table($"##{name}DrawTable", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + ImGui.TableNextColumn(); + ImGui.TableHeader("Slot"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Imc Ptr"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Imc File"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Model Ptr"); + ImGui.TableNextColumn(); + ImGui.TableHeader("Model File"); + + for (var i = 0; i < model->SlotCount; ++i) + { + var imc = (ResourceHandle*)model->IMCArray[i]; + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"Slot {i}"); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(imc == null ? "NULL" : $"0x{(ulong)imc:X}"); + ImGui.TableNextColumn(); + if (imc != null) + UiHelpers.Text(imc); + + var mdl = (RenderModel*)model->ModelArray[i]; + ImGui.TableNextColumn(); + ImGui.TextUnformatted(mdl == null ? "NULL" : $"0x{(ulong)mdl:X}"); + if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) + continue; + + ImGui.TableNextColumn(); + { + UiHelpers.Text(mdl->ResourceHandle); + } + } + } + + /// Draw resources with unusual reference count. + private unsafe void DrawResourceProblems() + { + var header = ImGui.CollapsingHeader("Resource Problems"); + ImGuiUtil.HoverTooltip("Draw resources with unusually high reference count to detect overflows."); + if (!header) + return; + + using var table = Table("##ProblemsTable", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit); + if (!table) + return; + + _resourceManager.IterateResources((_, r) => + { + if (r->RefCount < 10000) + return; + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(r->Category.ToString()); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(r->FileType.ToString("X")); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(r->Id.ToString("X")); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(((ulong)r).ToString("X")); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(r->RefCount.ToString()); + ImGui.TableNextColumn(); + ref var name = ref r->FileName; + if (name.Capacity > 15) + UiHelpers.Text(name.BufferPtr, (int)name.Length); + else + fixed (byte* ptr = name.Buffer) + { + UiHelpers.Text(ptr, (int)name.Length); + } + }); + } + + + /// Draw information about IPC options and availability. + private void DrawDebugTabIpc() + { + if (!ImGui.CollapsingHeader("IPC")) + { + _ipc.Tester.UnsubscribeEvents(); + return; + } + + _ipc.Tester.Draw(); + } + + /// Helper to print a property and its value in a 2-column table. + private static void PrintValue(string name, string value) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(name); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(value); + } +} diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs new file mode 100644 index 00000000..45e244d8 --- /dev/null +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Classes; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Collections; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using Penumbra.String.Classes; + +namespace Penumbra.UI.Tabs; + +public class EffectiveTab : ITab +{ + private readonly ModCollection.Manager _collectionManager; + + public EffectiveTab(ModCollection.Manager collectionManager) + => _collectionManager = collectionManager; + + public ReadOnlySpan Label + => "Effective Changes"u8; + + public void DrawContent() + { + SetupEffectiveSizes(); + DrawFilters(); + using var child = ImRaii.Child("##EffectiveChangesTab", -Vector2.One, false); + if (!child) + return; + + var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; + var skips = ImGuiClip.GetNecessarySkips(height); + using var table = ImRaii.Table("##EffectiveChangesTable", 3, ImGuiTableFlags.RowBg); + if (!table) + return; + + ImGui.TableSetupColumn("##gamePath", ImGuiTableColumnFlags.WidthFixed, _effectiveLeftTextLength); + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, _effectiveArrowLength); + ImGui.TableSetupColumn("##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength); + + DrawEffectiveRows(_collectionManager.Current, skips, height, + _effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0); + } + + // Sizes + private float _effectiveLeftTextLength; + private float _effectiveRightTextLength; + private float _effectiveUnscaledArrowLength; + private float _effectiveArrowLength; + + // Filters + private LowerString _effectiveGamePathFilter = LowerString.Empty; + private LowerString _effectiveFilePathFilter = LowerString.Empty; + + /// Setup table sizes. + private void SetupEffectiveSizes() + { + if (_effectiveUnscaledArrowLength == 0) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + _effectiveUnscaledArrowLength = + ImGui.CalcTextSize(FontAwesomeIcon.LongArrowAltLeft.ToIconString()).X / UiHelpers.Scale; + } + + _effectiveArrowLength = _effectiveUnscaledArrowLength * UiHelpers.Scale; + _effectiveLeftTextLength = 450 * UiHelpers.Scale; + _effectiveRightTextLength = ImGui.GetWindowSize().X - _effectiveArrowLength - _effectiveLeftTextLength; + } + + /// Draw the header line for filters. + private void DrawFilters() + { + var tmp = _effectiveGamePathFilter.Text; + ImGui.SetNextItemWidth(_effectiveLeftTextLength); + if (ImGui.InputTextWithHint("##gamePathFilter", "Filter game path...", ref tmp, 256)) + _effectiveGamePathFilter = tmp; + + ImGui.SameLine(_effectiveArrowLength + _effectiveLeftTextLength + 3 * ImGui.GetStyle().ItemSpacing.X); + ImGui.SetNextItemWidth(-1); + tmp = _effectiveFilePathFilter.Text; + 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.Select(p => (p.Key.ToString(), p.Value.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, 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(); + UiHelpers.CopyOnClickSelectable(path.Path); + + ImGui.TableNextColumn(); + ImGuiUtil.PrintIcon(FontAwesomeIcon.LongArrowAltLeft); + ImGui.TableNextColumn(); + UiHelpers.CopyOnClickSelectable(name.Path.InternalName); + ImGuiUtil.HoverTooltip($"\nChanged by {name.Mod.Name}."); + } + + /// Draw a line for a path and its name. + private static void DrawLine((string, LowerString) 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(KeyValuePair 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.Lower)) + return false; + + return _effectiveFilePathFilter.Length == 0 || fullPath.Path.FullName.ToLowerInvariant().Contains(_effectiveFilePathFilter.Lower); + } + + /// Check filters for meta manipulations. + private bool CheckFilters((string, LowerString) kvp) + { + var (name, path) = kvp; + if (_effectiveGamePathFilter.Length > 0 && !name.ToLowerInvariant().Contains(_effectiveGamePathFilter.Lower)) + return false; + + return _effectiveFilePathFilter.Length == 0 || path.Contains(_effectiveFilePathFilter.Lower); + } +} diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs new file mode 100644 index 00000000..6490dc2a --- /dev/null +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -0,0 +1,208 @@ +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Collections; +using Penumbra.UI.Classes; +using System; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using OtterGui.Widgets; +using Penumbra.Api.Enums; +using Penumbra.Interop; +using Penumbra.Mods; +using Penumbra.Services; +using Penumbra.UI.ModTab; +using ModFileSystemSelector = Penumbra.UI.ModTab.ModFileSystemSelector; + +namespace Penumbra.UI.Tabs; + +public class ModsTab : ITab +{ + private readonly ModFileSystemSelector _selector; + private readonly ModPanel _panel; + private readonly TutorialService _tutorial; + private readonly Mod.Manager _modManager; + private readonly ModCollection.Manager _collectionManager; + private readonly RedrawService _redrawService; + private readonly Configuration _config; + private readonly CollectionsTab _collectionsTab; + + public ModsTab(Mod.Manager modManager, ModCollection.Manager collectionManager, ModFileSystemSelector selector, ModPanel panel, + TutorialService tutorial, RedrawService redrawService, Configuration config, CollectionsTab collectionsTab) + { + _modManager = modManager; + _collectionManager = collectionManager; + _selector = selector; + _panel = panel; + _tutorial = tutorial; + _redrawService = redrawService; + _config = config; + _collectionsTab = collectionsTab; + } + + public bool IsVisible + => _modManager.Valid; + + public ReadOnlySpan Label + => "Mods"u8; + + public void DrawHeader() + => _tutorial.OpenTutorial(BasicTutorialSteps.Mods); + + public Mod SelectMod + { + set => _selector.SelectByValue(value); + } + + public void DrawContent() + { + try + { + _selector.Draw(GetModSelectorSize()); + ImGui.SameLine(); + using var group = ImRaii.Group(); + DrawHeaderLine(); + + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + + using (var child = ImRaii.Child("##ModsTabMod", new Vector2(-1, Penumbra.Config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight()), + true, ImGuiWindowFlags.HorizontalScrollbar)) + { + style.Pop(); + if (child) + _panel.Draw(); + + style.Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + } + + style.Push(ImGuiStyleVar.FrameRounding, 0); + DrawRedrawLine(); + } + catch (Exception e) + { + Penumbra.Log.Error($"Exception thrown during ModPanel Render:\n{e}"); + Penumbra.Log.Error($"{_modManager.Count} Mods\n" + + $"{_collectionManager.Current.AnonymizedName} Current Collection\n" + + $"{_collectionManager.Current.Settings.Count} Settings\n" + + $"{_selector.SortMode.Name} Sort Mode\n" + + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" + + $"{string.Join(", ", _collectionManager.Current.Inheritance.Select(c => c.AnonymizedName))} Inheritances\n" + + $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n"); + } + } + + private void DrawRedrawLine() + { + if (Penumbra.Config.HideRedrawBar) + { + _tutorial.SkipTutorial(BasicTutorialSteps.Redrawing); + return; + } + + var frameHeight = new Vector2(0, ImGui.GetFrameHeight()); + var frameColor = ImGui.GetColorU32(ImGuiCol.FrameBg); + using (var _ = ImRaii.Group()) + { + using (var font = ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGuiUtil.DrawTextButton(FontAwesomeIcon.InfoCircle.ToIconString(), frameHeight, frameColor); + ImGui.SameLine(); + } + + ImGuiUtil.DrawTextButton("Redraw: ", frameHeight, frameColor); + } + + var hovered = ImGui.IsItemHovered(); + _tutorial.OpenTutorial(BasicTutorialSteps.Redrawing); + if (hovered) + ImGui.SetTooltip($"The supported modifiers for '/penumbra redraw' are:\n{TutorialService.SupportedRedrawModifiers}"); + + void DrawButton(Vector2 size, string label, string lower) + { + if (ImGui.Button(label, size)) + { + if (lower.Length > 0) + _redrawService.RedrawObject(lower, RedrawType.Redraw); + else + _redrawService.RedrawAll(RedrawType.Redraw); + } + + ImGuiUtil.HoverTooltip(lower.Length > 0 ? $"Execute '/penumbra redraw {lower}'." : $"Execute '/penumbra redraw'."); + } + + using var disabled = ImRaii.Disabled(DalamudServices.SClientState.LocalPlayer == null); + ImGui.SameLine(); + var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 4 }; + DrawButton(buttonWidth, "All", string.Empty); + ImGui.SameLine(); + DrawButton(buttonWidth, "Self", "self"); + ImGui.SameLine(); + DrawButton(buttonWidth, "Target", "target"); + ImGui.SameLine(); + DrawButton(frameHeight with { X = ImGui.GetContentRegionAvail().X - 1 }, "Focus", "focus"); + } + + /// Draw the header line that can quick switch between collections. + private void DrawHeaderLine() + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0).Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + var buttonSize = new Vector2(ImGui.GetContentRegionAvail().X / 8f, 0); + + using (var _ = ImRaii.Group()) + { + DrawDefaultCollectionButton(3 * buttonSize); + ImGui.SameLine(); + DrawInheritedCollectionButton(3 * buttonSize); + ImGui.SameLine(); + _collectionsTab.DrawCollectionSelector("##collectionSelector", 2 * buttonSize.X, CollectionType.Current, false); + } + + _tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); + + if (!_collectionManager.CurrentCollectionInUse) + ImGuiUtil.DrawTextButton("The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg); + } + + private void DrawDefaultCollectionButton(Vector2 width) + { + var name = $"{TutorialService.DefaultCollection} ({_collectionManager.Default.Name})"; + var isCurrent = _collectionManager.Default == _collectionManager.Current; + var isEmpty = _collectionManager.Default == ModCollection.Empty; + var tt = isCurrent ? $"The current collection is already the configured {TutorialService.DefaultCollection}." + : isEmpty ? $"The {TutorialService.DefaultCollection} is configured to be empty." + : $"Set the {TutorialService.SelectedCollection} to the configured {TutorialService.DefaultCollection}."; + if (ImGuiUtil.DrawDisabledButton(name, width, tt, isCurrent || isEmpty)) + _collectionManager.SetCollection(_collectionManager.Default, CollectionType.Current); + } + + private void DrawInheritedCollectionButton(Vector2 width) + { + var noModSelected = _selector.Selected == null; + var collection = _selector.SelectedSettingCollection; + var modInherited = collection != _collectionManager.Current; + var (name, tt) = (noModSelected, modInherited) switch + { + (true, _) => ("Inherited Collection", "No mod selected."), + (false, true) => ($"Inherited Collection ({collection.Name})", + "Set the current collection to the collection the selected mod inherits its settings from."), + (false, false) => ("Not Inherited", "The selected mod does not inherit its settings."), + }; + if (ImGuiUtil.DrawDisabledButton(name, width, tt, noModSelected || !modInherited)) + _collectionManager.SetCollection(collection, CollectionType.Current); + } + + /// Get the correct size for the mod selector based on current config. + private float GetModSelectorSize() + { + var absoluteSize = Math.Clamp(_config.ModSelectorAbsoluteSize, Configuration.Constants.MinAbsoluteSize, + Math.Min(Configuration.Constants.MaxAbsoluteSize, ImGui.GetContentRegionAvail().X - 100)); + var relativeSize = _config.ScaleModSelector + ? Math.Clamp(_config.ModSelectorScaledSize, Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize) + : 0; + return !_config.ScaleModSelector + ? absoluteSize + : Math.Max(absoluteSize, relativeSize * ImGui.GetContentRegionAvail().X / 100); + } +} diff --git a/Penumbra/UI/Tabs/ResourceTab.cs b/Penumbra/UI/Tabs/ResourceTab.cs new file mode 100644 index 00000000..b77d88a5 --- /dev/null +++ b/Penumbra/UI/Tabs/ResourceTab.cs @@ -0,0 +1,152 @@ +using System; +using System.Linq; +using System.Numerics; +using Dalamud.Game; +using Dalamud.Interface; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; +using FFXIVClientStructs.Interop; +using FFXIVClientStructs.STD; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Interop.Loader; +using Penumbra.String.Classes; + +namespace Penumbra.UI.Tabs; + +public class ResourceTab : ITab +{ + private readonly Configuration _config; + private readonly ResourceManagerService _resourceManager; + private readonly SigScanner _sigScanner; + + public ResourceTab(Configuration config, ResourceManagerService resourceManager, SigScanner sigScanner) + { + _config = config; + _resourceManager = resourceManager; + _sigScanner = sigScanner; + } + + public ReadOnlySpan Label + => "Resource Manager"u8; + + public bool IsVisible + => _config.DebugMode; + + /// Draw a tab to iterate over the main resource maps and see what resources are currently loaded. + public void DrawContent() + { + // Filter for resources containing the input string. + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint("##resourceFilter", "Filter...", ref _resourceManagerFilter, Utf8GamePath.MaxGamePathLength); + + using var child = ImRaii.Child("##ResourceManagerTab", -Vector2.One); + if (!child) + return; + + unsafe + { + Penumbra.ResourceManagerService.IterateGraphs(DrawCategoryContainer); + } + + ImGui.NewLine(); + unsafe + { + ImGui.TextUnformatted( + $"Static Address: 0x{(ulong)_resourceManager.ResourceManagerAddress:X} (+0x{(ulong)_resourceManager.ResourceManagerAddress - (ulong)_sigScanner.Module.BaseAddress:X})"); + ImGui.TextUnformatted($"Actual Address: 0x{(ulong)_resourceManager.ResourceManager:X}"); + } + } + + 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, StdMap>* map) + { + if (map == null) + return; + + var label = GetNodeLabel((uint)category, ext, map->Count); + using var tree = ImRaii.TreeNode(label); + if (!tree || map->Count == 0) + return; + + using var table = ImRaii.Table("##table", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); + if (!table) + return; + + ImGui.TableSetupColumn("Hash", ImGuiTableColumnFlags.WidthFixed, _hashColumnWidth); + ImGui.TableSetupColumn("Ptr", ImGuiTableColumnFlags.WidthFixed, _hashColumnWidth); + ImGui.TableSetupColumn("Path", ImGuiTableColumnFlags.WidthFixed, _pathColumnWidth); + ImGui.TableSetupColumn("Refs", ImGuiTableColumnFlags.WidthFixed, _refsColumnWidth); + ImGui.TableHeadersRow(); + + _resourceManager.IterateResourceMap(map, (hash, r) => + { + // Filter unwanted names. + if (_resourceManagerFilter.Length != 0 + && !r->FileName.ToString().Contains(_resourceManagerFilter, StringComparison.OrdinalIgnoreCase)) + return; + + var address = $"0x{(ulong)r:X}"; + ImGuiUtil.TextNextColumn($"0x{hash:X8}"); + ImGui.TableNextColumn(); + ImGuiUtil.CopyOnClickSelectable(address); + + var resource = (Interop.Structs.ResourceHandle*)r; + ImGui.TableNextColumn(); + UiHelpers.Text(resource); + if (ImGui.IsItemClicked()) + { + var data = Interop.Structs.ResourceHandle.GetData(resource); + if (data != null) + { + var length = (int)Interop.Structs.ResourceHandle.GetLength(resource); + ImGui.SetClipboardText(string.Join(" ", + new ReadOnlySpan(data, length).ToArray().Select(b => b.ToString("X2")))); + } + } + + ImGuiUtil.HoverTooltip("Click to copy byte-wise file data to clipboard, if any."); + + ImGuiUtil.TextNextColumn(r->RefCount.ToString()); + }); + } + + /// Draw a full category for the resource manager. + private unsafe void DrawCategoryContainer(ResourceCategory category, + StdMap>>>* map, int idx) + { + if (map == 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)); + } + } + + /// Obtain a label for an extension node. + private static string GetNodeLabel(uint label, uint type, ulong count) + { + var (lowest, mid1, mid2, highest) = Functions.SplitBytes(type); + return highest == 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}"; + } + + /// Set the widths for a resource table. + private void SetTableWidths() + { + _hashColumnWidth = 100 * UiHelpers.Scale; + _pathColumnWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - 300 * UiHelpers.Scale; + _refsColumnWidth = 30 * UiHelpers.Scale; + } +} diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs new file mode 100644 index 00000000..60f781c5 --- /dev/null +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -0,0 +1,751 @@ +using System; +using System.IO; +using System.Numerics; +using System.Runtime.CompilerServices; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Utility; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using OtterGui.Widgets; +using Penumbra.Interop; +using Penumbra.Interop.Services; +using Penumbra.Mods; +using Penumbra.Services; +using Penumbra.UI.Classes; +using ModFileSystemSelector = Penumbra.UI.ModTab.ModFileSystemSelector; + +namespace Penumbra.UI.Tabs; + +public class SettingsTab : ITab +{ + public const int RootDirectoryMaxLength = 64; + + public ReadOnlySpan Label + => "Settings"u8; + + private readonly Configuration _config; + private readonly FontReloader _fontReloader; + private readonly TutorialService _tutorial; + private readonly Penumbra _penumbra; + private readonly FileDialogService _fileDialog; + private readonly Mod.Manager _modManager; + private readonly ModFileSystemSelector _selector; + private readonly CharacterUtility _characterUtility; + private readonly ResidentResourceManager _residentResources; + private readonly DalamudServices _dalamud; + + public SettingsTab(Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, + FileDialogService fileDialog, Mod.Manager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, + ResidentResourceManager residentResources, DalamudServices dalamud) + { + _config = config; + _fontReloader = fontReloader; + _tutorial = tutorial; + _penumbra = penumbra; + _fileDialog = fileDialog; + _modManager = modManager; + _selector = selector; + _characterUtility = characterUtility; + _residentResources = residentResources; + _dalamud = dalamud; + } + + public void DrawHeader() + { + _tutorial.OpenTutorial(BasicTutorialSteps.Fin); + _tutorial.OpenTutorial(BasicTutorialSteps.Faq1); + _tutorial.OpenTutorial(BasicTutorialSteps.Faq2); + _tutorial.OpenTutorial(BasicTutorialSteps.Faq3); + } + + public void DrawContent() + { + using var child = ImRaii.Child("##SettingsTab", -Vector2.One, false); + if (!child) + return; + + DrawEnabledBox(); + Checkbox("Lock Main Window", "Prevent the main window from being resized or moved.", Penumbra.Config.FixMainWindow, + v => Penumbra.Config.FixMainWindow = v); + + ImGui.NewLine(); + DrawRootFolder(); + DrawDirectoryButtons(); + ImGui.NewLine(); + + DrawGeneralSettings(); + DrawColorSettings(); + DrawAdvancedSettings(); + DrawSupportButtons(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private void Checkbox(string label, string tooltip, bool current, Action setter) + { + using var id = ImRaii.PushId(label); + var tmp = current; + if (ImGui.Checkbox(string.Empty, ref tmp) && tmp != current) + { + setter(tmp); + _config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker(label, tooltip); + } + + #region Main Settings + + /// + /// Do not change the directory without explicitly pressing enter or this button. + /// Shows up only if the current input does not correspond to the current directory. + /// + private static bool DrawPressEnterWarning(string newName, string old, float width, bool saved, bool selected) + { + using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); + var w = new Vector2(width, 0); + var (text, valid) = CheckRootDirectoryPath(newName, old, selected); + + return (ImGui.Button(text, w) || saved) && valid; + } + + /// Check a potential new root directory for validity and return the button text and whether it is valid. + private static (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); + + if (Path.GetDirectoryName(newName) == null) + return ("Path is not allowed to be a drive root. Please add a directory.", false); + + var desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); + if (IsSubPathOf(desktop, newName)) + return ("Path is not allowed to be on your Desktop.", false); + + var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + if (IsSubPathOf(programFiles, newName) || IsSubPathOf(programFilesX86, newName)) + return ("Path is not allowed to be in ProgramFiles.", false); + + var dalamud = DalamudServices.PluginInterface.ConfigDirectory.Parent!.Parent!; + if (IsSubPathOf(dalamud.FullName, newName)) + return ("Path is not allowed to be inside your Dalamud directories.", false); + + if (Functions.GetDownloadsFolder(out var downloads) && IsSubPathOf(downloads, newName)) + return ("Path is not allowed to be inside your Downloads folder.", false); + + var gameDir = DalamudServices.SGameData.GameData.DataPath.Parent!.Parent!.FullName; + if (IsSubPathOf(gameDir, newName)) + return ("Path is not allowed to be inside your game folder.", false); + + return selected + ? ($"Press Enter or Click Here to Save (Current Directory: {old})", true) + : ($"Click Here to Save (Current Directory: {old})", true); + } + + /// Changing the base mod directory. + private string? _newModDirectory; + + /// + /// Draw a directory picker button that toggles the directory picker. + /// Selecting a directory does behave the same as writing in the text input, i.e. needs to be saved. + /// + private void DrawDirectoryPickerButton() + { + if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Folder.ToIconString(), UiHelpers.IconButtonSize, + "Select a directory via dialog.", false, true)) + return; + + _newModDirectory ??= _config.ModDirectory; + // Use the current input as start directory if it exists, + // otherwise the current mod directory, otherwise the current application directory. + var startDir = Directory.Exists(_newModDirectory) + ? _newModDirectory + : Directory.Exists(_config.ModDirectory) + ? _config.ModDirectory + : "."; + + _fileDialog.OpenFolderPicker("Choose Mod Directory", (b, s) => _newModDirectory = b ? s : _newModDirectory, startDir, false); + } + + /// + /// Draw the text input for the mod directory, + /// as well as the directory picker button and the enter warning. + /// + private void DrawRootFolder() + { + if (_newModDirectory.IsNullOrEmpty()) + _newModDirectory = _config.ModDirectory; + + using var group = ImRaii.Group(); + ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); + var save = ImGui.InputText("##rootDirectory", ref _newModDirectory, RootDirectoryMaxLength, ImGuiInputTextFlags.EnterReturnsTrue); + var selected = ImGui.IsItemActive(); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3, 0)); + ImGui.SameLine(); + DrawDirectoryPickerButton(); + style.Pop(); + ImGui.SameLine(); + + const string tt = "This is where Penumbra will store your extracted mod files.\n" + + "TTMP files are not copied, just extracted.\n" + + "This directory needs to be accessible and you need write access here.\n" + + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" + + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" + + "Definitely do not place it in your Dalamud directory or any sub-directory thereof."; + ImGuiComponents.HelpMarker(tt); + _tutorial.OpenTutorial(BasicTutorialSteps.GeneralTooltips); + ImGui.SameLine(); + ImGui.TextUnformatted("Root Directory"); + ImGuiUtil.HoverTooltip(tt); + + group.Dispose(); + _tutorial.OpenTutorial(BasicTutorialSteps.ModDirectory); + ImGui.SameLine(); + var pos = ImGui.GetCursorPosX(); + ImGui.NewLine(); + + if (_config.ModDirectory != _newModDirectory + && _newModDirectory.Length != 0 + && DrawPressEnterWarning(_newModDirectory, Penumbra.Config.ModDirectory, pos, save, selected)) + _modManager.DiscoverMods(_newModDirectory); + } + + /// Draw the Open Directory and Rediscovery buttons. + private void DrawDirectoryButtons() + { + UiHelpers.DrawOpenDirectoryButton(0, _modManager.BasePath, _modManager.Valid); + ImGui.SameLine(); + var tt = _modManager.Valid + ? "Force Penumbra to completely re-scan your root directory as if it was restarted." + : "The currently selected folder is not valid. Please select a different folder."; + if (ImGuiUtil.DrawDisabledButton("Rediscover Mods", Vector2.Zero, tt, !_modManager.Valid)) + _modManager.DiscoverMods(); + } + + /// Draw the Enable Mods Checkbox. + private void DrawEnabledBox() + { + var enabled = _config.EnableMods; + if (ImGui.Checkbox("Enable Mods", ref enabled)) + _penumbra.SetEnabled(enabled); + + _tutorial.OpenTutorial(BasicTutorialSteps.EnableMods); + } + + #endregion + + #region General Settings + + /// Draw all settings pertaining to the Mod Selector. + private void DrawGeneralSettings() + { + if (!ImGui.CollapsingHeader("General")) + { + _tutorial.OpenTutorial(BasicTutorialSteps.GeneralSettings); + return; + } + + _tutorial.OpenTutorial(BasicTutorialSteps.GeneralSettings); + + DrawHidingSettings(); + UiHelpers.DefaultLineSpace(); + + DrawMiscSettings(); + UiHelpers.DefaultLineSpace(); + + DrawIdentificationSettings(); + UiHelpers.DefaultLineSpace(); + + DrawModSelectorSettings(); + UiHelpers.DefaultLineSpace(); + + DrawModHandlingSettings(); + ImGui.NewLine(); + } + + 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; + + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + if (ImGui.DragInt("##SingleSelectRadioMax", ref _singleGroupRadioMax, 0.01f, 1)) + _singleGroupRadioMax = Math.Max(1, _singleGroupRadioMax); + + if (ImGui.IsItemDeactivated()) + { + if (_singleGroupRadioMax != _config.SingleGroupRadioMax) + { + _config.SingleGroupRadioMax = _singleGroupRadioMax; + _config.Save(); + } + + _singleGroupRadioMax = int.MaxValue; + } + + ImGuiUtil.LabeledHelpMarker("Upper Limit for Single-Selection Group Radio Buttons", + "All Single-Selection Groups with more options than specified here will be displayed as Combo-Boxes at the top.\n" + + "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; + + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + if (ImGui.DragInt("##CollapsibleGroupMin", ref _collapsibleGroupMin, 0.01f, 1)) + _collapsibleGroupMin = Math.Max(2, _collapsibleGroupMin); + + if (ImGui.IsItemDeactivated()) + { + if (_collapsibleGroupMin != _config.OptionGroupCollapsibleMin) + { + _config.OptionGroupCollapsibleMin = _collapsibleGroupMin; + _config.Save(); + } + + _collapsibleGroupMin = int.MaxValue; + } + + ImGuiUtil.LabeledHelpMarker("Collapsible Option Group Limit", + "Lower Limit for option groups displaying the Collapse/Expand button at the top."); + } + + + /// Draw the window hiding state checkboxes. + private void DrawHidingSettings() + { + Checkbox("Hide Config Window when UI is Hidden", + "Hide the penumbra main window when you manually hide the in-game user interface.", _config.HideUiWhenUiHidden, + v => + { + _config.HideUiWhenUiHidden = v; + _dalamud.UiBuilder.DisableUserUiHide = !v; + }); + Checkbox("Hide Config Window when in Cutscenes", + "Hide the penumbra main window when you are currently watching a cutscene.", Penumbra.Config.HideUiInCutscenes, + v => + { + _config.HideUiInCutscenes = v; + _dalamud.UiBuilder.DisableCutsceneUiHide = !v; + }); + Checkbox("Hide Config Window when in GPose", + "Hide the penumbra main window when you are currently in GPose mode.", Penumbra.Config.HideUiInGPose, + v => + { + _config.HideUiInGPose = v; + _dalamud.UiBuilder.DisableGposeUiHide = !v; + }); + } + + /// Draw all settings that do not fit into other categories. + private void DrawMiscSettings() + { + Checkbox("Print Chat Command Success Messages to Chat", + "Chat Commands usually print messages on failure but also on success to confirm your action. You can disable this here.", + _config.PrintSuccessfulCommandsToChat, v => _config.PrintSuccessfulCommandsToChat = v); + Checkbox("Hide Redraw Bar in Mod Panel", "Hides the lower redraw buttons in the mod panel in your Mods tab.", + _config.HideRedrawBar, v => _config.HideRedrawBar = v); + DrawSingleSelectRadioMax(); + DrawCollapsibleGroupMin(); + } + + /// Draw all settings pertaining to actor identification for collections. + private void DrawIdentificationSettings() + { + Checkbox($"Use {TutorialService.AssignedCollections} 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", + "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", + "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", + "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", + "Use the owner's name to determine the appropriate individual collection for mounts, companions, accessories and combat pets.", + _config.UseOwnerNameForCharacterCollection, v => _config.UseOwnerNameForCharacterCollection = v); + } + + /// Different supported sort modes as a combo. + private void DrawFolderSortType() + { + var sortMode = _config.SortMode; + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + using (var combo = ImRaii.Combo("##sortMode", sortMode.Name)) + { + if (combo) + foreach (var val in Configuration.Constants.ValidSortModes) + { + if (ImGui.Selectable(val.Name, val.GetType() == sortMode.GetType()) && val.GetType() != sortMode.GetType()) + { + _config.SortMode = val; + _selector.SetFilterDirty(); + _config.Save(); + } + + ImGuiUtil.HoverTooltip(val.Description); + } + } + + ImGuiUtil.LabeledHelpMarker("Sort Mode", "Choose the sort mode for the mod selector in the mods tab."); + } + + private float _absoluteSelectorSize = float.NaN; + + /// Draw a selector for the absolute size of the mod selector in pixels. + private void DrawAbsoluteSizeSelector() + { + if (float.IsNaN(_absoluteSelectorSize)) + _absoluteSelectorSize = _config.ModSelectorAbsoluteSize; + + if (ImGuiUtil.DragFloat("##absoluteSize", ref _absoluteSelectorSize, UiHelpers.InputTextWidth.X, 1, + Configuration.Constants.MinAbsoluteSize, Configuration.Constants.MaxAbsoluteSize, "%.0f") + && _absoluteSelectorSize != _config.ModSelectorAbsoluteSize) + { + _config.ModSelectorAbsoluteSize = _absoluteSelectorSize; + _config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Mod Selector Absolute Size", + "The minimal absolute size of the mod selector in the mod tab in pixels."); + } + + private int _relativeSelectorSize = int.MaxValue; + + /// Draw a selector for the relative size of the mod selector as a percentage and a toggle to enable relative sizing. + private void DrawRelativeSizeSelector() + { + var scaleModSelector = _config.ScaleModSelector; + if (ImGui.Checkbox("Scale Mod Selector With Window Size", ref scaleModSelector)) + { + _config.ScaleModSelector = scaleModSelector; + _config.Save(); + } + + ImGui.SameLine(); + if (_relativeSelectorSize == int.MaxValue) + _relativeSelectorSize = _config.ModSelectorScaledSize; + if (ImGuiUtil.DragInt("##relativeSize", ref _relativeSelectorSize, UiHelpers.InputTextWidth.X - ImGui.GetCursorPosX(), 0.1f, + Configuration.Constants.MinScaledSize, Configuration.Constants.MaxScaledSize, "%i%%") + && _relativeSelectorSize != _config.ModSelectorScaledSize) + { + _config.ModSelectorScaledSize = _relativeSelectorSize; + _config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Mod Selector Relative Size", + "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window."); + } + + /// Draw all settings pertaining to the mod selector. + private void DrawModSelectorSettings() + { + DrawFolderSortType(); + DrawAbsoluteSizeSelector(); + DrawRelativeSizeSelector(); + Checkbox("Open Folders by Default", "Whether to start with all folders collapsed or expanded in the mod selector.", + _config.OpenFoldersByDefault, v => + { + _config.OpenFoldersByDefault = v; + _selector.SetFilterDirty(); + }); + + Widget.DoubleModifierSelector("Mod Deletion Modifier", + "A modifier you need to hold while clicking the Delete Mod button for it to take effect.", UiHelpers.InputTextWidth.X, + Penumbra.Config.DeleteModModifier, + v => + { + _config.DeleteModModifier = v; + _config.Save(); + }); + } + + /// Draw all settings pertaining to import and export of mods. + private void DrawModHandlingSettings() + { + Checkbox("Always Open Import at Default Directory", + "Open the import window at the location specified here every time, forgetting your previous path.", + _config.AlwaysOpenDefaultImport, v => _config.AlwaysOpenDefaultImport = v); + DrawDefaultModImportPath(); + DrawDefaultModAuthor(); + DrawDefaultModImportFolder(); + DrawDefaultModExportPath(); + } + + + /// Draw input for the default import path for a mod. + private void DrawDefaultModImportPath() + { + var tmp = _config.DefaultModImportPath; + var spacing = new Vector2(UiHelpers.ScaleX3); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); + + ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); + if (ImGui.InputText("##defaultModImport", ref tmp, 256)) + _config.DefaultModImportPath = tmp; + + if (ImGui.IsItemDeactivatedAfterEdit()) + _config.Save(); + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Folder.ToIconString()}##import", UiHelpers.IconButtonSize, + "Select a directory via dialog.", false, true)) + { + var startDir = _config.DefaultModImportPath.Length > 0 && Directory.Exists(_config.DefaultModImportPath) + ? _config.DefaultModImportPath + : Directory.Exists(_config.ModDirectory) + ? _config.ModDirectory + : null; + + _fileDialog.OpenFolderPicker("Choose Default Import Directory", (b, s) => + { + if (!b) + return; + + _config.DefaultModImportPath = s; + _config.Save(); + }, startDir, false); + } + + style.Pop(); + ImGuiUtil.LabeledHelpMarker("Default Mod Import Directory", + "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(UiHelpers.ScaleX3); + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); + ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); + if (ImGui.InputText("##defaultModExport", ref tmp, 256)) + _tempExportDirectory = tmp; + + if (ImGui.IsItemDeactivatedAfterEdit()) + _modManager.UpdateExportDirectory(_tempExportDirectory, true); + + ImGui.SameLine(); + if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Folder.ToIconString()}##export", UiHelpers.IconButtonSize, + "Select a directory via dialog.", false, true)) + { + var startDir = _config.ExportDirectory.Length > 0 && Directory.Exists(_config.ExportDirectory) + ? _config.ExportDirectory + : Directory.Exists(_config.ModDirectory) + ? _config.ModDirectory + : null; + _fileDialog.OpenFolderPicker("Choose Default Export Directory", (b, s) => + { + if (b) + Penumbra.ModManager.UpdateExportDirectory(s, true); + }, startDir, false); + } + + style.Pop(); + ImGuiUtil.LabeledHelpMarker("Default Mod Export Directory", + "Set the directory mods get saved to when using the export function or loaded from when reimporting backups.\n" + + "Keep this empty to use the root directory."); + } + + /// Draw input for the default name to input as author into newly generated mods. + private void DrawDefaultModAuthor() + { + var tmp = _config.DefaultModAuthor; + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + if (ImGui.InputText("##defaultAuthor", ref tmp, 64)) + _config.DefaultModAuthor = tmp; + + if (ImGui.IsItemDeactivatedAfterEdit()) + _config.Save(); + + ImGuiUtil.LabeledHelpMarker("Default Mod Author", "Set the default author stored for newly created mods."); + } + + /// Draw input for the default folder to sort put newly imported mods into. + private void DrawDefaultModImportFolder() + { + var tmp = _config.DefaultImportFolder; + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); + if (ImGui.InputText("##defaultImportFolder", ref tmp, 64)) + _config.DefaultImportFolder = tmp; + + if (ImGui.IsItemDeactivatedAfterEdit()) + _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."); + } + + #endregion + + /// Draw the entire Color subsection. + private void DrawColorSettings() + { + if (!ImGui.CollapsingHeader("Colors")) + return; + + foreach (var color in Enum.GetValues()) + { + var (defaultColor, name, description) = color.Data(); + var currentColor = _config.Colors.TryGetValue(color, out var current) ? current : defaultColor; + if (Widget.ColorPicker(name, description, currentColor, c => _config.Colors[color] = c, defaultColor)) + _config.Save(); + } + + ImGui.NewLine(); + } + + #region Advanced Settings + + /// Draw all advanced settings. + private void DrawAdvancedSettings() + { + var header = ImGui.CollapsingHeader("Advanced"); + _tutorial.OpenTutorial(BasicTutorialSteps.AdvancedSettings); + + if (!header) + return; + + Checkbox("Auto Deduplicate on Import", + "Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files.", + _config.AutoDeduplicateOnImport, v => _config.AutoDeduplicateOnImport = v); + Checkbox("Keep Default Metadata Changes on Import", + "Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. " + + "Toggle this to keep them, for example if an option in a mod is supposed to disable a metadata change from a prior option.", + _config.KeepDefaultMetaChanges, v => _config.KeepDefaultMetaChanges = v); + DrawWaitForPluginsReflection(); + DrawEnableHttpApiBox(); + DrawEnableDebugModeBox(); + DrawReloadResourceButton(); + DrawReloadFontsButton(); + ImGui.NewLine(); + } + + /// Draw a checkbox for the HTTP API that creates and destroys the web server when toggled. + private void DrawEnableHttpApiBox() + { + var http = _config.EnableHttpApi; + if (ImGui.Checkbox("##http", ref http)) + { + if (http) + _penumbra.HttpApi.CreateWebServer(); + else + _penumbra.HttpApi.ShutdownWebServer(); + + _config.EnableHttpApi = http; + _config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Enable HTTP API", + "Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws."); + } + + /// Draw a checkbox to toggle Debug mode. + private void DrawEnableDebugModeBox() + { + var tmp = _config.DebugMode; + if (ImGui.Checkbox("##debugMode", ref tmp) && tmp != _config.DebugMode) + { + _config.DebugMode = tmp; + _config.Save(); + } + + ImGui.SameLine(); + ImGuiUtil.LabeledHelpMarker("Enable Debug Mode", + "[DEBUG] Enable the Debug Tab and Resource Manager Tab as well as some additional data collection. Also open the config window on plugin load."); + } + + /// Draw a button that reloads resident resources. + private void DrawReloadResourceButton() + { + if (ImGuiUtil.DrawDisabledButton("Reload Resident Resources", Vector2.Zero, + "Reload some specific files that the game keeps in memory at all times.\nYou usually should not need to do this.", + !_characterUtility.Ready)) + _residentResources.Reload(); + } + + /// Draw a button that reloads fonts. + private void DrawReloadFontsButton() + { + if (ImGuiUtil.DrawDisabledButton("Reload Fonts", Vector2.Zero, "Force the game to reload its font files.", !_fontReloader.Valid)) + _fontReloader.Reload(); + } + + /// Draw a checkbox that toggles the dalamud setting to wait for plugins on open. + private void DrawWaitForPluginsReflection() + { + if (!_dalamud.GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool value)) + { + using var disabled = ImRaii.Disabled(); + Checkbox("Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)", string.Empty, false, v => { }); + } + else + { + Checkbox("Wait for Plugins on Startup", "This changes a setting in the Dalamud Configuration found at /xlsettings -> General.", + value, + v => _dalamud.SetDalamudConfig(DalamudServices.WaitingForPluginsOption, v, "doWaitForPluginsOnStartup")); + } + } + + #endregion + + /// Draw the support button group on the right-hand side of the window. + private void DrawSupportButtons() + { + var width = ImGui.CalcTextSize(UiHelpers.SupportInfoButtonText).X + ImGui.GetStyle().FramePadding.X * 2; + var xPos = ImGui.GetWindowWidth() - width; + // Respect the scroll bar width. + if (ImGui.GetScrollMaxY() > 0) + xPos -= ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().FramePadding.X; + + ImGui.SetCursorPos(new Vector2(xPos, ImGui.GetFrameHeightWithSpacing())); + UiHelpers.DrawSupportButton(_penumbra); + + ImGui.SetCursorPos(new Vector2(xPos, 0)); + UiHelpers.DrawDiscordButton(width); + + ImGui.SetCursorPos(new Vector2(xPos, 2 * ImGui.GetFrameHeightWithSpacing())); + UiHelpers.DrawGuideButton(width); + + ImGui.SetCursorPos(new Vector2(xPos, 3 * ImGui.GetFrameHeightWithSpacing())); + if (ImGui.Button("Restart Tutorial", new Vector2(width, 0))) + { + Penumbra.Config.TutorialStep = 0; + Penumbra.Config.Save(); + } + + ImGui.SetCursorPos(new Vector2(xPos, 4 * ImGui.GetFrameHeightWithSpacing())); + if (ImGui.Button("Show Changelogs", new Vector2(width, 0))) + _penumbra.ForceChangelogOpen(); + } +} diff --git a/Penumbra/UI/TutorialService.cs b/Penumbra/UI/TutorialService.cs new file mode 100644 index 00000000..b470bcb9 --- /dev/null +++ b/Penumbra/UI/TutorialService.cs @@ -0,0 +1,180 @@ +using System; +using System.Runtime.CompilerServices; +using OtterGui.Widgets; +using Penumbra.Collections; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; + +/// List of currently available tutorials. +public enum BasicTutorialSteps +{ + GeneralTooltips, + ModDirectory, + EnableMods, + AdvancedSettings, + GeneralSettings, + Collections, + EditingCollections, + CurrentCollection, + Inheritance, + ActiveCollections, + DefaultCollection, + InterfaceCollection, + SpecialCollections1, + SpecialCollections2, + Mods, + ModImport, + AdvancedHelp, + ModFilters, + CollectionSelectors, + Redrawing, + EnablingMods, + Priority, + ModOptions, + Fin, + Faq1, + Faq2, + Faq3, + Favorites, + Tags, +} + +/// Service for the in-game tutorial. +public class TutorialService +{ + public const string SelectedCollection = "Selected Collection"; + public const string DefaultCollection = "Base Collection"; + public const string InterfaceCollection = "Interface Collection"; + public const string ActiveCollections = "Active Collections"; + public const string AssignedCollections = "Assigned Collections"; + public const string GroupAssignment = "Group Assignment"; + public const string CharacterGroups = "Character Groups"; + public const string ConditionalGroup = "Group"; + public const string ConditionalIndividual = "Character"; + public const string IndividualAssignments = "Individual Assignments"; + + 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" + + " - any specific actor name to redraw all actors of that exactly matching name."; + + private readonly Configuration _config; + private readonly Tutorial _tutorial; + + public TutorialService(Configuration 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: Editing Collections", "First, we need to open the Collection Settings.\n\n" + + "In here, we can create new collections, delete collections, or make them inherit from each other.") + .Register($"Initial Setup, Step 5: {SelectedCollection}", + $"The {SelectedCollection} is the one we are currently editing. Any changes we make in our mod settings later in the next tab will edit this collection." + + $"We should already have a collection named {ModCollection.DefaultCollection} selected, and for our simple setup, we do not need to do anything here.\n\n") + .Register("Inheritance", + "This is a more advanced feature. Click the help button for more information, but we will ignore this for now.") + .Register($"Initial Setup, Step 6: {ActiveCollections}", + $"{ActiveCollections} are those that are actually assigned to conditions at the moment.\n\n" + + "Any collection assigned here will apply to the game under certain conditions.\n\n" + + $"The {SelectedCollection} is also active for technical reasons, while not necessarily being assigned to anything.\n\n" + + "Open this now to continue.") + .Register($"Initial Setup, Step 7: {DefaultCollection}", + $"The {DefaultCollection} - which should currently be set to a collection named {ModCollection.DefaultCollection} - is the main one.\n\n" + + $"As long as no more specific conditions apply to an object in the game, the mods from the {DefaultCollection} will be used.\n\n" + + "This is also the collection you need to use for all mods that are not directly associated with any character in the game or the user interface, like music mods.") + .Register("Interface Collection", + $"The {InterfaceCollection} - which should currently be set to None - is used exclusively for files categorized as 'UI' files by the game, which is mostly icons and the backgrounds for different UI windows etc.\n\n" + + $"If you have mods manipulating your interface, they should be enabled in the collection assigned to this slot. You can of course assign the same collection you assigned to the {DefaultCollection} to the {InterfaceCollection}, too, and enable all your UI mods in this one.") + .Register(GroupAssignment + 's', + "Collections assigned here are used for groups of characters for which specific conditions are met.\n\n" + + "The more specific the condition, the higher its priority (i.e. Your Character > Player Characters > Race).\n\n" + + $"{IndividualAssignments} always take precedence before groups.") + .Register(IndividualAssignments, + "Collections assigned here are used only for individual players or NPCs that fulfill the given criteria.\n\n" + + "They may also apply to objects 'owned' by those characters implicitly, e.g. minions or mounts - see the general settings for options on this.\n\n") + .Register("Initial Setup, Step 8: 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 9: 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.") // TODO + .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 11: 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 12: 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.") + .Register("FAQ 1", "Penumbra can not easily change which items a mod applies to.") + .Register("FAQ 2", + "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 3", "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/UiHelpers.cs b/Penumbra/UI/UiHelpers.cs new file mode 100644 index 00000000..11d06428 --- /dev/null +++ b/Penumbra/UI/UiHelpers.cs @@ -0,0 +1,236 @@ +using System.Diagnostics; +using System.IO; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using ImGuiNET; +using Lumina.Data.Parsing; +using Lumina.Excel.GeneratedSheets; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Api; +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; +using Penumbra.Interop.Structs; +using Penumbra.String; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; + +public static class UiHelpers +{ + /// Draw text given by a ByteString. + public static unsafe void Text(ByteString s) + => ImGuiNative.igTextUnformatted(s.Path, s.Path + s.Length); + + /// Draw text given by a byte pointer and length. + public static unsafe void Text(byte* s, int length) + => ImGuiNative.igTextUnformatted(s, s + length); + + /// Draw the name of a resource file. + public static unsafe void Text(ResourceHandle* resource) + => Text(resource->FileName().Path, resource->FileNameLength); + + /// Draw a ByteString as a selectable. + public static unsafe bool Selectable(ByteString s, bool selected) + { + var tmp = (byte)(selected ? 1 : 0); + return ImGuiNative.igSelectable_Bool(s.Path, tmp, ImGuiSelectableFlags.None, Vector2.Zero) != 0; + } + + /// + /// A selectable that copies its text to clipboard on selection and provides a on-hover tooltip about that, + /// using an ByteString. + /// + public static unsafe void CopyOnClickSelectable(ByteString text) + { + if (ImGuiNative.igSelectable_Bool(text.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero) != 0) + ImGuiNative.igSetClipboardText(text.Path); + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Click to copy to clipboard."); + } + + /// Apply Changed Item Counters to the Name if necessary. + public static string ChangedItemName(string name, object? data) + => data is int counter ? $"{counter} Files Manipulating {name}s" : name; + + /// + /// Draw a changed item, invoking the Api-Events for clicks and tooltips. + /// Also draw the item Id in grey if requested. + /// + public static void DrawChangedItem(PenumbraApi api, string name, object? data, bool drawId) + { + name = ChangedItemName(name, data); + var ret = ImGui.Selectable(name) ? MouseButton.Left : MouseButton.None; + ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; + ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; + + if (ret != MouseButton.None) + api.InvokeClick(ret, data); + + if (api.HasTooltip && ImGui.IsItemHovered()) + { + // We can not be sure that any subscriber actually prints something in any case. + // Circumvent ugly blank tooltip with less-ugly useless tooltip. + using var tt = ImRaii.Tooltip(); + using var group = ImRaii.Group(); + api.InvokeTooltip(data); + group.Dispose(); + if (ImGui.GetItemRectSize() == Vector2.Zero) + ImGui.TextUnformatted("No actions available."); + } + + if (!drawId || !GetChangedItemObject(data, out var text)) + return; + + ImGui.SameLine(ImGui.GetContentRegionAvail().X); + ImGuiUtil.RightJustify(text, ColorId.ItemId.Value(Penumbra.Config)); + } + + /// Return more detailed object information in text, if it exists. + public static bool GetChangedItemObject(object? obj, out string text) + { + switch (obj) + { + case Item it: + var quad = (Quad)it.ModelMain; + text = quad.C == 0 ? $"({quad.A}-{quad.B})" : $"({quad.A}-{quad.B}-{quad.C})"; + return true; + case ModelChara m: + text = $"({((CharacterBase.ModelType)m.Type).ToName()} {m.Model}-{m.Base}-{m.Variant})"; + return true; + default: + text = string.Empty; + return false; + } + } + + /// Draw a button to open the official discord server. + /// The desired width of the button. + public static void DrawDiscordButton(float width) + { + const string address = @"https://discord.gg/kVva7DHV4r"; + using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.DiscordColor); + if (ImGui.Button("Join Discord for Support", new Vector2(width, 0))) + try + { + var process = new ProcessStartInfo(address) + { + UseShellExecute = true, + }; + Process.Start(process); + } + catch + { + Penumbra.ChatService.NotificationMessage($"Unable to open Discord at {address}.", "Error", NotificationType.Error); + } + + ImGuiUtil.HoverTooltip($"Open {address}"); + } + + /// The longest support button text. + public const string SupportInfoButtonText = "Copy Support Info to Clipboard"; + + /// + /// Draw a button that copies the support info to clipboards. + /// + /// + public static void DrawSupportButton(Penumbra penumbra) + { + if (!ImGui.Button(SupportInfoButtonText)) + return; + + var text = penumbra.GatherSupportInformation(); + ImGui.SetClipboardText(text); + Penumbra.ChatService.NotificationMessage($"Copied Support Info to Clipboard.", "Success", NotificationType.Success); + } + + /// Draw a button to open a specific directory in a file explorer. + /// Specific ID for the given type of directory. + /// The directory to open. + /// Whether the button is available. + public static void DrawOpenDirectoryButton(int id, DirectoryInfo directory, bool condition) + { + using var _ = ImRaii.PushId(id); + if (ImGuiUtil.DrawDisabledButton("Open Directory", Vector2.Zero, "Open this directory in your configured file explorer.", + !condition || !Directory.Exists(directory.FullName))) + Process.Start(new ProcessStartInfo(directory.FullName) + { + UseShellExecute = true, + }); + } + + /// Draw the button that opens the ReniGuide. + public static void DrawGuideButton(float width) + { + const string address = @"https://reniguide.info/"; + using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.ReniColorButton) + .Push(ImGuiCol.ButtonHovered, Colors.ReniColorHovered) + .Push(ImGuiCol.ButtonActive, Colors.ReniColorActive); + if (ImGui.Button("Beginner's Guides", new Vector2(width, 0))) + try + { + var process = new ProcessStartInfo(address) + { + UseShellExecute = true, + }; + Process.Start(process); + } + catch + { + Penumbra.ChatService.NotificationMessage($"Could not open guide at {address} in external browser.", "Error", + NotificationType.Error); + } + + ImGuiUtil.HoverTooltip( + $"Open {address}\nImage and text based guides for most functionality of Penumbra made by Serenity.\n" + + "Not directly affiliated and potentially, but not usually out of date."); + } + + /// Draw default vertical space. + public static void DefaultLineSpace() + => ImGui.Dummy(DefaultSpace); + + /// Vertical spacing between groups. + public static Vector2 DefaultSpace; + + /// Width of most input fields. + public static Vector2 InputTextWidth; + + /// Frame Height for square icon buttons. + public static Vector2 IconButtonSize; + + /// Input Text Width with space for an additional button with spacing of 3 between them. + public static float InputTextMinusButton3; + + /// Input Text Width with space for an additional button with spacing of default item spacing between them. + public static float InputTextMinusButton; + + /// Multiples of the current Global Scale + public static float Scale; + + public static float ScaleX2; + public static float ScaleX3; + public static float ScaleX4; + public static float ScaleX5; + + public static void SetupCommonSizes() + { + if (ImGuiHelpers.GlobalScale != Scale) + { + Scale = ImGuiHelpers.GlobalScale; + DefaultSpace = new Vector2(0, 10 * Scale); + InputTextWidth = new Vector2(350f * Scale, 0); + ScaleX2 = Scale * 2; + ScaleX3 = Scale * 3; + ScaleX4 = Scale * 4; + ScaleX5 = Scale * 5; + } + + IconButtonSize = new Vector2(ImGui.GetFrameHeight()); + InputTextMinusButton3 = InputTextWidth.X - IconButtonSize.X - ScaleX3; + InputTextMinusButton = InputTextWidth.X - IconButtonSize.X - ImGui.GetStyle().ItemSpacing.X; + } +} diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs index 630033b9..08143a57 100644 --- a/Penumbra/UI/WindowSystem.cs +++ b/Penumbra/UI/WindowSystem.cs @@ -11,22 +11,28 @@ public class PenumbraWindowSystem : IDisposable { private readonly UiBuilder _uiBuilder; private readonly WindowSystem _windowSystem; + private readonly FileDialogService _fileDialog; public readonly ConfigWindow Window; public readonly PenumbraChangelog Changelog; - public PenumbraWindowSystem(DalamudPluginInterface pi, PenumbraChangelog changelog, ConfigWindow window, LaunchButton _, - ModEditWindow editWindow) + public PenumbraWindowSystem(DalamudPluginInterface pi, Configuration config, PenumbraChangelog changelog, ConfigWindow window, + LaunchButton _, + ModEditWindow editWindow, FileDialogService fileDialog) { _uiBuilder = pi.UiBuilder; + _fileDialog = fileDialog; Changelog = changelog; Window = window; _windowSystem = new WindowSystem("Penumbra"); _windowSystem.AddWindow(changelog.Changelog); _windowSystem.AddWindow(window); _windowSystem.AddWindow(editWindow); - - _uiBuilder.OpenConfigUi += Window.Toggle; - _uiBuilder.Draw += _windowSystem.Draw; + _uiBuilder.OpenConfigUi += Window.Toggle; + _uiBuilder.Draw += _windowSystem.Draw; + _uiBuilder.Draw += _fileDialog.Draw; + _uiBuilder.DisableGposeUiHide = !config.HideUiInGPose; + _uiBuilder.DisableCutsceneUiHide = !config.HideUiInCutscenes; + _uiBuilder.DisableUserUiHide = !config.HideUiWhenUiHidden; } public void ForceChangelogOpen() @@ -36,5 +42,6 @@ public class PenumbraWindowSystem : IDisposable { _uiBuilder.OpenConfigUi -= Window.Toggle; _uiBuilder.Draw -= _windowSystem.Draw; + _uiBuilder.Draw -= _fileDialog.Draw; } }