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)
{
InitializeBackgroundDropTarget();
CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
{
Position = ContentStartPosition,
+2
View File
@@ -33,6 +33,8 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
protected override void OnSetup(AtkUnitBase* addon)
{
InitializeBackgroundDropTarget();
WindowNode?.AddColor = _tintColor;
CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
@@ -26,6 +26,8 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase
protected override void OnSetup(AtkUnitBase* addon)
{
InitializeBackgroundDropTarget();
WindowNode?.AddColor = _tintColor;
CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
+64
View File
@@ -1,14 +1,19 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using AetherBags.Helpers;
using AetherBags.Inventory;
using AetherBags.Inventory.Categories;
using AetherBags.Inventory.Context;
using AetherBags.Inventory.Scanning;
using AetherBags.Inventory.State;
using AetherBags.Nodes.Input;
using AetherBags.Nodes.Inventory;
using AetherBags.Nodes.Layout;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace AetherBags.Addons;
@@ -19,6 +24,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon
protected readonly InventoryCategoryPinCoordinator PinCoordinator = new();
protected readonly HashSet<InventoryCategoryNode> HoverSubscribed = new();
protected DragDropNode BackgroundDropTarget = null!;
protected WrappingGridNode<InventoryCategoryNode> CategoriesNode = null!;
protected TextInputWithHintNode SearchInputNode = 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()
{
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()
{
var nodes = CategoriesNode.Nodes;
@@ -233,6 +291,12 @@ public abstract unsafe class InventoryAddonBase : NativeAddon
protected void ResizeWindow(float width, float height, bool recalcLayout)
{
SetWindowSize(width, height);
if (BackgroundDropTarget != null)
{
BackgroundDropTarget.Size = ContentSize;
}
LayoutContent();
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<(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<int, Dictionary<InventoryMappedLocation, InventoryMappedLocation>> GroupedLocationMaps = new();
private static uint _lastContextId;
public static uint ActiveContextId => _lastContextId;
public static bool HasActiveContext => _lastContextId != 0;
public static void RefreshMaps()
{
EligibleSlots.Clear();
@@ -134,9 +138,6 @@ public static unsafe class InventoryContextState
public static bool IsSlotBlocked(InventoryType container, int slot)
=> BlockedSlots.Contains((container, slot));
public static bool HasActiveContext
=> _lastContextId != 0;
public static InventoryMappedLocation GetVisualLocation(InventoryType realContainer, int slot)
{
var key = new InventoryMappedLocation((int)realContainer, slot);
+27 -5
View File
@@ -82,13 +82,35 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
{
get
{
if (!InventoryContextState.HasActiveContext)
return true;
uint contextId = InventoryContextState.ActiveContextId;
if (contextId == 0) return true;
if (!IsMainInventory)
return true;
bool isRetainerContext = contextId == 4;
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)
=> 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
public static string GetEmptyItemSlotsString()
@@ -184,7 +204,7 @@ public static unsafe class InventoryScanner
int total = InventorySourceDefinitions.GetTotalSlots(source);
uint empty = source switch
{
InventorySourceType.MainBags => FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance()->GetEmptySlotsInBag(),
InventorySourceType.MainBags => InventoryManager.Instance()->GetEmptySlotsInBag(),
InventorySourceType.SaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.SaddleBag),
InventorySourceType.PremiumSaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.PremiumSaddleBag),
InventorySourceType.AllSaddleBags => GetEmptySlotsInContainer(InventorySourceDefinitions.AllSaddleBags),
@@ -198,7 +218,7 @@ public static unsafe class InventoryScanner
private static uint GetEmptySlotsInContainer(InventoryType[] inventories)
{
uint empty = 0;
var inventoryManager = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance();
var inventoryManager = InventoryManager.Instance();
foreach (var inv in inventories)
{
var container = inventoryManager->GetInventoryContainer(inv);
@@ -60,13 +60,23 @@ public static class InventorySourceDefinitions
_ => 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
{
InventorySourceType.MainBags => 140, // 4 * 35
InventorySourceType.SaddleBag => 70, // 2 * 35
InventorySourceType.PremiumSaddleBag => 70, // 2 * 35
InventorySourceType.AllSaddleBags => 140, // 2 * 35
InventorySourceType.Retainer => 175, // 7 * 25
InventorySourceType.MainBags => 140, // 4 * 35
InventorySourceType.SaddleBag => 70, // 2 * 35
InventorySourceType.PremiumSaddleBag => 70, // 2 * 35
InventorySourceType.AllSaddleBags => 140, // 2 * 35
InventorySourceType.Retainer => Retainer.Length * 35, // 7 * 25
_ => 140,
};
}
@@ -51,8 +51,6 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode
};
AddNode(defaultCurrencyColorNode);
AddNode();
CheckboxNode cappedEnabledCheckbox = new CheckboxNode
{
Size = Size with { Y = 18 },
@@ -89,7 +87,7 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode
{
Size = Size with { Y = 18 },
IsVisible = true,
String = "Color Weekly Limit",
String = "Limited Currency Color",
IsChecked = config.ColorWhenLimited,
OnClick = isChecked =>
{
@@ -103,7 +101,7 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode
ColorInputRow limitCurrencyColorNode = new ColorInputRow
{
Label = "Limit Currency Color",
Label = "Color Weekly Limit",
Size = new Vector2(300, 24),
CurrentColor = config.LimitColor,
DefaultColor = new CurrencySettings().LimitColor,
+1 -1
View File
@@ -51,7 +51,7 @@ public class CurrencyNode : SimpleComponentNode
_countNode.TextColor =
isLimited ? config.LimitColor :
isCapped ? config.CappedColor :
isCapped ? config.CappedColor :
config.DefaultColor;
}
}