feat: update categories and item slots
This commit is contained in:
+566
-20
@@ -52,13 +52,21 @@ end
|
||||
-- texturePattern: Match icon texture path
|
||||
-- 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
|
||||
local DEFAULT_CATEGORIES = {
|
||||
order = {
|
||||
"Home", "BoE", "Weapon", "Armor", "Consumable", "Food", "Drink",
|
||||
"BoE", "Weapon", "Armor", "Consumable", "Food", "Drink",
|
||||
"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 = {
|
||||
["BoE"] = {
|
||||
name = "BoE",
|
||||
@@ -70,6 +78,7 @@ local DEFAULT_CATEGORIES = {
|
||||
priority = 75,
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
group = GROUP_MAIN,
|
||||
},
|
||||
["Weapon"] = {
|
||||
name = "Weapon",
|
||||
@@ -81,6 +90,7 @@ local DEFAULT_CATEGORIES = {
|
||||
priority = 70,
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
group = GROUP_MAIN,
|
||||
},
|
||||
["Armor"] = {
|
||||
name = "Armor",
|
||||
@@ -92,6 +102,7 @@ local DEFAULT_CATEGORIES = {
|
||||
priority = 70,
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
group = GROUP_MAIN,
|
||||
},
|
||||
["Consumable"] = {
|
||||
name = "Consumable",
|
||||
@@ -103,6 +114,7 @@ local DEFAULT_CATEGORIES = {
|
||||
priority = 50,
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
group = GROUP_MAIN,
|
||||
},
|
||||
["Food"] = {
|
||||
name = "Food",
|
||||
@@ -115,6 +127,7 @@ local DEFAULT_CATEGORIES = {
|
||||
priority = 55,
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
group = GROUP_MAIN,
|
||||
},
|
||||
["Drink"] = {
|
||||
name = "Drink",
|
||||
@@ -127,6 +140,7 @@ local DEFAULT_CATEGORIES = {
|
||||
priority = 55,
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
group = GROUP_MAIN,
|
||||
},
|
||||
["Trade Goods"] = {
|
||||
name = "Trade Goods",
|
||||
@@ -138,6 +152,7 @@ local DEFAULT_CATEGORIES = {
|
||||
priority = 40,
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
group = GROUP_MAIN,
|
||||
},
|
||||
["Reagent"] = {
|
||||
name = "Reagent",
|
||||
@@ -149,6 +164,7 @@ local DEFAULT_CATEGORIES = {
|
||||
priority = 40,
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
group = GROUP_MAIN,
|
||||
},
|
||||
["Recipe"] = {
|
||||
name = "Recipe",
|
||||
@@ -160,6 +176,7 @@ local DEFAULT_CATEGORIES = {
|
||||
priority = 40,
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
group = GROUP_MAIN,
|
||||
},
|
||||
["Quiver"] = {
|
||||
name = "Quiver",
|
||||
@@ -171,6 +188,7 @@ local DEFAULT_CATEGORIES = {
|
||||
priority = 40,
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
group = GROUP_MAIN,
|
||||
},
|
||||
["Container"] = {
|
||||
name = "Container",
|
||||
@@ -182,6 +200,7 @@ local DEFAULT_CATEGORIES = {
|
||||
priority = 40,
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
group = GROUP_MAIN,
|
||||
},
|
||||
["Soul Bag"] = {
|
||||
name = "Soul Bag",
|
||||
@@ -193,6 +212,7 @@ local DEFAULT_CATEGORIES = {
|
||||
priority = 45,
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
group = GROUP_MAIN,
|
||||
},
|
||||
["Miscellaneous"] = {
|
||||
name = "Miscellaneous",
|
||||
@@ -203,6 +223,7 @@ local DEFAULT_CATEGORIES = {
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
isFallback = true,
|
||||
group = GROUP_MAIN,
|
||||
},
|
||||
["Quest"] = {
|
||||
name = "Quest",
|
||||
@@ -214,6 +235,7 @@ local DEFAULT_CATEGORIES = {
|
||||
priority = 80,
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
group = GROUP_MAIN,
|
||||
},
|
||||
["Junk"] = {
|
||||
name = "Junk",
|
||||
@@ -225,17 +247,20 @@ local DEFAULT_CATEGORIES = {
|
||||
priority = 85,
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
group = GROUP_MAIN,
|
||||
},
|
||||
["Class Items"] = {
|
||||
name = "Class Items",
|
||||
icon = "Interface\\Icons\\INV_Misc_Ammo_Arrow_01",
|
||||
rules = {
|
||||
{ type = "itemType", value = "Projectile" }
|
||||
{ type = "itemType", value = "Projectile" },
|
||||
{ type = "isSoulShard", value = true }
|
||||
},
|
||||
matchMode = "any",
|
||||
priority = 90,
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
group = GROUP_CLASS,
|
||||
},
|
||||
["Keyring"] = {
|
||||
name = "Keyring",
|
||||
@@ -247,20 +272,56 @@ local DEFAULT_CATEGORIES = {
|
||||
priority = 40,
|
||||
enabled = true,
|
||||
isBuiltIn = true,
|
||||
group = GROUP_CLASS,
|
||||
},
|
||||
["Home"] = {
|
||||
name = "Home",
|
||||
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 = {},
|
||||
matchMode = "all",
|
||||
priority = 0,
|
||||
priority = -10,
|
||||
enabled = true,
|
||||
isBuiltIn = 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
|
||||
local function deepCopy(orig)
|
||||
local copy
|
||||
@@ -404,6 +465,130 @@ function CategoryManager:MigrateCategories()
|
||||
addon:Debug("CategoryManager: Migrated Junk category to use isJunk rule")
|
||||
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
|
||||
|
||||
-- Get all categories
|
||||
@@ -438,20 +623,50 @@ function CategoryManager:SaveCategories(categories)
|
||||
end
|
||||
|
||||
-- Add a new custom category
|
||||
-- If categoryId is nil, auto-generates a unique ID like "Custom_<time>_<random>"
|
||||
function CategoryManager:AddCategory(categoryId, definition)
|
||||
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
|
||||
addon:Debug("CategoryManager: Category already exists: " .. categoryId)
|
||||
return false
|
||||
end
|
||||
|
||||
definition.isBuiltIn = false
|
||||
definition.isBuiltIn = definition.isBuiltIn or false
|
||||
if not definition.group then
|
||||
definition.group = GROUP_MAIN
|
||||
end
|
||||
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)
|
||||
return true
|
||||
return true, categoryId
|
||||
end
|
||||
|
||||
-- Update an existing category
|
||||
@@ -499,32 +714,106 @@ function CategoryManager:DeleteCategory(categoryId)
|
||||
return true
|
||||
end
|
||||
|
||||
-- Move category up in order
|
||||
function CategoryManager:MoveCategoryUp(categoryId)
|
||||
-- Check if a category can move up within its group
|
||||
function CategoryManager:CanMoveUp(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 and i > 1 then
|
||||
cats.order[i] = cats.order[i - 1]
|
||||
cats.order[i - 1] = categoryId
|
||||
self:SaveCategories(cats)
|
||||
return true
|
||||
if id == categoryId then
|
||||
-- Find any previous non-hideControls category (can cross group boundaries)
|
||||
for j = i - 1, 1, -1 do
|
||||
local prevDef = cats.definitions[cats.order[j]]
|
||||
if prevDef and not prevDef.hideControls then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false -- very first movable category
|
||||
end
|
||||
end
|
||||
return false
|
||||
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)
|
||||
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 and i < count then
|
||||
cats.order[i] = cats.order[i + 1]
|
||||
cats.order[i + 1] = categoryId
|
||||
self:SaveCategories(cats)
|
||||
return true
|
||||
if id == categoryId then
|
||||
-- Find next non-hideControls category
|
||||
for j = i + 1, count do
|
||||
local nextDef = cats.definitions[cats.order[j]]
|
||||
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
|
||||
return false
|
||||
@@ -742,6 +1031,26 @@ function CategoryManager:CategorizeItem(itemData, bagID, slotID, isOtherChar)
|
||||
end
|
||||
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()
|
||||
|
||||
-- Debug: show white item categorization
|
||||
@@ -791,6 +1100,243 @@ function CategoryManager:BuildCategoryList()
|
||||
return list
|
||||
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
|
||||
function CategoryManager:GetRuleTypes()
|
||||
return {
|
||||
|
||||
+13
-4
@@ -36,8 +36,8 @@ function DB:Initialize()
|
||||
bankBagColumns = 8,
|
||||
bankColumns = 10,
|
||||
sortMethod = "quality", -- quality, name, type
|
||||
iconSize = 40,
|
||||
iconSpacing = 0,
|
||||
iconSize = 37,
|
||||
iconSpacing = 3,
|
||||
iconFontSize = 12,
|
||||
showQualityBorderEquipment = true,
|
||||
showQualityBorderOther = true,
|
||||
@@ -98,10 +98,10 @@ function DB:Initialize()
|
||||
Guda_CharDB.settings.bankColumns = 10
|
||||
end
|
||||
if not Guda_CharDB.settings.iconSize then
|
||||
Guda_CharDB.settings.iconSize = 40
|
||||
Guda_CharDB.settings.iconSize = 37
|
||||
end
|
||||
if not Guda_CharDB.settings.iconSpacing then
|
||||
Guda_CharDB.settings.iconSpacing = 0
|
||||
Guda_CharDB.settings.iconSpacing = 3
|
||||
end
|
||||
if not Guda_CharDB.settings.iconFontSize then
|
||||
Guda_CharDB.settings.iconFontSize = 12
|
||||
@@ -121,6 +121,15 @@ function DB:Initialize()
|
||||
if Guda_CharDB.settings.markUnusableItems == nil then
|
||||
Guda_CharDB.settings.markUnusableItems = true
|
||||
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
|
||||
if addon.Modules.CategoryManager then
|
||||
|
||||
+2
-2
@@ -53,8 +53,8 @@ addon.Constants = {
|
||||
SAVE_INTERVAL = 1800, -- 30 minutes in seconds
|
||||
|
||||
-- UI Constants
|
||||
BUTTON_SIZE = 40,
|
||||
BUTTON_SPACING = 0,
|
||||
BUTTON_SIZE = 37,
|
||||
BUTTON_SPACING = 3,
|
||||
BUTTONS_PER_ROW = 10,
|
||||
MIN_ICON_SIZE = 30,
|
||||
MAX_ICON_SIZE = 64,
|
||||
|
||||
@@ -27,6 +27,11 @@ function Main:Initialize()
|
||||
addon.Modules.MoneyTracker:Initialize()
|
||||
addon.Modules.EquipmentScanner:Initialize()
|
||||
|
||||
-- Initialize equipment sets (Outfitter/ItemRack integration)
|
||||
if addon.Modules.EquipmentSets then
|
||||
addon.Modules.EquipmentSets:Initialize()
|
||||
end
|
||||
|
||||
-- Initialize UI
|
||||
addon:Print("Initializing UI...")
|
||||
addon.Modules.BagFrame:Initialize()
|
||||
|
||||
@@ -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
|
||||
@@ -24,6 +24,7 @@ Data\BankScanner.lua
|
||||
Data\MailboxScanner.lua
|
||||
Data\MoneyTracker.lua
|
||||
Data\EquipmentScanner.lua
|
||||
Data\EquipmentSets.lua
|
||||
|
||||
Sorting\SortEngine.lua
|
||||
|
||||
|
||||
+167
-110
@@ -996,55 +996,131 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
|
||||
local categoryItemsProcessed = 0
|
||||
local CATEGORY_ITEMS_PER_BUDGET_CHECK = 8
|
||||
|
||||
local totalButtonsCreated = 0
|
||||
for _, catName in ipairs(categoryList) do
|
||||
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)
|
||||
-- Check merged groups setting
|
||||
local mergedGroups = addon.Modules.DB:GetSetting("mergedGroups") or {}
|
||||
|
||||
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
|
||||
-- Build category order index map for sorting within merged groups
|
||||
local catOrderIndex = {}
|
||||
for idx, catId in ipairs(categoryList) do
|
||||
catOrderIndex[catId] = idx
|
||||
end
|
||||
|
||||
-- Check if it fits in current row
|
||||
if currentX > 0 and currentX + blockWidth + 20 > totalWidth + 5 then
|
||||
currentX = 0
|
||||
currentY = currentY + rowMaxHeight
|
||||
rowMaxHeight = 0
|
||||
addon:DebugCategory(" -> wrap to new row, Y=%d", currentY)
|
||||
end
|
||||
-- Build merged group display lists if any groups are merged
|
||||
local mergedDisplayList = {} -- { { name, items, icon, catDef } }
|
||||
local processedCats = {} -- Track which categories were merged
|
||||
|
||||
-- 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 addon.Modules.CategoryManager then
|
||||
local catDef = addon.Modules.CategoryManager:GetCategory(catName)
|
||||
if catDef and catDef.name then
|
||||
displayName = catDef.name
|
||||
if addon.Modules.CategoryManager then
|
||||
local groupsByName = addon.Modules.CategoryManager:GetCategoriesByGroup()
|
||||
for groupName, catIds in pairs(groupsByName) do
|
||||
if mergedGroups[groupName] then
|
||||
-- Merge all items from categories in this group into one section
|
||||
local mergedItems = {}
|
||||
local mergedName = groupName
|
||||
local mergedIcon = nil
|
||||
for _, catId in ipairs(catIds) do
|
||||
local items = categories[catId]
|
||||
if items then
|
||||
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
|
||||
header.fullName = displayName
|
||||
header.isShortened = false
|
||||
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()
|
||||
end
|
||||
end
|
||||
|
||||
local itemY = currentY + 20
|
||||
local col = 0
|
||||
local row = 0
|
||||
-- Track total buttons created across all category blocks
|
||||
local totalButtonsCreated = 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
|
||||
local bagID = item.bagID
|
||||
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)
|
||||
button.inUse = true
|
||||
table.insert(itemButtons, button)
|
||||
-- Populate slot lookup for O(1) access
|
||||
if not slotToButton[bagID] then slotToButton[bagID] = {} end
|
||||
slotToButton[bagID][slot] = button
|
||||
|
||||
@@ -1074,7 +1149,6 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
|
||||
row = row + 1
|
||||
end
|
||||
|
||||
-- Frame budget check
|
||||
categoryItemsProcessed = categoryItemsProcessed + 1
|
||||
if categoryItemsProcessed >= CATEGORY_ITEMS_PER_BUDGET_CHECK then
|
||||
categoryItemsProcessed = 0
|
||||
@@ -1083,25 +1157,50 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
totalButtonsCreated = totalButtonsCreated + numItems
|
||||
if blockHeight > rowMaxHeight then rowMaxHeight = blockHeight end
|
||||
currentX = currentX + blockWidth + 20
|
||||
totalButtonsCreated = totalButtonsCreated + effectiveItems
|
||||
if blockHeight > rowMaxHeight then rowMaxHeight = blockHeight end
|
||||
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
|
||||
addon:DebugCategory("DisplayItemsByCategory: totalButtonsCreated=%d from categories", totalButtonsCreated)
|
||||
|
||||
-- Update Y for bottom sections
|
||||
local y = currentY + rowMaxHeight
|
||||
|
||||
-- Special sections at bottom (Hearthstone, Mount, Tools, Empty)
|
||||
|
||||
-- Special sections at bottom (only Mounts now)
|
||||
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
|
||||
|
||||
@@ -1112,10 +1211,6 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
|
||||
break
|
||||
end
|
||||
end
|
||||
-- Also check if Empty section should show
|
||||
if totalFreeSlots > 0 then
|
||||
hasAnyBottom = true
|
||||
end
|
||||
|
||||
if hasAnyBottom then
|
||||
y = y - 10
|
||||
@@ -1125,22 +1220,7 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
|
||||
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
|
||||
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
|
||||
if blockCols > perRow then blockCols = perRow end
|
||||
local blockRows = math.ceil(numItems / perRow)
|
||||
@@ -1158,61 +1238,38 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
|
||||
header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX, y)
|
||||
header:SetWidth(blockWidth)
|
||||
header.text:SetText(sec.name)
|
||||
if header.countText then header.countText:SetText("(" .. numItems .. ")"); header.countText:Show() end
|
||||
header:Show()
|
||||
|
||||
local itemY = y - 20
|
||||
local sCol = 0
|
||||
local sRow = 0
|
||||
|
||||
if sec.name == "Empty" then
|
||||
local bagID = firstFreeBag or 0
|
||||
local slotID = firstFreeSlot or 1
|
||||
local bagParent = self:GetBagParent(bagID)
|
||||
|
||||
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, itemY)
|
||||
button:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX + (sCol * (buttonSize + spacing)), itemY - (sRow * (buttonSize + spacing)))
|
||||
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
|
||||
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)
|
||||
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
|
||||
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
|
||||
if not slotToButton[item.bagID] then slotToButton[item.bagID] = {} end
|
||||
slotToButton[item.bagID][item.slotID] = button
|
||||
|
||||
sCol = sCol + 1
|
||||
if sCol >= blockCols then
|
||||
sCol = 0
|
||||
sRow = sRow + 1
|
||||
end
|
||||
sCol = sCol + 1
|
||||
if sCol >= blockCols then
|
||||
sCol = 0
|
||||
sRow = sRow + 1
|
||||
end
|
||||
end
|
||||
|
||||
if blockHeight > sectionMaxHeight then sectionMaxHeight = blockHeight end
|
||||
currentBottomX = currentBottomX + blockWidth + 20
|
||||
|
||||
|
||||
if currentBottomX >= totalWidth then
|
||||
currentBottomX = 0
|
||||
y = y - sectionMaxHeight - 5
|
||||
@@ -1220,7 +1277,7 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if currentBottomX > 0 then
|
||||
y = y - sectionMaxHeight
|
||||
end
|
||||
|
||||
+38
-63
@@ -779,6 +779,13 @@ function BankFrame:DisplayItemsByCategory(bankData, isOtherChar, charName)
|
||||
local CATEGORY_ITEMS_PER_BUDGET_CHECK = 8
|
||||
|
||||
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 numItems = items and table.getn(items) or 0
|
||||
if numItems > 0 then
|
||||
@@ -803,9 +810,19 @@ function BankFrame:DisplayItemsByCategory(bankData, isOtherChar, charName)
|
||||
headerIdx = headerIdx + 1
|
||||
header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", startX + currentX, startY - currentY)
|
||||
header:SetWidth(blockWidth)
|
||||
|
||||
|
||||
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
|
||||
if string.len(displayName) > 10 and numItems < 2 then
|
||||
displayName = string.sub(displayName, 1, 7) .. "..."
|
||||
@@ -865,22 +882,20 @@ function BankFrame:DisplayItemsByCategory(bankData, isOtherChar, charName)
|
||||
if blockHeight > rowMaxHeight then rowMaxHeight = blockHeight end
|
||||
currentX = currentX + blockWidth + 20
|
||||
end
|
||||
end -- isEmptyCategory else
|
||||
end
|
||||
|
||||
-- Update Y for bottom sections
|
||||
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 = {
|
||||
{ name = "Home", items = specialItems.Hearthstone },
|
||||
{ name = "Mounts", items = specialItems.Mount },
|
||||
{ name = "Tools", items = specialItems.Tools },
|
||||
{ name = "Empty", items = {} }
|
||||
}
|
||||
|
||||
|
||||
local x = startX
|
||||
y = startY - y
|
||||
|
||||
|
||||
local hasAnyBottom = false
|
||||
for _, sec in ipairs(bottomSections) do
|
||||
if table.getn(sec.items) > 0 then
|
||||
@@ -888,10 +903,6 @@ function BankFrame:DisplayItemsByCategory(bankData, isOtherChar, charName)
|
||||
break
|
||||
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
|
||||
y = y - 10
|
||||
@@ -901,22 +912,7 @@ function BankFrame:DisplayItemsByCategory(bankData, isOtherChar, charName)
|
||||
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
|
||||
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
|
||||
if blockCols > perRow then blockCols = perRow end
|
||||
local blockRows = math.ceil(numItems / perRow)
|
||||
@@ -934,59 +930,38 @@ function BankFrame:DisplayItemsByCategory(bankData, isOtherChar, charName)
|
||||
header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX, y)
|
||||
header:SetWidth(blockWidth)
|
||||
header.text:SetText(sec.name)
|
||||
if header.countText then header.countText:SetText("(" .. numItems .. ")"); header.countText:Show() end
|
||||
header:Show()
|
||||
|
||||
local itemY = y - 20
|
||||
local sCol = 0
|
||||
local sRow = 0
|
||||
|
||||
if sec.name == "Empty" then
|
||||
local bagID = firstFreeBag or -1
|
||||
local slotID = firstFreeSlot or 1
|
||||
local bagParent = self:GetBagParent(bagID)
|
||||
|
||||
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, itemY)
|
||||
button:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", x + currentBottomX + (sCol * (buttonSize + spacing)), itemY - (sRow * (buttonSize + spacing)))
|
||||
button:Show()
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
-- Populate slot lookup for O(1) access
|
||||
if not bankSlotToButton[item.bagID] then bankSlotToButton[item.bagID] = {} end
|
||||
bankSlotToButton[item.bagID][item.slotID] = button
|
||||
-- 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
|
||||
if sCol >= blockCols then
|
||||
sCol = 0
|
||||
sRow = sRow + 1
|
||||
end
|
||||
sCol = sCol + 1
|
||||
if sCol >= blockCols then
|
||||
sCol = 0
|
||||
sRow = sRow + 1
|
||||
end
|
||||
end
|
||||
|
||||
if blockHeight > sectionMaxHeight then sectionMaxHeight = blockHeight end
|
||||
currentBottomX = currentBottomX + blockWidth + 20
|
||||
|
||||
|
||||
if currentBottomX >= totalWidth then
|
||||
currentBottomX = 0
|
||||
y = y - sectionMaxHeight - 5
|
||||
|
||||
+21
-36
@@ -21,7 +21,6 @@ end
|
||||
-- Returns nothing, modifies tables in place
|
||||
function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, isOtherChar)
|
||||
local itemName = itemData.name or ""
|
||||
local itemType = itemData.type or ""
|
||||
local cat = "Miscellaneous"
|
||||
|
||||
-- Detect consumable restore/eat/drink tag for current character only
|
||||
@@ -32,38 +31,11 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems,
|
||||
end
|
||||
end
|
||||
|
||||
-- Priority 1: Special items (Hearthstone, Mounts, Tools)
|
||||
-- These are handled separately and go into specialItems table, not categories
|
||||
if string.find(itemName, "Hearthstone") 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
|
||||
-- Special items: only Mounts are handled separately now
|
||||
-- Home and Tools are real categories handled by CategoryManager rules
|
||||
if addon.Modules.SortEngine and addon.Modules.SortEngine.IsMount and addon.Modules.SortEngine.IsMount(itemData.texture) then
|
||||
table.insert(specialItems.Mount, {bagID = bagID, slotID = slotID, itemData = itemData})
|
||||
return
|
||||
elseif string.find(itemName, "Runed .* Rod") or
|
||||
itemType == "Fishing Pole" or
|
||||
string.find(itemName, "Mining Pick") or
|
||||
string.find(itemName, "Blacksmith Hammer") or
|
||||
itemName == "Arclight Spanner" or
|
||||
itemName == "Gyromatic Micro-Adjustor" or
|
||||
itemName == "Philosopher's Stone" or
|
||||
string.find(itemName, "Skinning Knife") or
|
||||
itemName == "Blood Scythe" or
|
||||
string.find(itemName, "Jeweler") or
|
||||
string.find(itemName, "Jewelry Kit") then
|
||||
table.insert(specialItems.Tools, {bagID = bagID, slotID = slotID, itemData = itemData})
|
||||
return
|
||||
end
|
||||
|
||||
-- Use CategoryManager rule engine if available, otherwise fall back to legacy logic
|
||||
@@ -218,22 +190,20 @@ function Guda_InitCategories()
|
||||
end
|
||||
|
||||
-- Reuse or create specialItems table
|
||||
-- Only Mount remains as a special item; Home and Tools are now real categories
|
||||
if not specialItemsCache then
|
||||
specialItemsCache = {
|
||||
Hearthstone = {},
|
||||
Mount = {},
|
||||
Tools = {}
|
||||
}
|
||||
else
|
||||
WipeTable(specialItemsCache.Hearthstone)
|
||||
WipeTable(specialItemsCache.Mount)
|
||||
WipeTable(specialItemsCache.Tools)
|
||||
end
|
||||
|
||||
return categoriesCache, specialItemsCache
|
||||
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)
|
||||
if not items then return end
|
||||
table.sort(items, function(a, b)
|
||||
@@ -243,6 +213,13 @@ function Guda_SortCategoryItems(items)
|
||||
if not a.itemData then return false 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
|
||||
local function tgRank(d)
|
||||
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:SetHeight(20)
|
||||
header:EnableMouse(true)
|
||||
|
||||
-- Category name text
|
||||
local text = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
||||
text:SetPoint("LEFT", header, "LEFT", 0, 0)
|
||||
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()
|
||||
if this.fullName and this.isShortened then
|
||||
GameTooltip:SetOwner(this, "ANCHOR_TOP")
|
||||
|
||||
+64
-39
@@ -639,6 +639,8 @@ local function ResetButtonVisualState(self)
|
||||
HideInnerShadow(self.innerShadow)
|
||||
if self.unusableOverlay then self.unusableOverlay:Hide() end
|
||||
HideJunkIcon(self)
|
||||
if self.categoryMarkIcon then self.categoryMarkIcon:Hide() end
|
||||
if self.categoryMarkShadow then self.categoryMarkShadow:Hide() end
|
||||
|
||||
-- Clear cooldown overlay
|
||||
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
|
||||
|
||||
emptySlotBg:ClearAllPoints()
|
||||
-- Use smaller padding for small icons
|
||||
local bgPadding = iconSize < 44 and 1 or 2
|
||||
emptySlotBg:SetPoint("TOPLEFT", self, "TOPLEFT", -bgPadding, bgPadding)
|
||||
emptySlotBg:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", bgPadding, -bgPadding)
|
||||
emptySlotBg:SetTexCoord(0.1, 0.9, 0.1, 0.9)
|
||||
emptySlotBg:SetPoint("TOPLEFT", self, "TOPLEFT", 0, 0)
|
||||
emptySlotBg:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", 0, 0)
|
||||
emptySlotBg:SetTexCoord(0.17, 0.83, 0.17, 0.83)
|
||||
end
|
||||
|
||||
-- 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
|
||||
|
||||
-- Calculate icon inset based on size
|
||||
local iconInset = iconSize < 44 and 10 or 15
|
||||
local iconDisplaySize = iconSize - iconInset
|
||||
-- Icon fills the full button slot
|
||||
local iconDisplaySize = iconSize
|
||||
|
||||
iconTexture:ClearAllPoints()
|
||||
iconTexture:SetPoint("CENTER", self, "CENTER", -0.5, 0.5)
|
||||
iconTexture:SetPoint("CENTER", self, "CENTER", 0, 0)
|
||||
iconTexture:SetWidth(iconDisplaySize)
|
||||
iconTexture:SetHeight(iconDisplaySize)
|
||||
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
|
||||
if self.qualityBorder then
|
||||
self.qualityBorder:ClearAllPoints()
|
||||
self.qualityBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -5, 5)
|
||||
self.qualityBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 5, -5)
|
||||
self.qualityBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -1, 1)
|
||||
self.qualityBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 1, -1)
|
||||
end
|
||||
|
||||
-- Position quest border around the icon
|
||||
if self.questBorder then
|
||||
self.questBorder:ClearAllPoints()
|
||||
self.questBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -5, 5)
|
||||
self.questBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 5, -5)
|
||||
self.questBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -1, 1)
|
||||
self.questBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 1, -1)
|
||||
end
|
||||
|
||||
-- Position quest icon in top-right corner
|
||||
@@ -1056,15 +1055,12 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
|
||||
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
|
||||
emptySlotBg:ClearAllPoints()
|
||||
-- Use smaller padding for small icons
|
||||
local bgPadding = iconSize < 44 and 1 or 2
|
||||
emptySlotBg:SetPoint("TOPLEFT", self, "TOPLEFT", -bgPadding, bgPadding)
|
||||
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)
|
||||
emptySlotBg:SetPoint("TOPLEFT", self, "TOPLEFT", 0, 0)
|
||||
emptySlotBg:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", 0, 0)
|
||||
emptySlotBg:SetTexCoord(0.17, 0.83, 0.17, 0.83)
|
||||
end
|
||||
|
||||
-- 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
|
||||
|
||||
-- Check if item is junk using the CategoryManager
|
||||
-- Check if item is junk and get category mark using the CategoryManager
|
||||
local isJunk = false
|
||||
local categoryMarkTexture = nil
|
||||
if itemData and addon.Modules.CategoryManager then
|
||||
local category = addon.Modules.CategoryManager:CategorizeItem(itemData, bagID, slotID, self.otherChar)
|
||||
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
|
||||
|
||||
-- Search filtering and junk opacity
|
||||
@@ -1410,37 +1444,28 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
|
||||
|
||||
if iconTexture then
|
||||
if self.hasItem then
|
||||
-- Scale icon proportionally based on button size
|
||||
-- For icons < 44: use smaller inset (4px) for better fit
|
||||
-- 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
|
||||
-- Icon fills the full button slot
|
||||
local iconDisplaySize = iconSize
|
||||
iconTexture:ClearAllPoints()
|
||||
iconTexture:SetPoint("CENTER", self, "CENTER", -0.5, 0.5)
|
||||
iconTexture:SetPoint("CENTER", self, "CENTER", 0, 0)
|
||||
iconTexture:SetWidth(iconDisplaySize)
|
||||
iconTexture:SetHeight(iconDisplaySize)
|
||||
-- Crop icon edges slightly
|
||||
iconTexture:SetTexCoord(0.08, 0.92, 0.08, 0.92)
|
||||
iconTexture:Show()
|
||||
|
||||
-- Position quality border around the icon (not the slot)
|
||||
-- Position quality border around the icon
|
||||
if self.qualityBorder then
|
||||
self.qualityBorder:ClearAllPoints()
|
||||
self.qualityBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -5, 5)
|
||||
self.qualityBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 5, -5)
|
||||
self.qualityBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -1, 1)
|
||||
self.qualityBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 1, -1)
|
||||
end
|
||||
|
||||
-- Position quest border around the icon (same as quality border)
|
||||
-- Position quest border around the icon
|
||||
if self.questBorder then
|
||||
self.questBorder:ClearAllPoints()
|
||||
self.questBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -5, 5)
|
||||
self.questBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 5, -5)
|
||||
self.questBorder:SetPoint("TOPLEFT", iconTexture, "TOPLEFT", -1, 1)
|
||||
self.questBorder:SetPoint("BOTTOMRIGHT", iconTexture, "BOTTOMRIGHT", 1, -1)
|
||||
end
|
||||
|
||||
-- Position quest icon in top-right corner
|
||||
|
||||
+222
-43
@@ -92,9 +92,9 @@ function Guda_SettingsPopup_OnShow(self)
|
||||
-- Load current settings
|
||||
local bagColumns = Guda.Modules.DB:GetSetting("bagColumns") 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 iconSpacing = Guda.Modules.DB:GetSetting("iconSpacing") or 0
|
||||
local iconSpacing = Guda.Modules.DB:GetSetting("iconSpacing") or 3
|
||||
local lockBags = Guda.Modules.DB:GetSetting("lockBags")
|
||||
if lockBags == nil then
|
||||
lockBags = false
|
||||
@@ -438,7 +438,7 @@ function Guda_SettingsPopup_IconSizeSlider_OnLoad(self)
|
||||
self:SetMinMaxValues(22, 64)
|
||||
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)
|
||||
end
|
||||
|
||||
@@ -503,7 +503,7 @@ end
|
||||
|
||||
-- Icon Spacing Slider OnLoad
|
||||
function Guda_SettingsPopup_IconSpacingSlider_OnLoad(self)
|
||||
getglobal(self:GetName().."Low"):SetText("-10px")
|
||||
getglobal(self:GetName().."Low"):SetText("0px")
|
||||
getglobal(self:GetName().."High"):SetText("20px")
|
||||
|
||||
local text = getglobal(self:GetName().."Text")
|
||||
@@ -515,10 +515,10 @@ function Guda_SettingsPopup_IconSpacingSlider_OnLoad(self)
|
||||
text:SetFont(font, 12, flags)
|
||||
end
|
||||
|
||||
self:SetMinMaxValues(-10, 20)
|
||||
self:SetMinMaxValues(0, 20)
|
||||
self:SetValueStep(1)
|
||||
|
||||
local currentValue = Guda.Modules.DB:GetSetting("iconSpacing") or 0
|
||||
local currentValue = Guda.Modules.DB:GetSetting("iconSpacing") or 3
|
||||
self:SetValue(currentValue)
|
||||
end
|
||||
|
||||
@@ -1409,6 +1409,30 @@ local function GetCategoryRowFrame(index)
|
||||
return row
|
||||
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
|
||||
function Guda_SettingsPopup_CategoriesTab_Update()
|
||||
if not Guda.Modules.CategoryManager then return end
|
||||
@@ -1416,11 +1440,10 @@ function Guda_SettingsPopup_CategoriesTab_Update()
|
||||
local scrollFrame = getglobal("Guda_SettingsPopup_CategoriesScrollFrame")
|
||||
if not scrollFrame then return end
|
||||
|
||||
local categoryOrder = Guda.Modules.CategoryManager:GetCategoryOrder()
|
||||
local totalCategories = table.getn(categoryOrder)
|
||||
local displayList, totalEntries = BuildCategoryDisplayList()
|
||||
|
||||
-- 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)
|
||||
|
||||
@@ -1428,13 +1451,28 @@ function Guda_SettingsPopup_CategoriesTab_Update()
|
||||
local row = GetCategoryRowFrame(i)
|
||||
if row then
|
||||
local dataIndex = i + offset
|
||||
if dataIndex <= totalCategories then
|
||||
local categoryId = categoryOrder[dataIndex]
|
||||
local categoryDef = Guda.Modules.CategoryManager:GetCategory(categoryId)
|
||||
if dataIndex <= totalEntries then
|
||||
local entry = displayList[dataIndex]
|
||||
|
||||
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.nameText:SetText(categoryDef.name or categoryId)
|
||||
row.checkbox:Show()
|
||||
row.checkbox:SetChecked(categoryDef.enabled and 1 or 0)
|
||||
|
||||
-- Show/hide delete button based on whether it's built-in
|
||||
@@ -1458,24 +1496,17 @@ function Guda_SettingsPopup_CategoriesTab_Update()
|
||||
row.upBtn:Show()
|
||||
row.downBtn:Show()
|
||||
|
||||
-- Enable/disable move buttons based on position
|
||||
if dataIndex == 1 then
|
||||
row.upBtn:Disable()
|
||||
-- Enable/disable move buttons based on group-aware boundaries
|
||||
if Guda.Modules.CategoryManager:CanMoveUp(categoryId) then
|
||||
row.upBtn:Enable()
|
||||
else
|
||||
-- Check if category above has hideControls (can't move above it)
|
||||
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
|
||||
row.upBtn:Disable()
|
||||
end
|
||||
|
||||
if dataIndex == totalCategories then
|
||||
row.downBtn:Disable()
|
||||
else
|
||||
if Guda.Modules.CategoryManager:CanMoveDown(categoryId) then
|
||||
row.downBtn:Enable()
|
||||
else
|
||||
row.downBtn:Disable()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1512,30 +1543,21 @@ end
|
||||
function Guda_SettingsPopup_AddCategory_OnClick()
|
||||
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
|
||||
local newDef = {
|
||||
name = "Custom " .. counter,
|
||||
name = "Custom Category",
|
||||
icon = "Interface\\Icons\\INV_Misc_QuestionMark",
|
||||
rules = {},
|
||||
matchMode = "any",
|
||||
priority = 80,
|
||||
enabled = true,
|
||||
isBuiltIn = false,
|
||||
group = Guda.Modules.CategoryManager:GetGroupMain(),
|
||||
}
|
||||
|
||||
-- Add to database
|
||||
if Guda.Modules.CategoryManager:AddCategory(newId, newDef) then
|
||||
-- Add to database with auto-generated ID
|
||||
local success, newId = Guda.Modules.CategoryManager:AddCategory(nil, newDef)
|
||||
if success and newId then
|
||||
-- Update display
|
||||
Guda_SettingsPopup_CategoriesTab_Update()
|
||||
-- Open editor for the new category
|
||||
@@ -1577,8 +1599,19 @@ end
|
||||
|
||||
local editorCategoryId = nil
|
||||
local editorMatchMode = "any"
|
||||
local editorGroup = "Main"
|
||||
local editorMark = nil -- current category mark texture path or nil
|
||||
local editorRules = {}
|
||||
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 MAX_RULES = 22
|
||||
|
||||
@@ -1625,6 +1658,91 @@ function Guda_CategoryEditor_OnLoad(self)
|
||||
local cancelBtn = getglobal("Guda_CategoryEditor_CancelButton")
|
||||
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
|
||||
local anyRadio = getglobal("Guda_CategoryEditor_MatchAny")
|
||||
if anyRadio then
|
||||
@@ -1641,6 +1759,19 @@ function Guda_CategoryEditor_OnLoad(self)
|
||||
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
|
||||
function Guda_CategoryEditor_OnShow(self)
|
||||
Guda_CategoryEditor_UpdateRulesDisplay()
|
||||
@@ -1655,6 +1786,8 @@ function Guda_CategoryEditor_Open(categoryId)
|
||||
|
||||
editorCategoryId = categoryId
|
||||
editorMatchMode = categoryDef.matchMode or "any"
|
||||
editorGroup = categoryDef.group or "Main"
|
||||
editorMark = categoryDef.categoryMark or nil
|
||||
|
||||
-- Copy rules
|
||||
editorRules = {}
|
||||
@@ -1690,9 +1823,29 @@ function Guda_CategoryEditor_Open(categoryId)
|
||||
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
|
||||
Guda_CategoryEditor_SetMatchMode(editorMatchMode)
|
||||
|
||||
-- Update mark button highlights
|
||||
Guda_CategoryEditor_UpdateMarkButtons()
|
||||
|
||||
-- Show editor
|
||||
local editor = getglobal("Guda_CategoryEditor")
|
||||
if editor then
|
||||
@@ -1700,6 +1853,8 @@ function Guda_CategoryEditor_Open(categoryId)
|
||||
end
|
||||
end
|
||||
|
||||
-- (Group is now an EditBox — no dropdown needed)
|
||||
|
||||
-- Set match mode (radio buttons)
|
||||
function Guda_CategoryEditor_SetMatchMode(mode)
|
||||
editorMatchMode = mode
|
||||
@@ -2065,6 +2220,9 @@ function Guda_CategoryEditor_Save()
|
||||
-- Set match mode
|
||||
categoryDef.matchMode = editorMatchMode
|
||||
|
||||
-- Set category mark
|
||||
categoryDef.categoryMark = editorMark
|
||||
|
||||
-- Set rules
|
||||
categoryDef.rules = {}
|
||||
for _, rule in ipairs(editorRules) do
|
||||
@@ -2073,8 +2231,29 @@ function Guda_CategoryEditor_Save()
|
||||
end
|
||||
end
|
||||
|
||||
-- Save to database
|
||||
Guda.Modules.CategoryManager:UpdateCategory(editorCategoryId, categoryDef)
|
||||
-- Read group from EditBox
|
||||
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
|
||||
Guda_SettingsPopup_CategoriesTab_Update()
|
||||
|
||||
@@ -919,7 +919,7 @@
|
||||
<!-- Name EditBox -->
|
||||
<EditBox name="Guda_CategoryEditor_NameEditBox" autoFocus="false" inherits="InputBoxTemplate">
|
||||
<Size>
|
||||
<AbsDimension x="200" y="20"/>
|
||||
<AbsDimension x="150" y="20"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
@@ -946,7 +946,7 @@
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset>
|
||||
<AbsDimension x="100" y="-77"/>
|
||||
<AbsDimension x="100" y="-97"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
@@ -979,12 +979,12 @@
|
||||
<!-- Rules ScrollFrame -->
|
||||
<ScrollFrame name="Guda_CategoryEditor_RulesScrollFrame" inherits="FauxScrollFrameTemplate">
|
||||
<Size>
|
||||
<AbsDimension x="335" y="280"/>
|
||||
<AbsDimension x="335" y="260"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset>
|
||||
<AbsDimension x="20" y="-130"/>
|
||||
<AbsDimension x="20" y="-150"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
@@ -1013,7 +1013,7 @@
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT">
|
||||
<Offset>
|
||||
<AbsDimension x="-20" y="-112"/>
|
||||
<AbsDimension x="-20" y="-132"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
|
||||
Reference in New Issue
Block a user