Improve layout

This commit is contained in:
Zeffuro
2025-12-20 09:21:44 +01:00
parent 659c295c16
commit 592fa392bf
10 changed files with 449 additions and 65 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAgentSatisfactionSupply_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb2cd0663609440e590f52980cafc1ba3822648_003F28_003Ffa48b62e_003FAgentSatisfactionSupply_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAgentSatisfactionSupply_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb2cd0663609440e590f52980cafc1ba3822648_003F28_003Ffa48b62e_003FAgentSatisfactionSupply_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AItem_002Eg_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003F8ec7cc8a18dbb6a6f3c21f8adcb4e2661dc7979_003FItem_002Eg_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
+104 -44
View File
@@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using AetherBags.Extensions; using AetherBags.Extensions;
@@ -15,58 +17,33 @@ namespace AetherBags.Addons;
public class AddonInventoryWindow : NativeAddon public class AddonInventoryWindow : NativeAddon
{ {
private InventoryCategoryNode _categoryNode; private WrappingGridNode<InventoryCategoryNode> _categoriesNode;
private InventoryDragDropNode _dragDropNode;
// Window constraints
private const float MinWindowWidth = 300;
private const float MaxWindowWidth = 800;
private const float MinWindowHeight = 200;
private const float MaxWindowHeight = 1000;
// Layout settings
private const float CategorySpacing = 10;
private const float ItemSize = 40;
private const float ItemPadding = 6;
protected override unsafe void OnSetup(AtkUnitBase* addon) protected override unsafe void OnSetup(AtkUnitBase* addon)
{ {
Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate); Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate);
_categoryNode = new InventoryCategoryNode addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
_categoriesNode = new WrappingGridNode<InventoryCategoryNode>
{ {
Position = ContentStartPosition, Position = ContentStartPosition,
Size = ContentSize, Size = ContentSize,
Category = new CategoryInfo HorizontalSpacing = CategorySpacing,
{ VerticalSpacing = CategorySpacing
Name = "AetherBags",
},
Items = InventoryState.GetInventoryItems()
}; };
_categoryNode.AttachNode(this); _categoriesNode.AttachNode(this);
/*
var data = InventoryState.GetInventoryItems().Find(item => item.Name.Contains("Cookie"));
if (data != null)
{
var item = data.Item;
_dragDropNode = new InventoryDragDropNode
{
Size = new Vector2(48),
IsVisible = true,
IconId = data.IconId,
AcceptedType = DragDropType.Nothing,
IsDraggable = false,
Payload = new DragDropPayload
{
Type = DragDropType.Item,
Int1 = (int)data.Item.Container,
Int2 = (int)data.Item.ItemId,
},
IsClickable = true,
OnRollOver = node => node.ShowInventoryItemTooltip(data.Item.Container, data.Item.Slot),
OnRollOut = node => node.HideTooltip(),
OnClicked = _ =>
{
AgentInventoryContext* context = AgentInventoryContext.Instance();
context->OpenForItemSlot(data.Item.Container, data.Item.Slot, 0, context->AddonId);
//item.UseItem();
},
ItemInfo = data
};
_dragDropNode.AttachNode(this);
}
*/
RefreshCategories();
} }
protected override unsafe void OnUpdate(AtkUnitBase* addon) protected override unsafe void OnUpdate(AtkUnitBase* addon)
@@ -76,11 +53,94 @@ public class AddonInventoryWindow : NativeAddon
private void OnInventoryUpdate(AddonEvent type, AddonArgs args) private void OnInventoryUpdate(AddonEvent type, AddonArgs args)
{ {
RefreshCategories();
}
protected override unsafe void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) {
RefreshCategories();
}
private void RefreshCategories()
{
var categories = InventoryState.GetInventoryItemCategories();
float maxContentWidth = MaxWindowWidth - (ContentStartPosition.X * 2);
int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth);
_categoriesNode.SyncWithListData(
categories,
node => node.CategorizedInventory,
data =>
{
var node = new InventoryCategoryNode
{
Size = ContentSize with { Y = 120 },
CategorizedInventory = data
};
UpdateItemsPerLine(node, maxItemsPerLine);
return node;
});
foreach (InventoryCategoryNode node in _categoriesNode.GetNodes<InventoryCategoryNode>())
{
UpdateItemsPerLine(node, maxItemsPerLine);
}
AutoSizeWindow();
}
private static void UpdateItemsPerLine(InventoryCategoryNode node, int maxItemsPerLine)
{
int itemCount = node.CategorizedInventory.Items.Count;
int itemsPerLine = Math.Min(itemCount, maxItemsPerLine);
node.SetItemsPerLine(itemsPerLine);
}
private int CalculateOptimalItemsPerLine(float availableWidth)
{
float itemWithPadding = ItemSize + ItemPadding;
int maxItems = (int)Math.Floor((availableWidth + ItemPadding) / itemWithPadding);
return Math.Clamp(maxItems, 1, 15);
}
private void AutoSizeWindow()
{
List<InventoryCategoryNode> childNodes = _categoriesNode.GetNodes<InventoryCategoryNode>().ToList();
if (childNodes.Count == 0)
{
ResizeWindow(MinWindowWidth, MinWindowHeight);
return;
}
float requiredWidth = childNodes.Max(node => node. Width);
requiredWidth += ContentStartPosition.X * 2;
float finalWidth = Math.Clamp(requiredWidth, MinWindowWidth, MaxWindowWidth);
float contentWidth = finalWidth - (ContentStartPosition.X * 2);
_categoriesNode.Size = new Vector2(contentWidth, MaxWindowHeight);
_categoriesNode.RecalculateLayout();
float requiredHeight = _categoriesNode.GetRequiredHeight();
requiredHeight += ContentStartPosition.Y + ContentStartPosition.X;
float finalHeight = Math.Clamp(requiredHeight, MinWindowHeight, MaxWindowHeight);
ResizeWindow(finalWidth, finalHeight);
}
private void ResizeWindow(float width, float height)
{
SetWindowSize(width, height);
_categoriesNode.Size = ContentSize;
_categoriesNode.RecalculateLayout();
} }
protected override unsafe void OnFinalize(AtkUnitBase* addon) protected override unsafe void OnFinalize(AtkUnitBase* addon)
{ {
Services.AddonLifecycle.UnregisterListener(OnInventoryUpdate); Services.AddonLifecycle.UnregisterListener(OnInventoryUpdate);
addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
} }
} }
@@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Linq;
using AetherBags;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
public static class AddonLifecycleExtensions {
extension(IAddonLifecycle addonLifecycle) {
public void LogAddon(string addonName, params AddonEvent[] loggedModules) {
if (loggedModules.Length is 0) {
loggedModules = [
AddonEvent.PostSetup,
AddonEvent.PostOpen,
AddonEvent.PostClose,
AddonEvent.PostShow,
AddonEvent.PostHide,
AddonEvent.PostRefresh,
AddonEvent.PostRequestedUpdate,
AddonEvent.PreFinalize,
];
}
ActiveLoggers.TryAdd(addonName, loggedModules.ToList());
foreach (var loggedModule in loggedModules) {
addonLifecycle.RegisterListener(loggedModule, addonName, Logger);
}
}
public void UnLogAddon(string addonName) {
if (!ActiveLoggers.TryGetValue(addonName, out var loggedModules)) return;
foreach (var loggedModule in loggedModules) {
addonLifecycle.UnregisterListener(loggedModule, addonName, Logger);
}
}
}
private static readonly Dictionary<string, List<AddonEvent>> ActiveLoggers = [];
private static void Logger(AddonEvent type, AddonArgs args) {
switch (args) {
case AddonReceiveEventArgs receiveEventArgs:
Services.Logger.Debug($"[{args.AddonName}] {(AtkEventType)receiveEventArgs.AtkEventType}: {receiveEventArgs.EventParam}");
break;
default:
Services.Logger.Debug($"{args.AddonName} called {type.ToString().Replace("Post", string.Empty)}");
break;
}
}
}
@@ -0,0 +1,5 @@
using System.Collections.Generic;
namespace AetherBags.Inventory;
public readonly record struct CategorizedInventory(CategoryInfo Category, List<ItemInfo> Items);
+48
View File
@@ -3,6 +3,7 @@ using System.Linq;
using Dalamud.Game.Inventory; using Dalamud.Game.Inventory;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.Sheets;
namespace AetherBags.Inventory; namespace AetherBags.Inventory;
@@ -34,6 +35,22 @@ public static unsafe class InventoryState
public static bool Contains(this List<InventoryType> inventoryTypes, GameInventoryType type) public static bool Contains(this List<InventoryType> inventoryTypes, GameInventoryType type)
=> inventoryTypes.Contains((InventoryType)type); => inventoryTypes.Contains((InventoryType)type);
public static List<CategorizedInventory> GetInventoryItemCategories()
{
var items = GetInventoryItems();
return items
.GroupBy(GetItemUiCategoryKey)
.OrderBy(g => g.Key)
.Select(g =>
{
var category = GetCategoryInfoForKey(g.Key, g.FirstOrDefault());
var list = g.OrderByDescending(i => i.ItemCount).ToList();
return new CategorizedInventory(category, list);
})
.ToList();
}
public static List<ItemInfo> GetInventoryItems() { public static List<ItemInfo> GetInventoryItems() {
List<InventoryType> inventories = [ InventoryType.Inventory1, InventoryType.Inventory2, InventoryType.Inventory3, InventoryType.Inventory4 ]; List<InventoryType> inventories = [ InventoryType.Inventory1, InventoryType.Inventory2, InventoryType.Inventory3, InventoryType.Inventory4 ];
List<InventoryItem> items = []; List<InventoryItem> items = [];
@@ -60,4 +77,35 @@ public static unsafe class InventoryState
return itemInfos; return itemInfos;
} }
public static List<ItemInfo> GetInventoryItems(string filterString, bool invert = false)
=> GetInventoryItems().Where(item => item.IsRegexMatch(filterString) != invert).ToList();
private static uint GetItemUiCategoryKey(ItemInfo info)
=> info.UiCategory.RowId;
private static CategoryInfo GetCategoryInfoForKey(uint key, ItemInfo? sample)
{
if (key == 0)
{
return new CategoryInfo
{
Name = "Misc",
Description = "Uncategorized items",
};
}
var uiCat = sample?.UiCategory.Value;
var name = uiCat?.Name.ToString();
if (string.IsNullOrWhiteSpace(name))
name = $"Category\\ {key}";
return new CategoryInfo
{
Name = name,
};
}
} }
+2 -1
View File
@@ -3,6 +3,7 @@ using System.Numerics;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using AetherBags.Extensions; using AetherBags.Extensions;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using Lumina.Excel;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
namespace AetherBags.Inventory; namespace AetherBags.Inventory;
@@ -25,7 +26,7 @@ public class ItemInfo : IEquatable<ItemInfo> {
public int Rarity => ItemData.Rarity; public int Rarity => ItemData.Rarity;
public int UiCategory => (int) ItemData.ItemUICategory.RowId; public RowRef<ItemUICategory> UiCategory => ItemData.ItemUICategory;
private string Description => ItemData.Description.ToString(); private string Description => ItemData.Description.ToString();
@@ -0,0 +1,94 @@
using System;
using KamiToolKit;
using KamiToolKit.Nodes;
namespace AetherBags.Nodes
{
public class HybridDirectionalStackNode<T> : LayoutListNode where T : NodeBase
{
public FlexGrowDirection GrowDirection
{
get;
set
{
field = value;
RecalculateLayout();
}
} = FlexGrowDirection.DownRight;
public bool Vertical
{
get;
set
{
field = value;
RecalculateLayout();
}
} = true;
public float Spacing
{
get;
set
{
field = value;
RecalculateLayout();
}
} = 1f;
public bool StretchCrossAxis
{
get;
set
{
field = value;
RecalculateLayout();
}
} = true;
protected override void InternalRecalculateLayout()
{
if (NodeList.Count == 0)
return;
bool alignRight = GrowDirection is FlexGrowDirection.DownLeft or FlexGrowDirection.UpLeft;
bool alignBottom = GrowDirection is FlexGrowDirection.UpRight or FlexGrowDirection.UpLeft;
float startX = alignRight ? Width : 0f;
float startY = alignBottom ? Height : 0f;
float cursor = 0f;
for (int i = 0; i < NodeList.Count; i++)
{
var node = NodeList[i];
if (StretchCrossAxis)
{
if (Vertical)
node.Width = Width;
else
node.Height = Height;
}
float x, y;
if (Vertical)
{
x = alignRight ? startX - node.Width : startX;
y = alignBottom ? startY - node.Height - cursor : startY + cursor;
cursor += node.Height + Spacing;
}
else
{
x = alignRight ? startX - node.Width - cursor : startX + cursor;
y = alignBottom ? startY - node.Height : startY;
cursor += node.Width + Spacing;
}
node.X = x;
node.Y = y;
AdjustNode(node);
}
}
}
}
+70 -22
View File
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using AetherBags.Extensions; using AetherBags.Extensions;
using AetherBags.Inventory; using AetherBags.Inventory;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes; using KamiToolKit.Classes;
@@ -14,6 +15,15 @@ public class InventoryCategoryNode : SimpleComponentNode
{ {
private readonly TextNode _categoryNameTextNode; private readonly TextNode _categoryNameTextNode;
private readonly HybridDirectionalFlexNode<DragDropNode> _itemGridNode; private readonly HybridDirectionalFlexNode<DragDropNode> _itemGridNode;
private const float ItemSize = 40;
private const float ItemHorizontalPadding = 6;
private const float ItemVerticalPadding = 6;
private const float HeaderHeight = 16;
private const float MinWidth = 40;
private float? _fixedWidth = null;
public InventoryCategoryNode() public InventoryCategoryNode()
{ {
_categoryNameTextNode = new TextNode _categoryNameTextNode = new TextNode
@@ -25,63 +35,101 @@ public class InventoryCategoryNode : SimpleComponentNode
_itemGridNode = new HybridDirectionalFlexNode<DragDropNode> _itemGridNode = new HybridDirectionalFlexNode<DragDropNode>
{ {
Position = new Vector2(0, 16), Position = new Vector2(0, HeaderHeight),
Size = new Vector2(240, 100), Size = new Vector2(240, 100),
FillRowsFirst = true, FillRowsFirst = true,
ItemsPerLine = 10, ItemsPerLine = 10,
HorizontalPadding = 6, HorizontalPadding = ItemHorizontalPadding,
VerticalPadding = 6, VerticalPadding = ItemVerticalPadding,
}; };
_itemGridNode.AttachNode(this); _itemGridNode.AttachNode(this);
} }
public required CategoryInfo Category public required CategorizedInventory CategorizedInventory
{ {
get; get;
set set
{ {
field = value; field = value;
_categoryNameTextNode.String = value.Name; _categoryNameTextNode.String = value.Category.Name;
_categoryNameTextNode.TextColor = value.Color; _categoryNameTextNode.TextColor = value.Category.Color;
_categoryNameTextNode.TooltipString = value.Description; _categoryNameTextNode.TooltipString = value.Category.Description;
}
}
public required List<ItemInfo> Items
{
get;
set
{
field = value;
UpdateItemGrid(); UpdateItemGrid();
RecalculateSize();
} }
} }
public void SetItemsPerLine(int itemsPerLine)
{
_itemGridNode.ItemsPerLine = itemsPerLine;
RecalculateSize();
}
public void SetFixedWidth(float width)
{
_fixedWidth = width;
RecalculateSize();
}
private void RecalculateSize()
{
int itemCount = CategorizedInventory.Items.Count;
if (itemCount == 0)
{
float width = _fixedWidth ?? MinWidth;
Size = new Vector2(width, HeaderHeight);
_categoryNameTextNode.Size = _categoryNameTextNode.Size with { X = width };
return;
}
int itemsPerLine = Math.Max(1, _itemGridNode.ItemsPerLine);
int rows = (int)Math.Ceiling((float)itemCount / itemsPerLine);
float calculatedWidth;
if (_fixedWidth. HasValue)
{
calculatedWidth = _fixedWidth.Value;
}
else
{
int actualColumns = Math.Min(itemCount, itemsPerLine);
calculatedWidth = actualColumns * ItemSize + (actualColumns - 1) * ItemHorizontalPadding;
calculatedWidth = Math.Max(calculatedWidth, MinWidth);
}
float height = HeaderHeight + rows * ItemSize + (rows - 1) * ItemVerticalPadding;
Size = new Vector2(calculatedWidth, height);
_itemGridNode.Size = new Vector2(calculatedWidth, height - HeaderHeight);
_categoryNameTextNode.Size = _categoryNameTextNode.Size with { X = calculatedWidth };
}
private bool UpdateItemGrid() private bool UpdateItemGrid()
{ {
var listUpdated = _itemGridNode.SyncWithListData(Items, node => node.ItemInfo, data => CreateInventoryDragDropNode(data)); var listUpdated = _itemGridNode.SyncWithListData(CategorizedInventory.Items, node => node.ItemInfo, CreateInventoryDragDropNode);
return listUpdated; return listUpdated;
} }
private unsafe InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data) private InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
{ {
InventoryItem item = data.Item;
InventoryDragDropNode node = new InventoryDragDropNode InventoryDragDropNode node = new InventoryDragDropNode
{ {
Size = new Vector2(40), Size = new Vector2(40),
IsVisible = true, IsVisible = true,
IconId = data.IconId, IconId = item.IconId,
AcceptedType = DragDropType.Nothing, AcceptedType = DragDropType.Nothing,
IsDraggable = false, IsDraggable = false,
Payload = new DragDropPayload Payload = new DragDropPayload
{ {
Type = DragDropType.Item, Type = DragDropType.Item,
Int1 = (int)data.Item.Container, Int1 = (int)item.Container,
Int2 = (int)data.Item.ItemId, Int2 = (int)item.ItemId,
}, },
IsClickable = true, IsClickable = true,
OnRollOver = node => node.ShowInventoryItemTooltip(data.Item.Container, data.Item.Slot), OnRollOver = node => node.ShowInventoryItemTooltip(item.Container, item.Slot),
OnRollOut = node => node.HideTooltip(), OnRollOut = node => node.HideTooltip(),
ItemInfo = data ItemInfo = data
}; };
+72
View File
@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using KamiToolKit;
using KamiToolKit. Nodes;
namespace AetherBags.Nodes
{
public class WrappingGridNode<T> : LayoutListNode where T : NodeBase
{
public float HorizontalSpacing { get; set; } = 10;
public float VerticalSpacing { get; set; } = 10;
private List<List<NodeBase>> _rows = new();
protected override void InternalRecalculateLayout()
{
if (NodeList. Count == 0)
return;
_rows.Clear();
float availableWidth = Width;
float currentX = 0f;
float currentY = 0f;
float rowHeight = 0f;
List<NodeBase> currentRow = new();
foreach (var node in NodeList)
{
float nodeWidth = node.Width;
float nodeHeight = node.Height;
if (currentX + nodeWidth > availableWidth && currentRow.Count > 0)
{
_rows.Add(currentRow);
currentRow = new();
currentY += rowHeight + VerticalSpacing;
currentX = 0f;
rowHeight = 0f;
}
node.X = currentX;
node. Y = currentY;
AdjustNode(node);
currentX += nodeWidth + HorizontalSpacing;
rowHeight = Math.Max(rowHeight, nodeHeight);
currentRow.Add(node);
}
if (currentRow.Count > 0)
{
_rows.Add(currentRow);
}
}
public float GetRequiredHeight()
{
if (NodeList.Count == 0)
return 0f;
float maxY = 0f;
foreach (var node in NodeList)
{
float nodeBottom = node.Y + node.Height;
maxY = Math. Max(maxY, nodeBottom);
}
return maxY;
}
}
}
+1
View File
@@ -43,6 +43,7 @@ public class Plugin : IDalamudPlugin
if (Services.ClientState.IsLoggedIn) { if (Services.ClientState.IsLoggedIn) {
Services.Framework.RunOnFrameworkThread(OnLogin); Services.Framework.RunOnFrameworkThread(OnLogin);
} }
Services.AddonLifecycle.LogAddon("Inventory");
} }
public void Dispose() public void Dispose()