Files
Guda-ClassicAPI/UI/BagFrame.lua
T

3522 lines
111 KiB
Lua

-- Guda Bag Frame
-- Main bag viewing UI
local addon = Guda
local BagFrame = {}
addon.Modules.BagFrame = BagFrame
-- Use centralized frame helpers for section headers and bag parents
function BagFrame:GetSectionHeader(index)
return Guda_GetSectionHeader("Guda_BagFrame", "Guda_BagFrame_ItemContainer", index)
end
function BagFrame:GetBagParent(bagID)
return Guda_GetBagParent("Guda_BagFrame", bagParents, bagID, "Guda_BagFrame_ItemContainer")
end
local currentViewChar = nil -- nil = current character
function BagFrame:GetCurrentViewChar()
return currentViewChar
end
local searchText = ""
local itemButtons = {}
local slotToButton = {} -- Fast O(1) lookup: slotToButton[bagID][slotID] = button
local showKeyring = false -- Toggle for keyring display
-- Track empty slots that should show as placeholders in Category View
-- Format: bagRecentlyEmptiedSlots[bagID][slotID] = { category = "CategoryName", timestamp = time, ... }
local bagRecentlyEmptiedSlots = {}
function BagFrame:ClearRecentlyEmptiedSlots()
for k in pairs(bagRecentlyEmptiedSlots) do
bagRecentlyEmptiedSlots[k] = nil
end
end
function BagFrame:MarkSlotAsEmptied(bagID, slotID, category, itemData)
if not bagRecentlyEmptiedSlots[bagID] then
bagRecentlyEmptiedSlots[bagID] = {}
end
bagRecentlyEmptiedSlots[bagID][slotID] = {
category = category or "Miscellaneous",
timestamp = GetTime(),
quality = itemData and itemData.quality or 0,
name = itemData and itemData.name or "",
iLevel = itemData and itemData.iLevel or 0,
}
addon:DebugCategory("BagFrame: Marked slot %d:%d as emptied (category: %s)", bagID, slotID, category or "Miscellaneous")
end
function BagFrame:UnmarkSlotAsEmptied(bagID, slotID)
if bagRecentlyEmptiedSlots[bagID] then
bagRecentlyEmptiedSlots[bagID][slotID] = nil
end
end
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
-- Global click catcher for clearing search focus
local clickCatcher = nil
--=====================================================
-- Deferred Usability Tint System
-- Prevents false positives when item data isn't fully loaded on bag open
-- Uses debouncing to handle rapid open/close safely
--=====================================================
local usabilityCheckFrame = nil
local USABILITY_CHECK_DELAY = 0.25 -- Delay before re-checking usability (seconds)
-- Cancel any pending deferred usability check
local function CancelDeferredUsabilityCheck()
if usabilityCheckFrame then
usabilityCheckFrame:Hide()
usabilityCheckFrame.pending = false
end
end
-- Update usability tints on all visible item buttons
local function UpdateAllUsabilityTints()
if not Guda_BagFrame or not Guda_BagFrame:IsShown() then return end
for _, bagParent in pairs(bagParents) do
if bagParent and bagParent.itemButtons then
for button in pairs(bagParent.itemButtons) do
if button.hasItem and button:IsShown() and Guda_ItemButton_UpdateUsableTint then
Guda_ItemButton_UpdateUsableTint(button)
end
end
end
end
end
-- Schedule a deferred usability check with debouncing
local function ScheduleDeferredUsabilityCheck()
-- Create frame on first use
if not usabilityCheckFrame then
usabilityCheckFrame = CreateFrame("Frame")
usabilityCheckFrame:Hide()
usabilityCheckFrame.elapsed = 0
usabilityCheckFrame.pending = false
usabilityCheckFrame:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + arg1
if this.elapsed >= USABILITY_CHECK_DELAY then
this:Hide()
this.pending = false
-- Only run if bag is still open
if Guda_BagFrame and Guda_BagFrame:IsShown() then
-- Clear detection cache and re-check all items
if addon.Modules.ItemDetection and addon.Modules.ItemDetection.ClearCache then
addon.Modules.ItemDetection:ClearCache()
end
UpdateAllUsabilityTints()
end
end
end)
end
-- Reset timer (debounce behavior)
usabilityCheckFrame.elapsed = 0
usabilityCheckFrame.pending = true
usabilityCheckFrame:Show()
end
-- Get player race icon path (using racial ability icons)
function Guda_GetPlayerRaceIcon()
local _, race = UnitRace("player")
-- Use racial ability icons that exist in vanilla
local raceIcons = {
Human = "Interface\\Icons\\Spell_Magic_PolymorphPig",
Dwarf = "Interface\\Icons\\Spell_Shadow_UnholyFrenzy",
NightElf = "Interface\\Icons\\Spell_Nature_Invisibility",
Gnome = "Interface\\Icons\\Ability_Repair",
Orc = "Interface\\Icons\\Ability_Racial_BloodRage",
Undead = "Interface\\Icons\\Spell_Shadow_RaiseDead",
Tauren = "Interface\\Icons\\Ability_Thunderclap",
Troll = "Interface\\Icons\\Ability_Racial_Avatar",
}
return raceIcons[race] or "Interface\\Icons\\INV_Misc_GroupNeedMore"
end
-- OnLoad
function Guda_BagFrame_OnLoad(self)
-- Set up initial backdrop
addon:ApplyBackdrop(self, "DEFAULT_FRAME")
-- Set up search box placeholder
local searchBox = getglobal(self:GetName().."_SearchBar_SearchBox")
if searchBox then
searchBox:SetText("Search, try ~equipment")
searchBox:SetTextColor(0.5, 0.5, 0.5, 1)
end
-- Create invisible full-screen frame to catch clicks outside bag
if not clickCatcher then
clickCatcher = CreateFrame("Frame", "Guda_ClickCatcher", UIParent)
clickCatcher:SetFrameStrata("BACKGROUND")
clickCatcher:SetAllPoints(UIParent)
clickCatcher:EnableMouse(true)
clickCatcher:Hide()
clickCatcher:SetScript("OnMouseDown", function()
Guda_BagFrame_ClearSearch()
end)
end
end
-- OnShow
function Guda_BagFrame_OnShow(self)
-- Play bag open sound
PlaySound("igBackPackOpen")
-- Save bag data when opening bags
addon.Modules.BagScanner:SaveToDatabase()
addon.Modules.MoneyTracker:Update()
-- Restore saved position if it exists (only if saved as BOTTOMRIGHT)
if addon and addon.Modules and addon.Modules.DB then
local pos = addon.Modules.DB:GetSetting("bagFramePosition")
if pos and pos.point == "BOTTOMRIGHT" and pos.x and pos.y then
self:ClearAllPoints()
self:SetPoint("BOTTOMRIGHT", "UIParent", "BOTTOMRIGHT", pos.x, pos.y)
end
end
-- Set lock state when frame is shown (ensures all child frames are loaded)
if BagFrame.UpdateLockState then
BagFrame:UpdateLockState()
end
-- Apply border visibility setting
if BagFrame.UpdateBorderVisibility then
BagFrame:UpdateBorderVisibility()
end
-- Apply search bar visibility setting
if BagFrame.UpdateSearchBarVisibility then
BagFrame:UpdateSearchBarVisibility()
end
-- Apply footer visibility setting
if BagFrame.UpdateFooterVisibility then
BagFrame:UpdateFooterVisibility()
end
-- Apply frame transparency
if Guda_ApplyBackgroundTransparency then
Guda_ApplyBackgroundTransparency()
end
BagFrame:Update()
-- Schedule deferred usability check to fix false positives from uncached item data
-- This runs after a short delay when item info is fully loaded by the WoW client
ScheduleDeferredUsabilityCheck()
end
-- OnHide
function Guda_BagFrame_OnHide(self)
-- Play bag close sound
PlaySound("igBackPackClose")
-- Close any open dropdown menus when the bag frame is hidden
CloseDropDownMenus()
-- Hide bag flyout
BagFrame:HideBagFlyout()
-- Hide tooltip (it may be showing for soul bag or other footer buttons)
GameTooltip:Hide()
-- Clear search field
Guda_BagFrame_ClearSearch()
-- Clear any pending update and work queue items
pendingUpdate = false
if addon.Modules.Utils and addon.Modules.Utils.ClearWorkQueue then
addon.Modules.Utils:ClearWorkQueue()
end
-- Cancel any pending throttled updates
local throttleFrame = getglobal("Guda_BagUpdateThrottle")
if throttleFrame then
throttleFrame:Hide()
end
-- Cancel any pending deferred usability check (debounce safety)
CancelDeferredUsabilityCheck()
-- Clear recently emptied slots tracking (reset placeholders)
BagFrame:ClearRecentlyEmptiedSlots()
-- Clean up all buttons when frame is hidden (safe since we're not displaying)
-- Use itemButtons hash instead of GetChildren() to avoid table allocation
for _, bagParent in pairs(bagParents) do
if bagParent and bagParent.itemButtons then
for button in pairs(bagParent.itemButtons) do
if button.hasItem ~= nil then
button:Hide()
button:ClearAllPoints()
end
end
end
end
end
-- Toggle visibility
function BagFrame:Toggle()
if Guda_BagFrame:IsShown() then
Guda_BagFrame:Hide()
else
Guda_BagFrame:Show()
end
end
-- Show specific character's bags
function BagFrame:ShowCharacter(fullName)
currentViewChar = fullName
self:Update()
end
-- Show current character
function BagFrame:ShowCurrentCharacter()
currentViewChar = nil
self:Update()
end
-- Update lock states of existing buttons (lightweight, used during drag)
function BagFrame:UpdateLockStates()
Guda_UpdateLockStates(bagParents)
end
-- Update a single slot without full frame redraw (used for manual item moves)
-- Returns true if successful, false if full redraw is needed
function BagFrame:UpdateSingleSlot(bagID, slotID, passedButton)
if not Guda_BagFrame:IsShown() then return false end
if currentViewChar then return false end -- Can't do single-slot for other characters
-- Use passed button if available (from UpdateChangedSlots iteration)
-- This avoids type mismatch issues with slot ID keys (string vs number)
local targetButton = passedButton
if not targetButton then
-- Fallback: Find the button for this slot using lookup table
if slotToButton[bagID] then
-- Try both numeric and string keys to handle type mismatches
targetButton = slotToButton[bagID][slotID] or slotToButton[bagID][tonumber(slotID)]
end
-- Ultimate fallback: search itemButtons array
if not targetButton then
for _, button in ipairs(itemButtons) do
if button.bagID == bagID and button.slotID == slotID then
targetButton = button
break
end
end
end
end
if not targetButton then return false end
-- Get fresh item data for this slot
local itemLink = GetContainerItemLink(bagID, slotID)
local itemData = nil
if itemLink then
local texture, itemCount, locked, slotQuality = GetContainerItemInfo(bagID, slotID)
local itemID = nil
local _, _, idStr = string.find(itemLink, "item:(%d+)")
if idStr then itemID = tonumber(idStr) end
if itemID then
local name, link, quality, iLevel, _, itemType, stackCount, subType, _, equipLoc = GetItemInfo(itemID)
itemData = {
link = itemLink,
texture = texture,
count = itemCount or 1,
quality = quality or slotQuality or 0,
name = name,
iLevel = iLevel,
type = itemType,
subclass = subType,
equipLoc = equipLoc,
stackSize = stackCount or 1,
locked = locked,
}
end
end
-- Update the button
local matchesFilter = self:PassesSearchFilter(itemData)
Guda_ItemButton_SetItem(targetButton, bagID, slotID, itemData, false, nil, matchesFilter, false)
return true
end
-- Update changed slots in a bag by comparing with cached data
-- Returns number of slots updated, or -1 if full redraw is needed
function BagFrame:UpdateChangedSlots(bagID)
if not Guda_BagFrame:IsShown() then return -1 end
if currentViewChar then return -1 end
-- Check if we have the slot lookup table for this bag
if not slotToButton[bagID] then return -1 end
local viewType = addon.Modules.DB:GetSetting("bagViewType") or "single"
local isCategoryView = (viewType == "category")
if isCategoryView then
-- Category view: always do full redraw since items may change categories
return -1
else
-- In Single View: check all slots
local numSlots = GetContainerNumSlots(bagID)
if not numSlots or numSlots == 0 then return -1 end
local updatedCount = 0
for slotID = 1, numSlots do
-- Try both numeric and string keys to handle type mismatches
local targetButton = slotToButton[bagID][slotID] or slotToButton[bagID][tostring(slotID)]
if not targetButton then
return -1
end
local currentLink = GetContainerItemLink(bagID, slotID)
local cachedLink = targetButton.itemData and targetButton.itemData.link or nil
local needsUpdate = false
if currentLink ~= cachedLink then
needsUpdate = true
elseif currentLink then
local _, currentCount = GetContainerItemInfo(bagID, slotID)
local cachedCount = targetButton.itemData and targetButton.itemData.count or 0
if currentCount ~= cachedCount then
needsUpdate = true
end
end
if needsUpdate then
-- Pass the button directly to avoid type mismatch lookup issues
if self:UpdateSingleSlot(bagID, slotID, targetButton) then
updatedCount = updatedCount + 1
else
return -1
end
end
end
return updatedCount
end
end
-- Bag flyout state
local bagFlyout = nil
local bagFlyoutExpanded = false
-- Create flyout frame for bags 1-4
local function CreateBagFlyout(parent)
if bagFlyout then return bagFlyout end
local flyout = CreateFrame("Frame", "Guda_BagFlyout", parent)
flyout:SetFrameStrata("DIALOG")
flyout:SetFrameLevel(150)
local slotSize = 32
local padding = 4
local numBags = 4
flyout:SetWidth(slotSize + padding * 2)
flyout:SetHeight(slotSize * numBags + padding * 2)
flyout:SetBackdrop({
bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 12,
insets = { left = 2, right = 2, top = 2, bottom = 2 }
})
flyout:SetBackdropColor(0.12, 0.12, 0.12, 0.95)
flyout:SetBackdropBorderColor(0.30, 0.30, 0.30, 1)
flyout:SetPoint("BOTTOMRIGHT", parent, "BOTTOMLEFT", -15, -9)
flyout:EnableMouse(true)
flyout:Hide()
-- Create 4 bag slot buttons inside the flyout (bags 1-4, stacked bottom to top)
flyout.bagSlots = {}
for i = 1, numBags do
local bagID = i
local btn = CreateFrame("Button", "Guda_BagFlyout_Slot" .. i, flyout, "ItemButtonTemplate")
btn:SetWidth(slotSize)
btn:SetHeight(slotSize)
if i == 1 then
btn:SetPoint("BOTTOM", flyout, "BOTTOM", 0, padding)
else
btn:SetPoint("BOTTOM", flyout.bagSlots[i - 1], "TOP", 0, 0)
end
btn.bagID = bagID
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
-- Hide template borders
local normalTex = getglobal(btn:GetName() .. "NormalTexture")
if normalTex then normalTex:SetTexture(nil); normalTex:Hide() end
local iconBorder = getglobal(btn:GetName() .. "IconBorder")
if iconBorder then iconBorder:Hide() end
-- Apply footer button backdrop
Guda_BagSlot_ApplyBackdrop(btn)
-- Register for drag (equip bags)
btn:RegisterForDrag("LeftButton")
btn:SetScript("OnDragStart", function()
Guda_BagSlot_OnDragStart(this, this.bagID)
end)
btn:SetScript("OnDragStop", function()
Guda_BagSlot_OnDragStop(this, this.bagID)
end)
btn:SetScript("OnClick", function()
Guda_BagSlot_OnClick(this, this.bagID)
end)
btn:SetScript("OnEnter", function()
Guda_BagSlot_OnEnter(this, this.bagID)
end)
btn:SetScript("OnLeave", function()
GameTooltip:Hide()
Guda_BagFrame_ClearHighlightedSlots()
Guda_BagSlot_OnLeave(this, this.bagID)
end)
-- Register for bag updates
btn:RegisterEvent("BAG_UPDATE")
btn:RegisterEvent("ITEM_LOCK_CHANGED")
btn:RegisterEvent("CURSOR_UPDATE")
btn:RegisterEvent("UNIT_INVENTORY_CHANGED")
btn:SetScript("OnEvent", function()
Guda_BagSlot_OnEvent(this, event, arg1)
end)
-- Accept drops
btn:SetScript("OnReceiveDrag", function()
if this and this.bagID and this.bagID ~= 0 and CursorHasItem and CursorHasItem() then
local inv = ContainerIDToInventoryID(this.bagID)
if EquipCursorItem then
EquipCursorItem(inv)
elseif PutItemInBag then
PutItemInBag(inv)
end
Guda_BagSlot_Update(this, this.bagID)
if BagFrame and BagFrame.Update then BagFrame:Update() end
end
end)
flyout.bagSlots[i] = btn
end
bagFlyout = flyout
return flyout
end
-- Update flyout bag slot textures
local function UpdateBagFlyout()
if not bagFlyout then return end
for i, btn in ipairs(bagFlyout.bagSlots) do
Guda_BagSlot_Update(btn, btn.bagID)
end
end
-- Toggle the bag flyout
function BagFrame:ToggleBagFlyout()
local bag0 = getglobal("Guda_BagFrame_Toolbar_BagSlot0")
if not bag0 then return end
if not bagFlyout then
CreateBagFlyout(bag0)
end
bagFlyoutExpanded = not bagFlyoutExpanded
if bagFlyoutExpanded then
UpdateBagFlyout()
bagFlyout:Show()
-- Gold border when active
bag0:SetBackdropBorderColor(1, 0.82, 0, 1)
else
bagFlyout:Hide()
-- Restore theme border when inactive
local fb = addon.Modules.Theme:GetValue("footerButtonBorder") or { 0.30, 0.30, 0.30, 1 }
bag0:SetBackdropBorderColor(fb[1], fb[2], fb[3], fb[4])
end
end
-- Hide the bag flyout (used when frame closes, etc.)
function BagFrame:HideBagFlyout()
bagFlyoutExpanded = false
if bagFlyout then bagFlyout:Hide() end
-- Restore theme border on bag0
local bag0 = getglobal("Guda_BagFrame_Toolbar_BagSlot0")
if bag0 then
local fb = addon.Modules.Theme:GetValue("footerButtonBorder") or { 0.30, 0.30, 0.30, 1 }
bag0:SetBackdropBorderColor(fb[1], fb[2], fb[3], fb[4])
end
end
-- Update bagline layout
function BagFrame:UpdateBaglineLayout()
local hideFooter = addon.Modules.DB:GetSetting("hideFooter")
local toolbar = getglobal("Guda_BagFrame_Toolbar")
if not toolbar then return end
if hideFooter then
toolbar:Hide()
return
end
local hideBagline = addon.Modules.DB:GetSetting("hideBagline")
local bag0 = getglobal("Guda_BagFrame_Toolbar_BagSlot0")
local bag1 = getglobal("Guda_BagFrame_Toolbar_BagSlot1")
local bag2 = getglobal("Guda_BagFrame_Toolbar_BagSlot2")
local bag3 = getglobal("Guda_BagFrame_Toolbar_BagSlot3")
local bag4 = getglobal("Guda_BagFrame_Toolbar_BagSlot4")
local keyring = getglobal("Guda_BagFrame_Toolbar_KeyringButton")
local soulbag = getglobal("Guda_BagFrame_Toolbar_SoulBagButton")
local info = getglobal("Guda_BagFrame_Toolbar_BagSlotsInfo")
local hearthstone = getglobal("Guda_BagFrame_HearthstoneFrame")
-- Determine last visible special button for info anchor
local lastButton = keyring
if soulbag and soulbag:IsShown() then
lastButton = soulbag
end
-- Update backpack texture based on bagline setting
if bag0 then
if hideBagline then
SetItemButtonTexture(bag0, "Interface\\AddOns\\Guda\\Assets\\bags")
else
SetItemButtonTexture(bag0, "Interface\\Buttons\\Button-Backpack-Up")
end
end
if hideBagline then
-- Hide bags 1-4, show only bag0 + keyring + soulbag
if bag1 then bag1:Hide() end
if bag2 then bag2:Hide() end
if bag3 then bag3:Hide() end
if bag4 then bag4:Hide() end
-- Anchor keyring next to bag0
if keyring and bag0 then
keyring:ClearAllPoints()
keyring:SetPoint("LEFT", bag0, "RIGHT", 2, 0)
end
-- Anchor soul bag next to keyring
if soulbag and soulbag:IsShown() then
soulbag:ClearAllPoints()
soulbag:SetPoint("LEFT", keyring, "RIGHT", 2, 0)
end
-- Anchor info next to last button
if info then
info:Show()
info:ClearAllPoints()
info:SetPoint("LEFT", lastButton, "RIGHT", 8, 0)
end
-- Anchor hearthstone next to info
if hearthstone then
hearthstone:ClearAllPoints()
hearthstone:SetPoint("LEFT", info, "RIGHT", 6, 0)
end
else
-- Standard horizontal layout - all bags visible
if bag1 then
bag1:Show()
bag1:ClearAllPoints()
bag1:SetPoint("LEFT", bag0, "RIGHT", 2, 0)
end
if bag2 then
bag2:Show()
bag2:ClearAllPoints()
bag2:SetPoint("LEFT", bag1, "RIGHT", 2, 0)
end
if bag3 then
bag3:Show()
bag3:ClearAllPoints()
bag3:SetPoint("LEFT", bag2, "RIGHT", 2, 0)
end
if bag4 then
bag4:Show()
bag4:ClearAllPoints()
bag4:SetPoint("LEFT", bag3, "RIGHT", 2, 0)
end
if keyring then
keyring:Show()
keyring:ClearAllPoints()
keyring:SetPoint("LEFT", bag4, "RIGHT", 2, 0)
end
-- Anchor soul bag next to keyring
if soulbag and soulbag:IsShown() then
soulbag:ClearAllPoints()
soulbag:SetPoint("LEFT", keyring, "RIGHT", 2, 0)
end
if info then
info:Show()
info:ClearAllPoints()
info:SetPoint("LEFT", lastButton, "RIGHT", 8, 0)
end
-- Anchor hearthstone next to info
if hearthstone then
hearthstone:ClearAllPoints()
hearthstone:SetPoint("LEFT", info, "RIGHT", 6, 0)
end
-- Hide flyout when switching to full bagline
self:HideBagFlyout()
end
-- Update soul shard count
Guda_BagFrame_UpdateSoulBagCount()
end
-- Deferred update state for frame budgeting
local pendingUpdate = false
local updateDebounceFrame = nil
local UPDATE_DEBOUNCE_TIME = 0.05 -- 50ms debounce for rapid updates
-- Schedule an update with debouncing (prevents multiple updates in rapid succession)
function BagFrame:ScheduleUpdate()
if pendingUpdate then return end
pendingUpdate = true
if not updateDebounceFrame then
updateDebounceFrame = CreateFrame("Frame")
updateDebounceFrame.elapsed = 0
end
updateDebounceFrame.elapsed = 0
updateDebounceFrame:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + arg1
if this.elapsed >= UPDATE_DEBOUNCE_TIME then
this:SetScript("OnUpdate", nil)
pendingUpdate = false
BagFrame:Update()
end
end)
end
-- Update display
function BagFrame:Update()
if not Guda_BagFrame:IsShown() then
return
end
local viewType = addon.Modules.DB:GetSetting("bagViewType") or "single"
addon:DebugCategory("Update() START: viewType=%s", viewType)
-- Report entry for frame budget tracking
if addon.Modules.Utils and addon.Modules.Utils.ReportEntry then
addon.Modules.Utils:ReportEntry()
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
if CursorHasItem and CursorHasItem() then
-- Check if we have any displayed items
-- Use itemButtons hash instead of GetChildren() to avoid table allocation
local hasDisplayedItems = false
for _, bagParent in pairs(bagParents) do
if bagParent and bagParent.itemButtons then
for button in pairs(bagParent.itemButtons) do
if button.hasItem and button:IsShown() then
hasDisplayedItems = true
break
end
end
end
if hasDisplayedItems then break end
end
if hasDisplayedItems then
self:UpdateLockStates()
return
end
-- If no items displayed yet, continue with full update
end
-- Mark all existing buttons as not in use (we'll mark active ones during display)
-- Use itemButtons hash instead of GetChildren() to avoid table allocation
local totalButtonsBefore = 0
local shownButtonsBefore = 0
for _, bagParent in pairs(bagParents) do
if bagParent and bagParent.itemButtons then
for button in pairs(bagParent.itemButtons) do
if button.hasItem ~= nil then
totalButtonsBefore = totalButtonsBefore + 1
if button:IsShown() then
shownButtonsBefore = shownButtonsBefore + 1
end
button.inUse = false
end
end
end
end
addon:DebugCategory("Update() BEFORE: totalButtons=%d, shownButtons=%d", totalButtonsBefore, shownButtonsBefore)
local bagData
local isOtherChar = false
local charName = ""
local titleFont = getglobal("Guda_BagFrame_Title")
local displayName
if currentViewChar then
-- Viewing another character
bagData = addon.Modules.DB:GetCharacterBags(currentViewChar)
isOtherChar = true
charName = currentViewChar
local dash = string.find(currentViewChar, "-")
if dash then
displayName = string.sub(currentViewChar, 1, dash - 1)
else
displayName = currentViewChar
end
else
-- Viewing current character - use cached data for performance
bagData = addon.Modules.BagScanner:GetBagData()
displayName = UnitName("player") or "Character"
end
if titleFont and displayName then
titleFont:SetText(string.format("%s's Bags", displayName))
end
-- Display items
local viewType = addon.Modules.DB:GetSetting("bagViewType") or "single"
-- Clear itemButtons table and slot lookup before rebuilding (prevents stale references)
for k in pairs(itemButtons) do
itemButtons[k] = nil
end
for k in pairs(slotToButton) do
slotToButton[k] = nil
end
-- Reset all section headers before displaying items
local i = 1
while true do
local header = getglobal("Guda_BagFrame_SectionHeader" .. i)
if not header then break end
header.inUse = false
header:Hide()
i = i + 1
end
if viewType == "category" then
addon:DebugCategory("Update() calling DisplayItemsByCategory")
self:DisplayItemsByCategory(bagData, isOtherChar, charName)
addon:DebugCategory("Update() DisplayItemsByCategory returned, itemButtons count=%d", table.getn(itemButtons))
-- Show sort button with merge icon/tooltip for category view
local sortBtn = getglobal("Guda_BagFrame_SortButton")
if sortBtn then
sortBtn:Show()
sortBtn.isCategoryView = true
end
else
self:DisplayItems(bagData, isOtherChar, charName)
local sortBtn = getglobal("Guda_BagFrame_SortButton")
if sortBtn then
sortBtn:Show()
sortBtn.isCategoryView = false
end
end
-- Update money
self:UpdateMoney()
-- Update hearthstone
self:UpdateHearthstone()
-- Update bag slots info
self:UpdateBagSlotsInfo(bagData, isOtherChar)
-- Update bagline layout (hover option)
self:UpdateBaglineLayout()
-- Clean up unused buttons AFTER display is complete (prevents drag/drop issues)
-- Use itemButtons hash instead of GetChildren() to avoid table allocation
local hiddenCount = 0
local stillShownCount = 0
for _, bagParent in pairs(bagParents) do
if bagParent and bagParent.itemButtons then
for button in pairs(bagParent.itemButtons) do
if button.hasItem ~= nil and not button.inUse then
button:Hide()
button:ClearAllPoints()
hiddenCount = hiddenCount + 1
elseif button.hasItem ~= nil and button:IsShown() then
stillShownCount = stillShownCount + 1
end
end
end
end
addon:DebugCategory("Update() CLEANUP: hidden=%d, stillShown=%d", hiddenCount, stillShownCount)
-- Record performance metrics
if addon.Modules.Utils and addon.Modules.Utils.RecordUpdateEnd then
addon.Modules.Utils:RecordUpdateEnd()
end
addon:DebugCategory("Update() END")
end
-- Delegate to centralized helpers
function BagFrame:GetSectionHeader(index)
return Guda_GetSectionHeader("Guda_BagFrame", "Guda_BagFrame_ItemContainer", index)
end
function BagFrame:GetBagParent(bagID)
return Guda_GetBagParent("Guda_BagFrame", bagParents, bagID, "Guda_BagFrame_ItemContainer")
end
-- Display items by category
function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
local buttonSize = addon.Modules.DB:GetSetting("iconSize") or addon.Constants.BUTTON_SIZE
local spacing = addon.Modules.DB:GetSetting("iconSpacing") or addon.Constants.BUTTON_SPACING
local perRow = addon.Modules.DB:GetSetting("bagColumns") or 10
local itemContainer = getglobal("Guda_BagFrame_ItemContainer")
addon:DebugCategory("DisplayItemsByCategory START: buttonSize=%d, spacing=%d, perRow=%d", buttonSize, spacing, perRow)
-- Use centralized category initialization
local categories, specialItems = Guda_InitCategories()
local categoryList = Guda_CategoryList
-- Categorize all items using centralized function
-- Skip soul bags here; they are handled separately below (like keyring)
local totalItemsCategorized = 0
for _, bagID in ipairs(addon.Constants.BAGS) do
if not hiddenBags[bagID] then
local isSoulBag = false
if isOtherChar then
local bag = bagData[bagID]
isSoulBag = bag and bag.bagType == "soul"
else
isSoulBag = addon.Modules.Utils:GetSpecializedBagType(bagID) == "soul"
end
if not isSoulBag then
local bag = bagData[bagID]
if bag and bag.slots then
for slotID, itemData in pairs(bag.slots) do
if itemData then
Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, isOtherChar)
totalItemsCategorized = totalItemsCategorized + 1
end
end
end
end
end
end
addon:DebugCategory("DisplayItemsByCategory: totalItemsCategorized=%d", totalItemsCategorized)
-- Calculate total empty slots and find first available one for drop target
local totalFreeSlots = 0
local firstFreeBag, firstFreeSlot
for _, bagID in ipairs(addon.Constants.BAGS) do
if not hiddenBags[bagID] then
local bag = bagData[bagID]
if bag then
totalFreeSlots = totalFreeSlots + (bag.freeSlots or 0)
if not firstFreeBag and (bag.freeSlots or 0) > 0 then
for s = 1, (bag.numSlots or 0) do
if not bag.slots or not bag.slots[s] then
firstFreeBag = bagID
firstFreeSlot = s
break
end
end
end
end
end
end
-- Handle Keyring if visible
if showKeyring and not hiddenBags[-2] then
local bag = bagData[-2]
if bag and bag.slots then
for slotID, itemData in pairs(bag.slots) do
if itemData then
table.insert(categories["Keyring"], {bagID = -2, slotID = slotID, itemData = itemData})
end
end
end
end
-- Handle Soul Bags if visible
if showSoulBag then
for _, bagID in ipairs(addon.Constants.BAGS) do
if not hiddenBags[bagID] then
local isSoulBag = false
if isOtherChar then
local bag = bagData[bagID]
isSoulBag = bag and bag.bagType == "soul"
else
isSoulBag = addon.Modules.Utils:GetSpecializedBagType(bagID) == "soul"
end
if isSoulBag then
local bag = bagData[bagID]
if bag and bag.slots then
for slotID, itemData in pairs(bag.slots) do
if itemData then
table.insert(categories["Soul Bag"], {bagID = bagID, slotID = slotID, itemData = itemData})
end
end
end
end
end
end
end
-- Ensure Keyring and Soul Bag are in the display list when they have items
-- (they may be missing from categoryList if user customized category order)
local hasKeyringInList, hasSoulBagInList = false, false
for _, catName in ipairs(categoryList) do
if catName == "Keyring" then hasKeyringInList = true end
if catName == "Soul Bag" then hasSoulBagInList = true end
end
if not hasKeyringInList and categories["Keyring"] and table.getn(categories["Keyring"]) > 0 then
table.insert(categoryList, "Keyring")
end
if not hasSoulBagInList and categories["Soul Bag"] and table.getn(categories["Soul Bag"]) > 0 then
table.insert(categoryList, "Soul Bag")
end
-- Add recently emptied slots as empty placeholders in their respective categories
-- This preserves the visual position of items that were just moved out
if not isOtherChar then
for bagID, slots in pairs(bagRecentlyEmptiedSlots) do
if not hiddenBags[bagID] then
for slotID, info in pairs(slots) do
-- Only add if slot is actually empty (not refilled)
local currentLink = GetContainerItemLink(bagID, slotID)
if not currentLink then
local catName = info.category
if catName and categories[catName] then
table.insert(categories[catName], {
bagID = bagID,
slotID = slotID,
itemData = {
quality = info.quality or 0,
name = info.name or "",
iLevel = info.iLevel or 0,
},
isEmpty = true,
})
addon:DebugCategory("BagFrame: Added empty placeholder for %d:%d to category %s", bagID, slotID, catName)
end
else
-- Slot has item now, remove from tracking
slots[slotID] = nil
end
end
end
end
end
-- Layout
local startX, startY = 10, -10
local currentX, currentY = 0, 0
local rowMaxHeight = 0
local headerIdx = 1
local totalWidth = perRow * (buttonSize + spacing)
-- Frame budget tracking for category item processing
local categoryItemsProcessed = 0
local CATEGORY_ITEMS_PER_BUDGET_CHECK = 8
-- Check merged groups setting
local mergedGroups = addon.Modules.DB:GetSetting("mergedGroups") or {}
-- Build category order index map for sorting within merged groups
local catOrderIndex = {}
for idx, catId in ipairs(categoryList) do
catOrderIndex[catId] = idx
end
-- Build merged group display lists if any groups are merged
local mergedDisplayList = {} -- { { name, items, icon, catDef } }
local processedCats = {} -- Track which categories were merged
if addon.Modules.CategoryManager then
local groupsByName = addon.Modules.CategoryManager:GetCategoriesByGroup()
for groupName, catIds in pairs(groupsByName) do
if mergedGroups[groupName] then
-- Merge all items from categories in this group into one section
local mergedItems = {}
local mergedName = groupName
local mergedIcon = nil
for _, catId in ipairs(catIds) do
local items = categories[catId]
if items then
local orderIdx = catOrderIndex[catId] or 999
for _, item in ipairs(items) do
item.categoryOrderIndex = orderIdx
table.insert(mergedItems, item)
end
end
processedCats[catId] = true
end
if table.getn(mergedItems) > 0 then
table.insert(mergedDisplayList, {
name = mergedName,
items = mergedItems,
icon = nil, -- Group header, no single icon
})
end
end
end
end
-- Track total buttons created across all category blocks
local totalButtonsCreated = 0
-- Helper to render a category block
local function RenderCategoryBlock(catName, items, numItems, catDef, isEmptyCat)
addon:DebugCategory(" Category '%s': %d items, pos=(%d,%d)", catName, numItems, currentX, currentY)
Guda_SortCategoryItems(items)
local effectiveItems = numItems
if isEmptyCat then effectiveItems = 1 end
local blockCols = effectiveItems
if blockCols > perRow then blockCols = perRow end
local blockRows = math.ceil(effectiveItems / perRow)
local blockWidth = blockCols * (buttonSize + spacing)
local blockHeight = 20 + (blockRows * (buttonSize + spacing)) + 5
-- Check if it fits in current row
if currentX > 0 and currentX + blockWidth + 20 > totalWidth + 5 then
currentX = 0
currentY = currentY + rowMaxHeight
rowMaxHeight = 0
end
-- Add Header
local header = self:GetSectionHeader(headerIdx)
headerIdx = headerIdx + 1
header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", startX + currentX, startY - currentY)
header:SetWidth(blockWidth)
-- Get display name from category definition
local displayName = catName
if catDef and catDef.name then
displayName = catDef.name
end
-- Set item count (gated by showCategoryCount setting)
if header.countText then
local showCount = addon.Modules.DB:GetSetting("showCategoryCount")
if showCount == nil then showCount = true end
if showCount and numItems > 1 then
header.countText:SetText("(" .. numItems .. ")")
header.countText:Show()
else
header.countText:Hide()
end
end
header.fullName = displayName
header.isShortened = false
if string.len(displayName) > 8 and effectiveItems < 2 then
displayName = string.sub(displayName, 1, 6) .. "..."
header.isShortened = true
end
header.text:SetText(displayName)
header:Show()
local itemY = currentY + 20
local col = 0
local row = 0
if isEmptyCat then
-- Render empty slot indicator
local bagID = firstFreeBag or 0
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, false, isOtherChar and charName or nil, true, true)
button.isReadOnly = false
button.inUse = true
table.insert(itemButtons, button)
else
for _, item in ipairs(items) do
local bagID = item.bagID
local slot = item.slotID
-- For empty placeholders, pass nil itemData so button shows as empty slot
local itemData = item.isEmpty and nil or item.itemData
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 + (col * (buttonSize + spacing)), startY - (itemY + (row * (buttonSize + spacing))))
button:Show()
local matchesFilter = self:PassesSearchFilter(itemData)
Guda_ItemButton_SetItem(button, bagID, slot, itemData, false, isOtherChar and charName or nil, matchesFilter, isOtherChar)
button.inUse = true
button.isEmptyPlaceholder = item.isEmpty or false
table.insert(itemButtons, button)
if not slotToButton[bagID] then slotToButton[bagID] = {} end
slotToButton[bagID][slot] = button
-- Remove from recently emptied if slot now has an item
if itemData and not item.isEmpty then
self:UnmarkSlotAsEmptied(bagID, slot)
end
col = col + 1
if col >= blockCols then
col = 0
row = row + 1
end
categoryItemsProcessed = categoryItemsProcessed + 1
if categoryItemsProcessed >= CATEGORY_ITEMS_PER_BUDGET_CHECK then
categoryItemsProcessed = 0
if addon.Modules.Utils and addon.Modules.Utils.CheckTimeout and addon.Modules.Utils:CheckTimeout() then
addon.Modules.Utils:ReportEntry()
end
end
end
end
totalButtonsCreated = totalButtonsCreated + effectiveItems
if blockHeight > rowMaxHeight then rowMaxHeight = blockHeight end
currentX = currentX + blockWidth + 20
end
-- Render merged group sections first
for _, merged in ipairs(mergedDisplayList) do
local numItems = table.getn(merged.items)
if numItems > 0 then
RenderCategoryBlock(merged.name, merged.items, numItems, { name = merged.name, icon = merged.icon })
end
end
-- Render individual categories (skip merged ones and Empty)
for _, catName in ipairs(categoryList) do
if not processedCats[catName] then
local catDef = addon.Modules.CategoryManager and addon.Modules.CategoryManager:GetCategory(catName) or nil
-- Handle Empty category specially
if catDef and catDef.isEmptyCategory then
if totalFreeSlots > 0 and catDef.enabled then
RenderCategoryBlock(catName, {}, 0, catDef, true)
end
else
local items = categories[catName]
local numItems = items and table.getn(items) or 0
if numItems > 0 then
RenderCategoryBlock(catName, items, numItems, catDef, false)
end
end
end
end
addon:DebugCategory("DisplayItemsByCategory: totalButtonsCreated=%d from categories", totalButtonsCreated)
-- Update Y for bottom sections
local y = currentY + rowMaxHeight
-- Special sections at bottom (only Mounts now)
local bottomSections = {
{ name = "Mounts", items = specialItems.Mount },
}
local x = startX
y = startY - y
local hasAnyBottom = false
for _, sec in ipairs(bottomSections) do
if table.getn(sec.items) > 0 then
hasAnyBottom = true
break
end
end
if hasAnyBottom then
y = y - 10
local currentBottomX = 0
local sectionMaxHeight = 0
for _, sec in ipairs(bottomSections) do
local items = sec.items
local numItems = table.getn(items)
if numItems > 0 then
local blockCols = numItems
if blockCols > perRow then blockCols = perRow end
local blockRows = math.ceil(numItems / perRow)
local blockWidth = blockCols * (buttonSize + spacing)
local blockHeight = 20 + (blockRows * (buttonSize + spacing))
if currentBottomX > 0 and currentBottomX + blockWidth + 20 > totalWidth + 5 then
currentBottomX = 0
y = y - sectionMaxHeight - 5
sectionMaxHeight = 0
end
local header = self:GetSectionHeader(headerIdx)
headerIdx = headerIdx + 1
header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX, y)
header:SetWidth(blockWidth)
header.text:SetText(sec.name)
if header.countText then header.countText:SetText("(" .. numItems .. ")"); header.countText:Show() end
header:Show()
local itemY = y - 20
local sCol = 0
local sRow = 0
for _, item in ipairs(items) do
local bagParent = self:GetBagParent(item.bagID)
local button = Guda_GetItemButton(bagParent)
button:SetParent(bagParent)
button:SetWidth(buttonSize)
button:SetHeight(buttonSize)
button:ClearAllPoints()
button:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX + (sCol * (buttonSize + spacing)), itemY - (sRow * (buttonSize + spacing)))
button:Show()
Guda_ItemButton_SetItem(button, item.bagID, item.slotID, item.itemData, false, isOtherChar and charName or nil, self:PassesSearchFilter(item.itemData), isOtherChar)
button.inUse = true
table.insert(itemButtons, button)
if not slotToButton[item.bagID] then slotToButton[item.bagID] = {} end
slotToButton[item.bagID][item.slotID] = button
sCol = sCol + 1
if sCol >= blockCols then
sCol = 0
sRow = sRow + 1
end
end
if blockHeight > sectionMaxHeight then sectionMaxHeight = blockHeight end
currentBottomX = currentBottomX + blockWidth + 20
if currentBottomX >= totalWidth then
currentBottomX = 0
y = y - sectionMaxHeight - 5
sectionMaxHeight = 0
end
end
end
if currentBottomX > 0 then
y = y - sectionMaxHeight
end
end
local finalHeight = math.abs(y) + 20
itemContainer:SetHeight(finalHeight)
self:ResizeFrame(nil, nil, perRow, finalHeight)
addon:DebugCategory("DisplayItemsByCategory END: finalHeight=%d, itemButtons=%d", finalHeight, table.getn(itemButtons))
end
-- Display items
function BagFrame:DisplayItems(bagData, isOtherChar, charName)
local x, y = 10, -10
local row = 0
local col = 0
local buttonSize = addon.Modules.DB:GetSetting("iconSize") or addon.Constants.BUTTON_SIZE
local spacing = addon.Modules.DB:GetSetting("iconSpacing") or addon.Constants.BUTTON_SPACING
local perRow = addon.Modules.DB:GetSetting("bagColumns") or 10
local itemContainer = getglobal("Guda_BagFrame_ItemContainer")
-- Separate bags into regular, enchant, herb, soul, quiver, and ammo types
local regularBags = {}
local enchantBags = {}
local herbBags = {}
local soulBags = {}
local quiverBags = {}
local ammoBags = {}
for _, bagID in ipairs(addon.Constants.BAGS) do
-- Skip hidden bags
if not hiddenBags[bagID] then
local bagType
if isOtherChar then
-- For other characters, use saved bag type
local bag = bagData[bagID]
bagType = bag and bag.bagType or "regular"
else
-- For current character, detect bag type in real-time using unified detector
bagType = addon.Modules.Utils:GetSpecializedBagType(bagID) or "regular"
end
if bagType == "enchant" then
table.insert(enchantBags, bagID)
elseif bagType == "herb" then
table.insert(herbBags, bagID)
elseif bagType == "soul" then
table.insert(soulBags, bagID)
elseif bagType == "quiver" then
table.insert(quiverBags, bagID)
elseif bagType == "ammo" then
table.insert(ammoBags, bagID)
else
table.insert(regularBags, bagID)
end
end
end
-- Build display order: regular -> enchant -> herb -> soul -> quiver -> ammo -> keyring
local bagsToShow = {}
for _, bagID in ipairs(regularBags) do
table.insert(bagsToShow, {bagID = bagID, needsSpacing = false})
end
-- Enchant
if table.getn(enchantBags) > 0 then
for i, bagID in ipairs(enchantBags) do
table.insert(bagsToShow, {bagID = bagID, needsSpacing = (i == 1)})
end
end
-- Herb
if table.getn(herbBags) > 0 then
for i, bagID in ipairs(herbBags) do
table.insert(bagsToShow, {bagID = bagID, needsSpacing = (i == 1)})
end
end
-- Soul (only when soul bag toggle is active)
if showSoulBag and table.getn(soulBags) > 0 then
for i, bagID in ipairs(soulBags) do
table.insert(bagsToShow, {bagID = bagID, needsSpacing = (i == 1)})
end
end
-- Quiver
if table.getn(quiverBags) > 0 then
for i, bagID in ipairs(quiverBags) do
table.insert(bagsToShow, {bagID = bagID, needsSpacing = (i == 1)})
end
end
-- Ammo
if table.getn(ammoBags) > 0 then
for i, bagID in ipairs(ammoBags) do
table.insert(bagsToShow, {bagID = bagID, needsSpacing = (i == 1)})
end
end
-- Add keyring at the end if toggled on and not hidden
if showKeyring and not hiddenBags[-2] then
table.insert(bagsToShow, {bagID = -2, needsSpacing = true})
end
-- Frame budget tracking for item processing
local itemsProcessed = 0
local ITEMS_PER_BUDGET_CHECK = 8 -- Check budget every N items
for _, bagInfo in ipairs(bagsToShow) do
local bagID = bagInfo.bagID
local bag = bagData[bagID]
-- Add spacing before enchant, herb, soul, quiver, ammo, or keyring sections
if bagInfo.needsSpacing then
if col > 0 then
-- Move to next row if not at start of row
col = 0
row = row + 1
end
-- Add extra spacing (0.5 row for tighter spacing)
row = row + 0.5
end
-- Get slot count for this bag
local numSlots
if isOtherChar and bag and bag.numSlots then
-- Use stored slot count for other characters
numSlots = bag.numSlots
else
-- Use current character's bag slot count
numSlots = addon.Modules.Utils:GetBagSlotCount(bagID)
end
-- Only show bags that have slots
if numSlots and numSlots > 0 then
-- Iterate through ALL slots (1 to numSlots) to show empty slots too
-- Ensure a per-bag parent frame exists and carries the bag ID (Blizzard expects parent:GetID() == bagID)
local bagParent = self:GetBagParent(bagID)
for slot = 1, numSlots do
local itemData = bag and bag.slots and bag.slots[slot] or nil
-- Check if item matches search filter
local matchesFilter = self:PassesSearchFilter(itemData)
local button = Guda_GetItemButton(bagParent)
button.inUse = true -- Mark this button as actively in use
-- Position button
local xPos = x + (col * (buttonSize + spacing))
local yPos = y - (row * (buttonSize + spacing))
button:ClearAllPoints()
button:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", xPos, yPos)
-- Set item data with filter match info
-- isReadOnly = true when viewing other characters (can't interact with their items)
Guda_ItemButton_SetItem(button, bagID, slot, itemData, false, isOtherChar and charName or nil, matchesFilter, isOtherChar)
table.insert(itemButtons, button)
-- Populate slot lookup for O(1) access
if not slotToButton[bagID] then slotToButton[bagID] = {} end
slotToButton[bagID][slot] = button
-- Advance position
col = col + 1
if col >= perRow then
col = 0
row = row + 1
end
-- Frame budget check: periodically check if we've exceeded the frame budget
itemsProcessed = itemsProcessed + 1
if itemsProcessed >= ITEMS_PER_BUDGET_CHECK then
itemsProcessed = 0
if addon.Modules.Utils and addon.Modules.Utils.CheckTimeout and addon.Modules.Utils:CheckTimeout() then
-- Budget exceeded - reset entry time and continue
-- (For full deferral, we would queue remaining work, but that requires
-- more complex state management. For now, we just note the budget usage.)
addon.Modules.Utils:ReportEntry()
end
end
end
end
end
-- Resize frame dynamically based on content
self:ResizeFrame(row, col, perRow)
-- Ensure cooldown visuals are current after (re)building buttons
if self.RefreshCooldowns then
self:RefreshCooldowns()
end
end
-- Resize frame based on number of rows and columns
function BagFrame:ResizeFrame(currentRow, currentCol, columns, overrideHeight)
return Guda_ResizeFrame("Guda_BagFrame", "Guda_BagFrame_ItemContainer", currentRow, currentCol, columns, overrideHeight)
end
-- Check if search is currently active
function BagFrame:IsSearchActive()
return searchText and searchText ~= "" and searchText ~= "Search, try ~equipment"
end
-- Check if item passes search filter (pfUI style)
function BagFrame:PassesSearchFilter(itemData)
if not self:IsSearchActive() then return true end
return Guda_PassesSearchFilter(itemData, searchText)
end
function BagFrame:UpdateMoney()
local hideFooter = addon.Modules.DB:GetSetting("hideFooter")
local moneyFrame = getglobal("Guda_BagFrame_MoneyFrame")
if hideFooter then
if moneyFrame then moneyFrame:Hide() end
return
end
if not moneyFrame then
addon:Debug("Guda_BagFrame exists: " .. tostring(getglobal("Guda_BagFrame") ~= nil))
-- Try to create it manually
self:CreateMoneyFrame()
moneyFrame = getglobal("Guda_BagFrame_MoneyFrame")
end
if moneyFrame then
MoneyFrame_Update("Guda_BagFrame_MoneyFrame", GetMoney())
moneyFrame:Show()
-- Ensure tooltip overlay exists
self:EnsureMoneyTooltipOverlay()
-- Also add tooltip to toolbar empty space
self:SetupToolbarTooltip()
else
addon:Debug("Still couldn't find or create MoneyFrame!")
end
end
-- Bag type display names
local BAG_TYPE_NAMES = {
soul = "Soul Bag",
herb = "Herb Bag",
enchant = "Enchanting Bag",
quiver = "Quiver",
ammo = "Ammo Pouch",
}
-- Update bag slots info text (show only regular bags, special bags in tooltip)
function BagFrame:UpdateBagSlotsInfo(bagData, isOtherChar)
local infoText = getglobal("Guda_BagFrame_Toolbar_BagSlotsInfo_Text")
if not infoText then return end
local regularTotal = 0
local regularUsed = 0
local specialBags = {} -- { [type] = { total, used, name } }
-- Count slots in bags 0-4, separating regular from special
for _, bagID in ipairs(addon.Constants.BAGS) do
local bag = bagData[bagID]
-- Get slot count for this bag
local numSlots
if isOtherChar and bag and bag.numSlots then
numSlots = bag.numSlots
else
numSlots = addon.Modules.Utils:GetBagSlotCount(bagID)
end
if numSlots and numSlots > 0 then
-- Determine if this is a special bag
local bagType = nil
if not isOtherChar then
bagType = addon.Modules.Utils:GetSpecializedBagType(bagID)
elseif bag and bag.bagType and bag.bagType ~= "regular" then
bagType = bag.bagType
end
-- Count used slots
local used = 0
if bag and bag.slots then
for slot = 1, numSlots do
if bag.slots[slot] then
used = used + 1
end
end
end
if bagType then
-- Special bag
if not specialBags[bagType] then
specialBags[bagType] = { total = 0, used = 0, name = BAG_TYPE_NAMES[bagType] or bagType }
end
specialBags[bagType].total = specialBags[bagType].total + numSlots
specialBags[bagType].used = specialBags[bagType].used + used
else
-- Regular bag
regularTotal = regularTotal + numSlots
regularUsed = regularUsed + used
end
end
end
-- Format: "24 / 80" (used / total) - regular bags only
infoText:SetText(string.format("%d / %d", regularUsed, regularTotal))
infoText:SetTextColor(0.7, 0.7, 0.7)
-- Resize info frame to fit text so hearthstone anchors tightly
local infoFrame = getglobal("Guda_BagFrame_Toolbar_BagSlotsInfo")
if infoFrame then
local textWidth = infoText:GetStringWidth()
if textWidth and textWidth > 0 then
infoFrame:SetWidth(textWidth + 4)
end
infoFrame.regularTotal = regularTotal
infoFrame.regularUsed = regularUsed
infoFrame.specialBags = specialBags
-- Setup tooltip scripts if not already done
if not infoFrame.tooltipSetup then
infoFrame:EnableMouse(true)
infoFrame:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:AddLine("Bag Slots", 1, 1, 1)
GameTooltip:AddLine(" ")
-- Regular bags
if this.regularTotal then
GameTooltip:AddDoubleLine("Regular Bags:", string.format("%d / %d", this.regularUsed, this.regularTotal), 1, 1, 1, 0.8, 0.8, 0.8)
end
-- Special bags
if this.specialBags then
for bagType, data in pairs(this.specialBags) do
GameTooltip:AddDoubleLine(data.name .. ":", string.format("%d / %d", data.used, data.total), 1, 0.82, 0, 0.8, 0.8, 0.8)
end
end
GameTooltip:Show()
end)
infoFrame:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
infoFrame.tooltipSetup = true
end
end
end
function BagFrame:CreateMoneyFrame()
local moneyFrame = CreateFrame("Frame", "Guda_BagFrame_MoneyFrame", Guda_BagFrame, "SmallMoneyFrameTemplate")
moneyFrame:SetPoint("BOTTOMRIGHT", Guda_BagFrame, "BOTTOMRIGHT", -15, 10)
moneyFrame:SetWidth(180)
moneyFrame:SetHeight(35)
addon:Debug("MoneyFrame created via CreateMoneyFrame")
end
-- Hearthstone item ID
local HEARTHSTONE_ID = 6948
-- Create hearthstone display frame
function BagFrame:CreateHearthstoneFrame()
local frameName = "Guda_BagFrame_HearthstoneFrame"
if getglobal(frameName) then return end
local toolbar = getglobal("Guda_BagFrame_Toolbar") or Guda_BagFrame
local frame = CreateFrame("Button", frameName, toolbar)
frame:SetWidth(20)
frame:SetHeight(20)
-- Default anchor; will be repositioned by UpdateBaglineLayout
local info = getglobal("Guda_BagFrame_Toolbar_BagSlotsInfo")
if info then
frame:SetPoint("LEFT", info, "RIGHT", 6, 0)
else
frame:SetPoint("LEFT", toolbar, "LEFT", 0, 0)
end
frame:SetFrameLevel((toolbar:GetFrameLevel() or 5) + 5)
-- Icon texture
local icon = frame:CreateTexture(frameName .. "_Icon", "ARTWORK")
icon:SetAllPoints(frame)
icon:SetTexture("Interface\\Icons\\INV_Misc_Rune_01")
-- Cooldown frame (use Model type with CooldownFrameTemplate in vanilla)
local cooldown = CreateFrame("Model", frameName .. "_Cooldown", frame, "CooldownFrameTemplate")
cooldown:SetAllPoints(frame)
cooldown:EnableMouse(false)
frame.cooldown = cooldown
-- Enable mouse and clicks
frame:EnableMouse(true)
frame:RegisterForClicks("LeftButtonUp", "RightButtonUp")
frame:SetScript("OnEnter", function()
BagFrame:ShowHearthstoneTooltip(this)
end)
frame:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
frame:SetScript("OnClick", function()
BagFrame:UseHearthstone()
end)
frame:SetScript("OnMouseUp", function()
if arg1 == "LeftButton" or arg1 == "RightButton" then
BagFrame:UseHearthstone()
end
end)
addon:Debug("HearthstoneFrame created")
end
-- Find hearthstone in bags
function BagFrame:FindHearthstone()
for bag = 0, 4 do
local numSlots = GetContainerNumSlots(bag)
for slot = 1, numSlots do
local link = GetContainerItemLink(bag, slot)
if link then
local itemId = addon.Modules.Utils:ExtractItemID(link)
if itemId and itemId == HEARTHSTONE_ID then
return bag, slot, link
end
end
end
end
return nil, nil, nil
end
-- Update hearthstone display
function BagFrame:UpdateHearthstone()
local hideFooter = addon.Modules.DB:GetSetting("hideFooter")
local frame = getglobal("Guda_BagFrame_HearthstoneFrame")
if hideFooter then
if frame then frame:Hide() end
return
end
if not frame then
self:CreateHearthstoneFrame()
frame = getglobal("Guda_BagFrame_HearthstoneFrame")
end
if not frame then return end
local bag, slot, link = self:FindHearthstone()
if bag then
frame:SetAlpha(1)
frame:Show()
-- Update cooldown
local start, duration, enable = GetContainerItemCooldown(bag, slot)
if frame.cooldown and start and duration and duration > 0 then
CooldownFrame_SetTimer(frame.cooldown, start, duration, enable)
elseif frame.cooldown then
frame.cooldown:Hide()
end
-- Store location for use
frame.bag = bag
frame.slot = slot
frame.link = link
else
frame:SetAlpha(0.3)
frame:Show()
frame.bag = nil
frame.slot = nil
frame.link = nil
if frame.cooldown then
frame.cooldown:Hide()
end
end
end
-- Show hearthstone tooltip
function BagFrame:ShowHearthstoneTooltip(frame)
if frame.bag and frame.slot then
GameTooltip:SetOwner(frame, "ANCHOR_TOP")
GameTooltip:SetBagItem(frame.bag, frame.slot)
GameTooltip:Show()
else
GameTooltip:SetOwner(frame, "ANCHOR_TOP")
GameTooltip:ClearLines()
GameTooltip:AddLine("Hearthstone", 1, 1, 1)
GameTooltip:AddLine("Not in bags", 1, 0, 0)
GameTooltip:Show()
end
end
-- Use hearthstone
function BagFrame:UseHearthstone()
local frame = getglobal("Guda_BagFrame_HearthstoneFrame")
if frame and frame.bag and frame.slot then
UseContainerItem(frame.bag, frame.slot)
end
end
-- Setup tooltip on toolbar empty space
function BagFrame:SetupToolbarTooltip()
local toolbar = getglobal("Guda_BagFrame_Toolbar")
if not toolbar then return end
-- Only set up once
if toolbar.tooltipSetup then return end
-- Get the existing OnEnter script (if any)
local originalOnEnter = toolbar:GetScript("OnEnter")
-- Set new OnEnter that shows money tooltip
toolbar:SetScript("OnEnter", function()
-- Call original if it exists
if originalOnEnter then
originalOnEnter()
end
-- Show money tooltip
addon:Debug("Toolbar OnEnter - showing money tooltip")
Guda_BagFrame_MoneyOnEnter(getglobal("Guda_BagFrame_MoneyFrame"))
end)
-- Set OnLeave to hide tooltip
toolbar:SetScript("OnLeave", function()
addon:Debug("Toolbar OnLeave - hiding tooltip")
GameTooltip:Hide()
end)
toolbar.tooltipSetup = true
addon:Debug("Toolbar tooltip handlers set up")
end
-- Save bag frame position (always as BOTTOMRIGHT)
local function SaveBagFramePosition()
local frame = getglobal("Guda_BagFrame")
if not frame or not addon or not addon.Modules or not addon.Modules.DB then return end
-- Always save as BOTTOMRIGHT coordinates
local right = frame:GetRight()
local bottom = frame:GetBottom()
local screenWidth = GetScreenWidth()
if right and bottom and screenWidth then
local xOffset = right - screenWidth
local yOffset = bottom
addon.Modules.DB:SetSetting("bagFramePosition", {
point = "BOTTOMRIGHT",
x = xOffset,
y = yOffset
})
end
end
-- Create transparent overlay for money tooltip
function BagFrame:EnsureMoneyTooltipOverlay()
local overlayName = "Guda_BagFrame_MoneyTooltipOverlay"
local overlay = getglobal(overlayName)
if not overlay then
local moneyFrame = getglobal("Guda_BagFrame_MoneyFrame")
if not moneyFrame then return end
-- Create transparent overlay frame
overlay = CreateFrame("Frame", overlayName, moneyFrame)
overlay:SetAllPoints(moneyFrame)
overlay:SetFrameLevel(moneyFrame:GetFrameLevel() + 1)
overlay:EnableMouse(true)
-- Set tooltip handlers on overlay
overlay:SetScript("OnEnter", function()
addon:Debug("Money overlay OnEnter triggered")
Guda_BagFrame_MoneyOnEnter(moneyFrame)
end)
overlay:SetScript("OnLeave", function()
addon:Debug("Money overlay OnLeave triggered")
GameTooltip:Hide()
end)
-- Forward drag events to bag frame (if not locked)
overlay:SetScript("OnMouseDown", function()
local searchBox = getglobal("Guda_BagFrame_SearchBar_SearchBox")
if searchBox then
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()
end
end)
overlay:SetScript("OnMouseUp", function()
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()
end
end)
addon:Debug("Money tooltip overlay created")
end
overlay:Show()
end
-- Money frame OnLoad handler
function Guda_BagFrame_MoneyFrame_OnLoad(self)
addon:Debug("MoneyFrame OnLoad called for: " .. self:GetName())
-- Set tooltip handlers on all money denomination buttons
local buttons = {"GoldButton", "SilverButton", "CopperButton"}
for _, buttonName in ipairs(buttons) do
local fullName = self:GetName() .. buttonName
local button = getglobal(fullName)
addon:Debug("Looking for button: " .. fullName .. " - Found: " .. tostring(button ~= nil))
if button then
button:SetScript("OnEnter", function()
addon:Debug("Money button OnEnter triggered")
Guda_BagFrame_MoneyOnEnter(this:GetParent())
end)
button:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
end
end
-- Also try setting handlers directly on the parent frame
addon:Debug("Setting handlers on parent frame as fallback")
self:EnableMouse(true)
self:SetScript("OnEnter", function()
addon:Debug("Parent frame OnEnter triggered")
Guda_BagFrame_MoneyOnEnter(this)
end)
self:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
end
-- Money tooltip handler
function Guda_BagFrame_MoneyOnEnter(self)
if not self then return end
-- Anchor tooltip to TOPRIGHT - aligns tooltip's right edge with money container's right edge
GameTooltip:SetOwner(self, "ANCHOR_TOPRIGHT", 0, 0)
GameTooltip:ClearLines()
-- Get all characters and total (realm filtered, all factions)
local chars = addon.Modules.DB:GetAllCharacters(false, true)
local totalMoney = addon.Modules.DB:GetTotalMoney(false, true)
-- Header with current realm total - use colored money
GameTooltip:AddLine(
"Current realm gold: " .. addon.Modules.Utils:FormatMoney(totalMoney, false, true),
1, 0.82, 0
)
GameTooltip:AddLine(" ")
-- List each character with class-colored names
for _, char in ipairs(chars) do
-- Get class color from WoW's built-in table using English class token
local classToken = char.classToken
local classColor = classToken and (CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS)[classToken]
local colorR, colorG, colorB = 0.7, 0.7, 0.7
if classColor then
colorR, colorG, colorB = classColor.r, classColor.g, classColor.b
end
-- Create colored name
local coloredName = addon.Modules.Utils:ColorText(char.name, colorR, colorG, colorB)
GameTooltip:AddLine(
coloredName .. ": " .. addon.Modules.Utils:FormatMoney(char.money or 0, false, true)
)
end
GameTooltip:Show()
end
-- Dropdown management
local function Guda_BagCharacterMenu_Initialize()
local characters = addon.Modules.DB:GetAllCharacters(false, true)
local info
local currentPlayerFullName = addon.Modules.DB:GetPlayerFullName()
local currentViewChar = addon.Modules.BagFrame:GetCurrentViewChar()
for i, char in ipairs(characters) do
local charFullName = char.fullName
local charClassToken = char.classToken
-- Get class color
local classColor = charClassToken and RAID_CLASS_COLORS[charClassToken]
local r, g, b = 1, 1, 1
if classColor then
r, g, b = classColor.r, classColor.g, classColor.b
end
-- Create colored name
local coloredName = addon.Modules.Utils:ColorText(char.name, r, g, b)
info = {}
info.text = coloredName
info.func = function()
if charFullName == currentPlayerFullName then
addon.Modules.BagFrame:ShowCurrentCharacter()
else
addon.Modules.BagFrame:ShowCharacter(charFullName)
end
end
info.checked = (currentViewChar == charFullName or (not currentViewChar and charFullName == currentPlayerFullName))
UIDropDownMenu_AddButton(info)
end
end
-- Toggle character dropdown
function Guda_BagFrame_ToggleCharacterDropdown(button)
local menuFrame = getglobal("Guda_BagCharacterMenu")
if not menuFrame then
menuFrame = CreateFrame("Frame", "Guda_BagCharacterMenu", UIParent, "UIDropDownMenuTemplate")
end
UIDropDownMenu_Initialize(menuFrame, Guda_BagCharacterMenu_Initialize, "MENU")
ToggleDropDownMenu(1, nil, menuFrame, "cursor", 0, 0)
Guda_ScaleDropdownFonts(14)
end
-- Mailbox character dropdown
local function Guda_BagMailboxMenu_Initialize()
local characters = addon.Modules.DB:GetAllCharacters(false, true)
local info
for i, char in ipairs(characters) do
local charFullName = char.fullName
local charClassToken = char.classToken
-- Get class color
local classColor = charClassToken and RAID_CLASS_COLORS[charClassToken]
local r, g, b = 1, 1, 1
if classColor then
r, g, b = classColor.r, classColor.g, classColor.b
end
-- Create colored name
local coloredName = addon.Modules.Utils:ColorText(char.name, r, g, b)
info = {}
info.text = coloredName
info.func = function()
addon.Modules.MailboxFrame:ShowCharacter(charFullName)
if not Guda_MailboxFrame:IsShown() then
Guda_MailboxFrame:Show()
end
end
local mailboxViewChar = addon.Modules.MailboxFrame:GetCurrentViewChar()
info.checked = (mailboxViewChar == charFullName or (not mailboxViewChar and charFullName == addon.Modules.DB:GetPlayerFullName()))
UIDropDownMenu_AddButton(info)
end
end
-- Toggle mail dropdown
function Guda_BagFrame_ToggleMailDropdown(button)
local menuFrame = getglobal("Guda_BagMailboxMenu")
if not menuFrame then
menuFrame = CreateFrame("Frame", "Guda_BagMailboxMenu", UIParent, "UIDropDownMenuTemplate")
end
UIDropDownMenu_Initialize(menuFrame, Guda_BagMailboxMenu_Initialize, "MENU")
ToggleDropDownMenu(1, nil, menuFrame, "cursor", 0, 0)
Guda_ScaleDropdownFonts(14)
end
-- Bank character dropdown
local function Guda_BagBankMenu_Initialize()
local characters = addon.Modules.DB:GetAllCharacters(false, true)
local info
for i, char in ipairs(characters) do
local charFullName = char.fullName
local charClassToken = char.classToken
-- Get class color
local classColor = charClassToken and RAID_CLASS_COLORS[charClassToken]
local r, g, b = 1, 1, 1
if classColor then
r, g, b = classColor.r, classColor.g, classColor.b
end
-- Create colored name
local charName = char.name
local coloredName = addon.Modules.Utils:ColorText(charName, r, g, b)
info = {}
info.text = coloredName
info.func = function()
Guda_BagFrame_ShowCharacterBank(charFullName, charName)
end
local bankViewChar = addon.Modules.BankFrame:GetCurrentViewChar()
info.checked = (bankViewChar == charFullName or (not bankViewChar and charFullName == addon.Modules.DB:GetPlayerFullName()))
UIDropDownMenu_AddButton(info)
end
end
-- Toggle bank dropdown
function Guda_BagFrame_ToggleBankDropdown(button)
local menuFrame = getglobal("Guda_BagBankMenu")
if not menuFrame then
menuFrame = CreateFrame("Frame", "Guda_BagBankMenu", UIParent, "UIDropDownMenuTemplate")
end
UIDropDownMenu_Initialize(menuFrame, Guda_BagBankMenu_Initialize, "MENU")
ToggleDropDownMenu(1, nil, menuFrame, "cursor", 0, 0)
Guda_ScaleDropdownFonts(14)
end
-- Show character's bank
function Guda_BagFrame_ShowCharacterBank(fullName, displayName)
-- Use the existing BankFrame module
if not addon.Modules.BankFrame then
addon:Print("Bank frame module not available")
return
end
-- Position bank frame at center of screen
if Guda_BankFrame then
Guda_BankFrame:ClearAllPoints()
Guda_BankFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
end
-- Show the character's bank
addon.Modules.BankFrame:ShowCharacter(fullName)
-- Make sure frame is shown
if Guda_BankFrame then
Guda_BankFrame:Show()
end
end
-- Clear search and restore placeholder
function Guda_BagFrame_ClearSearch()
local searchBox = getglobal("Guda_BagFrame_SearchBar_SearchBox")
if searchBox then
searchBox:SetText("Search, try ~equipment")
searchBox:SetTextColor(0.5, 0.5, 0.5, 1)
searchBox:ClearFocus()
end
-- Reset search state
searchText = ""
BagFrame.foundFirstMatch = false
BagFrame.warnedAboutParsing = false
BagFrame.warnedAboutNoName = false
-- Update display
BagFrame:Update()
end
-- Search changed handler
function Guda_BagFrame_OnSearchChanged(self)
local text = self:GetText()
-- Ignore placeholder text
if text == "Search, try ~equipment" then
text = ""
end
if text ~= searchText then
searchText = text
BagFrame.foundFirstMatch = false -- Reset debug flags
BagFrame.warnedAboutParsing = false
BagFrame.warnedAboutNoName = false
BagFrame:Update()
end
end
-- Keyring toggle handler
function Guda_BagFrame_ToggleKeyring()
showKeyring = not showKeyring
-- Update button appearance to show toggle state
local button = getglobal("Guda_BagFrame_Toolbar_KeyringButton")
if button then
if showKeyring then
-- Gold border when active
button:SetBackdropBorderColor(1, 0.82, 0, 1)
else
-- Restore theme border when inactive
local fb = addon.Modules.Theme:GetValue("footerButtonBorder") or { 0.30, 0.30, 0.30, 1 }
button:SetBackdropBorderColor(fb[1], fb[2], fb[3], fb[4])
end
end
-- Refresh display
BagFrame:Update()
end
-- Soul Bag toggle handler
function Guda_BagFrame_ToggleSoulBag()
showSoulBag = not showSoulBag
-- Update button appearance to show toggle state
local button = getglobal("Guda_BagFrame_Toolbar_SoulBagButton")
if button then
if showSoulBag then
-- Gold border when active
button:SetBackdropBorderColor(1, 0.82, 0, 1)
else
-- Restore theme border when inactive
local fb = addon.Modules.Theme:GetValue("footerButtonBorder") or { 0.30, 0.30, 0.30, 1 }
button:SetBackdropBorderColor(fb[1], fb[2], fb[3], fb[4])
end
end
-- Refresh display
BagFrame:Update()
end
-- Update soul shard count on the soul bag button
function Guda_BagFrame_UpdateSoulBagCount()
local button = getglobal("Guda_BagFrame_Toolbar_SoulBagButton")
if not button or not button:IsShown() then return end
local count = 0
for _, bagID in ipairs(addon.Constants.BAGS) do
local bagType = addon.Modules.Utils:GetSpecializedBagType(bagID)
if bagType == "soul" then
local numSlots = addon.Modules.Utils:GetBagSlotCount(bagID)
for slotID = 1, numSlots do
local texture, itemCount = GetContainerItemInfo(bagID, slotID)
if texture then
count = count + (itemCount or 1)
end
end
end
end
button.soulShardCount = count
if button.countText then
if count > 0 then
button.countText:SetText(tostring(count))
button.countText:Show()
else
button.countText:SetText("")
button.countText:Hide()
end
end
end
function Guda_BagFrame_Sort()
if currentViewChar then
addon:Print("Cannot sort another character's bags!")
return
end
-- Check if we're in category view - restack and clean
local sortBtn = getglobal("Guda_BagFrame_SortButton")
if sortBtn and sortBtn.isCategoryView then
Guda_BagFrame_MergeStacks()
return
end
local success, message = addon.Modules.SortEngine:ExecuteSort(
function() return addon.Modules.SortEngine:SortBagsPass() end,
function() return addon.Modules.SortEngine:AnalyzeBags() end,
function() BagFrame:Update() end,
"bags"
)
if not success and message == "already sorted" then
addon:Print("Bags are already sorted!")
end
end
-- Restack and Clean (for category view) - merges stacks and refreshes view
-- Queue-based approach like BagShui
function Guda_BagFrame_MergeStacks()
if currentViewChar then
addon:Print("Cannot restack for another character!")
return
end
-- Check if sorting is already in progress
if addon.Modules.SortEngine.sortingInProgress then
addon:Print("Sorting already in progress, please wait...")
return
end
local bagIDs = addon.Constants.BAGS
local moveQueue = {}
-- Collect all partial stacks grouped by item
local partialStacks = {}
for _, bagID in ipairs(bagIDs) do
local numSlots = addon.Modules.Utils:GetBagSlotCount(bagID)
if numSlots and numSlots > 0 then
for slot = 1, numSlots do
local link = GetContainerItemLink(bagID, slot)
if link then
local texture, count = GetContainerItemInfo(bagID, slot)
local _, _, itemID = string.find(link, "item:(%d+)")
itemID = tonumber(itemID)
if itemID then
local _, _, _, _, _, _, itemStackCount = GetItemInfo(itemID)
local maxStack = tonumber(itemStackCount) or 1
-- Only track items that can stack and aren't full
if maxStack > 1 and count < maxStack then
local groupKey = tostring(itemID)
if not partialStacks[groupKey] then
partialStacks[groupKey] = {
maxStack = maxStack,
stacks = {}
}
end
table.insert(partialStacks[groupKey].stacks, {
bagID = bagID,
slot = slot,
count = count
})
end
end
end
end
end
end
-- Build move queue for each item group
for _, group in pairs(partialStacks) do
if table.getn(group.stacks) > 1 then
-- Sort stacks: larger stacks first (targets), smaller stacks last (sources)
table.sort(group.stacks, function(a, b)
if not a then return false end
if not b then return true end
return (a.count or 0) > (b.count or 0)
end)
local sourceLoopStart = table.getn(group.stacks)
-- Process targets from start, sources from end
for targetIdx = 1, table.getn(group.stacks) - 1 do
local target = group.stacks[targetIdx]
if target.count < group.maxStack and target.count > 0 then
for sourceIdx = sourceLoopStart, targetIdx + 1, -1 do
local source = group.stacks[sourceIdx]
if source.count > 0 and target.count < group.maxStack then
-- Queue this move
table.insert(moveQueue, {
source = source,
target = target,
maxStack = group.maxStack
})
-- Calculate changes (for queue planning)
local oldTargetCount = target.count
target.count = math.min(target.count + source.count, group.maxStack)
source.count = source.count - (target.count - oldTargetCount)
-- Move source pointer if depleted
if source.count == 0 then
sourceLoopStart = sourceLoopStart - 1
end
-- Stop if target is full
if target.count >= group.maxStack then
break
end
end
end
end
end
end
end
if table.getn(moveQueue) == 0 then
-- No stacks to merge — clear placeholders and refresh view
BagFrame:ClearRecentlyEmptiedSlots()
-- Clear item detection cache to force fresh tooltip scans
if addon.Modules.ItemDetection and addon.Modules.ItemDetection.ClearCache then
addon.Modules.ItemDetection:ClearCache()
end
BagFrame:Update()
addon:Print("View refreshed (no stacks to merge)")
return
end
-- Set sorting flag and update button appearance
addon.Modules.SortEngine.sortingInProgress = true
addon.Modules.SortEngine:UpdateSortButtonState(true)
-- Process queue with delays
local queueIndex = 1
local retryCount = 0
local totalMoves = table.getn(moveQueue)
local function ProcessNextMove()
if queueIndex > table.getn(moveQueue) then
addon.Modules.SortEngine.sortingInProgress = false
addon.Modules.SortEngine:UpdateSortButtonState(false)
-- Clear placeholders and item detection cache
BagFrame:ClearRecentlyEmptiedSlots()
if addon.Modules.ItemDetection and addon.Modules.ItemDetection.ClearCache then
addon.Modules.ItemDetection:ClearCache()
end
-- Refresh the view
BagFrame:Update()
addon:Print("Restacked " .. totalMoves .. " stack(s)")
return
end
local move = moveQueue[queueIndex]
local source = move.source
local target = move.target
-- Check if items are locked
local _, _, sourceLocked = GetContainerItemInfo(source.bagID, source.slot)
local _, _, targetLocked = GetContainerItemInfo(target.bagID, target.slot)
if sourceLocked or targetLocked then
retryCount = retryCount + 1
if retryCount < 10 then
-- Retry after delay
Guda_ScheduleTimer(0.3, ProcessNextMove)
return
else
-- Give up on this move
retryCount = 0
queueIndex = queueIndex + 1
Guda_ScheduleTimer(0.1, ProcessNextMove)
return
end
end
-- Perform the move
ClearCursor()
PickupContainerItem(source.bagID, source.slot)
PickupContainerItem(target.bagID, target.slot)
ClearCursor()
-- Move to next
retryCount = 0
queueIndex = queueIndex + 1
Guda_ScheduleTimer(0.15, ProcessNextMove)
end
ProcessNextMove()
end
-- NOTE: Guda_ScheduleTimer is defined in Core/Utils.lua (loaded earlier)
-- to ensure it's available for all modules that need timers
-- Hook bag container buttons to open Guda Bag View
local function HookBagContainers()
-- Hook the main bag container buttons (bags 1-4)
for i = 1, 4 do
local buttonName = "CharacterBag"..i.."Slot"
local button = getglobal(buttonName)
if button then
local originalOnClick = button:GetScript("OnClick")
button:SetScript("OnClick", function()
local mouseButton = arg1 or "LeftButton" -- Vanilla uses global arg1
if mouseButton == "LeftButton" then
-- Open Guda Bag View instead of default bag
BagFrame:Toggle()
else
-- Allow right-click and other buttons to work normally
if originalOnClick then
originalOnClick()
end
end
end)
end
end
-- Also hook the backpack button
local backpackButton = getglobal("MainMenuBarBackpackButton")
if backpackButton then
local originalOnClick = backpackButton:GetScript("OnClick")
backpackButton:SetScript("OnClick", function()
local mouseButton = arg1 or "LeftButton" -- Vanilla uses global arg1
if mouseButton == "LeftButton" then
-- Open Guda Bag View instead of default bag
BagFrame:Toggle()
else
-- Allow right-click and other buttons to work normally
if originalOnClick then
originalOnClick()
end
end
end)
end
-- Hook keyring button if it exists
local keyringButton = getglobal("KeyRingButton")
if keyringButton then
local originalOnClick = keyringButton:GetScript("OnClick")
keyringButton:SetScript("OnClick", function()
local mouseButton = arg1 or "LeftButton" -- Vanilla uses global arg1
if mouseButton == "LeftButton" then
-- Toggle keyring in Guda Bag View
Guda_BagFrame_ToggleKeyring()
BagFrame:Toggle() -- Also open the bag frame
else
-- Allow right-click and other buttons to work normally
if originalOnClick then
originalOnClick()
end
end
end)
end
end
-- Alternative approach: Completely replace the bag open functions
local function ReplaceBagOpenFunctions()
-- Override OpenBag (if it exists)
if OpenBag then
local originalOpenBag = OpenBag
function OpenBag(bagId)
if bagId and bagId >= 0 and bagId <= 4 then
-- For regular bags, open Guda Bag View
BagFrame:Toggle()
else
-- For other containers, use original function
if originalOpenBag then
originalOpenBag(bagId)
end
end
end
end
-- Override ToggleBag (if it exists)
if ToggleBag then
local originalToggleBag = ToggleBag
function ToggleBag(bagId)
if bagId and bagId >= 0 and bagId <= 4 then
-- For regular bags, toggle Guda Bag View
BagFrame:Toggle()
else
-- For other containers, use original function
if originalToggleBag then
originalToggleBag(bagId)
end
end
end
end
end
-- Hook to default bag opening
local function HookDefaultBags()
-- Override ToggleBackpack (if it exists)
if ToggleBackpack then
local originalToggleBackpack = ToggleBackpack
function ToggleBackpack()
BagFrame:Toggle()
-- Force disable pfUI bags if enabled
if pfUI and pfUI.bag and pfUI.bag.right and pfUI.bag.right.Hide then
pfUI.bag.right:Hide()
end
end
end
-- Override OpenAllBags (if it exists)
if OpenAllBags then
local originalOpenAllBags = OpenAllBags
function OpenAllBags()
Guda_BagFrame:Show()
-- Force disable pfUI bags if enabled
if pfUI and pfUI.bag and pfUI.bag.right and pfUI.bag.right.Hide then
pfUI.bag.right:Hide()
end
end
end
-- Override CloseAllBags (if it exists)
if CloseAllBags then
local originalCloseAllBags = CloseAllBags
function CloseAllBags()
-- Do not auto-close our bags when interacting with a vendor
if isMerchantOpen then
return
end
Guda_BagFrame:Hide()
end
end
-- Hook individual bag opening functions (for bag slot buttons)
ReplaceBagOpenFunctions()
-- Hook the bag slot button clicks directly
HookBagContainers()
end
-- Update lock state (controls whether frame is draggable)
function BagFrame:UpdateLockState()
-- Safety check: ensure addon and modules exist
if not addon or not addon.Modules then return end
local frame = getglobal("Guda_BagFrame")
if not frame then return end
-- Check if DB module is available
if not addon.Modules.DB or not addon.Modules.DB.GetSetting then return end
local success, isLocked = pcall(function()
return addon.Modules.DB:GetSetting("lockBags")
end)
if not success then return end
if isLocked == nil then
isLocked = false
end
-- Get draggable areas
local toolbar = getglobal("Guda_BagFrame_Toolbar")
local moneyFrame = getglobal("Guda_BagFrame_MoneyFrame")
local itemContainer = getglobal("Guda_BagFrame_ItemContainer")
if isLocked then
-- Disable dragging on main frame
if frame.SetScript then
frame:SetScript("OnMouseDown", function()
local searchBox = getglobal("Guda_BagFrame_SearchBar_SearchBox")
if searchBox then
searchBox:ClearFocus()
end
end)
frame:SetScript("OnMouseUp", nil)
end
-- Disable dragging on toolbar
if toolbar and toolbar.SetScript then
toolbar:SetScript("OnMouseDown", function()
local searchBox = getglobal("Guda_BagFrame_SearchBar_SearchBox")
if searchBox then
searchBox:ClearFocus()
end
end)
toolbar:SetScript("OnMouseUp", nil)
end
-- Disable dragging on money frame (preserve tooltip handlers on child buttons)
if moneyFrame and moneyFrame.SetScript then
moneyFrame:SetScript("OnMouseDown", function()
local searchBox = getglobal("Guda_BagFrame_SearchBar_SearchBox")
if searchBox then
searchBox:ClearFocus()
end
end)
moneyFrame:SetScript("OnMouseUp", nil)
end
-- Disable dragging on item container
if itemContainer and itemContainer.SetScript then
itemContainer:SetScript("OnMouseDown", function()
local searchBox = getglobal("Guda_BagFrame_SearchBar_SearchBox")
if searchBox then
searchBox:ClearFocus()
end
end)
itemContainer:SetScript("OnMouseUp", nil)
end
else
-- Enable dragging on main frame
if frame and frame.SetScript then
frame:SetScript("OnMouseDown", function()
local searchBox = getglobal("Guda_BagFrame_SearchBar_SearchBox")
if searchBox then
searchBox:ClearFocus()
end
local bagFrame = getglobal("Guda_BagFrame")
if bagFrame and arg1 == "LeftButton" then
bagFrame:StartMoving()
end
end)
frame:SetScript("OnMouseUp", function()
local bagFrame = getglobal("Guda_BagFrame")
if bagFrame then
bagFrame:StopMovingOrSizing()
SaveBagFramePosition()
end
end)
end
-- Enable dragging on toolbar (title area)
if toolbar and toolbar.SetScript then
toolbar:SetScript("OnMouseDown", function()
local searchBox = getglobal("Guda_BagFrame_SearchBar_SearchBox")
if searchBox then
searchBox:ClearFocus()
end
local bagFrame = getglobal("Guda_BagFrame")
if bagFrame and arg1 == "LeftButton" then
bagFrame:StartMoving()
end
end)
toolbar:SetScript("OnMouseUp", function()
local bagFrame = getglobal("Guda_BagFrame")
if bagFrame then
bagFrame:StopMovingOrSizing()
SaveBagFramePosition()
end
end)
end
-- Enable dragging on money frame (preserve tooltip handlers on child buttons)
if moneyFrame and moneyFrame.SetScript then
moneyFrame:SetScript("OnMouseDown", function()
local searchBox = getglobal("Guda_BagFrame_SearchBar_SearchBox")
if searchBox then
searchBox:ClearFocus()
end
local bagFrame = getglobal("Guda_BagFrame")
if bagFrame and arg1 == "LeftButton" then
bagFrame:StartMoving()
end
end)
moneyFrame:SetScript("OnMouseUp", function()
local bagFrame = getglobal("Guda_BagFrame")
if bagFrame then
bagFrame:StopMovingOrSizing()
SaveBagFramePosition()
end
end)
end
-- Enable dragging on item container
if itemContainer and itemContainer.SetScript then
itemContainer:SetScript("OnMouseDown", function()
local searchBox = getglobal("Guda_BagFrame_SearchBar_SearchBox")
if searchBox then
searchBox:ClearFocus()
end
local bagFrame = getglobal("Guda_BagFrame")
if bagFrame and arg1 == "LeftButton" then
bagFrame:StartMoving()
end
end)
itemContainer:SetScript("OnMouseUp", function()
local bagFrame = getglobal("Guda_BagFrame")
if bagFrame then
bagFrame:StopMovingOrSizing()
SaveBagFramePosition()
end
end)
end
end
end
-- Update border visibility based on setting
function BagFrame:UpdateBorderVisibility()
if not addon or not addon.Modules or not addon.Modules.DB then return end
local frame = getglobal("Guda_BagFrame")
if not frame then return end
-- Use Theme module if available
if addon.Modules.Theme then
addon.Modules.Theme:ApplyToFrame(frame)
return
end
local hideBorders = addon.Modules.DB:GetSetting("hideBorders")
if hideBorders == nil then
hideBorders = false
end
-- Use helper function with constants
if hideBorders then
addon:ApplyBackdrop(frame, "MINIMALIST_BORDER", "DEFAULT")
else
addon:ApplyBackdrop(frame, "DEFAULT_FRAME", "DEFAULT")
end
end
-- Update search bar visibility based on setting
function BagFrame:UpdateSearchBarVisibility()
if not addon or not addon.Modules or not addon.Modules.DB then return end
local searchBar = getglobal("Guda_BagFrame_SearchBar")
local itemContainer = getglobal("Guda_BagFrame_ItemContainer")
if not searchBar or not itemContainer then return end
local showSearchBar = addon.Modules.DB:GetSetting("showSearchBar")
if showSearchBar == nil then
showSearchBar = true
end
if showSearchBar then
searchBar:Show()
-- Anchor ItemContainer to SearchBar's bottom
itemContainer:ClearAllPoints()
itemContainer:SetPoint("TOP", searchBar, "BOTTOM", 0, -5)
else
searchBar:Hide()
-- Anchor ItemContainer directly to frame top (skip search bar space)
itemContainer:ClearAllPoints()
itemContainer:SetPoint("TOP", "Guda_BagFrame", "TOP", 0, -40)
end
end
-- Update footer visibility based on settings
function BagFrame:UpdateFooterVisibility()
local hideFooter = addon.Modules.DB:GetSetting("hideFooter")
local toolbar = getglobal("Guda_BagFrame_Toolbar")
local moneyFrame = getglobal("Guda_BagFrame_MoneyFrame")
local hearthstoneFrame = getglobal("Guda_BagFrame_HearthstoneFrame")
if hideFooter then
if toolbar then toolbar:Hide() end
if moneyFrame then moneyFrame:Hide() end
if hearthstoneFrame then hearthstoneFrame:Hide() end
else
if toolbar then toolbar:Show() end
if moneyFrame then moneyFrame:Show() end
if hearthstoneFrame then hearthstoneFrame:Show() end
-- Trigger layout updates to ensure they are correctly positioned
self:UpdateBaglineLayout()
self:UpdateMoney()
self:UpdateHearthstone()
end
end
-- Bag Slot Button Handlers
-- Apply footer button backdrop styling
function Guda_BagSlot_ApplyBackdrop(button)
local Theme = addon.Modules.Theme
button:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8x8",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
edgeSize = 8,
insets = { left = 2, right = 2, top = 2, bottom = 2 },
})
local fbBg = Theme:GetValue("footerButtonBg") or { 0.12, 0.12, 0.12, 1 }
local fbBorder = Theme:GetValue("footerButtonBorder") or { 0.30, 0.30, 0.30, 1 }
button:SetBackdropColor(fbBg[1], fbBg[2], fbBg[3], fbBg[4])
button:SetBackdropBorderColor(fbBorder[1], fbBorder[2], fbBorder[3], fbBorder[4])
end
-- OnLoad handler for bag slot buttons
function Guda_BagSlot_OnLoad(button, bagID)
-- Hide borders from ItemButtonTemplate
local buttonName = button:GetName()
-- Hide the normal texture border
local normalTexture = getglobal(buttonName .. "NormalTexture")
if normalTexture then
normalTexture:SetTexture(nil)
normalTexture:Hide()
end
-- Hide icon border
local iconBorder = getglobal(buttonName .. "IconBorder")
if iconBorder then
iconBorder:Hide()
end
-- Apply footer button backdrop
Guda_BagSlot_ApplyBackdrop(button)
-- Set up the button with proper ID
if bagID == 0 then
-- Backpack (bag 0)
button.bagID = 0
button.hasItem = 1
local hideBagline = addon.Modules.DB:GetSetting("hideBagline")
if hideBagline then
SetItemButtonTexture(button, "Interface\\AddOns\\Guda\\Assets\\bags")
else
SetItemButtonTexture(button, "Interface\\Buttons\\Button-Backpack-Up")
end
else
-- Bags 1-4
local invSlot = ContainerIDToInventoryID(bagID)
button:SetID(invSlot)
button.bagID = bagID
-- REGISTER FOR DRAG - This is crucial for Classic
button:RegisterForDrag("LeftButton")
-- Register for updates
button:RegisterEvent("BAG_UPDATE")
button:RegisterEvent("ITEM_LOCK_CHANGED")
button:RegisterEvent("CURSOR_UPDATE")
button:RegisterEvent("UNIT_INVENTORY_CHANGED")
-- Accept drops (equip when a bag is dropped on this slot)
button:SetScript("OnReceiveDrag", function()
if this and this.bagID and this.bagID ~= 0 and CursorHasItem and CursorHasItem() then
local inv = ContainerIDToInventoryID(this.bagID)
if EquipCursorItem then
EquipCursorItem(inv)
elseif PutItemInBag then
PutItemInBag(inv)
end
Guda_BagSlot_Update(this, this.bagID)
if BagFrame and BagFrame.Update then BagFrame:Update() end
end
end)
end
-- Ensure we handle right-click toggling like BankFrame
if button.RegisterForClicks then
button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
end
-- Initial update
Guda_BagSlot_Update(button, bagID)
end
function Guda_BagSlot_OnDragStart(frame, bagID)
if bagID == 0 then return end
-- Check if we should start dragging (only if there's an item)
local invSlot = ContainerIDToInventoryID(bagID)
local texture = GetInventoryItemTexture("player", invSlot)
if texture then
frame:SetAlpha(0.6)
-- Immediate pickup for Classic
PickupInventoryItem(invSlot)
-- Instantly reflect the change in UI (slot is now empty on cursor pickup)
Guda_BagSlot_Update(frame, bagID)
if BagFrame and BagFrame.Update then BagFrame:Update() end
end
-- If no texture (empty slot), do nothing - drag won't start
end
function Guda_BagSlot_OnDragStop(frame, bagID)
frame:SetAlpha(1.0)
end
-- Update bag slot button texture
function Guda_BagSlot_Update(button, bagID)
local isHidden = hiddenBags[bagID]
if bagID == 0 then
-- Backpack texture depends on bagline setting
local hideBagline = addon.Modules.DB:GetSetting("hideBagline")
if hideBagline then
SetItemButtonTexture(button, "Interface\\AddOns\\Guda\\Assets\\bags")
else
SetItemButtonTexture(button, "Interface\\Buttons\\Button-Backpack-Up")
end
-- Dim if hidden
if isHidden then
SetItemButtonTextureVertexColor(button, 0.4, 0.4, 0.4)
else
SetItemButtonTextureVertexColor(button, 1.0, 1.0, 1.0)
end
return
end
-- Get the inventory slot ID for this bag
local invSlot = ContainerIDToInventoryID(bagID)
local texture = GetInventoryItemTexture("player", invSlot)
if texture then
-- Bag is equipped
SetItemButtonTexture(button, texture)
-- Dim if hidden
if isHidden then
SetItemButtonTextureVertexColor(button, 0.4, 0.4, 0.4)
else
SetItemButtonTextureVertexColor(button, 1.0, 1.0, 1.0)
end
else
-- No bag in this slot
SetItemButtonTexture(button, "Interface\\PaperDoll\\UI-PaperDoll-Slot-Bag")
SetItemButtonTextureVertexColor(button, 0.5, 0.5, 0.5)
end
end
-- OnEvent handler
function Guda_BagSlot_OnEvent(button, event, arg1)
local bagID = button.bagID
if not bagID then
return
end
if event == "BAG_UPDATE" then
if arg1 == bagID then
Guda_BagSlot_Update(button, bagID)
end
elseif event == "UNIT_INVENTORY_CHANGED" then
if arg1 == "player" then
Guda_BagSlot_Update(button, bagID)
end
elseif event == "ITEM_LOCK_CHANGED" or event == "CURSOR_UPDATE" then
Guda_BagSlot_Update(button, bagID)
end
end
-- OnClick handler
function Guda_BagSlot_OnClick(button, bagID)
local which = arg1 -- Vanilla uses global arg1 for mouse button name
-- Right-Click: toggle visibility
if which == "RightButton" then
hiddenBags[bagID] = not hiddenBags[bagID]
-- Update bag slot visual (dim/undim)
Guda_BagSlot_Update(button, bagID)
-- Refresh the bag display
BagFrame:Update()
return
end
-- Left-Click on bag0: toggle flyout if bagline is hidden
if which == "LeftButton" and bagID == 0 then
local hideBagline = addon.Modules.DB:GetSetting("hideBagline")
if hideBagline then
BagFrame:ToggleBagFlyout()
return
end
end
-- Left-Click: equip bag from cursor into this slot (bags 1-4 only)
if which == "LeftButton" then
if bagID ~= 0 and CursorHasItem and CursorHasItem() then
local invSlot = ContainerIDToInventoryID(bagID)
if EquipCursorItem then
EquipCursorItem(invSlot)
else
if PutItemInBag then PutItemInBag(invSlot) end
end
-- Update visuals after attempted equip
Guda_BagSlot_Update(button, bagID)
BagFrame:Update()
end
return
end
end
-- OnEnter handler for tooltip
function Guda_BagSlot_OnEnter(button, bagID)
GameTooltip:SetOwner(button, "ANCHOR_TOP")
if bagID == 0 then
-- Backpack tooltip
GameTooltip:SetText("Backpack", 1.0, 1.0, 1.0)
local numSlots = GetContainerNumSlots(0)
GameTooltip:AddLine(string.format("%d Slots", numSlots), 0.8, 0.8, 0.8)
if hiddenBags[bagID] then
GameTooltip:AddLine("(Hidden - Right-Click to show)", 0.8, 0.5, 0.5)
else
GameTooltip:AddLine("(Right-Click to hide)", 0.5, 0.8, 0.5)
end
Guda_BagFrame_HighlightBagSlots(0)
elseif bagID == -2 then
-- Keyring tooltip
GameTooltip:SetText("Keyring", 1.0, 1.0, 1.0)
local numSlots = GetContainerNumSlots(-2) or 0
GameTooltip:AddLine(string.format("%d Slots", numSlots), 0.8, 0.8, 0.8)
if hiddenBags[bagID] then
GameTooltip:AddLine("(Hidden - Right-Click to show)", 0.8, 0.5, 0.5)
else
GameTooltip:AddLine("(Right-Click to hide)", 0.5, 0.8, 0.5)
end
Guda_BagFrame_HighlightBagSlots(-2)
else
-- Bag slot tooltip
local invSlot = ContainerIDToInventoryID(bagID)
local hasItem = GetInventoryItemTexture("player", invSlot)
if hasItem then
-- Show bag item tooltip
GameTooltip:SetInventoryItem("player", invSlot)
if hiddenBags[bagID] then
GameTooltip:AddLine("(Hidden - Right-Click to show)", 0.8, 0.5, 0.5)
else
GameTooltip:AddLine("(Right-Click to hide)", 0.5, 0.8, 0.5)
end
Guda_BagFrame_HighlightBagSlots(bagID)
else
-- Empty slot
GameTooltip:SetText(string.format("Bag %d", bagID), 1.0, 1.0, 1.0)
GameTooltip:AddLine("Empty", 0.5, 0.5, 0.5)
if hiddenBags[bagID] then
GameTooltip:AddLine("(Hidden - Right-Click to show)", 0.8, 0.5, 0.5)
else
GameTooltip:AddLine("(Right-Click to hide)", 0.5, 0.8, 0.5)
end
Guda_BagFrame_HighlightBagSlots(bagID)
end
end
GameTooltip:Show()
end
-- OnLeave handler for tooltip
function Guda_BagSlot_OnLeave(button, bagID)
end
-- Highlight all item slots belonging to a specific bag by dimming others
function Guda_BagFrame_HighlightBagSlots(bagID)
-- Use itemButtons hash instead of GetChildren() to avoid table allocation
local highlightCount, dimCount = 0, 0
for _, bagParent in pairs(bagParents) do
if bagParent and bagParent.itemButtons then
for button in pairs(bagParent.itemButtons) do
if button and button:IsShown() and button.hasItem ~= nil and not button.isBagSlot then
if button.bagID == bagID then
button:SetAlpha(1.0)
highlightCount = highlightCount + 1
else
button:SetAlpha(0.25)
dimCount = dimCount + 1
end
end
end
end
end
end
-- Clear all highlighting by restoring full opacity to all slots
function Guda_BagFrame_ClearHighlightedSlots()
-- Restore alpha to whatever the search filter dictates (pfUI style). If no search, full opacity.
-- Use itemButtons hash instead of GetChildren() to avoid table allocation
local searchActive = BagFrame and BagFrame.IsSearchActive and BagFrame:IsSearchActive()
for _, bagParent in pairs(bagParents) do
if bagParent and bagParent.itemButtons then
for button in pairs(bagParent.itemButtons) do
if button and button:IsShown() and button.hasItem ~= nil and not button.isBagSlot then
if searchActive and BagFrame and BagFrame.PassesSearchFilter then
local matches = BagFrame:PassesSearchFilter(button.itemData)
button:SetAlpha(matches and 1.0 or 0.25)
else
button:SetAlpha(1.0)
end
end
end
end
end
end
-- Highlight a specific bag button in the toolbar
function Guda_BagFrame_HighlightBagButton(bagID)
if not bagID then return end
local buttonName
if bagID == -2 then
-- Keyring button
buttonName = "Guda_BagFrame_Toolbar_KeyringButton"
elseif bagID >= 0 and bagID <= 4 then
-- Bag buttons 0-4 (0 is backpack)
buttonName = "Guda_BagFrame_Toolbar_BagSlot" .. bagID
else
return
end
local button = getglobal(buttonName)
if button then
-- Set the button's pushed texture to highlight it
button:LockHighlight()
end
end
-- Clear bag button highlighting
function Guda_BagFrame_ClearBagButtonHighlight()
-- Clear highlight from all bag buttons (0-4)
for bagID = 0, 4 do
local buttonName = "Guda_BagFrame_Toolbar_BagSlot" .. bagID
local button = getglobal(buttonName)
if button then
button:UnlockHighlight()
end
end
-- Clear keyring button highlight
local keyringButton = getglobal("Guda_BagFrame_Toolbar_KeyringButton")
if keyringButton then
keyringButton:UnlockHighlight()
end
-- Clear soul bag button highlight
local soulBagButton = getglobal("Guda_BagFrame_Toolbar_SoulBagButton")
if soulBagButton then
soulBagButton:UnlockHighlight()
end
end
-- Initialize
function BagFrame:Initialize()
-- Hook default bag functions (with slight delay to ensure UI is loaded)
local frame = CreateFrame("Frame")
frame:RegisterEvent("PLAYER_LOGIN")
frame:SetScript("OnEvent", function()
HookDefaultBags()
-- Re-hook when character frame is opened (for safety)
local charFrame = getglobal("CharacterFrame")
if charFrame then
local originalShow = charFrame:GetScript("OnShow")
charFrame:SetScript("OnShow", function()
HookBagContainers()
if originalShow then
originalShow()
end
end)
end
end)
--=====================================================
-- Efficient Update Throttling System
-- Uses a single reusable frame and true debouncing
-- (resets timer when new events come in)
--=====================================================
local updateThrottle = {
frame = nil,
pending = false,
delay = 0.1, -- Default delay (100ms)
elapsed = 0,
minDelay = 0.05, -- Minimum delay (50ms) for responsive feel
maxDelay = 0.3, -- Maximum delay (300ms) during heavy operations
}
-- Initialize the throttle frame (created once, reused)
local function GetThrottleFrame()
if not updateThrottle.frame then
updateThrottle.frame = CreateFrame("Frame", "Guda_BagUpdateThrottle", UIParent)
updateThrottle.frame:Hide()
updateThrottle.frame:SetScript("OnUpdate", function()
updateThrottle.elapsed = updateThrottle.elapsed + arg1
if updateThrottle.elapsed >= updateThrottle.delay then
updateThrottle.frame:Hide()
updateThrottle.pending = false
updateThrottle.elapsed = 0
-- Only update if frame is shown and viewing current character
if not currentViewChar and Guda_BagFrame and Guda_BagFrame:IsShown() then
BagFrame:Update()
end
end
end)
end
return updateThrottle.frame
end
-- Schedule a debounced BagFrame update
-- If already pending, resets the timer (true debounce behavior)
local function ScheduleBagFrameUpdate(delay)
delay = delay or updateThrottle.minDelay
-- Clamp delay to reasonable bounds
if delay < updateThrottle.minDelay then
delay = updateThrottle.minDelay
elseif delay > updateThrottle.maxDelay then
delay = updateThrottle.maxDelay
end
-- Use longer delay if sorting is in progress
if addon.Modules.SortEngine and addon.Modules.SortEngine.sortingInProgress then
delay = updateThrottle.maxDelay
end
updateThrottle.delay = delay
updateThrottle.elapsed = 0 -- Reset timer (true debounce)
if not updateThrottle.pending then
updateThrottle.pending = true
GetThrottleFrame():Show()
end
end
-- Cancel any pending update (useful when frame is hidden)
local function CancelPendingUpdate()
if updateThrottle.frame then
updateThrottle.frame:Hide()
end
updateThrottle.pending = false
updateThrottle.elapsed = 0
end
-- Update on bag changes (debounced to prevent lag on rapid bag updates)
-- Register directly to access arg1 (bagID that changed) for incremental cache updates
local bagUpdateFrame = CreateFrame("Frame")
bagUpdateFrame:RegisterEvent("BAG_UPDATE")
bagUpdateFrame:SetScript("OnEvent", function()
if currentViewChar then return end
if not Guda_BagFrame:IsShown() then return end
-- Only handle player bags (0-4) - use tonumber for safe comparison
local bagID = tonumber(arg1)
if not bagID or bagID < 0 or bagID > 4 then
return -- Skip bank bags (5-10) and invalid bags
end
local viewType = addon.Modules.DB:GetSetting("bagViewType") or "single"
addon:DebugCategory("BAG_UPDATE (BagFrame): bagID=%d, viewType=%s", bagID, viewType)
-- Check if sorting is in progress - use full redraw with throttle
local isSorting = addon.Modules.SortEngine and addon.Modules.SortEngine.sortingInProgress
if not isSorting then
-- Try incremental update for manual item moves
-- NOTE: Don't clear ItemDetection cache - item properties don't change on move
addon.Modules.BagScanner:InvalidateBag(arg1)
-- Try to update only changed slots in this bag
local result = BagFrame:UpdateChangedSlots(arg1)
addon:DebugCategory("BAG_UPDATE: UpdateChangedSlots result=%s", tostring(result))
if result >= 0 then
-- Success - updated slots without full redraw
-- Cancel any pending full redraw to preserve incremental update
CancelPendingUpdate()
addon:DebugCategory("BAG_UPDATE: Incremental update succeeded, skipping full redraw")
return
end
-- Fall through to full redraw if incremental update failed
addon:DebugCategory("BAG_UPDATE: Incremental update failed, doing full redraw")
end
-- In category view, detect slots that just became empty to show placeholders
if viewType == "category" and not isSorting then
local numSlots = GetContainerNumSlots(bagID)
if numSlots and numSlots > 0 then
for slotID = 1, numSlots do
local currentLink = GetContainerItemLink(bagID, slotID)
local button = slotToButton[bagID] and slotToButton[bagID][slotID]
if button and button.hasItem and not currentLink and not button.isEmptyPlaceholder then
-- This slot had an item but is now empty — mark as recently emptied
local oldCategory = nil
if button.itemData and addon.Modules.CategoryManager then
oldCategory = addon.Modules.CategoryManager:CategorizeItem(button.itemData, bagID, slotID)
end
BagFrame:MarkSlotAsEmptied(bagID, slotID, oldCategory, button.itemData)
end
end
end
end
-- Sorting in progress or incremental update failed - use throttled full redraw
addon.Modules.BagScanner:InvalidateBag(arg1)
ScheduleBagFrameUpdate(0.1)
end)
-- Update item cooldown overlays when item cooldowns change
addon.Modules.Events:Register("BAG_UPDATE_COOLDOWN", function()
if not currentViewChar then
if BagFrame.RefreshCooldowns then
BagFrame:RefreshCooldowns()
end
end
end, "BagFrame")
-- Update on money changes
addon.Modules.Events:OnMoneyChanged(function()
BagFrame:UpdateMoney()
end, "BagFrame")
-- Update when items get locked/unlocked (debounced for trading, mailing, etc.)
local lockUpdatePending = false
addon.Modules.Events:Register("ITEM_LOCK_CHANGED", function()
if currentViewChar then return end
if not Guda_BagFrame:IsShown() then return end
-- In Category View, debounce lock state updates (fires rapidly during drags)
local viewType = addon.Modules.DB:GetSetting("bagViewType") or "single"
if viewType == "category" then
if not lockUpdatePending then
lockUpdatePending = true
Guda_ScheduleTimer(0.05, function()
lockUpdatePending = false
if Guda_BagFrame:IsShown() and not currentViewChar then
BagFrame:UpdateLockStates()
end
end)
end
return
end
-- Use slightly longer delay for lock changes in single view (they fire rapidly during drags)
ScheduleBagFrameUpdate(0.15)
end, "BagFrame")
-- Auto-open bag frame when interacting with mail, bank, auction, trade
local function AutoOpenBags()
local autoOpen = addon.Modules.DB:GetSetting("autoOpenBags")
if autoOpen == nil then autoOpen = true end
if autoOpen then
Guda_BagFrame:Show()
end
end
addon.Modules.Events:Register("MAIL_SHOW", AutoOpenBags, "BagFrame")
addon.Modules.Events:Register("BANKFRAME_OPENED", AutoOpenBags, "BagFrame")
addon.Modules.Events:Register("AUCTION_HOUSE_SHOW", AutoOpenBags, "BagFrame")
addon.Modules.Events:Register("TRADE_SHOW", AutoOpenBags, "BagFrame")
-- Track vendor interactions to avoid closing bags when a vendor is opened
addon.Modules.Events:Register("MERCHANT_SHOW", function()
isMerchantOpen = true
-- Auto-open bags when visiting a vendor (respects setting)
local autoOpen = addon.Modules.DB:GetSetting("autoOpenBags")
if autoOpen == nil then autoOpen = true end
if autoOpen then
local frameRef = getglobal("Guda_BagFrame")
if frameRef and not frameRef:IsShown() then
frameRef:Show()
end
end
-- Auto-sell junk items (spread across frames to avoid item locking)
local autoVendor = addon.Modules.DB:GetSetting("autoVendorJunk")
if autoVendor == nil then autoVendor = true end
if autoVendor then
local junkItems = {}
for bag = 0, 4 do
local numSlots = GetContainerNumSlots(bag)
for slot = 1, numSlots do
local link = GetContainerItemLink(bag, slot)
if link and string.find(link, "|cff9d9d9d") then
table.insert(junkItems, { bag = bag, slot = slot })
end
end
end
if table.getn(junkItems) > 0 then
local idx = 0
local soldCount = 0
local sellFrame = CreateFrame("Frame")
sellFrame:SetScript("OnUpdate", function()
if not isMerchantOpen then
this:SetScript("OnUpdate", nil)
if soldCount > 0 then
addon:Print("Sold " .. soldCount .. " junk item(s).")
end
return
end
idx = idx + 1
local item = junkItems[idx]
if item then
local link = GetContainerItemLink(item.bag, item.slot)
if link and string.find(link, "|cff9d9d9d") then
UseContainerItem(item.bag, item.slot)
soldCount = soldCount + 1
end
else
this:SetScript("OnUpdate", nil)
if soldCount > 0 then
addon:Print("Sold " .. soldCount .. " junk item(s).")
end
end
end)
end
end
end, "BagFrame")
addon.Modules.Events:Register("MERCHANT_CLOSED", function()
isMerchantOpen = false
end, "BagFrame")
-- Hide character dropdown when clicking on bag frame
local bagFrame = getglobal("Guda_BagFrame")
if bagFrame then
local originalOnMouseDown = bagFrame:GetScript("OnMouseDown")
bagFrame:SetScript("OnMouseDown", function()
if originalOnMouseDown then
originalOnMouseDown()
end
end)
end
addon:Debug("Bag frame initialized")
end
-- Refresh cooldown overlays for all visible item buttons
function BagFrame:RefreshCooldowns()
-- Use itemButtons hash instead of GetChildren() to avoid table allocation
for _, bagParent in pairs(bagParents) do
if bagParent and bagParent.itemButtons then
for button in pairs(bagParent.itemButtons) do
if button and button.hasItem and button:IsShown() and Guda_ItemButton_UpdateCooldown then
Guda_ItemButton_UpdateCooldown(button)
end
end
end
end
end