2666 lines
100 KiB
Lua
2666 lines
100 KiB
Lua
-- Local alias to the addon root table (must be defined before any usages below)
|
|
local addon = Guda
|
|
|
|
-- Item button pool
|
|
local buttonPool = {}
|
|
local nextButtonID = 1
|
|
local BUTTON_POOL_MAX = 500 -- Maximum buttons to create (bags ~80 + bank ~200 + keyring ~96 + buffer for both frames open)
|
|
|
|
-- Category drag-drop: track cursor item for category reassignment
|
|
local cursorItemInfo = nil -- { bagID, slotID, itemID, link }
|
|
local activeCategoryDropIndicator = nil -- the currently shown "+" indicator button
|
|
|
|
-- Get button pool statistics (for /guda perf command)
|
|
function Guda_GetButtonPoolStats()
|
|
local total = 0
|
|
local shown = 0
|
|
local hidden = 0
|
|
local inUse = 0
|
|
local available = 0
|
|
for _, button in pairs(buttonPool) do
|
|
total = total + 1
|
|
if button:IsShown() then
|
|
shown = shown + 1
|
|
else
|
|
hidden = hidden + 1
|
|
end
|
|
if button.inUse then
|
|
inUse = inUse + 1
|
|
else
|
|
available = available + 1
|
|
end
|
|
end
|
|
return {
|
|
total = total,
|
|
shown = shown,
|
|
hidden = hidden,
|
|
inUse = inUse,
|
|
available = available,
|
|
maxSize = BUTTON_POOL_MAX,
|
|
}
|
|
end
|
|
|
|
-- Reset button pool (for testing/debugging)
|
|
-- WARNING: Only call this when no bag/bank frames are visible!
|
|
function Guda_ResetButtonPool()
|
|
-- Hide and clear all buttons
|
|
for id, button in pairs(buttonPool) do
|
|
button:Hide()
|
|
button:ClearAllPoints()
|
|
-- Clear from parent tracking
|
|
local parent = button:GetParent()
|
|
if parent and parent.itemButtons then
|
|
parent.itemButtons[button] = nil
|
|
end
|
|
end
|
|
-- Clear the pool table
|
|
for k in pairs(buttonPool) do
|
|
buttonPool[k] = nil
|
|
end
|
|
-- Reset counter
|
|
nextButtonID = 1
|
|
end
|
|
|
|
-- Mark all buttons as available for reuse (called when switching view types)
|
|
-- This allows buttons from one frame to be reused by another
|
|
function Guda_ReleaseAllButtons()
|
|
for _, button in pairs(buttonPool) do
|
|
if not button.isBagSlot then
|
|
button.inUse = false
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Category drop indicator: a single shared frame (parented to UIParent) with plus icon
|
|
local HideCategoryDropIndicator -- forward declaration
|
|
local categoryDropIndicator = nil -- the single shared indicator frame
|
|
local dropIndicatorCategoryId = nil -- category the indicator is currently showing for
|
|
local dropCooldownTime = 0 -- GetTime() when cooldown expires
|
|
|
|
-- Helper: get category of cursor item using tracked info
|
|
local function GetCursorItemCategory()
|
|
local info = Guda_GetCursorItemInfo()
|
|
addon:Debug("GetCursorItemCat: info=%s", tostring(info ~= nil))
|
|
if not info or not info.itemID or not addon.Modules.CategoryManager then
|
|
addon:Debug("GetCursorItemCat: BAIL - info=%s itemID=%s catMgr=%s", tostring(info ~= nil), tostring(info and info.itemID), tostring(addon.Modules.CategoryManager ~= nil))
|
|
return nil
|
|
end
|
|
local itemName, itemLink, itemQuality, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture
|
|
if info.link then
|
|
itemName, itemLink, itemQuality, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture = GetItemInfo(info.link)
|
|
end
|
|
addon:Debug("GetCursorItemCat: itemName=%s itemType=%s", tostring(itemName), tostring(itemType))
|
|
if not itemName then return nil end
|
|
local itemData = {
|
|
link = info.link,
|
|
itemID = info.itemID,
|
|
name = itemName,
|
|
quality = itemQuality or 0,
|
|
class = itemType or "",
|
|
subClass = itemSubType or "",
|
|
equipLoc = itemEquipLoc or "",
|
|
stackCount = itemStackCount or 1,
|
|
level = itemLevel or 0,
|
|
minLevel = itemMinLevel or 0,
|
|
texture = itemTexture,
|
|
}
|
|
return addon.Modules.CategoryManager:CategorizeItem(itemData, info.bagID, info.slotID)
|
|
end
|
|
|
|
-- Create the single shared indicator frame (once)
|
|
local function GetOrCreateIndicator()
|
|
if categoryDropIndicator then return categoryDropIndicator end
|
|
|
|
local f = CreateFrame("Frame", "Guda_CategoryDropIndicator", UIParent)
|
|
f:SetWidth(36)
|
|
f:SetHeight(36)
|
|
f:SetFrameStrata("FULLSCREEN_DIALOG")
|
|
f:SetFrameLevel(500)
|
|
|
|
-- Slot background (same texture as empty bag slot, with green tint like GudaBags)
|
|
local slotBg = f:CreateTexture(nil, "BACKGROUND")
|
|
slotBg:SetPoint("TOPLEFT", f, "TOPLEFT", -9, 9)
|
|
slotBg:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 9, -9)
|
|
slotBg:SetTexture("Interface\\Buttons\\UI-EmptySlot")
|
|
slotBg:SetVertexColor(0.4, 0.8, 0.4, 0.9)
|
|
f.slotBg = slotBg
|
|
|
|
-- Plus icon centered
|
|
local plus = f:CreateTexture(nil, "OVERLAY")
|
|
plus:SetWidth(20)
|
|
plus:SetHeight(20)
|
|
plus:SetPoint("CENTER", f, "CENTER", 0, 0)
|
|
plus:SetTexture("Interface\\AddOns\\Guda\\Assets\\plus")
|
|
f.plus = plus
|
|
|
|
-- Mouse-enabled for drops
|
|
f:EnableMouse(true)
|
|
|
|
-- Handle drop on the indicator itself
|
|
local function DoIndicatorDrop()
|
|
if not activeCategoryDropIndicator then return end
|
|
local parentBtn = activeCategoryDropIndicator
|
|
if parentBtn and parentBtn:GetScript("OnReceiveDrag") then
|
|
local savedThis = getfenv(0)["this"]
|
|
getfenv(0)["this"] = parentBtn
|
|
parentBtn:GetScript("OnReceiveDrag")()
|
|
getfenv(0)["this"] = savedThis
|
|
end
|
|
end
|
|
|
|
f:SetScript("OnReceiveDrag", DoIndicatorDrop)
|
|
f:SetScript("OnMouseUp", function()
|
|
if CursorHasItem and CursorHasItem() then
|
|
DoIndicatorDrop()
|
|
end
|
|
end)
|
|
|
|
-- Tooltip on hover
|
|
f:SetScript("OnEnter", function()
|
|
if dropIndicatorCategoryId then
|
|
GameTooltip:SetOwner(this, "ANCHOR_TOP")
|
|
GameTooltip:SetText("Add item to this category", 1, 1, 1)
|
|
GameTooltip:AddLine("Drop here to permanently assign", 0.7, 0.7, 0.7)
|
|
GameTooltip:AddLine("this item to \"" .. tostring(dropIndicatorCategoryId) .. "\"", 0.5, 1, 0.5)
|
|
GameTooltip:Show()
|
|
end
|
|
end)
|
|
|
|
f:SetScript("OnLeave", function()
|
|
GameTooltip:Hide()
|
|
-- Delay hide: use an OnUpdate check instead of C_Timer
|
|
local elapsed = 0
|
|
this:SetScript("OnUpdate", function()
|
|
elapsed = elapsed + arg1
|
|
if elapsed >= 0.05 then
|
|
this:SetScript("OnUpdate", nil)
|
|
-- Check if mouse is still over the indicator or the parent button
|
|
if activeCategoryDropIndicator and MouseIsOver(activeCategoryDropIndicator) then
|
|
return
|
|
end
|
|
if this:IsMouseOver() then
|
|
return
|
|
end
|
|
HideCategoryDropIndicator()
|
|
end
|
|
end)
|
|
end)
|
|
|
|
f:Hide()
|
|
categoryDropIndicator = f
|
|
return f
|
|
end
|
|
|
|
local function ShowCategoryDropIndicator(button)
|
|
if activeCategoryDropIndicator == button then return end
|
|
|
|
-- Don't show during drop cooldown
|
|
if GetTime() < dropCooldownTime then return end
|
|
|
|
HideCategoryDropIndicator()
|
|
|
|
local ind = GetOrCreateIndicator()
|
|
local size = button:GetWidth()
|
|
|
|
-- Size to match icon
|
|
ind:SetWidth(size)
|
|
ind:SetHeight(size)
|
|
|
|
-- Update plus icon size (60% of icon, min 16)
|
|
local plusSize = math.max(16, math.floor(size * 0.6))
|
|
ind.plus:SetWidth(plusSize)
|
|
ind.plus:SetHeight(plusSize)
|
|
|
|
-- Position below the hovered button using screen coordinates
|
|
local buttonLeft = button:GetLeft()
|
|
local buttonBottom = button:GetBottom()
|
|
ind:ClearAllPoints()
|
|
if buttonLeft and buttonBottom then
|
|
ind:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", buttonLeft, buttonBottom - 2)
|
|
end
|
|
|
|
-- Track state
|
|
dropIndicatorCategoryId = button.categoryId or nil
|
|
activeCategoryDropIndicator = button
|
|
ind:Show()
|
|
end
|
|
|
|
HideCategoryDropIndicator = function()
|
|
if categoryDropIndicator then
|
|
categoryDropIndicator:Hide()
|
|
categoryDropIndicator:SetScript("OnUpdate", nil)
|
|
GameTooltip:Hide()
|
|
end
|
|
activeCategoryDropIndicator = nil
|
|
dropIndicatorCategoryId = nil
|
|
end
|
|
|
|
-- Track what item the cursor is holding (called before pickup)
|
|
function Guda_TrackCursorItem(bagID, slotID)
|
|
local link = GetContainerItemLink(bagID, slotID)
|
|
if link then
|
|
local itemID = addon.Modules.Utils:ExtractItemID(link)
|
|
cursorItemInfo = { bagID = bagID, slotID = slotID, itemID = itemID, link = link }
|
|
addon:Debug("TrackCursor: bag=%d slot=%d itemID=%s", bagID, slotID, tostring(itemID))
|
|
else
|
|
cursorItemInfo = nil
|
|
addon:Debug("TrackCursor: no link at bag=%d slot=%d", bagID, slotID)
|
|
end
|
|
end
|
|
|
|
function Guda_ClearCursorItem()
|
|
cursorItemInfo = nil
|
|
end
|
|
|
|
function Guda_GetCursorItemInfo()
|
|
return cursorItemInfo
|
|
end
|
|
|
|
-- Check if we're in category view for bags or bank
|
|
local function IsInCategoryView(isBank)
|
|
if not addon.Modules.DB then return false end
|
|
local key = isBank and "bankViewType" or "bagViewType"
|
|
return (addon.Modules.DB:GetSetting(key) or "single") == "category"
|
|
end
|
|
|
|
-- Set a tooltip from an item link, passing the BARE "item:ID:e:e:e" form rather
|
|
-- than the full colored link. The 1.12 native GameTooltip:SetHyperlink only
|
|
-- accepts the bare form; passing a full |cff..|Hitem:..|h[Name]|h|r link can
|
|
-- reach a bare-only native further down a re-hooked SetHyperlink chain (e.g. when
|
|
-- AtlasLoot / WoWTranslate / SuperCleverRoidMacros have re-hooked in an order that
|
|
-- bypasses Guda's own stripping hook) and throw "unknown link type" in that addon.
|
|
-- Always route Guda-originated SetHyperlink calls through this helper.
|
|
local function GudaSetTooltipHyperlink(tooltip, link)
|
|
if not link then return end
|
|
local _, _, bare = string.find(link, "|H(item:[^|]+)|h")
|
|
tooltip:SetHyperlink(bare or link)
|
|
end
|
|
|
|
-- Auto-clear cursor tracking when cursor is truly empty, and notify BagFrame
|
|
-- of drag-state transitions. In 1.12 CURSOR_UPDATE is unreliable for item
|
|
-- pickups, so we also poll CursorHasItem() on a throttled OnUpdate —
|
|
-- matching the Anniversary version's approach.
|
|
local cursorWatcher = CreateFrame("Frame")
|
|
cursorWatcher:RegisterEvent("CURSOR_UPDATE")
|
|
cursorWatcher:RegisterEvent("ITEM_LOCK_CHANGED")
|
|
cursorWatcher._lastCarrying = false
|
|
cursorWatcher._pollAccum = 0
|
|
|
|
local function NotifyDragState()
|
|
if addon.Modules.BagFrame and addon.Modules.BagFrame.SetDragging then
|
|
-- Suppress drop-target state updates while the user is dragging the
|
|
-- bag frame itself — nothing on the cursor there, and Update() is
|
|
-- deliberately short-circuited anyway.
|
|
if addon.Modules.BagFrame.IsFrameMoving
|
|
and addon.Modules.BagFrame:IsFrameMoving() then
|
|
return
|
|
end
|
|
local carrying = CursorHasItem and CursorHasItem() and true or false
|
|
if carrying ~= cursorWatcher._lastCarrying then
|
|
cursorWatcher._lastCarrying = carrying
|
|
addon.Modules.BagFrame:SetDragging(carrying)
|
|
end
|
|
end
|
|
end
|
|
|
|
cursorWatcher:SetScript("OnEvent", function()
|
|
NotifyDragState()
|
|
if not cursorItemInfo then
|
|
HideCategoryDropIndicator()
|
|
return
|
|
end
|
|
this.pendingCheck = true
|
|
end)
|
|
cursorWatcher:SetScript("OnUpdate", function()
|
|
-- Throttled cursor poll (~10Hz) as a safety net for 1.12's event gaps.
|
|
this._pollAccum = (this._pollAccum or 0) + arg1
|
|
if this._pollAccum >= 0.1 then
|
|
this._pollAccum = 0
|
|
NotifyDragState()
|
|
end
|
|
|
|
if this.pendingCheck then
|
|
this.pendingCheck = nil
|
|
if cursorItemInfo and (not CursorHasItem or not CursorHasItem()) then
|
|
cursorItemInfo = nil
|
|
HideCategoryDropIndicator()
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- Pulsing green glow applied to empty-category drop-target placeholder buttons.
|
|
-- Uses an OnUpdate-driven sine wave since vanilla 1.12 has no animation groups.
|
|
-- Uses a solid-color texture filling the full button, with additive blend +
|
|
-- alpha pulse — looks the same regardless of the user's iconSize setting.
|
|
local function EnsureDropTargetGlow(button)
|
|
if not button.dropGlow then
|
|
local g = button:CreateTexture(nil, "OVERLAY")
|
|
g:SetTexture(0.2, 1.0, 0.2, 1) -- solid green; alpha is modulated each frame
|
|
g:SetBlendMode("ADD")
|
|
g:SetAllPoints(button) -- cover the whole button, scales with iconSize
|
|
button.dropGlow = g
|
|
|
|
local driver = CreateFrame("Frame", nil, button)
|
|
driver._t = 0
|
|
driver:SetScript("OnUpdate", function()
|
|
this._t = this._t + arg1
|
|
-- period ~1.2s => 2π/1.2 ≈ 5.24
|
|
local phase = (math.sin(this._t * 5.24) + 1) * 0.5
|
|
if this:GetParent().dropGlow then
|
|
-- Subtle pulse: 0.15 → 0.45 alpha with additive blend.
|
|
this:GetParent().dropGlow:SetAlpha(0.15 + phase * 0.30)
|
|
end
|
|
end)
|
|
button.dropGlowDriver = driver
|
|
end
|
|
-- If the button was resized since last show, re-anchor defensively.
|
|
button.dropGlow:ClearAllPoints()
|
|
button.dropGlow:SetAllPoints(button)
|
|
button.dropGlow:Show()
|
|
button.dropGlowDriver:Show()
|
|
end
|
|
|
|
local function StopDropTargetGlow(button)
|
|
if button.dropGlow then button.dropGlow:Hide() end
|
|
if button.dropGlowDriver then button.dropGlowDriver:Hide() end
|
|
button.isDropTarget = false
|
|
button.dropTargetCategoryId = nil
|
|
end
|
|
|
|
-- Use shared tooltip from Utils module (retrieved on-demand to ensure Utils is loaded)
|
|
|
|
-- Helper function to check if an item is a quest item
|
|
-- Uses centralized ItemDetection module
|
|
local function IsQuestItem(bagID, slotID, isBank, itemData)
|
|
-- Use ItemDetection if available
|
|
if addon and addon.Modules and addon.Modules.ItemDetection then
|
|
local props = addon.Modules.ItemDetection:GetItemProperties(itemData, bagID, slotID)
|
|
return props.isQuestItem, props.isQuestStarter
|
|
end
|
|
-- Fallback to Utils
|
|
if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.IsQuestItem then
|
|
return addon.Modules.Utils:IsQuestItem(bagID, slotID, nil, false, isBank)
|
|
end
|
|
return false, false
|
|
end
|
|
|
|
--=====================================================
|
|
-- Inner Shadow (inset quality glow, GudaBags-inspired)
|
|
-- 4 gradient textures along edges colored by item quality
|
|
--=====================================================
|
|
local INNER_SHADOW_SIZE = 3
|
|
local INNER_SHADOW_ALPHA = 0.5
|
|
local INNER_SHADOW_INSET = 2 -- Pixels inset from icon edge so glow stays inside the slot
|
|
|
|
-- Create the 4-edge inner shadow textures on a button, anchored to an icon texture
|
|
local function CreateInnerShadow(button, anchorTo)
|
|
local shadow = {}
|
|
local inset = INNER_SHADOW_INSET
|
|
-- Top edge
|
|
shadow.top = button:CreateTexture(nil, "ARTWORK", nil, 1)
|
|
shadow.top:SetPoint("TOPLEFT", anchorTo, "TOPLEFT", inset, -inset)
|
|
shadow.top:SetPoint("TOPRIGHT", anchorTo, "TOPRIGHT", -inset, -inset)
|
|
shadow.top:SetHeight(INNER_SHADOW_SIZE)
|
|
shadow.top:SetTexture("Interface\\ChatFrame\\ChatFrameBackground")
|
|
shadow.top:Hide()
|
|
-- Bottom edge
|
|
shadow.bottom = button:CreateTexture(nil, "ARTWORK", nil, 1)
|
|
shadow.bottom:SetPoint("BOTTOMLEFT", anchorTo, "BOTTOMLEFT", inset, inset)
|
|
shadow.bottom:SetPoint("BOTTOMRIGHT", anchorTo, "BOTTOMRIGHT", -inset, inset)
|
|
shadow.bottom:SetHeight(INNER_SHADOW_SIZE)
|
|
shadow.bottom:SetTexture("Interface\\ChatFrame\\ChatFrameBackground")
|
|
shadow.bottom:Hide()
|
|
-- Left edge
|
|
shadow.left = button:CreateTexture(nil, "ARTWORK", nil, 1)
|
|
shadow.left:SetPoint("TOPLEFT", anchorTo, "TOPLEFT", inset, -inset)
|
|
shadow.left:SetPoint("BOTTOMLEFT", anchorTo, "BOTTOMLEFT", inset, inset)
|
|
shadow.left:SetWidth(INNER_SHADOW_SIZE)
|
|
shadow.left:SetTexture("Interface\\ChatFrame\\ChatFrameBackground")
|
|
shadow.left:Hide()
|
|
-- Right edge
|
|
shadow.right = button:CreateTexture(nil, "ARTWORK", nil, 1)
|
|
shadow.right:SetPoint("TOPRIGHT", anchorTo, "TOPRIGHT", -inset, -inset)
|
|
shadow.right:SetPoint("BOTTOMRIGHT", anchorTo, "BOTTOMRIGHT", -inset, inset)
|
|
shadow.right:SetWidth(INNER_SHADOW_SIZE)
|
|
shadow.right:SetTexture("Interface\\ChatFrame\\ChatFrameBackground")
|
|
shadow.right:Hide()
|
|
return shadow
|
|
end
|
|
|
|
-- Show inner shadow with a given quality color
|
|
local function ShowInnerShadow(shadow, r, g, b)
|
|
if not shadow then return end
|
|
-- Skip inner glow in square/pfUI style
|
|
if addon.Modules and addon.Modules.Theme and addon.Modules.Theme:GetSlotStyle() == "square" then
|
|
shadow.top:Hide()
|
|
shadow.bottom:Hide()
|
|
shadow.left:Hide()
|
|
shadow.right:Hide()
|
|
return
|
|
end
|
|
local a = INNER_SHADOW_ALPHA
|
|
shadow.top:SetGradientAlpha("VERTICAL", r, g, b, 0, r, g, b, a)
|
|
shadow.top:Show()
|
|
shadow.bottom:SetGradientAlpha("VERTICAL", r, g, b, a, r, g, b, 0)
|
|
shadow.bottom:Show()
|
|
shadow.left:SetGradientAlpha("HORIZONTAL", r, g, b, a, r, g, b, 0)
|
|
shadow.left:Show()
|
|
shadow.right:SetGradientAlpha("HORIZONTAL", r, g, b, 0, r, g, b, a)
|
|
shadow.right:Show()
|
|
end
|
|
|
|
-- Hide inner shadow
|
|
local function HideInnerShadow(shadow)
|
|
if not shadow then return end
|
|
shadow.top:Hide()
|
|
shadow.bottom:Hide()
|
|
shadow.left:Hide()
|
|
shadow.right:Hide()
|
|
end
|
|
|
|
--=====================================================
|
|
-- Quality/Quest Border — rounded backdrop border overlay
|
|
-- on top of the icon texture for a clean colored frame
|
|
--=====================================================
|
|
local QUALITY_BORDER_SIZE = 2 -- Border thickness
|
|
local QUALITY_BORDER_PADDING = 1 -- Inset from button edge
|
|
|
|
local qualityBorderBackdrop = {
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
edgeSize = 12,
|
|
insets = { left = 0, right = 0, top = 0, bottom = 0 },
|
|
}
|
|
|
|
local qualityBorderBackdropSquare = {
|
|
edgeFile = "Interface\\Buttons\\WHITE8x8",
|
|
edgeSize = 1,
|
|
insets = { left = -1, right = -1, top = -1, bottom = -1 },
|
|
}
|
|
|
|
-- Get or create the quality border frame for a button
|
|
local function GetQualityBorderFrame(button)
|
|
local qStyle = "rounded"
|
|
if addon.Modules and addon.Modules.Theme then
|
|
qStyle = addon.Modules.Theme:GetQualityBorderStyle()
|
|
end
|
|
|
|
if button._qualityBorder then
|
|
-- Update backdrop if style changed
|
|
if button._qualityBorderStyle ~= qStyle then
|
|
button._qualityBorderStyle = qStyle
|
|
local pad = QUALITY_BORDER_PADDING
|
|
button._qualityBorder:ClearAllPoints()
|
|
if qStyle == "square" then
|
|
button._qualityBorder:SetBackdrop(qualityBorderBackdropSquare)
|
|
button._qualityBorder:SetPoint("TOPLEFT", button, "TOPLEFT", -pad, pad)
|
|
button._qualityBorder:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", pad, -pad)
|
|
else
|
|
button._qualityBorder:SetBackdrop(qualityBorderBackdrop)
|
|
button._qualityBorder:SetPoint("TOPLEFT", button, "TOPLEFT", -pad, pad)
|
|
button._qualityBorder:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", pad, -pad)
|
|
end
|
|
end
|
|
return button._qualityBorder
|
|
end
|
|
|
|
local frame = CreateFrame("Frame", nil, button)
|
|
frame:SetFrameLevel(button:GetFrameLevel() + 3)
|
|
local pad = QUALITY_BORDER_PADDING
|
|
frame:SetPoint("TOPLEFT", button, "TOPLEFT", -pad, pad)
|
|
frame:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", pad, -pad)
|
|
if qStyle == "square" then
|
|
frame:SetBackdrop(qualityBorderBackdropSquare)
|
|
else
|
|
frame:SetBackdrop(qualityBorderBackdrop)
|
|
end
|
|
frame:Hide()
|
|
button._qualityBorder = frame
|
|
button._qualityBorderStyle = qStyle
|
|
return frame
|
|
end
|
|
|
|
-- Show quality border with color
|
|
local function TintSlotBorder(button, r, g, b)
|
|
local frame = GetQualityBorderFrame(button)
|
|
frame:SetBackdropBorderColor(r, g, b, 1)
|
|
frame:Show()
|
|
button._borderTinted = true
|
|
end
|
|
|
|
-- Hide quality border
|
|
local function ResetSlotBorder(button)
|
|
if not button._borderTinted then return end
|
|
if button._qualityBorder then
|
|
button._qualityBorder:Hide()
|
|
end
|
|
button._borderTinted = false
|
|
end
|
|
|
|
--=====================================================
|
|
-- Junk Icon Pool (Baganator-inspired memory optimization)
|
|
-- Uses frame pooling to avoid creating new frames per button
|
|
--=====================================================
|
|
local junkIconPool = {}
|
|
|
|
-- Get a junk icon from pool or create new one
|
|
local function AcquireJunkIcon()
|
|
local icon = table.remove(junkIconPool)
|
|
if not icon then
|
|
icon = CreateFrame("Frame", nil, UIParent)
|
|
icon:SetFrameStrata("HIGH")
|
|
icon:SetWidth(14)
|
|
icon:SetHeight(14)
|
|
|
|
local texture = icon:CreateTexture(nil, "OVERLAY")
|
|
texture:SetAllPoints(icon)
|
|
texture:SetTexture("Interface\\GossipFrame\\VendorGossipIcon")
|
|
texture:SetTexCoord(0, 1, 0, 1)
|
|
icon.texture = texture
|
|
end
|
|
return icon
|
|
end
|
|
|
|
-- Release a junk icon back to the pool
|
|
local function ReleaseJunkIcon(icon)
|
|
if icon then
|
|
icon:Hide()
|
|
icon:ClearAllPoints()
|
|
table.insert(junkIconPool, icon)
|
|
end
|
|
end
|
|
|
|
-- Update junk icon visibility and position (uses pooling)
|
|
local function UpdateJunkIcon(button, isJunk, iconSize)
|
|
if isJunk then
|
|
-- Acquire from pool if needed
|
|
if not button.junkIcon then
|
|
button.junkIcon = AcquireJunkIcon()
|
|
end
|
|
-- Scale icon size based on button size
|
|
local junkIconSize = math.max(10, math.min(14, iconSize * 0.30))
|
|
button.junkIcon:SetWidth(junkIconSize)
|
|
button.junkIcon:SetHeight(junkIconSize)
|
|
button.junkIcon:ClearAllPoints()
|
|
button.junkIcon:SetPoint("TOPLEFT", button, "TOPLEFT", 2, -2)
|
|
button.junkIcon:SetAlpha(1.0)
|
|
button.junkIcon:Show()
|
|
else
|
|
-- Release back to pool when not needed
|
|
if button.junkIcon then
|
|
ReleaseJunkIcon(button.junkIcon)
|
|
button.junkIcon = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Hide junk icon (releases to pool)
|
|
local function HideJunkIcon(button)
|
|
if button.junkIcon then
|
|
ReleaseJunkIcon(button.junkIcon)
|
|
button.junkIcon = nil
|
|
end
|
|
end
|
|
|
|
--=====================================================
|
|
-- Lock Icon Pool (same pattern as junk icon pool)
|
|
--=====================================================
|
|
local lockIconPool = {}
|
|
|
|
local function AcquireLockIcon()
|
|
local icon = table.remove(lockIconPool)
|
|
if not icon then
|
|
icon = CreateFrame("Frame", nil, UIParent)
|
|
icon:SetWidth(13)
|
|
icon:SetHeight(13)
|
|
|
|
-- Shadow (behind)
|
|
local shadow = icon:CreateTexture(nil, "BACKGROUND")
|
|
shadow:SetWidth(13)
|
|
shadow:SetHeight(13)
|
|
shadow:SetPoint("CENTER", icon, "CENTER", 1, -1)
|
|
shadow:SetTexture("Interface\\AddOns\\Guda\\Assets\\lock_glow")
|
|
shadow:SetVertexColor(0, 0, 0, 1)
|
|
icon.shadow = shadow
|
|
|
|
-- Icon (front)
|
|
local texture = icon:CreateTexture(nil, "OVERLAY")
|
|
texture:SetAllPoints(icon)
|
|
texture:SetTexture("Interface\\AddOns\\Guda\\Assets\\lock_glow")
|
|
icon.texture = texture
|
|
end
|
|
return icon
|
|
end
|
|
|
|
local function ReleaseLockIcon(icon)
|
|
if icon then
|
|
icon:Hide()
|
|
icon:ClearAllPoints()
|
|
icon:SetParent(UIParent)
|
|
table.insert(lockIconPool, icon)
|
|
end
|
|
end
|
|
|
|
local function UpdateLockIcon(button, iconSize)
|
|
local DB = addon.Modules.DB
|
|
if not DB then return end
|
|
|
|
local isLocked = false
|
|
if button.hasItem and button.itemData and button.itemData.link then
|
|
local Utils = addon.Modules.Utils
|
|
local itemID = Utils and Utils.ExtractItemID and Utils:ExtractItemID(button.itemData.link)
|
|
if itemID and DB:IsItemProtected(itemID) then
|
|
isLocked = true
|
|
end
|
|
end
|
|
|
|
if isLocked then
|
|
if not button.lockIcon then
|
|
button.lockIcon = AcquireLockIcon()
|
|
end
|
|
button.lockIcon:SetParent(button)
|
|
local lockSize = math.max(8, math.min(12, iconSize * 0.35 - 3))
|
|
button.lockIcon:SetWidth(lockSize)
|
|
button.lockIcon:SetHeight(lockSize)
|
|
if button.lockIcon.shadow then
|
|
button.lockIcon.shadow:SetWidth(lockSize)
|
|
button.lockIcon.shadow:SetHeight(lockSize)
|
|
end
|
|
button.lockIcon:ClearAllPoints()
|
|
button.lockIcon:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 4, -3)
|
|
button.lockIcon:SetFrameLevel(button:GetFrameLevel() + 5)
|
|
button.lockIcon:Show()
|
|
else
|
|
if button.lockIcon then
|
|
ReleaseLockIcon(button.lockIcon)
|
|
button.lockIcon = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
local function HideLockIcon(button)
|
|
if button.lockIcon then
|
|
ReleaseLockIcon(button.lockIcon)
|
|
button.lockIcon = nil
|
|
end
|
|
end
|
|
|
|
--=====================================================
|
|
-- Pin Icon Pool (indicates slot is pinned / sort-protected)
|
|
--=====================================================
|
|
local pinIconPool = {}
|
|
|
|
local function AcquirePinIcon()
|
|
local icon = table.remove(pinIconPool)
|
|
if not icon then
|
|
icon = CreateFrame("Frame", nil, UIParent)
|
|
icon:SetWidth(13)
|
|
icon:SetHeight(13)
|
|
|
|
-- Shadow (behind)
|
|
local shadow = icon:CreateTexture(nil, "BACKGROUND")
|
|
shadow:SetWidth(15)
|
|
shadow:SetHeight(15)
|
|
shadow:SetPoint("CENTER", icon, "CENTER", 0, 0)
|
|
shadow:SetTexture("Interface\\AddOns\\Guda\\Assets\\pin")
|
|
shadow:SetVertexColor(0, 0, 0, 0.9)
|
|
icon.shadow = shadow
|
|
|
|
-- Icon (front)
|
|
local texture = icon:CreateTexture(nil, "OVERLAY")
|
|
texture:SetAllPoints(icon)
|
|
texture:SetTexture("Interface\\AddOns\\Guda\\Assets\\pin")
|
|
icon.texture = texture
|
|
end
|
|
return icon
|
|
end
|
|
|
|
local function ReleasePinIcon(icon)
|
|
if icon then
|
|
icon:Hide()
|
|
icon:ClearAllPoints()
|
|
icon:SetParent(UIParent)
|
|
table.insert(pinIconPool, icon)
|
|
end
|
|
end
|
|
|
|
local function UpdatePinIcon(button, iconSize)
|
|
local DB = addon.Modules.DB
|
|
if not DB then return end
|
|
|
|
local isPinned = false
|
|
if button.bagID and button.slotID and not button.otherChar and not button.isReadOnly then
|
|
isPinned = DB:IsPinnedSlot(button.bagID, button.slotID)
|
|
end
|
|
|
|
if isPinned then
|
|
if not button.pinIcon then
|
|
button.pinIcon = AcquirePinIcon()
|
|
end
|
|
button.pinIcon:SetParent(button)
|
|
local pinSize = math.max(10, math.min(14, iconSize * 0.35))
|
|
button.pinIcon:SetWidth(pinSize)
|
|
button.pinIcon:SetHeight(pinSize)
|
|
if button.pinIcon.shadow then
|
|
button.pinIcon.shadow:SetWidth(pinSize + 2)
|
|
button.pinIcon.shadow:SetHeight(pinSize + 2)
|
|
end
|
|
button.pinIcon:ClearAllPoints()
|
|
button.pinIcon:SetPoint("TOPLEFT", button, "TOPLEFT", -2, 2)
|
|
button.pinIcon:SetFrameLevel(button:GetFrameLevel() + 5)
|
|
button.pinIcon:Show()
|
|
else
|
|
if button.pinIcon then
|
|
ReleasePinIcon(button.pinIcon)
|
|
button.pinIcon = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
local function HidePinIcon(button)
|
|
if button.pinIcon then
|
|
ReleasePinIcon(button.pinIcon)
|
|
button.pinIcon = nil
|
|
end
|
|
end
|
|
|
|
-- Hook UseContainerItem to prevent selling/disenchanting protected items
|
|
local OriginalUseContainerItem = UseContainerItem
|
|
UseContainerItem = function(bag, slot, ...)
|
|
local DB = addon.Modules.DB
|
|
if DB then
|
|
local link = GetContainerItemLink(bag, slot)
|
|
if link then
|
|
local Utils = addon.Modules.Utils
|
|
local itemID = Utils and Utils.ExtractItemID and Utils:ExtractItemID(link)
|
|
if itemID and DB:IsItemProtected(itemID) then
|
|
-- Block selling at merchant
|
|
if MerchantFrame and MerchantFrame:IsVisible() then
|
|
addon:Print(format(Guda_L["Cannot sell %s — item is protected"], link))
|
|
return
|
|
end
|
|
-- Block disenchant/milling/prospecting (spell targeting an item)
|
|
if SpellIsTargeting and SpellIsTargeting() then
|
|
SpellStopTargeting()
|
|
addon:Print(format(Guda_L["Cannot disenchant %s — item is protected"], link))
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return OriginalUseContainerItem(bag, slot)
|
|
end
|
|
|
|
-- Track cursor item for delete protection (GetCursorInfo doesn't exist in 1.12.1)
|
|
local cursorProtectedLink = nil
|
|
|
|
local OriginalPickupContainerItem = PickupContainerItem
|
|
PickupContainerItem = function(bag, slot, ...)
|
|
local DB = addon.Modules.DB
|
|
if DB then
|
|
local link = GetContainerItemLink(bag, slot)
|
|
if link then
|
|
local Utils = addon.Modules.Utils
|
|
local itemID = Utils and Utils.ExtractItemID and Utils:ExtractItemID(link)
|
|
if itemID and DB:IsItemProtected(itemID) then
|
|
cursorProtectedLink = link
|
|
else
|
|
cursorProtectedLink = nil
|
|
end
|
|
else
|
|
cursorProtectedLink = nil
|
|
end
|
|
end
|
|
return OriginalPickupContainerItem(bag, slot)
|
|
end
|
|
|
|
-- Hook delete confirmation popups
|
|
local function HookDeletePopup(dialogName)
|
|
if not StaticPopupDialogs or not StaticPopupDialogs[dialogName] then return end
|
|
local originalOnShow = StaticPopupDialogs[dialogName].OnShow
|
|
StaticPopupDialogs[dialogName].OnShow = function()
|
|
if cursorProtectedLink then
|
|
addon:Print(format(Guda_L["Cannot delete %s — item is protected"], cursorProtectedLink))
|
|
ClearCursor()
|
|
cursorProtectedLink = nil
|
|
this:Hide()
|
|
return
|
|
end
|
|
if originalOnShow then
|
|
return originalOnShow()
|
|
end
|
|
end
|
|
end
|
|
HookDeletePopup("DELETE_ITEM")
|
|
HookDeletePopup("DELETE_GOOD_ITEM")
|
|
|
|
--=====================================================
|
|
-- Unusable item detection (pfUI-inspired implementation)
|
|
-- Adds a red tint overlay to items that your character
|
|
-- cannot use (class/race/skill restrictions), excluding
|
|
-- purely broken durability cases.
|
|
--=====================================================
|
|
local function Guda_GetUnusableColor()
|
|
if pfUI and C and C.appearance and C.appearance.bags and C.appearance.bags.unusable_color then
|
|
local cr, cg, cb, ca = strsplit(",", C.appearance.bags.unusable_color)
|
|
local r = tonumber(cr) or 0.9
|
|
local g = tonumber(cg) or 0.2
|
|
local b = tonumber(cb) or 0.2
|
|
local a = tonumber(ca) or 1.0
|
|
return r, g, b, a
|
|
end
|
|
-- Then Blizzard's RED_FONT_COLOR if present
|
|
if RED_FONT_COLOR then
|
|
return RED_FONT_COLOR.r, RED_FONT_COLOR.g, RED_FONT_COLOR.b, 1.0
|
|
end
|
|
-- Default pfUI-like
|
|
return 0.9, 0.2, 0.2, 1.0
|
|
end
|
|
|
|
-- NOTE: IsItemUnusable detection is now handled by ItemDetection:IsUnusable()
|
|
-- which uses cached tooltip scanning to avoid duplicate scans per item.
|
|
-- The old IsItemUnusable, IsRedColor, and durabilityPattern have been removed
|
|
-- to prevent redundant tooltip scanning - all detection is now centralized.
|
|
|
|
-- Apply/remove red tint on item texture for unusable items.
|
|
-- When cacheOnly is true, skips the tooltip scan on cold cache and returns
|
|
-- untinted — the deferred pass in BagFrame fills in tints later.
|
|
local function Guda_ItemButton_UpdateUsableTint(self, cacheOnly)
|
|
-- Clear any existing tint/overlay first
|
|
if self.unusableOverlay and self.unusableOverlay.Hide then
|
|
self.unusableOverlay:Hide()
|
|
end
|
|
if SetItemButtonTextureVertexColor then
|
|
SetItemButtonTextureVertexColor(self, 1.0, 1.0, 1.0)
|
|
end
|
|
|
|
-- Check if feature is enabled
|
|
local markUnusable = true
|
|
if Guda and Guda.Modules and Guda.Modules.DB then
|
|
markUnusable = Guda.Modules.DB:GetSetting("markUnusableItems")
|
|
if markUnusable == nil then
|
|
markUnusable = true
|
|
end
|
|
end
|
|
|
|
-- If feature is disabled, just return after clearing
|
|
if not markUnusable then
|
|
return
|
|
end
|
|
|
|
-- Only evaluate for live (player) items; DB cached items from other chars cannot be scanned
|
|
if not self or not self.hasItem or not self.bagID or not self.slotID or self.isReadOnly or self.otherChar then
|
|
return
|
|
end
|
|
|
|
-- Use cached detection from ItemDetection module (avoids duplicate tooltip scans)
|
|
local unusable = false
|
|
if self.itemData and addon.Modules.ItemDetection then
|
|
if cacheOnly then
|
|
local cached = addon.Modules.ItemDetection:IsUnusableCached(self.itemData)
|
|
if cached == nil then
|
|
-- Unknown; leave untinted. Deferred pass fills this in later.
|
|
return
|
|
end
|
|
unusable = cached
|
|
else
|
|
unusable = addon.Modules.ItemDetection:IsUnusable(self.itemData, self.bagID, self.slotID)
|
|
end
|
|
end
|
|
|
|
-- Ensure overlay exists (created in OnLoad, but be defensive)
|
|
if not self.unusableOverlay then
|
|
local icon = getglobal(self:GetName().."IconTexture") or getglobal(self:GetName().."Icon") or self.icon or self.Icon
|
|
local overlay = (icon and icon:GetParent() or self):CreateTexture(nil, "OVERLAY")
|
|
overlay:SetAllPoints(icon or self)
|
|
overlay:SetTexture("Interface\\ChatFrame\\ChatFrameBackground")
|
|
overlay:Hide()
|
|
self.unusableOverlay = overlay
|
|
end
|
|
|
|
if unusable then
|
|
local r, g, b, a = Guda_GetUnusableColor()
|
|
-- Slightly reduce alpha to avoid over-darkening the icon
|
|
local alpha = (a or 1.0) * 0.45
|
|
self.unusableOverlay:SetVertexColor(r or 0.9, g or 0.2, b or 0.2, alpha)
|
|
self.unusableOverlay:Show()
|
|
else
|
|
self.unusableOverlay:Hide()
|
|
end
|
|
end
|
|
|
|
|
|
-- Check if a button is available for reuse
|
|
-- A button is available if it's either:
|
|
-- 1. Hidden (not shown)
|
|
-- 2. Marked as not in use (inUse == false) during an update cycle
|
|
local function IsButtonAvailable(button)
|
|
if not button:IsShown() then
|
|
return true
|
|
end
|
|
-- During update cycles, buttons are marked inUse = false before display
|
|
-- and inUse = true when assigned. This allows reuse without hiding first.
|
|
if button.inUse == false then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Create or get a button from the pool
|
|
function Guda_GetItemButton(parent)
|
|
local reuseCandidate = nil -- Button from different parent we can reparent
|
|
|
|
-- Try to reuse existing button from pool
|
|
for _, button in pairs(buttonPool) do
|
|
-- Skip bag slot buttons
|
|
if not button.isBagSlot and IsButtonAvailable(button) then
|
|
if button:GetParent() == parent then
|
|
-- Same parent - best case, reuse immediately
|
|
-- Mark as in use to prevent double-assignment
|
|
button.inUse = true
|
|
-- Re-register with parent (itemButtons hash may have been cleared)
|
|
if Guda_RegisterItemButton then
|
|
Guda_RegisterItemButton(parent, button)
|
|
end
|
|
return button
|
|
elseif not reuseCandidate then
|
|
-- Different parent but available - save as candidate for reparenting
|
|
reuseCandidate = button
|
|
end
|
|
end
|
|
end
|
|
|
|
-- If we found an available button from a different parent, reparent it
|
|
if reuseCandidate then
|
|
-- Mark as in use
|
|
reuseCandidate.inUse = true
|
|
|
|
-- Unregister from old parent
|
|
local oldParent = reuseCandidate:GetParent()
|
|
if oldParent and oldParent.itemButtons then
|
|
oldParent.itemButtons[reuseCandidate] = nil
|
|
end
|
|
|
|
-- Reparent to new parent
|
|
reuseCandidate:SetParent(parent)
|
|
|
|
-- Register with new parent
|
|
if Guda_RegisterItemButton then
|
|
Guda_RegisterItemButton(parent, reuseCandidate)
|
|
end
|
|
|
|
return reuseCandidate
|
|
end
|
|
|
|
-- Only create new button if under pool limit
|
|
if nextButtonID <= BUTTON_POOL_MAX then
|
|
local button = CreateFrame("Button", "Guda_ItemButton" .. nextButtonID, parent, "Guda_ItemButtonTemplate")
|
|
buttonPool[nextButtonID] = button
|
|
nextButtonID = nextButtonID + 1
|
|
button.inUse = true
|
|
|
|
-- Register button with parent for tracking (avoids GetChildren() allocation)
|
|
if Guda_RegisterItemButton then
|
|
Guda_RegisterItemButton(parent, button)
|
|
end
|
|
|
|
return button
|
|
end
|
|
|
|
-- Pool is at max and no available button found - this shouldn't normally happen
|
|
-- but as a fallback, force-reuse the first non-bag-slot button we find
|
|
for _, button in pairs(buttonPool) do
|
|
if not button.isBagSlot then
|
|
-- Hide it first (in case it was shown)
|
|
button:Hide()
|
|
button.inUse = true
|
|
|
|
-- Unregister from old parent
|
|
local oldParent = button:GetParent()
|
|
if oldParent and oldParent.itemButtons then
|
|
oldParent.itemButtons[button] = nil
|
|
end
|
|
|
|
-- Reparent
|
|
button:SetParent(parent)
|
|
|
|
-- Register with new parent
|
|
if Guda_RegisterItemButton then
|
|
Guda_RegisterItemButton(parent, button)
|
|
end
|
|
|
|
return button
|
|
end
|
|
end
|
|
|
|
-- Ultimate fallback (should never reach here) - create one more button
|
|
local button = CreateFrame("Button", "Guda_ItemButton" .. nextButtonID, parent, "Guda_ItemButtonTemplate")
|
|
buttonPool[nextButtonID] = button
|
|
nextButtonID = nextButtonID + 1
|
|
button.inUse = true
|
|
if Guda_RegisterItemButton then
|
|
Guda_RegisterItemButton(parent, button)
|
|
end
|
|
return button
|
|
end
|
|
|
|
-- Update quest icon based on item type (starter vs regular quest item)
|
|
local function Guda_ItemButton_UpdateQuestIcon(self, isQuest, isQuestStarter)
|
|
if not self.questIcon then return end
|
|
|
|
if isQuest then
|
|
-- Set appropriate texture based on quest type
|
|
if isQuestStarter then
|
|
-- Quest starter: exclamation mark
|
|
local texture = self.questIcon:GetRegions()
|
|
if texture and texture.SetTexture then
|
|
texture:SetTexture("Interface\\GossipFrame\\AvailableQuestIcon")
|
|
texture:SetTexCoord(0, 1, 0, 1)
|
|
end
|
|
else
|
|
-- Regular quest item: question mark
|
|
local texture = self.questIcon:GetRegions()
|
|
if texture and texture.SetTexture then
|
|
texture:SetTexture("Interface\\GossipFrame\\ActiveQuestIcon")
|
|
texture:SetTexCoord(0, 1, 0, 1)
|
|
end
|
|
end
|
|
self.questIcon:Show()
|
|
else
|
|
self.questIcon:Hide()
|
|
end
|
|
end
|
|
|
|
-- OnLoad handler
|
|
function Guda_ItemButton_OnLoad(self)
|
|
self.hasItem = false
|
|
self.bagID = nil
|
|
self.slotID = nil
|
|
self.itemData = nil
|
|
self.isBank = false
|
|
self.otherChar = nil
|
|
|
|
-- In square slot style, hide all rounded Blizzard template textures
|
|
local slotStyle = "rounded"
|
|
if addon.Modules and addon.Modules.Theme then
|
|
slotStyle = addon.Modules.Theme:GetSlotStyle()
|
|
end
|
|
if slotStyle == "square" then
|
|
-- Hide the rounded NormalTexture from ContainerFrameItemButtonTemplate
|
|
self:SetNormalTexture("")
|
|
local normalTex = getglobal(self:GetName() .. "NormalTexture")
|
|
if normalTex then
|
|
normalTex:SetTexture(nil)
|
|
normalTex:Hide()
|
|
end
|
|
-- Hide the QualityRing (rounded UI-EmptySlot)
|
|
local qRing = getglobal(self:GetName() .. "_QualityRing")
|
|
if qRing then
|
|
qRing:Hide()
|
|
end
|
|
-- Update EmptySlotBg to square style
|
|
local emptySlotBg = getglobal(self:GetName() .. "_EmptySlotBg")
|
|
if emptySlotBg then
|
|
emptySlotBg:SetTexture("Interface\\Buttons\\WHITE8x8")
|
|
emptySlotBg:SetVertexColor(0.05, 0.05, 0.05, 1)
|
|
emptySlotBg:ClearAllPoints()
|
|
emptySlotBg:SetPoint("TOPLEFT", self, "TOPLEFT", 0, 0)
|
|
emptySlotBg:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", 0, 0)
|
|
end
|
|
end
|
|
|
|
-- Create inner shadow for quality color glow (anchored to icon texture)
|
|
if not self.innerShadow then
|
|
local iconTex = getglobal(self:GetName() .. "IconTexture")
|
|
if iconTex then
|
|
self.innerShadow = CreateInnerShadow(self, iconTex)
|
|
end
|
|
end
|
|
|
|
-- Create quest icon overlay (exclamation mark in corner)
|
|
if not self.questIcon then
|
|
local iconFrame = CreateFrame("Frame", nil, self)
|
|
iconFrame:SetFrameLevel(self:GetFrameLevel() + 7) -- Above quest border
|
|
iconFrame:SetWidth(16)
|
|
iconFrame:SetHeight(16)
|
|
|
|
local texture = iconFrame:CreateTexture(nil, "OVERLAY")
|
|
texture:SetAllPoints(iconFrame)
|
|
texture:SetTexture("Interface\\GossipFrame\\AvailableQuestIcon")
|
|
texture:SetTexCoord(0, 1, 0, 1)
|
|
|
|
iconFrame:Hide()
|
|
self.questIcon = iconFrame
|
|
end
|
|
|
|
-- Note: Junk icon is acquired from pool on-demand in UpdateJunkIcon, not pre-created
|
|
|
|
-- Ensure the item button sits above its container backdrop and is mouse-enabled
|
|
local parent = self:GetParent()
|
|
if parent and parent.GetFrameLevel then
|
|
-- Place button above parent backdrop/mouse layer to reliably receive drops
|
|
local parentLevel = parent:GetFrameLevel()
|
|
if parentLevel and self:GetFrameLevel() <= parentLevel + 1 then
|
|
self:SetFrameLevel(parentLevel + 2)
|
|
end
|
|
end
|
|
|
|
-- Enable mouse and register for drag/drop (crucial for Classic/Vanilla WoW)
|
|
if self.EnableMouse then
|
|
self:EnableMouse(true)
|
|
end
|
|
if self.RegisterForDrag then
|
|
self:RegisterForDrag("LeftButton")
|
|
end
|
|
if self.RegisterForClicks then
|
|
self:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
end
|
|
|
|
-- Hide junk and lock icons when button is hidden (since they're parented to UIParent)
|
|
self:SetScript("OnHide", function()
|
|
HideJunkIcon(this)
|
|
HideLockIcon(this)
|
|
HidePinIcon(this)
|
|
end)
|
|
|
|
-- Track cursor item on drag start for category drag-drop
|
|
self:SetScript("OnDragStart", function()
|
|
if this.hasItem and this.bagID and this.slotID and not this.otherChar and not this.isReadOnly then
|
|
Guda_TrackCursorItem(this.bagID, this.slotID)
|
|
PickupContainerItem(this.bagID, this.slotID)
|
|
end
|
|
end)
|
|
|
|
self:SetScript("OnClick", function()
|
|
-- Click on a drop-target placeholder while carrying an item on the
|
|
-- cursor: route to the same handler as OnReceiveDrag (assign to
|
|
-- category). Without an item on the cursor a click is a no-op.
|
|
if this.isDropTarget then
|
|
if CursorHasItem and CursorHasItem() then
|
|
local handler = this:GetScript("OnReceiveDrag")
|
|
if handler then handler() end
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Lock/unlock item with Ctrl+Right-Click
|
|
if IsControlKeyDown() and arg1 == "RightButton" and this.hasItem and not this.otherChar and not this.isReadOnly then
|
|
local link = GetContainerItemLink(this.bagID, this.slotID)
|
|
if link and addon and addon.Modules and addon.Modules.Utils and addon.Modules.DB then
|
|
local itemID = addon.Modules.Utils:ExtractItemID(link)
|
|
if itemID then
|
|
-- If item is in an equipment set, toggle set protection exception
|
|
if addon.Modules.DB:GetSetting("autoLockSetItems") then
|
|
local EquipSets = addon.Modules.EquipmentSets
|
|
if EquipSets and EquipSets.IsInSet and EquipSets:IsInSet(itemID) then
|
|
local isNowExcepted = addon.Modules.DB:ToggleSetProtectionException(itemID)
|
|
if isNowExcepted then
|
|
addon:Print(format(Guda_L["%s set protection removed"], link))
|
|
else
|
|
addon:Print(format(Guda_L["%s set protection restored"], link))
|
|
end
|
|
-- Refresh bag/bank frames
|
|
if addon.Modules.BagFrame and addon.Modules.BagFrame.Update then
|
|
addon.Modules.BagFrame:Update()
|
|
end
|
|
if addon.Modules.BankFrame and addon.Modules.BankFrame.Update then
|
|
addon.Modules.BankFrame:Update()
|
|
end
|
|
return
|
|
end
|
|
end
|
|
local isNowLocked = addon.Modules.DB:ToggleItemLock(itemID)
|
|
if isNowLocked then
|
|
addon:Print(format(Guda_L["%s locked"], link))
|
|
else
|
|
addon:Print(format(Guda_L["%s unlocked"], link))
|
|
end
|
|
-- Refresh bag/bank frames
|
|
if addon.Modules.BagFrame and addon.Modules.BagFrame.Update then
|
|
addon.Modules.BagFrame:Update()
|
|
end
|
|
if addon.Modules.BankFrame and addon.Modules.BankFrame.Update then
|
|
addon.Modules.BankFrame:Update()
|
|
end
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Pin/unpin slot with Alt+Right-Click
|
|
if IsAltKeyDown() and arg1 == "RightButton" and not this.otherChar and not this.isReadOnly then
|
|
if this.bagID ~= nil and this.slotID and addon.Modules.DB then
|
|
local isNowPinned = addon.Modules.DB:TogglePinnedSlot(this.bagID, this.slotID)
|
|
local itemName = ""
|
|
if this.hasItem and this.itemData and this.itemData.link then
|
|
itemName = " " .. this.itemData.link
|
|
end
|
|
if isNowPinned then
|
|
addon:Print(format(Guda_L["Slot pinned %s (skipped during sort)"], itemName))
|
|
else
|
|
addon:Print(format(Guda_L["Slot unpinned %s"], itemName))
|
|
end
|
|
-- Refresh to show/hide pin icon
|
|
if addon.Modules.BagFrame and addon.Modules.BagFrame.Update then
|
|
addon.Modules.BagFrame:Update()
|
|
end
|
|
if addon.Modules.BankFrame and addon.Modules.BankFrame.Update then
|
|
addon.Modules.BankFrame:Update()
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
if IsAltKeyDown() and arg1 == "LeftButton" and this.hasItem and not this.otherChar and not this.isReadOnly then
|
|
local link = GetContainerItemLink(this.bagID, this.slotID)
|
|
if link and addon and addon.Modules and addon.Modules.Utils then
|
|
local itemID = addon.Modules.Utils:ExtractItemID(link)
|
|
if itemID then
|
|
local isQuest = IsQuestItem(this.bagID, this.slotID, this.isBank, this.itemData)
|
|
local isUnique = addon.Modules.Utils:IsUniqueItem(this.bagID, this.slotID, link)
|
|
|
|
-- Only pin to QuestItemBar if it's a unique quest item
|
|
if isQuest and isUnique and addon.Modules.QuestItemBar and addon.Modules.QuestItemBar.PinItem then
|
|
addon.Modules.QuestItemBar:PinItem(itemID)
|
|
return
|
|
end
|
|
|
|
-- Track non-unique quest items and regular items in TrackedItemBar
|
|
local trackedItems = addon.Modules.DB:GetSetting("trackedItems") or {}
|
|
if trackedItems[itemID] then
|
|
trackedItems[itemID] = nil
|
|
else
|
|
trackedItems[itemID] = true
|
|
end
|
|
addon.Modules.DB:SetSetting("trackedItems", trackedItems)
|
|
|
|
-- Update all item buttons
|
|
if Guda.Modules.BagFrame and Guda.Modules.BagFrame.Update then
|
|
Guda.Modules.BagFrame:Update()
|
|
end
|
|
if Guda.Modules.BankFrame and Guda.Modules.BankFrame.Update then
|
|
Guda.Modules.BankFrame:Update()
|
|
end
|
|
if Guda.Modules.TrackedItemBar and Guda.Modules.TrackedItemBar.Update then
|
|
Guda.Modules.TrackedItemBar:Update()
|
|
end
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Shift-click to link cached items to chat (remote bank, read-only, or closed bank)
|
|
if IsShiftKeyDown() and arg1 == "LeftButton" and this.hasItem then
|
|
if this.otherChar or this.isReadOnly or (this.isBank and this.bagID == -1 and not (getglobal("BankFrame") and getglobal("BankFrame"):IsVisible())) then
|
|
local link = this.itemData and this.itemData.link
|
|
if link and ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
|
|
ChatFrameEditBox:Insert(link)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Block disenchant/milling/prospecting on protected items
|
|
if SpellIsTargeting and SpellIsTargeting() and this.hasItem and this.bagID and this.slotID then
|
|
local link = GetContainerItemLink(this.bagID, this.slotID)
|
|
if link and addon and addon.Modules and addon.Modules.Utils and addon.Modules.DB then
|
|
local itemID = addon.Modules.Utils:ExtractItemID(link)
|
|
if itemID and addon.Modules.DB:IsItemProtected(itemID) then
|
|
SpellStopTargeting()
|
|
addon:Print(format(Guda_L["Cannot disenchant %s — item is protected"], link))
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Default behavior
|
|
if ContainerFrameItemButton_OnClick then
|
|
-- Mailbox clicks should be ignored except for Ctrl+Click (preview)
|
|
-- OR if it's a live mail item for the current player and Shift+Click (loot)
|
|
if this.isMail then
|
|
if IsControlKeyDown() then
|
|
ContainerFrameItemButton_OnClick(arg1)
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Track cursor item before pickup for category drag-drop
|
|
if this.hasItem and this.bagID and this.slotID and not CursorHasItem() then
|
|
Guda_TrackCursorItem(this.bagID, this.slotID)
|
|
end
|
|
|
|
ContainerFrameItemButton_OnClick(arg1)
|
|
|
|
-- Clear cursor tracking if item was placed (cursor no longer has item)
|
|
if not CursorHasItem() then
|
|
Guda_ClearCursorItem()
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- OnReceiveDrag: category reassignment in category view
|
|
self:SetScript("OnReceiveDrag", function()
|
|
local info = Guda_GetCursorItemInfo()
|
|
if not info then return end
|
|
|
|
-- Empty-category drop target: assign cursor item to this.dropTargetCategoryId
|
|
if this.isDropTarget and this.dropTargetCategoryId then
|
|
if info.itemID and addon.Modules.CategoryManager then
|
|
addon.Modules.CategoryManager:AssignItemToCategory(info.itemID, this.dropTargetCategoryId)
|
|
addon:Debug("Assigned item %d to empty category: %s", info.itemID, this.dropTargetCategoryId)
|
|
end
|
|
if CursorHasItem() then
|
|
PickupContainerItem(info.bagID, info.slotID)
|
|
end
|
|
dropCooldownTime = GetTime() + 0.3
|
|
HideCategoryDropIndicator()
|
|
Guda_ClearCursorItem()
|
|
if addon.Modules.BagFrame and addon.Modules.BagFrame.Update then
|
|
addon.Modules.BagFrame:Update()
|
|
end
|
|
return
|
|
end
|
|
|
|
local inCatView = IsInCategoryView(this.isBank)
|
|
if not inCatView then
|
|
-- Single view: let default swap happen
|
|
if ContainerFrameItemButton_OnClick then
|
|
ContainerFrameItemButton_OnClick("LeftButton")
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Category view: reassign dragged item to target item's category
|
|
if this.hasItem and this.itemData and addon.Modules.CategoryManager then
|
|
local targetCategory = addon.Modules.CategoryManager:CategorizeItem(this.itemData, this.bagID, this.slotID, this.otherChar)
|
|
if targetCategory and info.itemID then
|
|
addon.Modules.CategoryManager:AssignItemToCategory(info.itemID, targetCategory)
|
|
addon:Debug("Reassigned item %d to category: %s", info.itemID, targetCategory)
|
|
end
|
|
end
|
|
|
|
-- Put dragged item back in its original slot
|
|
if CursorHasItem() then
|
|
PickupContainerItem(info.bagID, info.slotID)
|
|
end
|
|
|
|
-- Set drop cooldown to prevent immediate re-show
|
|
dropCooldownTime = GetTime() + 0.3
|
|
HideCategoryDropIndicator()
|
|
Guda_ClearCursorItem()
|
|
|
|
-- Refresh frames
|
|
if addon.Modules.BagFrame and addon.Modules.BagFrame.Update then
|
|
addon.Modules.BagFrame:Update()
|
|
end
|
|
if addon.Modules.BankFrame and addon.Modules.BankFrame.Update then
|
|
local bankFrame = getglobal("Guda_BankFrame")
|
|
if bankFrame and bankFrame:IsShown() then
|
|
addon.Modules.BankFrame:Update()
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- Update the Blizzard cooldown overlay on this item button
|
|
function Guda_ItemButton_UpdateCooldown(self)
|
|
-- Only show cooldowns for live items of the current character
|
|
if not self then return end
|
|
|
|
local cooldown = getglobal(self:GetName().."Cooldown") or self.cooldown
|
|
if not cooldown then return end
|
|
|
|
-- Ensure cooldown overlay is NOT shown for read-only or other-character views
|
|
if self.isReadOnly or self.otherChar then
|
|
-- Clear any previous timer state and hide to avoid carry-over from pooled buttons
|
|
if CooldownFrame_SetTimer then
|
|
CooldownFrame_SetTimer(cooldown, 0, 0, 0)
|
|
elseif CooldownFrame_Set then
|
|
CooldownFrame_Set(cooldown, 0, 0, 0)
|
|
end
|
|
cooldown:Hide()
|
|
return
|
|
end
|
|
|
|
if not self.hasItem or not self.bagID or not self.slotID then
|
|
cooldown:Hide()
|
|
return
|
|
end
|
|
|
|
local start, duration, enable = GetContainerItemCooldown(self.bagID, self.slotID)
|
|
if start and duration and duration > 0 and enable == 1 then
|
|
if CooldownFrame_SetTimer then
|
|
CooldownFrame_SetTimer(cooldown, start, duration, enable)
|
|
elseif CooldownFrame_Set then
|
|
-- Some clients expose CooldownFrame_Set instead
|
|
CooldownFrame_Set(cooldown, start, duration, enable)
|
|
else
|
|
-- Fallback: show the frame if API missing
|
|
cooldown:Show()
|
|
end
|
|
else
|
|
cooldown:Hide()
|
|
end
|
|
end
|
|
|
|
--=====================================================
|
|
-- Helper functions for SetItem (extracted for clarity)
|
|
--=====================================================
|
|
|
|
-- Reset all visual state on a button (for reuse from pool)
|
|
local function ResetButtonVisualState(self)
|
|
if self.questIcon then self.questIcon:Hide() end
|
|
ResetSlotBorder(self)
|
|
HideInnerShadow(self.innerShadow)
|
|
if self.unusableOverlay then self.unusableOverlay:Hide() end
|
|
HideJunkIcon(self)
|
|
if self.categoryMarkIcon then self.categoryMarkIcon:Hide() end
|
|
local chargesText = getglobal(self:GetName().."_Charges")
|
|
if chargesText then chargesText:Hide() end
|
|
|
|
-- Reset effects the drop-target render may have applied on this pooled button
|
|
self:SetAlpha(1)
|
|
local iconTex = getglobal(self:GetName().."IconTexture") or getglobal(self:GetName().."Icon") or self.icon or self.Icon
|
|
if iconTex then
|
|
if iconTex.SetDesaturated then iconTex:SetDesaturated(false) end
|
|
iconTex:SetVertexColor(1, 1, 1)
|
|
end
|
|
|
|
-- Clear cooldown overlay
|
|
local cd = getglobal(self:GetName().."Cooldown") or self.cooldown
|
|
if cd then
|
|
if CooldownFrame_SetTimer then
|
|
CooldownFrame_SetTimer(cd, 0, 0, 0)
|
|
elseif CooldownFrame_Set then
|
|
CooldownFrame_Set(cd, 0, 0, 0)
|
|
end
|
|
if cd.Hide then cd:Hide() end
|
|
end
|
|
end
|
|
|
|
-- Configure drag/drop registration based on read-only state
|
|
local function SetupDragDrop(self)
|
|
if not self.isReadOnly and not self.otherChar then
|
|
if self.RegisterForDrag then
|
|
self:RegisterForDrag("LeftButton")
|
|
end
|
|
if self.RegisterForClicks then
|
|
self:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
end
|
|
if self.EnableMouse then
|
|
self:EnableMouse(true)
|
|
end
|
|
else
|
|
-- Disable drag for read-only or other character items
|
|
if self.RegisterForDrag then
|
|
self:RegisterForDrag() -- Clear drag registration
|
|
end
|
|
if self.EnableMouse then
|
|
self:EnableMouse(true) -- Still enable mouse for tooltips
|
|
end
|
|
end
|
|
end
|
|
|
|
-- 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 and slot style
|
|
local function UpdateEmptySlotBackground(self, emptySlotBg, iconSize)
|
|
if not emptySlotBg then return end
|
|
|
|
local slotStyle = "rounded"
|
|
if addon.Modules and addon.Modules.Theme then
|
|
slotStyle = addon.Modules.Theme:GetSlotStyle()
|
|
end
|
|
|
|
emptySlotBg:ClearAllPoints()
|
|
if slotStyle == "square" then
|
|
-- Flush anchors for pixel-perfect squared slots
|
|
emptySlotBg:SetTexture("Interface\\Buttons\\WHITE8x8")
|
|
emptySlotBg:SetVertexColor(0.05, 0.05, 0.05, 1)
|
|
emptySlotBg:SetPoint("TOPLEFT", self, "TOPLEFT", 0, 0)
|
|
emptySlotBg:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", 0, 0)
|
|
else
|
|
-- Extend 9px past button edges so the rounded corners of UI-EmptySlot
|
|
-- cover the square corners of the icon texture (same as GudaBags)
|
|
emptySlotBg:SetTexture("Interface\\Buttons\\UI-EmptySlot")
|
|
emptySlotBg:SetVertexColor(1, 1, 1, 1)
|
|
emptySlotBg:SetPoint("TOPLEFT", self, "TOPLEFT", -9, 9)
|
|
emptySlotBg:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", 9, -9)
|
|
end
|
|
emptySlotBg:SetTexCoord(0, 1, 0, 1)
|
|
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
|
|
|
|
-- Icon fills the full button slot
|
|
local iconDisplaySize = iconSize
|
|
|
|
iconTexture:ClearAllPoints()
|
|
iconTexture:SetPoint("CENTER", self, "CENTER", 0, 0)
|
|
iconTexture:SetWidth(iconDisplaySize)
|
|
iconTexture:SetHeight(iconDisplaySize)
|
|
iconTexture:SetTexCoord(0, 1, 0, 1)
|
|
iconTexture:Show()
|
|
|
|
-- 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 (tints the EmptySlotBg)
|
|
local function UpdateQualityBorder(self, itemQuality, itemLink, bagID, Utils)
|
|
if not itemQuality then
|
|
ResetSlotBorder(self)
|
|
HideInnerShadow(self.innerShadow)
|
|
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
|
|
-- Use the item link's title color directly (matches what the player sees)
|
|
local r, g, b
|
|
if itemLink and Utils and Utils.GetLinkColor then
|
|
r, g, b = Utils:GetLinkColor(itemLink)
|
|
end
|
|
if not r then
|
|
-- Fallback to quality-based color
|
|
if Utils and Utils.GetQualityColor then
|
|
r, g, b = Utils:GetQualityColor(itemQuality)
|
|
else
|
|
r, g, b = 1, 1, 1
|
|
end
|
|
end
|
|
TintSlotBorder(self, r, g, b)
|
|
-- Only show inner shadow for colored borders, not white (poor/common quality)
|
|
if r < 0.95 or g < 0.95 or b < 0.95 then
|
|
ShowInnerShadow(self.innerShadow, r, g, b)
|
|
else
|
|
HideInnerShadow(self.innerShadow)
|
|
end
|
|
else
|
|
ResetSlotBorder(self)
|
|
HideInnerShadow(self.innerShadow)
|
|
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 junk icon
|
|
HideJunkIcon(self)
|
|
|
|
-- Hide lock and pin icons
|
|
HideLockIcon(self)
|
|
HidePinIcon(self)
|
|
|
|
-- 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
|
|
local slotAlpha = 0.5
|
|
if addon.Modules and addon.Modules.Theme then
|
|
local sa = addon.Modules.Theme:GetValue("slotBgAlpha")
|
|
if sa then slotAlpha = sa.empty end
|
|
end
|
|
if slotAlpha > 0 then
|
|
emptySlotBg:Show()
|
|
emptySlotBg:SetAlpha(slotAlpha)
|
|
else
|
|
emptySlotBg:Hide()
|
|
end
|
|
end
|
|
|
|
if countText then countText:Hide() end
|
|
local chargesText = getglobal(self:GetName().."_Charges")
|
|
if chargesText then chargesText:Hide() end
|
|
|
|
ResetSlotBorder(self)
|
|
HideInnerShadow(self.innerShadow)
|
|
|
|
-- Hide quest elements
|
|
if self.questIcon then self.questIcon:Hide() end
|
|
end
|
|
|
|
--=====================================================
|
|
-- Main SetItem function (orchestrates helper functions)
|
|
--=====================================================
|
|
|
|
-- Set item data
|
|
function Guda_ItemButton_UpdateCharges(button)
|
|
if not button then return end
|
|
local chargesText = getglobal(button:GetName().."_Charges")
|
|
if not chargesText then return end
|
|
|
|
-- Saved banks / other-character views have no live item instance
|
|
-- behind the displayed slot. Never read the current player slot.
|
|
if button.isReadOnly or button.otherChar then
|
|
chargesText:Hide()
|
|
return
|
|
end
|
|
|
|
local charges = nil
|
|
if button.hasItem and button.itemData and addon.Modules.ItemDetection then
|
|
charges = addon.Modules.ItemDetection:GetCharges(button.itemData, button.bagID, button.slotID)
|
|
end
|
|
|
|
if charges and charges > 0 then
|
|
chargesText:SetText("x" .. charges)
|
|
chargesText:Show()
|
|
else
|
|
chargesText:Hide()
|
|
end
|
|
end
|
|
|
|
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)
|
|
-- Pooled buttons may have carried a drop-target glow from the previous
|
|
-- render — clear it before deciding what this button is now.
|
|
StopDropTargetGlow(self)
|
|
|
|
-- Drop-target pseudo-items (empty category placeholders during drag).
|
|
-- Shown with the category's icon, dim+desaturated, with a pulsing green
|
|
-- glow. OnReceiveDrag/OnClick will route to AssignItemToCategory.
|
|
if itemData and itemData.isDropTarget then
|
|
self.bagID = 0
|
|
self.slotID = 0
|
|
self.bagIndex = -100
|
|
self.itemData = itemData
|
|
self.isBank = isBank or false
|
|
self.otherChar = nil
|
|
self.isReadOnly = false
|
|
self.isMail = false
|
|
self.hasItem = false
|
|
self.isDropTarget = true
|
|
self.dropTargetCategoryId = itemData.categoryId
|
|
|
|
SetupDragDrop(self)
|
|
|
|
local iconSize = 37
|
|
if addon and addon.Modules and addon.Modules.Utils and addon.Modules.Utils.SafeCall then
|
|
iconSize = addon.Modules.Utils:SafeCall("DB", "GetSetting", "iconSize") or iconSize
|
|
end
|
|
self:SetWidth(iconSize)
|
|
self:SetHeight(iconSize)
|
|
|
|
if SetItemButtonTexture then
|
|
SetItemButtonTexture(self, itemData.texture)
|
|
end
|
|
local iconTexture = getglobal(self:GetName().."IconTexture") or getglobal(self:GetName().."Icon") or self.icon or self.Icon
|
|
if iconTexture then
|
|
iconTexture:SetTexture(itemData.texture)
|
|
-- Mirror PositionIconAndBorders() so the category icon fills the
|
|
-- full button at the user's configured iconSize. PositionIconAndBorders
|
|
-- itself is guarded by `self.hasItem`, which drop-targets don't set.
|
|
iconTexture:ClearAllPoints()
|
|
iconTexture:SetPoint("CENTER", self, "CENTER", 0, 0)
|
|
iconTexture:SetWidth(iconSize)
|
|
iconTexture:SetHeight(iconSize)
|
|
iconTexture:SetTexCoord(0, 1, 0, 1)
|
|
iconTexture:Show()
|
|
if iconTexture.SetDesaturated then iconTexture:SetDesaturated(true) end
|
|
iconTexture:SetVertexColor(0.6, 1.0, 0.6)
|
|
end
|
|
if SetItemButtonCount then SetItemButtonCount(self, 0) end
|
|
self:SetAlpha(0.85)
|
|
|
|
EnsureDropTargetGlow(self)
|
|
self:Show()
|
|
return
|
|
end
|
|
|
|
-- Set button properties
|
|
self.bagID = bagID
|
|
self.slotID = slotID
|
|
if self.SetID then
|
|
self:SetID(slotID or 0)
|
|
end
|
|
self.bagIndex = bagID or -100
|
|
self.itemData = itemData
|
|
self.isBank = isBank or false
|
|
self.otherChar = otherCharName
|
|
self.isReadOnly = isReadOnly or false
|
|
self.isMail = false
|
|
self.mailIndex = nil
|
|
self.mailItemIndex = nil
|
|
self.mailData = nil
|
|
|
|
-- Configure drag/drop
|
|
SetupDragDrop(self)
|
|
|
|
-- Default to true if not specified
|
|
if matchesFilter == nil then
|
|
matchesFilter = true
|
|
end
|
|
|
|
-- Get UI elements
|
|
local countText = getglobal(self:GetName().."Count")
|
|
local chargesText = getglobal(self:GetName().."_Charges")
|
|
local emptySlotBg = getglobal(self:GetName().."_EmptySlotBg")
|
|
local Utils = addon and addon.Modules and addon.Modules.Utils
|
|
|
|
-- Get icon size setting
|
|
local iconSize = 37
|
|
if Utils and Utils.SafeCall then
|
|
iconSize = Utils:SafeCall("DB", "GetSetting", "iconSize") or iconSize
|
|
end
|
|
if addon and addon.Constants then
|
|
iconSize = iconSize or addon.Constants.BUTTON_SIZE
|
|
end
|
|
self:SetWidth(iconSize)
|
|
self:SetHeight(iconSize)
|
|
|
|
-- Get item display info (texture, count, hasItem)
|
|
local displayTexture, displayCount, hasItem = GetItemDisplayInfo(bagID, slotID, itemData, self.isReadOnly)
|
|
self.hasItem = hasItem
|
|
|
|
-- Apply display based on whether slot has item
|
|
if self.hasItem then
|
|
-- Set texture
|
|
if SetItemButtonTexture then
|
|
SetItemButtonTexture(self, displayTexture)
|
|
end
|
|
local iconTexture = getglobal(self:GetName().."IconTexture") or getglobal(self:GetName().."Icon") or self.icon or self.Icon
|
|
if iconTexture and displayTexture then
|
|
iconTexture:SetTexture(displayTexture)
|
|
iconTexture:Show()
|
|
end
|
|
|
|
-- Set count
|
|
if SetItemButtonCount then SetItemButtonCount(self, displayCount or 1) end
|
|
|
|
-- Update cooldown overlay for live items
|
|
if not self.isReadOnly and not self.otherChar and Guda_ItemButton_UpdateCooldown then
|
|
Guda_ItemButton_UpdateCooldown(self)
|
|
else
|
|
local cd = getglobal(self:GetName().."Cooldown") or self.cooldown
|
|
if cd and cd.Hide then cd:Hide() end
|
|
end
|
|
|
|
-- Update unusable red overlay tint (cache-only on the layout path;
|
|
-- deferred pass in BagFrame runs the full scan spread via QueueWork).
|
|
if Guda_ItemButton_UpdateUsableTint then
|
|
Guda_ItemButton_UpdateUsableTint(self, true)
|
|
end
|
|
else
|
|
-- Clear empty slot
|
|
ClearItemButton(self, emptySlotBg, countText, bagID)
|
|
end
|
|
|
|
-- Update tracking checkmark
|
|
UpdateTrackingCheckmark(self, Utils)
|
|
local check = getglobal(self:GetName().."_Check")
|
|
if check then
|
|
local isTracked = false
|
|
if self.hasItem 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
|
|
|
|
-- Update lock and pin icons
|
|
UpdateLockIcon(self, iconSize)
|
|
UpdatePinIcon(self, iconSize)
|
|
|
|
-- Update empty slot bg anchors/texture based on slot style
|
|
UpdateEmptySlotBackground(self, emptySlotBg, iconSize)
|
|
|
|
-- Also resize the underlying slot textures so the border/background scale with the button.
|
|
-- On 1.12/Turtle, ItemButtonTemplate uses fixed-size textures, so we explicitly size them
|
|
-- to match the current iconSize instead of letting them use their default dimensions.
|
|
|
|
-- For 1.12.1 compatibility, access textures using both methods and proper naming
|
|
-- We'll set the NormalTexture later based on whether slot is empty or filled
|
|
|
|
-- ICON TEXTURE: Ensure we use the correct name ($parentIconTexture is standard)
|
|
local iconTexture = getglobal(self:GetName().."IconTexture") or getglobal(self:GetName().."Icon") or self.icon or self.Icon
|
|
|
|
-- Pushed texture follows the button size/position
|
|
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 for mouseover (not used for search)
|
|
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 (centered square pattern when slot is "active")
|
|
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
|
|
|
|
|
|
-- Apply icon font size setting to stack count text
|
|
if countText and countText.GetFont then
|
|
local font, _, flags = countText:GetFont()
|
|
local fontSize = 12
|
|
if Utils and Utils.SafeCall then
|
|
fontSize = Utils:SafeCall("DB", "GetSetting", "iconFontSize") or fontSize
|
|
end
|
|
countText:SetFont(font, fontSize, flags)
|
|
|
|
-- Adjust count text position based on icon size for better alignment
|
|
countText:ClearAllPoints()
|
|
if iconSize < 44 then
|
|
-- Smaller offset for small icons
|
|
countText:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -1, 1)
|
|
else
|
|
-- Standard offset for larger icons
|
|
countText:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -3, 3)
|
|
end
|
|
end
|
|
|
|
-- Apply icon font size setting to charges text (positioned above stack count)
|
|
if chargesText and chargesText.GetFont then
|
|
local font, _, flags = chargesText:GetFont()
|
|
local fontSize = 12
|
|
if Utils and Utils.SafeCall then
|
|
fontSize = Utils:SafeCall("DB", "GetSetting", "iconFontSize") or fontSize
|
|
end
|
|
chargesText:SetFont(font, fontSize, flags)
|
|
chargesText:SetTextColor(1, 0.82, 0)
|
|
chargesText:ClearAllPoints()
|
|
if iconSize < 44 then
|
|
chargesText:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -1, 1)
|
|
else
|
|
chargesText:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -3, 3)
|
|
end
|
|
end
|
|
|
|
-- Get live metadata if in live mode (quality, link, lock status)
|
|
local itemQuality, itemLink, isLocked
|
|
if not self.isReadOnly and bagID and slotID and self.hasItem then
|
|
-- Query live game state for metadata
|
|
-- Special handling for bank main bag (bagID == -1) which uses inventory slot API
|
|
if self.isBank and bagID == -1 then
|
|
local invSlot = 39 + slotID
|
|
itemLink = GetInventoryItemLink("player", invSlot)
|
|
isLocked = IsInventoryItemLocked(invSlot)
|
|
-- Quality will be determined from GetItemInfo below
|
|
else
|
|
local _, _, locked, quality = GetContainerItemInfo(bagID, slotID)
|
|
itemLink = GetContainerItemLink(bagID, slotID)
|
|
itemQuality = quality
|
|
isLocked = locked
|
|
end
|
|
|
|
-- Fall back to itemData.quality if live query returned nil (timing issue on bank open)
|
|
if itemQuality == nil and itemData and itemData.quality then
|
|
itemQuality = itemData.quality
|
|
end
|
|
|
|
-- Ensure itemData is populated for live items (needed for ItemDetection)
|
|
if itemLink then
|
|
local itemName, _, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture = GetItemInfo(itemLink)
|
|
-- Always prefer GetItemInfo quality — GetContainerItemInfo often
|
|
-- returns wrong quality (e.g. 1/white) for epic tokens in TurtleWoW 1.12
|
|
if itemRarity then
|
|
itemQuality = itemRarity
|
|
end
|
|
if itemQuality == nil and itemData and itemData.quality then
|
|
itemQuality = itemData.quality
|
|
end
|
|
if not itemData then
|
|
-- Create new itemData
|
|
if itemName then
|
|
itemData = {
|
|
link = itemLink,
|
|
name = itemName,
|
|
quality = itemRarity or itemQuality or 0,
|
|
class = itemType,
|
|
subclass = itemSubType,
|
|
texture = itemTexture,
|
|
count = 1,
|
|
}
|
|
self.itemData = itemData
|
|
end
|
|
else
|
|
-- ALWAYS update itemData.link with live link to ensure correct detection after swaps
|
|
-- This is critical: cached itemData might have stale link from before a swap
|
|
itemData.link = itemLink
|
|
-- Update other fields only if missing
|
|
if itemData.quality == nil then itemData.quality = itemRarity or itemQuality or 0 end
|
|
if not itemData.class and itemType then itemData.class = itemType end
|
|
if not itemData.subclass and itemSubType then itemData.subclass = itemSubType end
|
|
if not itemData.name and itemName then itemData.name = itemName end
|
|
self.itemData = itemData
|
|
end
|
|
end
|
|
elseif itemData then
|
|
-- Use cached metadata from database
|
|
itemQuality = itemData.quality
|
|
itemLink = itemData.link
|
|
isLocked = itemData.locked
|
|
end
|
|
|
|
if self.hasItem then
|
|
-- Icon already set above based on mode (live vs cached)
|
|
|
|
-- Gray out locked items (being traded, mailed, or auctioned)
|
|
-- Don't desaturate items from other characters since they're read-only anyway
|
|
if not self.otherChar and not self.isReadOnly then
|
|
SetItemButtonDesaturated(self, isLocked, 0.5, 0.5, 0.5)
|
|
end
|
|
|
|
-- Hide NormalTexture for filled slots
|
|
self:SetNormalTexture("")
|
|
local normalBorder = getglobal(self:GetName().."NormalTexture")
|
|
if normalBorder then
|
|
normalBorder:SetTexture("")
|
|
end
|
|
|
|
-- Show slot background overlay on filled items
|
|
if emptySlotBg then
|
|
local slotStyle = "rounded"
|
|
if addon.Modules and addon.Modules.Theme then
|
|
slotStyle = addon.Modules.Theme:GetSlotStyle()
|
|
end
|
|
if slotStyle == "square" then
|
|
-- Square mode: no rounded overlay needed, hide the slot bg
|
|
emptySlotBg:Hide()
|
|
else
|
|
-- Rounded mode: show UI-EmptySlot so rounded corners cover the icon
|
|
emptySlotBg:Show()
|
|
emptySlotBg:SetAlpha(1)
|
|
end
|
|
end
|
|
|
|
-- Check if item is junk and get category mark using the CategoryManager
|
|
local isJunk = false
|
|
local categoryMarkTexture = nil
|
|
if itemData and addon.Modules.CategoryManager then
|
|
local category = addon.Modules.CategoryManager:CategorizeItem(itemData, bagID, slotID, self.otherChar)
|
|
isJunk = (category == "Junk")
|
|
-- Get category mark icon if set
|
|
local catDef = addon.Modules.CategoryManager:GetCategory(category)
|
|
if catDef and catDef.categoryMark then
|
|
categoryMarkTexture = catDef.categoryMark
|
|
end
|
|
end
|
|
|
|
-- Update category mark overlay (bottom-left of icon texture)
|
|
if categoryMarkTexture then
|
|
local iconTex = getglobal(self:GetName().."IconTexture") or getglobal(self:GetName().."Icon") or self.icon or self.Icon
|
|
local anchor = iconTex or self
|
|
if not self.categoryMarkIcon then
|
|
self.categoryMarkIcon = self:CreateTexture(nil, "OVERLAY", 7)
|
|
end
|
|
local markSize = math.max(10, math.floor(iconSize * 0.3)) + 3
|
|
self.categoryMarkIcon:SetWidth(markSize)
|
|
self.categoryMarkIcon:SetHeight(markSize)
|
|
self.categoryMarkIcon:ClearAllPoints()
|
|
local markOffX, markOffY = 1, 2
|
|
if addon.Modules and addon.Modules.Theme and addon.Modules.Theme:GetSlotStyle() == "square" then
|
|
markOffX, markOffY = -1, -1
|
|
end
|
|
self.categoryMarkIcon:SetPoint("BOTTOMLEFT", anchor, "BOTTOMLEFT", markOffX, markOffY)
|
|
self.categoryMarkIcon:SetTexture(categoryMarkTexture)
|
|
self.categoryMarkIcon:SetVertexColor(1, 1, 1, 1)
|
|
self.categoryMarkIcon:SetAlpha(1)
|
|
self.categoryMarkIcon:Show()
|
|
else
|
|
if self.categoryMarkIcon then self.categoryMarkIcon:Hide() end
|
|
end
|
|
|
|
-- Search filtering and junk opacity
|
|
if matchesFilter then
|
|
if isJunk then
|
|
-- Junk items: configurable opacity (default 60%)
|
|
local junkOpacity = 0.6
|
|
if addon.Modules.DB and addon.Modules.DB.GetSetting then
|
|
junkOpacity = addon.Modules.DB:GetSetting("junkOpacity") or 0.6
|
|
end
|
|
self:SetAlpha(junkOpacity)
|
|
else
|
|
-- Normal items: full opacity (1.0)
|
|
self:SetAlpha(1.0)
|
|
end
|
|
else
|
|
-- Non-matching items: 25% opacity (0.25) - very dim
|
|
self:SetAlpha(0.25)
|
|
end
|
|
|
|
-- Show/hide junk icon (vendor sell icon in top-left corner)
|
|
UpdateJunkIcon(self, isJunk, iconSize)
|
|
|
|
-- Set count (use displayCount which was determined above based on mode)
|
|
if displayCount and displayCount > 1 then
|
|
countText:SetText(displayCount)
|
|
countText:Show()
|
|
else
|
|
countText:Hide()
|
|
end
|
|
|
|
-- Set quality border (tint slot bg) and inner shadow
|
|
do
|
|
local borderApplied = false
|
|
if itemQuality then
|
|
-- Check settings to determine if we should show borders
|
|
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
|
|
if itemLink and Utils and Utils.GetLinkColor then
|
|
r, g, b = Utils:GetLinkColor(itemLink)
|
|
end
|
|
if not r then
|
|
if Utils and Utils.GetQualityColor then
|
|
r, g, b = Utils:GetQualityColor(itemQuality)
|
|
else
|
|
r, g, b = 1, 1, 1
|
|
end
|
|
end
|
|
TintSlotBorder(self, r, g, b)
|
|
-- Only show inner shadow for colored borders, not white
|
|
if r < 0.95 or g < 0.95 or b < 0.95 then
|
|
ShowInnerShadow(self.innerShadow, r, g, b)
|
|
else
|
|
HideInnerShadow(self.innerShadow)
|
|
end
|
|
borderApplied = true
|
|
end
|
|
end
|
|
|
|
-- Quest items override with golden border
|
|
if not self.otherChar and not self.isReadOnly then
|
|
local isQuest, isQuestStarter = IsQuestItem(bagID, slotID, self.isBank, itemData)
|
|
Guda_ItemButton_UpdateQuestIcon(self, isQuest, isQuestStarter)
|
|
if isQuest then
|
|
TintSlotBorder(self, 1.0, 0.82, 0)
|
|
ShowInnerShadow(self.innerShadow, 1.0, 0.82, 0)
|
|
borderApplied = true
|
|
end
|
|
if self.questIcon then
|
|
if isQuest then
|
|
self.questIcon:Show()
|
|
else
|
|
self.questIcon:Hide()
|
|
end
|
|
end
|
|
end
|
|
|
|
if not borderApplied then
|
|
ResetSlotBorder(self)
|
|
HideInnerShadow(self.innerShadow)
|
|
end
|
|
end
|
|
|
|
-- Show/hide charges text (e.g. "x5" for Wizard Oil).
|
|
-- Kept as a standalone refresh so charge-only BAG_UPDATE events do not
|
|
-- need to rebuild the complete item button.
|
|
Guda_ItemButton_UpdateCharges(self)
|
|
|
|
-- Handle tracking toggle on click
|
|
-- Note: Tracking toggle is now handled in the main OnClick script above to avoid conflicts
|
|
-- and unified with QuestItemBar pinning logic.
|
|
|
|
self:Show()
|
|
else
|
|
self.hasItem = false
|
|
-- For empty slots, clear the icon texture
|
|
SetItemButtonTexture(self, nil)
|
|
|
|
-- Hide NormalTexture for empty slots (we use EmptySlotBg instead)
|
|
self:SetNormalTexture("")
|
|
local normalBorder = getglobal(self:GetName().."NormalTexture")
|
|
if normalBorder then
|
|
normalBorder:SetTexture("")
|
|
end
|
|
|
|
-- Show classic bag pattern background for empty slots
|
|
if emptySlotBg then
|
|
local slotAlpha = 0.5
|
|
if addon.Modules and addon.Modules.Theme then
|
|
local sa = addon.Modules.Theme:GetValue("slotBgAlpha")
|
|
if sa then slotAlpha = sa.empty end
|
|
end
|
|
if slotAlpha > 0 then
|
|
emptySlotBg:Show()
|
|
emptySlotBg:SetAlpha(slotAlpha)
|
|
else
|
|
emptySlotBg:Hide()
|
|
end
|
|
end
|
|
|
|
-- Dim empty slots when searching
|
|
if matchesFilter then
|
|
-- No search active or passes filter: normal opacity
|
|
self:SetAlpha(1.0)
|
|
else
|
|
-- Search active and doesn't match: very dim
|
|
self:SetAlpha(0.25)
|
|
end
|
|
|
|
countText:Hide()
|
|
|
|
-- Reset slot border and hide inner shadow for empty slots
|
|
ResetSlotBorder(self)
|
|
HideInnerShadow(self.innerShadow)
|
|
|
|
-- Hide quest icon for empty slots
|
|
if self.questIcon then
|
|
self.questIcon:Hide()
|
|
end
|
|
|
|
self:Show()
|
|
end
|
|
|
|
-- In 1.12.1, ItemButtonTemplate creates an icon named "$parentIconTexture"
|
|
local iconTexture = getglobal(self:GetName().."IconTexture")
|
|
if not iconTexture then
|
|
-- Fallback for other naming conventions
|
|
iconTexture = getglobal(self:GetName().."Icon") or self.icon or self.Icon
|
|
end
|
|
|
|
if iconTexture then
|
|
if self.hasItem then
|
|
-- Icon fills the full button slot
|
|
local iconDisplaySize = iconSize
|
|
iconTexture:ClearAllPoints()
|
|
iconTexture:SetPoint("CENTER", self, "CENTER", 0, 0)
|
|
iconTexture:SetWidth(iconDisplaySize)
|
|
iconTexture:SetHeight(iconDisplaySize)
|
|
-- Crop icon edges: pfUI-style inset in square mode, full in rounded
|
|
local slotStyle = "rounded"
|
|
if addon.Modules and addon.Modules.Theme then
|
|
slotStyle = addon.Modules.Theme:GetSlotStyle()
|
|
end
|
|
if slotStyle == "square" then
|
|
iconTexture:SetTexCoord(.08, .92, .08, .92)
|
|
else
|
|
iconTexture:SetTexCoord(0, 1, 0, 1)
|
|
end
|
|
iconTexture:Show()
|
|
|
|
-- Position quest icon in top-right corner
|
|
if self.questIcon then
|
|
-- Scale icon size based on button size
|
|
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
|
|
elseif not self.isMail then
|
|
-- Hide icon for empty slots, but keep it for mailbox custom icons
|
|
iconTexture:Hide()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- OnEnter handler (show tooltip)
|
|
function Guda_ItemButton_OnEnter(self)
|
|
addon:Debug("OnEnter FIRED: button=%s CursorHasItem=%s", tostring(self:GetName()), tostring(CursorHasItem and CursorHasItem()))
|
|
|
|
-- Drop-target placeholder: simple tooltip, skip the normal item tooltip path.
|
|
if self.isDropTarget then
|
|
GameTooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
|
GameTooltip:ClearAllPoints()
|
|
GameTooltip:SetPoint("BOTTOMRIGHT", self, "TOPLEFT", 10, 0)
|
|
local catName = self.dropTargetCategoryId or ""
|
|
local label = (Guda_L and Guda_L["Drop to assign to %s"]) or "Drop to assign to %s"
|
|
GameTooltip:SetText(format(label, catName), 1, 1, 1)
|
|
GameTooltip:Show()
|
|
return
|
|
end
|
|
|
|
-- Category drag-drop: show "+" indicator when hovering with cursor item in category view
|
|
local hasCursor = CursorHasItem and CursorHasItem()
|
|
if hasCursor then
|
|
addon:Debug("DropInd OnEnter: hasItem=%s otherChar=%s isBank=%s", tostring(self.hasItem), tostring(self.otherChar), tostring(self.isBank))
|
|
end
|
|
if hasCursor and self.hasItem and not self.otherChar then
|
|
local inCatView = IsInCategoryView(self.isBank)
|
|
addon:Debug("DropInd: inCatView=%s hasItemData=%s hasCatMgr=%s", tostring(inCatView), tostring(self.itemData ~= nil), tostring(addon.Modules.CategoryManager ~= nil))
|
|
if inCatView and self.itemData and addon.Modules.CategoryManager then
|
|
-- Don't show indicator if dragged item is already in the same category
|
|
local targetCategory = addon.Modules.CategoryManager:CategorizeItem(self.itemData, self.bagID, self.slotID, self.otherChar)
|
|
local cursorCategory = GetCursorItemCategory()
|
|
addon:Debug("DropInd: targetCat=%s cursorCat=%s cooldown=%s", tostring(targetCategory), tostring(cursorCategory), tostring(GetTime() < dropCooldownTime))
|
|
if targetCategory and cursorCategory and targetCategory ~= cursorCategory then
|
|
self.categoryId = targetCategory
|
|
addon:Debug("DropInd: SHOWING indicator for %s", tostring(targetCategory))
|
|
ShowCategoryDropIndicator(self)
|
|
else
|
|
addon:Debug("DropInd: NOT showing - same=%s targetNil=%s cursorNil=%s", tostring(targetCategory == cursorCategory), tostring(targetCategory == nil), tostring(cursorCategory == nil))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Highlight the corresponding bag button in the footer (works for empty and filled slots)
|
|
if not self.otherChar and self.bagID then
|
|
if self.isBank then
|
|
-- Bank item - highlight bank bag button
|
|
Guda_BankFrame_HighlightBagButton(self.bagID)
|
|
else
|
|
-- Regular bag item - highlight bag button
|
|
Guda_BagFrame_HighlightBagButton(self.bagID)
|
|
end
|
|
end
|
|
|
|
-- Early return for empty slots (no tooltip needed)
|
|
if not self.hasItem and not self.isMail then
|
|
return
|
|
end
|
|
|
|
-- Check if pfUI cursor tooltip mode is active
|
|
local pfuiCursorMode = false
|
|
if pfUI and pfUI.env and pfUI.env.C and pfUI.env.C.tooltip and pfUI.env.C.tooltip.position == "cursor" then
|
|
pfuiCursorMode = true
|
|
end
|
|
|
|
-- Set tooltip owner and position
|
|
if pfuiCursorMode then
|
|
-- For pfUI cursor mode, use ANCHOR_CURSOR like pfUI does
|
|
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
|
|
else
|
|
-- Standard positioning relative to item button
|
|
GameTooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
|
GameTooltip:ClearAllPoints()
|
|
GameTooltip:SetPoint("BOTTOMRIGHT", self, "TOPLEFT", 10, 0)
|
|
end
|
|
|
|
-- Mailbox tooltip handling
|
|
if self.isMail then
|
|
local currentPlayerName = addon.Modules.DB:GetPlayerFullName()
|
|
local isMailboxOpen = addon.Modules.MailboxScanner and addon.Modules.MailboxScanner:IsMailboxOpen()
|
|
|
|
if (not self.otherChar or self.otherChar == currentPlayerName) and self.mailIndex and isMailboxOpen then
|
|
-- Live mailbox for current character (only when mailbox is actually open)
|
|
GameTooltip:SetInboxItem(self.mailIndex, self.mailItemIndex or 1)
|
|
elseif self.itemData and (self.itemData.link or self.itemData.itemID) then
|
|
-- Read-only / other character mailbox OR current character mailbox when closed
|
|
GameTooltip.GudaViewedCharacter = self.otherChar or currentPlayerName
|
|
if self.itemData.link then
|
|
GudaSetTooltipHyperlink(GameTooltip, self.itemData.link)
|
|
else
|
|
GameTooltip:SetHyperlink("item:" .. self.itemData.itemID .. ":0:0:0")
|
|
end
|
|
elseif self.itemData and self.itemData.name then
|
|
-- Money or generic mail
|
|
GameTooltip:AddLine(self.itemData.name, 1, 1, 1)
|
|
elseif self.mailData and self.mailData.money and self.mailData.money > 0 then
|
|
-- Fallback for money only mail
|
|
GameTooltip:AddLine("Money", 1, 1, 1)
|
|
end
|
|
|
|
-- Add mailbox metadata if available
|
|
local mailData = self.mailData
|
|
|
|
if mailData then
|
|
-- If we already added a line (for money/generic), or SetHyperlink/SetInboxItem added lines,
|
|
-- we might want a separator if we're adding sender info.
|
|
if GameTooltip:NumLines() > 0 then
|
|
GameTooltip:AddLine(" ")
|
|
end
|
|
|
|
if mailData.sender then
|
|
GameTooltip:AddLine("From: " .. mailData.sender, 1, 1, 1)
|
|
end
|
|
if mailData.subject then
|
|
GameTooltip:AddLine("Subject: " .. mailData.subject, 1, 1, 0.8)
|
|
end
|
|
if (mailData.money or 0) > 0 and not (self.itemData and self.itemData.name == "Money") then
|
|
GameTooltip:AddLine("Money: " .. addon.Modules.Utils:FormatMoney(mailData.money), 1, 1, 1)
|
|
end
|
|
if (mailData.CODAmount or 0) > 0 then
|
|
GameTooltip:AddLine("COD: " .. addon.Modules.Utils:FormatMoney(mailData.CODAmount), 1, 0, 0)
|
|
end
|
|
if mailData.daysLeft then
|
|
GameTooltip:AddLine("Days left: " .. math.floor(mailData.daysLeft), 0.5, 0.5, 0.5)
|
|
end
|
|
end
|
|
|
|
|
|
GameTooltip:Show()
|
|
return
|
|
elseif self.otherChar or self.isReadOnly then
|
|
GameTooltip.GudaViewedCharacter = self.otherChar
|
|
if self.itemData and self.itemData.link then
|
|
local ok = pcall(GudaSetTooltipHyperlink, GameTooltip, self.itemData.link)
|
|
if not ok then
|
|
GameTooltip:Hide()
|
|
return
|
|
end
|
|
else
|
|
GameTooltip:Hide()
|
|
return
|
|
end
|
|
-- Special handling for bank main bag when bank might be closed
|
|
elseif self.isBank and self.bagID == -1 then
|
|
local bankFrame = getglobal("BankFrame")
|
|
if bankFrame and bankFrame:IsVisible() then
|
|
-- Bank is open - use SetBagItem which will trigger inventory slot handling
|
|
GameTooltip:SetBagItem(self.bagID, self.slotID)
|
|
elseif self.itemData and self.itemData.link then
|
|
-- Bank is closed - use cached link
|
|
GudaSetTooltipHyperlink(GameTooltip, self.itemData.link)
|
|
end
|
|
elseif self.bagID == -2 then
|
|
-- Keyring (bag -2): in 1.12 the native SetBagItem builds the keyring tooltip
|
|
-- by internally calling SetHyperlink with the FULL colored link
|
|
-- (|cff..|Hitem:..|h[Name]|h|r). When other addons (AtlasLoot, WoWTranslate)
|
|
-- have re-hooked SetHyperlink in an order that bypasses Guda's stripping, that
|
|
-- full link reaches a hook whose captured original is the raw Blizzard
|
|
-- SetHyperlink — which only accepts the BARE "item:ID:0:0:0" form and otherwise
|
|
-- throws "unknown link type". We avoid that internal path entirely by calling
|
|
-- SetHyperlink ourselves with the bare form (GudaSetTooltipHyperlink extracts it).
|
|
local link = GetContainerItemLink(self.bagID, self.slotID)
|
|
if link then
|
|
GudaSetTooltipHyperlink(GameTooltip, link)
|
|
else
|
|
GameTooltip:SetBagItem(self.bagID, self.slotID)
|
|
end
|
|
else
|
|
-- For live mode: use SetBagItem for all bags
|
|
GameTooltip:SetBagItem(self.bagID, self.slotID)
|
|
end
|
|
|
|
GameTooltip:Show()
|
|
|
|
-- Notify third-party tooltip addons (e.g. GFW_DisenchantPredictor, EnhTooltip)
|
|
-- These addons hook ContainerFrameItemButton_OnEnter which Guda's custom buttons bypass
|
|
if self.bagID and self.slotID and not self.otherChar and not self.isReadOnly then
|
|
local link = GetContainerItemLink(self.bagID, self.slotID)
|
|
if link then
|
|
local _, _, itemLink = string.find(link, "(item:%d+:%d+:%d+:%d+)")
|
|
if itemLink then
|
|
local itemName = GetItemInfo(itemLink)
|
|
-- GFWTooltip callback system
|
|
if GFWTooltip_Callbacks then
|
|
GameTooltip.gfwDone = nil -- Reset so callbacks are not skipped
|
|
for modName, callback in pairs(GFWTooltip_Callbacks) do
|
|
if type(callback) == "function" then
|
|
callback(GameTooltip, itemName, link, "CONTAINER")
|
|
end
|
|
end
|
|
GameTooltip:Show()
|
|
end
|
|
-- EnhTooltip callback system
|
|
if EnhTooltip and EnhTooltip.TooltipCall then
|
|
EnhTooltip.TooltipCall(GameTooltip, itemName, link, nil, nil, self.bagID, self.slotID)
|
|
GameTooltip:Show()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Debug: print item classification info to chat when debug mode is active
|
|
if addon.DEBUG and self.hasItem and self.bagID and self.slotID and not self._debugPrinted then
|
|
local link = self.itemData and self.itemData.link or GetContainerItemLink(self.bagID, self.slotID)
|
|
if link then
|
|
local itemID = addon.Modules.Utils:ExtractItemID(link)
|
|
if itemID then
|
|
local itemName, _, itemRarity, itemLevel, itemCategory, itemType, _, itemSubType = GetItemInfo(itemID)
|
|
addon:Debug("Item: %s (ID: %s)", tostring(itemName), tostring(itemID))
|
|
addon:Debug(" Category: %s | Type: %s | SubType: %s", tostring(itemCategory), tostring(itemType), tostring(itemSubType))
|
|
addon:Debug(" Quality: %s | iLvl: %s", tostring(itemRarity), tostring(itemLevel))
|
|
if addon.Modules.ItemDetection then
|
|
local props = addon.Modules.ItemDetection:GetItemProperties({link = link}, self.bagID, self.slotID)
|
|
local flags = {}
|
|
if props.isQuestItem then table.insert(flags, "Quest") end
|
|
if props.isQuestStarter then table.insert(flags, "Starter") end
|
|
if props.isQuestUsable then table.insert(flags, "Usable") end
|
|
if props.isJunk then table.insert(flags, "Junk") end
|
|
if props.isPermanentEnchant then table.insert(flags, "Enchant") end
|
|
if props.isUnusable then table.insert(flags, "Unusable") end
|
|
local flagStr = table.getn(flags) > 0 and table.concat(flags, ", ") or "none"
|
|
addon:Debug(" Flags: %s", flagStr)
|
|
end
|
|
self._debugPrinted = true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Handle merchant sell cursor
|
|
if MerchantFrame:IsShown() and not self.isBank and not self.otherChar and self.hasItem then
|
|
ShowContainerSellCursor(self.bagID, self.slotID)
|
|
else
|
|
ResetCursor()
|
|
end
|
|
end
|
|
|
|
|
|
-- OnLeave handler
|
|
function Guda_ItemButton_OnLeave(self)
|
|
-- Delay hide of drop indicator to allow mouse to move to indicator frame
|
|
if activeCategoryDropIndicator == self and categoryDropIndicator and categoryDropIndicator:IsShown() then
|
|
-- Use pooled timer to check if mouse moved to the indicator (avoids frame leak)
|
|
Guda_ScheduleTimer(0.05, function()
|
|
-- If mouse is over the indicator, don't hide
|
|
if categoryDropIndicator and categoryDropIndicator:IsMouseOver() then
|
|
return
|
|
end
|
|
-- If mouse is over the parent button, don't hide
|
|
if activeCategoryDropIndicator and MouseIsOver(activeCategoryDropIndicator) then
|
|
return
|
|
end
|
|
HideCategoryDropIndicator()
|
|
end)
|
|
else
|
|
HideCategoryDropIndicator()
|
|
end
|
|
self._debugPrinted = nil
|
|
-- Clear any viewed character hint on the tooltip when leaving
|
|
if GameTooltip then
|
|
GameTooltip.GudaViewedCharacter = nil
|
|
end
|
|
GameTooltip:Hide()
|
|
ResetCursor()
|
|
|
|
-- Clear bag button highlighting
|
|
if not self.otherChar then
|
|
if self.isBank then
|
|
Guda_BankFrame_ClearBagButtonHighlight()
|
|
else
|
|
Guda_BagFrame_ClearBagButtonHighlight()
|
|
end
|
|
end
|
|
end
|