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 @@ + + + + +