diff --git a/UI/BagFrame.xml b/UI/BagFrame.xml
index 708eedf..3b11a7f 100644
--- a/UI/BagFrame.xml
+++ b/UI/BagFrame.xml
@@ -765,6 +765,7 @@
end
end
end
+ Guda_ClearCursorItem()
@@ -809,6 +810,7 @@
end
end
end
+ Guda_ClearCursorItem()
diff --git a/UI/BankFrame.lua b/UI/BankFrame.lua
index 193f2c6..bac7c94 100644
--- a/UI/BankFrame.lua
+++ b/UI/BankFrame.lua
@@ -845,9 +845,53 @@ function BankFrame:DisplayItemsByCategory(bankData, isOtherChar, charName)
for _, catName in ipairs(categoryList) do
local catDef = addon.Modules.CategoryManager and addon.Modules.CategoryManager:GetCategory(catName) or nil
- -- Skip Empty category in bank (bank doesn't need empty slot indicator in category loop)
+ -- Handle Empty category: show a single slot with free slot count
if catDef and catDef.isEmptyCategory then
- -- Skip empty categories in bank view
+ if totalFreeSlots > 0 and catDef.enabled then
+ local blockWidth = 1 * (buttonSize + spacing)
+ local blockHeight = 20 + (1 * (buttonSize + spacing)) + 5
+
+ if currentX > 0 and currentX + blockWidth + 20 > totalWidth + 5 then
+ currentX = 0
+ currentY = currentY + rowMaxHeight
+ rowMaxHeight = 0
+ end
+
+ local header = self:GetSectionHeader(headerIdx)
+ headerIdx = headerIdx + 1
+ header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", startX + currentX, startY - currentY)
+ header:SetWidth(blockWidth)
+ local displayName = (catDef and catDef.name) or catName
+ header.fullName = displayName
+ header.isShortened = false
+ header.text:SetText(displayName)
+ if header.countText then header.countText:Hide() end
+ header:Show()
+
+ local itemY = currentY + 20
+ local bagID = firstFreeBag or -1
+ local slotID = firstFreeSlot or 1
+ local bagParent = self:GetBagParent(bagID)
+ local button = Guda_GetItemButton(bagParent)
+ button:SetParent(bagParent)
+ button:SetWidth(buttonSize)
+ button:SetHeight(buttonSize)
+ button:ClearAllPoints()
+ button:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", startX + currentX, startY - itemY)
+ button:Show()
+
+ local emptyItemData = {
+ texture = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Bag",
+ count = totalFreeSlots,
+ name = "Empty Slots"
+ }
+ Guda_ItemButton_SetItem(button, bagID, slotID, emptyItemData, true, isOtherChar and charName or nil, true, true)
+ button.isReadOnly = false
+ button.inUse = true
+
+ if blockHeight > rowMaxHeight then rowMaxHeight = blockHeight end
+ currentX = currentX + blockWidth + 20
+ end
else
local items = categories[catName]
diff --git a/UI/BankFrame.xml b/UI/BankFrame.xml
index 8355a0b..d831b6e 100644
--- a/UI/BankFrame.xml
+++ b/UI/BankFrame.xml
@@ -574,6 +574,30 @@
searchBox:ClearFocus()
end
+
+ -- Drop item into first available bank slot
+ if CursorHasItem and CursorHasItem() then
+ -- Try main bank slots (bag -1)
+ for slot = 1, GetContainerNumSlots(-1) do
+ if not GetContainerItemLink(-1, slot) then
+ PickupContainerItem(-1, slot)
+ break
+ end
+ end
+ -- Try bank bags 5-10
+ for bag = 5, 10 do
+ if CursorHasItem() then
+ for slot = 1, GetContainerNumSlots(bag) do
+ if not GetContainerItemLink(bag, slot) then
+ PickupContainerItem(bag, slot)
+ break
+ end
+ end
+ end
+ end
+ end
+ Guda_ClearCursorItem()
+
@@ -602,6 +626,30 @@
this:StopMovingOrSizing()
+
+ -- Drop item into first available bank slot
+ if CursorHasItem and CursorHasItem() then
+ -- Try main bank slots (bag -1)
+ for slot = 1, GetContainerNumSlots(-1) do
+ if not GetContainerItemLink(-1, slot) then
+ PickupContainerItem(-1, slot)
+ break
+ end
+ end
+ -- Try bank bags 5-10
+ for bag = 5, 10 do
+ if CursorHasItem() then
+ for slot = 1, GetContainerNumSlots(bag) do
+ if not GetContainerItemLink(bag, slot) then
+ PickupContainerItem(bag, slot)
+ break
+ end
+ end
+ end
+ end
+ end
+ Guda_ClearCursorItem()
+
diff --git a/UI/ItemButton.lua b/UI/ItemButton.lua
index b86d90f..5a5a7b7 100644
--- a/UI/ItemButton.lua
+++ b/UI/ItemButton.lua
@@ -6,6 +6,10 @@ local buttonPool = {}
local nextButtonID = 1
local BUTTON_POOL_MAX = 500 -- Maximum buttons to create (bags ~80 + bank ~200 + keyring ~96 + buffer for both frames open)
+-- Category drag-drop: track cursor item for category reassignment
+local cursorItemInfo = nil -- { bagID, slotID, itemID, link }
+local activeCategoryDropIndicator = nil -- the currently shown "+" indicator button
+
-- Get button pool statistics (for /guda perf command)
function Guda_GetButtonPoolStats()
local total = 0
@@ -67,6 +71,222 @@ function Guda_ReleaseAllButtons()
end
end
+-- Category drop indicator: a single shared frame (parented to UIParent) with plus icon
+local HideCategoryDropIndicator -- forward declaration
+local categoryDropIndicator = nil -- the single shared indicator frame
+local dropIndicatorCategoryId = nil -- category the indicator is currently showing for
+local dropCooldownTime = 0 -- GetTime() when cooldown expires
+
+-- Helper: get category of cursor item using tracked info
+local function GetCursorItemCategory()
+ local info = Guda_GetCursorItemInfo()
+ addon:Debug("GetCursorItemCat: info=%s", tostring(info ~= nil))
+ if not info or not info.itemID or not addon.Modules.CategoryManager then
+ addon:Debug("GetCursorItemCat: BAIL - info=%s itemID=%s catMgr=%s", tostring(info ~= nil), tostring(info and info.itemID), tostring(addon.Modules.CategoryManager ~= nil))
+ return nil
+ end
+ local itemName, itemLink, itemQuality, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture
+ if info.link then
+ itemName, itemLink, itemQuality, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture = GetItemInfo(info.link)
+ end
+ addon:Debug("GetCursorItemCat: itemName=%s itemType=%s", tostring(itemName), tostring(itemType))
+ if not itemName then return nil end
+ local itemData = {
+ link = info.link,
+ itemID = info.itemID,
+ name = itemName,
+ quality = itemQuality or 0,
+ class = itemType or "",
+ subClass = itemSubType or "",
+ equipLoc = itemEquipLoc or "",
+ stackCount = itemStackCount or 1,
+ level = itemLevel or 0,
+ minLevel = itemMinLevel or 0,
+ texture = itemTexture,
+ }
+ return addon.Modules.CategoryManager:CategorizeItem(itemData, info.bagID, info.slotID)
+end
+
+-- Create the single shared indicator frame (once)
+local function GetOrCreateIndicator()
+ if categoryDropIndicator then return categoryDropIndicator end
+
+ local f = CreateFrame("Frame", "Guda_CategoryDropIndicator", UIParent)
+ f:SetWidth(36)
+ f:SetHeight(36)
+ f:SetFrameStrata("FULLSCREEN_DIALOG")
+ f:SetFrameLevel(500)
+
+ -- Slot background (same texture as empty bag slot, with green tint like GudaBags)
+ local slotBg = f:CreateTexture(nil, "BACKGROUND")
+ slotBg:SetPoint("TOPLEFT", f, "TOPLEFT", -9, 9)
+ slotBg:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 9, -9)
+ slotBg:SetTexture("Interface\\Buttons\\UI-EmptySlot")
+ slotBg:SetVertexColor(0.4, 0.8, 0.4, 0.9)
+ f.slotBg = slotBg
+
+ -- Plus icon centered
+ local plus = f:CreateTexture(nil, "OVERLAY")
+ plus:SetWidth(20)
+ plus:SetHeight(20)
+ plus:SetPoint("CENTER", f, "CENTER", 0, 0)
+ plus:SetTexture("Interface\\AddOns\\Guda\\Assets\\plus")
+ f.plus = plus
+
+ -- Mouse-enabled for drops
+ f:EnableMouse(true)
+
+ -- Handle drop on the indicator itself
+ local function DoIndicatorDrop()
+ if not activeCategoryDropIndicator then return end
+ local parentBtn = activeCategoryDropIndicator
+ if parentBtn and parentBtn:GetScript("OnReceiveDrag") then
+ local savedThis = getfenv(0)["this"]
+ getfenv(0)["this"] = parentBtn
+ parentBtn:GetScript("OnReceiveDrag")()
+ getfenv(0)["this"] = savedThis
+ end
+ end
+
+ f:SetScript("OnReceiveDrag", DoIndicatorDrop)
+ f:SetScript("OnMouseUp", function()
+ if CursorHasItem and CursorHasItem() then
+ DoIndicatorDrop()
+ end
+ end)
+
+ -- Tooltip on hover
+ f:SetScript("OnEnter", function()
+ if dropIndicatorCategoryId then
+ GameTooltip:SetOwner(this, "ANCHOR_TOP")
+ GameTooltip:SetText("Add item to this category", 1, 1, 1)
+ GameTooltip:AddLine("Drop here to permanently assign", 0.7, 0.7, 0.7)
+ GameTooltip:AddLine("this item to \"" .. tostring(dropIndicatorCategoryId) .. "\"", 0.5, 1, 0.5)
+ GameTooltip:Show()
+ end
+ end)
+
+ f:SetScript("OnLeave", function()
+ GameTooltip:Hide()
+ -- Delay hide: use an OnUpdate check instead of C_Timer
+ local elapsed = 0
+ this:SetScript("OnUpdate", function()
+ elapsed = elapsed + arg1
+ if elapsed >= 0.05 then
+ this:SetScript("OnUpdate", nil)
+ -- Check if mouse is still over the indicator or the parent button
+ if activeCategoryDropIndicator and MouseIsOver(activeCategoryDropIndicator) then
+ return
+ end
+ if this:IsMouseOver() then
+ return
+ end
+ HideCategoryDropIndicator()
+ end
+ end)
+ end)
+
+ f:Hide()
+ categoryDropIndicator = f
+ return f
+end
+
+local function ShowCategoryDropIndicator(button)
+ if activeCategoryDropIndicator == button then return end
+
+ -- Don't show during drop cooldown
+ if GetTime() < dropCooldownTime then return end
+
+ HideCategoryDropIndicator()
+
+ local ind = GetOrCreateIndicator()
+ local size = button:GetWidth()
+
+ -- Size to match icon
+ ind:SetWidth(size)
+ ind:SetHeight(size)
+
+ -- Update plus icon size (60% of icon, min 16)
+ local plusSize = math.max(16, math.floor(size * 0.6))
+ ind.plus:SetWidth(plusSize)
+ ind.plus:SetHeight(plusSize)
+
+ -- Position below the hovered button using screen coordinates
+ local buttonLeft = button:GetLeft()
+ local buttonBottom = button:GetBottom()
+ ind:ClearAllPoints()
+ if buttonLeft and buttonBottom then
+ ind:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", buttonLeft, buttonBottom - 2)
+ end
+
+ -- Track state
+ dropIndicatorCategoryId = button.categoryId or nil
+ activeCategoryDropIndicator = button
+ ind:Show()
+end
+
+HideCategoryDropIndicator = function()
+ if categoryDropIndicator then
+ categoryDropIndicator:Hide()
+ categoryDropIndicator:SetScript("OnUpdate", nil)
+ GameTooltip:Hide()
+ end
+ activeCategoryDropIndicator = nil
+ dropIndicatorCategoryId = nil
+end
+
+-- Track what item the cursor is holding (called before pickup)
+function Guda_TrackCursorItem(bagID, slotID)
+ local link = GetContainerItemLink(bagID, slotID)
+ if link then
+ local itemID = addon.Modules.Utils:ExtractItemID(link)
+ cursorItemInfo = { bagID = bagID, slotID = slotID, itemID = itemID, link = link }
+ addon:Debug("TrackCursor: bag=%d slot=%d itemID=%s", bagID, slotID, tostring(itemID))
+ else
+ cursorItemInfo = nil
+ addon:Debug("TrackCursor: no link at bag=%d slot=%d", bagID, slotID)
+ end
+end
+
+function Guda_ClearCursorItem()
+ cursorItemInfo = nil
+end
+
+function Guda_GetCursorItemInfo()
+ return cursorItemInfo
+end
+
+-- Check if we're in category view for bags or bank
+local function IsInCategoryView(isBank)
+ if not addon.Modules.DB then return false end
+ local key = isBank and "bankViewType" or "bagViewType"
+ return (addon.Modules.DB:GetSetting(key) or "single") == "category"
+end
+
+-- Auto-clear cursor tracking when cursor is truly empty
+-- Uses a short delay to avoid clearing during the pickup transition
+-- (CURSOR_UPDATE fires before the item is fully on the cursor)
+local cursorWatcher = CreateFrame("Frame")
+cursorWatcher:RegisterEvent("CURSOR_UPDATE")
+cursorWatcher:SetScript("OnEvent", function()
+ -- Don't clear immediately — wait a frame to let the pickup finish
+ if not cursorItemInfo then
+ HideCategoryDropIndicator()
+ return
+ end
+ -- Schedule a check next frame
+ this.pendingCheck = true
+end)
+cursorWatcher:SetScript("OnUpdate", function()
+ if not this.pendingCheck then return end
+ this.pendingCheck = nil
+ -- Now check if cursor actually has an item
+ if cursorItemInfo and (not CursorHasItem or not CursorHasItem()) then
+ cursorItemInfo = nil
+ HideCategoryDropIndicator()
+ end
+end)
+
-- Use shared tooltip from Utils module (retrieved on-demand to ensure Utils is loaded)
-- Helper function to check if an item is a quest item
@@ -535,6 +755,14 @@ function Guda_ItemButton_OnLoad(self)
HideJunkIcon(this)
end)
+ -- Track cursor item on drag start for category drag-drop
+ self:SetScript("OnDragStart", function()
+ if this.hasItem and this.bagID and this.slotID and not this.otherChar and not this.isReadOnly then
+ Guda_TrackCursorItem(this.bagID, this.slotID)
+ PickupContainerItem(this.bagID, this.slotID)
+ end
+ end)
+
self:SetScript("OnClick", function()
if IsAltKeyDown() and arg1 == "LeftButton" and this.hasItem and not this.otherChar and not this.isReadOnly then
local link = GetContainerItemLink(this.bagID, this.slotID)
@@ -585,7 +813,62 @@ function Guda_ItemButton_OnLoad(self)
return
end
+ -- Track cursor item before pickup for category drag-drop
+ if this.hasItem and this.bagID and this.slotID and not CursorHasItem() then
+ Guda_TrackCursorItem(this.bagID, this.slotID)
+ end
+
ContainerFrameItemButton_OnClick(arg1)
+
+ -- Clear cursor tracking if item was placed (cursor no longer has item)
+ if not CursorHasItem() then
+ Guda_ClearCursorItem()
+ end
+ end
+ end)
+
+ -- OnReceiveDrag: category reassignment in category view
+ self:SetScript("OnReceiveDrag", function()
+ local info = Guda_GetCursorItemInfo()
+ if not info then return end
+
+ local inCatView = IsInCategoryView(this.isBank)
+ if not inCatView then
+ -- Single view: let default swap happen
+ if ContainerFrameItemButton_OnClick then
+ ContainerFrameItemButton_OnClick("LeftButton")
+ end
+ return
+ end
+
+ -- Category view: reassign dragged item to target item's category
+ if this.hasItem and this.itemData and addon.Modules.CategoryManager then
+ local targetCategory = addon.Modules.CategoryManager:CategorizeItem(this.itemData, this.bagID, this.slotID, this.otherChar)
+ if targetCategory and info.itemID then
+ addon.Modules.CategoryManager:AssignItemToCategory(info.itemID, targetCategory)
+ addon:Debug("Reassigned item %d to category: %s", info.itemID, targetCategory)
+ end
+ end
+
+ -- Put dragged item back in its original slot
+ if CursorHasItem() then
+ PickupContainerItem(info.bagID, info.slotID)
+ end
+
+ -- Set drop cooldown to prevent immediate re-show
+ dropCooldownTime = GetTime() + 0.3
+ HideCategoryDropIndicator()
+ Guda_ClearCursorItem()
+
+ -- Refresh frames
+ if addon.Modules.BagFrame and addon.Modules.BagFrame.Update then
+ addon.Modules.BagFrame:Update()
+ end
+ if addon.Modules.BankFrame and addon.Modules.BankFrame.Update then
+ local bankFrame = getglobal("Guda_BankFrame")
+ if bankFrame and bankFrame:IsShown() then
+ addon.Modules.BankFrame:Update()
+ end
end
end)
end
@@ -1420,6 +1703,30 @@ end
-- OnEnter handler (show tooltip)
function Guda_ItemButton_OnEnter(self)
+ addon:Debug("OnEnter FIRED: button=%s CursorHasItem=%s", tostring(self:GetName()), tostring(CursorHasItem and CursorHasItem()))
+ -- Category drag-drop: show "+" indicator when hovering with cursor item in category view
+ local hasCursor = CursorHasItem and CursorHasItem()
+ if hasCursor then
+ addon:Debug("DropInd OnEnter: hasItem=%s otherChar=%s isBank=%s", tostring(self.hasItem), tostring(self.otherChar), tostring(self.isBank))
+ end
+ if hasCursor and self.hasItem and not self.otherChar then
+ local inCatView = IsInCategoryView(self.isBank)
+ addon:Debug("DropInd: inCatView=%s hasItemData=%s hasCatMgr=%s", tostring(inCatView), tostring(self.itemData ~= nil), tostring(addon.Modules.CategoryManager ~= nil))
+ if inCatView and self.itemData and addon.Modules.CategoryManager then
+ -- Don't show indicator if dragged item is already in the same category
+ local targetCategory = addon.Modules.CategoryManager:CategorizeItem(self.itemData, self.bagID, self.slotID, self.otherChar)
+ local cursorCategory = GetCursorItemCategory()
+ addon:Debug("DropInd: targetCat=%s cursorCat=%s cooldown=%s", tostring(targetCategory), tostring(cursorCategory), tostring(GetTime() < dropCooldownTime))
+ if targetCategory and cursorCategory and targetCategory ~= cursorCategory then
+ self.categoryId = targetCategory
+ addon:Debug("DropInd: SHOWING indicator for %s", tostring(targetCategory))
+ ShowCategoryDropIndicator(self)
+ else
+ addon:Debug("DropInd: NOT showing - same=%s targetNil=%s cursorNil=%s", tostring(targetCategory == cursorCategory), tostring(targetCategory == nil), tostring(cursorCategory == nil))
+ end
+ end
+ end
+
-- Highlight the corresponding bag button in the footer (works for empty and filled slots)
if not self.otherChar and self.bagID then
if self.isBank then
@@ -1588,6 +1895,29 @@ end
-- OnLeave handler
function Guda_ItemButton_OnLeave(self)
+ -- Delay hide of drop indicator to allow mouse to move to indicator frame
+ if activeCategoryDropIndicator == self and categoryDropIndicator and categoryDropIndicator:IsShown() then
+ -- Use OnUpdate delay to check if mouse moved to the indicator
+ local elapsed = 0
+ local checkFrame = CreateFrame("Frame")
+ checkFrame:SetScript("OnUpdate", function()
+ elapsed = elapsed + arg1
+ if elapsed >= 0.05 then
+ this:SetScript("OnUpdate", nil)
+ -- If mouse is over the indicator, don't hide
+ if categoryDropIndicator and categoryDropIndicator:IsMouseOver() then
+ return
+ end
+ -- If mouse is over the parent button, don't hide
+ if activeCategoryDropIndicator and MouseIsOver(activeCategoryDropIndicator) then
+ return
+ end
+ HideCategoryDropIndicator()
+ end
+ end)
+ else
+ HideCategoryDropIndicator()
+ end
self._debugPrinted = nil
-- Clear any viewed character hint on the tooltip when leaving
if GameTooltip then