307 lines
10 KiB
C#
307 lines
10 KiB
C#
using System;
|
||
using System.Numerics;
|
||
using AetherBags.Extensions;
|
||
using AetherBags.Helpers;
|
||
using AetherBags.Inventory;
|
||
using AetherBags.Nodes.Layout;
|
||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||
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;
|
||
|
||
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 = 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 InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
|
||
{
|
||
InventoryItem item = data.Item;
|
||
|
||
return 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.Container,
|
||
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
|
||
};
|
||
}
|
||
|
||
private void OnPayloadAccepted(DragDropNode node, DragDropPayload payload, ItemInfo targetItemInfo)
|
||
{
|
||
if (payload.Type != DragDropType.Item && payload.Type != DragDropType.Inventory_Item)
|
||
return;
|
||
|
||
Services.Logger.Debug($"[OnPayload] Received payload of type {payload.Type}, Int1={payload.Int1}, Int2={payload.Int2}, RefIndex={payload.ReferenceIndex}, Text={payload.Text}");
|
||
var (sourceContainer, sourceSlot) = ResolveSourceFromPayload(payload);
|
||
|
||
if (sourceContainer == 0)
|
||
{
|
||
Services.Logger.Warning($"[OnPayload] Could not resolve source from payload");
|
||
return;
|
||
}
|
||
|
||
InventoryType targetContainer = targetItemInfo.Item.Container;
|
||
ushort targetSlot = (ushort)targetItemInfo.Item.Slot;
|
||
|
||
Services.Logger.Debug($"[OnPayload] Moving {sourceContainer}@{sourceSlot} -> {targetContainer}@{targetSlot}");
|
||
|
||
InventoryMoveHelper.MoveItem(sourceContainer, sourceSlot, targetContainer, targetSlot);
|
||
}
|
||
|
||
private static (InventoryType Container, ushort Slot) ResolveSourceFromPayload(DragDropPayload payload)
|
||
{
|
||
if (payload.Type == DragDropType.Inventory_Item)
|
||
{
|
||
return ((InventoryType)payload.Int1, (ushort)payload.Int2);
|
||
}
|
||
|
||
int containerId = payload.Int1;
|
||
int uiSlot = payload.Int2;
|
||
|
||
InventoryType sourceContainer = InventoryType.GetInventoryTypeFromContainerId(containerId);
|
||
|
||
if (sourceContainer == 0)
|
||
return (0, 0);
|
||
|
||
// Retainers have special handling: UI has 5 tabs × 35 slots, data has 7 pages × 25 slots
|
||
if (sourceContainer. IsRetainer)
|
||
{
|
||
// Container IDs 52-56 = UI tabs 0-4
|
||
int uiTabIndex = containerId - 52;
|
||
|
||
// Convert to global data index
|
||
int globalDataIndex = (uiTabIndex * 35) + uiSlot;
|
||
|
||
// Calculate data page and slot
|
||
int dataPage = globalDataIndex / 25;
|
||
int dataSlot = globalDataIndex % 25;
|
||
|
||
InventoryType dataContainer = InventoryType.RetainerPage1 + (uint)dataPage;
|
||
|
||
// Now resolve through sorter for the actual storage location
|
||
var (realContainer, realSlot) = dataContainer.GetRealItemLocation(dataSlot);
|
||
return (realContainer, realSlot);
|
||
}
|
||
|
||
// For non-retainers, use the standard resolution
|
||
var (realContainerOther, realSlotOther) = sourceContainer.GetRealItemLocation(uiSlot);
|
||
return (realContainerOther, realSlotOther);
|
||
}
|
||
} |