-- 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 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 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() -- 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 -- In Category View: update slots that HAVE button mappings -- AND check for NEW items that arrived in slots without buttons local updatedCount = 0 for slotID, targetButton in pairs(slotToButton[bagID]) do 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 -- CRITICAL: Check for NEW items that arrived in slots WITHOUT button mappings -- Category View only has buttons for filled slots, so new items need full redraw local numSlots = GetContainerNumSlots(bagID) if numSlots and numSlots > 0 then for checkSlotID = 1, numSlots do -- If slot has an item but no button mapping -> new item arrived -- Check both numeric and potential string keys local hasButton = slotToButton[bagID][checkSlotID] or slotToButton[bagID][tostring(checkSlotID)] if not hasButton then local currentLink = GetContainerItemLink(bagID, checkSlotID) if currentLink then return -1 -- Trigger full redraw to categorize new item end end end end return updatedCount 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() 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") -- Determine last visible special button for info anchor local lastButton = keyring if soulbag and soulbag:IsShown() then lastButton = soulbag 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 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 -- 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 -- 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 if header.countText then if numItems > 0 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 local itemData = 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 table.insert(itemButtons, button) if not slotToButton[bagID] then slotToButton[bagID] = {} end slotToButton[bagID][slot] = button 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 -- Update bag slots info text (excluding keyring) function BagFrame:UpdateBagSlotsInfo(bagData, isOtherChar) local infoText = getglobal("Guda_BagFrame_Toolbar_BagSlotsInfo_Text") if not infoText then return end local totalSlots = 0 local usedSlots = 0 -- Count slots in regular bags only (0-4), exclude keyring (-2) 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 totalSlots = totalSlots + numSlots -- Count used slots if bag and bag.slots then for slot = 1, numSlots do if bag.slots[slot] then usedSlots = usedSlots + 1 end end end end end -- Format: "24 / 80" (used / total) infoText:SetText(string.format("%d / %d", usedSlots, totalSlots)) infoText:SetTextColor(0.7, 0.7, 0.7) 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 frame = CreateFrame("Button", frameName, Guda_BagFrame) frame:SetWidth(20) frame:SetHeight(20) frame:SetPoint("BOTTOMRIGHT", Guda_BagFrame, "BOTTOMRIGHT", -150, 16) frame:SetFrameStrata("HIGH") frame:SetFrameLevel(10) -- 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) 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) 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) 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, just do a clean refresh -- 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 item detection cache to force fresh tooltip scans 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 -- 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.) addon.Modules.Events:Register("ITEM_LOCK_CHANGED", function() if currentViewChar then return end if not Guda_BagFrame:IsShown() then return end -- In Category View, just update lock states visually without full redraw local viewType = addon.Modules.DB:GetSetting("bagViewType") or "single" if viewType == "category" then -- Only update lock visual states, don't trigger full redraw BagFrame:UpdateLockStates() 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 mail is opened addon.Modules.Events:Register("MAIL_SHOW", function() Guda_BagFrame:Show() end, "BagFrame") -- Track vendor interactions to avoid closing bags when a vendor is opened addon.Modules.Events:Register("MERCHANT_SHOW", function() isMerchantOpen = true -- Optional: ensure bags are visible when a vendor is opened local frameRef = getglobal("Guda_BagFrame") if frameRef and not frameRef:IsShown() then frameRef:Show() 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