Add dragging between windows on background

This commit is contained in:
Zeffuro
2025-12-31 21:45:41 +01:00
parent b6084fb7e5
commit 0ba5c0698e
11 changed files with 144 additions and 23 deletions
@@ -22,6 +22,8 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
protected override void OnSetup(AtkUnitBase* addon) protected override void OnSetup(AtkUnitBase* addon)
{ {
InitializeBackgroundDropTarget();
CategoriesNode = new WrappingGridNode<InventoryCategoryNode> CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
{ {
Position = ContentStartPosition, Position = ContentStartPosition,
+2
View File
@@ -33,6 +33,8 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
protected override void OnSetup(AtkUnitBase* addon) protected override void OnSetup(AtkUnitBase* addon)
{ {
InitializeBackgroundDropTarget();
WindowNode?.AddColor = _tintColor; WindowNode?.AddColor = _tintColor;
CategoriesNode = new WrappingGridNode<InventoryCategoryNode> CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
@@ -26,6 +26,8 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase
protected override void OnSetup(AtkUnitBase* addon) protected override void OnSetup(AtkUnitBase* addon)
{ {
InitializeBackgroundDropTarget();
WindowNode?.AddColor = _tintColor; WindowNode?.AddColor = _tintColor;
CategoriesNode = new WrappingGridNode<InventoryCategoryNode> CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
+64
View File
@@ -1,14 +1,19 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using AetherBags.Helpers;
using AetherBags.Inventory; using AetherBags.Inventory;
using AetherBags.Inventory.Categories; using AetherBags.Inventory.Categories;
using AetherBags.Inventory.Context;
using AetherBags.Inventory.Scanning;
using AetherBags.Inventory.State; using AetherBags.Inventory.State;
using AetherBags.Nodes.Input; using AetherBags.Nodes.Input;
using AetherBags.Nodes.Inventory; using AetherBags.Nodes.Inventory;
using AetherBags.Nodes.Layout; using AetherBags.Nodes.Layout;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit; using KamiToolKit;
using KamiToolKit.Classes;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
namespace AetherBags.Addons; namespace AetherBags.Addons;
@@ -19,6 +24,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon
protected readonly InventoryCategoryPinCoordinator PinCoordinator = new(); protected readonly InventoryCategoryPinCoordinator PinCoordinator = new();
protected readonly HashSet<InventoryCategoryNode> HoverSubscribed = new(); protected readonly HashSet<InventoryCategoryNode> HoverSubscribed = new();
protected DragDropNode BackgroundDropTarget = null!;
protected WrappingGridNode<InventoryCategoryNode> CategoriesNode = null!; protected WrappingGridNode<InventoryCategoryNode> CategoriesNode = null!;
protected TextInputWithHintNode SearchInputNode = null!; protected TextInputWithHintNode SearchInputNode = null!;
protected InventoryFooterNode FooterNode = null!; protected InventoryFooterNode FooterNode = null!;
@@ -129,6 +135,26 @@ public abstract unsafe class InventoryAddonBase : NativeAddon
} }
} }
protected void InitializeBackgroundDropTarget()
{
BackgroundDropTarget = new DragDropNode
{
Position = ContentStartPosition,
Size = ContentSize,
IconId = 0,
IsDraggable = false,
IsClickable = false,
AcceptedType = DragDropType.Item,
};
BackgroundDropTarget.DragDropBackgroundNode.IsVisible = false;
BackgroundDropTarget.IconNode.IsVisible = false;
BackgroundDropTarget.OnPayloadAccepted = OnBackgroundPayloadAccepted;
BackgroundDropTarget.AttachNode(this);
}
protected virtual InventoryCategoryNode CreateCategoryNode() protected virtual InventoryCategoryNode CreateCategoryNode()
{ {
return new InventoryCategoryNode return new InventoryCategoryNode
@@ -139,6 +165,38 @@ public abstract unsafe class InventoryAddonBase : NativeAddon
}; };
} }
private void OnBackgroundPayloadAccepted(DragDropNode node, DragDropPayload acceptedPayload)
{
if (!acceptedPayload.IsValidInventoryPayload) return;
InventoryLocation emptyLocation = InventoryScanner.GetFirstEmptySlot(InventoryState.SourceType);
if (!emptyLocation.IsValid)
{
Services.Logger.Error("No empty slots available to receive drop.");
return;
}
InventoryMappedLocation visualLocation = InventoryContextState.GetVisualLocation(emptyLocation.Container, emptyLocation.Slot);
var visualInvType = InventoryType.GetInventoryTypeFromContainerId(visualLocation.Container);
int absoluteIndex = visualInvType.GetInventoryStartIndex + visualLocation.Slot;
var targetPayload = new DragDropPayload
{
Type = DragDropType.Item,
Int1 = visualLocation.Container,
Int2 = visualLocation.Slot,
ReferenceIndex = (short)absoluteIndex
};
Services.Logger.Debug($"[BackgroundDrop] Target: {emptyLocation} -> Visual: {visualLocation} (Ref: {absoluteIndex})");
InventoryMoveHelper.HandleItemMovePayload(acceptedPayload, targetPayload);
ManualRefresh();
}
protected void WireHoverHandlers() protected void WireHoverHandlers()
{ {
var nodes = CategoriesNode.Nodes; var nodes = CategoriesNode.Nodes;
@@ -233,6 +291,12 @@ public abstract unsafe class InventoryAddonBase : NativeAddon
protected void ResizeWindow(float width, float height, bool recalcLayout) protected void ResizeWindow(float width, float height, bool recalcLayout)
{ {
SetWindowSize(width, height); SetWindowSize(width, height);
if (BackgroundDropTarget != null)
{
BackgroundDropTarget.Size = ContentSize;
}
LayoutContent(); LayoutContent();
if (recalcLayout) if (recalcLayout)
@@ -10,12 +10,16 @@ public static unsafe class InventoryContextState
{ {
private static readonly HashSet<(int page, int slot)> EligibleSlots = new(); private static readonly HashSet<(int page, int slot)> EligibleSlots = new();
private static readonly HashSet<(InventoryType container, int slot)> BlockedSlots = new(); private static readonly HashSet<(InventoryType container, int slot)> BlockedSlots = new();
// map from real (containerId, slot) -> visual (containerId, slot)
private static readonly Dictionary<InventoryMappedLocation, InventoryMappedLocation> VisualLocationMap = new(); private static readonly Dictionary<InventoryMappedLocation, InventoryMappedLocation> VisualLocationMap = new();
private static readonly Dictionary<int, Dictionary<InventoryMappedLocation, InventoryMappedLocation>> GroupedLocationMaps = new(); private static readonly Dictionary<int, Dictionary<InventoryMappedLocation, InventoryMappedLocation>> GroupedLocationMaps = new();
private static uint _lastContextId; private static uint _lastContextId;
public static uint ActiveContextId => _lastContextId;
public static bool HasActiveContext => _lastContextId != 0;
public static void RefreshMaps() public static void RefreshMaps()
{ {
EligibleSlots.Clear(); EligibleSlots.Clear();
@@ -134,9 +138,6 @@ public static unsafe class InventoryContextState
public static bool IsSlotBlocked(InventoryType container, int slot) public static bool IsSlotBlocked(InventoryType container, int slot)
=> BlockedSlots.Contains((container, slot)); => BlockedSlots.Contains((container, slot));
public static bool HasActiveContext
=> _lastContextId != 0;
public static InventoryMappedLocation GetVisualLocation(InventoryType realContainer, int slot) public static InventoryMappedLocation GetVisualLocation(InventoryType realContainer, int slot)
{ {
var key = new InventoryMappedLocation((int)realContainer, slot); var key = new InventoryMappedLocation((int)realContainer, slot);
+27 -5
View File
@@ -82,13 +82,35 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
{ {
get get
{ {
if (!InventoryContextState.HasActiveContext) uint contextId = InventoryContextState.ActiveContextId;
return true; if (contextId == 0) return true;
if (!IsMainInventory) bool isRetainerContext = contextId == 4;
return true; bool isSaddlebagContext = contextId == 29;
bool isMainContext = !isRetainerContext && isSaddlebagContext == false;
return InventoryContextState.IsEligible(InventoryPage, Item.Slot); if (IsMainInventory)
{
if (!isMainContext) return true;
return InventoryContextState.IsEligible(InventoryPage, Item.Slot);
}
if (Item.Container.IsRetainer)
{
// ...but the context isn't for Retainers, don't dim it.
if (!isRetainerContext)
return true;
}
// 3. If we are looking at a Saddlebag item...
if (Item.Container.IsSaddleBag)
{
if (!isSaddlebagContext)
return true;
}
return true;
} }
} }
@@ -173,7 +173,27 @@ public static unsafe class InventoryScanner
} }
public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType) public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType)
=> FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance()->GetInventoryContainer(inventoryType); => InventoryManager.Instance()->GetInventoryContainer(inventoryType);
public static InventoryLocation GetFirstEmptySlot(InventorySourceType source)
{
var manager = InventoryManager.Instance();
var containers = InventorySourceDefinitions.GetContainersForSource(source);
foreach (var type in containers)
{
var container = manager->GetInventoryContainer(type);
if (container == null || container->Size == 0) continue;
for (int i = 0; i < container->Size; i++)
{
if (container->Items[i].ItemId == 0)
return new InventoryLocation(type, (ushort)i);
}
}
return InventoryLocation.Invalid;
}
// Backwards compability TODO: Remove // Backwards compability TODO: Remove
public static string GetEmptyItemSlotsString() public static string GetEmptyItemSlotsString()
@@ -184,7 +204,7 @@ public static unsafe class InventoryScanner
int total = InventorySourceDefinitions.GetTotalSlots(source); int total = InventorySourceDefinitions.GetTotalSlots(source);
uint empty = source switch uint empty = source switch
{ {
InventorySourceType.MainBags => FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance()->GetEmptySlotsInBag(), InventorySourceType.MainBags => InventoryManager.Instance()->GetEmptySlotsInBag(),
InventorySourceType.SaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.SaddleBag), InventorySourceType.SaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.SaddleBag),
InventorySourceType.PremiumSaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.PremiumSaddleBag), InventorySourceType.PremiumSaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.PremiumSaddleBag),
InventorySourceType.AllSaddleBags => GetEmptySlotsInContainer(InventorySourceDefinitions.AllSaddleBags), InventorySourceType.AllSaddleBags => GetEmptySlotsInContainer(InventorySourceDefinitions.AllSaddleBags),
@@ -198,7 +218,7 @@ public static unsafe class InventoryScanner
private static uint GetEmptySlotsInContainer(InventoryType[] inventories) private static uint GetEmptySlotsInContainer(InventoryType[] inventories)
{ {
uint empty = 0; uint empty = 0;
var inventoryManager = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance(); var inventoryManager = InventoryManager.Instance();
foreach (var inv in inventories) foreach (var inv in inventories)
{ {
var container = inventoryManager->GetInventoryContainer(inv); var container = inventoryManager->GetInventoryContainer(inv);
@@ -60,13 +60,23 @@ public static class InventorySourceDefinitions
_ => MainBags, _ => MainBags,
}; };
public static InventoryType[] GetContainersForSource(InventorySourceType source) => source switch
{
InventorySourceType.MainBags => MainBags,
InventorySourceType.SaddleBag => SaddleBag,
InventorySourceType.PremiumSaddleBag => PremiumSaddleBag,
InventorySourceType.AllSaddleBags => AllSaddleBags,
InventorySourceType.Retainer => Retainer,
_ => MainBags,
};
public static int GetTotalSlots(InventorySourceType source) => source switch public static int GetTotalSlots(InventorySourceType source) => source switch
{ {
InventorySourceType.MainBags => 140, // 4 * 35 InventorySourceType.MainBags => 140, // 4 * 35
InventorySourceType.SaddleBag => 70, // 2 * 35 InventorySourceType.SaddleBag => 70, // 2 * 35
InventorySourceType.PremiumSaddleBag => 70, // 2 * 35 InventorySourceType.PremiumSaddleBag => 70, // 2 * 35
InventorySourceType.AllSaddleBags => 140, // 2 * 35 InventorySourceType.AllSaddleBags => 140, // 2 * 35
InventorySourceType.Retainer => 175, // 7 * 25 InventorySourceType.Retainer => Retainer.Length * 35, // 7 * 25
_ => 140, _ => 140,
}; };
} }
@@ -51,8 +51,6 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode
}; };
AddNode(defaultCurrencyColorNode); AddNode(defaultCurrencyColorNode);
AddNode();
CheckboxNode cappedEnabledCheckbox = new CheckboxNode CheckboxNode cappedEnabledCheckbox = new CheckboxNode
{ {
Size = Size with { Y = 18 }, Size = Size with { Y = 18 },
@@ -89,7 +87,7 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode
{ {
Size = Size with { Y = 18 }, Size = Size with { Y = 18 },
IsVisible = true, IsVisible = true,
String = "Color Weekly Limit", String = "Limited Currency Color",
IsChecked = config.ColorWhenLimited, IsChecked = config.ColorWhenLimited,
OnClick = isChecked => OnClick = isChecked =>
{ {
@@ -103,7 +101,7 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode
ColorInputRow limitCurrencyColorNode = new ColorInputRow ColorInputRow limitCurrencyColorNode = new ColorInputRow
{ {
Label = "Limit Currency Color", Label = "Color Weekly Limit",
Size = new Vector2(300, 24), Size = new Vector2(300, 24),
CurrentColor = config.LimitColor, CurrentColor = config.LimitColor,
DefaultColor = new CurrencySettings().LimitColor, DefaultColor = new CurrencySettings().LimitColor,
+1 -1
View File
@@ -51,7 +51,7 @@ public class CurrencyNode : SimpleComponentNode
_countNode.TextColor = _countNode.TextColor =
isLimited ? config.LimitColor : isLimited ? config.LimitColor :
isCapped ? config.CappedColor : isCapped ? config.CappedColor :
config.DefaultColor; config.DefaultColor;
} }
} }