diff --git a/Core/Database.lua b/Core/Database.lua
index 0bdebcf..533b830 100644
--- a/Core/Database.lua
+++ b/Core/Database.lua
@@ -47,12 +47,20 @@ function DB:Initialize()
bgTransparency = 0.15,
showTrackedItems = true,
showTooltipCounts = true,
+ bagViewType = "single", -- single, category
+ bankViewType = "single", -- single, category
trackedItems = {},
},
}
end
-- Ensure new settings exist for existing installations
+ if Guda_CharDB.settings.bagViewType == nil then
+ Guda_CharDB.settings.bagViewType = "single"
+ end
+ if Guda_CharDB.settings.bankViewType == nil then
+ Guda_CharDB.settings.bankViewType = "single"
+ end
if Guda_CharDB.settings.showTooltipCounts == nil then
Guda_CharDB.settings.showTooltipCounts = true
end
diff --git a/Core/Utils.lua b/Core/Utils.lua
index b862dbc..ca78ade 100644
--- a/Core/Utils.lua
+++ b/Core/Utils.lua
@@ -155,6 +155,46 @@ function Utils:FormatTimeAgo(timestamp)
end
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
+
+-- Check if an item is a quest item by scanning its tooltip
+function Utils:IsQuestItemTooltip(bagID, slotID)
+ if not bagID or not slotID then return false end
+
+ local tooltip = GetScanTooltip()
+ tooltip:ClearLines()
+ tooltip:SetBagItem(bagID, slotID)
+
+ -- Check all tooltip lines for quest-related text
+ for i = 1, tooltip:NumLines() do
+ local line = getglobal("GudaBagScanTooltipTextLeft" .. i)
+ if line then
+ local text = line:GetText()
+ if text then
+ -- Check for quest starter patterns
+ if string.find(text, "Quest Starter") or
+ string.find(text, "This Item Begins a Quest") or
+ string.find(text, "Use: Starts a Quest") then
+ return true
+ -- Check for regular quest item patterns
+ elseif string.find(text, "Quest Item") or
+ string.find(text, "Manual") then
+ return true
+ end
+ end
+ end
+ end
+ return false
+end
+
-- Get bag slot count
function Utils:GetBagSlotCount(bagID)
if bagID == -1 then
@@ -190,17 +230,6 @@ function Utils:TruncateText(text, maxLen)
return string.sub(text, 1, maxLen - 3) .. "..."
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
-
--- Check if a bag is Quiver or Ammo Pouch
function Utils:IsAmmoQuiverBag(bagID)
-- Skip backpack, bank, and keyring
if bagID == 0 or bagID == -1 or bagID == -2 then
diff --git a/Guda.toc b/Guda.toc
index 35b31b4..af79b25 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.3.4
+## Version: 1.4.0
## SavedVariables: Guda_DB
## SavedVariablesPerCharacter: Guda_CharDB
diff --git a/Sorting/SortEngine.lua b/Sorting/SortEngine.lua
index 0127b65..2f892b2 100644
--- a/Sorting/SortEngine.lua
+++ b/Sorting/SortEngine.lua
@@ -56,14 +56,16 @@ local CATEGORY_ORDER = {
["Projectile"] = 3,
["Weapon"] = 4, -- Non-equippable weapons
["Armor"] = 5, -- Non-equippable armor
- ["Quiver"] = 6,
- ["Reagent"] = 7,
- ["Trade Goods"] = 8,
- ["Recipe"] = 9,
- ["Container"] = 10,
- ["Quest"] = 11,
- ["Key"] = 12,
- ["Miscellaneous"] = 13,
+ ["Tools"] = 6,
+ ["Quiver"] = 8,
+ ["Reagent"] = 9,
+ ["Trade Goods"] = 10,
+ ["Recipe"] = 11,
+ ["Container"] = 12,
+ ["Key"] = 14,
+ ["Miscellaneous"] = 15,
+ ["Quest"] = 16,
+ ["Class Items"] = 17,
}
-- Subclass ordering for grouping related items
@@ -144,7 +146,7 @@ local function GetTexturePattern(textureName)
end
-- Check if an item is a mount by texture path
-local function IsMount(itemTexture)
+function SortEngine.IsMount(itemTexture)
if not itemTexture then return false end
local textureLower = string.lower(itemTexture)
@@ -157,6 +159,7 @@ local function IsMount(itemTexture)
return false
end
+local IsMount = SortEngine.IsMount
-- Determine subclass order for grouping related items
local function GetSubclassOrder(subclass, itemName)
diff --git a/UI/BagFrame.lua b/UI/BagFrame.lua
index 1032ade..b06c8fa 100644
--- a/UI/BagFrame.lua
+++ b/UI/BagFrame.lua
@@ -322,7 +322,25 @@ function BagFrame:Update()
end
-- Display items
- self:DisplayItems(bagData, isOtherChar, charName)
+ local viewType = addon.Modules.DB:GetSetting("bagViewType") or "single"
+
+ -- Reset all section headers before displaying items
+ local i = 1
+ while true do
+ local header = getglobal("Guda_BagFrame_SectionHeader" .. i)
+ if not header then break end
+ header.inUse = false
+ header:Hide()
+ i = i + 1
+ end
+
+ if viewType == "category" then
+ self:DisplayItemsByCategory(bagData, isOtherChar, charName)
+ if getglobal("Guda_BagFrame_SortButton") then getglobal("Guda_BagFrame_SortButton"):Hide() end
+ else
+ self:DisplayItems(bagData, isOtherChar, charName)
+ if getglobal("Guda_BagFrame_SortButton") then getglobal("Guda_BagFrame_SortButton"):Show() end
+ end
-- Update money
self:UpdateMoney()
@@ -347,6 +365,391 @@ function BagFrame:Update()
end
end
+-- Helper to get or create section header
+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
+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]
+end
+
+-- Display items by category
+function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
+ local buttonSize = addon.Modules.DB:GetSetting("iconSize") or addon.Constants.BUTTON_SIZE
+ local spacing = addon.Modules.DB:GetSetting("iconSpacing") or addon.Constants.BUTTON_SPACING
+ local perRow = addon.Modules.DB:GetSetting("bagColumns") or 10
+ local itemContainer = getglobal("Guda_BagFrame_ItemContainer")
+
+ -- Group items by category
+ local categories = {}
+ local categoryList = {
+ "Weapon", "Armor", "Consumable", "Food", "Drink", "Trade Goods", "Reagent", "Recipe", "Quiver", "Container", "Soul Bag", "Keyring", "Miscellaneous", "Quest", "Class Items"
+ }
+ for _, cat in ipairs(categoryList) do categories[cat] = {} end
+
+ local specialItems = {
+ Hearthstone = {},
+ Mount = {},
+ Tools = {}
+ }
+
+ for _, bagID in ipairs(addon.Constants.BAGS) do
+ if not hiddenBags[bagID] then
+ local bag = bagData[bagID]
+ if bag and bag.slots then
+ for slotID, itemData in pairs(bag.slots) do
+ if itemData then
+ local cat = "Miscellaneous"
+ local itemName = itemData.name or ""
+
+ -- Priority 1: Special items (Hearthstone, Mounts, Tools)
+ if string.find(itemName, "Hearthstone") then
+ table.insert(specialItems.Hearthstone, {bagID = bagID, slotID = slotID, itemData = itemData})
+ 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})
+ elseif string.find(itemName, "Runed .* Rod") or
+ string.find(itemName, "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})
+
+ -- Priority 2: Class Items (Soul Shards, Arrows, Bullets)
+ elseif 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})
+
+ -- Priority 3: Quest Items
+ elseif (not isOtherChar and addon.Modules.Utils:IsQuestItemTooltip(bagID, slotID)) or itemData.class == "Quest" then
+ table.insert(categories["Quest"], {bagID = bagID, slotID = slotID, itemData = itemData})
+
+ -- Priority 4: Food and Drink
+ elseif 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})
+
+ -- Priority 4: Equipment (Weapon and Armor)
+ elseif itemData.equipSlot and itemData.equipSlot ~= "" then
+ if itemData.class == "Weapon" or itemData.class == "Armor" then
+ cat = itemData.class
+ else
+ cat = "Armor"
+ end
+ table.insert(categories[cat], {bagID = bagID, slotID = slotID, itemData = itemData})
+
+ -- Priority 5: Other Categories
+ else
+ cat = itemData.class or "Miscellaneous"
+ if not categories[cat] then cat = "Miscellaneous" end
+ table.insert(categories[cat], {bagID = bagID, slotID = slotID, itemData = itemData})
+ end
+ end
+ end
+ end
+ end
+ end
+
+ -- Calculate total empty slots and find first available one for drop target
+ local totalFreeSlots = 0
+ local firstFreeBag, firstFreeSlot
+ for _, bagID in ipairs(addon.Constants.BAGS) do
+ if not hiddenBags[bagID] then
+ local bag = bagData[bagID]
+ if bag then
+ totalFreeSlots = totalFreeSlots + (bag.freeSlots or 0)
+ if not firstFreeBag and (bag.freeSlots or 0) > 0 then
+ for s = 1, (bag.numSlots or 0) do
+ if not bag.slots or not bag.slots[s] then
+ firstFreeBag = bagID
+ firstFreeSlot = s
+ break
+ end
+ end
+ end
+ end
+ end
+ end
+
+ -- Handle Keyring if visible
+ if showKeyring and not hiddenBags[-2] then
+ local bag = bagData[-2]
+ if bag and bag.slots then
+ for slotID, itemData in pairs(bag.slots) do
+ if itemData then
+ table.insert(categories["Keyring"], {bagID = -2, slotID = slotID, itemData = itemData})
+ end
+ end
+ end
+ end
+
+ -- Layout
+ local startX, startY = 10, -10
+ local currentX, currentY = 0, 0
+ local rowMaxHeight = 0
+ local headerIdx = 1
+ local totalWidth = perRow * (buttonSize + spacing)
+
+ for _, catName in ipairs(categoryList) do
+ local items = categories[catName]
+ local numItems = items and table.getn(items) or 0
+ if numItems > 0 then
+ -- Sort items in category: Subclass > Quality > Name
+ table.sort(items, function(a, b)
+ 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)
+
+ local blockCols = numItems
+ if blockCols > perRow then blockCols = perRow end
+ local blockRows = math.ceil(numItems / perRow)
+ local blockWidth = blockCols * (buttonSize + spacing)
+ local blockHeight = 20 + (blockRows * (buttonSize + spacing)) + 5 -- 20 header, 5 padding
+
+ -- Check if it fits in current row
+ if currentX > 0 and currentX + blockWidth + 20 > totalWidth + 5 then
+ currentX = 0
+ currentY = currentY + rowMaxHeight
+ rowMaxHeight = 0
+ end
+
+ -- Add Header
+ local header = self:GetSectionHeader(headerIdx)
+ headerIdx = headerIdx + 1
+ header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", startX + currentX, startY - currentY)
+ header:SetWidth(blockWidth)
+
+ local displayName = catName
+ header.fullName = catName
+ header.isShortened = false
+ if string.len(displayName) > 10 and numItems < 2 then
+ displayName = string.sub(displayName, 1, 7) .. "..."
+ header.isShortened = true
+ end
+ header.text:SetText(displayName)
+ header:Show()
+
+ local itemY = currentY + 20
+ local col = 0
+ local row = 0
+ for _, item in ipairs(items) do
+ local bagID = item.bagID
+ local slot = item.slotID
+ local itemData = item.itemData
+
+ local bagParent = self:GetBagParent(bagID)
+ local button = Guda_GetItemButton(bagParent)
+
+ button:SetParent(bagParent)
+ button:SetWidth(buttonSize)
+ button:SetHeight(buttonSize)
+ button:ClearAllPoints()
+ button:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", startX + currentX + (col * (buttonSize + spacing)), startY - (itemY + (row * (buttonSize + spacing))))
+ button:Show()
+
+ local matchesFilter = self:PassesSearchFilter(itemData)
+ Guda_ItemButton_SetItem(button, bagID, slot, itemData, false, isOtherChar and charName or nil, matchesFilter, isOtherChar)
+ button.inUse = true
+
+ col = col + 1
+ if col >= blockCols then
+ col = 0
+ row = row + 1
+ end
+ end
+
+ if blockHeight > rowMaxHeight then rowMaxHeight = blockHeight end
+ currentX = currentX + blockWidth + 20 -- 20px gap between categories
+ end
+ end
+
+ -- Update Y for bottom sections
+ local y = currentY + rowMaxHeight
+
+ -- Special sections at bottom (Hearthstone, Mount, Tools, Empty)
+ local bottomSections = {
+ { name = "Home", items = specialItems.Hearthstone },
+ { name = "Mounts", items = specialItems.Mount },
+ { name = "Tools", items = specialItems.Tools },
+ { name = "Empty", items = {} }
+ }
+
+ local x = startX
+ y = startY - y -- convert to coordinate relative to TOPLEFT
+
+ -- Add spacing before special section
+ local hasAnyBottom = false
+ for _, sec in ipairs(bottomSections) do
+ if table.getn(sec.items) > 0 then
+ hasAnyBottom = true
+ break
+ end
+ end
+
+ if hasAnyBottom then
+ y = y - 10
+ local currentBottomX = 0
+ local sectionMaxHeight = 0
+
+ for _, sec in ipairs(bottomSections) do
+ local items = sec.items
+ local numItems = table.getn(items)
+ if sec.name == "Empty" then
+ numItems = (totalFreeSlots > 0) and 1 or 0
+ end
+ if numItems > 0 then
+ -- Sort Tools (Hearthstone/Mounts don't usually need it but good for consistency)
+ if sec.name == "Tools" then
+ table.sort(items, function(a, b)
+ 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
+
+ local blockCols = numItems
+ if blockCols > perRow then blockCols = perRow end
+ local blockRows = math.ceil(numItems / perRow)
+ local blockWidth = blockCols * (buttonSize + spacing)
+ local blockHeight = 20 + (blockRows * (buttonSize + spacing))
+
+ -- Check if it fits in current row (Inline block for bottom sections too)
+ if currentBottomX > 0 and currentBottomX + blockWidth + 20 > totalWidth + 5 then
+ currentBottomX = 0
+ y = y - sectionMaxHeight - 5
+ sectionMaxHeight = 0
+ end
+
+ -- Add Header
+ local header = self:GetSectionHeader(headerIdx)
+ headerIdx = headerIdx + 1
+ header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX, y)
+ header:SetWidth(blockWidth)
+ header.text:SetText(sec.name)
+ header:Show()
+
+ local itemY = y - 20
+ local sCol = 0
+ local sRow = 0
+
+ if sec.name == "Empty" then
+ -- Display one empty slot button with count
+ local bagID = firstFreeBag or 0
+ local slotID = firstFreeSlot or 1
+ local bagParent = self:GetBagParent(bagID)
+ local button = Guda_GetItemButton(bagParent)
+ button:SetParent(bagParent)
+ button:SetWidth(buttonSize)
+ button:SetHeight(buttonSize)
+ button:ClearAllPoints()
+ button:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX, itemY)
+ button:Show()
+
+ local emptyItemData = {
+ texture = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Bag",
+ count = totalFreeSlots,
+ name = "Empty Slots"
+ }
+ -- Use fake data but real IDs for drop handling
+ Guda_ItemButton_SetItem(button, bagID, slotID, emptyItemData, false, isOtherChar and charName or nil, true, true)
+ -- Ensure it's not actually read-only for drop behavior (it should still receive clicks/drops)
+ button.isReadOnly = false
+ button.inUse = true
+ else
+ for _, item in ipairs(items) do
+ local bagParent = self:GetBagParent(item.bagID)
+ local button = Guda_GetItemButton(bagParent)
+ button:SetParent(bagParent)
+ button:SetWidth(buttonSize)
+ button:SetHeight(buttonSize)
+ button:ClearAllPoints()
+ button:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX + (sCol * (buttonSize + spacing)), itemY - (sRow * (buttonSize + spacing)))
+ button:Show()
+ Guda_ItemButton_SetItem(button, item.bagID, item.slotID, item.itemData, false, isOtherChar and charName or nil, self:PassesSearchFilter(item.itemData), isOtherChar)
+ button.inUse = true
+
+ sCol = sCol + 1
+ if sCol >= blockCols then
+ sCol = 0
+ sRow = sRow + 1
+ end
+ end
+ end
+
+ if blockHeight > sectionMaxHeight then sectionMaxHeight = blockHeight end
+ currentBottomX = currentBottomX + blockWidth + 20 -- Match the 20px gap used in top categories
+
+ -- If we wrapped exactly at the end of a block
+ if currentBottomX >= totalWidth then
+ currentBottomX = 0
+ y = y - sectionMaxHeight - 5
+ sectionMaxHeight = 0
+ end
+ end
+ end
+
+ if currentBottomX > 0 then
+ y = y - sectionMaxHeight
+ end
+ end
+
+ -- Update container height
+ local finalHeight = math.abs(y) + 20
+ itemContainer:SetHeight(finalHeight)
+ self:ResizeFrame(nil, nil, perRow, finalHeight)
+end
+
-- Display items
function BagFrame:DisplayItems(bagData, isOtherChar, charName)
local x, y = 10, -10
@@ -465,17 +868,7 @@ function BagFrame:DisplayItems(bagData, isOtherChar, charName)
if numSlots and numSlots > 0 then
-- Iterate through ALL slots (1 to numSlots) to show empty slots too
-- Ensure a per-bag parent frame exists and carries the bag ID (Blizzard expects parent:GetID() == bagID)
- local bagParent
- do
- 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
- bagParent = bagParents[bagID]
- end
+ local bagParent = self:GetBagParent(bagID)
for slot = 1, numSlots do
local itemData = bag and bag.slots and bag.slots[slot] or nil
@@ -519,12 +912,12 @@ function BagFrame:DisplayItems(bagData, isOtherChar, charName)
end
-- Resize frame based on number of rows and columns
-function BagFrame:ResizeFrame(currentRow, currentCol, 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 + 1
+ local totalRows = (currentRow or 0) + 1
-- Ensure at least 1 row
if totalRows < 1 then
@@ -533,7 +926,7 @@ function BagFrame:ResizeFrame(currentRow, currentCol, columns)
-- Calculate required dimensions based on columns
local containerWidth = (columns * (buttonSize + spacing)) + 20
- local containerHeight = (totalRows * (buttonSize + spacing)) + 20
+ local containerHeight = overrideHeight or ((totalRows * (buttonSize + spacing)) + 20)
local frameWidth = containerWidth + 20
-- Check if search bar is visible
diff --git a/UI/BankFrame.lua b/UI/BankFrame.lua
index 4fe30f3..2a1d963 100644
--- a/UI/BankFrame.lua
+++ b/UI/BankFrame.lua
@@ -177,7 +177,25 @@ function BankFrame:Update()
end
end
- self:DisplayItems(bankData, isOtherChar, charName)
+ local viewType = addon.Modules.DB:GetSetting("bankViewType") or "single"
+
+ -- Reset all section headers before displaying items
+ local i = 1
+ while true do
+ local header = getglobal("Guda_BankFrame_SectionHeader" .. i)
+ if not header then break end
+ header.inUse = false
+ header:Hide()
+ i = i + 1
+ end
+
+ if viewType == "category" then
+ self:DisplayItemsByCategory(bankData, isOtherChar, charName)
+ if getglobal("Guda_BankFrame_SortButton") then getglobal("Guda_BankFrame_SortButton"):Hide() end
+ else
+ self:DisplayItems(bankData, isOtherChar, charName)
+ if getglobal("Guda_BankFrame_SortButton") then getglobal("Guda_BankFrame_SortButton"):Show() end
+ end
-- Update money
self:UpdateMoney()
@@ -199,6 +217,391 @@ function BankFrame:Update()
end
end
+-- Helper to get or create section header
+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
+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]
+end
+
+-- Display items by category
+function BankFrame:DisplayItemsByCategory(bankData, isOtherChar, charName)
+ local buttonSize = addon.Modules.DB:GetSetting("iconSize") or addon.Constants.BUTTON_SIZE
+ local spacing = addon.Modules.DB:GetSetting("iconSpacing") or addon.Constants.BUTTON_SPACING
+ local perRow = addon.Modules.DB:GetSetting("bankColumns") or 10
+ local itemContainer = getglobal("Guda_BankFrame_ItemContainer")
+
+ -- Group items by category
+ local categories = {}
+ local categoryList = {
+ "Weapon", "Armor", "Consumable", "Food", "Drink", "Trade Goods", "Reagent", "Recipe", "Quiver", "Container", "Soul Bag", "Miscellaneous", "Quest", "Class Items"
+ }
+ for _, cat in ipairs(categoryList) do categories[cat] = {} end
+
+ local specialItems = {
+ Hearthstone = {},
+ Mount = {},
+ Tools = {},
+ Empty = {}
+ }
+
+ -- Helper to assign category
+ local function CategorizeItem(itemData, bagID, slotID)
+ local itemName = itemData.name or ""
+ local cat = "Miscellaneous"
+
+ -- 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
+ string.find(itemName, "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
+ if (not isOtherChar and addon.Modules.Utils:IsQuestItemTooltip(bagID, slotID)) or itemData.class == "Quest" then
+ cat = "Quest"
+
+ -- Priority 4: Food and Drink
+ elseif 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
+
+ -- Priority 4: Equipment (Weapon and Armor)
+ elseif itemData.equipSlot and itemData.equipSlot ~= "" then
+ if itemData.class == "Weapon" or itemData.class == "Armor" then
+ cat = itemData.class
+ else
+ cat = "Armor"
+ end
+
+ -- Priority 5: Other Categories
+ else
+ cat = itemData.class or "Miscellaneous"
+ end
+
+ if not categories[cat] then cat = "Miscellaneous" end
+ table.insert(categories[cat], {bagID = bagID, slotID = slotID, itemData = itemData})
+ end
+
+ -- Bank slots
+ for _, bagID in ipairs(addon.Constants.BANK_BAGS) do
+ if not hiddenBankBags[bagID] then
+ local bag = bankData[bagID]
+ if bag and bag.slots then
+ for slotID, itemData in pairs(bag.slots) do
+ if itemData then
+ CategorizeItem(itemData, bagID, slotID)
+ end
+ end
+ end
+ end
+ end
+
+ -- Calculate total empty slots and find first available one for drop target
+ local totalFreeSlots = 0
+ local firstFreeBag, firstFreeSlot
+ for _, bagID in ipairs(addon.Constants.BANK_BAGS) do
+ if not hiddenBankBags[bagID] then
+ local bag = bankData[bagID]
+ if bag then
+ totalFreeSlots = totalFreeSlots + (bag.freeSlots or 0)
+ if not firstFreeBag and (bag.freeSlots or 0) > 0 then
+ for s = 1, (bag.numSlots or 0) do
+ if not bag.slots or not bag.slots[s] then
+ firstFreeBag = bagID
+ firstFreeSlot = s
+ break
+ end
+ end
+ end
+ end
+ end
+ end
+
+ -- Layout
+ local startX, startY = 5, -10
+ local currentX, currentY = 0, 0
+ local rowMaxHeight = 0
+ local headerIdx = 1
+ local totalWidth = perRow * (buttonSize + spacing)
+
+ for _, catName in ipairs(categoryList) do
+ local items = categories[catName]
+ local numItems = items and table.getn(items) or 0
+ if numItems > 0 then
+ -- Sort items in category: Subclass > Quality > Name
+ table.sort(items, function(a, b)
+ 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)
+
+ local blockCols = numItems
+ if blockCols > perRow then blockCols = perRow end
+ local blockRows = math.ceil(numItems / perRow)
+ local blockWidth = blockCols * (buttonSize + spacing)
+ local blockHeight = 20 + (blockRows * (buttonSize + spacing)) + 5
+
+ -- Check if it fits in current row
+ if currentX > 0 and currentX + blockWidth + 20 > totalWidth + 5 then
+ currentX = 0
+ currentY = currentY + rowMaxHeight
+ rowMaxHeight = 0
+ end
+
+ -- Add Header
+ local header = self:GetSectionHeader(headerIdx)
+ headerIdx = headerIdx + 1
+ header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", startX + currentX, startY - currentY)
+ header:SetWidth(blockWidth)
+
+ local displayName = catName
+ header.fullName = catName
+ header.isShortened = false
+ if string.len(displayName) > 10 and numItems < 2 then
+ displayName = string.sub(displayName, 1, 7) .. "..."
+ header.isShortened = true
+ end
+ header.text:SetText(displayName)
+ header:Show()
+
+ local itemY = currentY + 20
+ local col = 0
+ local row = 0
+ for _, item in ipairs(items) do
+ local bagID = item.bagID
+ local slot = item.slotID
+ local itemData = item.itemData
+
+ local bagParent = self:GetBagParent(bagID)
+ local button = Guda_GetItemButton(bagParent)
+
+ button:SetParent(bagParent)
+ button:SetWidth(buttonSize)
+ button:SetHeight(buttonSize)
+ button:ClearAllPoints()
+ button:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", startX + currentX + (col * (buttonSize + spacing)), startY - (itemY + (row * (buttonSize + spacing))))
+ button:Show()
+
+ local matchesFilter = self:PassesSearchFilter(itemData)
+ Guda_ItemButton_SetItem(button, bagID, slot, itemData, true, isOtherChar and charName or nil, matchesFilter, isOtherChar or isReadOnlyMode)
+ button.inUse = true
+
+ col = col + 1
+ if col >= blockCols then
+ col = 0
+ row = row + 1
+ end
+ end
+
+ if blockHeight > rowMaxHeight then rowMaxHeight = blockHeight end
+ currentX = currentX + blockWidth + 20 -- 20px gap between categories
+ end
+ end
+
+ -- Update Y for bottom sections
+ local y = currentY + rowMaxHeight
+
+ -- Special sections at bottom (Hearthstone, Mount, Tools, Empty)
+ local bottomSections = {
+ { name = "Home", items = specialItems.Hearthstone },
+ { name = "Mounts", items = specialItems.Mount },
+ { name = "Tools", items = specialItems.Tools },
+ { name = "Empty", items = {} }
+ }
+
+ local x = startX
+ y = startY - y -- convert to coordinate relative to TOPLEFT
+
+ -- Add spacing before special section
+ local hasAnyBottom = false
+ for _, sec in ipairs(bottomSections) do
+ if table.getn(sec.items) > 0 then
+ hasAnyBottom = true
+ break
+ end
+ end
+
+ if hasAnyBottom then
+ y = y - 10
+ local currentBottomX = 0
+ local sectionMaxHeight = 0
+
+ for _, sec in ipairs(bottomSections) do
+ local items = sec.items
+ local numItems = table.getn(items)
+ if sec.name == "Empty" then
+ numItems = (totalFreeSlots > 0) and 1 or 0
+ end
+ if numItems > 0 then
+ -- Sort Tools (Hearthstone/Mounts don't usually need it but good for consistency)
+ if sec.name == "Tools" then
+ table.sort(items, function(a, b)
+ 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
+
+ local blockCols = numItems
+ if blockCols > perRow then blockCols = perRow end
+ local blockRows = math.ceil(numItems / perRow)
+ local blockWidth = blockCols * (buttonSize + spacing)
+ local blockHeight = 20 + (blockRows * (buttonSize + spacing))
+
+ -- Check if it fits in current row (Inline block for bottom sections too)
+ if currentBottomX > 0 and currentBottomX + blockWidth + 20 > totalWidth + 5 then
+ currentBottomX = 0
+ y = y - sectionMaxHeight - 5
+ sectionMaxHeight = 0
+ end
+
+ -- Add Header
+ local header = self:GetSectionHeader(headerIdx)
+ headerIdx = headerIdx + 1
+ header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX, y)
+ header:SetWidth(blockWidth)
+ header.text:SetText(sec.name)
+ header:Show()
+
+ local itemY = y - 20
+ local sCol = 0
+ local sRow = 0
+
+ if sec.name == "Empty" then
+ -- Display one empty slot button with count
+ local bagID = firstFreeBag or -1
+ local slotID = firstFreeSlot or 1
+ local bagParent = self:GetBagParent(bagID)
+ local button = Guda_GetItemButton(bagParent)
+ button:SetParent(bagParent)
+ button:SetWidth(buttonSize)
+ button:SetHeight(buttonSize)
+ button:ClearAllPoints()
+ button:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX, itemY)
+ button:Show()
+
+ local emptyItemData = {
+ texture = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Bag",
+ count = totalFreeSlots,
+ name = "Empty Slots"
+ }
+ -- Use fake data but real IDs for drop handling
+ Guda_ItemButton_SetItem(button, bagID, slotID, emptyItemData, true, isOtherChar and charName or nil, true, true)
+ -- Ensure it's not actually read-only for drop behavior
+ button.isReadOnly = false
+ button.inUse = true
+ else
+ for _, item in ipairs(items) do
+ local bagParent = self:GetBagParent(item.bagID)
+ local button = Guda_GetItemButton(bagParent)
+ button:SetParent(bagParent)
+ button:SetWidth(buttonSize)
+ button:SetHeight(buttonSize)
+ button:ClearAllPoints()
+ button:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX + (sCol * (buttonSize + spacing)), itemY - (sRow * (buttonSize + spacing)))
+ button:Show()
+ Guda_ItemButton_SetItem(button, item.bagID, item.slotID, item.itemData, true, isOtherChar and charName or nil, self:PassesSearchFilter(item.itemData), isOtherChar or isReadOnlyMode)
+ button.inUse = true
+
+ sCol = sCol + 1
+ if sCol >= blockCols then
+ sCol = 0
+ sRow = sRow + 1
+ end
+ end
+ end
+
+ if blockHeight > sectionMaxHeight then sectionMaxHeight = blockHeight end
+ currentBottomX = currentBottomX + blockWidth + 20 -- Match the 20px gap used in top categories
+
+ -- If we wrapped exactly at the end of a block
+ if currentBottomX >= totalWidth then
+ currentBottomX = 0
+ y = y - sectionMaxHeight - 5
+ sectionMaxHeight = 0
+ end
+ end
+ end
+
+ if currentBottomX > 0 then
+ y = y - sectionMaxHeight
+ end
+ end
+
+ -- Update container height
+ local finalHeight = math.abs(y) + 20
+ itemContainer:SetHeight(finalHeight)
+ self:ResizeFrame(nil, nil, perRow, finalHeight)
+end
+
-- Display items
function BankFrame:DisplayItems(bankData, isOtherChar, charName)
local x, y = 5, -10
@@ -304,15 +707,7 @@ function BankFrame:DisplayItems(bankData, isOtherChar, charName)
local matchesFilter = self:PassesSearchFilter(itemData)
-- Ensure a per-bag parent frame exists and carries the bag ID
- local bankBagParent
- if not bankBagParents[bagID] then
- bankBagParents[bagID] = CreateFrame("Frame", "Guda_BankFrame_BankBagParent"..bagID, itemContainer)
- bankBagParents[bagID]:SetAllPoints(itemContainer)
- if bankBagParents[bagID].SetID then
- bankBagParents[bagID]:SetID(bagID)
- end
- end
- bankBagParent = bankBagParents[bagID]
+ local bankBagParent = self:GetBagParent(bagID)
local button = Guda_GetItemButton(bankBagParent)
if button.isBagSlot then break end
@@ -341,12 +736,12 @@ function BankFrame:DisplayItems(bankData, isOtherChar, charName)
end
-- Resize bank frame based on number of rows and columns
-function BankFrame:ResizeFrame(currentRow, currentCol, columns)
+function BankFrame: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 + 1
+ local totalRows = (currentRow or 0) + 1
if totalRows < 1 then
totalRows = 1
end
@@ -358,7 +753,7 @@ function BankFrame:ResizeFrame(currentRow, currentCol, columns)
-- Calculate required dimensions
local containerWidth = (columns * (buttonSize + spacing)) + 10
- local containerHeight = (totalRows * (buttonSize + spacing)) + 20
+ local containerHeight = overrideHeight or ((totalRows * (buttonSize + spacing)) + 20)
local frameWidth = containerWidth + 30
-- Check if search bar is visible
@@ -854,14 +1249,16 @@ function BankFrame:EnsureBagButtonsInitialized()
btn = CreateFrame("Button", name, toolbar, "ItemButtonTemplate")
-- Anchor sequenced to the left; position similar to XML
if suffix == "BankBagMain" then
- btn:SetSize(24, 24)
+ btn:SetWidth(24)
+ btn:SetHeight(24)
btn:SetPoint("LEFT", toolbar, "LEFT", 13, 0)
else
-- Determine previous button
local prev
if bagID == 5 then prev = getglobal("Guda_BankFrame_Toolbar_BankBagMain")
else prev = getglobal("Guda_BankFrame_Toolbar_BankBag"..tostring(bagID-1)) end
- btn:SetSize(24, 24)
+ btn:SetWidth(24)
+ btn:SetHeight(24)
if prev then
btn:SetPoint("LEFT", prev, "RIGHT", 2, 0)
else
diff --git a/UI/ItemButton.lua b/UI/ItemButton.lua
index 5d83090..dd659c8 100644
--- a/UI/ItemButton.lua
+++ b/UI/ItemButton.lua
@@ -10,11 +10,19 @@ 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)
+local function IsQuestItem(bagID, slotID, isBank)
if not bagID or not slotID then return false end
scanTooltip:ClearLines()
- scanTooltip:SetBagItem(bagID, slotID)
+ if isBank and bagID == -1 then
+ if scanTooltip.SetInventoryItem then
+ scanTooltip:SetInventoryItem("player", 39 + slotID)
+ else
+ scanTooltip:SetBagItem(bagID, slotID)
+ end
+ else
+ scanTooltip:SetBagItem(bagID, slotID)
+ end
local isQuestItem = false
local isQuestStarter = false
@@ -338,7 +346,7 @@ function Guda_ItemButton_OnLoad(self)
if link and addon and addon.Modules and addon.Modules.Utils then
local itemID = addon.Modules.Utils:ExtractItemID(link)
if itemID then
- local isQuest = IsQuestItem(this.bagID, this.slotID)
+ local isQuest = IsQuestItem(this.bagID, this.slotID, this.isBank)
if isQuest and addon.Modules.QuestItemBar and addon.Modules.QuestItemBar.PinItem then
addon.Modules.QuestItemBar:PinItem(itemID)
return
diff --git a/UI/SettingsPopup.lua b/UI/SettingsPopup.lua
index f49f8ba..076d209 100644
--- a/UI/SettingsPopup.lua
+++ b/UI/SettingsPopup.lua
@@ -107,6 +107,8 @@ function Guda_SettingsPopup_OnShow(self)
hoverBagline = false
end
local bgTransparency = Guda.Modules.DB:GetSetting("bgTransparency") or 0.15
+ local bagViewType = Guda.Modules.DB:GetSetting("bagViewType") or "single"
+ local bankViewType = Guda.Modules.DB:GetSetting("bankViewType") or "single"
-- Update sliders and checkboxes
local bagSlider = getglobal("Guda_SettingsPopup_BagColumnsSlider")
@@ -124,6 +126,8 @@ function Guda_SettingsPopup_OnShow(self)
local hoverBaglineCheckbox = getglobal("Guda_SettingsPopup_HoverBaglineCheckbox")
local hideFooterCheckbox = getglobal("Guda_SettingsPopup_HideFooterCheckbox")
local showTooltipCountsCheckbox = getglobal("Guda_SettingsPopup_ShowTooltipCountsCheckbox")
+ local bagViewButton = getglobal("Guda_SettingsPopup_BagViewTypeButton")
+ local bankViewButton = getglobal("Guda_SettingsPopup_BankViewTypeButton")
local showTooltipCounts = Guda.Modules.DB:GetSetting("showTooltipCounts")
if showTooltipCounts == nil then
@@ -186,6 +190,22 @@ function Guda_SettingsPopup_OnShow(self)
showTooltipCountsCheckbox:SetChecked(showTooltipCounts and 1 or 0)
end
+ if bagViewButton then
+ if bagViewType == "single" then
+ bagViewButton:SetText("Bag View: Single")
+ else
+ bagViewButton:SetText("Bag View: Category")
+ end
+ end
+
+ if bankViewButton then
+ if bankViewType == "single" then
+ bankViewButton:SetText("Bank View: Single")
+ else
+ bankViewButton:SetText("Bank View: Category")
+ end
+ end
+
-- Update display (might be too tall for current frame size)
local frame = getglobal("Guda_SettingsPopup")
@@ -919,6 +939,40 @@ function Guda_SettingsPopup_ShowTooltipCountsCheckbox_OnClick(self)
end
end
+-- Bag View Type Button OnClick
+function Guda_SettingsPopup_BagViewTypeButton_OnClick()
+ local current = Guda.Modules.DB:GetSetting("bagViewType") or "single"
+ local newValue = (current == "single") and "category" or "single"
+ Guda.Modules.DB:SetSetting("bagViewType", newValue)
+
+ local btn = getglobal("Guda_SettingsPopup_BagViewTypeButton")
+ if btn then
+ btn:SetText(newValue == "single" and "Bag View: Single" or "Bag View: Category")
+ end
+
+ -- Refresh bag frame if it's open
+ if Guda_BagFrame:IsShown() then
+ Guda.Modules.BagFrame:Update()
+ end
+end
+
+-- Bank View Type Button OnClick
+function Guda_SettingsPopup_BankViewTypeButton_OnClick()
+ local current = Guda.Modules.DB:GetSetting("bankViewType") or "single"
+ local newValue = (current == "single") and "category" or "single"
+ Guda.Modules.DB:SetSetting("bankViewType", newValue)
+
+ local btn = getglobal("Guda_SettingsPopup_BankViewTypeButton")
+ if btn then
+ btn:SetText(newValue == "single" and "Bank View: Single" or "Bank View: Category")
+ end
+
+ -- Refresh bank frame if it's open
+ if Guda_BankFrame:IsShown() then
+ Guda.Modules.BankFrame:Update()
+ end
+end
+
-- Initialize
function SettingsPopup:Initialize()
Guda:Debug("Settings popup initialized")
diff --git a/UI/SettingsPopup.xml b/UI/SettingsPopup.xml
index d75bd29..ae35e62 100644
--- a/UI/SettingsPopup.xml
+++ b/UI/SettingsPopup.xml
@@ -394,6 +394,31 @@
+
+
+
+
+