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 Guda_CharDB.settings.markUnusableItems = true
end end
-- Initialize CategoryManager for custom categories
if addon.Modules.CategoryManager then
addon.Modules.CategoryManager:Initialize()
end
-- Initialize this character's data -- Initialize this character's data
if not Guda_DB.characters[fullName] then if not Guda_DB.characters[fullName] then
local localizedClass, englishClass = UnitClass("player") local localizedClass, englishClass = UnitClass("player")
+200 -240
View File
@@ -4,261 +4,221 @@ local addon = Guda
local Tooltip = {} local Tooltip = {}
addon.Modules.Tooltip = 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) -- Helper function to get item ID from link (Lua 5.0 compatible)
local function GetItemIDFromLink(link) 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 if type(link) == "number" then return link end
local _, _, itemID = string.find(link, "item:(%d+)")
-- Try to find itemID in a standard link or a raw item:ID string return itemID and tonumber(itemID) or nil
local _, _, itemID = string.find(link, "item:(%d+)")
return itemID and tonumber(itemID) or nil
end 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 function CountCurrentCharacterItems(itemID)
local bagCount = 0 local bagCount = 0
local bankCount = 0 local bankCount = 0
local mailCount = 0 local mailCount = 0
local equippedCount = 0 local equippedCount = 0
-- Count current character's bags in real-time -- Count bags in real-time
local bagsToCount = {0, 1, 2, 3, 4, -2} bagCount = CountFromLiveContainer({0, 1, 2, 3, 4, -2}, itemID)
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 current character's bank in real-time if bank is open -- Count bank: live if open, otherwise from saved data
local bankFrame = getglobal("BankFrame") local bankFrame = getglobal("BankFrame")
if bankFrame and bankFrame:IsVisible() then if bankFrame and bankFrame:IsVisible() then
-- Main bank slots (-1) -- Main bank + bank bags
local numMainSlots = GetContainerNumSlots(-1) or 24 bankCount = CountFromLiveContainer({-1, 5, 6, 7, 8, 9, 10, 11}, itemID)
for slot = 1, numMainSlots do else
local link = GetContainerItemLink(-1, slot) -- Use saved data
if link then local playerName = addon.Modules.DB:GetPlayerFullName()
local slotItemID = GetItemIDFromLink(link) local charData = Guda_DB and Guda_DB.characters and Guda_DB.characters[playerName]
if slotItemID == itemID then if charData then
local _, count = GetContainerItemInfo(-1, slot) bankCount = CountFromSavedContainers(charData.bank, itemID)
bankCount = bankCount + (count or 1) end
end end
end
end
-- Bank bags (5-11) -- Count mailbox: live if open, otherwise from saved data
for bagID = 5, 11 do if addon.Modules.MailboxScanner and addon.Modules.MailboxScanner:IsMailboxOpen() then
local numSlots = GetContainerNumSlots(bagID) mailCount = CountFromLiveMailbox(itemID)
if numSlots and numSlots > 0 then else
for slot = 1, numSlots do local playerName = addon.Modules.DB:GetPlayerFullName()
local link = GetContainerItemLink(bagID, slot) local charData = Guda_DB and Guda_DB.characters and Guda_DB.characters[playerName]
if link then if charData then
local slotItemID = GetItemIDFromLink(link) mailCount = CountFromSavedMailbox(charData.mailbox, itemID)
if slotItemID == itemID then end
local _, count = GetContainerItemInfo(bagID, slot) end
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 current character's mailbox in real-time if mailbox is open -- Count equipped items in real-time
if addon.Modules.MailboxScanner and addon.Modules.MailboxScanner:IsMailboxOpen() then equippedCount = CountFromLiveEquipped(itemID)
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
-- Fallback: if we don't have the count but header says there's an item, assume at least 1. return bagCount, bankCount, equippedCount, mailCount
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
end 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) local function CountItemsForCharacter(itemID, characterData, isCurrentChar)
-- For current character, use real-time counting to avoid database sync issues -- For current character, use real-time counting
if isCurrentChar then if isCurrentChar then
return CountCurrentCharacterItems(itemID) return CountCurrentCharacterItems(itemID)
end end
-- For other characters, use saved data -- For other characters, use saved data
local bagCount = 0 local bagCount = CountFromSavedContainers(characterData.bags, itemID)
local bankCount = 0 local bankCount = CountFromSavedContainers(characterData.bank, itemID)
local mailCount = 0 local mailCount = CountFromSavedMailbox(characterData.mailbox, itemID)
local equippedCount = 0 local equippedCount = CountFromSavedEquipped(characterData.equipped, itemID)
-- Count bags from saved data return bagCount, bankCount, equippedCount, mailCount
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
end end
+241 -210
View File
@@ -5,6 +5,64 @@ local addon = Guda
local Utils = {} local Utils = {}
addon.Modules.Utils = 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 -- Format money (copper to gold/silver/copper string) - WoW 1.12.1 version
function Utils:FormatMoney(copper, showZero, useColors) function Utils:FormatMoney(copper, showZero, useColors)
if not copper or copper == 0 then if not copper or copper == 0 then
@@ -190,49 +248,123 @@ local function GetScanTooltip()
return scanTooltip return scanTooltip
end end
-- Check if an item is a quest item by scanning its tooltip -- Check if an item is a quest item by scanning its tooltip (internal helper)
function Utils:IsQuestItemTooltip(bagID, slotID) -- Returns: isQuestItem, isQuestStarter
if not bagID or not slotID then return false end 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 for i = 1, tooltip:NumLines() do
local line = getglobal("GudaBagScanTooltipTextLeft" .. i) local line = getglobal(tooltipName .. "TextLeft" .. i)
if line then if line then
local text = line:GetText() local text = line:GetText()
if text then if text then
local tl = string.lower(text) local tl = string.lower(text)
-- Only match explicit quest markers to avoid misclassifying consumables/recipes -- Check for quest starter patterns first
-- Include "manual" which some quest items use (same as ItemButton detection)
if string.find(tl, "quest starter") or if string.find(tl, "quest starter") or
string.find(tl, "this item begins a quest") or string.find(tl, "this item begins a quest") or
string.find(tl, "starts a quest") or string.find(tl, "starts a quest") then
string.find(tl, "quest item") or isQuestItem = true
string.find(tl, "manual") then isQuestStarter = true
break
-- Double check category to avoid misidentifying equipment with "Use:" -- Check for regular quest item patterns
local link = GetContainerItemLink(bagID, slotID) elseif string.find(tl, "quest item") or
if link and self.ExtractItemID and self.GetItemInfoSafe then string.find(tl, "manual") then
local itemID = self:ExtractItemID(link) isQuestItem = true
if itemID then -- Don't break, might still find a quest starter pattern
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
end end
end end
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 end
-- Check if an item has a gray title in its tooltip or link -- 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) .. "..." return string.sub(text, 1, maxLen - 3) .. "..."
end 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 -- Returns: "soul", "herb", "enchant", "quiver", "ammo", or nil
-- This is the consolidated bag type detection function with tooltip fallback
function Utils:GetSpecializedBagType(bagID) function Utils:GetSpecializedBagType(bagID)
-- Skip backpack, bank, and keyring -- Skip backpack, bank, and keyring
if bagID == 0 or bagID == -1 or bagID == -2 then if bagID == 0 or bagID == -1 or bagID == -2 then
@@ -437,44 +448,94 @@ function Utils:GetSpecializedBagType(bagID)
end end
local itemID = self:ExtractItemID(link) 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 end
local itemName, _, itemRarity, itemLevel, itemCategory, itemType, itemStackCount, itemSubType = self:GetItemInfoSafe(itemID) -- Fallback: tooltip scanning for all bag types
if itemType then local tooltip = GetScanTooltip()
-- Check for exact subtype matches tooltip:ClearLines()
local typeLower = string.lower(itemType) tooltip:SetInventoryItem("player", invSlot)
-- Soul Bag / Soul Pouch for i = 1, tooltip:NumLines() do
if string.find(typeLower, "soul bag") or string.find(typeLower, "soul pouch") then local line = getglobal("GudaBagScanTooltipTextLeft" .. i)
return "soul" if line then
end local text = line:GetText()
if text then
local textLower = string.lower(text)
-- Herb Bag -- Soul Bag / Soul Pouch
if string.find(typeLower, "herb bag") then if string.find(textLower, "soul bag") or string.find(textLower, "soul pouch") or
return "herb" (string.find(textLower, "soul") and (string.find(textLower, "bag") or string.find(textLower, "pouch"))) then
end return "soul"
end
-- Enchanting Bag -- Herb Bag
if string.find(typeLower, "enchanting bag") then if string.find(textLower, "herb bag") then
return "enchant" return "herb"
end end
-- Quiver -- Enchanting Bag
if string.find(typeLower, "quiver") then if string.find(textLower, "enchanting bag") then
return "quiver" return "enchant"
end end
-- Ammo Pouch -- Quiver
if string.find(typeLower, "ammo pouch") then if string.find(textLower, "quiver") then
return "ammo" return "quiver"
end
-- Ammo Pouch
if string.find(textLower, "ammo pouch") then
return "ammo"
end
end
end end
end end
return nil return nil
end 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) -- Get container priority for sorting (higher = more important)
function Utils:GetContainerPriority(bagID) function Utils:GetContainerPriority(bagID)
local bagType = self:GetSpecializedBagType(bagID) local bagType = self:GetSpecializedBagType(bagID)
@@ -727,37 +788,7 @@ function Utils:IsEnchantingItem(itemLink)
return false return false
end end
-- Check if a bag is Enchanting Bag (parallel to IsHerbBag) -- Check if a bag is Enchanting Bag
function Utils:IsEnchantBag(bagID) function Utils:IsEnchantBag(bagID)
-- Skip backpack, bank, and keyring return self:IsBagType(bagID, "enchant")
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
end end
+3 -1
View File
@@ -2,17 +2,19 @@
## Title: Guda ## Title: Guda
## Notes: All-in-one bag and bank addon for World of Warcraft 1.12.1 (Turtle WoW) ## Notes: All-in-one bag and bank addon for World of Warcraft 1.12.1 (Turtle WoW)
## Author: Vati ## Author: Vati
## Version: 1.6.1 ## Version: 1.6.2
## SavedVariables: Guda_DB ## SavedVariables: Guda_DB
## SavedVariablesPerCharacter: Guda_CharDB ## SavedVariablesPerCharacter: Guda_CharDB
Localization.lua Localization.lua
Core\Init.lua Core\Init.lua
Core\Constants.lua
Core\Database.lua Core\Database.lua
DB\QuestItems.lua DB\QuestItems.lua
Core\Events.lua Core\Events.lua
Core\Utils.lua Core\Utils.lua
Core\CategoryManager.lua
Core\Tooltip.lua Core\Tooltip.lua
Data\BagScanner.lua Data\BagScanner.lua
+19 -5
View File
@@ -590,10 +590,17 @@ local function AddSortKeys(items)
item.isMount = isMount item.isMount = isMount
-- Class and slot ordering -- 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.sortedClass = CATEGORY_ORDER["Junk"] or 99
item.equipSlotOrder = 999 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 elseif isEquippable then
item.sortedClass = 1 -- All equippable gear gets priority class item.sortedClass = 1 -- All equippable gear gets priority class
item.equipSlotOrder = EQUIP_SLOT_ORDER[itemSubType] or 999 item.equipSlotOrder = EQUIP_SLOT_ORDER[itemSubType] or 999
@@ -1042,12 +1049,19 @@ local function BuildGreyTailPositions(bagIDs, greyCount)
return tailSlots return tailSlots
end 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 function SplitGreyItems(items)
local nonGreys, greys = {}, {} local nonGreys, greys = {}, {}
for _, item in ipairs(items) do for _, item in ipairs(items) do
-- Use same logic as AddSortKeys for determining Junk/Grey status (stability) -- Use same logic as AddSortKeys for determining Junk status (stability)
if tonumber(item.quality or 0) == 0 or IsItemGrayTooltip(item.bagID, item.slot, item.data.link) then 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) table.insert(greys, item)
else else
table.insert(nonGreys, item) table.insert(nonGreys, item)
+156 -3
View File
@@ -385,6 +385,9 @@ function BagFrame:Update()
-- Update money -- Update money
self:UpdateMoney() self:UpdateMoney()
-- Update hearthstone
self:UpdateHearthstone()
-- Update bag slots info -- Update bag slots info
self:UpdateBagSlotsInfo(bagData, isOtherChar) self:UpdateBagSlotsInfo(bagData, isOtherChar)
@@ -504,9 +507,16 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
headerIdx = headerIdx + 1 headerIdx = headerIdx + 1
header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", startX + currentX, startY - currentY) header:SetPoint("TOPLEFT", itemContainer, "TOPLEFT", startX + currentX, startY - currentY)
header:SetWidth(blockWidth) header:SetWidth(blockWidth)
-- Get display name from category definition
local displayName = catName 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 header.isShortened = false
if string.len(displayName) > 8 and numItems < 2 then if string.len(displayName) > 8 and numItems < 2 then
displayName = string.sub(displayName, 1, 6) .. "..." displayName = string.sub(displayName, 1, 6) .. "..."
@@ -571,6 +581,10 @@ function BagFrame:DisplayItemsByCategory(bagData, isOtherChar, charName)
break break
end end
end end
-- Also check if Empty section should show
if totalFreeSlots > 0 then
hasAnyBottom = true
end
if hasAnyBottom then if hasAnyBottom then
y = y - 10 y = y - 10
@@ -935,6 +949,141 @@ function BagFrame:CreateMoneyFrame()
addon:Debug("MoneyFrame created via CreateMoneyFrame") addon:Debug("MoneyFrame created via CreateMoneyFrame")
end 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 -- Setup tooltip on toolbar empty space
function BagFrame:SetupToolbarTooltip() function BagFrame:SetupToolbarTooltip()
local toolbar = getglobal("Guda_BagFrame_Toolbar") local toolbar = getglobal("Guda_BagFrame_Toolbar")
@@ -1889,17 +2038,21 @@ function BagFrame:UpdateFooterVisibility()
local hideFooter = addon.Modules.DB:GetSetting("hideFooter") local hideFooter = addon.Modules.DB:GetSetting("hideFooter")
local toolbar = getglobal("Guda_BagFrame_Toolbar") local toolbar = getglobal("Guda_BagFrame_Toolbar")
local moneyFrame = getglobal("Guda_BagFrame_MoneyFrame") local moneyFrame = getglobal("Guda_BagFrame_MoneyFrame")
local hearthstoneFrame = getglobal("Guda_BagFrame_HearthstoneFrame")
if hideFooter then if hideFooter then
if toolbar then toolbar:Hide() end if toolbar then toolbar:Hide() end
if moneyFrame then moneyFrame:Hide() end if moneyFrame then moneyFrame:Hide() end
if hearthstoneFrame then hearthstoneFrame:Hide() end
else else
if toolbar then toolbar:Show() end if toolbar then toolbar:Show() end
if moneyFrame then moneyFrame:Show() end if moneyFrame then moneyFrame:Show() end
if hearthstoneFrame then hearthstoneFrame:Show() end
-- Trigger layout updates to ensure they are correctly positioned -- Trigger layout updates to ensure they are correctly positioned
self:UpdateBaglineLayout() self:UpdateBaglineLayout()
self:UpdateMoney() self:UpdateMoney()
self:UpdateHearthstone()
end end
end end
+65 -35
View File
@@ -5,10 +5,18 @@ local FrameHelpers = {}
addon.Modules.FrameHelpers = FrameHelpers addon.Modules.FrameHelpers = FrameHelpers
-- Standard category list used by both Bag and Bank frames -- Standard category list used by both Bag and Bank frames
-- Now dynamically built from CategoryManager if available
Guda_CategoryList = { Guda_CategoryList = {
"BoE", "Weapon", "Armor", "Consumable", "Food", "Drink", "Trade Goods", "Reagent", "Recipe", "Quiver", "Container", "Soul Bag", "Miscellaneous", "Quest", "Junk", "Class Items", "Keyring" "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 -- Categorize a single item into categories and specialItems tables
-- Returns nothing, modifies tables in place -- Returns nothing, modifies tables in place
function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, isOtherChar) function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, isOtherChar)
@@ -25,8 +33,20 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems,
end end
-- Priority 1: Special items (Hearthstone, Mounts, Tools) -- Priority 1: Special items (Hearthstone, Mounts, Tools)
-- These are handled separately and go into specialItems table, not categories
if string.find(itemName, "Hearthstone") then 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 return
elseif addon.Modules.SortEngine and addon.Modules.SortEngine.IsMount and addon.Modules.SortEngine.IsMount(itemData.texture) then elseif addon.Modules.SortEngine and addon.Modules.SortEngine.IsMount and addon.Modules.SortEngine.IsMount(itemData.texture) then
table.insert(specialItems.Mount, {bagID = bagID, slotID = slotID, itemData = itemData}) table.insert(specialItems.Mount, {bagID = bagID, slotID = slotID, itemData = itemData})
@@ -44,42 +64,26 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems,
return return
end 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) -- Priority 2: Class Items (Soul Shards, Arrows, Bullets)
if addon.Modules.Utils:IsSoulShard(itemData.link) or if addon.Modules.Utils:IsSoulShard(itemData.link) or
itemData.class == "Projectile" or itemData.class == "Projectile" or
itemData.subclass == "Arrow" or itemData.subclass == "Arrow" or
itemData.subclass == "Bullet" then itemData.subclass == "Bullet" then
table.insert(categories["Class Items"], {bagID = bagID, slotID = slotID, itemData = itemData}) table.insert(categories["Class Items"], {bagID = bagID, slotID = slotID, itemData = itemData})
return return
end end
-- Priority 3: Quest Items -- Priority 3: Quest Items (use consolidated detection)
-- Check multiple sources: tooltip scan, itemClass, itemType, and QuestItemsDB local isQuestItem, _ = addon.Modules.Utils:IsQuestItem(bagID, slotID, itemData, isOtherChar, false)
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
if isQuestItem then if isQuestItem then
table.insert(categories["Quest"], {bagID = bagID, slotID = slotID, itemData = itemData}) table.insert(categories["Quest"], {bagID = bagID, slotID = slotID, itemData = itemData})
return return
@@ -106,7 +110,7 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems,
return return
end 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 if (itemData.class == "Weapon" or itemData.class == "Armor") and not isOtherChar then
local isBoE = addon.Modules.Utils:IsBindOnEquip(bagID, slotID, itemData.link) local isBoE = addon.Modules.Utils:IsBindOnEquip(bagID, slotID, itemData.link)
if isBoE then if isBoE then
@@ -117,7 +121,7 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems,
return return
end 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 if (itemData.class == "Weapon" or itemData.class == "Armor") and isOtherChar then
table.insert(categories[itemData.class], {bagID = bagID, slotID = slotID, itemData = itemData}) table.insert(categories[itemData.class], {bagID = bagID, slotID = slotID, itemData = itemData})
return return
@@ -131,10 +135,36 @@ end
-- Initialize empty category tables -- Initialize empty category tables
function Guda_InitCategories() 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 = {} 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 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 = { local specialItems = {
Hearthstone = {}, Hearthstone = {},
Mount = {}, Mount = {},
+347 -227
View File
@@ -8,90 +8,13 @@ local nextButtonID = 1
local scanTooltip = CreateFrame("GameTooltip", "Guda_QuestScanTooltip", nil, "GameTooltipTemplate") local scanTooltip = CreateFrame("GameTooltip", "Guda_QuestScanTooltip", nil, "GameTooltipTemplate")
scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") scanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
-- Check if an item is a quest item by scanning its tooltip -- Helper function to check if an item is a quest item
-- Check if an item is a quest item by scanning its tooltip and determine type -- Delegates to consolidated Utils:IsQuestItem() function
local function IsQuestItem(bagID, slotID, isBank) local function IsQuestItem(bagID, slotID, isBank)
bagID = tonumber(bagID) if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.IsQuestItem then
slotID = tonumber(slotID) return addon.Modules.Utils:IsQuestItem(bagID, slotID, nil, false, isBank)
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)
end end
return false, false
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
end end
--===================================================== --=====================================================
@@ -466,50 +389,31 @@ function Guda_ItemButton_UpdateCooldown(self)
end end
end end
-- Set item data --=====================================================
function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCharName, matchesFilter, isReadOnly) -- Helper functions for SetItem (extracted for clarity)
-- 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
-- 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.questBorder then self.questBorder:Hide() end
if self.questIcon then self.questIcon:Hide() end if self.questIcon then self.questIcon:Hide() end
if self.qualityBorder then self.qualityBorder:Hide() end if self.qualityBorder then self.qualityBorder:Hide() end
if self.unusableOverlay then self.unusableOverlay:Hide() end if self.unusableOverlay then self.unusableOverlay:Hide() end
self.bagID = bagID -- Clear cooldown overlay
self.slotID = slotID local cd = getglobal(self:GetName().."Cooldown") or self.cooldown
-- Also set the Blizzard slot ID for compatibility with ContainerFrameItemButtonTemplate behavior if cd then
-- ALWAYS set ID to something (0 if nil) to avoid leaking old IDs when button is reused if CooldownFrame_SetTimer then
if self.SetID then CooldownFrame_SetTimer(cd, 0, 0, 0)
self:SetID(slotID or 0) elseif CooldownFrame_Set then
CooldownFrame_Set(cd, 0, 0, 0)
end
if cd.Hide then cd:Hide() end
end end
-- Explicitly set bag index to avoid Blizzard's ContainerFrameItemButton_OnEnter logic end
-- 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
-- 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 not self.isReadOnly and not self.otherChar then
if self.RegisterForDrag then if self.RegisterForDrag then
self:RegisterForDrag("LeftButton") 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 self:EnableMouse(true) -- Still enable mouse for tooltips
end end
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 if matchesFilter == nil then
matchesFilter = true matchesFilter = true
end end
self.mailData = nil -- Clear mail metadata by default -- Get UI elements
-- Use Blizzard's default count fontstring (ContainerFrameItemButtonTemplate creates $parentCount)
local countText = getglobal(self:GetName().."Count") local countText = getglobal(self:GetName().."Count")
local emptySlotBg = getglobal(self:GetName().."_EmptySlotBg") 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 local iconSize = 37
if addon and addon.Modules and addon.Modules.DB and addon.Modules.DB.GetSetting then if Utils and Utils.SafeCall then
iconSize = addon.Modules.DB:GetSetting("iconSize") or iconSize iconSize = Utils:SafeCall("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
end 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 iconSize = iconSize or addon.Constants.BUTTON_SIZE
end end
self:SetWidth(iconSize) self:SetWidth(iconSize)
self:SetHeight(iconSize) self:SetHeight(iconSize)
-- In live mode (readOnly=false), query real-time game state instead of cached DB -- Get item display info (texture, count, hasItem)
-- In read-only mode (readOnly=true), use cached itemData from DB local displayTexture, displayCount, hasItem = GetItemDisplayInfo(bagID, slotID, itemData, self.isReadOnly)
local displayTexture, displayCount self.hasItem = hasItem
if not self.isReadOnly then -- Apply display based on whether slot has item
-- 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
if self.hasItem then if self.hasItem then
if SetItemButtonTexture then -- Set texture
SetItemButtonTexture(self, displayTexture) if SetItemButtonTexture then
SetItemButtonTexture(self, displayTexture)
end 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 local iconTexture = getglobal(self:GetName().."IconTexture") or getglobal(self:GetName().."Icon") or self.icon or self.Icon
if iconTexture and displayTexture then if iconTexture and displayTexture then
iconTexture:SetTexture(displayTexture) iconTexture:SetTexture(displayTexture)
iconTexture:Show() iconTexture:Show()
end end
-- Set count
if SetItemButtonCount then SetItemButtonCount(self, displayCount or 1) end if SetItemButtonCount then SetItemButtonCount(self, displayCount or 1) end
if emptySlotBg then emptySlotBg:Hide() end if emptySlotBg then emptySlotBg:Hide() end
-- Update cooldown overlay for live items -- Update cooldown overlay for live items
if not self.isReadOnly and not self.otherChar and Guda_ItemButton_UpdateCooldown then if not self.isReadOnly and not self.otherChar and Guda_ItemButton_UpdateCooldown then
Guda_ItemButton_UpdateCooldown(self) Guda_ItemButton_UpdateCooldown(self)
else else
-- Ensure cooldown is hidden for read-only/other character views
local cd = getglobal(self:GetName().."Cooldown") or self.cooldown local cd = getglobal(self:GetName().."Cooldown") or self.cooldown
if cd and cd.Hide then cd:Hide() end if cd and cd.Hide then cd:Hide() end
end end
-- Update unusable red overlay tint -- Update unusable red overlay tint
if Guda_ItemButton_UpdateUsableTint then if Guda_ItemButton_UpdateUsableTint then
Guda_ItemButton_UpdateUsableTint(self) Guda_ItemButton_UpdateUsableTint(self)
end end
else else
-- Fully clear all item button state for empty slots -- Clear empty slot
if SetItemButtonTexture then SetItemButtonTexture(self, nil) end ClearItemButton(self, emptySlotBg, countText, bagID)
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
end end
-- Update tracking checkmark -- Update tracking checkmark
UpdateTrackingCheckmark(self, Utils)
local check = getglobal(self:GetName().."_Check") local check = getglobal(self:GetName().."_Check")
if check then if check then
local isTracked = false local isTracked = false
if self.hasItem then if self.hasItem and self.itemData and self.itemData.link then
local itemID = addon.Modules.Utils:ExtractItemID(self.itemData and self.itemData.link) local itemID = Utils and Utils.ExtractItemID and Utils:ExtractItemID(self.itemData.link)
if itemID then 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 if trackedItems[itemID] then
isTracked = true isTracked = true
end end
@@ -735,10 +866,8 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
if countText and countText.GetFont then if countText and countText.GetFont then
local font, _, flags = countText:GetFont() local font, _, flags = countText:GetFont()
local fontSize = 12 local fontSize = 12
if addon and addon.Modules and addon.Modules.DB and addon.Modules.DB.GetSetting then if Utils and Utils.SafeCall then
fontSize = addon.Modules.DB:GetSetting("iconFontSize") or fontSize fontSize = Utils:SafeCall("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
end end
countText:SetFont(font, fontSize, flags) 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:SetBackdropBorderColor(0.2, 0.8, 1.0, 1)
self.qualityBorder:Show() self.qualityBorder:Show()
elseif itemQuality then 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 local showEquipmentBorder, showOtherBorder
if addon and addon.Modules and addon.Modules.DB and addon.Modules.DB.GetSetting then if Utils and Utils.SafeCall then
showEquipmentBorder = addon.Modules.DB:GetSetting("showQualityBorderEquipment") showEquipmentBorder = Utils:SafeCall("DB", "GetSetting", "showQualityBorderEquipment")
showOtherBorder = addon.Modules.DB:GetSetting("showQualityBorderOther") showOtherBorder = Utils:SafeCall("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")
end end
-- Default to true if settings not found -- Default to true if settings not found
@@ -832,14 +958,10 @@ function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCha
showOtherBorder = true showOtherBorder = true
end end
-- Check if item is equipment (nil-safe) -- Check if item is equipment
local isEquipment = false local isEquipment = false
if itemLink then if itemLink and Utils and Utils.IsEquipment then
if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.IsEquipment then isEquipment = Utils:IsEquipment(itemLink)
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
end end
-- Determine if we should show the border based on item type and settings -- 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 if shouldShowBorder then
-- Show colored border for all items (Poor, Common, Uncommon, Rare, Epic, etc.) -- Show colored border for all items (Poor, Common, Uncommon, Rare, Epic, etc.)
local r, g, b = 1, 1, 1 local r, g, b = 1, 1, 1
if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.GetQualityColor then if Utils and Utils.GetQualityColor then
r, g, b = addon.Modules.Utils:GetQualityColor(itemQuality) r, g, b = 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)
end end
self.qualityBorder:SetBackdropBorderColor(r, g, b, 1) self.qualityBorder:SetBackdropBorderColor(r, g, b, 1)
self.qualityBorder:Show() self.qualityBorder:Show()
+800
View File
@@ -52,19 +52,23 @@ function Guda_SettingsPopup_SelectTab(tabName)
-- Hide all tab content frames -- Hide all tab content frames
local generalTab = getglobal("Guda_SettingsPopup_GeneralTab") local generalTab = getglobal("Guda_SettingsPopup_GeneralTab")
local iconsTab = getglobal("Guda_SettingsPopup_IconsTab") local iconsTab = getglobal("Guda_SettingsPopup_IconsTab")
local categoriesTab = getglobal("Guda_SettingsPopup_CategoriesTab")
local guideTab = getglobal("Guda_SettingsPopup_GuideTab") local guideTab = getglobal("Guda_SettingsPopup_GuideTab")
if generalTab then generalTab:Hide() end if generalTab then generalTab:Hide() end
if iconsTab then iconsTab:Hide() end if iconsTab then iconsTab:Hide() end
if categoriesTab then categoriesTab:Hide() end
if guideTab then guideTab:Hide() end if guideTab then guideTab:Hide() end
-- Reset all tab button backgrounds to inactive (0.1 alpha) -- Reset all tab button backgrounds to inactive (0.1 alpha)
local generalBg = getglobal("Guda_SettingsPopup_GeneralTabButton_Bg") local generalBg = getglobal("Guda_SettingsPopup_GeneralTabButton_Bg")
local iconsBg = getglobal("Guda_SettingsPopup_IconsTabButton_Bg") local iconsBg = getglobal("Guda_SettingsPopup_IconsTabButton_Bg")
local categoriesBg = getglobal("Guda_SettingsPopup_CategoriesTabButton_Bg")
local guideBg = getglobal("Guda_SettingsPopup_GuideTabButton_Bg") local guideBg = getglobal("Guda_SettingsPopup_GuideTabButton_Bg")
if generalBg then generalBg:SetTexture(1, 1, 1, 0.1) end if generalBg then generalBg:SetTexture(1, 1, 1, 0.1) end
if iconsBg then iconsBg: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 if guideBg then guideBg:SetTexture(1, 1, 1, 0.1) end
-- Show selected tab and highlight its button -- Show selected tab and highlight its button
@@ -74,6 +78,10 @@ function Guda_SettingsPopup_SelectTab(tabName)
elseif tabName == "icons" then elseif tabName == "icons" then
if iconsTab then iconsTab:Show() end if iconsTab then iconsTab:Show() end
if iconsBg then iconsBg:SetTexture(1, 1, 1, 0.3) 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 elseif tabName == "guide" then
if guideTab then guideTab:Show() end if guideTab then guideTab:Show() end
if guideBg then guideBg:SetTexture(1, 1, 1, 0.3) 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 -- No immediate UI update needed
end 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 -- Initialize
function SettingsPopup:Initialize() function SettingsPopup:Initialize()
Guda:Debug("Settings popup initialized") Guda:Debug("Settings popup initialized")
+379 -5
View File
@@ -62,7 +62,7 @@
<!-- Tab Buttons (GudaPlates style) --> <!-- Tab Buttons (GudaPlates style) -->
<Button name="Guda_SettingsPopup_GeneralTabButton" enableMouse="true"> <Button name="Guda_SettingsPopup_GeneralTabButton" enableMouse="true">
<Size> <Size>
<AbsDimension x="150" y="28"/> <AbsDimension x="113" y="28"/>
</Size> </Size>
<Anchors> <Anchors>
<Anchor point="TOPLEFT" relativeTo="$parent" relativePoint="TOPLEFT"> <Anchor point="TOPLEFT" relativeTo="$parent" relativePoint="TOPLEFT">
@@ -94,7 +94,7 @@
<Button name="Guda_SettingsPopup_IconsTabButton" enableMouse="true"> <Button name="Guda_SettingsPopup_IconsTabButton" enableMouse="true">
<Size> <Size>
<AbsDimension x="150" y="28"/> <AbsDimension x="113" y="28"/>
</Size> </Size>
<Anchors> <Anchors>
<Anchor point="LEFT" relativeTo="Guda_SettingsPopup_GeneralTabButton" relativePoint="RIGHT"> <Anchor point="LEFT" relativeTo="Guda_SettingsPopup_GeneralTabButton" relativePoint="RIGHT">
@@ -124,9 +124,9 @@
</Scripts> </Scripts>
</Button> </Button>
<Button name="Guda_SettingsPopup_GuideTabButton" enableMouse="true"> <Button name="Guda_SettingsPopup_CategoriesTabButton" enableMouse="true">
<Size> <Size>
<AbsDimension x="150" y="28"/> <AbsDimension x="113" y="28"/>
</Size> </Size>
<Anchors> <Anchors>
<Anchor point="LEFT" relativeTo="Guda_SettingsPopup_IconsTabButton" relativePoint="RIGHT"> <Anchor point="LEFT" relativeTo="Guda_SettingsPopup_IconsTabButton" relativePoint="RIGHT">
@@ -142,7 +142,39 @@
</Texture> </Texture>
</Layer> </Layer>
<Layer level="OVERLAY"> <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> <Anchors>
<Anchor point="CENTER"/> <Anchor point="CENTER"/>
</Anchors> </Anchors>
@@ -600,6 +632,106 @@
</Frames> </Frames>
</Frame> </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 --> <!-- Guide Tab Content -->
<Frame name="Guda_SettingsPopup_GuideTab" hidden="true"> <Frame name="Guda_SettingsPopup_GuideTab" hidden="true">
<Anchors> <Anchors>
@@ -651,4 +783,246 @@
</Scripts> </Scripts>
</Frame> </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> </Ui>