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

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()
{