-- FrameHelpers: central utilities for frame headers and bag parents local addon = Guda local FrameHelpers = {} addon.Modules.FrameHelpers = FrameHelpers -- Standard category list used by both Bag and Bank frames Guda_CategoryList = { "BoE", "Weapon", "Armor", "Consumable", "Food", "Drink", "Trade Goods", "Reagent", "Recipe", "Quiver", "Container", "Soul Bag", "Miscellaneous", "Quest", "Junk", "Class Items", "Keyring" } -- Categorize a single item into categories and specialItems tables -- Returns nothing, modifies tables in place function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, isOtherChar) local itemName = itemData.name or "" local itemType = itemData.type or "" local cat = "Miscellaneous" -- Detect consumable restore/eat/drink tag for current character only if not isOtherChar and addon.Modules.Utils and addon.Modules.Utils.GetConsumableRestoreTag then local tag = addon.Modules.Utils:GetConsumableRestoreTag(bagID, slotID) if tag then itemData.restoreTag = tag end end -- Priority 1: Special items (Hearthstone, Mounts, Tools) if string.find(itemName, "Hearthstone") then table.insert(specialItems.Hearthstone, {bagID = bagID, slotID = slotID, itemData = itemData}) return elseif addon.Modules.SortEngine and addon.Modules.SortEngine.IsMount and addon.Modules.SortEngine.IsMount(itemData.texture) then table.insert(specialItems.Mount, {bagID = bagID, slotID = slotID, itemData = itemData}) return elseif string.find(itemName, "Runed .* Rod") or itemType == "Fishing Pole" or string.find(itemName, "Mining Pick") or string.find(itemName, "Blacksmith Hammer") or itemName == "Arclight Spanner" or itemName == "Gyromatic Micro-Adjustor" or itemName == "Philosopher's Stone" or string.find(itemName, "Skinning Knife") or itemName == "Blood Scythe" then table.insert(specialItems.Tools, {bagID = bagID, slotID = slotID, itemData = itemData}) return end -- Priority 2: Class Items (Soul Shards, Arrows, Bullets) if addon.Modules.Utils:IsSoulShard(itemData.link) or itemData.class == "Projectile" or itemData.subclass == "Arrow" or itemData.subclass == "Bullet" then table.insert(categories["Class Items"], {bagID = bagID, slotID = slotID, itemData = itemData}) return end -- Priority 3: Quest Items -- Check multiple sources: tooltip scan, itemClass, itemType, and QuestItemsDB local isQuestItem = false if itemData.class == "Quest" or itemData.type == "Quest" then isQuestItem = true elseif not isOtherChar then isQuestItem = addon.Modules.Utils:IsQuestItemTooltip(bagID, slotID) end -- Also check the QuestItemsDB for known faction-specific quest items if not isQuestItem and itemData.link then local itemID = addon.Modules.Utils:ExtractItemID(itemData.link) if itemID and addon.IsQuestItemByID then local playerFaction = UnitFactionGroup("player") isQuestItem = addon:IsQuestItemByID(itemID, playerFaction) end end if isQuestItem then table.insert(categories["Quest"], {bagID = bagID, slotID = slotID, itemData = itemData}) return end -- Priority 4: Junk (Gray items) if itemData.quality == 0 or addon.Modules.Utils:IsItemGrayTooltip(bagID, slotID, itemData.link) then table.insert(categories["Junk"], {bagID = bagID, slotID = slotID, itemData = itemData}) return end -- Priority 5: Food and Drink if itemData.class == "Consumable" then cat = "Consumable" local sub = itemData.subclass or "" if sub == "Food & Drink" or string.find(sub, "Food") or string.find(sub, "Drink") then if string.find(sub, "Drink") then cat = "Drink" else cat = "Food" end end table.insert(categories[cat], {bagID = bagID, slotID = slotID, itemData = itemData}) return end -- Priority 6: BoE Equipment (Armor/Weapons that bind when equipped) if (itemData.class == "Weapon" or itemData.class == "Armor") and not isOtherChar then local isBoE = addon.Modules.Utils:IsBindOnEquip(bagID, slotID, itemData.link) if isBoE then table.insert(categories["BoE"], {bagID = bagID, slotID = slotID, itemData = itemData}) else table.insert(categories[itemData.class], {bagID = bagID, slotID = slotID, itemData = itemData}) end return end -- Priority 7: Equipment for other characters (can't scan tooltip) if (itemData.class == "Weapon" or itemData.class == "Armor") and isOtherChar then table.insert(categories[itemData.class], {bagID = bagID, slotID = slotID, itemData = itemData}) return end -- Priority 8: Other Categories cat = itemData.class or "Miscellaneous" if not categories[cat] then cat = "Miscellaneous" end table.insert(categories[cat], {bagID = bagID, slotID = slotID, itemData = itemData}) end -- Initialize empty category tables function Guda_InitCategories() local categories = {} for _, cat in ipairs(Guda_CategoryList) do categories[cat] = {} end local specialItems = { Hearthstone = {}, Mount = {}, Tools = {} } return categories, specialItems end -- Sort items within a category function Guda_SortCategoryItems(items) table.sort(items, function(a, b) -- Rank Trade Goods: meat (name ends with 'meat') = 2, egg (contains 'egg') = 1, others = 0 local function tgRank(d) if not d or not d.name then return 0 end local t = d.type or d.class or "" if t ~= "Trade Goods" then return 0 end local n = string.lower(d.name) if string.find(n, "meat$") then return 2 end if string.find(n, "egg") then return 1 end return 0 end local ra = tgRank(a.itemData) local rb = tgRank(b.itemData) if ra ~= rb then return ra > rb end -- Priority: consumable restore tags (eat > drink > restore > nil) local pa = a.itemData and a.itemData.restoreTag or nil local pb = b.itemData and b.itemData.restoreTag or nil local function pr(t) if t == "eat" then return 3 end if t == "drink" then return 2 end if t == "restore" then return 1 end return 0 end if pr(pa) ~= pr(pb) then return pr(pa) > pr(pb) end -- Fallback: subclass, quality, name if a.itemData.subclass ~= b.itemData.subclass then return (a.itemData.subclass or "") < (b.itemData.subclass or "") end if a.itemData.quality ~= b.itemData.quality then return a.itemData.quality > b.itemData.quality end return (a.itemData.name or "") < (b.itemData.name or "") end) end -- 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 -- GetContainerItemInfo returns: texture, itemCount, locked, quality, readable -- The 3rd return value is the lock state (boolean or nil) local _, _, locked = GetContainerItemInfo(button.bagID, button.slotID) if not button.otherChar and not button.isReadOnly and SetItemButtonDesaturated then -- locked can be true/1 (locked) or nil/false (unlocked) 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