Files
Guda-ClassicAPI/UI/ItemButtonPerformanceClassicAPI.lua
T

504 lines
17 KiB
Lua

-- Guda ClassicAPI ItemButton performance layer
-- Keeps the original ItemButton behavior but removes permanent idle polling
-- and consolidates per-drop-target glow OnUpdate work into one shared driver.
-- Lua 5.0 compatible.
local addon = Guda
local ItemButtonPerformance = {}
addon.Modules.ItemButtonPerformance = ItemButtonPerformance
local cursorWatcher = nil
local originalCursorOnEvent = nil
local originalCursorOnUpdate = nil
local cursorUpdateWrapper = nil
local cursorEventWrapper = nil
-- ItemButton.lua creates the watcher with OnUpdate already active. Mirror that
-- real initial state so the first idle RefreshCursorPolling() can disable it.
local cursorPollingEnabled = true
local glowDrivers = {}
local glowDriverCount = 0
local glowScanAccum = 0
local glowPulseTime = 0
local GLOW_RESCAN_INTERVAL = 0.75
local sharedGlowDriver = CreateFrame("Frame", "Guda_SharedDropGlowDriver", UIParent)
sharedGlowDriver:Hide()
local function IsShown(frame)
return frame and frame.IsShown and frame:IsShown()
end
local function IsRelevantFrameOpen()
if IsShown(getglobal("Guda_BagFrame")) then return true end
if IsShown(getglobal("Guda_BankFrame")) then return true end
return false
end
local function CursorIsCarrying()
return CursorHasItem and CursorHasItem() and true or false
end
local function FindCursorWatcher()
if type(GetFramesRegisteredForEvent) ~= "function" then
return nil
end
local frames = { GetFramesRegisteredForEvent("CURSOR_UPDATE") }
for i = 1, table.getn(frames) do
local frame = frames[i]
if frame
and frame._lastCarrying ~= nil
and frame._pollAccum ~= nil
and frame.GetScript
and frame:GetScript("OnUpdate") then
return frame
end
end
return nil
end
local function ShouldCursorPoll()
if IsRelevantFrameOpen() then return true end
if CursorIsCarrying() then return true end
if cursorWatcher and cursorWatcher.pendingCheck then return true end
return false
end
local function ScanGlowDrivers()
if type(EnumerateFrames) ~= "function" then return end
local frame = EnumerateFrames()
while frame do
if frame._t ~= nil and frame.GetParent then
local parent = frame:GetParent()
if parent
and parent.dropGlowDriver == frame
and parent.dropGlow then
if not glowDrivers[frame] then
glowDrivers[frame] = parent
glowDriverCount = glowDriverCount + 1
end
-- The original driver only computes the same sine wave for
-- its own button. The shared driver below does that once for
-- every active glow, so the per-button OnUpdate is redundant.
if frame.GetScript and frame:GetScript("OnUpdate") then
frame:SetScript("OnUpdate", nil)
end
end
end
frame = EnumerateFrames(frame)
end
end
local function StartSharedGlowDriver()
if not sharedGlowDriver:IsShown() then
-- BagFrame:SetDragging() redraws drop targets synchronously, so by the
-- time the cursor watcher reaches here the glow drivers normally
-- already exist. Discover them once immediately; only rare late-created
-- drivers require the slow fallback rescan below.
ScanGlowDrivers()
glowScanAccum = 0
sharedGlowDriver:Show()
end
end
sharedGlowDriver:SetScript("OnUpdate", function()
glowPulseTime = glowPulseTime + arg1
glowScanAccum = glowScanAccum + arg1
-- Rare fallback for drivers created after the initial drag redraw. Keep
-- this deliberately infrequent: EnumerateFrames is much more expensive
-- than the single shared sine calculation we are trying to optimize.
if glowScanAccum >= GLOW_RESCAN_INTERVAL then
glowScanAccum = 0
ScanGlowDrivers()
end
local phase = (math.sin(glowPulseTime * 5.24) + 1) * 0.5
local alpha = 0.15 + phase * 0.30
local anyActive = false
for driver, parent in pairs(glowDrivers) do
if driver and parent and driver.IsShown and driver:IsShown()
and parent.dropGlow and parent.dropGlow.IsShown
and parent.dropGlow:IsShown() then
parent.dropGlow:SetAlpha(alpha)
anyActive = true
end
end
if not CursorIsCarrying() and not anyActive then
this:Hide()
end
end)
local function SetCursorPolling(enabled)
if not cursorWatcher or not originalCursorOnUpdate then return end
enabled = enabled and true or false
if enabled == cursorPollingEnabled then return end
cursorPollingEnabled = enabled
if enabled then
cursorWatcher:SetScript("OnUpdate", cursorUpdateWrapper)
else
cursorWatcher:SetScript("OnUpdate", nil)
end
end
local function RefreshCursorPolling()
if ShouldCursorPoll() then
SetCursorPolling(true)
if CursorIsCarrying() then
StartSharedGlowDriver()
end
else
SetCursorPolling(false)
end
end
local function HookVisibilityFrame(frame)
if not frame or frame._gudaClassicAPIPerfHooked then return end
frame._gudaClassicAPIPerfHooked = true
local oldOnShow = frame:GetScript("OnShow")
local oldOnHide = frame:GetScript("OnHide")
frame:SetScript("OnShow", function()
if oldOnShow then oldOnShow() end
RefreshCursorPolling()
end)
frame:SetScript("OnHide", function()
if oldOnHide then oldOnHide() end
-- The other bag/bank frame can change visibility in the same call
-- stack, so defer the idle decision one frame.
if Guda_ScheduleTimer then
Guda_ScheduleTimer(0, RefreshCursorPolling)
else
RefreshCursorPolling()
end
end)
end
function ItemButtonPerformance:Initialize()
if cursorWatcher then return true end
cursorWatcher = FindCursorWatcher()
if not cursorWatcher then
addon:Debug("ClassicAPI ItemButton perf: cursor watcher not found")
return false
end
originalCursorOnEvent = cursorWatcher:GetScript("OnEvent")
originalCursorOnUpdate = cursorWatcher:GetScript("OnUpdate")
cursorUpdateWrapper = function()
-- Preserve the original 1.12 safety-net logic while relevant UI is
-- open or an item is being carried.
originalCursorOnUpdate()
if CursorIsCarrying() then
StartSharedGlowDriver()
end
if not ShouldCursorPoll() then
SetCursorPolling(false)
end
end
cursorEventWrapper = function()
if originalCursorOnEvent then
originalCursorOnEvent()
end
RefreshCursorPolling()
end
cursorWatcher:SetScript("OnEvent", cursorEventWrapper)
HookVisibilityFrame(getglobal("Guda_BagFrame"))
HookVisibilityFrame(getglobal("Guda_BankFrame"))
-- Prime discovery once so any lazily-created glow drivers that already
-- exist are converted to the shared driver immediately.
ScanGlowDrivers()
RefreshCursorPolling()
addon:Debug("ClassicAPI ItemButton perf initialized: idle cursor polling gated, shared glow driver active")
return true
end
function ItemButtonPerformance:GetStats()
return {
cursorWatcherFound = cursorWatcher ~= nil,
cursorPollingEnabled = cursorPollingEnabled,
sharedGlowDrivers = glowDriverCount,
sharedGlowActive = sharedGlowDriver:IsShown(),
}
end
-- All XML frames are guaranteed to exist by PLAYER_LOGIN. The fallback retry
-- handles unusual load orders without keeping another permanent OnUpdate alive.
local initFrame = CreateFrame("Frame")
initFrame:RegisterEvent("PLAYER_LOGIN")
initFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
initFrame:SetScript("OnEvent", function()
if ItemButtonPerformance:Initialize() then
this:UnregisterEvent("PLAYER_LOGIN")
this:UnregisterEvent("PLAYER_ENTERING_WORLD")
end
end)
--=====================================================
-- Consolidated from UI/UsabilityTintClassicAPI.lua
--=====================================================
-- Guda ClassicAPI usability tint safety
-- Re-applies unusable-item tint after sorting/moves without synchronous mass scans.
-- Lua 5.0 compatible.
local addon = Guda
if not addon or not Guda_ItemButton_SetItem then return end
local OriginalSetItem = Guda_ItemButton_SetItem
local function EnsureUnusableOverlay(button)
if button.unusableOverlay then return button.unusableOverlay end
local overlay = button:CreateTexture(nil, "OVERLAY")
overlay:SetAllPoints(button)
overlay:SetTexture("Interface\\ChatFrame\\ChatFrameBackground")
overlay:Hide()
button.unusableOverlay = overlay
return overlay
end
-- Mirror ItemButton.lua's unusable color selection exactly. The original helper
-- is local to ItemButton.lua, so this safety layer cannot call it directly.
local function 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
if RED_FONT_COLOR then
return RED_FONT_COLOR.r, RED_FONT_COLOR.g, RED_FONT_COLOR.b, 1.0
end
return 0.9, 0.2, 0.2, 1.0
end
local function ApplyUnusableTint(button, unusable)
if not button then return end
local markUnusable = true
if addon.Modules and addon.Modules.DB then
local setting = addon.Modules.DB:GetSetting("markUnusableItems")
if setting ~= nil then markUnusable = setting and true or false end
end
local overlay = button.unusableOverlay
if not markUnusable or not unusable then
if overlay then overlay:Hide() end
return
end
overlay = EnsureUnusableOverlay(button)
local r, g, b, a = GetUnusableColor()
-- Match ItemButton.lua exactly: configured alpha is reduced once by 45%.
local alpha = (a or 1.0) * 0.45
overlay:SetVertexColor(r or 0.9, g or 0.2, b or 0.2, alpha)
overlay:Show()
end
local function RefreshUsabilityAsync(button, itemData, bagID, slotID, isReadOnly, otherCharName)
if not button or not itemData or not itemData.link then return end
if isReadOnly or otherCharName then return end
local detection = addon.Modules and addon.Modules.ItemDetection
if not detection then return end
-- Never scan tooltip state while the sort engine is actively moving/locking items.
-- The final post-sort redraw will call SetItem again and schedule a safe refresh.
if addon.Modules.SortEngine and addon.Modules.SortEngine.sortingInProgress then
return
end
local link = itemData.link
local cached = detection.IsUnusableCached and detection:IsUnusableCached(itemData) or nil
if cached ~= nil then
ApplyUnusableTint(button, cached)
return
end
-- Avoid duplicate jobs when BAG_UPDATE causes several redraws for the same item.
if button._gudaUsabilityPendingLink == link then return end
button._gudaUsabilityPendingLink = link
local function DoRefresh()
if button._gudaUsabilityPendingLink ~= link then return end
button._gudaUsabilityPendingLink = nil
if not button.IsShown or not button:IsShown() or not button.hasItem then return end
if button.otherChar or button.isReadOnly then return end
if button.bagID ~= bagID or button.slotID ~= slotID then return end
if not button.itemData or button.itemData.link ~= link then return end
local props = detection:GetItemProperties(button.itemData, button.bagID, button.slotID)
ApplyUnusableTint(button, props and props.isUnusable)
end
local Utils = addon.Modules and addon.Modules.Utils
if Utils and Utils.QueueWork then
Utils:QueueWork(DoRefresh)
else
DoRefresh()
end
end
function Guda_ItemButton_SetItem(self, bagID, slotID, itemData, isBank, otherCharName, matchesFilter, isReadOnly)
OriginalSetItem(self, bagID, slotID, itemData, isBank, otherCharName, matchesFilter, isReadOnly)
if not self then return end
local currentData = self.itemData or itemData
local currentLink = currentData and currentData.link or nil
-- Cancel a stale pending job if this pooled button now represents another item.
if self._gudaUsabilityPendingLink and self._gudaUsabilityPendingLink ~= currentLink then
self._gudaUsabilityPendingLink = nil
end
if self.hasItem and currentData then
RefreshUsabilityAsync(self, currentData, tonumber(bagID), tonumber(slotID), isReadOnly, otherCharName)
elseif self.unusableOverlay then
self.unusableOverlay:Hide()
end
end
addon:Debug("ClassicAPI usability tint safety enabled")
--=====================================================
-- Consolidated from UI/TooltipPositionFix.lua
--=====================================================
-- Guda item/comparison tooltip positioning fix.
-- Keeps Guda item tooltips toward the inside of the screen so comparison
-- tooltips have room to render with pfUI/DFUI and the ClassicAPI tooltip code.
local addon = Guda
if not addon or not Guda_ItemButton_OnEnter then return end
local originalOnEnter = Guda_ItemButton_OnEnter
local function IsCursorAnchored()
if GameTooltip and GameTooltip.GetAnchorType then
return GameTooltip:GetAnchorType() == "ANCHOR_CURSOR"
end
-- Compatibility with pfUI versions that expose cursor positioning only
-- through their configuration table.
if pfUI and pfUI.env and pfUI.env.C and pfUI.env.C.tooltip then
return pfUI.env.C.tooltip.position == "cursor"
end
return false
end
local function PositionMainTooltip(button)
if not button or not GameTooltip or not GameTooltip:IsShown() then return end
if IsCursorAnchored() then return end
local centerX = button.GetCenter and button:GetCenter()
local screenWidth = UIParent and UIParent.GetWidth and UIParent:GetWidth()
if not centerX or not screenWidth or screenWidth <= 0 then return end
GameTooltip:ClearAllPoints()
if centerX < (screenWidth / 2) then
-- Item is on the left: grow the tooltip group toward the right.
GameTooltip:SetPoint("BOTTOMLEFT", button, "TOPRIGHT", -10, 0)
else
-- Preserve Guda's historical position for items on the right.
GameTooltip:SetPoint("BOTTOMRIGHT", button, "TOPLEFT", 10, 0)
end
end
local function GetComparisonGap(tooltip)
local separation = 6
local edgeSize = 0
if tooltip and tooltip.GetBackdrop then
local backdrop = tooltip:GetBackdrop()
if type(backdrop) == "table" and backdrop.edgeSize then
edgeSize = backdrop.edgeSize
end
end
return separation + edgeSize
end
local function PositionComparisonTooltips()
if not GameTooltip or not GameTooltip:IsShown() then return end
local first = ShoppingTooltip1
local second = ShoppingTooltip2
if not first or not first:IsShown() then return end
local screenWidth = UIParent and UIParent.GetWidth and UIParent:GetWidth()
local left = GameTooltip.GetLeft and GameTooltip:GetLeft()
local right = GameTooltip.GetRight and GameTooltip:GetRight()
if not screenWidth or not left or not right then return end
local leftSpace = left
local rightSpace = screenWidth - right
local useLeft = leftSpace > rightSpace
local gap = GetComparisonGap(first)
first:SetOwner(GameTooltip, "ANCHOR_NONE")
first:ClearAllPoints()
if useLeft then
first:SetPoint("TOPRIGHT", GameTooltip, "TOPLEFT", -gap, -10)
else
first:SetPoint("TOPLEFT", GameTooltip, "TOPRIGHT", gap, -10)
end
if second and second:IsShown() then
second:SetOwner(first, "ANCHOR_NONE")
second:ClearAllPoints()
if useLeft then
second:SetPoint("TOPRIGHT", first, "TOPLEFT", -gap, 0)
else
second:SetPoint("TOPLEFT", first, "TOPRIGHT", gap, 0)
end
end
end
function Guda_ItemButton_OnEnter(self)
originalOnEnter(self)
-- Empty/drop-target buttons either have no item tooltip or use a small
-- custom tooltip that does not participate in equipment comparison.
if not self or self.isDropTarget or not self.hasItem then return end
if not GameTooltip or not GameTooltip:IsShown() then return end
if IsCursorAnchored() then return end
PositionMainTooltip(self)
-- Comparison tooltips may already have been shown synchronously by a
-- tooltip hook. Re-anchor them now; if they are shown later, ClassicAPI's
-- own side selection will use the corrected GameTooltip position.
PositionComparisonTooltips()
end
addon:Debug("Guda comparison tooltip positioning fix enabled")