Files
AetherBags/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs
T
2025-12-24 19:50:11 +01:00

331 lines
10 KiB
C#

using System;
using System.Numerics;
using AetherBags.Inventory;
using AetherBags.Nodes.Layout;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
// TODO: Switch back to CS version when Dalamud Updated
using DragDropFixedNode = AetherBags.Nodes.DragDropNode;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace AetherBags.Nodes.Inventory;
public class InventoryCategoryNode : SimpleComponentNode
{
private readonly TextNode _categoryNameTextNode;
private readonly HybridDirectionalFlexNode<DragDropFixedNode> _itemGridNode;
private const float FallbackItemSize = 46;
private const float HeaderHeight = 16;
private const float MinWidth = 40;
private float? _fixedWidth;
private int _hoverRefs;
private bool _headerSuppressed;
private bool _headerExpanded;
private float _baseHeaderWidth = 96f;
private string _fullHeaderText = string.Empty;
public event Action<InventoryCategoryNode, bool>? HeaderHoverChanged;
public InventoryCategoryNode()
{
_categoryNameTextNode = new TextNode
{
Size = new Vector2(96, 16),
AlignmentType = AlignmentType.Left,
};
_categoryNameTextNode.AddEvent(AtkEventType.MouseOver, BeginHeaderHover);
_categoryNameTextNode.AddEvent(AtkEventType.MouseOut, EndHeaderHover);
_categoryNameTextNode.TextFlags |= TextFlags.OverflowHidden | TextFlags.Ellipsis;
_categoryNameTextNode.TextFlags &= ~(TextFlags.WordWrap | TextFlags.MultiLine);
_categoryNameTextNode.AddFlags(NodeFlags.EmitsEvents | NodeFlags.HasCollision);
_categoryNameTextNode.AttachNode(this);
_itemGridNode = new HybridDirectionalFlexNode<DragDropFixedNode>
{
Position = new Vector2(0, HeaderHeight),
Size = new Vector2(240, 92),
FillRowsFirst = true,
ItemsPerLine = 10,
HorizontalPadding = 5,
VerticalPadding = 2,
};
_itemGridNode.NodeFlags |= NodeFlags.EmitsEvents;
_itemGridNode.AttachNode(this);
}
public CategorizedInventory CategorizedInventory
{
get;
set
{
field = value;
_fullHeaderText = value.Category.Name;
_categoryNameTextNode.String = _fullHeaderText;
_categoryNameTextNode.TextColor = value.Category.Color;
_categoryNameTextNode.TooltipString = value.Category.Description;
UpdateItemGrid();
RecalculateSize();
}
}
public int ItemsPerLine
{
get => _itemGridNode.ItemsPerLine;
set
{
if (_itemGridNode.ItemsPerLine == value) return;
_itemGridNode.ItemsPerLine = value;
RecalculateSize();
}
}
public float? FixedWidth
{
get => _fixedWidth;
set
{
if (_fixedWidth.Equals(value)) return;
_fixedWidth = value;
RecalculateSize();
}
}
public void BeginHeaderHover()
{
_hoverRefs++;
if (_hoverRefs != 1) return;
_headerExpanded = true;
ApplyHeaderVisualStateAndSize();
HeaderHoverChanged?.Invoke(this, true);
}
public void EndHeaderHover()
{
if (_hoverRefs <= 0) return;
_hoverRefs--;
if (_hoverRefs != 0) return;
_headerExpanded = false;
ApplyHeaderVisualStateAndSize();
HeaderHoverChanged?.Invoke(this, false);
}
public void SetHeaderSuppressed(bool suppressed)
{
if (_headerSuppressed == suppressed) return;
_headerSuppressed = suppressed;
ApplyHeaderVisualStateAndSize();
}
private void ApplyHeaderVisualStateAndSize()
{
_categoryNameTextNode.IsVisible = !_headerSuppressed;
if (_headerSuppressed)
return;
var flags = _categoryNameTextNode.TextFlags;
flags &= ~(TextFlags.WordWrap | TextFlags.MultiLine);
if (_headerExpanded)
{
flags &= ~(TextFlags.OverflowHidden | TextFlags.Ellipsis);
_categoryNameTextNode.TextFlags = flags;
if (!string.IsNullOrEmpty(_fullHeaderText))
_categoryNameTextNode.String = _fullHeaderText;
Vector2 drawSize = _categoryNameTextNode.GetTextDrawSize();
float expandedWidth = MathF.Max(_baseHeaderWidth, drawSize.X + 4f);
_categoryNameTextNode.Size = _categoryNameTextNode.Size with { X = expandedWidth };
}
else
{
_categoryNameTextNode.Size = _categoryNameTextNode.Size with { X = _baseHeaderWidth };
if (!string.IsNullOrEmpty(_fullHeaderText))
_categoryNameTextNode.String = _fullHeaderText;
flags |= (TextFlags.OverflowHidden | TextFlags.Ellipsis);
_categoryNameTextNode.TextFlags = flags;
}
}
private void RecalculateSize()
{
int itemCount = CategorizedInventory.Items.Count;
if (itemCount == 0)
{
float width = _fixedWidth ?? MinWidth;
Size = new Vector2(width, HeaderHeight);
_baseHeaderWidth = width;
_itemGridNode.Position = new Vector2(0, HeaderHeight);
_itemGridNode.Size = new Vector2(width, 0);
ApplyHeaderVisualStateAndSize();
return;
}
int itemsPerLine = _itemGridNode.ItemsPerLine;
if (itemsPerLine < 1) itemsPerLine = 1;
int rows = (itemCount + itemsPerLine - 1) / itemsPerLine;
int actualColumns = Math.Min(itemCount, itemsPerLine);
float cellW, cellH;
if (_itemGridNode.Nodes.Count > 0)
{
var firstChild = _itemGridNode.Nodes[0];
cellW = firstChild.Width;
cellH = firstChild.Height;
}
else
{
cellW = FallbackItemSize;
cellH = FallbackItemSize;
}
float hPad = _itemGridNode.HorizontalPadding;
float vPad = _itemGridNode.VerticalPadding;
float calculatedWidth;
if (_fixedWidth.HasValue)
{
calculatedWidth = _fixedWidth.Value;
}
else
{
calculatedWidth = actualColumns * cellW + (actualColumns - 1) * hPad;
if (calculatedWidth < MinWidth) calculatedWidth = MinWidth;
}
float height = HeaderHeight + rows * cellH + (rows - 1) * vPad;
Size = new Vector2(calculatedWidth, height);
_itemGridNode.Position = new Vector2(0, HeaderHeight);
_itemGridNode.Size = new Vector2(calculatedWidth, height - HeaderHeight);
_baseHeaderWidth = calculatedWidth;
ApplyHeaderVisualStateAndSize();
}
private void UpdateItemGrid()
{
_itemGridNode.SyncWithListData(
CategorizedInventory.Items,
node => node.ItemInfo,
CreateInventoryDragDropNode);
}
private InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
{
InventoryItem item = data.Item;
var node = new InventoryDragDropNode
{
Size = new Vector2(42, 46),
IsVisible = true,
IconId = item.IconId,
AcceptedType = DragDropType.Item,
IsDraggable = true,
Payload = new DragDropPayload
{
Type = DragDropType.Inventory_Item,
Int1 = (int)item.GetInventoryType(),
Int2 = item.Slot,
},
IsClickable = true,
OnEnd = _ =>
{
System.AddonInventoryWindow.ManualInventoryRefresh();
},
OnPayloadAccepted = (n, p) => OnPayloadAccepted(n, p, data),
OnRollOver = n =>
{
BeginHeaderHover();
n.ShowInventoryItemTooltip(item.Container, item.Slot);
},
OnRollOut = n =>
{
EndHeaderHover();
n.HideTooltip();
},
ItemInfo = data
};
return node;
}
private unsafe void OnPayloadAccepted(DragDropNode node, DragDropPayload payload, ItemInfo itemInfo)
{
if (payload.Type != DragDropType.Item) return;
InventoryItem item = itemInfo.Item;
Services.Logger.Debug($"Inventory DragDropNode Payload Accepted: {payload.Type} Int1: {payload.Int1} Int2: {payload.Int2} ReferenceIndex: {payload.ReferenceIndex}");
InventoryType inventoryType = InventoryType.GetInventoryTypeFromContainerId(payload.Int1);
ushort sourceSlot = (ushort)payload.Int2;
ItemOrderModuleSorterItemEntry* itemEntry = item.GetItemOrderData();
Services.Logger.Debug($"{item.Slot} vs {item.GetSlot()}: entry: {itemEntry->Slot}");
Services.Logger.Info($"[OnPayload] Moving {inventoryType}@{sourceSlot} -> {item.Container}@{item.Slot} -> {item.Name.ExtractText()}");
InventoryManager.Instance()->MoveItemSlot(inventoryType, sourceSlot, item.Container, item.GetSlot(), true);
// System.AddonInventoryWindow.ManualInventoryRefresh();
// Should work for swapping item but need a fake empty slot to put new items in probably.
// Services.Logger.Debug($"Moving Item from {inventoryType} Slot {sourceSlot} to {itemInfo.Item.Container} Slot {itemInfo.Item.GetSlot()}");
//MoveItem(inventoryType, sourceSlot, itemInfo.Item.Container, itemInfo.Item.GetSlot());
}
// Possibly still use this
private unsafe void MoveItem(InventoryType sourceInventory, uint sourceSlot, InventoryType destinationInventory, uint destinationSlot)
{
var sourceContainerId = sourceInventory.AgentItemContainerId;
var destinationContainerId = destinationInventory.AgentItemContainerId;
if (sourceContainerId != 0 && destinationContainerId != 0) {
var atkValues = stackalloc AtkValue[4];
for (var i = 0; i < 4; i++) atkValues[i].Type = ValueType.UInt;
atkValues[0].UInt = sourceContainerId;
atkValues[1].UInt = sourceSlot;
atkValues[2].UInt = destinationContainerId;
atkValues[3].UInt = destinationSlot;
var retVal = stackalloc AtkValue[1];
RaptureAtkModule* atkModule = RaptureAtkModule.Instance();
// (RaptureAtkModule* a1, void* outValue, AtkValue* atkValues);
// (AtkValue* returnValue, AtkValue* values, uint valueCount)
atkModule->HandleItemMove(retVal, atkValues, 4);
}
}
}