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
-- 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
View File
@@ -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
View File
@@ -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,
+5
View File
@@ -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()
+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\MoneyTracker.lua
Data\EquipmentScanner.lua
Data\EquipmentSets.lua
Sorting\SortEngine.lua
+167 -110
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+5 -5
View File
@@ -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>