Files
AetherBags/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs
T
2026-01-02 23:23:49 +01:00

327 lines
11 KiB
C#

using System;
using System.Numerics;
using AetherBags.Helpers;
using AetherBags.Hooks;
using AetherBags.Inventory;
using AetherBags.Inventory.Categories;
using AetherBags.Inventory.Items;
using AetherBags.Nodes.Layout;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Inventory;
public class InventoryCategoryNode : SimpleComponentNode
{
private readonly TextNode _categoryNameTextNode;
private readonly HybridDirectionalFlexNode<DragDropNode> _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 Action? OnRefreshRequested { get; set; }
public Action? OnDragEnd { get; set; }
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<DragDropNode>
{
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 = System.Config.General.ShowCategoryItemCount
? $"{value.Category.Name} ({value.Items.Count})"
: value.Category.Name;
_categoryNameTextNode.String = _fullHeaderText;
_categoryNameTextNode.TextColor = value.Category.Color;
_categoryNameTextNode.TextTooltip = 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 bool IsPinnedInConfig => CategorizedInventory.Category?.IsPinned ?? false;
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 = Math.Max(1, _itemGridNode.ItemsPerLine);
int rows = (itemCount + itemsPerLine - 1) / itemsPerLine;
int actualColumns = Math.Min(itemCount, itemsPerLine);
float cellW = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Width : FallbackItemSize;
float cellH = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Height : FallbackItemSize;
float hPad = _itemGridNode.HorizontalPadding;
float vPad = _itemGridNode.VerticalPadding;
float calculatedWidth = _fixedWidth ?? Math.Max(MinWidth, actualColumns * cellW + (actualColumns - 1) * hPad);
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 unsafe InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
{
InventoryItem item = data.Item;
InventoryMappedLocation visualLocation = data.VisualLocation;
var visualInvType = InventoryType.GetInventoryTypeFromContainerId(visualLocation.Container);
int absoluteIndex = visualInvType.GetInventoryStartIndex + visualLocation.Slot;
DragDropPayload nodePayload = new DragDropPayload
{
// Int1 is always the container ID, for Item DragDrop Int2 is only used as a fallback
// ReferenceIndex is the absolute index that's actually used
Type = DragDropType.Item,
Int1 = visualLocation.Container,
Int2 = visualLocation.Slot,
ReferenceIndex = (short)absoluteIndex
};
return new InventoryDragDropNode
{
Size = new Vector2(42, 46),
Alpha = data.VisualAlpha,
AddColor = data.HighlightOverlayColor,
IsDraggable = !data.IsSlotBlocked,
IsVisible = true,
IconId = item.IconId,
AcceptedType = DragDropType.Item,
Payload = nodePayload,
IsClickable = true,
OnDiscard = node => OnDiscard(node, data),
OnEnd = _ => OnDragEnd?.Invoke(),
OnPayloadAccepted = (node, acceptedPayload) => OnPayloadAccepted(node, acceptedPayload, data),
OnRollOver = node =>
{
BeginHeaderHover();
node.ShowInventoryItemTooltip(item.Container, item.Slot);
},
OnRollOut = node =>
{
EndHeaderHover();
ushort addonId = RaptureAtkUnitManager.Instance()->GetAddonByNode(node)->Id;
AtkStage.Instance()->TooltipManager.HideTooltip(addonId);
},
ItemInfo = data
};
}
public void RefreshNodeVisuals()
{
foreach (var node in _itemGridNode.Nodes)
{
if (node is not InventoryDragDropNode itemNode || itemNode.ItemInfo == null) continue;
itemNode.Alpha = itemNode.ItemInfo.VisualAlpha;
itemNode.AddColor = itemNode.ItemInfo.HighlightOverlayColor;
itemNode.IsDraggable = !itemNode.ItemInfo.IsSlotBlocked;
}
}
private unsafe void OnDiscard(DragDropNode node, ItemInfo item)
{
uint addonId = RaptureAtkUnitManager.Instance()->GetAddonByNode(node)->Id;
AgentInventoryContext.Instance()->DiscardItem(item.Item.GetLinkedItem(), item.Item.Container, item.Item.Slot, addonId);
}
private void OnPayloadAccepted(DragDropNode node, DragDropPayload acceptedPayload, ItemInfo targetItemInfo)
{
try
{
// KTK clears node.Payload before invoking this, so setting it manually again
var nodePayload = new DragDropPayload
{
Type = DragDropType.Item,
Int1 = targetItemInfo.VisualLocation.Container,
Int2 = targetItemInfo.VisualLocation.Slot,
ReferenceIndex = (short)(targetItemInfo.Item.Container.GetInventoryStartIndex + targetItemInfo.VisualLocation.Slot)
};
Services.Logger.DebugOnly($"[OnPayload] ACCEPTED payload: Type={acceptedPayload.Type} Int1={acceptedPayload.Int1} Int2={acceptedPayload.Int2} Ref={acceptedPayload.ReferenceIndex}");
Services.Logger.DebugOnly($"[OnPayload] NODE payload: Type={nodePayload.Type} Int1={nodePayload.Int1} Int2={nodePayload.Int2} Ref={nodePayload.ReferenceIndex}");
if (!acceptedPayload.IsValidInventoryPayload || !nodePayload.IsValidInventoryPayload)
{
Services.Logger.Warning($"[OnPayload] Invalid payload type: Accepted={acceptedPayload.Type} Node={nodePayload.Type}");
return;
}
if (acceptedPayload.IsSameBaseContainer(nodePayload))
{
Services.Logger.DebugOnly("[OnPayload] Source and target are in the same base container, skipping move.");
node.IconId = targetItemInfo.IconId;
node.Payload = nodePayload;
return;
}
var sourceCopy = acceptedPayload;
var targetCopy = nodePayload;
InventoryMoveHelper.HandleItemMovePayload(sourceCopy, targetCopy);
OnRefreshRequested?.Invoke();
}
catch (Exception ex)
{
Services.Logger.Error(ex, "[OnPayload] Error handling payload acceptance");
}
}
}