Heavily improve changed item display.

This commit is contained in:
Ottermandias 2025-03-01 00:33:56 +01:00
parent 1ebe4099d6
commit deba8ac910
30 changed files with 360 additions and 126 deletions

@ -1 +1 @@
Subproject commit 3bf047bfa293817a691b7f06032bae7aeb2e4dc7
Subproject commit c347d29d980b0191d1d071170cf2ec229e3efdcf

@ -1 +1 @@
Subproject commit bc339208d1d453582eb146533c572823146a4592
Subproject commit 955c4e6b281bf0781689b15c01a868b0de5881b4

View file

@ -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);
}
}

View file

@ -65,7 +65,7 @@ public sealed class ModChangedItemAdapter(WeakReference<ModStorage> storage)
: throw new ObjectDisposedException("The underlying mod storage of this IPC container was disposed.");
}
private sealed class ChangedItemDictionaryAdapter(SortedList<string, IIdentifiedObjectData?> data) : IReadOnlyDictionary<string, object?>
private sealed class ChangedItemDictionaryAdapter(SortedList<string, IIdentifiedObjectData> data) : IReadOnlyDictionary<string, object?>
{
public IEnumerator<KeyValuePair<string, object?>> GetEnumerator()
=> data.Select(d => new KeyValuePair<string, object?>(d.Key, d.Value?.ToInternalObject())).GetEnumerator();

View file

@ -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<string, (SingleArray<IMod>, IIdentifiedObjectData?)> _changedItems = [];
private readonly SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData)> _changedItems = [];
public readonly ConcurrentDictionary<Utf8GamePath, ModPath> 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<string, (SingleArray<IMod>, IIdentifiedObjectData?)> ChangedItems
public IReadOnlyDictionary<string, (SingleArray<IMod>, 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<string, IIdentifiedObjectData?>(512);
var items = new SortedList<string, IIdentifiedObjectData>(512);
void AddItems(IMod mod)
{

View file

@ -46,8 +46,8 @@ public partial class ModCollection
internal IReadOnlyDictionary<Utf8GamePath, ModPath> ResolvedFiles
=> _cache?.ResolvedFiles ?? new ConcurrentDictionary<Utf8GamePath, ModPath>();
internal IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)> ChangedItems
=> _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, IIdentifiedObjectData?)>();
internal IReadOnlyDictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)> ChangedItems
=> _cache?.ChangedItems ?? new Dictionary<string, (SingleArray<IMod>, IIdentifiedObjectData)>();
internal IEnumerable<SingleArray<ModConflicts>> AllConflicts
=> _cache?.AllConflicts ?? Array.Empty<SingleArray<ModConflicts>>();

View file

@ -12,7 +12,7 @@ namespace Penumbra.Communication;
/// <item>Parameter is the clicked object data if any. </item>
/// </list>
/// </summary>
public sealed class ChangedItemClick() : EventWrapper<MouseButton, IIdentifiedObjectData?, ChangedItemClick.Priority>(nameof(ChangedItemClick))
public sealed class ChangedItemClick() : EventWrapper<MouseButton, IIdentifiedObjectData, ChangedItemClick.Priority>(nameof(ChangedItemClick))
{
public enum Priority
{

View file

@ -10,7 +10,7 @@ namespace Penumbra.Communication;
/// <item>Parameter is the hovered object data if any. </item>
/// </list>
/// </summary>
public sealed class ChangedItemHover() : EventWrapper<IIdentifiedObjectData?, ChangedItemHover.Priority>(nameof(ChangedItemHover))
public sealed class ChangedItemHover() : EventWrapper<IIdentifiedObjectData, ChangedItemHover.Priority>(nameof(ChangedItemHover))
{
public enum Priority
{

View file

@ -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<string, IIdentifiedObjectData?> changedItems)
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
{
// Nothing specific
}

View file

@ -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<string, IIdentifiedObjectData?> changedItems)
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
=> identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace, Slot));
public MetaIndex FileIndex()

View file

@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations;
public readonly record struct EqpIdentifier(PrimaryId SetId, EquipSlot Slot) : IMetaIdentifier, IComparable<EqpIdentifier>
{
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
=> identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace.MidlanderMale, Slot));
public MetaIndex FileIndex()

View file

@ -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<string, IIdentifiedObjectData?> changedItems)
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> 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));

View file

@ -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<string, IIdentifiedObjectData?> changedItems)
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> 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()

View file

@ -8,7 +8,7 @@ namespace Penumbra.Meta.Manipulations;
public readonly record struct GmpIdentifier(PrimaryId SetId) : IMetaIdentifier, IComparable<GmpIdentifier>
{
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
=> identifier.Identify(changedItems, GamePaths.Mdl.Equipment(SetId, GenderRace.MidlanderMale, EquipSlot.Head));
public MetaIndex FileIndex()

View file

@ -19,7 +19,7 @@ public enum MetaManipulationType : byte
public interface IMetaIdentifier
{
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems);
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems);
public MetaIndex FileIndex();

View file

@ -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<string, IIdentifiedObjectData?> changedItems)
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
=> AddChangedItems(identifier, changedItems, false);
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems, bool allVariants)
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems, bool allVariants)
{
var path = ObjectType switch
{

View file

@ -8,8 +8,8 @@ namespace Penumbra.Meta.Manipulations;
public readonly record struct RspIdentifier(SubRace SubRace, RspAttribute Attribute) : IMetaIdentifier
{
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
=> changedItems.TryAdd($"{SubRace.ToName()} {Attribute.ToFullString()}", null);
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
=> changedItems.UpdateCountOrSet($"{SubRace.ToName()} {Attribute.ToFullString()}", () => new IdentifiedName());
public MetaIndex FileIndex()
=> MetaIndex.HumanCmp;

View file

@ -136,7 +136,7 @@ public sealed class CombiningModGroup : IModGroup
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, MetaDictionary manipulations)
=> Data[setting.AsIndex].AddDataTo(redirections, manipulations);
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
{
foreach (var container in DataContainers)
identifier.AddChangedItems(container, changedItems);

View file

@ -53,7 +53,7 @@ public interface IModGroup
public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer);
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, MetaDictionary manipulations);
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems);
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems);
/// <summary> Ensure that a value is valid for a group. </summary>
public Setting FixSetting(Setting setting);

View file

@ -131,7 +131,7 @@ public class ImcModGroup(Mod mod) : IModGroup
}
}
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
=> Identifier.AddChangedItems(identifier, changedItems, AllVariants);
public Setting FixSetting(Setting setting)

View file

@ -122,7 +122,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
}
}
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
{
foreach (var container in DataContainers)
identifier.AddChangedItems(container, changedItems);

View file

@ -107,7 +107,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
OptionData[setting.AsIndex].AddDataTo(redirections, manipulations);
}
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> changedItems)
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
{
foreach (var container in DataContainers)
identifier.AddChangedItems(container, changedItems);

View file

@ -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)

View file

@ -101,13 +101,14 @@ public sealed class Mod : IMod
}
// Cache
public readonly SortedList<string, IIdentifiedObjectData?> ChangedItems = new();
public readonly SortedList<string, IIdentifiedObjectData> 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; }
}

View file

@ -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<nint> _unfolded = [];
private readonly Dictionary<nint, NodeVisibility> _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)}");

View file

@ -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
}
/// <summary> Check if a changed item should be drawn based on its category. </summary>
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));
/// <summary> Draw the icon corresponding to the category of a changed item. </summary>
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
}
}
/// <summary>
/// Draw a changed item, invoking the Api-Events for clicks and tooltips.
/// Also draw the item ID in grey if requested.
/// </summary>
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);
}
/// <summary> Draw the model information, right-justified. </summary>
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);
}
/// <summary> Draw the model information, right-justified. </summary>
public static void DrawModelData(ReadOnlySpan<byte> 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);
}
/// <summary> Draw a header line with the different icon types to filter them. </summary>
@ -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<TexFile>("ui/uld/levelup2_hr1.tex");
if (unk == null)

View file

@ -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<Container> 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<IdentifiedItem>>();
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<IdentifiedItem> 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<byte> 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<ChangedItemsCache>(_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);
}
}

View file

@ -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);
}
/// <summary> Draw a pair of filters and return the variable width of the flexible column. </summary>
@ -67,22 +74,25 @@ public class ChangedItemsTab(
}
/// <summary> Apply the current filters. </summary>
private bool FilterChangedItem(KeyValuePair<string, (SingleArray<IMod>, IIdentifiedObjectData?)> item)
private bool FilterChangedItem(KeyValuePair<string, (SingleArray<IMod>, IIdentifiedObjectData)> item)
=> drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter)
&& (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter)));
/// <summary> Draw a full column for a changed item. </summary>
private void DrawChangedItemColumn(KeyValuePair<string, (SingleArray<IMod>, IIdentifiedObjectData?)> item)
private void DrawChangedItemColumn(KeyValuePair<string, (SingleArray<IMod>, 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<IMod> 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)));
}
}

View file

@ -748,7 +748,7 @@ public class DebugTab : Window, ITab, IUiService
}
private string _changedItemPath = string.Empty;
private readonly Dictionary<string, IIdentifiedObjectData?> _changedItems = [];
private readonly Dictionary<string, IIdentifiedObjectData> _changedItems = [];
private void DrawChangedItemTest()
{

View file

@ -10,7 +10,7 @@ namespace Penumbra.Util;
public static class IdentifierExtensions
{
public static void AddChangedItems(this ObjectIdentification identifier, IModDataContainer container,
IDictionary<string, IIdentifiedObjectData?> changedItems)
IDictionary<string, IIdentifiedObjectData> 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<string, IIdentifiedObjectData?> changedItems)
public static void RemoveMachinistOffhands(this SortedList<string, IIdentifiedObjectData> changedItems)
{
for (var i = 0; i < changedItems.Count; i++)
{
@ -31,7 +31,7 @@ public static class IdentifierExtensions
}
}
public static void RemoveMachinistOffhands(this SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData?)> changedItems)
public static void RemoveMachinistOffhands(this SortedList<string, (SingleArray<IMod>, IIdentifiedObjectData)> changedItems)
{
for (var i = 0; i < changedItems.Count; i++)
{