diff --git a/OtterGui b/OtterGui index 3bf047bf..c347d29d 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 3bf047bfa293817a691b7f06032bae7aeb2e4dc7 +Subproject commit c347d29d980b0191d1d071170cf2ec229e3efdcf diff --git a/Penumbra.GameData b/Penumbra.GameData index bc339208..955c4e6b 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit bc339208d1d453582eb146533c572823146a4592 +Subproject commit 955c4e6b281bf0781689b15c01a868b0de5881b4 diff --git a/Penumbra/Api/Api/UiApi.cs b/Penumbra/Api/Api/UiApi.cs index 515874c0..b14f67ae 100644 --- a/Penumbra/Api/Api/UiApi.cs +++ b/Penumbra/Api/Api/UiApi.cs @@ -81,21 +81,21 @@ public class UiApi : IPenumbraApiUi, IApiService, IDisposable public void CloseMainWindow() => _configWindow.IsOpen = false; - private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData? data) + private void OnChangedItemClick(MouseButton button, IIdentifiedObjectData data) { if (ChangedItemClicked == null) return; - var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0); + var (type, id) = data.ToApiObject(); ChangedItemClicked.Invoke(button, type, id); } - private void OnChangedItemHover(IIdentifiedObjectData? data) + private void OnChangedItemHover(IIdentifiedObjectData data) { if (ChangedItemTooltip == null) return; - var (type, id) = data?.ToApiObject() ?? (ChangedItemType.None, 0); + var (type, id) = data.ToApiObject(); ChangedItemTooltip.Invoke(type, id); } } diff --git a/Penumbra/Api/ModChangedItemAdapter.cs b/Penumbra/Api/ModChangedItemAdapter.cs index 8842f20a..8d2d473c 100644 --- a/Penumbra/Api/ModChangedItemAdapter.cs +++ b/Penumbra/Api/ModChangedItemAdapter.cs @@ -65,7 +65,7 @@ public sealed class ModChangedItemAdapter(WeakReference storage) : throw new ObjectDisposedException("The underlying mod storage of this IPC container was disposed."); } - private sealed class ChangedItemDictionaryAdapter(SortedList data) : IReadOnlyDictionary + private sealed class ChangedItemDictionaryAdapter(SortedList data) : IReadOnlyDictionary { public IEnumerator> GetEnumerator() => data.Select(d => new KeyValuePair(d.Key, d.Value?.ToInternalObject())).GetEnumerator(); diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index a80928d0..42c8b27d 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -23,7 +23,7 @@ public sealed class CollectionCache : IDisposable private readonly CollectionCacheManager _manager; private readonly ModCollection _collection; public readonly CollectionModData ModData = new(); - private readonly SortedList, IIdentifiedObjectData?)> _changedItems = []; + private readonly SortedList, IIdentifiedObjectData)> _changedItems = []; public readonly ConcurrentDictionary ResolvedFiles = new(); public readonly CustomResourceCache CustomResources; public readonly MetaCache Meta; @@ -43,7 +43,7 @@ public sealed class CollectionCache : IDisposable private int _changedItemsSaveCounter = -1; // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary, IIdentifiedObjectData?)> ChangedItems + public IReadOnlyDictionary, IIdentifiedObjectData)> ChangedItems { get { @@ -441,7 +441,7 @@ public sealed class CollectionCache : IDisposable // Skip IMCs because they would result in far too many false-positive items, // since they are per set instead of per item-slot/item/variant. var identifier = _manager.MetaFileManager.Identifier; - var items = new SortedList(512); + var items = new SortedList(512); void AddItems(IMod mod) { diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 0b38dde8..716b153e 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -46,8 +46,8 @@ public partial class ModCollection internal IReadOnlyDictionary ResolvedFiles => _cache?.ResolvedFiles ?? new ConcurrentDictionary(); - internal IReadOnlyDictionary, IIdentifiedObjectData?)> ChangedItems - => _cache?.ChangedItems ?? new Dictionary, IIdentifiedObjectData?)>(); + internal IReadOnlyDictionary, IIdentifiedObjectData)> ChangedItems + => _cache?.ChangedItems ?? new Dictionary, IIdentifiedObjectData)>(); internal IEnumerable> AllConflicts => _cache?.AllConflicts ?? Array.Empty>(); diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs index 1aac4454..2d27f36a 100644 --- a/Penumbra/Communication/ChangedItemClick.cs +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -12,7 +12,7 @@ namespace Penumbra.Communication; /// Parameter is the clicked object data if any. /// /// -public sealed class ChangedItemClick() : EventWrapper(nameof(ChangedItemClick)) +public sealed class ChangedItemClick() : EventWrapper(nameof(ChangedItemClick)) { public enum Priority { diff --git a/Penumbra/Communication/ChangedItemHover.cs b/Penumbra/Communication/ChangedItemHover.cs index 4e72b558..92d770f7 100644 --- a/Penumbra/Communication/ChangedItemHover.cs +++ b/Penumbra/Communication/ChangedItemHover.cs @@ -10,7 +10,7 @@ namespace Penumbra.Communication; /// Parameter is the hovered object data if any. /// /// -public sealed class ChangedItemHover() : EventWrapper(nameof(ChangedItemHover)) +public sealed class ChangedItemHover() : EventWrapper(nameof(ChangedItemHover)) { public enum Priority { diff --git a/Penumbra/Meta/Manipulations/AtchIdentifier.cs b/Penumbra/Meta/Manipulations/AtchIdentifier.cs index bce37620..c248c48b 100644 --- a/Penumbra/Meta/Manipulations/AtchIdentifier.cs +++ b/Penumbra/Meta/Manipulations/AtchIdentifier.cs @@ -31,7 +31,7 @@ public readonly record struct AtchIdentifier(AtchType Type, GenderRace GenderRac public override string ToString() => $"Atch - {Type.ToAbbreviation()} - {GenderRace.ToName()} - {EntryIndex}"; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { // Nothing specific } diff --git a/Penumbra/Meta/Manipulations/Eqdp.cs b/Penumbra/Meta/Manipulations/Eqdp.cs index 285f2309..c8423b92 100644 --- a/Penumbra/Meta/Manipulations/Eqdp.cs +++ b/Penumbra/Meta/Manipulations/Eqdp.cs @@ -15,7 +15,7 @@ public readonly record struct EqdpIdentifier(PrimaryId SetId, EquipSlot Slot, Ge public Gender Gender => GenderRace.Split().Item1; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, Slot)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/Eqp.cs b/Penumbra/Meta/Manipulations/Eqp.cs index c71f2f4d..154aca40 100644 --- a/Penumbra/Meta/Manipulations/Eqp.cs +++ b/Penumbra/Meta/Manipulations/Eqp.cs @@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : IMetaIdentifier, IComparable { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace.MidlanderMale, Slot)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/Est.cs b/Penumbra/Meta/Manipulations/Est.cs index 007cd02f..8a450eee 100644 --- a/Penumbra/Meta/Manipulations/Est.cs +++ b/Penumbra/Meta/Manipulations/Est.cs @@ -24,17 +24,17 @@ public readonly record struct EstIdentifier(PrimaryId SetId, EstType Slot, Gende public Gender Gender => GenderRace.Split().Item1; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { switch (Slot) { case EstType.Hair: - changedItems.TryAdd( - $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Hair {SetId}", null); + changedItems.UpdateCountOrSet( + $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Hair {SetId}", () => new IdentifiedName()); break; case EstType.Face: - changedItems.TryAdd( - $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Face {SetId}", null); + changedItems.UpdateCountOrSet( + $"Customization: {GenderRace.Split().Item2.ToName()} {GenderRace.Split().Item1.ToName()} Face {SetId}", () => new IdentifiedName()); break; case EstType.Body: identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, EquipSlot.Body)); diff --git a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs index 6a1ceaea..1365d9d3 100644 --- a/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs +++ b/Penumbra/Meta/Manipulations/GlobalEqpManipulation.cs @@ -70,7 +70,7 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier public override string ToString() => $"Global EQP - {Type}{(Condition != 0 ? $" - {Condition.Id}" : string.Empty)}"; - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { var path = Type switch { @@ -86,9 +86,9 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier if (path.Length > 0) identifier.Identify(changedItems, path); else if (Type is GlobalEqpType.DoNotHideVieraHats) - changedItems["All Hats for Viera"] = null; + changedItems.UpdateCountOrSet("All Hats for Viera", () => new IdentifiedName()); else if (Type is GlobalEqpType.DoNotHideHrothgarHats) - changedItems["All Hats for Hrothgar"] = null; + changedItems.UpdateCountOrSet("All Hats for Hrothgar", () => new IdentifiedName()); } public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/Gmp.cs b/Penumbra/Meta/Manipulations/Gmp.cs index 8cd07bfd..5bc81f26 100644 --- a/Penumbra/Meta/Manipulations/Gmp.cs +++ b/Penumbra/Meta/Manipulations/Gmp.cs @@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, IComparable { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace.MidlanderMale, EquipSlot.Head)); public MetaIndex FileIndex() diff --git a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs index 999fd906..c897bb2a 100644 --- a/Penumbra/Meta/Manipulations/IMetaIdentifier.cs +++ b/Penumbra/Meta/Manipulations/IMetaIdentifier.cs @@ -19,7 +19,7 @@ public enum MetaManipulationType : byte public interface IMetaIdentifier { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); public MetaIndex FileIndex(); diff --git a/Penumbra/Meta/Manipulations/Imc.cs b/Penumbra/Meta/Manipulations/Imc.cs index 6e893043..fa726708 100644 --- a/Penumbra/Meta/Manipulations/Imc.cs +++ b/Penumbra/Meta/Manipulations/Imc.cs @@ -27,10 +27,10 @@ public readonly record struct ImcIdentifier( : this(primaryId, variant, slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, 0, slot, BodySlot.Unknown) { } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => AddChangedItems(identifier, changedItems, false); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems, bool allVariants) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems, bool allVariants) { var path = ObjectType switch { diff --git a/Penumbra/Meta/Manipulations/Rsp.cs b/Penumbra/Meta/Manipulations/Rsp.cs index 9dc4fe90..5f91a37c 100644 --- a/Penumbra/Meta/Manipulations/Rsp.cs +++ b/Penumbra/Meta/Manipulations/Rsp.cs @@ -8,8 +8,8 @@ namespace Penumbra.Meta.Manipulations; public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attribute) : IMetaIdentifier { - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) - => changedItems.TryAdd($"{SubRace.ToName()} {Attribute.ToFullString()}", null); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + => changedItems.UpdateCountOrSet($"{SubRace.ToName()} {Attribute.ToFullString()}", () => new IdentifiedName()); public MetaIndex FileIndex() => MetaIndex.HumanCmp; diff --git a/Penumbra/Mods/Groups/CombiningModGroup.cs b/Penumbra/Mods/Groups/CombiningModGroup.cs index 80f3c4c0..90a962b7 100644 --- a/Penumbra/Mods/Groups/CombiningModGroup.cs +++ b/Penumbra/Mods/Groups/CombiningModGroup.cs @@ -136,7 +136,7 @@ public sealed class CombiningModGroup : IModGroup public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations) => Data[setting.AsIndex].AddDataTo(redirections, manipulations); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { foreach (var container in DataContainers) identifier.AddChangedItems(container, changedItems); diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index 96422caf..cc961b0f 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -53,7 +53,7 @@ public interface IModGroup public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer); public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations); - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems); /// Ensure that a value is valid for a group. public Setting FixSetting(Setting setting); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 2a1854ed..5ec32274 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -131,7 +131,7 @@ public class ImcModGroup(Mod mod) : IModGroup } } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) => Identifier.AddChangedItems(identifier, changedItems, AllVariants); public Setting FixSetting(Setting setting) diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 0c9aa805..82555314 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -122,7 +122,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup } } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { foreach (var container in DataContainers) identifier.AddChangedItems(container, changedItems); diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index ab0c2d4f..c250182a 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -107,7 +107,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); } - public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) + public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems) { foreach (var container in DataContainers) identifier.AddChangedItems(container, changedItems); diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 4bf22272..130c8fcb 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -139,6 +139,7 @@ public class ModCacheManager : IDisposable, IService mod.ChangedItems.RemoveMachinistOffhands(); mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); + ++mod.LastChangedItemsUpdate; } private static void UpdateCounts(Mod mod) diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 488e3dc1..9829d5a0 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -101,13 +101,14 @@ public sealed class Mod : IMod } // Cache - public readonly SortedList ChangedItems = new(); + public readonly SortedList ChangedItems = new(); public string LowerChangedItemsString { get; internal set; } = string.Empty; public string AllTagsLower { get; internal set; } = string.Empty; - public int TotalFileCount { get; internal set; } - public int TotalSwapCount { get; internal set; } - public int TotalManipulations { get; internal set; } + public int TotalFileCount { get; internal set; } + public int TotalSwapCount { get; internal set; } + public int TotalManipulations { get; internal set; } + public ushort LastChangedItemsUpdate { get; internal set; } public bool HasOptions { get; internal set; } } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 3482f620..eb9aa93d 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -25,7 +25,6 @@ public class ResourceTreeViewer( private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; - private readonly CommunicatorService _communicator = communicator; private readonly HashSet _unfolded = []; private readonly Dictionary _filterCache = []; @@ -278,7 +277,7 @@ public class ResourceTreeViewer( if (ImGui.IsItemClicked()) ImGui.SetClipboardText(resourceNode.FullPath.ToPath()); if (hasMod && ImGui.IsItemClicked(ImGuiMouseButton.Right) && ImGui.GetIO().KeyCtrl) - _communicator.SelectTab.Invoke(TabType.Mods, mod); + communicator.SelectTab.Invoke(TabType.Mods, mod); ImGuiUtil.HoverTooltip( $"{resourceNode.FullPath.ToPath()}\n\nClick to copy to clipboard.{(hasMod ? "\nControl + Right-Click to jump to mod." : string.Empty)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); diff --git a/Penumbra/UI/ChangedItemDrawer.cs b/Penumbra/UI/ChangedItemDrawer.cs index af9782d5..a9070360 100644 --- a/Penumbra/UI/ChangedItemDrawer.cs +++ b/Penumbra/UI/ChangedItemDrawer.cs @@ -9,6 +9,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.GameData.Data; using Penumbra.Services; @@ -86,18 +87,20 @@ public class ChangedItemDrawer : IDisposable, IUiService } /// Check if a changed item should be drawn based on its category. - public bool FilterChangedItem(string name, IIdentifiedObjectData? data, LowerString filter) + public bool FilterChangedItem(string name, IIdentifiedObjectData data, LowerString filter) => (_config.Ephemeral.ChangedItemFilter == ChangedItemFlagExtensions.AllFlags || _config.Ephemeral.ChangedItemFilter.HasFlag(data.GetIcon().ToFlag())) && (filter.IsEmpty || !data.IsFilteredOut(name, filter)); /// Draw the icon corresponding to the category of a changed item. - public void DrawCategoryIcon(IIdentifiedObjectData? data) - => DrawCategoryIcon(data.GetIcon().ToFlag()); + public void DrawCategoryIcon(IIdentifiedObjectData data, float height) + => DrawCategoryIcon(data.GetIcon().ToFlag(), height); public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType) + => DrawCategoryIcon(iconFlagType, ImGui.GetFrameHeight()); + + public void DrawCategoryIcon(ChangedItemIconFlag iconFlagType, float height) { - var height = ImGui.GetFrameHeight(); if (!_icons.TryGetValue(iconFlagType, out var icon)) { ImGui.Dummy(new Vector2(height)); @@ -114,50 +117,50 @@ public class ChangedItemDrawer : IDisposable, IUiService } } - /// - /// Draw a changed item, invoking the Api-Events for clicks and tooltips. - /// Also draw the item ID in grey if requested. - /// - public void DrawChangedItem(string name, IIdentifiedObjectData? data) + public void ChangedItemHandling(IIdentifiedObjectData data, bool leftClicked) { - name = data?.ToName(name) ?? name; - using (ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)) - .Push(ImGuiStyleVar.ItemSpacing, new Vector2(ImGui.GetStyle().ItemSpacing.X, ImGui.GetStyle().CellPadding.Y * 2))) - { - var ret = ImGui.Selectable(name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) - ? MouseButton.Left - : MouseButton.None; - ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; - ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; - if (ret != MouseButton.None) - _communicator.ChangedItemClick.Invoke(ret, data); - } + var ret = leftClicked ? MouseButton.Left : MouseButton.None; + ret = ImGui.IsItemClicked(ImGuiMouseButton.Right) ? MouseButton.Right : ret; + ret = ImGui.IsItemClicked(ImGuiMouseButton.Middle) ? MouseButton.Middle : ret; + if (ret != MouseButton.None) + _communicator.ChangedItemClick.Invoke(ret, data); + if (!ImGui.IsItemHovered()) + return; - if (_communicator.ChangedItemHover.HasTooltip && ImGui.IsItemHovered()) - { - // We can not be sure that any subscriber actually prints something in any case. - // Circumvent ugly blank tooltip with less-ugly useless tooltip. - using var tt = ImRaii.Tooltip(); - using (ImRaii.Group()) - { - _communicator.ChangedItemHover.Invoke(data); - } - - if (ImGui.GetItemRectSize() == Vector2.Zero) - ImGui.TextUnformatted("No actions available."); - } + using var tt = ImUtf8.Tooltip(); + if (data.Count == 1) + ImUtf8.Text("This item is changed through a single effective change.\n"); + else + ImUtf8.Text($"This item is changed through {data.Count} distinct effective changes.\n"); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3 * ImUtf8.GlobalScale); + ImGui.Separator(); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3 * ImUtf8.GlobalScale); + _communicator.ChangedItemHover.Invoke(data); } /// Draw the model information, right-justified. - public static void DrawModelData(IIdentifiedObjectData? data) + public static void DrawModelData(IIdentifiedObjectData data, float height) { - var additionalData = data?.AdditionalData ?? string.Empty; + var additionalData = data.AdditionalData; if (additionalData.Length == 0) return; - ImGui.SameLine(ImGui.GetContentRegionAvail().X); - ImGui.AlignTextToFramePadding(); - ImGuiUtil.RightJustify(additionalData, ColorId.ItemId.Value()); + ImGui.SameLine(); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (height - ImGui.GetTextLineHeight()) / 2); + ImUtf8.TextRightAligned(additionalData, ImGui.GetStyle().ItemInnerSpacing.X); + } + + /// Draw the model information, right-justified. + public static void DrawModelData(ReadOnlySpan text, float height) + { + if (text.Length == 0) + return; + + ImGui.SameLine(); + using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (height - ImGui.GetTextLineHeight()) / 2); + ImUtf8.TextRightAligned(text, ImGui.GetStyle().ItemInnerSpacing.X); } /// Draw a header line with the different icon types to filter them. @@ -276,7 +279,7 @@ public class ChangedItemDrawer : IDisposable, IUiService return true; } - private static unsafe IDalamudTextureWrap? LoadUnknownTexture(IDataManager gameData, ITextureProvider textureProvider) + private static IDalamudTextureWrap? LoadUnknownTexture(IDataManager gameData, ITextureProvider textureProvider) { var unk = gameData.GetFile("ui/uld/levelup2_hr1.tex"); if (unk == null) diff --git a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs index a7bdadd3..ac4fd167 100644 --- a/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs @@ -1,47 +1,268 @@ +using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; using ImGuiNET; using OtterGui; using OtterGui.Classes; -using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Mods; +using Penumbra.String; namespace Penumbra.UI.ModsTab; -public class ModPanelChangedItemsTab(ModFileSystemSelector selector, ChangedItemDrawer drawer) : ITab, IUiService +public class ModPanelChangedItemsTab( + ModFileSystemSelector selector, + ChangedItemDrawer drawer, + ImGuiCacheService cacheService, + EphemeralConfig config) + : ITab, IUiService { + private readonly ImGuiCacheService.CacheId _cacheId = cacheService.GetNewId(); + + private class ChangedItemsCache + { + private Mod? _lastSelected; + private ushort _lastUpdate; + private ChangedItemIconFlag _filter = ChangedItemFlagExtensions.DefaultFlags; + private bool _reset; + public readonly List Data = []; + public bool AnyExpandable { get; private set; } + + public record struct Container + { + public IIdentifiedObjectData Data; + public ByteString Text; + public ByteString ModelData; + public uint Id; + public int Children; + public ChangedItemIconFlag Icon; + public bool Expandable; + public bool Expanded; + public bool Child; + + public static Container Single(string text, IIdentifiedObjectData data) + => new() + { + Child = false, + Text = ByteString.FromStringUnsafe(data.ToName(text), false), + ModelData = ByteString.FromStringUnsafe(data.AdditionalData, false), + Icon = data.GetIcon().ToFlag(), + Expandable = false, + Expanded = false, + Data = data, + Id = 0, + Children = 0, + }; + + public static Container Parent(string text, IIdentifiedObjectData data, uint id, int children, bool expanded) + => new() + { + Child = false, + Text = ByteString.FromStringUnsafe(data.ToName(text), false), + ModelData = ByteString.FromStringUnsafe(data.AdditionalData, false), + Icon = data.GetIcon().ToFlag(), + Expandable = true, + Expanded = expanded, + Data = data, + Id = id, + Children = children, + }; + + public static Container Indent(string text, IIdentifiedObjectData data) + => new() + { + Child = true, + Text = ByteString.FromStringUnsafe(data.ToName(text), false), + ModelData = ByteString.FromStringUnsafe(data.AdditionalData, false), + Icon = data.GetIcon().ToFlag(), + Expandable = false, + Expanded = false, + Data = data, + Id = 0, + Children = 0, + }; + } + + public void Reset() + => _reset = true; + + public void Update(Mod? mod, ChangedItemDrawer drawer, ChangedItemIconFlag filter) + { + if (mod == _lastSelected && _lastSelected!.LastChangedItemsUpdate == _lastUpdate && _filter == filter && !_reset) + return; + + _reset = false; + Data.Clear(); + AnyExpandable = false; + _lastSelected = mod; + _filter = filter; + if (_lastSelected == null) + return; + + _lastUpdate = _lastSelected.LastChangedItemsUpdate; + var tmp = new Dictionary<(PrimaryId, FullEquipType), List>(); + + foreach (var (s, i) in _lastSelected.ChangedItems) + { + if (i is not IdentifiedItem item) + continue; + + if (!drawer.FilterChangedItem(s, item, LowerString.Empty)) + continue; + + if (tmp.TryGetValue((item.Item.PrimaryId, item.Item.Type), out var p)) + p.Add(item); + else + tmp[(item.Item.PrimaryId, item.Item.Type)] = [item]; + } + + foreach (var list in tmp.Values) + { + list.Sort((i1, i2) => + { + // reversed + var count = i2.Count.CompareTo(i1.Count); + if (count != 0) + return count; + + return string.Compare(i1.Item.Name, i2.Item.Name, StringComparison.Ordinal); + }); + } + + var sortedTmp = tmp.Values.OrderBy(s => s[0].Item.Name).ToArray(); + + var sortedTmpIdx = 0; + foreach (var (s, i) in _lastSelected.ChangedItems) + { + if (i is IdentifiedItem) + continue; + + if (!drawer.FilterChangedItem(s, i, LowerString.Empty)) + continue; + + while (sortedTmpIdx < sortedTmp.Length + && string.Compare(sortedTmp[sortedTmpIdx][0].Item.Name, s, StringComparison.Ordinal) <= 0) + AddList(sortedTmp[sortedTmpIdx++]); + + Data.Add(Container.Single(s, i)); + } + + for (; sortedTmpIdx < sortedTmp.Length; ++sortedTmpIdx) + AddList(sortedTmp[sortedTmpIdx]); + return; + + void AddList(List list) + { + var mainItem = list[0]; + if (list.Count == 1) + { + Data.Add(Container.Single(mainItem.Item.Name, mainItem)); + } + else + { + var id = ImUtf8.GetId($"{mainItem.Item.PrimaryId}{(int)mainItem.Item.Type}"); + var expanded = ImGui.GetStateStorage().GetBool(id, false); + Data.Add(Container.Parent(mainItem.Item.Name, mainItem, id, list.Count - 1, expanded)); + AnyExpandable = true; + if (!expanded) + return; + + foreach (var item in list.Skip(1)) + Data.Add(Container.Indent(item.Item.Name, item)); + } + } + } + } + public ReadOnlySpan Label => "Changed Items"u8; public bool IsVisible => selector.Selected!.ChangedItems.Count > 0; + private ImGuiStoragePtr _stateStorage; + + private Vector2 _buttonSize; + public void DrawContent() { + if (cacheService.Cache(_cacheId, () => (new ChangedItemsCache(), "ModPanelChangedItemsCache")) is not { } cache) + return; + drawer.DrawTypeFilter(); + + _stateStorage = ImGui.GetStateStorage(); + cache.Update(selector.Selected, drawer, config.ChangedItemFilter); ImGui.Separator(); - using var table = ImRaii.Table("##changedItems", 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, + _buttonSize = new Vector2(ImGui.GetStyle().ItemSpacing.Y + ImGui.GetFrameHeight()); + using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, Vector2.Zero) + .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FramePadding, Vector2.Zero) + .Push(ImGuiStyleVar.SelectableTextAlign, new Vector2(0.01f, 0.5f)); + + using var table = ImUtf8.Table("##changedItems"u8, cache.AnyExpandable ? 2 : 1, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, new Vector2(ImGui.GetContentRegionAvail().X, -1)); if (!table) return; - var zipList = ZipList.FromSortedList(selector.Selected!.ChangedItems); - var height = ImGui.GetFrameHeightWithSpacing(); - ImGui.TableNextColumn(); - var skips = ImGuiClip.GetNecessarySkips(height); - var remainder = ImGuiClip.FilteredClippedDraw(zipList, skips, CheckFilter, DrawChangedItem); - ImGuiClip.DrawEndDummy(remainder, height); + if (cache.AnyExpandable) + { + ImUtf8.TableSetupColumn("##exp"u8, ImGuiTableColumnFlags.WidthFixed, _buttonSize.Y); + ImUtf8.TableSetupColumn("##text"u8, ImGuiTableColumnFlags.WidthStretch); + ImGuiClip.ClippedDraw(cache.Data, DrawContainerExpandable, _buttonSize.Y); + } + else + { + ImGuiClip.ClippedDraw(cache.Data, DrawContainer, ImGui.GetFrameHeightWithSpacing()); + } } - private bool CheckFilter((string Name, IIdentifiedObjectData? Data) kvp) - => drawer.FilterChangedItem(kvp.Name, kvp.Data, LowerString.Empty); + private void DrawContainerExpandable(ChangedItemsCache.Container obj, int idx) + { + using var id = ImUtf8.PushId(idx); + ImGui.TableNextColumn(); + if (obj.Expandable) + { + using var color = ImRaii.PushColor(ImGuiCol.Button, 0); + if (ImUtf8.IconButton(obj.Expanded ? FontAwesomeIcon.CaretDown : FontAwesomeIcon.CaretRight, + obj.Expanded ? "Hide the other items using the same model." : + obj.Children > 1 ? $"Show {obj.Children} other items using the same model." : + "Show one other item using the same model.", + _buttonSize)) + { + _stateStorage.SetBool(obj.Id, !obj.Expanded); + if (cacheService.TryGetCache(_cacheId, out var cache)) + cache.Reset(); + } + } + else + { + ImGui.Dummy(_buttonSize); + } - private void DrawChangedItem((string Name, IIdentifiedObjectData? Data) kvp) + DrawBaseContainer(obj, idx); + } + + private void DrawContainer(ChangedItemsCache.Container obj, int idx) + { + using var id = ImUtf8.PushId(idx); + DrawBaseContainer(obj, idx); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DrawBaseContainer(in ChangedItemsCache.Container obj, int idx) { ImGui.TableNextColumn(); - drawer.DrawCategoryIcon(kvp.Data); - ImGui.SameLine(); - drawer.DrawChangedItem(kvp.Name, kvp.Data); - ChangedItemDrawer.DrawModelData(kvp.Data); + using var indent = ImRaii.PushIndent(1, obj.Child); + drawer.DrawCategoryIcon(obj.Icon, _buttonSize.Y); + ImGui.SameLine(0, 0); + var clicked = ImUtf8.Selectable(obj.Text.Span, false, ImGuiSelectableFlags.None, _buttonSize with { X = 0 }); + drawer.ChangedItemHandling(obj.Data, clicked); + ChangedItemDrawer.DrawModelData(obj.ModelData.Span, _buttonSize.Y); } } diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 5bac7d35..6cee22d6 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -3,6 +3,7 @@ using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections.Manager; @@ -26,30 +27,36 @@ public class ChangedItemsTab( private LowerString _changedItemFilter = LowerString.Empty; private LowerString _changedItemModFilter = LowerString.Empty; + private Vector2 _buttonSize; public void DrawContent() { collectionHeader.Draw(true); drawer.DrawTypeFilter(); var varWidth = DrawFilters(); - using var child = ImRaii.Child("##changedItemsChild", -Vector2.One); + using var child = ImUtf8.Child("##changedItemsChild"u8, -Vector2.One); if (!child) return; - var height = ImGui.GetFrameHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; - var skips = ImGuiClip.GetNecessarySkips(height); - using var list = ImRaii.Table("##changedItems", 3, ImGuiTableFlags.RowBg, -Vector2.One); + _buttonSize = new Vector2(ImGui.GetStyle().ItemSpacing.Y + ImGui.GetFrameHeight()); + using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, Vector2.Zero) + .Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero) + .Push(ImGuiStyleVar.FramePadding, Vector2.Zero) + .Push(ImGuiStyleVar.SelectableTextAlign, new Vector2(0.01f, 0.5f)); + + var skips = ImGuiClip.GetNecessarySkips(_buttonSize.Y); + using var list = ImUtf8.Table("##changedItems"u8, 3, ImGuiTableFlags.RowBg, -Vector2.One); if (!list) return; const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed; - ImGui.TableSetupColumn("items", flags, 450 * UiHelpers.Scale); - ImGui.TableSetupColumn("mods", flags, varWidth - 130 * UiHelpers.Scale); - ImGui.TableSetupColumn("id", flags, 130 * UiHelpers.Scale); + ImUtf8.TableSetupColumn("items"u8, flags, 450 * UiHelpers.Scale); + ImUtf8.TableSetupColumn("mods"u8, flags, varWidth - 140 * UiHelpers.Scale); + ImUtf8.TableSetupColumn("id"u8, flags, 140 * UiHelpers.Scale); var items = collectionManager.Active.Current.ChangedItems; var rest = ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn); - ImGuiClip.DrawEndDummy(rest, height); + ImGuiClip.DrawEndDummy(rest, _buttonSize.Y); } /// Draw a pair of filters and return the variable width of the flexible column. @@ -67,22 +74,25 @@ public class ChangedItemsTab( } /// Apply the current filters. - private bool FilterChangedItem(KeyValuePair, IIdentifiedObjectData?)> item) + private bool FilterChangedItem(KeyValuePair, IIdentifiedObjectData)> item) => drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter) && (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter))); /// Draw a full column for a changed item. - private void DrawChangedItemColumn(KeyValuePair, IIdentifiedObjectData?)> item) + private void DrawChangedItemColumn(KeyValuePair, IIdentifiedObjectData)> item) { ImGui.TableNextColumn(); - drawer.DrawCategoryIcon(item.Value.Item2); - ImGui.SameLine(); - drawer.DrawChangedItem(item.Key, item.Value.Item2); + drawer.DrawCategoryIcon(item.Value.Item2, _buttonSize.Y); + ImGui.SameLine(0, 0); + var name = item.Value.Item2.ToName(item.Key); + var clicked = ImUtf8.Selectable(name, false, ImGuiSelectableFlags.None, _buttonSize with { X = 0 }); + drawer.ChangedItemHandling(item.Value.Item2, clicked); + ImGui.TableNextColumn(); DrawModColumn(item.Value.Item1); ImGui.TableNextColumn(); - ChangedItemDrawer.DrawModelData(item.Value.Item2); + ChangedItemDrawer.DrawModelData(item.Value.Item2, _buttonSize.Y); } private void DrawModColumn(SingleArray mods) @@ -90,19 +100,18 @@ public class ChangedItemsTab( if (mods.Count <= 0) return; - var first = mods[0]; - using var style = ImRaii.PushStyle(ImGuiStyleVar.SelectableTextAlign, new Vector2(0, 0.5f)); - if (ImGui.Selectable(first.Name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight())) + var first = mods[0]; + if (ImUtf8.Selectable(first.Name.Text, false, ImGuiSelectableFlags.None, _buttonSize with { X = 0 }) && ImGui.GetIO().KeyCtrl && first is Mod mod) communicator.SelectTab.Invoke(TabType.Mods, mod); - if (ImGui.IsItemHovered()) - { - using var _ = ImRaii.Tooltip(); - ImGui.TextUnformatted("Hold Control and click to jump to mod.\n"); - if (mods.Count > 1) - ImGui.TextUnformatted("Other mods affecting this item:\n" + string.Join("\n", mods.Skip(1).Select(m => m.Name))); - } + if (!ImGui.IsItemHovered()) + return; + + using var _ = ImRaii.Tooltip(); + ImUtf8.Text("Hold Control and click to jump to mod.\n"u8); + if (mods.Count > 1) + ImUtf8.Text("Other mods affecting this item:\n" + string.Join("\n", mods.Skip(1).Select(m => m.Name))); } } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 8f76a54a..42502290 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -748,7 +748,7 @@ public class DebugTab : Window, ITab, IUiService } private string _changedItemPath = string.Empty; - private readonly Dictionary _changedItems = []; + private readonly Dictionary _changedItems = []; private void DrawChangedItemTest() { diff --git a/Penumbra/Util/IdentifierExtensions.cs b/Penumbra/Util/IdentifierExtensions.cs index 5bd3f77c..f744e940 100644 --- a/Penumbra/Util/IdentifierExtensions.cs +++ b/Penumbra/Util/IdentifierExtensions.cs @@ -10,7 +10,7 @@ namespace Penumbra.Util; public static class IdentifierExtensions { public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container, - IDictionary changedItems) + IDictionary changedItems) { foreach (var gamePath in container.Files.Keys.Concat(container.FileSwaps.Keys)) identifier.Identify(changedItems, gamePath.ToString()); @@ -19,7 +19,7 @@ public static class IdentifierExtensions manip.AddChangedItems(identifier, changedItems); } - public static void RemoveMachinistOffhands(this SortedList changedItems) + public static void RemoveMachinistOffhands(this SortedList changedItems) { for (var i = 0; i < changedItems.Count; i++) { @@ -31,7 +31,7 @@ public static class IdentifierExtensions } } - public static void RemoveMachinistOffhands(this SortedList, IIdentifiedObjectData?)> changedItems) + public static void RemoveMachinistOffhands(this SortedList, IIdentifiedObjectData)> changedItems) { for (var i = 0; i < changedItems.Count; i++) {