feat: update categories and item slots

This commit is contained in:
Vati
2026-02-28 00:18:06 +04:00
parent 14bf159048
commit 184d461e3e
12 changed files with 1384 additions and 322 deletions
+566 -20
View File
@@ -52,13 +52,21 @@ end
-- texturePattern: Match icon texture path -- texturePattern: Match icon texture path
-- itemID: Specific item IDs (table of IDs) -- itemID: Specific item IDs (table of IDs)
-- Group constants
local GROUP_MAIN = "Main"
local GROUP_OTHER = "Other"
local GROUP_CLASS = "Class"
-- Default category definitions that replicate the existing hardcoded behavior -- Default category definitions that replicate the existing hardcoded behavior
local DEFAULT_CATEGORIES = { local DEFAULT_CATEGORIES = {
order = { order = {
"Home", "BoE", "Weapon", "Armor", "Consumable", "Food", "Drink", "BoE", "Weapon", "Armor", "Consumable", "Food", "Drink",
"Trade Goods", "Reagent", "Recipe", "Quiver", "Container", "Trade Goods", "Reagent", "Recipe", "Quiver", "Container",
"Soul Bag", "Miscellaneous", "Quest", "Junk", "Class Items", "Keyring" "Soul Bag", "Miscellaneous", "Quest", "Junk",
"Class Items", "Keyring",
"Home", "Tools", "Empty"
}, },
itemOverrides = {}, -- flat map: [itemID] = categoryId
definitions = { definitions = {
["BoE"] = { ["BoE"] = {
name = "BoE", name = "BoE",
@@ -70,6 +78,7 @@ local DEFAULT_CATEGORIES = {
priority = 75, priority = 75,
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
group = GROUP_MAIN,
}, },
["Weapon"] = { ["Weapon"] = {
name = "Weapon", name = "Weapon",
@@ -81,6 +90,7 @@ local DEFAULT_CATEGORIES = {
priority = 70, priority = 70,
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
group = GROUP_MAIN,
}, },
["Armor"] = { ["Armor"] = {
name = "Armor", name = "Armor",
@@ -92,6 +102,7 @@ local DEFAULT_CATEGORIES = {
priority = 70, priority = 70,
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
group = GROUP_MAIN,
}, },
["Consumable"] = { ["Consumable"] = {
name = "Consumable", name = "Consumable",
@@ -103,6 +114,7 @@ local DEFAULT_CATEGORIES = {
priority = 50, priority = 50,
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
group = GROUP_MAIN,
}, },
["Food"] = { ["Food"] = {
name = "Food", name = "Food",
@@ -115,6 +127,7 @@ local DEFAULT_CATEGORIES = {
priority = 55, priority = 55,
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
group = GROUP_MAIN,
}, },
["Drink"] = { ["Drink"] = {
name = "Drink", name = "Drink",
@@ -127,6 +140,7 @@ local DEFAULT_CATEGORIES = {
priority = 55, priority = 55,
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
group = GROUP_MAIN,
}, },
["Trade Goods"] = { ["Trade Goods"] = {
name = "Trade Goods", name = "Trade Goods",
@@ -138,6 +152,7 @@ local DEFAULT_CATEGORIES = {
priority = 40, priority = 40,
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
group = GROUP_MAIN,
}, },
["Reagent"] = { ["Reagent"] = {
name = "Reagent", name = "Reagent",
@@ -149,6 +164,7 @@ local DEFAULT_CATEGORIES = {
priority = 40, priority = 40,
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
group = GROUP_MAIN,
}, },
["Recipe"] = { ["Recipe"] = {
name = "Recipe", name = "Recipe",
@@ -160,6 +176,7 @@ local DEFAULT_CATEGORIES = {
priority = 40, priority = 40,
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
group = GROUP_MAIN,
}, },
["Quiver"] = { ["Quiver"] = {
name = "Quiver", name = "Quiver",
@@ -171,6 +188,7 @@ local DEFAULT_CATEGORIES = {
priority = 40, priority = 40,
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
group = GROUP_MAIN,
}, },
["Container"] = { ["Container"] = {
name = "Container", name = "Container",
@@ -182,6 +200,7 @@ local DEFAULT_CATEGORIES = {
priority = 40, priority = 40,
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
group = GROUP_MAIN,
}, },
["Soul Bag"] = { ["Soul Bag"] = {
name = "Soul Bag", name = "Soul Bag",
@@ -193,6 +212,7 @@ local DEFAULT_CATEGORIES = {
priority = 45, priority = 45,
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
group = GROUP_MAIN,
}, },
["Miscellaneous"] = { ["Miscellaneous"] = {
name = "Miscellaneous", name = "Miscellaneous",
@@ -203,6 +223,7 @@ local DEFAULT_CATEGORIES = {
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
isFallback = true, isFallback = true,
group = GROUP_MAIN,
}, },
["Quest"] = { ["Quest"] = {
name = "Quest", name = "Quest",
@@ -214,6 +235,7 @@ local DEFAULT_CATEGORIES = {
priority = 80, priority = 80,
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
group = GROUP_MAIN,
}, },
["Junk"] = { ["Junk"] = {
name = "Junk", name = "Junk",
@@ -225,17 +247,20 @@ local DEFAULT_CATEGORIES = {
priority = 85, priority = 85,
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
group = GROUP_MAIN,
}, },
["Class Items"] = { ["Class Items"] = {
name = "Class Items", name = "Class Items",
icon = "Interface\\Icons\\INV_Misc_Ammo_Arrow_01", icon = "Interface\\Icons\\INV_Misc_Ammo_Arrow_01",
rules = { rules = {
{ type = "itemType", value = "Projectile" } { type = "itemType", value = "Projectile" },
{ type = "isSoulShard", value = true }
}, },
matchMode = "any", matchMode = "any",
priority = 90, priority = 90,
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
group = GROUP_CLASS,
}, },
["Keyring"] = { ["Keyring"] = {
name = "Keyring", name = "Keyring",
@@ -247,20 +272,56 @@ local DEFAULT_CATEGORIES = {
priority = 40, priority = 40,
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
group = GROUP_CLASS,
}, },
["Home"] = { ["Home"] = {
name = "Home", name = "Home",
icon = "Interface\\Icons\\INV_Misc_Rune_01", icon = "Interface\\Icons\\INV_Misc_Rune_01",
rules = {
{ type = "itemID", value = {6948} }
},
matchMode = "all",
priority = 100,
enabled = true,
isBuiltIn = true,
group = GROUP_OTHER,
},
["Tools"] = {
name = "Tools",
icon = "Interface\\Icons\\Trade_BlackSmithing",
rules = {
{ type = "isProfessionTool", value = true }
},
matchMode = "all",
priority = 60,
enabled = true,
isBuiltIn = true,
group = GROUP_OTHER,
},
["Empty"] = {
name = "Empty",
icon = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Bag",
rules = {}, rules = {},
matchMode = "all", matchMode = "all",
priority = 0, priority = -10,
enabled = true, enabled = true,
isBuiltIn = true, isBuiltIn = true,
hideControls = true, hideControls = true,
isEmptyCategory = true,
group = GROUP_OTHER,
}, },
} }
} }
-- Group definitions for display order and built-in group mapping
local GROUP_ORDER = { GROUP_MAIN, GROUP_CLASS, GROUP_OTHER }
-- Map of built-in category IDs to their default groups (for migration)
local BUILTIN_GROUP_MAP = {}
for id, def in pairs(DEFAULT_CATEGORIES.definitions) do
BUILTIN_GROUP_MAP[id] = def.group
end
-- Deep copy a table -- Deep copy a table
local function deepCopy(orig) local function deepCopy(orig)
local copy local copy
@@ -404,6 +465,130 @@ function CategoryManager:MigrateCategories()
addon:Debug("CategoryManager: Migrated Junk category to use isJunk rule") addon:Debug("CategoryManager: Migrated Junk category to use isJunk rule")
end end
end end
-- Migrate: Add group to all categories that lack it
for id, def in pairs(cats.definitions) do
if not def.group then
-- Use built-in mapping if available, otherwise default to Main
def.group = BUILTIN_GROUP_MAP[id] or GROUP_MAIN
addon:Debug("CategoryManager: Added group '%s' to category: %s", def.group, id)
end
end
-- Migrate: Convert per-category itemOverrides arrays to flat map at categories level
if not cats.itemOverrides then
cats.itemOverrides = {}
end
local migratedOverrides = false
for catId, def in pairs(cats.definitions) do
if def.itemOverrides and type(def.itemOverrides) == "table" then
-- Check if it's an array (old format) by looking for numeric keys
local isArray = false
for k, v in pairs(def.itemOverrides) do
if type(k) == "number" then
isArray = true
break
end
end
if isArray then
for _, itemID in ipairs(def.itemOverrides) do
cats.itemOverrides[itemID] = catId
migratedOverrides = true
end
def.itemOverrides = nil
end
end
end
if migratedOverrides then
addon:Debug("CategoryManager: Migrated per-category itemOverrides to flat map")
end
-- Migrate Home category: add rules and remove hideControls
local homeCat = cats.definitions["Home"]
if homeCat and homeCat.isBuiltIn then
-- Remove hideControls
if homeCat.hideControls then
homeCat.hideControls = nil
addon:Debug("CategoryManager: Removed hideControls from Home")
end
-- Add rules if empty
if not homeCat.rules or table.getn(homeCat.rules) == 0 then
homeCat.rules = { { type = "itemID", value = {6948} } }
homeCat.priority = 100
addon:Debug("CategoryManager: Added itemID rule to Home category")
end
-- Ensure group is Other
if homeCat.group ~= GROUP_OTHER then
homeCat.group = GROUP_OTHER
end
end
-- Migrate Class Items: add isSoulShard rule if missing
local classItemsCat = cats.definitions["Class Items"]
if classItemsCat and classItemsCat.isBuiltIn then
local hasSoulShard = false
if classItemsCat.rules then
for _, rule in ipairs(classItemsCat.rules) do
if rule.type == "isSoulShard" then
hasSoulShard = true
break
end
end
end
if not hasSoulShard then
if not classItemsCat.rules then classItemsCat.rules = {} end
table.insert(classItemsCat.rules, { type = "isSoulShard", value = true })
classItemsCat.matchMode = "any"
addon:Debug("CategoryManager: Added isSoulShard rule to Class Items")
end
end
-- Ensure new categories in the order list are in correct positions
-- Check if order needs rebuilding to include new group-based ordering
local hasTools, hasEmpty = false, false
for _, id in ipairs(cats.order) do
if id == "Tools" then hasTools = true end
if id == "Empty" then hasEmpty = true end
end
-- If Tools or Empty were just added by the built-in migration above,
-- they're already at end of order. Move them to the Other group area.
if hasTools or hasEmpty then
-- Rebuild order to respect groups: Other, Main, Class
local grouped = {}
for _, g in ipairs(GROUP_ORDER) do
grouped[g] = {}
end
grouped["_ungrouped"] = {}
for _, id in ipairs(cats.order) do
local def = cats.definitions[id]
if def then
local g = def.group or GROUP_MAIN
if grouped[g] then
table.insert(grouped[g], id)
else
table.insert(grouped["_ungrouped"], id)
end
end
end
-- Rebuild order
local newOrder = {}
for _, g in ipairs(GROUP_ORDER) do
if grouped[g] then
for _, id in ipairs(grouped[g]) do
table.insert(newOrder, id)
end
end
end
for _, id in ipairs(grouped["_ungrouped"]) do
table.insert(newOrder, id)
end
cats.order = newOrder
addon:Debug("CategoryManager: Rebuilt category order for group ordering")
end
end end
-- Get all categories -- Get all categories
@@ -438,20 +623,50 @@ function CategoryManager:SaveCategories(categories)
end end
-- Add a new custom category -- Add a new custom category
-- If categoryId is nil, auto-generates a unique ID like "Custom_<time>_<random>"
function CategoryManager:AddCategory(categoryId, definition) function CategoryManager:AddCategory(categoryId, definition)
local cats = self:GetCategories() local cats = self:GetCategories()
-- Auto-generate ID if not provided
if not categoryId then
categoryId = "Custom_" .. time() .. "_" .. math.random(1000, 9999)
-- Ensure unique
while cats.definitions[categoryId] do
categoryId = "Custom_" .. time() .. "_" .. math.random(1000, 9999)
end
end
if cats.definitions[categoryId] then if cats.definitions[categoryId] then
addon:Debug("CategoryManager: Category already exists: " .. categoryId) addon:Debug("CategoryManager: Category already exists: " .. categoryId)
return false return false
end end
definition.isBuiltIn = false definition.isBuiltIn = definition.isBuiltIn or false
if not definition.group then
definition.group = GROUP_MAIN
end
cats.definitions[categoryId] = definition cats.definitions[categoryId] = definition
table.insert(cats.order, categoryId)
-- Insert at end of the category's group in the order list
local insertPos = nil
local group = definition.group
-- Find last category in the same group
for i = table.getn(cats.order), 1, -1 do
local existDef = cats.definitions[cats.order[i]]
if existDef and existDef.group == group then
insertPos = i + 1
break
end
end
if insertPos then
-- Lua 5.0 table.insert with position
table.insert(cats.order, insertPos, categoryId)
else
table.insert(cats.order, categoryId)
end
self:SaveCategories(cats) self:SaveCategories(cats)
return true return true, categoryId
end end
-- Update an existing category -- Update an existing category
@@ -499,32 +714,106 @@ function CategoryManager:DeleteCategory(categoryId)
return true return true
end end
-- Move category up in order -- Check if a category can move up within its group
function CategoryManager:MoveCategoryUp(categoryId) function CategoryManager:CanMoveUp(categoryId)
local cats = self:GetCategories() local cats = self:GetCategories()
local def = cats.definitions[categoryId]
if not def or def.hideControls then return false end
for i, id in ipairs(cats.order) do for i, id in ipairs(cats.order) do
if id == categoryId and i > 1 then if id == categoryId then
cats.order[i] = cats.order[i - 1] -- Find any previous non-hideControls category (can cross group boundaries)
cats.order[i - 1] = categoryId for j = i - 1, 1, -1 do
self:SaveCategories(cats) local prevDef = cats.definitions[cats.order[j]]
return true if prevDef and not prevDef.hideControls then
return true
end
end
return false -- very first movable category
end end
end end
return false return false
end end
-- Move category down in order -- Check if a category can move down (crosses group boundaries)
function CategoryManager:CanMoveDown(categoryId)
local cats = self:GetCategories()
local def = cats.definitions[categoryId]
if not def or def.hideControls then return false end
local count = table.getn(cats.order)
for i, id in ipairs(cats.order) do
if id == categoryId then
-- Find any next non-hideControls category (can cross group boundaries)
for j = i + 1, count do
local nextDef = cats.definitions[cats.order[j]]
if nextDef and not nextDef.hideControls then
return true
end
end
return false -- very last movable category
end
end
return false
end
-- Move category up in order (crosses group boundaries, changes group when crossing)
function CategoryManager:MoveCategoryUp(categoryId)
local cats = self:GetCategories()
local def = cats.definitions[categoryId]
if not def or def.hideControls then return false end
for i, id in ipairs(cats.order) do
if id == categoryId then
-- Find previous non-hideControls category
for j = i - 1, 1, -1 do
local prevDef = cats.definitions[cats.order[j]]
if prevDef and not prevDef.hideControls then
-- Swap positions
cats.order[i] = cats.order[j]
cats.order[j] = categoryId
-- If crossing into a different group, adopt that group
local prevGroup = prevDef.group or GROUP_MAIN
if (def.group or GROUP_MAIN) ~= prevGroup then
def.group = prevGroup
end
self:SaveCategories(cats)
return true
end
end
return false
end
end
return false
end
-- Move category down in order (crosses group boundaries, changes group when crossing)
function CategoryManager:MoveCategoryDown(categoryId) function CategoryManager:MoveCategoryDown(categoryId)
local cats = self:GetCategories() local cats = self:GetCategories()
local def = cats.definitions[categoryId]
if not def or def.hideControls then return false end
local count = table.getn(cats.order) local count = table.getn(cats.order)
for i, id in ipairs(cats.order) do for i, id in ipairs(cats.order) do
if id == categoryId and i < count then if id == categoryId then
cats.order[i] = cats.order[i + 1] -- Find next non-hideControls category
cats.order[i + 1] = categoryId for j = i + 1, count do
self:SaveCategories(cats) local nextDef = cats.definitions[cats.order[j]]
return true if nextDef and not nextDef.hideControls then
-- Swap positions
cats.order[i] = cats.order[j]
cats.order[j] = categoryId
-- If crossing into a different group, adopt that group
local nextGroup = nextDef.group or GROUP_MAIN
if (def.group or GROUP_MAIN) ~= nextGroup then
def.group = nextGroup
end
self:SaveCategories(cats)
return true
end
end
return false
end end
end end
return false return false
@@ -742,6 +1031,26 @@ function CategoryManager:CategorizeItem(itemData, bagID, slotID, isOtherChar)
end end
cacheMisses = cacheMisses + 1 cacheMisses = cacheMisses + 1
-- Check item overrides first (flat map: itemID -> categoryId)
if itemData and itemData.link then
local itemID = addon.Modules.Utils:ExtractItemID(itemData.link)
if itemID then
local cats = self:GetCategories()
if cats.itemOverrides then
local overrideCatId = cats.itemOverrides[itemID]
if overrideCatId then
local overrideDef = cats.definitions[overrideCatId]
if overrideDef and overrideDef.enabled then
if cacheKey then
categoryCache[cacheKey] = overrideCatId
end
return overrideCatId
end
end
end
end
end
local sortedCats = self:GetCategoriesByPriority() local sortedCats = self:GetCategoriesByPriority()
-- Debug: show white item categorization -- Debug: show white item categorization
@@ -791,6 +1100,243 @@ function CategoryManager:BuildCategoryList()
return list return list
end end
-------------------------------------------
-- Group Management
-------------------------------------------
-- Get ordered list of unique groups from the category order
function CategoryManager:GetGroups()
local cats = self:GetCategories()
local seen = {}
local groups = {}
for _, id in ipairs(cats.order) do
local def = cats.definitions[id]
if def then
local g = def.group or GROUP_MAIN
if not seen[g] then
seen[g] = true
table.insert(groups, g)
end
end
end
return groups
end
-- Get categories organized by group: { groupName => {catIds} }
-- Also returns ungrouped list for any categories without a group
function CategoryManager:GetCategoriesByGroup()
local cats = self:GetCategories()
local result = {}
local ungrouped = {}
for _, id in ipairs(cats.order) do
local def = cats.definitions[id]
if def then
local g = def.group
if g then
if not result[g] then result[g] = {} end
table.insert(result[g], id)
else
table.insert(ungrouped, id)
end
end
end
return result, ungrouped
end
-- Change a category's group
function CategoryManager:SetCategoryGroup(categoryId, groupName)
local cats = self:GetCategories()
local def = cats.definitions[categoryId]
if not def then return false end
local oldGroup = def.group or GROUP_MAIN
if oldGroup == groupName then return true end
def.group = groupName
-- Remove from current position in order
local currentPos = nil
for i, id in ipairs(cats.order) do
if id == categoryId then
currentPos = i
break
end
end
if currentPos then
table.remove(cats.order, currentPos)
end
-- Insert at end of new group
local insertPos = nil
for i = table.getn(cats.order), 1, -1 do
local existDef = cats.definitions[cats.order[i]]
if existDef and (existDef.group or GROUP_MAIN) == groupName then
insertPos = i + 1
break
end
end
if insertPos then
table.insert(cats.order, insertPos, categoryId)
else
table.insert(cats.order, categoryId)
end
self:SaveCategories(cats)
return true
end
-- Get the group order (unique groups in display order)
function CategoryManager:GetGroupOrder()
return self:GetGroups()
end
-- Get group constants
function CategoryManager:GetGroupMain() return GROUP_MAIN end
function CategoryManager:GetGroupOther() return GROUP_OTHER end
function CategoryManager:GetGroupClass() return GROUP_CLASS end
-------------------------------------------
-- Item Override System
-------------------------------------------
-- Assign an item to a specific category by item ID
-- Uses flat map at categories level: cats.itemOverrides[itemID] = categoryId
function CategoryManager:AssignItemToCategory(itemID, categoryId)
if not itemID or not categoryId then return false end
local cats = self:GetCategories()
local def = cats.definitions[categoryId]
if not def then return false end
if not cats.itemOverrides then
cats.itemOverrides = {}
end
cats.itemOverrides[itemID] = categoryId
self:SaveCategories(cats)
self:ClearCache()
return true
end
-- Remove an item from a specific category's overrides
function CategoryManager:RemoveItemFromCategory(itemID, categoryId)
if not itemID or not categoryId then return false end
local cats = self:GetCategories()
if not cats.itemOverrides then return false end
if cats.itemOverrides[itemID] == categoryId then
cats.itemOverrides[itemID] = nil
self:SaveCategories(cats)
self:ClearCache()
return true
end
return false
end
-- Remove an item override regardless of category
function CategoryManager:RemoveItemOverride(itemID)
if not itemID then return end
local cats = self:GetCategories()
if cats.itemOverrides then
cats.itemOverrides[itemID] = nil
end
end
-------------------------------------------
-- Equipment Set Category Sync
-------------------------------------------
-- Saved properties for equipment set categories that were user-edited
-- Preserves user changes (enabled state, order position) when sets are deleted/recreated
local savedEquipSetProps = {}
-- Sync equipment set categories with current set data from EquipmentSets module
function CategoryManager:SyncEquipmentSetCategories()
local equipSets = addon.Modules.EquipmentSets
if not equipSets then return end
local showEquipSets = addon.Modules.DB:GetSetting("showEquipSetCategories")
if showEquipSets == false then return end
local setNames = equipSets:GetAllSetNames()
if not setNames then return end
local cats = self:GetCategories()
local existingSetCats = {}
-- Find existing EquipSet categories
for id, def in pairs(cats.definitions) do
if string.find(id, "^EquipSet:") then
existingSetCats[id] = true
end
end
-- Create/update categories for current sets
for _, setName in ipairs(setNames) do
local catId = "EquipSet:" .. setName
if not cats.definitions[catId] then
-- Check for saved properties from a previously deleted set
local props = savedEquipSetProps[catId]
local defaultMark = "Interface\\AddOns\\Guda\\Assets\\equipment"
local newDef = {
name = setName,
icon = "Interface\\Icons\\INV_Chest_Chain_04",
rules = {},
matchMode = "all",
priority = 65,
enabled = props and props.enabled or true,
isBuiltIn = false,
isEquipSetCategory = true,
group = GROUP_MAIN,
categoryMark = props and props.categoryMark or defaultMark,
}
cats.definitions[catId] = newDef
-- Add to order
local insertPos = nil
for i = table.getn(cats.order), 1, -1 do
local existDef = cats.definitions[cats.order[i]]
if existDef and (existDef.group or GROUP_MAIN) == GROUP_MAIN then
insertPos = i + 1
break
end
end
if insertPos then
table.insert(cats.order, insertPos, catId)
else
table.insert(cats.order, catId)
end
addon:Debug("CategoryManager: Created equipment set category: " .. catId)
end
existingSetCats[catId] = nil -- Mark as still active
end
-- Remove categories for sets that no longer exist
for catId in pairs(existingSetCats) do
local def = cats.definitions[catId]
if def then
-- Save user-edited properties before deletion
savedEquipSetProps[catId] = {
enabled = def.enabled,
categoryMark = def.categoryMark,
}
end
cats.definitions[catId] = nil
for i, id in ipairs(cats.order) do
if id == catId then
table.remove(cats.order, i)
break
end
end
addon:Debug("CategoryManager: Removed equipment set category: " .. catId)
end
self:SaveCategories(cats)
end
-- Get available rule types for UI -- Get available rule types for UI
function CategoryManager:GetRuleTypes() function CategoryManager:GetRuleTypes()
return { return {
+13 -4
View File
@@ -36,8 +36,8 @@ function DB:Initialize()
bankBagColumns = 8, bankBagColumns = 8,
bankColumns = 10, bankColumns = 10,
sortMethod = "quality", -- quality, name, type sortMethod = "quality", -- quality, name, type
iconSize = 40, iconSize = 37,
iconSpacing = 0, iconSpacing = 3,
iconFontSize = 12, iconFontSize = 12,
showQualityBorderEquipment = true, showQualityBorderEquipment = true,
showQualityBorderOther = true, showQualityBorderOther = true,
@@ -98,10 +98,10 @@ function DB:Initialize()
Guda_CharDB.settings.bankColumns = 10 Guda_CharDB.settings.bankColumns = 10
end end
if not Guda_CharDB.settings.iconSize then if not Guda_CharDB.settings.iconSize then
Guda_CharDB.settings.iconSize = 40 Guda_CharDB.settings.iconSize = 37
end end
if not Guda_CharDB.settings.iconSpacing then if not Guda_CharDB.settings.iconSpacing then
Guda_CharDB.settings.iconSpacing = 0 Guda_CharDB.settings.iconSpacing = 3
end end
if not Guda_CharDB.settings.iconFontSize then if not Guda_CharDB.settings.iconFontSize then
Guda_CharDB.settings.iconFontSize = 12 Guda_CharDB.settings.iconFontSize = 12
@@ -121,6 +121,15 @@ function DB:Initialize()
if Guda_CharDB.settings.markUnusableItems == nil then if Guda_CharDB.settings.markUnusableItems == nil then
Guda_CharDB.settings.markUnusableItems = true Guda_CharDB.settings.markUnusableItems = true
end end
if Guda_CharDB.settings.mergedGroups == nil then
Guda_CharDB.settings.mergedGroups = {}
end
if Guda_CharDB.settings.showEquipSetCategories == nil then
Guda_CharDB.settings.showEquipSetCategories = true
end
if Guda_CharDB.settings.markEquipmentSets == nil then
Guda_CharDB.settings.markEquipmentSets = true
end
-- Initialize CategoryManager for custom categories -- Initialize CategoryManager for custom categories
if addon.Modules.CategoryManager then if addon.Modules.CategoryManager then
+2 -2
View File
@@ -53,8 +53,8 @@ addon.Constants = {
SAVE_INTERVAL = 1800, -- 30 minutes in seconds SAVE_INTERVAL = 1800, -- 30 minutes in seconds
-- UI Constants -- UI Constants
BUTTON_SIZE = 40, BUTTON_SIZE = 37,
BUTTON_SPACING = 0, BUTTON_SPACING = 3,
BUTTONS_PER_ROW = 10, BUTTONS_PER_ROW = 10,
MIN_ICON_SIZE = 30, MIN_ICON_SIZE = 30,
MAX_ICON_SIZE = 64, MAX_ICON_SIZE = 64,
+5
View File
@@ -27,6 +27,11 @@ function Main:Initialize()
addon.Modules.MoneyTracker:Initialize() addon.Modules.MoneyTracker:Initialize()
addon.Modules.EquipmentScanner:Initialize() addon.Modules.EquipmentScanner:Initialize()
-- Initialize equipment sets (Outfitter/ItemRack integration)
if addon.Modules.EquipmentSets then
addon.Modules.EquipmentSets:Initialize()
end
-- Initialize UI -- Initialize UI
addon:Print("Initializing UI...") addon:Print("Initializing UI...")
addon.Modules.BagFrame:Initialize() addon.Modules.BagFrame:Initialize()
+280
View File
@@ -0,0 +1,280 @@
-- Guda Equipment Sets Module
-- Detects and tracks equipment sets from Outfitter and ItemRack addons
-- Provides API for checking if items belong to equipment sets
local addon = Guda
local EquipmentSets = {}
addon.Modules.EquipmentSets = EquipmentSets
-- Internal state
local setData = {} -- { setName => { itemIDs = {[itemID] = true} } }
local itemToSets = {} -- { [itemID] => { setName1 = true, setName2 = true } }
local initialized = false
local outfitterReady = false
local itemRackReady = false
-------------------------------------------
-- Public API
-------------------------------------------
-- Check if an item ID belongs to any equipment set
function EquipmentSets:IsInSet(itemID)
if not itemID then return false end
return itemToSets[itemID] ~= nil
end
-- Get set names that contain a specific item ID
-- Returns a table of set names or nil
function EquipmentSets:GetSetNames(itemID)
if not itemID then return nil end
local sets = itemToSets[itemID]
if not sets then return nil end
local names = {}
for name in pairs(sets) do
table.insert(names, name)
end
if table.getn(names) == 0 then return nil end
return names
end
-- Get all known set names (sorted)
function EquipmentSets:GetAllSetNames()
local names = {}
for name in pairs(setData) do
table.insert(names, name)
end
table.sort(names)
return names
end
-------------------------------------------
-- Internal: Rebuild item-to-set index
-------------------------------------------
local function RebuildItemIndex()
itemToSets = {}
for setName, data in pairs(setData) do
if data.itemIDs then
for itemID in pairs(data.itemIDs) do
if not itemToSets[itemID] then
itemToSets[itemID] = {}
end
itemToSets[itemID][setName] = true
end
end
end
end
-------------------------------------------
-- Outfitter Integration
-------------------------------------------
local function ScanOutfitter()
-- Check if Outfitter is loaded and initialized
if not Outfitter_GetCategoryOrder then return false end
addon:Debug("EquipmentSets: Scanning Outfitter outfits...")
local categoryOrder = Outfitter_GetCategoryOrder()
if not categoryOrder then return false end
local scannedSets = 0
for _, catID in ipairs(categoryOrder) do
local outfits = nil
if Outfitter_GetOutfitsByCategoryID then
outfits = Outfitter_GetOutfitsByCategoryID(catID)
end
if outfits then
for _, outfit in ipairs(outfits) do
local setName = outfit.Name
if setName and outfit.Items then
local itemIDs = {}
for slotName, item in pairs(outfit.Items) do
if item then
local itemID = nil
-- Outfitter stores item codes
if item.Code then
itemID = tonumber(item.Code)
elseif item.ItemID then
itemID = tonumber(item.ItemID)
end
if itemID and itemID > 0 then
itemIDs[itemID] = true
end
end
end
setData[setName] = { itemIDs = itemIDs, source = "Outfitter" }
scannedSets = scannedSets + 1
end
end
end
end
addon:Debug("EquipmentSets: Scanned %d Outfitter outfits", scannedSets)
return scannedSets > 0
end
local function HookOutfitterEvents()
if not Outfitter_RegisterOutfitEvent then return end
local events = { "ADD", "DELETE", "EDIT", "RENAME" }
for _, eventName in ipairs(events) do
local success, err = pcall(function()
Outfitter_RegisterOutfitEvent(eventName, function()
-- Rescan after a brief delay to let Outfitter finish its update
addon:Debug("EquipmentSets: Outfitter event '%s', rescanning...", eventName)
ScanOutfitter()
RebuildItemIndex()
-- Sync categories
if addon.Modules.CategoryManager then
addon.Modules.CategoryManager:SyncEquipmentSetCategories()
end
end)
end)
if not success then
addon:Debug("EquipmentSets: Failed to hook Outfitter event '%s': %s", eventName, tostring(err))
end
end
end
-------------------------------------------
-- ItemRack Integration
-------------------------------------------
local function ScanItemRack()
-- Check if ItemRack is loaded
if not ItemRackUser or not ItemRackUser.Sets then return false end
addon:Debug("EquipmentSets: Scanning ItemRack sets...")
local scannedSets = 0
for setName, setInfo in pairs(ItemRackUser.Sets) do
-- Skip internal sets (start with special chars)
if not string.find(setName, "^~") then
local itemIDs = {}
if setInfo.equip then
for slot, itemString in pairs(setInfo.equip) do
-- ItemRack format: "itemID:enchant:suffix:unique"
if type(itemString) == "string" then
local _, _, idStr = string.find(itemString, "^(%d+)")
local itemID = tonumber(idStr)
if itemID and itemID > 0 then
itemIDs[itemID] = true
end
elseif type(itemString) == "number" then
if itemString > 0 then
itemIDs[itemString] = true
end
end
end
end
setData[setName] = { itemIDs = itemIDs, source = "ItemRack" }
scannedSets = scannedSets + 1
end
end
addon:Debug("EquipmentSets: Scanned %d ItemRack sets", scannedSets)
return scannedSets > 0
end
-------------------------------------------
-- Full Scan (all sources)
-------------------------------------------
local function FullScan()
setData = {}
local hasOutfitter = ScanOutfitter()
local hasItemRack = ScanItemRack()
RebuildItemIndex()
-- Sync equipment set categories
if addon.Modules.CategoryManager then
addon.Modules.CategoryManager:SyncEquipmentSetCategories()
end
if hasOutfitter or hasItemRack then
addon:Debug("EquipmentSets: Full scan complete, %d total sets", table.getn(EquipmentSets:GetAllSetNames()))
end
end
-------------------------------------------
-- Initialization
-------------------------------------------
function EquipmentSets:Initialize()
if initialized then return end
initialized = true
-- Register for ADDON_LOADED to catch late-loading addons
addon.Modules.Events:Register("ADDON_LOADED", function(event, addonName)
if addonName == "Outfitter" then
-- Outfitter needs its INIT event before scanning
outfitterReady = true
addon:Debug("EquipmentSets: Outfitter loaded, waiting for INIT...")
elseif addonName == "ItemRack" then
itemRackReady = true
addon:Debug("EquipmentSets: ItemRack loaded, scanning...")
FullScan()
end
end, "EquipmentSets")
-- Register for PLAYER_ENTERING_WORLD to catch already-loaded addons
addon.Modules.Events:Register("PLAYER_ENTERING_WORLD", function()
-- Check if Outfitter is already loaded
if Outfitter_GetCategoryOrder or gOutfitter_Initialized then
outfitterReady = true
HookOutfitterEvents()
FullScan()
end
-- Check if ItemRack is already loaded
if ItemRackUser and ItemRackUser.Sets then
itemRackReady = true
FullScan()
end
end, "EquipmentSets")
-- Hook Outfitter's INIT event if available (fires after Outfitter finishes setup)
-- This uses a frame to check periodically since OUTFITTER_INIT is a custom event
local initCheckFrame = CreateFrame("Frame")
initCheckFrame.elapsed = 0
initCheckFrame.checks = 0
initCheckFrame:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + arg1
if this.elapsed < 1 then return end
this.elapsed = 0
this.checks = this.checks + 1
-- Check if Outfitter became available
if not outfitterReady and (gOutfitter_Initialized or Outfitter_GetCategoryOrder) then
outfitterReady = true
HookOutfitterEvents()
FullScan()
this:Hide()
return
end
-- Stop checking after 30 seconds
if this.checks > 30 then
this:Hide()
-- Do a final scan anyway in case addons loaded without events
if Outfitter_GetCategoryOrder or (ItemRackUser and ItemRackUser.Sets) then
FullScan()
end
end
end)
initCheckFrame:Show()
addon:Debug("EquipmentSets: Module initialized")
end
-- Force a rescan of all equipment set sources
function EquipmentSets:Rescan()
FullScan()
end
+1
View File
@@ -24,6 +24,7 @@ Data\BankScanner.lua
Data\MailboxScanner.lua Data\MailboxScanner.lua
Data\MoneyTracker.lua Data\MoneyTracker.lua
Data\EquipmentScanner.lua Data\EquipmentScanner.lua
Data\EquipmentSets.lua
Sorting\SortEngine.lua Sorting\SortEngine.lua
+162 -105
View File
@@ -996,55 +996,131 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
local categoryItemsProcessed = 0 local categoryItemsProcessed = 0
local CATEGORY_ITEMS_PER_BUDGET_CHECK = 8 local CATEGORY_ITEMS_PER_BUDGET_CHECK = 8
local totalButtonsCreated = 0 -- Check merged groups setting
for _, catName in ipairs(categoryList) do local mergedGroups = addon.Modules.DB:GetSetting("mergedGroups") or {}
local items = categories[catName]
local numItems = items and table.getn(items) or 0
if numItems > 0 then
addon:DebugCategory(" Category '%s': %d items, pos=(%d,%d)", catName, numItems, currentX, currentY)
-- Sort items using centralized sorter
Guda_SortCategoryItems(items)
local blockCols = numItems -- Build category order index map for sorting within merged groups
if blockCols > perRow then blockCols = perRow end local catOrderIndex = {}
local blockRows = math.ceil(numItems / perRow) for idx, catId in ipairs(categoryList) do
local blockWidth = blockCols * (buttonSize + spacing) catOrderIndex[catId] = idx
local blockHeight = 20 + (blockRows * (buttonSize + spacing)) + 5 end
-- Check if it fits in current row -- Build merged group display lists if any groups are merged
if currentX > 0 and currentX + blockWidth + 20 > totalWidth + 5 then local mergedDisplayList = {} -- { { name, items, icon, catDef } }
currentX = 0 local processedCats = {} -- Track which categories were merged
currentY = currentY + rowMaxHeight
rowMaxHeight = 0
addon:DebugCategory(" -> wrap to new row, Y=%d", currentY)
end
-- Add Header if addon.Modules.CategoryManager then
local header = self:GetSectionHeader(headerIdx) local groupsByName = addon.Modules.CategoryManager:GetCategoriesByGroup()
headerIdx = headerIdx + 1 for groupName, catIds in pairs(groupsByName) do
header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", startX + currentX, startY - currentY) if mergedGroups[groupName] then
header:SetWidth(blockWidth) -- Merge all items from categories in this group into one section
local mergedItems = {}
-- Get display name from category definition local mergedName = groupName
local displayName = catName local mergedIcon = nil
if addon.Modules.CategoryManager then for _, catId in ipairs(catIds) do
local catDef = addon.Modules.CategoryManager:GetCategory(catName) local items = categories[catId]
if catDef and catDef.name then if items then
displayName = catDef.name local orderIdx = catOrderIndex[catId] or 999
for _, item in ipairs(items) do
item.categoryOrderIndex = orderIdx
table.insert(mergedItems, item)
end
end
processedCats[catId] = true
end
if table.getn(mergedItems) > 0 then
table.insert(mergedDisplayList, {
name = mergedName,
items = mergedItems,
icon = nil, -- Group header, no single icon
})
end end
end end
header.fullName = displayName end
header.isShortened = false end
if string.len(displayName) > 8 and numItems < 2 then
displayName = string.sub(displayName, 1, 6) .. "..."
header.isShortened = true
end
header.text:SetText(displayName)
header:Show()
local itemY = currentY + 20 -- Track total buttons created across all category blocks
local col = 0 local totalButtonsCreated = 0
local row = 0
-- Helper to render a category block
local function RenderCategoryBlock(catName, items, numItems, catDef, isEmptyCat)
addon:DebugCategory(" Category '%s': %d items, pos=(%d,%d)", catName, numItems, currentX, currentY)
Guda_SortCategoryItems(items)
local effectiveItems = numItems
if isEmptyCat then effectiveItems = 1 end
local blockCols = effectiveItems
if blockCols > perRow then blockCols = perRow end
local blockRows = math.ceil(effectiveItems / 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)
-- Get display name from category definition
local displayName = catName
if catDef and catDef.name then
displayName = catDef.name
end
-- Set item count
if header.countText then
if numItems > 0 then
header.countText:SetText("(" .. numItems .. ")")
header.countText:Show()
else
header.countText:Hide()
end
end
header.fullName = displayName
header.isShortened = false
if string.len(displayName) > 8 and effectiveItems < 2 then
displayName = string.sub(displayName, 1, 6) .. "..."
header.isShortened = true
end
header.text:SetText(displayName)
header:Show()
local itemY = currentY + 20
local col = 0
local row = 0
if isEmptyCat then
-- Render empty slot indicator
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", startX + currentX, startY - itemY)
button:Show()
local emptyItemData = {
texture = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Bag",
count = totalFreeSlots,
name = "Empty Slots"
}
Guda_ItemButton_SetItem(button, bagID, slotID, emptyItemData, false, isOtherChar and charName or nil, true, true)
button.isReadOnly = false
button.inUse = true
table.insert(itemButtons, button)
else
for _, item in ipairs(items) do for _, item in ipairs(items) do
local bagID = item.bagID local bagID = item.bagID
local slot = item.slotID local slot = item.slotID
@@ -1064,7 +1140,6 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
Guda_ItemButton_SetItem(button, bagID, slot, itemData, false, isOtherChar and charName or nil, matchesFilter, isOtherChar) Guda_ItemButton_SetItem(button, bagID, slot, itemData, false, isOtherChar and charName or nil, matchesFilter, isOtherChar)
button.inUse = true button.inUse = true
table.insert(itemButtons, button) table.insert(itemButtons, button)
-- Populate slot lookup for O(1) access
if not slotToButton[bagID] then slotToButton[bagID] = {} end if not slotToButton[bagID] then slotToButton[bagID] = {} end
slotToButton[bagID][slot] = button slotToButton[bagID][slot] = button
@@ -1074,7 +1149,6 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
row = row + 1 row = row + 1
end end
-- Frame budget check
categoryItemsProcessed = categoryItemsProcessed + 1 categoryItemsProcessed = categoryItemsProcessed + 1
if categoryItemsProcessed >= CATEGORY_ITEMS_PER_BUDGET_CHECK then if categoryItemsProcessed >= CATEGORY_ITEMS_PER_BUDGET_CHECK then
categoryItemsProcessed = 0 categoryItemsProcessed = 0
@@ -1083,10 +1157,38 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
end end
end end
end end
end
totalButtonsCreated = totalButtonsCreated + numItems totalButtonsCreated = totalButtonsCreated + effectiveItems
if blockHeight > rowMaxHeight then rowMaxHeight = blockHeight end if blockHeight > rowMaxHeight then rowMaxHeight = blockHeight end
currentX = currentX + blockWidth + 20 currentX = currentX + blockWidth + 20
end
-- Render merged group sections first
for _, merged in ipairs(mergedDisplayList) do
local numItems = table.getn(merged.items)
if numItems > 0 then
RenderCategoryBlock(merged.name, merged.items, numItems, { name = merged.name, icon = merged.icon })
end
end
-- Render individual categories (skip merged ones and Empty)
for _, catName in ipairs(categoryList) do
if not processedCats[catName] then
local catDef = addon.Modules.CategoryManager and addon.Modules.CategoryManager:GetCategory(catName) or nil
-- Handle Empty category specially
if catDef and catDef.isEmptyCategory then
if totalFreeSlots > 0 and catDef.enabled then
RenderCategoryBlock(catName, {}, 0, catDef, true)
end
else
local items = categories[catName]
local numItems = items and table.getn(items) or 0
if numItems > 0 then
RenderCategoryBlock(catName, items, numItems, catDef, false)
end
end
end end
end end
addon:DebugCategory("DisplayItemsByCategory: totalButtonsCreated=%d from categories", totalButtonsCreated) addon:DebugCategory("DisplayItemsByCategory: totalButtonsCreated=%d from categories", totalButtonsCreated)
@@ -1094,12 +1196,9 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
-- Update Y for bottom sections -- Update Y for bottom sections
local y = currentY + rowMaxHeight local y = currentY + rowMaxHeight
-- Special sections at bottom (Hearthstone, Mount, Tools, Empty) -- Special sections at bottom (only Mounts now)
local bottomSections = { local bottomSections = {
{ name = "Home", items = specialItems.Hearthstone },
{ name = "Mounts", items = specialItems.Mount }, { name = "Mounts", items = specialItems.Mount },
{ name = "Tools", items = specialItems.Tools },
{ name = "Empty", items = {} }
} }
local x = startX local x = startX
@@ -1112,10 +1211,6 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
break break
end end
end end
-- Also check if Empty section should show
if totalFreeSlots > 0 then
hasAnyBottom = true
end
if hasAnyBottom then if hasAnyBottom then
y = y - 10 y = y - 10
@@ -1125,22 +1220,7 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
for _, sec in ipairs(bottomSections) do for _, sec in ipairs(bottomSections) do
local items = sec.items local items = sec.items
local numItems = table.getn(items) local numItems = table.getn(items)
if sec.name == "Empty" then
numItems = (totalFreeSlots > 0) and 1 or 0
end
if numItems > 0 then if numItems > 0 then
if sec.name == "Tools" then
table.sort(items, function(a, b)
-- Guard against nil entries
if not a or not a.itemData then return false end
if not b or not b.itemData then return true end
if (a.itemData.quality or 0) ~= (b.itemData.quality or 0) then
return (a.itemData.quality or 0) > (b.itemData.quality or 0)
end
return (a.itemData.name or "") < (b.itemData.name or "")
end)
end
local blockCols = numItems local blockCols = numItems
if blockCols > perRow then blockCols = perRow end if blockCols > perRow then blockCols = perRow end
local blockRows = math.ceil(numItems / perRow) local blockRows = math.ceil(numItems / perRow)
@@ -1158,55 +1238,32 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX, y) header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX, y)
header:SetWidth(blockWidth) header:SetWidth(blockWidth)
header.text:SetText(sec.name) header.text:SetText(sec.name)
if header.countText then header.countText:SetText("(" .. numItems .. ")"); header.countText:Show() end
header:Show() header:Show()
local itemY = y - 20 local itemY = y - 20
local sCol = 0 local sCol = 0
local sRow = 0 local sRow = 0
if sec.name == "Empty" then for _, item in ipairs(items) do
local bagID = firstFreeBag or 0 local bagParent = self:GetBagParent(item.bagID)
local slotID = firstFreeSlot or 1
local bagParent = self:GetBagParent(bagID)
local button = Guda_GetItemButton(bagParent) local button = Guda_GetItemButton(bagParent)
button:SetParent(bagParent) button:SetParent(bagParent)
button:SetWidth(buttonSize) button:SetWidth(buttonSize)
button:SetHeight(buttonSize) button:SetHeight(buttonSize)
button:ClearAllPoints() button:ClearAllPoints()
button:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX, itemY) button:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX + (sCol * (buttonSize + spacing)), itemY - (sRow * (buttonSize + spacing)))
button:Show() button:Show()
Guda_ItemButton_SetItem(button, item.bagID, item.slotID, item.itemData, false, isOtherChar and charName or nil, self:PassesSearchFilter(item.itemData), isOtherChar)
local emptyItemData = {
texture = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Bag",
count = totalFreeSlots,
name = "Empty Slots"
}
Guda_ItemButton_SetItem(button, bagID, slotID, emptyItemData, false, isOtherChar and charName or nil, true, true)
button.isReadOnly = false
button.inUse = true button.inUse = true
table.insert(itemButtons, button) table.insert(itemButtons, button)
else if not slotToButton[item.bagID] then slotToButton[item.bagID] = {} end
for _, item in ipairs(items) do slotToButton[item.bagID][item.slotID] = button
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
table.insert(itemButtons, button)
-- Populate slot lookup for O(1) access
if not slotToButton[item.bagID] then slotToButton[item.bagID] = {} end
slotToButton[item.bagID][item.slotID] = button
sCol = sCol + 1 sCol = sCol + 1
if sCol >= blockCols then if sCol >= blockCols then
sCol = 0 sCol = 0
sRow = sRow + 1 sRow = sRow + 1
end
end end
end end
+32 -57
View File
@@ -779,6 +779,13 @@ function BankFrame:DisplayItemsByCategory(bankData, isOtherChar, charName)
local CATEGORY_ITEMS_PER_BUDGET_CHECK = 8 local CATEGORY_ITEMS_PER_BUDGET_CHECK = 8
for _, catName in ipairs(categoryList) do for _, catName in ipairs(categoryList) do
local catDef = addon.Modules.CategoryManager and addon.Modules.CategoryManager:GetCategory(catName) or nil
-- Skip Empty category in bank (bank doesn't need empty slot indicator in category loop)
if catDef and catDef.isEmptyCategory then
-- Skip empty categories in bank view
else
local items = categories[catName] local items = categories[catName]
local numItems = items and table.getn(items) or 0 local numItems = items and table.getn(items) or 0
if numItems > 0 then if numItems > 0 then
@@ -805,7 +812,17 @@ function BankFrame:DisplayItemsByCategory(bankData, isOtherChar, charName)
header:SetWidth(blockWidth) header:SetWidth(blockWidth)
local displayName = catName local displayName = catName
header.fullName = catName if catDef and catDef.name then
displayName = catDef.name
end
-- Set item count
if header.countText then
header.countText:SetText("(" .. numItems .. ")")
header.countText:Show()
end
header.fullName = displayName
header.isShortened = false header.isShortened = false
if string.len(displayName) > 10 and numItems < 2 then if string.len(displayName) > 10 and numItems < 2 then
displayName = string.sub(displayName, 1, 7) .. "..." displayName = string.sub(displayName, 1, 7) .. "..."
@@ -865,17 +882,15 @@ function BankFrame:DisplayItemsByCategory(bankData, isOtherChar, charName)
if blockHeight > rowMaxHeight then rowMaxHeight = blockHeight end if blockHeight > rowMaxHeight then rowMaxHeight = blockHeight end
currentX = currentX + blockWidth + 20 currentX = currentX + blockWidth + 20
end end
end -- isEmptyCategory else
end end
-- Update Y for bottom sections -- Update Y for bottom sections
local y = currentY + rowMaxHeight local y = currentY + rowMaxHeight
-- Special sections at bottom (Hearthstone, Mount, Tools, Empty) -- Special sections at bottom (only Mounts now; Home/Tools/Empty are real categories)
local bottomSections = { local bottomSections = {
{ name = "Home", items = specialItems.Hearthstone },
{ name = "Mounts", items = specialItems.Mount }, { name = "Mounts", items = specialItems.Mount },
{ name = "Tools", items = specialItems.Tools },
{ name = "Empty", items = {} }
} }
local x = startX local x = startX
@@ -888,10 +903,6 @@ function BankFrame:DisplayItemsByCategory(bankData, isOtherChar, charName)
break break
end end
end end
-- Also show bottom section if there are free slots (for Empty drop target)
if totalFreeSlots > 0 then
hasAnyBottom = true
end
if hasAnyBottom then if hasAnyBottom then
y = y - 10 y = y - 10
@@ -901,22 +912,7 @@ function BankFrame:DisplayItemsByCategory(bankData, isOtherChar, charName)
for _, sec in ipairs(bottomSections) do for _, sec in ipairs(bottomSections) do
local items = sec.items local items = sec.items
local numItems = table.getn(items) local numItems = table.getn(items)
if sec.name == "Empty" then
numItems = (totalFreeSlots > 0) and 1 or 0
end
if numItems > 0 then if numItems > 0 then
if sec.name == "Tools" then
table.sort(items, function(a, b)
-- Guard against nil entries
if not a or not a.itemData then return false end
if not b or not b.itemData then return true end
if (a.itemData.quality or 0) ~= (b.itemData.quality or 0) then
return (a.itemData.quality or 0) > (b.itemData.quality or 0)
end
return (a.itemData.name or "") < (b.itemData.name or "")
end)
end
local blockCols = numItems local blockCols = numItems
if blockCols > perRow then blockCols = perRow end if blockCols > perRow then blockCols = perRow end
local blockRows = math.ceil(numItems / perRow) local blockRows = math.ceil(numItems / perRow)
@@ -934,53 +930,32 @@ function BankFrame:DisplayItemsByCategory(bankData, isOtherChar, charName)
header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX, y) header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX, y)
header:SetWidth(blockWidth) header:SetWidth(blockWidth)
header.text:SetText(sec.name) header.text:SetText(sec.name)
if header.countText then header.countText:SetText("(" .. numItems .. ")"); header.countText:Show() end
header:Show() header:Show()
local itemY = y - 20 local itemY = y - 20
local sCol = 0 local sCol = 0
local sRow = 0 local sRow = 0
if sec.name == "Empty" then for _, item in ipairs(items) do
local bagID = firstFreeBag or -1 local bagParent = self:GetBagParent(item.bagID)
local slotID = firstFreeSlot or 1
local bagParent = self:GetBagParent(bagID)
local button = Guda_GetItemButton(bagParent) local button = Guda_GetItemButton(bagParent)
button:SetParent(bagParent) button:SetParent(bagParent)
button:SetWidth(buttonSize) button:SetWidth(buttonSize)
button:SetHeight(buttonSize) button:SetHeight(buttonSize)
button:ClearAllPoints() button:ClearAllPoints()
button:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX, itemY) button:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX + (sCol * (buttonSize + spacing)), itemY - (sRow * (buttonSize + spacing)))
button:Show() 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)
local emptyItemData = {
texture = "Interface\\PaperDoll\\UI-PaperDoll-Slot-Bag",
count = totalFreeSlots,
name = "Empty Slots"
}
Guda_ItemButton_SetItem(button, bagID, slotID, emptyItemData, true, isOtherChar and charName or nil, true, true)
button.isReadOnly = false
button.inUse = true button.inUse = true
else -- Populate slot lookup for O(1) access
for _, item in ipairs(items) do if not bankSlotToButton[item.bagID] then bankSlotToButton[item.bagID] = {} end
local bagParent = self:GetBagParent(item.bagID) bankSlotToButton[item.bagID][item.slotID] = button
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
-- Populate slot lookup for O(1) access
if not bankSlotToButton[item.bagID] then bankSlotToButton[item.bagID] = {} end
bankSlotToButton[item.bagID][item.slotID] = button
sCol = sCol + 1 sCol = sCol + 1
if sCol >= blockCols then if sCol >= blockCols then
sCol = 0 sCol = 0
sRow = sRow + 1 sRow = sRow + 1
end
end end
end end
+21 -36
View File
@@ -21,7 +21,6 @@ end
-- Returns nothing, modifies tables in place -- Returns nothing, modifies tables in place
function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, isOtherChar) function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, isOtherChar)
local itemName = itemData.name or "" local itemName = itemData.name or ""
local itemType = itemData.type or ""
local cat = "Miscellaneous" local cat = "Miscellaneous"
-- Detect consumable restore/eat/drink tag for current character only -- Detect consumable restore/eat/drink tag for current character only
@@ -32,38 +31,11 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems,
end end
end end
-- Priority 1: Special items (Hearthstone, Mounts, Tools) -- Special items: only Mounts are handled separately now
-- These are handled separately and go into specialItems table, not categories -- Home and Tools are real categories handled by CategoryManager rules
if string.find(itemName, "Hearthstone") then if addon.Modules.SortEngine and addon.Modules.SortEngine.IsMount and addon.Modules.SortEngine.IsMount(itemData.texture) then
-- Only show in Home section if Home category is enabled
local showHome = true
if addon.Modules.CategoryManager then
local homeCat = addon.Modules.CategoryManager:GetCategory("Home")
if homeCat then
showHome = homeCat.enabled
end
end
if showHome then
table.insert(specialItems.Hearthstone, {bagID = bagID, slotID = slotID, itemData = itemData})
end
-- Always return - if Home is disabled, Hearthstone is hidden completely
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}) table.insert(specialItems.Mount, {bagID = bagID, slotID = slotID, itemData = itemData})
return 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" or
string.find(itemName, "Jeweler") or
string.find(itemName, "Jewelry Kit") then
table.insert(specialItems.Tools, {bagID = bagID, slotID = slotID, itemData = itemData})
return
end end
-- Use CategoryManager rule engine if available, otherwise fall back to legacy logic -- Use CategoryManager rule engine if available, otherwise fall back to legacy logic
@@ -218,22 +190,20 @@ function Guda_InitCategories()
end end
-- Reuse or create specialItems table -- Reuse or create specialItems table
-- Only Mount remains as a special item; Home and Tools are now real categories
if not specialItemsCache then if not specialItemsCache then
specialItemsCache = { specialItemsCache = {
Hearthstone = {},
Mount = {}, Mount = {},
Tools = {}
} }
else else
WipeTable(specialItemsCache.Hearthstone)
WipeTable(specialItemsCache.Mount) WipeTable(specialItemsCache.Mount)
WipeTable(specialItemsCache.Tools)
end end
return categoriesCache, specialItemsCache return categoriesCache, specialItemsCache
end end
-- Sort items within a category -- Sort items within a category (or merged group)
-- Items may have a categoryOrderIndex field set by merged group display
function Guda_SortCategoryItems(items) function Guda_SortCategoryItems(items)
if not items then return end if not items then return end
table.sort(items, function(a, b) table.sort(items, function(a, b)
@@ -243,6 +213,13 @@ function Guda_SortCategoryItems(items)
if not a.itemData then return false end if not a.itemData then return false end
if not b.itemData then return true end if not b.itemData then return true end
-- Primary: category order index (for merged groups)
local oa = a.categoryOrderIndex or 0
local ob = b.categoryOrderIndex or 0
if oa ~= ob then
return oa < ob
end
-- Rank Trade Goods: meat (name ends with 'meat') = 2, egg (contains 'egg') = 1, others = 0 -- Rank Trade Goods: meat (name ends with 'meat') = 2, egg (contains 'egg') = 1, others = 0
local function tgRank(d) local function tgRank(d)
if not d or not d.name then return 0 end if not d or not d.name then return 0 end
@@ -290,10 +267,18 @@ function Guda_GetSectionHeader(framePrefix, containerName, index)
header = CreateFrame("Frame", name, container) header = CreateFrame("Frame", name, container)
header:SetHeight(20) header:SetHeight(20)
header:EnableMouse(true) header:EnableMouse(true)
-- Category name text
local text = header:CreateFontString(nil, "OVERLAY", "GameFontNormal") local text = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
text:SetPoint("LEFT", header, "LEFT", 0, 0) text:SetPoint("LEFT", header, "LEFT", 0, 0)
header.text = text header.text = text
-- Item count text
local countText = header:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
countText:SetPoint("LEFT", text, "RIGHT", 4, 0)
countText:SetTextColor(0.6, 0.6, 0.6)
header.countText = countText
header:SetScript("OnEnter", function() header:SetScript("OnEnter", function()
if this.fullName and this.isShortened then if this.fullName and this.isShortened then
GameTooltip:SetOwner(this, "ANCHOR_TOP") GameTooltip:SetOwner(this, "ANCHOR_TOP")
+64 -39
View File
@@ -639,6 +639,8 @@ local function ResetButtonVisualState(self)
HideInnerShadow(self.innerShadow) HideInnerShadow(self.innerShadow)
if self.unusableOverlay then self.unusableOverlay:Hide() end if self.unusableOverlay then self.unusableOverlay:Hide() end
HideJunkIcon(self) HideJunkIcon(self)
if self.categoryMarkIcon then self.categoryMarkIcon:Hide() end
if self.categoryMarkShadow then self.categoryMarkShadow:Hide() end
-- Clear cooldown overlay -- Clear cooldown overlay
local cd = getglobal(self:GetName().."Cooldown") or self.cooldown local cd = getglobal(self:GetName().."Cooldown") or self.cooldown
@@ -738,11 +740,9 @@ local function UpdateEmptySlotBackground(self, emptySlotBg, iconSize)
if not emptySlotBg then return end if not emptySlotBg then return end
emptySlotBg:ClearAllPoints() emptySlotBg:ClearAllPoints()
-- Use smaller padding for small icons emptySlotBg:SetPoint("TOPLEFT", self, "TOPLEFT", 0, 0)
local bgPadding = iconSize < 44 and 1 or 2 emptySlotBg:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", 0, 0)
emptySlotBg:SetPoint("TOPLEFT", self, "TOPLEFT", -bgPadding, bgPadding) emptySlotBg:SetTexCoord(0.17, 0.83, 0.17, 0.83)
emptySlotBg:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", bgPadding, -bgPadding)
emptySlotBg:SetTexCoord(0.1, 0.9, 0.1, 0.9)
end end
-- Resize texture elements to match button size -- Resize texture elements to match button size
@@ -793,12 +793,11 @@ local function PositionIconAndBorders(self, iconSize)
if not iconTexture or not self.hasItem then return end if not iconTexture or not self.hasItem then return end
-- Calculate icon inset based on size -- Icon fills the full button slot
local iconInset = iconSize < 44 and 10 or 15 local iconDisplaySize = iconSize
local iconDisplaySize = iconSize - iconInset
iconTexture:ClearAllPoints() iconTexture:ClearAllPoints()
iconTexture:SetPoint("CENTER", self, "CENTER", -0.5, 0.5) iconTexture:SetPoint("CENTER", self, "CENTER", 0, 0)
iconTexture:SetWidth(iconDisplaySize) iconTexture:SetWidth(iconDisplaySize)
iconTexture:SetHeight(iconDisplaySize) iconTexture:SetHeight(iconDisplaySize)
iconTexture:SetTexCoord(0.08, 0.92, 0.08, 0.92) iconTexture:SetTexCoord(0.08, 0.92, 0.08, 0.92)
@@ -807,15 +806,15 @@ local function PositionIconAndBorders(self, iconSize)
-- Position quality border around the icon -- Position quality border around the icon
if self.qualityBorder then if self.qualityBorder then
self.qualityBorder:ClearAllPoints() self.qualityBorder:ClearAllPoints()
self.qualityBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -5, 5) self.qualityBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -1, 1)
self.qualityBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 5, -5) self.qualityBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 1, -1)
end end
-- Position quest border around the icon -- Position quest border around the icon
if self.questBorder then if self.questBorder then
self.questBorder:ClearAllPoints() self.questBorder:ClearAllPoints()
self.questBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -5, 5) self.questBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -1, 1)
self.questBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 5, -5) self.questBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 1, -1)
end end
-- Position quest icon in top-right corner -- Position quest icon in top-right corner
@@ -1056,15 +1055,12 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
end end
end end
-- Resize empty slot background to match icon size (slightly larger to ensure coverage) -- Resize empty slot background to fill the full button
if emptySlotBg then if emptySlotBg then
emptySlotBg:ClearAllPoints() emptySlotBg:ClearAllPoints()
-- Use smaller padding for small icons emptySlotBg:SetPoint("TOPLEFT", self, "TOPLEFT", 0, 0)
local bgPadding = iconSize < 44 and 1 or 2 emptySlotBg:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", 0, 0)
emptySlotBg:SetPoint("TOPLEFT", self, "TOPLEFT", -bgPadding, bgPadding) emptySlotBg:SetTexCoord(0.17, 0.83, 0.17, 0.83)
emptySlotBg:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", bgPadding, -bgPadding)
-- Crop texture edges slightly to remove any built-in padding
emptySlotBg:SetTexCoord(0.1, 0.9, 0.1, 0.9)
end end
-- Also resize the underlying slot textures so the border/background scale with the button. -- Also resize the underlying slot textures so the border/background scale with the button.
@@ -1231,11 +1227,49 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
end end
end end
-- Check if item is junk using the CategoryManager -- Check if item is junk and get category mark using the CategoryManager
local isJunk = false local isJunk = false
local categoryMarkTexture = nil
if itemData and addon.Modules.CategoryManager then if itemData and addon.Modules.CategoryManager then
local category = addon.Modules.CategoryManager:CategorizeItem(itemData, bagID, slotID, self.otherChar) local category = addon.Modules.CategoryManager:CategorizeItem(itemData, bagID, slotID, self.otherChar)
isJunk = (category == "Junk") isJunk = (category == "Junk")
-- Get category mark icon if set
local catDef = addon.Modules.CategoryManager:GetCategory(category)
if catDef and catDef.categoryMark then
categoryMarkTexture = catDef.categoryMark
end
end
-- Update category mark overlay (bottom-left of icon texture)
if categoryMarkTexture then
local iconTex = getglobal(self:GetName().."IconTexture") or getglobal(self:GetName().."Icon") or self.icon or self.Icon
local anchor = iconTex or self
-- Create mark textures lazily
if not self.categoryMarkShadow then
self.categoryMarkShadow = self:CreateTexture(nil, "OVERLAY")
self.categoryMarkShadow:SetVertexColor(0, 0, 0, 1)
end
if not self.categoryMarkIcon then
self.categoryMarkIcon = self:CreateTexture(nil, "OVERLAY")
end
-- Size relative to icon (roughly 40% of icon display size)
local markSize = math.max(10, math.floor(iconSize * 0.3))
local shadowSize = markSize + 2
self.categoryMarkShadow:SetWidth(shadowSize)
self.categoryMarkShadow:SetHeight(shadowSize)
self.categoryMarkShadow:ClearAllPoints()
self.categoryMarkShadow:SetPoint("BOTTOMLEFT", anchor, "BOTTOMLEFT", -1, -1)
self.categoryMarkIcon:SetWidth(markSize)
self.categoryMarkIcon:SetHeight(markSize)
self.categoryMarkIcon:ClearAllPoints()
self.categoryMarkIcon:SetPoint("BOTTOMLEFT", anchor, "BOTTOMLEFT", 0, 0)
self.categoryMarkShadow:SetTexture(categoryMarkTexture)
self.categoryMarkShadow:Show()
self.categoryMarkIcon:SetTexture(categoryMarkTexture)
self.categoryMarkIcon:Show()
else
if self.categoryMarkIcon then self.categoryMarkIcon:Hide() end
if self.categoryMarkShadow then self.categoryMarkShadow:Hide() end
end end
-- Search filtering and junk opacity -- Search filtering and junk opacity
@@ -1410,37 +1444,28 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
if iconTexture then if iconTexture then
if self.hasItem then if self.hasItem then
-- Scale icon proportionally based on button size -- Icon fills the full button slot
-- For icons < 44: use smaller inset (4px) for better fit local iconDisplaySize = iconSize
-- For icons >= 44: use larger inset (15px) for classic look
local iconInset
if iconSize < 44 then
iconInset = 10 -- Small inset for small icons
else
iconInset = 15 -- Larger inset for larger icons
end
local iconDisplaySize = iconSize - iconInset
iconTexture:ClearAllPoints() iconTexture:ClearAllPoints()
iconTexture:SetPoint("CENTER", self, "CENTER", -0.5, 0.5) iconTexture:SetPoint("CENTER", self, "CENTER", 0, 0)
iconTexture:SetWidth(iconDisplaySize) iconTexture:SetWidth(iconDisplaySize)
iconTexture:SetHeight(iconDisplaySize) iconTexture:SetHeight(iconDisplaySize)
-- Crop icon edges slightly -- Crop icon edges slightly
iconTexture:SetTexCoord(0.08, 0.92, 0.08, 0.92) iconTexture:SetTexCoord(0.08, 0.92, 0.08, 0.92)
iconTexture:Show() iconTexture:Show()
-- Position quality border around the icon (not the slot) -- Position quality border around the icon
if self.qualityBorder then if self.qualityBorder then
self.qualityBorder:ClearAllPoints() self.qualityBorder:ClearAllPoints()
self.qualityBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -5, 5) self.qualityBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -1, 1)
self.qualityBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 5, -5) self.qualityBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 1, -1)
end end
-- Position quest border around the icon (same as quality border) -- Position quest border around the icon
if self.questBorder then if self.questBorder then
self.questBorder:ClearAllPoints() self.questBorder:ClearAllPoints()
self.questBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -5, 5) self.questBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -1, 1)
self.questBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 5, -5) self.questBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 1, -1)
end end
-- Position quest icon in top-right corner -- Position quest icon in top-right corner
+222 -43
View File
@@ -92,9 +92,9 @@ function Guda_SettingsPopup_OnShow(self)
-- Load current settings -- Load current settings
local bagColumns = Guda.Modules.DB:GetSetting("bagColumns") or 10 local bagColumns = Guda.Modules.DB:GetSetting("bagColumns") or 10
local bankColumns = Guda.Modules.DB:GetSetting("bankColumns") or 10 local bankColumns = Guda.Modules.DB:GetSetting("bankColumns") or 10
local iconSize = Guda.Modules.DB:GetSetting("iconSize") or 40 local iconSize = Guda.Modules.DB:GetSetting("iconSize") or 37
local iconFontSize = Guda.Modules.DB:GetSetting("iconFontSize") or 12 local iconFontSize = Guda.Modules.DB:GetSetting("iconFontSize") or 12
local iconSpacing = Guda.Modules.DB:GetSetting("iconSpacing") or 0 local iconSpacing = Guda.Modules.DB:GetSetting("iconSpacing") or 3
local lockBags = Guda.Modules.DB:GetSetting("lockBags") local lockBags = Guda.Modules.DB:GetSetting("lockBags")
if lockBags == nil then if lockBags == nil then
lockBags = false lockBags = false
@@ -438,7 +438,7 @@ function Guda_SettingsPopup_IconSizeSlider_OnLoad(self)
self:SetMinMaxValues(22, 64) self:SetMinMaxValues(22, 64)
self:SetValueStep(1) self:SetValueStep(1)
local currentValue = Guda.Modules.DB:GetSetting("iconSize") or addon.Constants.BUTTON_SIZE local currentValue = Guda.Modules.DB:GetSetting("iconSize") or 37
self:SetValue(currentValue) self:SetValue(currentValue)
end end
@@ -503,7 +503,7 @@ end
-- Icon Spacing Slider OnLoad -- Icon Spacing Slider OnLoad
function Guda_SettingsPopup_IconSpacingSlider_OnLoad(self) function Guda_SettingsPopup_IconSpacingSlider_OnLoad(self)
getglobal(self:GetName().."Low"):SetText("-10px") getglobal(self:GetName().."Low"):SetText("0px")
getglobal(self:GetName().."High"):SetText("20px") getglobal(self:GetName().."High"):SetText("20px")
local text = getglobal(self:GetName().."Text") local text = getglobal(self:GetName().."Text")
@@ -515,10 +515,10 @@ function Guda_SettingsPopup_IconSpacingSlider_OnLoad(self)
text:SetFont(font, 12, flags) text:SetFont(font, 12, flags)
end end
self:SetMinMaxValues(-10, 20) self:SetMinMaxValues(0, 20)
self:SetValueStep(1) self:SetValueStep(1)
local currentValue = Guda.Modules.DB:GetSetting("iconSpacing") or 0 local currentValue = Guda.Modules.DB:GetSetting("iconSpacing") or 3
self:SetValue(currentValue) self:SetValue(currentValue)
end end
@@ -1409,6 +1409,30 @@ local function GetCategoryRowFrame(index)
return row return row
end end
-- Build a flat display list with group headers inserted
local function BuildCategoryDisplayList()
if not Guda.Modules.CategoryManager then return {}, 0 end
local categoryOrder = Guda.Modules.CategoryManager:GetCategoryOrder()
local displayList = {} -- entries: { type = "header"|"category", groupName, categoryId, categoryDef }
local lastGroup = nil
for _, catId in ipairs(categoryOrder) do
local catDef = Guda.Modules.CategoryManager:GetCategory(catId)
if catDef then
local group = catDef.group or "Main"
-- Insert group header if group changed
if group ~= lastGroup then
table.insert(displayList, { type = "header", groupName = group })
lastGroup = group
end
table.insert(displayList, { type = "category", categoryId = catId, categoryDef = catDef })
end
end
return displayList, table.getn(displayList)
end
-- Update the category list display -- Update the category list display
function Guda_SettingsPopup_CategoriesTab_Update() function Guda_SettingsPopup_CategoriesTab_Update()
if not Guda.Modules.CategoryManager then return end if not Guda.Modules.CategoryManager then return end
@@ -1416,11 +1440,10 @@ function Guda_SettingsPopup_CategoriesTab_Update()
local scrollFrame = getglobal("Guda_SettingsPopup_CategoriesScrollFrame") local scrollFrame = getglobal("Guda_SettingsPopup_CategoriesScrollFrame")
if not scrollFrame then return end if not scrollFrame then return end
local categoryOrder = Guda.Modules.CategoryManager:GetCategoryOrder() local displayList, totalEntries = BuildCategoryDisplayList()
local totalCategories = table.getn(categoryOrder)
-- Update scroll frame -- Update scroll frame
FauxScrollFrame_Update(scrollFrame, totalCategories, CATEGORY_VISIBLE_ROWS, CATEGORY_ROW_HEIGHT) FauxScrollFrame_Update(scrollFrame, totalEntries, CATEGORY_VISIBLE_ROWS, CATEGORY_ROW_HEIGHT)
local offset = FauxScrollFrame_GetOffset(scrollFrame) local offset = FauxScrollFrame_GetOffset(scrollFrame)
@@ -1428,13 +1451,28 @@ function Guda_SettingsPopup_CategoriesTab_Update()
local row = GetCategoryRowFrame(i) local row = GetCategoryRowFrame(i)
if row then if row then
local dataIndex = i + offset local dataIndex = i + offset
if dataIndex <= totalCategories then if dataIndex <= totalEntries then
local categoryId = categoryOrder[dataIndex] local entry = displayList[dataIndex]
local categoryDef = Guda.Modules.CategoryManager:GetCategory(categoryId)
if entry.type == "header" then
-- Show as group header row
row.categoryId = nil
row.nameText:SetText("|cffffd100-- " .. entry.groupName .. " --|r")
row.nameText:SetTextColor(1, 0.82, 0)
row.checkbox:Hide()
row.editBtn:Hide()
row.upBtn:Hide()
row.downBtn:Hide()
row.deleteBtn:Hide()
row.builtInText:Hide()
row:Show()
elseif entry.type == "category" then
local categoryId = entry.categoryId
local categoryDef = entry.categoryDef
if categoryDef then
row.categoryId = categoryId row.categoryId = categoryId
row.nameText:SetText(categoryDef.name or categoryId) row.nameText:SetText(categoryDef.name or categoryId)
row.checkbox:Show()
row.checkbox:SetChecked(categoryDef.enabled and 1 or 0) row.checkbox:SetChecked(categoryDef.enabled and 1 or 0)
-- Show/hide delete button based on whether it's built-in -- Show/hide delete button based on whether it's built-in
@@ -1458,24 +1496,17 @@ function Guda_SettingsPopup_CategoriesTab_Update()
row.upBtn:Show() row.upBtn:Show()
row.downBtn:Show() row.downBtn:Show()
-- Enable/disable move buttons based on position -- Enable/disable move buttons based on group-aware boundaries
if dataIndex == 1 then if Guda.Modules.CategoryManager:CanMoveUp(categoryId) then
row.upBtn:Disable() row.upBtn:Enable()
else else
-- Check if category above has hideControls (can't move above it) row.upBtn:Disable()
local aboveCatId = categoryOrder[dataIndex - 1]
local aboveCatDef = Guda.Modules.CategoryManager:GetCategory(aboveCatId)
if aboveCatDef and aboveCatDef.hideControls then
row.upBtn:Disable()
else
row.upBtn:Enable()
end
end end
if dataIndex == totalCategories then if Guda.Modules.CategoryManager:CanMoveDown(categoryId) then
row.downBtn:Disable()
else
row.downBtn:Enable() row.downBtn:Enable()
else
row.downBtn:Disable()
end end
end end
@@ -1512,30 +1543,21 @@ end
function Guda_SettingsPopup_AddCategory_OnClick() function Guda_SettingsPopup_AddCategory_OnClick()
if not Guda.Modules.CategoryManager then return end if not Guda.Modules.CategoryManager then return end
-- Generate unique ID
local baseId = "Custom"
local counter = 1
local newId = baseId .. counter
local cats = Guda.Modules.CategoryManager:GetCategories()
while cats.definitions[newId] do
counter = counter + 1
newId = baseId .. counter
end
-- Create new category definition -- Create new category definition
local newDef = { local newDef = {
name = "Custom " .. counter, name = "Custom Category",
icon = "Interface\\Icons\\INV_Misc_QuestionMark", icon = "Interface\\Icons\\INV_Misc_QuestionMark",
rules = {}, rules = {},
matchMode = "any", matchMode = "any",
priority = 80, priority = 80,
enabled = true, enabled = true,
isBuiltIn = false, isBuiltIn = false,
group = Guda.Modules.CategoryManager:GetGroupMain(),
} }
-- Add to database -- Add to database with auto-generated ID
if Guda.Modules.CategoryManager:AddCategory(newId, newDef) then local success, newId = Guda.Modules.CategoryManager:AddCategory(nil, newDef)
if success and newId then
-- Update display -- Update display
Guda_SettingsPopup_CategoriesTab_Update() Guda_SettingsPopup_CategoriesTab_Update()
-- Open editor for the new category -- Open editor for the new category
@@ -1577,8 +1599,19 @@ end
local editorCategoryId = nil local editorCategoryId = nil
local editorMatchMode = "any" local editorMatchMode = "any"
local editorGroup = "Main"
local editorMark = nil -- current category mark texture path or nil
local editorRules = {} local editorRules = {}
local editorRuleFrames = {} local editorRuleFrames = {}
-- Available mark icons (texture paths)
local MARK_ICONS = {
"Interface\\AddOns\\Guda\\Assets\\equipment",
"Interface\\AddOns\\Guda\\Assets\\plus",
"Interface\\AddOns\\Guda\\Assets\\fav",
"Interface\\AddOns\\Guda\\Assets\\combat",
"Interface\\AddOns\\Guda\\Assets\\Cog",
}
local RULE_ROW_HEIGHT = 28 local RULE_ROW_HEIGHT = 28
local MAX_RULES = 22 local MAX_RULES = 22
@@ -1625,6 +1658,91 @@ function Guda_CategoryEditor_OnLoad(self)
local cancelBtn = getglobal("Guda_CategoryEditor_CancelButton") local cancelBtn = getglobal("Guda_CategoryEditor_CancelButton")
if cancelBtn then cancelBtn:SetText("Cancel") end if cancelBtn then cancelBtn:SetText("Cancel") end
-- Create group EditBox if it doesn't exist
if not getglobal("Guda_CategoryEditor_GroupEditBox") then
local nameBox = getglobal("Guda_CategoryEditor_NameEditBox")
if nameBox then
-- Label
local groupLabel = self:CreateFontString("Guda_CategoryEditor_GroupLabel", "OVERLAY", "GameFontNormalSmall")
groupLabel:SetPoint("LEFT", nameBox, "RIGHT", 14, 0)
groupLabel:SetText("Group:")
groupLabel:SetTextColor(0.7, 0.7, 0.7)
-- EditBox
local groupBox = CreateFrame("EditBox", "Guda_CategoryEditor_GroupEditBox", self, "InputBoxTemplate")
groupBox:SetWidth(100)
groupBox:SetHeight(22)
groupBox:SetPoint("LEFT", groupLabel, "RIGHT", 6, 0)
groupBox:SetAutoFocus(false)
groupBox:SetMaxLetters(20)
groupBox:SetScript("OnEscapePressed", function() this:ClearFocus() end)
groupBox:SetScript("OnEnterPressed", function() this:ClearFocus() end)
end
end
-- Create mark icon selector row
if not getglobal("Guda_CategoryEditor_MarkLabel") then
local MARK_BTN_SIZE = 22
local MARK_BTN_SPACING = 6
local markLabel = self:CreateFontString("Guda_CategoryEditor_MarkLabel", "OVERLAY", "GameFontNormalSmall")
markLabel:SetPoint("TOPLEFT", self, "TOPLEFT", 20, -68)
markLabel:SetText("Mark:")
markLabel:SetTextColor(0.7, 0.7, 0.7)
-- "None" button (first)
local noneBtn = CreateFrame("Button", "Guda_CategoryEditor_MarkNone", self)
noneBtn:SetWidth(MARK_BTN_SIZE)
noneBtn:SetHeight(MARK_BTN_SIZE)
noneBtn:SetPoint("LEFT", markLabel, "RIGHT", 8, 0)
noneBtn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8", edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 })
noneBtn:SetBackdropColor(0.15, 0.15, 0.15, 0.8)
noneBtn:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)
noneBtn.markPath = nil
local noneTex = noneBtn:CreateTexture(nil, "ARTWORK")
noneTex:SetWidth(12)
noneTex:SetHeight(12)
noneTex:SetPoint("CENTER", noneBtn, "CENTER", 0, 0)
noneTex:SetTexture("Interface\\Buttons\\UI-StopButton")
noneTex:SetVertexColor(0.6, 0.6, 0.6)
noneBtn:SetScript("OnClick", function()
editorMark = nil
Guda_CategoryEditor_UpdateMarkButtons()
end)
self.markButtons = { noneBtn }
-- Icon buttons
local prevBtn = noneBtn
for i = 1, table.getn(MARK_ICONS) do
local iconPath = MARK_ICONS[i]
local btn = CreateFrame("Button", "Guda_CategoryEditor_Mark" .. i, self)
btn:SetWidth(MARK_BTN_SIZE)
btn:SetHeight(MARK_BTN_SIZE)
btn:SetPoint("LEFT", prevBtn, "RIGHT", MARK_BTN_SPACING, 0)
btn:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8x8", edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 })
btn:SetBackdropColor(0.15, 0.15, 0.15, 0.8)
btn:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)
btn.markPath = iconPath
local tex = btn:CreateTexture(nil, "ARTWORK")
tex:SetWidth(16)
tex:SetHeight(16)
tex:SetPoint("CENTER", btn, "CENTER", 0, 0)
tex:SetTexture(iconPath)
btn:SetScript("OnClick", function()
editorMark = this.markPath
Guda_CategoryEditor_UpdateMarkButtons()
end)
table.insert(self.markButtons, btn)
prevBtn = btn
end
end
-- Create radio button labels -- Create radio button labels
local anyRadio = getglobal("Guda_CategoryEditor_MatchAny") local anyRadio = getglobal("Guda_CategoryEditor_MatchAny")
if anyRadio then if anyRadio then
@@ -1641,6 +1759,19 @@ function Guda_CategoryEditor_OnLoad(self)
end end
end end
-- Update mark button highlights (gold = selected, gray = unselected)
function Guda_CategoryEditor_UpdateMarkButtons()
local editor = getglobal("Guda_CategoryEditor")
if not editor or not editor.markButtons then return end
for _, btn in ipairs(editor.markButtons) do
if btn.markPath == editorMark then
btn:SetBackdropBorderColor(1, 0.82, 0, 1)
else
btn:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)
end
end
end
-- OnShow for Category Editor -- OnShow for Category Editor
function Guda_CategoryEditor_OnShow(self) function Guda_CategoryEditor_OnShow(self)
Guda_CategoryEditor_UpdateRulesDisplay() Guda_CategoryEditor_UpdateRulesDisplay()
@@ -1655,6 +1786,8 @@ function Guda_CategoryEditor_Open(categoryId)
editorCategoryId = categoryId editorCategoryId = categoryId
editorMatchMode = categoryDef.matchMode or "any" editorMatchMode = categoryDef.matchMode or "any"
editorGroup = categoryDef.group or "Main"
editorMark = categoryDef.categoryMark or nil
-- Copy rules -- Copy rules
editorRules = {} editorRules = {}
@@ -1690,9 +1823,29 @@ function Guda_CategoryEditor_Open(categoryId)
end end
end end
-- Set group EditBox
local groupBox = getglobal("Guda_CategoryEditor_GroupEditBox")
if groupBox then
groupBox:SetText(editorGroup or "")
-- Disable group editing for built-in categories
if categoryDef.isBuiltIn then
groupBox:EnableMouse(false)
groupBox:EnableKeyboard(false)
groupBox:SetTextColor(0.5, 0.5, 0.5)
else
groupBox:EnableMouse(true)
groupBox:EnableKeyboard(true)
groupBox:SetTextColor(1, 1, 1)
end
groupBox:ClearFocus()
end
-- Set match mode -- Set match mode
Guda_CategoryEditor_SetMatchMode(editorMatchMode) Guda_CategoryEditor_SetMatchMode(editorMatchMode)
-- Update mark button highlights
Guda_CategoryEditor_UpdateMarkButtons()
-- Show editor -- Show editor
local editor = getglobal("Guda_CategoryEditor") local editor = getglobal("Guda_CategoryEditor")
if editor then if editor then
@@ -1700,6 +1853,8 @@ function Guda_CategoryEditor_Open(categoryId)
end end
end end
-- (Group is now an EditBox — no dropdown needed)
-- Set match mode (radio buttons) -- Set match mode (radio buttons)
function Guda_CategoryEditor_SetMatchMode(mode) function Guda_CategoryEditor_SetMatchMode(mode)
editorMatchMode = mode editorMatchMode = mode
@@ -2065,6 +2220,9 @@ function Guda_CategoryEditor_Save()
-- Set match mode -- Set match mode
categoryDef.matchMode = editorMatchMode categoryDef.matchMode = editorMatchMode
-- Set category mark
categoryDef.categoryMark = editorMark
-- Set rules -- Set rules
categoryDef.rules = {} categoryDef.rules = {}
for _, rule in ipairs(editorRules) do for _, rule in ipairs(editorRules) do
@@ -2073,8 +2231,29 @@ function Guda_CategoryEditor_Save()
end end
end end
-- Save to database -- Read group from EditBox
Guda.Modules.CategoryManager:UpdateCategory(editorCategoryId, categoryDef) local groupBox = getglobal("Guda_CategoryEditor_GroupEditBox")
local newGroup = "Main" -- default
if groupBox then
local text = groupBox:GetText() or ""
-- Trim whitespace
text = string.gsub(text, "^%s+", "")
text = string.gsub(text, "%s+$", "")
if text ~= "" then
newGroup = text
end
end
-- Set group (handle group change via SetCategoryGroup for proper reordering)
local oldGroup = categoryDef.group or "Main"
if newGroup ~= oldGroup then
-- Save definition first, then move group
Guda.Modules.CategoryManager:UpdateCategory(editorCategoryId, categoryDef)
Guda.Modules.CategoryManager:SetCategoryGroup(editorCategoryId, newGroup)
else
-- Save to database
Guda.Modules.CategoryManager:UpdateCategory(editorCategoryId, categoryDef)
end
-- Refresh displays -- Refresh displays
Guda_SettingsPopup_CategoriesTab_Update() Guda_SettingsPopup_CategoriesTab_Update()
+5 -5
View File
@@ -919,7 +919,7 @@
<!-- Name EditBox --> <!-- Name EditBox -->
<EditBox name="Guda_CategoryEditor_NameEditBox" autoFocus="false" inherits="InputBoxTemplate"> <EditBox name="Guda_CategoryEditor_NameEditBox" autoFocus="false" inherits="InputBoxTemplate">
<Size> <Size>
<AbsDimension x="200" y="20"/> <AbsDimension x="150" y="20"/>
</Size> </Size>
<Anchors> <Anchors>
<Anchor point="TOPLEFT"> <Anchor point="TOPLEFT">
@@ -946,7 +946,7 @@
<Anchors> <Anchors>
<Anchor point="TOPLEFT"> <Anchor point="TOPLEFT">
<Offset> <Offset>
<AbsDimension x="100" y="-77"/> <AbsDimension x="100" y="-97"/>
</Offset> </Offset>
</Anchor> </Anchor>
</Anchors> </Anchors>
@@ -979,12 +979,12 @@
<!-- Rules ScrollFrame --> <!-- Rules ScrollFrame -->
<ScrollFrame name="Guda_CategoryEditor_RulesScrollFrame" inherits="FauxScrollFrameTemplate"> <ScrollFrame name="Guda_CategoryEditor_RulesScrollFrame" inherits="FauxScrollFrameTemplate">
<Size> <Size>
<AbsDimension x="335" y="280"/> <AbsDimension x="335" y="260"/>
</Size> </Size>
<Anchors> <Anchors>
<Anchor point="TOPLEFT"> <Anchor point="TOPLEFT">
<Offset> <Offset>
<AbsDimension x="20" y="-130"/> <AbsDimension x="20" y="-150"/>
</Offset> </Offset>
</Anchor> </Anchor>
</Anchors> </Anchors>
@@ -1013,7 +1013,7 @@
<Anchors> <Anchors>
<Anchor point="TOPRIGHT"> <Anchor point="TOPRIGHT">
<Offset> <Offset>
<AbsDimension x="-20" y="-112"/> <AbsDimension x="-20" y="-132"/>
</Offset> </Offset>
</Anchor> </Anchor>
</Anchors> </Anchors>