-------------------------------------------------------------------------------- -- S-Frames: Bag Container UI (Bags/Container.lua) -------------------------------------------------------------------------------- SFrames.Bags.Container = {} local SLOT_SIZE = 36 local SPACING = 6 local MARGIN = 10 local TOP_OFFSET = 52 -- Space for title + search bar + gold local TEXT_EMPTY = "\231\169\186" local TEXT_ITEM = "\231\137\169\229\147\129" local TEXT_BACKPACK = "\232\131\140\229\140\133" local TEXT_BAGS_TITLE = "\232\131\140\229\140\133" local TEXT_BANK_TITLE = "\233\147\182\232\161\140" local TEXT_SORT = "\230\149\180\231\144\134" local TEXT_SETTINGS = "\232\174\190\231\189\174" local TEXT_BAG_SLOT = "\232\131\140\229\140\133\230\167\189" local TEXT_BAG_EMPTY = "\231\169\186" local TEXT_BAG_SLOTS = "\230\160\188" local TEXT_EQUIPPED_BAG = "\229\183\178\232\163\133\229\164\135\232\131\140\229\140\133" local TEXT_HS_USE = "\231\130\185\229\135\187\228\189\191\231\148\168\231\130\137\231\159\179" local TEXT_HS_NOT_FOUND = "\232\131\140\229\140\133\228\184\173\230\156\170\230\137\190\229\136\176\231\130\137\231\159\179\227\128\130" local TEXT_CHARACTER = "\232\167\146\232\137\178" local TEXT_ONLINE = "\229\156\168\231\186\191" local TEXT_OFFLINE = "\231\166\187\231\186\191" local PANEL_BG_ALPHA = 0.55 local SLOT_BG_ALPHA = 0.22 local CHARACTER_SELECTOR_ICON = "Interface\\CHARACTERFRAME\\TemporaryPortrait-Female-Human" local _A = SFrames.ActiveTheme local BagFrame = nil local ItemSlots = {} local playerBagInvSlots = { [1] = 20, [2] = 21, [3] = 22, [4] = 23 } local function IsLiveBankOpen() local bankFrame = _G["SFramesBankFrame"] if not bankFrame or not bankFrame:IsVisible() then return false end if not SFrames.Bags.Bank then return false end return not SFrames.Bags.Bank.isOffline end local function SetTooltipFromContainerItem(bagID, slotID) if not GameTooltip then return false end GameTooltip:ClearLines() if GameTooltip.SetBagItem then local ok = pcall(function() GameTooltip:SetBagItem(bagID, slotID) end) if ok then local left1 = _G["GameTooltipTextLeft1"] if left1 and left1:GetText() and left1:GetText() ~= "" then return true end end end local link = GetContainerItemLink(bagID, slotID) if link then local ok = pcall(function() GameTooltip:SetHyperlink(link) end) if ok then local left1 = _G["GameTooltipTextLeft1"] if left1 and left1:GetText() and left1:GetText() ~= "" then return true end end local name = GetItemInfo(link) if name and name ~= "" then GameTooltip:SetText(name, 1, 1, 1) return true end end return false end local function HasTooltipText() local left1 = _G["GameTooltipTextLeft1"] return left1 and left1:GetText() and left1:GetText() ~= "" end local function GetItemNameFromLink(link) if type(link) ~= "string" or link == "" then return nil end local name = GetItemInfo(link) if name and name ~= "" then return name end local _, _, parsed = string.find(link, "%[(.+)%]") if parsed and parsed ~= "" then return parsed end return nil end local function ShowMerchantCursorForSlot(button) if not button then return end if button.bagID == nil or button.slotID == nil then return end if SFrames.Bags.Container and SFrames.Bags.Container.isOffline then return end if not ((MerchantFrame and MerchantFrame:IsVisible()) or (SFramesMerchantFrame and SFramesMerchantFrame:IsVisible())) then return end local texture, _, locked = GetContainerItemInfo(button.bagID, button.slotID) if not texture or locked then return end -- Prefer container-aware API when available. if ShowContainerSellCursor then local ok = pcall(function() ShowContainerSellCursor(button.bagID, button.slotID) end) if ok then return end end -- Fallback to generic merchant sell cursor. if ShowMerchantSellCursor then local ok = pcall(function() ShowMerchantSellCursor() end) if ok then return end end -- Final fallback for clients lacking both APIs. if SetCursor then pcall(function() SetCursor("BUY_CURSOR") end) end end local function ResolvePlayerBagInvSlots() local fallback = { [1] = 20, [2] = 21, [3] = 22, [4] = 23 } for bagIndex = 1, 4 do local invSlot = nil local liveBtn = _G["CharacterBag" .. (bagIndex - 1) .. "Slot"] -- Most reliable when Blizzard character UI is loaded. if liveBtn and liveBtn.GetID then local id = liveBtn:GetID() if type(id) == "number" and id > 0 then invSlot = id end end if (not invSlot) and ContainerIDToInventoryID then local ok, slotID = pcall(function() return ContainerIDToInventoryID(bagIndex) end) if ok and type(slotID) == "number" and slotID > 0 then invSlot = slotID end end if (not invSlot) and GetInventorySlotInfo then local slotNames = { "Bag" .. (bagIndex - 1) .. "Slot", "Bag" .. bagIndex .. "Slot", "CharacterBag" .. (bagIndex - 1) .. "Slot", } for _, slotName in ipairs(slotNames) do local ok, slotID = pcall(function() return GetInventorySlotInfo(slotName) end) if ok and type(slotID) == "number" and slotID > 0 then invSlot = slotID break end end end playerBagInvSlots[bagIndex] = invSlot or fallback[bagIndex] end end local function GetLivePlayerBagIconTexture(bagIndex) local btn = _G["CharacterBag" .. (bagIndex - 1) .. "Slot"] if not btn then return nil end local icon = _G[btn:GetName() .. "IconTexture"] or _G[btn:GetName() .. "Icon"] if icon and icon.GetTexture then local tex = icon:GetTexture() if tex and tex ~= "" then return tex end end return nil end local function IsDisabledIconTexture(tex) if type(tex) ~= "string" then return false end local lower = string.lower(tex) lower = string.gsub(lower, "\\", "/") return string.find(lower, "disabled", 1, true) ~= nil end local function IsPaperdollBagPlaceholder(tex) if type(tex) ~= "string" then return false end local lower = string.lower(tex) lower = string.gsub(lower, "\\", "/") if string.find(lower, "ui-paperdoll-slot-bag", 1, true) then return true end -- Some clients may return slightly different placeholder paths. if string.find(lower, "paperdoll", 1, true) and string.find(lower, "slot", 1, true) and string.find(lower, "bag", 1, true) then return true end return false end local function IsUsableBagIconTexture(tex) if type(tex) ~= "string" or tex == "" then return false end if IsPaperdollBagPlaceholder(tex) then return false end if IsDisabledIconTexture(tex) then return false end return true end local function GetIconFromItemLink(link) if not link then return nil end local _, _, _, _, _, _, _, _, tex = GetItemInfo(link) if tex then return tex end local _, _, itemID = string.find(link, "item:(%d+)") if itemID then local _, _, _, _, _, _, _, _, tex2 = GetItemInfo("item:" .. itemID) if tex2 then return tex2 end end return nil end local function BuildPlayerTradeLinkSet() local links = {} if not (TradeFrame and TradeFrame:IsVisible()) then return links end if not GetTradePlayerItemLink then return links end for i = 1, 6 do local link = GetTradePlayerItemLink(i) if link and link ~= "" then links[link] = true end end return links end local function CreateCoinDisplay(parent, frameName) local frame = CreateFrame("Frame", frameName, parent, "SmallMoneyFrameTemplate") frame:SetFrameStrata(parent:GetFrameStrata()) frame:SetFrameLevel(parent:GetFrameLevel() + 30) frame:SetWidth(140) frame:SetHeight(16) return frame end local function SetCoinDisplayMoney(display, copper) if not display then return end local value = tonumber(copper) or 0 if value < 0 then value = 0 end if SmallMoneyFrame_SetAmount then local ok = pcall(function() SmallMoneyFrame_SetAmount(display, value) end) if ok then return end end if MoneyFrame_Update and display.GetName then local frameName = display:GetName() if frameName and frameName ~= "" then pcall(function() MoneyFrame_Update(frameName, value) end) end end end -- Create a single item slot button local function CreateSlot(parent, id) local button = CreateFrame("Button", "SFramesBagSlot" .. id, parent, "ItemButtonTemplate") button:RegisterForClicks("LeftButtonUp", "RightButtonUp") button:RegisterForDrag("LeftButton") -- Rounded backdrop style (matching CharacterPanel equipment slots) local DEFAULT_BORDER = (_A and _A.slotBorder) or { 0.25, 0.25, 0.3, 0.8 } button:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 18, insets = { left = 2, right = 2, top = 2, bottom = 2 } }) button:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.9) button:SetBackdropBorderColor(DEFAULT_BORDER[1], DEFAULT_BORDER[2], DEFAULT_BORDER[3], DEFAULT_BORDER[4]) -- Inset icon within the rounded border local icon = _G[button:GetName() .. "IconTexture"] if icon then icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) icon:ClearAllPoints() icon:SetPoint("TOPLEFT", button, "TOPLEFT", 4, -4) icon:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -4, 4) end local qualGlow = button:CreateTexture(nil, "OVERLAY") qualGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") qualGlow:SetBlendMode("ADD") qualGlow:SetAlpha(0.8) qualGlow:SetWidth(SLOT_SIZE * 1.9) qualGlow:SetHeight(SLOT_SIZE * 1.9) qualGlow:SetPoint("CENTER", button, "CENTER", 0, 0) qualGlow:Hide() button.qualGlow = qualGlow function button:SetBorderColor(r, g, b, a) self.qualGlow:SetVertexColor(r, g, b) self.qualGlow:Show() self._qualityBorder = true end function button:ShowBorder() self._qualityBorder = true end function button:HideBorder() self.qualGlow:Hide() self._qualityBorder = false end -- Hide the ugly default rounded Blizzard border local nt = _G[button:GetName() .. "NormalTexture"] if nt then nt:SetTexture(nil) nt:Hide() end -- Grey item marker (a small coin/junk icon in the corner) local junkIcon = button:CreateTexture(nil, "OVERLAY") junkIcon:SetTexture("Interface\\Buttons\\UI-GroupLoot-Coin-Up") junkIcon:SetWidth(14) junkIcon:SetHeight(14) -- Place it on top of everything junkIcon:SetPoint("TOPLEFT", button, "TOPLEFT", 1, -1) junkIcon:Hide() button.junkIcon = junkIcon local previewGlow = button:CreateTexture(nil, "OVERLAY") previewGlow:SetTexture("Interface\\Buttons\\ButtonHilight-Square") previewGlow:SetBlendMode("ADD") previewGlow:SetAllPoints(button) previewGlow:Hide() button.previewGlow = previewGlow local tradeText = button:CreateFontString(nil, "OVERLAY") tradeText:SetFont("Fonts\\Zekton.ttf", 10, "OUTLINE") if setglobal then -- check Vanilla or use default font tradeText:SetFontObject(GameFontNormal) end tradeText:SetPoint("CENTER", button, "CENTER", 0, 0) tradeText:SetText("交易") tradeText:SetTextColor(0, 1, 0, 1) tradeText:Hide() button.tradeText = tradeText button:SetScript("OnEnter", function() if this.bagID == nil or this.slotID == nil then return end SFrames.Bags._hoveredSlot = this GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:ClearLines() if SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar then local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Container.offlineChar) if data and data.bags[this.bagID] and data.bags[this.bagID].items[this.slotID] then local link = data.bags[this.bagID].items[this.slotID].link local shown = false if link then local _, _, itemStr = string.find(link, "(item:[%-?%d:]+)") local ok = false if itemStr then ok = pcall(function() GameTooltip:SetHyperlink(itemStr) end) else ok = pcall(function() GameTooltip:SetHyperlink(link) end) end shown = ok and HasTooltipText() if not shown then local name = GetItemNameFromLink(link) if name and name ~= "" then GameTooltip:SetText(name, 1, 1, 1) shown = true end end if not shown then GameTooltip:SetText(TEXT_ITEM, 1, 1, 1) end else GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65) end else GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65) end else local shown = SetTooltipFromContainerItem(this.bagID, this.slotID) if not shown then GameTooltip:SetText(TEXT_EMPTY, 0.65, 0.65, 0.65) end end if IsControlKeyDown() then ShowInspectCursor() else ShowMerchantCursorForSlot(this) end GameTooltip:Show() end) button:SetScript("OnLeave", function() SFrames.Bags._hoveredSlot = nil this.controlDownLast = nil GameTooltip:Hide() if HideContainerSellCursor and this.bagID and this.slotID then pcall(function() HideContainerSellCursor(this.bagID, this.slotID) end) end ResetCursor() end) local cooldown = CreateFrame("Model", button:GetName().."Cooldown", button, "CooldownFrameTemplate") cooldown:SetAllPoints(button) function button:SplitStack(split) if not split or split < 1 then return end if self.bagID == nil or self.slotID == nil then return end SplitContainerItem(self.bagID, self.slotID, split) end button:SetScript("OnClick", function() local bagID = this.bagID local slotID = this.slotID local isOffline = SFrames.Bags.Container.isOffline -- Helper: get item link for this slot (works both online and offline) local function GetSlotLink() if isOffline and SFrames.Bags.Container.offlineChar then local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Container.offlineChar) if data and data.bags[bagID] and data.bags[bagID].items[slotID] then return data.bags[bagID].items[slotID].link end return nil end return GetContainerItemLink(bagID, slotID) end if IsControlKeyDown() and arg1 == "LeftButton" then local link = GetSlotLink() if link and DressUpItemLink then DressUpItemLink(link) return end end if IsShiftKeyDown() then local eb = ChatFrameEditBox if eb and eb.IsVisible and eb:IsVisible() then local link = GetSlotLink() if link then eb:Insert(link) end return end if not isOffline and arg1 == "LeftButton" and (not CursorHasItem()) and OpenStackSplitFrame then local _, itemCount = GetContainerItemInfo(bagID, slotID) if itemCount and itemCount > 1 then OpenStackSplitFrame(itemCount, this, "BOTTOMLEFT", "TOPLEFT") return end end end -- Block all other actions in offline mode if isOffline then return end if arg1 == "RightButton" then if SFrames.Mail and SFrames.Mail.TryAddItemFromBag then if SFrames.Mail.TryAddItemFromBag(bagID, slotID) then return end end if this.bagID >= 0 and IsLiveBankOpen() and AutoStoreBagItem then local ok = pcall(function() AutoStoreBagItem(this.bagID, this.slotID) end) if ok then return end end if TradeFrame and TradeFrame:IsVisible() and not IsShiftKeyDown() then local _, _, locked = GetContainerItemInfo(this.bagID, this.slotID) if locked then local link = GetSlotLink() if link and GetTradePlayerItemLink then local inTradeSlot = nil for i = 1, 6 do if GetTradePlayerItemLink(i) == link then inTradeSlot = i break end end if inTradeSlot then ClearCursor() ClickTradeButton(inTradeSlot) ClearCursor() return end if BagFrame and BagFrame:IsVisible() then SFrames.Bags.Container:UpdateLayout() end end else PickupContainerItem(this.bagID, this.slotID) local tradeSlot = TradeFrame_GetAvailableSlot and TradeFrame_GetAvailableSlot() if tradeSlot then ClickTradeButton(tradeSlot) end if CursorHasItem() then ClearCursor() end return end end if AuctionFrame and AuctionFrame:IsShown() and not IsShiftKeyDown() then if AuctionFrameBrowse and AuctionFrameBrowse:IsShown() then local link = GetContainerItemLink(this.bagID, this.slotID) if link then local _, _, itemName = string.find(link, "%[(.+)%]") if itemName and BrowseName then BrowseName:SetText(itemName) if AuctionFrameBrowse_Search then AuctionFrameBrowse_Search() end end end return elseif AuctionFrameAuctions and AuctionFrameAuctions:IsShown() then PickupContainerItem(this.bagID, this.slotID) if AuctionsItemButton then AuctionsItemButton:Click() end if CursorHasItem() then ClearCursor() end return end end UseContainerItem(this.bagID, this.slotID) else PickupContainerItem(this.bagID, this.slotID) end end) button:SetScript("OnDragStart", function() if SFrames.Bags.Container.isOffline then return end if CursorHasItem() then return end PickupContainerItem(this.bagID, this.slotID) end) button:SetScript("OnReceiveDrag", function() if SFrames.Bags.Container.isOffline then return end if CursorHasItem() then PickupContainerItem(this.bagID, this.slotID) end end) return button end local function SaveBagFramePosition() if not (BagFrame and SFramesDB and SFramesDB.Bags) then return end local point, _, relPoint, x, y = BagFrame:GetPoint() if not point or not relPoint then return end SFramesDB.Bags.bagPosition = { point = point, relPoint = relPoint, x = x or 0, y = y or 0, } end local function ApplyBagFramePosition() if not BagFrame then return end BagFrame:ClearAllPoints() local pos = SFramesDB and SFramesDB.Bags and SFramesDB.Bags.bagPosition if pos and pos.point and pos.relPoint and type(pos.x) == "number" and type(pos.y) == "number" then BagFrame:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y) else BagFrame:SetPoint("RIGHT", UIParent, "RIGHT", -20, 0) end end function SFrames.Bags.Container:PreviewBagSlots(targetBagID) for _, btn in ipairs(ItemSlots) do if btn and btn:IsShown() then local icon = _G[btn:GetName() .. "IconTexture"] local isMatch = (btn.bagID == targetBagID) if icon then icon:SetVertexColor(1, 1, 1) end if btn.previewGlow then if isMatch then btn.previewGlow:Show() else btn.previewGlow:Hide() end end end end end function SFrames.Bags.Container:ClearBagPreview() for _, btn in ipairs(ItemSlots) do if btn and btn:IsShown() then local icon = _G[btn:GetName() .. "IconTexture"] if icon then icon:SetVertexColor(1, 1, 1) end if btn.previewGlow then btn.previewGlow:Hide() end end end -- Restore search dimming state after preview. local query = "" if SFramesBagSearchBox and SFramesBagSearchBox.GetText then query = SFramesBagSearchBox:GetText() or "" end if SFrames.Bags.Features and SFrames.Bags.Features.ApplySearch then SFrames.Bags.Features:ApplySearch(query) end end -- Build/Update the item slot grid function SFrames.Bags.Container:UpdateLayout() if not BagFrame then return end local cols = (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.columns) or 10 local spacing = (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.bagSpacing) or SPACING spacing = tonumber(spacing) or SPACING if spacing < 0 then spacing = 0 end local slots = {} local isOffline = self.isOffline local charName = self.offlineChar local offlineDB = nil if isOffline and charName then offlineDB = SFrames.Bags.Offline:GetCharacterData(charName) if not offlineDB then isOffline = false self.isOffline = false self.offlineChar = nil if BagFrame and BagFrame.RefreshCharacterSelectorText then BagFrame.RefreshCharacterSelectorText() end end end local tradeLinks = nil if not isOffline and TradeFrame and TradeFrame:IsVisible() then tradeLinks = BuildPlayerTradeLinkSet() end -- Collect all slots (bags 0-4) for bag = 0, 4 do local size = 0 if isOffline and offlineDB then if offlineDB.bags[bag] then size = offlineDB.bags[bag].size end else size = GetContainerNumSlots(bag) end for slot = 1, size do table.insert(slots, { bag = bag, slot = slot }) end end local numSlots = table.getn(slots) if numSlots == 0 then numSlots = 1 end local rows = math.ceil(numSlots / cols) -- Resize frame local width = MARGIN * 2 + (cols * SLOT_SIZE) + math.max(0, (cols - 1)) * spacing local height = MARGIN * 2 + TOP_OFFSET + (rows * SLOT_SIZE) + math.max(0, (rows - 1)) * spacing + 24 BagFrame:SetWidth(math.max(width, 160)) BagFrame:SetHeight(height) -- Position & update slots for i, meta in ipairs(slots) do local btn = ItemSlots[i] if not btn then btn = CreateSlot(BagFrame, i) ItemSlots[i] = btn end btn.bagID = meta.bag btn.slotID = meta.slot local row = math.floor((i - 1) / cols) local col = math.mod((i - 1), cols) btn:ClearAllPoints() btn:SetPoint("TOPLEFT", BagFrame, "TOPLEFT", MARGIN + col * (SLOT_SIZE + spacing), -(MARGIN + TOP_OFFSET + row * (SLOT_SIZE + spacing))) -- Fetch item info local texture, count, quality, link, locked if isOffline and offlineDB then if offlineDB.bags[meta.bag] and offlineDB.bags[meta.bag].items[meta.slot] then local item = offlineDB.bags[meta.bag].items[meta.slot] texture = item.texture count = item.count quality = item.quality link = item.link locked = false end else local t, c, l, q = GetContainerItemInfo(meta.bag, meta.slot) texture = t; count = c; locked = l; quality = q link = GetContainerItemLink(meta.bag, meta.slot) end SetItemButtonTexture(btn, texture) SetItemButtonCount(btn, count) local iconTex = _G[btn:GetName() .. "IconTexture"] local isDesaturated = locked and (not tradeLinks) if btn.tradeText then btn.tradeText:Hide() end if tradeLinks and link then local isLockedByTrade = locked and (tradeLinks[link] and true or false) if isLockedByTrade then if btn.tradeText then btn.tradeText:Show() end isDesaturated = true else -- Ignore stale locked flags for items no longer in trade. isDesaturated = false SFramesBagsTooltipScanner = SFramesBagsTooltipScanner or CreateFrame("GameTooltip", "SFramesBagsTooltipScanner", nil, "GameTooltipTemplate") SFramesBagsTooltipScanner:SetOwner(UIParent, "ANCHOR_NONE") SFramesBagsTooltipScanner:ClearLines() local hasItem = SFramesBagsTooltipScanner:SetBagItem(meta.bag, meta.slot) if hasItem then for i = 1, 10 do local line = _G["SFramesBagsTooltipScannerTextLeft" .. i] if line then local text = line:GetText() if text and (text == ITEM_SOULBOUND or text == ITEM_BIND_QUEST or text == "Quest Item") then isDesaturated = true break end else break end end end end elseif not isOffline and TradeFrame and TradeFrame:IsVisible() and link then SFramesBagsTooltipScanner = SFramesBagsTooltipScanner or CreateFrame("GameTooltip", "SFramesBagsTooltipScanner", nil, "GameTooltipTemplate") SFramesBagsTooltipScanner:SetOwner(UIParent, "ANCHOR_NONE") SFramesBagsTooltipScanner:ClearLines() local hasItem = SFramesBagsTooltipScanner:SetBagItem(meta.bag, meta.slot) if hasItem then for i = 1, 10 do local line = _G["SFramesBagsTooltipScannerTextLeft" .. i] if line then local text = line:GetText() if text and (text == ITEM_SOULBOUND or text == ITEM_BIND_QUEST or text == "Quest Item") then isDesaturated = true break end else break end end end end if iconTex then if isDesaturated then iconTex:SetVertexColor(0.5, 0.5, 0.5) else iconTex:SetVertexColor(1, 1, 1) end end if btn.previewGlow then btn.previewGlow:Hide() end -- Quality border & Grey marker btn:HideBorder() btn.junkIcon:Hide() if link then -- 1st attempt: Safest 1.12 generic approach: Read the color straight out of the hyperlink! -- Standard link format: |cffAABBCC|Hitem:... local _, _, hex = string.find(link, "|c(%x+)|H") local parsedColor = false if hex and string.len(hex) == 8 then local hexLower = string.lower(hex) if hexLower == "ff9d9d9d" then -- Poor / Grey Item btn:SetBorderColor(0.5, 0.5, 0.5, 1) btn:ShowBorder() btn.junkIcon:Show() parsedColor = true elseif hexLower == "ffffffff" then -- Common / White Item (No special border) parsedColor = true else -- Green, Blue, Purple, Orange local r = tonumber(string.sub(hex, 3, 4), 16) / 255 local g = tonumber(string.sub(hex, 5, 6), 16) / 255 local b = tonumber(string.sub(hex, 7, 8), 16) / 255 btn:SetBorderColor(r, g, b, 1) btn:ShowBorder() parsedColor = true end end -- 2nd attempt fallback: Use the API quality return values if regex fails if not parsedColor then local q = quality if not q then local _, _, itemString = string.find(link, "item:(%d+)") if itemString then local _, _, scanRarity = GetItemInfo("item:" .. itemString) q = scanRarity end end if q then if q == 0 then btn:SetBorderColor(0.5, 0.5, 0.5, 1) btn:ShowBorder() btn.junkIcon:Show() elseif q > 1 then local r, g, b = GetItemQualityColor(q) btn:SetBorderColor(r, g, b, 1) btn:ShowBorder() end end end end -- Cooldowns local cooldown = _G[btn:GetName() .. "Cooldown"] if cooldown then if isOffline then cooldown:Hide() else local start, duration, enable = GetContainerItemCooldown(meta.bag, meta.slot) if start and duration and start > 0 and duration > 0 then CooldownFrame_SetTimer(cooldown, start, duration, enable) cooldown:Show() else cooldown:Hide() end end end btn:Show() end -- Hide excess buttons for i = numSlots + 1, table.getn(ItemSlots) do if ItemSlots[i] then ItemSlots[i]:Hide() end end if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end -- Update money display if BagFrame.moneyFrame then local copper = 0 if isOffline and offlineDB then copper = offlineDB.money or 0 else copper = GetMoney() end SetCoinDisplayMoney(BagFrame.moneyFrame, copper) end end -- Main frame initialization (called once after PLAYER_LOGIN) function SFrames.Bags.Container:Initialize() if BagFrame then return end BagFrame = CreateFrame("Frame", "SFramesBagFrame", UIParent) BagFrame:SetWidth(420) BagFrame:SetHeight(200) BagFrame:SetFrameStrata("HIGH") BagFrame:SetToplevel(true) BagFrame:EnableMouse(true) BagFrame:SetMovable(true) BagFrame:SetClampedToScreen(true) BagFrame:RegisterForDrag("LeftButton") BagFrame:SetScript("OnDragStart", function() this:StartMoving() end) BagFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() SaveBagFramePosition() end) ApplyBagFramePosition() tinsert(UISpecialFrames, "SFramesBagFrame") BagFrame:SetScript("OnUpdate", function() local btn = SFrames.Bags._hoveredSlot if not btn then return end if not GameTooltip:IsOwned(btn) then SFrames.Bags._hoveredSlot = nil return end if IsControlKeyDown() then if not btn.controlDownLast then btn.controlDownLast = true ShowInspectCursor() end else if btn.controlDownLast then btn.controlDownLast = false ResetCursor() ShowMerchantCursorForSlot(btn) end end end) -- ESC menu style rounded backdrop BagFrame:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 14, insets = { left = 3, right = 3, top = 3, bottom = 3 }, }) local _A = SFrames.ActiveTheme if _A and _A.panelBg then BagFrame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], _A.panelBg[4] or 0.95) BagFrame:SetBackdropBorderColor(_A.panelBorder[1], _A.panelBorder[2], _A.panelBorder[3], _A.panelBorder[4] or 0.9) else BagFrame:SetBackdropColor(0.12, 0.06, 0.10, 0.95) BagFrame:SetBackdropBorderColor(0.55, 0.30, 0.42, 0.9) end local bagShadow = CreateFrame("Frame", nil, BagFrame) bagShadow:SetPoint("TOPLEFT", BagFrame, "TOPLEFT", -5, 5) bagShadow:SetPoint("BOTTOMRIGHT", BagFrame, "BOTTOMRIGHT", 5, -5) bagShadow:SetFrameLevel(math.max(BagFrame:GetFrameLevel() - 1, 0)) bagShadow:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 }, }) bagShadow:SetBackdropColor(0, 0, 0, 0.55) bagShadow:SetBackdropBorderColor(0, 0, 0, 0.4) local scale = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.scale) == "number" and SFramesDB.Bags.scale) or 0.85 BagFrame:SetScale(scale) local bagAlpha = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.alpha) == "number" and SFramesDB.Bags.alpha) or 1 BagFrame:SetAlpha(bagAlpha) local titleIco = SFrames:CreateIcon(BagFrame, "backpack", 14) titleIco:SetDrawLayer("OVERLAY") titleIco:SetPoint("TOPLEFT", BagFrame, "TOPLEFT", 10, -7) titleIco:SetVertexColor(_A.title[1], _A.title[2], _A.title[3]) local titleFS = SFrames:CreateFontString(BagFrame, 12, "LEFT") titleFS:SetPoint("LEFT", titleIco, "RIGHT", 4, 0) titleFS:SetText(TEXT_BAGS_TITLE) titleFS:SetTextColor(_A.title[1], _A.title[2], _A.title[3]) BagFrame.title = titleFS -- Close button local closeBtn = CreateFrame("Button", "SFramesBagClose", BagFrame, "UIPanelCloseButton") closeBtn:SetPoint("TOPRIGHT", BagFrame, "TOPRIGHT", 0, 0) closeBtn:SetScript("OnClick", function() SFrames.Bags.Container:Close() end) -- Money display BagFrame.moneyFrame = CreateCoinDisplay(BagFrame, "SFramesBagMoneyFrame") BagFrame.moneyFrame:SetPoint("RIGHT", BagFrame, "BOTTOMRIGHT", -8, 17) SetCoinDisplayMoney(BagFrame.moneyFrame, 0) -- Bag slot management buttons (5 slots: 0=backpack, 1-4 = equipped bags) BagFrame.bagSlotBtns = {} local BAG_BTN_SIZE = 22 ResolvePlayerBagInvSlots() local bagInvSlots = playerBagInvSlots local bagBar = CreateFrame("Frame", "SFramesBagBar", BagFrame) bagBar:SetWidth((BAG_BTN_SIZE * 5) + (3 * 4)) bagBar:SetHeight(BAG_BTN_SIZE) bagBar:SetPoint("BOTTOMLEFT", BagFrame, "BOTTOMLEFT", 8, 6) bagBar:SetFrameStrata(BagFrame:GetFrameStrata()) bagBar:SetFrameLevel(BagFrame:GetFrameLevel() + 40) bagBar:EnableMouse(false) BagFrame.bagBar = bagBar local function PlaceCursorItemInBagSlot(invSlot) if not invSlot or not CursorHasItem() then return false end local ok = false if EquipCursorItem then ok = pcall(function() EquipCursorItem(invSlot) end) if ok and (not CursorHasItem()) then return true end end -- In vanilla this can both place and swap bag items in bag slots. ok = pcall(function() PickupBagFromSlot(invSlot) end) if ok and (not CursorHasItem()) then return true end if PutItemInBag then ok = pcall(function() PutItemInBag(invSlot) end) if ok and (not CursorHasItem()) then return true end end -- Fallback path if PutItemInBag is unavailable. if CursorHasItem() then local fallbackOk = pcall(function() PickupInventoryItem(invSlot) end) ok = fallbackOk or ok end return ok and (not CursorHasItem()) end for bagIndex = 0, 4 do local bsBtn = CreateFrame("Button", "SFramesBagMgrBtn"..bagIndex, bagBar) bsBtn:SetWidth(BAG_BTN_SIZE) bsBtn:SetHeight(BAG_BTN_SIZE) bsBtn:EnableMouse(true) bsBtn:SetFrameStrata(bagBar:GetFrameStrata()) bsBtn:SetFrameLevel(bagBar:GetFrameLevel() + 1) -- Anchor: first one at BOTTOMLEFT, rest chained RIGHT if bagIndex == 0 then bsBtn:SetPoint("TOPLEFT", bagBar, "TOPLEFT", 0, 0) else bsBtn:SetPoint("LEFT", _G["SFramesBagMgrBtn" .. (bagIndex-1)], "RIGHT", 3, 0) end bsBtn.bagID = bagIndex -- Rounded backdrop (matching item slot style) bsBtn:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 } }) bsBtn:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.9) bsBtn:SetBackdropBorderColor(_A.slotBorder[1], _A.slotBorder[2], _A.slotBorder[3], _A.slotBorder[4] or 0.8) -- Icon texture inset within border local bsIcon = bsBtn:CreateTexture(nil, "OVERLAY") bsIcon:SetPoint("TOPLEFT", bsBtn, "TOPLEFT", 2, -2) bsIcon:SetPoint("BOTTOMRIGHT", bsBtn, "BOTTOMRIGHT", -2, 2) bsIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92) bsIcon:SetBlendMode("BLEND") bsIcon:SetVertexColor(1, 1, 1, 1) bsBtn.icon = bsIcon local bsHighlight = bsBtn:CreateTexture(nil, "HIGHLIGHT") bsHighlight:SetTexture("Interface\\Buttons\\ButtonHilight-Square") bsHighlight:SetBlendMode("ADD") bsHighlight:SetAllPoints(bsBtn) bsBtn:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:ClearLines() if SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar then local data = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Container.offlineChar) if this.bagID == 0 then local slots = data and data.bags and data.bags[0] and data.bags[0].size or 0 if slots and slots > 0 then GameTooltip:SetText(string.format("%s (%d%s)", TEXT_BACKPACK, slots, TEXT_BAG_SLOTS), 1, 1, 1) else GameTooltip:SetText(TEXT_BACKPACK, 1, 1, 1) end else local shown = false local slots = data and data.bags and data.bags[this.bagID] and data.bags[this.bagID].size or 0 local bagMeta = data and data.equippedBags and data.equippedBags[this.bagID] if bagMeta and bagMeta.link then local ok = pcall(function() GameTooltip:SetHyperlink(bagMeta.link) end) shown = ok and HasTooltipText() if not shown then local name = GetItemNameFromLink(bagMeta.link) if name and name ~= "" then GameTooltip:SetText(name, 1, 1, 1) shown = true end end end if not shown then if slots and slots > 0 then GameTooltip:SetText(string.format("%s %d (%d%s)", TEXT_BAG_SLOT, this.bagID, slots, TEXT_BAG_SLOTS), 1, 1, 1) else GameTooltip:SetText(string.format("%s %d (%s)", TEXT_BAG_SLOT, this.bagID, TEXT_BAG_EMPTY), 0.6, 0.6, 0.6) end end end GameTooltip:AddLine(TEXT_OFFLINE, 0.75, 0.75, 0.75) else if this.bagID == 0 then GameTooltip:SetText(TEXT_BACKPACK, 1, 1, 1) else local invSlot = playerBagInvSlots[this.bagID] local shown = false if invSlot then if GameTooltip.SetInventoryItem then local ok = pcall(function() GameTooltip:SetInventoryItem("player", invSlot) end) if ok then local left1 = _G["GameTooltipTextLeft1"] if left1 and left1:GetText() and left1:GetText() ~= "" then shown = true end end end if not shown then local bagLink = GetInventoryItemLink("player", invSlot) if bagLink then local ok = pcall(function() GameTooltip:SetHyperlink(bagLink) end) if ok then shown = true else local name = GetItemInfo(bagLink) or (TEXT_EQUIPPED_BAG.." "..this.bagID) GameTooltip:SetText(name, 1, 1, 1) shown = true end end end if not shown then local slots = GetContainerNumSlots(this.bagID) or 0 if slots > 0 then GameTooltip:SetText(string.format("%s %d (%d%s)", TEXT_BAG_SLOT, this.bagID, slots, TEXT_BAG_SLOTS), 1, 1, 1) else GameTooltip:SetText(string.format("%s %d (%s)", TEXT_BAG_SLOT, this.bagID, TEXT_BAG_EMPTY), 0.6, 0.6, 0.6) end end else GameTooltip:SetText(string.format("%s %d", TEXT_BAG_SLOT, this.bagID), 0.8, 0.8, 0.8) end end end if BagFrame:IsVisible() then SFrames.Bags.Container:PreviewBagSlots(this.bagID) end GameTooltip:Show() end) bsBtn:SetScript("OnLeave", function() GameTooltip:Hide() if BagFrame:IsVisible() then SFrames.Bags.Container:ClearBagPreview() end end) bsBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp") bsBtn:RegisterForDrag("LeftButton") bsBtn:SetScript("OnDragStart", function() if this.bagID <= 0 then return end local invSlot = playerBagInvSlots[this.bagID] if invSlot then pcall(function() PickupBagFromSlot(invSlot) end) end end) bsBtn:SetScript("OnReceiveDrag", function() if not CursorHasItem() then return end if this.bagID == 0 then if PutItemInBackpack then pcall(function() PutItemInBackpack() end) end else local invSlot = playerBagInvSlots[this.bagID] PlaceCursorItemInBagSlot(invSlot) end if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end if BagFrame:IsVisible() then SFrames.Bags.Container:UpdateLayout() end end) bsBtn:SetScript("OnClick", function() if CursorHasItem() then if this.bagID == 0 then if PutItemInBackpack then pcall(function() PutItemInBackpack() end) end else local invSlot = playerBagInvSlots[this.bagID] PlaceCursorItemInBagSlot(invSlot) end if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end if BagFrame:IsVisible() then SFrames.Bags.Container:UpdateLayout() end return end if this.bagID > 0 then local invSlot = playerBagInvSlots[this.bagID] if invSlot then pcall(function() PickupBagFromSlot(invSlot) end) end elseif arg1 == "RightButton" then -- Backpack slot has no equip slot to pick up from. else SFrames.Bags.Container:Toggle() end end) BagFrame.bagSlotBtns[bagIndex] = bsBtn end -- Update bag slot textures whenever bag contents change BagFrame.UpdateBagSlotIcons = function() local isOffline = SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar local offlineDB = nil if isOffline then offlineDB = SFrames.Bags.Offline:GetCharacterData(SFrames.Bags.Container.offlineChar) end ResolvePlayerBagInvSlots() for bagIndex = 0, 4 do local btn = BagFrame.bagSlotBtns[bagIndex] if btn then if bagIndex == 0 then btn.icon:SetTexture("Interface\\Buttons\\Button-Backpack-Up") elseif isOffline and offlineDB then local slots = offlineDB.bags and offlineDB.bags[bagIndex] and offlineDB.bags[bagIndex].size or 0 local bagMeta = offlineDB.equippedBags and offlineDB.equippedBags[bagIndex] local tex = nil if bagMeta and bagMeta.link then tex = GetIconFromItemLink(bagMeta.link) end if (not tex) and bagMeta and IsUsableBagIconTexture(bagMeta.texture) then tex = bagMeta.texture end if (not tex) and slots and slots > 0 then tex = "Interface\\Icons\\INV_Misc_Bag_08" end if not tex then tex = "Interface\\Icons\\INV_Misc_Bag_08" end btn.icon:SetTexture(tex) else local invSlot = playerBagInvSlots[bagIndex] local tex = nil local slots = GetContainerNumSlots(bagIndex) or 0 if invSlot then local link = GetInventoryItemLink("player", invSlot) tex = GetIconFromItemLink(link) if (not tex) and slots > 0 then local rawTex = GetInventoryItemTexture("player", invSlot) if type(rawTex) ~= "string" then tex = rawTex elseif IsUsableBagIconTexture(rawTex) then tex = rawTex end end end if (not tex) and slots > 0 then local liveTex = GetLivePlayerBagIconTexture(bagIndex) if IsUsableBagIconTexture(liveTex) then tex = liveTex end end if tex then btn.icon:SetTexture(tex) else btn.icon:SetTexture("Interface\\Icons\\INV_Misc_Bag_08") end end btn:Enable() if btn.icon.SetDesaturated then pcall(function() btn.icon:SetDesaturated(false) end) end btn.icon:SetVertexColor(1, 1, 1, 1) btn.icon:SetAlpha(1) end end end local function QueueBagSlotIconRefreshes() if not BagFrame or not BagFrame.UpdateBagSlotIcons then return end local elapsed = 0 local nextRefresh = 1 local refreshPoints = { 0.08, 0.25, 0.55 } local refreshTimer = CreateFrame("Frame") refreshTimer:SetScript("OnUpdate", function() elapsed = elapsed + arg1 if nextRefresh <= table.getn(refreshPoints) and elapsed >= refreshPoints[nextRefresh] then if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end if BagFrame:IsVisible() then SFrames.Bags.Container:UpdateLayout() end nextRefresh = nextRefresh + 1 end if nextRefresh > table.getn(refreshPoints) then this:SetScript("OnUpdate", nil) end end) end BagFrame.UpdateBagSlotIcons() QueueBagSlotIconRefreshes() -- Search bar local eb = CreateFrame("EditBox", "SFramesBagSearchBox", BagFrame, "InputBoxTemplate") eb:SetWidth(120) eb:SetHeight(18) eb:SetPoint("TOPLEFT", BagFrame, "TOPLEFT", 10, -26) eb:SetAutoFocus(false) eb:SetScript("OnEnterPressed", function() this:ClearFocus() end) eb:SetScript("OnEscapePressed", function() this:ClearFocus(); this:SetText("") end) eb:SetScript("OnTextChanged", function() SFrames.Bags.Features:ApplySearch(this:GetText()) end) local function CreateHeaderIconButton(name, parent, iconPath) local btn = CreateFrame("Button", name, parent) btn:SetWidth(18) btn:SetHeight(18) btn:SetFrameStrata(parent:GetFrameStrata()) btn:SetFrameLevel(parent:GetFrameLevel() + 45) btn:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 } }) btn:SetBackdropColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 0.86) btn:SetBackdropBorderColor(_A.slotBorder[1], _A.slotBorder[2], _A.slotBorder[3], _A.slotBorder[4] or 0.8) local icon = btn:CreateTexture(nil, "ARTWORK") icon:SetTexture(iconPath or "Interface\\Icons\\INV_Misc_QuestionMark") icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2) icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2) icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) btn.icon = icon local hl = btn:CreateTexture(nil, "HIGHLIGHT") hl:SetTexture("Interface\\Buttons\\ButtonHilight-Square") hl:SetBlendMode("ADD") hl:SetAllPoints(btn) return btn end -- Character selector: icon button-triggered dropdown. local function GetCurrentCharacterName() local live = UnitName("player") if type(live) == "string" and live ~= "" then return live end local cached = SFrames.Bags.Offline:GetCurrentPlayerName() if type(cached) == "string" and cached ~= "" then return cached end return TEXT_CHARACTER end local function GetOfflineBankTargetCharacter() if SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar then return SFrames.Bags.Container.offlineChar end return GetCurrentCharacterName() end local charBtn = CreateHeaderIconButton("SFramesBagCharBtn", BagFrame, CHARACTER_SELECTOR_ICON) charBtn:SetPoint("TOPRIGHT", BagFrame, "TOPRIGHT", -8, -26) BagFrame.charSelectBtn = charBtn local cfgBtn = CreateHeaderIconButton("SFramesBagConfigBtn", BagFrame, "Interface\\Icons\\INV_Misc_Gear_01") cfgBtn:SetPoint("RIGHT", charBtn, "LEFT", -4, 0) SFrames:SetIcon(cfgBtn.icon, "settings") cfgBtn:SetScript("OnClick", function() if SFrames.ConfigUI and SFrames.ConfigUI.Build then SFrames.ConfigUI:Build("bags") end end) cfgBtn:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:SetText(TEXT_SETTINGS, 1, 1, 1) GameTooltip:Show() end) cfgBtn:SetScript("OnLeave", function() GameTooltip:Hide() end) local bankBtn = CreateHeaderIconButton("SFramesBagOfflineBankBtn", BagFrame, "Interface\\Icons\\INV_Misc_Key_05") bankBtn:SetPoint("RIGHT", cfgBtn, "LEFT", -4, 0) SFrames:SetIcon(bankBtn.icon, "gold") bankBtn:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:SetText(TEXT_OFFLINE .. TEXT_BANK_TITLE, 1, 1, 1) local target = GetOfflineBankTargetCharacter() if target and target ~= "" and target ~= TEXT_CHARACTER then GameTooltip:AddLine(target, 0.8, 0.8, 0.8) end GameTooltip:Show() end) bankBtn:SetScript("OnLeave", function() GameTooltip:Hide() end) bankBtn:SetScript("OnClick", function() if not (SFrames.Bags and SFrames.Bags.Bank) then return end local target = GetOfflineBankTargetCharacter() if target == TEXT_CHARACTER then target = nil end if SFrames.Bags.Bank.OpenOffline then SFrames.Bags.Bank:OpenOffline(target) else SFrames.Bags.Bank:Open() end end) -- Sort button (icon) local sortBtn = CreateHeaderIconButton("SFramesBagSortBtn", BagFrame, "Interface\\Icons\\INV_Misc_Note_05") sortBtn:SetPoint("LEFT", eb, "RIGHT", 6, 0) SFrames:SetIcon(sortBtn.icon, "backpack") sortBtn:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:SetText(TEXT_SORT, 1, 1, 1) GameTooltip:Show() end) sortBtn:SetScript("OnLeave", function() GameTooltip:Hide() end) sortBtn:SetScript("OnClick", function() if SFrames.Bags.Sort then SFrames.Bags.Sort:Start() end end) local hsBtn = CreateHeaderIconButton("SFramesBagHSBtn", BagFrame, "Interface\\Icons\\INV_Misc_Rune_01") hsBtn:SetPoint("LEFT", sortBtn, "RIGHT", 4, 0) SFrames:SetIcon(hsBtn.icon, "hearthstone") hsBtn:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:SetText(TEXT_HS_USE, 1, 1, 1) GameTooltip:Show() end) hsBtn:SetScript("OnLeave", function() GameTooltip:Hide() end) hsBtn:SetScript("OnClick", function() for bag = 0, 4 do for slot = 1, GetContainerNumSlots(bag) do local link = GetContainerItemLink(bag, slot) if link and (string.find(link, "6948") or string.find(link, "Hearthstone")) then UseContainerItem(bag, slot) return end end end SFrames:Print(TEXT_HS_NOT_FOUND) end) local keyBtn = CreateHeaderIconButton("SFramesBagKeyBtn", BagFrame, "Interface\\Icons\\INV_Misc_Key_04") keyBtn:SetPoint("LEFT", hsBtn, "RIGHT", 4, 0) SFrames:SetIcon(keyBtn.icon, "key") keyBtn:RegisterForClicks("LeftButtonUp") keyBtn:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:SetText("\233\146\165\229\140\153\233\147\190", 1, 0.82, 0) GameTooltip:Show() end) keyBtn:SetScript("OnLeave", function() GameTooltip:Hide() end) keyBtn:SetScript("OnClick", function() if SFrames.Bags._origToggleKeyRing then SFrames.Bags._origToggleKeyRing() elseif ToggleKeyRing then ToggleKeyRing() end end) local dbMenu = CreateFrame("Frame", "SFramesBagDropdown", BagFrame, "UIDropDownMenuTemplate") dbMenu:Hide() dbMenu:SetPoint("TOPRIGHT", charBtn, "BOTTOMRIGHT", 0, 0) local function RefreshCharacterSelectorText() if not BagFrame or not BagFrame.charSelectBtn then return end if SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar then BagFrame.charSelectorLabel = SFrames.Bags.Container.offlineChar .. " (" .. TEXT_OFFLINE .. ")" else BagFrame.charSelectorLabel = GetCurrentCharacterName() .. " (" .. TEXT_ONLINE .. ")" end end BagFrame.RefreshCharacterSelectorText = RefreshCharacterSelectorText local function OnDropdownClick() local char = this.value if char == "ONLINE" then SFrames.Bags.Container.isOffline = false SFrames.Bags.Container.offlineChar = nil else SFrames.Bags.Container.isOffline = true SFrames.Bags.Container.offlineChar = char end RefreshCharacterSelectorText() SFrames.Bags.Container:UpdateLayout() end UIDropDownMenu_Initialize(dbMenu, function() local info = UIDropDownMenu_CreateInfo and UIDropDownMenu_CreateInfo() or {} local currentName = GetCurrentCharacterName() info.text = currentName .. " (" .. TEXT_ONLINE .. ")" info.value = "ONLINE" info.func = OnDropdownClick info.checked = (not SFrames.Bags.Container.isOffline) UIDropDownMenu_AddButton(info) local chars = SFrames.Bags.Offline:GetCharacterList() table.sort(chars) for _, char in ipairs(chars) do if char ~= currentName then info = UIDropDownMenu_CreateInfo and UIDropDownMenu_CreateInfo() or {} info.text = char .. " (" .. TEXT_OFFLINE .. ")" info.value = char info.func = OnDropdownClick info.checked = SFrames.Bags.Container.isOffline and SFrames.Bags.Container.offlineChar == char UIDropDownMenu_AddButton(info) end end end) charBtn:SetScript("OnClick", function() ToggleDropDownMenu(1, nil, dbMenu, this, 0, 0) end) charBtn:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:SetText(BagFrame.charSelectorLabel or TEXT_CHARACTER, 1, 1, 1) GameTooltip:Show() end) charBtn:SetScript("OnLeave", function() GameTooltip:Hide() end) RefreshCharacterSelectorText() -- React to bag updates while open BagFrame:RegisterEvent("BAG_UPDATE") BagFrame:RegisterEvent("PLAYER_MONEY") BagFrame:RegisterEvent("BAG_UPDATE_COOLDOWN") BagFrame:RegisterEvent("UNIT_INVENTORY_CHANGED") BagFrame:RegisterEvent("PLAYER_ENTERING_WORLD") BagFrame:RegisterEvent("TRADE_SHOW") BagFrame:RegisterEvent("TRADE_CLOSED") BagFrame:RegisterEvent("TRADE_UPDATE") BagFrame:RegisterEvent("TRADE_PLAYER_ITEM_CHANGED") BagFrame:RegisterEvent("TRADE_TARGET_ITEM_CHANGED") BagFrame:RegisterEvent("TRADE_ACCEPT_UPDATE") BagFrame:SetScript("OnEvent", function() if BagFrame:IsVisible() then SFrames.Bags.Container:UpdateLayout() end if event == "PLAYER_ENTERING_WORLD" then if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end QueueBagSlotIconRefreshes() elseif event == "BAG_UPDATE" or event == "UNIT_INVENTORY_CHANGED" then if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end elseif event == "TRADE_SHOW" or event == "TRADE_CLOSED" or event == "TRADE_UPDATE" or event == "TRADE_PLAYER_ITEM_CHANGED" or event == "TRADE_TARGET_ITEM_CHANGED" or event == "TRADE_ACCEPT_UPDATE" then if event == "TRADE_CLOSED" then for _, btn in ipairs(ItemSlots) do if btn.tradeText then btn.tradeText:Hide() end local iconTex = _G[btn:GetName() .. "IconTexture"] if iconTex and iconTex.SetDesaturated then pcall(function() iconTex:SetDesaturated(false) end) end if iconTex then iconTex:SetVertexColor(1, 1, 1, 1) end end end if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end QueueBagSlotIconRefreshes() end end) BagFrame:Hide() end function SFrames.Bags.Container:Toggle() if not BagFrame then return end if BagFrame:IsVisible() then self:Close() else self:Open() end end function SFrames.Bags.Container:Open() if not BagFrame then return end self.isOffline = false self.offlineChar = nil if BagFrame.RefreshCharacterSelectorText then BagFrame.RefreshCharacterSelectorText() end -- Clear search if SFramesBagSearchBox then SFramesBagSearchBox:SetText("") end self:UpdateLayout() BagFrame:Show() local elapsed = 0 local nextRefresh = 1 local refreshPoints = { 0.05, 0.18, 0.40 } local refreshTimer = CreateFrame("Frame") refreshTimer:SetScript("OnUpdate", function() elapsed = elapsed + arg1 if nextRefresh <= table.getn(refreshPoints) and elapsed >= refreshPoints[nextRefresh] then if BagFrame:IsVisible() then if BagFrame.UpdateBagSlotIcons then BagFrame.UpdateBagSlotIcons() end SFrames.Bags.Container:UpdateLayout() end nextRefresh = nextRefresh + 1 end if nextRefresh > table.getn(refreshPoints) then this:SetScript("OnUpdate", nil) end end) PlaySound("igBackPackOpen") end function SFrames.Bags.Container:Close() if not BagFrame then return end BagFrame:Hide() PlaySound("igBackPackClose") end