From cabcaadde3607a492ceb20ee1006457316ce30e6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 28 Dec 2025 00:06:32 +0100 Subject: [PATCH] Current state, all done except for file system selector. --- Luna | 2 +- .../Collections/Manager/CollectionStorage.cs | 773 +++++++++--------- Penumbra/Communication/CollectionChange.cs | 4 +- Penumbra/Communication/CollectionRename.cs | 20 + Penumbra/EphemeralConfig.cs | 38 +- Penumbra/Mods/Editor/ModEditor.cs | 19 +- Penumbra/Mods/Editor/ModFileCollection.cs | 405 +++++---- Penumbra/Services/CommunicatorService.cs | 3 + Penumbra/Services/ConfigMigrationService.cs | 2 +- Penumbra/Services/StainService.cs | 265 +++--- Penumbra/UI/AdvancedWindow/FileEditor.cs | 107 ++- Penumbra/UI/AdvancedWindow/ItemSelector.cs | 94 +++ Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 76 +- .../Materials/MtrlTab.ColorTable.cs | 16 +- .../Materials/MtrlTab.CommonColorTable.cs | 10 +- .../Materials/MtrlTab.LegacyColorTable.cs | 28 +- .../Materials/MtrlTab.LivePreview.cs | 6 +- .../UI/AdvancedWindow/Materials/MtrlTab.cs | 8 +- Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs | 6 +- .../AdvancedWindow/ModEditWindow.Deformers.cs | 40 +- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 93 +-- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 2 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 1 + Penumbra/UI/AdvancedWindow/OverviewTable.cs | 141 ++++ Penumbra/UI/Classes/CollectionSelectHeader.cs | 6 +- Penumbra/UI/Classes/IncognitoService.cs | 2 +- Penumbra/UI/CollectionTab/CollectionFilter.cs | 13 + Penumbra/UI/CollectionTab/CollectionPanel.cs | 30 +- .../UI/CollectionTab/CollectionSelector.cs | 182 ++--- Penumbra/UI/Combos/SingleGroupCombo.cs | 52 ++ Penumbra/UI/Combos/StainCombo.cs | 118 --- Penumbra/UI/MainWindow/MainWindow.cs | 1 - Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs | 64 +- Penumbra/UI/ModsTab/ModPanelConflictsTab.cs | 1 - .../UI/ModsTab/Selector/ModFileSystemCache.cs | 10 +- Penumbra/UI/Tabs/CollectionButtonFooter.cs | 90 ++ .../UI/Tabs/CollectionModeHeaderFooter.cs | 66 ++ Penumbra/UI/Tabs/CollectionsTab.cs | 193 +---- Penumbra/UI/Tabs/Debug/ActionTmbListDrawer.cs | 51 ++ Penumbra/UI/Tabs/Debug/DebugTab.cs | 143 ++-- Penumbra/UI/Tabs/Debug/EmoteListDrawer.cs | 79 ++ 41 files changed, 1749 insertions(+), 1511 deletions(-) create mode 100644 Penumbra/Communication/CollectionRename.cs create mode 100644 Penumbra/UI/AdvancedWindow/ItemSelector.cs create mode 100644 Penumbra/UI/AdvancedWindow/OverviewTable.cs create mode 100644 Penumbra/UI/CollectionTab/CollectionFilter.cs create mode 100644 Penumbra/UI/Combos/SingleGroupCombo.cs delete mode 100644 Penumbra/UI/Combos/StainCombo.cs create mode 100644 Penumbra/UI/Tabs/CollectionButtonFooter.cs create mode 100644 Penumbra/UI/Tabs/CollectionModeHeaderFooter.cs create mode 100644 Penumbra/UI/Tabs/Debug/ActionTmbListDrawer.cs create mode 100644 Penumbra/UI/Tabs/Debug/EmoteListDrawer.cs diff --git a/Luna b/Luna index e52d0dab..d81c7881 160000 --- a/Luna +++ b/Luna @@ -1 +1 @@ -Subproject commit e52d0dab9fd7f64d108125b79e387052fae2434f +Subproject commit d81c788133b8b557febbad0bf74baee9588215eb diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index b35f98e7..26478c3a 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -1,377 +1,396 @@ -using Dalamud.Interface.ImGuiNotification; -using Luna; -using Penumbra.Communication; -using Penumbra.Mods.Manager; -using Penumbra.Mods.Manager.OptionEditor; -using Penumbra.Mods.Settings; -using Penumbra.Services; - -namespace Penumbra.Collections.Manager; - -/// A contiguously incrementing ID managed by the CollectionCreator. -public readonly record struct LocalCollectionId(int Id) : IAdditionOperators -{ - public static readonly LocalCollectionId Zero = new(0); - - public static LocalCollectionId operator +(LocalCollectionId left, int right) - => new(left.Id + right); -} - -public class CollectionStorage : IReadOnlyList, IDisposable, IService -{ - private readonly CommunicatorService _communicator; - private readonly SaveService _saveService; - private readonly ModStorage _modStorage; - - public ModCollection Create(string name, int index, ModCollection? duplicate) - { - var newCollection = duplicate?.Duplicate(name, CurrentCollectionId, index) - ?? ModCollection.CreateEmpty(name, CurrentCollectionId, index, _modStorage.Count); - _collectionsByLocal[CurrentCollectionId] = newCollection; - CurrentCollectionId += 1; - return newCollection; - } - - public ModCollection CreateFromData(Guid id, string name, int version, Dictionary allSettings, - IReadOnlyList inheritances) - { - var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, - new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, inheritances); - _collectionsByLocal[CurrentCollectionId] = newCollection; - CurrentCollectionId += 1; - return newCollection; - } - - public ModCollection CreateTemporary(string name, int index, int globalChangeCounter) - { - var newCollection = ModCollection.CreateTemporary(name, CurrentCollectionId, index, globalChangeCounter); - _collectionsByLocal[CurrentCollectionId] = newCollection; - CurrentCollectionId += 1; - return newCollection; - } - - public void Delete(ModCollection collection) - => _collectionsByLocal.Remove(collection.Identity.LocalId); - - /// The empty collection is always available at Index 0. - private readonly List _collections = - [ - ModCollection.Empty, - ]; - - /// A list of all collections ever created still existing by their local id. - private readonly Dictionary - _collectionsByLocal = new() { [LocalCollectionId.Zero] = ModCollection.Empty }; - - - public readonly ModCollection DefaultNamed; - - /// Incremented by 1 because the empty collection gets Zero. - public LocalCollectionId CurrentCollectionId { get; private set; } = LocalCollectionId.Zero + 1; - - /// Default enumeration skips the empty collection. - public IEnumerator GetEnumerator() - => _collections.Skip(1).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public int Count - => _collections.Count; - - public ModCollection this[int index] - => _collections[index]; - - /// Find a collection by its name. If the name is empty or None, the empty collection is returned. - public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection) - { - if (name.Length != 0) - return _collections.FindFirst(c => string.Equals(c.Identity.Name, name, StringComparison.OrdinalIgnoreCase), out collection); - - collection = ModCollection.Empty; - return true; - } - - /// Find a collection by its id. If the GUID is empty, the empty collection is returned. - public bool ById(Guid id, [NotNullWhen(true)] out ModCollection? collection) - { - if (id != Guid.Empty) - return _collections.FindFirst(c => c.Identity.Id == id, out collection); - - collection = ModCollection.Empty; - return true; - } - - /// Find a collection by an identifier, which is interpreted as a GUID first and if it does not correspond to one, as a name. - public bool ByIdentifier(string identifier, [NotNullWhen(true)] out ModCollection? collection) - { - if (Guid.TryParse(identifier, out var guid)) - return ById(guid, out collection); - - return ByName(identifier, out collection); - } - - /// Find a collection by its local ID if it still exists, otherwise returns the empty collection. - public ModCollection ByLocalId(LocalCollectionId localId) - => _collectionsByLocal.TryGetValue(localId, out var coll) ? coll : ModCollection.Empty; - - public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage) - { - _communicator = communicator; - _saveService = saveService; - _modStorage = modStorage; - _communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted, ModDiscoveryStarted.Priority.CollectionStorage); - _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionStorage); - _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.CollectionStorage); - _communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.CollectionStorage); - _communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.CollectionStorage); - ReadCollections(out DefaultNamed); - } - - public void Dispose() - { - _communicator.ModDiscoveryStarted.Unsubscribe(OnModDiscoveryStarted); - _communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished); - _communicator.ModPathChanged.Unsubscribe(OnModPathChange); - _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); - _communicator.ModFileChanged.Unsubscribe(OnModFileChanged); - } - - /// - /// Add a new collection of the given name. - /// If duplicate is not-null, the new collection will be a duplicate of it. - /// If the name of the collection would result in an already existing filename, skip it. - /// Returns true if the collection was successfully created and fires a Inactive event. - /// Also sets the current collection to the new collection afterwards. - /// - public bool AddCollection(string name, ModCollection? duplicate) - { - if (name.Length == 0) - return false; - - var newCollection = Create(name, _collections.Count, duplicate); - _collections.Add(newCollection); - _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); - Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.Identity.AnonymizedName}.", NotificationType.Success, false); - _communicator.CollectionChange.Invoke(new CollectionChange.Arguments(CollectionType.Inactive, null, newCollection, string.Empty)); - return true; - } - - /// - /// Remove the given collection if it exists and is neither the empty nor the default-named collection. - /// - public bool RemoveCollection(ModCollection collection) - { - if (collection.Identity.Index <= ModCollection.Empty.Identity.Index || collection.Identity.Index >= _collections.Count) - { - Penumbra.Messager.NotificationMessage("Can not remove the empty collection.", NotificationType.Error, false); - return false; - } - - if (collection.Identity.Index == DefaultNamed.Identity.Index) - { - Penumbra.Messager.NotificationMessage("Can not remove the default collection.", NotificationType.Error, false); - return false; - } - - Delete(collection); - _saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection)); - _collections.RemoveAt(collection.Identity.Index); - // Update indices. - for (var i = collection.Identity.Index; i < Count; ++i) - _collections[i].Identity.Index = i; - _collectionsByLocal.Remove(collection.Identity.LocalId); - - Penumbra.Messager.NotificationMessage($"Deleted collection {collection.Identity.AnonymizedName}.", NotificationType.Success, false); - _communicator.CollectionChange.Invoke(new CollectionChange.Arguments(CollectionType.Inactive, collection, null, string.Empty)); - return true; - } - - /// Remove all settings for not currently-installed mods from the given collection. - public int CleanUnavailableSettings(ModCollection collection) - { - var count = collection.Settings.Unused.Count; - if (count > 0) - { - ((Dictionary)collection.Settings.Unused).Clear(); - _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); - } - - return count; - } - - /// Remove a specific setting for not currently-installed mods from the given collection. - public void CleanUnavailableSetting(ModCollection collection, string? setting) - { - if (setting != null && ((Dictionary)collection.Settings.Unused).Remove(setting)) - _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); - } - - /// - /// Read all collection files in the Collection Directory. - /// Ensure that the default named collection exists, and apply inheritances afterward. - /// Duplicate collection files are not deleted, just not added here. - /// - private void ReadCollections(out ModCollection defaultNamedCollection) - { - Penumbra.Log.Debug("[Collections] Reading saved collections..."); - foreach (var file in _saveService.FileNames.CollectionFiles) - { - if (!ModCollectionSave.LoadFromFile(file, out var id, out var name, out var version, out var settings, out var inheritance)) - continue; - - if (id == Guid.Empty) - { - Penumbra.Messager.NotificationMessage("Collection without ID found.", NotificationType.Warning); - continue; - } - - if (ById(id, out _)) - { - Penumbra.Messager.NotificationMessage($"Duplicate collection found: {id} already exists. Import skipped.", - NotificationType.Warning); - continue; - } - - var collection = CreateFromData(id, name, version, settings, inheritance); - var correctName = _saveService.FileNames.CollectionFile(collection); - if (file.FullName != correctName) - try - { - if (version >= 2) - { - try - { - File.Move(file.FullName, correctName, false); - Penumbra.Messager.NotificationMessage( - $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, renamed.", - NotificationType.Warning); - } - catch (Exception ex) - { - Penumbra.Messager.NotificationMessage( - $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, rename failed:\n{ex}", - NotificationType.Warning); - } - } - else - { - _saveService.ImmediateSaveSync(new ModCollectionSave(_modStorage, collection)); - try - { - File.Move(file.FullName, file.FullName + ".bak", true); - Penumbra.Log.Information($"Migrated collection {name} to Guid {id} with backup of old file."); - } - catch (Exception ex) - { - Penumbra.Log.Information($"Migrated collection {name} to Guid {id}, rename of old file failed:\n{ex}"); - } - } - } - catch (Exception e) - { - Penumbra.Messager.NotificationMessage(e, - $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, but could not rename.", - NotificationType.Error); - } - - _collections.Add(collection); - } - - defaultNamedCollection = SetDefaultNamedCollection(); - Penumbra.Log.Debug($"[Collections] Found {Count} saved collections."); - } - - /// - /// Add the collection with the default name if it does not exist. - /// It should always be ensured that it exists, otherwise it will be created. - /// This can also not be deleted, so there are always at least the empty and a collection with default name. - /// - private ModCollection SetDefaultNamedCollection() - { - if (ByName(ModCollectionIdentity.DefaultCollectionName, out var collection)) - return collection; - - if (AddCollection(ModCollectionIdentity.DefaultCollectionName, null)) - return _collections[^1]; - - Penumbra.Messager.NotificationMessage( - $"Unknown problem creating a collection with the name {ModCollectionIdentity.DefaultCollectionName}, which is required to exist.", - NotificationType.Error); - return Count > 1 ? _collections[1] : _collections[0]; - } - - /// Move all settings in all collections to unused settings. - private void OnModDiscoveryStarted() - { - foreach (var collection in this) - collection.Settings.PrepareModDiscovery(_modStorage); - } - - /// Restore all settings in all collections to mods. - private void OnModDiscoveryFinished() - { - // Re-apply all mod settings. - foreach (var collection in this) - collection.Settings.ApplyModSettings(collection, _saveService, _modStorage); - } - - /// Add or remove a mod from all collections, or re-save all collections where the mod has settings. - private void OnModPathChange(in ModPathChanged.Arguments arguments) - { - switch (arguments.Type) - { - case ModPathChangeType.Added: - foreach (var collection in this) - collection.Settings.AddMod(arguments.Mod); - break; - case ModPathChangeType.Deleted: - foreach (var collection in this) - collection.Settings.RemoveMod(arguments.Mod); - break; - case ModPathChangeType.Moved: - var index = arguments.Mod.Index; - foreach (var collection in this.Where(collection => collection.GetOwnSettings(index) is not null)) - _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); - break; - case ModPathChangeType.Reloaded: - foreach (var collection in this) - { - if (collection.GetOwnSettings(arguments.Mod.Index)?.Settings.FixAll(arguments.Mod) ?? false) - _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); - collection.Settings.SetTemporary(arguments.Mod.Index, null); - } - - break; - } - } - - /// Save all collections where the mod has settings and the change requires saving. - private void OnModOptionChange(in ModOptionChanged.Arguments arguments) - { - arguments.Type.HandlingInfo(out var requiresSaving, out _, out _); - if (!requiresSaving) - return; - - foreach (var collection in this) - { - if (collection.GetOwnSettings(arguments.Mod.Index)?.HandleChanges(arguments.Type, arguments.Mod, arguments.Group, arguments.Option, arguments.DeletedIndex) ?? false) - _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); - collection.Settings.SetTemporary(arguments.Mod.Index, null); - } - } - - /// Update change counters when changing files. - private void OnModFileChanged(in ModFileChanged.Arguments arguments) - { - if (arguments.File.CurrentUsage == 0) - return; - - foreach (var collection in this) - { - var (settings, _) = collection.GetActualSettings(arguments.Mod.Index); - if (settings is { Enabled: true }) - collection.Counters.IncrementChange(); - } - } -} +using Dalamud.Interface.ImGuiNotification; +using Luna; +using Penumbra.Communication; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.Mods.Settings; +using Penumbra.Services; + +namespace Penumbra.Collections.Manager; + +/// A contiguously incrementing ID managed by the CollectionCreator. +public readonly record struct LocalCollectionId(int Id) : IAdditionOperators +{ + public static readonly LocalCollectionId Zero = new(0); + + public static LocalCollectionId operator +(LocalCollectionId left, int right) + => new(left.Id + right); +} + +public class CollectionStorage : IReadOnlyList, IDisposable, IService +{ + private readonly CommunicatorService _communicator; + private readonly SaveService _saveService; + private readonly ModStorage _modStorage; + + public ModCollection Create(string name, int index, ModCollection? duplicate) + { + var newCollection = duplicate?.Duplicate(name, CurrentCollectionId, index) + ?? ModCollection.CreateEmpty(name, CurrentCollectionId, index, _modStorage.Count); + _collectionsByLocal[CurrentCollectionId] = newCollection; + CurrentCollectionId += 1; + return newCollection; + } + + public ModCollection CreateFromData(Guid id, string name, int version, Dictionary allSettings, + IReadOnlyList inheritances) + { + var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, + new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, inheritances); + _collectionsByLocal[CurrentCollectionId] = newCollection; + CurrentCollectionId += 1; + return newCollection; + } + + public ModCollection CreateTemporary(string name, int index, int globalChangeCounter) + { + var newCollection = ModCollection.CreateTemporary(name, CurrentCollectionId, index, globalChangeCounter); + _collectionsByLocal[CurrentCollectionId] = newCollection; + CurrentCollectionId += 1; + return newCollection; + } + + public void Delete(ModCollection collection) + => _collectionsByLocal.Remove(collection.Identity.LocalId); + + /// The empty collection is always available at Index 0. + private readonly List _collections = + [ + ModCollection.Empty, + ]; + + /// A list of all collections ever created still existing by their local id. + private readonly Dictionary + _collectionsByLocal = new() { [LocalCollectionId.Zero] = ModCollection.Empty }; + + + public readonly ModCollection DefaultNamed; + + /// Incremented by 1 because the empty collection gets Zero. + public LocalCollectionId CurrentCollectionId { get; private set; } = LocalCollectionId.Zero + 1; + + /// Default enumeration skips the empty collection. + public IEnumerator GetEnumerator() + => _collections.Skip(1).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _collections.Count; + + public ModCollection this[int index] + => _collections[index]; + + /// Find a collection by its name. If the name is empty or None, the empty collection is returned. + public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection) + { + if (name.Length != 0) + return _collections.FindFirst(c => string.Equals(c.Identity.Name, name, StringComparison.OrdinalIgnoreCase), out collection); + + collection = ModCollection.Empty; + return true; + } + + /// Find a collection by its id. If the GUID is empty, the empty collection is returned. + public bool ById(Guid id, [NotNullWhen(true)] out ModCollection? collection) + { + if (id != Guid.Empty) + return _collections.FindFirst(c => c.Identity.Id == id, out collection); + + collection = ModCollection.Empty; + return true; + } + + /// Find a collection by an identifier, which is interpreted as a GUID first and if it does not correspond to one, as a name. + public bool ByIdentifier(string identifier, [NotNullWhen(true)] out ModCollection? collection) + { + if (Guid.TryParse(identifier, out var guid)) + return ById(guid, out collection); + + return ByName(identifier, out collection); + } + + /// Find a collection by its local ID if it still exists, otherwise returns the empty collection. + public ModCollection ByLocalId(LocalCollectionId localId) + => _collectionsByLocal.TryGetValue(localId, out var coll) ? coll : ModCollection.Empty; + + public CollectionStorage(CommunicatorService communicator, SaveService saveService, ModStorage modStorage) + { + _communicator = communicator; + _saveService = saveService; + _modStorage = modStorage; + _communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted, ModDiscoveryStarted.Priority.CollectionStorage); + _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionStorage); + _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.CollectionStorage); + _communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.CollectionStorage); + _communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.CollectionStorage); + ReadCollections(out DefaultNamed); + } + + public void Dispose() + { + _communicator.ModDiscoveryStarted.Unsubscribe(OnModDiscoveryStarted); + _communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished); + _communicator.ModPathChanged.Unsubscribe(OnModPathChange); + _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); + _communicator.ModFileChanged.Unsubscribe(OnModFileChanged); + } + + /// + /// Add a new collection of the given name. + /// If duplicate is not-null, the new collection will be a duplicate of it. + /// If the name of the collection would result in an already existing filename, skip it. + /// Returns true if the collection was successfully created and fires a Inactive event. + /// Also sets the current collection to the new collection afterwards. + /// + public bool AddCollection(string name, ModCollection? duplicate) + { + if (name.Length == 0) + return false; + + var newCollection = Create(name, _collections.Count, duplicate); + _collections.Add(newCollection); + _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); + Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.Identity.AnonymizedName}.", NotificationType.Success, + false); + _communicator.CollectionChange.Invoke(new CollectionChange.Arguments(CollectionType.Inactive, null, newCollection, string.Empty)); + return true; + } + + /// Rename a collection. + /// The collection to rename. + /// The new name for the collection. + /// True if a change has taken place. + public bool RenameCollection(ModCollection collection, string newName) + { + var oldName = collection.Identity.Name; + if (newName == oldName) + return false; + + collection.Identity.Name = newName; + _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + _communicator.CollectionRename.Invoke(new CollectionRename.Arguments(collection, oldName, newName)); + return true; + } + + /// + /// Remove the given collection if it exists and is neither the empty nor the default-named collection. + /// + public bool RemoveCollection(ModCollection collection) + { + if (collection.Identity.Index <= ModCollection.Empty.Identity.Index || collection.Identity.Index >= _collections.Count) + { + Penumbra.Messager.NotificationMessage("Can not remove the empty collection.", NotificationType.Error, false); + return false; + } + + if (collection.Identity.Index == DefaultNamed.Identity.Index) + { + Penumbra.Messager.NotificationMessage("Can not remove the default collection.", NotificationType.Error, false); + return false; + } + + Delete(collection); + _saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection)); + _collections.RemoveAt(collection.Identity.Index); + // Update indices. + for (var i = collection.Identity.Index; i < Count; ++i) + _collections[i].Identity.Index = i; + _collectionsByLocal.Remove(collection.Identity.LocalId); + + Penumbra.Messager.NotificationMessage($"Deleted collection {collection.Identity.AnonymizedName}.", NotificationType.Success, false); + _communicator.CollectionChange.Invoke(new CollectionChange.Arguments(CollectionType.Inactive, collection, null, string.Empty)); + return true; + } + + /// Remove all settings for not currently-installed mods from the given collection. + public int CleanUnavailableSettings(ModCollection collection) + { + var count = collection.Settings.Unused.Count; + if (count > 0) + { + ((Dictionary)collection.Settings.Unused).Clear(); + _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + } + + return count; + } + + /// Remove a specific setting for not currently-installed mods from the given collection. + public void CleanUnavailableSetting(ModCollection collection, string? setting) + { + if (setting != null && ((Dictionary)collection.Settings.Unused).Remove(setting)) + _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + } + + /// + /// Read all collection files in the Collection Directory. + /// Ensure that the default named collection exists, and apply inheritances afterward. + /// Duplicate collection files are not deleted, just not added here. + /// + private void ReadCollections(out ModCollection defaultNamedCollection) + { + Penumbra.Log.Debug("[Collections] Reading saved collections..."); + foreach (var file in _saveService.FileNames.CollectionFiles) + { + if (!ModCollectionSave.LoadFromFile(file, out var id, out var name, out var version, out var settings, out var inheritance)) + continue; + + if (id == Guid.Empty) + { + Penumbra.Messager.NotificationMessage("Collection without ID found.", NotificationType.Warning); + continue; + } + + if (ById(id, out _)) + { + Penumbra.Messager.NotificationMessage($"Duplicate collection found: {id} already exists. Import skipped.", + NotificationType.Warning); + continue; + } + + var collection = CreateFromData(id, name, version, settings, inheritance); + var correctName = _saveService.FileNames.CollectionFile(collection); + if (file.FullName != correctName) + try + { + if (version >= 2) + { + try + { + File.Move(file.FullName, correctName, false); + Penumbra.Messager.NotificationMessage( + $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, renamed.", + NotificationType.Warning); + } + catch (Exception ex) + { + Penumbra.Messager.NotificationMessage( + $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, rename failed:\n{ex}", + NotificationType.Warning); + } + } + else + { + _saveService.ImmediateSaveSync(new ModCollectionSave(_modStorage, collection)); + try + { + File.Move(file.FullName, file.FullName + ".bak", true); + Penumbra.Log.Information($"Migrated collection {name} to Guid {id} with backup of old file."); + } + catch (Exception ex) + { + Penumbra.Log.Information($"Migrated collection {name} to Guid {id}, rename of old file failed:\n{ex}"); + } + } + } + catch (Exception e) + { + Penumbra.Messager.NotificationMessage(e, + $"Collection {file.Name} does not correspond to {collection.Identity.Identifier}, but could not rename.", + NotificationType.Error); + } + + _collections.Add(collection); + } + + defaultNamedCollection = SetDefaultNamedCollection(); + Penumbra.Log.Debug($"[Collections] Found {Count} saved collections."); + } + + /// + /// Add the collection with the default name if it does not exist. + /// It should always be ensured that it exists, otherwise it will be created. + /// This can also not be deleted, so there are always at least the empty and a collection with default name. + /// + private ModCollection SetDefaultNamedCollection() + { + if (ByName(ModCollectionIdentity.DefaultCollectionName, out var collection)) + return collection; + + if (AddCollection(ModCollectionIdentity.DefaultCollectionName, null)) + return _collections[^1]; + + Penumbra.Messager.NotificationMessage( + $"Unknown problem creating a collection with the name {ModCollectionIdentity.DefaultCollectionName}, which is required to exist.", + NotificationType.Error); + return Count > 1 ? _collections[1] : _collections[0]; + } + + /// Move all settings in all collections to unused settings. + private void OnModDiscoveryStarted() + { + foreach (var collection in this) + collection.Settings.PrepareModDiscovery(_modStorage); + } + + /// Restore all settings in all collections to mods. + private void OnModDiscoveryFinished() + { + // Re-apply all mod settings. + foreach (var collection in this) + collection.Settings.ApplyModSettings(collection, _saveService, _modStorage); + } + + /// Add or remove a mod from all collections, or re-save all collections where the mod has settings. + private void OnModPathChange(in ModPathChanged.Arguments arguments) + { + switch (arguments.Type) + { + case ModPathChangeType.Added: + foreach (var collection in this) + collection.Settings.AddMod(arguments.Mod); + break; + case ModPathChangeType.Deleted: + foreach (var collection in this) + collection.Settings.RemoveMod(arguments.Mod); + break; + case ModPathChangeType.Moved: + var index = arguments.Mod.Index; + foreach (var collection in this.Where(collection => collection.GetOwnSettings(index) is not null)) + _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + break; + case ModPathChangeType.Reloaded: + foreach (var collection in this) + { + if (collection.GetOwnSettings(arguments.Mod.Index)?.Settings.FixAll(arguments.Mod) ?? false) + _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + collection.Settings.SetTemporary(arguments.Mod.Index, null); + } + + break; + } + } + + /// Save all collections where the mod has settings and the change requires saving. + private void OnModOptionChange(in ModOptionChanged.Arguments arguments) + { + arguments.Type.HandlingInfo(out var requiresSaving, out _, out _); + if (!requiresSaving) + return; + + foreach (var collection in this) + { + if (collection.GetOwnSettings(arguments.Mod.Index) + ?.HandleChanges(arguments.Type, arguments.Mod, arguments.Group, arguments.Option, arguments.DeletedIndex) + ?? false) + _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + collection.Settings.SetTemporary(arguments.Mod.Index, null); + } + } + + /// Update change counters when changing files. + private void OnModFileChanged(in ModFileChanged.Arguments arguments) + { + if (arguments.File.CurrentUsage == 0) + return; + + foreach (var collection in this) + { + var (settings, _) = collection.GetActualSettings(arguments.Mod.Index); + if (settings is { Enabled: true }) + collection.Counters.IncrementChange(); + } + } +} diff --git a/Penumbra/Communication/CollectionChange.cs b/Penumbra/Communication/CollectionChange.cs index 32c07169..e6aa8ac2 100644 --- a/Penumbra/Communication/CollectionChange.cs +++ b/Penumbra/Communication/CollectionChange.cs @@ -39,8 +39,8 @@ public sealed class CollectionChange(Logger log) /// ItemSwapTab = 0, - /// - CollectionSelector = 0, + /// + CollectionSelectorCache = 0, /// IndividualAssignmentUi = 0, diff --git a/Penumbra/Communication/CollectionRename.cs b/Penumbra/Communication/CollectionRename.cs new file mode 100644 index 00000000..8d39b9c2 --- /dev/null +++ b/Penumbra/Communication/CollectionRename.cs @@ -0,0 +1,20 @@ +using Luna; +using Penumbra.Collections; + +namespace Penumbra.Communication; + +public sealed class CollectionRename(Logger log) + : EventBase(nameof(CollectionRename), log) +{ + public enum Priority + { + /// + CollectionSelectorCache = int.MinValue, + } + + /// The arguments for a collection rename event. + /// The renamed collection. + /// The old name of the collection. + /// The new name of the collection. + public readonly record struct Arguments(ModCollection Collection, string OldName, string NewName); +} diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index bfee3d68..4cd2516e 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -26,25 +26,25 @@ public class EphemeralConfig : ISavable, IDisposable, IService public float ModSelectorMinimumScale { get; set; } = 0.1f; public float ModSelectorMaximumScale { get; set; } = 0.5f; - public int Version { get; set; } = Configuration.Constants.CurrentVersion; - public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; - public bool DebugSeparateWindow { get; set; } = false; - public int TutorialStep { get; set; } = 0; - public bool EnableResourceLogging { get; set; } = false; - public string ResourceLoggingFilter { get; set; } = string.Empty; - public bool EnableResourceWatcher { get; set; } = false; - public bool OnlyAddMatchingResources { get; set; } = true; - public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; - public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; - public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; - public CollectionsTab.PanelMode CollectionPanel { get; set; } = CollectionsTab.PanelMode.SimpleAssignment; - public TabType SelectedTab { get; set; } = TabType.Settings; - public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags; - public bool FixMainWindow { get; set; } = false; - public string LastModPath { get; set; } = string.Empty; - public HashSet AdvancedEditingOpenForModPaths { get; set; } = []; - public bool ForceRedrawOnFileChange { get; set; } = false; - public bool IncognitoMode { get; set; } = false; + public int Version { get; set; } = Configuration.Constants.CurrentVersion; + public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; + public bool DebugSeparateWindow { get; set; } = false; + public int TutorialStep { get; set; } = 0; + public bool EnableResourceLogging { get; set; } = false; + public string ResourceLoggingFilter { get; set; } = string.Empty; + public bool EnableResourceWatcher { get; set; } = false; + public bool OnlyAddMatchingResources { get; set; } = true; + public ResourceTypeFlag ResourceWatcherResourceTypes { get; set; } = ResourceExtensions.AllResourceTypes; + public ResourceCategoryFlag ResourceWatcherResourceCategories { get; set; } = ResourceExtensions.AllResourceCategories; + public RecordType ResourceWatcherRecordTypes { get; set; } = ResourceWatcher.AllRecords; + public CollectionPanelMode CollectionPanel { get; set; } = CollectionPanelMode.SimpleAssignment; + public TabType SelectedTab { get; set; } = TabType.Settings; + public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags; + public bool FixMainWindow { get; set; } = false; + public string LastModPath { get; set; } = string.Empty; + public HashSet AdvancedEditingOpenForModPaths { get; set; } = []; + public bool ForceRedrawOnFileChange { get; set; } = false; + public bool IncognitoMode { get; set; } = false; /// /// Load the current configuration. diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index c77f2d5e..39769244 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -56,7 +56,8 @@ public class ModEditor( SwapEditor.Revert(Option!); MetaEditor.Load(Mod!, Option!); Duplicates.Clear(); - MdlMaterialEditor.ScanModels(Mod!); + MdlMaterialEditor.ScanModels(Mod!); + OptionLoaded?.Invoke(); }); } @@ -80,21 +81,22 @@ public class ModEditor( Files.UpdatePaths(Mod!, Option!); MetaEditor.Load(Mod!, Option!); FileEditor.Clear(); - Duplicates.Clear(); + Duplicates.Clear(); + OptionLoaded?.Invoke(); }); } /// Load the correct option by indices for the currently loaded mod if possible, unload if not. private void LoadOption(int groupIdx, int dataIdx, bool message) { - if (Mod != null && Mod.Groups.Count > groupIdx) + if (Mod is not null && Mod.Groups.Count > groupIdx) { if (groupIdx == -1 && dataIdx == 0) { Group = null; Option = Mod.Default; GroupIdx = groupIdx; - DataIdx = dataIdx; + DataIdx = dataIdx; return; } @@ -105,7 +107,7 @@ public class ModEditor( { Option = Group.DataContainers[dataIdx]; GroupIdx = groupIdx; - DataIdx = dataIdx; + DataIdx = dataIdx; return; } } @@ -119,6 +121,8 @@ public class ModEditor( Penumbra.Log.Error($"Loading invalid option {groupIdx} {dataIdx} for Mod {Mod?.Name ?? "Unknown"}."); } + public event Action? OptionLoaded; + public void Clear() { Duplicates.Clear(); @@ -126,7 +130,8 @@ public class ModEditor( Files.Clear(); MetaEditor.Clear(); Mod = null; - LoadOption(0, 0, false); + LoadOption(0, 0, false); + OptionLoaded?.Invoke(); } public void Dispose() @@ -146,7 +151,7 @@ public class ModEditor( foreach (var subDir in baseDir.GetDirectories()) { ClearEmptySubDirectories(subDir); - if (subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0) + if (subDir.GetFiles().Length is 0 && subDir.GetDirectories().Length is 0) subDir.Delete(); } } diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index cec4ecbe..06d23868 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -1,208 +1,197 @@ -using Luna; -using Penumbra.Mods.SubMods; -using Penumbra.String.Classes; - -namespace Penumbra.Mods.Editor; - -public class ModFileCollection : IDisposable -{ - private readonly List _available = []; - private readonly List _mtrl = []; - private readonly List _mdl = []; - private readonly List _tex = []; - private readonly List _shpk = []; - private readonly List _pbd = []; - private readonly List _atch = []; - - private readonly SortedSet _missing = []; - private readonly HashSet _usedPaths = []; - - public IReadOnlySet Missing - => Ready ? _missing : []; - - public IReadOnlySet UsedPaths - => Ready ? _usedPaths : []; - - public IReadOnlyList Available - => Ready ? _available : []; - - public IReadOnlyList Mtrl - => Ready ? _mtrl : []; - - public IReadOnlyList Mdl - => Ready ? _mdl : []; - - public IReadOnlyList Tex - => Ready ? _tex : []; - - public IReadOnlyList Shpk - => Ready ? _shpk : []; - - public IReadOnlyList Pbd - => Ready ? _pbd : []; - - public IReadOnlyList Atch - => Ready ? _atch : []; - - public bool Ready { get; private set; } = true; - - public void UpdateAll(Mod mod, IModDataContainer option) - { - UpdateFiles(mod, CancellationToken.None); - UpdatePaths(mod, option, false, CancellationToken.None); - } - - public void UpdatePaths(Mod mod, IModDataContainer option) - => UpdatePaths(mod, option, true, CancellationToken.None); - - public void Clear() - { - ClearFiles(); - ClearPaths(false, CancellationToken.None); - } - - public void Dispose() - => Clear(); - - public void ClearMissingFiles() - => _missing.Clear(); - - public void RemoveUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath) - { - _usedPaths.Remove(gamePath); - if (file != null) - { - --file.CurrentUsage; - file.SubModUsage.RemoveAll(p => p.Item1 == option && p.Item2.Equals(gamePath)); - } - } - - public void RemoveUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath) - => RemoveUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); - - public void AddUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath) - { - _usedPaths.Add(gamePath); - if (file == null) - return; - - ++file.CurrentUsage; - file.SubModUsage.Add((option, gamePath)); - } - - public void AddUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath) - => AddUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); - - public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath) - { - var oldPath = file.SubModUsage[pathIdx]; - _usedPaths.Remove(oldPath.Item2); - if (!gamePath.IsEmpty) - { - file.SubModUsage[pathIdx] = (oldPath.Item1, gamePath); - _usedPaths.Add(gamePath); - } - else - { - --file.CurrentUsage; - file.SubModUsage.RemoveAt(pathIdx); - } - } - - private void UpdateFiles(Mod mod, CancellationToken tok) - { - tok.ThrowIfCancellationRequested(); - ClearFiles(); - - foreach (var file in mod.ModPath.EnumerateDirectories().Where(d => !d.IsHidden()).SelectMany(FileExtensions.EnumerateNonHiddenFiles)) - { - tok.ThrowIfCancellationRequested(); - if (!FileRegistry.FromFile(mod.ModPath, file, out var registry)) - continue; - - _available.Add(registry); - switch (Path.GetExtension(registry.File.FullName).ToLowerInvariant()) - { - case ".mtrl": - _mtrl.Add(registry); - break; - case ".mdl": - _mdl.Add(registry); - break; - case ".tex": - _tex.Add(registry); - break; - case ".shpk": - _shpk.Add(registry); - break; - case ".pbd": - _pbd.Add(registry); - break; - case ".atch": - _atch.Add(registry); - break; - } - } - } - - private void ClearFiles() - { - _available.Clear(); - _mtrl.Clear(); - _mdl.Clear(); - _tex.Clear(); - _shpk.Clear(); - _pbd.Clear(); - _atch.Clear(); - } - - private void ClearPaths(bool clearRegistries, CancellationToken tok) - { - if (clearRegistries) - foreach (var reg in _available) - { - tok.ThrowIfCancellationRequested(); - reg.CurrentUsage = 0; - reg.SubModUsage.Clear(); - } - - _missing.Clear(); - _usedPaths.Clear(); - } - - private void UpdatePaths(Mod mod, IModDataContainer option, bool clearRegistries, CancellationToken tok) - { - tok.ThrowIfCancellationRequested(); - ClearPaths(clearRegistries, tok); - - tok.ThrowIfCancellationRequested(); - - foreach (var subMod in mod.AllDataContainers) - { - foreach (var (gamePath, file) in subMod.Files) - { - tok.ThrowIfCancellationRequested(); - if (!file.Exists) - { - _missing.Add(file); - if (subMod == option) - _usedPaths.Add(gamePath); - } - else - { - var registry = _available.Find(x => x.File.Equals(file)); - if (registry == null) - continue; - - if (subMod == option) - { - ++registry.CurrentUsage; - _usedPaths.Add(gamePath); - } - - registry.SubModUsage.Add((subMod, gamePath)); - } - } - } - } -} +using ImSharp.Containers; +using Luna; +using Penumbra.Mods.SubMods; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.Editor; + +public class ModFileCollection : IDisposable +{ + private readonly ObservableList _available = []; + private readonly ObservableList _mtrl = []; + private readonly ObservableList _mdl = []; + private readonly ObservableList _tex = []; + private readonly ObservableList _shpk = []; + private readonly ObservableList _pbd = []; + private readonly ObservableList _atch = []; + + private readonly SortedSet _missing = []; + private readonly HashSet _usedPaths = []; + + public IReadOnlySet Missing + => Ready ? _missing : []; + + public IReadOnlySet UsedPaths + => Ready ? _usedPaths : []; + + public IObservableList Available + => Ready ? _available : []; + + public IObservableList Mtrl + => Ready ? _mtrl : []; + + public IObservableList Mdl + => Ready ? _mdl : []; + + public IObservableList Tex + => Ready ? _tex : []; + + public IObservableList Shpk + => Ready ? _shpk : []; + + public IObservableList Pbd + => Ready ? _pbd : []; + + public IObservableList Atch + => Ready ? _atch : []; + + public bool Ready { get; private set; } = true; + + public void UpdateAll(Mod mod, IModDataContainer option) + { + UpdateFiles(mod, CancellationToken.None); + UpdatePaths(mod, option, false, CancellationToken.None); + } + + public void UpdatePaths(Mod mod, IModDataContainer option) + => UpdatePaths(mod, option, true, CancellationToken.None); + + public void Clear() + { + ClearFiles(); + ClearPaths(false, CancellationToken.None); + } + + public void Dispose() + => Clear(); + + public void ClearMissingFiles() + => _missing.Clear(); + + public void RemoveUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath) + { + _usedPaths.Remove(gamePath); + if (file != null) + { + --file.CurrentUsage; + file.SubModUsage.RemoveAll(p => p.Item1 == option && p.Item2.Equals(gamePath)); + } + } + + public void RemoveUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath) + => RemoveUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); + + public void AddUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath) + { + _usedPaths.Add(gamePath); + if (file == null) + return; + + ++file.CurrentUsage; + file.SubModUsage.Add((option, gamePath)); + } + + public void AddUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath) + => AddUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); + + public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath) + { + var oldPath = file.SubModUsage[pathIdx]; + _usedPaths.Remove(oldPath.Item2); + if (!gamePath.IsEmpty) + { + file.SubModUsage[pathIdx] = (oldPath.Item1, gamePath); + _usedPaths.Add(gamePath); + } + else + { + --file.CurrentUsage; + file.SubModUsage.RemoveAt(pathIdx); + } + } + + private void UpdateFiles(Mod mod, CancellationToken tok) + { + tok.ThrowIfCancellationRequested(); + ClearFiles(); + + foreach (var file in mod.ModPath.EnumerateDirectories().Where(d => !d.IsHidden()).SelectMany(FileExtensions.EnumerateNonHiddenFiles)) + { + tok.ThrowIfCancellationRequested(); + if (!FileRegistry.FromFile(mod.ModPath, file, out var registry)) + continue; + + _available.Add(registry); + switch (Path.GetExtension(registry.File.FullName).ToLowerInvariant()) + { + case ".mtrl": _mtrl.Add(registry); break; + case ".mdl": _mdl.Add(registry); break; + case ".tex": _tex.Add(registry); break; + case ".shpk": _shpk.Add(registry); break; + case ".pbd": _pbd.Add(registry); break; + case ".atch": _atch.Add(registry); break; + } + } + } + + private void ClearFiles() + { + _available.Clear(); + _mtrl.Clear(); + _mdl.Clear(); + _tex.Clear(); + _shpk.Clear(); + _pbd.Clear(); + _atch.Clear(); + } + + private void ClearPaths(bool clearRegistries, CancellationToken tok) + { + if (clearRegistries) + foreach (var reg in _available) + { + tok.ThrowIfCancellationRequested(); + reg.CurrentUsage = 0; + reg.SubModUsage.Clear(); + } + + _missing.Clear(); + _usedPaths.Clear(); + } + + private void UpdatePaths(Mod mod, IModDataContainer option, bool clearRegistries, CancellationToken tok) + { + tok.ThrowIfCancellationRequested(); + ClearPaths(clearRegistries, tok); + + tok.ThrowIfCancellationRequested(); + + foreach (var subMod in mod.AllDataContainers) + { + foreach (var (gamePath, file) in subMod.Files) + { + tok.ThrowIfCancellationRequested(); + if (!file.Exists) + { + _missing.Add(file); + if (subMod == option) + _usedPaths.Add(gamePath); + } + else + { + var registry = _available.Find(x => x.File.Equals(file)); + if (registry == null) + continue; + + if (subMod == option) + { + ++registry.CurrentUsage; + _usedPaths.Add(gamePath); + } + + registry.SubModUsage.Add((subMod, gamePath)); + } + } + } + } +} diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index 591956e7..adf7a859 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -5,6 +5,9 @@ namespace Penumbra.Services; public class CommunicatorService(ServiceManager services) : IService { + /// + public readonly CollectionRename CollectionRename = services.GetService(); + /// public readonly CollectionChange CollectionChange = services.GetService(); diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index ef197664..2e55ed25 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -124,7 +124,7 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu ?? _config.Ephemeral.ResourceWatcherResourceCategories; _config.Ephemeral.ResourceWatcherRecordTypes = _data["ResourceWatcherRecordTypes"]?.ToObject() ?? _config.Ephemeral.ResourceWatcherRecordTypes; - _config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject() ?? _config.Ephemeral.CollectionPanel; + _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; diff --git a/Penumbra/Services/StainService.cs b/Penumbra/Services/StainService.cs index 78563d4b..51a84619 100644 --- a/Penumbra/Services/StainService.cs +++ b/Penumbra/Services/StainService.cs @@ -1,4 +1,3 @@ -using Dalamud.Bindings.ImGui; using Dalamud.Plugin.Services; using ImSharp; using OtterGui.Widgets; @@ -8,121 +7,181 @@ using Penumbra.GameData.Files.StainMapStructs; using Penumbra.Interop.Services; using Penumbra.Interop.Structs; using Penumbra.UI.AdvancedWindow.Materials; -using FilterComboColors = Penumbra.UI.FilterComboColors; -using MouseWheelType = OtterGui.Widgets.MouseWheelType; namespace Penumbra.Services; -//public sealed class StainTemplateCombo(FilterComboColors[] stainCombos, StmFile stmFile) : SimpleFilterCombo(SimpleFilterType.Text) -// where TDyePack : unmanaged, IDyePack -//{ -// public override StringU8 DisplayString(in StmKeyType value) -// => new($"{value,4}"); -// -// public override string FilterString(in StmKeyType value) -// => $"{value,4}"; -// -// public override IEnumerable GetBaseItems() -// => throw new NotImplementedException(); -// -// protected override bool DrawFilter(float width, FilterComboBaseCache> cache) -// { -// using var font = Im.Font.PushDefault(); -// return base.DrawFilter(width, cache); -// } -// -// public bool Draw(Utf8StringHandler label, Utf8StringHandler preview, Utf8StringHandler tooltip, ref int currentSelection, float previewWidth, float itemHeight, -// ComboFlags flags = ComboFlags.None) -// { -// using var font = Im.Font.PushMono(); -// using var style = ImStyleDouble.ButtonTextAlign.Push(new Vector2(1, 0.5f)) -// .PushX(ImStyleDouble.ItemSpacing, Im.Style.ItemInnerSpacing.X); -// var spaceSize = Im.Font.Mono.GetCharacterAdvance(' '); -// var spaces = (int)(previewWidth / spaceSize) - 1; -// return base.Draw(label, preview.PadLeft(spaces), tooltip, ref currentSelection, previewWidth, itemHeight, flags); -// } -// -// protected override bool DrawSelectable(int globalIdx, bool selected) -// { -// var ret = base.DrawSelectable(globalIdx, selected); -// var selection = stainCombos[CurrentDyeChannel].CurrentSelection.Key; -// if (selection == 0 || !stmFile.TryGetValue(Items[globalIdx], selection, out var colors)) -// return ret; -// -// Im.Line.Same(); -// -// var frame = new Vector2(Im.Style.TextHeight); -// Im.Color.Button("D"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.DiffuseColor), 1), 0, frame); -// Im.Line.Same(); -// Im.Color.Button("S"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.SpecularColor), 1), 0, frame); -// Im.Line.Same(); -// Im.Color.Button("E"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.EmissiveColor), 1), 0, frame); -// return ret; -// } -//} - -public class StainService : Luna.IService +public sealed record StainTemplate(StringPair Id, Vector4 Diffuse, Vector4 Specular, Vector4 Emissive, int Key, bool Found) { - public sealed class StainTemplateCombo(FilterComboColors[] stainCombos, StmFile stmFile) - : FilterComboCache(stmFile.Entries.Keys.Prepend(0), MouseWheelType.None, Penumbra.Log) - where TDyePack : unmanaged, IDyePack + public Vector4 Diffuse { get; set; } = Diffuse; + public Vector4 Specular { get; set; } = Specular; + public Vector4 Emissive { get; set; } = Emissive; + public bool Found { get; set; } = Found; +} + +public sealed class TemplateFilter : TextFilterBase +{ + protected override string ToFilterString(in StainTemplate item, int globalIndex) + => item.Id; +} + +public sealed class StainTemplateCombo : ImSharp.FilterComboBase + where TDyePack : unmanaged, IDyePack +{ + private readonly StainService.StainCombo[] _stainCombos; + private readonly StmFile _stmFile; + + private int _currentDyeChannel; + private ushort _currentSelection; + + public StainTemplateCombo(StainService.StainCombo[] stainCombos, StmFile stmFile) + : base(new TemplateFilter()) { - // FIXME There might be a better way to handle that. - public int CurrentDyeChannel = 0; + PreviewAlignment = new Vector2(0.90f, 0.5f); + _stainCombos = stainCombos; + _stmFile = stmFile; + ComputeWidth = true; + } - protected override float GetFilterWidth() + protected override FilterComboBaseCache CreateCache() + => new Cache(this); + + private sealed class Cache : FilterComboBaseCache + { + private readonly StainTemplateCombo _parent; + private int _dyeChannel; + + public Cache(StainTemplateCombo parent) + : base(parent) { - var baseSize = Im.Font.CalculateSize("0000"u8).X + Im.Style.ScrollbarSize + Im.Style.ItemInnerSpacing.X; - if (stainCombos[CurrentDyeChannel].CurrentSelection.Key == 0) - return baseSize; - - return baseSize + Im.Style.TextHeight * 3 + Im.Style.ItemInnerSpacing.X * 3; + _parent = parent; + foreach (var combo in _parent._stainCombos) + combo.SelectionChanged += OnSelectionChanged; + _dyeChannel = _parent._currentDyeChannel; } - protected override string ToString(StmKeyType obj) - => $"{obj,4}"; - - protected override void DrawFilter(int currentSelected, float width) + public override void Update() { - using var font = Im.Font.PushDefault(); - base.DrawFilter(currentSelected, width); + base.Update(); + if (_dyeChannel != _parent._currentDyeChannel) + { + UpdateItems(); + ComputeWidth(); + _dyeChannel = _parent._currentDyeChannel; + } } - public override bool Draw(string label, string preview, string tooltip, ref int currentSelection, float previewWidth, float itemHeight, - ImGuiComboFlags flags = ImGuiComboFlags.None) + private void OnSelectionChanged(Luna.FilterComboColors.Item obj) { - using var font = Im.Font.PushMono(); - using var style = ImStyleDouble.ButtonTextAlign.Push(new Vector2(1, 0.5f)) - .PushX(ImStyleDouble.ItemSpacing, Im.Style.ItemInnerSpacing.X); - var spaceSize = Im.Font.Mono.GetCharacterAdvance(' '); - var spaces = (int)(previewWidth / spaceSize) - 1; - return base.Draw(label, preview.PadLeft(spaces), tooltip, ref currentSelection, previewWidth, itemHeight, flags); + UpdateItems(); + ComputeWidth(); } - protected override bool DrawSelectable(int globalIdx, bool selected) + private void UpdateItems() { - var ret = base.DrawSelectable(globalIdx, selected); - var selection = stainCombos[CurrentDyeChannel].CurrentSelection.Key; - if (selection == 0 || !stmFile.TryGetValue(Items[globalIdx], selection, out var colors)) - return ret; + foreach (var item in UnfilteredItems) + { + var dye = _parent._stainCombos[_parent._currentDyeChannel].CurrentSelection.Id; + if (dye > 0 && _parent._stmFile.TryGetValue(item.Key, dye, out var dyes)) + { + item.Found = true; + item.Diffuse = new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.DiffuseColor), 1); + item.Specular = new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.SpecularColor), 1); + item.Emissive = new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.EmissiveColor), 1); + } + else + { + item.Found = false; + } + } + } - Im.Line.Same(); - - var frame = new Vector2(Im.Style.TextHeight); - Im.Color.Button("D"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.DiffuseColor), 1), 0, frame); - Im.Line.Same(); - Im.Color.Button("S"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.SpecularColor), 1), 0, frame); - Im.Line.Same(); - Im.Color.Button("E"u8, new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)colors.EmissiveColor), 1), 0, frame); - return ret; + + protected override void ComputeWidth() + { + ComboWidth = Im.Font.Mono.CalculateTextSize("0000"u8).X + Im.Style.ScrollbarSize + Im.Style.ItemInnerSpacing.X; + if (_parent._stainCombos[_parent._currentDyeChannel].CurrentSelection.Id is 0) + return; + + ComboWidth += Im.Style.TextHeight * 3 + Im.Style.ItemInnerSpacing.X * 3; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + foreach (var combo in _parent._stainCombos) + combo.SelectionChanged -= OnSelectionChanged; } } + public bool Draw(Utf8StringHandler label, ushort currentSelection, int currentDyeChannel, + Utf8StringHandler tooltip, out int newSelection, float previewWidth, float itemHeight, + ComboFlags flags = ComboFlags.None) + { + Flags = flags; + _currentDyeChannel = currentDyeChannel; + _currentSelection = currentSelection; + using var font = Im.Font.PushMono(); + if (!base.Draw(label, $"{_currentSelection,4}", tooltip, previewWidth, out var selection)) + { + newSelection = 0; + return false; + } + + newSelection = selection.Key; + return true; + } + + protected override IEnumerable GetItems() + { + var dye = _stainCombos[_currentDyeChannel].CurrentSelection.Id; + foreach (var key in _stmFile.Entries.Keys.Prepend(0)) + { + if (dye > 0 && _stmFile.TryGetValue(key, dye, out var dyes)) + yield return new StainTemplate(new StringPair($"{key,4}"), + new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.DiffuseColor), 1), + new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.SpecularColor), 1), + new Vector4(MtrlTab.PseudoSqrtRgb((Vector3)dyes.EmissiveColor), 1), key.Int, true); + else + yield return new StainTemplate(new StringPair($"{key,4}"), Vector4.Zero, Vector4.Zero, Vector4.Zero, key.Int, false); + } + } + + protected override float ItemHeight + => Im.Style.TextHeightWithSpacing; + + protected override bool DrawItem(in StainTemplate item, int globalIndex, bool selected) + { + var ret = Im.Selectable(item.Id.Utf8, selected); + if (item.Found) + { + Im.Line.SameInner(); + var frame = new Vector2(Im.Style.TextHeight); + Im.Color.Button("D"u8, item.Diffuse, 0, frame); + Im.Line.SameInner(); + Im.Color.Button("S"u8, item.Specular, 0, frame); + Im.Line.SameInner(); + Im.Color.Button("E"u8, item.Emissive, 0, frame); + } + + return ret; + } + + protected override bool IsSelected(StainTemplate item, int globalIndex) + => item.Key == _currentSelection; + + protected override bool DrawFilter(float width, FilterComboBaseCache cache) + { + using var font = Im.Font.PushDefault(); + return base.DrawFilter(width, cache); + } +} + +public class StainService : Luna.IService +{ public const int ChannelCount = 2; - public readonly DictStain StainData; - public readonly FilterComboColors StainCombo1; - public readonly FilterComboColors StainCombo2; // FIXME is there a better way to handle this? + public readonly StainCombo StainCombo1; + public readonly StainCombo StainCombo2; // FIXME is there a better way to handle this? public readonly StmFile LegacyStmFile; public readonly StmFile GudStmFile; public readonly StainTemplateCombo LegacyTemplateCombo; @@ -130,9 +189,8 @@ public class StainService : Luna.IService public unsafe StainService(IDataManager dataManager, CharacterUtility characterUtility, DictStain stainData) { - StainData = stainData; - StainCombo1 = CreateStainCombo(); - StainCombo2 = CreateStainCombo(); + StainCombo1 = new StainCombo(stainData); + StainCombo2 = new StainCombo(stainData); if (characterUtility.Address == null) { @@ -146,14 +204,14 @@ public class StainService : Luna.IService } - FilterComboColors[] stainCombos = [StainCombo1, StainCombo2]; + StainCombo[] stainCombos = [StainCombo1, StainCombo2]; LegacyTemplateCombo = new StainTemplateCombo(stainCombos, LegacyStmFile); GudTemplateCombo = new StainTemplateCombo(stainCombos, GudStmFile); } /// Retrieves the instance for the given channel. Indexing is zero-based. - public FilterComboColors GetStainCombo(int channel) + public StainCombo GetStainCombo(int channel) => channel switch { 0 => StainCombo1, @@ -180,8 +238,9 @@ public class StainService : Luna.IService return new StmFile(dataManager); } - private FilterComboColors CreateStainCombo() - => new(140, MouseWheelType.None, - () => StainData.Value.Prepend(new KeyValuePair(0, ("None", 0, false))).ToList(), - Penumbra.Log); + public sealed class StainCombo(DictStain stainData) : Luna.FilterComboColors + { + protected override IEnumerable GetItems() + => stainData.Value.Select(t => new Item(new StringPair(t.Value.Name), t.Value.Dye, t.Key, t.Value.Gloss)).Prepend(None); + } } diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index ee7a2abb..7b6ffccc 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -2,8 +2,6 @@ using Dalamud.Interface.ImGuiNotification; using Dalamud.Plugin.Services; using ImSharp; using Luna; -using OtterGui.Classes; -using OtterGui.Widgets; using Penumbra.Communication; using Penumbra.GameData.Data; using Penumbra.GameData.Files; @@ -11,7 +9,6 @@ using Penumbra.Mods.Editor; using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.UI.Classes; -using MouseWheelType = OtterGui.Widgets.MouseWheelType; namespace Penumbra.UI.AdvancedWindow; @@ -24,7 +21,7 @@ public class FileEditor( FileDialogService fileDialog, string tabName, string fileType, - Func> getFiles, + Func> getFiles, Func drawEdit, Func getInitialPath, Func parseFile) @@ -74,10 +71,15 @@ public class FileEditor( _defaultFile = null; } - private FileRegistry? _currentPath; - private T? _currentFile; - private Exception? _currentException; - private bool _changed; + private FileRegistry? CurrentPath + { + get => _combo.Selected; + set => _combo.Selected = value; + } + + private T? _currentFile; + private Exception? _currentException; + private bool _changed; private string _defaultPath = typeof(T) == typeof(ModEditWindow.PbdTab) ? GamePaths.Pbd.Path : string.Empty; private bool _inInput; @@ -163,7 +165,7 @@ public class FileEditor( public void Reset() { _currentException = null; - _currentPath = null; + CurrentPath = null; (_currentFile as IDisposable)?.Dispose(); _currentFile = null; _changed = false; @@ -171,26 +173,32 @@ public class FileEditor( private void DrawFileSelectCombo() { - if (_combo.Draw("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {fileType} File...", string.Empty, - Im.ContentRegion.Available.X, Im.Style.TextHeight) - && _combo.CurrentSelection != null) - UpdateCurrentFile(_combo.CurrentSelection); + if (CurrentPath is not null) + { + if (_combo.Draw("##select"u8, CurrentPath.RelPath.Path.Span, StringU8.Empty, Im.ContentRegion.Available.X, out var newSelection)) + UpdateCurrentFile(newSelection); + } + else + { + if (_combo.Draw("##select"u8, $"Select {fileType} File...", StringU8.Empty, Im.ContentRegion.Available.X, out var newSelection)) + UpdateCurrentFile(newSelection); + } } private void UpdateCurrentFile(FileRegistry path) { - if (ReferenceEquals(_currentPath, path)) + if (ReferenceEquals(CurrentPath, path)) return; _changed = false; - _currentPath = path; + CurrentPath = path; _currentException = null; try { - var bytes = File.ReadAllBytes(_currentPath.File.FullName); + var bytes = File.ReadAllBytes(CurrentPath.File.FullName); (_currentFile as IDisposable)?.Dispose(); _currentFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file. - _currentFile = parseFile(bytes, _currentPath.File.FullName, true); + _currentFile = parseFile(bytes, CurrentPath.File.FullName, true); } catch (Exception e) { @@ -210,9 +218,9 @@ public class FileEditor( public void SaveFile() { - compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); + compactor.WriteAllBytes(CurrentPath!.File.FullName, _currentFile!.Write()); if (owner.Mod is not null) - communicator.ModFileChanged.Invoke(new ModFileChanged.Arguments(owner.Mod, _currentPath)); + communicator.ModFileChanged.Invoke(new ModFileChanged.Arguments(owner.Mod, CurrentPath)); _changed = false; } @@ -221,8 +229,8 @@ public class FileEditor( if (ImEx.Button("Reset Changes"u8, Vector2.Zero, $"Reset all changes made to the {fileType} file.", !_changed)) { - var tmp = _currentPath; - _currentPath = null; + var tmp = CurrentPath; + CurrentPath = null; UpdateCurrentFile(tmp!); } } @@ -233,7 +241,7 @@ public class FileEditor( if (!child) return; - if (_currentPath is not null) + if (CurrentPath is not null) { if (_currentFile is null) { @@ -253,7 +261,7 @@ public class FileEditor( if (!_inInput && _defaultPath.Length > 0) { - if (_currentPath is not null) + if (CurrentPath is not null) { Im.Line.New(); Im.Line.New(); @@ -278,47 +286,70 @@ public class FileEditor( } } - private class Combo(Func> generator) - : FilterComboCache(generator, MouseWheelType.None, Penumbra.Log) + private sealed class Combo : FilterComboBase { - protected override bool DrawSelectable(int globalIdx, bool selected) + private sealed class FileFilter : RegexFilterBase + { + // TODO: Avoid ToString. + public override bool WouldBeVisible(in FileRegistry item, int globalIndex) + => WouldBeVisible(item.File.FullName) || item.SubModUsage.Any(f => WouldBeVisible(f.Item2.ToString())); + + /// Unused. + protected override string ToFilterString(in FileRegistry item, int globalIndex) + => string.Empty; + } + + private readonly Func> _getFiles; + + public FileRegistry? Selected; + + public Combo(Func> getFiles) + { + _getFiles = getFiles; + Filter = new FileFilter(); + } + + protected override IEnumerable GetItems() + => _getFiles(); + + protected override float ItemHeight + => Im.Style.TextHeightWithSpacing; + + protected override bool DrawItem(in FileRegistry item, int globalIndex, bool selected) { - var file = Items[globalIdx]; bool ret; - using (ImGuiColor.Text.Push(ColorId.HandledConflictMod.Value(), file.IsOnPlayer)) + using (ImGuiColor.Text.Push(ColorId.HandledConflictMod.Value(), item.IsOnPlayer)) { - ret = Im.Selectable(file.RelPath.ToString(), selected); + ret = Im.Selectable(item.RelPath.Path.Span, selected); } if (Im.Item.Hovered()) { - using var tt = Im.Tooltip.Begin(); + using var style = Im.Style.PushDefault(ImStyleDouble.WindowPadding); + using var tt = Im.Tooltip.Begin(); Im.Text("All Game Paths"u8); Im.Separator(); using var t = Im.Table.Begin("##Tooltip"u8, 2, TableFlags.SizingFixedFit); if (t) - { - foreach (var (option, gamePath) in file.SubModUsage) + foreach (var (option, gamePath) in item.SubModUsage) { t.DrawColumn(gamePath.Path.Span); using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value()); t.DrawColumn(option.GetFullName()); } - } } - if (file.SubModUsage.Count > 0) + if (item.SubModUsage.Count > 0) { Im.Line.Same(); using var color = ImGuiColor.Text.Push(ColorId.ItemId.Value()); - ImEx.TextRightAligned($"{file.SubModUsage[0].Item2.Path}"); + ImEx.TextRightAligned($"{item.SubModUsage[0].Item2.Path}"); } return ret; } - protected override bool IsVisible(int globalIndex, LowerString filter) - => filter.IsContained(Items[globalIndex].File.FullName) - || Items[globalIndex].SubModUsage.Any(f => filter.IsContained(f.Item2.ToString())); + protected override bool IsSelected(FileRegistry item, int globalIndex) + => item.Equals(Selected); } } diff --git a/Penumbra/UI/AdvancedWindow/ItemSelector.cs b/Penumbra/UI/AdvancedWindow/ItemSelector.cs new file mode 100644 index 00000000..d659fc1b --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ItemSelector.cs @@ -0,0 +1,94 @@ +using ImSharp; +using Penumbra.Collections; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Mods; +using Penumbra.UI.Classes; +using Penumbra.UI.ModsTab; + +namespace Penumbra.UI.AdvancedWindow; + +public sealed class ItemSelector(ActiveCollections collections, ItemData data, ModFileSystemSelector? selector, FullEquipType type) + : FilterComboBase(new ItemFilter()) +{ + public EquipItem CurrentSelection = new(string.Empty, default, default, default, default, default, FullEquipType.Unknown, default, default, + default); + + public sealed record CacheItem(EquipItem Item, StringPair Name, Vector4 Color, bool InCurrentMod, StringU8[] CollectionMods) + { + public Vector4 Color { get; set; } = Color; + + public CacheItem(EquipItem item, Mod currentMod, ModCollection currentCollection) + : this(item, new StringPair(item.Name), Im.Style[ImGuiColor.Text], currentMod.ChangedItems.Any(c => c.Key == item.Name), + ConvertCollection(item, currentCollection)) + { + if (InCurrentMod) + Color = ColorId.ResTreeLocalPlayer.Value().ToVector(); + else if (CollectionMods.Length > 0) + Color = ColorId.ResTreeNonNetworked.Value().ToVector(); + } + + public CacheItem(EquipItem item, ModCollection currentCollection) + : this(item, new StringPair(item.Name), Im.Style[ImGuiColor.Text], false, ConvertCollection(item, currentCollection)) + { + if (CollectionMods.Length > 0) + Color = ColorId.ResTreeNonNetworked.Value().ToVector(); + } + + private static StringU8[] ConvertCollection(in EquipItem item, ModCollection collection) + { + if (!collection.ChangedItems.TryGetValue(item.Name, out var mods)) + return []; + + var ret = new StringU8[mods.Item1.Count]; + for (var i = 0; i < mods.Item1.Count; ++i) + ret[i] = new StringU8(mods.Item1[i].Name); + return ret; + } + } + + private sealed class ItemFilter : PartwiseFilterBase + { + protected override string ToFilterString(in CacheItem item, int globalIndex) + => item.Name.Utf16; + } + + + protected override IEnumerable GetItems() + { + var list = data.ByType[type]; + if (selector?.Selected is { } currentMod && currentMod.ChangedItems.Values.Any(c => c is IdentifiedItem i && i.Item.Type == type)) + return list.Select(item => new CacheItem(item, currentMod, collections.Current)).OrderByDescending(i => i.InCurrentMod) + .ThenByDescending(i => i.CollectionMods.Length); + + if (selector is null) + return list.Select(item => new CacheItem(item, collections.Current)).OrderBy(i => i.CollectionMods.Length); + + return list.Select(item => new CacheItem(item, collections.Current)).OrderByDescending(i => i.CollectionMods.Length); + } + + protected override float ItemHeight + => Im.Style.TextHeightWithSpacing; + + protected override bool DrawItem(in CacheItem item, int globalIndex, bool selected) + { + using var color = item.Color.W > 0 ? ImGuiColor.Text.Push(item.Color) : null; + var ret = Im.Selectable(item.Name.Utf8, selected); + if (item.CollectionMods.Length > 0 && Im.Item.Hovered()) + { + using var style = Im.Style.PushDefault(ImStyleDouble.WindowPadding); + using var tt = Im.Tooltip.Begin(); + foreach (var mod in item.CollectionMods) + Im.Text(mod); + } + + if (ret) + CurrentSelection = item.Item; + return ret; + } + + protected override bool IsSelected(CacheItem item, int globalIndex) + => item.Item.Equals(CurrentSelection); +} diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 989756d9..db94ac30 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -2,7 +2,6 @@ using Dalamud.Interface.ImGuiNotification; using ImSharp; using Luna; using Luna.Generators; -using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.Communication; @@ -12,7 +11,6 @@ using Penumbra.GameData.Structs; using Penumbra.Import.Structs; using Penumbra.Meta; using Penumbra.Mods; -using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.Mods.ItemSwap; using Penumbra.Mods.Manager; @@ -22,8 +20,6 @@ using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.UI.ModsTab; -using ITab = OtterGui.Widgets.ITab; -using MouseWheelType = OtterGui.Widgets.MouseWheelType; namespace Penumbra.UI.AdvancedWindow; @@ -152,44 +148,6 @@ public class ItemSwapTab : IDisposable, ITab _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); } - - - private class ItemSelector(ActiveCollections collections, ItemData data, ModFileSystemSelector? selector, FullEquipType type) - : FilterComboCache<(EquipItem Item, bool InMod, SingleArray InCollection)>(() => - { - var list = data.ByType[type]; - var enumerable = selector?.Selected is { } mod && mod.ChangedItems.Values.Any(o => o is IdentifiedItem i && i.Item.Type == type) - ? list.Select(i => (i, mod.ChangedItems.ContainsKey(i.Name), - collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray())) - .OrderByDescending(p => p.Item2).ThenByDescending(p => p.Item3.Count) - : selector is null - ? list.Select(i => (i, false, - collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray())) - .OrderBy(p => p.Item3.Count) - : list.Select(i => (i, false, - collections.Current.ChangedItems.TryGetValue(i.Name, out var m) ? m.Item1 : new SingleArray())) - .OrderByDescending(p => p.Item3.Count); - return enumerable.ToList(); - }, MouseWheelType.None, Penumbra.Log) - { - protected override bool DrawSelectable(int globalIdx, bool selected) - { - var (_, inMod, inCollection) = Items[globalIdx]; - using var color = inMod - ? ImGuiColor.Text.Push(ColorId.ResTreeLocalPlayer.Value()) - : inCollection.Count > 0 - ? ImGuiColor.Text.Push(ColorId.ResTreeNonNetworked.Value()) - : null; - var ret = base.DrawSelectable(globalIdx, selected); - if (inCollection.Count > 0) - Im.Tooltip.OnHover(string.Join('\n', inCollection.Select(m => m.Name))); - return ret; - } - - protected override string ToString((EquipItem Item, bool InMod, SingleArray InCollection) obj) - => obj.Item.Name; - } - private readonly Dictionary _selectors; private readonly ItemSwapContainer _swapData; @@ -241,17 +199,17 @@ public class ItemSwapTab : IDisposable, ITab case SwapType.Ring: case SwapType.Glasses: var values = _selectors[_lastTab]; - if (values.Source.CurrentSelection.Item.Type != FullEquipType.Unknown - && values.Target.CurrentSelection.Item.Type != FullEquipType.Unknown) - _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item, values.Source.CurrentSelection.Item, + if (values.Source.CurrentSelection.Type is not FullEquipType.Unknown + && values.Target.CurrentSelection.Type is not FullEquipType.Unknown) + _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection, values.Source.CurrentSelection, _useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing); break; case SwapType.BetweenSlots: var (_, _, selectorFrom) = GetAccessorySelector(_slotFrom, true); var (_, _, selectorTo) = GetAccessorySelector(_slotTo, false); - if (selectorFrom.CurrentSelection.Item.Valid && selectorTo.CurrentSelection.Item.Valid) - _affectedItems = _swapData.LoadTypeSwap(ToEquipSlot(_slotTo), selectorTo.CurrentSelection.Item, ToEquipSlot(_slotFrom), - selectorFrom.CurrentSelection.Item, + if (selectorFrom.CurrentSelection.Valid && selectorTo.CurrentSelection.Valid) + _affectedItems = _swapData.LoadTypeSwap(ToEquipSlot(_slotTo), selectorTo.CurrentSelection, ToEquipSlot(_slotFrom), + selectorFrom.CurrentSelection, _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: @@ -315,10 +273,10 @@ public class ItemSwapTab : IDisposable, ITab $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}{OriginalAuthor()}"; case SwapType.BetweenSlots: return - $"Created by swapping {GetAccessorySelector(_slotFrom, true).Item3.CurrentSelection.Item.Name} onto {GetAccessorySelector(_slotTo, false).Item3.CurrentSelection.Item.Name} in {_mod!.Name}{OriginalAuthor()}"; + $"Created by swapping {GetAccessorySelector(_slotFrom, true).Item3.CurrentSelection.Name} onto {GetAccessorySelector(_slotTo, false).Item3.CurrentSelection.Name} in {_mod!.Name}{OriginalAuthor()}"; default: return - $"Created by swapping {_selectors[_lastTab].Source.CurrentSelection.Item.Name} onto {_selectors[_lastTab].Target.CurrentSelection.Item.Name} in {_mod!.Name}{OriginalAuthor()}"; + $"Created by swapping {_selectors[_lastTab].Source.CurrentSelection.Name} onto {_selectors[_lastTab].Target.CurrentSelection.Name} in {_mod!.Name}{OriginalAuthor()}"; } } @@ -543,9 +501,7 @@ public class ItemSwapTab : IDisposable, ITab } table.NextColumn(); - _dirty |= selector.Draw("##itemSource", selector.CurrentSelection.Item.Name, string.Empty, - InputWidth * 2 * Im.Style.GlobalScale, - Im.Style.TextHeightWithSpacing); + _dirty |= selector.Draw("##itemSource"u8, selector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _); (article1, _, selector) = GetAccessorySelector(_slotTo, false); table.DrawFrameColumn($"and put {article2} on {article1}"); @@ -566,8 +522,7 @@ public class ItemSwapTab : IDisposable, ITab } table.NextColumn(); - _dirty |= selector.Draw("##itemTarget", selector.CurrentSelection.Item.Name, string.Empty, InputWidth * 2 * Im.Style.GlobalScale, - Im.Style.TextHeightWithSpacing); + _dirty |= selector.Draw("##itemTarget"u8, selector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _); if (_affectedItems is not { Count: > 1 }) return; @@ -576,7 +531,7 @@ public class ItemSwapTab : IDisposable, ITab if (Im.Item.Hovered()) { using var tt = Im.Tooltip.Begin(); - foreach (var item in _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Item.Name))) + foreach (var item in _affectedItems.Where(i => !ReferenceEquals(i.Name, selector.CurrentSelection.Name))) Im.Text(item.Name); } } @@ -610,8 +565,7 @@ public class ItemSwapTab : IDisposable, ITab return; table.DrawFrameColumn(text1); table.NextColumn(); - _dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Item.Name, string.Empty, - InputWidth * 2 * Im.Style.GlobalScale, Im.Style.TextHeightWithSpacing); + _dirty |= sourceSelector.Draw("##itemSource"u8, sourceSelector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _); if (type is SwapType.Ring) { @@ -621,9 +575,7 @@ public class ItemSwapTab : IDisposable, ITab table.DrawFrameColumn(text2); table.NextColumn(); - _dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Item.Name, string.Empty, - InputWidth * 2 * Im.Style.GlobalScale, - Im.Style.TextHeightWithSpacing); + _dirty |= targetSelector.Draw("##itemTarget"u8, targetSelector.CurrentSelection.Name, StringU8.Empty, InputWidth * 2 * Im.Style.GlobalScale, out _); if (type is SwapType.Ring) { Im.Line.Same(); @@ -638,7 +590,7 @@ public class ItemSwapTab : IDisposable, ITab if (Im.Item.Hovered()) { using var tt = Im.Tooltip.Begin(); - foreach (var item in _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Item.Name))) + foreach (var item in _affectedItems.Where(i => !ReferenceEquals(i.Name, targetSelector.CurrentSelection.Name))) Im.Text(item.Name); } } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs index 264cffd3..d9ca832c 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.ColorTable.cs @@ -1,4 +1,3 @@ -using Dalamud.Bindings.ImGui; using ImSharp; using Luna; using Penumbra.GameData.Files.MaterialStructs; @@ -87,8 +86,8 @@ public partial class MtrlTab var rowBIdx = rowAIdx | 1; var dyeA = dyeTable?[_colorTableSelectedPair << 1] ?? default; var dyeB = dyeTable?[(_colorTableSelectedPair << 1) | 1] ?? default; - var previewDyeA = _stainService.GetStainCombo(dyeA.Channel).CurrentSelection.Key; - var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Key; + var previewDyeA = _stainService.GetStainCombo(dyeA.Channel).CurrentSelection.Id; + var previewDyeB = _stainService.GetStainCombo(dyeB.Channel).CurrentSelection.Id; var dyePackA = _stainService.GudStmFile.GetValueOrNull(dyeA.Template, previewDyeA); var dyePackB = _stainService.GudStmFile.GetValueOrNull(dyeB.Template, previewDyeB); using (var columns = Im.Columns(2, "ColorTable"u8)) @@ -599,11 +598,10 @@ public partial class MtrlTab value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1)); Im.Line.Same(subColWidth); Im.Item.SetNextWidth(scalarSize); - _stainService.GudTemplateCombo.CurrentDyeChannel = dye.Channel; - if (_stainService.GudTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, - scalarSize + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ImGuiComboFlags.NoArrowButton)) + if (_stainService.GudTemplateCombo.Draw("##dyeTemplate"u8, dye.Template, dye.Channel, StringU8.Empty, out var newSelection, + scalarSize + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ComboFlags.NoArrowButton)) { - dye.Template = _stainService.GudTemplateCombo.CurrentSelection.UShort; + dye.Template = (ushort) newSelection; ret = true; } @@ -613,8 +611,8 @@ public partial class MtrlTab 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, + _stainService.StainCombo1.CurrentSelection.Id, + _stainService.StainCombo2.CurrentSelection.Id, ], rowIdx); return ret; diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs index 0a1eee39..a271426c 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.CommonColorTable.cs @@ -83,8 +83,8 @@ public partial class MtrlTab private bool DrawPreviewDye(bool disabled) { - var (dyeId1, (name1, dyeColor1, gloss1)) = _stainService.StainCombo1.CurrentSelection; - var (dyeId2, (name2, dyeColor2, gloss2)) = _stainService.StainCombo2.CurrentSelection; + var (name1, _, dyeId1, _) = _stainService.StainCombo1.CurrentSelection; + var (name2, _, dyeId2, _) = _stainService.StainCombo2.CurrentSelection; 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; @@ -103,12 +103,10 @@ public partial class MtrlTab } Im.Line.Same(); - var label = dyeId1 is 0 ? "Preview Dye 1###previewDye1" : $"{name1} (Preview 1)###previewDye1"; - if (_stainService.StainCombo1.Draw(label, dyeColor1, string.Empty, true, gloss1)) + if (_stainService.StainCombo1.Draw(dyeId1 is 0 ? "Preview Dye 1###pd1" : $"{name1} (Preview 1)###pd1")) UpdateColorTablePreview(); Im.Line.Same(); - label = dyeId2 is 0 ? "Preview Dye 2###previewDye2" : $"{name2} (Preview 2)###previewDye2"; - if (_stainService.StainCombo2.Draw(label, dyeColor2, string.Empty, true, gloss2)) + if (_stainService.StainCombo2.Draw(dyeId2 is 0 ? "Preview Dye 2###pd2" : $"{name2} (Preview 2)###pd2")) UpdateColorTablePreview(); return false; } diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs index 94a66085..7f4ddbca 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LegacyColorTable.cs @@ -1,4 +1,3 @@ -using Dalamud.Bindings.ImGui; using ImSharp; using Luna; using Penumbra.GameData.Files.MaterialStructs; @@ -177,13 +176,13 @@ public partial class MtrlTab ret |= CtTileTransformMatrix(row.TileTransform, floatSize, false, m => table[rowIdx].TileTransform = m); - if (dyeTable != null) + if (dyeTable is not null) { Im.Table.NextColumn(); - if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize - + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ImGuiComboFlags.NoArrowButton)) + if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate"u8, dye.Template, 0, StringU8.Empty, out var newSelection, + intSize + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ComboFlags.NoArrowButton)) { - dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection.UShort; + dyeTable[rowIdx].Template = (ushort)newSelection; ret = true; } @@ -294,11 +293,10 @@ public partial class MtrlTab ret |= CtDragScalar("##DyeChannel"u8, "Dye Channel"u8, dye.Channel + 1, "%hhd"u8, 1, StainService.ChannelCount, 0.25f, value => dyeTable[rowIdx].Channel = (byte)(Math.Clamp(value, 1, StainService.ChannelCount) - 1)); Im.Line.SameInner(); - _stainService.LegacyTemplateCombo.CurrentDyeChannel = dye.Channel; - if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate", dye.Template.ToString(), string.Empty, intSize - + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ImGuiComboFlags.NoArrowButton)) + if (_stainService.LegacyTemplateCombo.Draw("##dyeTemplate"u8, dye.Template, dye.Channel, StringU8.Empty, out var newSelection, + intSize + Im.Style.ScrollbarSize / 2, Im.Style.TextHeightWithSpacing, ComboFlags.NoArrowButton)) { - dyeTable[rowIdx].Template = _stainService.LegacyTemplateCombo.CurrentSelection.UShort; + dyeTable[rowIdx].Template = (ushort)newSelection; ret = true; } @@ -313,8 +311,8 @@ public partial class MtrlTab private bool DrawLegacyDyePreview(int rowIdx, bool disabled, LegacyColorDyeTableRow dye, float floatSize) { - var stain = _stainService.StainCombo1.CurrentSelection.Key; - if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) + var stain = _stainService.StainCombo1.CurrentSelection.Id; + if (stain is 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) return false; using var style = ImStyleDouble.ItemSpacing.Push(Im.Style.ItemSpacing / 2); @@ -331,8 +329,8 @@ public partial class MtrlTab private bool DrawLegacyDyePreview(int rowIdx, bool disabled, ColorDyeTableRow dye, float floatSize) { - var stain = _stainService.GetStainCombo(dye.Channel).CurrentSelection.Key; - if (stain == 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) + var stain = _stainService.GetStainCombo(dye.Channel).CurrentSelection.Id; + if (stain is 0 || !_stainService.LegacyStmFile.TryGetValue(dye.Template, stain, out var values)) return false; using var style = ImStyleDouble.ItemSpacing.Push(Im.Style.ItemSpacing / 2); @@ -341,8 +339,8 @@ public partial class MtrlTab ret = ret && Mtrl.ApplyDyeToRow(_stainService.LegacyStmFile, [ - _stainService.StainCombo1.CurrentSelection.Key, - _stainService.StainCombo2.CurrentSelection.Key, + _stainService.StainCombo1.CurrentSelection.Id, + _stainService.StainCombo2.CurrentSelection.Id, ], rowIdx); Im.Line.Same(); diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs index 84156806..cae85aa4 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.LivePreview.cs @@ -226,7 +226,7 @@ public partial class MtrlTab }; if (dyeRow.Channel < StainService.ChannelCount) { - StainId stainId = _stainService.GetStainCombo(dyeRow.Channel).CurrentSelection.Key; + StainId stainId = _stainService.GetStainCombo(dyeRow.Channel).CurrentSelection.Id; if (_stainService.LegacyStmFile.TryGetValue(dyeRow.Template, stainId, out var legacyDyes)) row.ApplyDye(dyeRow, legacyDyes); if (_stainService.GudStmFile.TryGetValue(dyeRow.Template, stainId, out var gudDyes)) @@ -260,8 +260,8 @@ public partial class MtrlTab { ReadOnlySpan stainIds = [ - _stainService.StainCombo1.CurrentSelection.Key, - _stainService.StainCombo2.CurrentSelection.Key, + _stainService.StainCombo1.CurrentSelection.Id, + _stainService.StainCombo2.CurrentSelection.Id, ]; rows.ApplyDye(_stainService.LegacyStmFile, stainIds, dyeRows); rows.ApplyDye(_stainService.GudStmFile, stainIds, dyeRows); diff --git a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs index f7b284fd..3149581a 100644 --- a/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs +++ b/Penumbra/UI/AdvancedWindow/Materials/MtrlTab.cs @@ -89,17 +89,17 @@ public sealed partial class MtrlTab : IWritable, IDisposable } DrawMaterialLivePreviewRebind(disabled); - + Im.Dummy(new Vector2(Im.Style.TextHeight / 2)); var ret = DrawBackFaceAndTransparency(disabled); - + Im.Dummy(new Vector2(Im.Style.TextHeight / 2)); ret |= DrawShaderSection(disabled); - + ret |= DrawTextureSection(disabled); ret |= DrawColorTableSection(disabled); ret |= DrawConstantsSection(disabled); - + Im.Dummy(new Vector2(Im.Style.TextHeight / 2)); DrawOtherMaterialDetails(disabled); diff --git a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs index 88ef8200..62702c8f 100644 --- a/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs +++ b/Penumbra/UI/AdvancedWindow/Meta/MetaDrawer.cs @@ -41,8 +41,12 @@ public abstract class MetaDrawer(ModMetaEditor editor, Meta var height = ColumnHeight; using var clipper = new Im.ListClipper(Count, height); - foreach (var (identifier, value) in clipper.Iterate(Enumerate())) + foreach (var (index, (identifier, value)) in clipper.Iterate(Enumerate().Index())) + { + id.Push(index); DrawEntry(identifier, value); + id.Pop(); + } } public abstract ReadOnlySpan Label { get; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs index 93e67d16..21651ecb 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Deformers.cs @@ -1,5 +1,4 @@ using ImSharp; -using OtterGui; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; @@ -61,17 +60,19 @@ public partial class ModEditWindow } } + private sealed class BoneCache(PbdData pbdData) : BasicFilterCache(pbdData.BoneFilter) + { + protected override IEnumerable GetItems() + => pbdData.SelectedDeformer is null || pbdData.SelectedDeformer.IsEmpty ? [] : pbdData.SelectedDeformer.DeformMatrices.Keys; + } + private void DrawBoneSelector() { + var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Get((int)_pbdData.SelectedRaceCode), () => new BoneCache(_pbdData)); using var group = Im.Group(); var width = 200 * Im.Style.GlobalScale; - using (ImStyleSingle.FrameRounding.Push(0) - .Push(ImStyleDouble.ItemSpacing, Vector2.Zero)) - { - Im.Item.SetNextWidth(width); - Im.Input.Text("##boneFilter"u8, ref _pbdData.BoneFilter, "Filter..."u8); - } - + _pbdData.BoneFilter.DrawFilter("Filter..."u8, new Vector2(width, Im.Style.FrameHeight)); + Im.Cursor.Y -= Im.Style.ItemSpacing.Y; using var child = Im.Child.Begin("Bone"u8, new Vector2(width, Im.ContentRegion.Maximum.Y - Im.Style.FrameHeight - Im.Style.WindowPadding.Y), true); if (!child) @@ -80,23 +81,14 @@ public partial class ModEditWindow if (_pbdData.SelectedDeformer is null) return; - if (_pbdData.SelectedDeformer.IsEmpty) - { + if (cache.AllItems.Count is 0) Im.Text(""u8); - } else - { - var height = Im.Style.TextHeightWithSpacing; - var skips = ImGuiClip.GetNecessarySkips(height); - var remainder = ImGuiClip.FilteredClippedDraw(_pbdData.SelectedDeformer.DeformMatrices.Keys, skips, - b => b.Contains(_pbdData.BoneFilter), bone - => - { - if (Im.Selectable(bone, bone == _pbdData.SelectedBone)) - _pbdData.SelectedBone = bone; - }); - ImGuiClip.DrawEndDummy(remainder, height); - } + foreach (var item in cache) + { + if (Im.Selectable(item, item == _pbdData.SelectedBone)) + _pbdData.SelectedBone = item; + } } private bool DrawBoneData(PbdTab tab, bool disabled) @@ -324,8 +316,8 @@ public partial class ModEditWindow public GenderRace SelectedRaceCode = GenderRace.Unknown; public RacialDeformer? SelectedDeformer; public string? SelectedBone; + public TextFilter BoneFilter = new(); public string NewBoneName = string.Empty; - public string BoneFilter = string.Empty; public string RaceCodeFilter = string.Empty; public TransformMatrix? CopiedMatrix; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index f5f2b022..2b84d6ce 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -1,7 +1,6 @@ using Dalamud.Interface; using ImSharp; using Luna; -using OtterGui; using Penumbra.Mods.Editor; using Penumbra.Mods.SubMods; using Penumbra.String.Classes; @@ -20,10 +19,7 @@ public partial class ModEditWindow private int _pathIdx = -1; private int _folderSkip; private bool _overviewMode; - - private string _fileOverviewFilter1 = string.Empty; - private string _fileOverviewFilter2 = string.Empty; - private string _fileOverviewFilter3 = string.Empty; + private readonly OverviewTable _overviewTable; private bool CheckFilter(FileRegistry registry) => _fileFilter.Length is 0 || registry.File.FullName.Contains(_fileFilter, StringComparison.OrdinalIgnoreCase); @@ -40,9 +36,7 @@ public partial class ModEditWindow DrawOptionSelectHeader(); DrawButtonHeader(); - if (_overviewMode) - DrawFileManagementOverview(); - else + if (!_overviewMode) DrawFileManagementNormal(); using var child = Im.Child.Begin("##files"u8, Im.ContentRegion.Available, true); @@ -50,65 +44,11 @@ public partial class ModEditWindow return; if (_overviewMode) - DrawFilesOverviewMode(); + _overviewTable.Draw(); else DrawFilesNormalMode(); } - private void DrawFilesOverviewMode() - { - var height = Im.Style.TextHeightWithSpacing + 2 * Im.Style.CellPadding.Y; - var skips = ImGuiClip.GetNecessarySkips(height); - - using var table = Im.Table.Begin("##table"u8, 3, TableFlags.RowBackground | TableFlags.BordersInnerVertical, Im.ContentRegion.Available); - - if (!table) - return; - - var width = Im.ContentRegion.Available.X / 8; - - table.SetupColumn("##file"u8, TableColumnFlags.WidthFixed, width * 3); - table.SetupColumn("##path"u8, TableColumnFlags.WidthFixed, width * 3 + Im.Style.FrameBorderThickness); - table.SetupColumn("##option"u8, TableColumnFlags.WidthFixed, width * 2); - - var idx = 0; - - var files = _editor.Files.Available.SelectMany(f => - { - var file = f.RelPath.ToString(); - return f.SubModUsage.Count == 0 - ? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1) - : f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.GetFullName(), - _editor.Option! == s.Item1 && Mod!.HasOptions ? 0x40008000u : 0u)); - }); - - void DrawLine((string, string, string, uint) data) - { - using var id = Im.Id.Push(idx++); - if (data.Item4 is not 0) - Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, data.Item4); - - ImEx.CopyOnClickSelectable(data.Item1); - Im.Table.NextColumn(); - if (data.Item4 is not 0) - Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, data.Item4); - - ImEx.CopyOnClickSelectable(data.Item2); - Im.Table.NextColumn(); - if (data.Item4 is not 0) - Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, data.Item4); - - ImEx.CopyOnClickSelectable(data.Item3); - } - - bool Filter((string, string, string, uint) data) - => data.Item1.Contains(_fileOverviewFilter1, StringComparison.OrdinalIgnoreCase) - && data.Item2.Contains(_fileOverviewFilter2, StringComparison.OrdinalIgnoreCase) - && data.Item3.Contains(_fileOverviewFilter3, StringComparison.OrdinalIgnoreCase); - - var end = ImGuiClip.FilteredClippedDraw(files, skips, Filter, DrawLine); - ImGuiClip.DrawEndDummy(end, height); - } private void DrawFilesNormalMode() { @@ -154,7 +94,7 @@ public partial class ModEditWindow { using var tt = Im.Tooltip.Begin(); using var c = ImGuiColor.Text.PushDefault(); - Im.Text(StringU8.Join((byte) '\n', text)); + Im.Text(StringU8.Join((byte)'\n', text)); } @@ -255,7 +195,7 @@ public partial class ModEditWindow var tmp = _fileIdx == i && _pathIdx == j ? _gamePathEdit : gamePath.ToString(); var pos = Im.Cursor.X - Im.Style.FrameHeight; Im.Item.SetNextWidth(-1); - if (Im.Input.Text(StringU8.Empty, ref tmp, maxLength:Utf8GamePath.MaxGamePathLength)) + if (Im.Input.Text(StringU8.Empty, ref tmp, maxLength: Utf8GamePath.MaxGamePathLength)) { _fileIdx = i; _pathIdx = j; @@ -341,7 +281,8 @@ public partial class ModEditWindow if (Im.Button("Add Paths"u8)) _editor.FileEditor.AddPathsToSelected(_editor.Option!, _editor.Files.Available.Where(_selectedFiles.Contains), _folderSkip); - Im.Tooltip.OnHover("Add the file path converted to a game path to all selected files for the current option, optionally skipping the first N folders."u8); + Im.Tooltip.OnHover( + "Add the file path converted to a game path to all selected files for the current option, optionally skipping the first N folders."u8); Im.Line.Same(); @@ -365,7 +306,7 @@ public partial class ModEditWindow Im.Line.Same(); var changes = _editor.FileEditor.Changes; - var tt2 = changes ? "Apply the current file setup to the currently selected option."u8 : "No changes made."u8; + var tt2 = changes ? "Apply the current file setup to the currently selected option."u8 : "No changes made."u8; if (ImEx.Button("Apply Changes"u8, Vector2.Zero, tt2, !changes)) { var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, _editor.Option!); @@ -412,22 +353,4 @@ public partial class ModEditWindow ImEx.TextRightAligned($"{_selectedFiles.Count} / {_editor.Files.Available.Count} Files Selected"); } - - private void DrawFileManagementOverview() - { - using var style = ImStyleSingle.FrameRounding.Push(0) - .Push(ImStyleDouble.ItemSpacing, Vector2.Zero) - .Push(ImStyleSingle.FrameBorderThickness, Im.Style.ChildBorderThickness); - - var width = Im.ContentRegion.Available.X / 8; - - Im.Item.SetNextWidth(width * 3); - Im.Input.Text("##fileFilter"u8, ref _fileOverviewFilter1, "Filter file..."u8); - Im.Line.Same(); - Im.Item.SetNextWidth(width * 3); - Im.Input.Text("##pathFilter"u8, ref _fileOverviewFilter2, "Filter path..."u8); - Im.Line.Same(); - Im.Item.SetNextWidth(width * 2); - Im.Input.Text("##optionFilter"u8, ref _fileOverviewFilter3, "Filter option..."u8); - } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index e37fa953..8d90e87d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -107,7 +107,7 @@ public partial class ModEditWindow private void DrawEditHeader(MetaManipulationType type) { var drawer = _metaDrawers.Get(type); - if (drawer == null) + if (drawer is null) return; var oldPos = Im.Cursor.Y; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index f26c0afd..1b6547e7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -621,6 +621,7 @@ public partial class ModEditWindow : IndexedWindow, IDisposable _fileDialog = fileDialog; _framework = framework; _metaDrawers = metaDrawers; + _overviewTable = new OverviewTable(_editor); _optionSelect = new OptionSelectCombo(editor, this); _materialTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, diff --git a/Penumbra/UI/AdvancedWindow/OverviewTable.cs b/Penumbra/UI/AdvancedWindow/OverviewTable.cs new file mode 100644 index 00000000..5311249c --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/OverviewTable.cs @@ -0,0 +1,141 @@ +using ImSharp; +using ImSharp.Containers; +using ImSharp.Table; +using Penumbra.Mods.Editor; +using Penumbra.Mods.SubMods; +using Penumbra.String.Classes; + +namespace Penumbra.UI.AdvancedWindow; + +public sealed class OverviewTable(ModEditor parent) + : DefaultTable(new StringU8("##overview"u8), + new FileColumn + { + Label = new StringU8("File"u8), + Flags = TableColumnFlags.WidthStretch, + }, + new PathColumn + { + Label = new StringU8("Path"u8), + Flags = TableColumnFlags.WidthStretch, + }, + new OptionColumn + { + Label = new StringU8("Option"u8), + Flags = TableColumnFlags.WidthStretch, + }) +{ + public sealed record OverviewFile( + StringPair File, + StringPair Path, + StringPair OptionName, + IModDataContainer? Option, + ColorParameter Color) + { + public ColorParameter Color { get; set; } = Color; + + public OverviewFile(FileRegistry file) + : this(new StringPair(file.RelPath.Path.Span), new StringPair("Unused", new StringU8("Unused"u8)), StringPair.Empty, null, + 0x40000080) + { } + + public OverviewFile(FileRegistry file, IModDataContainer option, Utf8GamePath gamePath, bool tint) + : this(new StringPair(file.RelPath.Path.Span), new StringPair(gamePath.Path.Span), new StringPair(option.GetFullName()), + option, tint ? 0x40008000 : ColorParameter.Default) + { } + } + + private sealed class FileColumn : TextColumn + { + protected override string ComparisonText(in OverviewFile item, int globalIndex) + => item.File; + + protected override StringU8 DisplayText(in OverviewFile item, int globalIndex) + => item.File; + + public override void DrawColumn(in OverviewFile item, int globalIndex) + { + Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, item.Color); + ImEx.CopyOnClickSelectable(item.File.Utf8); + } + + public override float ComputeWidth(IEnumerable _) + => 3 / 8f; + } + + private sealed class PathColumn : TextColumn + { + protected override string ComparisonText(in OverviewFile item, int globalIndex) + => item.Path; + + protected override StringU8 DisplayText(in OverviewFile item, int globalIndex) + => item.Path; + + public override void DrawColumn(in OverviewFile item, int globalIndex) + { + Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, item.Color); + ImEx.CopyOnClickSelectable(item.Path.Utf8); + } + + public override float ComputeWidth(IEnumerable _) + => 3 / 8f; + } + + private sealed class OptionColumn : TextColumn + { + protected override string ComparisonText(in OverviewFile item, int globalIndex) + => item.OptionName; + + protected override StringU8 DisplayText(in OverviewFile item, int globalIndex) + => item.OptionName; + + public override void DrawColumn(in OverviewFile item, int globalIndex) + { + Im.Table.SetBackgroundColor(TableBackgroundTarget.Cell, item.Color); + ImEx.CopyOnClickSelectable(item.OptionName.Utf8); + } + + public override float ComputeWidth(IEnumerable _) + => 2 / 8f; + } + + public override IEnumerable GetItems() + => parent.Files.Available.SelectMany(f => + { + return f.SubModUsage.Count is 0 + ? [new OverviewFile(f)] + : f.SubModUsage.Select(s => new OverviewFile(f, s.Item1, s.Item2, parent.Option! == s.Item1 && parent.Mod!.HasOptions)); + }); + + protected override TableCache CreateCache() + => new Cache(this, parent); + + private sealed class Cache : TableCache + { + private readonly ModEditor _editor; + + public Cache(OverviewTable table, ModEditor editor) + : base(table) + { + _editor = editor; + _editor.Files.Available.OnChange += OnChange; + _editor.OptionLoaded += OnOptionLoaded; + } + + private void OnOptionLoaded() + { + foreach (var item in UnfilteredItems.Where(i => i.Option is not null)) + item.Color = _editor.Option == item.Option && _editor.Mod!.HasOptions ? 0x40008000 : ColorParameter.Default; + } + + private void OnChange(in ObservableList.ChangeArguments args) + => Dirty |= IManagedCache.DirtyFlags.Custom; + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _editor.Files.Available.OnChange -= OnChange; + _editor.OptionLoaded -= OnOptionLoaded; + } + } +} diff --git a/Penumbra/UI/Classes/CollectionSelectHeader.cs b/Penumbra/UI/Classes/CollectionSelectHeader.cs index 206352c2..6e41cf33 100644 --- a/Penumbra/UI/Classes/CollectionSelectHeader.cs +++ b/Penumbra/UI/Classes/CollectionSelectHeader.cs @@ -46,7 +46,8 @@ public class CollectionSelectHeader( tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); if (!_activeCollections.CurrentCollectionInUse) - ImEx.TextFramed("The currently selected collection is not used in any way."u8, -Vector2.UnitX, Colors.PressEnterWarningBg); + ImEx.TextFramed("The currently selected collection is not used in any way."u8, Im.ContentRegion.Available with { Y = 0 }, + Colors.PressEnterWarningBg); } private void DrawTemporaryCheckbox() @@ -185,6 +186,7 @@ public class CollectionSelectHeader( tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); if (!_activeCollections.CurrentCollectionInUse) - ImEx.TextFramed("The currently selected collection is not used in any way."u8, -Vector2.UnitX, Colors.PressEnterWarningBg); + ImEx.TextFramed("The currently selected collection is not used in any way."u8, Im.ContentRegion.Available with { Y = 0 }, + Colors.PressEnterWarningBg); } } diff --git a/Penumbra/UI/Classes/IncognitoService.cs b/Penumbra/UI/Classes/IncognitoService.cs index e6bdaa78..40f6135b 100644 --- a/Penumbra/UI/Classes/IncognitoService.cs +++ b/Penumbra/UI/Classes/IncognitoService.cs @@ -24,7 +24,7 @@ public class IncognitoService(TutorialService tutorial, Configuration config) : } if (!hold) - Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, $"\nHold {config.IncognitoModifier} while clicking to toggle."); + Im.Tooltip.OnHover($"\nHold {config.IncognitoModifier} while clicking to toggle.", HoveredFlags.AllowWhenDisabled, true); tutorial.OpenTutorial(BasicTutorialSteps.Incognito); } diff --git a/Penumbra/UI/CollectionTab/CollectionFilter.cs b/Penumbra/UI/CollectionTab/CollectionFilter.cs new file mode 100644 index 00000000..57d0a44f --- /dev/null +++ b/Penumbra/UI/CollectionTab/CollectionFilter.cs @@ -0,0 +1,13 @@ +using ImSharp; +using Luna; + +namespace Penumbra.UI.CollectionTab; + +public sealed class CollectionFilter : TextFilterBase, IUiService +{ + public override bool WouldBeVisible(in CollectionSelector.Entry item, int globalIndex) + => base.WouldBeVisible(in item, globalIndex) || WouldBeVisible(item.AnonymousName.Utf16); + + protected override string ToFilterString(in CollectionSelector.Entry item, int globalIndex) + => item.Collection.Identity.Name; +} diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index d6cca03e..9811ab09 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -1,4 +1,3 @@ -using Dalamud.Game.ClientState.Objects; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ImGuiNotification; @@ -14,6 +13,7 @@ using Penumbra.GameData.Enums; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; +using Penumbra.UI.Tabs; namespace Penumbra.UI.CollectionTab; @@ -26,8 +26,9 @@ public sealed class CollectionPanel( ITargetManager targets, ModStorage mods, SaveService saveService, - IncognitoService incognito) - : IDisposable + IncognitoService incognito, + Configuration config) + : IDisposable, IPanel { private readonly CollectionStorage _collections = manager.Storage; private readonly ActiveCollections _active = manager.Active; @@ -223,13 +224,8 @@ public sealed class CollectionPanel( { using var style = ImStyleDouble.ButtonTextAlign.Push(new Vector2(0, 0.5f)); Im.Item.SetNextWidth(width); - if (ImEx.InputOnDeactivation.Text("##name"u8, collection.Identity.Name, out string newName) - && newName != collection.Identity.Name) - { - collection.Identity.Name = newName; - saveService.QueueSave(new ModCollectionSave(mods, collection)); - selector.RestoreCollections(); - } + if (ImEx.InputOnDeactivation.Text("##name"u8, collection.Identity.Name, out string newName)) + _collections.RenameCollection(collection, newName); } if (_collections.DefaultNamed == collection) @@ -770,4 +766,18 @@ public sealed class CollectionPanel( ret.Add((type, localPre, post, name, border)); } } + + public ReadOnlySpan Id + => "cp"u8; + + public void Draw() + { + switch (config.Ephemeral.CollectionPanel) + { + case CollectionPanelMode.SimpleAssignment: DrawSimple(); break; + case CollectionPanelMode.IndividualAssignment: DrawIndividualPanel(); break; + case CollectionPanelMode.GroupAssignment: DrawGroupPanel(); break; + case CollectionPanelMode.Details: DrawDetailsPanel(); break; + } + } } diff --git a/Penumbra/UI/CollectionTab/CollectionSelector.cs b/Penumbra/UI/CollectionTab/CollectionSelector.cs index 8e339111..02f49a8e 100644 --- a/Penumbra/UI/CollectionTab/CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/CollectionSelector.cs @@ -1,5 +1,5 @@ using ImSharp; -using OtterGui; +using Luna; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Communication; @@ -9,87 +9,20 @@ using Penumbra.UI.Classes; namespace Penumbra.UI.CollectionTab; -public sealed class CollectionSelector : ItemSelector, IDisposable +public sealed class CollectionSelector(ActiveCollections active, TutorialService tutorial, IncognitoService incognito) : IPanel { - private readonly Configuration _config; - private readonly CommunicatorService _communicator; - private readonly CollectionStorage _storage; - private readonly ActiveCollections _active; - private readonly TutorialService _tutorial; - private readonly IncognitoService _incognito; + public ReadOnlySpan Id + => "##cs"u8; 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) + public record struct Entry(ModCollection Collection, StringU8 Name, StringPair AnonymousName) { - _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 Entry(ModCollection collection) + : this(collection, + collection.Identity.Name.Length > 0 ? new StringU8(collection.Identity.Name) : new StringU8(collection.Identity.AnonymizedName), + new StringPair(collection.Identity.AnonymizedName)) + { } } public void DragTargetAssignment(CollectionType type, ActorIdentifier identifier) @@ -98,45 +31,72 @@ public sealed class CollectionSelector : ItemSelector, IDisposabl if (!target.Success || _dragging is null || !target.IsDropping("Collection"u8)) return; - _active.SetCollection(_dragging, type, _active.Individuals.GetGroup(identifier)); + active.SetCollection(_dragging, type, active.Individuals.GetGroup(identifier)); _dragging = null; } - public void Dispose() + public void Draw() { - _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) + Im.Cursor.Y += Im.Style.FramePadding.Y; + var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current); + using var color = ImGuiColor.Header.Push(ColorId.SelectedCollection.Value()); + foreach (var item in cache) { - 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; + Im.Cursor.X += Im.Style.FramePadding.X; + var ret = Im.Selectable(incognito.IncognitoMode ? item.AnonymousName : item.Name, active.Current == item.Collection); + using var source = Im.DragDrop.Source(); + + if (active.Current == item.Collection) + tutorial.OpenTutorial(BasicTutorialSteps.CurrentCollection); + + if (source) + { + _dragging = item.Collection; + source.SetPayload("Collection"u8); + Im.Text($"Assigning {(incognito.IncognitoMode ? item.AnonymousName : item.Name)} to..."); + } + + if (ret) + active.SetCollection(item.Collection, CollectionType.Current); + } + } + + public sealed class Cache : BasicFilterCache, IService + { + private readonly CollectionStorage _collections; + private readonly CommunicatorService _communicator; + + public Cache(CollectionFilter filter, CollectionStorage collections, CommunicatorService communicator) + : base(filter) + { + _collections = collections; + _communicator = communicator; + _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.CollectionSelectorCache); + _communicator.CollectionRename.Subscribe(OnCollectionRename, CollectionRename.Priority.CollectionSelectorCache); + } + + private void OnCollectionRename(in CollectionRename.Arguments arguments) + => Dirty |= IManagedCache.DirtyFlags.Custom; + + private void OnCollectionChange(in CollectionChange.Arguments arguments) + { + if (arguments.Type is CollectionType.Inactive) + Dirty |= IManagedCache.DirtyFlags.Custom; + } + + protected override IEnumerable GetItems() + { + yield return new Entry(_collections.DefaultNamed); + + foreach (var collection in _collections.Where(c => c != _collections.DefaultNamed).OrderBy(c => c.Identity.Name)) + yield return new Entry(collection); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _communicator.CollectionChange.Unsubscribe(OnCollectionChange); + _communicator.CollectionRename.Unsubscribe(OnCollectionRename); } } } diff --git a/Penumbra/UI/Combos/SingleGroupCombo.cs b/Penumbra/UI/Combos/SingleGroupCombo.cs new file mode 100644 index 00000000..f340ee11 --- /dev/null +++ b/Penumbra/UI/Combos/SingleGroupCombo.cs @@ -0,0 +1,52 @@ +using ImSharp; +using Luna; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.UI.ModsTab.Groups; + +namespace Penumbra.UI; + +public sealed class SingleGroupCombo : FilterComboBase, IUiService +{ + private class OptionFilter : Utf8FilterBase + { + protected override ReadOnlySpan ToFilterString(in Test item, int globalIndex) + => item.Name; + } + + public SingleGroupCombo() + => Filter = new OptionFilter(); + + public readonly record struct Test(int OptionIndex, StringU8 Name, StringU8 Description); + + private readonly WeakReference _group = new(null!); + private Setting _currentOption; + + public void Draw(ModGroupDrawer parent, SingleModGroup group, int groupIndex, Setting currentOption) + { + _currentOption = currentOption; + _group.SetTarget(group); + if (base.Draw(group.Name, group.OptionData[currentOption.AsIndex].Name, StringU8.Empty, UiHelpers.InputTextWidth.X * 3 / 4, + out var newOption)) + parent.SetModSetting(group, groupIndex, Setting.Single(newOption.OptionIndex)); + } + + protected override IEnumerable GetItems() + => _group.TryGetTarget(out var target) + ? target.OptionData.Select(o => new Test(o.GetIndex(), new StringU8(o.Name), new StringU8(o.Description))) + : []; + + protected override float ItemHeight + => Im.Style.TextHeightWithSpacing; + + protected override bool DrawItem(in Test item, int globalIndex, bool selected) + { + var ret = Im.Selectable(item.Name, selected); + if (item.Description.Length > 0) + LunaStyle.DrawHelpMarker(item.Description, treatAsHovered: Im.Item.Hovered()); + return ret; + } + + protected override bool IsSelected(Test item, int globalIndex) + => item.OptionIndex == _currentOption.AsIndex; +} diff --git a/Penumbra/UI/Combos/StainCombo.cs b/Penumbra/UI/Combos/StainCombo.cs deleted file mode 100644 index 33692073..00000000 --- a/Penumbra/UI/Combos/StainCombo.cs +++ /dev/null @@ -1,118 +0,0 @@ -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; - -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/MainWindow/MainWindow.cs b/Penumbra/UI/MainWindow/MainWindow.cs index 7182d484..ac0f15f0 100644 --- a/Penumbra/UI/MainWindow/MainWindow.cs +++ b/Penumbra/UI/MainWindow/MainWindow.cs @@ -3,7 +3,6 @@ using ImSharp; using Luna; using Penumbra.Services; using Penumbra.UI.Classes; -using Penumbra.UI.Tabs; using TabType = Penumbra.Api.Enums.TabType; namespace Penumbra.UI.MainWindow; diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index aeed0ae3..3ab02759 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -1,65 +1,22 @@ using ImSharp; using Luna; -using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Mods; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; -using MouseWheelType = OtterGui.Widgets.MouseWheelType; namespace Penumbra.UI.ModsTab.Groups; -public sealed class ModGroupDrawer : IUiService +public sealed class ModGroupDrawer(Configuration config, CollectionManager collectionManager, SingleGroupCombo combo) + : IUiService { private readonly List<(IModGroup, int)> _blockGroupCache = []; private bool _temporary; private bool _locked; private TemporaryModSettings? _tempSettings; private ModSettings? _settings; - private readonly SingleGroupCombo _combo; - private readonly Configuration _config; - private readonly CollectionManager _collectionManager; - - public ModGroupDrawer(Configuration config, CollectionManager collectionManager) - { - _config = config; - _collectionManager = collectionManager; - _combo = new SingleGroupCombo(this); - } - - private sealed class SingleGroupCombo(ModGroupDrawer parent) - : FilterComboCache(() => _group!.Options, MouseWheelType.Control, Penumbra.Log) - { - private static IModGroup? _group; - private static int _groupIdx; - - protected override bool DrawSelectable(int globalIdx, bool selected) - { - var option = _group!.Options[globalIdx]; - var ret = Im.Selectable(option.Name, globalIdx == CurrentSelectionIdx); - - if (option.Description.Length > 0) - LunaStyle.DrawHelpMarker(option.Description, treatAsHovered: Im.Item.Hovered()); - - return ret; - } - - protected override string ToString(IModOption obj) - => obj.Name; - - public void Draw(IModGroup group, int groupIndex, int currentOption) - { - _group = group; - _groupIdx = groupIndex; - CurrentSelectionIdx = currentOption; - CurrentSelection = _group.Options[CurrentSelectionIdx]; - if (Draw(string.Empty, CurrentSelection.Name, string.Empty, ref CurrentSelectionIdx, UiHelpers.InputTextWidth.X * 3 / 4, - Im.Style.TextHeightWithSpacing)) - parent.SetModSetting(_group, _groupIdx, Setting.Single(CurrentSelectionIdx)); - } - } public void Draw(Mod mod, ModSettings settings, TemporaryModSettings? tempSettings) { @@ -79,7 +36,7 @@ public sealed class ModGroupDrawer : IUiService switch (group.Behaviour) { - case GroupDrawBehaviour.SingleSelection when group.Options.Count <= _config.SingleGroupRadioMax: + case GroupDrawBehaviour.SingleSelection when group.Options.Count <= config.SingleGroupRadioMax: case GroupDrawBehaviour.MultiSelection: _blockGroupCache.Add((group, idx)); break; @@ -120,9 +77,8 @@ public sealed class ModGroupDrawer : IUiService private void DrawSingleGroupCombo(IModGroup group, int groupIdx, Setting setting) { using var id = Im.Id.Push(groupIdx); - var selectedOption = setting.AsIndex; using var disabled = Im.Disabled(_locked); - _combo.Draw(group, groupIdx, selectedOption); + combo.Draw(this, (SingleModGroup)group, groupIdx, setting); if (group.Description.Length > 0) { LunaStyle.DrawHelpMarkerLabel(group.Name, group.Description); @@ -227,7 +183,7 @@ public sealed class ModGroupDrawer : IUiService private void DrawCollapseHandling(IReadOnlyList options, float minWidth, Action draw) { - if (options.Count <= _config.OptionGroupCollapsibleMin) + if (options.Count <= config.OptionGroupCollapsibleMin) { draw(); } @@ -272,21 +228,21 @@ public sealed class ModGroupDrawer : IUiService } private ModCollection Current - => _collectionManager.Active.Current; + => collectionManager.Active.Current; [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private void SetModSetting(IModGroup group, int groupIdx, Setting setting) + internal void SetModSetting(IModGroup group, int groupIdx, Setting setting) { - if (_temporary || _config.DefaultTemporaryMode) + if (_temporary || config.DefaultTemporaryMode) { _tempSettings ??= new TemporaryModSettings(group.Mod, _settings); _tempSettings!.ForceInherit = false; _tempSettings!.Settings[groupIdx] = setting; - _collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings); + collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings); } else { - _collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting); + collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting); } } } diff --git a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs index d8a6fc52..c477aae7 100644 --- a/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelConflictsTab.cs @@ -1,7 +1,6 @@ using Dalamud.Interface; using ImSharp; using Luna; -using OtterGui.Widgets; using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.Meta.Manipulations; diff --git a/Penumbra/UI/ModsTab/Selector/ModFileSystemCache.cs b/Penumbra/UI/ModsTab/Selector/ModFileSystemCache.cs index 4655d478..e9191076 100644 --- a/Penumbra/UI/ModsTab/Selector/ModFileSystemCache.cs +++ b/Penumbra/UI/ModsTab/Selector/ModFileSystemCache.cs @@ -120,7 +120,15 @@ public sealed class ModFileSystemCache(ModFileSystemDrawer parent) } public override void Update() - { } + { + if (ColorsDirty) + { + CollapsedFolderColor = ColorId.FolderCollapsed.Value().ToVector(); + ExpandedFolderColor = ColorId.FolderExpanded.Value().ToVector(); + LineColor = ColorId.FolderLine.Value().ToVector(); + Dirty &= ~IManagedCache.DirtyFlags.Colors; + } + } protected override ModData ConvertNode(in IFileSystemNode node) => new((IFileSystemData)node); diff --git a/Penumbra/UI/Tabs/CollectionButtonFooter.cs b/Penumbra/UI/Tabs/CollectionButtonFooter.cs new file mode 100644 index 00000000..e4e0bb55 --- /dev/null +++ b/Penumbra/UI/Tabs/CollectionButtonFooter.cs @@ -0,0 +1,90 @@ +using ImSharp; +using Luna; +using Penumbra.Collections.Manager; +using Penumbra.Services; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.Tabs; + +public sealed class CollectionButtonFooter : ButtonFooter +{ + public CollectionButtonFooter(CollectionManager collectionManager, CommunicatorService communicator, Configuration configuration, + TutorialService tutorial, IncognitoService incognito) + { + Buttons.AddButton(new AddButton(collectionManager.Storage), 100); + Buttons.AddButton(new DuplicateButton(collectionManager.Storage, collectionManager.Active), 50); + Buttons.AddButton(new DeleteButton(collectionManager.Storage, collectionManager.Active, configuration), 0); + } + + public sealed class AddButton(CollectionStorage collections) : BaseIconButton + { + public override AwesomeIcon Icon + => LunaStyle.AddObjectIcon; + + public override bool HasTooltip + => true; + + public override void DrawTooltip() + => Im.Text("Add a new, empty collection."u8); + + public override void OnClick() + => Im.Popup.Open("NewCollection"u8); + + protected override void PostDraw() + { + if (!InputPopup.OpenName("NewCollection"u8, out var newCollectionName)) + return; + + collections.AddCollection(newCollectionName, null); + } + } + + public sealed class DeleteButton(CollectionStorage collections, ActiveCollections active, Configuration config) + : BaseIconButton + { + public override AwesomeIcon Icon + => LunaStyle.DeleteIcon; + + public override bool HasTooltip + => true; + + public override bool Enabled + => collections.DefaultNamed != active.Current + && config.DeleteModModifier.IsActive(); + + public override void DrawTooltip() + { + Im.Text("Delete the current collection."u8); + if (collections.DefaultNamed == active.Current) + Im.Text("The default collection cannot be deleted."u8); + else if (!config.DeleteModModifier.IsActive()) + Im.Text($"Hold {config.DeleteModModifier} to delete the current collection."); + } + + public override void OnClick() + => collections.RemoveCollection(active.Current); + } + + public sealed class DuplicateButton(CollectionStorage collections, ActiveCollections active) : BaseIconButton + { + public override AwesomeIcon Icon + => LunaStyle.DuplicateIcon; + + public override bool HasTooltip + => true; + + public override void DrawTooltip() + => Im.Text("Duplicate the currently selected collection to a new one."u8); + + public override void OnClick() + => Im.Popup.Open("DuplicateCollection"u8); + + protected override void PostDraw() + { + if (!InputPopup.OpenName("DuplicateCollection"u8, out var newCollectionName)) + return; + + collections.AddCollection(newCollectionName, active.Current); + } + } +} diff --git a/Penumbra/UI/Tabs/CollectionModeHeaderFooter.cs b/Penumbra/UI/Tabs/CollectionModeHeaderFooter.cs new file mode 100644 index 00000000..0b7a4a80 --- /dev/null +++ b/Penumbra/UI/Tabs/CollectionModeHeaderFooter.cs @@ -0,0 +1,66 @@ +using ImSharp; +using Luna; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.Tabs; + +public enum CollectionPanelMode +{ + SimpleAssignment, + IndividualAssignment, + GroupAssignment, + Details, +}; + +public sealed class CollectionModeHeader(Configuration config, TutorialService tutorial, IncognitoService incognito) : IHeader +{ + public bool Collapsed + => false; + + private CollectionPanelMode Mode + { + get => config.Ephemeral.CollectionPanel; + set + { + config.Ephemeral.CollectionPanel = value; + config.Ephemeral.Save(); + } + } + + public void Draw(Vector2 size) + { + var withSpacing = Im.Style.FrameHeightWithSpacing; + var buttonSize = new Vector2((Im.ContentRegion.Available.X - withSpacing) / 4f, Im.Style.FrameHeight); + + var tabSelectedColor = Im.Style[ImGuiColor.TabSelected]; + using var color = ImGuiColor.Button.Push(tabSelectedColor, Mode is CollectionPanelMode.SimpleAssignment); + if (Im.Button("Simple Assignments"u8, buttonSize)) + Mode = CollectionPanelMode.SimpleAssignment; + color.Pop(); + tutorial.OpenTutorial(BasicTutorialSteps.SimpleAssignments); + Im.Line.NoSpacing(); + + color.Push(ImGuiColor.Button, tabSelectedColor, Mode is CollectionPanelMode.IndividualAssignment); + if (Im.Button("Individual Assignments"u8, buttonSize)) + Mode = CollectionPanelMode.IndividualAssignment; + color.Pop(); + tutorial.OpenTutorial(BasicTutorialSteps.IndividualAssignments); + Im.Line.NoSpacing(); + + color.Push(ImGuiColor.Button, tabSelectedColor, Mode is CollectionPanelMode.GroupAssignment); + if (Im.Button("Group Assignments"u8, buttonSize)) + Mode = CollectionPanelMode.GroupAssignment; + color.Pop(); + tutorial.OpenTutorial(BasicTutorialSteps.GroupAssignments); + Im.Line.NoSpacing(); + + color.Push(ImGuiColor.Button, tabSelectedColor, Mode is CollectionPanelMode.Details); + if (Im.Button("Collection Details"u8, buttonSize)) + Mode = CollectionPanelMode.Details; + color.Pop(); + tutorial.OpenTutorial(BasicTutorialSteps.CollectionDetails); + Im.Line.NoSpacing(); + + incognito.DrawToggle(withSpacing); + } +} diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 6c819000..cc1eef02 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -1,151 +1,42 @@ -using Dalamud.Plugin.Services; -using Dalamud.Plugin; -using ImSharp; -using Luna; -using Penumbra.Api.Enums; -using Penumbra.Collections.Manager; -using Penumbra.GameData.Actors; -using Penumbra.Mods.Manager; -using Penumbra.Services; -using Penumbra.UI.Classes; -using Penumbra.UI.CollectionTab; - -namespace Penumbra.UI.Tabs; - -public sealed class CollectionsTab : ITab, IDisposable -{ - private readonly EphemeralConfig _config; - private readonly CollectionSelector _selector; - private readonly CollectionPanel _panel; - private readonly TutorialService _tutorial; - private readonly IncognitoService _incognito; - - public enum PanelMode - { - SimpleAssignment, - IndividualAssignment, - GroupAssignment, - Details, - }; - - public PanelMode Mode - { - get => _config.CollectionPanel; - set - { - _config.CollectionPanel = value; - _config.Save(); - } - } - - public TabType Identifier - => TabType.Collections; - - public CollectionsTab(IDalamudPluginInterface pi, Configuration configuration, CommunicatorService communicator, IncognitoService incognito, - CollectionManager collectionManager, ModStorage modStorage, ActorManager actors, ITargetManager targets, TutorialService tutorial, SaveService saveService) - { - _config = configuration.Ephemeral; - _tutorial = tutorial; - _incognito = incognito; - _selector = new CollectionSelector(configuration, communicator, collectionManager.Storage, collectionManager.Active, _tutorial, incognito); - _panel = new CollectionPanel(pi, communicator, collectionManager, _selector, actors, targets, modStorage, saveService, incognito); - } - - public void Dispose() - { - _selector.Dispose(); - _panel.Dispose(); - } - - public ReadOnlySpan Label - => "Collections"u8; - - public void DrawContent() - { - var width = Im.Font.CalculateSize("nnnnnnnnnnnnnnnnnnnnnnnnnn"u8).X; - using (Im.Group()) - { - _selector.Draw(width); - } - - _tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections); - - Im.Line.Same(); - using (Im.Group()) - { - DrawHeaderLine(); - DrawPanel(); - } - } - - public void DrawHeader() - { - _tutorial.OpenTutorial(BasicTutorialSteps.Collections); - } - - private void DrawHeaderLine() - { - var withSpacing = Im.Style.FrameHeightWithSpacing; - using var style = ImStyleSingle.FrameRounding.Push(0).Push(ImStyleDouble.ItemSpacing, Vector2.Zero); - var buttonSize = new Vector2((Im.ContentRegion.Available.X - withSpacing) / 4f, Im.Style.FrameHeight); - - using var _ = Im.Group(); - var tabSelectedColor = Im.Style[ImGuiColor.TabSelected]; - using var color = ImGuiColor.Button.Push(tabSelectedColor, Mode is PanelMode.SimpleAssignment); - if (Im.Button("Simple Assignments"u8, buttonSize)) - Mode = PanelMode.SimpleAssignment; - color.Pop(); - _tutorial.OpenTutorial(BasicTutorialSteps.SimpleAssignments); - Im.Line.Same(); - - color.Push(ImGuiColor.Button, tabSelectedColor, Mode is PanelMode.IndividualAssignment); - if (Im.Button("Individual Assignments"u8, buttonSize)) - Mode = PanelMode.IndividualAssignment; - color.Pop(); - _tutorial.OpenTutorial(BasicTutorialSteps.IndividualAssignments); - Im.Line.Same(); - - color.Push(ImGuiColor.Button, tabSelectedColor, Mode is PanelMode.GroupAssignment); - if (Im.Button("Group Assignments"u8, buttonSize)) - Mode = PanelMode.GroupAssignment; - color.Pop(); - _tutorial.OpenTutorial(BasicTutorialSteps.GroupAssignments); - Im.Line.Same(); - - color.Push(ImGuiColor.Button, tabSelectedColor, Mode is PanelMode.Details); - if (Im.Button("Collection Details"u8, buttonSize)) - Mode = PanelMode.Details; - color.Pop(); - _tutorial.OpenTutorial(BasicTutorialSteps.CollectionDetails); - Im.Line.Same(); - - _incognito.DrawToggle(withSpacing); - } - - private void DrawPanel() - { - using var style = ImStyleDouble.ItemSpacing.Push(Vector2.Zero); - using var child = Im.Child.Begin("##CollectionSettings"U8, Im.ContentRegion.Available with { Y = 0 }, true); - if (!child) - return; - - style.Pop(); - switch (Mode) - { - case PanelMode.SimpleAssignment: - _panel.DrawSimple(); - break; - case PanelMode.IndividualAssignment: - _panel.DrawIndividualPanel(); - break; - case PanelMode.GroupAssignment: - _panel.DrawGroupPanel(); - break; - case PanelMode.Details: - _panel.DrawDetailsPanel(); - break; - } - - style.Push(ImStyleDouble.ItemSpacing, Vector2.Zero); - } -} +using ImSharp; +using Luna; +using Penumbra.Api.Enums; +using Penumbra.UI.Classes; +using Penumbra.UI.CollectionTab; + +namespace Penumbra.UI.Tabs; + +public sealed class CollectionsTab : TwoPanelLayout, ITab +{ + private readonly TutorialService _tutorial; + + public TabType Identifier + => TabType.Collections; + + public CollectionsTab(TutorialService tutorial, CollectionButtonFooter leftFooter, CollectionSelector leftPanel, CollectionFilter filter, + CollectionModeHeader rightHeader, CollectionPanel rightPanel) + { + LeftHeader = new FilterHeader(filter, new StringU8("Filter..."u8)); + LeftPanel = leftPanel; + LeftFooter = leftFooter; + RightHeader = rightHeader; + RightPanel = rightPanel; + RightFooter = NopHeaderFooter.Instance; + _tutorial = tutorial; + } + + public override ReadOnlySpan Label + => "Collections"u8; + + protected override void DrawLeftGroup() + { + base.DrawLeftGroup(); + _tutorial.OpenTutorial(BasicTutorialSteps.EditingCollections); + } + + public void DrawContent() + => Draw(); + + public void PostTabButton() + => _tutorial.OpenTutorial(BasicTutorialSteps.Collections); +} diff --git a/Penumbra/UI/Tabs/Debug/ActionTmbListDrawer.cs b/Penumbra/UI/Tabs/Debug/ActionTmbListDrawer.cs new file mode 100644 index 00000000..b6ba1401 --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/ActionTmbListDrawer.cs @@ -0,0 +1,51 @@ +using ImSharp; +using Luna; +using Penumbra.Interop.Services; +using Penumbra.String; + +namespace Penumbra.UI.Tabs.Debug; + +public sealed class ActionTmbListDrawer(SchedulerResourceManagementService service) : IUiService +{ + public readonly SchedulerResourceManagementService Service = service; + public readonly IFilter KeyFilter = new TmbKeyFilter(); + + public sealed class Cache(ActionTmbListDrawer parent) : BasicFilterCache(parent.KeyFilter) + { + protected override IEnumerable GetItems() + => parent.Service.ActionTmbs.OrderBy(t => t.Value).Select(k => new TmbEntry(k.Key, k.Value)); + } + + public readonly struct TmbEntry(CiByteString key, uint value) + { + public readonly StringPair Key = new(key.ToString()); + public readonly StringU8 Value = new($"{value}"); + + public void Draw() + { + Im.Table.DrawColumn(Value); + Im.Table.DrawColumn(Key.Utf8); + } + } + + public sealed class TmbKeyFilter : RegexFilterBase + { + protected override string ToFilterString(in TmbEntry item, int globalIndex) + => item.Key.Utf16; + } + + public void Draw() + { + KeyFilter.DrawFilter("Key"u8, Im.ContentRegion.Available); + using var table = Im.Table.Begin("##table"u8, 2, + TableFlags.RowBackground | TableFlags.ScrollY | TableFlags.ScrollX | TableFlags.SizingFixedFit, + Im.ContentRegion.Available with { Y = 12 * Im.Style.TextHeightWithSpacing }); + if (!table) + return; + + var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current, () => new Cache(this)); + using var clip = new Im.ListClipper(cache.Count, Im.Style.TextHeightWithSpacing); + foreach (var tmb in clip.Iterate(cache)) + tmb.Draw(); + } +} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index b2535d39..cb4cb578 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -1,12 +1,10 @@ using Dalamud.Interface; using Dalamud.Plugin.Services; -using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Interface.Colors; using ImSharp; using Luna; @@ -15,7 +13,6 @@ using Penumbra.Api; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; -using Penumbra.GameData.DataContainers; using Penumbra.GameData.Files; using Penumbra.GameData.Interop; using Penumbra.Import.Structs; @@ -28,7 +25,6 @@ using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.String; using Penumbra.UI.Classes; -using ImGuiClip = OtterGui.ImGuiClip; using Penumbra.Api.IpcTester; using Penumbra.GameData.Data; using Penumbra.Interop.Hooks.PostProcessing; @@ -65,54 +61,54 @@ public class Diagnostics(ServiceManager provider) : IUiService public sealed class DebugTab : Window, ITab { - private readonly Configuration _config; - private readonly CollectionManager _collectionManager; - private readonly ModManager _modManager; - private readonly ValidityChecker _validityChecker; - private readonly HttpApi _httpApi; - private readonly ActorManager _actors; - private readonly StainService _stains; - private readonly GlobalVariablesDrawer _globalVariablesDrawer; - private readonly ResourceManagerService _resourceManager; - private readonly ResourceLoader _resourceLoader; - private readonly CollectionResolver _collectionResolver; - private readonly DrawObjectState _drawObjectState; - private readonly PathState _pathState; - private readonly SubfileHelper _subfileHelper; - private readonly IdentifiedCollectionCache _identifiedCollectionCache; - private readonly CutsceneService _cutsceneService; - private readonly ModImportManager _modImporter; - private readonly ImportPopup _importPopup; - private readonly FrameworkManager _framework; - private readonly TextureManager _textureManager; - private readonly ShaderReplacementFixer _shaderReplacementFixer; - private readonly RedrawService _redraws; - private readonly DictEmote _emotes; - private readonly Diagnostics _diagnostics; - private readonly ObjectManager _objects; - private readonly IDataManager _dataManager; - private readonly IpcTester _ipcTester; - private readonly CrashHandlerPanel _crashHandlerPanel; - private readonly TexHeaderDrawer _texHeaderDrawer; - private readonly HookOverrideDrawer _hookOverrides; - private readonly RsfService _rsfService; - private readonly SchedulerResourceManagementService _schedulerService; - private readonly ObjectIdentification _objectIdentification; - private readonly RenderTargetDrawer _renderTargetDrawer; - private readonly ModMigratorDebug _modMigratorDebug; - private readonly ShapeInspector _shapeInspector; - private readonly FileWatcher.FileWatcherDrawer _fileWatcherDrawer; - private readonly DragDropManager _dragDropManager; + private readonly Configuration _config; + private readonly CollectionManager _collectionManager; + private readonly ModManager _modManager; + private readonly ValidityChecker _validityChecker; + private readonly HttpApi _httpApi; + private readonly ActorManager _actors; + private readonly StainService _stains; + private readonly GlobalVariablesDrawer _globalVariablesDrawer; + private readonly ResourceManagerService _resourceManager; + private readonly ResourceLoader _resourceLoader; + private readonly CollectionResolver _collectionResolver; + private readonly DrawObjectState _drawObjectState; + private readonly PathState _pathState; + private readonly SubfileHelper _subfileHelper; + private readonly IdentifiedCollectionCache _identifiedCollectionCache; + private readonly CutsceneService _cutsceneService; + private readonly ModImportManager _modImporter; + private readonly ImportPopup _importPopup; + private readonly FrameworkManager _framework; + private readonly TextureManager _textureManager; + private readonly ShaderReplacementFixer _shaderReplacementFixer; + private readonly RedrawService _redraws; + private readonly EmoteListDrawer _emotes; + private readonly Diagnostics _diagnostics; + private readonly ObjectManager _objects; + private readonly IDataManager _dataManager; + private readonly IpcTester _ipcTester; + private readonly CrashHandlerPanel _crashHandlerPanel; + private readonly TexHeaderDrawer _texHeaderDrawer; + private readonly HookOverrideDrawer _hookOverrides; + private readonly RsfService _rsfService; + private readonly ActionTmbListDrawer _actionTmbs; + private readonly ObjectIdentification _objectIdentification; + private readonly RenderTargetDrawer _renderTargetDrawer; + private readonly ModMigratorDebug _modMigratorDebug; + private readonly ShapeInspector _shapeInspector; + private readonly FileWatcher.FileWatcherDrawer _fileWatcherDrawer; + private readonly DragDropManager _dragDropManager; public DebugTab(Configuration config, CollectionManager collectionManager, ObjectManager objects, IDataManager dataManager, ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, ResourceManagerService resourceManager, ResourceLoader resourceLoader, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework, - TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, + TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, EmoteListDrawer emotes, Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer, HookOverrideDrawer hookOverrides, RsfService rsfService, GlobalVariablesDrawer globalVariablesDrawer, - SchedulerResourceManagementService schedulerService, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer, + ActionTmbListDrawer actionTmbs, ObjectIdentification objectIdentification, RenderTargetDrawer renderTargetDrawer, ModMigratorDebug modMigratorDebug, ShapeInspector shapeInspector, FileWatcher.FileWatcherDrawer fileWatcherDrawer, DragDropManager dragDropManager) : base("Penumbra Debug Window", WindowFlags.NoCollapse) @@ -152,7 +148,7 @@ public sealed class DebugTab : Window, ITab _hookOverrides = hookOverrides; _rsfService = rsfService; _globalVariablesDrawer = globalVariablesDrawer; - _schedulerService = schedulerService; + _actionTmbs = actionTmbs; _objectIdentification = objectIdentification; _renderTargetDrawer = renderTargetDrawer; _modMigratorDebug = modMigratorDebug; @@ -205,7 +201,7 @@ public sealed class DebugTab : Window, ITab _globalVariablesDrawer.Draw(); DrawCloudApi(); DrawDebugTabIpc(); - if(Im.Tree.Header("Drag & Drop Manager"u8)) + if (Im.Tree.Header("Drag & Drop Manager"u8)) _dragDropManager.DrawDebugInfo(); } @@ -742,7 +738,7 @@ public sealed class DebugTab : Window, ITab { using var table = Im.Table.Begin("###TmbTable"u8, 2, TableFlags.SizingFixedFit); if (table) - foreach (var (id, name) in _schedulerService.ListedTmbs.OrderBy(kvp => kvp.Key)) + foreach (var (id, name) in _actionTmbs.Service.ListedTmbs.OrderBy(kvp => kvp.Key)) table.DrawDataPair($"{id:D6}", name.Span); } } @@ -814,11 +810,6 @@ public sealed class DebugTab : Window, ITab Im.Selectable(item.Key); } - - private string _emoteSearchFile = string.Empty; - private string _emoteSearchName = string.Empty; - - private AtchFile? _atchFile; private void DrawAtch() @@ -842,57 +833,19 @@ public sealed class DebugTab : Window, ITab AtchDrawer.Draw(_atchFile); } + private void DrawEmotes() { using var mainTree = Im.Tree.Node("Emotes"u8); - if (!mainTree) - return; - - Im.Input.Text("File Name"u8, ref _emoteSearchFile); - Im.Input.Text("Emote Name"u8, ref _emoteSearchName); - using var table = Im.Table.Begin("##table"u8, 2, TableFlags.RowBackground | TableFlags.ScrollY | TableFlags.SizingFixedFit, - new Vector2(-1, 12 * Im.Style.TextHeightWithSpacing)); - if (!table) - return; - - var skips = ImGuiClip.GetNecessarySkips(Im.Style.TextHeightWithSpacing); - var dummy = ImGuiClip.FilteredClippedDraw(_emotes, skips, - p => p.Key.Contains(_emoteSearchFile, StringComparison.OrdinalIgnoreCase) - && (_emoteSearchName.Length == 0 - || p.Value.Any(s => s.Name.ToDalamudString().TextValue.Contains(_emoteSearchName, StringComparison.OrdinalIgnoreCase))), - p => - { - Im.Table.DrawColumn(p.Key); - Im.Table.DrawColumn(StringU8.Join(", "u8, p.Value.Select(v => v.Name.ToDalamudString().TextValue))); - }); - ImGuiClip.DrawEndDummy(dummy, Im.Style.TextHeightWithSpacing); + if (mainTree) + _emotes.Draw(); } - private string _tmbKeyFilter = string.Empty; - private CiByteString _tmbKeyFilterU8 = CiByteString.Empty; - private void DrawActionTmbs() { using var mainTree = Im.Tree.Node("Action TMBs"u8); - if (!mainTree) - return; - - if (Im.Input.Text("Key"u8, ref _tmbKeyFilter)) - _tmbKeyFilterU8 = CiByteString.FromString(_tmbKeyFilter, out var r, MetaDataComputation.All) ? r : CiByteString.Empty; - using var table = Im.Table.Begin("##table"u8, 2, TableFlags.RowBackground | TableFlags.ScrollY | TableFlags.SizingFixedFit, - new Vector2(-1, 12 * Im.Style.TextHeightWithSpacing)); - if (!table) - return; - - var skips = ImGuiClip.GetNecessarySkips(Im.Style.TextHeightWithSpacing); - var dummy = ImGuiClip.FilteredClippedDraw(_schedulerService.ActionTmbs.OrderBy(r => r.Value), skips, - kvp => kvp.Key.Contains(_tmbKeyFilterU8), - p => - { - Im.Table.DrawColumn($"{p.Value}"); - Im.Table.DrawColumn(p.Key.Span); - }); - ImGuiClip.DrawEndDummy(dummy, Im.Style.TextHeightWithSpacing); + if (mainTree) + _actionTmbs.Draw(); } private void DrawStainTemplates() diff --git a/Penumbra/UI/Tabs/Debug/EmoteListDrawer.cs b/Penumbra/UI/Tabs/Debug/EmoteListDrawer.cs new file mode 100644 index 00000000..7682ca73 --- /dev/null +++ b/Penumbra/UI/Tabs/Debug/EmoteListDrawer.cs @@ -0,0 +1,79 @@ +using ImSharp; +using Lumina.Excel.Sheets; +using Luna; +using Penumbra.GameData.Data; +using Penumbra.GameData.DataContainers; + +namespace Penumbra.UI.Tabs.Debug; + +public sealed class EmoteListDrawer(DictEmote emotes) : IUiService +{ + public readonly DictEmote Emotes = emotes; + public readonly IFilter FileFilter = new EmoteFileFilter(); + public readonly IFilter NameFilter = new EmoteNameFilter(); + + public sealed class Cache(EmoteListDrawer parent) + : BasicFilterCache(new PairFilter(parent.FileFilter, parent.NameFilter)) + { + protected override IEnumerable GetItems() + => parent.Emotes.Value.Select(kvp => new EmoteEntry(kvp.Key, kvp.Value)); + } + + public sealed class EmoteFileFilter : RegexFilterBase + { + protected override string ToFilterString(in EmoteEntry item, int globalIndex) + => item.File.Utf16; + } + + public sealed class EmoteNameFilter : RegexFilterBase + { + public override bool WouldBeVisible(in EmoteEntry item, int globalIndex) + => Text.Length is 0 || item.Emotes.Any(e => WouldBeVisible(e.Utf16)); + + protected override string ToFilterString(in EmoteEntry item, int globalIndex) + => string.Empty; + } + + public readonly struct EmoteEntry + { + public readonly StringPair File; + public readonly List Emotes; + + public EmoteEntry(string key, IReadOnlyList emotes) + { + File = new StringPair(key); + Emotes = emotes.Select(e => new StringPair(e.Name.ExtractTextExtended())).ToList(); + } + + public void Draw() + { + Im.Table.DrawColumn(File.Utf8); + if (Emotes.Count > 0) + Im.Table.DrawColumn(Emotes[0].Utf8); + + foreach (var emote in Emotes.Skip(1)) + { + Im.Line.NoSpacing(); + Im.Text(", "u8); + Im.Line.NoSpacing(); + Im.Text(emote.Utf16); + } + } + } + + public void Draw() + { + FileFilter.DrawFilter("File Name"u8, Im.ContentRegion.Available); + NameFilter.DrawFilter("Emote Name"u8, Im.ContentRegion.Available); + using var table = Im.Table.Begin("##table"u8, 2, + TableFlags.RowBackground | TableFlags.ScrollY | TableFlags.ScrollX | TableFlags.SizingFixedFit, + Im.ContentRegion.Available with { Y = 12 * Im.Style.TextHeightWithSpacing }); + if (!table) + return; + + var cache = CacheManager.Instance.GetOrCreateCache(Im.Id.Current, () => new Cache(this)); + using var clip = new Im.ListClipper(cache.Count, Im.Style.TextHeightWithSpacing); + foreach (var emote in clip.Iterate(cache)) + emote.Draw(); + } +}