diff --git a/Assets/Envelope.blp b/Assets/Envelope.blp new file mode 100644 index 0000000..5d5ea4f Binary files /dev/null and b/Assets/Envelope.blp differ diff --git a/Core/Database.lua b/Core/Database.lua index 533b830..f77621e 100644 --- a/Core/Database.lua +++ b/Core/Database.lua @@ -131,6 +131,7 @@ function DB:Initialize() money = 0, bags = {}, bank = {}, + mailbox = {}, -- Add mailbox storage equipped = {}, -- Add equipped items storage character = {}, -- Add character info storage lastUpdate = time(), @@ -153,6 +154,10 @@ function DB:Initialize() char.character = {} addon:Debug("Added character field to existing character") end + if not char.mailbox then + char.mailbox = {} + addon:Debug("Added mailbox field to existing character") + end end addon:Debug("Database initialized for %s", fullName) @@ -215,6 +220,58 @@ function DB:SaveMoney(copper) end end +-- Save mailbox data +function DB:SaveMailbox(mailboxData) + local char = self:GetCurrentCharacter() + if char then + char.mailbox = mailboxData + char.lastUpdate = time() + addon:Debug("Saved mailbox data") + end +end + +-- Add a single mail entry to a character's mailbox +function DB:AddMailToCharacter(name, realm, mailRow) + local fullName = name .. "-" .. (realm or playerRealm) + local char = Guda_DB.characters[fullName] + + if char then + if not char.mailbox then + char.mailbox = {} + end + + -- Check if this exact mail already exists (simplistic check) + local exists = false + for _, m in ipairs(char.mailbox) do + if m.sender == mailRow.sender and m.subject == mailRow.subject and m.money == mailRow.money then + if (not m.item and not mailRow.item) or (m.item and mailRow.item and m.item.name == mailRow.item.name and m.item.count == mailRow.item.count) then + exists = true + -- Update link/itemID if missing in existing but present in new + if mailRow.item and m.item then + if not m.item.link and mailRow.item.link then + m.item.link = mailRow.item.link + addon:Debug("Updated link for existing mail item") + end + if not m.item.itemID and mailRow.item.itemID then + m.item.itemID = mailRow.item.itemID + addon:Debug("Updated itemID for existing mail item") + end + end + break + end + end + end + + if not exists then + table.insert(char.mailbox, 1, mailRow) -- Add to beginning + char.lastUpdate = time() + addon:Debug("Added outgoing mail to %s's mailbox", fullName) + return true + end + end + return false +end + -- Get all characters (optionally filter by faction and/or realm) function DB:GetAllCharacters(sameFactionOnly, currentRealmOnly) local chars = {} @@ -256,6 +313,65 @@ function DB:GetCharacterBank(fullName) return char and char.bank or {} end +-- Get character's mailbox +function DB:GetCharacterMailbox(fullName) + local char = Guda_DB.characters[fullName] + return char and char.mailbox or {} +end + +-- Find an item ID and link by name in any character's data +function DB:FindItemByName(name) + if not name or name == "" or not Guda_DB or not Guda_DB.characters then return nil, nil end + + for fullName, char in pairs(Guda_DB.characters) do + -- Check bags + if char.bags then + for bagID, bagData in pairs(char.bags) do + if type(bagData) == "table" and bagData.slots then + for slotID, item in pairs(bagData.slots) do + if item and item.name == name and item.link then + local itemID = addon.Modules.Utils:ExtractItemID(item.link) + if itemID then return itemID, item.link end + end + end + end + end + end + -- Check bank + if char.bank then + for bagID, bagData in pairs(char.bank) do + if type(bagData) == "table" and bagData.slots then + for slotID, item in pairs(bagData.slots) do + if item and item.name == name and item.link then + local itemID = addon.Modules.Utils:ExtractItemID(item.link) + if itemID then return itemID, item.link end + end + end + end + end + end + -- Check equipped + if char.equipped then + for slot, item in pairs(char.equipped) do + if item and item.name == name and item.link then + local itemID = addon.Modules.Utils:ExtractItemID(item.link) + if itemID then return itemID, item.link end + end + end + end + -- Check mailbox + if char.mailbox then + for _, mail in ipairs(char.mailbox) do + if mail.item and mail.item.name == name and mail.item.link then + local itemID = addon.Modules.Utils:ExtractItemID(mail.item.link) + if itemID then return itemID, mail.item.link end + end + end + end + end + return nil, nil +end + -- Get character's equipped items function DB:GetCharacterEquipped(fullName) local char = Guda_DB.characters[fullName] diff --git a/Core/Events.lua b/Core/Events.lua index ec4b7a4..b690f16 100644 --- a/Core/Events.lua +++ b/Core/Events.lua @@ -79,3 +79,11 @@ end function Events:OnPlayerLogout(callback, owner) self:Register("PLAYER_LOGOUT", callback, owner) end + +function Events:OnMailShow(callback, owner) + self:Register("MAIL_SHOW", callback, owner) +end + +function Events:OnMailClosed(callback, owner) + self:Register("MAIL_CLOSED", callback, owner) +end diff --git a/Core/Init.lua b/Core/Init.lua index aa68ab4..9066e04 100644 --- a/Core/Init.lua +++ b/Core/Init.lua @@ -102,11 +102,13 @@ addon.Modules = { Tooltip = {}, BagScanner = {}, BankScanner = {}, + MailboxScanner = {}, MoneyTracker = {}, EquipmentScanner = {}, SortEngine = {}, BagFrame = {}, BankFrame = {}, + MailboxFrame = {}, QuestItemBar = {}, TrackedItemBar = {}, SettingsPopup = {}, diff --git a/Core/Main.lua b/Core/Main.lua index 9316884..2025155 100644 --- a/Core/Main.lua +++ b/Core/Main.lua @@ -18,6 +18,7 @@ function Main:Initialize() -- Initialize scanners addon.Modules.BagScanner:Initialize() addon.Modules.BankScanner:Initialize() + addon.Modules.MailboxScanner:Initialize() addon.Modules.MoneyTracker:Initialize() addon.Modules.EquipmentScanner:Initialize() @@ -25,6 +26,7 @@ function Main:Initialize() addon:Print("Initializing UI...") addon.Modules.BagFrame:Initialize() addon.Modules.BankFrame:Initialize() + addon.Modules.MailboxFrame:Initialize() addon:Debug("Checking QuestItemBar module...") if addon.Modules.QuestItemBar and addon.Modules.QuestItemBar.isLoaded then @@ -103,6 +105,10 @@ function Main:SetupSlashCommands() -- Toggle bank addon.Modules.BankFrame:Toggle() + elseif msg == "mail" or msg == "mailbox" then + -- Toggle mailbox + addon.Modules.MailboxFrame:Toggle() + elseif msg == "sort" then -- Sort bags addon.Modules.SortEngine:SortBags() @@ -143,6 +149,7 @@ function Main:SetupSlashCommands() addon:Print("Commands:") addon:Print("/guda - Toggle bags") addon:Print("/guda bank - Toggle bank") + addon:Print("/guda mail - Toggle mailbox") addon:Print("/guda sort - Sort bags") addon:Print("/guda sortbank - Sort bank") addon:Print("/guda track - Toggle item tracking") diff --git a/Core/Tooltip.lua b/Core/Tooltip.lua index 8663ce8..ba118f5 100644 --- a/Core/Tooltip.lua +++ b/Core/Tooltip.lua @@ -7,12 +7,16 @@ addon.Modules.Tooltip = Tooltip -- Helper function to get item ID from link (Lua 5.0 compatible) local function GetItemIDFromLink(link) if not link then return nil end - local _, _, itemID = strfind(link, "item:(%d+):?") + if type(link) == "number" then return link end + + -- Try to find itemID in a standard link or a raw item:ID string + local _, _, itemID = string.find(link, "item:(%d+)") return itemID and tonumber(itemID) or nil end local function CountCurrentCharacterItems(itemID) local bagCount = 0 local bankCount = 0 + local mailCount = 0 local equippedCount = 0 -- Count current character's bags in real-time @@ -83,6 +87,74 @@ local function CountCurrentCharacterItems(itemID) end end + -- Count current character's mailbox in real-time if mailbox is open + if addon.Modules.MailboxScanner and addon.Modules.MailboxScanner:IsMailboxOpen() then + local numInboxItems = GetInboxNumItems() + for i = 1, numInboxItems do + local _, _, _, _, _, _, _, hasItem = GetInboxHeaderInfo(i) + if hasItem then + -- Turtle WoW supports up to 12 attachments per mail. + -- We use GetInboxNumAttachments if available to avoid over-scanning. + local numAttachments = 0 + if GetInboxNumAttachments then + numAttachments = GetInboxNumAttachments(i) or 0 + end + + -- Fallback: if we don't have the count but header says there's an item, assume at least 1. + if numAttachments == 0 and hasItem then + numAttachments = 1 + end + + for j = 1, numAttachments do -- Turtle WoW supports up to 12 attachments + local name, _, count = GetInboxItem(i, j) + if name then + local itemLink = addon.Modules.Utils:GetInboxItemLink(i, j) + if itemLink then + local slotItemID = GetItemIDFromLink(itemLink) + if slotItemID == itemID then + mailCount = mailCount + (count or 1) + end + end + end + end + end + end + else + -- Mailbox not open - use saved data + local playerName = addon.Modules.DB:GetPlayerFullName() + local charData = Guda_DB and Guda_DB.characters and Guda_DB.characters[playerName] + if charData and charData.mailbox and type(charData.mailbox) == "table" then + for _, mail in ipairs(charData.mailbox) do + if mail.items then + for _, item in ipairs(mail.items) do + local slotItemID = item.link and GetItemIDFromLink(item.link) + if slotItemID == itemID then + mailCount = mailCount + (item.count or 1) + elseif not slotItemID and item.name then + -- Fallback to name matching if link is missing + local targetName = GetItemInfo(itemID) + if targetName == item.name then + mailCount = mailCount + (item.count or 1) + end + end + end + elseif mail.item then -- Fallback for single item data structure + local item = mail.item + local slotItemID = item.link and GetItemIDFromLink(item.link) + if slotItemID == itemID then + mailCount = mailCount + (item.count or 1) + elseif not slotItemID and item.name then + -- Fallback to name matching if link is missing + local targetName = GetItemInfo(itemID) + if targetName == item.name then + mailCount = mailCount + (item.count or 1) + end + end + end + end + end + end + -- Count equipped items in real-time for slotID = 1, 19 do -- All equipment slots local link = GetInventoryItemLink("player", slotID) @@ -94,7 +166,7 @@ local function CountCurrentCharacterItems(itemID) end end - return bagCount, bankCount, equippedCount + return bagCount, bankCount, equippedCount, mailCount end -- Count items for a specific character with real-time data for current character @@ -107,6 +179,7 @@ local function CountItemsForCharacter(itemID, characterData, isCurrentChar) -- For other characters, use saved data local bagCount = 0 local bankCount = 0 + local mailCount = 0 local equippedCount = 0 -- Count bags from saved data @@ -141,6 +214,38 @@ local function CountItemsForCharacter(itemID, characterData, isCurrentChar) end end + -- Count mailbox from saved data + if characterData.mailbox and type(characterData.mailbox) == "table" then + for _, mail in ipairs(characterData.mailbox) do + if mail.items then + for _, item in ipairs(mail.items) do + local slotItemID = item.link and GetItemIDFromLink(item.link) + if slotItemID == itemID then + mailCount = mailCount + (item.count or 1) + elseif not slotItemID and item.name then + -- Fallback to name matching if link is missing + local targetName = GetItemInfo(itemID) + if targetName == item.name then + mailCount = mailCount + (item.count or 1) + end + end + end + elseif mail.item then -- Fallback for single item data structure + local item = mail.item + local slotItemID = item.link and GetItemIDFromLink(item.link) + if slotItemID == itemID then + mailCount = mailCount + (item.count or 1) + elseif not slotItemID and item.name then + -- Fallback to name matching if link is missing + local targetName = GetItemInfo(itemID) + if targetName == item.name then + mailCount = mailCount + (item.count or 1) + end + end + end + end + end + -- Count equipped items from saved data if characterData.equipped and type(characterData.equipped) == "table" then for slotName, itemData in pairs(characterData.equipped) do @@ -153,7 +258,7 @@ local function CountItemsForCharacter(itemID, characterData, isCurrentChar) end end - return bagCount, bankCount, equippedCount + return bagCount, bankCount, equippedCount, mailCount end @@ -187,8 +292,15 @@ function Tooltip:AddInventoryInfo(tooltip, link) return end + -- Guard against double-adding for the same item on the same tooltip + if tooltip.GudaInventoryAdded == itemID then + return + end + tooltip.GudaInventoryAdded = itemID + local totalBags = 0 local totalBank = 0 + local totalMail = 0 local totalEquipped = 0 local characterCounts = {} local hasAnyItems = false @@ -201,18 +313,20 @@ function Tooltip:AddInventoryInfo(tooltip, link) -- Ensure charData is actually a table and on current realm if type(charData) == "table" and charData.realm == currentRealm then local isCurrentChar = (charName == currentPlayerName) - local bagCount, bankCount, equippedCount = CountItemsForCharacter(itemID, charData, isCurrentChar) + local bagCount, bankCount, equippedCount, mailCount = CountItemsForCharacter(itemID, charData, isCurrentChar) - if bagCount > 0 or bankCount > 0 or equippedCount > 0 then + if bagCount > 0 or bankCount > 0 or equippedCount > 0 or mailCount > 0 then hasAnyItems = true totalBags = totalBags + bagCount totalBank = totalBank + bankCount + totalMail = totalMail + mailCount totalEquipped = totalEquipped + equippedCount table.insert(characterCounts, { name = charData.name or charName, classToken = charData.classToken, bagCount = bagCount, bankCount = bankCount, + mailCount = mailCount, equippedCount = equippedCount, isCurrent = isCurrentChar }) @@ -221,7 +335,7 @@ function Tooltip:AddInventoryInfo(tooltip, link) -- If charData is not a table (string, number, etc.), just skip it end - local totalCount = totalBags + totalBank + totalEquipped + local totalCount = totalBags + totalBank + totalMail + totalEquipped if hasAnyItems then @@ -233,7 +347,16 @@ function Tooltip:AddInventoryInfo(tooltip, link) -- Total line with cyan label and white count local totalText = "|cFF00FFFFTotal|r: |cFFFFFFFF" .. totalCount .. "|r" - local breakdownText = "(|cFF00FFFFBags|r: |cFFFFFFFF" .. totalBags .. "|r | |cFF00FFFFBank|r: |cFFFFFFFF" .. totalBank .. "|r)" + local breakdownParts = {} + if totalBags > 0 then table.insert(breakdownParts, "|cFF00FFFFBags|r: |cFFFFFFFF" .. totalBags .. "|r") end + if totalBank > 0 then table.insert(breakdownParts, "|cFF00FFFFBank|r: |cFFFFFFFF" .. totalBank .. "|r") end + if totalMail > 0 then table.insert(breakdownParts, "|cFF00FFFFMail|r: |cFFFFFFFF" .. totalMail .. "|r") end + if totalEquipped > 0 then table.insert(breakdownParts, "|cFF00FFFFEquipped|r: |cFFFFFFFF" .. totalEquipped .. "|r") end + + local breakdownText = "" + if table.getn(breakdownParts) > 0 then + breakdownText = "(" .. table.concat(breakdownParts, " | ") .. ")" + end tooltip:AddDoubleLine(totalText, breakdownText, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0) -- Sort with current character first @@ -254,6 +377,9 @@ function Tooltip:AddInventoryInfo(tooltip, link) if charInfo.bankCount > 0 then table.insert(parts, "|cFF00FFFFBank|r: |cFFFFFFFF" .. charInfo.bankCount .. "|r") end + if charInfo.mailCount > 0 then + table.insert(parts, "|cFF00FFFFMail|r: |cFFFFFFFF" .. charInfo.mailCount .. "|r") + end if charInfo.equippedCount > 0 then table.insert(parts, "|cFF00FFFFEquipped|r: |cFFFFFFFF" .. charInfo.equippedCount .. "|r") end @@ -453,6 +579,19 @@ function Tooltip:Initialize() end) end + -- Hook SetInboxItem for mailbox + local oldSetInboxItem = GameTooltip.SetInboxItem + function GameTooltip:SetInboxItem(index, itemIndex) + return WithDeferredMoney(self, function() + local ret = oldSetInboxItem(self, index, itemIndex) + local link = addon.Modules.Utils:GetInboxItemLink(index, itemIndex) + if link then + Tooltip:AddInventoryInfo(self, link) + end + return ret + end) + end + -- Hook SetTradeSkillItem for profession reagents and items local oldSetTradeSkillItem = GameTooltip.SetTradeSkillItem function GameTooltip:SetTradeSkillItem(skillIndex, reagentIndex) diff --git a/Core/Utils.lua b/Core/Utils.lua index 4997fe8..6e65be3 100644 --- a/Core/Utils.lua +++ b/Core/Utils.lua @@ -95,6 +95,36 @@ function Utils:GetItemInfo(itemLink) return nil end +-- Create a hidden tooltip for scanning +local scanTooltip = CreateFrame("GameTooltip", "GudaBagScanTooltip", nil, "GameTooltipTemplate") +scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") + +-- Get item link from mailbox attachment (WoW 1.12.1 workaround) +function Utils:GetInboxItemLink(index, itemIndex) + -- Try global function first (if it exists on this server/version) + if GetInboxItemLink then + -- Turtle WoW might support (index, itemIndex) for multiple attachments + local link = GetInboxItemLink(index, itemIndex or 1) + if link then return link end + + -- Fallback to single argument if that failed + link = GetInboxItemLink(index) + if link then return link end + end + + -- In 1.12.1, GameTooltip:GetHyperlink() does not exist. + -- Let's try to use GetItemInfo(name) as the primary way. + local name, texture, count, quality = GetInboxItem(index, itemIndex or 1) + if name then + local itemName, link = GetItemInfo(name) + if link then + return link + end + end + + return nil +end + -- Get quality color function Utils:GetQualityColor(quality) local color = addon.Constants.QUALITY_COLORS[quality] or addon.Constants.QUALITY_COLORS[1] @@ -156,12 +186,7 @@ function Utils:FormatTimeAgo(timestamp) end -- Create hidden tooltip for scanning (only once) -local scanTooltip = nil local function GetScanTooltip() - if not scanTooltip then - scanTooltip = CreateFrame("GameTooltip", "GudaBagScanTooltip", nil, "GameTooltipTemplate") - scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") - end return scanTooltip end diff --git a/Data/MailboxScanner.lua b/Data/MailboxScanner.lua new file mode 100644 index 0000000..bcf2644 --- /dev/null +++ b/Data/MailboxScanner.lua @@ -0,0 +1,323 @@ +-- Guda Mailbox Scanner +-- Scans and stores mailbox contents + +local addon = Guda + +local MailboxScanner = {} +addon.Modules.MailboxScanner = MailboxScanner + +local mailboxOpen = false + +-- Scan all mailbox items and return data +function MailboxScanner:ScanMailbox() + if not mailboxOpen then + addon:Debug("Cannot scan mailbox - not open") + return {} + end + + local mailboxData = {} + local numItems = GetInboxNumItems() + + for i = 1, numItems do + local mailRows = self:ScanMailItemRows(i) + for _, row in ipairs(mailRows) do + table.insert(mailboxData, row) + end + end + + return mailboxData +end + +-- Scan a single mail into one or more rows (flattened) +function MailboxScanner:ScanMailItemRows(index) + -- GetInboxHeaderInfo(index) returns: packageIcon, stationeryIcon, sender, subject, money, CODAmount, daysLeft, hasItem, wasRead, wasReturned, textCreated, canReply, isGM + local packageIcon, stationeryIcon, sender, subject, money, CODAmount, daysLeft, hasItem, wasRead, wasReturned, textCreated, canReply, isGM = GetInboxHeaderInfo(index) + + local rows = {} + + if hasItem then + -- Turtle WoW supports up to 12 attachments per mail. + -- We use GetInboxNumAttachments if available to avoid over-scanning on servers + -- that might ignore the second argument of GetInboxItem. + local numAttachments = 0 + if GetInboxNumAttachments then + numAttachments = GetInboxNumAttachments(index) or 0 + end + + -- Fallback: if we don't have the count but header says there's an item, assume at least 1. + if numAttachments == 0 and hasItem then + numAttachments = 1 + end + + for itemIndex = 1, numAttachments do + -- GetInboxItem(index, itemIndex) returns: name, texture, count, quality, canUse + local name, texture, count, quality, canUse = GetInboxItem(index, itemIndex) + if not name then break end + + local itemLink = addon.Modules.Utils:GetInboxItemLink(index, itemIndex) + local itemID = itemLink and addon.Modules.Utils:ExtractItemID(itemLink) + + -- Fallback 1: If link/itemID is missing, try GetItemInfo(name) which might be cached now + if not itemID or not itemLink then + local _, link = GetItemInfo(name) + if link then + itemLink = link + itemID = addon.Modules.Utils:ExtractItemID(link) + addon:Debug("Recovered link from GetItemInfo for %s", name) + end + end + + -- Fallback 2: If still missing, try to recover from existing database (any character) + if not itemID or not itemLink then + itemID, itemLink = addon.Modules.DB:FindItemByName(name) + if itemID then + addon:Debug("Recovered link from DB for %s", name) + end + end + + local itemData = { + link = itemLink, + texture = texture or "Interface\\Icons\\INV_Misc_Bag_08", + count = count or 1, + quality = quality or 0, + name = name, + itemID = itemID, + } + + -- Ensure we have a link if we have an itemID + if itemData.itemID and not itemData.link then + itemData.link = "item:" .. itemData.itemID .. ":0:0:0" + end + + -- If we have a link, try to get more detailed info from cache + if itemData.link then + local itemName, link, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(itemData.link) + if itemName then + itemData.name = itemName + itemData.quality = itemQuality or itemData.quality + itemData.iLevel = iLevel + itemData.type = itemType + itemData.class = itemCategory + itemData.subclass = itemSubType + itemData.equipSlot = itemEquipLoc + if itemTexture then itemData.texture = itemTexture end + end + end + + table.insert(rows, { + sender = sender, + subject = subject, + money = (itemIndex == 1) and money or 0, -- Attach money only to the first row of this mail + CODAmount = (itemIndex == 1) and CODAmount or 0, + daysLeft = daysLeft, + hasItem = true, + item = itemData, + mailIndex = index, + itemIndex = itemIndex, + wasRead = wasRead, + packageIcon = packageIcon, + }) + end + end + + -- If no items found but there is money or it's just a letter + if table.getn(rows) == 0 then + table.insert(rows, { + sender = sender, + subject = subject, + money = money, + CODAmount = CODAmount, + daysLeft = daysLeft, + hasItem = false, + item = nil, + mailIndex = index, + itemIndex = 1, + wasRead = wasRead, + packageIcon = packageIcon, + }) + end + + return rows +end + +-- Save current mailbox to database +function MailboxScanner:SaveToDatabase() + if not mailboxOpen then + return + end + + local mailboxData = self:ScanMailbox() + addon.Modules.DB:SaveMailbox(mailboxData) + addon:Debug("Mailbox data saved") +end + +-- Handle outgoing mail +function MailboxScanner:OnSendMail(recipient, subject, body) + if not recipient or recipient == "" then return end + + -- In WoW 1.12.1, SendMail(recipient, subject, body) is the signature. + -- To get the attached item, we use GetSendMailItem(). + -- GetSendMailItem() returns: name, texture, count, quality + local name, texture, count, quality = GetSendMailItem() + local moneyAmount = GetSendMailMoney() + + local itemData = nil + if name then + local _, link = GetItemInfo(name) + itemData = { + name = name, + texture = texture or "Interface\\Icons\\INV_Misc_Bag_08", + count = count or 1, + quality = quality or 0, + link = link, + itemID = addon.Modules.Utils:ExtractItemID(link), + } + + -- Try to get more info if it's in cache + local itemName, retLink, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(name) + if itemName then + itemData.link = retLink or itemData.link + itemData.itemID = addon.Modules.Utils:ExtractItemID(itemData.link) or itemData.itemID + itemData.quality = itemQuality or itemData.quality + itemData.iLevel = iLevel + itemData.type = itemType + itemData.class = itemCategory + itemData.subclass = itemSubType + itemData.equipSlot = itemEquipLoc + if itemTexture then itemData.texture = itemTexture end + end + + -- Double check itemID + if not itemData.itemID and itemData.link then + itemData.itemID = addon.Modules.Utils:ExtractItemID(itemData.link) + end + end + + if itemData or moneyAmount > 0 then + local mailRow = { + sender = UnitName("player"), + subject = (subject and subject ~= "") and subject or "No Subject", + money = moneyAmount, + CODAmount = 0, + daysLeft = 30, -- Outgoing mail typically has 30 days + hasItem = itemData ~= nil, + item = itemData, + wasRead = false, + } + + addon.Modules.DB:AddMailToCharacter(recipient, nil, mailRow) + end +end + +-- Handle auction house buyouts +function MailboxScanner:OnAuctionBid(type, index, bid) + local name, texture, count, quality, canUse, level, minBid, minIncrement, buyoutPrice, bidAmount, highBidder, owner = GetAuctionItemInfo(type, index) + + if name and buyoutPrice > 0 and bid >= buyoutPrice then + local link = GetAuctionItemLink(type, index) + local itemData = { + name = name, + texture = texture or "Interface\\Icons\\INV_Misc_Bag_08", + count = count or 1, + quality = quality or 0, + link = link, + itemID = addon.Modules.Utils:ExtractItemID(link), + } + + -- Try to get more info if it's in cache + local itemName, retLink, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(link or name) + if itemName then + itemData.link = retLink or itemData.link + itemData.itemID = addon.Modules.Utils:ExtractItemID(itemData.link) or itemData.itemID + itemData.quality = itemQuality or itemData.quality + itemData.iLevel = iLevel + itemData.type = itemType + itemData.class = itemCategory + itemData.subclass = itemSubType + itemData.equipSlot = itemEquipLoc + if itemTexture then itemData.texture = itemTexture end + end + + local mailRow = { + sender = "Auction House", + subject = "Auction won: " .. name, + money = 0, + CODAmount = 0, + daysLeft = 30, + hasItem = true, + item = itemData, + wasRead = false, + } + + addon.Modules.DB:AddMailToCharacter(UnitName("player"), nil, mailRow) + addon:Debug("Captured AH buyout: %s", name) + end +end + +-- Initialize mailbox scanner +function MailboxScanner:Initialize() + -- Hook SendMail to capture outgoing mail to alts + local originalSendMail = SendMail + SendMail = function(recipient, subject, body) + MailboxScanner:OnSendMail(recipient, subject, body) + return originalSendMail(recipient, subject, body) + end + + -- Hook PlaceAuctionBid to capture AH buyouts + local originalPlaceAuctionBid = PlaceAuctionBid + PlaceAuctionBid = function(type, index, bid) + MailboxScanner:OnAuctionBid(type, index, bid) + return originalPlaceAuctionBid(type, index, bid) + end + + -- Mailbox opened + addon.Modules.Events:OnMailShow(function() + mailboxOpen = true + addon:Debug("Mailbox opened") + + -- Delay scan slightly to ensure item info is available + local frame = CreateFrame("Frame") + local elapsed = 0 + frame:SetScript("OnUpdate", function() + elapsed = elapsed + arg1 + if elapsed >= 0.5 then + frame:SetScript("OnUpdate", nil) + if mailboxOpen then + MailboxScanner:SaveToDatabase() + end + end + end) + end, "MailboxScanner") + + -- Register for GET_ITEM_INFO_RECEIVED to refresh if item data arrives + addon.Modules.Events:Register("GET_ITEM_INFO_RECEIVED", function() + if mailboxOpen then + addon:Debug("GET_ITEM_INFO_RECEIVED: Refreshing mailbox") + MailboxScanner:SaveToDatabase() + end + end, "MailboxScanner") + + -- Register for MAIL_INBOX_UPDATE to detect when mail content changes + addon.Modules.Events:Register("MAIL_INBOX_UPDATE", function() + if mailboxOpen then + addon:Debug("MAIL_INBOX_UPDATE: Refreshing mailbox") + MailboxScanner:SaveToDatabase() + end + end, "MailboxScanner") + + -- Register for UI_ERROR_MESSAGE to handle "item not found" situations if needed + -- (Some items might not be in cache and fail silently otherwise) + + -- Mailbox closed + addon.Modules.Events:OnMailClosed(function() + -- Final save on close + MailboxScanner:SaveToDatabase() + mailboxOpen = false + addon:Debug("Mailbox closed") + end, "MailboxScanner") +end + +-- Check if mailbox is currently open +function MailboxScanner:IsMailboxOpen() + return mailboxOpen +end diff --git a/Guda.toc b/Guda.toc index e5347af..dab2d1e 100644 --- a/Guda.toc +++ b/Guda.toc @@ -2,7 +2,7 @@ ## Title: Guda ## Notes: All-in-one bag and bank addon for Turtle WoW ## Author: Vati -## Version: 1.4.8 +## Version: 1.5.0 ## SavedVariables: Guda_DB ## SavedVariablesPerCharacter: Guda_CharDB @@ -16,6 +16,7 @@ Core\Tooltip.lua Data\BagScanner.lua Data\BankScanner.lua +Data\MailboxScanner.lua Data\MoneyTracker.lua Data\EquipmentScanner.lua @@ -23,14 +24,17 @@ Sorting\SortEngine.lua # UI Lua must load before XML so OnLoad handlers exist UI\ItemButton.lua +UI\FrameHelpers.lua UI\BagFrame.lua UI\BankFrame.lua +UI\MailboxFrame.lua UI\QuestItemBar.lua UI\TrackedItemBar.lua UI\SettingsPopup.lua UI\ItemButton.xml UI\BagFrame.xml UI\BankFrame.xml +UI\MailboxFrame.xml UI\SettingsPopup.xml Core\Main.lua diff --git a/Sorting/SortEngine.lua b/Sorting/SortEngine.lua index 3623a60..e2747d0 100644 --- a/Sorting/SortEngine.lua +++ b/Sorting/SortEngine.lua @@ -57,6 +57,7 @@ local CATEGORY_ORDER = { ["Weapon"] = 4, -- Non-equippable weapons ["Armor"] = 5, -- Non-equippable armor ["Tools"] = 6, + ["Quest"] = 7, ["Quiver"] = 8, ["Reagent"] = 9, ["Trade Goods"] = 10, @@ -64,7 +65,6 @@ local CATEGORY_ORDER = { ["Container"] = 12, ["Key"] = 14, ["Miscellaneous"] = 15, - ["Quest"] = 16, ["Junk"] = 17, ["Class Items"] = 18, } @@ -97,100 +97,137 @@ local GEM_PATTERNS = { local scanTooltip = CreateFrame("GameTooltip", "Guda_SortScanTooltip", nil, "GameTooltipTemplate") scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") --- Check if an item is a quest item by scanning its tooltip -local function IsQuestItemTooltip(bagID, slotID) - if not bagID or not slotID then return false end +-- Property cache to prevent race conditions during rapid moves +local propertyCache = {} - scanTooltip:ClearLines() - scanTooltip:SetBagItem(bagID, slotID) +function SortEngine:ClearCache() + propertyCache = {} +end - -- Check all tooltip lines for quest-related text (case-insensitive) - for i = 1, scanTooltip:NumLines() do - local line = getglobal("Guda_SortScanTooltipTextLeft" .. i) - if line then - local text = line:GetText() - if text then - local tl = string.lower(text) - if string.find(tl, "quest starter") or - string.find(tl, "this item begins a quest") or - string.find(tl, "starts a quest") or - string.find(tl, "quest item") or - string.find(tl, "manual") then - return true +local function GetItemProperties(bagID, slotID, itemLink) + if not itemLink then return nil end + + -- Extract base item link for stable caching (item:ID:Enchant:...) + local _, _, baseLink = string.find(itemLink, "(item:%d+:%d+:%d+:%d+:%d+:%d+:%d+:%d+)") + local cacheKey = baseLink or itemLink + + if propertyCache[cacheKey] then + return propertyCache[cacheKey] + end + + local props = { + isQuest = false, + isQuestStarter = false, + isQuestUsable = false, + isGray = false, + restoreTag = nil + } + + -- Quality check from link color code (|cff9d9d9d is gray) + if string.find(itemLink, "|cff9d9d9d") then + props.isGray = true + end + + if bagID and slotID then + scanTooltip:ClearLines() + scanTooltip:SetBagItem(bagID, slotID) + + local numLines = scanTooltip:NumLines() + if numLines and numLines > 0 then + for i = 1, numLines do + local line = getglobal("Guda_SortScanTooltipTextLeft" .. i) + if line then + local text = line:GetText() + if text then + local tl = string.lower(text) + + -- Quest item check + if string.find(tl, "quest starter") or + string.find(tl, "this item begins a quest") or + string.find(tl, "starts a quest") or + string.find(tl, "quest item") or + string.find(tl, "manual") then + props.isQuest = true + end + + -- Starter check + if string.find(tl, "quest starter") or + string.find(tl, "this item begins a quest") or + string.find(tl, "starts a quest") then + props.isQuestStarter = true + end + + -- Usable check + if string.find(tl, "use:") or + string.find(tl, "right%-click") or + string.find(tl, "right click") or + string.find(tl, "click to") then + props.isQuestUsable = true + end + + -- Restore tag check (higher priority tags override lower ones) + if string.find(tl, "while eating") then + props.restoreTag = "eat" + elseif string.find(tl, "while drinking") then + if props.restoreTag ~= "eat" then + props.restoreTag = "drink" + end + elseif string.find(tl, "use: restores") then + if not props.restoreTag then + props.restoreTag = "restore" + end + end + + -- Gray check (header line color fallback) + if i == 1 and string.find(text, "|cff9d9d9d") then + props.isGray = true + end + end end end end end - return false + + propertyCache[cacheKey] = props + return props +end + +-- Check if an item is a quest item by scanning its tooltip +local function IsQuestItemTooltip(bagID, slotID) + if not bagID or not slotID then return false end + local link = GetContainerItemLink(bagID, slotID) + local props = GetItemProperties(bagID, slotID, link) + return props and props.isQuest or false end -- Check if a quest item is usable (has 'Use:' or click to text) local function IsQuestItemUsable(bagID, slotID) if not bagID or not slotID then return false end - - scanTooltip:ClearLines() - scanTooltip:SetBagItem(bagID, slotID) - - for i = 1, scanTooltip:NumLines() do - local line = getglobal("Guda_SortScanTooltipTextLeft" .. i) - if line then - local text = line:GetText() - if text then - local tl = string.lower(text) - if string.find(tl, "use:") or string.find(tl, "right%-click") or string.find(tl, "right click") or string.find(tl, "click to") or string.find(tl, "starts a quest") then - return true - end - end - end - end - return false + local link = GetContainerItemLink(bagID, slotID) + local props = GetItemProperties(bagID, slotID, link) + return props and props.isQuestUsable or false end -- Check if item is a quest starter (explicit 'Starts a Quest' or 'This Item Begins a Quest') local function IsQuestItemStarter(bagID, slotID) if not bagID or not slotID then return false end - - scanTooltip:ClearLines() - scanTooltip:SetBagItem(bagID, slotID) - - for i = 1, scanTooltip:NumLines() do - local line = getglobal("Guda_SortScanTooltipTextLeft" .. i) - if line then - local text = line:GetText() - if text then - local tl = string.lower(text) - if string.find(tl, "quest starter") or string.find(tl, "this item begins a quest") or string.find(tl, "starts a quest") then - return true - end - end - end - end - return false + local link = GetContainerItemLink(bagID, slotID) + local props = GetItemProperties(bagID, slotID, link) + return props and props.isQuestStarter or false end -- Check if an item has a gray title in its tooltip or link local function IsItemGrayTooltip(bagID, slotID, itemLink) - -- Check link first if provided - if itemLink and string.find(itemLink, "|cff9d9d9d") then - return true - end + local props = GetItemProperties(bagID, slotID, itemLink) + return props and props.isGray or false +end - if not bagID or not slotID then return false end - - scanTooltip:ClearLines() - scanTooltip:SetBagItem(bagID, slotID) - - local line = getglobal("Guda_SortScanTooltipTextLeft1") - if line then - local text = line:GetText() - if text then - -- Poor quality color code is |cff9d9d9d - if string.find(text, "|cff9d9d9d") then - return true - end - end - end - return false +-- Check if an item has a restore tag (eat, drink, or restore) +local function GetItemRestoreTagTooltip(bagID, slotID) + if not bagID or not slotID then return nil end + local link = GetContainerItemLink(bagID, slotID) + local props = GetItemProperties(bagID, slotID, link) + return props and props.restoreTag or nil end -- Extract itemID from item link @@ -200,6 +237,19 @@ local function GetItemID(link) return tonumber(itemID) or 0 end +-- Get item quality from link color code +local function GetQualityFromLink(link) + if not link then return 1 end + if string.find(link, "|cff9d9d9d") then return 0 end -- Gray + if string.find(link, "|cffffffff") then return 1 end -- White + if string.find(link, "|cff1eff00") then return 2 end -- Green + if string.find(link, "|cff0070dd") then return 3 end -- Blue + if string.find(link, "|cffa335ee") then return 4 end -- Purple + if string.find(link, "|cffff8000") then return 5 end -- Orange + if string.find(link, "|cffe6cc80") then return 6 end -- Red/Artifact + return 1 +end + -- Extract texture pattern for grouping similar items local function GetTexturePattern(textureName) if not textureName then return "" end @@ -466,24 +516,30 @@ local function AddSortKeys(items) for _, item in ipairs(items) do if item.data and item.data.link then local itemID = GetItemID(item.data.link) - local itemName, itemLink, itemRarity, itemLevel, itemCategory, itemType, itemStackCount, - itemSubType, itemTexture, itemEquipLoc, itemSellPrice = GetItemInfo(itemID) - item.itemName = itemName - --addon:Print("itemTexture:%s",itemTexture or 0); - --addon:Print("itemEquipLoc:%s",itemEquipLoc or 0); - --addon:Print("itemSubType:%s",itemSubType or 0); - --addon:Print("itemType:%s",itemType or 0); - --addon:Print("itemCategory:%s",itemCategory or 0); - --addon:Print("itemLevel:%s",itemLevel or 0); + + -- Use data collected during the initial scan (item.data) for absolute stability + local d = item.data + local itemName = d.name + local itemLink = d.link + local itemRarity = d.quality + local itemLevel = d.iLevel + local itemCategory = d.class + local itemType = d.type + local itemSubType = d.subclass + local itemTexture = d.texture + local itemEquipLoc = d.equipLoc + local itemStackSize = d.stackSize or 1 + + item.itemName = itemName or "" + if not itemName then - -- Skip items that couldn't be loaded + -- Skip items that couldn't be loaded (fallback safety) item.sortedClass = 999 item.isEquippable = false item.priority = 1000 item.equipSlotOrder = 999 item.invertedQuality = 0 item.invertedItemLevel = 0 - item.itemName = "" item.sortedSubclass = 999 item.subclass = "" item.texturePattern = "" @@ -519,15 +575,14 @@ local function AddSortKeys(items) item.equipSlotOrder = EQUIP_SLOT_ORDER[itemSubType] or 999 else item.sortedClass = CATEGORY_ORDER[itemCategory] or 99 - -- Heuristic: Detect items that should be in the Quest category (priority 11) + -- Heuristic: Detect items that should be in the Quest category (priority 7) -- but aren't categorized as such by the game (e.g. some "Manual" items) - if item.sortedClass ~= (CATEGORY_ORDER["Quest"] or 99) then - local nameLower = item.itemName and string.lower(item.itemName) or "" + if item.sortedClass ~= (CATEGORY_ORDER["Quest"] or 7) then + local nameLower = string.lower(item.itemName) if string.find(nameLower, "manual") or string.find(nameLower, "quest") then - item.sortedClass = 11 + item.sortedClass = CATEGORY_ORDER["Quest"] or 7 elseif IsQuestItemTooltip(item.bagID, item.slot) then - -- If name-based heuristic fails, check tooltip - item.sortedClass = 11 + item.sortedClass = CATEGORY_ORDER["Quest"] or 7 end end item.equipSlotOrder = 999 @@ -535,31 +590,24 @@ local function AddSortKeys(items) -- Subclass ordering -- Detect consumable restore/eat/drink tag (if available) BEFORE computing subclass priority - item.restoreTag = nil - if addon.Modules.Utils and addon.Modules.Utils.GetConsumableRestoreTag then - if item.bagID and item.slot then - local tag = addon.Modules.Utils:GetConsumableRestoreTag(item.bagID, item.slot) - if tag then - item.restoreTag = tag - end + item.restoreTag = GetItemRestoreTagTooltip(item.bagID, item.slot) + + local baseSub = GetSubclassOrder(itemSubType, item.itemName) + local function _cons_prk(t) + if t == "eat" then return -300 end + if t == "drink" then return -200 end + if t == "restore" then return -100 end + return 0 end - end - local baseSub = GetSubclassOrder(itemSubType, item.name) - local function _cons_prk(t) - if t == "eat" then return -300 end - if t == "drink" then return -200 end - if t == "restore" then return -100 end - return 0 - end - item.sortedSubclass = _cons_prk(item.restoreTag) + baseSub + item.sortedSubclass = _cons_prk(item.restoreTag) + baseSub item.subclass = itemSubType or "" -- Quest flags: mark quest items and detect starter/usable states item.isQuest = false item.isQuestStarter = false item.isQuestUsable = false - local nameLower = item.itemName and string.lower(item.itemName) or "" - if itemCategory == "Quest" or string.find(nameLower, "quest") or (item.data and item.data.class == "Quest") or IsQuestItemTooltip(item.bagID, item.slot) then + local nameLower = string.lower(item.itemName) + if itemCategory == "Quest" or string.find(nameLower, "quest") or item.data.class == "Quest" or IsQuestItemTooltip(item.bagID, item.slot) then item.isQuest = true if IsQuestItemStarter(item.bagID, item.slot) then item.isQuestStarter = true end if IsQuestItemUsable(item.bagID, item.slot) then item.isQuestUsable = true end @@ -568,7 +616,6 @@ local function AddSortKeys(items) -- Texture pattern for grouping similar items (especially trade goods) item.texturePattern = GetTexturePattern(itemTexture) -- Group Trade Goods: meats (names ending with 'meat') before eggs - local nameLower = item.itemName and string.lower(item.itemName) or "" if itemType == "Trade Goods" then if string.find(nameLower, "meat$") then item.texturePattern = "trade_meat" @@ -584,12 +631,9 @@ local function AddSortKeys(items) item.invertedItemID = -tonumber(itemID) -- Stack info for reverse stack sorting - item.maxStackCount = tonumber(itemStackCount) or 1 + item.maxStackCount = tonumber(itemStackSize) or 1 item.isStackable = item.maxStackCount > 1 item.stackCount = tonumber(item.data.count) or 1 - - -- Name for alphabetical sorting - item.itemName = item.name or "" end end end @@ -670,6 +714,13 @@ local function SortItems(items) if ra ~= rb then return ra > rb end + elseif a.isQuest ~= b.isQuest then + -- If only one is a quest item and they are in the same sortedClass, + -- put the quest item first within that class (or just let sortedClass handle it) + -- Since we moved Quest to its own class 7, this might not be needed unless + -- they are in different classes but one is marked isQuest. + -- However, if they have same sortedClass, we want quest items first. + return a.isQuest end if a.sortedSubclass ~= b.sortedSubclass then return a.sortedSubclass < b.sortedSubclass @@ -740,6 +791,11 @@ local function CollectItems(bagIDs) -- Get fresh item info with ALL return values local itemID = GetItemID(itemLink) local name, link, quality, iLevel, category, itemType, stackCount, subType, iconTex, equipLoc, sellPrice = GetItemInfo(itemID) + + -- Fallback for quality if GetItemInfo fails (ensures stability) + if not quality then + quality = GetQualityFromLink(itemLink) + end sequence = sequence + 1 table.insert(items, { @@ -748,14 +804,16 @@ local function CollectItems(bagIDs) sequence = sequence, -- Preserve original order data = { link = itemLink, - texture = texture, + texture = texture or iconTex, count = itemCount or 1, quality = quality or 0, name = name, iLevel = iLevel, type = itemType, class = category, - subclass = subType, -- NOW INCLUDED! + subclass = subType, + equipLoc = equipLoc, + stackSize = stackCount or 1, locked = locked, }, quality = quality or 0, @@ -891,8 +949,6 @@ end -- and spilling into previous bags when needed. local function BuildGreyTailPositions(bagIDs, greyCount) local positions = {} - local index = 1 - if greyCount <= 0 then return positions end -- Order bags: lowest priority first (these are considered "last"), @@ -920,26 +976,42 @@ local function BuildGreyTailPositions(bagIDs, greyCount) end) -- Collect tail slots from end to start, spilling to previous bags as needed. + local tailSlots = {} for _, info in ipairs(ordered) do for slot = info.numSlots, 1, -1 do - if index <= greyCount then - positions[index] = { bag = info.bagID, slot = slot } - index = index + 1 + if table.getn(tailSlots) < greyCount then + table.insert(tailSlots, { bag = info.bagID, slot = slot }) else break end end - if index > greyCount then break end + if table.getn(tailSlots) >= greyCount then break end end - return positions + -- STABILITY FIX: Sort the collected tail slots to match the ascending scan order. + -- This ensures that identical items don't swap places every pass. + -- Ascending order: Priority DESC, BagID ASC, Slot ASC (matching BuildTargetPositions) + table.sort(tailSlots, function(a, b) + local aPrio = tonumber(addon.Modules.Utils:GetContainerPriority(a.bag)) or 0 + local bPrio = tonumber(addon.Modules.Utils:GetContainerPriority(b.bag)) or 0 + if aPrio ~= bPrio then + return aPrio > bPrio + end + if a.bag ~= b.bag then + return a.bag < b.bag + end + return a.slot < b.slot + end) + + return tailSlots end -- Split a list of collected items into non-greys and greys (quality 0) local function SplitGreyItems(items) local nonGreys, greys = {}, {} for _, item in ipairs(items) do - if tonumber(item.quality or 0) == 0 then + -- Use same logic as AddSortKeys for determining Junk/Grey status (stability) + if tonumber(item.quality or 0) == 0 or IsItemGrayTooltip(item.bagID, item.slot, item.data.link) then table.insert(greys, item) else table.insert(nonGreys, item) @@ -1026,20 +1098,8 @@ function SortEngine:AnalyzeContainer(bagIDs, containerType) for i, item in ipairs(greys) do local target = tailPositions[i] - -- FIX: Check if grey item is already in a tail position of a valid regular bag - local isInTailPosition = false - - -- Check if this grey item is already in the end slots of any valid regular bag - for _, bagID in ipairs(validRegularBags) do - local numSlots = addon.Modules.Utils:GetBagSlotCount(bagID) - if item.bagID == bagID and item.slot >= (numSlots - table.getn(greys) + 1) then - isInTailPosition = true - break - end - end - - -- Only count as out of place if it's NOT in a tail position AND doesn't match target - if target and (item.bagID ~= target.bag or item.slot ~= target.slot) and not isInTailPosition then + -- Only count as out of place if it doesn't match the specific stable target + if target and (item.bagID ~= target.bag or item.slot ~= target.slot) then totalOutOfPlace = totalOutOfPlace + 1 end end @@ -1326,6 +1386,9 @@ end --=========================================================================== function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sortType) + -- Clear property cache at the start of a sort operation + self:ClearCache() + -- Analyze to determine how many passes are needed local analysis = analyzeFunction() diff --git a/UI/BagFrame.lua b/UI/BagFrame.lua index 3e30964..6ba7d05 100644 --- a/UI/BagFrame.lua +++ b/UI/BagFrame.lua @@ -6,7 +6,19 @@ 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 showKeyring = false -- Toggle for keyring display @@ -109,6 +121,9 @@ end -- OnHide function Guda_BagFrame_OnHide(self) + -- Close any open dropdown menus when the bag frame is hidden + CloseDropDownMenus() + -- Clean up all buttons when frame is hidden (safe since we're not displaying) for _, bagParent in pairs(bagParents) do if bagParent then @@ -146,21 +161,7 @@ end -- Update lock states of existing buttons (lightweight, used during drag) function BagFrame:UpdateLockStates() - for _, bagParent in pairs(bagParents) do - if bagParent then - local buttons = { bagParent:GetChildren() } - for _, button in ipairs(buttons) do - if button.hasItem ~= nil and button:IsShown() and button.bagID and button.slotID then - -- Get live lock state - local _, _, locked = GetContainerItemInfo(button.bagID, button.slotID) - -- Update desaturation (gray out locked items) - if not button.otherChar and not button.isReadOnly and SetItemButtonDesaturated then - SetItemButtonDesaturated(button, locked, 0.5, 0.5, 0.5) - end - end - end - end - end + Guda_UpdateLockStates(bagParents) end -- Update bagline layout (hover option) @@ -365,44 +366,13 @@ function BagFrame:Update() end end --- Helper to get or create section header +-- Delegate to centralized helpers function BagFrame:GetSectionHeader(index) - local name = "Guda_BagFrame_SectionHeader" .. index - local header = getglobal(name) - if not header then - header = CreateFrame("Frame", name, getglobal("Guda_BagFrame_ItemContainer")) - header:SetHeight(20) - header:EnableMouse(true) - local text = header:CreateFontString(nil, "OVERLAY", "GameFontNormal") - text:SetPoint("LEFT", header, "LEFT", 0, 0) - header.text = text - - header:SetScript("OnEnter", function() - if this.fullName and this.isShortened then - GameTooltip:SetOwner(this, "ANCHOR_TOP") - GameTooltip:SetText(this.fullName) - GameTooltip:Show() - end - end) - header:SetScript("OnLeave", function() - GameTooltip:Hide() - end) - end - header.inUse = true - return header + return Guda_GetSectionHeader("Guda_BagFrame", "Guda_BagFrame_ItemContainer", index) end --- Helper to get or create bag parent frame function BagFrame:GetBagParent(bagID) - local itemContainer = getglobal("Guda_BagFrame_ItemContainer") - if not bagParents[bagID] then - bagParents[bagID] = CreateFrame("Frame", "Guda_BagFrame_BagParent"..bagID, itemContainer) - bagParents[bagID]:SetAllPoints(itemContainer) - if bagParents[bagID].SetID then - bagParents[bagID]:SetID(bagID) - end - end - return bagParents[bagID] + return Guda_GetBagParent("Guda_BagFrame", bagParents, bagID, "Guda_BagFrame_ItemContainer") end -- Display items by category @@ -953,113 +923,7 @@ end -- Resize frame based on number of rows and columns function BagFrame:ResizeFrame(currentRow, currentCol, columns, overrideHeight) - local buttonSize = addon.Modules.DB:GetSetting("iconSize") or addon.Constants.BUTTON_SIZE - local spacing = addon.Modules.DB:GetSetting("iconSpacing") or addon.Constants.BUTTON_SPACING - - -- Calculate actual number of rows used - local totalRows = (currentRow or 0) + 1 - - -- Ensure at least 1 row - if totalRows < 1 then - totalRows = 1 - end - - -- Calculate required dimensions based on columns - local containerWidth = (columns * (buttonSize + spacing)) + 20 - local containerHeight = overrideHeight or ((totalRows * (buttonSize + spacing)) + 20) - local frameWidth = containerWidth + 20 - - -- Check if search bar is visible - local showSearchBar = addon.Modules.DB:GetSetting("showSearchBar") - if showSearchBar == nil then - showSearchBar = true - end - - -- Adjust frame height based on search bar visibility - -- Footer height varies: more space needed when search bar is visible - local titleHeight = 40 - local searchBarHeight = 30 - local footerHeight - local frameHeight - - local hideFooter = addon.Modules.DB:GetSetting("hideFooter") - - if hideFooter then - footerHeight = 10 -- Small padding at bottom - frameHeight = containerHeight + titleHeight + (showSearchBar and searchBarHeight or 0) + footerHeight - elseif showSearchBar then - footerHeight = 55 -- Increased footer height when search bar is visible (toolbar 40px + spacing 15px) - frameHeight = containerHeight + titleHeight + searchBarHeight + footerHeight -- 125 total - else - footerHeight = 45 -- Normal footer height (toolbar 40px + spacing 5px) - frameHeight = containerHeight + titleHeight + footerHeight -- 85 total - end - - -- Minimum sizes - if containerWidth < 200 then - containerWidth = 200 - frameWidth = 220 - end - if containerHeight < 150 then - containerHeight = 150 - end - if frameHeight < 250 then - frameHeight = 250 - end - - -- Maximum sizes - if containerWidth > 1250 then - containerWidth = 1250 - frameWidth = 1270 - end - if containerHeight > 1000 then - containerHeight = 1000 - end - if frameHeight > 1200 then - frameHeight = 1200 - end - - -- Resize frames - local bagFrame = getglobal("Guda_BagFrame") - local itemContainer = getglobal("Guda_BagFrame_ItemContainer") - - if bagFrame then - bagFrame:SetWidth(frameWidth) - bagFrame:SetHeight(frameHeight) - - -- Always use BOTTOMRIGHT anchor to make frame grow left - bagFrame:ClearAllPoints() - - if addon and addon.Modules and addon.Modules.DB then - local pos = addon.Modules.DB:GetSetting("bagFramePosition") - -- Only use saved position if it was saved as BOTTOMRIGHT - if pos and pos.point == "BOTTOMRIGHT" and pos.x and pos.y then - bagFrame:SetPoint("BOTTOMRIGHT", "UIParent", "BOTTOMRIGHT", pos.x, pos.y) - else - -- Default position: bottom right corner - bagFrame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -50, 100) - end - else - -- Fallback to default if DB not available - bagFrame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -50, 100) - end - end - - if itemContainer then - itemContainer:SetWidth(containerWidth) - itemContainer:SetHeight(containerHeight) - end - - -- Resize search bar and toolbar to match container width - local searchBar = getglobal("Guda_BagFrame_SearchBar") - if searchBar then - searchBar:SetWidth(containerWidth) - end - - local toolbar = getglobal("Guda_BagFrame_Toolbar") - if toolbar then - toolbar:SetWidth(containerWidth) - end + return Guda_ResizeFrame("Guda_BagFrame", "Guda_BagFrame_ItemContainer", currentRow, currentCol, columns, overrideHeight) end -- Check if search is currently active @@ -1069,74 +933,8 @@ end -- Check if item passes search filter (pfUI style) function BagFrame:PassesSearchFilter(itemData) --- If no search text, everything matches - if not self:IsSearchActive() then - return true - end - - -- Empty slots don't match when searching (pfUI style - they get dimmed) - if not itemData then - return false - end - - -- Get item name from itemData.name or parse from link - local itemName = itemData.name - if not itemName and itemData.link then - -- Parse name from item link: |cffffffff|Hitem:...|h[Item Name]|h|r - local _, _, name = string.find(itemData.link, "%[(.+)%]") - itemName = name - if not self.warnedAboutParsing then - addon:Print("DEBUG: Had to parse item name from link: " .. (itemName or "FAILED")) - self.warnedAboutParsing = true - end - end - - if not itemName then - if not self.warnedAboutNoName then - addon:Print("DEBUG: Item has no name and no link! texture = " .. tostring(itemData.texture)) - self.warnedAboutNoName = true - end - return false - end - - -- Case-insensitive search in item name - local search = string.lower(searchText) - - -- Advanced search (pfUI style categories) - if string.sub(search, 1, 1) == "~" then - local category = string.sub(search, 2) - local itemType = itemData.class or "" - local itemQuality = itemData.quality or -1 - - if category == "equipment" or category == "armor" or category == "weapon" then - if itemType == "Armor" or itemType == "Weapon" then return true end - elseif category == "consumable" then - if itemType == "Consumable" then return true end - elseif category == "tradegoods" or category == "trades" then - if itemType == "Trade Goods" then return true end - elseif category == "quest" then - local isQuest, isQuestStarter = Guda_GetQuestInfo(itemData.bagID, itemData.slotID, itemData.isBank) - if isQuest or isQuestStarter or itemType == "Quest" then return true end - elseif category == "reagent" then - if itemType == "Reagent" then return true end - elseif category == "common" then if itemQuality == 1 then return true end - elseif category == "uncommon" then if itemQuality == 2 then return true end - elseif category == "rare" then if itemQuality == 3 then return true end - elseif category == "epic" then if itemQuality == 4 then return true end - elseif category == "legendary" then if itemQuality == 5 then return true end - end - end - - itemName = string.lower(itemName) - -- Check if item name contains search text - local matches = string.find(itemName, search, 1, true) ~= nil - - -- Debug: print first match found - if matches and not self.foundFirstMatch then - self.foundFirstMatch = true - end - - return matches + if not self:IsSearchActive() then return true end + return Guda_PassesSearchFilter(itemData, searchText) end function BagFrame:UpdateMoney() @@ -1405,119 +1203,15 @@ function Guda_BagFrame_MoneyOnEnter(self) end -- Dropdown management -local characterDropdown = nil -local bankDropdown = nil - --- Toggle character dropdown -function Guda_BagFrame_ToggleCharacterDropdown(button) --- Hide bank dropdown if it's shown - if bankDropdown and bankDropdown:IsShown() then - bankDropdown:Hide() - end - - if characterDropdown and characterDropdown:IsShown() then - characterDropdown:Hide() - return - end - - if not characterDropdown then - -- Create dropdown frame - characterDropdown = CreateFrame("Frame", "Guda_CharacterDropdown", UIParent) - characterDropdown:SetFrameStrata("DIALOG") - characterDropdown:SetWidth(200) - characterDropdown:SetBackdrop({ - bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", - edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", - tile = true, - tileSize = 16, - edgeSize = 16, - insets = { left = 4, right = 4, top = 4, bottom = 4 } - }) - characterDropdown:SetBackdropColor(0, 0, 0, 0.95) - characterDropdown:EnableMouse(true) - characterDropdown:Hide() - - characterDropdown.buttons = {} - end - - -- Position dropdown below the button - characterDropdown:ClearAllPoints() - characterDropdown:SetPoint("TOPLEFT", button, "BOTTOMLEFT", 0, -2) - - -- Clear existing buttons - for _, btn in ipairs(characterDropdown.buttons) do - btn:Hide() - end - characterDropdown.buttons = {} - - -- Get all characters on current realm - local chars = addon.Modules.DB:GetAllCharacters(false, true) - - -- Add "Current Character" option at the top - local yOffset = -8 - local currentCharButton = CreateFrame("Button", nil, characterDropdown) - currentCharButton:SetWidth(188) - currentCharButton:SetHeight(20) - currentCharButton:SetPoint("TOP", characterDropdown, "TOP", 0, yOffset) - - -- Button background on hover - local currentCharBg = currentCharButton:CreateTexture(nil, "BACKGROUND") - currentCharBg:SetAllPoints() - currentCharBg:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") - currentCharBg:SetBlendMode("ADD") - currentCharBg:SetAlpha(0) - - -- Button text - local currentCharText = currentCharButton:CreateFontString(nil, "ARTWORK", "GameFontHighlight") - currentCharText:SetPoint("LEFT", currentCharButton, "LEFT", 8, 0) - currentCharText:SetText("Characters") - - -- Button scripts - currentCharButton:SetScript("OnEnter", function() - currentCharBg:SetAlpha(0.3) - end) - currentCharButton:SetScript("OnLeave", function() - currentCharBg:SetAlpha(0) - end) - currentCharButton:SetScript("OnClick", function() - addon.Modules.BagFrame:ShowCurrentCharacter() - characterDropdown:Hide() - end) - - table.insert(characterDropdown.buttons, currentCharButton) - yOffset = yOffset - 20 - - -- Add separator - local separator = characterDropdown:CreateTexture(nil, "ARTWORK") - separator:SetHeight(1) - separator:SetWidth(180) - separator:SetPoint("TOP", characterDropdown, "TOP", 0, yOffset) - separator:SetTexture(1, 1, 1, 0.2) - yOffset = yOffset - 4 - - -- Get current player's full name for comparison +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() - -- Add character buttons - for _, char in ipairs(chars) do - -- Capture variables in local scope for closure + for i, char in ipairs(characters) do local charFullName = char.fullName - local charName = char.name - local charMoney = char.money or 0 local charClassToken = char.classToken - local isCurrentChar = (charFullName == currentPlayerFullName) - - local charButton = CreateFrame("Button", nil, characterDropdown) - charButton:SetWidth(188) - charButton:SetHeight(20) - charButton:SetPoint("TOP", characterDropdown, "TOP", 0, yOffset) - - -- Button background on hover - local charBg = charButton:CreateTexture(nil, "BACKGROUND") - charBg:SetAllPoints() - charBg:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") - charBg:SetBlendMode("ADD") - charBg:SetAlpha(0) -- Get class color local classColor = charClassToken and RAID_CLASS_COLORS[charClassToken] @@ -1526,169 +1220,115 @@ function Guda_BagFrame_ToggleCharacterDropdown(button) r, g, b = classColor.r, classColor.g, classColor.b end - -- Button text - local charText = charButton:CreateFontString(nil, "ARTWORK", "GameFontHighlight") - charText:SetPoint("LEFT", charButton, "LEFT", 8, 0) - charText:SetText(charName) - charText:SetTextColor(r, g, b) + -- Create colored name + local coloredName = addon.Modules.Utils:ColorText(char.name, r, g, b) - -- Money text - local moneyText = charButton:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") - moneyText:SetPoint("RIGHT", charButton, "RIGHT", -8, 0) - moneyText:SetText(addon.Modules.Utils:FormatMoney(charMoney)) - moneyText:SetTextColor(0.7, 0.7, 0.7) - - -- Button scripts - charButton:SetScript("OnEnter", function() - charBg:SetAlpha(0.3) - end) - charButton:SetScript("OnLeave", function() - charBg:SetAlpha(0) - end) - charButton:SetScript("OnClick", function() - if charFullName then - if isCurrentChar then - -- Clicking current character - show current live view - addon.Modules.BagFrame:ShowCurrentCharacter() - else - -- Clicking different character - show their stored bags - addon.Modules.BagFrame:ShowCharacter(charFullName) - end - characterDropdown:Hide() + info = {} + info.text = coloredName + info.func = function() + if charFullName == currentPlayerFullName then + addon.Modules.BagFrame:ShowCurrentCharacter() else - addon:Print("Error: Character fullName is nil") + addon.Modules.BagFrame:ShowCharacter(charFullName) end - end) - - table.insert(characterDropdown.buttons, charButton) - yOffset = yOffset - 20 + end + info.checked = (currentViewChar == charFullName or (not currentViewChar and charFullName == currentPlayerFullName)) + UIDropDownMenu_AddButton(info) end - - -- Set dropdown height based on content - characterDropdown:SetHeight(math.abs(yOffset) + 8) - - -- Show dropdown - characterDropdown:Show() end --- Hide dropdown when clicking elsewhere -local function HideCharacterDropdown() - if characterDropdown then - characterDropdown:Hide() +-- 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) --- Hide character dropdown if it's shown - if characterDropdown and characterDropdown:IsShown() then - characterDropdown:Hide() + local menuFrame = getglobal("Guda_BagBankMenu") + if not menuFrame then + menuFrame = CreateFrame("Frame", "Guda_BagBankMenu", UIParent, "UIDropDownMenuTemplate") end - - if bankDropdown and bankDropdown:IsShown() then - bankDropdown:Hide() - return - end - - if not bankDropdown then - -- Create dropdown frame - bankDropdown = CreateFrame("Frame", "Guda_BankDropdown", UIParent) - bankDropdown:SetFrameStrata("DIALOG") - bankDropdown:SetWidth(200) - bankDropdown:SetBackdrop({ - bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", - edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", - tile = true, - tileSize = 16, - edgeSize = 16, - insets = { left = 4, right = 4, top = 4, bottom = 4 } - }) - bankDropdown:SetBackdropColor(0, 0, 0, 0.95) - bankDropdown:EnableMouse(true) - bankDropdown:Hide() - - bankDropdown.buttons = {} - end - - -- Position dropdown below the button - bankDropdown:ClearAllPoints() - bankDropdown:SetPoint("TOPLEFT", button, "BOTTOMLEFT", 0, -2) - -- Clear existing buttons - for _, btn in ipairs(bankDropdown.buttons) do - btn:Hide() - end - bankDropdown.buttons = {} - - -- Get all characters on current realm - local chars = addon.Modules.DB:GetAllCharacters(false, true) - - local yOffset = -8 - - -- Add character buttons - for _, char in ipairs(chars) do - -- Capture variables in local scope for closure - local charFullName = char.fullName - local charName = char.name - local charMoney = char.money or 0 - local charClassToken = char.classToken - - local charButton = CreateFrame("Button", nil, bankDropdown) - charButton:SetWidth(188) - charButton:SetHeight(20) - charButton:SetPoint("TOP", bankDropdown, "TOP", 0, yOffset) - - -- Button background on hover - local charBg = charButton:CreateTexture(nil, "BACKGROUND") - charBg:SetAllPoints() - charBg:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") - charBg:SetBlendMode("ADD") - charBg:SetAlpha(0) - - -- 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 - - -- Button text - local charText = charButton:CreateFontString(nil, "ARTWORK", "GameFontHighlight") - charText:SetPoint("LEFT", charButton, "LEFT", 8, 0) - charText:SetText(charName) - charText:SetTextColor(r, g, b) - - -- Money text - local moneyText = charButton:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") - moneyText:SetPoint("RIGHT", charButton, "RIGHT", -8, 0) - moneyText:SetText(addon.Modules.Utils:FormatMoney(charMoney)) - moneyText:SetTextColor(0.7, 0.7, 0.7) - - -- Button scripts - charButton:SetScript("OnEnter", function() - charBg:SetAlpha(0.3) - end) - charButton:SetScript("OnLeave", function() - charBg:SetAlpha(0) - end) - charButton:SetScript("OnClick", function() - if charFullName then - -- Show bank for this character - Guda_BagFrame_ShowCharacterBank(charFullName, charName) - bankDropdown:Hide() - else - addon:Print("Error: Character fullName is nil") - end - end) - - table.insert(bankDropdown.buttons, charButton) - yOffset = yOffset - 20 - end - - -- Set dropdown height based on content - bankDropdown:SetHeight(math.abs(yOffset) + 8) - - -- Show dropdown - bankDropdown:Show() + UIDropDownMenu_Initialize(menuFrame, Guda_BagBankMenu_Initialize, "MENU") + ToggleDropDownMenu(1, nil, menuFrame, "cursor", 0, 0) end -- Show character's bank @@ -2567,7 +2207,6 @@ function BagFrame:Initialize() if bagFrame then local originalOnMouseDown = bagFrame:GetScript("OnMouseDown") bagFrame:SetScript("OnMouseDown", function() - HideCharacterDropdown() if originalOnMouseDown then originalOnMouseDown() end diff --git a/UI/BagFrame.xml b/UI/BagFrame.xml index aa887a5..68f0e85 100644 --- a/UI/BagFrame.xml +++ b/UI/BagFrame.xml @@ -248,6 +248,52 @@ + + + @@ -654,15 +700,6 @@ - - - - - - - - - diff --git a/UI/BankFrame.lua b/UI/BankFrame.lua index 446fa21..9244477 100644 --- a/UI/BankFrame.lua +++ b/UI/BankFrame.lua @@ -7,6 +7,9 @@ local BankFrame = {} addon.Modules.BankFrame = BankFrame local currentViewChar = nil +function BankFrame:GetCurrentViewChar() + return currentViewChar +end local searchText = "" local isReadOnlyMode = false -- Track if viewing saved bank (read-only) or live bank (interactive) local hiddenBankBags = {} -- Track which bank bags are hidden (bagID -> true/false) @@ -78,6 +81,18 @@ function Guda_BankFrame_OnShow(self) BankFrame:Update() end +-- OnHide +function Guda_BankFrame_OnHide(self) + -- Close any open dropdown menus when the bank frame is hidden + CloseDropDownMenus() + + -- Close the actual Blizzard bank too + local blizzardBankFrame = getglobal("BankFrame") + if blizzardBankFrame and blizzardBankFrame:IsShown() then + CloseBankFrame() + end +end + -- Toggle visibility function BankFrame:Toggle() if Guda_BankFrame:IsShown() then @@ -103,21 +118,7 @@ end -- Update lock states of existing buttons (lightweight, used during drag) function BankFrame:UpdateLockStates() - for _, bankBagParent in pairs(bankBagParents) do - if bankBagParent then - local buttons = { bankBagParent:GetChildren() } - for _, button in ipairs(buttons) do - if button.hasItem ~= nil and button:IsShown() and button.bagID and button.slotID then - -- Get live lock state - local _, _, locked = GetContainerItemInfo(button.bagID, button.slotID) - -- Update desaturation (gray out locked items) - if not button.otherChar and not button.isReadOnly and SetItemButtonDesaturated then - SetItemButtonDesaturated(button, locked, 0.5, 0.5, 0.5) - end - end - end - end - end + Guda_UpdateLockStates(bankBagParents) end -- Update display @@ -217,44 +218,13 @@ function BankFrame:Update() end end --- Helper to get or create section header +-- Use centralized frame helpers for section headers and bag parents function BankFrame:GetSectionHeader(index) - local name = "Guda_BankFrame_SectionHeader" .. index - local header = getglobal(name) - if not header then - header = CreateFrame("Frame", name, getglobal("Guda_BankFrame_ItemContainer")) - header:SetHeight(20) - header:EnableMouse(true) - local text = header:CreateFontString(nil, "OVERLAY", "GameFontNormal") - text:SetPoint("LEFT", header, "LEFT", 0, 0) - header.text = text - - header:SetScript("OnEnter", function() - if this.fullName and this.isShortened then - GameTooltip:SetOwner(this, "ANCHOR_TOP") - GameTooltip:SetText(this.fullName) - GameTooltip:Show() - end - end) - header:SetScript("OnLeave", function() - GameTooltip:Hide() - end) - end - header.inUse = true - return header + return Guda_GetSectionHeader("Guda_BankFrame", "Guda_BankFrame_ItemContainer", index) end --- Helper to get or create bank bag parent frame function BankFrame:GetBagParent(bagID) - local itemContainer = getglobal("Guda_BankFrame_ItemContainer") - if not bankBagParents[bagID] then - bankBagParents[bagID] = CreateFrame("Frame", "Guda_BankFrame_BagParent"..bagID, itemContainer) - bankBagParents[bagID]:SetAllPoints(itemContainer) - if bankBagParents[bagID].SetID then - bankBagParents[bagID]:SetID(bagID) - end - end - return bankBagParents[bagID] + return Guda_GetBagParent("Guda_BankFrame", bankBagParents, bagID, "Guda_BankFrame_ItemContainer") end -- Display items by category @@ -1018,59 +988,8 @@ end -- Check if item passes search filter (pfUI style) function BankFrame:PassesSearchFilter(itemData) - -- If no search text, everything matches - if not self:IsSearchActive() then - return true - end - - -- Empty slots don't match when searching (pfUI style - they get dimmed) - if not itemData then - return false - end - - -- Get item name from itemData.name or parse from link - local itemName = itemData.name - if not itemName and itemData.link then - -- Parse name from item link: |cffffffff|Hitem:...|h[Item Name]|h|r - local _, _, name = string.find(itemData.link, "%[(.+)%]") - itemName = name - end - - if not itemName then - return false - end - - -- Case-insensitive search in item name - local search = string.lower(searchText) - - -- Advanced search (pfUI style categories) - if string.sub(search, 1, 1) == "~" then - local category = string.sub(search, 2) - local itemType = itemData.class or "" - local itemQuality = itemData.quality or -1 - - if category == "equipment" or category == "armor" or category == "weapon" then - if itemType == "Armor" or itemType == "Weapon" then return true end - elseif category == "consumable" then - if itemType == "Consumable" then return true end - elseif category == "tradegoods" or category == "trades" then - if itemType == "Trade Goods" then return true end - elseif category == "quest" then - local isQuest, isQuestStarter = Guda_GetQuestInfo(itemData.bagID, itemData.slotID, itemData.isBank) - if isQuest or isQuestStarter or itemType == "Quest" then return true end - elseif category == "reagent" then - if itemType == "Reagent" then return true end - elseif category == "common" then if itemQuality == 1 then return true end - elseif category == "uncommon" then if itemQuality == 2 then return true end - elseif category == "rare" then if itemQuality == 3 then return true end - elseif category == "epic" then if itemQuality == 4 then return true end - elseif category == "legendary" then if itemQuality == 5 then return true end - end - end - - itemName = string.lower(itemName) - -- Check if item name contains search text - return string.find(itemName, search, 1, true) ~= nil + if not self:IsSearchActive() then return true end + return Guda_PassesSearchFilter(itemData, searchText) end -- Search changed handler @@ -1829,69 +1748,16 @@ function Guda_BankFrame_ClearBagButtonHighlight() end -- Bank character dropdown (similar to BagFrame's bank dropdown) -local bankCharDropdown +local function Guda_BankCharacterMenu_Initialize() + local characters = addon.Modules.DB:GetAllCharacters(false, true) + local info + local currentPlayerFullName = addon.Modules.DB:GetPlayerFullName() + local currentViewChar = addon.Modules.BankFrame:GetCurrentViewChar() -function Guda_BankFrame_ToggleBankDropdown(button) - if bankCharDropdown and bankCharDropdown:IsShown() then - bankCharDropdown:Hide() - return - end - - if not bankCharDropdown then - -- Create dropdown frame - bankCharDropdown = CreateFrame("Frame", "Guda_BankCharDropdown", UIParent) - bankCharDropdown:SetFrameStrata("DIALOG") - bankCharDropdown:SetWidth(200) - bankCharDropdown:SetBackdrop({ - bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", - edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", - tile = true, - tileSize = 16, - edgeSize = 16, - insets = { left = 4, right = 4, top = 4, bottom = 4 } - }) - bankCharDropdown:SetBackdropColor(0, 0, 0, 0.95) - bankCharDropdown:EnableMouse(true) - bankCharDropdown:Hide() - - bankCharDropdown.buttons = {} - end - - -- Position dropdown below the button - bankCharDropdown:ClearAllPoints() - bankCharDropdown:SetPoint("TOPLEFT", button, "BOTTOMLEFT", 0, -2) - - -- Clear existing buttons - for _, btn in ipairs(bankCharDropdown.buttons) do - btn:Hide() - end - bankCharDropdown.buttons = {} - - -- Get all characters on current realm - local chars = addon.Modules.DB:GetAllCharacters(false, true) - - local yOffset = -8 - - -- Add character buttons - for _, char in ipairs(chars) do - -- Capture variables in local scope for closure + for i, char in ipairs(characters) do local charFullName = char.fullName - local charName = char.name - local charMoney = char.money or 0 local charClassToken = char.classToken - - local charButton = CreateFrame("Button", nil, bankCharDropdown) - charButton:SetWidth(188) - charButton:SetHeight(20) - charButton:SetPoint("TOP", bankCharDropdown, "TOP", 0, yOffset) - - -- Button background on hover - local charBg = charButton:CreateTexture(nil, "BACKGROUND") - charBg:SetAllPoints() - charBg:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") - charBg:SetBlendMode("ADD") - charBg:SetAlpha(0) - + -- Get class color local classColor = charClassToken and RAID_CLASS_COLORS[charClassToken] local r, g, b = 1, 1, 1 @@ -1899,44 +1765,30 @@ function Guda_BankFrame_ToggleBankDropdown(button) r, g, b = classColor.r, classColor.g, classColor.b end - -- Button text - local charText = charButton:CreateFontString(nil, "ARTWORK", "GameFontHighlight") - charText:SetPoint("LEFT", charButton, "LEFT", 8, 0) - charText:SetText(charName) - charText:SetTextColor(r, g, b) + -- Create colored name + local coloredName = addon.Modules.Utils:ColorText(char.name, r, g, b) - -- Money text - local moneyText = charButton:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") - moneyText:SetPoint("RIGHT", charButton, "RIGHT", -8, 0) - moneyText:SetText(addon.Modules.Utils:FormatMoney(charMoney)) - moneyText:SetTextColor(0.7, 0.7, 0.7) - - -- Button scripts - charButton:SetScript("OnEnter", function() - charBg:SetAlpha(0.3) - end) - charButton:SetScript("OnLeave", function() - charBg:SetAlpha(0) - end) - charButton:SetScript("OnClick", function() - if charFullName then - -- Show bank for this character using the BankFrame module - addon.Modules.BankFrame:ShowCharacter(charFullName) - bankCharDropdown:Hide() + info = {} + info.text = coloredName + info.func = function() + if charFullName == currentPlayerFullName then + addon.Modules.BankFrame:ShowCurrentCharacter() else - addon:Print("Error: Character fullName is nil") + addon.Modules.BankFrame:ShowCharacter(charFullName) end - end) - - table.insert(bankCharDropdown.buttons, charButton) - yOffset = yOffset - 20 + end + info.checked = (currentViewChar == charFullName or (not currentViewChar and charFullName == currentPlayerFullName)) + UIDropDownMenu_AddButton(info) end +end - -- Set dropdown height based on content - bankCharDropdown:SetHeight(math.abs(yOffset) + 8) - - -- Show dropdown - bankCharDropdown:Show() +function Guda_BankFrame_ToggleBankDropdown(button) + local menuFrame = getglobal("Guda_BankCharacterMenu") + if not menuFrame then + menuFrame = CreateFrame("Frame", "Guda_BankCharacterMenu", UIParent, "UIDropDownMenuTemplate") + end + UIDropDownMenu_Initialize(menuFrame, Guda_BankCharacterMenu_Initialize, "MENU") + ToggleDropDownMenu(1, nil, menuFrame, "cursor", 0, 0) end -- Drag handlers for bank bag slots diff --git a/UI/BankFrame.xml b/UI/BankFrame.xml index 0d22445..225f974 100644 --- a/UI/BankFrame.xml +++ b/UI/BankFrame.xml @@ -573,15 +573,6 @@ - - - - - - - - - -- Clear search focus when clicking in item area @@ -602,11 +593,7 @@ Guda_BankFrame_OnShow(this) - Guda_BankFrame:Hide() - -- Close the actual Blizzard bank too - if BankFrame and BankFrame:IsShown() then - CloseBankFrame() - end + Guda_BankFrame_OnHide(this) -- Clear search box focus when clicking on bank frame diff --git a/UI/FrameHelpers.lua b/UI/FrameHelpers.lua new file mode 100644 index 0000000..91466da --- /dev/null +++ b/UI/FrameHelpers.lua @@ -0,0 +1,209 @@ +-- FrameHelpers: central utilities for frame headers and bag parents +local addon = Guda + +local FrameHelpers = {} +addon.Modules.FrameHelpers = FrameHelpers + +-- Create or return a section header for a given frame prefix and container +function Guda_GetSectionHeader(framePrefix, containerName, index) + local name = framePrefix .. "_SectionHeader" .. index + local header = getglobal(name) + if not header then + local container = getglobal(containerName) + header = CreateFrame("Frame", name, container) + header:SetHeight(20) + header:EnableMouse(true) + local text = header:CreateFontString(nil, "OVERLAY", "GameFontNormal") + text:SetPoint("LEFT", header, "LEFT", 0, 0) + header.text = text + + header:SetScript("OnEnter", function() + if this.fullName and this.isShortened then + GameTooltip:SetOwner(this, "ANCHOR_TOP") + GameTooltip:SetText(this.fullName) + GameTooltip:Show() + end + end) + header:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + end + header.inUse = true + return header +end + +-- Create or return a bag parent frame for a frame prefix and bag parents table +function Guda_GetBagParent(framePrefix, parentsTable, bagID, containerName) + local container = getglobal(containerName) + if not parentsTable[bagID] then + local name = framePrefix .. "_BagParent" .. bagID + parentsTable[bagID] = CreateFrame("Frame", name, container) + parentsTable[bagID]:SetAllPoints(container) + if parentsTable[bagID].SetID then + parentsTable[bagID]:SetID(bagID) + end + end + return parentsTable[bagID] +end + +-- Update lock/desaturation states for a table of parent frames +function Guda_UpdateLockStates(parentsTable) + if not parentsTable then return end + for _, parent in pairs(parentsTable) do + if parent then + local buttons = { parent:GetChildren() } + for _, button in ipairs(buttons) do + if button.hasItem ~= nil and button:IsShown() and button.bagID and button.slotID then + local ok, name, texture, count, quality, canUse = pcall(function() return GetContainerItemInfo(button.bagID, button.slotID) end) + local _, _, locked = nil, nil, nil + if ok then + -- older GetContainerItemInfo returns texture, count, locked etc in different orders; try to call a safe wrapper if available + local infoOk, iName, iTexture, iCount, iQuality, iCanUse, iLocked = pcall(function() return GetContainerItemInfo(button.bagID, button.slotID) end) + if infoOk then + -- Try to find 'locked' boolean among returned values (best-effort) + for _, val in ipairs({iName, iTexture, iCount, iQuality, iCanUse, iLocked}) do + if type(val) == "boolean" then locked = val; break end + end + end + end + if not button.otherChar and not button.isReadOnly and SetItemButtonDesaturated and locked ~= nil then + SetItemButtonDesaturated(button, locked, 0.5, 0.5, 0.5) + end + end + end + end + end +end + +-- Shared search filter used by BagFrame and BankFrame +function Guda_PassesSearchFilter(itemData, searchText) + -- If no search text, everything matches + if not searchText or searchText == "" then + return true + end + + -- Ignore common placeholders + if searchText == "Search, try ~equipment" or searchText == "Search bank..." then + return true + end + + -- Empty slots don't match when searching + if not itemData then + return false + end + + local itemName = itemData.name + if not itemName and itemData.link then + local _, _, name = string.find(itemData.link, "%[(.+)%]") + itemName = name + end + + if not itemName then return false end + + local search = string.lower(searchText) + + if string.sub(search, 1, 1) == "~" then + local category = string.sub(search, 2) + local itemType = itemData.class or "" + local itemQuality = itemData.quality or -1 + + if category == "equipment" or category == "armor" or category == "weapon" then + if itemType == "Armor" or itemType == "Weapon" then return true end + elseif category == "consumable" then + if itemType == "Consumable" then return true end + elseif category == "tradegoods" or category == "trades" then + if itemType == "Trade Goods" then return true end + elseif category == "quest" then + local isQuest, isQuestStarter = Guda_GetQuestInfo(itemData.bagID, itemData.slotID, itemData.isBank) + if isQuest or isQuestStarter or itemType == "Quest" then return true end + elseif category == "reagent" then + if itemType == "Reagent" then return true end + elseif category == "common" then if itemQuality == 1 then return true end + elseif category == "uncommon" then if itemQuality == 2 then return true end + elseif category == "rare" then if itemQuality == 3 then return true end + elseif category == "epic" then if itemQuality == 4 then return true end + elseif category == "legendary" then if itemQuality == 5 then return true end + end + end + + itemName = string.lower(itemName) + return string.find(itemName, string.lower(searchText), 1, true) ~= nil +end + +-- Generic ResizeFrame for Bag/Bank frames +function Guda_ResizeFrame(frameName, containerName, currentRow, currentCol, columns, overrideHeight) + 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 totalRows = (currentRow or 0) + 1 + if totalRows < 1 then totalRows = 1 end + + local containerWidth = (columns * (buttonSize + spacing)) + 20 + local containerHeight = overrideHeight or ((totalRows * (buttonSize + spacing)) + 20) + local frameWidth = containerWidth + 20 + + local showSearchBar = addon.Modules.DB:GetSetting("showSearchBar") + if showSearchBar == nil then showSearchBar = true end + + local titleHeight = 40 + local searchBarHeight = 30 + local footerHeight = 45 + local frameHeight + + local hideFooter = addon.Modules.DB:GetSetting("hideFooter") + if hideFooter then + footerHeight = 10 + frameHeight = containerHeight + titleHeight + (showSearchBar and searchBarHeight or 0) + footerHeight + elseif showSearchBar then + frameHeight = containerHeight + titleHeight + searchBarHeight + footerHeight + else + frameHeight = containerHeight + titleHeight + footerHeight + end + + if containerWidth < 200 then + containerWidth = 200 + frameWidth = 220 + end + if containerHeight < 150 then containerHeight = 150 end + if frameHeight < 250 then frameHeight = 250 end + + if containerWidth > 1250 then containerWidth = 1250; frameWidth = 1270 end + if containerHeight > 1000 then containerHeight = 1000 end + if frameHeight > 1200 then frameHeight = 1200 end + + local frame = getglobal(frameName) + local itemContainer = getglobal(containerName) + + if frame then + frame:SetWidth(frameWidth) + frame:SetHeight(frameHeight) + frame:ClearAllPoints() + -- Try to preserve saved position if present (saved only for Bag frame) + if addon and addon.Modules and addon.Modules.DB then + local settingName = (frameName == "Guda_BagFrame") and "bagFramePosition" or nil + if settingName then + local pos = addon.Modules.DB:GetSetting(settingName) + if pos and pos.point == "BOTTOMRIGHT" and pos.x and pos.y then + frame:SetPoint("BOTTOMRIGHT", "UIParent", "BOTTOMRIGHT", pos.x, pos.y) + else + frame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -50, 100) + end + else + frame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -50, 100) + end + else + frame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -50, 100) + end + end + + if itemContainer then + itemContainer:SetWidth(containerWidth) + itemContainer:SetHeight(containerHeight) + end + + -- Resize search bar and toolbar to match container width + local searchBar = getglobal(frameName .. "_SearchBar") + if searchBar then searchBar:SetWidth(containerWidth) end + local toolbar = getglobal(frameName .. "_Toolbar") + if toolbar then toolbar:SetWidth(containerWidth) end +end diff --git a/UI/ItemButton.lua b/UI/ItemButton.lua index db454a7..96e1b0e 100644 --- a/UI/ItemButton.lua +++ b/UI/ItemButton.lua @@ -11,6 +11,8 @@ scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") -- Check if an item is a quest item by scanning its tooltip -- Check if an item is a quest item by scanning its tooltip and determine type local function IsQuestItem(bagID, slotID, isBank) + bagID = tonumber(bagID) + slotID = tonumber(slotID) if not bagID or not slotID then return false end scanTooltip:ClearLines() @@ -117,6 +119,8 @@ end -- Scan tooltip for red text that is NOT a durability line local function IsItemUnusable(bagID, slotID, isBank) + bagID = tonumber(bagID) + slotID = tonumber(slotID) if not bagID or not slotID then return false end -- Some clients require SetOwner before every SetBagItem/SetInventoryItem to populate lines @@ -377,6 +381,15 @@ function Guda_ItemButton_OnLoad(self) -- Default behavior if ContainerFrameItemButton_OnClick then + -- Mailbox clicks should be ignored except for Ctrl+Click (preview) + -- OR if it's a live mail item for the current player and Shift+Click (loot) + if this.isMail then + if IsControlKeyDown() then + ContainerFrameItemButton_OnClick(arg1) + end + return + end + ContainerFrameItemButton_OnClick(arg1) end end) @@ -425,6 +438,10 @@ end -- Set item data function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCharName, matchesFilter, isReadOnly) + -- Proactively convert to number to avoid comparisons with strings in downstream functions + bagID = tonumber(bagID) + slotID = tonumber(slotID) + -- Proactively clear any previous cooldown overlay state before reassigning this pooled button do local cd = getglobal(self:GetName().."Cooldown") or self.cooldown @@ -447,13 +464,20 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha self.bagID = bagID self.slotID = slotID -- Also set the Blizzard slot ID for compatibility with ContainerFrameItemButtonTemplate behavior - if self.SetID and slotID then - self:SetID(slotID) + -- ALWAYS set ID to something (0 if nil) to avoid leaking old IDs when button is reused + if self.SetID then + self:SetID(slotID or 0) end + -- Explicitly set bag index to avoid Blizzard's ContainerFrameItemButton_OnEnter logic + -- from picking up this button as part of a real bag. + self.bagIndex = bagID or -100 -- Use an invalid bag index for non-bag buttons self.itemData = itemData self.isBank = isBank or false self.otherChar = otherCharName self.isReadOnly = isReadOnly or false -- Track if this is read-only mode + self.isMail = false -- Clear mailbox flag by default + self.mailIndex = nil + self.mailItemIndex = nil -- Re-register for drag/drop every time (crucial for button reuse in Classic/Vanilla) if not self.isReadOnly and not self.otherChar then @@ -481,6 +505,8 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha matchesFilter = true end + self.mailData = nil -- Clear mail metadata by default + -- Use Blizzard's default count fontstring (ContainerFrameItemButtonTemplate creates $parentCount) local countText = getglobal(self:GetName().."Count") local emptySlotBg = getglobal(self:GetName().."_EmptySlotBg") @@ -538,7 +564,17 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha -- Apply the determined texture and count if self.hasItem then - if SetItemButtonTexture then SetItemButtonTexture(self, displayTexture) end + if SetItemButtonTexture then + SetItemButtonTexture(self, displayTexture) + end + + -- Explicitly set and show icon texture as SetItemButtonTexture can be unreliable for custom paths in 1.12 + local iconTexture = getglobal(self:GetName().."IconTexture") or getglobal(self:GetName().."Icon") or self.icon or self.Icon + if iconTexture and displayTexture then + iconTexture:SetTexture(displayTexture) + iconTexture:Show() + end + if SetItemButtonCount then SetItemButtonCount(self, displayCount or 1) end if emptySlotBg then emptySlotBg:Hide() end -- Update cooldown overlay for live items @@ -625,6 +661,9 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha -- For 1.12.1 compatibility, access textures using both methods and proper naming -- We'll set the NormalTexture later based on whether slot is empty or filled + -- ICON TEXTURE: Ensure we use the correct name ($parentIconTexture is standard) + local iconTexture = getglobal(self:GetName().."IconTexture") or getglobal(self:GetName().."Icon") or self.icon or self.Icon + -- Pushed texture follows the button size/position local pushedTexture = getglobal(self:GetName().."PushedTexture") if not pushedTexture and self.GetPushedTexture then @@ -744,7 +783,7 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha -- Special border for keyring items (cyan/blue) self.qualityBorder:SetBackdropBorderColor(0.2, 0.8, 1.0, 1) self.qualityBorder:Show() - elseif itemQuality and itemLink then + elseif itemQuality then -- Check settings to determine if we should show borders (nil-safe) local showEquipmentBorder, showOtherBorder if addon and addon.Modules and addon.Modules.DB and addon.Modules.DB.GetSetting then @@ -765,10 +804,12 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha -- Check if item is equipment (nil-safe) local isEquipment = false - if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.IsEquipment then - isEquipment = addon.Modules.Utils:IsEquipment(itemLink) - elseif Guda and Guda.Modules and Guda.Modules.Utils and Guda.Modules.Utils.IsEquipment then - isEquipment = Guda.Modules.Utils:IsEquipment(itemLink) + if itemLink then + if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.IsEquipment then + isEquipment = addon.Modules.Utils:IsEquipment(itemLink) + elseif Guda and Guda.Modules and Guda.Modules.Utils and Guda.Modules.Utils.IsEquipment then + isEquipment = Guda.Modules.Utils:IsEquipment(itemLink) + end end -- Determine if we should show the border based on item type and settings @@ -920,8 +961,8 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha self.questIcon:ClearAllPoints() self.questIcon:SetPoint("TOPRIGHT", self, "TOPRIGHT", 1, 0) end - else - -- Hide icon for empty slots + elseif not self.isMail then + -- Hide icon for empty slots, but keep it for mailbox custom icons iconTexture:Hide() end end @@ -929,7 +970,7 @@ end -- OnEnter handler (show tooltip) function Guda_ItemButton_OnEnter(self) --- Highlight the corresponding bag button in the footer (works for empty and filled slots) + -- Highlight the corresponding bag button in the footer (works for empty and filled slots) if not self.otherChar and self.bagID then if self.isBank then -- Bank item - highlight bank bag button @@ -941,7 +982,7 @@ function Guda_ItemButton_OnEnter(self) end -- Early return for empty slots (no tooltip needed) - if not self.hasItem then + if not self.hasItem and not self.isMail then return end @@ -949,7 +990,71 @@ function Guda_ItemButton_OnEnter(self) -- important position of tooltip GameTooltip:SetPoint("BOTTOMRIGHT", self, "TOPLEFT", 10, 0) - if self.otherChar or self.isReadOnly then + -- Mailbox tooltip handling + if self.isMail then + local currentPlayerName = addon.Modules.DB:GetPlayerFullName() + local isMailboxOpen = addon.Modules.MailboxScanner and addon.Modules.MailboxScanner:IsMailboxOpen() + + if (not self.otherChar or self.otherChar == currentPlayerName) and self.mailIndex and isMailboxOpen then + -- Live mailbox for current character (only when mailbox is actually open) + GameTooltip:SetInboxItem(self.mailIndex, self.mailItemIndex or 1) + elseif self.itemData and (self.itemData.link or self.itemData.itemID) then + -- Read-only / other character mailbox OR current character mailbox when closed + GameTooltip.GudaViewedCharacter = self.otherChar or currentPlayerName + if self.itemData.link then + GameTooltip:SetHyperlink(self.itemData.link) + else + GameTooltip:SetHyperlink("item:" .. self.itemData.itemID .. ":0:0:0") + end + elseif self.itemData and self.itemData.name then + -- Money or generic mail + GameTooltip:AddLine(self.itemData.name, 1, 1, 1) + elseif self.mailData and self.mailData.money and self.mailData.money > 0 then + -- Fallback for money only mail + GameTooltip:AddLine("Money", 1, 1, 1) + end + + -- Add mailbox metadata if available + local mailData = self.mailData + + if mailData then + -- If we already added a line (for money/generic), or SetHyperlink/SetInboxItem added lines, + -- we might want a separator if we're adding sender info. + if GameTooltip:NumLines() > 0 then + GameTooltip:AddLine(" ") + end + + if mailData.sender then + GameTooltip:AddLine("From: " .. mailData.sender, 1, 1, 1) + end + if mailData.subject then + GameTooltip:AddLine("Subject: " .. mailData.subject, 1, 1, 0.8) + end + if (mailData.money or 0) > 0 and not (self.itemData and self.itemData.name == "Money") then + GameTooltip:AddLine("Money: " .. addon.Modules.Utils:FormatMoney(mailData.money), 1, 1, 1) + end + if (mailData.CODAmount or 0) > 0 then + GameTooltip:AddLine("COD: " .. addon.Modules.Utils:FormatMoney(mailData.CODAmount), 1, 0, 0) + end + if mailData.daysLeft then + GameTooltip:AddLine("Days left: " .. math.floor(mailData.daysLeft), 0.5, 0.5, 0.5) + end + end + + -- Add Inventory counts for mailbox items at the very bottom + if addon.Modules.Tooltip and addon.Modules.Tooltip.AddInventoryInfo then + local link = self.itemData and (self.itemData.link or (self.itemData.itemID and ("item:" .. self.itemData.itemID .. ":0:0:0"))) + if not link and self.mailIndex and isMailboxOpen then + link = addon.Modules.Utils:GetInboxItemLink(self.mailIndex, self.mailItemIndex or 1) + end + if link then + addon.Modules.Tooltip:AddInventoryInfo(GameTooltip, link) + end + end + + GameTooltip:Show() + return + elseif self.otherChar or self.isReadOnly then GameTooltip.GudaViewedCharacter = self.otherChar if self.itemData and self.itemData.link then GameTooltip:SetHyperlink(self.itemData.link) @@ -1017,8 +1122,9 @@ end -- OnLeave handler function Guda_ItemButton_OnLeave(self) -- Clear any viewed character hint on the tooltip when leaving - if GameTooltip and GameTooltip.GudaViewedCharacter then + if GameTooltip then GameTooltip.GudaViewedCharacter = nil + GameTooltip.GudaInventoryAdded = nil end GameTooltip:Hide() ResetCursor() diff --git a/UI/MailboxFrame.lua b/UI/MailboxFrame.lua new file mode 100644 index 0000000..9738ed3 --- /dev/null +++ b/UI/MailboxFrame.lua @@ -0,0 +1,376 @@ +-- Mailbox Frame +-- Mailbox viewing UI + +local addon = Guda + +local MailboxFrame = {} +addon.Modules.MailboxFrame = MailboxFrame + +local currentViewChar = nil +local searchText = "" +local isReadOnlyMode = true -- Mailbox is always read-only in this addon (viewing offline data) +local itemButtons = {} +local mailboxRows = {} +local currentPage = 1 +local ITEMS_PER_PAGE = 7 +local mailboxClickCatcher = nil + +-- OnLoad +function Guda_MailboxFrame_OnLoad(self) + -- Apply border visibility setting + if MailboxFrame.UpdateBorderVisibility then + MailboxFrame:UpdateBorderVisibility() + end + + -- Set up initial backdrop + -- (No longer needed here as UpdateBorderVisibility handles it) + + -- Set up search box placeholder + local searchBox = getglobal(self:GetName().."_SearchBar_SearchBox") + if searchBox then + searchBox:SetText("Search mailbox...") + searchBox:SetTextColor(0.5, 0.5, 0.5, 1) + end + + -- Create invisible full-screen frame to catch clicks outside the mailbox frame while typing in search + if not mailboxClickCatcher then + mailboxClickCatcher = CreateFrame("Frame", "Guda_MailboxClickCatcher", UIParent) + mailboxClickCatcher:SetFrameStrata("BACKGROUND") + mailboxClickCatcher:SetAllPoints(UIParent) + mailboxClickCatcher:EnableMouse(true) + mailboxClickCatcher:Hide() + + mailboxClickCatcher:SetScript("OnMouseDown", function() + if Guda_MailboxFrame_ClearSearch then + Guda_MailboxFrame_ClearSearch() + end + end) + end +end + +-- Clear search focus +function Guda_MailboxFrame_ClearSearch() + local searchBox = getglobal("Guda_MailboxFrame_SearchBar_SearchBox") + if searchBox then + searchBox:ClearFocus() + end +end + +-- OnShow +function Guda_MailboxFrame_OnShow(self) + -- Apply border visibility setting + if MailboxFrame.UpdateBorderVisibility then + MailboxFrame:UpdateBorderVisibility() + end + + -- Apply frame transparency + if Guda_ApplyBackgroundTransparency then + Guda_ApplyBackgroundTransparency() + end + + MailboxFrame:Update() +end + +-- Update border visibility +function MailboxFrame:UpdateBorderVisibility() + local hideBorders = addon.Modules.DB:GetSetting("hideBorders") + if hideBorders then + Guda:ApplyBackdrop(Guda_MailboxFrame, "MINIMALIST_BORDER", "DEFAULT") + else + Guda:ApplyBackdrop(Guda_MailboxFrame, "DEFAULT_FRAME", "DEFAULT") + end +end + +-- OnHide +function Guda_MailboxFrame_OnHide(self) + -- Close any open dropdown menus when the mailbox frame is hidden + CloseDropDownMenus() +end + +-- Toggle visibility +function MailboxFrame:Toggle() + if Guda_MailboxFrame:IsShown() then + Guda_MailboxFrame:Hide() + else + Guda_MailboxFrame:Show() + end +end + +-- Show specific character's mailbox +function MailboxFrame:GetCurrentViewChar() + return currentViewChar +end + +function MailboxFrame:ShowCharacter(fullName) + currentViewChar = fullName + currentPage = 1 + self:Update() +end + +-- Pagination +function MailboxFrame:NextPage() + currentPage = currentPage + 1 + self:Update() +end + +function MailboxFrame:PrevPage() + if currentPage > 1 then + currentPage = currentPage - 1 + self:Update() + end +end + +-- Initialize module +function MailboxFrame:Initialize() + -- Register events if needed +end + +-- Update the mailbox frame +function MailboxFrame:Update() + if not Guda_MailboxFrame:IsShown() then return end + + -- Determine which character to show + local charFullName = currentViewChar or addon.Modules.DB:GetPlayerFullName() + local mailboxData = addon.Modules.DB:GetCharacterMailbox(charFullName) + + -- Extract character name from fullName + local charName = charFullName + local dashPos = string.find(charFullName, "-") + if dashPos then + charName = string.sub(charFullName, 1, dashPos - 1) + end + + getglobal("Guda_MailboxFrame_Title"):SetText(charName .. "'s Mailbox") + + -- Filter items based on search text + local filteredItems = {} + local totalItems = table.getn(mailboxData) + for i, mail in ipairs(mailboxData) do + local matchesSearch = true + if searchText ~= "" then + matchesSearch = false + if mail.sender and string.find(string.lower(mail.sender), searchText) then + matchesSearch = true + elseif mail.subject and string.find(string.lower(mail.subject), searchText) then + matchesSearch = true + elseif mail.item and mail.item.name and string.find(string.lower(mail.item.name), searchText) then + matchesSearch = true + end + end + + if matchesSearch then + table.insert(filteredItems, mail) + end + end + + -- Display items + local emptyMessage = getglobal("Guda_MailboxFrame_EmptyMessage") + if emptyMessage then + if totalItems == 0 then + emptyMessage:SetText("No mailbox data found for this character.\n\nVisit a mailbox in-game to save your mail data.") + emptyMessage:Show() + else + emptyMessage:Hide() + end + end + + self:DisplayItems(filteredItems, charFullName, totalItems) +end + +-- Display mailbox items in a list +function MailboxFrame:DisplayItems(items, charFullName, totalMails) + local container = getglobal("Guda_MailboxFrame_ItemContainer") + + -- Hide all existing rows first + for _, row in pairs(mailboxRows) do + row:Hide() + end + + local totalItems = table.getn(items) + local totalPages = math.max(1, math.ceil(totalItems / ITEMS_PER_PAGE)) + + if currentPage > totalPages then + currentPage = totalPages + end + + local startIndex = (currentPage - 1) * ITEMS_PER_PAGE + 1 + local endIndex = math.min(startIndex + ITEMS_PER_PAGE - 1, totalItems) + + local rowIndex = 1 + for i = startIndex, endIndex do + local mail = items[i] + local row = mailboxRows[rowIndex] + if not row then + row = CreateFrame("Frame", "Guda_MailboxRow" .. rowIndex, container, "Guda_MailboxRowTemplate") + mailboxRows[rowIndex] = row + end + + row:ClearAllPoints() + row:SetPoint("TOPLEFT", container, "TOPLEFT", 0, -(rowIndex - 1) * 55) + + -- Row Money Frame + local moneyFrame = getglobal(row:GetName() .. "_MoneyFrame") + if moneyFrame then + -- Ensure row money frame doesn't auto-update to character money + moneyFrame.moneyType = "STATIC" + moneyFrame:UnregisterAllEvents() + moneyFrame:SetScript("OnShow", nil) -- Disable Blizzard's auto-update OnShow + moneyFrame:SetScript("OnEvent", nil) + + -- Restore right alignment for the money frame to avoid overlap with icon + moneyFrame:ClearAllPoints() + moneyFrame:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", -10, 8) + end + + if (mail.money or 0) > 0 then + moneyFrame:Show() + MoneyFrame_Update(moneyFrame:GetName(), mail.money) + else + if moneyFrame then moneyFrame:Hide() end + end + + -- Fill row data + local sender = mail.sender or "Unknown" + getglobal(row:GetName() .. "_Sender"):SetText(sender) + + local subject = mail.subject or "No Subject" + if mail.item and mail.item.quality then + local r, g, b = addon.Modules.Utils:GetQualityColor(mail.item.quality) + subject = addon.Modules.Utils:ColorText(subject, r, g, b) + end + getglobal(row:GetName() .. "_Subject"):SetText(subject) + + local expireText = "" + if mail.daysLeft then + if mail.daysLeft < 1 then + expireText = string.format("|cffff0000%dh|r", math.floor(mail.daysLeft * 24)) + else + expireText = string.format("%dd", math.floor(mail.daysLeft)) + end + end + getglobal(row:GetName() .. "_ExpireTime"):SetText(expireText) + + -- Item Button + local itemButton = getglobal(row:GetName() .. "_ItemButton") + -- Re-assign shared namespace function if needed, but it should be available globally or via addon + + itemButton.isBank = false + itemButton.otherChar = charFullName + itemButton.mailData = mail + itemButton.isMail = true + itemButton.mailIndex = mail.mailIndex + itemButton.mailItemIndex = mail.itemIndex or 1 + + -- Center vertically in the row + if itemButton then + itemButton:ClearAllPoints() + itemButton:SetPoint("LEFT", row, "LEFT", 10, 0) + end + + if mail.item and (mail.item.texture or mail.item.link) then + Guda_ItemButton_SetItem(itemButton, nil, nil, mail.item, false, charFullName, true, true) + itemButton.isMail = true + itemButton.mailData = mail + itemButton.mailIndex = mail.mailIndex + itemButton.mailItemIndex = mail.itemIndex or 1 + else + Guda_ItemButton_SetItem(itemButton, nil, nil, nil, false, charFullName, true, true) + itemButton.isMail = true + itemButton.mailData = mail + itemButton.mailIndex = mail.mailIndex + itemButton.mailItemIndex = 1 + + local icon = getglobal(itemButton:GetName().."IconTexture") or getglobal(itemButton:GetName().."Icon") + if icon then + if (mail.money or 0) > 0 then + icon:SetTexture("Interface\\Icons\\INV_Misc_Coin_01") + elseif mail.packageIcon then + icon:SetTexture(mail.packageIcon) + else + icon:SetTexture("Interface\\Icons\\INV_Letter_15") + end + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + icon:Show() + end + if itemButton.qualityBorder then itemButton.qualityBorder:Hide() end + end + + row:Show() + rowIndex = rowIndex + 1 + end + + -- Update pagination buttons + local prevBtn = getglobal("Guda_MailboxFrame_PrevPageButton") + local nextBtn = getglobal("Guda_MailboxFrame_NextPageButton") + + if currentPage > 1 then + prevBtn:Enable() + else + prevBtn:Disable() + end + + if currentPage < totalPages then + nextBtn:Enable() + else + nextBtn:Disable() + end + + -- Update pagination text + local paginationText = getglobal("Guda_MailboxFrame_Pagination_Text") + if paginationText then + if totalMails == 0 then + paginationText:SetText("No Mail") + else + paginationText:SetText(string.format("%d/%d (items: %d)", currentPage, totalPages, totalMails or 0)) + end + end +end + +-- Search text changed +function Guda_MailboxFrame_OnSearchTextChanged() + local searchBox = getglobal("Guda_MailboxFrame_SearchBar_SearchBox") + local text = searchBox:GetText() + if text == "Search mailbox..." then + searchText = "" + else + searchText = string.lower(text) + end + currentPage = 1 + MailboxFrame:Update() +end + +-- Show character selection menu +local function Guda_MailboxCharacterMenu_Initialize() + local characters = addon.Modules.DB:GetAllCharacters(true, 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() MailboxFrame:ShowCharacter(charFullName) end + info.checked = (currentViewChar == char.fullName or (not currentViewChar and char.fullName == addon.Modules.DB:GetPlayerFullName())) + UIDropDownMenu_AddButton(info) + end +end + +function Guda_MailboxFrame_ShowCharacterMenu() + local menuFrame = getglobal("Guda_MailboxCharacterMenu") + if not menuFrame then + menuFrame = CreateFrame("Frame", "Guda_MailboxCharacterMenu", UIParent, "UIDropDownMenuTemplate") + end + UIDropDownMenu_Initialize(menuFrame, Guda_MailboxCharacterMenu_Initialize, "MENU") + ToggleDropDownMenu(1, nil, menuFrame, "cursor", 0, 0) +end diff --git a/UI/MailboxFrame.xml b/UI/MailboxFrame.xml new file mode 100644 index 0000000..94d1590 --- /dev/null +++ b/UI/MailboxFrame.xml @@ -0,0 +1,355 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Guda_MailboxFrame_OnSearchTextChanged() + + + this:ClearFocus() + + + this:ClearFocus() + + + if this:GetText() == "Search mailbox..." then + this:SetText("") + this:SetTextColor(1, 1, 1, 1) + end + if Guda_MailboxClickCatcher then + Guda_MailboxClickCatcher:Show() + end + + + if this:GetText() == "" then + this:SetText("Search mailbox...") + this:SetTextColor(0.5, 0.5, 0.5, 1) + end + if Guda_MailboxClickCatcher then + Guda_MailboxClickCatcher:Hide() + end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Guda_MailboxFrame_OnLoad(this) + + + Guda_MailboxFrame_OnShow(this) + + + Guda_MailboxFrame_OnHide(this) + + + this:StartMoving() + + + this:StopMovingOrSizing() + + + + diff --git a/UI/SettingsPopup.lua b/UI/SettingsPopup.lua index 56f4030..a57b499 100644 --- a/UI/SettingsPopup.lua +++ b/UI/SettingsPopup.lua @@ -368,7 +368,7 @@ function Guda_ApplyBackgroundTransparency() local transparency = Guda.Modules.DB:GetSetting("bgTransparency") or 0.15 local alpha = 1.0 - transparency - local frames = { "Guda_BagFrame", "Guda_BankFrame", "Guda_SettingsPopup", "Guda_QuestItemBar" } + local frames = { "Guda_BagFrame", "Guda_BankFrame", "Guda_MailboxFrame", "Guda_SettingsPopup", "Guda_QuestItemBar" } for _, frameName in ipairs(frames) do local frame = getglobal(frameName) if frame then @@ -603,6 +603,15 @@ function Guda_SettingsPopup_HideBordersCheckbox_OnClick(self) end end + local mailboxFrame = getglobal("Guda_MailboxFrame") + if mailboxFrame then + if isChecked then + Guda:ApplyBackdrop(mailboxFrame, "MINIMALIST_BORDER", "DEFAULT") + else + Guda:ApplyBackdrop(mailboxFrame, "DEFAULT_FRAME", "DEFAULT") + end + end + -- Update border visibility on settings frame if SettingsPopup.UpdateBorderVisibility then SettingsPopup:UpdateBorderVisibility()