Merge pull request #28 from vatichild/feat/categories

feat: categories
This commit is contained in:
Vati
2026-01-19 19:04:22 +04:00
committed by GitHub
12 changed files with 3150 additions and 726 deletions
+769
View File
@@ -0,0 +1,769 @@
-- 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 = {
"Home", "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 = 75,
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 = "isJunk", value = true }
},
matchMode = "any",
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,
},
["Home"] = {
name = "Home",
icon = "Interface\\Icons\\INV_Misc_Rune_01",
rules = {},
matchMode = "all",
priority = 0,
enabled = true,
isBuiltIn = true,
hideControls = 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
-- Migrate BoE priority to be higher than Weapon/Armor (75 > 70)
local boeCat = cats.definitions["BoE"]
if boeCat and boeCat.isBuiltIn and boeCat.priority and boeCat.priority < 75 then
boeCat.priority = 75
addon:Debug("CategoryManager: Migrated BoE priority to 75")
end
-- Migrate Junk category to use isJunk rule type
local junkCat = cats.definitions["Junk"]
if junkCat and junkCat.isBuiltIn then
local needsUpdate = false
-- Check if priority is too low
if not junkCat.priority or junkCat.priority < 85 then
junkCat.priority = 85
needsUpdate = true
end
-- Migrate from quality=0 rule to isJunk rule
if junkCat.rules then
for i, rule in ipairs(junkCat.rules) do
if rule.type == "quality" and rule.value == 0 then
junkCat.rules = { { type = "isJunk", value = true } }
needsUpdate = true
break
end
end
end
-- Check if rules are missing or wrong
if not junkCat.rules or table.getn(junkCat.rules) == 0 then
junkCat.rules = { { type = "isJunk", value = true } }
needsUpdate = true
end
if needsUpdate then
addon:Debug("CategoryManager: Migrated Junk category to use isJunk rule")
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
-- For quality 0 (gray/junk), also check tooltip as fallback
if ruleValue == 0 then
if itemData.quality == 0 then
return true
end
-- Tooltip fallback for gray detection (current character only)
if not isOtherChar and addon.Modules.Utils and addon.Modules.Utils.IsItemGrayTooltip then
return addon.Modules.Utils:IsItemGrayTooltip(bagID, slotID, itemData.link)
end
return false
end
return itemData.quality == ruleValue
elseif ruleType == "qualityMin" then
-- Minimum quality check (item quality >= ruleValue)
return (itemData.quality or 0) >= (ruleValue or 0)
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
-- Use consolidated quest detection from Utils
local isQuestItem, _ = addon.Modules.Utils:IsQuestItem(bagID, slotID, itemData, isOtherChar, false)
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 == tonumber(id) then return true end
end
return false
else
-- Convert string to number if needed
return itemID == tonumber(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
elseif ruleType == "isJunk" then
-- Junk items: gray items (quality 0) OR white equippable items (quality 1 + Weapon/Armor)
local quality = itemData.quality
local isGray = false
local isWhiteEquip = false
-- Check for gray items (quality 0)
if quality == 0 then
isGray = true
elseif not isOtherChar and addon.Modules.Utils and addon.Modules.Utils.IsItemGrayTooltip then
-- Tooltip fallback for gray detection
isGray = addon.Modules.Utils:IsItemGrayTooltip(bagID, slotID, itemData.link)
end
-- Check for white equippable items (quality 1 + Weapon/Armor)
if quality == 1 then
local itemClass = itemData.class or ""
addon:Debug("isJunk check: quality=%s, class='%s', name='%s'", tostring(quality), tostring(itemClass), tostring(itemData.name))
if itemClass == "Weapon" or itemClass == "Armor" then
isWhiteEquip = true
addon:Debug("isJunk: WHITE EQUIP DETECTED - %s", tostring(itemData.name))
end
end
local isJunk = isGray or isWhiteEquip
return isJunk == 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()
-- Debug: show white item categorization
if itemData.quality == 1 and (itemData.class == "Weapon" or itemData.class == "Armor") then
addon:Debug("Categorizing white equip: %s (class=%s, quality=%s)", tostring(itemData.name), tostring(itemData.class), tostring(itemData.quality))
for i, entry in ipairs(sortedCats) do
addon:Debug(" Cat %d: %s (priority=%s, enabled=%s)", i, entry.id, tostring(entry.def.priority), tostring(entry.def.enabled))
if i > 10 then break end -- Limit debug output
end
end
for _, entry in ipairs(sortedCats) do
if not entry.def.isFallback then
local matches = self:EvaluateCategoryRules(entry.def, itemData, bagID, slotID, isOtherChar)
-- Debug: show which category matched for white equip
if itemData.quality == 1 and (itemData.class == "Weapon" or itemData.class == "Armor") and matches then
addon:Debug(" -> MATCHED: %s", entry.id)
end
if matches 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 = "qualityMin", name = "Quality (min)", description = "Match items with at least this quality" },
{ id = "isBoE", name = "Bind on Equip", description = "Match items that bind when equipped" },
{ id = "isQuestItem", name = "Quest Item", description = "Match quest items" },
{ id = "isJunk", name = "Is Junk", description = "Match junk items (gray + white equippable)" },
{ 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
+166
View File
@@ -0,0 +1,166 @@
-- Guda Constants Module
-- Centralized location for all magic numbers and hardcoded values
local addon = Guda
-- Extend the existing Constants table from Init.lua
local C = addon.Constants
--=============================================================================
-- Bag IDs
--=============================================================================
C.BAG_BACKPACK = 0
C.BAG_FIRST = 1
C.BAG_LAST = 4
C.BANK_FIRST = 5
C.BANK_LAST = 11
C.KEYRING_BAG = -2
C.BANK_CONTAINER = -1
-- Bag ID ranges for iteration
C.BAG_IDS = {0, 1, 2, 3, 4}
C.BANK_BAG_IDS = {5, 6, 7, 8, 9, 10, 11}
C.ALL_BAG_IDS = {0, 1, 2, 3, 4, -2} -- Including keyring
--=============================================================================
-- Equipment Slots
--=============================================================================
C.EQUIPMENT_SLOT_FIRST = 1
C.EQUIPMENT_SLOT_LAST = 19
--=============================================================================
-- Item IDs
--=============================================================================
C.SOUL_SHARD_ID = 6265
C.HEARTHSTONE_ID = 6948
--=============================================================================
-- Colors (r, g, b tables for easy unpacking)
--=============================================================================
C.COLORS = {
-- Special item borders
KEYRING_CYAN = {r = 0.2, g = 0.8, b = 1.0},
QUEST_GOLD = {r = 1.0, g = 0.82, b = 0},
-- Text colors
GRAY_TEXT = {r = 0.5, g = 0.5, b = 0.5},
WHITE_TEXT = {r = 1.0, g = 1.0, b = 1.0},
GOLD_TITLE = {r = 1.0, g = 0.82, b = 0},
CYAN_LABEL = {r = 0, g = 1.0, b = 1.0},
-- Unusable item tint
UNUSABLE_RED = {r = 0.9, g = 0.2, b = 0.2, a = 0.45},
-- Lock/desaturate
LOCKED_GRAY = {r = 0.5, g = 0.5, b = 0.5},
}
--=============================================================================
-- UI Thresholds
--=============================================================================
C.ICON_SIZE_THRESHOLD = 44 -- Below this, use smaller insets/padding
C.ICON_INSET_SMALL = 10 -- Inset for small icons
C.ICON_INSET_LARGE = 15 -- Inset for large icons
C.ICON_TEXCOORD_CROP = 0.08 -- Texture coordinate crop amount
--=============================================================================
-- Database / Cleanup
--=============================================================================
C.CLEANUP_OLD_CHARS_DAYS = 90
--=============================================================================
-- Tooltip Scanning
--=============================================================================
C.QUEST_TOOLTIP_PATTERNS = {
"quest starter",
"this item begins a quest",
"starts a quest",
"quest item",
"manual",
}
C.BIND_ON_EQUIP_PATTERN = "binds when equipped"
--=============================================================================
-- Specialized Bag Types
--=============================================================================
C.BAG_TYPES = {
SOUL = "soul",
HERB = "herb",
ENCHANT = "enchant",
QUIVER = "quiver",
AMMO = "ammo",
}
-- Tooltip patterns for bag type detection
C.BAG_TYPE_PATTERNS = {
soul = {"soul bag", "soul pouch"},
herb = {"herb bag"},
enchant = {"enchanting bag"},
quiver = {"quiver"},
ammo = {"ammo pouch"},
}
--=============================================================================
-- Item Categories (for GetItemInfo)
--=============================================================================
C.ITEM_CATEGORIES = {
WEAPON = "Weapon",
ARMOR = "Armor",
CONSUMABLE = "Consumable",
CONTAINER = "Container",
TRADE_GOODS = "Trade Goods",
PROJECTILE = "Projectile",
QUIVER = "Quiver",
REAGENT = "Reagent",
RECIPE = "Recipe",
KEY = "Key",
MISCELLANEOUS = "Miscellaneous",
QUEST = "Quest",
}
--=============================================================================
-- Money Formatting
--=============================================================================
C.MONEY = {
COPPER_PER_SILVER = 100,
SILVER_PER_GOLD = 100,
COPPER_PER_GOLD = 10000,
}
-- Color codes for money display
C.MONEY_COLORS = {
GOLD = "|cFFFFD700",
SILVER = "|cFFC7C7CF",
COPPER = "|cFFEDA55F",
WHITE = "|cFFFFFFFF",
}
--=============================================================================
-- Frame Constants
--=============================================================================
C.FRAME = {
TITLE_HEIGHT = 40,
SEARCH_BAR_HEIGHT = 30,
FOOTER_HEIGHT = 45,
FOOTER_HEIGHT_HIDDEN = 10,
MIN_WIDTH = 200,
MIN_HEIGHT = 150,
MAX_WIDTH = 1250,
MAX_HEIGHT = 1000,
}
--=============================================================================
-- Bank Slots
--=============================================================================
C.BANK_MAIN_SLOTS = 24 -- Number of slots in main bank container
--=============================================================================
-- Tooltip Hook Settings
--=============================================================================
C.TOOLTIP = {
DEBOUNCE_TIME = 0.2, -- Seconds to wait before clearing cache
MAX_MONEY_FRAMES = 8, -- Maximum number of money frames to search
}
addon:Debug("Constants module loaded")
+5
View File
@@ -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")
+200 -240
View File
@@ -4,261 +4,221 @@ local addon = Guda
local Tooltip = {}
addon.Modules.Tooltip = Tooltip
--=============================================================================
-- Item Counting Helper Functions (extracted for clarity and reuse)
--=============================================================================
-- Helper function to get item ID from link (Lua 5.0 compatible)
local function GetItemIDFromLink(link)
if not link then return nil end
if not link then return nil end
if type(link) == "number" then return link end
-- Try to find itemID in a standard link or a raw item:ID string
local _, _, itemID = string.find(link, "item:(%d+)")
return itemID and tonumber(itemID) or nil
local _, _, itemID = string.find(link, "item:(%d+)")
return itemID and tonumber(itemID) or nil
end
-- Count items in saved bag/bank data structure
-- Used for both bags and bank counting from saved character data
local function CountFromSavedContainers(containersData, itemID)
local count = 0
if not containersData or type(containersData) ~= "table" then
return count
end
for bagID, bagData in pairs(containersData) do
if bagData and type(bagData) == "table" and bagData.slots and type(bagData.slots) == "table" then
for slotID, itemData in pairs(bagData.slots) do
if itemData and type(itemData) == "table" and itemData.link then
local slotItemID = GetItemIDFromLink(itemData.link)
if slotItemID == itemID then
count = count + (itemData.count or 1)
end
end
end
end
end
return count
end
-- Count items in saved mailbox data structure
local function CountFromSavedMailbox(mailboxData, itemID)
local count = 0
if not mailboxData or type(mailboxData) ~= "table" then
return count
end
for _, mail in ipairs(mailboxData) do
local itemsToCheck = mail.items or (mail.item and {mail.item}) or {}
for _, item in ipairs(itemsToCheck) do
local slotItemID = item.link and GetItemIDFromLink(item.link)
if slotItemID == itemID then
count = count + (item.count or 1)
elseif not slotItemID and item.name then
-- Fallback to name matching if link is missing
local targetName = GetItemInfo(itemID)
if targetName == item.name then
count = count + (item.count or 1)
end
end
end
end
return count
end
-- Count items in saved equipped data structure
local function CountFromSavedEquipped(equippedData, itemID)
local count = 0
if not equippedData or type(equippedData) ~= "table" then
return count
end
for slotName, itemData in pairs(equippedData) do
if itemData and type(itemData) == "table" and itemData.link then
local slotItemID = GetItemIDFromLink(itemData.link)
if slotItemID == itemID then
count = count + 1
end
end
end
return count
end
-- Count items in live container (bags or bank)
local function CountFromLiveContainer(bagIDs, itemID)
local count = 0
for _, bagID in ipairs(bagIDs) do
local numSlots = GetContainerNumSlots(bagID)
if numSlots and numSlots > 0 then
for slot = 1, numSlots do
local link = GetContainerItemLink(bagID, slot)
if link then
local slotItemID = GetItemIDFromLink(link)
if slotItemID == itemID then
local _, itemCount = GetContainerItemInfo(bagID, slot)
count = count + (itemCount or 1)
end
end
end
end
end
return count
end
-- Count items in live mailbox
local function CountFromLiveMailbox(itemID)
local count = 0
if not (addon.Modules.MailboxScanner and addon.Modules.MailboxScanner:IsMailboxOpen()) then
return count
end
local numInboxItems = GetInboxNumItems()
for i = 1, numInboxItems do
local _, _, _, _, _, _, _, hasItem = GetInboxHeaderInfo(i)
if hasItem then
local numAttachments = GetInboxNumAttachments and GetInboxNumAttachments(i) or 1
if numAttachments == 0 and hasItem then
numAttachments = 1
end
for j = 1, numAttachments do
local name, _, itemCount = GetInboxItem(i, j)
if name then
local itemLink = addon.Modules.Utils:GetInboxItemLink(i, j)
if itemLink then
local slotItemID = GetItemIDFromLink(itemLink)
if slotItemID == itemID then
count = count + (itemCount or 1)
end
end
end
end
end
end
return count
end
-- Count items in live equipment slots
local function CountFromLiveEquipped(itemID)
local count = 0
for slotID = 1, 19 do
local link = GetInventoryItemLink("player", slotID)
if link then
local slotItemID = GetItemIDFromLink(link)
if slotItemID == itemID then
count = count + 1
end
end
end
return count
end
--=============================================================================
-- Main Counting Functions
--=============================================================================
-- Count items for current character using live game data
local function CountCurrentCharacterItems(itemID)
local bagCount = 0
local bankCount = 0
local mailCount = 0
local equippedCount = 0
local bagCount = 0
local bankCount = 0
local mailCount = 0
local equippedCount = 0
-- Count current character's bags in real-time
local bagsToCount = {0, 1, 2, 3, 4, -2}
for _, bagID in ipairs(bagsToCount) do
local numSlots = GetContainerNumSlots(bagID)
for slot = 1, numSlots do
local link = GetContainerItemLink(bagID, slot)
if link then
local slotItemID = GetItemIDFromLink(link)
if slotItemID == itemID then
local _, count = GetContainerItemInfo(bagID, slot)
bagCount = bagCount + (count or 1)
end
end
end
end
-- Count bags in real-time
bagCount = CountFromLiveContainer({0, 1, 2, 3, 4, -2}, itemID)
-- Count current character's bank in real-time if bank is open
local bankFrame = getglobal("BankFrame")
if bankFrame and bankFrame:IsVisible() then
-- Main bank slots (-1)
local numMainSlots = GetContainerNumSlots(-1) or 24
for slot = 1, numMainSlots do
local link = GetContainerItemLink(-1, slot)
if link then
local slotItemID = GetItemIDFromLink(link)
if slotItemID == itemID then
local _, count = GetContainerItemInfo(-1, slot)
bankCount = bankCount + (count or 1)
end
end
end
-- Count bank: live if open, otherwise from saved data
local bankFrame = getglobal("BankFrame")
if bankFrame and bankFrame:IsVisible() then
-- Main bank + bank bags
bankCount = CountFromLiveContainer({-1, 5, 6, 7, 8, 9, 10, 11}, itemID)
else
-- Use saved data
local playerName = addon.Modules.DB:GetPlayerFullName()
local charData = Guda_DB and Guda_DB.characters and Guda_DB.characters[playerName]
if charData then
bankCount = CountFromSavedContainers(charData.bank, itemID)
end
end
-- Bank bags (5-11)
for bagID = 5, 11 do
local numSlots = GetContainerNumSlots(bagID)
if numSlots and numSlots > 0 then
for slot = 1, numSlots do
local link = GetContainerItemLink(bagID, slot)
if link then
local slotItemID = GetItemIDFromLink(link)
if slotItemID == itemID then
local _, count = GetContainerItemInfo(bagID, slot)
bankCount = bankCount + (count or 1)
end
end
end
end
end
else
-- Bank not open - use saved data for bank counts
local playerName = addon.Modules.DB:GetPlayerFullName()
local charData = Guda_DB and Guda_DB.characters and Guda_DB.characters[playerName]
if charData and charData.bank and type(charData.bank) == "table" then
for bagID, bagData in pairs(charData.bank) do
if bagData and type(bagData) == "table" and bagData.slots and type(bagData.slots) == "table" then
for slotID, itemData in pairs(bagData.slots) do
if itemData and type(itemData) == "table" and itemData.link then
local slotItemID = GetItemIDFromLink(itemData.link)
if slotItemID == itemID then
bankCount = bankCount + (itemData.count or 1)
end
end
end
end
end
end
end
-- Count mailbox: live if open, otherwise from saved data
if addon.Modules.MailboxScanner and addon.Modules.MailboxScanner:IsMailboxOpen() then
mailCount = CountFromLiveMailbox(itemID)
else
local playerName = addon.Modules.DB:GetPlayerFullName()
local charData = Guda_DB and Guda_DB.characters and Guda_DB.characters[playerName]
if charData then
mailCount = CountFromSavedMailbox(charData.mailbox, itemID)
end
end
-- Count current character's mailbox in real-time if mailbox is open
if addon.Modules.MailboxScanner and addon.Modules.MailboxScanner:IsMailboxOpen() then
local numInboxItems = GetInboxNumItems()
for i = 1, numInboxItems do
local _, _, _, _, _, _, _, hasItem = GetInboxHeaderInfo(i)
if hasItem then
-- Turtle WoW supports up to 12 attachments per mail.
-- We use GetInboxNumAttachments if available to avoid over-scanning.
local numAttachments = 0
if GetInboxNumAttachments then
numAttachments = GetInboxNumAttachments(i) or 0
end
-- Count equipped items in real-time
equippedCount = CountFromLiveEquipped(itemID)
-- Fallback: if we don't have the count but header says there's an item, assume at least 1.
if numAttachments == 0 and hasItem then
numAttachments = 1
end
for j = 1, numAttachments do -- Turtle WoW supports up to 12 attachments
local name, _, count = GetInboxItem(i, j)
if name then
local itemLink = addon.Modules.Utils:GetInboxItemLink(i, j)
if itemLink then
local slotItemID = GetItemIDFromLink(itemLink)
if slotItemID == itemID then
mailCount = mailCount + (count or 1)
end
end
end
end
end
end
else
-- Mailbox not open - use saved data
local playerName = addon.Modules.DB:GetPlayerFullName()
local charData = Guda_DB and Guda_DB.characters and Guda_DB.characters[playerName]
if charData and charData.mailbox and type(charData.mailbox) == "table" then
for _, mail in ipairs(charData.mailbox) do
if mail.items then
for _, item in ipairs(mail.items) do
local slotItemID = item.link and GetItemIDFromLink(item.link)
if slotItemID == itemID then
mailCount = mailCount + (item.count or 1)
elseif not slotItemID and item.name then
-- Fallback to name matching if link is missing
local targetName = GetItemInfo(itemID)
if targetName == item.name then
mailCount = mailCount + (item.count or 1)
end
end
end
elseif mail.item then -- Fallback for single item data structure
local item = mail.item
local slotItemID = item.link and GetItemIDFromLink(item.link)
if slotItemID == itemID then
mailCount = mailCount + (item.count or 1)
elseif not slotItemID and item.name then
-- Fallback to name matching if link is missing
local targetName = GetItemInfo(itemID)
if targetName == item.name then
mailCount = mailCount + (item.count or 1)
end
end
end
end
end
end
-- Count equipped items in real-time
for slotID = 1, 19 do -- All equipment slots
local link = GetInventoryItemLink("player", slotID)
if link then
local slotItemID = GetItemIDFromLink(link)
if slotItemID == itemID then
equippedCount = equippedCount + 1
end
end
end
return bagCount, bankCount, equippedCount, mailCount
return bagCount, bankCount, equippedCount, mailCount
end
-- Count items for a specific character with real-time data for current character
-- Count items for a specific character (current or other)
local function CountItemsForCharacter(itemID, characterData, isCurrentChar)
-- For current character, use real-time counting to avoid database sync issues
if isCurrentChar then
return CountCurrentCharacterItems(itemID)
end
-- For current character, use real-time counting
if isCurrentChar then
return CountCurrentCharacterItems(itemID)
end
-- For other characters, use saved data
local bagCount = 0
local bankCount = 0
local mailCount = 0
local equippedCount = 0
-- For other characters, use saved data
local bagCount = CountFromSavedContainers(characterData.bags, itemID)
local bankCount = CountFromSavedContainers(characterData.bank, itemID)
local mailCount = CountFromSavedMailbox(characterData.mailbox, itemID)
local equippedCount = CountFromSavedEquipped(characterData.equipped, itemID)
-- Count bags from saved data
if characterData.bags and type(characterData.bags) == "table" then
for bagID, bagData in pairs(characterData.bags) do
if bagData and type(bagData) == "table" and bagData.slots and type(bagData.slots) == "table" then
for slotID, itemData in pairs(bagData.slots) do
if itemData and type(itemData) == "table" and itemData.link then
local slotItemID = GetItemIDFromLink(itemData.link)
if slotItemID == itemID then
bagCount = bagCount + (itemData.count or 1)
end
end
end
end
end
end
-- Count bank from saved data
if characterData.bank and type(characterData.bank) == "table" then
for bagID, bagData in pairs(characterData.bank) do
if bagData and type(bagData) == "table" and bagData.slots and type(bagData.slots) == "table" then
for slotID, itemData in pairs(bagData.slots) do
if itemData and type(itemData) == "table" and itemData.link then
local slotItemID = GetItemIDFromLink(itemData.link)
if slotItemID == itemID then
bankCount = bankCount + (itemData.count or 1)
end
end
end
end
end
end
-- Count mailbox from saved data
if characterData.mailbox and type(characterData.mailbox) == "table" then
for _, mail in ipairs(characterData.mailbox) do
if mail.items then
for _, item in ipairs(mail.items) do
local slotItemID = item.link and GetItemIDFromLink(item.link)
if slotItemID == itemID then
mailCount = mailCount + (item.count or 1)
elseif not slotItemID and item.name then
-- Fallback to name matching if link is missing
local targetName = GetItemInfo(itemID)
if targetName == item.name then
mailCount = mailCount + (item.count or 1)
end
end
end
elseif mail.item then -- Fallback for single item data structure
local item = mail.item
local slotItemID = item.link and GetItemIDFromLink(item.link)
if slotItemID == itemID then
mailCount = mailCount + (item.count or 1)
elseif not slotItemID and item.name then
-- Fallback to name matching if link is missing
local targetName = GetItemInfo(itemID)
if targetName == item.name then
mailCount = mailCount + (item.count or 1)
end
end
end
end
end
-- Count equipped items from saved data
if characterData.equipped and type(characterData.equipped) == "table" then
for slotName, itemData in pairs(characterData.equipped) do
if itemData and type(itemData) == "table" and itemData.link then
local slotItemID = GetItemIDFromLink(itemData.link)
if slotItemID == itemID then
equippedCount = equippedCount + 1
end
end
end
end
return bagCount, bankCount, equippedCount, mailCount
return bagCount, bankCount, equippedCount, mailCount
end
+241 -210
View File
@@ -5,6 +5,64 @@ local addon = Guda
local Utils = {}
addon.Modules.Utils = Utils
--=============================================================================
-- SafeCall: Nil-safe module method invocation
-- Replaces verbose nil-checks like:
-- if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.Method then
-- return addon.Modules.Utils:Method(arg1, arg2)
-- end
-- With:
-- return Utils:SafeCall("Utils", "Method", arg1, arg2)
--=============================================================================
-- Call a method on a module safely, returns nil if module/method doesn't exist
-- Parameters:
-- moduleName: Name of the module in addon.Modules (e.g., "Utils", "DB", "BagFrame")
-- methodName: Name of the method to call (e.g., "GetQualityColor", "GetSetting")
-- ...: Arguments to pass to the method
-- Returns: The return value(s) of the method, or nil if not callable
function Utils:SafeCall(moduleName, methodName, ...)
if not addon or not addon.Modules then
return nil
end
local module = addon.Modules[moduleName]
if not module then
return nil
end
local method = module[methodName]
if not method or type(method) ~= "function" then
return nil
end
-- Call method with module as self (for : style calls)
return method(module, unpack(arg))
end
-- Check if a module method exists without calling it
function Utils:HasMethod(moduleName, methodName)
if not addon or not addon.Modules then
return false
end
local module = addon.Modules[moduleName]
if not module then
return false
end
local method = module[methodName]
return method ~= nil and type(method) == "function"
end
-- Get a module reference safely
function Utils:GetModule(moduleName)
if not addon or not addon.Modules then
return nil
end
return addon.Modules[moduleName]
end
-- Format money (copper to gold/silver/copper string) - WoW 1.12.1 version
function Utils:FormatMoney(copper, showZero, useColors)
if not copper or copper == 0 then
@@ -190,49 +248,123 @@ local function GetScanTooltip()
return scanTooltip
end
-- Check if an item is a quest item by scanning its tooltip
function Utils:IsQuestItemTooltip(bagID, slotID)
if not bagID or not slotID then return false end
-- Check if an item is a quest item by scanning its tooltip (internal helper)
-- Returns: isQuestItem, isQuestStarter
local function ScanTooltipForQuest(tooltip, tooltipName)
local isQuestItem = false
local isQuestStarter = false
local tooltip = GetScanTooltip()
tooltip:ClearLines()
tooltip:SetBagItem(bagID, slotID)
-- Check tooltip lines for explicit quest-related phrases (case-insensitive)
for i = 1, tooltip:NumLines() do
local line = getglobal("GudaBagScanTooltipTextLeft" .. i)
local line = getglobal(tooltipName .. "TextLeft" .. i)
if line then
local text = line:GetText()
if text then
local tl = string.lower(text)
-- Only match explicit quest markers to avoid misclassifying consumables/recipes
-- Include "manual" which some quest items use (same as ItemButton detection)
-- Check for quest starter patterns first
if string.find(tl, "quest starter") or
string.find(tl, "this item begins a quest") or
string.find(tl, "starts a quest") or
string.find(tl, "quest item") or
string.find(tl, "manual") then
-- Double check category to avoid misidentifying equipment with "Use:"
local link = GetContainerItemLink(bagID, slotID)
if link and self.ExtractItemID and self.GetItemInfoSafe then
local itemID = self:ExtractItemID(link)
if itemID then
local _, _, _, _, itemCategory, itemType = self:GetItemInfoSafe(itemID)
-- If it's Weapon or Armor, and NOT categorized as Quest, then it's not a Quest Item
if (itemCategory == "Weapon" or itemCategory == "Armor" or itemType == "Weapon" or itemType == "Armor") and
(itemCategory ~= "Quest" and itemType ~= "Quest") then
return false
end
end
end
return true
string.find(tl, "starts a quest") then
isQuestItem = true
isQuestStarter = true
break
-- Check for regular quest item patterns
elseif string.find(tl, "quest item") or
string.find(tl, "manual") then
isQuestItem = true
-- Don't break, might still find a quest starter pattern
end
end
end
end
return false
return isQuestItem, isQuestStarter
end
-- Consolidated quest item detection function
-- Handles tooltip scanning, category checks, equipment filtering, and QuestItemsDB lookup
-- Parameters:
-- bagID, slotID: Required for tooltip scanning (can be nil for other char items)
-- itemData: Optional item data table with link, class/category, type fields
-- isOtherChar: Boolean, true if checking an item from another character's saved data
-- isBank: Boolean, true if the item is in the bank
-- Returns: isQuestItem (boolean), isQuestStarter (boolean)
function Utils:IsQuestItem(bagID, slotID, itemData, isOtherChar, isBank)
bagID = tonumber(bagID)
slotID = tonumber(slotID)
local isQuestItem = false
local isQuestStarter = false
-- Get item link and category info
local itemLink = itemData and itemData.link
local itemCategory = itemData and (itemData.class or itemData.category) or ""
local itemType = itemData and itemData.type or ""
local itemID
-- For live items, query the link directly
if not isOtherChar and bagID and slotID then
itemLink = GetContainerItemLink(bagID, slotID)
end
if itemLink then
itemID = self:ExtractItemID(itemLink)
if itemID then
local _, _, _, _, cat, typ = self:GetItemInfoSafe(itemID)
itemCategory = cat or itemCategory
itemType = typ or itemType
end
end
-- Check if item is equipment (should not be classified as quest unless explicitly Quest category)
local isEquipment = (itemCategory == "Weapon" or itemCategory == "Armor" or
itemType == "Weapon" or itemType == "Armor")
local isQuestCategory = (itemCategory == "Quest" or itemType == "Quest")
-- Priority 1: If explicitly categorized as Quest, it's a quest item
if isQuestCategory then
return true, false
end
-- Priority 2: Tooltip scanning for current character items
if not isOtherChar and bagID and slotID then
local tooltip = GetScanTooltip()
tooltip:ClearLines()
-- Handle bank items differently
if isBank and bagID == -1 then
if tooltip.SetInventoryItem then
tooltip:SetInventoryItem("player", 39 + slotID)
else
tooltip:SetBagItem(bagID, slotID)
end
else
tooltip:SetBagItem(bagID, slotID)
end
isQuestItem, isQuestStarter = ScanTooltipForQuest(tooltip, "GudaBagScanTooltip")
-- Filter out equipment that has quest-like text but isn't categorized as Quest
if isQuestItem and isEquipment and not isQuestCategory then
isQuestItem = false
isQuestStarter = false
end
end
-- Priority 3: Check QuestItemsDB for known faction-specific quest items
if not isQuestItem and itemID and addon.IsQuestItemByID then
local playerFaction = UnitFactionGroup("player")
if addon:IsQuestItemByID(itemID, playerFaction) then
isQuestItem = true
end
end
return isQuestItem, isQuestStarter
end
-- Legacy compatibility wrapper - keep the old function name working
function Utils:IsQuestItemTooltip(bagID, slotID)
local isQuest, _ = self:IsQuestItem(bagID, slotID, nil, false, false)
return isQuest
end
-- Check if an item has a gray title in its tooltip or link
@@ -296,129 +428,8 @@ function Utils:TruncateText(text, maxLen)
return string.sub(text, 1, maxLen - 3) .. "..."
end
function Utils:IsAmmoQuiverBag(bagID)
-- Skip backpack, bank, and keyring
if bagID == 0 or bagID == -1 or bagID == -2 then
return false
end
-- Get the bag item
local invSlot = ContainerIDToInventoryID(bagID)
if not invSlot then
return false
end
local link = GetInventoryItemLink("player", invSlot)
if not link then
return false
end
-- Use tooltip scanning to get item class (more reliable in 1.12.1)
local tooltip = GetScanTooltip()
tooltip:ClearLines()
tooltip:SetInventoryItem("player", invSlot)
-- Scan tooltip lines for "Quiver" or "Ammo Pouch"
for i = 1, tooltip:NumLines() do
local line = getglobal("GudaBagScanTooltipTextLeft" .. i)
if line then
local text = line:GetText()
if text then
-- Check if the line contains "Quiver" or "Ammo Pouch"
if string.find(text, "Quiver") or string.find(text, "Ammo Pouch") then
return true
end
end
end
end
return false
end
-- Check if a bag is Herb Bag
function Utils:IsHerbBag(bagID)
-- Skip backpack, bank, and keyring
if bagID == 0 or bagID == -1 or bagID == -2 then
return false
end
local invSlot = ContainerIDToInventoryID(bagID)
if not invSlot then return false end
local link = GetInventoryItemLink("player", invSlot)
if not link then return false end
-- Prefer specialized type detection
local bagType = self:GetSpecializedBagType(bagID)
if bagType == "herb" then return true end
-- Fallback: tooltip scan for "Herb Bag"
local tooltip = GetScanTooltip()
tooltip:ClearLines()
tooltip:SetInventoryItem("player", invSlot)
for i = 1, tooltip:NumLines() do
local line = getglobal("GudaBagScanTooltipTextLeft" .. i)
if line then
local text = line:GetText()
if text and string.find(string.lower(text), "herb bag") then
return true
end
end
end
return false
end
-- Check if a bag is Soul Bag
function Utils:IsSoulBag(bagID)
-- Skip backpack, bank, and keyring
if bagID == 0 or bagID == -1 or bagID == -2 then
return false
end
-- Get the bag item
local invSlot = ContainerIDToInventoryID(bagID)
if not invSlot then
return false
end
local link = GetInventoryItemLink("player", invSlot)
if not link then
return false
end
-- First try GetSpecializedBagType
local bagType = self:GetSpecializedBagType(bagID)
if bagType == "soul" then
return true
end
-- Fallback: Use tooltip scanning (more reliable in 1.12.1)
local tooltip = GetScanTooltip()
tooltip:ClearLines()
tooltip:SetInventoryItem("player", invSlot)
-- Scan tooltip lines for "Soul Bag" or "Soul Pouch"
for i = 1, tooltip:NumLines() do
local line = getglobal("GudaBagScanTooltipTextLeft" .. i)
if line then
local text = line:GetText()
if text then
-- Check if the line contains "Soul Bag" or "Soul Pouch" or just "Soul" in bag name
local textLower = string.lower(text)
if string.find(textLower, "soul bag") or string.find(textLower, "soul pouch") or
(string.find(textLower, "soul") and (string.find(textLower, "bag") or string.find(textLower, "pouch"))) then
return true
end
end
end
end
return false
end
-- Returns: "soul", "herb", "enchant", "quiver", "ammo", or nil
-- This is the consolidated bag type detection function with tooltip fallback
function Utils:GetSpecializedBagType(bagID)
-- Skip backpack, bank, and keyring
if bagID == 0 or bagID == -1 or bagID == -2 then
@@ -437,44 +448,94 @@ function Utils:GetSpecializedBagType(bagID)
end
local itemID = self:ExtractItemID(link)
if not itemID then
return nil
-- Try GetItemInfo first (more reliable when available)
if itemID then
local _, _, _, _, _, itemType = self:GetItemInfoSafe(itemID)
if itemType then
local typeLower = string.lower(itemType)
if string.find(typeLower, "soul bag") or string.find(typeLower, "soul pouch") then
return "soul"
end
if string.find(typeLower, "herb bag") then
return "herb"
end
if string.find(typeLower, "enchanting bag") then
return "enchant"
end
if string.find(typeLower, "quiver") then
return "quiver"
end
if string.find(typeLower, "ammo pouch") then
return "ammo"
end
end
end
local itemName, _, itemRarity, itemLevel, itemCategory, itemType, itemStackCount, itemSubType = self:GetItemInfoSafe(itemID)
if itemType then
-- Check for exact subtype matches
local typeLower = string.lower(itemType)
-- Fallback: tooltip scanning for all bag types
local tooltip = GetScanTooltip()
tooltip:ClearLines()
tooltip:SetInventoryItem("player", invSlot)
-- Soul Bag / Soul Pouch
if string.find(typeLower, "soul bag") or string.find(typeLower, "soul pouch") then
return "soul"
end
for i = 1, tooltip:NumLines() do
local line = getglobal("GudaBagScanTooltipTextLeft" .. i)
if line then
local text = line:GetText()
if text then
local textLower = string.lower(text)
-- Herb Bag
if string.find(typeLower, "herb bag") then
return "herb"
end
-- Soul Bag / Soul Pouch
if string.find(textLower, "soul bag") or string.find(textLower, "soul pouch") or
(string.find(textLower, "soul") and (string.find(textLower, "bag") or string.find(textLower, "pouch"))) then
return "soul"
end
-- Enchanting Bag
if string.find(typeLower, "enchanting bag") then
return "enchant"
end
-- Herb Bag
if string.find(textLower, "herb bag") then
return "herb"
end
-- Quiver
if string.find(typeLower, "quiver") then
return "quiver"
end
-- Enchanting Bag
if string.find(textLower, "enchanting bag") then
return "enchant"
end
-- Ammo Pouch
if string.find(typeLower, "ammo pouch") then
return "ammo"
-- Quiver
if string.find(textLower, "quiver") then
return "quiver"
end
-- Ammo Pouch
if string.find(textLower, "ammo pouch") then
return "ammo"
end
end
end
end
return nil
end
-- Simple helper to check if a bag is of a specific type
function Utils:IsBagType(bagID, bagType)
return self:GetSpecializedBagType(bagID) == bagType
end
-- Convenience wrappers for common bag type checks
function Utils:IsAmmoQuiverBag(bagID)
local bagType = self:GetSpecializedBagType(bagID)
return bagType == "quiver" or bagType == "ammo"
end
function Utils:IsHerbBag(bagID)
return self:IsBagType(bagID, "herb")
end
function Utils:IsSoulBag(bagID)
return self:IsBagType(bagID, "soul")
end
-- Get container priority for sorting (higher = more important)
function Utils:GetContainerPriority(bagID)
local bagType = self:GetSpecializedBagType(bagID)
@@ -727,37 +788,7 @@ function Utils:IsEnchantingItem(itemLink)
return false
end
-- Check if a bag is Enchanting Bag (parallel to IsHerbBag)
-- Check if a bag is Enchanting Bag
function Utils:IsEnchantBag(bagID)
-- Skip backpack, bank, and keyring
if bagID == 0 or bagID == -1 or bagID == -2 then
return false
end
local invSlot = ContainerIDToInventoryID(bagID)
if not invSlot then return false end
local link = GetInventoryItemLink("player", invSlot)
if not link then return false end
-- Prefer specialized type detection
local bagType = self:GetSpecializedBagType(bagID)
if bagType == "enchant" then return true end
-- Fallback: tooltip scan for "Enchanting Bag"
local tooltip = GetScanTooltip()
tooltip:ClearLines()
tooltip:SetInventoryItem("player", invSlot)
for i = 1, tooltip:NumLines() do
local line = getglobal("GudaBagScanTooltipTextLeft" .. i)
if line then
local text = line:GetText()
if text and string.find(string.lower(text), "enchanting bag") then
return true
end
end
end
return false
return self:IsBagType(bagID, "enchant")
end
+3 -1
View File
@@ -2,17 +2,19 @@
## Title: Guda
## Notes: All-in-one bag and bank addon for World of Warcraft 1.12.1 (Turtle WoW)
## Author: Vati
## Version: 1.6.1
## Version: 1.6.2
## SavedVariables: Guda_DB
## SavedVariablesPerCharacter: Guda_CharDB
Localization.lua
Core\Init.lua
Core\Constants.lua
Core\Database.lua
DB\QuestItems.lua
Core\Events.lua
Core\Utils.lua
Core\CategoryManager.lua
Core\Tooltip.lua
Data\BagScanner.lua
+19 -5
View File
@@ -590,10 +590,17 @@ local function AddSortKeys(items)
item.isMount = isMount
-- Class and slot ordering
if itemRarity == 0 or IsItemGrayTooltip(item.bagID, item.slot, item.data.link) then
-- Check for items that should be treated as junk:
-- 1. Gray items (quality 0)
-- 2. Items with gray tooltip
-- 3. White equippable items (quality 1 Weapon/Armor) - vendor trash
local isGrayItem = itemRarity == 0 or IsItemGrayTooltip(item.bagID, item.slot, item.data.link)
local isWhiteEquip = (itemRarity == 1) and (itemCategory == "Weapon" or itemCategory == "Armor")
if isGrayItem or isWhiteEquip then
item.sortedClass = CATEGORY_ORDER["Junk"] or 99
item.equipSlotOrder = 999
item.isEquippable = false -- Treat gray gear as junk, not gear
item.isEquippable = false -- Treat junk gear as junk, not gear
elseif isEquippable then
item.sortedClass = 1 -- All equippable gear gets priority class
item.equipSlotOrder = EQUIP_SLOT_ORDER[itemSubType] or 999
@@ -1042,12 +1049,19 @@ local function BuildGreyTailPositions(bagIDs, greyCount)
return tailSlots
end
-- Split a list of collected items into non-greys and greys (quality 0)
-- Split a list of collected items into non-junk and junk items
-- Junk includes: gray items (quality 0), gray tooltip items, white equippable items (quality 1 Weapon/Armor)
local function SplitGreyItems(items)
local nonGreys, greys = {}, {}
for _, item in ipairs(items) do
-- Use same logic as AddSortKeys for determining Junk/Grey status (stability)
if tonumber(item.quality or 0) == 0 or IsItemGrayTooltip(item.bagID, item.slot, item.data.link) then
-- Use same logic as AddSortKeys for determining Junk status (stability)
local quality = tonumber(item.quality or 0)
local isGray = quality == 0 or IsItemGrayTooltip(item.bagID, item.slot, item.data.link)
-- White equippable items (Weapon/Armor) are also treated as junk
local itemClass = item.class or ""
local isWhiteEquip = (quality == 1) and (itemClass == "Weapon" or itemClass == "Armor")
if isGray or isWhiteEquip then
table.insert(greys, item)
else
table.insert(nonGreys, item)
+156 -3
View File
@@ -385,6 +385,9 @@ function BagFrame:Update()
-- Update money
self:UpdateMoney()
-- Update hearthstone
self:UpdateHearthstone()
-- Update bag slots info
self:UpdateBagSlotsInfo(bagData, isOtherChar)
@@ -504,9 +507,16 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
headerIdx = headerIdx + 1
header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", startX + currentX, startY - currentY)
header:SetWidth(blockWidth)
-- Get display name from category definition
local displayName = catName
header.fullName = catName
if addon.Modules.CategoryManager then
local catDef = addon.Modules.CategoryManager:GetCategory(catName)
if catDef and catDef.name then
displayName = catDef.name
end
end
header.fullName = displayName
header.isShortened = false
if string.len(displayName) > 8 and numItems < 2 then
displayName = string.sub(displayName, 1, 6) .. "..."
@@ -571,6 +581,10 @@ 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
@@ -935,6 +949,141 @@ function BagFrame:CreateMoneyFrame()
addon:Debug("MoneyFrame created via CreateMoneyFrame")
end
-- Hearthstone item ID
local HEARTHSTONE_ID = 6948
-- Create hearthstone display frame
function BagFrame:CreateHearthstoneFrame()
local frameName = "Guda_BagFrame_HearthstoneFrame"
if getglobal(frameName) then return end
local frame = CreateFrame("Button", frameName, Guda_BagFrame)
frame:SetWidth(20)
frame:SetHeight(20)
frame:SetPoint("BOTTOMRIGHT", Guda_BagFrame, "BOTTOMRIGHT", -150, 16)
frame:SetFrameStrata("HIGH")
frame:SetFrameLevel(10)
-- Icon texture
local icon = frame:CreateTexture(frameName .. "_Icon", "ARTWORK")
icon:SetAllPoints(frame)
icon:SetTexture("Interface\\Icons\\INV_Misc_Rune_01")
-- Cooldown frame (use Model type with CooldownFrameTemplate in vanilla)
local cooldown = CreateFrame("Model", frameName .. "_Cooldown", frame, "CooldownFrameTemplate")
cooldown:SetAllPoints(frame)
cooldown:EnableMouse(false)
frame.cooldown = cooldown
-- Enable mouse and clicks
frame:EnableMouse(true)
frame:RegisterForClicks("LeftButtonUp", "RightButtonUp")
frame:SetScript("OnEnter", function()
BagFrame:ShowHearthstoneTooltip(this)
end)
frame:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
frame:SetScript("OnClick", function()
BagFrame:UseHearthstone()
end)
frame:SetScript("OnMouseUp", function()
if arg1 == "LeftButton" or arg1 == "RightButton" then
BagFrame:UseHearthstone()
end
end)
addon:Debug("HearthstoneFrame created")
end
-- Find hearthstone in bags
function BagFrame:FindHearthstone()
for bag = 0, 4 do
local numSlots = GetContainerNumSlots(bag)
for slot = 1, numSlots do
local link = GetContainerItemLink(bag, slot)
if link then
local itemId = addon.Modules.Utils:ExtractItemID(link)
if itemId and itemId == HEARTHSTONE_ID then
return bag, slot, link
end
end
end
end
return nil, nil, nil
end
-- Update hearthstone display
function BagFrame:UpdateHearthstone()
local hideFooter = addon.Modules.DB:GetSetting("hideFooter")
local frame = getglobal("Guda_BagFrame_HearthstoneFrame")
if hideFooter then
if frame then frame:Hide() end
return
end
if not frame then
self:CreateHearthstoneFrame()
frame = getglobal("Guda_BagFrame_HearthstoneFrame")
end
if not frame then return end
local bag, slot, link = self:FindHearthstone()
if bag then
frame:SetAlpha(1)
frame:Show()
-- Update cooldown
local start, duration, enable = GetContainerItemCooldown(bag, slot)
if frame.cooldown and start and duration and duration > 0 then
CooldownFrame_SetTimer(frame.cooldown, start, duration, enable)
elseif frame.cooldown then
frame.cooldown:Hide()
end
-- Store location for use
frame.bag = bag
frame.slot = slot
frame.link = link
else
frame:SetAlpha(0.3)
frame:Show()
frame.bag = nil
frame.slot = nil
frame.link = nil
if frame.cooldown then
frame.cooldown:Hide()
end
end
end
-- Show hearthstone tooltip
function BagFrame:ShowHearthstoneTooltip(frame)
if frame.bag and frame.slot then
GameTooltip:SetOwner(frame, "ANCHOR_TOP")
GameTooltip:SetBagItem(frame.bag, frame.slot)
GameTooltip:Show()
else
GameTooltip:SetOwner(frame, "ANCHOR_TOP")
GameTooltip:ClearLines()
GameTooltip:AddLine("Hearthstone", 1, 1, 1)
GameTooltip:AddLine("Not in bags", 1, 0, 0)
GameTooltip:Show()
end
end
-- Use hearthstone
function BagFrame:UseHearthstone()
local frame = getglobal("Guda_BagFrame_HearthstoneFrame")
if frame and frame.bag and frame.slot then
UseContainerItem(frame.bag, frame.slot)
end
end
-- Setup tooltip on toolbar empty space
function BagFrame:SetupToolbarTooltip()
local toolbar = getglobal("Guda_BagFrame_Toolbar")
@@ -1889,17 +2038,21 @@ function BagFrame:UpdateFooterVisibility()
local hideFooter = addon.Modules.DB:GetSetting("hideFooter")
local toolbar = getglobal("Guda_BagFrame_Toolbar")
local moneyFrame = getglobal("Guda_BagFrame_MoneyFrame")
local hearthstoneFrame = getglobal("Guda_BagFrame_HearthstoneFrame")
if hideFooter then
if toolbar then toolbar:Hide() end
if moneyFrame then moneyFrame:Hide() end
if hearthstoneFrame then hearthstoneFrame:Hide() end
else
if toolbar then toolbar:Show() end
if moneyFrame then moneyFrame:Show() end
if hearthstoneFrame then hearthstoneFrame:Show() end
-- Trigger layout updates to ensure they are correctly positioned
self:UpdateBaglineLayout()
self:UpdateMoney()
self:UpdateHearthstone()
end
end
+65 -35
View File
@@ -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,8 +33,20 @@ 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})
-- Only show in Home section if Home category is enabled
local showHome = true
if addon.Modules.CategoryManager then
local homeCat = addon.Modules.CategoryManager:GetCategory("Home")
if homeCat then
showHome = homeCat.enabled
end
end
if showHome then
table.insert(specialItems.Hearthstone, {bagID = bagID, slotID = slotID, itemData = itemData})
end
-- Always return - if Home is disabled, Hearthstone is hidden completely
return
elseif addon.Modules.SortEngine and addon.Modules.SortEngine.IsMount and addon.Modules.SortEngine.IsMount(itemData.texture) then
table.insert(specialItems.Mount, {bagID = bagID, slotID = slotID, itemData = itemData})
@@ -44,42 +64,26 @@ 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")
if isQuestCategory then
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
local playerFaction = UnitFactionGroup("player")
isQuestItem = addon:IsQuestItemByID(itemID, playerFaction)
end
end
-- Priority 3: Quest Items (use consolidated detection)
local isQuestItem, _ = addon.Modules.Utils:IsQuestItem(bagID, slotID, itemData, isOtherChar, false)
if isQuestItem then
table.insert(categories["Quest"], {bagID = bagID, slotID = slotID, itemData = itemData})
return
@@ -106,7 +110,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 +121,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 +135,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 = {},
+347 -227
View File
@@ -8,90 +8,13 @@ local nextButtonID = 1
local scanTooltip = CreateFrame("GameTooltip", "Guda_QuestScanTooltip", nil, "GameTooltipTemplate")
scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
-- Check if an item is a quest item by scanning its tooltip
-- Check if an item is a quest item by scanning its tooltip and determine type
-- Helper function to check if an item is a quest item
-- Delegates to consolidated Utils:IsQuestItem() function
local function IsQuestItem(bagID, slotID, isBank)
bagID = tonumber(bagID)
slotID = tonumber(slotID)
if not bagID or not slotID then return false end
scanTooltip:ClearLines()
if isBank and bagID == -1 then
if scanTooltip.SetInventoryItem then
scanTooltip:SetInventoryItem("player", 39 + slotID)
else
scanTooltip:SetBagItem(bagID, slotID)
end
else
scanTooltip:SetBagItem(bagID, slotID)
if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.IsQuestItem then
return addon.Modules.Utils:IsQuestItem(bagID, slotID, nil, false, isBank)
end
local isQuestItem = false
local isQuestStarter = false
-- Check all tooltip lines for quest-related text
for i = 1, scanTooltip:NumLines() do
local line = getglobal("Guda_QuestScanTooltipTextLeft" .. i)
if line then
local text = line:GetText()
if text then
-- Check for quest starter patterns
if string.find(text, "Quest Starter") or
string.find(text, "This Item Begins a Quest") or
string.find(text, "Use: Starts a Quest") then
isQuestItem = true
isQuestStarter = true
break
-- Check for regular quest item patterns
elseif string.find(text, "Quest Item") or
string.find(text, "Manual") then
isQuestItem = true
-- Don't break, might still find a quest starter pattern
end
end
end
end
-- Also check item category/type via GetItemInfo for "Quest"
-- Turtle WoW GetItemInfo returns: name, link, rarity, level, itemCategory, itemType, stack, subType, texture, equipLoc, sellPrice
local link = GetContainerItemLink(bagID, slotID)
local itemID
local itemCategory, itemType
if link and addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.ExtractItemID and addon.Modules.Utils.GetItemInfoSafe then
itemID = addon.Modules.Utils:ExtractItemID(link)
if itemID then
_, _, _, _, itemCategory, itemType = addon.Modules.Utils:GetItemInfoSafe(itemID)
end
end
-- If it's a Weapon or Armor, it shouldn't be a QuestItem unless it's specifically categorized as Quest
-- This avoids "Use:" equipment showing up in the quest bar
if itemCategory == "Weapon" or itemCategory == "Armor" or itemType == "Weapon" or itemType == "Armor" then
if itemCategory ~= "Quest" and itemType ~= "Quest" then
isQuestItem = false
isQuestStarter = false
end
end
if not isQuestItem then
if itemCategory == "Quest" or itemType == "Quest" then
isQuestItem = true
end
end
-- Check the QuestItemsDB for known faction-specific quest items
if not isQuestItem then
if itemID and addon.IsQuestItemByID then
local playerFaction = UnitFactionGroup("player")
local isDBQuestItem = addon:IsQuestItemByID(itemID, playerFaction)
if isDBQuestItem then
isQuestItem = true
end
end
end
return isQuestItem, isQuestStarter
return false, false
end
--=====================================================
@@ -466,50 +389,31 @@ function Guda_ItemButton_UpdateCooldown(self)
end
end
-- Set item data
function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCharName, matchesFilter, isReadOnly)
-- Proactively convert to number to avoid comparisons with strings in downstream functions
bagID = tonumber(bagID)
slotID = tonumber(slotID)
-- Proactively clear any previous cooldown overlay state before reassigning this pooled button
do
local cd = getglobal(self:GetName().."Cooldown") or self.cooldown
if cd then
if CooldownFrame_SetTimer then
CooldownFrame_SetTimer(cd, 0, 0, 0)
elseif CooldownFrame_Set then
CooldownFrame_Set(cd, 0, 0, 0)
end
if cd.Hide then cd:Hide() end
end
end
--=====================================================
-- Helper functions for SetItem (extracted for clarity)
--=====================================================
-- Reset all visual state before reassigning pooled button
-- Reset all visual state on a button (for reuse from pool)
local function ResetButtonVisualState(self)
if self.questBorder then self.questBorder:Hide() end
if self.questIcon then self.questIcon:Hide() end
if self.qualityBorder then self.qualityBorder:Hide() end
if self.unusableOverlay then self.unusableOverlay:Hide() end
self.bagID = bagID
self.slotID = slotID
-- Also set the Blizzard slot ID for compatibility with ContainerFrameItemButtonTemplate behavior
-- ALWAYS set ID to something (0 if nil) to avoid leaking old IDs when button is reused
if self.SetID then
self:SetID(slotID or 0)
-- Clear cooldown overlay
local cd = getglobal(self:GetName().."Cooldown") or self.cooldown
if cd then
if CooldownFrame_SetTimer then
CooldownFrame_SetTimer(cd, 0, 0, 0)
elseif CooldownFrame_Set then
CooldownFrame_Set(cd, 0, 0, 0)
end
if cd.Hide then cd:Hide() end
end
-- Explicitly set bag index to avoid Blizzard's ContainerFrameItemButton_OnEnter logic
-- from picking up this button as part of a real bag.
self.bagIndex = bagID or -100 -- Use an invalid bag index for non-bag buttons
self.itemData = itemData
self.isBank = isBank or false
self.otherChar = otherCharName
self.isReadOnly = isReadOnly or false -- Track if this is read-only mode
self.isMail = false -- Clear mailbox flag by default
self.mailIndex = nil
self.mailItemIndex = nil
end
-- Re-register for drag/drop every time (crucial for button reuse in Classic/Vanilla)
-- Configure drag/drop registration based on read-only state
local function SetupDragDrop(self)
if not self.isReadOnly and not self.otherChar then
if self.RegisterForDrag then
self:RegisterForDrag("LeftButton")
@@ -529,137 +433,364 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
self:EnableMouse(true) -- Still enable mouse for tooltips
end
end
end
-- Default to true if not specified (for non-filtered displays)
-- Get display texture and count for an item slot
-- Returns: texture, count, hasItem (boolean)
local function GetItemDisplayInfo(bagID, slotID, itemData, isReadOnly)
local displayTexture, displayCount
local hasItem = false
if not isReadOnly then
-- LIVE MODE: Query game state directly
local liveTexture, liveCount = GetContainerItemInfo(bagID, slotID)
if liveTexture then
displayTexture = liveTexture
displayCount = liveCount
hasItem = true
elseif bagID == -2 then
-- Fallback for keyring in 1.12.1
local link = GetContainerItemLink(bagID, slotID)
if link then
hasItem = true
local _, _, _, _, _, _, _, _, itemTexture = GetItemInfo(link)
displayTexture = itemTexture
displayCount = 1
end
end
else
-- READ-ONLY MODE: Use cached itemData
if itemData and itemData.texture then
displayTexture = itemData.texture
displayCount = itemData.count
hasItem = true
end
end
return displayTexture, displayCount, hasItem
end
-- Update the tracking checkmark on an item button
local function UpdateTrackingCheckmark(self, Utils)
local check = getglobal(self:GetName().."_Check")
if not check then return end
local isTracked = false
if self.hasItem and self.itemData and self.itemData.link then
local itemID = Utils and Utils.ExtractItemID and Utils:ExtractItemID(self.itemData.link)
if itemID then
local trackedItems = Utils and Utils.SafeCall and Utils:SafeCall("DB", "GetSetting", "trackedItems") or {}
if trackedItems[itemID] then
isTracked = true
end
end
end
if isTracked then
check:Show()
else
check:Hide()
end
end
-- Resize empty slot background to match icon size
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)
end
-- Resize texture elements to match button size
local function ResizeTextureElements(self, iconSize)
-- Pushed texture
local pushedTexture = getglobal(self:GetName().."PushedTexture")
if not pushedTexture and self.GetPushedTexture then
pushedTexture = self:GetPushedTexture()
end
if pushedTexture then
pushedTexture:ClearAllPoints()
pushedTexture:SetPoint("CENTER", self, "CENTER", 0, 0)
pushedTexture:SetWidth(iconSize)
pushedTexture:SetHeight(iconSize)
end
-- Highlight texture
local highlightTexture = getglobal(self:GetName().."HighlightTexture")
if not highlightTexture and self.GetHighlightTexture then
highlightTexture = self:GetHighlightTexture()
end
if highlightTexture then
highlightTexture:ClearAllPoints()
highlightTexture:SetPoint("CENTER", self, "CENTER", 0, 0)
highlightTexture:SetWidth(iconSize)
highlightTexture:SetHeight(iconSize)
end
-- Checked texture
local checkedTexture = getglobal(self:GetName().."CheckedTexture")
if not checkedTexture and self.GetCheckedTexture then
checkedTexture = self:GetCheckedTexture()
end
if checkedTexture then
checkedTexture:ClearAllPoints()
checkedTexture:SetPoint("CENTER", self, "CENTER", 0, 0)
checkedTexture:SetWidth(iconSize)
checkedTexture:SetHeight(iconSize)
end
end
-- Position icon and borders based on icon size
local function PositionIconAndBorders(self, iconSize)
local iconTexture = getglobal(self:GetName().."IconTexture")
if not iconTexture then
iconTexture = getglobal(self:GetName().."Icon") or self.icon or self.Icon
end
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
iconTexture:ClearAllPoints()
iconTexture:SetPoint("CENTER", self, "CENTER", -0.5, 0.5)
iconTexture:SetWidth(iconDisplaySize)
iconTexture:SetHeight(iconDisplaySize)
iconTexture:SetTexCoord(0.08, 0.92, 0.08, 0.92)
iconTexture:Show()
-- 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)
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)
end
-- Position quest icon in top-right corner
if self.questIcon then
local questIconSize = math.max(12, math.min(20, iconSize * 0.35))
self.questIcon:SetWidth(questIconSize)
self.questIcon:SetHeight(questIconSize)
self.questIcon:ClearAllPoints()
self.questIcon:SetPoint("TOPRIGHT", self, "TOPRIGHT", 1, 0)
end
end
-- Update quality border display
local function UpdateQualityBorder(self, itemQuality, itemLink, bagID, Utils)
if not self.qualityBorder then return end
if bagID == -2 then
-- Special border for keyring items (cyan/blue)
self.qualityBorder:SetBackdropBorderColor(0.2, 0.8, 1.0, 1)
self.qualityBorder:Show()
return
end
if not itemQuality then
self.qualityBorder:Hide()
return
end
-- Check settings
local showEquipmentBorder, showOtherBorder
if Utils and Utils.SafeCall then
showEquipmentBorder = Utils:SafeCall("DB", "GetSetting", "showQualityBorderEquipment")
showOtherBorder = Utils:SafeCall("DB", "GetSetting", "showQualityBorderOther")
end
if showEquipmentBorder == nil then showEquipmentBorder = true end
if showOtherBorder == nil then showOtherBorder = true end
-- Check if item is equipment
local isEquipment = false
if itemLink and Utils and Utils.IsEquipment then
isEquipment = Utils:IsEquipment(itemLink)
end
local shouldShowBorder = (isEquipment and showEquipmentBorder) or (not isEquipment and showOtherBorder)
if shouldShowBorder then
local r, g, b = 1, 1, 1
if Utils and Utils.GetQualityColor then
r, g, b = Utils:GetQualityColor(itemQuality)
end
self.qualityBorder:SetBackdropBorderColor(r, g, b, 1)
self.qualityBorder:Show()
else
self.qualityBorder:Hide()
end
end
-- Clear item button for empty slot
local function ClearItemButton(self, emptySlotBg, countText, bagID)
self.hasItem = false
if SetItemButtonTexture then SetItemButtonTexture(self, nil) end
if SetItemButtonCount then SetItemButtonCount(self, 0) end
if SetItemButtonDesaturated then SetItemButtonDesaturated(self, false) end
-- Clear cooldown overlay
local cooldown = getglobal(self:GetName().."Cooldown") or self.cooldown
if cooldown and cooldown.Hide then cooldown:Hide() end
-- Clear icon texture
local iconTexture = getglobal(self:GetName().."IconTexture")
if not iconTexture then
iconTexture = getglobal(self:GetName().."Icon") or self.icon or self.Icon
end
if iconTexture then
iconTexture:SetTexture(nil)
iconTexture:Hide()
end
-- Clear unusable tint
if SetItemButtonTextureVertexColor then
SetItemButtonTextureVertexColor(self, 1.0, 1.0, 1.0)
end
if self.unusableOverlay and self.unusableOverlay.Hide then
self.unusableOverlay:Hide()
end
-- Hide normal texture
self:SetNormalTexture("")
local normalBorder = getglobal(self:GetName().."NormalTexture")
if normalBorder then normalBorder:SetTexture("") end
-- Show/hide empty slot background
if emptySlotBg then
emptySlotBg:Show()
emptySlotBg:SetAlpha(0.5)
end
if countText then countText:Hide() end
-- Handle quality border for empty keyring slots
if self.qualityBorder then
if bagID == -2 then
self.qualityBorder:SetBackdropBorderColor(0.2, 0.8, 1.0, 0.5)
self.qualityBorder:Show()
else
self.qualityBorder:Hide()
end
end
-- Hide quest elements
if self.questBorder then self.questBorder:Hide() end
if self.questIcon then self.questIcon:Hide() end
end
--=====================================================
-- Main SetItem function (orchestrates helper functions)
--=====================================================
-- Set item data
function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCharName, matchesFilter, isReadOnly)
-- Proactively convert to number to avoid comparisons with strings in downstream functions
bagID = tonumber(bagID)
slotID = tonumber(slotID)
-- Reset all visual state before reassigning pooled button
ResetButtonVisualState(self)
-- Set button properties
self.bagID = bagID
self.slotID = slotID
if self.SetID then
self:SetID(slotID or 0)
end
self.bagIndex = bagID or -100
self.itemData = itemData
self.isBank = isBank or false
self.otherChar = otherCharName
self.isReadOnly = isReadOnly or false
self.isMail = false
self.mailIndex = nil
self.mailItemIndex = nil
self.mailData = nil
-- Configure drag/drop
SetupDragDrop(self)
-- Default to true if not specified
if matchesFilter == nil then
matchesFilter = true
end
self.mailData = nil -- Clear mail metadata by default
-- Use Blizzard's default count fontstring (ContainerFrameItemButtonTemplate creates $parentCount)
-- Get UI elements
local countText = getglobal(self:GetName().."Count")
local emptySlotBg = getglobal(self:GetName().."_EmptySlotBg")
local Utils = addon and addon.Modules and addon.Modules.Utils
-- Apply icon size setting (nil-safe)
-- Get icon size setting
local iconSize = 37
if addon and addon.Modules and addon.Modules.DB and addon.Modules.DB.GetSetting then
iconSize = addon.Modules.DB:GetSetting("iconSize") or iconSize
elseif Guda and Guda.Modules and Guda.Modules.DB and Guda.Modules.DB.GetSetting then
iconSize = Guda.Modules.DB:GetSetting("iconSize") or iconSize
if Utils and Utils.SafeCall then
iconSize = Utils:SafeCall("DB", "GetSetting", "iconSize") or iconSize
end
if addon and addon.Constants and addon.Constants.BUTTON_SIZE then
if addon and addon.Constants then
iconSize = iconSize or addon.Constants.BUTTON_SIZE
end
self:SetWidth(iconSize)
self:SetHeight(iconSize)
-- In live mode (readOnly=false), query real-time game state instead of cached DB
-- In read-only mode (readOnly=true), use cached itemData from DB
local displayTexture, displayCount
-- Get item display info (texture, count, hasItem)
local displayTexture, displayCount, hasItem = GetItemDisplayInfo(bagID, slotID, itemData, self.isReadOnly)
self.hasItem = hasItem
if not self.isReadOnly then
-- LIVE MODE: Always query game state directly, never use cached itemData
local liveTexture, liveCount = GetContainerItemInfo(bagID, slotID)
if liveTexture then
displayTexture = liveTexture
displayCount = liveCount
self.hasItem = true
elseif bagID == -2 then
-- Fallback for keyring in 1.12.1
local link = GetContainerItemLink(bagID, slotID)
if link then
self.hasItem = true
-- We might not have the texture from GetContainerItemInfo, try to get it from GetItemInfo
local _, _, _, _, _, _, _, _, itemTexture = GetItemInfo(link)
displayTexture = itemTexture
displayCount = 1 -- Keyring items are usually unique anyway
else
self.hasItem = false
end
else
-- No item in this slot (even if itemData has cached data)
self.hasItem = false
end
else
-- READ-ONLY MODE: Use cached itemData from DB (can't query other characters)
if itemData and itemData.texture then
displayTexture = itemData.texture
displayCount = itemData.count
self.hasItem = true
else
self.hasItem = false
end
end
-- Apply the determined texture and count
-- Apply display based on whether slot has item
if self.hasItem then
if SetItemButtonTexture then
SetItemButtonTexture(self, displayTexture)
-- Set texture
if SetItemButtonTexture then
SetItemButtonTexture(self, displayTexture)
end
-- Explicitly set and show icon texture as SetItemButtonTexture can be unreliable for custom paths in 1.12
local iconTexture = getglobal(self:GetName().."IconTexture") or getglobal(self:GetName().."Icon") or self.icon or self.Icon
if iconTexture and displayTexture then
iconTexture:SetTexture(displayTexture)
iconTexture:Show()
end
-- Set count
if SetItemButtonCount then SetItemButtonCount(self, displayCount or 1) end
if emptySlotBg then emptySlotBg:Hide() end
-- Update cooldown overlay for live items
if not self.isReadOnly and not self.otherChar and Guda_ItemButton_UpdateCooldown then
Guda_ItemButton_UpdateCooldown(self)
else
-- Ensure cooldown is hidden for read-only/other character views
local cd = getglobal(self:GetName().."Cooldown") or self.cooldown
if cd and cd.Hide then cd:Hide() end
end
-- Update unusable red overlay tint
if Guda_ItemButton_UpdateUsableTint then
Guda_ItemButton_UpdateUsableTint(self)
Guda_ItemButton_UpdateUsableTint(self)
end
else
-- Fully clear all item button state for empty slots
if SetItemButtonTexture then SetItemButtonTexture(self, nil) end
if SetItemButtonCount then SetItemButtonCount(self, 0) end
if SetItemButtonDesaturated then SetItemButtonDesaturated(self, false) end
-- Ensure cooldown overlay is hidden for empty slots
local cooldown = getglobal(self:GetName().."Cooldown") or self.cooldown
if cooldown and cooldown.Hide then cooldown:Hide() end
-- Also clear the icon texture directly
local iconTexture = getglobal(self:GetName().."IconTexture")
if not iconTexture then
iconTexture = getglobal(self:GetName().."Icon") or self.icon or self.Icon
end
if iconTexture then
iconTexture:SetTexture(nil)
iconTexture:Hide()
end
if emptySlotBg then emptySlotBg:Show() end
-- Ensure any unusable tint is cleared on empty
if SetItemButtonTextureVertexColor then
SetItemButtonTextureVertexColor(self, 1.0, 1.0, 1.0)
end
-- Clear unusable overlay for empty slots ✅ FIX: Clear red overlay when item is sold
if self.unusableOverlay and self.unusableOverlay.Hide then
self.unusableOverlay:Hide()
end
-- Clear empty slot
ClearItemButton(self, emptySlotBg, countText, bagID)
end
-- Update tracking checkmark
UpdateTrackingCheckmark(self, Utils)
local check = getglobal(self:GetName().."_Check")
if check then
local isTracked = false
if self.hasItem then
local itemID = addon.Modules.Utils:ExtractItemID(self.itemData and self.itemData.link)
if self.hasItem and self.itemData and self.itemData.link then
local itemID = Utils and Utils.ExtractItemID and Utils:ExtractItemID(self.itemData.link)
if itemID then
local trackedItems = addon.Modules.DB:GetSetting("trackedItems") or {}
local trackedItems = Utils and Utils.SafeCall and Utils:SafeCall("DB", "GetSetting", "trackedItems") or {}
if trackedItems[itemID] then
isTracked = true
end
@@ -735,10 +866,8 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
if countText and countText.GetFont then
local font, _, flags = countText:GetFont()
local fontSize = 12
if addon and addon.Modules and addon.Modules.DB and addon.Modules.DB.GetSetting then
fontSize = addon.Modules.DB:GetSetting("iconFontSize") or fontSize
elseif Guda and Guda.Modules and Guda.Modules.DB and Guda.Modules.DB.GetSetting then
fontSize = Guda.Modules.DB:GetSetting("iconFontSize") or fontSize
if Utils and Utils.SafeCall then
fontSize = Utils:SafeCall("DB", "GetSetting", "iconFontSize") or fontSize
end
countText:SetFont(font, fontSize, flags)
@@ -814,14 +943,11 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
self.qualityBorder:SetBackdropBorderColor(0.2, 0.8, 1.0, 1)
self.qualityBorder:Show()
elseif itemQuality then
-- Check settings to determine if we should show borders (nil-safe)
-- Check settings to determine if we should show borders
local showEquipmentBorder, showOtherBorder
if addon and addon.Modules and addon.Modules.DB and addon.Modules.DB.GetSetting then
showEquipmentBorder = addon.Modules.DB:GetSetting("showQualityBorderEquipment")
showOtherBorder = addon.Modules.DB:GetSetting("showQualityBorderOther")
elseif Guda and Guda.Modules and Guda.Modules.DB and Guda.Modules.DB.GetSetting then
showEquipmentBorder = Guda.Modules.DB:GetSetting("showQualityBorderEquipment")
showOtherBorder = Guda.Modules.DB:GetSetting("showQualityBorderOther")
if Utils and Utils.SafeCall then
showEquipmentBorder = Utils:SafeCall("DB", "GetSetting", "showQualityBorderEquipment")
showOtherBorder = Utils:SafeCall("DB", "GetSetting", "showQualityBorderOther")
end
-- Default to true if settings not found
@@ -832,14 +958,10 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
showOtherBorder = true
end
-- Check if item is equipment (nil-safe)
-- Check if item is equipment
local isEquipment = false
if itemLink then
if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.IsEquipment then
isEquipment = addon.Modules.Utils:IsEquipment(itemLink)
elseif Guda and Guda.Modules and Guda.Modules.Utils and Guda.Modules.Utils.IsEquipment then
isEquipment = Guda.Modules.Utils:IsEquipment(itemLink)
end
if itemLink and Utils and Utils.IsEquipment then
isEquipment = Utils:IsEquipment(itemLink)
end
-- Determine if we should show the border based on item type and settings
@@ -848,10 +970,8 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
if shouldShowBorder then
-- Show colored border for all items (Poor, Common, Uncommon, Rare, Epic, etc.)
local r, g, b = 1, 1, 1
if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.GetQualityColor then
r, g, b = addon.Modules.Utils:GetQualityColor(itemQuality)
elseif Guda and Guda.Modules and Guda.Modules.Utils and Guda.Modules.Utils.GetQualityColor then
r, g, b = Guda.Modules.Utils:GetQualityColor(itemQuality)
if Utils and Utils.GetQualityColor then
r, g, b = Utils:GetQualityColor(itemQuality)
end
self.qualityBorder:SetBackdropBorderColor(r, g, b, 1)
self.qualityBorder:Show()
+800
View File
@@ -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,798 @@ 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(160)
nameText:SetJustifyH("LEFT")
row.nameText = nameText
-- Edit button
local editBtn = CreateFrame("Button", rowName .. "_EditBtn", row, "UIPanelButtonTemplate")
editBtn:SetWidth(40)
editBtn:SetHeight(18)
editBtn:SetPoint("LEFT", nameText, "RIGHT", 5, 0)
editBtn:SetText("Edit")
editBtn:SetScript("OnClick", function()
local catId = this:GetParent().categoryId
if catId then
Guda_CategoryEditor_Open(catId)
end
end)
row.editBtn = editBtn
-- Move Up button
local upBtn = CreateFrame("Button", rowName .. "_UpBtn", row)
upBtn:SetWidth(20)
upBtn:SetHeight(20)
upBtn:SetPoint("LEFT", editBtn, "RIGHT", 5, 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
-- Hide all controls for hideControls categories (only checkbox visible)
if categoryDef.hideControls then
row.editBtn:Hide()
row.upBtn:Hide()
row.downBtn:Hide()
row.deleteBtn:Hide()
row.builtInText:Hide()
else
row.editBtn:Show()
row.upBtn:Show()
row.downBtn:Show()
-- Enable/disable move buttons based on position
if dataIndex == 1 then
row.upBtn:Disable()
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
end
if dataIndex == totalCategories then
row.downBtn:Disable()
else
row.downBtn:Enable()
end
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 button texts
local addBtn = getglobal("Guda_SettingsPopup_AddCategoryButton")
if addBtn then
addBtn:SetText("+ Add Category")
end
local resetBtn = getglobal("Guda_SettingsPopup_ResetCategoriesButton")
if resetBtn then
resetBtn:SetText("Reset Defaults")
end
end
-- Add new custom category
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,
icon = "Interface\\Icons\\INV_Misc_QuestionMark",
rules = {},
matchMode = "any",
priority = 80,
enabled = true,
isBuiltIn = false,
}
-- Add to database
if Guda.Modules.CategoryManager:AddCategory(newId, newDef) then
-- Update display
Guda_SettingsPopup_CategoriesTab_Update()
-- Open editor for the new category
Guda_CategoryEditor_Open(newId)
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
-------------------------------------------
-- Category Editor Functions
-------------------------------------------
local editorCategoryId = nil
local editorMatchMode = "any"
local editorRules = {}
local editorRuleFrames = {}
local RULE_ROW_HEIGHT = 28
local MAX_RULES = 6
-- Rule type options for dropdown
local RULE_TYPE_OPTIONS = {
{ id = "itemType", name = "Item Type" },
{ id = "itemSubtype", name = "Item Subtype" },
{ id = "namePattern", name = "Name Contains" },
{ id = "itemID", name = "Item ID" },
{ id = "quality", name = "Quality (exact)" },
{ id = "qualityMin", name = "Quality (min)" },
{ id = "isBoE", name = "Bind on Equip" },
{ id = "isQuestItem", name = "Quest Item" },
{ id = "isJunk", name = "Is Junk" },
{ id = "restoreTag", name = "Restore Type" },
{ id = "isSoulShard", name = "Soul Shard" },
{ id = "isProjectile", name = "Projectile" },
}
-- Value options for specific rule types
local RULE_VALUE_OPTIONS = {
itemType = { "Armor", "Weapon", "Consumable", "Container", "Trade Goods", "Projectile", "Quiver", "Reagent", "Recipe", "Key", "Miscellaneous", "Quest" },
quality = { "0 - Poor", "1 - Common", "2 - Uncommon", "3 - Rare", "4 - Epic", "5 - Legendary" },
qualityMin = { "0 - Poor", "1 - Common", "2 - Uncommon", "3 - Rare", "4 - Epic", "5 - Legendary" },
isBoE = { "true", "false" },
isQuestItem = { "true", "false" },
isJunk = { "true", "false" },
isSoulShard = { "true", "false" },
isProjectile = { "true", "false" },
restoreTag = { "eat", "drink", "restore" },
}
-- OnLoad for Category Editor
function Guda_CategoryEditor_OnLoad(self)
Guda:ApplyBackdrop(self, "DEFAULT_FRAME")
-- Set button texts
local addBtn = getglobal("Guda_CategoryEditor_AddRuleButton")
if addBtn then addBtn:SetText("+ Add Rule") end
local saveBtn = getglobal("Guda_CategoryEditor_SaveButton")
if saveBtn then saveBtn:SetText("Save") end
local cancelBtn = getglobal("Guda_CategoryEditor_CancelButton")
if cancelBtn then cancelBtn:SetText("Cancel") end
-- Create radio button labels
local anyRadio = getglobal("Guda_CategoryEditor_MatchAny")
if anyRadio then
local label = anyRadio:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
label:SetPoint("LEFT", anyRadio, "RIGHT", 2, 0)
label:SetText("Any rule")
end
local allRadio = getglobal("Guda_CategoryEditor_MatchAll")
if allRadio then
local label = allRadio:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
label:SetPoint("LEFT", allRadio, "RIGHT", 2, 0)
label:SetText("All rules")
end
end
-- OnShow for Category Editor
function Guda_CategoryEditor_OnShow(self)
Guda_CategoryEditor_UpdateRulesDisplay()
end
-- Open Category Editor for a specific category
function Guda_CategoryEditor_Open(categoryId)
if not Guda.Modules.CategoryManager then return end
local categoryDef = Guda.Modules.CategoryManager:GetCategory(categoryId)
if not categoryDef then return end
editorCategoryId = categoryId
editorMatchMode = categoryDef.matchMode or "any"
-- Copy rules
editorRules = {}
if categoryDef.rules then
for i, rule in ipairs(categoryDef.rules) do
table.insert(editorRules, { type = rule.type, value = rule.value })
end
end
-- Set title
local title = getglobal("Guda_CategoryEditor_Title")
if title then
if categoryDef.isBuiltIn then
title:SetText("Edit Category (Built-in)")
else
title:SetText("Edit Category")
end
end
-- Set name
local nameBox = getglobal("Guda_CategoryEditor_NameEditBox")
if nameBox then
nameBox:SetText(categoryDef.name or categoryId)
-- Disable name editing for built-in categories
if categoryDef.isBuiltIn then
nameBox:EnableMouse(false)
nameBox:EnableKeyboard(false)
nameBox:SetTextColor(0.5, 0.5, 0.5)
else
nameBox:EnableMouse(true)
nameBox:EnableKeyboard(true)
nameBox:SetTextColor(1, 1, 1)
end
end
-- Set match mode
Guda_CategoryEditor_SetMatchMode(editorMatchMode)
-- Show editor
local editor = getglobal("Guda_CategoryEditor")
if editor then
editor:Show()
end
end
-- Set match mode (radio buttons)
function Guda_CategoryEditor_SetMatchMode(mode)
editorMatchMode = mode
local anyRadio = getglobal("Guda_CategoryEditor_MatchAny")
local allRadio = getglobal("Guda_CategoryEditor_MatchAll")
if anyRadio then anyRadio:SetChecked(mode == "any" and 1 or 0) end
if allRadio then allRadio:SetChecked(mode == "all" and 1 or 0) end
end
-- Get or create a rule row frame
local function GetRuleRowFrame(index)
if editorRuleFrames[index] then
return editorRuleFrames[index]
end
local container = getglobal("Guda_CategoryEditor_RulesContainer")
if not container then return nil end
local rowName = "Guda_CategoryEditor_RuleRow" .. index
local row = CreateFrame("Frame", rowName, container)
row:SetHeight(RULE_ROW_HEIGHT)
row:SetWidth(360)
row:SetPoint("TOPLEFT", container, "TOPLEFT", 0, -((index - 1) * RULE_ROW_HEIGHT))
-- Rule type dropdown button
local typeBtn = CreateFrame("Button", rowName .. "_TypeBtn", row, "UIPanelButtonTemplate")
typeBtn:SetWidth(120)
typeBtn:SetHeight(22)
typeBtn:SetPoint("LEFT", row, "LEFT", 0, 0)
typeBtn:SetText("Select Type")
typeBtn.ruleIndex = index
typeBtn:SetScript("OnClick", function()
Guda_CategoryEditor_ShowTypeDropdown(this, this.ruleIndex)
end)
row.typeBtn = typeBtn
-- Value input (editbox for text, button for dropdowns)
local valueBox = CreateFrame("EditBox", rowName .. "_ValueBox", row, "InputBoxTemplate")
valueBox:SetWidth(140)
valueBox:SetHeight(22)
valueBox:SetPoint("LEFT", typeBtn, "RIGHT", 5, 0)
valueBox:SetAutoFocus(false)
valueBox.ruleIndex = index
valueBox:SetScript("OnTextChanged", function()
local idx = this.ruleIndex
if editorRules[idx] then
editorRules[idx].value = this:GetText()
end
end)
valueBox:SetScript("OnEscapePressed", function() this:ClearFocus() end)
valueBox:SetScript("OnEnterPressed", function() this:ClearFocus() end)
row.valueBox = valueBox
-- Value dropdown button (for predefined values)
local valueBtn = CreateFrame("Button", rowName .. "_ValueBtn", row, "UIPanelButtonTemplate")
valueBtn:SetWidth(140)
valueBtn:SetHeight(22)
valueBtn:SetPoint("LEFT", typeBtn, "RIGHT", 5, 0)
valueBtn:SetText("Select Value")
valueBtn.ruleIndex = index
valueBtn:SetScript("OnClick", function()
Guda_CategoryEditor_ShowValueDropdown(this, this.ruleIndex)
end)
valueBtn:Hide()
row.valueBtn = valueBtn
-- Delete button
local deleteBtn = CreateFrame("Button", rowName .. "_DeleteBtn", row, "UIPanelCloseButton")
deleteBtn:SetWidth(22)
deleteBtn:SetHeight(22)
deleteBtn:SetPoint("LEFT", valueBox, "RIGHT", 5, 0)
deleteBtn.ruleIndex = index
deleteBtn:SetScript("OnClick", function()
Guda_CategoryEditor_RemoveRule(this.ruleIndex)
end)
row.deleteBtn = deleteBtn
editorRuleFrames[index] = row
return row
end
-- Update rules display
function Guda_CategoryEditor_UpdateRulesDisplay()
local numRules = table.getn(editorRules)
for i = 1, MAX_RULES do
local row = GetRuleRowFrame(i)
if row then
if i <= numRules then
local rule = editorRules[i]
row.ruleIndex = i
row.typeBtn.ruleIndex = i
row.valueBox.ruleIndex = i
row.valueBtn.ruleIndex = i
row.deleteBtn.ruleIndex = i
-- Set type button text
local typeName = "Select Type"
for _, opt in ipairs(RULE_TYPE_OPTIONS) do
if opt.id == rule.type then
typeName = opt.name
break
end
end
row.typeBtn:SetText(typeName)
-- Show appropriate value input
if RULE_VALUE_OPTIONS[rule.type] then
-- Use dropdown for predefined values
row.valueBox:Hide()
row.valueBtn:Show()
local displayValue = tostring(rule.value or "Select")
-- Format quality display
if (rule.type == "quality" or rule.type == "qualityMin") and type(rule.value) == "number" then
local qualNames = { [0]="Poor", [1]="Common", [2]="Uncommon", [3]="Rare", [4]="Epic", [5]="Legendary" }
displayValue = rule.value .. " - " .. (qualNames[rule.value] or "")
end
row.valueBtn:SetText(displayValue)
else
-- Use editbox for text input
row.valueBtn:Hide()
row.valueBox:Show()
row.valueBox:SetText(tostring(rule.value or ""))
end
row:Show()
else
row:Hide()
end
end
end
-- Enable/disable Add Rule button
local addBtn = getglobal("Guda_CategoryEditor_AddRuleButton")
if addBtn then
if numRules >= MAX_RULES then
addBtn:Disable()
else
addBtn:Enable()
end
end
end
-- Add a new rule
function Guda_CategoryEditor_AddRule()
if table.getn(editorRules) >= MAX_RULES then return end
table.insert(editorRules, { type = "itemType", value = "Consumable" })
Guda_CategoryEditor_UpdateRulesDisplay()
end
-- Remove a rule
function Guda_CategoryEditor_RemoveRule(index)
if index > 0 and index <= table.getn(editorRules) then
table.remove(editorRules, index)
Guda_CategoryEditor_UpdateRulesDisplay()
end
end
-- Helper to set rule type (called from dropdown)
function Guda_CategoryEditor_SetRuleType(ruleIndex, typeId)
if not editorRules[ruleIndex] then return end
editorRules[ruleIndex].type = typeId
-- Reset value when type changes
if RULE_VALUE_OPTIONS[typeId] then
editorRules[ruleIndex].value = RULE_VALUE_OPTIONS[typeId][1]
-- Convert to proper type
if typeId == "quality" or typeId == "qualityMin" then
editorRules[ruleIndex].value = 0
elseif typeId == "isBoE" or typeId == "isQuestItem" or typeId == "isSoulShard" or typeId == "isProjectile" then
editorRules[ruleIndex].value = true
end
else
editorRules[ruleIndex].value = ""
end
Guda_CategoryEditor_UpdateRulesDisplay()
end
-- Helper to set rule value (called from dropdown)
function Guda_CategoryEditor_SetRuleValue(ruleIndex, val, ruleType)
if not editorRules[ruleIndex] then return end
if ruleType == "quality" or ruleType == "qualityMin" then
local num = tonumber(string.sub(val, 1, 1))
editorRules[ruleIndex].value = num or 0
elseif ruleType == "isBoE" or ruleType == "isQuestItem" or ruleType == "isSoulShard" or ruleType == "isProjectile" then
editorRules[ruleIndex].value = (val == "true")
else
editorRules[ruleIndex].value = val
end
Guda_CategoryEditor_UpdateRulesDisplay()
end
-- Show type dropdown menu
function Guda_CategoryEditor_ShowTypeDropdown(button, ruleIndex)
local menu = {}
for i = 1, table.getn(RULE_TYPE_OPTIONS) do
local opt = RULE_TYPE_OPTIONS[i]
table.insert(menu, {
text = opt.name,
ruleIndex = ruleIndex,
typeId = opt.id,
})
end
Guda_ShowSimpleDropdown(button, menu, "type")
end
-- Show value dropdown menu
function Guda_CategoryEditor_ShowValueDropdown(button, ruleIndex)
local rule = editorRules[ruleIndex]
if not rule then return end
local options = RULE_VALUE_OPTIONS[rule.type]
if not options then return end
local menu = {}
for i = 1, table.getn(options) do
local val = options[i]
table.insert(menu, {
text = val,
ruleIndex = ruleIndex,
ruleType = rule.type,
value = val,
})
end
Guda_ShowSimpleDropdown(button, menu, "value")
end
-- Simple dropdown menu helper
local dropdownFrame = nil
function Guda_ShowSimpleDropdown(anchor, menuItems, menuType)
if not dropdownFrame then
dropdownFrame = CreateFrame("Frame", "Guda_SimpleDropdown", UIParent)
dropdownFrame:SetFrameStrata("FULLSCREEN_DIALOG")
dropdownFrame:SetWidth(150)
dropdownFrame:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 }
})
dropdownFrame:SetBackdropColor(0, 0, 0, 1)
dropdownFrame:EnableMouse(true)
dropdownFrame:Hide()
dropdownFrame:SetScript("OnLeave", function()
-- Hide after a short delay if mouse leaves
this.hideTimer = 0.5
end)
dropdownFrame:SetScript("OnUpdate", function()
if this.hideTimer then
this.hideTimer = this.hideTimer - arg1
if this.hideTimer <= 0 then
this.hideTimer = nil
-- Check if mouse is over any child
if not MouseIsOver(this) then
this:Hide()
end
end
end
end)
end
-- Clear old buttons
local children = { dropdownFrame:GetChildren() }
for _, child in ipairs(children) do
child:Hide()
child:SetParent(nil)
end
-- Create menu buttons
local btnHeight = 20
local totalHeight = 10
for i, item in ipairs(menuItems) do
local btn = CreateFrame("Button", nil, dropdownFrame)
btn:SetWidth(140)
btn:SetHeight(btnHeight)
btn:SetPoint("TOPLEFT", dropdownFrame, "TOPLEFT", 5, -(5 + (i-1) * btnHeight))
local text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
text:SetPoint("LEFT", btn, "LEFT", 5, 0)
text:SetText(item.text)
btn.text = text
local highlight = btn:CreateTexture(nil, "HIGHLIGHT")
highlight:SetAllPoints(btn)
highlight:SetTexture(1, 1, 1, 0.2)
-- Store data on button for vanilla Lua closure compatibility
btn.menuType = menuType
btn.ruleIndex = item.ruleIndex
btn.typeId = item.typeId
btn.ruleType = item.ruleType
btn.value = item.value
btn:SetScript("OnClick", function()
dropdownFrame:Hide()
if this.menuType == "type" then
Guda_CategoryEditor_SetRuleType(this.ruleIndex, this.typeId)
elseif this.menuType == "value" then
Guda_CategoryEditor_SetRuleValue(this.ruleIndex, this.value, this.ruleType)
end
end)
btn:SetScript("OnEnter", function()
dropdownFrame.hideTimer = nil
end)
totalHeight = totalHeight + btnHeight
end
totalHeight = totalHeight + 5
dropdownFrame:SetHeight(totalHeight)
dropdownFrame:ClearAllPoints()
dropdownFrame:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, 0)
dropdownFrame:Show()
dropdownFrame.hideTimer = nil
end
-- Save category changes
function Guda_CategoryEditor_Save()
if not editorCategoryId or not Guda.Modules.CategoryManager then return end
local categoryDef = Guda.Modules.CategoryManager:GetCategory(editorCategoryId)
if not categoryDef then return end
-- Get name (only for custom categories)
local nameBox = getglobal("Guda_CategoryEditor_NameEditBox")
if nameBox and not categoryDef.isBuiltIn then
categoryDef.name = nameBox:GetText()
end
-- Set match mode
categoryDef.matchMode = editorMatchMode
-- Set rules
categoryDef.rules = {}
for _, rule in ipairs(editorRules) do
if rule.type and rule.type ~= "" then
table.insert(categoryDef.rules, { type = rule.type, value = rule.value })
end
end
-- Save to database
Guda.Modules.CategoryManager:UpdateCategory(editorCategoryId, categoryDef)
-- Refresh displays
Guda_SettingsPopup_CategoriesTab_Update()
Guda_SettingsPopup_RefreshBagFrames()
-- Close editor
local editor = getglobal("Guda_CategoryEditor")
if editor then editor:Hide() end
Guda:Print("Category '" .. (categoryDef.name or editorCategoryId) .. "' saved.")
end
-- Initialize
function SettingsPopup:Initialize()
Guda:Debug("Settings popup initialized")
+379 -5
View File
@@ -62,7 +62,7 @@
<!-- Tab Buttons (GudaPlates style) -->
<Button name="Guda_SettingsPopup_GeneralTabButton" enableMouse="true">
<Size>
<AbsDimension x="150" y="28"/>
<AbsDimension x="113" y="28"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parent" relativePoint="TOPLEFT">
@@ -94,7 +94,7 @@
<Button name="Guda_SettingsPopup_IconsTabButton" enableMouse="true">
<Size>
<AbsDimension x="150" y="28"/>
<AbsDimension x="113" y="28"/>
</Size>
<Anchors>
<Anchor point="LEFT" relativeTo="Guda_SettingsPopup_GeneralTabButton" relativePoint="RIGHT">
@@ -124,9 +124,9 @@
</Scripts>
</Button>
<Button name="Guda_SettingsPopup_GuideTabButton" enableMouse="true">
<Button name="Guda_SettingsPopup_CategoriesTabButton" enableMouse="true">
<Size>
<AbsDimension x="150" y="28"/>
<AbsDimension x="113" y="28"/>
</Size>
<Anchors>
<Anchor point="LEFT" relativeTo="Guda_SettingsPopup_IconsTabButton" relativePoint="RIGHT">
@@ -142,7 +142,39 @@
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString name="$parent_Text" inherits="GameFontNormal" text="Quick Guide">
<FontString name="$parent_Text" inherits="GameFontNormal" text="Categories">
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnClick>
Guda_SettingsPopup_SelectTab("categories")
</OnClick>
</Scripts>
</Button>
<Button name="Guda_SettingsPopup_GuideTabButton" enableMouse="true">
<Size>
<AbsDimension x="113" y="28"/>
</Size>
<Anchors>
<Anchor point="LEFT" relativeTo="Guda_SettingsPopup_CategoriesTabButton" relativePoint="RIGHT">
<Offset>
<AbsDimension x="2" y="0"/>
</Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parent_Bg" setAllPoints="true">
<Color r="1" g="1" b="1" a="0.1"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString name="$parent_Text" inherits="GameFontNormal" text="Guide">
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
@@ -600,6 +632,106 @@
</Frames>
</Frame>
<!-- Categories Tab Content -->
<Frame name="Guda_SettingsPopup_CategoriesTab" hidden="true">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parent" relativePoint="TOPLEFT">
<Offset>
<AbsDimension x="15" y="-90"/>
</Offset>
</Anchor>
<Anchor point="BOTTOMRIGHT" relativeTo="$parent" relativePoint="BOTTOMRIGHT">
<Offset>
<AbsDimension x="-15" y="15"/>
</Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parent_Header" inherits="GameFontNormal" text="Manage item categories and their display order:">
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="5" y="-5"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- Scroll Frame for Category List -->
<ScrollFrame name="Guda_SettingsPopup_CategoriesScrollFrame" inherits="FauxScrollFrameTemplate">
<Size>
<AbsDimension x="440" y="320"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="5" y="-25"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnVerticalScroll>
FauxScrollFrame_OnVerticalScroll(18, Guda_SettingsPopup_CategoriesTab_Update)
</OnVerticalScroll>
<OnShow>
Guda_SettingsPopup_CategoriesTab_Update()
</OnShow>
</Scripts>
</ScrollFrame>
<!-- Category Row Container -->
<Frame name="Guda_SettingsPopup_CategoryListContainer">
<Size>
<AbsDimension x="420" y="320"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="Guda_SettingsPopup_CategoriesScrollFrame" relativePoint="TOPLEFT"/>
</Anchors>
</Frame>
<!-- Add Category Button -->
<Button name="Guda_SettingsPopup_AddCategoryButton" inherits="UIPanelButtonTemplate">
<Size>
<AbsDimension x="120" y="25"/>
</Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="$parent" relativePoint="BOTTOMLEFT">
<Offset>
<AbsDimension x="5" y="10"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
Guda_SettingsPopup_AddCategory_OnClick()
</OnClick>
</Scripts>
</Button>
<!-- Reset to Defaults Button -->
<Button name="Guda_SettingsPopup_ResetCategoriesButton" inherits="UIPanelButtonTemplate">
<Size>
<AbsDimension x="120" y="25"/>
</Size>
<Anchors>
<Anchor point="LEFT" relativeTo="Guda_SettingsPopup_AddCategoryButton" relativePoint="RIGHT">
<Offset>
<AbsDimension x="10" y="0"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
Guda_SettingsPopup_ResetCategories_OnClick()
</OnClick>
</Scripts>
</Button>
</Frames>
</Frame>
<!-- Guide Tab Content -->
<Frame name="Guda_SettingsPopup_GuideTab" hidden="true">
<Anchors>
@@ -651,4 +783,246 @@
</Scripts>
</Frame>
<!-- Category Editor Popup -->
<Frame name="Guda_CategoryEditor" toplevel="true" movable="true" enableMouse="true" hidden="true" parent="UIParent" frameStrata="DIALOG">
<Size>
<AbsDimension x="400" y="380"/>
</Size>
<Anchors>
<Anchor point="CENTER" relativePoint="CENTER" relativeTo="UIParent">
<Offset>
<AbsDimension x="0" y="50"/>
</Offset>
</Anchor>
</Anchors>
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
<BackgroundInsets>
<AbsInset left="11" right="12" top="12" bottom="11"/>
</BackgroundInsets>
<TileSize>
<AbsValue val="32"/>
</TileSize>
<EdgeSize>
<AbsValue val="32"/>
</EdgeSize>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<!-- Title -->
<FontString name="$parent_Title" inherits="GameFontNormalLarge" text="Edit Category">
<Anchors>
<Anchor point="TOP">
<Offset>
<AbsDimension x="0" y="-15"/>
</Offset>
</Anchor>
</Anchors>
<Color r="1" g="0.82" b="0"/>
</FontString>
<!-- Name Label -->
<FontString name="$parent_NameLabel" inherits="GameFontNormal" text="Name:">
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="20" y="-45"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
<!-- Match Mode Label -->
<FontString name="$parent_MatchModeLabel" inherits="GameFontNormal" text="Match Mode:">
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="20" y="-80"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
<!-- Rules Label -->
<FontString name="$parent_RulesLabel" inherits="GameFontNormal" text="Rules:">
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="20" y="-115"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<!-- Close Button -->
<Button name="$parent_CloseButton" inherits="UIPanelCloseButton">
<Anchors>
<Anchor point="TOPRIGHT">
<Offset>
<AbsDimension x="-5" y="-5"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
Guda_CategoryEditor:Hide()
</OnClick>
</Scripts>
</Button>
<!-- Name EditBox -->
<EditBox name="Guda_CategoryEditor_NameEditBox" autoFocus="false" inherits="InputBoxTemplate">
<Size>
<AbsDimension x="200" y="20"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="80" y="-42"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnEscapePressed>
this:ClearFocus()
</OnEscapePressed>
<OnEnterPressed>
this:ClearFocus()
</OnEnterPressed>
</Scripts>
</EditBox>
<!-- Match Mode: Any Radio -->
<CheckButton name="Guda_CategoryEditor_MatchAny" inherits="UIRadioButtonTemplate">
<Size>
<AbsDimension x="20" y="20"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="100" y="-77"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
Guda_CategoryEditor_SetMatchMode("any")
</OnClick>
</Scripts>
</CheckButton>
<!-- Match Mode: All Radio -->
<CheckButton name="Guda_CategoryEditor_MatchAll" inherits="UIRadioButtonTemplate">
<Size>
<AbsDimension x="20" y="20"/>
</Size>
<Anchors>
<Anchor point="LEFT" relativeTo="Guda_CategoryEditor_MatchAny" relativePoint="RIGHT">
<Offset>
<AbsDimension x="60" y="0"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
Guda_CategoryEditor_SetMatchMode("all")
</OnClick>
</Scripts>
</CheckButton>
<!-- Rules Container -->
<Frame name="Guda_CategoryEditor_RulesContainer">
<Size>
<AbsDimension x="360" y="180"/>
</Size>
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="20" y="-130"/>
</Offset>
</Anchor>
</Anchors>
</Frame>
<!-- Add Rule Button -->
<Button name="Guda_CategoryEditor_AddRuleButton" inherits="UIPanelButtonTemplate">
<Size>
<AbsDimension x="100" y="22"/>
</Size>
<Anchors>
<Anchor point="TOPRIGHT">
<Offset>
<AbsDimension x="-20" y="-112"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
Guda_CategoryEditor_AddRule()
</OnClick>
</Scripts>
</Button>
<!-- Save Button -->
<Button name="Guda_CategoryEditor_SaveButton" inherits="UIPanelButtonTemplate">
<Size>
<AbsDimension x="100" y="25"/>
</Size>
<Anchors>
<Anchor point="BOTTOMRIGHT">
<Offset>
<AbsDimension x="-20" y="15"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
Guda_CategoryEditor_Save()
</OnClick>
</Scripts>
</Button>
<!-- Cancel Button -->
<Button name="Guda_CategoryEditor_CancelButton" inherits="UIPanelButtonTemplate">
<Size>
<AbsDimension x="100" y="25"/>
</Size>
<Anchors>
<Anchor point="RIGHT" relativeTo="Guda_CategoryEditor_SaveButton" relativePoint="LEFT">
<Offset>
<AbsDimension x="-10" y="0"/>
</Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
Guda_CategoryEditor:Hide()
</OnClick>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnLoad>
Guda_CategoryEditor_OnLoad(this)
</OnLoad>
<OnShow>
Guda_CategoryEditor_OnShow(this)
</OnShow>
<OnMouseDown>
if (arg1 == "LeftButton") then
this:StartMoving()
end
</OnMouseDown>
<OnMouseUp>
this:StopMovingOrSizing()
</OnMouseUp>
</Scripts>
</Frame>
</Ui>