Improves inventory refresh logic and fixes UI glitches
This commit is contained in:
@@ -14,6 +14,14 @@ using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
|
|||||||
|
|
||||||
namespace AetherBags.AddonLifecycles;
|
namespace AetherBags.AddonLifecycles;
|
||||||
|
|
||||||
|
public static unsafe class DragDropState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if the game's drag-drop manager is currently dragging.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsDragging => AtkStage.Instance()->DragDropManager.IsDragging;
|
||||||
|
}
|
||||||
|
|
||||||
public class InventoryLifecycles : IDisposable
|
public class InventoryLifecycles : IDisposable
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -30,12 +38,12 @@ public class InventoryLifecycles : IDisposable
|
|||||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, retainer, OnPreFinalize);
|
Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, retainer, OnPreFinalize);
|
||||||
|
|
||||||
// PreRefresh Handlers
|
// PreRefresh Handlers
|
||||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PreRefresh, ["Inventory", "InventoryLarge", "InventoryExpansion"], InventoryPreRefreshHandler);
|
Services.AddonLifecycle.RegisterListener(AddonEvent.PreRefresh, bags, InventoryPreRefreshHandler);
|
||||||
|
|
||||||
// PostRequestedUpdate
|
// PostRequestedUpdate
|
||||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate);
|
Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate);
|
||||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "InventoryBuddy", OnSaddleBagUpdate);
|
Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "InventoryBuddy", OnSaddleBagUpdate);
|
||||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, ["InventoryRetainer", "InventoryRetainerLarge"], OnRetainerInventoryUpdate);
|
Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, retainer, OnRetainerInventoryUpdate);
|
||||||
|
|
||||||
// PreShow
|
// PreShow
|
||||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PreOpen, "InventoryBuddy", OnSaddleBagOpen);
|
Services.AddonLifecycle.RegisterListener(AddonEvent.PreOpen, "InventoryBuddy", OnSaddleBagOpen);
|
||||||
@@ -181,6 +189,10 @@ public class InventoryLifecycles : IDisposable
|
|||||||
if (IsInUnsafeState())
|
if (IsInUnsafeState())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (DragDropState.IsDragging)
|
||||||
|
return;
|
||||||
|
|
||||||
|
System.LootedItemsTracker.FlushPendingChanges();
|
||||||
System.AddonInventoryWindow?.RefreshFromLifecycle();
|
System.AddonInventoryWindow?.RefreshFromLifecycle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +201,9 @@ public class InventoryLifecycles : IDisposable
|
|||||||
if (IsInUnsafeState())
|
if (IsInUnsafeState())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (DragDropState.IsDragging)
|
||||||
|
return;
|
||||||
|
|
||||||
System.AddonSaddleBagWindow?.RefreshFromLifecycle();
|
System.AddonSaddleBagWindow?.RefreshFromLifecycle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +212,9 @@ public class InventoryLifecycles : IDisposable
|
|||||||
if (IsInUnsafeState())
|
if (IsInUnsafeState())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (DragDropState.IsDragging)
|
||||||
|
return;
|
||||||
|
|
||||||
System.AddonRetainerWindow?.RefreshFromLifecycle();
|
System.AddonRetainerWindow?.RefreshFromLifecycle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,12 +101,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
|||||||
private void OnLootedItemsChanged(IReadOnlyList<LootedItemInfo> lootedItems)
|
private void OnLootedItemsChanged(IReadOnlyList<LootedItemInfo> lootedItems)
|
||||||
{
|
{
|
||||||
if (!IsOpen || !IsSetupComplete) return;
|
if (!IsOpen || !IsSetupComplete) return;
|
||||||
|
UpdateLootedCategory(lootedItems);
|
||||||
Services.Framework.RunOnTick(() =>
|
|
||||||
{
|
|
||||||
if (!IsOpen) return;
|
|
||||||
UpdateLootedCategory(lootedItems);
|
|
||||||
}, delayTicks: 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateLootedCategory(IReadOnlyList<LootedItemInfo> lootedItems)
|
private void UpdateLootedCategory(IReadOnlyList<LootedItemInfo> lootedItems)
|
||||||
@@ -119,6 +114,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
|||||||
{
|
{
|
||||||
CategoriesNode.SetHoistedNode(_lootedCategoryNode);
|
CategoriesNode.SetHoistedNode(_lootedCategoryNode);
|
||||||
}
|
}
|
||||||
|
AutoSizeWindow();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -131,6 +127,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
|||||||
|
|
||||||
CategoriesNode.RemoveNode(_lootedCategoryNode);
|
CategoriesNode.RemoveNode(_lootedCategoryNode);
|
||||||
}
|
}
|
||||||
|
AutoSizeWindow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +155,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
|||||||
}, delayTicks: 3);
|
}, delayTicks: 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected override void OnFinalize(AtkUnitBase* addon)
|
protected override void OnFinalize(AtkUnitBase* addon)
|
||||||
{
|
{
|
||||||
System.LootedItemsTracker.OnLootedItemsChanged -= OnLootedItemsChanged;
|
System.LootedItemsTracker.OnLootedItemsChanged -= OnLootedItemsChanged;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AetherBags.AddonLifecycles;
|
||||||
using AetherBags.Configuration;
|
using AetherBags.Configuration;
|
||||||
using AetherBags.Helpers;
|
using AetherBags.Helpers;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
@@ -169,11 +170,10 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
CategoriesNode.SyncWithListDataByKey<CategorizedInventory, InventoryCategoryNode, uint>(
|
CategoriesNode.SyncWithListDataByKey<CategorizedInventory, InventoryCategoryNode, uint>(
|
||||||
dataList: categories,
|
dataList: categories,
|
||||||
getKeyFromData: categorizedInventory => categorizedInventory.Key,
|
getKeyFromData: categorizedInventory => categorizedInventory.Key,
|
||||||
getKeyFromNode: node => node.Key,
|
getKeyFromNode: node => node.CategorizedInventory.Key,
|
||||||
updateNode: (node, data) =>
|
updateNode: (node, data) =>
|
||||||
{
|
{
|
||||||
node.CategorizedInventory = data;
|
node.SetCategoryData(data, Math.Min(data.Items.Count, maxItemsPerLine));
|
||||||
node.ItemsPerLine = Math.Min(data.Items.Count, maxItemsPerLine);
|
|
||||||
node.RefreshNodeVisuals();
|
node.RefreshNodeVisuals();
|
||||||
},
|
},
|
||||||
createNodeMethod: _ => CreateCategoryNode());
|
createNodeMethod: _ => CreateCategoryNode());
|
||||||
@@ -409,6 +409,9 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
{
|
{
|
||||||
base.OnRequestedUpdate(addon, numberArrayData, stringArrayData);
|
base.OnRequestedUpdate(addon, numberArrayData, stringArrayData);
|
||||||
|
|
||||||
|
if (DragDropState.IsDragging)
|
||||||
|
return;
|
||||||
|
|
||||||
InventoryState.RefreshFromGame();
|
InventoryState.RefreshFromGame();
|
||||||
RefreshCategoriesCore(autosize: true);
|
RefreshCategoriesCore(autosize: true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using AetherBags.Inventory.Items;
|
using AetherBags.Inventory.Items;
|
||||||
using AetherBags.Inventory.Scanning;
|
using AetherBags.Inventory.Scanning;
|
||||||
|
using Dalamud.Game.Inventory;
|
||||||
using Dalamud.Game.Inventory.InventoryEventArgTypes;
|
using Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
|
using Lumina.Excel.Sheets;
|
||||||
|
|
||||||
namespace AetherBags.Inventory;
|
namespace AetherBags.Inventory;
|
||||||
|
|
||||||
@@ -12,20 +15,32 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
|||||||
{
|
{
|
||||||
private static IReadOnlyList<InventoryType> StandardInventories => InventoryScanner.StandardInventories;
|
private static IReadOnlyList<InventoryType> StandardInventories => InventoryScanner.StandardInventories;
|
||||||
|
|
||||||
|
private const int BatchDelayMs = 300;
|
||||||
|
|
||||||
private readonly List<LootedItemInfo> _lootedItems = new(capacity: 64);
|
private readonly List<LootedItemInfo> _lootedItems = new(capacity: 64);
|
||||||
|
private readonly Dictionary<(uint ItemId, bool IsHq), (InventoryItem Item, int Quantity)> _pendingChanges = new(capacity: 32);
|
||||||
|
|
||||||
private bool _isEnabled;
|
private bool _isEnabled;
|
||||||
|
private long _batchStartTick;
|
||||||
|
private bool _hasPendingRemoval;
|
||||||
|
|
||||||
public event Action<IReadOnlyList<LootedItemInfo>>? OnLootedItemsChanged;
|
public event Action<IReadOnlyList<LootedItemInfo>>? OnLootedItemsChanged;
|
||||||
|
|
||||||
public IReadOnlyList<LootedItemInfo> LootedItems => _lootedItems;
|
public IReadOnlyList<LootedItemInfo> LootedItems => _lootedItems;
|
||||||
|
|
||||||
|
public bool HasPendingChanges => _pendingChanges.Count > 0 || _hasPendingRemoval;
|
||||||
|
|
||||||
public void Enable()
|
public void Enable()
|
||||||
{
|
{
|
||||||
if (_isEnabled) return;
|
if (_isEnabled) return;
|
||||||
|
|
||||||
_isEnabled = true;
|
_isEnabled = true;
|
||||||
_lootedItems.Clear();
|
_lootedItems.Clear();
|
||||||
|
_pendingChanges.Clear();
|
||||||
|
_batchStartTick = 0;
|
||||||
|
_hasPendingRemoval = false;
|
||||||
Services.GameInventory.InventoryChangedRaw += OnInventoryChangedRaw;
|
Services.GameInventory.InventoryChangedRaw += OnInventoryChangedRaw;
|
||||||
|
Services.Framework.Update += OnFrameworkUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Disable()
|
public void Disable()
|
||||||
@@ -34,13 +49,17 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
|||||||
|
|
||||||
_isEnabled = false;
|
_isEnabled = false;
|
||||||
Services.GameInventory.InventoryChangedRaw -= OnInventoryChangedRaw;
|
Services.GameInventory.InventoryChangedRaw -= OnInventoryChangedRaw;
|
||||||
|
Services.Framework.Update -= OnFrameworkUpdate;
|
||||||
_lootedItems.Clear();
|
_lootedItems.Clear();
|
||||||
|
_pendingChanges.Clear();
|
||||||
|
_batchStartTick = 0;
|
||||||
|
_hasPendingRemoval = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
_lootedItems.Clear();
|
_lootedItems.Clear();
|
||||||
OnLootedItemsChanged?.Invoke(_lootedItems);
|
_hasPendingRemoval = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveByIndex(int index)
|
public void RemoveByIndex(int index)
|
||||||
@@ -50,12 +69,20 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
|||||||
if (_lootedItems[i].Index == index)
|
if (_lootedItems[i].Index == index)
|
||||||
{
|
{
|
||||||
_lootedItems.RemoveAt(i);
|
_lootedItems.RemoveAt(i);
|
||||||
OnLootedItemsChanged?.Invoke(_lootedItems);
|
_hasPendingRemoval = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void FlushPendingChanges()
|
||||||
|
{
|
||||||
|
if (_pendingChanges.Count == 0 && !_hasPendingRemoval) return;
|
||||||
|
|
||||||
|
_hasPendingRemoval = false;
|
||||||
|
OnLootedItemsChanged?.Invoke(_lootedItems);
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Disable();
|
Disable();
|
||||||
@@ -66,13 +93,18 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
|||||||
if (!_isEnabled) return;
|
if (!_isEnabled) return;
|
||||||
if (!Services.ClientState.IsLoggedIn) return;
|
if (!Services.ClientState.IsLoggedIn) return;
|
||||||
|
|
||||||
bool hasChanges = false;
|
bool anyAdded = false;
|
||||||
|
|
||||||
foreach (var eventData in events)
|
foreach (var eventData in events)
|
||||||
{
|
{
|
||||||
if (!StandardInventories.Contains((InventoryType)eventData.Item.ContainerType)) continue;
|
if (!StandardInventories.Contains((InventoryType)eventData.Item.ContainerType))
|
||||||
|
continue;
|
||||||
|
|
||||||
if (eventData is not (InventoryItemAddedArgs or InventoryItemChangedArgs)) continue;
|
if (eventData.Item.ContainerType == GameInventoryType.DamagedGear)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (eventData is not (InventoryItemAddedArgs or InventoryItemChangedArgs))
|
||||||
|
continue;
|
||||||
|
|
||||||
if (eventData is InventoryItemChangedArgs changedArgs &&
|
if (eventData is InventoryItemChangedArgs changedArgs &&
|
||||||
changedArgs.OldItemState.Quantity >= changedArgs.Item.Quantity)
|
changedArgs.OldItemState.Quantity >= changedArgs.Item.Quantity)
|
||||||
@@ -80,22 +112,71 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var inventoryItem = (InventoryItem*)eventData.Item.Address;
|
if (ShouldFilterItem(eventData.Item.ItemId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var inventoryItem = *(InventoryItem*)eventData.Item.Address;
|
||||||
var changeAmount = eventData is InventoryItemChangedArgs changed
|
var changeAmount = eventData is InventoryItemChangedArgs changed
|
||||||
? changed.Item.Quantity - changed.OldItemState.Quantity
|
? changed.Item.Quantity - changed.OldItemState.Quantity
|
||||||
: eventData.Item.Quantity;
|
: eventData.Item.Quantity;
|
||||||
|
|
||||||
_lootedItems.Add(new LootedItemInfo(
|
var key = (inventoryItem.ItemId, IsHq: inventoryItem.Flags.HasFlag(InventoryItem.ItemFlags.HighQuality));
|
||||||
_lootedItems.Count,
|
|
||||||
*inventoryItem,
|
|
||||||
changeAmount));
|
|
||||||
|
|
||||||
hasChanges = true;
|
if (_pendingChanges.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
_pendingChanges[key] = (inventoryItem, existing.Quantity + changeAmount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_pendingChanges[key] = (inventoryItem, changeAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
anyAdded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasChanges)
|
if (anyAdded && _batchStartTick == 0)
|
||||||
{
|
{
|
||||||
OnLootedItemsChanged?.Invoke(_lootedItems);
|
_batchStartTick = Environment.TickCount64;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnFrameworkUpdate(IFramework framework)
|
||||||
|
{
|
||||||
|
if (_batchStartTick == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Environment.TickCount64 < _batchStartTick + BatchDelayMs)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_batchStartTick = 0;
|
||||||
|
|
||||||
|
if (_pendingChanges.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var ((itemId, isHq), (item, quantity)) in _pendingChanges)
|
||||||
|
{
|
||||||
|
if (quantity <= 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_lootedItems.Add(new LootedItemInfo(
|
||||||
|
_lootedItems.Count,
|
||||||
|
item,
|
||||||
|
quantity));
|
||||||
|
}
|
||||||
|
|
||||||
|
_pendingChanges.Clear();
|
||||||
|
|
||||||
|
OnLootedItemsChanged?.Invoke(_lootedItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldFilterItem(uint itemId)
|
||||||
|
{
|
||||||
|
if (!Services.DataManager.GetExcelSheet<Item>().TryGetRow(itemId, out var item))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (item.ItemUICategory.RowId == 62)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using AetherBags.Helpers;
|
using AetherBags.Helpers;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
using AetherBags.Inventory.Categories;
|
using AetherBags.Inventory.Categories;
|
||||||
@@ -33,6 +36,11 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
|||||||
private float _baseHeaderWidth = 96f;
|
private float _baseHeaderWidth = 96f;
|
||||||
private string _fullHeaderText = string.Empty;
|
private string _fullHeaderText = string.Empty;
|
||||||
|
|
||||||
|
private uint _lastCategoryKey;
|
||||||
|
private int _lastItemCount;
|
||||||
|
private ulong _lastItemsHash;
|
||||||
|
private int _lastItemsPerLine;
|
||||||
|
|
||||||
public event Action<InventoryCategoryNode, bool>? HeaderHoverChanged;
|
public event Action<InventoryCategoryNode, bool>? HeaderHoverChanged;
|
||||||
public Action? OnRefreshRequested { get; set; }
|
public Action? OnRefreshRequested { get; set; }
|
||||||
public Action? OnDragEnd { get; set; }
|
public Action? OnDragEnd { get; set; }
|
||||||
@@ -68,26 +76,68 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
|||||||
_itemGridNode.AttachNode(this);
|
_itemGridNode.AttachNode(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CategorizedInventory _categorizedInventory;
|
||||||
|
|
||||||
public CategorizedInventory CategorizedInventory
|
public CategorizedInventory CategorizedInventory
|
||||||
{
|
{
|
||||||
get;
|
get => _categorizedInventory;
|
||||||
set
|
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
|
if (categoryChanged || itemsChanged || itemsPerLineChanged)
|
||||||
? $"{value.Category.Name} ({value.Items.Count})"
|
{
|
||||||
: value.Category.Name;
|
|
||||||
|
|
||||||
_categoryNameTextNode.String = _fullHeaderText;
|
|
||||||
_categoryNameTextNode.TextColor = value.Category.Color;
|
|
||||||
_categoryNameTextNode.TextTooltip = value.Category.Description;
|
|
||||||
|
|
||||||
UpdateItemGrid();
|
|
||||||
RecalculateSize();
|
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
|
public int ItemsPerLine
|
||||||
{
|
{
|
||||||
get => _itemGridNode.ItemsPerLine;
|
get => _itemGridNode.ItemsPerLine;
|
||||||
@@ -212,10 +262,43 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
|||||||
|
|
||||||
private void UpdateItemGrid()
|
private void UpdateItemGrid()
|
||||||
{
|
{
|
||||||
_itemGridNode.SyncWithListData(
|
_itemGridNode.SyncWithListDataByKey<ItemInfo, InventoryDragDropNode, ulong>(
|
||||||
CategorizedInventory.Items,
|
dataList: CategorizedInventory.Items,
|
||||||
node => node.ItemInfo,
|
getKeyFromData: item => item.Key,
|
||||||
CreateInventoryDragDropNode);
|
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)
|
private unsafe InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
|
||||||
@@ -268,16 +351,31 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
|||||||
|
|
||||||
public void RefreshNodeVisuals()
|
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;
|
var info = itemNode.ItemInfo;
|
||||||
itemNode.AddColor = itemNode.ItemInfo.HighlightOverlayColor;
|
float newAlpha = info.VisualAlpha;
|
||||||
itemNode.IsDraggable = !itemNode.ItemInfo.IsSlotBlocked;
|
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)
|
private unsafe void OnDiscard(DragDropNode node, ItemInfo item)
|
||||||
{
|
{
|
||||||
uint addonId = RaptureAtkUnitManager.Instance()->GetAddonByNode(node)->Id;
|
uint addonId = RaptureAtkUnitManager.Instance()->GetAddonByNode(node)->Id;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Numerics;
|
|
||||||
using AetherBags.Inventory.Items;
|
using AetherBags.Inventory.Items;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
using FFXIVClientStructs.FFXIV.Common.Math;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
using KamiToolKit.Classes;
|
using KamiToolKit.Classes;
|
||||||
using KamiToolKit.Nodes;
|
using KamiToolKit.Nodes;
|
||||||
@@ -11,11 +11,10 @@ namespace AetherBags.Nodes.Inventory;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A display-only item node for looted items. Not draggable, but shows tooltip and can be dismissed.
|
/// A display-only item node for looted items. Not draggable, but shows tooltip and can be dismissed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public unsafe class LootedItemDisplayNode : SimpleComponentNode
|
public sealed unsafe class LootedItemDisplayNode : SimpleComponentNode
|
||||||
{
|
{
|
||||||
private readonly IconNode _iconNode;
|
private readonly IconNode _iconNode;
|
||||||
private readonly TextNode _quantityTextNode;
|
private readonly TextNode _quantityTextNode;
|
||||||
private readonly ResNode _collisionNode;
|
|
||||||
|
|
||||||
public Action<LootedItemDisplayNode>? OnDismiss { get; set; }
|
public Action<LootedItemDisplayNode>? OnDismiss { get; set; }
|
||||||
|
|
||||||
@@ -27,54 +26,42 @@ public unsafe class LootedItemDisplayNode : SimpleComponentNode
|
|||||||
{
|
{
|
||||||
Position = new Vector2(0, 0),
|
Position = new Vector2(0, 0),
|
||||||
Size = new Vector2(42, 46),
|
Size = new Vector2(42, 46),
|
||||||
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled,
|
|
||||||
};
|
};
|
||||||
|
_iconNode.AddEvent(AtkEventType.MouseClick, OnMouseClick);
|
||||||
_iconNode.AttachNode(this);
|
_iconNode.AttachNode(this);
|
||||||
|
|
||||||
_quantityTextNode = new TextNode
|
_quantityTextNode = new TextNode
|
||||||
{
|
{
|
||||||
Size = new Vector2(40.0f, 12.0f),
|
Size = new Vector2(40.0f, 12.0f),
|
||||||
Position = new Vector2(4.0f, 34.0f),
|
Position = new Vector2(4.0f, 34.0f),
|
||||||
NodeFlags = NodeFlags.Enabled | NodeFlags.EmitsEvents,
|
|
||||||
Color = ColorHelper.GetColor(50),
|
Color = ColorHelper.GetColor(50),
|
||||||
TextOutlineColor = ColorHelper.GetColor(51),
|
TextOutlineColor = ColorHelper.GetColor(51),
|
||||||
TextFlags = TextFlags.Edge,
|
TextFlags = TextFlags.Edge,
|
||||||
AlignmentType = AlignmentType.Right,
|
AlignmentType = AlignmentType.Right,
|
||||||
};
|
};
|
||||||
_quantityTextNode.AttachNode(this);
|
_quantityTextNode.AttachNode(this);
|
||||||
|
}
|
||||||
|
|
||||||
_collisionNode = new ResNode
|
public LootedItemInfo LootedItem
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set
|
||||||
{
|
{
|
||||||
Size = new Vector2(42, 46),
|
bool needsCollisionUpdate = field is null && value is not null;
|
||||||
NodeFlags = NodeFlags.Enabled | NodeFlags.EmitsEvents | NodeFlags.HasCollision,
|
field = value;
|
||||||
};
|
var item = value.Item;
|
||||||
_collisionNode.AddEvent(AtkEventType.MouseOver, OnMouseOver);
|
_iconNode.IconId = item.IconId;
|
||||||
_collisionNode.AddEvent(AtkEventType.MouseOut, OnMouseOut);
|
_iconNode.ItemTooltip = item.ItemId;
|
||||||
_collisionNode.AddEvent(AtkEventType.MouseClick, OnMouseClick);
|
_quantityTextNode.String = value.Quantity > 1 ? value.Quantity.ToString() : string.Empty;
|
||||||
_collisionNode.AttachNode(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LootedItemInfo LootedItem { get; private set; } = null!;
|
if (needsCollisionUpdate)
|
||||||
|
{
|
||||||
public void SetLootedItem(LootedItemInfo lootedItem)
|
var addon = RaptureAtkUnitManager.Instance()->GetAddonByNode(this);
|
||||||
{
|
if (addon is not null)
|
||||||
LootedItem = lootedItem;
|
addon->UpdateCollisionNodeList(false);
|
||||||
var item = lootedItem.Item;
|
}
|
||||||
_iconNode.IconId = item.IconId;
|
}
|
||||||
_quantityTextNode.String = lootedItem.Quantity > 1 ? lootedItem.Quantity.ToString() : string.Empty;
|
} = null!;
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMouseClick(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
|
private void OnMouseClick(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using AetherBags.Inventory.Items;
|
using AetherBags.Inventory.Items;
|
||||||
using AetherBags.Nodes.Layout;
|
using AetherBags.Nodes.Layout;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
@@ -27,6 +28,9 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
|||||||
|
|
||||||
private IReadOnlyList<LootedItemInfo> _lootedItems = Array.Empty<LootedItemInfo>();
|
private IReadOnlyList<LootedItemInfo> _lootedItems = Array.Empty<LootedItemInfo>();
|
||||||
|
|
||||||
|
private int _lastItemCount;
|
||||||
|
private long _lastItemsHash;
|
||||||
|
|
||||||
private int _hoverRefs;
|
private int _hoverRefs;
|
||||||
private bool _headerExpanded;
|
private bool _headerExpanded;
|
||||||
private float _baseHeaderWidth = 96f;
|
private float _baseHeaderWidth = 96f;
|
||||||
@@ -95,10 +99,40 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
|||||||
|
|
||||||
public void UpdateLootedItems(IReadOnlyList<LootedItemInfo> lootedItems)
|
public void UpdateLootedItems(IReadOnlyList<LootedItemInfo> lootedItems)
|
||||||
{
|
{
|
||||||
|
long newHash = ComputeItemsHash(lootedItems);
|
||||||
|
bool itemsChanged = lootedItems.Count != _lastItemCount || newHash != _lastItemsHash;
|
||||||
|
|
||||||
|
_lastItemCount = lootedItems.Count;
|
||||||
|
_lastItemsHash = newHash;
|
||||||
_lootedItems = lootedItems;
|
_lootedItems = lootedItems;
|
||||||
|
|
||||||
UpdateHeaderText();
|
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()
|
private void UpdateHeaderText()
|
||||||
@@ -163,20 +197,26 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
|||||||
|
|
||||||
private void SyncItemGrid()
|
private void SyncItemGrid()
|
||||||
{
|
{
|
||||||
_itemGridNode.SyncWithListData(
|
_itemGridNode.SyncWithListDataByKey<LootedItemInfo, LootedItemDisplayNode, int>(
|
||||||
_lootedItems,
|
dataList: _lootedItems,
|
||||||
node => node.LootedItem,
|
getKeyFromData: item => item.Index,
|
||||||
CreateLootedItemNode);
|
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)
|
private LootedItemDisplayNode CreateLootedItemNode(LootedItemInfo lootedItem)
|
||||||
{
|
{
|
||||||
var node = new LootedItemDisplayNode
|
return new LootedItemDisplayNode
|
||||||
{
|
{
|
||||||
OnDismiss = OnItemDismissed,
|
OnDismiss = OnItemDismissed,
|
||||||
|
LootedItem = lootedItem,
|
||||||
};
|
};
|
||||||
node.SetLootedItem(lootedItem);
|
|
||||||
return node;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnItemDismissed(LootedItemDisplayNode node)
|
private void OnItemDismissed(LootedItemDisplayNode node)
|
||||||
|
|||||||
@@ -0,0 +1,502 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
using KamiToolKit;
|
||||||
|
using KamiToolKit.Nodes;
|
||||||
|
|
||||||
|
namespace AetherBags.Nodes.Layout;
|
||||||
|
|
||||||
|
public abstract class DeferrableLayoutListNode : SimpleComponentNode
|
||||||
|
{
|
||||||
|
protected readonly List<NodeBase> NodeList = [];
|
||||||
|
private bool _suppressRecalculateLayout;
|
||||||
|
private int _deferRecalcDepth;
|
||||||
|
private bool _pendingRecalc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hide and detach a node from the UI tree without disposing it.
|
||||||
|
/// Disposal happens later when KamiToolKit cleans up detached nodes.
|
||||||
|
/// </summary>
|
||||||
|
protected static void SafeDetachNode(NodeBase node)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
node.IsVisible = false;
|
||||||
|
node.DetachNode();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Services.Logger.Error(ex, $"[SafeDetachNode] Error detaching {node.GetType().Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<T> GetNodes<T>() where T : NodeBase
|
||||||
|
{
|
||||||
|
for (int i = 0; i < NodeList.Count; i++)
|
||||||
|
{
|
||||||
|
if (NodeList[i] is T t)
|
||||||
|
yield return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<NodeBase> Nodes => NodeList;
|
||||||
|
|
||||||
|
public bool ClipListContents
|
||||||
|
{
|
||||||
|
get => NodeFlags.HasFlag(NodeFlags.Clip);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value)
|
||||||
|
AddFlags(NodeFlags.Clip);
|
||||||
|
else
|
||||||
|
RemoveFlags(NodeFlags.Clip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float ItemSpacing { get; set; }
|
||||||
|
|
||||||
|
public float FirstItemSpacing { get; set; }
|
||||||
|
|
||||||
|
public void RecalculateLayout()
|
||||||
|
{
|
||||||
|
if (_suppressRecalculateLayout) return;
|
||||||
|
|
||||||
|
if (_deferRecalcDepth > 0)
|
||||||
|
{
|
||||||
|
_pendingRecalc = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
InternalRecalculateLayout();
|
||||||
|
|
||||||
|
for (int i = 0; i < NodeList.Count; i++)
|
||||||
|
{
|
||||||
|
if (NodeList[i] is DeferrableLayoutListNode subNode)
|
||||||
|
subNode.RecalculateLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Obsolete]
|
||||||
|
protected virtual void AdjustNode(NodeBase node) { }
|
||||||
|
|
||||||
|
protected abstract void InternalRecalculateLayout();
|
||||||
|
|
||||||
|
public ICollection<NodeBase> InitialNodes
|
||||||
|
{
|
||||||
|
init => AddNode(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddNode(IEnumerable<NodeBase> nodes)
|
||||||
|
{
|
||||||
|
_suppressRecalculateLayout = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var node in nodes)
|
||||||
|
{
|
||||||
|
AddNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_suppressRecalculateLayout = false;
|
||||||
|
}
|
||||||
|
RecalculateLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void AddNode(NodeBase? node)
|
||||||
|
{
|
||||||
|
if (node is null) return;
|
||||||
|
|
||||||
|
NodeList.Add(node);
|
||||||
|
|
||||||
|
node.AttachNode(this);
|
||||||
|
|
||||||
|
RecalculateLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveNode(params NodeBase[] items)
|
||||||
|
{
|
||||||
|
_suppressRecalculateLayout = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var node in items)
|
||||||
|
{
|
||||||
|
RemoveNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_suppressRecalculateLayout = false;
|
||||||
|
}
|
||||||
|
RecalculateLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void RemoveNode(NodeBase node)
|
||||||
|
{
|
||||||
|
if (!NodeList.Contains(node)) return;
|
||||||
|
|
||||||
|
NodeList.Remove(node);
|
||||||
|
SafeDetachNode(node);
|
||||||
|
|
||||||
|
RecalculateLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddDummy(float size = 0.0f)
|
||||||
|
{
|
||||||
|
var dummyNode = new ResNode
|
||||||
|
{
|
||||||
|
Size = new Vector2(size, size),
|
||||||
|
};
|
||||||
|
|
||||||
|
AddNode(dummyNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Clear()
|
||||||
|
{
|
||||||
|
_suppressRecalculateLayout = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int i = NodeList.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var node = NodeList[i];
|
||||||
|
NodeList.RemoveAt(i);
|
||||||
|
SafeDetachNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_suppressRecalculateLayout = false;
|
||||||
|
}
|
||||||
|
RecalculateLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate TU CreateNewNode<in T, out TU>(T data) where TU : NodeBase;
|
||||||
|
|
||||||
|
public delegate T GetDataFromNode<out T, in TU>(TU node) where TU : NodeBase;
|
||||||
|
|
||||||
|
private List<NodeBase>? _existingScratch;
|
||||||
|
private List<NodeBase>? _desiredScratch;
|
||||||
|
private List<NodeBase>? _toRemoveScratch;
|
||||||
|
private HashSet<object>? _dataKeysScratch;
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private List<NodeBase> RentExistingList(int capacity)
|
||||||
|
{
|
||||||
|
var list = _existingScratch ?? new List<NodeBase>(capacity);
|
||||||
|
list.Clear();
|
||||||
|
if (list.Capacity < capacity) list.Capacity = capacity;
|
||||||
|
_existingScratch = null;
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private void ReturnExistingList(List<NodeBase> list)
|
||||||
|
{
|
||||||
|
list.Clear();
|
||||||
|
_existingScratch = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private List<NodeBase> RentDesiredList(int capacity)
|
||||||
|
{
|
||||||
|
var list = _desiredScratch ?? new List<NodeBase>(capacity);
|
||||||
|
list.Clear();
|
||||||
|
if (list.Capacity < capacity) list.Capacity = capacity;
|
||||||
|
_desiredScratch = null;
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private void ReturnDesiredList(List<NodeBase> list)
|
||||||
|
{
|
||||||
|
list.Clear();
|
||||||
|
_desiredScratch = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private List<NodeBase> RentRemoveList(int capacity)
|
||||||
|
{
|
||||||
|
var list = _toRemoveScratch ?? new List<NodeBase>(capacity);
|
||||||
|
list.Clear();
|
||||||
|
if (list.Capacity < capacity) list.Capacity = capacity;
|
||||||
|
_toRemoveScratch = null;
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private void ReturnRemoveList(List<NodeBase> list)
|
||||||
|
{
|
||||||
|
list.Clear();
|
||||||
|
_toRemoveScratch = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private HashSet<object> RentKeySet(int capacity)
|
||||||
|
{
|
||||||
|
var set = _dataKeysScratch ?? new HashSet<object>(capacity);
|
||||||
|
set.Clear();
|
||||||
|
_dataKeysScratch = null;
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private void ReturnKeySet(HashSet<object> set)
|
||||||
|
{
|
||||||
|
set.Clear();
|
||||||
|
_dataKeysScratch = set;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public bool SyncWithListDataByKey<T, TU, TKey>(
|
||||||
|
IReadOnlyList<T> dataList,
|
||||||
|
Func<T, TKey> getKeyFromData,
|
||||||
|
Func<TU, TKey> getKeyFromNode,
|
||||||
|
Action<TU, T> updateNode,
|
||||||
|
CreateNewNode<T, TU> createNodeMethod,
|
||||||
|
IEqualityComparer<TKey>? keyComparer = null) where TU : NodeBase where TKey : notnull
|
||||||
|
{
|
||||||
|
keyComparer ??= EqualityComparer<TKey>.Default;
|
||||||
|
|
||||||
|
int dataCount = dataList.Count;
|
||||||
|
|
||||||
|
var desiredKeys = RentKeySet(dataCount);
|
||||||
|
for (int i = 0; i < dataCount; i++)
|
||||||
|
{
|
||||||
|
desiredKeys.Add(getKeyFromData(dataList[i])!);
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing = RentExistingList(NodeList.Count);
|
||||||
|
var toRemove = RentRemoveList(16);
|
||||||
|
|
||||||
|
for (int i = 0; i < NodeList.Count; i++)
|
||||||
|
{
|
||||||
|
if (NodeList[i] is TU tu)
|
||||||
|
{
|
||||||
|
var key = getKeyFromNode(tu);
|
||||||
|
if (desiredKeys.Contains(key))
|
||||||
|
{
|
||||||
|
existing.Add(tu);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toRemove.Add(tu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool structureChanged = toRemove.Count > 0;
|
||||||
|
|
||||||
|
if (toRemove.Count > 0)
|
||||||
|
{
|
||||||
|
_suppressRecalculateLayout = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int i = 0; i < toRemove.Count; i++)
|
||||||
|
{
|
||||||
|
var node = toRemove[i];
|
||||||
|
NodeList.Remove(node);
|
||||||
|
SafeDetachNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_suppressRecalculateLayout = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<TKey, TU>? byKey = null;
|
||||||
|
if (existing.Count > 0)
|
||||||
|
{
|
||||||
|
byKey = new Dictionary<TKey, TU>(existing.Count, keyComparer);
|
||||||
|
for (int i = 0; i < existing.Count; i++)
|
||||||
|
{
|
||||||
|
var tu = (TU)existing[i];
|
||||||
|
var key = getKeyFromNode(tu);
|
||||||
|
byKey.TryAdd(key, tu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var desired = RentDesiredList(dataCount);
|
||||||
|
|
||||||
|
_suppressRecalculateLayout = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int i = 0; i < dataCount; i++)
|
||||||
|
{
|
||||||
|
var data = dataList[i];
|
||||||
|
var key = getKeyFromData(data);
|
||||||
|
|
||||||
|
if (byKey != null && byKey.TryGetValue(key, out var existingNode))
|
||||||
|
{
|
||||||
|
updateNode(existingNode, data);
|
||||||
|
desired.Add(existingNode);
|
||||||
|
byKey.Remove(key);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var newNode = createNodeMethod(data);
|
||||||
|
NodeList.Add(newNode);
|
||||||
|
newNode.AttachNode(this);
|
||||||
|
updateNode(newNode, data);
|
||||||
|
desired.Add(newNode);
|
||||||
|
structureChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_suppressRecalculateLayout = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool orderChanged = false;
|
||||||
|
if (!structureChanged && desired.Count > 0)
|
||||||
|
{
|
||||||
|
int tuIndex = 0;
|
||||||
|
for (int i = 0; i < NodeList.Count && tuIndex < desired.Count; i++)
|
||||||
|
{
|
||||||
|
if (NodeList[i] is TU)
|
||||||
|
{
|
||||||
|
if (!ReferenceEquals(NodeList[i], desired[tuIndex]))
|
||||||
|
{
|
||||||
|
orderChanged = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tuIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tuIndex != desired.Count)
|
||||||
|
orderChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structureChanged || orderChanged)
|
||||||
|
{
|
||||||
|
int insertIndex = -1;
|
||||||
|
for (int i = 0; i < NodeList.Count; i++)
|
||||||
|
{
|
||||||
|
if (NodeList[i] is TU)
|
||||||
|
{
|
||||||
|
insertIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insertIndex < 0)
|
||||||
|
insertIndex = NodeList.Count;
|
||||||
|
|
||||||
|
for (int i = NodeList.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (NodeList[i] is TU)
|
||||||
|
NodeList.RemoveAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insertIndex > NodeList.Count)
|
||||||
|
insertIndex = NodeList.Count;
|
||||||
|
|
||||||
|
NodeList.InsertRange(insertIndex, desired);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReturnKeySet(desiredKeys);
|
||||||
|
ReturnExistingList(existing);
|
||||||
|
ReturnRemoveList(toRemove);
|
||||||
|
ReturnDesiredList(desired);
|
||||||
|
|
||||||
|
if (structureChanged || orderChanged)
|
||||||
|
{
|
||||||
|
RecalculateLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
return structureChanged || orderChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SyncWithListData<T, TU>(
|
||||||
|
IEnumerable<T> dataList,
|
||||||
|
GetDataFromNode<T?, TU> getDataFromNode,
|
||||||
|
CreateNewNode<T, TU> createNodeMethod) where TU : NodeBase
|
||||||
|
{
|
||||||
|
_suppressRecalculateLayout = true;
|
||||||
|
var anythingChanged = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var existing = RentExistingList(NodeList.Count);
|
||||||
|
for (int i = 0; i < NodeList.Count; i++)
|
||||||
|
{
|
||||||
|
if (NodeList[i] is TU tu)
|
||||||
|
existing.Add(tu);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dataSet = new HashSet<T>(EqualityComparer<T>.Default);
|
||||||
|
foreach (var d in dataList)
|
||||||
|
dataSet.Add(d);
|
||||||
|
|
||||||
|
var represented = new HashSet<T>(EqualityComparer<T>.Default);
|
||||||
|
|
||||||
|
for (int i = 0; i < existing.Count; i++)
|
||||||
|
{
|
||||||
|
var tu = (TU)existing[i];
|
||||||
|
var nodeData = getDataFromNode(tu);
|
||||||
|
|
||||||
|
if (nodeData is null || !dataSet.Contains(nodeData))
|
||||||
|
{
|
||||||
|
NodeList.Remove(tu);
|
||||||
|
SafeDetachNode(tu);
|
||||||
|
anythingChanged = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
represented.Add(nodeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var data in dataSet)
|
||||||
|
{
|
||||||
|
if (represented.Contains(data))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var newNode = createNodeMethod(data);
|
||||||
|
NodeList.Add(newNode);
|
||||||
|
newNode.AttachNode(this);
|
||||||
|
anythingChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReturnExistingList(existing);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_suppressRecalculateLayout = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anythingChanged)
|
||||||
|
RecalculateLayout();
|
||||||
|
|
||||||
|
return anythingChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReorderNodes(Comparison<NodeBase> comparison)
|
||||||
|
{
|
||||||
|
NodeList.Sort(comparison);
|
||||||
|
RecalculateLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable DeferRecalculateLayout()
|
||||||
|
{
|
||||||
|
_deferRecalcDepth++;
|
||||||
|
return new RecalcDeferToken(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private void EndDefer()
|
||||||
|
{
|
||||||
|
_deferRecalcDepth--;
|
||||||
|
if (_deferRecalcDepth == 0 && _pendingRecalc)
|
||||||
|
{
|
||||||
|
_pendingRecalc = false;
|
||||||
|
RecalculateLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly struct RecalcDeferToken(DeferrableLayoutListNode owner) : IDisposable
|
||||||
|
{
|
||||||
|
public void Dispose() => owner.EndDefer();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
using KamiToolKit;
|
using KamiToolKit;
|
||||||
using KamiToolKit.Nodes;
|
|
||||||
|
|
||||||
namespace AetherBags.Nodes.Layout;
|
namespace AetherBags.Nodes.Layout;
|
||||||
|
|
||||||
public class HybridDirectionalFlexNode : HybridDirectionalFlexNode<NodeBase> { }
|
public class HybridDirectionalFlexNode : HybridDirectionalFlexNode<NodeBase> { }
|
||||||
|
|
||||||
public class HybridDirectionalFlexNode<T> : LayoutListNode where T : NodeBase
|
public class HybridDirectionalFlexNode<T> : DeferrableLayoutListNode where T : NodeBase
|
||||||
{
|
{
|
||||||
public FlexGrowDirection GrowDirection
|
public FlexGrowDirection GrowDirection
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
using KamiToolKit;
|
using KamiToolKit;
|
||||||
using KamiToolKit.Nodes;
|
|
||||||
|
|
||||||
namespace AetherBags.Nodes.Layout;
|
namespace AetherBags.Nodes.Layout;
|
||||||
|
|
||||||
public class HybridDirectionalStackNode<T> : LayoutListNode where T : NodeBase
|
public class HybridDirectionalStackNode<T> : DeferrableLayoutListNode where T : NodeBase
|
||||||
{
|
{
|
||||||
public FlexGrowDirection GrowDirection
|
public FlexGrowDirection GrowDirection
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using KamiToolKit;
|
using KamiToolKit;
|
||||||
using KamiToolKit.Nodes;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -7,7 +6,7 @@ using System.Runtime.CompilerServices;
|
|||||||
|
|
||||||
namespace AetherBags.Nodes.Layout;
|
namespace AetherBags.Nodes.Layout;
|
||||||
|
|
||||||
public sealed class WrappingGridNode<T> : LayoutListNode where T : NodeBase
|
public sealed class WrappingGridNode<T> : DeferrableLayoutListNode where T : NodeBase
|
||||||
{
|
{
|
||||||
public float HorizontalSpacing { get; set; } = 10f;
|
public float HorizontalSpacing { get; set; } = 10f;
|
||||||
public float VerticalSpacing { get; set; } = 10f;
|
public float VerticalSpacing { get; set; } = 10f;
|
||||||
@@ -36,9 +35,6 @@ public sealed class WrappingGridNode<T> : LayoutListNode where T : NodeBase
|
|||||||
private bool _lastuseStableInsert;
|
private bool _lastuseStableInsert;
|
||||||
private int _lastCompactLookahead;
|
private int _lastCompactLookahead;
|
||||||
|
|
||||||
private int _deferRecalcDepth;
|
|
||||||
private bool _pendingRecalc;
|
|
||||||
|
|
||||||
private int[] _orderScratch = Array.Empty<int>();
|
private int[] _orderScratch = Array.Empty<int>();
|
||||||
|
|
||||||
private T? _hoistedNode;
|
private T? _hoistedNode;
|
||||||
@@ -80,7 +76,7 @@ public sealed class WrappingGridNode<T> : LayoutListNode where T : NodeBase
|
|||||||
{
|
{
|
||||||
if (_pinned.Add(node))
|
if (_pinned.Add(node))
|
||||||
{
|
{
|
||||||
RequestRecalculateLayout();
|
RecalculateLayout();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -90,7 +86,7 @@ public sealed class WrappingGridNode<T> : LayoutListNode where T : NodeBase
|
|||||||
{
|
{
|
||||||
if (_pinned.Remove(node))
|
if (_pinned.Remove(node))
|
||||||
{
|
{
|
||||||
RequestRecalculateLayout();
|
RecalculateLayout();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -100,7 +96,7 @@ public sealed class WrappingGridNode<T> : LayoutListNode where T : NodeBase
|
|||||||
{
|
{
|
||||||
if (_pinned.Count == 0) return;
|
if (_pinned.Count == 0) return;
|
||||||
_pinned.Clear();
|
_pinned.Clear();
|
||||||
RequestRecalculateLayout();
|
RecalculateLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsPinned(T node) => _pinned.Contains(node);
|
public bool IsPinned(T node) => _pinned.Contains(node);
|
||||||
@@ -1013,34 +1009,6 @@ public sealed class WrappingGridNode<T> : LayoutListNode where T : NodeBase
|
|||||||
_orderScratch = new int[newSize];
|
_orderScratch = new int[newSize];
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDisposable DeferRecalculateLayout()
|
|
||||||
{
|
|
||||||
_deferRecalcDepth++;
|
|
||||||
return new RecalcDeferToken<T>(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private void RequestRecalculateLayout()
|
|
||||||
{
|
|
||||||
if (_deferRecalcDepth > 0)
|
|
||||||
{
|
|
||||||
_pendingRecalc = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
RecalculateLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EndDefer()
|
|
||||||
{
|
|
||||||
_deferRecalcDepth--;
|
|
||||||
if (_deferRecalcDepth == 0 && _pendingRecalc)
|
|
||||||
{
|
|
||||||
_pendingRecalc = false;
|
|
||||||
RecalculateLayout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class RowsReadOnlyView : IReadOnlyList<IReadOnlyList<NodeBase>>
|
private sealed class RowsReadOnlyView : IReadOnlyList<IReadOnlyList<NodeBase>>
|
||||||
{
|
{
|
||||||
private readonly List<List<NodeBase>> _rows;
|
private readonly List<List<NodeBase>> _rows;
|
||||||
@@ -1068,11 +1036,4 @@ public sealed class WrappingGridNode<T> : LayoutListNode where T : NodeBase
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public int GetHashCode(TRef obj) => RuntimeHelpers.GetHashCode(obj);
|
public int GetHashCode(TRef obj) => RuntimeHelpers.GetHashCode(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private readonly struct RecalcDeferToken<TRef>(WrappingGridNode<TRef> owner) : IDisposable
|
|
||||||
where TRef : NodeBase
|
|
||||||
{
|
|
||||||
public void Dispose() => owner.EndDefer();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user