Files
pfUI/modules/tooltip.lua
T
Brues 907d282a68 Centralize cooldown setup and settings
Every pfUI cooldown is now made by CreateCooldown(parent, kind) in the
new api/cooldown.lua, which replaces the cooldown module. A kind names
what the cooldown times: actionbar, bags, blizzard (spellbook and
character), unitframe, indicator, nameplate, buff, tooltip, totem or
lossofcontrol. The pfCooldownType, pfCooldownStyleText,
pfCooldownStyleAnimation and pfCooldownSize fields are gone. The new
file owns everything the cooldowns share:
- the numbers' font and size
- the formatter
- each kind's numbers, sweep and sweep direction
- the GCD threshold for spell kinds
- the CooldownFrame_SetTimer hook for other addons' Cooldown frames
UpdateCooldowns applies changed settings to every cooldown at once.
GetAuraTimer moves here from the unit frames.

All settings live in one C.cooldown table, on one page: Settings ->
Cooldown. It has the shared font, size, threshold, tenths and colors,
then "Show Numbers On", "Show Sweep On" and "Reverse Sweep On" lists
with a checkbox per kind. The per-module options are removed:
- each unit frame's timer text, animation and reverse, and the
  indicators' "Show Time Left"
- the nameplate debuff timer options
- the Blizzard and foreign font sizes and toggles
- Action Bars' cooldown text size
- the reverse options of bars, bags, buffs and totems
The old appearance.cd settings are not migrated.

The player buff frame and the tooltip aura icons now show the native
countdown too, which replaces their own text and OnUpdate ticking. The
buff frame keeps its inside or below-the-icon placement. Weapon
enchants report only their time left, so they start through
SetCooldownTimeLeft: numbers, no sweep. A slim poll remains for their
charges and renewals, since no event reports those. Buff indicators
now take the aura's real duration, which gives them a correct sweep.
The spellbook and character slots now hide GCD numbers like the other
spell cooldowns.
2026-10-31 19:17:14 -05:00

416 lines
16 KiB
Lua

pfUI:RegisterModule("tooltip", function ()
local rawborder, default_border = GetBorderSize()
pfUI.tooltip = CreateFrame('Frame', "pfTooltip", GameTooltip)
pfUI.tooltip.anchorframe = CreateFrame('Frame', "pfTooltipAnchor", UIParent)
pfUI.tooltip.anchorframe:SetSize(128, 72)
pfUI.tooltip.anchorframe:SetPoint("TOP", UIParent, "TOP", 0, -50)
pfUI.tooltip.anchorframe:Hide()
UpdateMovable(pfUI.tooltip.anchorframe)
function pfUI.tooltip:GetAnchorPoint()
local prefix = "TOP"
local suffix = "RIGHT"
local px, py = UIParent:GetCenter()
local tx, ty = this.anchorframe:GetCenter()
if (tx < px) then
suffix = "LEFT"
end
if (ty < py) then
prefix = "BOTTOM"
end
return prefix..suffix
end
if C.tooltip.position == "cursor" then
-- Cursor mode makes the tooltip follow the mouse. The client has no
-- mouse-move event, so following means polling GetCursorPosition() via an
-- invisible follower frame that the tooltip anchors to. The follower is
-- only shown while a tooltip is visible -- an OnUpdate fires only while its
-- frame is shown, so the poll stops the moment the tooltip hides instead
-- of running forever.
local follower, Reposition
if C.tooltip.cursoralign ~= "native" then
local size = tonumber(C.tooltip.cursoroffset) * 2
follower = CreateFrame("Frame", nil, UIParent)
follower:SetSize(size, size)
follower:Hide()
Reposition = function()
local scale = UIParent:GetScale()
local x, y = GetCursorPosition()
follower:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x/scale, y/scale)
if C.tooltip.cursoralign == "top" then
follower:SetWidth(GameTooltip:GetWidth())
end
end
follower:SetScript("OnUpdate", function()
-- throttle - cursor following doesn't need to be every frame
if (this.tick or 0) > GetTime() then return end
this.tick = GetTime() + (pfUI.throttle and pfUI.throttle:Get("tooltip_cursor") or 0.1)
Reposition()
end)
-- stop polling as soon as the tooltip is gone
pfUI.tooltip:SetScript("OnHide", function() follower:Hide() end)
end
function _G.GameTooltip_SetDefaultAnchor(tooltip, parent)
tooltip:SetOwner(parent, "ANCHOR_CURSOR")
if not follower then return end
-- position the follower right away so the tooltip doesn't flash at a
-- stale spot before the first OnUpdate tick
follower:Show()
Reposition()
if C.tooltip.cursoralign == "top" then
tooltip:SetPoint("BOTTOMLEFT", follower, "TOPLEFT", 0, 0)
elseif C.tooltip.cursoralign == "left" then
tooltip:SetPoint("BOTTOMRIGHT", follower, "LEFT", 0, 0)
elseif C.tooltip.cursoralign == "right" then
tooltip:SetPoint("BOTTOMLEFT", follower, "RIGHT", 0, 0)
end
end
end
function pfUI.tooltip:GetUnit()
pfUI.tooltip.unit = "none"
if GameTooltip:HasUnit() then
local _, guid = GameTooltip:GetUnitGUID()
local token = guid and UnitTokenFromGUID(guid)
if token then pfUI.tooltip.unit = token end
end
return pfUI.tooltip.unit
end
pfUI.tooltip.dodge = {
"pfPanelRight", "pfChatRight"
}
if C.appearance.bags.movable == "0" then
table.insert(pfUI.tooltip.dodge, "pfBag")
end
pfUI.tooltip:SetAllPoints()
pfUI.tooltip:SetScript("OnShow", function()
if C.tooltip.itemid == "1" and GameTooltip:HasItem() then
local _, _, itemID = GameTooltip:GetItem()
GameTooltip:AddLine(T["ItemID"] .. ": " .. itemID, .25,.5,1)
GameTooltip:Show()
elseif C.tooltip.spellid == "1" and GameTooltip:HasSpell() then
local _, _, spellID = GameTooltip:GetSpell()
GameTooltip:AddLine(T["SpellID"] .. ": " .. spellID, .25,.5,1)
GameTooltip:Show()
end
if GameTooltip:GetAnchorType() == "ANCHOR_NONE" then
GameTooltip:ClearAllPoints()
if C.tooltip.position == "bottom" then
if pfUI.panel and pfUI.panel.right:IsShown() then
GameTooltip:SetPoint("BOTTOMRIGHT", pfUI.panel.right, "TOPRIGHT", 0, default_border*2)
else
GameTooltip:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -5, 5)
end
elseif C.tooltip.position == "chat" then
local anchor = nil
for _, frame in pairs(pfUI.tooltip.dodge) do
if _G[frame] and _G[frame]:IsShown() then
anchor = _G[frame]
end
end
if anchor then
GameTooltip:SetPoint("BOTTOMRIGHT", anchor, "TOPRIGHT", 0, default_border*3)
else
GameTooltip:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -5, 5)
end
elseif C.tooltip.position == "free" then
local point = this:GetAnchorPoint()
if point then
GameTooltip:SetPoint(point, this.anchorframe, point, 0, 0)
end
end
end
end)
pfUI.tooltipStatusBar = CreateFrame('Frame', nil, GameTooltipStatusBar)
pfUI.tooltipStatusBar:SetPoint("TOPLEFT", 0, 8)
pfUI.tooltipStatusBar:SetPoint("TOPRIGHT", 0, 8)
pfUI.tooltipStatusBar:SetHeight(12)
pfUI.tooltipStatusBar:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
pfUI.tooltipStatusBar:SetScript("OnEvent", function()
this.name = UnitName("mouseover")
this.level = UnitLevel("mouseover")
end)
pfUI.tooltipStatusBar.text = CreateFrame("Frame", nil, pfUI.tooltipStatusBar)
pfUI.tooltipStatusBar.text:SetFrameLevel(16)
pfUI.tooltipStatusBar.text:SetAllPoints()
pfUI.tooltipStatusBar.HP = pfUI.tooltipStatusBar.text:CreateFontString("Status", "OVERLAY", "GameFontNormal")
pfUI.tooltipStatusBar.HP:SetAllPoints()
pfUI.tooltipStatusBar.HP:SetNonSpaceWrap(false)
pfUI.tooltipStatusBar.HP:SetFontObject(GameFontWhite)
pfUI.tooltipStatusBar.HP:SetFont(C.tooltip.font_tooltip, C.tooltip.font_tooltip_size + 2, "OUTLINE")
if GameTooltip.SetClampRectInsets then
GameTooltip:SetClampRectInsets(0, 0, 16, 0)
else
GameTooltipStatusBar:SetClampedToScreen(true)
pfUI.tooltipStatusBar:SetClampedToScreen(true)
end
pfUI.tooltipStatusBar:SetScript("OnUpdate", function()
local hp = GameTooltipStatusBar:GetValue()
local _, hpmax = GameTooltipStatusBar:GetMinMaxValues()
local rhp, rhpmax, estimated
if hpmax > 100 or (round(hpmax/100*hp) ~= hp) then
rhp, rhpmax = hp, hpmax
elseif pfUI.libhealth and pfUI.libhealth.enabled then
rhp, rhpmax, estimated = pfUI.libhealth:GetUnitHealthByName(this.name, this.level, tonumber(hp), tonumber(hpmax))
end
if C.tooltip.alwaysperc == "0" and ( estimated or hpmax > 100 or round(hpmax/100*hp) ~= hp ) then
pfUI.tooltipStatusBar.HP:SetFormattedText("%s / %s", Abbreviate(rhp), Abbreviate(rhpmax))
elseif hpmax > 0 then
pfUI.tooltipStatusBar.HP:SetFormattedText("%s%%", ceil(hp/hpmax*100))
else
pfUI.tooltipStatusBar.HP:SetText("")
end
end)
GameTooltipStatusBar:SetHeight(8)
GameTooltipStatusBar:ClearAllPoints()
GameTooltipStatusBar:SetPoint("BOTTOMLEFT", GameTooltip, "TOPLEFT", 0, default_border)
GameTooltipStatusBar:SetPoint("BOTTOMRIGHT", GameTooltip, "TOPRIGHT", 0, default_border)
GameTooltipStatusBar:SetStatusBarTexture(pfUI.media[C.tooltip.statusbar.texture])
CreateBackdrop(GameTooltipStatusBar, nil, nil, tonumber(C.tooltip.alpha))
CreateBackdropShadow(GameTooltipStatusBar)
GameTooltipStatusBar.SetStatusBarColor_orig = GameTooltipStatusBar.SetStatusBarColor
GameTooltipStatusBar.SetStatusBarColor = function() return end
-- A row of buff icons anchored above the tooltip for mouseover units.
-- Icons come from a ClassicAPI object pool: ReleaseAll hides the whole
-- set each refresh, then we Acquire and re-anchor left-to-right.
local BUFF_SIZE, BUFF_SPACING, BUFF_MAX = 20, 2, 32
local auraSlots = {} -- reusable GetAuraSlots buffer for UpdateBuffs
pfUI.tooltip.buffs = CreateFrame("Frame", "pfTooltipBuffs", GameTooltip)
pfUI.tooltip.buffs:SetPoint("BOTTOMLEFT", GameTooltipStatusBar, "TOPLEFT", 0, default_border + 2)
pfUI.tooltip.buffs:SetHeight(BUFF_SIZE)
local function CreateBuffIcon()
local icon = CreateFrame("Frame", nil, pfUI.tooltip.buffs)
icon:SetSize(BUFF_SIZE, BUFF_SIZE)
icon.texture = icon:CreateTexture(nil, "BACKGROUND")
icon.texture:SetTexCoord(.07, .93, .07, .93)
icon.texture:SetAllPoints(icon)
CreateBackdrop(icon)
icon.stacks = icon:CreateFontString(nil, "OVERLAY", "GameFontNormal")
icon.stacks:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 1, 0)
icon.stacks:SetFont(C.tooltip.font_tooltip, C.tooltip.font_tooltip_size, "OUTLINE")
icon.stacks:SetTextColor(1, 1, 1, 1)
icon.cd = CreateCooldown(icon, "tooltip")
return icon
end
pfUI.tooltip.buffpool = CreateObjectPool(CreateBuffIcon, function(_, icon)
icon:Hide()
icon:ClearAllPoints()
end)
function pfUI.tooltip:UpdateBuffs(unit)
pfUI.tooltip.buffpool:ReleaseAll()
if C.tooltip.showbuffs ~= "1" or not unit or unit == "none" then
pfUI.tooltip.buffs:Hide()
return
end
local prev, count = nil, 0
-- one GetAuraSlots enumeration + positional by-slot reads: no per-aura
-- table and no per-index re-walk of the aura array
local n = ScanAuraSlots(unit, "HELPFUL", auraSlots, BUFF_MAX)
for i = 1, n do
local name, texture, applications, _, duration, expirationTime = C_UnitAuras.UnitAuraBySlot(unit, auraSlots[i])
if not name then break end
count = count + 1
local icon = pfUI.tooltip.buffpool:Acquire()
icon.texture:SetTexture(texture)
icon.stacks:SetText(applications and applications > 1 and applications or "")
local start, dur = GetAuraTimer(duration, expirationTime)
CooldownFrame_SetTimer(icon.cd, start, dur, dur > 0 and 1 or 0)
if prev then
icon:SetPoint("LEFT", prev, "RIGHT", BUFF_SPACING, 0)
else
icon:SetPoint("BOTTOMLEFT", pfUI.tooltip.buffs, "BOTTOMLEFT", 0, 0)
end
icon:Show()
prev = icon
end
if count > 0 then
pfUI.tooltip.buffs:SetWidth(count * BUFF_SIZE + (count - 1) * BUFF_SPACING)
pfUI.tooltip.buffs:Show()
else
pfUI.tooltip.buffs:Hide()
end
end
-- A unit tooltip names the unit's target, so it rebuilds when that changes:
-- UNIT_TARGET by the unit's GUID, which unit events fire with whatever token
-- names the unit. SetUnit re-runs OnTooltipSetUnit and with it UpdateUnit.
-- It goes through the token the tooltip was built from -- "mouseover" for a
-- world unit stops naming it once the cursor leaves, so a fading tooltip is
-- left to fade -- and waits a frame: the rebuild re-points this frame's
-- registration, which the dispatch is still walking.
local function Retarget()
local _, unit = GameTooltip:GetUnit()
local _, guid = GameTooltip:GetUnitGUID()
if unit and GameTooltip:IsShown() and UnitGUID(unit) == guid then
GameTooltip:SetUnit(unit)
end
end
pfUI.tooltip.retarget = CreateFrame("Frame")
pfUI.tooltip.retarget:SetScript("OnEvent", function()
RunNextFrame(Retarget)
end)
-- pfUI's unit lines go on whenever a unit is set, not only when the tooltip
-- shows, so a tooltip rebuilt while it is up keeps them
GameTooltip:HookScript("OnTooltipSetUnit", function()
pfUI.tooltip:UpdateUnit()
end)
-- the buff row belongs to the unit it was built for
GameTooltip:HookScript("OnTooltipCleared", function()
pfUI.tooltip:UpdateBuffs("none")
end)
GameTooltip:HookScript("OnHide", function()
pfUI.tooltip:UpdateBuffs("none")
pfUI.tooltip.retarget:UnregisterEvent("UNIT_TARGET")
end)
function pfUI.tooltip:UpdateUnit()
local unit = pfUI.tooltip:GetUnit()
pfUI.tooltip:UpdateBuffs(unit)
if unit == "none" then return end
pfUI.tooltip.retarget:RegisterUnitEvent("UNIT_TARGET", UnitGUID(unit))
local pvpname = UnitPVPName(unit)
local name = UnitName(unit)
local validTarget, target = pcall(UnitName, unit .. 'target')
local class = UnitClassBase(unit)
local guild, rankstr, rankid = GetGuildInfo(unit)
local reaction = UnitReaction(unit, "player")
local pvptitle = gsub(gsub(pvpname or name, name, "", 1), "^%s*(.-)%s*$", "%1")
local hp = UnitHealth(unit)
local hpm = UnitHealthMax(unit)
if name then
if UnitIsPlayer(unit) and class then
local color = PFUI_CLASS_COLORS[class]
GameTooltipStatusBar:SetStatusBarColor_orig(color:GetRGB())
GameTooltip:SetBackdropBorderColor(color:GetRGB())
GameTooltipTextLeft1:SetText("|c" .. color.colorStr .. name)
elseif reaction then
local color = UnitReactionColor[reaction]
GameTooltipStatusBar:SetStatusBarColor_orig(color.r, color.g, color.b)
GameTooltip:SetBackdropBorderColor(color.r, color.g, color.b)
end
if pvptitle ~= name and pvptitle ~= "" then
GameTooltip:AppendText(" |cff666666["..pvptitle.."]|r")
end
end
if guild then
local rank = ""
local lead = ""
if C.tooltip.extguild == "1" then
if rankstr then rank = " |cffaaaaaa(" .. rankstr .. ")" end
if rankid and rankid == 0 then lead = "|cffffcc00*|r" end
end
GameTooltip:AddLine("<" .. guild .. ">" .. lead .. rank, 0.3, 1, 0.5)
end
if validTarget and target then
local targetClass = UnitClassBase(unit .. "target")
local targetReaction = UnitReaction("player",unit .. "target")
if UnitIsPlayer(unit .. "target") and targetClass then
local color = PFUI_CLASS_COLORS[targetClass]
GameTooltip:AddLine(target, color.r, color.g, color.b)
elseif targetReaction then
local color = UnitReactionColor[targetReaction]
if color then
GameTooltip:AddLine(target, color.r, color.g, color.b)
else
GameTooltip:AddLine(target, .5, .5, .5)
end
end
end
if C.tooltip.movespeed == "1" then
local currentSpeed = GetUnitSpeed(unit)
if currentSpeed and currentSpeed > 0 then
local pct = floor(currentSpeed / 7 * 100 + 0.5)
GameTooltip:AddLine(T["Speed"] .. ": " .. pct .. "%", 0.7, 0.7, 1)
end
end
if C.tooltip.unitid == "1" and not UnitIsPlayer(unit) then
local npcID = C_CreatureInfo.GetCreatureID(UnitGUID(unit))
if npcID then
GameTooltip:AddLine(T["UnitID"] .. ": " .. npcID, .25,.5,1)
end
end
if hp and hpm then
if hp >= 1000 then hp = round(hp / 1000, 1) .. "k" end
if hpm >= 1000 then hpm = round(hpm / 1000, 1) .. "k" end
pfUI.tooltipStatusBar.HP:SetText(hp .. " / " .. hpm)
end
GameTooltip:Show()
end
if C.tooltip.aurasource == "1" or C.tooltip.spellid == "1" then
hooksecurefunc(GameTooltip, "SetUnitAura", function(self, ...)
local aura = C_UnitAuras.GetAuraDataByIndex(unpack(arg))
if not aura then return end
if C.tooltip.aurasource == "1" then
local caster = aura.sourceUnit and UnitName(aura.sourceUnit)
if not caster and aura.sourceGUID then
caster = UnitNameFromGUID(aura.sourceGUID)
end
if caster and caster ~= "" then
local classToken
if aura.sourceUnit and UnitIsPlayer(aura.sourceUnit) then
classToken = UnitClassBase(aura.sourceUnit)
elseif aura.sourceGUID then
classToken = select(2, GetPlayerInfoByGUID(aura.sourceGUID))
end
if classToken then
caster = PFUI_CLASS_COLORS[classToken]:WrapTextInColorCode(caster)
end
self:AddLine(T["Cast by"] .. ": " .. caster, .25,.5,1)
end
end
if C.tooltip.spellid == "1" then
self:AddLine(T["SpellID"] .. ": " .. aura.spellId, .25,.5,1)
end
self:Show()
end)
end
end)