diff --git a/Core/CategoryManager.lua b/Core/CategoryManager.lua new file mode 100644 index 0000000..f43ce18 --- /dev/null +++ b/Core/CategoryManager.lua @@ -0,0 +1,686 @@ +-- Guda Category Manager +-- Handles custom category definitions and rule-based item categorization + +local addon = Guda + +local CategoryManager = {} +addon.Modules.CategoryManager = CategoryManager + +-- Rule Types: +-- itemType: Match by GetItemInfo type (Armor, Weapon, Consumable, etc.) +-- itemSubtype: Match by subtype (Cloth, Potion, Herb, etc.) +-- namePattern: Lua pattern match on item name +-- quality: Match by quality level (0=Gray, 1=White, 2=Green, 3=Blue, 4=Purple, 5=Orange) +-- isBoE: Boolean for Bind on Equip items +-- isQuestItem: Boolean for quest items +-- texturePattern: Match icon texture path +-- itemID: Specific item IDs (table of IDs) + +-- Default category definitions that replicate the existing hardcoded behavior +local DEFAULT_CATEGORIES = { + order = { + "BoE", "Weapon", "Armor", "Consumable", "Food", "Drink", + "Trade Goods", "Reagent", "Recipe", "Quiver", "Container", + "Soul Bag", "Miscellaneous", "Quest", "Junk", "Class Items", "Keyring" + }, + definitions = { + ["BoE"] = { + name = "BoE", + icon = "Interface\\Icons\\INV_Misc_Orb_01", + rules = { + { type = "isBoE", value = true } + }, + matchMode = "all", + priority = 60, + enabled = true, + isBuiltIn = true, + }, + ["Weapon"] = { + name = "Weapon", + icon = "Interface\\Icons\\INV_Sword_04", + rules = { + { type = "itemType", value = "Weapon" } + }, + matchMode = "all", + priority = 70, + enabled = true, + isBuiltIn = true, + }, + ["Armor"] = { + name = "Armor", + icon = "Interface\\Icons\\INV_Chest_Chain", + rules = { + { type = "itemType", value = "Armor" } + }, + matchMode = "all", + priority = 70, + enabled = true, + isBuiltIn = true, + }, + ["Consumable"] = { + name = "Consumable", + icon = "Interface\\Icons\\INV_Potion_54", + rules = { + { type = "itemType", value = "Consumable" } + }, + matchMode = "all", + priority = 50, + enabled = true, + isBuiltIn = true, + }, + ["Food"] = { + name = "Food", + icon = "Interface\\Icons\\INV_Misc_Food_14", + rules = { + { type = "itemType", value = "Consumable" }, + { type = "restoreTag", value = "eat" } + }, + matchMode = "all", + priority = 55, + enabled = true, + isBuiltIn = true, + }, + ["Drink"] = { + name = "Drink", + icon = "Interface\\Icons\\INV_Drink_07", + rules = { + { type = "itemType", value = "Consumable" }, + { type = "restoreTag", value = "drink" } + }, + matchMode = "all", + priority = 55, + enabled = true, + isBuiltIn = true, + }, + ["Trade Goods"] = { + name = "Trade Goods", + icon = "Interface\\Icons\\INV_Fabric_Silk_02", + rules = { + { type = "itemType", value = "Trade Goods" } + }, + matchMode = "all", + priority = 40, + enabled = true, + isBuiltIn = true, + }, + ["Reagent"] = { + name = "Reagent", + icon = "Interface\\Icons\\INV_Misc_Dust_02", + rules = { + { type = "itemType", value = "Reagent" } + }, + matchMode = "all", + priority = 40, + enabled = true, + isBuiltIn = true, + }, + ["Recipe"] = { + name = "Recipe", + icon = "Interface\\Icons\\INV_Scroll_03", + rules = { + { type = "itemType", value = "Recipe" } + }, + matchMode = "all", + priority = 40, + enabled = true, + isBuiltIn = true, + }, + ["Quiver"] = { + name = "Quiver", + icon = "Interface\\Icons\\INV_Misc_Quiver_03", + rules = { + { type = "itemType", value = "Quiver" } + }, + matchMode = "all", + priority = 40, + enabled = true, + isBuiltIn = true, + }, + ["Container"] = { + name = "Container", + icon = "Interface\\Icons\\INV_Misc_Bag_07", + rules = { + { type = "itemType", value = "Container" } + }, + matchMode = "all", + priority = 40, + enabled = true, + isBuiltIn = true, + }, + ["Soul Bag"] = { + name = "Soul Bag", + icon = "Interface\\Icons\\INV_Misc_Bag_EnchantedMageweave", + rules = { + { type = "itemSubtype", value = "Soul Bag" } + }, + matchMode = "all", + priority = 45, + enabled = true, + isBuiltIn = true, + }, + ["Miscellaneous"] = { + name = "Miscellaneous", + icon = "Interface\\Icons\\INV_Misc_Rune_01", + rules = {}, + matchMode = "any", + priority = 0, + enabled = true, + isBuiltIn = true, + isFallback = true, + }, + ["Quest"] = { + name = "Quest", + icon = "Interface\\Icons\\INV_Misc_Book_08", + rules = { + { type = "isQuestItem", value = true } + }, + matchMode = "all", + priority = 80, + enabled = true, + isBuiltIn = true, + }, + ["Junk"] = { + name = "Junk", + icon = "Interface\\Icons\\INV_Misc_Gear_06", + rules = { + { type = "quality", value = 0 } + }, + matchMode = "all", + priority = 85, + enabled = true, + isBuiltIn = true, + }, + ["Class Items"] = { + name = "Class Items", + icon = "Interface\\Icons\\INV_Misc_Ammo_Arrow_01", + rules = { + { type = "itemType", value = "Projectile" } + }, + matchMode = "any", + priority = 90, + enabled = true, + isBuiltIn = true, + }, + ["Keyring"] = { + name = "Keyring", + icon = "Interface\\Icons\\INV_Misc_Key_04", + rules = { + { type = "itemType", value = "Key" } + }, + matchMode = "all", + priority = 40, + enabled = true, + isBuiltIn = true, + }, + } +} + +-- Deep copy a table +local function deepCopy(orig) + local copy + if type(orig) == "table" then + copy = {} + for k, v in pairs(orig) do + copy[k] = deepCopy(v) + end + else + copy = orig + end + return copy +end + +-- Get default categories (returns a deep copy) +function CategoryManager:GetDefaultCategories() + return deepCopy(DEFAULT_CATEGORIES) +end + +-- Initialize categories from database or defaults +function CategoryManager:Initialize() + if not Guda_CharDB then return end + + if not Guda_CharDB.categories then + Guda_CharDB.categories = self:GetDefaultCategories() + addon:Debug("CategoryManager: Initialized with default categories") + else + -- Ensure all built-in categories exist (migration support) + self:MigrateCategories() + end +end + +-- Migrate/update categories to ensure all built-ins exist +function CategoryManager:MigrateCategories() + local cats = Guda_CharDB.categories + if not cats then return end + + -- Ensure definitions table exists + if not cats.definitions then + cats.definitions = {} + end + + -- Ensure order table exists + if not cats.order then + cats.order = {} + end + + -- Add any missing built-in categories + for id, def in pairs(DEFAULT_CATEGORIES.definitions) do + if not cats.definitions[id] then + cats.definitions[id] = deepCopy(def) + -- Add to end of order if not present + local found = false + for _, orderId in ipairs(cats.order) do + if orderId == id then + found = true + break + end + end + if not found then + table.insert(cats.order, id) + end + addon:Debug("CategoryManager: Added missing built-in category: " .. id) + end + end + + -- Migrate Food/Drink categories to use restoreTag instead of itemSubtype + -- This fixes the issue where subtype "Food & Drink" matched both categories + local foodCat = cats.definitions["Food"] + if foodCat and foodCat.isBuiltIn then + local needsUpdate = false + if foodCat.rules then + for _, rule in ipairs(foodCat.rules) do + if rule.type == "itemSubtype" then + needsUpdate = true + break + end + end + end + if needsUpdate then + foodCat.rules = { + { type = "itemType", value = "Consumable" }, + { type = "restoreTag", value = "eat" } + } + addon:Debug("CategoryManager: Migrated Food category to use restoreTag") + end + end + + local drinkCat = cats.definitions["Drink"] + if drinkCat and drinkCat.isBuiltIn then + local needsUpdate = false + if drinkCat.rules then + for _, rule in ipairs(drinkCat.rules) do + if rule.type == "itemSubtype" then + needsUpdate = true + break + end + end + end + if needsUpdate then + drinkCat.rules = { + { type = "itemType", value = "Consumable" }, + { type = "restoreTag", value = "drink" } + } + addon:Debug("CategoryManager: Migrated Drink category to use restoreTag") + end + end +end + +-- Get all categories +function CategoryManager:GetCategories() + if not Guda_CharDB or not Guda_CharDB.categories then + return self:GetDefaultCategories() + end + return Guda_CharDB.categories +end + +-- Get category order +function CategoryManager:GetCategoryOrder() + local cats = self:GetCategories() + return cats.order or {} +end + +-- Get category definition by ID +function CategoryManager:GetCategory(categoryId) + local cats = self:GetCategories() + if cats.definitions then + return cats.definitions[categoryId] + end + return nil +end + +-- Save categories to database +function CategoryManager:SaveCategories(categories) + if not Guda_CharDB then return end + Guda_CharDB.categories = categories +end + +-- Add a new custom category +function CategoryManager:AddCategory(categoryId, definition) + local cats = self:GetCategories() + + if cats.definitions[categoryId] then + addon:Debug("CategoryManager: Category already exists: " .. categoryId) + return false + end + + definition.isBuiltIn = false + cats.definitions[categoryId] = definition + table.insert(cats.order, categoryId) + + self:SaveCategories(cats) + return true +end + +-- Update an existing category +function CategoryManager:UpdateCategory(categoryId, definition) + local cats = self:GetCategories() + + if not cats.definitions[categoryId] then + addon:Debug("CategoryManager: Category not found: " .. categoryId) + return false + end + + -- Preserve isBuiltIn flag + definition.isBuiltIn = cats.definitions[categoryId].isBuiltIn + cats.definitions[categoryId] = definition + + self:SaveCategories(cats) + return true +end + +-- Delete a category (only custom categories can be deleted) +function CategoryManager:DeleteCategory(categoryId) + local cats = self:GetCategories() + local def = cats.definitions[categoryId] + + if not def then + return false + end + + if def.isBuiltIn then + addon:Debug("CategoryManager: Cannot delete built-in category: " .. categoryId) + return false + end + + cats.definitions[categoryId] = nil + + -- Remove from order + for i, id in ipairs(cats.order) do + if id == categoryId then + table.remove(cats.order, i) + break + end + end + + self:SaveCategories(cats) + return true +end + +-- Move category up in order +function CategoryManager:MoveCategoryUp(categoryId) + local cats = self:GetCategories() + + 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 + end + end + return false +end + +-- Move category down in order +function CategoryManager:MoveCategoryDown(categoryId) + local cats = self:GetCategories() + 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 + end + end + return false +end + +-- Toggle category enabled state +function CategoryManager:ToggleCategory(categoryId) + local cats = self:GetCategories() + local def = cats.definitions[categoryId] + + if def then + def.enabled = not def.enabled + self:SaveCategories(cats) + return true + end + return false +end + +-- Reset all categories to defaults +function CategoryManager:ResetToDefaults() + Guda_CharDB.categories = self:GetDefaultCategories() + addon:Debug("CategoryManager: Reset to default categories") +end + +------------------------------------------- +-- Rule Evaluation Engine +------------------------------------------- + +-- Evaluate a single rule against item data +function CategoryManager:EvaluateRule(rule, itemData, bagID, slotID, isOtherChar) + local ruleType = rule.type + local ruleValue = rule.value + + if ruleType == "itemType" then + return (itemData.class == ruleValue) or (itemData.type == ruleValue) + + elseif ruleType == "itemSubtype" then + local subclass = itemData.subclass or "" + -- Check for partial match (e.g., "Food" matches "Food & Drink") + if string.find(subclass, ruleValue) then + return true + end + return subclass == ruleValue + + elseif ruleType == "namePattern" then + local itemName = itemData.name or "" + return string.find(itemName, ruleValue) ~= nil + + elseif ruleType == "quality" then + return itemData.quality == ruleValue + + elseif ruleType == "isBoE" then + if isOtherChar then return false end + if itemData.class ~= "Weapon" and itemData.class ~= "Armor" then + return false + end + local isBoE = addon.Modules.Utils:IsBindOnEquip(bagID, slotID, itemData.link) + return isBoE == ruleValue + + elseif ruleType == "isQuestItem" then + local isQuestItem = false + local itemCategory = itemData.class or itemData.category or "" + local itemType = itemData.type or "" + local isEquipment = (itemCategory == "Weapon" or itemCategory == "Armor") + local isQuestCategory = (itemCategory == "Quest" or itemType == "Quest") + + if isQuestCategory then + isQuestItem = true + elseif not isOtherChar then + isQuestItem = addon.Modules.Utils:IsQuestItemTooltip(bagID, slotID) + if isQuestItem and isEquipment and not isQuestCategory then + isQuestItem = false + end + end + + -- Also check QuestItemsDB + if not isQuestItem and itemData.link then + local itemID = addon.Modules.Utils:ExtractItemID(itemData.link) + if itemID and addon.IsQuestItemByID then + local playerFaction = UnitFactionGroup("player") + isQuestItem = addon:IsQuestItemByID(itemID, playerFaction) + end + end + + return isQuestItem == ruleValue + + elseif ruleType == "texturePattern" then + local texture = itemData.texture or "" + return string.find(string.lower(texture), string.lower(ruleValue)) ~= nil + + elseif ruleType == "itemID" then + if not itemData.link then return false end + local itemID = addon.Modules.Utils:ExtractItemID(itemData.link) + if not itemID then return false end + + -- ruleValue can be a single ID or a table of IDs + if type(ruleValue) == "table" then + for _, id in ipairs(ruleValue) do + if itemID == id then return true end + end + return false + else + return itemID == ruleValue + end + + elseif ruleType == "isSoulShard" then + return addon.Modules.Utils:IsSoulShard(itemData.link) == ruleValue + + elseif ruleType == "isProjectile" then + local isProj = (itemData.class == "Projectile" or + itemData.subclass == "Arrow" or + itemData.subclass == "Bullet") + return isProj == ruleValue + + elseif ruleType == "restoreTag" then + -- restoreTag is set by tooltip scanning: "eat", "drink", or "restore" + local tag = itemData.restoreTag + if not tag then return false end + return tag == ruleValue + end + + return false +end + +-- Evaluate all rules for a category +function CategoryManager:EvaluateCategoryRules(categoryDef, itemData, bagID, slotID, isOtherChar) + if not categoryDef.enabled then + return false + end + + local rules = categoryDef.rules or {} + + -- No rules = fallback category (matches everything) + if table.getn(rules) == 0 then + return categoryDef.isFallback == true + end + + local matchMode = categoryDef.matchMode or "any" + + if matchMode == "all" then + -- All rules must match + for _, rule in ipairs(rules) do + if not self:EvaluateRule(rule, itemData, bagID, slotID, isOtherChar) then + return false + end + end + return true + else + -- Any rule must match + for _, rule in ipairs(rules) do + if self:EvaluateRule(rule, itemData, bagID, slotID, isOtherChar) then + return true + end + end + return false + end +end + +-- Get sorted categories by priority (highest first) +function CategoryManager:GetCategoriesByPriority() + local cats = self:GetCategories() + local sorted = {} + + for id, def in pairs(cats.definitions) do + if def.enabled then + table.insert(sorted, { id = id, def = def }) + end + end + + table.sort(sorted, function(a, b) + return (a.def.priority or 0) > (b.def.priority or 0) + end) + + return sorted +end + +-- Categorize an item using the rule engine +-- Returns category ID or "Miscellaneous" as fallback +function CategoryManager:CategorizeItem(itemData, bagID, slotID, isOtherChar) + local sortedCats = self:GetCategoriesByPriority() + + for _, entry in ipairs(sortedCats) do + if not entry.def.isFallback then + if self:EvaluateCategoryRules(entry.def, itemData, bagID, slotID, isOtherChar) then + return entry.id + end + end + end + + return "Miscellaneous" +end + +-- Build the Guda_CategoryList from current category order (for compatibility) +function CategoryManager:BuildCategoryList() + local order = self:GetCategoryOrder() + local list = {} + + for _, id in ipairs(order) do + local def = self:GetCategory(id) + if def and def.enabled then + table.insert(list, id) + end + end + + return list +end + +-- Get available rule types for UI +function CategoryManager:GetRuleTypes() + return { + { id = "itemType", name = "Item Type", description = "Match by item class (Armor, Weapon, etc.)" }, + { id = "itemSubtype", name = "Item Subtype", description = "Match by item subclass (Cloth, Potion, etc.)" }, + { id = "namePattern", name = "Name Pattern", description = "Match item name (supports Lua patterns)" }, + { id = "quality", name = "Quality", description = "Match by item quality (0=Gray to 5=Legendary)" }, + { id = "isBoE", name = "Bind on Equip", description = "Match items that bind when equipped" }, + { id = "isQuestItem", name = "Quest Item", description = "Match quest items" }, + { id = "texturePattern", name = "Icon Pattern", description = "Match icon texture path" }, + { id = "itemID", name = "Item ID", description = "Match specific item IDs" }, + { id = "isSoulShard", name = "Soul Shard", description = "Match soul shards" }, + { id = "isProjectile", name = "Projectile", description = "Match arrows and bullets" }, + { id = "restoreTag", name = "Restore Type", description = "Match by consumable type (eat, drink, restore)" }, + } +end + +-- Get common item types for UI dropdowns +function CategoryManager:GetItemTypes() + return { + "Armor", "Weapon", "Consumable", "Container", "Trade Goods", + "Projectile", "Quiver", "Reagent", "Recipe", "Key", "Miscellaneous", "Quest" + } +end + +-- Get quality names for UI +function CategoryManager:GetQualityNames() + return { + [0] = "Poor (Gray)", + [1] = "Common (White)", + [2] = "Uncommon (Green)", + [3] = "Rare (Blue)", + [4] = "Epic (Purple)", + [5] = "Legendary (Orange)", + } +end diff --git a/Core/Database.lua b/Core/Database.lua index be0ef7d..bf2a4b9 100644 --- a/Core/Database.lua +++ b/Core/Database.lua @@ -122,6 +122,11 @@ function DB:Initialize() Guda_CharDB.settings.markUnusableItems = true end + -- Initialize CategoryManager for custom categories + if addon.Modules.CategoryManager then + addon.Modules.CategoryManager:Initialize() + end + -- Initialize this character's data if not Guda_DB.characters[fullName] then local localizedClass, englishClass = UnitClass("player") diff --git a/Guda.toc b/Guda.toc index 5f887d9..029233b 100644 --- a/Guda.toc +++ b/Guda.toc @@ -13,6 +13,7 @@ Core\Database.lua DB\QuestItems.lua Core\Events.lua Core\Utils.lua +Core\CategoryManager.lua Core\Tooltip.lua Data\BagScanner.lua diff --git a/UI/FrameHelpers.lua b/UI/FrameHelpers.lua index e27e4cb..7dac7b9 100644 --- a/UI/FrameHelpers.lua +++ b/UI/FrameHelpers.lua @@ -5,10 +5,18 @@ local FrameHelpers = {} addon.Modules.FrameHelpers = FrameHelpers -- Standard category list used by both Bag and Bank frames +-- Now dynamically built from CategoryManager if available Guda_CategoryList = { "BoE", "Weapon", "Armor", "Consumable", "Food", "Drink", "Trade Goods", "Reagent", "Recipe", "Quiver", "Container", "Soul Bag", "Miscellaneous", "Quest", "Junk", "Class Items", "Keyring" } +-- Rebuild category list from CategoryManager +function Guda_RefreshCategoryList() + if addon.Modules.CategoryManager then + Guda_CategoryList = addon.Modules.CategoryManager:BuildCategoryList() + end +end + -- Categorize a single item into categories and specialItems tables -- Returns nothing, modifies tables in place function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, isOtherChar) @@ -25,6 +33,7 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, 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 table.insert(specialItems.Hearthstone, {bagID = bagID, slotID = slotID, itemData = itemData}) return @@ -44,22 +53,28 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, return end + -- Use CategoryManager rule engine if available, otherwise fall back to legacy logic + if addon.Modules.CategoryManager then + cat = addon.Modules.CategoryManager:CategorizeItem(itemData, bagID, slotID, isOtherChar) + if not categories[cat] then cat = "Miscellaneous" end + table.insert(categories[cat], {bagID = bagID, slotID = slotID, itemData = itemData}) + return + end + + -- Legacy categorization logic (fallback if CategoryManager not available) -- Priority 2: Class Items (Soul Shards, Arrows, Bullets) - if addon.Modules.Utils:IsSoulShard(itemData.link) or - itemData.class == "Projectile" or - itemData.subclass == "Arrow" or + if addon.Modules.Utils:IsSoulShard(itemData.link) or + itemData.class == "Projectile" or + itemData.subclass == "Arrow" or itemData.subclass == "Bullet" then table.insert(categories["Class Items"], {bagID = bagID, slotID = slotID, itemData = itemData}) return end -- Priority 3: Quest Items - -- Check multiple sources: tooltip scan, itemClass, itemType, and QuestItemsDB local isQuestItem = false local itemCategory = itemData.class or itemData.category or "" local itemType = itemData.type or "" - - -- If it's a Weapon or Armor, it shouldn't be a QuestItem unless it's specifically categorized as Quest local isEquipment = (itemCategory == "Weapon" or itemCategory == "Armor" or itemType == "Weapon" or itemType == "Armor") local isQuestCategory = (itemCategory == "Quest" or itemType == "Quest") @@ -67,12 +82,10 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, isQuestItem = true elseif not isOtherChar then isQuestItem = addon.Modules.Utils:IsQuestItemTooltip(bagID, slotID) - -- If tooltip said it is quest, but it is equipment and not quest category, reject it if isQuestItem and isEquipment and not isQuestCategory then isQuestItem = false end end - -- Also check the QuestItemsDB for known faction-specific quest items if not isQuestItem and itemData.link then local itemID = addon.Modules.Utils:ExtractItemID(itemData.link) if itemID and addon.IsQuestItemByID then @@ -106,7 +119,7 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, return end - -- Priority 6: BoE Equipment (Armor/Weapons that bind when equipped) + -- Priority 6: BoE Equipment if (itemData.class == "Weapon" or itemData.class == "Armor") and not isOtherChar then local isBoE = addon.Modules.Utils:IsBindOnEquip(bagID, slotID, itemData.link) if isBoE then @@ -117,7 +130,7 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, return end - -- Priority 7: Equipment for other characters (can't scan tooltip) + -- Priority 7: Equipment for other characters if (itemData.class == "Weapon" or itemData.class == "Armor") and isOtherChar then table.insert(categories[itemData.class], {bagID = bagID, slotID = slotID, itemData = itemData}) return @@ -131,10 +144,36 @@ end -- Initialize empty category tables function Guda_InitCategories() + -- Refresh category list from CategoryManager (enabled categories only for display) + Guda_RefreshCategoryList() + + -- Create tables for ALL categories (not just enabled) so items can be categorized + -- Items in disabled categories simply won't be displayed local categories = {} - for _, cat in ipairs(Guda_CategoryList) do - categories[cat] = {} + + if addon.Modules.CategoryManager then + -- Get full category order (all categories, not just enabled) + local allCategories = addon.Modules.CategoryManager:GetCategoryOrder() + for _, cat in ipairs(allCategories) do + categories[cat] = {} + end + else + -- Fallback: use the display list + for _, cat in ipairs(Guda_CategoryList) do + categories[cat] = {} + end end + + -- Always ensure Miscellaneous exists as fallback + if not categories["Miscellaneous"] then + categories["Miscellaneous"] = {} + end + + -- Always ensure Keyring exists (handled specially in BagFrame) + if not categories["Keyring"] then + categories["Keyring"] = {} + end + local specialItems = { Hearthstone = {}, Mount = {}, diff --git a/UI/SettingsPopup.lua b/UI/SettingsPopup.lua index 1fbf4b1..59a1591 100644 --- a/UI/SettingsPopup.lua +++ b/UI/SettingsPopup.lua @@ -52,19 +52,23 @@ function Guda_SettingsPopup_SelectTab(tabName) -- Hide all tab content frames local generalTab = getglobal("Guda_SettingsPopup_GeneralTab") local iconsTab = getglobal("Guda_SettingsPopup_IconsTab") + local categoriesTab = getglobal("Guda_SettingsPopup_CategoriesTab") local guideTab = getglobal("Guda_SettingsPopup_GuideTab") if generalTab then generalTab:Hide() end if iconsTab then iconsTab:Hide() end + if categoriesTab then categoriesTab:Hide() end if guideTab then guideTab:Hide() end -- Reset all tab button backgrounds to inactive (0.1 alpha) local generalBg = getglobal("Guda_SettingsPopup_GeneralTabButton_Bg") local iconsBg = getglobal("Guda_SettingsPopup_IconsTabButton_Bg") + local categoriesBg = getglobal("Guda_SettingsPopup_CategoriesTabButton_Bg") local guideBg = getglobal("Guda_SettingsPopup_GuideTabButton_Bg") if generalBg then generalBg:SetTexture(1, 1, 1, 0.1) end if iconsBg then iconsBg:SetTexture(1, 1, 1, 0.1) end + if categoriesBg then categoriesBg:SetTexture(1, 1, 1, 0.1) end if guideBg then guideBg:SetTexture(1, 1, 1, 0.1) end -- Show selected tab and highlight its button @@ -74,6 +78,10 @@ function Guda_SettingsPopup_SelectTab(tabName) elseif tabName == "icons" then if iconsTab then iconsTab:Show() end if iconsBg then iconsBg:SetTexture(1, 1, 1, 0.3) end + elseif tabName == "categories" then + if categoriesTab then categoriesTab:Show() end + if categoriesBg then categoriesBg:SetTexture(1, 1, 1, 0.3) end + Guda_SettingsPopup_CategoriesTab_Update() elseif tabName == "guide" then if guideTab then guideTab:Show() end if guideBg then guideBg:SetTexture(1, 1, 1, 0.3) end @@ -1160,6 +1168,234 @@ function Guda_SettingsPopup_ReverseStackSortCheckbox_OnClick(self) -- No immediate UI update needed end +------------------------------------------- +-- Categories Tab Functions +------------------------------------------- + +-- Number of visible rows in the category list +local CATEGORY_ROW_HEIGHT = 22 +local CATEGORY_VISIBLE_ROWS = 14 +local categoryRowFrames = {} + +-- Create or get a category row frame +local function GetCategoryRowFrame(index) + if categoryRowFrames[index] then + return categoryRowFrames[index] + end + + local container = getglobal("Guda_SettingsPopup_CategoryListContainer") + if not container then return nil end + + local rowName = "Guda_SettingsPopup_CategoryRow" .. index + local row = CreateFrame("Frame", rowName, container) + row:SetHeight(CATEGORY_ROW_HEIGHT) + row:SetWidth(420) + row:SetPoint("TOPLEFT", container, "TOPLEFT", 0, -((index - 1) * CATEGORY_ROW_HEIGHT)) + + -- Background highlight + local bg = row:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints(row) + bg:SetTexture(1, 1, 1, 0) + row.bg = bg + + -- Enable checkbox + local checkbox = CreateFrame("CheckButton", rowName .. "_Checkbox", row, "UICheckButtonTemplate") + checkbox:SetWidth(20) + checkbox:SetHeight(20) + checkbox:SetPoint("LEFT", row, "LEFT", 0, 0) + checkbox:SetScript("OnClick", function() + local catId = this:GetParent().categoryId + if catId and Guda.Modules.CategoryManager then + Guda.Modules.CategoryManager:ToggleCategory(catId) + Guda_SettingsPopup_CategoriesTab_Update() + Guda_SettingsPopup_RefreshBagFrames() + end + end) + row.checkbox = checkbox + + -- Category name + local nameText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal") + nameText:SetPoint("LEFT", checkbox, "RIGHT", 5, 0) + nameText:SetWidth(200) + nameText:SetJustifyH("LEFT") + row.nameText = nameText + + -- Move Up button + local upBtn = CreateFrame("Button", rowName .. "_UpBtn", row) + upBtn:SetWidth(20) + upBtn:SetHeight(20) + upBtn:SetPoint("LEFT", nameText, "RIGHT", 10, 0) + upBtn:SetNormalTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Up") + upBtn:SetPushedTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Down") + upBtn:SetHighlightTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Highlight") + upBtn:SetScript("OnClick", function() + local catId = this:GetParent().categoryId + if catId and Guda.Modules.CategoryManager then + Guda.Modules.CategoryManager:MoveCategoryUp(catId) + Guda_SettingsPopup_CategoriesTab_Update() + Guda_SettingsPopup_RefreshBagFrames() + end + end) + row.upBtn = upBtn + + -- Move Down button + local downBtn = CreateFrame("Button", rowName .. "_DownBtn", row) + downBtn:SetWidth(20) + downBtn:SetHeight(20) + downBtn:SetPoint("LEFT", upBtn, "RIGHT", 2, 0) + downBtn:SetNormalTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Up") + downBtn:SetPushedTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Down") + downBtn:SetHighlightTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Highlight") + downBtn:SetScript("OnClick", function() + local catId = this:GetParent().categoryId + if catId and Guda.Modules.CategoryManager then + Guda.Modules.CategoryManager:MoveCategoryDown(catId) + Guda_SettingsPopup_CategoriesTab_Update() + Guda_SettingsPopup_RefreshBagFrames() + end + end) + row.downBtn = downBtn + + -- Delete button (only for custom categories) + local deleteBtn = CreateFrame("Button", rowName .. "_DeleteBtn", row, "UIPanelCloseButton") + deleteBtn:SetWidth(20) + deleteBtn:SetHeight(20) + deleteBtn:SetPoint("LEFT", downBtn, "RIGHT", 5, 0) + deleteBtn:SetScript("OnClick", function() + local catId = this:GetParent().categoryId + if catId and Guda.Modules.CategoryManager then + local def = Guda.Modules.CategoryManager:GetCategory(catId) + if def and not def.isBuiltIn then + Guda.Modules.CategoryManager:DeleteCategory(catId) + Guda_SettingsPopup_CategoriesTab_Update() + Guda_SettingsPopup_RefreshBagFrames() + end + end + end) + row.deleteBtn = deleteBtn + + -- Built-in indicator + local builtInText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + builtInText:SetPoint("LEFT", deleteBtn, "RIGHT", 5, 0) + builtInText:SetText("(Built-in)") + builtInText:SetTextColor(0.5, 0.5, 0.5) + row.builtInText = builtInText + + -- Hover highlight + row:EnableMouse(true) + row:SetScript("OnEnter", function() + this.bg:SetTexture(1, 1, 1, 0.1) + end) + row:SetScript("OnLeave", function() + this.bg:SetTexture(1, 1, 1, 0) + end) + + categoryRowFrames[index] = row + return row +end + +-- Update the category list display +function Guda_SettingsPopup_CategoriesTab_Update() + if not Guda.Modules.CategoryManager then return end + + local scrollFrame = getglobal("Guda_SettingsPopup_CategoriesScrollFrame") + if not scrollFrame then return end + + local categoryOrder = Guda.Modules.CategoryManager:GetCategoryOrder() + local totalCategories = table.getn(categoryOrder) + + -- Update scroll frame + FauxScrollFrame_Update(scrollFrame, totalCategories, CATEGORY_VISIBLE_ROWS, CATEGORY_ROW_HEIGHT) + + local offset = FauxScrollFrame_GetOffset(scrollFrame) + + for i = 1, CATEGORY_VISIBLE_ROWS do + 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 categoryDef then + row.categoryId = categoryId + row.nameText:SetText(categoryDef.name or categoryId) + row.checkbox:SetChecked(categoryDef.enabled and 1 or 0) + + -- Show/hide delete button based on whether it's built-in + if categoryDef.isBuiltIn then + row.deleteBtn:Hide() + row.builtInText:Show() + else + row.deleteBtn:Show() + row.builtInText:Hide() + end + + -- Enable/disable move buttons based on position + if dataIndex == 1 then + row.upBtn:Disable() + else + row.upBtn:Enable() + end + + if dataIndex == totalCategories then + row.downBtn:Disable() + else + row.downBtn:Enable() + end + + -- Set text color based on enabled state + if categoryDef.enabled then + row.nameText:SetTextColor(1, 1, 1) + else + row.nameText:SetTextColor(0.5, 0.5, 0.5) + end + + row:Show() + else + row:Hide() + end + else + row:Hide() + end + end + end + + -- Set reset button text + local resetBtn = getglobal("Guda_SettingsPopup_ResetCategoriesButton") + if resetBtn then + resetBtn:SetText("Reset to Defaults") + end +end + +-- Reset categories to defaults +function Guda_SettingsPopup_ResetCategories_OnClick() + if Guda.Modules.CategoryManager then + Guda.Modules.CategoryManager:ResetToDefaults() + Guda_SettingsPopup_CategoriesTab_Update() + Guda_SettingsPopup_RefreshBagFrames() + Guda:Print("Categories reset to defaults.") + end +end + +-- Refresh bag and bank frames after category changes +function Guda_SettingsPopup_RefreshBagFrames() + -- Refresh category list + Guda_RefreshCategoryList() + + -- Update bag frame if visible + local bagFrame = getglobal("Guda_BagFrame") + if bagFrame and bagFrame:IsShown() then + Guda.Modules.BagFrame:Update() + end + + -- Update bank frame if visible + local bankFrame = getglobal("Guda_BankFrame") + if bankFrame and bankFrame:IsShown() then + Guda.Modules.BankFrame:Update() + end +end + -- Initialize function SettingsPopup:Initialize() Guda:Debug("Settings popup initialized") diff --git a/UI/SettingsPopup.xml b/UI/SettingsPopup.xml index 8b67f0a..20652b3 100644 --- a/UI/SettingsPopup.xml +++ b/UI/SettingsPopup.xml @@ -62,7 +62,7 @@ - + + + + +