fix: move item to empty category

This commit is contained in:
Vati
2026-04-16 02:05:51 +04:00
parent 150c9d3574
commit 1af002fa08
4 changed files with 331 additions and 56 deletions
+22 -2
View File
@@ -97,6 +97,7 @@ local FRAME_BUDGET_SECONDS = 0.1 -- 100ms budget per frame (same as Baganator)
local lastEntryTime = 0
local workQueue = {}
local workQueueFrame = nil
local workQueuePaused = false -- When true, new QueueWork calls enqueue but don't resume processing
-- Report that we're starting work (call at the beginning of expensive operations)
-- This resets the frame budget timer
@@ -159,8 +160,11 @@ function Utils:QueueWork(callback, context)
end)
end
-- Show the frame to start processing
workQueueFrame:Show()
-- Show the frame to start processing (unless we're paused — new items
-- stay in the queue and are drained when ResumeWorkQueue is called).
if not workQueuePaused then
workQueueFrame:Show()
end
end
-- Clear all queued work (useful when frame is hidden)
@@ -171,6 +175,22 @@ function Utils:ClearWorkQueue()
end
end
-- Pause work queue processing (e.g. during a frame drag). Work stays queued
-- (including items QueueWork'd while paused); call ResumeWorkQueue to drain.
function Utils:PauseWorkQueue()
workQueuePaused = true
if workQueueFrame then
workQueueFrame:Hide()
end
end
function Utils:ResumeWorkQueue()
workQueuePaused = false
if workQueueFrame and table.getn(workQueue) > 0 then
workQueueFrame:Show()
end
end
-- Get the number of items in the work queue (for debugging)
function Utils:GetWorkQueueSize()
return table.getn(workQueue)
+1 -1
View File
@@ -2,7 +2,7 @@
## Title: Guda
## Notes: All-in-one bag and bank addon for World of Warcraft 1.12.1 (Turtle WoW)
## Author: Vati
## Version: 2.2.8
## Version: 2.2.9
## SavedVariables: Guda_DB
## SavedVariablesPerCharacter: Guda_CharDB
## OptionalDeps: pfUI
+126 -42
View File
@@ -60,6 +60,28 @@ local showSoulBag = false -- Toggle for soul bag display
local hiddenBags = {} -- Track which bags are hidden (bagID -> true/false)
local bagParents = {} -- Per-bag parent frames to carry bagID for Blizzard item button templates
local isMerchantOpen = false -- Track whether a vendor window is currently open to prevent auto-closing bags
local isDragging = false -- True while the cursor carries an item and the bag is open in category view
local isFrameMoving = false -- True between StartMoving and StopMovingOrSizing on the bag frame
function BagFrame:IsDragging()
return isDragging
end
function BagFrame:IsFrameMoving()
return isFrameMoving
end
-- Called by the cursor watcher in ItemButton.lua on CURSOR_UPDATE edges.
-- Toggles the dragging flag and triggers a redraw so empty-category drop
-- targets appear/disappear in category view.
function BagFrame:SetDragging(state)
state = state and true or false
if isDragging == state then return end
isDragging = state
if Guda_BagFrame and Guda_BagFrame:IsShown() then
BagFrame:Update()
end
end
-- Global click catcher for clearing search focus
local clickCatcher = nil
@@ -83,6 +105,10 @@ end
-- Update usability tints on all visible item buttons
local function UpdateAllUsabilityTints()
if not Guda_BagFrame or not Guda_BagFrame:IsShown() then return end
-- Don't scan ~80 tooltips synchronously while the user is dragging the
-- frame — it can stall the engine for multiple seconds on cold caches.
-- The caller will reschedule after the drag ends.
if isFrameMoving then return end
for _, bagParent in pairs(bagParents) do
if bagParent and bagParent.itemButtons then
@@ -104,6 +130,11 @@ local function ScheduleDeferredUsabilityCheck()
usabilityCheckFrame.elapsed = 0
usabilityCheckFrame.pending = false
usabilityCheckFrame:SetScript("OnUpdate", function()
-- Hold the timer at its current elapsed value while the user is
-- dragging the frame. Resuming the countdown during a drag would
-- kick off ~80 synchronous tooltip scans right when the engine
-- is starved for cycles, producing a multi-second apparent freeze.
if isFrameMoving then return end
this.elapsed = this.elapsed + arg1
if this.elapsed >= USABILITY_CHECK_DELAY then
this:Hide()
@@ -800,6 +831,16 @@ function BagFrame:Update()
return
end
-- Frame is being dragged around the screen: skip the full category rebuild
-- (it can take ~100ms+ on a full inventory and lands inside the native
-- StartMoving loop, producing a multi-second apparent freeze). Refresh
-- lock states cheaply instead; EndFrameMove will run one rebuild on drop
-- to catch up any changes that arrived during the move.
if isFrameMoving then
self:UpdateLockStates()
return
end
local viewType = addon.Modules.DB:GetSetting("bagViewType") or "single"
addon:DebugCategory("Update() START: viewType=%s", viewType)
@@ -809,8 +850,11 @@ function BagFrame:Update()
end
-- If cursor is holding an item (mid-drag), only update lock states, don't rebuild UI
-- BUT only if we already have items displayed - otherwise we need to do initial build
-- BUT only if we already have items displayed - otherwise we need to do initial build.
-- Exception: in Category View we DO need the full rebuild so the empty-category
-- drop targets can appear/disappear alongside the drag state.
if CursorHasItem and CursorHasItem() then
local inCategoryView = viewType == "category"
-- Check if we have any displayed items
-- Use itemButtons hash instead of GetChildren() to avoid table allocation
local hasDisplayedItems = false
@@ -826,11 +870,12 @@ function BagFrame:Update()
if hasDisplayedItems then break end
end
if hasDisplayedItems then
if hasDisplayedItems and not inCategoryView then
self:UpdateLockStates()
return
end
-- If no items displayed yet, continue with full update
-- If no items displayed yet, or we're in category view (drop-target
-- placeholders need to render), continue with full update
end
-- Mark all existing buttons as not in use (we'll mark active ones during display)
@@ -1122,6 +1167,40 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
end
end
-- Inject drop-target pseudo-items for every currently-empty user-assignable
-- category while the user is dragging. They render at the tail of the bag
-- grid; dropping on one calls CategoryManager:AssignItemToCategory.
-- Excluded: system pseudo-categories (Keyring/Soul Bag/Empty), auto-only
-- categories whose membership is determined by item class (Quiver,
-- Container, Class Items), and EquipSet:* overrides (equipment-set
-- membership is managed via the equipment-set UI, not bag drag).
if not isOtherChar and isDragging and addon.Modules.CategoryManager then
local DROP_TARGET_BLOCKLIST = {
["Keyring"] = true, ["Soul Bag"] = true, ["Empty"] = true,
["Quiver"] = true, ["Container"] = true, ["Class Items"] = true,
}
local allCats = addon.Modules.CategoryManager:GetCategories()
local defs = allCats and allCats.definitions or {}
for _, catName in ipairs(Guda_CategoryList) do
if not DROP_TARGET_BLOCKLIST[catName]
and string.sub(catName, 1, 9) ~= "EquipSet:"
and categories[catName] and table.getn(categories[catName]) == 0
and defs[catName] and defs[catName].enabled ~= false then
local icon = defs[catName].icon or "Interface\\AddOns\\Guda\\Assets\\plus"
table.insert(categories[catName], {
bagID = 0, slotID = 0,
itemData = {
isDropTarget = true,
categoryId = catName,
texture = icon,
name = catName,
quality = 0,
},
})
end
end
end
-- Layout (theme-aware padding)
local _pad = { startX = 10, startY = -10 }
if addon.Modules and addon.Modules.Theme and addon.Modules.Theme.GetFramePadding then
@@ -2191,6 +2270,34 @@ local function SaveBagFramePosition()
end
end
-- Centralized move start/stop so every drag handler sets the same flag and
-- we don't drift across six duplicated call sites.
local function BeginFrameMove()
local bagFrame = getglobal("Guda_BagFrame")
if not bagFrame then return end
isFrameMoving = true
-- Pause the background work queue (CacheWarmer tooltip scans etc.) so it
-- doesn't eat 100ms per render frame and kill drag FPS.
if addon.Modules.Utils and addon.Modules.Utils.PauseWorkQueue then
addon.Modules.Utils:PauseWorkQueue()
end
bagFrame:StartMoving()
end
local function EndFrameMove()
local bagFrame = getglobal("Guda_BagFrame")
if not bagFrame then return end
bagFrame:StopMovingOrSizing()
isFrameMoving = false
SaveBagFramePosition()
if addon.Modules.Utils and addon.Modules.Utils.ResumeWorkQueue then
addon.Modules.Utils:ResumeWorkQueue()
end
-- One rebuild to catch up anything that was short-circuited during the move.
BagFrame:Update()
end
-- Create transparent overlay for money tooltip
function BagFrame:EnsureMoneyTooltipOverlay()
local overlayName = "Guda_BagFrame_MoneyTooltipOverlay"
@@ -2224,11 +2331,10 @@ function BagFrame:EnsureMoneyTooltipOverlay()
searchBox:ClearFocus()
end
local bagFrame = getglobal("Guda_BagFrame")
local isLocked = addon.Modules.DB and addon.Modules.DB:GetSetting("lockBags")
if bagFrame and not isLocked and arg1 == "LeftButton" then
bagFrame:StartMoving()
if not isLocked and arg1 == "LeftButton" then
BeginFrameMove()
end
end)
@@ -2238,12 +2344,10 @@ function BagFrame:EnsureMoneyTooltipOverlay()
Guda_ShowGoldTrackingMenu(moneyFrame)
return
end
local bagFrame = getglobal("Guda_BagFrame")
local isLocked = addon.Modules.DB and addon.Modules.DB:GetSetting("lockBags")
if bagFrame and not isLocked then
bagFrame:StopMovingOrSizing()
SaveBagFramePosition()
if not isLocked then
EndFrameMove()
end
end)
@@ -3484,17 +3588,12 @@ function BagFrame:UpdateLockState()
searchBox:ClearFocus()
end
local bagFrame = getglobal("Guda_BagFrame")
if bagFrame and arg1 == "LeftButton" then
bagFrame:StartMoving()
if arg1 == "LeftButton" then
BeginFrameMove()
end
end)
frame:SetScript("OnMouseUp", function()
local bagFrame = getglobal("Guda_BagFrame")
if bagFrame then
bagFrame:StopMovingOrSizing()
SaveBagFramePosition()
end
EndFrameMove()
end)
end
@@ -3506,17 +3605,12 @@ function BagFrame:UpdateLockState()
searchBox:ClearFocus()
end
local bagFrame = getglobal("Guda_BagFrame")
if bagFrame and arg1 == "LeftButton" then
bagFrame:StartMoving()
if arg1 == "LeftButton" then
BeginFrameMove()
end
end)
toolbar:SetScript("OnMouseUp", function()
local bagFrame = getglobal("Guda_BagFrame")
if bagFrame then
bagFrame:StopMovingOrSizing()
SaveBagFramePosition()
end
EndFrameMove()
end)
end
@@ -3528,17 +3622,12 @@ function BagFrame:UpdateLockState()
searchBox:ClearFocus()
end
local bagFrame = getglobal("Guda_BagFrame")
if bagFrame and arg1 == "LeftButton" then
bagFrame:StartMoving()
if arg1 == "LeftButton" then
BeginFrameMove()
end
end)
moneyFrame:SetScript("OnMouseUp", function()
local bagFrame = getglobal("Guda_BagFrame")
if bagFrame then
bagFrame:StopMovingOrSizing()
SaveBagFramePosition()
end
EndFrameMove()
end)
end
@@ -3550,17 +3639,12 @@ function BagFrame:UpdateLockState()
searchBox:ClearFocus()
end
local bagFrame = getglobal("Guda_BagFrame")
if bagFrame and arg1 == "LeftButton" then
bagFrame:StartMoving()
if arg1 == "LeftButton" then
BeginFrameMove()
end
end)
itemContainer:SetScript("OnMouseUp", function()
local bagFrame = getglobal("Guda_BagFrame")
if bagFrame then
bagFrame:StopMovingOrSizing()
SaveBagFramePosition()
end
EndFrameMove()
end)
end
end
+182 -11
View File
@@ -263,30 +263,97 @@ local function IsInCategoryView(isBank)
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)
-- Auto-clear cursor tracking when cursor is truly empty, and notify BagFrame
-- of drag-state transitions. In 1.12 CURSOR_UPDATE is unreliable for item
-- pickups, so we also poll CursorHasItem() on a throttled OnUpdate —
-- matching the Anniversary version's approach.
local cursorWatcher = CreateFrame("Frame")
cursorWatcher:RegisterEvent("CURSOR_UPDATE")
cursorWatcher:RegisterEvent("ITEM_LOCK_CHANGED")
cursorWatcher._lastCarrying = false
cursorWatcher._pollAccum = 0
local function NotifyDragState()
if addon.Modules.BagFrame and addon.Modules.BagFrame.SetDragging then
-- Suppress drop-target state updates while the user is dragging the
-- bag frame itself — nothing on the cursor there, and Update() is
-- deliberately short-circuited anyway.
if addon.Modules.BagFrame.IsFrameMoving
and addon.Modules.BagFrame:IsFrameMoving() then
return
end
local carrying = CursorHasItem and CursorHasItem() and true or false
if carrying ~= cursorWatcher._lastCarrying then
cursorWatcher._lastCarrying = carrying
addon.Modules.BagFrame:SetDragging(carrying)
end
end
end
cursorWatcher:SetScript("OnEvent", function()
-- Don't clear immediately — wait a frame to let the pickup finish
NotifyDragState()
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()
-- Throttled cursor poll (~10Hz) as a safety net for 1.12's event gaps.
this._pollAccum = (this._pollAccum or 0) + arg1
if this._pollAccum >= 0.1 then
this._pollAccum = 0
NotifyDragState()
end
if this.pendingCheck then
this.pendingCheck = nil
if cursorItemInfo and (not CursorHasItem or not CursorHasItem()) then
cursorItemInfo = nil
HideCategoryDropIndicator()
end
end
end)
-- Pulsing green glow applied to empty-category drop-target placeholder buttons.
-- Uses an OnUpdate-driven sine wave since vanilla 1.12 has no animation groups.
-- Uses a solid-color texture filling the full button, with additive blend +
-- alpha pulse — looks the same regardless of the user's iconSize setting.
local function EnsureDropTargetGlow(button)
if not button.dropGlow then
local g = button:CreateTexture(nil, "OVERLAY")
g:SetTexture(0.2, 1.0, 0.2, 1) -- solid green; alpha is modulated each frame
g:SetBlendMode("ADD")
g:SetAllPoints(button) -- cover the whole button, scales with iconSize
button.dropGlow = g
local driver = CreateFrame("Frame", nil, button)
driver._t = 0
driver:SetScript("OnUpdate", function()
this._t = this._t + arg1
-- period ~1.2s => 2π/1.2 ≈ 5.24
local phase = (math.sin(this._t * 5.24) + 1) * 0.5
if this:GetParent().dropGlow then
-- Subtle pulse: 0.15 → 0.45 alpha with additive blend.
this:GetParent().dropGlow:SetAlpha(0.15 + phase * 0.30)
end
end)
button.dropGlowDriver = driver
end
-- If the button was resized since last show, re-anchor defensively.
button.dropGlow:ClearAllPoints()
button.dropGlow:SetAllPoints(button)
button.dropGlow:Show()
button.dropGlowDriver:Show()
end
local function StopDropTargetGlow(button)
if button.dropGlow then button.dropGlow:Hide() end
if button.dropGlowDriver then button.dropGlowDriver:Hide() end
button.isDropTarget = false
button.dropTargetCategoryId = nil
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
@@ -1080,6 +1147,17 @@ function Guda_ItemButton_OnLoad(self)
end)
self:SetScript("OnClick", function()
-- Click on a drop-target placeholder while carrying an item on the
-- cursor: route to the same handler as OnReceiveDrag (assign to
-- category). Without an item on the cursor a click is a no-op.
if this.isDropTarget then
if CursorHasItem and CursorHasItem() then
local handler = this:GetScript("OnReceiveDrag")
if handler then handler() end
end
return
end
-- Lock/unlock item with Ctrl+Right-Click
if IsControlKeyDown() and arg1 == "RightButton" and this.hasItem and not this.otherChar and not this.isReadOnly then
local link = GetContainerItemLink(this.bagID, this.slotID)
@@ -1240,6 +1318,24 @@ function Guda_ItemButton_OnLoad(self)
local info = Guda_GetCursorItemInfo()
if not info then return end
-- Empty-category drop target: assign cursor item to this.dropTargetCategoryId
if this.isDropTarget and this.dropTargetCategoryId then
if info.itemID and addon.Modules.CategoryManager then
addon.Modules.CategoryManager:AssignItemToCategory(info.itemID, this.dropTargetCategoryId)
addon:Debug("Assigned item %d to empty category: %s", info.itemID, this.dropTargetCategoryId)
end
if CursorHasItem() then
PickupContainerItem(info.bagID, info.slotID)
end
dropCooldownTime = GetTime() + 0.3
HideCategoryDropIndicator()
Guda_ClearCursorItem()
if addon.Modules.BagFrame and addon.Modules.BagFrame.Update then
addon.Modules.BagFrame:Update()
end
return
end
local inCatView = IsInCategoryView(this.isBank)
if not inCatView then
-- Single view: let default swap happen
@@ -1337,6 +1433,14 @@ local function ResetButtonVisualState(self)
local chargesText = getglobal(self:GetName().."_Charges")
if chargesText then chargesText:Hide() end
-- Reset effects the drop-target render may have applied on this pooled button
self:SetAlpha(1)
local iconTex = getglobal(self:GetName().."IconTexture") or getglobal(self:GetName().."Icon") or self.icon or self.Icon
if iconTex then
if iconTex.SetDesaturated then iconTex:SetDesaturated(false) end
iconTex:SetVertexColor(1, 1, 1)
end
-- Clear cooldown overlay
local cd = getglobal(self:GetName().."Cooldown") or self.cooldown
if cd then
@@ -1658,6 +1762,60 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
-- Reset all visual state before reassigning pooled button
ResetButtonVisualState(self)
-- Pooled buttons may have carried a drop-target glow from the previous
-- render — clear it before deciding what this button is now.
StopDropTargetGlow(self)
-- Drop-target pseudo-items (empty category placeholders during drag).
-- Shown with the category's icon, dim+desaturated, with a pulsing green
-- glow. OnReceiveDrag/OnClick will route to AssignItemToCategory.
if itemData and itemData.isDropTarget then
self.bagID = 0
self.slotID = 0
self.bagIndex = -100
self.itemData = itemData
self.isBank = isBank or false
self.otherChar = nil
self.isReadOnly = false
self.isMail = false
self.hasItem = false
self.isDropTarget = true
self.dropTargetCategoryId = itemData.categoryId
SetupDragDrop(self)
local iconSize = 37
if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.SafeCall then
iconSize = addon.Modules.Utils:SafeCall("DB", "GetSetting", "iconSize") or iconSize
end
self:SetWidth(iconSize)
self:SetHeight(iconSize)
if SetItemButtonTexture then
SetItemButtonTexture(self, itemData.texture)
end
local iconTexture = getglobal(self:GetName().."IconTexture") or getglobal(self:GetName().."Icon") or self.icon or self.Icon
if iconTexture then
iconTexture:SetTexture(itemData.texture)
-- Mirror PositionIconAndBorders() so the category icon fills the
-- full button at the user's configured iconSize. PositionIconAndBorders
-- itself is guarded by `self.hasItem`, which drop-targets don't set.
iconTexture:ClearAllPoints()
iconTexture:SetPoint("CENTER", self, "CENTER", 0, 0)
iconTexture:SetWidth(iconSize)
iconTexture:SetHeight(iconSize)
iconTexture:SetTexCoord(0, 1, 0, 1)
iconTexture:Show()
if iconTexture.SetDesaturated then iconTexture:SetDesaturated(true) end
iconTexture:SetVertexColor(0.6, 1.0, 0.6)
end
if SetItemButtonCount then SetItemButtonCount(self, 0) end
self:SetAlpha(0.85)
EnsureDropTargetGlow(self)
self:Show()
return
end
-- Set button properties
self.bagID = bagID
@@ -2198,6 +2356,19 @@ 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()))
-- Drop-target placeholder: simple tooltip, skip the normal item tooltip path.
if self.isDropTarget then
GameTooltip:SetOwner(UIParent, "ANCHOR_NONE")
GameTooltip:ClearAllPoints()
GameTooltip:SetPoint("BOTTOMRIGHT", self, "TOPLEFT", 10, 0)
local catName = self.dropTargetCategoryId or ""
local label = (Guda_L and Guda_L["Drop to assign to %s"]) or "Drop to assign to %s"
GameTooltip:SetText(format(label, catName), 1, 1, 1)
GameTooltip:Show()
return
end
-- Category drag-drop: show "+" indicator when hovering with cursor item in category view
local hasCursor = CursorHasItem and CursorHasItem()
if hasCursor then