diff --git a/Luna b/Luna
index cb294f47..950ebea5 160000
--- a/Luna
+++ b/Luna
@@ -1 +1 @@
-Subproject commit cb294f476476f7a3d8b56a0072dbd300b3d54c4f
+Subproject commit 950ebea591f4ab7dc0900cce22415c5221df3685
diff --git a/OtterGui b/OtterGui
index 18e62ab2..1459e2b8 160000
--- a/OtterGui
+++ b/OtterGui
@@ -1 +1 @@
-Subproject commit 18e62ab2d8b9ac7028a33707eb35f8f9c61f245a
+Subproject commit 1459e2b8f5e1687f659836709e23571235d4206c
diff --git a/Penumbra.Api b/Penumbra.Api
index c23ee05c..09cfde3d 160000
--- a/Penumbra.Api
+++ b/Penumbra.Api
@@ -1 +1 @@
-Subproject commit c23ee05c1e9fa103eaa52e6aa7e855ef568ee669
+Subproject commit 09cfde3dd43aa5afcfd147ccbe3ee61534556f12
diff --git a/Penumbra.GameData b/Penumbra.GameData
index 0901f2b7..885e5362 160000
--- a/Penumbra.GameData
+++ b/Penumbra.GameData
@@ -1 +1 @@
-Subproject commit 0901f2b7075c280c5216198921a3c09209b667d8
+Subproject commit 885e536267f814fc4e62e11a70a82cdde7b4778d
diff --git a/Penumbra/Api/Api/UiApi.cs b/Penumbra/Api/Api/UiApi.cs
index f776990b..7926a8e9 100644
--- a/Penumbra/Api/Api/UiApi.cs
+++ b/Penumbra/Api/Api/UiApi.cs
@@ -4,19 +4,20 @@ using Penumbra.Communication;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI;
+using Penumbra.UI.MainWindow;
namespace Penumbra.Api.Api;
public class UiApi : IPenumbraApiUi, Luna.IApiService, IDisposable
{
private readonly CommunicatorService _communicator;
- private readonly ConfigWindow _configWindow;
+ private readonly MainWindow _mainWindow;
private readonly ModManager _modManager;
- public UiApi(CommunicatorService communicator, ConfigWindow configWindow, ModManager modManager)
+ public UiApi(CommunicatorService communicator, MainWindow mainWindow, ModManager modManager)
{
_communicator = communicator;
- _configWindow = configWindow;
+ _mainWindow = mainWindow;
_modManager = modManager;
_communicator.ChangedItemHover.Subscribe(OnChangedItemHover, ChangedItemHover.Priority.Default);
_communicator.ChangedItemClick.Subscribe(OnChangedItemClick, ChangedItemClick.Priority.Default);
@@ -57,7 +58,7 @@ public class UiApi : IPenumbraApiUi, Luna.IApiService, IDisposable
public PenumbraApiEc OpenMainWindow(TabType tab, string modDirectory, string modName)
{
- _configWindow.IsOpen = true;
+ _mainWindow.IsOpen = true;
if (!Enum.IsDefined(tab))
return PenumbraApiEc.InvalidArgument;
@@ -77,7 +78,7 @@ public class UiApi : IPenumbraApiUi, Luna.IApiService, IDisposable
}
public void CloseMainWindow()
- => _configWindow.IsOpen = false;
+ => _mainWindow.IsOpen = false;
private void OnChangedItemClick(in ChangedItemClick.Arguments arguments)
{
diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs
index 3a79c2b4..ad48e8b4 100644
--- a/Penumbra/Collections/Cache/CollectionCacheManager.cs
+++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs
@@ -85,8 +85,8 @@ public class CollectionCacheManager : IDisposable, IService
foreach (var collection in _storage)
{
- collection._cache?.Dispose();
- collection._cache = null;
+ collection.Cache?.Dispose();
+ collection.Cache = null;
}
}
@@ -115,10 +115,10 @@ public class CollectionCacheManager : IDisposable, IService
if (collection.Identity.Index == ModCollection.Empty.Identity.Index)
return false;
- if (collection._cache != null)
+ if (collection.Cache != null)
return false;
- collection._cache = new CollectionCache(this, collection);
+ collection.Cache = new CollectionCache(this, collection);
if (collection.Identity.Index > 0)
Interlocked.Increment(ref _count);
Penumbra.Log.Verbose($"Created new cache for collection {collection.Identity.AnonymizedName}.");
@@ -146,10 +146,10 @@ public class CollectionCacheManager : IDisposable, IService
Penumbra.Log.Error(
$"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, no cache exists.");
}
- else if (collection._cache!.Calculating != -1)
+ else if (collection.Cache!.Calculating != -1)
{
Penumbra.Log.Error(
- $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, already in calculation on [{collection._cache!.Calculating}].");
+ $"[{Environment.CurrentManagedThreadId}] Recalculating effective file list for {collection.Identity.AnonymizedName} failed, already in calculation on [{collection.Cache!.Calculating}].");
}
else
{
@@ -162,7 +162,7 @@ public class CollectionCacheManager : IDisposable, IService
private void FullRecalculation(ModCollection collection)
{
- var cache = collection._cache;
+ var cache = collection.Cache;
if (cache is not { Calculating: -1 })
return;
@@ -235,11 +235,11 @@ public class CollectionCacheManager : IDisposable, IService
case ModPathChangeType.Deleted:
case ModPathChangeType.StartingReload:
foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(index).Settings?.Enabled == true))
- collection._cache!.RemoveMod(arguments.Mod, true);
+ collection.Cache!.RemoveMod(arguments.Mod, true);
break;
case ModPathChangeType.Moved:
foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(index).Settings?.Enabled == true))
- collection._cache!.ReloadMod(arguments.Mod, true);
+ collection.Cache!.ReloadMod(arguments.Mod, true);
break;
}
}
@@ -251,7 +251,7 @@ public class CollectionCacheManager : IDisposable, IService
var index = arguments.Mod.Index;
foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(index).Settings?.Enabled == true))
- collection._cache!.AddMod(arguments.Mod, true);
+ collection.Cache!.AddMod(arguments.Mod, true);
}
/// Apply a mod change to all collections with a cache.
@@ -281,7 +281,7 @@ public class CollectionCacheManager : IDisposable, IService
var index = arguments.Mod.Index;
foreach (var collection in _storage.Where(collection
=> collection.HasCache && collection.GetActualSettings(index).Settings is { Enabled: true }))
- collection._cache!.RemoveMod(arguments.Mod, false);
+ collection.Cache!.RemoveMod(arguments.Mod, false);
}
else
{
@@ -295,9 +295,9 @@ public class CollectionCacheManager : IDisposable, IService
=> collection.HasCache && collection.GetActualSettings(index).Settings is { Enabled: true }))
{
if (justAdd)
- collection._cache!.AddMod(arguments.Mod, true);
+ collection.Cache!.AddMod(arguments.Mod, true);
else
- collection._cache!.ReloadMod(arguments.Mod, true);
+ collection.Cache!.ReloadMod(arguments.Mod, true);
}
}
}
@@ -316,7 +316,7 @@ public class CollectionCacheManager : IDisposable, IService
if (!collection.HasCache)
return;
- var cache = collection._cache!;
+ var cache = collection.Cache!;
switch (arguments.Type)
{
case ModSettingChange.Inheritance: cache.ReloadMod(arguments.Mod!, true); break;
@@ -366,8 +366,8 @@ public class CollectionCacheManager : IDisposable, IService
if (!collection.HasCache)
return;
- collection._cache!.Dispose();
- collection._cache = null;
+ collection.Cache!.Dispose();
+ collection.Cache = null;
if (collection.Identity.Index > 0)
Interlocked.Decrement(ref _count);
Penumbra.Log.Verbose($"Cleared cache of collection {collection.Identity.AnonymizedName}.");
@@ -398,9 +398,9 @@ public class CollectionCacheManager : IDisposable, IService
{
foreach (var collection in Active)
{
- collection._cache!.ResolvedFiles.Clear();
- collection._cache!.Meta.Reset();
- collection._cache!.ConflictDict.Clear();
+ collection.Cache!.ResolvedFiles.Clear();
+ collection.Cache!.Meta.Reset();
+ collection.Cache!.ConflictDict.Clear();
}
}
diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs
index 9adee887..4e034de4 100644
--- a/Penumbra/Collections/Manager/ActiveCollections.cs
+++ b/Penumbra/Collections/Manager/ActiveCollections.cs
@@ -320,7 +320,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (!_storage.ByName(defaultName, out var defaultCollection))
{
Penumbra.Messager.NotificationMessage(
- $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Identity.Name}.",
+ $"Last choice of {"Base Collection"} {defaultName} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning);
Default = ModCollection.Empty;
configChanged = true;
@@ -335,7 +335,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (!_storage.ByName(interfaceName, out var interfaceCollection))
{
Penumbra.Messager.NotificationMessage(
- $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Identity.Name}.",
+ $"Last choice of Interface Collection {interfaceName} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning);
Interface = ModCollection.Empty;
configChanged = true;
@@ -350,7 +350,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (!_storage.ByName(currentName, out var currentCollection))
{
Penumbra.Messager.NotificationMessage(
- $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.",
+ $"Last choice of Selected Collection {currentName} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.",
NotificationType.Warning);
Current = _storage.DefaultNamed;
configChanged = true;
@@ -395,7 +395,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (!_storage.ById(defaultId, out var defaultCollection))
{
Penumbra.Messager.NotificationMessage(
- $"Last choice of {TutorialService.DefaultCollection} {defaultId} is not available, reset to {ModCollection.Empty.Identity.Name}.",
+ $"Last choice of {"Base Collection"} {defaultId} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning);
Default = ModCollection.Empty;
configChanged = true;
@@ -410,7 +410,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (!_storage.ById(interfaceId, out var interfaceCollection))
{
Penumbra.Messager.NotificationMessage(
- $"Last choice of {TutorialService.InterfaceCollection} {interfaceId} is not available, reset to {ModCollection.Empty.Identity.Name}.",
+ $"Last choice of {"Interface Collection"} {interfaceId} is not available, reset to {ModCollection.Empty.Identity.Name}.",
NotificationType.Warning);
Interface = ModCollection.Empty;
configChanged = true;
@@ -425,7 +425,7 @@ public class ActiveCollections : ISavable, IDisposable, IService
if (!_storage.ById(currentId, out var currentCollection))
{
Penumbra.Messager.NotificationMessage(
- $"Last choice of {TutorialService.SelectedCollection} {currentId} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.",
+ $"Last choice of Selected Collection {currentId} is not available, reset to {ModCollectionIdentity.DefaultCollectionName}.",
NotificationType.Warning);
Current = _storage.DefaultNamed;
configChanged = true;
diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs
index ba071d16..824b8ac9 100644
--- a/Penumbra/Collections/ModCollection.Cache.Access.cs
+++ b/Penumbra/Collections/ModCollection.Cache.Access.cs
@@ -10,48 +10,48 @@ namespace Penumbra.Collections;
public partial class ModCollection
{
// Only active collections need to have a cache.
- internal CollectionCache? _cache;
+ public CollectionCache? Cache;
public bool HasCache
- => _cache != null;
+ => Cache != null;
// Handle temporary mods for this collection.
public void Apply(TemporaryMod tempMod, bool created)
{
if (created)
- _cache?.AddMod(tempMod, tempMod.TotalManipulations > 0);
+ Cache?.AddMod(tempMod, tempMod.TotalManipulations > 0);
else
- _cache?.ReloadMod(tempMod, tempMod.TotalManipulations > 0);
+ Cache?.ReloadMod(tempMod, tempMod.TotalManipulations > 0);
}
public void Remove(TemporaryMod tempMod)
{
- _cache?.RemoveMod(tempMod, tempMod.TotalManipulations > 0);
+ Cache?.RemoveMod(tempMod, tempMod.TotalManipulations > 0);
}
public IEnumerable ReverseResolvePath(FullPath path)
- => _cache?.ReverseResolvePath(path) ?? Array.Empty();
+ => Cache?.ReverseResolvePath(path) ?? [];
public HashSet[] ReverseResolvePaths(IReadOnlyCollection paths)
- => _cache?.ReverseResolvePaths(paths) ?? paths.Select(_ => new HashSet()).ToArray();
+ => Cache?.ReverseResolvePaths(paths) ?? paths.Select(_ => new HashSet()).ToArray();
public FullPath? ResolvePath(Utf8GamePath path)
- => _cache?.ResolvePath(path);
+ => Cache?.ResolvePath(path);
// Obtain data from the cache.
internal MetaCache? MetaCache
- => _cache?.Meta;
+ => Cache?.Meta;
internal IReadOnlyDictionary ResolvedFiles
- => _cache?.ResolvedFiles ?? new ConcurrentDictionary();
+ => Cache?.ResolvedFiles ?? new ConcurrentDictionary();
internal IReadOnlyDictionary, IIdentifiedObjectData)> ChangedItems
- => _cache?.ChangedItems ?? new Dictionary, IIdentifiedObjectData)>();
+ => Cache?.ChangedItems ?? new Dictionary, IIdentifiedObjectData)>();
internal IEnumerable> AllConflicts
- => _cache?.AllConflicts ?? Array.Empty>();
+ => Cache?.AllConflicts ?? [];
internal SingleArray Conflicts(Mod mod)
- => _cache?.Conflicts(mod) ?? new SingleArray();
+ => Cache?.Conflicts(mod) ?? new SingleArray();
}
diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs
index 11e70b1c..2f641f7a 100644
--- a/Penumbra/CommandHandler.cs
+++ b/Penumbra/CommandHandler.cs
@@ -13,6 +13,7 @@ using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.UI;
using Penumbra.UI.Knowledge;
+using Penumbra.UI.MainWindow;
namespace Penumbra;
@@ -24,7 +25,7 @@ public class CommandHandler : IDisposable, IApiService
private readonly RedrawService _redrawService;
private readonly IChatGui _chat;
private readonly Configuration _config;
- private readonly ConfigWindow _configWindow;
+ private readonly MainWindow _mainWindow;
private readonly ActorManager _actors;
private readonly ModManager _modManager;
private readonly CollectionManager _collectionManager;
@@ -33,14 +34,14 @@ public class CommandHandler : IDisposable, IApiService
private readonly KnowledgeWindow _knowledgeWindow;
public CommandHandler(IFramework framework, ICommandManager commandManager, IChatGui chat, RedrawService redrawService,
- Configuration config, ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors,
+ Configuration config, MainWindow mainWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors,
Penumbra penumbra,
CollectionEditor collectionEditor, KnowledgeWindow knowledgeWindow)
{
_commandManager = commandManager;
_redrawService = redrawService;
_config = config;
- _configWindow = configWindow;
+ _mainWindow = mainWindow;
_modManager = modManager;
_collectionManager = collectionManager;
_actors = actors;
@@ -146,11 +147,11 @@ public class CommandHandler : IDisposable, IApiService
private bool ToggleWindow(string arguments)
{
- var value = ParseTrueFalseToggle(arguments) ?? !_configWindow.IsOpen;
- if (value == _configWindow.IsOpen)
+ var value = ParseTrueFalseToggle(arguments) ?? !_mainWindow.IsOpen;
+ if (value == _mainWindow.IsOpen)
return false;
- _configWindow.Toggle();
+ _mainWindow.Toggle();
return true;
}
@@ -211,12 +212,12 @@ public class CommandHandler : IDisposable, IApiService
if (value)
{
Print("Penumbra UI locked in place.");
- _configWindow.Flags |= WindowFlags.NoMove | WindowFlags.NoResize;
+ _mainWindow.Flags |= WindowFlags.NoMove | WindowFlags.NoResize;
}
else
{
Print("Penumbra UI unlocked.");
- _configWindow.Flags &= ~(WindowFlags.NoMove | WindowFlags.NoResize);
+ _mainWindow.Flags &= ~(WindowFlags.NoMove | WindowFlags.NoResize);
}
_config.Ephemeral.FixMainWindow = value;
diff --git a/Penumbra/Communication/CollectionChange.cs b/Penumbra/Communication/CollectionChange.cs
index e577e69d..32c07169 100644
--- a/Penumbra/Communication/CollectionChange.cs
+++ b/Penumbra/Communication/CollectionChange.cs
@@ -1,61 +1,68 @@
-using Luna;
-using Penumbra.Collections;
-using Penumbra.Collections.Manager;
-using Penumbra.UI.CollectionTab;
-
-namespace Penumbra.Communication;
-
-/// Triggered whenever collection setup is changed.
-public sealed class CollectionChange(Logger log)
- : EventBase(nameof(CollectionChange), log)
-{
- public enum Priority
- {
- ///
- DalamudSubstitutionProvider = -3,
-
- ///
- CollectionCacheManager = -2,
-
- ///
- ActiveCollections = -1,
-
- ///
- TempModManager = 0,
-
- ///
- InheritanceManager = 0,
-
- ///
- IdentifiedCollectionCache = 0,
-
- ///
- ItemSwapTab = 0,
-
- ///
- CollectionSelector = 0,
-
- ///
- IndividualAssignmentUi = 0,
-
- ///
- ModFileSystemSelector = 0,
-
- ///
+using Luna;
+using Penumbra.Collections;
+using Penumbra.Collections.Manager;
+using Penumbra.UI.CollectionTab;
+using Penumbra.UI.MainWindow;
+
+namespace Penumbra.Communication;
+
+/// Triggered whenever collection setup is changed.
+public sealed class CollectionChange(Logger log)
+ : EventBase(nameof(CollectionChange), log)
+{
+ public enum Priority
+ {
+ ///
+ EffectiveChangesCache = int.MinValue,
+
+ ///
+ DalamudSubstitutionProvider = -3,
+
+ ///
+ CollectionCacheManager = -2,
+
+ ///
+ ActiveCollections = -1,
+
+ ///
+ TempModManager = 0,
+
+ ///
+ InheritanceManager = 0,
+
+ ///
+ IdentifiedCollectionCache = 0,
+
+ ///
+ ChangedItemsTabCache = 0,
+
+ ///
+ ItemSwapTab = 0,
+
+ ///
+ CollectionSelector = 0,
+
+ ///
+ IndividualAssignmentUi = 0,
+
+ ///
+ ModFileSystemSelector = 0,
+
+ ///
ModSelection = 10,
///
- CollectionCombo = 15,
- }
-
- /// The arguments for a collection change event.
- /// The type of the changed collection ( or for additions or deletions).
- /// The old collection, or null on additions.
- /// The new collection, or null on deletions.
- /// The display name for Individual collections or an empty string otherwise.
- public readonly record struct Arguments(
- CollectionType Type,
- ModCollection? OldCollection,
- ModCollection? NewCollection,
- string DisplayName);
-}
+ CollectionCombo = 15,
+ }
+
+ /// The arguments for a collection change event.
+ /// The type of the changed collection ( or for additions or deletions).
+ /// The old collection, or null on additions.
+ /// The new collection, or null on deletions.
+ /// The display name for Individual collections or an empty string otherwise.
+ public readonly record struct Arguments(
+ CollectionType Type,
+ ModCollection? OldCollection,
+ ModCollection? NewCollection,
+ string DisplayName);
+}
diff --git a/Penumbra/Communication/ModDirectoryChanged.cs b/Penumbra/Communication/ModDirectoryChanged.cs
index 2bea68a5..cb80248b 100644
--- a/Penumbra/Communication/ModDirectoryChanged.cs
+++ b/Penumbra/Communication/ModDirectoryChanged.cs
@@ -1,5 +1,6 @@
using Luna;
using Penumbra.Api.Api;
+using Penumbra.UI.Classes;
namespace Penumbra.Communication;
@@ -12,7 +13,7 @@ public sealed class ModDirectoryChanged(Logger log)
///
Api = 0,
- ///
+ ///
FileDialogService = 0,
}
diff --git a/Penumbra/Communication/ResolvedFileChanged.cs b/Penumbra/Communication/ResolvedFileChanged.cs
index b9c220ef..80109ed8 100644
--- a/Penumbra/Communication/ResolvedFileChanged.cs
+++ b/Penumbra/Communication/ResolvedFileChanged.cs
@@ -1,44 +1,84 @@
-using Luna;
-using Penumbra.Collections;
-using Penumbra.Mods.Editor;
-using Penumbra.String.Classes;
-
-namespace Penumbra.Communication;
-
-/// Triggered whenever a redirection in a mod collection cache is manipulated.
-public sealed class ResolvedFileChanged(Logger log) : EventBase(
- nameof(ResolvedFileChanged), log)
-{
- public enum Type
- {
- Added,
- Removed,
- Replaced,
- FullRecomputeStart,
- FullRecomputeFinished,
- }
-
- public enum Priority
- {
- ///
- DalamudSubstitutionProvider = 0,
-
- ///
- SchedulerResourceManagementService = 0,
- }
-
- /// The arguments for a ResolvedFileChanged event.
- /// The type of the redirection change.
- /// The collection with a changed cache.
- /// The game path to be redirected or empty for FullRecompute
- /// The new redirection path or empty for Removed or FullRecompute.
- /// The old redirection path for Replaced, or empty.
- /// The mod responsible for the new redirection if any.
- public readonly record struct Arguments(
- Type Type,
- ModCollection Collection,
- Utf8GamePath GamePath,
- FullPath OldRedirection,
- FullPath NewRedirection,
- IMod? Mod);
-}
+using Luna;
+using Penumbra.Collections;
+using Penumbra.Meta.Manipulations;
+using Penumbra.Mods.Editor;
+using Penumbra.String.Classes;
+using Penumbra.UI.MainWindow;
+
+namespace Penumbra.Communication;
+
+/// Triggered whenever a redirection in a mod collection cache is manipulated.
+public sealed class ResolvedFileChanged(Logger log) : EventBase(
+ nameof(ResolvedFileChanged), log)
+{
+ public enum Type
+ {
+ Added,
+ Removed,
+ Replaced,
+ FullRecomputeStart,
+ FullRecomputeFinished,
+ }
+
+ public enum Priority
+ {
+ ///
+ DalamudSubstitutionProvider = 0,
+
+ ///
+ SchedulerResourceManagementService = 0,
+
+ ///
+ EffectiveChangesCache = int.MinValue,
+ }
+
+ /// The arguments for a ResolvedFileChanged event.
+ /// The type of the redirection change.
+ /// The collection with a changed cache.
+ /// The game path to be redirected or empty for FullRecompute
+ /// The new redirection path or empty for Removed or FullRecompute.
+ /// The old redirection path for Replaced, or empty.
+ /// The mod responsible for the new redirection if any.
+ public readonly record struct Arguments(
+ Type Type,
+ ModCollection Collection,
+ Utf8GamePath GamePath,
+ FullPath OldRedirection,
+ FullPath NewRedirection,
+ IMod? Mod);
+}
+
+/// Triggered whenever a meta edit in a mod collection cache is manipulated.
+public sealed class ResolvedMetaChanged(Logger log) : EventBase(
+ nameof(ResolvedMetaChanged), log)
+{
+ public enum Type
+ {
+ Added,
+ Removed,
+ Replaced,
+ FullRecomputeStart,
+ FullRecomputeFinished,
+ }
+
+ public enum Priority
+ {
+ ///
+ EffectiveChangesCache = int.MinValue,
+ }
+
+ /// The arguments for a ResolvedMetaChanged event.
+ /// The type of the meta edit change.
+ /// The collection with a changed cache.
+ /// The meta identifier changed, or invalid for recomputes.
+ /// The old value for the meta identifier if any, null otherwise.
+ /// The new value for the meta identifier if any, null otherwise.
+ /// The mod responsible for the new meta edit if any.
+ public readonly record struct Arguments(
+ Type Type,
+ ModCollection Collection,
+ IMetaIdentifier Identifier,
+ object? OldValue,
+ object? NewValue,
+ IMod? Mod);
+}
diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs
index e4e09cf8..b83bed55 100644
--- a/Penumbra/Configuration.cs
+++ b/Penumbra/Configuration.cs
@@ -2,14 +2,12 @@ using Dalamud.Configuration;
using Dalamud.Interface.ImGuiNotification;
using Luna;
using Newtonsoft.Json;
-using OtterGui.Filesystem;
using Penumbra.Import.Structs;
using Penumbra.Interop.Services;
-using Penumbra.Mods;
-using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI.Classes;
using Penumbra.UI.ModsTab;
+using Penumbra.UI.ModsTab.Selector;
using Penumbra.UI.ResourceWatcher;
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
@@ -94,7 +92,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService
[JsonConverter(typeof(SortModeConverter))]
[JsonProperty(Order = int.MaxValue)]
- public ISortMode SortMode = ISortMode.FoldersFirst;
+ public ISortMode SortMode = ISortMode.FoldersFirst;
public bool OpenFoldersByDefault { get; set; } = false;
public int SingleGroupRadioMax { get; set; } = 2;
@@ -178,40 +176,24 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public const int MinScaledSize = 5;
public const int MinimumSizeX = 900;
public const int MinimumSizeY = 675;
-
- public static readonly ISortMode[] ValidSortModes =
- {
- ISortMode.FoldersFirst,
- ISortMode.Lexicographical,
- new ModFileSystem.ImportDate(),
- new ModFileSystem.InverseImportDate(),
- ISortMode.InverseFoldersFirst,
- ISortMode.InverseLexicographical,
- ISortMode.FoldersLast,
- ISortMode.InverseFoldersLast,
- ISortMode.InternalOrder,
- ISortMode.InverseInternalOrder,
- };
}
/// Convert SortMode Types to their name.
- private class SortModeConverter : JsonConverter>
+ private class SortModeConverter : JsonConverter
{
- public override void WriteJson(JsonWriter writer, ISortMode? value, JsonSerializer serializer)
+ public override void WriteJson(JsonWriter writer, ISortMode? value, JsonSerializer serializer)
{
- value ??= ISortMode.FoldersFirst;
+ value ??= ISortMode.FoldersFirst;
serializer.Serialize(writer, value.GetType().Name);
}
- public override ISortMode ReadJson(JsonReader reader, Type objectType, ISortMode? existingValue,
- bool hasExistingValue,
- JsonSerializer serializer)
+ public override ISortMode ReadJson(JsonReader reader, Type objectType, ISortMode? existingValue,
+ bool hasExistingValue, JsonSerializer serializer)
{
- var name = serializer.Deserialize(reader);
- if (name == null || !Constants.ValidSortModes.FindFirst(s => s.GetType().Name == name, out var mode))
- return existingValue ?? ISortMode.FoldersFirst;
+ if (serializer.Deserialize(reader) is { } name)
+ return ISortMode.Valid.GetValueOrDefault(name, existingValue ?? ISortMode.FoldersFirst);
- return mode;
+ return existingValue ?? ISortMode.FoldersFirst;
}
}
@@ -220,8 +202,9 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public void Save(StreamWriter writer)
{
- using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
- var serializer = new JsonSerializer { Formatting = Formatting.Indented };
+ using var jWriter = new JsonTextWriter(writer);
+ jWriter.Formatting = Formatting.Indented;
+ var serializer = new JsonSerializer { Formatting = Formatting.Indented };
serializer.Serialize(jWriter, this);
}
diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs
index dba07d29..bfee3d68 100644
--- a/Penumbra/EphemeralConfig.cs
+++ b/Penumbra/EphemeralConfig.cs
@@ -1,12 +1,12 @@
using Dalamud.Interface.ImGuiNotification;
using Luna;
using Newtonsoft.Json;
-using Penumbra.Api.Enums;
using Penumbra.Communication;
using Penumbra.Enums;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI;
+using Penumbra.UI.Classes;
using Penumbra.UI.ResourceWatcher;
using Penumbra.UI.Tabs;
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
diff --git a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs
index 6bea8465..7269a39e 100644
--- a/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs
+++ b/Penumbra/Interop/Hooks/PostProcessing/PreBoneDeformerReplacer.cs
@@ -50,7 +50,7 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, Luna.IRequired
private SafeResourceHandle GetPreBoneDeformerForCharacter(CharacterBase* drawObject)
{
var resolveData = _collectionResolver.IdentifyCollection(&drawObject->DrawObject, true);
- if (resolveData.ModCollection._cache is not { } cache)
+ if (resolveData.ModCollection.Cache is not { } cache)
return _resourceLoader.LoadResolvedSafeResource(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath.Path, resolveData);
return cache.CustomResources.Get(ResourceCategory.Chara, ResourceType.Pbd, PreBoneDeformerPath, resolveData);
diff --git a/Penumbra/Interop/ResourceTree/ResolveContext.cs b/Penumbra/Interop/ResourceTree/ResolveContext.cs
index eb5b9324..7ed4b8c8 100644
--- a/Penumbra/Interop/ResourceTree/ResolveContext.cs
+++ b/Penumbra/Interop/ResourceTree/ResolveContext.cs
@@ -14,6 +14,7 @@ using Penumbra.Meta;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.UI;
+using Penumbra.UI.Classes;
using static Penumbra.Interop.Structs.StructExtensions;
using CharaBase = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase;
using SpanTextWriter = Luna.SpanTextWriter;
diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs
index 08dee818..e6d600ff 100644
--- a/Penumbra/Interop/ResourceTree/ResourceNode.cs
+++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs
@@ -3,6 +3,7 @@ using Penumbra.Mods;
using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.UI;
+using Penumbra.UI.Classes;
namespace Penumbra.Interop.ResourceTree;
diff --git a/Penumbra/Interop/ResourceTree/ResourceTree.cs b/Penumbra/Interop/ResourceTree/ResourceTree.cs
index 1ebfe53d..cda4486c 100644
--- a/Penumbra/Interop/ResourceTree/ResourceTree.cs
+++ b/Penumbra/Interop/ResourceTree/ResourceTree.cs
@@ -9,6 +9,7 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Hooks.PostProcessing;
using Penumbra.UI;
+using Penumbra.UI.Classes;
using CustomizeData = FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData;
using CustomizeIndex = Dalamud.Game.ClientState.Objects.Enums.CustomizeIndex;
using ModelType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CharacterBase.ModelType;
@@ -192,7 +193,7 @@ public class ResourceTree(
{
var genericContext = globalContext.CreateContext(&human->CharacterBase);
- var cache = globalContext.Collection._cache;
+ var cache = globalContext.Collection.Cache;
if (cache is not null
&& cache.CustomResources.TryGetValue(PreBoneDeformerReplacer.PreBoneDeformerPath, out var pbdHandle)
&& genericContext.CreateNodeFromPbd(pbdHandle.ResourceHandle) is { } pbdNode)
diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs
index 5383beab..43e43838 100644
--- a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs
+++ b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs
@@ -4,6 +4,7 @@ using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.String.Classes;
using Penumbra.UI;
+using Penumbra.UI.Classes;
namespace Penumbra.Interop.ResourceTree;
diff --git a/Penumbra/Meta/Manipulations/AtchIdentifier.cs b/Penumbra/Meta/Manipulations/AtchIdentifier.cs
index c248c48b..a6e19392 100644
--- a/Penumbra/Meta/Manipulations/AtchIdentifier.cs
+++ b/Penumbra/Meta/Manipulations/AtchIdentifier.cs
@@ -29,7 +29,7 @@ public readonly record struct AtchIdentifier(AtchType Type, GenderRace GenderRac
}
public override string ToString()
- => $"Atch - {Type.ToAbbreviation()} - {GenderRace.ToName()} - {EntryIndex}";
+ => $"ATCH - {Type.ToAbbreviation()} - {GenderRace.ToName()} - {EntryIndex}";
public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems)
{
diff --git a/Penumbra/Meta/Manipulations/AtrIdentifier.cs b/Penumbra/Meta/Manipulations/AtrIdentifier.cs
index ca65f6aa..79e61a3c 100644
--- a/Penumbra/Meta/Manipulations/AtrIdentifier.cs
+++ b/Penumbra/Meta/Manipulations/AtrIdentifier.cs
@@ -47,7 +47,7 @@ public readonly record struct AtrIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
public override string ToString()
{
var sb = new StringBuilder(64);
- sb.Append("Shp - ")
+ sb.Append("ATR - ")
.Append(Attribute);
if (Slot is HumanSlot.Unknown)
{
diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs
index c8423b92..250e589d 100644
--- a/Penumbra/Meta/Manipulations/Eqdp.cs
+++ b/Penumbra/Meta/Manipulations/Eqdp.cs
@@ -22,7 +22,7 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge
=> CharacterUtilityData.EqdpIdx(GenderRace, Slot.IsAccessory());
public override string ToString()
- => $"Eqdp - {SetId} - {Slot.ToName()} - {GenderRace.ToName()}";
+ => $"EQDP - {SetId} - {Slot.ToName()} - {GenderRace.ToName()}";
public bool Validate()
{
diff --git a/Penumbra/Meta/Manipulations/Eqp.cs b/Penumbra/Meta/Manipulations/Eqp.cs
index 154aca40..1e762e25 100644
--- a/Penumbra/Meta/Manipulations/Eqp.cs
+++ b/Penumbra/Meta/Manipulations/Eqp.cs
@@ -15,7 +15,7 @@ public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : I
=> MetaIndex.Eqp;
public override string ToString()
- => $"Eqp - {SetId} - {Slot}";
+ => $"EQP - {SetId} - {Slot}";
public bool Validate()
{
diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs
index 46a275a5..b5192a9f 100644
--- a/Penumbra/Meta/Manipulations/Est.cs
+++ b/Penumbra/Meta/Manipulations/Est.cs
@@ -54,7 +54,7 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende
=> (MetaIndex)Slot;
public override string ToString()
- => $"Est - {SetId} - {Slot} - {GenderRace.ToName()}";
+ => $"EST - {SetId} - {Slot} - {GenderRace.ToName()}";
public bool Validate()
{
diff --git a/Penumbra/Meta/Manipulations/Gmp.cs b/Penumbra/Meta/Manipulations/Gmp.cs
index 5bc81f26..474857d3 100644
--- a/Penumbra/Meta/Manipulations/Gmp.cs
+++ b/Penumbra/Meta/Manipulations/Gmp.cs
@@ -15,7 +15,7 @@ public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier,
=> MetaIndex.Gmp;
public override string ToString()
- => $"Gmp - {SetId}";
+ => $"GMP - {SetId}";
public bool Validate()
// No known conditions.
diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs
index fa726708..31d68268 100644
--- a/Penumbra/Meta/Manipulations/Imc.cs
+++ b/Penumbra/Meta/Manipulations/Imc.cs
@@ -62,9 +62,9 @@ public readonly record struct ImcIdentifier(
public override string ToString()
=> ObjectType switch
{
- ObjectType.Equipment or ObjectType.Accessory => $"Imc - {PrimaryId} - {EquipSlot.ToName()} - {Variant}",
- ObjectType.DemiHuman => $"Imc - {PrimaryId} - DemiHuman - {SecondaryId} - {EquipSlot.ToName()} - {Variant}",
- _ => $"Imc - {PrimaryId} - {ObjectType.ToName()} - {SecondaryId} - {BodySlot} - {Variant}",
+ ObjectType.Equipment or ObjectType.Accessory => $"IMC - {PrimaryId} - {EquipSlot.ToName()} - {Variant}",
+ ObjectType.DemiHuman => $"IMC - {PrimaryId} - DemiHuman - {SecondaryId} - {EquipSlot.ToName()} - {Variant}",
+ _ => $"IMC - {PrimaryId} - {ObjectType.ToName()} - {SecondaryId} - {BodySlot} - {Variant}",
};
diff --git a/Penumbra/Meta/Manipulations/ShpIdentifier.cs b/Penumbra/Meta/Manipulations/ShpIdentifier.cs
index 0a5b71b7..3915122b 100644
--- a/Penumbra/Meta/Manipulations/ShpIdentifier.cs
+++ b/Penumbra/Meta/Manipulations/ShpIdentifier.cs
@@ -66,7 +66,7 @@ public readonly record struct ShpIdentifier(
public override string ToString()
{
var sb = new StringBuilder(64);
- sb.Append("Shp - ")
+ sb.Append("SHP - ")
.Append(Shape);
if (Slot is HumanSlot.Unknown)
{
diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs
index c1f8b96b..c1773427 100644
--- a/Penumbra/Mods/Manager/ModFileSystem.cs
+++ b/Penumbra/Mods/Manager/ModFileSystem.cs
@@ -1,12 +1,50 @@
using Dalamud.Interface.ImGuiNotification;
+using ImSharp;
using Luna;
using OtterGui.Filesystem;
+using Penumbra.Api.Enums;
using Penumbra.Communication;
using Penumbra.Services;
+using Penumbra.UI.Classes;
+using Penumbra.UI.ModsTab;
+using Penumbra.UI.ModsTab.Selector;
using FileSystemChangeType = OtterGui.Filesystem.FileSystemChangeType;
namespace Penumbra.Mods.Manager;
+public sealed class ModTab : TwoPanelLayout, ITab
+{
+ public override ReadOnlySpan Label
+ => "Mods2"u8;
+
+ public ModTab(ModFileSystemDrawer drawer, ModPanel panel, CollectionSelectHeader collectionHeader)
+ {
+ LeftHeader = drawer.Header;
+ LeftFooter = drawer.Footer;
+ LeftPanel = drawer;
+ RightPanel = panel;
+ RightHeader = collectionHeader;
+ }
+
+ public void DrawContent()
+ => Draw();
+
+ public TabType Identifier
+ => TabType.Mods;
+
+ protected override void SetSize(Vector2 newSize)
+ {
+ base.SetSize(newSize);
+ ((ModFileSystemDrawer)LeftPanel).Config.Ephemeral.CurrentModSelectorWidth = newSize.X / Im.Style.GlobalScale;
+ }
+
+ protected override float MinimumWidth
+ => ((ModFileSystemDrawer)LeftPanel).Footer.Buttons.Count * Im.Style.FrameHeight;
+
+ protected override float MaximumWidth
+ => Im.Window.Width - 500 * Im.Style.GlobalScale;
+}
+
public sealed class ModFileSystem2 : BaseFileSystem, IDisposable, IRequiredService
{
private readonly Configuration _config;
@@ -14,7 +52,7 @@ public sealed class ModFileSystem2 : BaseFileSystem, IDisposable, IRequiredServi
private readonly ModFileSystemSaver _saver;
public ModFileSystem2(Configuration config, CommunicatorService communicator, SaveService saveService, Logger log, ModStorage modStorage)
- : base("ModFileSystem", log)
+ : base("ModFileSystem", log, true)
{
_config = config;
_communicator = communicator;
diff --git a/Penumbra/Mods/Manager/ModFileSystemSaver.cs b/Penumbra/Mods/Manager/ModFileSystemSaver.cs
index a88d73b9..9939f3bd 100644
--- a/Penumbra/Mods/Manager/ModFileSystemSaver.cs
+++ b/Penumbra/Mods/Manager/ModFileSystemSaver.cs
@@ -15,6 +15,9 @@ public sealed class ModFileSystemSaver(Logger log, BaseFileSystem fileSystem, Sa
protected override string EmptyFoldersFile(FilenameService provider)
=> provider.FileSystemEmptyFolders;
+ protected override string SelectionFile(FilenameService provider)
+ => provider.FileSystemSelectedNodes;
+
protected override string MigrationFile(FilenameService provider)
=> provider.OldFilesystemFile;
diff --git a/Penumbra/Mods/Manager/SortModes.cs b/Penumbra/Mods/Manager/SortModes.cs
deleted file mode 100644
index 247c14a4..00000000
--- a/Penumbra/Mods/Manager/SortModes.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using Luna;
-
-namespace Penumbra.Mods.Manager;
-
-public readonly struct ImportDate : ISortMode
-{
- public ReadOnlySpan Name
- => "Import Date (Older First)"u8;
-
- public ReadOnlySpan Description
- => "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."u8;
-
- public IEnumerable GetChildren(IFileSystemFolder f)
- => f.GetSubFolders().Cast().Concat(f.GetLeaves().OfType>().OrderBy(l => l.Value.ImportDate));
-}
-
-public readonly struct InverseImportDate : ISortMode
-{
- public ReadOnlySpan Name
- => "Import Date (Newer First)"u8;
-
- public ReadOnlySpan Description
- => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."u8;
-
- public IEnumerable GetChildren(IFileSystemFolder f)
- => f.GetSubFolders().Cast()
- .Concat(f.GetLeaves().OfType>().OrderByDescending(l => l.Value.ImportDate));
-}
diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs
index ee9c0d75..f5c03447 100644
--- a/Penumbra/Penumbra.cs
+++ b/Penumbra/Penumbra.cs
@@ -3,8 +3,6 @@ using Dalamud.Plugin.Services;
using ImSharp;
using Lumina.Excel.Sheets;
using Luna;
-using OtterGui;
-using OtterGui.Tasks;
using Penumbra.Api;
using Penumbra.Api.Enums;
using Penumbra.Collections;
@@ -21,6 +19,7 @@ using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI;
using Penumbra.UI.AdvancedWindow;
+using Penumbra.UI.MainWindow;
using Penumbra.UI.Tabs;
using ChangedItemClick = Penumbra.Communication.ChangedItemClick;
using ChangedItemHover = Penumbra.Communication.ChangedItemHover;
@@ -131,7 +130,7 @@ public class Penumbra : IDalamudPlugin
private void SetupInterface()
{
- AsyncTask.Run(() =>
+ Task.Run(() =>
{
var system = _services.GetService();
system.Window.Setup(this, _services.GetService());
@@ -144,12 +143,16 @@ public class Penumbra : IDalamudPlugin
var mods = _services.GetService();
var editWindowFactory = _services.GetService();
foreach (var identifier in _config.Ephemeral.AdvancedEditingOpenForModPaths)
+ {
if (mods.TryGetMod(identifier, out var mod))
editWindowFactory.OpenForMod(mod);
+ }
}
}
else
+ {
system.Dispose();
+ }
}
);
}
@@ -203,8 +206,8 @@ public class Penumbra : IDalamudPlugin
"Ktisis", "Brio",
"heliosphere-plugin", "VfxEditor", "IllusioVitae", "Aetherment",
"DynamicBridge", "GagSpeak", "ProjectGagSpeak", "RoleplayingVoiceDalamud", "AQuestReborn",
- "MareSynchronos", "LoporritSync", "KittenSync", "Snowcloak", "LightlessSync", "Sphene", "XivSync", "MareSempiterne" /* PlayerSync */, "AnatoliIliou", "LaciSynchroni"
-
+ "MareSynchronos", "LoporritSync", "KittenSync", "Snowcloak", "LightlessSync", "Sphene", "XivSync",
+ "MareSempiterne" /* PlayerSync */, "AnatoliIliou", "LaciSynchroni",
];
var plugins = _services.GetService().InstalledPlugins
.GroupBy(p => p.InternalName)
@@ -238,7 +241,7 @@ public class Penumbra : IDalamudPlugin
sb.Append(
$"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}{(cloudSynced ? ", Cloud-Synced" : "")}\n");
sb.Append(
- $"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n");
+ $"> **`Free Drive Space: `** {(drive != null ? FormattingFunctions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n");
sb.Append($"> **`Game Data Files: `** {(_gameData.HasModifiedGameDataFiles ? "Modified" : "Pristine")}\n");
sb.Append($"> **`Auto-Deduplication: `** {_config.AutoDeduplicateOnImport}\n");
sb.Append($"> **`Auto-UI-Reduplication: `** {_config.AutoReduplicateUiOnImport}\n");
@@ -285,7 +288,7 @@ public class Penumbra : IDalamudPlugin
sb.Append($"> **`{id[0].Incognito(name) + ':',-29}`** {collection.Identity.AnonymizedName}\n");
foreach (var collection in _collectionManager.Caches.Active)
- PrintCollection(collection, collection._cache!);
+ PrintCollection(collection, collection.Cache!);
return sb.ToString();
diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj
index bf385f5c..dfc2a136 100644
--- a/Penumbra/Penumbra.csproj
+++ b/Penumbra/Penumbra.csproj
@@ -72,7 +72,7 @@
-
+
diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs
index 2bdadb78..591956e7 100644
--- a/Penumbra/Services/CommunicatorService.cs
+++ b/Penumbra/Services/CommunicatorService.cs
@@ -1,85 +1,88 @@
-using Luna;
-using Penumbra.Communication;
-
-namespace Penumbra.Services;
-
-public class CommunicatorService(ServiceManager services) : IService
-{
- ///
- public readonly CollectionChange CollectionChange = services.GetService();
-
- ///
- public readonly TemporaryGlobalModChange TemporaryGlobalModChange = services.GetService();
-
- ///
- public readonly CreatingCharacterBase CreatingCharacterBase = services.GetService();
-
- ///
- public readonly CreatedCharacterBase CreatedCharacterBase = services.GetService();
-
- ///
- public readonly MtrlLoaded MtrlLoaded = services.GetService();
-
- ///
- public readonly ModDataChanged ModDataChanged = services.GetService();
-
- ///
- public readonly ModOptionChanged ModOptionChanged = services.GetService();
-
- ///
- public readonly ModDiscoveryStarted ModDiscoveryStarted = services.GetService();
-
- ///
- public readonly ModDiscoveryFinished ModDiscoveryFinished = services.GetService();
-
- ///
- public readonly ModDirectoryChanged ModDirectoryChanged = services.GetService();
-
- ///
- public readonly ModFileChanged ModFileChanged = services.GetService();
-
- ///
- public readonly ModPathChanged ModPathChanged = services.GetService();
-
- ///
- public readonly ModSettingChanged ModSettingChanged = services.GetService();
-
- ///
- public readonly CollectionInheritanceChanged CollectionInheritanceChanged = services.GetService();
-
- ///
- public readonly EnabledChanged EnabledChanged = services.GetService();
-
- ///
- public readonly PreSettingsTabBarDraw PreSettingsTabBarDraw = services.GetService();
-
- ///
- public readonly PreSettingsPanelDraw PreSettingsPanelDraw = services.GetService();
-
- ///
- public readonly PostEnabledDraw PostEnabledDraw = services.GetService();
-
- ///
- public readonly PostSettingsPanelDraw PostSettingsPanelDraw = services.GetService();
-
- ///
- public readonly ChangedItemHover ChangedItemHover = services.GetService();
-
- ///
- public readonly ChangedItemClick ChangedItemClick = services.GetService();
-
- ///
- public readonly SelectTab SelectTab = services.GetService();
-
- ///
- public readonly ResolvedFileChanged ResolvedFileChanged = services.GetService();
-
- ///
- public readonly PcpCreation PcpCreation = services.GetService();
-
- ///
- public readonly PcpParsing PcpParsing = services.GetService();
-
- ///
- public readonly CharacterUtilityFinished LoadingFinished = services.GetService();
-}
+using Luna;
+using Penumbra.Communication;
+
+namespace Penumbra.Services;
+
+public class CommunicatorService(ServiceManager services) : IService
+{
+ ///
+ public readonly CollectionChange CollectionChange = services.GetService();
+
+ ///
+ public readonly TemporaryGlobalModChange TemporaryGlobalModChange = services.GetService();
+
+ ///
+ public readonly CreatingCharacterBase CreatingCharacterBase = services.GetService();
+
+ ///
+ public readonly CreatedCharacterBase CreatedCharacterBase = services.GetService();
+
+ ///
+ public readonly MtrlLoaded MtrlLoaded = services.GetService();
+
+ ///
+ public readonly ModDataChanged ModDataChanged = services.GetService();
+
+ ///
+ public readonly ModOptionChanged ModOptionChanged = services.GetService();
+
+ ///
+ public readonly ModDiscoveryStarted ModDiscoveryStarted = services.GetService();
+
+ ///
+ public readonly ModDiscoveryFinished ModDiscoveryFinished = services.GetService();
+
+ ///
+ public readonly ModDirectoryChanged ModDirectoryChanged = services.GetService();
+
+ ///
+ public readonly ModFileChanged ModFileChanged = services.GetService();
+
+ ///
+ public readonly ModPathChanged ModPathChanged = services.GetService();
+
+ ///
+ public readonly ModSettingChanged ModSettingChanged = services.GetService();
+
+ ///
+ public readonly CollectionInheritanceChanged CollectionInheritanceChanged = services.GetService();
+
+ ///
+ public readonly EnabledChanged EnabledChanged = services.GetService();
+
+ ///
+ public readonly PreSettingsTabBarDraw PreSettingsTabBarDraw = services.GetService();
+
+ ///
+ public readonly PreSettingsPanelDraw PreSettingsPanelDraw = services.GetService();
+
+ ///
+ public readonly PostEnabledDraw PostEnabledDraw = services.GetService();
+
+ ///
+ public readonly PostSettingsPanelDraw PostSettingsPanelDraw = services.GetService();
+
+ ///
+ public readonly ChangedItemHover ChangedItemHover = services.GetService();
+
+ ///
+ public readonly ChangedItemClick ChangedItemClick = services.GetService();
+
+ ///
+ public readonly SelectTab SelectTab = services.GetService();
+
+ ///
+ public readonly ResolvedFileChanged ResolvedFileChanged = services.GetService();
+
+ ///
+ public readonly ResolvedMetaChanged ResolvedMetaChanged = services.GetService();
+
+ ///
+ public readonly PcpCreation PcpCreation = services.GetService();
+
+ ///
+ public readonly PcpParsing PcpParsing = services.GetService();
+
+ ///
+ public readonly CharacterUtilityFinished LoadingFinished = services.GetService();
+}
diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs
index 36688027..ef197664 100644
--- a/Penumbra/Services/ConfigMigrationService.cs
+++ b/Penumbra/Services/ConfigMigrationService.cs
@@ -1,432 +1,431 @@
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-using OtterGui.Filesystem;
-using Penumbra.Api.Enums;
-using Penumbra.Collections;
-using Penumbra.Collections.Manager;
-using Penumbra.Enums;
-using Penumbra.Interop.Services;
-using Penumbra.Mods;
-using Penumbra.Mods.Editor;
-using Penumbra.Mods.Manager;
-using Penumbra.Mods.Settings;
-using Penumbra.UI;
-using Penumbra.UI.Classes;
-using Penumbra.UI.ResourceWatcher;
-using Penumbra.UI.Tabs;
-using TabType = Penumbra.Api.Enums.TabType;
-
-namespace Penumbra.Services;
-
-///
-/// Contains everything to migrate from older versions of the config to the current,
-/// including deprecated fields.
-///
-public class ConfigMigrationService(SaveService saveService, BackupService backupService) : Luna.IService
-{
- private Configuration _config = null!;
- private JObject _data = null!;
-
- public string CurrentCollection = ModCollectionIdentity.DefaultCollectionName;
- public string DefaultCollection = ModCollectionIdentity.DefaultCollectionName;
- public string ForcedCollection = string.Empty;
- public Dictionary CharacterCollections = [];
- public Dictionary ModSortOrder = [];
- public bool InvertModListOrder;
- public bool SortFoldersFirst;
- public SortModeV3 SortMode = SortModeV3.FoldersFirst;
-
- /// Add missing colors to the dictionary if necessary.
- private static void AddColors(Configuration config, bool forceSave)
- {
- var save = false;
- foreach (var color in Enum.GetValues())
- save |= config.Colors.TryAdd(color, color.Data().DefaultColor);
-
- if (save || forceSave)
- config.Save();
-
- Colors.SetColors(config);
- }
-
- public void Migrate(CharacterUtility utility, Configuration config)
- {
- _config = config;
- // Do this on every migration from now on for a while
- // because it stayed alive for a bunch of people for some reason.
- DeleteMetaTmp();
-
- if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(saveService.FileNames.ConfigurationFile))
- {
- AddColors(config, false);
- return;
- }
-
- _data = JObject.Parse(File.ReadAllText(saveService.FileNames.ConfigurationFile));
- CreateBackup();
-
- Version0To1();
- Version1To2(utility);
- Version2To3();
- Version3To4();
- Version4To5();
- Version5To6();
- Version6To7();
- Version7To8();
- Version8To9();
- Version9To10();
- AddColors(config, true);
- }
-
- private void Version9To10()
- {
- if (_config.Version != 9)
+using Luna;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Penumbra.Collections;
+using Penumbra.Collections.Manager;
+using Penumbra.Enums;
+using Penumbra.Interop.Services;
+using Penumbra.Mods.Editor;
+using Penumbra.Mods.Manager;
+using Penumbra.Mods.Settings;
+using Penumbra.UI;
+using Penumbra.UI.Classes;
+using Penumbra.UI.ResourceWatcher;
+using Penumbra.UI.Tabs;
+using TabType = Penumbra.Api.Enums.TabType;
+
+namespace Penumbra.Services;
+
+///
+/// Contains everything to migrate from older versions of the config to the current,
+/// including deprecated fields.
+///
+public class ConfigMigrationService(SaveService saveService, BackupService backupService) : IService
+{
+ private Configuration _config = null!;
+ private JObject _data = null!;
+
+ public string CurrentCollection = ModCollectionIdentity.DefaultCollectionName;
+ public string DefaultCollection = ModCollectionIdentity.DefaultCollectionName;
+ public string ForcedCollection = string.Empty;
+ public Dictionary CharacterCollections = [];
+ public Dictionary ModSortOrder = [];
+ public bool InvertModListOrder;
+ public bool SortFoldersFirst;
+ public SortModeV3 SortMode = SortModeV3.FoldersFirst;
+
+ /// Add missing colors to the dictionary if necessary.
+ private static void AddColors(Configuration config, bool forceSave)
+ {
+ var save = false;
+ foreach (var color in Enum.GetValues())
+ save |= config.Colors.TryAdd(color, color.Data().DefaultColor);
+
+ if (save || forceSave)
+ config.Save();
+
+ Colors.SetColors(config);
+ }
+
+ public void Migrate(CharacterUtility utility, Configuration config)
+ {
+ _config = config;
+ // Do this on every migration from now on for a while
+ // because it stayed alive for a bunch of people for some reason.
+ DeleteMetaTmp();
+
+ if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(saveService.FileNames.ConfigurationFile))
+ {
+ AddColors(config, false);
+ return;
+ }
+
+ _data = JObject.Parse(File.ReadAllText(saveService.FileNames.ConfigurationFile));
+ CreateBackup();
+
+ Version0To1();
+ Version1To2(utility);
+ Version2To3();
+ Version3To4();
+ Version4To5();
+ Version5To6();
+ Version6To7();
+ Version7To8();
+ Version8To9();
+ Version9To10();
+ AddColors(config, true);
+ }
+
+ private void Version9To10()
+ {
+ if (_config.Version != 9)
return;
- backupService.CreateMigrationBackup("pre_filesystem_update", saveService.FileNames.OldFilesystemFile);
- _config.Version = 10;
- _config.Ephemeral.Version = 10;
+ backupService.CreateMigrationBackup("pre_filesystem_update", saveService.FileNames.OldFilesystemFile);
+ _config.Version = 10;
+ _config.Ephemeral.Version = 10;
_config.Save();
- _config.Ephemeral.Save();
+ _config.Ephemeral.Save();
}
-
- // Migrate to ephemeral config.
- private void Version8To9()
- {
- if (_config.Version != 8)
- return;
-
- backupService.CreateMigrationBackup("pre_collection_identifiers");
- _config.Version = 9;
- _config.Ephemeral.Version = 9;
- _config.Save();
- _config.Ephemeral.Save();
- }
-
- // Migrate to ephemeral config.
- private void Version7To8()
- {
- if (_config.Version != 7)
- return;
-
- _config.Version = 8;
- _config.Ephemeral.Version = 8;
-
- _config.Ephemeral.LastSeenVersion = _data["LastSeenVersion"]?.ToObject() ?? _config.Ephemeral.LastSeenVersion;
- _config.Ephemeral.DebugSeparateWindow = _data["DebugSeparateWindow"]?.ToObject() ?? _config.Ephemeral.DebugSeparateWindow;
- _config.Ephemeral.TutorialStep = _data["TutorialStep"]?.ToObject() ?? _config.Ephemeral.TutorialStep;
- _config.Ephemeral.EnableResourceLogging = _data["EnableResourceLogging"]?.ToObject() ?? _config.Ephemeral.EnableResourceLogging;
- _config.Ephemeral.ResourceLoggingFilter = _data["ResourceLoggingFilter"]?.ToObject() ?? _config.Ephemeral.ResourceLoggingFilter;
- _config.Ephemeral.EnableResourceWatcher = _data["EnableResourceWatcher"]?.ToObject() ?? _config.Ephemeral.EnableResourceWatcher;
- _config.Ephemeral.OnlyAddMatchingResources =
- _data["OnlyAddMatchingResources"]?.ToObject() ?? _config.Ephemeral.OnlyAddMatchingResources;
- _config.Ephemeral.ResourceWatcherResourceTypes = _data["ResourceWatcherResourceTypes"]?.ToObject()
- ?? _config.Ephemeral.ResourceWatcherResourceTypes;
- _config.Ephemeral.ResourceWatcherResourceCategories = _data["ResourceWatcherResourceCategories"]?.ToObject()
- ?? _config.Ephemeral.ResourceWatcherResourceCategories;
- _config.Ephemeral.ResourceWatcherRecordTypes =
- _data["ResourceWatcherRecordTypes"]?.ToObject() ?? _config.Ephemeral.ResourceWatcherRecordTypes;
- _config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject() ?? _config.Ephemeral.CollectionPanel;
- _config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject() ?? _config.Ephemeral.SelectedTab;
- _config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject()
- ?? _config.Ephemeral.ChangedItemFilter;
- _config.Ephemeral.FixMainWindow = _data["FixMainWindow"]?.ToObject() ?? _config.Ephemeral.FixMainWindow;
- _config.Ephemeral.Save();
- }
-
- // Gendered special collections were added.
- private void Version6To7()
- {
- if (_config.Version != 6)
- return;
-
- ActiveCollectionMigration.MigrateUngenderedCollections(saveService.FileNames);
- _config.Version = 7;
- }
-
-
- // A new tutorial step was inserted in the middle.
- // The UI collection and a new tutorial for it was added.
- // The migration for the UI collection itself happens in the ActiveCollections file.
- private void Version5To6()
- {
- if (_config.Version != 5)
- return;
-
- if (_config.Ephemeral.TutorialStep == 25)
- _config.Ephemeral.TutorialStep = 27;
-
- _config.Version = 6;
- }
-
- // Mod backup extension was changed from .zip to .pmp.
- // Actual migration takes place in ModManager.
- private void Version4To5()
- {
- if (_config.Version != 4)
- return;
-
- ModBackup.MigrateModBackups = true;
- _config.Version = 5;
- }
-
- // SortMode was changed from an enum to a type.
- private void Version3To4()
- {
- if (_config.Version != 3)
- return;
-
- SortMode = _data[nameof(SortMode)]?.ToObject() ?? SortMode;
- _config.SortMode = SortMode switch
- {
- SortModeV3.FoldersFirst => ISortMode.FoldersFirst,
- SortModeV3.Lexicographical => ISortMode.Lexicographical,
- SortModeV3.InverseFoldersFirst => ISortMode.InverseFoldersFirst,
- SortModeV3.InverseLexicographical => ISortMode.InverseLexicographical,
- SortModeV3.FoldersLast => ISortMode.FoldersLast,
- SortModeV3.InverseFoldersLast => ISortMode.InverseFoldersLast,
- SortModeV3.InternalOrder => ISortMode.InternalOrder,
- SortModeV3.InternalOrderInverse => ISortMode.InverseInternalOrder,
- _ => ISortMode.FoldersFirst,
- };
- _config.Version = 4;
- }
-
- // SortFoldersFirst was changed from a bool to the enum SortMode.
- private void Version2To3()
- {
- if (_config.Version != 2)
- return;
-
- SortFoldersFirst = _data[nameof(SortFoldersFirst)]?.ToObject() ?? false;
- SortMode = SortFoldersFirst ? SortModeV3.FoldersFirst : SortModeV3.Lexicographical;
- _config.Version = 3;
- }
-
- // The forced collection was removed due to general inheritance.
- // Sort Order was moved to a separate file and may contain empty folders.
- // Active collections in general were moved to their own file.
- // Delete the penumbrametatmp folder if it exists.
- private void Version1To2(CharacterUtility utility)
- {
- if (_config.Version != 1)
- return;
-
- // Ensure the right meta files are loaded.
- DeleteMetaTmp();
- if (utility.Ready)
- utility.LoadCharacterResources();
- ResettleSortOrder();
- ResettleCollectionSettings();
- ResettleForcedCollection();
- _config.Version = 2;
- }
-
- private void DeleteMetaTmp()
- {
- var path = Path.Combine(_config.ModDirectory, "penumbrametatmp");
- if (!Directory.Exists(path))
- return;
-
- try
- {
- Directory.Delete(path, true);
- }
- catch (Exception e)
- {
- Penumbra.Log.Error($"Could not delete the outdated penumbrametatmp folder:\n{e}");
- }
- }
-
- private void ResettleForcedCollection()
- {
- ForcedCollection = _data[nameof(ForcedCollection)]?.ToObject() ?? ForcedCollection;
- if (ForcedCollection.Length <= 0)
- return;
-
- // Add the previous forced collection to all current collections except itself as an inheritance.
- foreach (var collection in saveService.FileNames.CollectionFiles)
- {
- try
- {
- var jObject = JObject.Parse(File.ReadAllText(collection.FullName));
- if (jObject["Name"]?.ToObject() == ForcedCollection)
- continue;
-
- jObject[nameof(ModCollectionInheritance.DirectlyInheritsFrom)] = JToken.FromObject(new List { ForcedCollection });
- File.WriteAllText(collection.FullName, jObject.ToString());
- }
- catch (Exception e)
- {
- Penumbra.Log.Error(
- $"Could not transfer forced collection {ForcedCollection} to inheritance of collection {collection}:\n{e}");
- }
- }
- }
-
- // Move the current sort order to its own file.
- private void ResettleSortOrder()
- {
- ModSortOrder = _data[nameof(ModSortOrder)]?.ToObject>() ?? ModSortOrder;
- var file = saveService.FileNames.OldFilesystemFile;
- using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew);
- using var writer = new StreamWriter(stream);
- using var j = new JsonTextWriter(writer);
- j.Formatting = Formatting.Indented;
- j.WriteStartObject();
- j.WritePropertyName("Data");
- j.WriteStartObject();
- foreach (var (mod, path) in ModSortOrder.Where(kvp => Directory.Exists(Path.Combine(_config.ModDirectory, kvp.Key))))
- {
- j.WritePropertyName(mod, true);
- j.WriteValue(path);
- }
-
- j.WriteEndObject();
- j.WritePropertyName("EmptyFolders");
- j.WriteStartArray();
- j.WriteEndArray();
- j.WriteEndObject();
- }
-
- // Move the active collections to their own file.
- private void ResettleCollectionSettings()
- {
- CurrentCollection = _data[nameof(CurrentCollection)]?.ToObject() ?? CurrentCollection;
- DefaultCollection = _data[nameof(DefaultCollection)]?.ToObject() ?? DefaultCollection;
- CharacterCollections = _data[nameof(CharacterCollections)]?.ToObject>() ?? CharacterCollections;
- SaveActiveCollectionsV0(DefaultCollection, CurrentCollection, DefaultCollection,
- CharacterCollections.Select(kvp => (kvp.Key, kvp.Value)), Array.Empty<(CollectionType, string)>());
- }
-
- // Outdated saving using the Characters list.
- private void SaveActiveCollectionsV0(string def, string ui, string current, IEnumerable<(string, string)> characters,
- IEnumerable<(CollectionType, string)> special)
- {
- var file = saveService.FileNames.ActiveCollectionsFile;
- try
- {
- using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew);
- using var writer = new StreamWriter(stream);
- using var j = new JsonTextWriter(writer);
- j.Formatting = Formatting.Indented;
- j.WriteStartObject();
- j.WritePropertyName(nameof(ActiveCollectionData.Default));
- j.WriteValue(def);
- j.WritePropertyName(nameof(ActiveCollectionData.Interface));
- j.WriteValue(ui);
- j.WritePropertyName(nameof(ActiveCollectionData.Current));
- j.WriteValue(current);
- foreach (var (type, collection) in special)
- {
- j.WritePropertyName(type.ToString());
- j.WriteValue(collection);
- }
-
- j.WritePropertyName("Characters");
- j.WriteStartObject();
- foreach (var (character, collection) in characters)
- {
- j.WritePropertyName(character, true);
- j.WriteValue(collection);
- }
-
- j.WriteEndObject();
- j.WriteEndObject();
- Penumbra.Log.Verbose("Active Collections saved.");
- }
- catch (Exception e)
- {
- Penumbra.Log.Error($"Could not save active collections to file {file}:\n{e}");
- }
- }
-
- // Collections were introduced and the previous CurrentCollection got put into ModDirectory.
- private void Version0To1()
- {
- if (_config.Version != 0)
- return;
-
- _config.ModDirectory = _data[nameof(CurrentCollection)]?.ToObject() ?? string.Empty;
- _config.Version = 1;
- ResettleCollectionJson();
- }
-
- /// Move the previous mod configurations to a new default collection file.
- private void ResettleCollectionJson()
- {
- var collectionJson = new FileInfo(Path.Combine(_config.ModDirectory, "collection.json"));
- if (!collectionJson.Exists)
- return;
-
- var defaultCollectionFile = new FileInfo(saveService.FileNames.CollectionFile(ModCollectionIdentity.DefaultCollectionName));
- if (defaultCollectionFile.Exists)
- return;
-
- try
- {
- var text = File.ReadAllText(collectionJson.FullName);
- var data = JArray.Parse(text);
-
- var maxPriority = ModPriority.Default;
- var dict = new Dictionary();
- foreach (var setting in data.Cast())
- {
- var modName = setting["FolderName"]?.ToObject()!;
- var enabled = setting["Enabled"]?.ToObject() ?? false;
- var priority = setting["Priority"]?.ToObject() ?? ModPriority.Default;
- var settings = setting["Settings"]!.ToObject>()
- ?? setting["Conf"]!.ToObject>();
-
- dict[modName] = new ModSettings.SavedSettings()
- {
- Enabled = enabled,
- Priority = priority,
- Settings = settings!,
- };
- maxPriority = maxPriority.Max(priority);
- }
-
- InvertModListOrder = _data[nameof(InvertModListOrder)]?.ToObject() ?? InvertModListOrder;
- if (!InvertModListOrder)
- dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority });
-
- var emptyStorage = new ModStorage();
- // Only used for saving and immediately discarded, so the local collection id here is irrelevant.
- var collection = ModCollection.CreateFromData(saveService, emptyStorage, ModCollectionIdentity.New(ModCollectionIdentity.DefaultCollectionName, LocalCollectionId.Zero, 1), 0, dict, []);
- saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection));
- }
- catch (Exception e)
- {
- Penumbra.Log.Error($"Could not migrate the old collection file to new collection files:\n{e}");
- throw;
- }
- }
-
- // Create a backup of the configuration file specifically.
- private void CreateBackup()
- {
- var name = saveService.FileNames.ConfigurationFile;
- var bakName = name + ".bak";
- try
- {
- File.Copy(name, bakName, true);
- }
- catch (Exception e)
- {
- Penumbra.Log.Error($"Could not create backup copy of config at {bakName}:\n{e}");
- }
- }
-
- public enum SortModeV3 : byte
- {
- FoldersFirst = 0x00,
- Lexicographical = 0x01,
- InverseFoldersFirst = 0x02,
- InverseLexicographical = 0x03,
- FoldersLast = 0x04,
- InverseFoldersLast = 0x05,
- InternalOrder = 0x06,
- InternalOrderInverse = 0x07,
- }
-}
+
+ // Migrate to ephemeral config.
+ private void Version8To9()
+ {
+ if (_config.Version != 8)
+ return;
+
+ backupService.CreateMigrationBackup("pre_collection_identifiers");
+ _config.Version = 9;
+ _config.Ephemeral.Version = 9;
+ _config.Save();
+ _config.Ephemeral.Save();
+ }
+
+ // Migrate to ephemeral config.
+ private void Version7To8()
+ {
+ if (_config.Version != 7)
+ return;
+
+ _config.Version = 8;
+ _config.Ephemeral.Version = 8;
+
+ _config.Ephemeral.LastSeenVersion = _data["LastSeenVersion"]?.ToObject() ?? _config.Ephemeral.LastSeenVersion;
+ _config.Ephemeral.DebugSeparateWindow = _data["DebugSeparateWindow"]?.ToObject() ?? _config.Ephemeral.DebugSeparateWindow;
+ _config.Ephemeral.TutorialStep = _data["TutorialStep"]?.ToObject() ?? _config.Ephemeral.TutorialStep;
+ _config.Ephemeral.EnableResourceLogging = _data["EnableResourceLogging"]?.ToObject() ?? _config.Ephemeral.EnableResourceLogging;
+ _config.Ephemeral.ResourceLoggingFilter = _data["ResourceLoggingFilter"]?.ToObject() ?? _config.Ephemeral.ResourceLoggingFilter;
+ _config.Ephemeral.EnableResourceWatcher = _data["EnableResourceWatcher"]?.ToObject() ?? _config.Ephemeral.EnableResourceWatcher;
+ _config.Ephemeral.OnlyAddMatchingResources =
+ _data["OnlyAddMatchingResources"]?.ToObject() ?? _config.Ephemeral.OnlyAddMatchingResources;
+ _config.Ephemeral.ResourceWatcherResourceTypes = _data["ResourceWatcherResourceTypes"]?.ToObject()
+ ?? _config.Ephemeral.ResourceWatcherResourceTypes;
+ _config.Ephemeral.ResourceWatcherResourceCategories = _data["ResourceWatcherResourceCategories"]?.ToObject()
+ ?? _config.Ephemeral.ResourceWatcherResourceCategories;
+ _config.Ephemeral.ResourceWatcherRecordTypes =
+ _data["ResourceWatcherRecordTypes"]?.ToObject() ?? _config.Ephemeral.ResourceWatcherRecordTypes;
+ _config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject() ?? _config.Ephemeral.CollectionPanel;
+ _config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject() ?? _config.Ephemeral.SelectedTab;
+ _config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject()
+ ?? _config.Ephemeral.ChangedItemFilter;
+ _config.Ephemeral.FixMainWindow = _data["FixMainWindow"]?.ToObject() ?? _config.Ephemeral.FixMainWindow;
+ _config.Ephemeral.Save();
+ }
+
+ // Gendered special collections were added.
+ private void Version6To7()
+ {
+ if (_config.Version != 6)
+ return;
+
+ ActiveCollectionMigration.MigrateUngenderedCollections(saveService.FileNames);
+ _config.Version = 7;
+ }
+
+
+ // A new tutorial step was inserted in the middle.
+ // The UI collection and a new tutorial for it was added.
+ // The migration for the UI collection itself happens in the ActiveCollections file.
+ private void Version5To6()
+ {
+ if (_config.Version != 5)
+ return;
+
+ if (_config.Ephemeral.TutorialStep == 25)
+ _config.Ephemeral.TutorialStep = 27;
+
+ _config.Version = 6;
+ }
+
+ // Mod backup extension was changed from .zip to .pmp.
+ // Actual migration takes place in ModManager.
+ private void Version4To5()
+ {
+ if (_config.Version != 4)
+ return;
+
+ ModBackup.MigrateModBackups = true;
+ _config.Version = 5;
+ }
+
+ // SortMode was changed from an enum to a type.
+ private void Version3To4()
+ {
+ if (_config.Version != 3)
+ return;
+
+ SortMode = _data[nameof(SortMode)]?.ToObject() ?? SortMode;
+ _config.SortMode = SortMode switch
+ {
+ SortModeV3.FoldersFirst => ISortMode.FoldersFirst,
+ SortModeV3.Lexicographical => ISortMode.Lexicographical,
+ SortModeV3.InverseFoldersFirst => ISortMode.InverseFoldersFirst,
+ SortModeV3.InverseLexicographical => ISortMode.InverseLexicographical,
+ SortModeV3.FoldersLast => ISortMode.FoldersLast,
+ SortModeV3.InverseFoldersLast => ISortMode.InverseFoldersLast,
+ SortModeV3.InternalOrder => ISortMode.InternalOrder,
+ SortModeV3.InternalOrderInverse => ISortMode.InverseInternalOrder,
+ _ => ISortMode.FoldersFirst,
+ };
+ _config.Version = 4;
+ }
+
+ // SortFoldersFirst was changed from a bool to the enum SortMode.
+ private void Version2To3()
+ {
+ if (_config.Version != 2)
+ return;
+
+ SortFoldersFirst = _data[nameof(SortFoldersFirst)]?.ToObject() ?? false;
+ SortMode = SortFoldersFirst ? SortModeV3.FoldersFirst : SortModeV3.Lexicographical;
+ _config.Version = 3;
+ }
+
+ // The forced collection was removed due to general inheritance.
+ // Sort Order was moved to a separate file and may contain empty folders.
+ // Active collections in general were moved to their own file.
+ // Delete the penumbrametatmp folder if it exists.
+ private void Version1To2(CharacterUtility utility)
+ {
+ if (_config.Version != 1)
+ return;
+
+ // Ensure the right meta files are loaded.
+ DeleteMetaTmp();
+ if (utility.Ready)
+ utility.LoadCharacterResources();
+ ResettleSortOrder();
+ ResettleCollectionSettings();
+ ResettleForcedCollection();
+ _config.Version = 2;
+ }
+
+ private void DeleteMetaTmp()
+ {
+ var path = Path.Combine(_config.ModDirectory, "penumbrametatmp");
+ if (!Directory.Exists(path))
+ return;
+
+ try
+ {
+ Directory.Delete(path, true);
+ }
+ catch (Exception e)
+ {
+ Penumbra.Log.Error($"Could not delete the outdated penumbrametatmp folder:\n{e}");
+ }
+ }
+
+ private void ResettleForcedCollection()
+ {
+ ForcedCollection = _data[nameof(ForcedCollection)]?.ToObject() ?? ForcedCollection;
+ if (ForcedCollection.Length <= 0)
+ return;
+
+ // Add the previous forced collection to all current collections except itself as an inheritance.
+ foreach (var collection in saveService.FileNames.CollectionFiles)
+ {
+ try
+ {
+ var jObject = JObject.Parse(File.ReadAllText(collection.FullName));
+ if (jObject["Name"]?.ToObject() == ForcedCollection)
+ continue;
+
+ jObject[nameof(ModCollectionInheritance.DirectlyInheritsFrom)] = JToken.FromObject(new List { ForcedCollection });
+ File.WriteAllText(collection.FullName, jObject.ToString());
+ }
+ catch (Exception e)
+ {
+ Penumbra.Log.Error(
+ $"Could not transfer forced collection {ForcedCollection} to inheritance of collection {collection}:\n{e}");
+ }
+ }
+ }
+
+ // Move the current sort order to its own file.
+ private void ResettleSortOrder()
+ {
+ ModSortOrder = _data[nameof(ModSortOrder)]?.ToObject>() ?? ModSortOrder;
+ var file = saveService.FileNames.OldFilesystemFile;
+ using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew);
+ using var writer = new StreamWriter(stream);
+ using var j = new JsonTextWriter(writer);
+ j.Formatting = Formatting.Indented;
+ j.WriteStartObject();
+ j.WritePropertyName("Data");
+ j.WriteStartObject();
+ foreach (var (mod, path) in ModSortOrder.Where(kvp => Directory.Exists(Path.Combine(_config.ModDirectory, kvp.Key))))
+ {
+ j.WritePropertyName(mod, true);
+ j.WriteValue(path);
+ }
+
+ j.WriteEndObject();
+ j.WritePropertyName("EmptyFolders");
+ j.WriteStartArray();
+ j.WriteEndArray();
+ j.WriteEndObject();
+ }
+
+ // Move the active collections to their own file.
+ private void ResettleCollectionSettings()
+ {
+ CurrentCollection = _data[nameof(CurrentCollection)]?.ToObject() ?? CurrentCollection;
+ DefaultCollection = _data[nameof(DefaultCollection)]?.ToObject() ?? DefaultCollection;
+ CharacterCollections = _data[nameof(CharacterCollections)]?.ToObject>() ?? CharacterCollections;
+ SaveActiveCollectionsV0(DefaultCollection, CurrentCollection, DefaultCollection,
+ CharacterCollections.Select(kvp => (kvp.Key, kvp.Value)), Array.Empty<(CollectionType, string)>());
+ }
+
+ // Outdated saving using the Characters list.
+ private void SaveActiveCollectionsV0(string def, string ui, string current, IEnumerable<(string, string)> characters,
+ IEnumerable<(CollectionType, string)> special)
+ {
+ var file = saveService.FileNames.ActiveCollectionsFile;
+ try
+ {
+ using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew);
+ using var writer = new StreamWriter(stream);
+ using var j = new JsonTextWriter(writer);
+ j.Formatting = Formatting.Indented;
+ j.WriteStartObject();
+ j.WritePropertyName(nameof(ActiveCollectionData.Default));
+ j.WriteValue(def);
+ j.WritePropertyName(nameof(ActiveCollectionData.Interface));
+ j.WriteValue(ui);
+ j.WritePropertyName(nameof(ActiveCollectionData.Current));
+ j.WriteValue(current);
+ foreach (var (type, collection) in special)
+ {
+ j.WritePropertyName(type.ToString());
+ j.WriteValue(collection);
+ }
+
+ j.WritePropertyName("Characters");
+ j.WriteStartObject();
+ foreach (var (character, collection) in characters)
+ {
+ j.WritePropertyName(character, true);
+ j.WriteValue(collection);
+ }
+
+ j.WriteEndObject();
+ j.WriteEndObject();
+ Penumbra.Log.Verbose("Active Collections saved.");
+ }
+ catch (Exception e)
+ {
+ Penumbra.Log.Error($"Could not save active collections to file {file}:\n{e}");
+ }
+ }
+
+ // Collections were introduced and the previous CurrentCollection got put into ModDirectory.
+ private void Version0To1()
+ {
+ if (_config.Version != 0)
+ return;
+
+ _config.ModDirectory = _data[nameof(CurrentCollection)]?.ToObject() ?? string.Empty;
+ _config.Version = 1;
+ ResettleCollectionJson();
+ }
+
+ /// Move the previous mod configurations to a new default collection file.
+ private void ResettleCollectionJson()
+ {
+ var collectionJson = new FileInfo(Path.Combine(_config.ModDirectory, "collection.json"));
+ if (!collectionJson.Exists)
+ return;
+
+ var defaultCollectionFile = new FileInfo(saveService.FileNames.CollectionFile(ModCollectionIdentity.DefaultCollectionName));
+ if (defaultCollectionFile.Exists)
+ return;
+
+ try
+ {
+ var text = File.ReadAllText(collectionJson.FullName);
+ var data = JArray.Parse(text);
+
+ var maxPriority = ModPriority.Default;
+ var dict = new Dictionary();
+ foreach (var setting in data.Cast())
+ {
+ var modName = setting["FolderName"]?.ToObject()!;
+ var enabled = setting["Enabled"]?.ToObject() ?? false;
+ var priority = setting["Priority"]?.ToObject() ?? ModPriority.Default;
+ var settings = setting["Settings"]!.ToObject>()
+ ?? setting["Conf"]!.ToObject>();
+
+ dict[modName] = new ModSettings.SavedSettings
+ {
+ Enabled = enabled,
+ Priority = priority,
+ Settings = settings!,
+ };
+ maxPriority = maxPriority.Max(priority);
+ }
+
+ InvertModListOrder = _data[nameof(InvertModListOrder)]?.ToObject() ?? InvertModListOrder;
+ if (!InvertModListOrder)
+ dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority });
+
+ var emptyStorage = new ModStorage();
+ // Only used for saving and immediately discarded, so the local collection id here is irrelevant.
+ var collection = ModCollection.CreateFromData(saveService, emptyStorage,
+ ModCollectionIdentity.New(ModCollectionIdentity.DefaultCollectionName, LocalCollectionId.Zero, 1), 0, dict, []);
+ saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection));
+ }
+ catch (Exception e)
+ {
+ Penumbra.Log.Error($"Could not migrate the old collection file to new collection files:\n{e}");
+ throw;
+ }
+ }
+
+ // Create a backup of the configuration file specifically.
+ private void CreateBackup()
+ {
+ var name = saveService.FileNames.ConfigurationFile;
+ var bakName = name + ".bak";
+ try
+ {
+ File.Copy(name, bakName, true);
+ }
+ catch (Exception e)
+ {
+ Penumbra.Log.Error($"Could not create backup copy of config at {bakName}:\n{e}");
+ }
+ }
+
+ public enum SortModeV3 : byte
+ {
+ FoldersFirst = 0x00,
+ Lexicographical = 0x01,
+ InverseFoldersFirst = 0x02,
+ InverseLexicographical = 0x03,
+ FoldersLast = 0x04,
+ InverseFoldersLast = 0x05,
+ InternalOrder = 0x06,
+ InternalOrderInverse = 0x07,
+ }
+}
diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs
index 65362ee1..ba3ef826 100644
--- a/Penumbra/Services/FilenameService.cs
+++ b/Penumbra/Services/FilenameService.cs
@@ -17,6 +17,7 @@ public sealed class FilenameService(IDalamudPluginInterface pi) : BaseFilePathPr
public readonly string FileSystemEmptyFolders = Path.Combine(pi.ConfigDirectory.FullName, "mod_filesystem", "empty_folders.json");
public readonly string FileSystemExpandedFolders = Path.Combine(pi.ConfigDirectory.FullName, "mod_filesystem", "expanded_folders.json");
public readonly string FileSystemLockedNodes = Path.Combine(pi.ConfigDirectory.FullName, "mod_filesystem", "locked_nodes.json");
+ public readonly string FileSystemSelectedNodes = Path.Combine(pi.ConfigDirectory.FullName, "mod_filesystem", "selected_nodes.json");
public readonly string CrashHandlerExe =
Path.Combine(pi.AssemblyLocation.DirectoryName!, "Penumbra.CrashHandler.exe");
diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs
index 174e0d0f..97da119d 100644
--- a/Penumbra/Services/StainService.cs
+++ b/Penumbra/Services/StainService.cs
@@ -8,11 +8,11 @@ using Penumbra.GameData.Files.StainMapStructs;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.UI.AdvancedWindow.Materials;
+using FilterComboColors = Penumbra.UI.Classes.FilterComboColors;
using MouseWheelType = OtterGui.Widgets.MouseWheelType;
namespace Penumbra.Services;
-// TODO
//public sealed class StainTemplateCombo(FilterComboColors[] stainCombos, StmFile stmFile) : SimpleFilterCombo(SimpleFilterType.Text)
// where TDyePack : unmanaged, IDyePack
//{
diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs
index 70a88c00..c5b3a311 100644
--- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs
+++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs
@@ -565,7 +565,7 @@ public class ItemSwapTab : IDisposable, ITab
Im.Line.Same();
ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Count - 1} other Items.", Vector2.Zero,
- Colors.PressEnterWarningBg);
+ new Rgba32(Colors.PressEnterWarningBg).Color);
if (Im.Item.Hovered())
ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Item.Name))
.Select(i => i.Name)));
@@ -626,7 +626,7 @@ public class ItemSwapTab : IDisposable, ITab
Im.Line.Same();
ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Count - 1} other Items.", Vector2.Zero,
- Colors.PressEnterWarningBg);
+ new Rgba32(Colors.PressEnterWarningBg).Color);
if (Im.Item.Hovered())
ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Item.Name))
.Select(i => i.Name)));
@@ -723,7 +723,7 @@ public class ItemSwapTab : IDisposable, ITab
ModelRace.AuRa,
ModelRace.Hrothgar,
],
- RaceEnumExtensions.ToName);
+ ModelRaceExtensions.ToName);
}
}
@@ -750,7 +750,7 @@ public class ItemSwapTab : IDisposable, ITab
private static void DrawSwap(Swap swap)
{
- var flags = swap.ChildSwaps.Count == 0 ? ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.DefaultOpen;
+ var flags = swap.ChildSwaps.Count is 0 ? ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.DefaultOpen;
using var tree = ImRaii.TreeNode(SwapToString(swap), flags);
if (!tree)
return;
diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs
index a6c36161..264cffd3 100644
--- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs
+++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs
@@ -1,10 +1,6 @@
using Dalamud.Bindings.ImGui;
-using Dalamud.Interface;
using ImSharp;
using Luna;
-using OtterGui;
-using OtterGui.Raii;
-using OtterGui.Text;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.GameData.Files.StainMapStructs;
using Penumbra.Services;
@@ -30,7 +26,7 @@ public partial class MtrlTab
var framePadding = Im.Style.FramePadding;
var buttonWidth = (Im.ContentRegion.Available.X - itemSpacing * 7.0f) * 0.125f;
var frameHeight = Im.Style.FrameHeight;
- var highlighterSize = ImEx.Icon.CalculateSize(FontAwesomeIcon.Crosshairs.Icon()) + framePadding * 2.0f;
+ var highlighterSize = ImEx.Icon.CalculateSize(LunaStyle.OnHoverIcon) + framePadding * 2.0f;
using var font = Im.Font.PushMono();
using var alignment = ImStyleDouble.ButtonTextAlign.Push(new Vector2(0, 0.5f));
@@ -56,19 +52,19 @@ public partial class MtrlTab
CtBlendRect(
rcMin with { X = rcMax.X - frameHeight * 3 - itemInnerSpacing * 2 },
rcMax with { X = rcMax.X - (frameHeight + itemInnerSpacing) * 2 },
- ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].DiffuseColor)),
- ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].DiffuseColor))
+ PseudoSqrtRgb((Vector3)table[pairIndex << 1].DiffuseColor),
+ PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].DiffuseColor)
);
CtBlendRect(
rcMin with { X = rcMax.X - frameHeight * 2 - itemInnerSpacing },
rcMax with { X = rcMax.X - frameHeight - itemInnerSpacing },
- ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].SpecularColor)),
- ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].SpecularColor))
+ PseudoSqrtRgb((Vector3)table[pairIndex << 1].SpecularColor),
+ PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].SpecularColor)
);
CtBlendRect(
rcMin with { X = rcMax.X - frameHeight }, rcMax,
- ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[pairIndex << 1].EmissiveColor)),
- ImGuiUtil.ColorConvertFloat3ToU32(PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].EmissiveColor))
+ PseudoSqrtRgb((Vector3)table[pairIndex << 1].EmissiveColor),
+ PseudoSqrtRgb((Vector3)table[(pairIndex << 1) | 1].EmissiveColor)
);
if (j < 7)
Im.Line.Same();
@@ -95,95 +91,95 @@ public partial class MtrlTab
var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Key;
var dyePackA = _stainService.GudStmFile.GetValueOrNull(dyeA.Template, previewDyeA);
var dyePackB = _stainService.GudStmFile.GetValueOrNull(dyeB.Template, previewDyeB);
- using (var columns = ImUtf8.Columns(2, "ColorTable"u8))
+ using (var columns = Im.Columns(2, "ColorTable"u8))
{
- using (ImUtf8.PushId("RowHeaderA"u8))
+ using (Im.Id.Push("RowHeaderA"u8))
{
retA |= DrawRowHeader(rowAIdx, disabled);
}
columns.Next();
- using (ImUtf8.PushId("RowHeaderB"u8))
+ using (Im.Id.Push("RowHeaderB"u8))
{
retB |= DrawRowHeader(rowBIdx, disabled);
}
}
DrawHeader(" Colors"u8);
- using (var columns = ImUtf8.Columns(2, "ColorTable"u8))
+ using (var columns = Im.Columns(2, "ColorTable"u8))
{
- using var dis = ImRaii.Disabled(disabled);
- using (ImUtf8.PushId("ColorsA"u8))
+ using var dis = Im.Disabled(disabled);
+ using (Im.Id.Push("ColorsA"u8))
{
retA |= DrawColors(table, dyeTable, dyePackA, rowAIdx);
}
columns.Next();
- using (ImUtf8.PushId("ColorsB"u8))
+ using (Im.Id.Push("ColorsB"u8))
{
retB |= DrawColors(table, dyeTable, dyePackB, rowBIdx);
}
}
DrawHeader(" Physical Parameters"u8);
- using (var columns = ImUtf8.Columns(2, "ColorTable"u8))
+ using (var columns = Im.Columns(2, "ColorTable"u8))
{
- using var dis = ImRaii.Disabled(disabled);
- using (ImUtf8.PushId("PbrA"u8))
+ using var dis = Im.Disabled(disabled);
+ using (Im.Id.Push("PbrA"u8))
{
retA |= DrawPbr(table, dyeTable, dyePackA, rowAIdx);
}
columns.Next();
- using (ImUtf8.PushId("PbrB"u8))
+ using (Im.Id.Push("PbrB"u8))
{
retB |= DrawPbr(table, dyeTable, dyePackB, rowBIdx);
}
}
DrawHeader(" Sheen Layer Parameters"u8);
- using (var columns = ImUtf8.Columns(2, "ColorTable"u8))
+ using (var columns = Im.Columns(2, "ColorTable"u8))
{
- using var dis = ImRaii.Disabled(disabled);
- using (ImUtf8.PushId("SheenA"u8))
+ using var dis = Im.Disabled(disabled);
+ using (Im.Id.Push("SheenA"u8))
{
retA |= DrawSheen(table, dyeTable, dyePackA, rowAIdx);
}
columns.Next();
- using (ImUtf8.PushId("SheenB"u8))
+ using (Im.Id.Push("SheenB"u8))
{
retB |= DrawSheen(table, dyeTable, dyePackB, rowBIdx);
}
}
DrawHeader(" Pair Blending"u8);
- using (var columns = ImUtf8.Columns(2, "ColorTable"u8))
+ using (var columns = Im.Columns(2, "ColorTable"u8))
{
- using var dis = ImRaii.Disabled(disabled);
- using (ImUtf8.PushId("BlendingA"u8))
+ using var dis = Im.Disabled(disabled);
+ using (Im.Id.Push("BlendingA"u8))
{
retA |= DrawBlending(table, dyeTable, dyePackA, rowAIdx);
}
columns.Next();
- using (ImUtf8.PushId("BlendingB"u8))
+ using (Im.Id.Push("BlendingB"u8))
{
retB |= DrawBlending(table, dyeTable, dyePackB, rowBIdx);
}
}
DrawHeader(" Material Template"u8);
- using (var columns = ImUtf8.Columns(2, "ColorTable"u8))
+ using (var columns = Im.Columns(2, "ColorTable"u8))
{
- using var dis = ImRaii.Disabled(disabled);
- using (ImUtf8.PushId("TemplateA"u8))
+ using var dis = Im.Disabled(disabled);
+ using (Im.Id.Push("TemplateA"u8))
{
retA |= DrawTemplate(table, dyeTable, dyePackA, rowAIdx);
}
columns.Next();
- using (ImUtf8.PushId("TemplateB"u8))
+ using (Im.Id.Push("TemplateB"u8))
{
retB |= DrawTemplate(table, dyeTable, dyePackB, rowBIdx);
}
@@ -192,31 +188,31 @@ public partial class MtrlTab
if (dyeTable != null)
{
DrawHeader(" Dye Properties"u8);
- using var columns = ImUtf8.Columns(2, "ColorTable"u8);
- using var dis = ImRaii.Disabled(disabled);
- using (ImUtf8.PushId("DyeA"u8))
+ using var columns = Im.Columns(2, "ColorTable"u8);
+ using var dis = Im.Disabled(disabled);
+ using (Im.Id.Push("DyeA"u8))
{
retA |= DrawDye(dyeTable, dyePackA, rowAIdx);
}
columns.Next();
- using (ImUtf8.PushId("DyeB"u8))
+ using (Im.Id.Push("DyeB"u8))
{
retB |= DrawDye(dyeTable, dyePackB, rowBIdx);
}
}
DrawHeader(" Further Content"u8);
- using (var columns = ImUtf8.Columns(2, "ColorTable"u8))
+ using (var columns = Im.Columns(2, "ColorTable"u8))
{
- using var dis = ImRaii.Disabled(disabled);
- using (ImUtf8.PushId("FurtherA"u8))
+ using var dis = Im.Disabled(disabled);
+ using (Im.Id.Push("FurtherA"u8))
{
retA |= DrawFurther(table, dyeTable, dyePackA, rowAIdx);
}
columns.Next();
- using (ImUtf8.PushId("FurtherB"u8))
+ using (Im.Id.Push("FurtherB"u8))
{
retB |= DrawFurther(table, dyeTable, dyePackB, rowBIdx);
}
@@ -235,7 +231,7 @@ public partial class MtrlTab
{
var headerColor = Im.Style[ImGuiColor.Header];
using var _ = ImGuiColor.HeaderHovered.Push(headerColor).Push(ImGuiColor.HeaderActive, headerColor);
- ImUtf8.CollapsingHeader(label, ImGuiTreeNodeFlags.Leaf);
+ Im.Tree.Header(label, TreeNodeFlags.Leaf);
}
private bool DrawRowHeader(int rowIdx, bool disabled)
@@ -276,7 +272,7 @@ public partial class MtrlTab
ret |= CtColorPicker("Specular Color"u8, default, row.SpecularColor,
c => table[rowIdx].SpecularColor = c);
- if (dyeTable != null)
+ if (dyeTable is not null)
{
Im.Line.Same(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeSpecularColor"u8, "Apply Specular Color on Dye"u8, dye.SpecularColor,
@@ -287,7 +283,7 @@ public partial class MtrlTab
ret |= CtColorPicker("Emissive Color"u8, default, row.EmissiveColor,
c => table[rowIdx].EmissiveColor = c);
- if (dyeTable != null)
+ if (dyeTable is not null)
{
Im.Line.Same(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeEmissiveColor"u8, "Apply Emissive Color on Dye"u8, dye.EmissiveColor,
@@ -308,7 +304,7 @@ public partial class MtrlTab
- Im.Style.FrameHeight
- scalarSize;
- var isRowB = (rowIdx & 1) != 0;
+ var isRowB = (rowIdx & 1) is not 0;
var ret = false;
ref var row = ref table[rowIdx];
@@ -317,7 +313,7 @@ public partial class MtrlTab
Im.Item.SetNextWidth(scalarSize);
ret |= CtDragHalf(isRowB ? "Field #19"u8 : "Anisotropy Degree"u8, default, row.Anisotropy, "%.2f"u8, 0.0f, HalfMaxValue, 0.1f,
v => table[rowIdx].Anisotropy = v);
- if (dyeTable != null)
+ if (dyeTable is not null)
{
Im.Line.Same(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeAnisotropy"u8, isRowB ? "Apply Field #19 on Dye"u8 : "Apply Anisotropy Degree on Dye"u8,
@@ -347,37 +343,37 @@ public partial class MtrlTab
ret |= CtDragScalar("Shader ID"u8, default, row.ShaderId, "%d"u8, (ushort)0, (ushort)255, 0.25f,
v => table[rowIdx].ShaderId = v);
- ImGui.Dummy(new Vector2(Im.Style.TextHeight / 2));
+ Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
Im.Item.SetNextWidth(scalarSize + itemSpacing + 64.0f);
ret |= CtSphereMapIndexPicker("###SphereMapIndex"u8, default, row.SphereMapIndex, false,
v => table[rowIdx].SphereMapIndex = v);
Im.Line.SameInner();
- ImUtf8.Text("Sphere Map"u8);
- if (dyeTable != null)
+ Im.Text("Sphere Map"u8);
+ if (dyeTable is not null)
{
- var textRectMin = ImGui.GetItemRectMin();
- var textRectMax = ImGui.GetItemRectMax();
+ var textRectMin = Im.Item.UpperLeftCorner;
+ var textRectMax = Im.Item.LowerRightCorner;
Im.Line.Same(dyeOffset);
- var cursor = ImGui.GetCursorScreenPos();
- ImGui.SetCursorScreenPos(cursor with { Y = float.Lerp(textRectMin.Y, textRectMax.Y, 0.5f) - Im.Style.FrameHeight * 0.5f });
+ var cursor = Im.Cursor.ScreenPosition;
+ Im.Cursor.ScreenPosition = cursor with { Y = float.Lerp(textRectMin.Y, textRectMax.Y, 0.5f) - Im.Style.FrameHeight * 0.5f };
ret |= CtApplyStainCheckbox("##dyeSphereMapIndex"u8, "Apply Sphere Map on Dye"u8, dye.SphereMapIndex,
b => dyeTable[rowIdx].SphereMapIndex = b);
Im.Line.SameInner();
- ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursor.Y });
+ Im.Cursor.ScreenPosition = Im.Cursor.ScreenPosition with { Y = cursor.Y };
Im.Item.SetNextWidth(scalarSize + itemSpacing + 64.0f);
- using var dis = ImRaii.Disabled();
+ using var dis = Im.Disabled();
CtSphereMapIndexPicker("###SphereMapIndexDye"u8, "Dye Preview for Sphere Map"u8, dyePack?.SphereMapIndex ?? ushort.MaxValue, false,
Nop);
}
- ImGui.Dummy(new Vector2(64.0f, 0.0f));
+ Im.Dummy(new Vector2(64.0f, 0.0f));
Im.Line.Same();
Im.Item.SetNextWidth(scalarSize);
ret |= CtDragScalar("Sphere Map Intensity"u8, default, (float)row.SphereMapMask * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f,
HalfMaxValue * 100.0f, 1.0f,
v => table[rowIdx].SphereMapMask = (Half)(v * 0.01f));
- if (dyeTable != null)
+ if (dyeTable is not null)
{
Im.Line.Same(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeSphereMapMask"u8, "Apply Sphere Map Intensity on Dye"u8, dye.SphereMapMask,
@@ -387,25 +383,24 @@ public partial class MtrlTab
CtDragScalar("##dyeSphereMapMask"u8, "Dye Preview for Sphere Map Intensity"u8, (float?)dyePack?.SphereMapMask * 100.0f, "%.0f%%"u8);
}
- ImGui.Dummy(new Vector2(Im.Style.TextHeight / 2));
+ Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
var leftLineHeight = 64.0f + Im.Style.FramePadding.Y * 2.0f;
var rightLineHeight = 3.0f * Im.Style.FrameHeight + 2.0f * Im.Style.ItemSpacing.Y;
var lineHeight = Math.Max(leftLineHeight, rightLineHeight);
- var cursorPos = ImGui.GetCursorScreenPos();
- ImGui.SetCursorScreenPos(cursorPos + new Vector2(0.0f, (lineHeight - leftLineHeight) * 0.5f));
+ var cursorPos = Im.Cursor.ScreenPosition;
+ Im.Cursor.ScreenPosition = cursorPos + new Vector2(0.0f, (lineHeight - leftLineHeight) * 0.5f);
Im.Item.SetNextWidth(scalarSize + (itemSpacing + 64.0f) * 2.0f);
ret |= CtTileIndexPicker("###TileIndex"u8, default, row.TileIndex, false,
v => table[rowIdx].TileIndex = v);
Im.Line.SameInner();
- ImUtf8.Text("Tile"u8);
+ Im.Text("Tile"u8);
Im.Line.Same(subColWidth);
- ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() with { Y = cursorPos.Y + (lineHeight - rightLineHeight) * 0.5f });
- using (ImUtf8.Child("###TileProperties"u8,
- new Vector2(Im.ContentRegion.Available.X, float.Lerp(rightLineHeight, lineHeight, 0.5f))))
+ Im.Cursor.ScreenPosition = Im.Cursor.ScreenPosition with { Y = cursorPos.Y + (lineHeight - rightLineHeight) * 0.5f };
+ using (Im.Child.Begin("###TileProperties"u8, Im.ContentRegion.Available with { Y = float.Lerp(rightLineHeight, lineHeight, 0.5f) }))
{
- ImGui.Dummy(new Vector2(scalarSize, 0.0f));
+ Im.Dummy(new Vector2(scalarSize, 0.0f));
Im.Line.SameInner();
Im.Item.SetNextWidth(scalarSize);
ret |= CtDragScalar("Tile Opacity"u8, default, (float)row.TileAlpha * 100.0f, "%.0f%%"u8, 0.0f, HalfMaxValue * 100.0f, 1.0f,
@@ -414,9 +409,8 @@ public partial class MtrlTab
ret |= CtTileTransformMatrix(row.TileTransform, scalarSize, true,
m => table[rowIdx].TileTransform = m);
Im.Line.SameInner();
- ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos()
- - new Vector2(0.0f, (Im.Style.FrameHeight + Im.Style.ItemSpacing.Y) * 0.5f));
- ImUtf8.Text("Tile Transform"u8);
+ Im.Cursor.ScreenPosition -= new Vector2(0.0f, (Im.Style.FrameHeight + Im.Style.ItemSpacing.Y) * 0.5f);
+ Im.Text("Tile Transform"u8);
}
return ret;
@@ -440,7 +434,7 @@ public partial class MtrlTab
ret |= CtDragScalar("Roughness"u8, default, (float)row.Roughness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f,
1.0f,
v => table[rowIdx].Roughness = (Half)(v * 0.01f));
- if (dyeTable != null)
+ if (dyeTable is not null)
{
Im.Line.Same(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeRoughness"u8, "Apply Roughness on Dye"u8, dye.Roughness,
@@ -455,7 +449,7 @@ public partial class MtrlTab
ret |= CtDragScalar("Metalness"u8, default, (float)row.Metalness * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f,
1.0f,
v => table[rowIdx].Metalness = (Half)(v * 0.01f));
- if (dyeTable != null)
+ if (dyeTable is not null)
{
Im.Line.Same(subColWidth + dyeOffset);
ret |= CtApplyStainCheckbox("##dyeMetalness"u8, "Apply Metalness on Dye"u8, dye.Metalness,
@@ -485,7 +479,7 @@ public partial class MtrlTab
Im.Item.SetNextWidth(scalarSize);
ret |= CtDragScalar("Sheen"u8, default, (float)row.SheenRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f, HalfMaxValue * 100.0f, 1.0f,
v => table[rowIdx].SheenRate = (Half)(v * 0.01f));
- if (dyeTable != null)
+ if (dyeTable is not null)
{
Im.Line.Same(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeSheenRate"u8, "Apply Sheen on Dye"u8, dye.SheenRate,
@@ -500,7 +494,7 @@ public partial class MtrlTab
ret |= CtDragScalar("Sheen Tint"u8, default, (float)row.SheenTintRate * 100.0f, "%.0f%%"u8, HalfMinValue * 100.0f,
HalfMaxValue * 100.0f, 1.0f,
v => table[rowIdx].SheenTintRate = (Half)(v * 0.01f));
- if (dyeTable != null)
+ if (dyeTable is not null)
{
Im.Line.Same(subColWidth + dyeOffset);
ret |= CtApplyStainCheckbox("##dyeSheenTintRate"u8, "Apply Sheen Tint on Dye"u8, dye.SheenTintRate,
@@ -514,7 +508,7 @@ public partial class MtrlTab
ret |= CtDragScalar("Sheen Roughness"u8, default, 100.0f / (float)row.SheenAperture, "%.0f%%"u8, 100.0f / HalfMaxValue,
100.0f / HalfEpsilon, 1.0f,
v => table[rowIdx].SheenAperture = (Half)(100.0f / v));
- if (dyeTable != null)
+ if (dyeTable is not null)
{
Im.Line.Same(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeSheenRoughness"u8, "Apply Sheen Roughness on Dye"u8, dye.SheenAperture,
@@ -545,7 +539,7 @@ public partial class MtrlTab
Im.Item.SetNextWidth(scalarSize);
ret |= CtDragHalf("Field #11"u8, default, row.Scalar11, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f,
v => table[rowIdx].Scalar11 = v);
- if (dyeTable != null)
+ if (dyeTable is not null)
{
Im.Line.Same(dyeOffset);
ret |= CtApplyStainCheckbox("##dyeScalar11"u8, "Apply Field #11 on Dye"u8, dye.Scalar3,
@@ -555,7 +549,7 @@ public partial class MtrlTab
CtDragHalf("##dyePreviewScalar11"u8, "Dye Preview for Field #11"u8, dyePack?.Scalar3, "%.2f"u8);
}
- ImGui.Dummy(new Vector2(Im.Style.TextHeight / 2));
+ Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
Im.Item.SetNextWidth(scalarSize);
ret |= CtDragHalf("Field #3"u8, default, row.Scalar3, "%.2f"u8, HalfMinValue, HalfMaxValue, 0.1f,
@@ -594,7 +588,7 @@ public partial class MtrlTab
private bool DrawDye(ColorDyeTable dyeTable, DyePack? dyePack, int rowIdx)
{
var scalarSize = ColorTableScalarSize * Im.Style.GlobalScale;
- var applyButtonWidth = ImUtf8.CalcTextSize("Apply Preview Dye"u8).X + Im.Style.FramePadding.X * 2.0f;
+ var applyButtonWidth = Im.Font.CalculateSize("Apply Preview Dye"u8).X + Im.Style.FramePadding.X * 2.0f;
var subColWidth = CalculateSubColumnWidth(2, applyButtonWidth);
var ret = false;
@@ -614,10 +608,10 @@ public partial class MtrlTab
}
Im.Line.SameInner();
- ImUtf8.Text("Dye Template"u8);
+ Im.Text("Dye Template"u8);
Im.Line.Same(Im.ContentRegion.Available.X - applyButtonWidth + Im.Style.ItemSpacing.X);
- using var dis = ImRaii.Disabled(!dyePack.HasValue);
- if (ImUtf8.Button("Apply Preview Dye"u8))
+ using var dis = Im.Disabled(!dyePack.HasValue);
+ if (Im.Button("Apply Preview Dye"u8))
ret |= Mtrl.ApplyDyeToRow(_stainService.GudStmFile, [
_stainService.StainCombo1.CurrentSelection.Key,
_stainService.StainCombo2.CurrentSelection.Key,
@@ -626,13 +620,13 @@ public partial class MtrlTab
return ret;
}
- private static void CenteredTextInRest(string text)
- => AlignedTextInRest(text, 0.5f);
+ private static void CenteredTextInRest(Utf8StringHandler text)
+ => AlignedTextInRest(ref text, 0.5f);
- private static void AlignedTextInRest(string text, float alignment)
+ private static void AlignedTextInRest(ref Utf8StringHandler text, float alignment)
{
- var width = ImGui.CalcTextSize(text).X;
- ImGui.SetCursorScreenPos(ImGui.GetCursorScreenPos() + new Vector2((Im.ContentRegion.Available.X - width) * alignment, 0.0f));
+ var width = Im.Font.CalculateSize(text).X;
+ Im.Cursor.ScreenPosition += new Vector2((Im.ContentRegion.Available.X - width) * alignment, 0.0f);
Im.Text(text);
}
diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs
index e4c7a5db..600ce44f 100644
--- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs
+++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs
@@ -1,6 +1,5 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
-using Dalamud.Interface.Utility;
using ImSharp;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.GameData.Files;
@@ -26,8 +25,8 @@ public partial class MtrlTab
if (!_shpkLoading && !TextureIds.Contains(ShpkFile.TableSamplerId) || Mtrl.Table == null)
return false;
- ImGui.Dummy(new Vector2(Im.Style.TextHeight / 2));
- if (!ImUtf8.CollapsingHeader("Color Table"u8, ImGuiTreeNodeFlags.DefaultOpen))
+ Im.Dummy(new Vector2(Im.Style.TextHeight / 2));
+ if (!Im.Tree.Header("Color Table"u8, TreeNodeFlags.DefaultOpen))
return false;
ColorTableCopyAllClipboardButton();
@@ -91,7 +90,7 @@ public partial class MtrlTab
{
var (dyeId1, (name1, dyeColor1, gloss1)) = _stainService.StainCombo1.CurrentSelection;
var (dyeId2, (name2, dyeColor2, gloss2)) = _stainService.StainCombo2.CurrentSelection;
- var tt = dyeId1 == 0 && dyeId2 == 0
+ var tt = dyeId1 is 0 && dyeId2 is 0
? "Select a preview dye first."u8
: "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."u8;
if (ImUtf8.ButtonEx("Apply Preview Dye"u8, tt, disabled: disabled || dyeId1 == 0 && dyeId2 == 0))
@@ -303,31 +302,31 @@ public partial class MtrlTab
CancelColorTableHighlight();
}
- private static void CtBlendRect(Vector2 rcMin, Vector2 rcMax, uint topColor, uint bottomColor)
+ private static void CtBlendRect(Vector2 rcMin, Vector2 rcMax, Rgba32 topColor, Rgba32 bottomColor)
{
var frameRounding = Im.Style.FrameRounding;
var frameThickness = Im.Style.FrameBorderThickness;
var borderColor = ImGuiColor.Border.Get();
- var drawList = ImGui.GetWindowDrawList();
+ var drawList = Im.Window.DrawList.Shape;
if (topColor == bottomColor)
{
- drawList.AddRectFilled(rcMin, rcMax, topColor, frameRounding, ImDrawFlags.RoundCornersDefault);
+ drawList.RectangleFilled(rcMin, rcMax, topColor, frameRounding);
}
else
{
- drawList.AddRectFilled(
+ drawList.RectangleFilled(
rcMin, rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) },
- topColor, frameRounding, ImDrawFlags.RoundCornersTopLeft | ImDrawFlags.RoundCornersTopRight);
- drawList.AddRectFilledMultiColor(
+ topColor, frameRounding, ImDrawFlagsRectangle.RoundCornersTop);
+ drawList.RectangleMulticolor(
rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 1.0f / 3) },
rcMax with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) },
topColor, topColor, bottomColor, bottomColor);
- drawList.AddRectFilled(
+ drawList.RectangleFilled(
rcMin with { Y = float.Lerp(rcMin.Y, rcMax.Y, 2.0f / 3) }, rcMax,
- bottomColor, frameRounding, ImDrawFlags.RoundCornersBottomLeft | ImDrawFlags.RoundCornersBottomRight);
+ bottomColor, frameRounding, ImDrawFlagsRectangle.RoundCornersTop);
}
- drawList.AddRect(rcMin, rcMax, borderColor.Color, frameRounding, ImDrawFlags.RoundCornersDefault, frameThickness);
+ drawList.Rectangle(rcMin, rcMax, borderColor.Color, frameRounding, default, frameThickness);
}
private static bool CtColorPicker(ReadOnlySpan label, ReadOnlySpan description, HalfColor current, Action setter,
diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs
index 479ebd1c..28440146 100644
--- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs
+++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs
@@ -77,7 +77,7 @@ public sealed partial class MtrlTab : IWritable, IDisposable
if (!ImUtf8.ButtonEx("Update MTRL Version to Dawntrail"u8,
"Try using this if the material can not be loaded or should use legacy shaders.\n\nThis is not revertible."u8,
- new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg))
+ new Vector2(-0.1f, 0), false, 0, new Rgba32(Colors.PressEnterWarningBg).Color))
return false;
Mtrl.MigrateToDawntrail();
diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs
index 426188b1..88a57ae2 100644
--- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs
+++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTabFactory.cs
@@ -4,6 +4,7 @@ using Penumbra.GameData.Interop;
using Penumbra.Interop.Hooks.Objects;
using Penumbra.Interop.ResourceTree;
using Penumbra.Services;
+using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow.Materials;
diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs
index 3f0f80e1..8d99af7f 100644
--- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs
+++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs
@@ -91,7 +91,7 @@ public partial class ModEditWindow
if (!ImUtf8.ButtonEx("Update MDL Version from V5 to V6"u8,
"Try using this if the bone weights of a pre-Dawntrail model seem wrong.\n\nThis is not revertible."u8,
- new Vector2(-0.1f, 0), false, 0, Colors.PressEnterWarningBg))
+ new Vector2(-0.1f, 0), false, 0, new Rgba32(Colors.PressEnterWarningBg).Color))
return;
tab.Mdl.ConvertV5ToV6();
@@ -198,7 +198,7 @@ public partial class ModEditWindow
var size = new Vector2(Im.ContentRegion.Available.X, 0);
using var frame = ImRaii.FramedGroup("Exceptions", size, headerPreIcon: FontAwesomeIcon.TimesCircle,
- borderColor: Colors.RegexWarningBorder);
+ borderColor: new Rgba32(Colors.RegexWarningBorder).Color);
var spaceAvail = Im.ContentRegion.Available.X - Im.Style.ItemSpacing.X - 100;
foreach (var (index, exception) in tab.IoExceptions.Index())
@@ -304,7 +304,7 @@ public partial class ModEditWindow
private void DrawDocumentationLink(string address)
{
- const string text = "Documentation →";
+ var text = "Documentation →"u8;
var framePadding = Im.Style.FramePadding;
var width = ImGui.CalcTextSize(text).X + framePadding.X * 2;
diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs
index 975338c3..42e0462c 100644
--- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs
+++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs
@@ -8,6 +8,7 @@ using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Mods.SubMods;
using Penumbra.String.Classes;
+using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs
index e81442bf..d5f3a664 100644
--- a/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs
+++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.ShpkTab.cs
@@ -6,6 +6,7 @@ using Penumbra.GameData.Files.ShaderStructs;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
using Penumbra.UI.AdvancedWindow.Materials;
+using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs
index 10533fba..69a7613e 100644
--- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs
+++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs
@@ -56,8 +56,8 @@ public partial class ModEditWindow
{
TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input"u8, "Import Image..."u8,
"Can import game paths as well as your own files."u8, Mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath);
- if (_textureSelectCombo.Draw("##combo",
- "Select the textures included in this mod on your drive or the ones they replace from the game files.", tex.Path,
+ if (_textureSelectCombo.Draw("##combo"u8,
+ "Select the textures included in this mod on your drive or the ones they replace from the game files."u8, tex.Path,
Mod.ModPath.FullName.Length + 1, out var newPath)
&& newPath != tex.Path)
tex.Load(_textures, newPath);
@@ -199,7 +199,7 @@ public partial class ModEditWindow
case TaskStatus.WaitingForActivation:
case TaskStatus.WaitingToRun:
case TaskStatus.Running:
- ImGuiUtil.DrawTextButton("Computing...", -Vector2.UnitX, Colors.PressEnterWarningBg);
+ ImGuiUtil.DrawTextButton("Computing...", -Vector2.UnitX, new Rgba32(Colors.PressEnterWarningBg).Color);
break;
case TaskStatus.Canceled:
case TaskStatus.Faulted:
diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs
index 8eeedfba..05246aba 100644
--- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs
+++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs
@@ -407,9 +407,9 @@ public partial class ModEditWindow : IndexedWindow, IDisposable
var width = ImGui.CalcTextSize("NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN ").X;
table.SetupColumn("file"u8, TableColumnFlags.WidthStretch);
- table.SetupColumn("size", TableColumnFlags.WidthFixed, ImGui.CalcTextSize("NNN.NNN "u8).X);
+ table.SetupColumn("size"u8, TableColumnFlags.WidthFixed, ImGui.CalcTextSize("NNN.NNN "u8).X);
table.SetupColumn("hash"u8, TableColumnFlags.WidthFixed,
- ImGui.GetWindowWidth() > 2 * width ? width : ImGui.CalcTextSize("NNNNNNNN... ").X);
+ ImGui.GetWindowWidth() > 2 * width ? width : Im.Font.CalculateSize("NNNNNNNN... "u8).X);
foreach (var (set, size, hash) in _editor.Duplicates.Duplicates.Where(s => s.Paths.Length > 1))
{
ImGui.TableNextColumn();
diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindowFactory.cs b/Penumbra/UI/AdvancedWindow/ModEditWindowFactory.cs
index b281577f..3e422a33 100644
--- a/Penumbra/UI/AdvancedWindow/ModEditWindowFactory.cs
+++ b/Penumbra/UI/AdvancedWindow/ModEditWindowFactory.cs
@@ -11,6 +11,7 @@ using Penumbra.Mods.Editor;
using Penumbra.Services;
using Penumbra.UI.AdvancedWindow.Materials;
using Penumbra.UI.AdvancedWindow.Meta;
+using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs
index 31d38f6a..c9608247 100644
--- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs
+++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs
@@ -1,17 +1,12 @@
-using Dalamud.Interface.Utility;
-using Dalamud.Bindings.ImGui;
using ImSharp;
-using OtterGui;
-using OtterGui.Raii;
-using OtterGui.Text;
+using Luna;
using Penumbra.Mods.Editor;
-using Penumbra.Mods.Manager;
using Penumbra.Mods.SubMods;
using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
-public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Luna.IUiService
+public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : IUiService
{
private string _newModName = string.Empty;
@@ -20,25 +15,24 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu
if (modMerger.MergeFromMod == null)
return;
- using var tab = ImRaii.TabItem("Merge Mods");
+ using var tab = Im.TabBar.BeginItem("Merge Mods"u8);
if (!tab)
return;
- ImGui.Dummy(Vector2.One);
+ Im.Dummy(Vector2.Zero);
var size = 550 * Im.Style.GlobalScale;
DrawMergeInto(size);
Im.Line.Same();
DrawMergeIntoDesc();
- ImGui.Dummy(Vector2.One);
+ Im.Dummy(Vector2.Zero);
Im.Separator();
- ImGui.Dummy(Vector2.One);
+ Im.Dummy(Vector2.Zero);
DrawSplitOff(size);
Im.Line.Same();
DrawSplitOffDesc();
-
DrawError();
DrawWarnings();
}
@@ -47,13 +41,13 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu
{
using var bigGroup = Im.Group();
var minComboSize = 300 * Im.Style.GlobalScale;
- var textSize = ImUtf8.CalcTextSize($"Merge {modMerger.MergeFromMod!.Name} into ").X;
+ var textSize = Im.Font.CalculateSize($"Merge {modMerger.MergeFromMod!.Name} into ").X;
- ImGui.AlignTextToFramePadding();
+ Im.Cursor.FrameAlign();
using (Im.Group())
{
- ImUtf8.Text("Merge "u8);
+ Im.Text("Merge "u8);
Im.Line.Same(0, 0);
if (size - textSize < minComboSize)
{
@@ -66,56 +60,56 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu
}
Im.Line.Same(0, 0);
- ImUtf8.Text(" into"u8);
+ Im.Text(" into"u8);
}
Im.Line.Same();
- DrawCombo(size - ImGui.GetItemRectSize().X - Im.Style.ItemSpacing.X);
+ DrawCombo(size - Im.Item.Size.X - Im.Style.ItemSpacing.X);
using (Im.Group())
{
- using var disabled = ImRaii.Disabled(modMerger.MergeFromMod.HasOptions);
+ using var disabled = Im.Disabled(modMerger.MergeFromMod.HasOptions);
var buttonWidth = (size - Im.Style.ItemSpacing.X) / 2;
var group = modMerger.MergeToMod?.Groups.FirstOrDefault(g => g.Name == modMerger.OptionGroupName);
var color = group != null || modMerger.OptionGroupName.Length is 0 && modMerger.OptionName.Length is 0
? Colors.PressEnterWarningBg
- : Colors.DiscordColor;
+ : LunaStyle.DiscordColor;
using var style = ImStyleBorder.Frame.Push(color);
Im.Item.SetNextWidth(buttonWidth);
- ImGui.InputTextWithHint("##optionGroupInput", "Target Option Group", ref modMerger.OptionGroupName, 64);
- ImGuiUtil.HoverTooltip(
- "The name of the new or existing option group to find or create the option in. Leave both group and option name blank for the default option.\n"
- + "A red border indicates an existing option group, a blue border indicates a new one.");
+ Im.Input.Text("##optionGroupInput"u8, ref modMerger.OptionGroupName, "Target Option Group"u8);
+ Im.Tooltip.OnHover(
+ "The name of the new or existing option group to find or create the option in. Leave both group and option name blank for the default option.\n"u8
+ + "A red border indicates an existing option group, a blue border indicates a new one."u8);
Im.Line.Same();
- color = color == Colors.DiscordColor
- ? Colors.DiscordColor
+ color = color == LunaStyle.DiscordColor
+ ? LunaStyle.DiscordColor
: group == null || group.Options.Any(o => o.Name == modMerger.OptionName)
? Colors.PressEnterWarningBg
- : Colors.DiscordColor;
+ : LunaStyle.DiscordColor;
style.Push(ImGuiColor.Border, color);
Im.Item.SetNextWidth(buttonWidth);
- ImGui.InputTextWithHint("##optionInput", "Target Option Name", ref modMerger.OptionName, 64);
- ImGuiUtil.HoverTooltip(
- "The name of the new or existing option to merge this mod into. Leave both group and option name blank for the default option.\n"
- + "A red border indicates an existing option, a blue border indicates a new one.");
+ Im.Input.Text("##optionInput"u8, ref modMerger.OptionName, "Target Option Name"u8);
+ Im.Tooltip.OnHover(
+ "The name of the new or existing option to merge this mod into. Leave both group and option name blank for the default option.\n"u8
+ + "A red border indicates an existing option, a blue border indicates a new one."u8);
}
if (modMerger.MergeFromMod.HasOptions)
Im.Tooltip.OnHover("You can only specify a target option if the source mod has no true options itself."u8,
HoveredFlags.AllowWhenDisabled);
- if (ImGuiUtil.DrawDisabledButton("Merge", new Vector2(size, 0),
- modMerger.CanMerge ? string.Empty : "Please select a target mod different from the current mod.", !modMerger.CanMerge))
+ if (ImEx.Button("Merge"u8, new Vector2(size, 0),
+ modMerger.CanMerge ? StringU8.Empty : "Please select a target mod different from the current mod."u8, !modMerger.CanMerge))
modMerger.Merge();
}
private void DrawMergeIntoDesc()
{
- ImGuiUtil.TextWrapped(modMerger.MergeFromMod!.HasOptions
- ? "The currently selected mod has options.\n\nThis means, that all of those options will be merged into the target. If merging an option is not possible due to the redirections already existing in an existing option, it will revert all changes and break."
- : "The currently selected mod has no true options.\n\nThis means that you can select an existing or new option to merge all its changes into in the target mod. On failure to merge into an existing option, all changes will be reverted.");
+ Im.TextWrapped(modMerger.MergeFromMod!.HasOptions
+ ? "The currently selected mod has options.\n\nThis means, that all of those options will be merged into the target. If merging an option is not possible due to the redirections already existing in an existing option, it will revert all changes and break."u8
+ : "The currently selected mod has no true options.\n\nThis means that you can select an existing or new option to merge all its changes into in the target mod. On failure to merge into an existing option, all changes will be reverted."u8);
}
private void DrawCombo(float width)
@@ -129,37 +123,37 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu
{
using var group = Im.Group();
Im.Item.SetNextWidth(size);
- ImGui.InputTextWithHint("##newModInput", "New Mod Name...", ref _newModName, 64);
- ImGuiUtil.HoverTooltip("Choose a name for the newly created mod. This does not need to be unique.");
+ Im.Input.Text("##newModInput"u8, ref _newModName, "New Mod Name..."u8);
+ Im.Tooltip.OnHover("Choose a name for the newly created mod. This does not need to be unique."u8);
var tt = _newModName.Length == 0
- ? "Please enter a name for the newly created mod first."
+ ? "Please enter a name for the newly created mod first."u8
: modMerger.SelectedOptions.Count == 0
- ? "Please select at least one option to split off."
- : string.Empty;
- var buttonText =
- $"Split Off {modMerger.SelectedOptions.Count} Option{(modMerger.SelectedOptions.Count > 1 ? "s" : string.Empty)}###SplitOff";
- if (ImGuiUtil.DrawDisabledButton(buttonText, new Vector2(size, 0), tt, tt.Length > 0))
+ ? "Please select at least one option to split off."u8
+ : StringU8.Empty;
+ if (ImEx.Button(
+ $"Split Off {modMerger.SelectedOptions.Count} Option{(modMerger.SelectedOptions.Count > 1 ? "s" : string.Empty)}###SplitOff",
+ new Vector2(size, 0), tt, tt.Length > 0))
modMerger.SplitIntoMod(_newModName);
- ImGui.Dummy(Vector2.One);
+ Im.Dummy(Vector2.One);
var buttonSize = new Vector2((size - 2 * Im.Style.ItemSpacing.X) / 3, 0);
- if (ImGui.Button("Select All", buttonSize))
+ if (Im.Button("Select All"u8, buttonSize))
modMerger.SelectedOptions.UnionWith(modMerger.MergeFromMod!.AllDataContainers);
Im.Line.Same();
- if (ImGui.Button("Unselect All", buttonSize))
+ if (Im.Button("Unselect All"u8, buttonSize))
modMerger.SelectedOptions.Clear();
Im.Line.Same();
- if (ImGui.Button("Invert Selection", buttonSize))
+ if (Im.Button("Invert Selection"u8, buttonSize))
modMerger.SelectedOptions.SymmetricExceptWith(modMerger.MergeFromMod!.AllDataContainers);
DrawOptionTable(size);
}
- private void DrawSplitOffDesc()
+ private static void DrawSplitOffDesc()
{
- ImGuiUtil.TextWrapped("Here you can create a copy or a partial copy of the currently selected mod.\n\n"
- + "Select as many of the options you want to copy over, enter a new mod name and click Split Off.\n\n"
- + "You can right-click option groups to select or unselect all options from that specific group, and use the three buttons above the table for quick manipulation of your selection.\n\n"
- + "Only required files will be copied over to the new mod. The names of options and groups will be retained. If the Default option is not selected, the new mods default option will be empty.");
+ Im.TextWrapped("Here you can create a copy or a partial copy of the currently selected mod.\n\n"u8
+ + "Select as many of the options you want to copy over, enter a new mod name and click Split Off.\n\n"u8
+ + "You can right-click option groups to select or unselect all options from that specific group, and use the three buttons above the table for quick manipulation of your selection.\n\n"u8
+ + "Only required files will be copied over to the new mod. The names of options and groups will be retained. If the Default option is not selected, the new mods default option will be empty."u8);
}
private void DrawOptionTable(float size)
@@ -186,48 +180,47 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu
table.SetupColumn("#Files"u8, TableColumnFlags.WidthFixed, 50 * Im.Style.GlobalScale);
table.SetupColumn("#Swaps"u8, TableColumnFlags.WidthFixed, 50 * Im.Style.GlobalScale);
table.SetupColumn("#Manips"u8, TableColumnFlags.WidthFixed, 50 * Im.Style.GlobalScale);
- ImGui.TableHeadersRow();
+ table.HeaderRow();
foreach (var (idx, option) in options.Index())
{
- using var id = ImRaii.PushId(idx);
+ using var id = Im.Id.Push(idx);
var selected = modMerger.SelectedOptions.Contains(option);
- ImGui.TableNextColumn();
- if (ImGui.Checkbox("##check", ref selected))
+ table.NextColumn();
+ if (Im.Checkbox("##check"u8, ref selected))
Handle(option, selected);
if (option.Group is not { } group)
{
- ImGuiUtil.DrawTableColumn(option.GetFullName());
- ImGui.TableNextColumn();
+ table.DrawColumn(option.GetFullName());
+ table.NextColumn();
}
else
{
- ImGuiUtil.DrawTableColumn(option.GetName());
-
- ImGui.TableNextColumn();
- ImGui.Selectable(group.Name, false);
- if (ImGui.BeginPopupContextItem("##groupContext"))
+ table.DrawColumn(option.GetName());
+ table.NextColumn();
+ Im.Selectable(group.Name);
+ using var popup = Im.Popup.BeginContextItem("##groupContext"u8);
+ if (popup)
{
- if (ImGui.MenuItem("Select All"))
+ if (Im.Menu.Item("Select All"u8))
// ReSharper disable once PossibleMultipleEnumeration
foreach (var opt in group.DataContainers)
Handle(opt, true);
- if (ImGui.MenuItem("Unselect All"))
+ if (Im.Menu.Item("Unselect All"u8))
// ReSharper disable once PossibleMultipleEnumeration
foreach (var opt in group.DataContainers)
Handle(opt, false);
- ImGui.EndPopup();
}
}
- ImGui.TableNextColumn();
- ImGuiUtil.RightAlign(option.Files.Count.ToString(), 3 * Im.Style.GlobalScale);
- ImGui.TableNextColumn();
- ImGuiUtil.RightAlign(option.FileSwaps.Count.ToString(), 3 * Im.Style.GlobalScale);
- ImGui.TableNextColumn();
- ImGuiUtil.RightAlign(option.Manipulations.Count.ToString(), 3 * Im.Style.GlobalScale);
+ table.NextColumn();
+ ImEx.TextRightAligned($"{option.Files.Count}", 3 * Im.Style.GlobalScale);
+ table.NextColumn();
+ ImEx.TextRightAligned($"{option.FileSwaps.Count}", 3 * Im.Style.GlobalScale);
+ table.NextColumn();
+ ImEx.TextRightAligned($"{option.Manipulations.Count}", 3 * Im.Style.GlobalScale);
continue;
void Handle(IModDataContainer option2, bool selected2)
@@ -246,15 +239,15 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu
return;
Im.Separator();
- ImGui.Dummy(Vector2.One);
+ Im.Dummy(Vector2.One);
using var color = ImGuiColor.Text.Push(Colors.TutorialBorder);
foreach (var warning in modMerger.Warnings.SkipLast(1))
{
- ImGuiUtil.TextWrapped(warning);
+ Im.TextWrapped(warning);
Im.Separator();
}
- ImGuiUtil.TextWrapped(modMerger.Warnings[^1]);
+ Im.TextWrapped(modMerger.Warnings[^1]);
}
private void DrawError()
@@ -263,8 +256,8 @@ public class ModMergeTab(ModMerger modMerger, ModComboWithoutCurrent combo) : Lu
return;
Im.Separator();
- ImGui.Dummy(Vector2.One);
+ Im.Dummy(Vector2.One);
using var color = ImGuiColor.Text.Push(Colors.RegexWarningBorder);
- ImGuiUtil.TextWrapped(modMerger.Error.ToString());
+ Im.TextWrapped(modMerger.Error.ToString());
}
}
diff --git a/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs b/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs
index 8470e8ad..51c998ce 100644
--- a/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs
+++ b/Penumbra/UI/AdvancedWindow/OptionSelectCombo.cs
@@ -17,7 +17,7 @@ public sealed class OptionSelectCombo(ModEditor editor, ModEditWindow window)
protected override void DrawCombo(string label, string preview, string tooltip, int currentSelected, float previewWidth, float itemHeight,
ImGuiComboFlags flags)
{
- _border.PushBorder(ImStyleBorder.Frame, ColorId.FolderLine.Value());
+ _border.Push(ImStyleBorder.Frame, ColorId.FolderLine.Value());
base.DrawCombo(label, preview, tooltip, currentSelected, previewWidth, itemHeight, flags);
_border.Dispose();
}
diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs
index 6c122a41..c9764cca 100644
--- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs
+++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs
@@ -60,7 +60,7 @@ public class ResourceTreeViewer(
if (!_task.IsCompleted)
{
Im.Line.New();
- Im.Text("Calculating character list...");
+ Im.Text("Calculating character list..."u8);
}
else if (_task.Exception != null)
{
diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs
index 5b1af9dc..6e94f72f 100644
--- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs
+++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs
@@ -3,6 +3,7 @@ using Luna;
using Penumbra.GameData.Files;
using Penumbra.Interop.ResourceTree;
using Penumbra.Services;
+using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow;
diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/Classes/ChangedItemDrawer.cs
similarity index 98%
rename from Penumbra/UI/ChangedItemDrawer.cs
rename to Penumbra/UI/Classes/ChangedItemDrawer.cs
index d6256158..6ccaef5b 100644
--- a/Penumbra/UI/ChangedItemDrawer.cs
+++ b/Penumbra/UI/Classes/ChangedItemDrawer.cs
@@ -9,10 +9,9 @@ using Luna;
using Penumbra.Communication;
using Penumbra.GameData.Data;
using Penumbra.Services;
-using Penumbra.UI.Classes;
using MouseButton = Penumbra.Api.Enums.MouseButton;
-namespace Penumbra.UI;
+namespace Penumbra.UI.Classes;
public class ChangedItemDrawer : IDisposable, IUiService
{
@@ -119,7 +118,7 @@ public class ChangedItemDrawer : IDisposable, IUiService
var ret = leftClicked ? MouseButton.Left : MouseButton.None;
ret = Im.Item.RightClicked() ? MouseButton.Right : ret;
ret = Im.Item.MiddleClicked() ? MouseButton.Middle : ret;
- if (ret != MouseButton.None)
+ if (ret is not MouseButton.None)
_communicator.ChangedItemClick.Invoke(new ChangedItemClick.Arguments(ret, data));
if (!Im.Item.Hovered())
return;
@@ -139,7 +138,7 @@ public class ChangedItemDrawer : IDisposable, IUiService
public static void DrawModelData(IIdentifiedObjectData data, float height)
{
var additionalData = data.AdditionalData;
- if (additionalData.Length == 0)
+ if (additionalData.Length is 0)
return;
Im.Line.Same();
@@ -151,7 +150,7 @@ public class ChangedItemDrawer : IDisposable, IUiService
/// Draw the model information, right-justified.
public static void DrawModelData(ReadOnlySpan text, float height)
{
- if (text.Length == 0)
+ if (text.Length is 0)
return;
Im.Line.Same();
diff --git a/Penumbra/UI/ChangedItemIconFlag.cs b/Penumbra/UI/Classes/ChangedItemIconFlag.cs
similarity index 97%
rename from Penumbra/UI/ChangedItemIconFlag.cs
rename to Penumbra/UI/Classes/ChangedItemIconFlag.cs
index adb901b7..6cc0842f 100644
--- a/Penumbra/UI/ChangedItemIconFlag.cs
+++ b/Penumbra/UI/Classes/ChangedItemIconFlag.cs
@@ -1,7 +1,7 @@
using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
-namespace Penumbra.UI;
+namespace Penumbra.UI.Classes;
[Flags]
public enum ChangedItemIconFlag : uint
diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs
index a829d9e5..206352c2 100644
--- a/Penumbra/UI/Classes/CollectionSelectHeader.cs
+++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs
@@ -19,7 +19,7 @@ public class CollectionSelectHeader(
CollectionResolver resolver,
Configuration config,
CollectionCombo combo)
- : IUiService
+ : IHeader
{
private readonly ActiveCollections _activeCollections = collectionManager.Active;
private static readonly AwesomeIcon Icon = FontAwesomeIcon.Stopwatch;
@@ -161,4 +161,30 @@ public class CollectionSelectHeader(
_activeCollections.SetCollection(collection!, CollectionType.Current);
Im.Line.Same();
}
+
+ public bool Collapsed
+ => false;
+
+ public void Draw(Vector2 size)
+ {
+ using var style = ImStyleDouble.ItemSpacing.Push(Vector2.Zero);
+ DrawTemporaryCheckbox();
+ Im.Line.Same();
+ var comboWidth = (size.X - Im.Style.FrameHeight) / 4f;
+ var buttonSize = new Vector2(comboWidth * 3f / 4f, 0f);
+ using (var _ = Im.Group())
+ {
+ DrawCollectionButton(buttonSize, GetDefaultCollectionInfo(), 1);
+ DrawCollectionButton(buttonSize, GetInterfaceCollectionInfo(), 2);
+ DrawCollectionButton(buttonSize, GetPlayerCollectionInfo(), 3);
+ DrawCollectionButton(buttonSize, GetInheritedCollectionInfo(), 4);
+
+ combo.Draw("##collectionSelector"u8, comboWidth, ColorId.SelectedCollection.Value());
+ }
+
+ tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors);
+
+ if (!_activeCollections.CurrentCollectionInUse)
+ ImEx.TextFramed("The currently selected collection is not used in any way."u8, -Vector2.UnitX, Colors.PressEnterWarningBg);
+ }
}
diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs
index 16b3c4c0..9eadb706 100644
--- a/Penumbra/UI/Classes/Colors.cs
+++ b/Penumbra/UI/Classes/Colors.cs
@@ -1,5 +1,4 @@
using ImSharp;
-using OtterGui.Custom;
namespace Penumbra.UI.Classes;
@@ -41,17 +40,13 @@ public enum ColorId : short
public static class Colors
{
// These are written as 0xAABBGGRR.
- public const uint PressEnterWarningBg = 0xFF202080;
- public const uint RegexWarningBorder = 0xFF0000B0;
- public const uint MetaInfoText = 0xAAFFFFFF;
- public const uint RedTableBgTint = 0x40000080;
- public const uint DiscordColor = CustomGui.DiscordColor;
- public const uint FilterActive = 0x807070FF;
- public const uint TutorialMarker = 0xFF20FFFF;
- public const uint TutorialBorder = 0xD00000FF;
- public const uint ReniColorButton = CustomGui.ReniColorButton;
- public const uint ReniColorHovered = CustomGui.ReniColorHovered;
- public const uint ReniColorActive = CustomGui.ReniColorActive;
+ public static readonly Vector4 PressEnterWarningBg = new(0.5f, 0.125f, 0.125f, 1);
+ public static readonly Vector4 RegexWarningBorder = new(0.7f, 0, 0, 1);
+ public static readonly Vector4 MetaInfoText = new(1, 1, 1, 2f / 3);
+ public const uint RedTableBgTint = 0x40000080;
+ public const uint FilterActive = 0x807070FF;
+ public const uint TutorialMarker = 0xFF20FFFF;
+ public const uint TutorialBorder = 0xD00000FF;
public static (uint DefaultColor, string Name, string Description) Data(this ColorId color)
=> color switch
diff --git a/Penumbra/UI/Classes/Combos.cs b/Penumbra/UI/Classes/Combos.cs
index 326534dc..f1cb292e 100644
--- a/Penumbra/UI/Classes/Combos.cs
+++ b/Penumbra/UI/Classes/Combos.cs
@@ -9,10 +9,10 @@ public static class Combos
{
// Different combos to use with enums.
public static bool Race(string label, ModelRace current, out ModelRace race, float unscaledWidth = 100)
- => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out race, RaceEnumExtensions.ToName, 1);
+ => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out race, ModelRaceExtensions.ToName, 1);
public static bool Gender(string label, Gender current, out Gender gender, float unscaledWidth = 120)
- => ImGuiUtil.GenericEnumCombo(label, unscaledWidth, current, out gender, RaceEnumExtensions.ToName, 1);
+ => ImGuiUtil.GenericEnumCombo(label, unscaledWidth, current, out gender, GenderExtensions.ToName, 1);
public static bool EqdpEquipSlot(string label, EquipSlot current, out EquipSlot slot, float unscaledWidth = 100)
=> ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out slot, EquipSlotExtensions.EqdpSlots,
@@ -27,7 +27,7 @@ public static class Combos
EquipSlotExtensions.ToName);
public static bool SubRace(string label, SubRace current, out SubRace subRace, float unscaledWidth = 150)
- => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out subRace, RaceEnumExtensions.ToName, 1);
+ => ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out subRace, SubRaceExtensions.ToName, 1);
public static bool RspAttribute(string label, RspAttribute current, out RspAttribute attribute, float unscaledWidth = 200)
=> ImGuiUtil.GenericEnumCombo(label, unscaledWidth * Im.Style.GlobalScale, current, out attribute,
diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/Classes/FileDialogService.cs
similarity index 99%
rename from Penumbra/UI/FileDialogService.cs
rename to Penumbra/UI/Classes/FileDialogService.cs
index 00fcc189..0a6cb6d9 100644
--- a/Penumbra/UI/FileDialogService.cs
+++ b/Penumbra/UI/Classes/FileDialogService.cs
@@ -6,7 +6,7 @@ using Luna;
using Penumbra.Communication;
using Penumbra.Services;
-namespace Penumbra.UI;
+namespace Penumbra.UI.Classes;
public class FileDialogService : IDisposable, IUiService
{
diff --git a/Penumbra/UI/IncognitoService.cs b/Penumbra/UI/Classes/IncognitoService.cs
similarity index 95%
rename from Penumbra/UI/IncognitoService.cs
rename to Penumbra/UI/Classes/IncognitoService.cs
index 9d930790..e6bdaa78 100644
--- a/Penumbra/UI/IncognitoService.cs
+++ b/Penumbra/UI/Classes/IncognitoService.cs
@@ -1,8 +1,7 @@
using ImSharp;
using Luna;
-using Penumbra.UI.Classes;
-namespace Penumbra.UI;
+namespace Penumbra.UI.Classes;
public class IncognitoService(TutorialService tutorial, Configuration config) : IUiService
{
diff --git a/Penumbra/Mods/Manager/ModCombo.cs b/Penumbra/UI/Classes/ModCombo.cs
similarity index 89%
rename from Penumbra/Mods/Manager/ModCombo.cs
rename to Penumbra/UI/Classes/ModCombo.cs
index 4cc57aaf..6937663b 100644
--- a/Penumbra/Mods/Manager/ModCombo.cs
+++ b/Penumbra/UI/Classes/ModCombo.cs
@@ -1,8 +1,10 @@
using ImSharp;
using Luna;
+using Penumbra.Mods;
using Penumbra.Mods.Editor;
+using Penumbra.Mods.Manager;
-namespace Penumbra.Mods.Manager;
+namespace Penumbra.UI.Classes;
public class ModCombo(ModStorage modStorage) : SimpleFilterCombo(SimpleFilterType.Regex), IUiService
{
diff --git a/Penumbra/UI/Classes/StainCombo.cs b/Penumbra/UI/Classes/StainCombo.cs
new file mode 100644
index 00000000..8fbcda0c
--- /dev/null
+++ b/Penumbra/UI/Classes/StainCombo.cs
@@ -0,0 +1,118 @@
+using Dalamud.Bindings.ImGui;
+using Dalamud.Interface.Utility;
+using OtterGui;
+using OtterGui.Extensions;
+using OtterGui.Log;
+using OtterGui.Raii;
+using OtterGui.Widgets;
+
+namespace Penumbra.UI.Classes;
+
+public class FilterComboColors : FilterComboCache>
+{
+ private readonly float _comboWidth;
+ private readonly ImRaii.Color _color = new();
+ private Vector2 _buttonSize;
+ private uint _currentColor;
+ private bool _currentGloss;
+
+ protected override int UpdateCurrentSelected(int currentSelected)
+ {
+ if (CurrentSelection.Value.Color != _currentColor)
+ {
+ CurrentSelectionIdx = Items.IndexOf(c => c.Value.Color == _currentColor);
+ CurrentSelection = CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : default;
+ return base.UpdateCurrentSelected(CurrentSelectionIdx);
+ }
+
+ return currentSelected;
+ }
+
+ public FilterComboColors(float comboWidth, MouseWheelType allowMouseWheel,
+ Func>> colors,
+ Logger log)
+ : base(colors, allowMouseWheel, log)
+ {
+ _comboWidth = comboWidth;
+ SearchByParts = true;
+ }
+
+ protected override float GetFilterWidth()
+ {
+ // Hack to not color the filter frame.
+ _color.Pop();
+ return _buttonSize.X + ImGui.GetStyle().ScrollbarSize;
+ }
+
+ protected override void DrawList(float width, float itemHeight)
+ {
+ using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
+ .Push(ImGuiStyleVar.WindowPadding, Vector2.Zero)
+ .Push(ImGuiStyleVar.FrameRounding, 0);
+ _buttonSize = new Vector2(_comboWidth * ImGuiHelpers.GlobalScale, 0);
+ if (ImGui.GetScrollMaxY() > 0)
+ _buttonSize.X += ImGui.GetStyle().ScrollbarSize;
+ base.DrawList(width, itemHeight);
+ }
+
+ protected override string ToString(KeyValuePair obj)
+ => obj.Value.Name;
+
+ protected override bool DrawSelectable(int globalIdx, bool selected)
+ {
+ var (_, (name, color, gloss)) = Items[globalIdx];
+ // Push the stain color to type and if it is too bright, turn the text color black.
+ var contrastColor = ImGuiUtil.ContrastColorBw(color);
+ using var colors = ImRaii.PushColor(ImGuiCol.Button, color, color != 0)
+ .Push(ImGuiCol.Text, contrastColor);
+ var ret = ImGui.Button(name, _buttonSize);
+ if (selected)
+ {
+ ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), 0xFF2020D0, 0, ImDrawFlags.None,
+ ImGuiHelpers.GlobalScale);
+ ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin() + new Vector2(ImGuiHelpers.GlobalScale),
+ ImGui.GetItemRectMax() - new Vector2(ImGuiHelpers.GlobalScale), contrastColor, 0, ImDrawFlags.None, ImGuiHelpers.GlobalScale);
+ }
+
+ if (gloss)
+ ImGui.GetWindowDrawList().AddRectFilledMultiColor(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), 0x50FFFFFF, 0x50000000,
+ 0x50FFFFFF, 0x50000000);
+
+ return ret;
+ }
+
+ public virtual bool Draw(string label, uint color, string name, bool found, bool gloss, float previewWidth,
+ MouseWheelType mouseWheel = MouseWheelType.Control)
+ {
+ _currentColor = color;
+ _currentGloss = gloss;
+ var preview = found && ImGui.CalcTextSize(name).X <= previewWidth ? name : string.Empty;
+
+ AllowMouseWheel = mouseWheel;
+ _color.Push(ImGuiCol.FrameBg, color, found && color != 0)
+ .Push(ImGuiCol.Text, ImGuiUtil.ContrastColorBw(color), preview.Length > 0);
+ var change = Draw(label, preview, found ? name : string.Empty, previewWidth, ImGui.GetFrameHeight(), ImGuiComboFlags.NoArrowButton);
+ return change;
+ }
+
+ protected override void PostCombo(float previewWidth)
+ {
+ _color.Dispose();
+ if (_currentGloss)
+ {
+ var min = ImGui.GetItemRectMin();
+ ImGui.GetWindowDrawList().AddRectFilledMultiColor(min, new Vector2(min.X + previewWidth, ImGui.GetItemRectMax().Y), 0x50FFFFFF,
+ 0x50000000, 0x50FFFFFF, 0x50000000);
+ }
+ }
+
+ protected override void OnMouseWheel(string preview, ref int index, int steps)
+ {
+ UpdateCurrentSelected(0);
+ base.OnMouseWheel(preview, ref index, steps);
+ }
+
+ public bool Draw(string label, uint color, string name, bool found, bool gloss,
+ MouseWheelType mouseWheel = MouseWheelType.Control)
+ => Draw(label, color, name, found, gloss, ImGui.GetFrameHeight(), mouseWheel);
+}
diff --git a/Penumbra/UI/Classes/TutorialService.cs b/Penumbra/UI/Classes/TutorialService.cs
new file mode 100644
index 00000000..7a6c6f6c
--- /dev/null
+++ b/Penumbra/UI/Classes/TutorialService.cs
@@ -0,0 +1,148 @@
+using ImSharp;
+
+namespace Penumbra.UI.Classes;
+
+/// List of currently available tutorials.
+public enum BasicTutorialSteps
+{
+ GeneralTooltips,
+ ModDirectory,
+ EnableMods,
+ Deprecated1,
+ GeneralSettings,
+ Collections,
+ EditingCollections,
+ CurrentCollection,
+ SimpleAssignments,
+ IndividualAssignments,
+ GroupAssignments,
+ CollectionDetails,
+ Incognito,
+ Deprecated2,
+ Mods,
+ ModImport,
+ AdvancedHelp,
+ ModFilters,
+ CollectionSelectors,
+ Redrawing,
+ EnablingMods,
+ Priority,
+ ModOptions,
+ Fin,
+ Deprecated3,
+ Faq1,
+ Faq2,
+ Favorites,
+ Tags,
+}
+
+/// Service for the in-game tutorial.
+public class TutorialService(EphemeralConfig config) : Luna.IUiService
+{
+ private readonly Luna.Tutorial _tutorial = new Luna.Tutorial
+ {
+ BorderColor = new Rgba32(Colors.TutorialBorder).ToVector(),
+ HighlightColor = new Rgba32(Colors.TutorialMarker).ToVector(),
+ PopupLabel = new StringU8("Settings Tutorial"u8),
+ }
+ .Register("General Tooltips"u8, "This symbol gives you further information about whatever setting it appears next to.\n\n"u8
+ + "Hover over them when you are unsure what something does or how to do something."u8)
+ .Register("Initial Setup, Step 1: Mod Directory"u8,
+ "The first step is to set up your mod directory, which is where your mods are extracted to.\n\n"u8
+ + "The mod directory should be a short path - like 'C:\\FFXIVMods' - on your fastest available drive. Faster drives improve performance.\n\n"u8
+ + "The folder should be an empty folder no other applications write to."u8)
+ .Register("Initial Setup, Step 2: Enable Mods"u8, "Do not forget to enable your mods in case they are not."u8)
+ .Deprecated()
+ .Register("General Settings"u8, "Look through all of these settings before starting, they might help you a lot!\n\n"u8
+ + "If you do not know what some of these do yet, return to this later!"u8)
+ .Register("Initial Setup, Step 3: Collections"u8, "Collections are lists of settings for your installed mods.\n\n"u8
+ + "This is our next stop!\n\n"u8
+ + "Go here after setting up your root folder to continue the tutorial!"u8)
+ .Register("Initial Setup, Step 4: Managing Collections"u8,
+ "On the left, we have the collection selector. Here, we can create new collections - either empty ones or by duplicating existing ones - and delete any collections not needed anymore.\n"u8
+ + "There will always be one collection called \"Default\" that can not be deleted."u8)
+ .Register("Initial Setup, Step 5: Selected Collection"u8,
+ "The Selected Collection is the one we highlighted in the selector. It is the collection we are currently looking at and editing.\nAny changes we make in our mod settings later in the next tab will edit this collection.\n"u8
+ + "We should already have the collection named \"Default\" selected, and for our simple setup, we do not need to do anything here.\n\n"u8)
+ .Register("Initial Setup, Step 6: Simple Assignments"u8,
+ "Aside from being a collection of settings, we can also assign collections to different functions. This is used to make different mods apply to different characters.\n"u8
+ + "The Simple Assignments panel shows you the possible assignments that are enough for most people along with descriptions.\n"u8
+ + "If you are just starting, you can see that the \"Default\" collection is currently assigned to Default and Interface.\n"u8
+ + "You can also assign 'Use No Mods' instead of a collection by clicking on the function buttons."u8)
+ .Register("Individual Assignments"u8,
+ "In the Individual Assignments panel, you can manually create assignments for very specific characters or monsters, not just yourself or ones you can currently target."u8)
+ .Register("Group Assignments"u8,
+ "In the Group Assignments panel, you can create Assignments for more specific groups of characters based on race or age."u8)
+ .Register("Collection Details"u8,
+ "In the Collection Details panel, you can see a detailed overview over the usage of the currently selected collection, as well as remove outdated mod settings and setup inheritance.\n"u8
+ + "Inheritance can be used to make one collection take the settings of another as long as it does not setup the mod in question itself."u8)
+ .Register("Incognito Mode"u8,
+ "This button can toggle Incognito Mode, which shortens all collection names to two letters and a number,\n"u8
+ + "and all displayed individual character names to their initials and world, in case you want to share screenshots.\n"u8
+ + "It is strongly recommended to not show your characters name in public screenshots when using Penumbra."u8)
+ .Deprecated()
+ .Register("Initial Setup, Step 7: Mods"u8, "Our last stop is the Mods tab, where you can import and setup your mods.\n\n"u8
+ + "Please go there after verifying that your Selected Collection and Default Collection are setup to your liking."u8)
+ .Register("Initial Setup, Step 8: Mod Import"u8,
+ "Click this button to open a file selector with which to select TTMP mod files. You can select multiple at once.\n\n"u8
+ + "It is not recommended to import huge mod packs of all your TexTools mods, but rather import the mods themselves, otherwise you lose out on a lot of Penumbra features!\n\n"u8
+ + "A feature to import raw texture mods for Tattoos etc. is available under Advanced Editing, but is currently a work in progress."u8)
+ .Register("Advanced Help"u8, "Click this button to get detailed information on what you can do in the mod selector.\n\n"u8
+ + "Import and select a mod now to continue."u8)
+ .Register("Mod Filters"u8, "You can filter the available mods by name, author, changed items or various attributes here."u8)
+ .Register("Collection Selectors"u8, "This row provides shortcuts to set your Selected Collection.\n\n"u8
+ + "The first button sets it to your Base Collection (if any).\n\n"u8
+ + "The second button sets it to the collection the settings of the currently selected mod are inherited from (if any).\n\n"u8
+ + "The third is a regular collection selector to let you choose among all your collections."u8)
+ .Register("Redrawing"u8,
+ "Whenever you change your mod configuration, changes do not immediately take effect. You will need to force the game to reload the relevant files (or if this is not possible, restart the game).\n\n"u8
+ + "For this, Penumbra has these buttons as well as the '/penumbra redraw' command, which redraws all actors at once. You can also use several modifiers described in the help marker instead.\n\n"u8
+ + "Feel free to use these slash commands (e.g. '/penumbra redraw self') as a macro, too."u8)
+ .Register("Initial Setup, Step 9: Enabling Mods"u8,
+ "Enable a mod here. Disabled mods will not apply to anything in the current collection.\n\n"u8
+ + "Mods can be enabled or disabled in a collection, or they can be unconfigured, in which case they will use Inheritance."u8)
+ .Register("Initial Setup, Step 10: Priority"u8,
+ "If two enabled mods in one collection change the same files, there is a conflict.\n\n"u8
+ + "Conflicts can be solved by setting a priority. The mod with the higher number will be used for all the conflicting files.\n\n"u8
+ + "Conflicts are not a problem, as long as they are correctly resolved with priorities. Negative priorities are possible."u8)
+ .Register("Mod Options"u8, "Many mods have options themselves. You can also choose those here.\n\n"u8
+ + "Pulldown-options are mutually exclusive, whereas checkmark options can all be enabled separately."u8)
+ .Register("Initial Setup - Fin"u8, "Now you should have all information to get Penumbra running and working!\n\n"u8
+ + "If there are further questions or you need more help for the advanced features, take a look at the guide linked in the settings page."u8)
+ .Deprecated()
+ .Register("FAQ 1"u8,
+ "It is advised to not use TexTools and Penumbra at the same time. Penumbra may refuse to work if TexTools broke your game indices."u8)
+ .Register("FAQ 2"u8, "Penumbra can change the skin material a mod uses. This is under advanced editing."u8)
+ .Register("Favorites"u8,
+ "You can now toggle mods as favorites using this button. You can filter for favorited mods in the mod selector. Favorites are stored locally, not within the mod, but independently of collections."u8)
+ .Register("Tags"u8,
+ "Mods can now have two types of tags:\n\n- Local Tags are those that you can set for yourself. They are stored locally and are not saved in any way in the mod directory itself.\n- Mod Tags are stored in the mod metadata, are set by the mod creator and are exported together with the mod, they can only be edited in the Edit Mod tab.\n\nIf a mod has a tag in its Mod Tags, this overwrites any identical Local Tags.\n\nYou can filter for tags in the mod selector via 't:text'."u8)
+ .EnsureSize(Enum.GetValues().Length);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void OpenTutorial(BasicTutorialSteps step)
+ => _tutorial.Open((int)step, config.TutorialStep, v =>
+ {
+ config.TutorialStep = v;
+ config.Save();
+ });
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SkipTutorial(BasicTutorialSteps step)
+ => _tutorial.Skip((int)step, config.TutorialStep, v =>
+ {
+ config.TutorialStep = v;
+ config.Save();
+ });
+
+ /// Update the current tutorial step if tutorials have changed since last update.
+ public void UpdateTutorialStep()
+ {
+ var tutorial = _tutorial.CurrentEnabledId(config.TutorialStep);
+ if (tutorial != config.TutorialStep)
+ {
+ config.TutorialStep = tutorial;
+ config.Save();
+ }
+ }
+}
diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs
index c4de27b9..bcc910ed 100644
--- a/Penumbra/UI/CollectionTab/CollectionPanel.cs
+++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs
@@ -272,7 +272,7 @@ public sealed class CollectionPanel(
if (!context)
return;
- using (ImGuiColor.Text.Push(Colors.DiscordColor))
+ using (ImGuiColor.Text.Push(LunaStyle.DiscordColor))
{
if (Im.Menu.Item("Use no mods."u8))
_active.SetCollection(ModCollection.Empty, type, _active.Individuals.GetGroup(identifier));
@@ -349,7 +349,7 @@ public sealed class CollectionPanel(
return;
using var target = Im.DragDrop.Target();
- if (!target || !target.IsDropping("DragIndividual"))
+ if (!target || !target.IsDropping("DragIndividual"u8))
return;
var currentIdx = _active.Individuals.Index(id);
@@ -376,7 +376,7 @@ public sealed class CollectionPanel(
break;
case CollectionType.MalePlayerCharacter:
ImEx.TextMultiColored("Overruled by "u8)
- .Then("Male Racial Player"u8, Colors.DiscordColor)
+ .Then("Male Racial Player"u8, LunaStyle.DiscordColor)
.Then(", "u8)
.Then("Your Character"u8, ColorId.HandledConflictMod.Value().Color)
.Then(", or "u8)
@@ -386,7 +386,7 @@ public sealed class CollectionPanel(
break;
case CollectionType.FemalePlayerCharacter:
ImEx.TextMultiColored("Overruled by "u8)
- .Then("Female Racial Player"u8, Colors.ReniColorActive)
+ .Then("Female Racial Player"u8, LunaStyle.ReniColorActive)
.Then(", "u8)
.Then("Your Character"u8, ColorId.HandledConflictMod.Value().Color)
.Then(", or "u8)
@@ -396,24 +396,24 @@ public sealed class CollectionPanel(
break;
case CollectionType.MaleNonPlayerCharacter:
ImEx.TextMultiColored("Overruled by "u8)
- .Then("Male Racial NPC"u8, Colors.DiscordColor)
+ .Then("Male Racial NPC"u8, LunaStyle.DiscordColor)
.Then(", "u8)
.Then("Children"u8, ColorId.FolderLine.Value().Color)
.Then(", "u8)
.Then("Elderly"u8, Colors.MetaInfoText)
.Then(", or "u8)
.Then("Individual "u8, ColorId.NewMod.Value().Color)
- .Then("Assignments.")
+ .Then("Assignments."u8)
.End();
break;
case CollectionType.FemaleNonPlayerCharacter:
ImEx.TextMultiColored("Overruled by "u8)
- .Then("Female Racial NPC"u8, Colors.ReniColorActive)
+ .Then("Female Racial NPC"u8, LunaStyle.ReniColorActive)
.Then(", "u8)
.Then("Children"u8, ColorId.FolderLine.Value().Color)
.Then(", "u8)
.Then("Elderly"u8, Colors.MetaInfoText)
- .Then(", or ")
+ .Then(", or "u8)
.Then("Individual "u8, ColorId.NewMod.Value().Color)
.Then("Assignments."u8)
.End();
diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs
index a4d19e3d..8e339111 100644
--- a/Penumbra/UI/CollectionTab/CollectionSelector.cs
+++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs
@@ -1,144 +1,142 @@
-using ImSharp;
-using OtterGui;
-using Penumbra.Collections;
-using Penumbra.Collections.Manager;
-using Penumbra.Communication;
-using Penumbra.GameData.Actors;
-using Penumbra.Services;
-using Penumbra.UI.Classes;
-
-namespace Penumbra.UI.CollectionTab;
-
-public sealed class CollectionSelector : ItemSelector, IDisposable
-{
- private readonly Configuration _config;
- private readonly CommunicatorService _communicator;
- private readonly CollectionStorage _storage;
- private readonly ActiveCollections _active;
- private readonly TutorialService _tutorial;
- private readonly IncognitoService _incognito;
-
- private ModCollection? _dragging;
-
- public CollectionSelector(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active,
- TutorialService tutorial, IncognitoService incognito)
- : base([], Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter)
- {
- _config = config;
- _communicator = communicator;
- _storage = storage;
- _active = active;
- _tutorial = tutorial;
- _incognito = incognito;
-
- _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionSelector);
- // Set items.
- OnCollectionChange(new CollectionChange.Arguments(CollectionType.Inactive, null, null, string.Empty));
- // Set selection.
- OnCollectionChange(new CollectionChange.Arguments(CollectionType.Current, null, _active.Current, string.Empty));
- }
-
- protected override bool OnDelete(int idx)
- {
- if (idx < 0 || idx >= Items.Count)
- return false;
-
- // Always return false since we handle the selection update ourselves.
- _storage.RemoveCollection(Items[idx]);
- return false;
- }
-
- protected override bool DeleteButtonEnabled()
- => _storage.DefaultNamed != Current && _config.DeleteModModifier.IsActive();
-
- protected override string DeleteButtonTooltip()
- => _storage.DefaultNamed == Current
- ? $"The selected collection {Name(Current)} can not be deleted."
- : $"Delete the currently selected collection {(Current != null ? Name(Current) : string.Empty)}. Hold {_config.DeleteModModifier} to delete.";
-
- protected override bool OnAdd(string name)
- => _storage.AddCollection(name, null);
-
- protected override bool OnDuplicate(string name, int idx)
- {
- if (idx < 0 || idx >= Items.Count)
- return false;
-
- return _storage.AddCollection(name, Items[idx]);
- }
-
- protected override bool Filtered(int idx)
- => !Items[idx].Identity.Name.Contains(Filter, StringComparison.OrdinalIgnoreCase);
-
- private const string PayloadString = "Collection";
-
- protected override bool OnDraw(int idx)
- {
- using var color = ImGuiColor.Header.Push(ColorId.SelectedCollection.Value());
- var ret = Im.Selectable(Name(Items[idx]), idx == CurrentIdx);
- using var source = Im.DragDrop.Source();
-
- if (idx == CurrentIdx)
- _tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection);
-
- if (source)
- {
- _dragging = Items[idx];
- source.SetPayload(PayloadString);
- Im.Text($"Assigning {Name(_dragging)} to...");
- }
-
- if (ret)
- _active.SetCollection(Items[idx], CollectionType.Current);
-
- return ret;
- }
-
- public void DragTargetAssignment(CollectionType type, ActorIdentifier identifier)
- {
- using var target = Im.DragDrop.Target();
- if (!target.Success || _dragging is null || !target.IsDropping(PayloadString))
- return;
-
- _active.SetCollection(_dragging, type, _active.Individuals.GetGroup(identifier));
- _dragging = null;
- }
-
- public void Dispose()
- {
- _communicator.CollectionChange.Unsubscribe(OnCollectionChange);
- }
-
- private string Name(ModCollection collection)
- => _incognito.IncognitoMode || collection.Identity.Name.Length == 0 ? collection.Identity.AnonymizedName : collection.Identity.Name;
-
- public void RestoreCollections()
- {
- Items.Clear();
- Items.Add(_storage.DefaultNamed);
- foreach (var c in _storage.OrderBy(c => c.Identity.Name).Where(c => c != _storage.DefaultNamed))
- Items.Add(c);
- SetFilterDirty();
- SetCurrent(_active.Current);
- }
-
- private void OnCollectionChange(in CollectionChange.Arguments arguments)
- {
- switch (arguments.Type)
- {
- case CollectionType.Temporary: return;
- case CollectionType.Current:
- if (arguments.NewCollection is not null)
- SetCurrent(arguments.NewCollection);
- SetFilterDirty();
- return;
- case CollectionType.Inactive:
- RestoreCollections();
- SetFilterDirty();
- return;
- default:
- SetFilterDirty();
- return;
- }
- }
-}
+using ImSharp;
+using OtterGui;
+using Penumbra.Collections;
+using Penumbra.Collections.Manager;
+using Penumbra.Communication;
+using Penumbra.GameData.Actors;
+using Penumbra.Services;
+using Penumbra.UI.Classes;
+
+namespace Penumbra.UI.CollectionTab;
+
+public sealed class CollectionSelector : ItemSelector, IDisposable
+{
+ private readonly Configuration _config;
+ private readonly CommunicatorService _communicator;
+ private readonly CollectionStorage _storage;
+ private readonly ActiveCollections _active;
+ private readonly TutorialService _tutorial;
+ private readonly IncognitoService _incognito;
+
+ private ModCollection? _dragging;
+
+ public CollectionSelector(Configuration config, CommunicatorService communicator, CollectionStorage storage, ActiveCollections active,
+ TutorialService tutorial, IncognitoService incognito)
+ : base([], Flags.Delete | Flags.Add | Flags.Duplicate | Flags.Filter)
+ {
+ _config = config;
+ _communicator = communicator;
+ _storage = storage;
+ _active = active;
+ _tutorial = tutorial;
+ _incognito = incognito;
+
+ _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionSelector);
+ // Set items.
+ OnCollectionChange(new CollectionChange.Arguments(CollectionType.Inactive, null, null, string.Empty));
+ // Set selection.
+ OnCollectionChange(new CollectionChange.Arguments(CollectionType.Current, null, _active.Current, string.Empty));
+ }
+
+ protected override bool OnDelete(int idx)
+ {
+ if (idx < 0 || idx >= Items.Count)
+ return false;
+
+ // Always return false since we handle the selection update ourselves.
+ _storage.RemoveCollection(Items[idx]);
+ return false;
+ }
+
+ protected override bool DeleteButtonEnabled()
+ => _storage.DefaultNamed != Current && _config.DeleteModModifier.IsActive();
+
+ protected override string DeleteButtonTooltip()
+ => _storage.DefaultNamed == Current
+ ? $"The selected collection {Name(Current)} can not be deleted."
+ : $"Delete the currently selected collection {(Current != null ? Name(Current) : string.Empty)}. Hold {_config.DeleteModModifier} to delete.";
+
+ protected override bool OnAdd(string name)
+ => _storage.AddCollection(name, null);
+
+ protected override bool OnDuplicate(string name, int idx)
+ {
+ if (idx < 0 || idx >= Items.Count)
+ return false;
+
+ return _storage.AddCollection(name, Items[idx]);
+ }
+
+ protected override bool Filtered(int idx)
+ => !Items[idx].Identity.Name.Contains(Filter, StringComparison.OrdinalIgnoreCase);
+
+ protected override bool OnDraw(int idx)
+ {
+ using var color = ImGuiColor.Header.Push(ColorId.SelectedCollection.Value());
+ var ret = Im.Selectable(Name(Items[idx]), idx == CurrentIdx);
+ using var source = Im.DragDrop.Source();
+
+ if (idx == CurrentIdx)
+ _tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection);
+
+ if (source)
+ {
+ _dragging = Items[idx];
+ source.SetPayload("Collection"u8);
+ Im.Text($"Assigning {Name(_dragging)} to...");
+ }
+
+ if (ret)
+ _active.SetCollection(Items[idx], CollectionType.Current);
+
+ return ret;
+ }
+
+ public void DragTargetAssignment(CollectionType type, ActorIdentifier identifier)
+ {
+ using var target = Im.DragDrop.Target();
+ if (!target.Success || _dragging is null || !target.IsDropping("Collection"u8))
+ return;
+
+ _active.SetCollection(_dragging, type, _active.Individuals.GetGroup(identifier));
+ _dragging = null;
+ }
+
+ public void Dispose()
+ {
+ _communicator.CollectionChange.Unsubscribe(OnCollectionChange);
+ }
+
+ private string Name(ModCollection collection)
+ => _incognito.IncognitoMode || collection.Identity.Name.Length == 0 ? collection.Identity.AnonymizedName : collection.Identity.Name;
+
+ public void RestoreCollections()
+ {
+ Items.Clear();
+ Items.Add(_storage.DefaultNamed);
+ foreach (var c in _storage.OrderBy(c => c.Identity.Name).Where(c => c != _storage.DefaultNamed))
+ Items.Add(c);
+ SetFilterDirty();
+ SetCurrent(_active.Current);
+ }
+
+ private void OnCollectionChange(in CollectionChange.Arguments arguments)
+ {
+ switch (arguments.Type)
+ {
+ case CollectionType.Temporary: return;
+ case CollectionType.Current:
+ if (arguments.NewCollection is not null)
+ SetCurrent(arguments.NewCollection);
+ SetFilterDirty();
+ return;
+ case CollectionType.Inactive:
+ RestoreCollections();
+ SetFilterDirty();
+ return;
+ default:
+ SetFilterDirty();
+ return;
+ }
+ }
+}
diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs
index 13be56f4..9a5f2e95 100644
--- a/Penumbra/UI/CollectionTab/InheritanceUi.cs
+++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs
@@ -221,7 +221,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService
var tt = inheritance switch
{
InheritanceManager.ValidInheritance.Empty => "No valid collection to inherit from selected.",
- InheritanceManager.ValidInheritance.Valid => $"Let the {TutorialService.SelectedCollection} inherit from this collection.",
+ InheritanceManager.ValidInheritance.Valid => $"Let the Selected Collection inherit from this collection.",
InheritanceManager.ValidInheritance.Self => "The collection can not inherit from itself.",
InheritanceManager.ValidInheritance.Contained => "Already inheriting from this collection.",
InheritanceManager.ValidInheritance.Circle => "Inheriting from this collection would lead to cyclic inheritance.",
@@ -307,7 +307,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService
}
Im.Tooltip.OnHover(
- $"Control + Right-Click to switch the {TutorialService.SelectedCollection} to this one.{(withDelete ? "\nControl + Shift + Right-Click to remove this inheritance."u8 : StringU8.Empty)}");
+ $"Control + Right-Click to switch the Selected Collection to this one.{(withDelete ? "\nControl + Shift + Right-Click to remove this inheritance."u8 : StringU8.Empty)}");
}
private string Name(ModCollection collection)
diff --git a/Penumbra/UI/GlobalModImporter.cs b/Penumbra/UI/GlobalModImporter.cs
new file mode 100644
index 00000000..592131b7
--- /dev/null
+++ b/Penumbra/UI/GlobalModImporter.cs
@@ -0,0 +1,45 @@
+using System.Collections.Frozen;
+using Dalamud.Interface.DragDrop;
+using ImSharp;
+using Luna;
+using Penumbra.Mods.Manager;
+
+namespace Penumbra.UI;
+
+public sealed class GlobalModImporter : IRequiredService, IDisposable
+{
+ public const string DragDropId = "ModDragDrop";
+ private readonly DragDropManager _dragDropManager;
+ private readonly ModImportManager _importManager;
+
+ /// All default extensions for valid mod imports.
+ public static readonly FrozenSet ValidModExtensions = FrozenSet.Create(StringComparer.OrdinalIgnoreCase,
+ ".ttmp", ".ttmp2", ".pmp", ".pcp", ".zip", ".rar", ".7z"
+ );
+
+ public GlobalModImporter(DragDropManager dragDropManager, ModImportManager modImportManager)
+ {
+ _dragDropManager = dragDropManager;
+ _importManager = modImportManager;
+ dragDropManager.AddSource(DragDropId, ValidExtension, DragTooltip);
+ dragDropManager.AddTarget(DragDropId, ImportFiles);
+ }
+
+ public void Dispose()
+ {
+ _dragDropManager.RemoveSource(DragDropId);
+ _dragDropManager.RemoveTarget(DragDropId);
+ }
+
+ private void ImportFiles(IReadOnlyList files, IReadOnlyList _)
+ => _importManager.AddUnpack(files.Where(f => ValidModExtensions.Contains(Path.GetExtension(f))));
+
+ private static bool ValidExtension(IDragDropManager manager)
+ => manager.Extensions.Any(ValidModExtensions.Contains);
+
+ private static bool DragTooltip(IDragDropManager manager)
+ {
+ Im.Text($"Dragging mods for import:\n\t{StringU8.Join("\n\t"u8, manager.Files.Select(Path.GetFileName))}");
+ return true;
+ }
+}
diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs
index fb963fd9..ad4c117e 100644
--- a/Penumbra/UI/LaunchButton.cs
+++ b/Penumbra/UI/LaunchButton.cs
@@ -10,21 +10,21 @@ namespace Penumbra.UI;
///
public class LaunchButton : IDisposable, Luna.IUiService
{
- private readonly ConfigWindow _configWindow;
- private readonly IUiBuilder _uiBuilder;
- private readonly ITitleScreenMenu _title;
- private readonly string _fileName;
- private readonly ITextureProvider _textureProvider;
+ private readonly MainWindow.MainWindow _mainWindow;
+ private readonly IUiBuilder _uiBuilder;
+ private readonly ITitleScreenMenu _title;
+ private readonly string _fileName;
+ private readonly ITextureProvider _textureProvider;
private IReadOnlyTitleScreenMenuEntry? _entry;
///
/// Register the launch button to be created on the next draw event.
///
- public LaunchButton(IDalamudPluginInterface pi, ITitleScreenMenu title, ConfigWindow ui, ITextureProvider textureProvider)
+ public LaunchButton(IDalamudPluginInterface pi, ITitleScreenMenu title, MainWindow.MainWindow ui, ITextureProvider textureProvider)
{
_uiBuilder = pi.UiBuilder;
- _configWindow = ui;
+ _mainWindow = ui;
_textureProvider = textureProvider;
_title = title;
_entry = null;
@@ -59,5 +59,5 @@ public class LaunchButton : IDisposable, Luna.IUiService
}
private void OnTriggered()
- => _configWindow.Toggle();
+ => _mainWindow.Toggle();
}
diff --git a/Penumbra/UI/MainWindow/ChangedItemsTab.cs b/Penumbra/UI/MainWindow/ChangedItemsTab.cs
new file mode 100644
index 00000000..71499ced
--- /dev/null
+++ b/Penumbra/UI/MainWindow/ChangedItemsTab.cs
@@ -0,0 +1,191 @@
+using ImSharp;
+using Luna;
+using Penumbra.Api.Enums;
+using Penumbra.Collections.Manager;
+using Penumbra.Communication;
+using Penumbra.GameData.Data;
+using Penumbra.Mods;
+using Penumbra.Mods.Editor;
+using Penumbra.Services;
+using Penumbra.UI.Classes;
+
+namespace Penumbra.UI.MainWindow;
+
+public class UiState : ISavable, IService
+{
+ public string ChangedItemTabNameFilter = string.Empty;
+ public string ChangedItemTabModFilter = string.Empty;
+ public ChangedItemIconFlag ChangedItemTabCategoryFilter = ChangedItemFlagExtensions.DefaultFlags;
+
+ public string ToFilePath(FilenameService fileNames)
+ => "uiState";
+
+ public void Save(StreamWriter writer)
+ { }
+}
+
+public sealed class ChangedItemsTab(
+ CollectionManager collectionManager,
+ CollectionSelectHeader collectionHeader,
+ ChangedItemDrawer drawer,
+ CommunicatorService communicator)
+ : ITab
+{
+ public ReadOnlySpan Label
+ => "Changed Items"u8;
+
+ public TabType Identifier
+ => TabType.ChangedItems;
+
+ private Vector2 _buttonSize;
+
+ private readonly ChangedItemFilter _filter = new(drawer, new UiState());
+
+ private sealed class ChangedItemFilter(ChangedItemDrawer drawer, UiState uiState) : IFilter-
+ {
+ public bool WouldBeVisible(in Item item, int globalIndex)
+ => drawer.FilterChangedItem(item.Name, item.Data, uiState.ChangedItemTabNameFilter)
+ && (uiState.ChangedItemTabModFilter.Length is 0
+ || item.Mods.Any(m => m.Name.Contains(uiState.ChangedItemTabModFilter, StringComparison.OrdinalIgnoreCase)));
+
+ public event Action? FilterChanged;
+
+ public bool DrawFilter(ReadOnlySpan label, Vector2 availableRegion)
+ {
+ var varWidth = Im.ContentRegion.Available.X
+ - 450 * Im.Style.GlobalScale
+ - Im.Style.ItemSpacing.X;
+ Im.Item.SetNextWidth(450 * Im.Style.GlobalScale);
+ var ret = Im.Input.Text("##changedItemsFilter"u8, ref uiState.ChangedItemTabNameFilter, "Filter Item..."u8);
+ Im.Line.Same();
+ Im.Item.SetNextWidth(varWidth);
+ ret |= Im.Input.Text("##changedItemsModFilter"u8, ref uiState.ChangedItemTabModFilter, "Filter Mods..."u8);
+ if (!ret)
+ return false;
+
+ FilterChanged?.Invoke();
+ return true;
+ }
+
+ public void Clear()
+ {
+ uiState.ChangedItemTabModFilter = string.Empty;
+ uiState.ChangedItemTabNameFilter = string.Empty;
+ uiState.ChangedItemTabCategoryFilter = ChangedItemFlagExtensions.DefaultFlags;
+ FilterChanged?.Invoke();
+ }
+ }
+
+ private readonly record struct Item(string Label, IIdentifiedObjectData Data, SingleArray Mods)
+ {
+ public readonly string Name = Data.ToName(Label);
+ public readonly StringU8 ItemName = new(Data.ToName(Label));
+ public readonly StringU8 Mod = Mods.Count > 0 ? new StringU8(Mods[0].Name) : StringU8.Empty;
+ public readonly StringU8 ModelData = new(Data.AdditionalData);
+ public readonly ChangedItemIconFlag CategoryIcon = Data.GetIcon().ToFlag();
+
+ public readonly StringU8 Tooltip = Mods.Count > 1
+ ? new StringU8($"Other mods affecting this item:\n{StringU8.Join((byte)'\n', Mods.Skip(1).Select(m => m.Name))}")
+ : StringU8.Empty;
+ }
+
+ private sealed class Cache : BasicFilterCache
-
+ {
+ private readonly ActiveCollections _collections;
+ private readonly CollectionChange _collectionChange;
+
+ public Cache(ActiveCollections collections, CommunicatorService communicator, IFilter
- filter)
+ : base(filter)
+ {
+ _collections = collections;
+ _collectionChange = communicator.CollectionChange;
+ _collectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ChangedItemsTabCache);
+ }
+
+
+
+ private void OnCollectionChange(in CollectionChange.Arguments arguments)
+ => FilterDirty = true;
+
+ protected override IEnumerable
- GetItems()
+ => _collections.Current.ChangedItems.Select(kvp => new Item(kvp.Key, kvp.Value.Item2, kvp.Value.Item1));
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ _collectionChange.Unsubscribe(OnCollectionChange);
+ }
+ }
+
+ public void DrawContent()
+ {
+ collectionHeader.Draw(true);
+ drawer.DrawTypeFilter();
+ _filter.DrawFilter("##Filter"u8, Im.ContentRegion.Available);
+ using var child = Im.Child.Begin("##changedItemsChild"u8, Im.ContentRegion.Available);
+ if (!child)
+ return;
+
+ _buttonSize = new Vector2(Im.Style.ItemSpacing.Y + Im.Style.FrameHeight);
+ using var style = ImStyleDouble.CellPadding.Push(Vector2.Zero)
+ .Push(ImStyleDouble.ItemSpacing, Vector2.Zero)
+ .Push(ImStyleDouble.FramePadding, Vector2.Zero)
+ .Push(ImStyleDouble.SelectableTextAlign, new Vector2(0.01f, 0.5f));
+
+ using var table = Im.Table.Begin("##changedItems"u8, 3, TableFlags.RowBackground, Im.ContentRegion.Available);
+ if (!table)
+ return;
+
+ var varWidth = Im.ContentRegion.Available.X
+ - 450 * Im.Style.GlobalScale
+ - Im.Style.ItemSpacing.X;
+ const TableColumnFlags flags = TableColumnFlags.NoResize | TableColumnFlags.WidthFixed;
+ table.SetupColumn("items"u8, flags, 450 * Im.Style.GlobalScale);
+ table.SetupColumn("mods"u8, flags, varWidth - 140 * Im.Style.GlobalScale);
+ table.SetupColumn("id"u8, flags, 140 * Im.Style.GlobalScale);
+
+ var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current,
+ () => new Cache(collectionManager.Active, communicator, _filter));
+ using var clipper = new Im.ListClipper(cache.Count, _buttonSize.Y);
+ foreach (var (idx, item) in clipper.Iterate(cache).Index())
+ {
+ using var id = Im.Id.Push(idx);
+ DrawChangedItemColumn(table, item);
+ }
+ }
+
+ /// Draw a full column for a changed item.
+ private void DrawChangedItemColumn(in Im.TableDisposable table, in Item item)
+ {
+ table.NextColumn();
+ drawer.DrawCategoryIcon(item.CategoryIcon, _buttonSize.Y);
+ Im.Line.NoSpacing();
+ var clicked = Im.Selectable(item.ItemName, false, SelectableFlags.None, _buttonSize with { X = 0 });
+ drawer.ChangedItemHandling(item.Data, clicked);
+
+ table.NextColumn();
+ DrawModColumn(item);
+
+ table.NextColumn();
+ ChangedItemDrawer.DrawModelData(item.ModelData, _buttonSize.Y);
+ }
+
+ private void DrawModColumn(in Item item)
+ {
+ if (item.Mods.Count <= 0)
+ return;
+
+ if (Im.Selectable(item.Mod, false, SelectableFlags.None, _buttonSize with { X = 0 })
+ && Im.Io.KeyControl
+ && item.Mods[0] is Mod mod)
+ communicator.SelectTab.Invoke(new SelectTab.Arguments(TabType.Mods, mod));
+
+ if (!Im.Item.Hovered())
+ return;
+
+ using var _ = Im.Tooltip.Begin();
+ Im.Text("Hold Control and click to jump to mod.\n"u8);
+ if (!item.Tooltip.IsEmpty)
+ Im.Text(item.Tooltip);
+ }
+}
diff --git a/Penumbra/UI/MainWindow/EffectiveTab.cs b/Penumbra/UI/MainWindow/EffectiveTab.cs
new file mode 100644
index 00000000..44244aef
--- /dev/null
+++ b/Penumbra/UI/MainWindow/EffectiveTab.cs
@@ -0,0 +1,179 @@
+using Dalamud.Interface;
+using ImSharp;
+using Luna;
+using Penumbra.Api.Enums;
+using Penumbra.Collections.Manager;
+using Penumbra.Communication;
+using Penumbra.Meta.Manipulations;
+using Penumbra.Mods.Editor;
+using Penumbra.Services;
+using Penumbra.String.Classes;
+using Penumbra.UI.Classes;
+
+namespace Penumbra.UI.MainWindow;
+
+public sealed class EffectiveTab(
+ CollectionManager collectionManager,
+ CollectionSelectHeader collectionHeader,
+ CommunicatorService communicatorService)
+ : ITab
+{
+ public ReadOnlySpan Label
+ => "Effective Changes"u8;
+
+ public void DrawContent()
+ {
+ collectionHeader.Draw(true);
+ var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current, () => new Cache(collectionManager, communicatorService, _filter));
+ cache.Draw();
+ }
+
+ public TabType Identifier
+ => TabType.EffectiveChanges;
+
+ private readonly PairFilter
- _filter = new(new GamePathFilter(), new FullPathFilter());
+
+ private sealed class Cache : BasicFilterCache
- , IPanel
+ {
+ private readonly CollectionManager _collectionManager;
+ private readonly CommunicatorService _communicator;
+ private float _arrowSize;
+ private float _gamePathSize;
+ private static readonly AwesomeIcon Arrow = FontAwesomeIcon.LongArrowAltLeft;
+
+ private new PairFilter
- Filter
+ => (PairFilter
- )base.Filter;
+
+ public override void Update()
+ {
+ if (FontDirty)
+ {
+ _arrowSize = ImEx.Icon.CalculateSize(Arrow).X;
+ _gamePathSize = 450 * Im.Style.GlobalScale;
+ Dirty &= ~IManagedCache.DirtyFlags.Font;
+ }
+
+ base.Update();
+ }
+
+ public Cache(CollectionManager collectionManager, CommunicatorService communicator, IFilter
- filter)
+ : base(filter)
+ {
+ _collectionManager = collectionManager;
+ _communicator = communicator;
+
+ _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.EffectiveChangesCache);
+ _communicator.ResolvedFileChanged.Subscribe(OnResolvedFileChange, ResolvedFileChanged.Priority.EffectiveChangesCache);
+ _communicator.ResolvedMetaChanged.Subscribe(OnResolvedMetaChange, ResolvedMetaChanged.Priority.EffectiveChangesCache);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ _communicator.CollectionChange.Unsubscribe(OnCollectionChange);
+ _communicator.ResolvedFileChanged.Unsubscribe(OnResolvedFileChange);
+ _communicator.ResolvedMetaChanged.Unsubscribe(OnResolvedMetaChange);
+ }
+
+ private void OnResolvedFileChange(in ResolvedFileChanged.Arguments arguments)
+ => Dirty |= IManagedCache.DirtyFlags.Custom;
+
+ private void OnResolvedMetaChange(in ResolvedMetaChanged.Arguments arguments)
+ => Dirty |= IManagedCache.DirtyFlags.Custom;
+
+ private void OnCollectionChange(in CollectionChange.Arguments arguments)
+ {
+ if (arguments.Type is CollectionType.Current)
+ Dirty |= IManagedCache.DirtyFlags.Custom;
+ }
+
+ protected override IEnumerable
- GetItems()
+ => _collectionManager.Active.Current.Cache is null
+ ? []
+ : _collectionManager.Active.Current.Cache.ResolvedFiles.Select(f => new Item(f.Value.Mod, f.Key.Path.Span, f.Value.Path))
+ .OrderBy(i => i.GamePath.Utf16)
+ .Concat(_collectionManager.Active.Current.Cache.Meta.IdentifierSources.Select(s => new Item(s.Item2, s.Item1))
+ .OrderBy(i => i.GamePath.Utf16));
+
+ public ReadOnlySpan Id
+ => "EC"u8;
+
+ public void Draw()
+ {
+ DrawFilters();
+ DrawTable();
+ }
+
+ private void DrawFilters()
+ {
+ using var style = ImStyleSingle.FrameRounding.Push(0).PushX(ImStyleDouble.ItemSpacing, 0);
+
+ Filter.Filter1.DrawFilter("Filter game path..."u8, new Vector2(_gamePathSize + Im.Style.CellPadding.X, Im.Style.FrameHeight));
+ Im.Line.Same(0, _arrowSize + 2 * Im.Style.CellPadding.X);
+ Filter.Filter2.DrawFilter("Filter file path..."u8, Im.ContentRegion.Available with { Y = Im.Style.FrameHeight });
+
+ }
+
+ private void DrawTable()
+ {
+ using var table = Im.Table.Begin("t"u8, 3, TableFlags.RowBackground | TableFlags.ScrollY, Im.ContentRegion.Available);
+ if (!table)
+ return;
+
+ table.SetupColumn("gp"u8, TableColumnFlags.WidthFixed, _gamePathSize);
+ table.SetupColumn("a"u8, TableColumnFlags.WidthFixed, _arrowSize);
+ table.SetupColumn("fp"u8, TableColumnFlags.WidthStretch);
+
+ using var clipper = new Im.ListClipper(Count, Im.Style.TextHeightWithSpacing);
+ foreach (var item in clipper.Iterate(this))
+ {
+ table.NextColumn();
+ ImEx.CopyOnClickSelectable(item.GamePath.Utf8);
+
+ table.NextColumn();
+ ImEx.Icon.Draw(Arrow);
+
+ table.NextColumn();
+ ImEx.CopyOnClickSelectable(item.FilePath.InternalName.Span);
+ if (!item.IsMeta)
+ Im.Tooltip.OnHover($"\nChanged by {item.Mod.Name}.");
+ }
+ }
+ }
+
+ private sealed class GamePathFilter : RegexFilterBase
-
+ {
+ protected override string ToFilterString(in Item item, int globalIndex)
+ => item.GamePath.Utf16;
+ }
+
+ private sealed class FullPathFilter : RegexFilterBase
-
+ {
+ protected override string ToFilterString(in Item item, int globalIndex)
+ => item.FilePath.FullName;
+ }
+
+ private sealed class Item
+ {
+ public IMod Mod;
+ public StringPair GamePath;
+ public FullPath FilePath;
+ public bool IsMeta;
+
+ public Item(IMod mod, ReadOnlySpan gamePath, FullPath filePath)
+ {
+ Mod = mod;
+ GamePath = new StringPair(gamePath);
+ FilePath = filePath;
+ IsMeta = false;
+ }
+
+ public Item(IMod mod, IMetaIdentifier identifier)
+ {
+ Mod = mod;
+ GamePath = new StringPair($"{identifier}");
+ FilePath = new FullPath(mod.Name);
+ IsMeta = true;
+ }
+ }
+}
diff --git a/Penumbra/UI/Tabs/MainTabBar.cs b/Penumbra/UI/MainWindow/MainTabBar.cs
similarity index 86%
rename from Penumbra/UI/Tabs/MainTabBar.cs
rename to Penumbra/UI/MainWindow/MainTabBar.cs
index 60526d34..1bf73669 100644
--- a/Penumbra/UI/Tabs/MainTabBar.cs
+++ b/Penumbra/UI/MainWindow/MainTabBar.cs
@@ -1,21 +1,24 @@
using Luna;
using Penumbra.Api.Enums;
using Penumbra.Communication;
+using Penumbra.Mods.Manager;
using Penumbra.Services;
+using Penumbra.UI.Tabs;
using Penumbra.UI.Tabs.Debug;
using Watcher = Penumbra.UI.ResourceWatcher.ResourceWatcher;
-namespace Penumbra.UI.Tabs;
+namespace Penumbra.UI.MainWindow;
public sealed class MainTabBar : TabBar, IDisposable
{
- public readonly ModsTab Mods;
+ public readonly Tabs.ModsTab Mods;
private readonly EphemeralConfig _config;
private readonly SelectTab _selectTab;
public MainTabBar(Logger log,
SettingsTab settings,
- ModsTab mods,
+ Tabs.ModsTab mods,
+ ModTab mods2,
CollectionsTab collections,
ChangedItemsTab changedItems,
EffectiveTab effectiveChanges,
@@ -24,7 +27,7 @@ public sealed class MainTabBar : TabBar, IDisposable
Watcher watcher,
OnScreenTab onScreen,
MessagesTab messages, EphemeralConfig config, CommunicatorService communicator)
- : base(nameof(MainTabBar), log, settings, collections, mods, changedItems, effectiveChanges, onScreen,
+ : base(nameof(MainTabBar), log, settings, collections, mods, mods2, changedItems, effectiveChanges, onScreen,
resources, watcher, debug, messages)
{
Mods = mods;
diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/MainWindow/MainWindow.cs
similarity index 97%
rename from Penumbra/UI/ConfigWindow.cs
rename to Penumbra/UI/MainWindow/MainWindow.cs
index f054a09b..7182d484 100644
--- a/Penumbra/UI/ConfigWindow.cs
+++ b/Penumbra/UI/MainWindow/MainWindow.cs
@@ -6,9 +6,9 @@ using Penumbra.UI.Classes;
using Penumbra.UI.Tabs;
using TabType = Penumbra.Api.Enums.TabType;
-namespace Penumbra.UI;
+namespace Penumbra.UI.MainWindow;
-public sealed class ConfigWindow : Window
+public sealed class MainWindow : Window
{
private readonly IDalamudPluginInterface _pluginInterface;
private readonly Configuration _config;
@@ -17,7 +17,7 @@ public sealed class ConfigWindow : Window
private MainTabBar _configTabs = null!;
private string? _lastException;
- public ConfigWindow(IDalamudPluginInterface pi, Configuration config, ValidityChecker checker,
+ public MainWindow(IDalamudPluginInterface pi, Configuration config, ValidityChecker checker,
TutorialService tutorial)
: base(GetLabel(checker))
{
diff --git a/Penumbra/UI/Tabs/MessagesTab.cs b/Penumbra/UI/MainWindow/MessagesTab.cs
similarity index 100%
rename from Penumbra/UI/Tabs/MessagesTab.cs
rename to Penumbra/UI/MainWindow/MessagesTab.cs
diff --git a/Penumbra/UI/Tabs/OnScreenTab.cs b/Penumbra/UI/MainWindow/OnScreenTab.cs
similarity index 93%
rename from Penumbra/UI/Tabs/OnScreenTab.cs
rename to Penumbra/UI/MainWindow/OnScreenTab.cs
index c39eaba8..2d1f4c62 100644
--- a/Penumbra/UI/Tabs/OnScreenTab.cs
+++ b/Penumbra/UI/MainWindow/OnScreenTab.cs
@@ -2,7 +2,7 @@ using Luna;
using Penumbra.Api.Enums;
using Penumbra.UI.AdvancedWindow;
-namespace Penumbra.UI.Tabs;
+namespace Penumbra.UI.MainWindow;
public sealed class OnScreenTab(ResourceTreeViewerFactory resourceTreeViewerFactory) : ITab
{
diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs
index ec0746ab..c8545c10 100644
--- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs
+++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs
@@ -1,6 +1,4 @@
-using Dalamud.Bindings.ImGui;
using ImSharp;
-using OtterGui.Text;
using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@@ -34,7 +32,7 @@ public class AddGroupDrawer : Luna.IUiService
public void Draw(Mod mod, float width)
{
- var buttonWidth = new Vector2((width - ImUtf8.ItemInnerSpacing.X) / 2, 0);
+ var buttonWidth = new Vector2((width - Im.Style.ItemInnerSpacing.X) / 2, 0);
DrawBasicGroups(mod, width, buttonWidth);
DrawImcData(mod, buttonWidth);
}
@@ -42,7 +40,7 @@ public class AddGroupDrawer : Luna.IUiService
private void DrawBasicGroups(Mod mod, float width, Vector2 buttonWidth)
{
Im.Item.SetNextWidth(width);
- if (ImUtf8.InputText("##name"u8, ref _groupName, "Enter New Name..."u8))
+ if (Im.Input.Text("##name"u8, ref _groupName, "Enter New Name..."u8))
_groupNameValid = ModGroupEditor.VerifyFileName(mod, null, _groupName, false);
DrawSingleGroupButton(mod, buttonWidth);
@@ -53,10 +51,9 @@ public class AddGroupDrawer : Luna.IUiService
private void DrawSingleGroupButton(Mod mod, Vector2 width)
{
- if (!ImUtf8.ButtonEx("Add Single Group"u8, _groupNameValid
- ? "Add a new single selection option group to this mod."u8
- : "Can not add a new group of this name."u8,
- width, !_groupNameValid))
+ if (!ImEx.Button("Add Single Group"u8, width, _groupNameValid
+ ? "Add a new single selection option group to this mod."u8
+ : "Can not add a new group of this name."u8, !_groupNameValid))
return;
_modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _groupName);
@@ -66,10 +63,9 @@ public class AddGroupDrawer : Luna.IUiService
private void DrawMultiGroupButton(Mod mod, Vector2 width)
{
- if (!ImUtf8.ButtonEx("Add Multi Group"u8, _groupNameValid
- ? "Add a new multi selection option group to this mod."u8
- : "Can not add a new group of this name."u8,
- width, !_groupNameValid))
+ if (!ImEx.Button("Add Multi Group"u8, width, _groupNameValid
+ ? "Add a new multi selection option group to this mod."u8
+ : "Can not add a new group of this name."u8, !_groupNameValid))
return;
_modManager.OptionEditor.AddModGroup(mod, GroupType.Multi, _groupName);
@@ -79,10 +75,9 @@ public class AddGroupDrawer : Luna.IUiService
private void DrawCombiningGroupButton(Mod mod, Vector2 width)
{
- if (!ImUtf8.ButtonEx("Add Combining Group"u8, _groupNameValid
- ? "Add a new combining option group to this mod."u8
- : "Can not add a new group of this name."u8,
- width, !_groupNameValid))
+ if (!ImEx.Button("Add Combining Group"u8, width, _groupNameValid
+ ? "Add a new combining option group to this mod."u8
+ : "Can not add a new group of this name."u8, !_groupNameValid))
return;
_modManager.OptionEditor.AddModGroup(mod, GroupType.Combining, _groupName);
@@ -103,7 +98,7 @@ public class AddGroupDrawer : Luna.IUiService
}
else if (_imcIdentifier.ObjectType is ObjectType.DemiHuman)
{
- var quarterWidth = (width - ImUtf8.ItemInnerSpacing.X / Im.Style.GlobalScale) / 2;
+ var quarterWidth = (width - Im.Style.ItemInnerSpacing.X / Im.Style.GlobalScale) / 2;
change |= ImcMetaDrawer.DrawSecondaryId(ref _imcIdentifier, width);
Im.Line.SameInner();
change |= ImcMetaDrawer.DrawSlot(ref _imcIdentifier, quarterWidth);
@@ -130,12 +125,11 @@ public class AddGroupDrawer : Luna.IUiService
private void DrawImcButton(Mod mod, Vector2 width)
{
- if (ImUtf8.ButtonEx("Add IMC Group"u8, !_groupNameValid
- ? "Can not add a new group of this name."u8
- : _entryInvalid
- ? "The associated IMC entry is invalid."u8
- : "Add a new multi selection option group to this mod."u8,
- width, !_groupNameValid || _entryInvalid))
+ if (ImEx.Button("Add IMC Group"u8, width, !_groupNameValid
+ ? "Can not add a new group of this name."u8
+ : _entryInvalid
+ ? "The associated IMC entry is invalid."u8
+ : "Add a new multi selection option group to this mod."u8, !_groupNameValid || _entryInvalid))
{
_modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcIdentifier, _defaultEntry);
_groupName = string.Empty;
@@ -148,7 +142,7 @@ public class AddGroupDrawer : Luna.IUiService
var text = _imcFileExists
? "IMC Entry Does Not Exist"u8
: "IMC File Does Not Exist"u8;
- ImUtf8.TextFramed(text, Colors.PressEnterWarningBg, width);
+ ImEx.TextFramed(text, width, Colors.PressEnterWarningBg);
}
}
diff --git a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs
index b9e64f70..a6761afa 100644
--- a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs
+++ b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs
@@ -1,112 +1,95 @@
-using Dalamud.Interface;
-using Dalamud.Bindings.ImGui;
-using ImSharp;
-using OtterGui;
-using OtterGui.Raii;
-using OtterGui.Text;
-using Penumbra.Mods.Groups;
-using Penumbra.Mods.SubMods;
-
-namespace Penumbra.UI.ModsTab.Groups;
-
-public readonly struct CombiningModGroupEditDrawer(ModGroupEditDrawer editor, CombiningModGroup group) : IModGroupEditDrawer
-{
- public void Draw()
- {
- foreach (var (optionIdx, option) in group.OptionData.Index())
- {
- using var id = ImUtf8.PushId(optionIdx);
- editor.DrawOptionPosition(group, option, optionIdx);
-
- Im.Line.SameInner();
- editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx);
-
- Im.Line.SameInner();
- editor.DrawOptionName(option);
-
- Im.Line.SameInner();
- editor.DrawOptionDescription(option);
-
- Im.Line.SameInner();
- editor.DrawOptionDelete(option);
- }
-
- DrawNewOption();
- DrawContainerNames();
- }
-
- private void DrawNewOption()
- {
- var count = group.OptionData.Count;
- if (count >= IModGroup.MaxCombiningOptions)
- return;
-
- var name = editor.DrawNewOptionBase(group, count);
-
- var validName = name.Length > 0;
- if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName
- ? "Add a new option to this group."u8
- : "Please enter a name for the new option."u8, default, !validName))
- {
- editor.ModManager.OptionEditor.CombiningEditor.AddOption(group, name);
- editor.NewOptionName = null;
- }
- }
-
- private unsafe void DrawContainerNames()
- {
- if (ImUtf8.ButtonEx("Edit Container Names"u8,
- "Add optional names to separate data containers of the combining group.\nThose are just for easier identification while editing the mod, and are not generally displayed to the user."u8,
- new Vector2(400 * Im.Style.GlobalScale, 0)))
- ImUtf8.OpenPopup("DataContainerNames"u8);
-
- var sizeX = group.OptionData.Count * (Im.Style.ItemInnerSpacing.X + Im.Style.FrameHeight) + 300 * Im.Style.GlobalScale;
- ImGui.SetNextWindowSize(new Vector2(sizeX, Im.Style.FrameHeightWithSpacing * Math.Min(16, group.Data.Count) + 200 * Im.Style.GlobalScale));
- using var popup = ImUtf8.Popup("DataContainerNames"u8);
- if (!popup)
- return;
-
- foreach (var option in group.OptionData)
- {
- ImUtf8.RotatedText(option.Name, true);
- Im.Line.SameInner();
- }
-
- Im.Line.New();
- Im.Separator();
- using var child = ImUtf8.Child("##Child"u8, Im.ContentRegion.Available);
- ImGuiClip.ClippedDraw(group.Data, DrawRow, Im.Style.FrameHeightWithSpacing);
- }
-
- private void DrawRow(CombinedDataContainer container, int index)
- {
- using var id = ImUtf8.PushId(index);
- using (ImRaii.Disabled())
- {
- for (var i = 0; i < group.OptionData.Count; ++i)
- {
- id.Push(i);
- var check = (index & (1 << i)) != 0;
- ImUtf8.Checkbox(""u8, ref check);
- Im.Line.SameInner();
- id.Pop();
- }
- }
-
- var name = editor.CombiningDisplayIndex == index ? editor.CombiningDisplayName ?? container.Name : container.Name;
- if (ImUtf8.InputText("##Nothing"u8, ref name, "Optional Display Name..."u8))
- {
- editor.CombiningDisplayIndex = index;
- editor.CombiningDisplayName = name;
- }
-
- if (ImGui.IsItemDeactivatedAfterEdit())
- editor.ModManager.OptionEditor.CombiningEditor.SetDisplayName(container, name);
-
- if (ImGui.IsItemDeactivated())
- {
- editor.CombiningDisplayIndex = -1;
- editor.CombiningDisplayName = null;
- }
- }
-}
+using ImSharp;
+using Luna;
+using Penumbra.Mods.Groups;
+using Penumbra.Mods.SubMods;
+
+namespace Penumbra.UI.ModsTab.Groups;
+
+public readonly struct CombiningModGroupEditDrawer(ModGroupEditDrawer editor, CombiningModGroup group) : IModGroupEditDrawer
+{
+ public void Draw()
+ {
+ foreach (var (optionIdx, option) in group.OptionData.Index())
+ {
+ using var id = Im.Id.Push(optionIdx);
+ editor.DrawOptionPosition(group, option, optionIdx);
+
+ Im.Line.SameInner();
+ editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx);
+
+ Im.Line.SameInner();
+ editor.DrawOptionName(option);
+
+ Im.Line.SameInner();
+ editor.DrawOptionDescription(option);
+
+ Im.Line.SameInner();
+ editor.DrawOptionDelete(option);
+ }
+
+ DrawNewOption();
+ DrawContainerNames();
+ }
+
+ private void DrawNewOption()
+ {
+ var count = group.OptionData.Count;
+ if (count >= IModGroup.MaxCombiningOptions)
+ return;
+
+ var name = editor.DrawNewOptionBase(group, count);
+
+ var validName = name.Length > 0;
+ if (ImEx.Icon.Button(LunaStyle.AddObjectIcon, validName
+ ? "Add a new option to this group."u8
+ : "Please enter a name for the new option."u8, !validName))
+ {
+ editor.ModManager.OptionEditor.CombiningEditor.AddOption(group, name);
+ editor.NewOptionName = null;
+ }
+ }
+
+ private void DrawContainerNames()
+ {
+ if (ImEx.Button("Edit Container Names"u8, new Vector2(400 * Im.Style.GlobalScale, 0),
+ "Add optional names to separate data containers of the combining group.\nThose are just for easier identification while editing the mod, and are not generally displayed to the user."u8))
+ Im.Popup.Open("names"u8);
+
+ var sizeX = group.OptionData.Count * (Im.Style.ItemInnerSpacing.X + Im.Style.FrameHeight) + 300 * Im.Style.GlobalScale;
+ Im.Window.SetNextSize(new Vector2(sizeX,
+ Im.Style.FrameHeightWithSpacing * Math.Min(16, group.Data.Count) + 200 * Im.Style.GlobalScale));
+ using var popup = Im.Popup.Begin("names"u8);
+ if (!popup)
+ return;
+
+ foreach (var option in group.OptionData)
+ {
+ ImEx.RotatedText(option.Name, true);
+ Im.Line.SameInner();
+ }
+
+ Im.Line.New();
+ Im.Separator();
+ using var child = Im.Child.Begin("##Child"u8, Im.ContentRegion.Available);
+ Im.ListClipper.Draw(group.Data, DrawRow, Im.Style.FrameHeightWithSpacing);
+ }
+
+ private void DrawRow(CombinedDataContainer container, int index)
+ {
+ using var id = Im.Id.Push(index);
+ using (Im.Disabled())
+ {
+ for (var i = 0; i < group.OptionData.Count; ++i)
+ {
+ id.Push(i);
+ var check = (index & (1 << i)) != 0;
+ Im.Checkbox(""u8, ref check);
+ Im.Line.SameInner();
+ id.Pop();
+ }
+ }
+
+ if (ImEx.InputOnDeactivation.Text("##Nothing"u8, container.Name, out string newName, "Optional Display Name..."u8))
+ editor.ModManager.OptionEditor.CombiningEditor.SetDisplayName(container, newName);
+ }
+}
diff --git a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs
index f491e7bc..d53410b1 100644
--- a/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs
+++ b/Penumbra/UI/ModsTab/Groups/ImcModGroupEditDrawer.cs
@@ -1,187 +1,189 @@
-using Dalamud.Interface;
-using Dalamud.Bindings.ImGui;
-using ImSharp;
-using OtterGui.Raii;
-using OtterGui.Text;
-using OtterGuiInternal.Utility;
-using Penumbra.GameData.Structs;
-using Penumbra.Meta;
-using Penumbra.Mods.Groups;
-using Penumbra.Mods.Manager.OptionEditor;
-using Penumbra.Mods.SubMods;
-using Penumbra.UI.AdvancedWindow.Meta;
-
-namespace Penumbra.UI.ModsTab.Groups;
-
-public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGroup group) : IModGroupEditDrawer
-{
- public void Draw()
- {
- var identifier = group.Identifier;
- var defaultEntry = ImcChecker.GetDefaultEntry(identifier, true).Entry;
- var entry = group.DefaultEntry;
- var changes = false;
-
- var width = editor.AvailableWidth.X - 3 * ImUtf8.ItemInnerSpacing.X - Im.Style.ItemSpacing.X - ImUtf8.CalcTextSize("All Variants"u8).X - ImUtf8.CalcTextSize("Only Attributes"u8).X - 2 * ImUtf8.FrameHeight;
- ImEx.TextFramed(identifier.ToString(), new Vector2(width, 0), Rgba32.Transparent);
-
- Im.Line.SameInner();
- var allVariants = group.AllVariants;
- if (ImUtf8.Checkbox("All Variants"u8, ref allVariants))
- editor.ModManager.OptionEditor.ImcEditor.ChangeAllVariants(group, allVariants);
- Im.Tooltip.OnHover("Make this group overwrite all corresponding variants for this identifier, not just the one specified."u8);
-
- Im.Line.Same();
- var onlyAttributes = group.OnlyAttributes;
- if (ImUtf8.Checkbox("Only Attributes"u8, ref onlyAttributes))
- editor.ModManager.OptionEditor.ImcEditor.ChangeOnlyAttributes(group, onlyAttributes);
- Im.Tooltip.OnHover("Only overwrite the attribute flags and take all the other values from the game's default entry instead of the one configured here.\n\nMainly useful if used with All Variants to keep the material IDs for each variant."u8);
-
- using (ImUtf8.Group())
- {
- ImUtf8.TextFrameAligned("Material ID"u8);
- ImUtf8.TextFrameAligned("VFX ID"u8);
- ImUtf8.TextFrameAligned("Decal ID"u8);
- }
-
- Im.Line.Same();
- using (ImUtf8.Group())
- {
- changes |= ImcMetaDrawer.DrawMaterialId(defaultEntry, ref entry, true);
- changes |= ImcMetaDrawer.DrawVfxId(defaultEntry, ref entry, true);
- changes |= ImcMetaDrawer.DrawDecalId(defaultEntry, ref entry, true);
- }
-
- Im.Line.Same(0, editor.PriorityWidth);
- using (ImUtf8.Group())
- {
- ImUtf8.TextFrameAligned("Material Animation ID"u8);
- ImUtf8.TextFrameAligned("Sound ID"u8);
- ImUtf8.TextFrameAligned("Can Be Disabled"u8);
- }
-
- Im.Line.Same();
-
- using (ImUtf8.Group())
- {
- changes |= ImcMetaDrawer.DrawMaterialAnimationId(defaultEntry, ref entry, true);
- changes |= ImcMetaDrawer.DrawSoundId(defaultEntry, ref entry, true);
- var canBeDisabled = group.CanBeDisabled;
- if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled))
- editor.ModManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled);
- }
-
- if (changes)
- editor.ModManager.OptionEditor.ImcEditor.ChangeDefaultEntry(group, entry);
-
- ImGui.Dummy(Vector2.Zero);
- DrawOptions();
- var attributeCache = new ImcAttributeCache(group);
- DrawNewOption(attributeCache);
- ImGui.Dummy(Vector2.Zero);
-
-
- using (ImUtf8.Group())
- {
- ImUtf8.TextFrameAligned("Default Attributes"u8);
- foreach (var option in group.OptionData.Where(o => !o.IsDisableSubMod))
- ImUtf8.TextFrameAligned(option.Name);
- }
-
- Im.Line.SameInner();
- using (ImUtf8.Group())
- {
- DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group);
- foreach (var (idx, option) in group.OptionData.Index().Where(o => !o.Item.IsDisableSubMod))
- {
- using var id = ImUtf8.PushId(idx);
- DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option,
- group.DefaultEntry.AttributeMask);
- }
- }
- }
-
- private void DrawOptions()
- {
- foreach (var (optionIdx, option) in group.OptionData.Index())
- {
- using var id = ImRaii.PushId(optionIdx);
- editor.DrawOptionPosition(group, option, optionIdx);
-
- Im.Line.SameInner();
- editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx);
-
- Im.Line.SameInner();
- editor.DrawOptionName(option);
-
- Im.Line.SameInner();
- editor.DrawOptionDescription(option);
-
- if (!option.IsDisableSubMod)
- {
- Im.Line.SameInner();
- editor.DrawOptionDelete(option);
- }
- }
- }
-
- private void DrawNewOption(in ImcAttributeCache cache)
- {
- var dis = cache.LowestUnsetMask == 0;
- var name = editor.DrawNewOptionBase(group, group.Options.Count);
- var validName = name.Length > 0;
- var tt = dis
- ? "No Free Attribute Slots for New Options..."u8
- : validName
- ? "Add a new option to this group."u8
- : "Please enter a name for the new option."u8;
- if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, default, !validName || dis))
- {
- editor.ModManager.OptionEditor.ImcEditor.AddOption(group, cache, name);
- editor.NewOptionName = null;
- }
- }
-
- private static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data,
- ushort? defaultMask = null)
- {
- for (var i = 0; i < ImcEntry.NumAttributes; ++i)
- {
- using var id = ImRaii.PushId(i);
- var flag = 1 << i;
- var value = (mask & flag) != 0;
- var inDefault = defaultMask.HasValue && (defaultMask & flag) != 0;
- using (ImRaii.Disabled(defaultMask != null && !cache.CanChange(i)))
- {
- if (inDefault ? NegativeCheckbox.Instance.Draw(""u8, ref value) : ImUtf8.Checkbox(""u8, ref value))
- {
- if (data is ImcModGroup g)
- editor.ChangeDefaultAttribute(g, cache, i, value);
- else
- editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value);
- }
- }
-
- Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "ABCDEFGHIJ"u8.Slice(i, 1));
- if (i != 9)
- Im.Line.SameInner();
- }
- }
-
- private sealed class NegativeCheckbox : OtterGui.Text.Widget.MultiStateCheckbox
- {
- public static readonly NegativeCheckbox Instance = new();
-
- protected override void RenderSymbol(bool value, Vector2 position, float size)
- {
- if (value)
- SymbolHelpers.RenderCross(ImGui.GetWindowDrawList(), position, ImGuiColor.CheckMark.Get().Color, size);
- }
-
- protected override bool NextValue(bool value)
- => !value;
-
- protected override bool PreviousValue(bool value)
- => !value;
- }
-}
+using ImSharp;
+using Luna;
+using Penumbra.GameData.Structs;
+using Penumbra.Meta;
+using Penumbra.Mods.Groups;
+using Penumbra.Mods.Manager.OptionEditor;
+using Penumbra.Mods.SubMods;
+using Penumbra.UI.AdvancedWindow.Meta;
+
+namespace Penumbra.UI.ModsTab.Groups;
+
+public readonly struct ImcModGroupEditDrawer(ModGroupEditDrawer editor, ImcModGroup group) : IModGroupEditDrawer
+{
+ public void Draw()
+ {
+ var identifier = group.Identifier;
+ var defaultEntry = ImcChecker.GetDefaultEntry(identifier, true).Entry;
+ var entry = group.DefaultEntry;
+ var changes = false;
+
+ var width = editor.AvailableWidth.X
+ - 3 * Im.Style.ItemInnerSpacing.X
+ - Im.Style.ItemSpacing.X
+ - Im.Font.CalculateSize("All Variants"u8).X
+ - Im.Font.CalculateSize("Only Attributes"u8).X
+ - 2 * Im.Style.FrameHeight;
+ ImEx.TextFramed(identifier.ToString(), new Vector2(width, 0), Rgba32.Transparent);
+
+ Im.Line.SameInner();
+ var allVariants = group.AllVariants;
+ if (Im.Checkbox("All Variants"u8, ref allVariants))
+ editor.ModManager.OptionEditor.ImcEditor.ChangeAllVariants(group, allVariants);
+ Im.Tooltip.OnHover("Make this group overwrite all corresponding variants for this identifier, not just the one specified."u8);
+
+ Im.Line.Same();
+ var onlyAttributes = group.OnlyAttributes;
+ if (Im.Checkbox("Only Attributes"u8, ref onlyAttributes))
+ editor.ModManager.OptionEditor.ImcEditor.ChangeOnlyAttributes(group, onlyAttributes);
+ Im.Tooltip.OnHover(
+ "Only overwrite the attribute flags and take all the other values from the game's default entry instead of the one configured here.\n\nMainly useful if used with All Variants to keep the material IDs for each variant."u8);
+
+ using (Im.Group())
+ {
+ ImEx.TextFrameAligned("Material ID"u8);
+ ImEx.TextFrameAligned("VFX ID"u8);
+ ImEx.TextFrameAligned("Decal ID"u8);
+ }
+
+ Im.Line.Same();
+ using (Im.Group())
+ {
+ changes |= ImcMetaDrawer.DrawMaterialId(defaultEntry, ref entry, true);
+ changes |= ImcMetaDrawer.DrawVfxId(defaultEntry, ref entry, true);
+ changes |= ImcMetaDrawer.DrawDecalId(defaultEntry, ref entry, true);
+ }
+
+ Im.Line.Same(0, editor.PriorityWidth);
+ using (Im.Group())
+ {
+ ImEx.TextFrameAligned("Material Animation ID"u8);
+ ImEx.TextFrameAligned("Sound ID"u8);
+ ImEx.TextFrameAligned("Can Be Disabled"u8);
+ }
+
+ Im.Line.Same();
+
+ using (Im.Group())
+ {
+ changes |= ImcMetaDrawer.DrawMaterialAnimationId(defaultEntry, ref entry, true);
+ changes |= ImcMetaDrawer.DrawSoundId(defaultEntry, ref entry, true);
+ var canBeDisabled = group.CanBeDisabled;
+ if (Im.Checkbox("##disabled"u8, ref canBeDisabled))
+ editor.ModManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled);
+ }
+
+ if (changes)
+ editor.ModManager.OptionEditor.ImcEditor.ChangeDefaultEntry(group, entry);
+
+ Im.Dummy(Vector2.Zero);
+ DrawOptions();
+ var attributeCache = new ImcAttributeCache(group);
+ DrawNewOption(attributeCache);
+ Im.Dummy(Vector2.Zero);
+
+
+ using (Im.Group())
+ {
+ ImEx.TextFrameAligned("Default Attributes"u8);
+ foreach (var option in group.OptionData.Where(o => !o.IsDisableSubMod))
+ ImEx.TextFrameAligned(option.Name);
+ }
+
+ Im.Line.SameInner();
+ using (Im.Group())
+ {
+ DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group);
+ foreach (var (idx, option) in group.OptionData.Index().Where(o => !o.Item.IsDisableSubMod))
+ {
+ using var id = Im.Id.Push(idx);
+ DrawAttributes(editor.ModManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option,
+ group.DefaultEntry.AttributeMask);
+ }
+ }
+ }
+
+ private void DrawOptions()
+ {
+ foreach (var (optionIdx, option) in group.OptionData.Index())
+ {
+ using var id = Im.Id.Push(optionIdx);
+ editor.DrawOptionPosition(group, option, optionIdx);
+
+ Im.Line.SameInner();
+ editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx);
+
+ Im.Line.SameInner();
+ editor.DrawOptionName(option);
+
+ Im.Line.SameInner();
+ editor.DrawOptionDescription(option);
+
+ if (!option.IsDisableSubMod)
+ {
+ Im.Line.SameInner();
+ editor.DrawOptionDelete(option);
+ }
+ }
+ }
+
+ private void DrawNewOption(in ImcAttributeCache cache)
+ {
+ var dis = cache.LowestUnsetMask is 0;
+ var name = editor.DrawNewOptionBase(group, group.Options.Count);
+ var validName = name.Length > 0;
+ var tt = dis
+ ? "No Free Attribute Slots for New Options..."u8
+ : validName
+ ? "Add a new option to this group."u8
+ : "Please enter a name for the new option."u8;
+ if (ImEx.Icon.Button(LunaStyle.AddObjectIcon, tt, !validName || dis))
+ {
+ editor.ModManager.OptionEditor.ImcEditor.AddOption(group, cache, name);
+ editor.NewOptionName = null;
+ }
+ }
+
+ private static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data,
+ ushort? defaultMask = null)
+ {
+ for (var i = 0; i < ImcEntry.NumAttributes; ++i)
+ {
+ using var id = Im.Id.Push(i);
+ var flag = 1 << i;
+ var value = (mask & flag) is not 0;
+ var inDefault = defaultMask.HasValue && (defaultMask & flag) is not 0;
+ using (Im.Disabled(defaultMask is not null && !cache.CanChange(i)))
+ {
+ if (inDefault ? NegativeCheckbox.Instance.Draw(""u8, ref value) : Im.Checkbox(""u8, ref value))
+ {
+ if (data is ImcModGroup g)
+ editor.ChangeDefaultAttribute(g, cache, i, value);
+ else
+ editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value);
+ }
+ }
+
+ Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "ABCDEFGHIJ"u8.Slice(i, 1));
+ if (i != 9)
+ Im.Line.SameInner();
+ }
+ }
+
+ private sealed class NegativeCheckbox : OtterGui.Text.Widget.MultiStateCheckbox
+ {
+ public static readonly NegativeCheckbox Instance = new();
+
+ protected override void RenderSymbol(bool value, Vector2 position, float size)
+ {
+ if (value)
+ Im.Render.Cross(Im.Window.DrawList, position, ImGuiColor.CheckMark.Get(), size);
+ }
+
+ protected override bool NextValue(bool value)
+ => !value;
+
+ protected override bool PreviousValue(bool value)
+ => !value;
+ }
+}
diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs
index 1aa58872..5ceb658e 100644
--- a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs
+++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs
@@ -1,370 +1,343 @@
-using Dalamud.Interface;
-using Dalamud.Interface.ImGuiNotification;
-using Dalamud.Bindings.ImGui;
-using ImSharp;
-using Luna;
-using OtterGui;
-using OtterGui.Raii;
-using OtterGui.Text;
-using OtterGui.Text.EndObjects;
-using Penumbra.Meta;
-using Penumbra.Mods;
-using Penumbra.Mods.Groups;
-using Penumbra.Mods.Manager;
-using Penumbra.Mods.Manager.OptionEditor;
-using Penumbra.Mods.Settings;
-using Penumbra.Mods.SubMods;
-using Penumbra.Services;
-using Penumbra.UI.Classes;
-
-namespace Penumbra.UI.ModsTab.Groups;
-
-public sealed class ModGroupEditDrawer(
- ModManager modManager,
- Configuration config,
- FilenameService filenames,
- DescriptionEditPopup descriptionPopup,
- ImcChecker imcChecker) : IUiService
-{
- private static ReadOnlySpan AcrossGroupsLabel
- => "##DragOptionAcross"u8;
-
- private static ReadOnlySpan InsideGroupLabel
- => "##DragOptionInside"u8;
-
- internal readonly ImcChecker ImcChecker = imcChecker;
- internal readonly ModManager ModManager = modManager;
- internal readonly Queue ActionQueue = new();
-
- internal Vector2 OptionIdxSelectable;
- internal Vector2 AvailableWidth;
- internal float PriorityWidth;
-
- internal string? NewOptionName;
- private IModGroup? _newOptionGroup;
-
- private Vector2 _buttonSize;
- private float _groupNameWidth;
- private float _optionNameWidth;
- private float _spacing;
- private bool _deleteEnabled;
-
- private string? _currentGroupName;
- private ModPriority? _currentGroupPriority;
- private IModGroup? _currentGroupEdited;
- private bool _isGroupNameValid = true;
-
- private IModGroup? _dragDropGroup;
- private IModOption? _dragDropOption;
- private bool _draggingAcross;
-
- internal string? CombiningDisplayName;
- internal int CombiningDisplayIndex;
-
- public void Draw(Mod mod)
- {
- PrepareStyle();
-
- using var id = ImUtf8.PushId("##GroupEdit"u8);
- foreach (var (groupIdx, group) in mod.Groups.Index())
- DrawGroup(group, groupIdx);
-
- while (ActionQueue.TryDequeue(out var action))
- action.Invoke();
- }
-
- private void DrawGroup(IModGroup group, int idx)
- {
- using var id = ImUtf8.PushId(idx);
- using var frame = ImRaii.FramedGroup($"Group #{idx + 1}");
- DrawGroupNameRow(group, idx);
- group.EditDrawer(this).Draw();
- }
-
- private void DrawGroupNameRow(IModGroup group, int idx)
- {
- DrawGroupName(group);
- Im.Line.SameInner();
- DrawGroupMoveButtons(group, idx);
- Im.Line.SameInner();
- DrawGroupOpenFile(group, idx);
- Im.Line.SameInner();
- DrawGroupDescription(group);
- Im.Line.SameInner();
- DrawGroupDelete(group);
- Im.Line.SameInner();
- DrawGroupPriority(group);
- }
-
- private void DrawGroupName(IModGroup group)
- {
- var text = _currentGroupEdited == group ? _currentGroupName ?? group.Name : group.Name;
- Im.Item.SetNextWidth(_groupNameWidth);
- using var border = ImRaii.PushFrameBorder(Im.Style.GlobalScale * 2, Colors.RegexWarningBorder, !_isGroupNameValid);
- if (ImUtf8.InputText("##GroupName"u8, ref text))
- {
- _currentGroupEdited = group;
- _currentGroupName = text;
- _isGroupNameValid = text == group.Name || ModGroupEditor.VerifyFileName(group.Mod, group, text, false);
- }
-
- if (ImGui.IsItemDeactivated())
- {
- if (_currentGroupName != null && _isGroupNameValid)
- ModManager.OptionEditor.RenameModGroup(group, _currentGroupName);
- _currentGroupName = null;
- _currentGroupEdited = null;
- _isGroupNameValid = true;
- }
-
- var tt = _isGroupNameValid
- ? "Change the Group name."u8
- : "Current name can not be used for this group."u8;
- Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, tt);
- }
-
- private void DrawGroupDelete(IModGroup group)
- {
- if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled))
- ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteModGroup(group));
-
- if (_deleteEnabled)
- Im.Tooltip.OnHover("Delete this option group."u8);
- else
- Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled,
- $"Delete this option group.\nHold {config.DeleteModModifier} while clicking to delete.");
- }
-
- private void DrawGroupPriority(IModGroup group)
- {
- var priority = _currentGroupEdited == group
- ? (_currentGroupPriority ?? group.Priority).Value
- : group.Priority.Value;
- Im.Item.SetNextWidth(PriorityWidth);
- if (ImGui.InputInt("##GroupPriority", ref priority))
- {
- _currentGroupEdited = group;
- _currentGroupPriority = new ModPriority(priority);
- }
-
- if (ImGui.IsItemDeactivated())
- {
- if (_currentGroupPriority.HasValue)
- ModManager.OptionEditor.ChangeGroupPriority(group, _currentGroupPriority.Value);
- _currentGroupEdited = null;
- _currentGroupPriority = null;
- }
-
- ImGuiUtil.HoverTooltip("Group Priority");
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void DrawGroupDescription(IModGroup group)
- {
- if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit group description."u8))
- descriptionPopup.Open(group);
- }
-
- private void DrawGroupMoveButtons(IModGroup group, int idx)
- {
- var isFirst = idx == 0;
- if (ImUtf8.IconButton(FontAwesomeIcon.ArrowUp, isFirst))
- ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx - 1));
-
- if (isFirst)
- Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Can not move this group further upwards."u8);
- else
- Im.Tooltip.OnHover($"Move this group up to group {idx}.");
-
-
- Im.Line.SameInner();
- var isLast = idx == group.Mod.Groups.Count - 1;
- if (ImUtf8.IconButton(FontAwesomeIcon.ArrowDown, isLast))
- ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx + 1));
-
- if (isLast)
- Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Can not move this group further downwards."u8);
- else
- Im.Tooltip.OnHover($"Move this group down to group {idx + 2}.");
- }
-
- private void DrawGroupOpenFile(IModGroup group, int idx)
- {
- var fileName = filenames.OptionGroupFile(group.Mod, idx, config.ReplaceNonAsciiOnImport);
- var fileExists = File.Exists(fileName);
- if (ImUtf8.IconButton(FontAwesomeIcon.FileExport, !fileExists))
- try
- {
- Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true });
- }
- catch (Exception e)
- {
- Penumbra.Messager.NotificationMessage(e, "Could not open editor.", NotificationType.Error);
- }
-
- if (fileExists)
- Im.Tooltip.OnHover($"Open the {group.Name} json file in the text editor of your choice.");
- else
- Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"The {group.Name} json file does not exist.");
- }
-
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx)
- {
- ImGui.AlignTextToFramePadding();
- ImUtf8.Selectable($"Option #{optionIdx + 1}", false, size: OptionIdxSelectable);
- Target(group, optionIdx);
- Source(option);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal void DrawOptionDefaultSingleBehaviour(IModGroup group, IModOption option, int optionIdx)
- {
- var isDefaultOption = group.DefaultSettings.AsIndex == optionIdx;
- if (ImUtf8.RadioButton("##default"u8, isDefaultOption))
- ModManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx));
- Im.Tooltip.OnHover($"Set {option.Name} as the default choice for this group.");
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal void DrawOptionDefaultMultiBehaviour(IModGroup group, IModOption option, int optionIdx)
- {
- var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx);
- if (ImUtf8.Checkbox("##default"u8, ref isDefaultOption))
- ModManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption));
- Im.Tooltip.OnHover($"{(isDefaultOption ? "Disable"u8 : "Enable"u8)} {option.Name} per default in this group.");
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal void DrawOptionDescription(IModOption option)
- {
- if (ImUtf8.IconButton(FontAwesomeIcon.Edit, "Edit option description."u8))
- descriptionPopup.Open(option);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal void DrawOptionPriority(MultiSubMod option)
- {
- var priority = option.Priority.Value;
- Im.Item.SetNextWidth(PriorityWidth);
- if (ImUtf8.InputScalarOnDeactivated("##Priority"u8, ref priority))
- ModManager.OptionEditor.MultiEditor.ChangeOptionPriority(option, new ModPriority(priority));
- Im.Tooltip.OnHover("Option priority inside the mod."u8);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal void DrawOptionName(IModOption option)
- {
- var name = option.Name;
- Im.Item.SetNextWidth(_optionNameWidth);
- if (ImUtf8.InputTextOnDeactivated("##Name"u8, ref name))
- ModManager.OptionEditor.RenameOption(option, name);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal void DrawOptionDelete(IModOption option)
- {
- if (ImUtf8.IconButton(FontAwesomeIcon.Trash, !_deleteEnabled))
- ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteOption(option));
-
- if (_deleteEnabled)
- Im.Tooltip.OnHover("Delete this option."u8);
- else
- Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled,
- $"Delete this option.\nHold {config.DeleteModModifier} while clicking to delete.");
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal string DrawNewOptionBase(IModGroup group, int count)
- {
- ImGui.AlignTextToFramePadding();
- ImUtf8.Selectable($"Option #{count + 1}", false, size: OptionIdxSelectable);
- Target(group, count);
-
- Im.Line.SameInner();
- ImUtf8.IconDummy();
-
- Im.Line.SameInner();
- Im.Item.SetNextWidth(_optionNameWidth);
- var newName = _newOptionGroup == group
- ? NewOptionName ?? string.Empty
- : string.Empty;
- if (ImUtf8.InputText("##newOption"u8, ref newName, "Add new option..."u8))
- {
- NewOptionName = newName;
- _newOptionGroup = group;
- }
-
- Im.Line.SameInner();
- return newName;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void Source(IModOption option)
- {
- using var source = ImUtf8.DragDropSource();
- if (!source)
- return;
-
- var across = option.Group is ITexToolsGroup;
-
- if (!DragDropSource.SetPayload(across ? AcrossGroupsLabel : InsideGroupLabel))
- {
- _dragDropGroup = option.Group;
- _dragDropOption = option;
- _draggingAcross = across;
- }
-
- ImUtf8.Text($"Dragging option {option.Name} from group {option.Group.Name}...");
- }
-
- private void Target(IModGroup group, int optionIdx)
- {
- if (_dragDropGroup != group
- && (!_draggingAcross || (_dragDropGroup != null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions })))
- return;
-
- using var target = ImUtf8.DragDropTarget();
- if (!target.IsDropping(_draggingAcross ? AcrossGroupsLabel : InsideGroupLabel))
- return;
-
- if (_dragDropGroup != null && _dragDropOption != null)
- {
- if (_dragDropGroup == group)
- {
- var sourceOption = _dragDropOption;
- ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveOption(sourceOption, optionIdx));
- }
- else
- {
- // Move from one group to another by deleting, then adding, then moving the option.
- var sourceOption = _dragDropOption;
- ActionQueue.Enqueue(() =>
- {
- ModManager.OptionEditor.DeleteOption(sourceOption);
- if (ModManager.OptionEditor.AddOption(group, sourceOption) is { } newOption)
- ModManager.OptionEditor.MoveOption(newOption, optionIdx);
- });
- }
- }
-
- _dragDropGroup = null;
- _dragDropOption = null;
- _draggingAcross = false;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void PrepareStyle()
- {
- var totalWidth = 400f * Im.Style.GlobalScale;
- _buttonSize = new Vector2(ImUtf8.FrameHeight);
- PriorityWidth = 50 * Im.Style.GlobalScale;
- AvailableWidth = new Vector2(totalWidth + 3 * _spacing + 2 * _buttonSize.X + PriorityWidth, 0);
- _groupNameWidth = totalWidth - 3 * (_buttonSize.X + _spacing);
- _spacing = Im.Style.ItemInnerSpacing.X;
- OptionIdxSelectable = ImUtf8.CalcTextSize("Option #88."u8);
- _optionNameWidth = totalWidth - OptionIdxSelectable.X - _buttonSize.X - 2 * _spacing;
- _deleteEnabled = config.DeleteModModifier.IsActive();
- }
-}
+using Dalamud.Interface;
+using Dalamud.Interface.ImGuiNotification;
+using ImSharp;
+using Luna;
+using OtterGui.Raii;
+using Penumbra.Meta;
+using Penumbra.Mods;
+using Penumbra.Mods.Groups;
+using Penumbra.Mods.Manager;
+using Penumbra.Mods.Manager.OptionEditor;
+using Penumbra.Mods.Settings;
+using Penumbra.Mods.SubMods;
+using Penumbra.Services;
+using Penumbra.UI.Classes;
+
+namespace Penumbra.UI.ModsTab.Groups;
+
+public sealed class ModGroupEditDrawer(
+ ModManager modManager,
+ Configuration config,
+ FilenameService filenames,
+ DescriptionEditPopup descriptionPopup,
+ ImcChecker imcChecker) : IUiService
+{
+ private static ReadOnlySpan AcrossGroupsLabel
+ => "##DragOptionAcross"u8;
+
+ private static ReadOnlySpan InsideGroupLabel
+ => "##DragOptionInside"u8;
+
+ internal readonly ImcChecker ImcChecker = imcChecker;
+ internal readonly ModManager ModManager = modManager;
+ internal readonly Queue ActionQueue = new();
+
+ internal Vector2 OptionIdxSelectable;
+ internal Vector2 AvailableWidth;
+ internal float PriorityWidth;
+
+ internal string? NewOptionName;
+ private IModGroup? _newOptionGroup;
+
+ private Vector2 _buttonSize;
+ private float _groupNameWidth;
+ private float _optionNameWidth;
+ private float _spacing;
+ private bool _deleteEnabled;
+
+ private string? _currentGroupName;
+ private IModGroup? _currentGroupEdited;
+ private bool _isGroupNameValid = true;
+
+ private IModGroup? _dragDropGroup;
+ private IModOption? _dragDropOption;
+ private bool _draggingAcross;
+
+ public void Draw(Mod mod)
+ {
+ PrepareStyle();
+
+ using var id = Im.Id.Push("ge"u8);
+ foreach (var (groupIdx, group) in mod.Groups.Index())
+ DrawGroup(group, groupIdx);
+
+ while (ActionQueue.TryDequeue(out var action))
+ action.Invoke();
+ }
+
+ private void DrawGroup(IModGroup group, int idx)
+ {
+ using var id = Im.Id.Push(idx);
+ using var frame = ImRaii.FramedGroup($"Group #{idx + 1}");
+ DrawGroupNameRow(group, idx);
+ group.EditDrawer(this).Draw();
+ }
+
+ private void DrawGroupNameRow(IModGroup group, int idx)
+ {
+ DrawGroupName(group);
+ Im.Line.SameInner();
+ DrawGroupMoveButtons(group, idx);
+ Im.Line.SameInner();
+ DrawGroupOpenFile(group, idx);
+ Im.Line.SameInner();
+ DrawGroupDescription(group);
+ Im.Line.SameInner();
+ DrawGroupDelete(group);
+ Im.Line.SameInner();
+ DrawGroupPriority(group);
+ }
+
+ private void DrawGroupName(IModGroup group)
+ {
+ var text = _currentGroupEdited == group ? _currentGroupName ?? group.Name : group.Name;
+ Im.Item.SetNextWidth(_groupNameWidth);
+ using var border = ImStyleBorder.Frame.Push(Colors.RegexWarningBorder, Im.Style.GlobalScale * 2, !_isGroupNameValid);
+ if (Im.Input.Text("##GroupName"u8, ref text))
+ {
+ _currentGroupEdited = group;
+ _currentGroupName = text;
+ _isGroupNameValid = text == group.Name || ModGroupEditor.VerifyFileName(group.Mod, group, text, false);
+ }
+
+ if (Im.Item.Deactivated)
+ {
+ if (_currentGroupName != null && _isGroupNameValid)
+ ModManager.OptionEditor.RenameModGroup(group, _currentGroupName);
+ _currentGroupName = null;
+ _currentGroupEdited = null;
+ _isGroupNameValid = true;
+ }
+
+ var tt = _isGroupNameValid
+ ? "Change the Group name."u8
+ : "Current name can not be used for this group."u8;
+ Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, tt);
+ }
+
+ private void DrawGroupDelete(IModGroup group)
+ {
+ if (ImEx.Icon.Button(LunaStyle.DeleteIcon, !_deleteEnabled))
+ ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteModGroup(group));
+
+ Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Delete this option group."u8);
+ if (!_deleteEnabled)
+ Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"Hold {config.DeleteModModifier} while clicking to delete.");
+ }
+
+ private void DrawGroupPriority(IModGroup group)
+ {
+ Im.Item.SetNextWidth(PriorityWidth);
+ if (ImEx.InputOnDeactivation.Scalar("##GroupPriority"u8, group.Priority.Value, out var newPriority))
+ ModManager.OptionEditor.ChangeGroupPriority(group, new ModPriority(newPriority));
+ Im.Tooltip.OnHover("Group Priority"u8);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void DrawGroupDescription(IModGroup group)
+ {
+ if (ImEx.Icon.Button(LunaStyle.EditIcon, "Edit group description."u8))
+ descriptionPopup.Open(group);
+ }
+
+ private void DrawGroupMoveButtons(IModGroup group, int idx)
+ {
+ var isFirst = idx is 0;
+ if (ImEx.Icon.Button(FontAwesomeIcon.ArrowUp.Icon(), isFirst))
+ ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx - 1));
+
+ if (isFirst)
+ Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Can not move this group further upwards."u8);
+ else
+ Im.Tooltip.OnHover($"Move this group up to group {idx}.");
+
+
+ Im.Line.SameInner();
+ var isLast = idx == group.Mod.Groups.Count - 1;
+ if (ImEx.Icon.Button(FontAwesomeIcon.ArrowDown.Icon(), isLast))
+ ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveModGroup(group, idx + 1));
+
+ if (isLast)
+ Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Can not move this group further downwards."u8);
+ else
+ Im.Tooltip.OnHover($"Move this group down to group {idx + 2}.");
+ }
+
+ private void DrawGroupOpenFile(IModGroup group, int idx)
+ {
+ var fileName = filenames.OptionGroupFile(group.Mod, idx, config.ReplaceNonAsciiOnImport);
+ var fileExists = File.Exists(fileName);
+ if (ImEx.Icon.Button(LunaStyle.OpenExternalIcon, !fileExists))
+ try
+ {
+ Process.Start(new ProcessStartInfo(fileName) { UseShellExecute = true });
+ }
+ catch (Exception e)
+ {
+ Penumbra.Messager.NotificationMessage(e, "Could not open editor.", NotificationType.Error);
+ }
+
+ if (fileExists)
+ Im.Tooltip.OnHover($"Open the {group.Name} json file in the text editor of your choice.");
+ else
+ Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"The {group.Name} json file does not exist.");
+ }
+
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx)
+ {
+ Im.Cursor.FrameAlign();
+ Im.Selectable($"Option #{optionIdx + 1}", size: OptionIdxSelectable);
+ Target(group, optionIdx);
+ Source(option);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal void DrawOptionDefaultSingleBehaviour(IModGroup group, IModOption option, int optionIdx)
+ {
+ var isDefaultOption = group.DefaultSettings.AsIndex == optionIdx;
+ if (Im.RadioButton("##default"u8, isDefaultOption))
+ ModManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx));
+ Im.Tooltip.OnHover($"Set {option.Name} as the default choice for this group.");
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal void DrawOptionDefaultMultiBehaviour(IModGroup group, IModOption option, int optionIdx)
+ {
+ var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx);
+ if (Im.Checkbox("##default"u8, ref isDefaultOption))
+ ModManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption));
+ Im.Tooltip.OnHover($"{(isDefaultOption ? "Disable"u8 : "Enable"u8)} {option.Name} per default in this group.");
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal void DrawOptionDescription(IModOption option)
+ {
+ if (ImEx.Icon.Button(LunaStyle.EditIcon, "Edit option description."u8))
+ descriptionPopup.Open(option);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal void DrawOptionPriority(MultiSubMod option)
+ {
+ Im.Item.SetNextWidth(PriorityWidth);
+ if (ImEx.InputOnDeactivation.Scalar("##Priority"u8, option.Priority.Value, out var newValue))
+ ModManager.OptionEditor.MultiEditor.ChangeOptionPriority(option, new ModPriority(newValue));
+ Im.Tooltip.OnHover("Option priority inside the mod."u8);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal void DrawOptionName(IModOption option)
+ {
+ Im.Item.SetNextWidth(_optionNameWidth);
+ if (ImEx.InputOnDeactivation.Text("##Name"u8, option.Name, out string newName))
+ ModManager.OptionEditor.RenameOption(option, newName);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal void DrawOptionDelete(IModOption option)
+ {
+ if (ImEx.Icon.Button(LunaStyle.DeleteIcon, !_deleteEnabled))
+ ActionQueue.Enqueue(() => ModManager.OptionEditor.DeleteOption(option));
+
+ if (_deleteEnabled)
+ Im.Tooltip.OnHover("Delete this option."u8);
+ else
+ Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled,
+ $"Delete this option.\nHold {config.DeleteModModifier} while clicking to delete.");
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal string DrawNewOptionBase(IModGroup group, int count)
+ {
+ Im.Cursor.FrameAlign();
+ Im.Selectable($"Option #{count + 1}", size: OptionIdxSelectable);
+ Target(group, count);
+
+ Im.Line.SameInner();
+ Im.FrameDummy();
+
+ Im.Line.SameInner();
+ Im.Item.SetNextWidth(_optionNameWidth);
+ var newName = _newOptionGroup == group
+ ? NewOptionName ?? string.Empty
+ : string.Empty;
+ if (Im.Input.Text("##newOption"u8, ref newName, "Add new option..."u8))
+ {
+ NewOptionName = newName;
+ _newOptionGroup = group;
+ }
+
+ Im.Line.SameInner();
+ return newName;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void Source(IModOption option)
+ {
+ using var source = Im.DragDrop.Source();
+ if (!source)
+ return;
+
+ var across = option.Group is ITexToolsGroup;
+
+ if (!source.SetPayload(across ? AcrossGroupsLabel : InsideGroupLabel))
+ {
+ _dragDropGroup = option.Group;
+ _dragDropOption = option;
+ _draggingAcross = across;
+ }
+
+ Im.Text($"Dragging option {option.Name} from group {option.Group.Name}...");
+ }
+
+ private void Target(IModGroup group, int optionIdx)
+ {
+ if (_dragDropGroup != group
+ && (!_draggingAcross || _dragDropGroup is not null && group is MultiModGroup { Options.Count: >= IModGroup.MaxMultiOptions }))
+ return;
+
+ using var target = Im.DragDrop.Target();
+ if (!target.IsDropping(_draggingAcross ? AcrossGroupsLabel : InsideGroupLabel))
+ return;
+
+ if (_dragDropGroup is not null && _dragDropOption is not null)
+ {
+ if (_dragDropGroup == group)
+ {
+ var sourceOption = _dragDropOption;
+ ActionQueue.Enqueue(() => ModManager.OptionEditor.MoveOption(sourceOption, optionIdx));
+ }
+ else
+ {
+ // Move from one group to another by deleting, then adding, then moving the option.
+ var sourceOption = _dragDropOption;
+ ActionQueue.Enqueue(() =>
+ {
+ ModManager.OptionEditor.DeleteOption(sourceOption);
+ if (ModManager.OptionEditor.AddOption(group, sourceOption) is { } newOption)
+ ModManager.OptionEditor.MoveOption(newOption, optionIdx);
+ });
+ }
+ }
+
+ _dragDropGroup = null;
+ _dragDropOption = null;
+ _draggingAcross = false;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void PrepareStyle()
+ {
+ var totalWidth = 400f * Im.Style.GlobalScale;
+ _buttonSize = new Vector2(Im.Style.FrameHeight);
+ PriorityWidth = 50 * Im.Style.GlobalScale;
+ AvailableWidth = new Vector2(totalWidth + 3 * _spacing + 2 * _buttonSize.X + PriorityWidth, 0);
+ _groupNameWidth = totalWidth - 3 * (_buttonSize.X + _spacing);
+ _spacing = Im.Style.ItemInnerSpacing.X;
+ OptionIdxSelectable = Im.Font.CalculateSize("Option #88."u8);
+ _optionNameWidth = totalWidth - OptionIdxSelectable.X - _buttonSize.X - 2 * _spacing;
+ _deleteEnabled = config.DeleteModModifier.IsActive();
+ }
+}
diff --git a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs
index 12e6ccbf..60a48543 100644
--- a/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs
+++ b/Penumbra/UI/ModsTab/Groups/SingleModGroupEditDrawer.cs
@@ -1,68 +1,65 @@
-using Dalamud.Interface;
-using Dalamud.Bindings.ImGui;
-using ImSharp;
-using OtterGui.Raii;
-using OtterGui.Text;
-using Penumbra.Mods.Groups;
-
-namespace Penumbra.UI.ModsTab.Groups;
-
-public readonly struct SingleModGroupEditDrawer(ModGroupEditDrawer editor, SingleModGroup group) : IModGroupEditDrawer
-{
- public void Draw()
- {
- foreach (var (optionIdx, option) in group.OptionData.Index())
- {
- using var id = ImRaii.PushId(optionIdx);
- editor.DrawOptionPosition(group, option, optionIdx);
-
- Im.Line.SameInner();
- editor.DrawOptionDefaultSingleBehaviour(group, option, optionIdx);
-
- Im.Line.SameInner();
- editor.DrawOptionName(option);
-
- Im.Line.SameInner();
- editor.DrawOptionDescription(option);
-
- Im.Line.SameInner();
- editor.DrawOptionDelete(option);
-
- Im.Line.SameInner();
- ImGui.Dummy(new Vector2(editor.PriorityWidth, 0));
- }
-
- DrawNewOption();
- DrawConvertButton();
- }
-
- private void DrawConvertButton()
- {
- var convertible = group.Options.Count <= IModGroup.MaxMultiOptions;
- var g = group;
- var e = editor.ModManager.OptionEditor.SingleEditor;
- if (ImUtf8.ButtonEx("Convert to Multi Group", editor.AvailableWidth, !convertible))
- editor.ActionQueue.Enqueue(() => e.ChangeToMulti(g));
- if (!convertible)
- Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled,
- "Can not convert to multi group since maximum number of options is exceeded."u8);
- }
-
- private void DrawNewOption()
- {
- var count = group.Options.Count;
- if (count >= int.MaxValue)
- return;
-
- var name = editor.DrawNewOptionBase(group, count);
-
- var validName = name.Length > 0;
- if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName
- ? "Add a new option to this group."u8
- : "Please enter a name for the new option."u8, default, !validName))
- {
- editor.ModManager.OptionEditor.SingleEditor.AddOption(group, name);
- editor.NewOptionName = null;
- }
- }
-}
+using ImSharp;
+using Luna;
+using Penumbra.Mods.Groups;
+
+namespace Penumbra.UI.ModsTab.Groups;
+
+public readonly struct SingleModGroupEditDrawer(ModGroupEditDrawer editor, SingleModGroup group) : IModGroupEditDrawer
+{
+ public void Draw()
+ {
+ foreach (var (optionIdx, option) in group.OptionData.Index())
+ {
+ using var id = Im.Id.Push(optionIdx);
+ editor.DrawOptionPosition(group, option, optionIdx);
+
+ Im.Line.SameInner();
+ editor.DrawOptionDefaultSingleBehaviour(group, option, optionIdx);
+
+ Im.Line.SameInner();
+ editor.DrawOptionName(option);
+
+ Im.Line.SameInner();
+ editor.DrawOptionDescription(option);
+
+ Im.Line.SameInner();
+ editor.DrawOptionDelete(option);
+
+ Im.Line.SameInner();
+ Im.Dummy(new Vector2(editor.PriorityWidth, 0));
+ }
+
+ DrawNewOption();
+ DrawConvertButton();
+ }
+
+ private void DrawConvertButton()
+ {
+ var convertible = group.Options.Count <= IModGroup.MaxMultiOptions;
+ var g = group;
+ var e = editor.ModManager.OptionEditor.SingleEditor;
+ if (ImEx.Button("Convert to Multi Group"u8, editor.AvailableWidth, !convertible))
+ editor.ActionQueue.Enqueue(() => e.ChangeToMulti(g));
+ if (!convertible)
+ Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled,
+ "Can not convert to multi group since maximum number of options is exceeded."u8);
+ }
+
+ private void DrawNewOption()
+ {
+ var count = group.Options.Count;
+ if (count >= int.MaxValue)
+ return;
+
+ var name = editor.DrawNewOptionBase(group, count);
+
+ var validName = name.Length > 0;
+ if (ImEx.Icon.Button(LunaStyle.AddObjectIcon, validName
+ ? "Add a new option to this group."u8
+ : "Please enter a name for the new option."u8, !validName))
+ {
+ editor.ModManager.OptionEditor.SingleEditor.AddOption(group, name);
+ editor.NewOptionName = null;
+ }
+ }
+}
diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs
index 242d0bce..533de9c1 100644
--- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs
+++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs
@@ -184,7 +184,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector SortMode
- => _config.SortMode;
+ => ISortMode.FoldersFirst;
protected override uint ExpandedFolderColor
=> ColorId.FolderExpanded.Value().Color;
diff --git a/Penumbra/UI/ModsTab/ModFilter.cs b/Penumbra/UI/ModsTab/ModFilter.cs
index 5f120b7d..514408f4 100644
--- a/Penumbra/UI/ModsTab/ModFilter.cs
+++ b/Penumbra/UI/ModsTab/ModFilter.cs
@@ -31,7 +31,7 @@ public static class ModFilterExtensions
{
public const ModFilter UnfilteredStateMods = (ModFilter)((1 << 22) - 1);
- public static IReadOnlyList<(ModFilter On, ModFilter Off, string Name)> TriStatePairs =
+ public static readonly IReadOnlyList<(ModFilter On, ModFilter Off, string Name)> TriStatePairs =
[
(ModFilter.Enabled, ModFilter.Disabled, "Enabled"),
(ModFilter.IsNew, ModFilter.NotNew, "Newly Imported"),
@@ -43,7 +43,7 @@ public static class ModFilterExtensions
(ModFilter.Temporary, ModFilter.NotTemporary, "Temporary"),
];
- public static IReadOnlyList> Groups =
+ public static readonly IReadOnlyList> Groups =
[
[
(ModFilter.NoConflict, "Has No Conflicts"),
diff --git a/Penumbra/UI/ModsTab/ModPanel.cs b/Penumbra/UI/ModsTab/ModPanel.cs
index 10083804..cfc55cd3 100644
--- a/Penumbra/UI/ModsTab/ModPanel.cs
+++ b/Penumbra/UI/ModsTab/ModPanel.cs
@@ -1,74 +1,78 @@
-using Dalamud.Plugin;
-using ImSharp;
-using Penumbra.Mods;
-using Penumbra.Services;
-
-namespace Penumbra.UI.ModsTab;
-
-public class ModPanel : IDisposable, Luna.IUiService
-{
- private readonly MultiModPanel _multiModPanel;
- private readonly ModSelection _selection;
- private readonly ModPanelHeader _header;
- private readonly ModPanelTabBar _tabs;
- private bool _resetCursor;
-
- public ModPanel(IDalamudPluginInterface pi, ModSelection selection, ModPanelTabBar tabs,
- MultiModPanel multiModPanel, CommunicatorService communicator)
- {
- _selection = selection;
- _tabs = tabs;
- _multiModPanel = multiModPanel;
- _header = new ModPanelHeader(pi, communicator);
- _selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModPanel);
- OnSelectionChange(new ModSelection.Arguments(null, _selection.Mod));
- }
-
- public void Draw()
- {
- if (!_valid)
- {
- _multiModPanel.Draw();
- return;
- }
-
- if (_resetCursor)
- {
- _resetCursor = false;
- Im.Scroll.X = 0;
- }
-
- _header.Draw();
- Im.Cursor.X += Im.Scroll.X;
- using var child = Im.Child.Begin("Tabs"u8,
- Im.ContentRegion.Available with { X = Im.Window.MaximumContentRegion.X - Im.Window.MinimumContentRegion.X });
- if (child)
- _tabs.Draw(_mod);
- }
-
- public void Dispose()
- {
- _selection.Unsubscribe(OnSelectionChange);
- _header.Dispose();
- }
-
- private bool _valid;
- private Mod _mod = null!;
-
- private void OnSelectionChange(in ModSelection.Arguments arguments)
- {
- _resetCursor = true;
- if (arguments.NewSelection is null || _selection.Mod is null)
- {
- _valid = false;
- }
- else
- {
- _valid = true;
- _mod = arguments.NewSelection;
- _header.ChangeMod(_mod);
- _tabs.Settings.Reset();
- _tabs.Edit.Reset();
- }
- }
-}
+using Dalamud.Plugin;
+using ImSharp;
+using Luna;
+using Penumbra.Mods;
+using Penumbra.Services;
+
+namespace Penumbra.UI.ModsTab;
+
+public class ModPanel : IDisposable, IPanel
+{
+ private readonly MultiModPanel _multiModPanel;
+ private readonly ModSelection _selection;
+ private readonly ModPanelHeader _header;
+ private readonly ModPanelTabBar _tabs;
+ private bool _resetCursor;
+
+ public ModPanel(IDalamudPluginInterface pi, ModSelection selection, ModPanelTabBar tabs,
+ MultiModPanel multiModPanel, CommunicatorService communicator)
+ {
+ _selection = selection;
+ _tabs = tabs;
+ _multiModPanel = multiModPanel;
+ _header = new ModPanelHeader(pi, communicator);
+ _selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModPanel);
+ OnSelectionChange(new ModSelection.Arguments(null, _selection.Mod));
+ }
+
+ public ReadOnlySpan Id
+ => "MP"u8;
+
+ public void Draw()
+ {
+ if (!_valid)
+ {
+ _multiModPanel.Draw();
+ return;
+ }
+
+ if (_resetCursor)
+ {
+ _resetCursor = false;
+ Im.Scroll.X = 0;
+ }
+
+ _header.Draw();
+ Im.Cursor.X += Im.Scroll.X;
+ using var child = Im.Child.Begin("Tabs"u8,
+ Im.ContentRegion.Available with { X = Im.Window.MaximumContentRegion.X - Im.Window.MinimumContentRegion.X });
+ if (child)
+ _tabs.Draw(_mod);
+ }
+
+ public void Dispose()
+ {
+ _selection.Unsubscribe(OnSelectionChange);
+ _header.Dispose();
+ }
+
+ private bool _valid;
+ private Mod _mod = null!;
+
+ private void OnSelectionChange(in ModSelection.Arguments arguments)
+ {
+ _resetCursor = true;
+ if (arguments.NewSelection is null || _selection.Mod is null)
+ {
+ _valid = false;
+ }
+ else
+ {
+ _valid = true;
+ _mod = arguments.NewSelection;
+ _header.ChangeMod(_mod);
+ _tabs.Settings.Reset();
+ _tabs.Edit.Reset();
+ }
+ }
+}
diff --git a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs
index 52992deb..fcb5b956 100644
--- a/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs
+++ b/Penumbra/UI/ModsTab/ModPanelCollectionsTab.cs
@@ -1,138 +1,137 @@
-using Dalamud.Bindings.ImGui;
-using ImSharp;
-using Luna;
-using Penumbra.Collections;
-using Penumbra.Collections.Manager;
-using Penumbra.Mods;
-using Penumbra.UI.Classes;
-
-namespace Penumbra.UI.ModsTab;
-
-public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSelector selector) : ITab
-{
- private enum ModState
- {
- Enabled,
- Disabled,
- Unconfigured,
- }
-
- private readonly List<(ModCollection, ModCollection, uint, ModState)> _cache = [];
-
- public ReadOnlySpan Label
- => "Collections"u8;
+using ImSharp;
+using Luna;
+using Penumbra.Collections;
+using Penumbra.Collections.Manager;
+using Penumbra.Mods;
+using Penumbra.UI.Classes;
- public ModPanelTab Identifier
+namespace Penumbra.UI.ModsTab;
+
+public class ModPanelCollectionsTab(CollectionManager manager, ModFileSystemSelector selector) : ITab
+{
+ private enum ModState
+ {
+ Enabled,
+ Disabled,
+ Unconfigured,
+ }
+
+ private readonly List<(ModCollection, ModCollection, uint, ModState)> _cache = [];
+
+ public ReadOnlySpan Label
+ => "Collections"u8;
+
+ public ModPanelTab Identifier
=> ModPanelTab.Collections;
-
- public void DrawContent()
- {
- var (direct, inherited) = CountUsage(selector.Selected!);
- Im.Line.New();
- switch (direct)
- {
- case 1: Im.Text("This Mod is directly configured in 1 collection."u8); break;
- case 0: Im.Text("This mod is entirely unused."u8, Colors.RegexWarningBorder); break;
- default: Im.Text($"This Mod is directly configured in {direct} collections."); break;
- }
- if (inherited > 0)
- Im.Text($"It is also implicitly used in {inherited} {(inherited == 1 ? "collection" : "collections")} through inheritance.");
-
- Im.Line.New();
- Im.Separator();
- Im.Line.New();
- using var table = Im.Table.Begin("##modCollections"u8, 3, TableFlags.SizingFixedFit | TableFlags.RowBackground);
- if (!table)
- return;
-
- var size = Im.Font.CalculateSize(ToText(ModState.Unconfigured)).X + 20 * Im.Style.GlobalScale;
- var collectionSize = 200 * Im.Style.GlobalScale;
- table.SetupColumn("Collection"u8, TableColumnFlags.WidthFixed, collectionSize);
- table.SetupColumn("State"u8, TableColumnFlags.WidthFixed, size);
- table.SetupColumn("Inherited From"u8, TableColumnFlags.WidthFixed, collectionSize);
-
- ImGui.TableHeadersRow();
- foreach (var (idx, (collection, parent, color, state)) in _cache.Index())
- {
- using var id = Im.Id.Push(idx);
- table.DrawColumn(collection.Identity.Name);
-
- table.NextColumn();
- Im.Text(ToText(state), color);
-
- using (var context = Im.Popup.BeginContextItem("Context"u8))
- {
- if (context)
- {
- Im.Text(collection.Identity.Name);
- Im.Separator();
- using (Im.Disabled(state is ModState.Enabled && parent == collection))
- {
- if (Im.Menu.Item("Enable"u8))
- {
- if (parent != collection)
- manager.Editor.SetModInheritance(collection, selector.Selected!, false);
- manager.Editor.SetModState(collection, selector.Selected!, true);
- }
- }
-
- using (Im.Disabled(state is ModState.Disabled && parent == collection))
- {
- if (Im.Menu.Item("Disable"u8))
- {
- if (parent != collection)
- manager.Editor.SetModInheritance(collection, selector.Selected!, false);
- manager.Editor.SetModState(collection, selector.Selected!, false);
- }
- }
-
- using (Im.Disabled(parent != collection))
- {
- if (Im.Menu.Item("Inherit"u8))
- manager.Editor.SetModInheritance(collection, selector.Selected!, true);
- }
- }
- }
-
- table.DrawColumn(parent == collection ? StringU8.Empty : parent.Identity.Name);
- }
- }
-
- private static ReadOnlySpan ToText(ModState state)
- => state switch
- {
- ModState.Unconfigured => "Unconfigured"u8,
- ModState.Enabled => "Enabled"u8,
- ModState.Disabled => "Disabled"u8,
- _ => "Unknown"u8,
- };
-
- private (int Direct, int Inherited) CountUsage(Mod mod)
- {
- _cache.Clear();
- var undefined = ColorId.UndefinedMod.Value();
- var enabled = ColorId.EnabledMod.Value();
- var inherited = ColorId.InheritedMod.Value();
- var disabled = ColorId.DisabledMod.Value();
- var disInherited = ColorId.InheritedDisabledMod.Value();
- var directCount = 0;
- var inheritedCount = 0;
- foreach (var collection in manager.Storage)
- {
- var (settings, parent) = collection.GetInheritedSettings(mod.Index);
- var (color, text) = settings == null
- ? (undefined, ModState.Unconfigured)
- : settings.Enabled
- ? (parent == collection ? enabled : inherited, ModState.Enabled)
- : (parent == collection ? disabled : disInherited, ModState.Disabled);
- _cache.Add((collection, parent, color.Color, text));
-
- if (color == enabled)
- ++directCount;
- else if (color == inherited)
- ++inheritedCount;
- }
-
- return (directCount, inheritedCount);
- }
-}
+
+ public void DrawContent()
+ {
+ var (direct, inherited) = CountUsage(selector.Selected!);
+ Im.Line.New();
+ switch (direct)
+ {
+ case 1: Im.Text("This Mod is directly configured in 1 collection."u8); break;
+ case 0: Im.Text("This mod is entirely unused."u8, Colors.RegexWarningBorder); break;
+ default: Im.Text($"This Mod is directly configured in {direct} collections."); break;
+ }
+ if (inherited > 0)
+ Im.Text($"It is also implicitly used in {inherited} {(inherited == 1 ? "collection" : "collections")} through inheritance.");
+
+ Im.Line.New();
+ Im.Separator();
+ Im.Line.New();
+ using var table = Im.Table.Begin("##modCollections"u8, 3, TableFlags.SizingFixedFit | TableFlags.RowBackground);
+ if (!table)
+ return;
+
+ var size = Im.Font.CalculateSize(ToText(ModState.Unconfigured)).X + 20 * Im.Style.GlobalScale;
+ var collectionSize = 200 * Im.Style.GlobalScale;
+ table.SetupColumn("Collection"u8, TableColumnFlags.WidthFixed, collectionSize);
+ table.SetupColumn("State"u8, TableColumnFlags.WidthFixed, size);
+ table.SetupColumn("Inherited From"u8, TableColumnFlags.WidthFixed, collectionSize);
+ table.HeaderRow();
+
+ foreach (var (idx, (collection, parent, color, state)) in _cache.Index())
+ {
+ using var id = Im.Id.Push(idx);
+ table.DrawColumn(collection.Identity.Name);
+
+ table.NextColumn();
+ Im.Text(ToText(state), color);
+
+ using (var context = Im.Popup.BeginContextItem("Context"u8))
+ {
+ if (context)
+ {
+ Im.Text(collection.Identity.Name);
+ Im.Separator();
+ using (Im.Disabled(state is ModState.Enabled && parent == collection))
+ {
+ if (Im.Menu.Item("Enable"u8))
+ {
+ if (parent != collection)
+ manager.Editor.SetModInheritance(collection, selector.Selected!, false);
+ manager.Editor.SetModState(collection, selector.Selected!, true);
+ }
+ }
+
+ using (Im.Disabled(state is ModState.Disabled && parent == collection))
+ {
+ if (Im.Menu.Item("Disable"u8))
+ {
+ if (parent != collection)
+ manager.Editor.SetModInheritance(collection, selector.Selected!, false);
+ manager.Editor.SetModState(collection, selector.Selected!, false);
+ }
+ }
+
+ using (Im.Disabled(parent != collection))
+ {
+ if (Im.Menu.Item("Inherit"u8))
+ manager.Editor.SetModInheritance(collection, selector.Selected!, true);
+ }
+ }
+ }
+
+ table.DrawColumn(parent == collection ? StringU8.Empty : parent.Identity.Name);
+ }
+ }
+
+ private static ReadOnlySpan ToText(ModState state)
+ => state switch
+ {
+ ModState.Unconfigured => "Unconfigured"u8,
+ ModState.Enabled => "Enabled"u8,
+ ModState.Disabled => "Disabled"u8,
+ _ => "Unknown"u8,
+ };
+
+ private (int Direct, int Inherited) CountUsage(Mod mod)
+ {
+ _cache.Clear();
+ var undefined = ColorId.UndefinedMod.Value();
+ var enabled = ColorId.EnabledMod.Value();
+ var inherited = ColorId.InheritedMod.Value();
+ var disabled = ColorId.DisabledMod.Value();
+ var disInherited = ColorId.InheritedDisabledMod.Value();
+ var directCount = 0;
+ var inheritedCount = 0;
+ foreach (var collection in manager.Storage)
+ {
+ var (settings, parent) = collection.GetInheritedSettings(mod.Index);
+ var (color, text) = settings == null
+ ? (undefined, ModState.Unconfigured)
+ : settings.Enabled
+ ? (parent == collection ? enabled : inherited, ModState.Enabled)
+ : (parent == collection ? disabled : disInherited, ModState.Disabled);
+ _cache.Add((collection, parent, color.Color, text));
+
+ if (color == enabled)
+ ++directCount;
+ else if (color == inherited)
+ ++inheritedCount;
+ }
+
+ return (directCount, inheritedCount);
+ }
+}
diff --git a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs
index a0fe226f..be4d741c 100644
--- a/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs
+++ b/Penumbra/UI/ModsTab/ModPanelDescriptionTab.cs
@@ -1,58 +1,58 @@
-using Dalamud.Bindings.ImGui;
-using ImSharp;
-using Luna;
-using OtterGui.Widgets;
-using Penumbra.Mods.Manager;
-
-namespace Penumbra.UI.ModsTab;
-
-public class ModPanelDescriptionTab(
- ModFileSystemSelector selector,
- TutorialService tutorial,
- ModManager modManager,
- PredefinedTagManager predefinedTagsConfig)
- : ITab