Improves inventory refresh logic and fixes UI glitches
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using AetherBags.Helpers;
|
||||
using AetherBags.Inventory;
|
||||
using AetherBags.Inventory.Categories;
|
||||
@@ -33,6 +36,11 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
||||
private float _baseHeaderWidth = 96f;
|
||||
private string _fullHeaderText = string.Empty;
|
||||
|
||||
private uint _lastCategoryKey;
|
||||
private int _lastItemCount;
|
||||
private ulong _lastItemsHash;
|
||||
private int _lastItemsPerLine;
|
||||
|
||||
public event Action<InventoryCategoryNode, bool>? HeaderHoverChanged;
|
||||
public Action? OnRefreshRequested { get; set; }
|
||||
public Action? OnDragEnd { get; set; }
|
||||
@@ -68,26 +76,68 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
||||
_itemGridNode.AttachNode(this);
|
||||
}
|
||||
|
||||
private CategorizedInventory _categorizedInventory;
|
||||
|
||||
public CategorizedInventory CategorizedInventory
|
||||
{
|
||||
get;
|
||||
set
|
||||
get => _categorizedInventory;
|
||||
set => SetCategoryData(value, _itemGridNode.ItemsPerLine);
|
||||
}
|
||||
|
||||
public void SetCategoryData(CategorizedInventory data, int itemsPerLine)
|
||||
{
|
||||
bool categoryChanged = data.Key != _lastCategoryKey;
|
||||
bool itemsPerLineChanged = itemsPerLine != _lastItemsPerLine;
|
||||
|
||||
ulong itemsHash = ComputeItemsHash(CollectionsMarshal.AsSpan(data.Items));
|
||||
bool itemsChanged = data.Items.Count != _lastItemCount || itemsHash != _lastItemsHash;
|
||||
|
||||
_lastCategoryKey = data.Key;
|
||||
_lastItemCount = data.Items.Count;
|
||||
_lastItemsHash = itemsHash;
|
||||
_lastItemsPerLine = itemsPerLine;
|
||||
|
||||
_categorizedInventory = data;
|
||||
|
||||
_fullHeaderText = System.Config.General.ShowCategoryItemCount
|
||||
? $"{data.Category.Name} ({data.Items.Count})"
|
||||
: data.Category.Name;
|
||||
|
||||
_categoryNameTextNode.String = _fullHeaderText;
|
||||
_categoryNameTextNode.TextColor = data.Category.Color;
|
||||
_categoryNameTextNode.TextTooltip = data.Category.Description;
|
||||
|
||||
if (itemsChanged || categoryChanged)
|
||||
{
|
||||
field = value;
|
||||
using (_itemGridNode.DeferRecalculateLayout())
|
||||
{
|
||||
_itemGridNode.ItemsPerLine = itemsPerLine;
|
||||
UpdateItemGrid();
|
||||
}
|
||||
}
|
||||
else if (itemsPerLineChanged)
|
||||
{
|
||||
_itemGridNode.ItemsPerLine = itemsPerLine;
|
||||
}
|
||||
|
||||
_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();
|
||||
if (categoryChanged || itemsChanged || itemsPerLineChanged)
|
||||
{
|
||||
RecalculateSize();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong ComputeItemsHash(ReadOnlySpan<ItemInfo> items)
|
||||
{
|
||||
ulong hash = 14695981039346656037UL; // FNV-1a offset basis
|
||||
foreach (var item in items)
|
||||
{
|
||||
hash ^= item.Key;
|
||||
hash *= 1099511628211UL; // FNV-1a prime
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
public int ItemsPerLine
|
||||
{
|
||||
get => _itemGridNode.ItemsPerLine;
|
||||
@@ -212,10 +262,43 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
||||
|
||||
private void UpdateItemGrid()
|
||||
{
|
||||
_itemGridNode.SyncWithListData(
|
||||
CategorizedInventory.Items,
|
||||
node => node.ItemInfo,
|
||||
CreateInventoryDragDropNode);
|
||||
_itemGridNode.SyncWithListDataByKey<ItemInfo, InventoryDragDropNode, ulong>(
|
||||
dataList: CategorizedInventory.Items,
|
||||
getKeyFromData: item => item.Key,
|
||||
getKeyFromNode: node => node.ItemInfo?.Key ?? 0,
|
||||
updateNode: UpdateInventoryDragDropNode,
|
||||
createNodeMethod: CreateInventoryDragDropNode);
|
||||
}
|
||||
|
||||
private void UpdateInventoryDragDropNode(InventoryDragDropNode node, ItemInfo data)
|
||||
{
|
||||
if (node.ItemInfo?.Key == data.Key)
|
||||
{
|
||||
node.ItemInfo = data;
|
||||
node.Alpha = data.VisualAlpha;
|
||||
node.AddColor = data.HighlightOverlayColor;
|
||||
node.IsDraggable = !data.IsSlotBlocked;
|
||||
return;
|
||||
}
|
||||
|
||||
InventoryItem item = data.Item;
|
||||
InventoryMappedLocation visualLocation = data.VisualLocation;
|
||||
|
||||
var visualInvType = InventoryType.GetInventoryTypeFromContainerId(visualLocation.Container);
|
||||
int absoluteIndex = visualInvType.GetInventoryStartIndex + visualLocation.Slot;
|
||||
|
||||
node.ItemInfo = data;
|
||||
node.IconId = item.IconId;
|
||||
node.Alpha = data.VisualAlpha;
|
||||
node.AddColor = data.HighlightOverlayColor;
|
||||
node.IsDraggable = !data.IsSlotBlocked;
|
||||
node.Payload = new DragDropPayload
|
||||
{
|
||||
Type = DragDropType.Item,
|
||||
Int1 = visualLocation.Container,
|
||||
Int2 = visualLocation.Slot,
|
||||
ReferenceIndex = (short)absoluteIndex
|
||||
};
|
||||
}
|
||||
|
||||
private unsafe InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
|
||||
@@ -268,16 +351,31 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
||||
|
||||
public void RefreshNodeVisuals()
|
||||
{
|
||||
foreach (var node in _itemGridNode.Nodes)
|
||||
var nodes = _itemGridNode.Nodes;
|
||||
for (int i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
if (node is not InventoryDragDropNode itemNode || itemNode.ItemInfo == null) continue;
|
||||
if (nodes[i] is not InventoryDragDropNode itemNode || itemNode.ItemInfo == null)
|
||||
continue;
|
||||
|
||||
itemNode.Alpha = itemNode.ItemInfo.VisualAlpha;
|
||||
itemNode.AddColor = itemNode.ItemInfo.HighlightOverlayColor;
|
||||
itemNode.IsDraggable = !itemNode.ItemInfo.IsSlotBlocked;
|
||||
var info = itemNode.ItemInfo;
|
||||
float newAlpha = info.VisualAlpha;
|
||||
Vector3 newColor = info.HighlightOverlayColor;
|
||||
bool newDraggable = !info.IsSlotBlocked;
|
||||
|
||||
if (!NearlyEqual(itemNode.Alpha, newAlpha))
|
||||
itemNode.Alpha = newAlpha;
|
||||
|
||||
if (itemNode.AddColor != newColor)
|
||||
itemNode.AddColor = newColor;
|
||||
|
||||
if (itemNode.IsDraggable != newDraggable)
|
||||
itemNode.IsDraggable = newDraggable;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool NearlyEqual(float a, float b) => MathF.Abs(a - b) < 0.001f;
|
||||
|
||||
private unsafe void OnDiscard(DragDropNode node, ItemInfo item)
|
||||
{
|
||||
uint addonId = RaptureAtkUnitManager.Instance()->GetAddonByNode(node)->Id;
|
||||
@@ -325,4 +423,4 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
||||
Services.Logger.Error(ex, "[OnPayload] Error handling payload acceptance");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using AetherBags.Inventory.Items;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Common.Math;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Nodes;
|
||||
@@ -11,11 +11,10 @@ namespace AetherBags.Nodes.Inventory;
|
||||
/// <summary>
|
||||
/// A display-only item node for looted items. Not draggable, but shows tooltip and can be dismissed.
|
||||
/// </summary>
|
||||
public unsafe class LootedItemDisplayNode : SimpleComponentNode
|
||||
public sealed unsafe class LootedItemDisplayNode : SimpleComponentNode
|
||||
{
|
||||
private readonly IconNode _iconNode;
|
||||
private readonly TextNode _quantityTextNode;
|
||||
private readonly ResNode _collisionNode;
|
||||
|
||||
public Action<LootedItemDisplayNode>? OnDismiss { get; set; }
|
||||
|
||||
@@ -27,54 +26,42 @@ public unsafe class LootedItemDisplayNode : SimpleComponentNode
|
||||
{
|
||||
Position = new Vector2(0, 0),
|
||||
Size = new Vector2(42, 46),
|
||||
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled,
|
||||
};
|
||||
_iconNode.AddEvent(AtkEventType.MouseClick, OnMouseClick);
|
||||
_iconNode.AttachNode(this);
|
||||
|
||||
_quantityTextNode = new TextNode
|
||||
{
|
||||
Size = new Vector2(40.0f, 12.0f),
|
||||
Position = new Vector2(4.0f, 34.0f),
|
||||
NodeFlags = NodeFlags.Enabled | NodeFlags.EmitsEvents,
|
||||
Color = ColorHelper.GetColor(50),
|
||||
TextOutlineColor = ColorHelper.GetColor(51),
|
||||
TextFlags = TextFlags.Edge,
|
||||
AlignmentType = AlignmentType.Right,
|
||||
};
|
||||
_quantityTextNode.AttachNode(this);
|
||||
}
|
||||
|
||||
_collisionNode = new ResNode
|
||||
public LootedItemInfo LootedItem
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
Size = new Vector2(42, 46),
|
||||
NodeFlags = NodeFlags.Enabled | NodeFlags.EmitsEvents | NodeFlags.HasCollision,
|
||||
};
|
||||
_collisionNode.AddEvent(AtkEventType.MouseOver, OnMouseOver);
|
||||
_collisionNode.AddEvent(AtkEventType.MouseOut, OnMouseOut);
|
||||
_collisionNode.AddEvent(AtkEventType.MouseClick, OnMouseClick);
|
||||
_collisionNode.AttachNode(this);
|
||||
}
|
||||
bool needsCollisionUpdate = field is null && value is not null;
|
||||
field = value;
|
||||
var item = value.Item;
|
||||
_iconNode.IconId = item.IconId;
|
||||
_iconNode.ItemTooltip = item.ItemId;
|
||||
_quantityTextNode.String = value.Quantity > 1 ? value.Quantity.ToString() : string.Empty;
|
||||
|
||||
public LootedItemInfo LootedItem { get; private set; } = null!;
|
||||
|
||||
public void SetLootedItem(LootedItemInfo lootedItem)
|
||||
{
|
||||
LootedItem = lootedItem;
|
||||
var item = lootedItem.Item;
|
||||
_iconNode.IconId = item.IconId;
|
||||
_quantityTextNode.String = lootedItem.Quantity > 1 ? lootedItem.Quantity.ToString() : string.Empty;
|
||||
}
|
||||
|
||||
private void OnMouseOver(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
|
||||
{
|
||||
var item = LootedItem.Item;
|
||||
_collisionNode.ShowInventoryItemTooltip(item.Container, item.Slot);
|
||||
}
|
||||
|
||||
private void OnMouseOut(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
|
||||
{
|
||||
ushort addonId = RaptureAtkUnitManager.Instance()->GetAddonByNode(_collisionNode)->Id;
|
||||
AtkStage.Instance()->TooltipManager.HideTooltip(addonId);
|
||||
}
|
||||
if (needsCollisionUpdate)
|
||||
{
|
||||
var addon = RaptureAtkUnitManager.Instance()->GetAddonByNode(this);
|
||||
if (addon is not null)
|
||||
addon->UpdateCollisionNodeList(false);
|
||||
}
|
||||
}
|
||||
} = null!;
|
||||
|
||||
private void OnMouseClick(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using AetherBags.Inventory.Items;
|
||||
using AetherBags.Nodes.Layout;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
@@ -27,6 +28,9 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
||||
|
||||
private IReadOnlyList<LootedItemInfo> _lootedItems = Array.Empty<LootedItemInfo>();
|
||||
|
||||
private int _lastItemCount;
|
||||
private long _lastItemsHash;
|
||||
|
||||
private int _hoverRefs;
|
||||
private bool _headerExpanded;
|
||||
private float _baseHeaderWidth = 96f;
|
||||
@@ -95,10 +99,40 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
||||
|
||||
public void UpdateLootedItems(IReadOnlyList<LootedItemInfo> lootedItems)
|
||||
{
|
||||
long newHash = ComputeItemsHash(lootedItems);
|
||||
bool itemsChanged = lootedItems.Count != _lastItemCount || newHash != _lastItemsHash;
|
||||
|
||||
_lastItemCount = lootedItems.Count;
|
||||
_lastItemsHash = newHash;
|
||||
_lootedItems = lootedItems;
|
||||
|
||||
UpdateHeaderText();
|
||||
SyncItemGrid();
|
||||
RecalculateSize();
|
||||
|
||||
if (itemsChanged)
|
||||
{
|
||||
using (_itemGridNode.DeferRecalculateLayout())
|
||||
{
|
||||
SyncItemGrid();
|
||||
}
|
||||
RecalculateSize();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static long ComputeItemsHash(IReadOnlyList<LootedItemInfo> items)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
long hash = unchecked((long)14695981039346656037UL);
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
hash ^= items[i].Index;
|
||||
hash *= 1099511628211L;
|
||||
hash ^= items[i].Item.ItemId;
|
||||
hash *= 1099511628211L;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateHeaderText()
|
||||
@@ -163,20 +197,26 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
||||
|
||||
private void SyncItemGrid()
|
||||
{
|
||||
_itemGridNode.SyncWithListData(
|
||||
_lootedItems,
|
||||
node => node.LootedItem,
|
||||
CreateLootedItemNode);
|
||||
_itemGridNode.SyncWithListDataByKey<LootedItemInfo, LootedItemDisplayNode, int>(
|
||||
dataList: _lootedItems,
|
||||
getKeyFromData: item => item.Index,
|
||||
getKeyFromNode: node => node.LootedItem?.Index ?? -1,
|
||||
updateNode: UpdateLootedItemNode,
|
||||
createNodeMethod: CreateLootedItemNode);
|
||||
}
|
||||
|
||||
private static void UpdateLootedItemNode(LootedItemDisplayNode node, LootedItemInfo data)
|
||||
{
|
||||
node.LootedItem = data;
|
||||
}
|
||||
|
||||
private LootedItemDisplayNode CreateLootedItemNode(LootedItemInfo lootedItem)
|
||||
{
|
||||
var node = new LootedItemDisplayNode
|
||||
return new LootedItemDisplayNode
|
||||
{
|
||||
OnDismiss = OnItemDismissed,
|
||||
LootedItem = lootedItem,
|
||||
};
|
||||
node.SetLootedItem(lootedItem);
|
||||
return node;
|
||||
}
|
||||
|
||||
private void OnItemDismissed(LootedItemDisplayNode node)
|
||||
|
||||
Reference in New Issue
Block a user