Penumbra/Penumbra/UI/ModsTab/ModPanelChangedItemsTab.cs
2025-10-28 14:54:42 +01:00

356 lines
14 KiB
C#

using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using ImSharp;
using Luna;
using OtterGui;
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.Mods.Manager;
using Penumbra.String;
using Penumbra.UI.Classes;
namespace Penumbra.UI.ModsTab;
public class ModPanelChangedItemsTab(
ModFileSystemSelector selector,
ChangedItemDrawer drawer,
ImGuiCacheService cacheService,
Configuration config,
ModDataEditor dataEditor)
: ITab, Luna.IUiService
{
private readonly ImGuiCacheService.CacheId _cacheId = cacheService.GetNewId();
private class ChangedItemsCache
{
private Mod? _lastSelected;
private ushort _lastUpdate;
private ChangedItemIconFlag _filter = ChangedItemFlagExtensions.DefaultFlags;
private ChangedItemMode _lastMode;
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, ChangedItemMode mode)
{
if (mod == _lastSelected
&& _lastSelected!.LastChangedItemsUpdate == _lastUpdate
&& _filter == filter
&& !_reset
&& _lastMode == mode)
return;
_reset = false;
Data.Clear();
AnyExpandable = false;
_lastSelected = mod;
_filter = filter;
_lastMode = mode;
if (_lastSelected == null)
return;
_lastUpdate = _lastSelected.LastChangedItemsUpdate;
if (mode is ChangedItemMode.Alphabetical)
{
foreach (var (s, i) in _lastSelected.ChangedItems)
{
if (drawer.FilterChangedItem(s, i, string.Empty))
Data.Add(Container.Single(s, i));
}
return;
}
var tmp = new Dictionary<(PrimaryId, FullEquipType), List<IdentifiedItem>>();
var defaultExpansion = _lastMode is ChangedItemMode.GroupedExpanded;
foreach (var (s, i) in _lastSelected.ChangedItems)
{
if (i is not IdentifiedItem item)
continue;
if (!drawer.FilterChangedItem(s, item, string.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 preferred = _lastSelected.PreferredChangedItems.Contains(i2.Item.Id)
.CompareTo(_lastSelected.PreferredChangedItems.Contains(i1.Item.Id));
if (preferred != 0)
return preferred;
// 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, string.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, defaultExpansion);
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;
private Rgba32 _starColor;
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.Ephemeral.ChangedItemFilter, config.ChangedItemDisplay);
ImGui.Separator();
_buttonSize = new Vector2(ImGui.GetStyle().ItemSpacing.Y + Im.Style.FrameHeight);
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 color = ImGuiColor.Button.Push(Rgba32.Transparent)
.Push(ImGuiColor.ButtonActive, Rgba32.Transparent)
.Push(ImGuiColor.ButtonHovered, Rgba32.Transparent);
using var table = Im.Table.Begin("##changedItems"u8, cache.AnyExpandable ? 2 : 1, TableFlags.RowBackground | TableFlags.ScrollY,
new Vector2(Im.ContentRegion.Available.X, -1));
if (!table)
return;
_starColor = ColorId.ChangedItemPreferenceStar.Value();
if (cache.AnyExpandable)
{
table.SetupColumn("##exp"u8, TableColumnFlags.WidthFixed, _buttonSize.Y);
table.SetupColumn("##text"u8, TableColumnFlags.WidthStretch);
ImGuiClip.ClippedDraw(cache.Data, DrawContainerExpandable, _buttonSize.Y);
}
else
{
ImGuiClip.ClippedDraw(cache.Data, DrawContainer, _buttonSize.Y);
}
}
private void DrawContainerExpandable(ChangedItemsCache.Container obj, int idx)
{
using var id = ImUtf8.PushId(idx);
ImGui.TableNextColumn();
if (obj.Expandable)
{
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 if (obj is { Child: true, Data: IdentifiedItem item })
{
DrawPreferredButton(item, idx);
}
else
{
ImGui.Dummy(_buttonSize);
}
DrawBaseContainer(obj, idx);
}
private void DrawContainer(ChangedItemsCache.Container obj, int idx)
{
using var id = ImUtf8.PushId(idx);
DrawBaseContainer(obj, idx);
}
private void DrawPreferredButton(IdentifiedItem item, int idx)
{
var textColor = Im.Mouse.IsHoveringRectangle(Rectangle.FromSize(Im.Cursor.ScreenPosition, _buttonSize))
? LunaStyle.FavoriteColor
: _starColor;
if (ImEx.Icon.Button(LunaStyle.FavoriteIcon,
"Prefer displaying this item instead of the current primary item.\n\nRight-click for more options."u8,
textColor: textColor, size: _buttonSize))
dataEditor.AddPreferredItem(selector.Selected!, item.Item.Id, false, true);
using var context = ImUtf8.PopupContextItem("StarContext"u8);
if (!context)
return;
if (cacheService.TryGetCache<ChangedItemsCache>(_cacheId, out var cache))
for (--idx; idx >= 0; --idx)
{
if (!cache.Data[idx].Expanded)
continue;
if (cache.Data[idx].Data is IdentifiedItem it)
{
if (selector.Selected!.PreferredChangedItems.Contains(it.Item.Id)
&& ImUtf8.MenuItem("Remove Parent from Local Preferred Items"u8))
dataEditor.RemovePreferredItem(selector.Selected!, it.Item.Id, false);
if (selector.Selected!.DefaultPreferredItems.Contains(it.Item.Id)
&& ImUtf8.MenuItem("Remove Parent from Default Preferred Items"u8))
dataEditor.RemovePreferredItem(selector.Selected!, it.Item.Id, true);
}
break;
}
var enabled = !selector.Selected!.DefaultPreferredItems.Contains(item.Item.Id);
if (enabled)
{
if (ImUtf8.MenuItem("Add to Local and Default Preferred Changed Items"u8))
dataEditor.AddPreferredItem(selector.Selected!, item.Item.Id, true, true);
}
else
{
if (ImUtf8.MenuItem("Remove from Default Preferred Changed Items"u8))
dataEditor.RemovePreferredItem(selector.Selected!, item.Item.Id, true);
}
if (ImUtf8.MenuItem("Reset Local Preferred Items to Default"u8))
dataEditor.ResetPreferredItems(selector.Selected!);
if (ImUtf8.MenuItem("Clear Local and Default Preferred Items not Changed by the Mod"u8))
dataEditor.ClearInvalidPreferredItems(selector.Selected!);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DrawBaseContainer(in ChangedItemsCache.Container obj, int _)
{
ImGui.TableNextColumn();
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);
}
}