DopingControl v0.6.4

This commit is contained in:
2026-08-30 15:37:17 +02:00
parent ecab5ba845
commit 695dd9dcd4
23 changed files with 2278 additions and 296 deletions
+118
View File
@@ -0,0 +1,118 @@
-- DopingControl ui/cellsheet.lua
--
-- The matrix draws one small box per cell: a solid background with a 1px
-- border whose color carries the state. Done with SetBackdrop that costs NINE
-- texture regions per cell (measured in the client: bare frame 0, backdrop
-- with bgFile only 1, backdrop with edgeFile 9). At 805 cells that is ~7200
-- regions of pure decoration.
--
-- Instead every box is baked into one sheet and a cell owns a single texture
-- region that picks its box with SetTexCoord. The border is part of the image,
-- so the result is pixel-identical -- the sheet is generated from the same
-- DC.COLORS palette by tools/gen-cellsheet.js.
--
-- Pure Lua on purpose: no WoW API at file scope, so the offline tests can
-- dofile it under lua50.exe.
DC_Cells = {}
DC_Cells.SHEET = {
path = "Interface\\AddOns\\DopingControl\\textures\\cells",
size = 256, -- power of two, mandatory: a 12x12 TGA fails silently
tileW = 64, -- holds the widest sprite (34) with room to spare
tileH = 32, -- holds CELL_H (23)
perRow = 4, -- 256 / 64
}
-- Cell height never varies; only the width does (trinket columns are 34).
DC_Cells.CELL_H = 23
DC_Cells.WIDTHS = { 30, 34 }
-- Every (background, border) pair a CELL can take, in sheet order. Borders
-- given as a number are DC.QUALITY indices, everything else is a DC.COLORS
-- key. Order is the wire format of the sheet: appending is safe, reordering
-- means regenerating the TGA.
DC_Cells.VARIANTS = {
{ name = "HAS", bg = "hatBg", border = "hatBorder" },
{ name = "MISSING", bg = "fehltBg", border = "fehltBorder" },
{ name = "UNKNOWN", bg = "unbekBg", border = "unbekBorder" },
{ name = "SKIP", bg = "skip", border = "skipBorder" },
{ name = "NEUTRAL", bg = "theadBg", border = "line" },
{ name = "ITEM_HAS", bg = "theadBg", border = "hatBorder" },
{ name = "ITEM_MISSING", bg = "theadBg", border = "fehltBorder" },
{ name = "Q_RARE", bg = "theadBg", border = "rareBorder" },
{ name = "Q_EPIC", bg = "theadBg", border = "epicBorder" },
{ name = "Q_0", bg = "theadBg", border = 0 },
{ name = "Q_1", bg = "theadBg", border = 1 },
{ name = "Q_2", bg = "theadBg", border = 2 },
{ name = "Q_5", bg = "theadBg", border = 5 },
{ name = "Q_6", bg = "theadBg", border = 6 },
}
DC_Cells.INDEX = {}
for i = 1, table.getn(DC_Cells.VARIANTS) do
DC_Cells.INDEX[DC_Cells.VARIANTS[i].name] = i
end
-- Tile slot of a variant/width pair: each variant owns two consecutive tiles,
-- one per width, in DC_Cells.WIDTHS order.
local function slotOf(name, w)
local vi = DC_Cells.INDEX[name]
if not vi then
return nil
end
for k = 1, table.getn(DC_Cells.WIDTHS) do
if DC_Cells.WIDTHS[k] == w then
return (vi - 1) * table.getn(DC_Cells.WIDTHS) + (k - 1)
end
end
return nil
end
DC_Cells.SlotOf = slotOf
-- SetTexCoord arguments for one box. Returns four numbers or nil -- nil so a
-- typo shows up as a missing box instead of silently sampling tile 0.
function DC_Cells.Coords(name, w)
local slot = slotOf(name, w)
if not slot then
return nil
end
local S = DC_Cells.SHEET
local col = math.mod(slot, S.perRow)
local row = math.floor(slot / S.perRow)
local x0 = col * S.tileW
local y0 = row * S.tileH
return x0 / S.size,
(x0 + w) / S.size,
y0 / S.size,
(y0 + DC_Cells.CELL_H) / S.size
end
-- State + slot kind + item quality -> variant name. Mirrors exactly what the
-- old SetBackdropG call sites in ui/matrix.lua chose -- which, for a nil
-- quality, differed BY KIND (see ui/matrix.lua's M.PaintItemTile, old lines
-- 1965-1990, confirmed at tools/luatests/test_cellsheet.lua):
-- * trinket: M.qualityColors(item.quality) ran unconditionally, and for
-- quality == nil that resolves to DC.QUALITY[1] (common/white) via the
-- `q or 1` fallback -- state-INDEPENDENT.
-- * ench: quality == nil skipped M.qualityColors and fell back to
-- C.hatBorder/C.fehltBorder/C.line BY STATE -- exactly
-- ITEM_HAS/ITEM_MISSING/NEUTRAL.
-- So trinket's nil-quality case must NOT share ench's state-based fallback.
function DC_Cells.VariantFor(state, kind, quality)
if kind == "ench" or kind == "trinket" then
if quality == 3 then return "Q_RARE" end
if quality == 4 then return "Q_EPIC" end
if quality ~= nil then return "Q_" .. quality end
if kind == "trinket" then
return "Q_1"
end
if state == "HAS" then return "ITEM_HAS" end
if state == "MISSING" then return "ITEM_MISSING" end
return "NEUTRAL"
end
if state == "HAS" then return "HAS" end
if state == "MISSING" then return "MISSING" end
if state == "UNKNOWN" then return "UNKNOWN" end
return "SKIP"
end
+731 -58
View File
File diff suppressed because it is too large Load Diff
+68 -10
View File
@@ -149,10 +149,12 @@ O.GRID_LABEL_W = 74 -- role/class label column (expectation grid)
O.BOOK_NAME_W = 96 -- player-name column (skill book grid)
O.ROW_H = 22 -- one grid row's height, both grids
-- equipment has the most configurable slots (11 armor enchants + neck +
-- 2 rings; trinkets are item tiles and never configurable, see
-- slotsForTab) -- the widest grid the pools have to serve.
O.MAX_SLOT_COLS = 14
-- consumables (v6 slot model) has the most configurable slots (22 incl.
-- the five school protection columns);
-- equipment follows with 14 (11 armor enchants + neck + 2 rings --
-- trinkets are item tiles and never configurable, see slotsForTab).
-- The widest grid the pools have to serve.
O.MAX_SLOT_COLS = 22
-- x-offset of the 20 px check button inside its O.CELL_W-wide column
O.CELL_INSET = math.floor((O.CELL_W - 20) / 2)
@@ -489,6 +491,17 @@ function O.ClassTitle(class)
.. string.lower(string.sub(class, 2))
end
-- Label of the demo-mode button. Demo mode is TRANSIENT (core/demo.lua:
-- never SavedVariables), so it gets a self-relabeling button rather than a
-- checkbox -- the checkbox row above is bound to db fields, and a checkbox
-- whose state dies on reload would read like a broken setting.
function O.DemoButtonLabel()
if DC.demoMode then
return "Demo mode: ON (click to stop)"
end
return "Demo mode: OFF (click to start)"
end
-- ==================================================================
-- WOW SECTION -- frame, widgets, wiring
-- ==================================================================
@@ -504,7 +517,7 @@ if CreateFrame then
-- O.GRID_LABEL_W label column O.COL_PITCH column pitch
-- O.BOOK_NAME_W player-name column O.ROW_H row pitch
-- O.CELL_INSET check button inset O.MAX_SLOT_COLS widest grid
-- The two grids differ in column count (14 equipment slots vs. 15
-- The two grids differ in column count (22 consumable slots vs. 15
-- weapon skills); the frame is BUILT at the wider of the two and
-- narrowed per tab by ApplyEditorWidth, so no widget is ever created
-- against a width it later has to grow past.
@@ -558,9 +571,9 @@ if CreateFrame then
elseif tab == "CLASSBUFFS" then
return DC.SLOTS_CLASSBUFFS or {}
elseif tab == "EQUIPMENT" then
-- only kind=="ench" slots are configurable here (14: the 11
-- armor slots + neck/rings, which are enchantable on this
-- server -- data/enchants.lua). T1/T2 (trinket item tiles)
-- only kind=="ench" slots are configurable here (15: the 11
-- armor slots + neck/rings/waist, which are enchantable on
-- this server -- data/enchants.lua). T1/T2 (trinket item tiles)
-- have no enchant question to ask and never appear: the
-- matrix always shows them (item present/absent decides,
-- expectations are never consulted for tiles).
@@ -1536,6 +1549,42 @@ if CreateFrame then
place(cbTest, -28)
AddTooltip(cbTest, "Replace the scan with a fixed simulated raid.\nChat outputs are disabled while active.")
-- Demo mode sits right below the test-mode row: same idea (the
-- simulated raid), plus a transient expectation layer that shows
-- all six school protection columns at once. A BUTTON, not a
-- checkbox -- the state is never saved, so it relabels itself
-- instead of pretending to be a stored setting.
local btnDemo = CreateButton(f, "DopingControlOptDemoBtn",
O.DemoButtonLabel(), 200,
function()
-- Capture the widget BEFORE SetDemoMode runs. `this` is a
-- plain global the 1.12 dispatcher sets per script call and
-- never restores: SetDemoMode synchronously drives
-- DC_Matrix.Refresh -> M.Render, whose leftover-hide loop
-- Hide()s pooled rows and group heads that carry their own
-- OnHide handlers (ui/matrix.lua M.RowOnHide /
-- M.GroupHeadOnHide -- they read `this` themselves). After
-- that nested dispatch `this` is the last-hidden widget, and
-- a row frame is a plain Frame with no SetText. Same order
-- the other handlers in this file already keep
-- (GridCellOnClick, ResetOnClick): finish the `this` reads
-- and writes before refreshing anything.
local btn = this
if DC.SetDemoMode then
DC.SetDemoMode(not DC.demoMode)
end
btn:SetText(O.DemoButtonLabel())
end)
place(btnDemo, -28)
AddTooltip(btnDemo, "Simulated raid with ALL SIX protection columns"
.. " (fire, frost, nature, shadow, holy, arcane) shown at once."
.. "\nYour saved expectations are left untouched, and demo mode"
.. " is never saved - it ends with the next reload."
-- the demo layer is snapshotted on entry (core/demo.lua), so
-- grid edits made while it is armed do not reach the matrix
.. "\nThe view is a snapshot: restart demo mode to pick up"
.. " expectation changes made while it is running.")
local cbMinimap = CreateCheckbox(f, "Show minimap button",
function() return db().showMinimapButton end,
function(v)
@@ -1788,6 +1837,11 @@ if CreateFrame then
end
RefreshEditor() -- grid/books pools + tab visibility + buttons
ResetDisarm(getglobal("DopingControlOptResetBtn"))
-- demo mode can have been toggled by /dc demo meanwhile
local bd = getglobal("DopingControlOptDemoBtn")
if bd then
bd:SetText(O.DemoButtonLabel())
end
end)
-- ESC closes
@@ -1827,14 +1881,18 @@ if CreateFrame then
f:Show()
end
-- external state changes (/dc test, minimap toggle) can resync an
-- open options window
-- external state changes (/dc test, /dc demo, minimap toggle) can
-- resync an open options window
function O.Refresh()
if frame and frame:IsShown() then
for i = 1, table.getn(checkboxes) do
local cb = checkboxes[i]
cb:SetChecked(cb.dcGet() and 1 or nil)
end
local bd = getglobal("DopingControlOptDemoBtn")
if bd then
bd:SetText(O.DemoButtonLabel())
end
RefreshEditor() -- a fresh scan can add or drop book rows
end
end
+29 -2
View File
@@ -59,6 +59,22 @@ function R.SendDisabled(source)
return source == "sim"
end
-- The chat API rejects the WHOLE message with "Invalid escape code in chat
-- message" when it meets a "|" that does not open a valid escape sequence
-- (|c |r |H |h |T |t |n ||). Our header line uses " | " as a plain ASCII
-- separator, so every report was refused. "||" is the escape for a literal
-- pipe and still renders as a single "|", which is why this runs at the
-- send point and NOT inside DC_Model.reportLines: the preview shows what
-- reportLines returns and must keep showing the text as it APPEARS in chat.
-- Report lines are pure ASCII by design (no item links, no colour codes),
-- so doubling every pipe cannot damage a real escape sequence here.
function R.ChatSafe(line)
if not line then
return line
end
return (string.gsub(line, "|", "||"))
end
-- Create a drain queue from an array of lines. Takes an independent copy
-- (the preview may be rebuilt while an old queue is still draining).
function R.QueueCreate(lines)
@@ -135,7 +151,18 @@ end
function R.BuildLines(store, dbt, tab)
store = store or DC.store or { players = {} }
tab = tab or "CONSUMABLES"
local expect = (dbt and dbt.expectations) or DC.DEFAULT_EXPECT or {}
-- Same expectation resolver the matrix uses, or the report preview
-- would show different columns and different gap totals than the grid
-- it was opened from (demo mode). Safe for the SEND path too: the demo
-- layer only applies over the "sim" store, and R.SendDisabled blocks
-- sending on exactly that source. Guarded because ui/report.lua is
-- loadable without ui/matrix.lua (tools/luatests/test_report_queue.lua).
local expect
if DC_Matrix and DC_Matrix.ExpectTable then
expect = DC_Matrix.ExpectTable(dbt)
else
expect = (dbt and dbt.expectations) or DC.DEFAULT_EXPECT or {}
end
local function roleOf(p)
return DC_Roles.resolve(dbt, store.source, p)
end
@@ -234,7 +261,7 @@ if CreateFrame then
end
local line = R.QueueTick(queue, GetTime(), currentSource())
if line then
SendChatMessage(line, R.CHAT_TYPE)
SendChatMessage(R.ChatSafe(line), R.CHAT_TYPE)
UpdateSendState()
end
if queue.done or queue.aborted then
+202 -12
View File
@@ -44,6 +44,88 @@ W.SOLID_BACKDROP = {
W.WHITE = { r = 1, g = 1, b = 1 }
-- ------------------------------------------------------------------
-- Cell-box fallback palette: variant name -> (bg, border) color reference,
-- used ONLY when ui/cellsheet.lua's baked sheet is unavailable (see
-- W.HasCellSheet below). `border` is a DC.COLORS key, or a number that
-- indexes DC.QUALITY -- same convention as DC_Cells.VARIANTS in
-- ui/cellsheet.lua, which this table intentionally mirrors: colors are
-- pulled LIVE from DC.COLORS/DC.QUALITY (core/const.lua, already loaded --
-- ui/cellsheet.lua only bakes that same palette into a texture, it does
-- not own it), so nothing here duplicates a color VALUE. The pairing
-- itself (which key goes with which variant) can't be derived
-- mechanically -- it is the one piece of information that has to exist
-- twice given the failure mode this guards (ui/cellsheet.lua not loaded
-- at all). tools/luatests/test_widgets.lua cross-checks every entry
-- against the real DC_Cells.VARIANTS so the two cannot silently drift.
W.CELL_FALLBACK_VARIANTS = {
HAS = { bg = "hatBg", border = "hatBorder" },
MISSING = { bg = "fehltBg", border = "fehltBorder" },
UNKNOWN = { bg = "unbekBg", border = "unbekBorder" },
SKIP = { bg = "skip", border = "skipBorder" },
NEUTRAL = { bg = "theadBg", border = "line" },
ITEM_HAS = { bg = "theadBg", border = "hatBorder" },
ITEM_MISSING = { bg = "theadBg", border = "fehltBorder" },
Q_RARE = { bg = "theadBg", border = "rareBorder" },
Q_EPIC = { bg = "theadBg", border = "epicBorder" },
Q_0 = { bg = "theadBg", border = 0 },
Q_1 = { bg = "theadBg", border = 1 },
Q_2 = { bg = "theadBg", border = 2 },
Q_5 = { bg = "theadBg", border = 5 },
Q_6 = { bg = "theadBg", border = 6 },
}
-- ------------------------------------------------------------------
-- DC_Cells fallback shim -- covers a mid-session /reload where THIS file
-- (ui/widgets.lua, an EXISTING file whose new content /reload happily
-- re-executes) deploys, but ui/cellsheet.lua (a NEW file added in the same
-- change) does not: 1.12's /reload re-runs existing files but never loads
-- a file that was not already part of the running session (verified
-- in-game). Left alone, DC_Cells stays nil, and it is not only
-- W.MakeCell/W.SetCellBox below that touch it -- ui/matrix.lua calls
-- DC_Cells.VariantFor(...) directly at two paint sites (M.PaintItemTile
-- and the legacy trinket path, ui/matrix.lua ~2182/2381 -- out of scope
-- for this change) and would error on the very first painted item/trinket
-- cell regardless of anything done here.
--
-- The shim supplies ONLY the pure classification function -- state/kind/
-- quality -> variant NAME string -- no sheet, no texture, no coordinate
-- math, because none of that can work without ui/cellsheet.lua's baked
-- sheet file. It is a verbatim copy of DC_Cells.VariantFor's logic (see
-- ui/cellsheet.lua); test_widgets.lua cross-checks the two stay in sync.
-- Installs only when DC_Cells does not already exist, so the normal
-- (full-restart) load path -- where ui/cellsheet.lua runs before this file
-- per DopingControl.toc and sets the real DC_Cells -- is untouched.
if not DC_Cells then
DC_Cells = {
VariantFor = function(state, kind, quality)
if kind == "ench" or kind == "trinket" then
if quality == 3 then return "Q_RARE" end
if quality == 4 then return "Q_EPIC" end
if quality ~= nil then return "Q_" .. quality end
if kind == "trinket" then
return "Q_1"
end
if state == "HAS" then return "ITEM_HAS" end
if state == "MISSING" then return "ITEM_MISSING" end
return "NEUTRAL"
end
if state == "HAS" then return "HAS" end
if state == "MISSING" then return "MISSING" end
if state == "UNKNOWN" then return "UNKNOWN" end
return "SKIP"
end,
}
end
-- Whether ui/cellsheet.lua's baked sheet is actually available (as
-- opposed to the shim above, which never sets .SHEET). One seam shared by
-- W.MakeCell and W.SetCellBox, and directly testable offline without
-- CreateFrame (unlike W.MakeCell itself).
function W.HasCellSheet()
return DC_Cells ~= nil and DC_Cells.SHEET ~= nil
end
-- ------------------------------------------------------------------
-- Font helper (we keep OUR pixel
-- size -- the matrix layout depends on it -- and only take pfUI's font
@@ -125,28 +207,117 @@ end
-- ------------------------------------------------------------------
-- Cell factory: 30x23 state cell (the 30x23 size is part of the fixed
-- visual layout). Carries one texture region (check/cross glyph OR trinket
-- item icon) and one FontString (?, skip dot, trinket monogram); the
-- matrix paint code shows exactly one of them per state.
-- visual layout). The box (background + 1px border) is one texture region
-- sampled from the baked sheet in ui/cellsheet.lua -- SetBackdrop with an
-- edgeFile costs NINE texture regions per cell, this costs one. The icon
-- (check/cross glyph OR trinket item icon) and FontString (?, skip dot,
-- trinket monogram) are created on demand via W.CellIcon / W.CellText;
-- the matrix paint code shows exactly one of them per state.
-- Pool children stay anonymous (documented factory exception).
-- ------------------------------------------------------------------
function W.MakeCell(parent, name, w, h)
local c = CreateFrame("Frame", name, parent)
c:SetWidth(w or 30)
c:SetHeight(h or 23)
c:SetBackdrop(W.SOLID_BACKDROP)
c:EnableMouse(true)
c.icon = c:CreateTexture(nil, "ARTWORK")
c.icon:SetPoint("CENTER", c, "CENTER", 0, 0)
c.icon:SetWidth(16)
c.icon:SetHeight(16)
c.icon:Hide()
c.text = c:CreateFontString(nil, "OVERLAY")
W.ApplyFont(c.text, 9)
c.text:SetPoint("CENTER", c, "CENTER", 0, 0)
c.dcW = w or 30
if W.HasCellSheet() then
-- The state box (background + 1px border) comes from one baked sheet:
-- SetBackdrop with an edgeFile costs NINE texture regions per cell,
-- this costs one. See ui/cellsheet.lua.
c.boxTex = c:CreateTexture(nil, "BACKGROUND")
c.boxTex:SetAllPoints(c)
c.boxTex:SetTexture(DC_Cells.SHEET.path)
-- Without an initial texcoord the region would show the WHOLE sheet
-- stretched across the cell until the first paint. Start on NEUTRAL.
local l, r, t, b = DC_Cells.Coords("NEUTRAL", c.dcW)
if l then
c.boxTex:SetTexCoord(l, r, t, b)
c.dcBoxVariant = "NEUTRAL"
c.dcBoxW = c.dcW
end
else
-- Fallback: ui/cellsheet.lua did not load this session (see
-- W.HasCellSheet above) -- pre-refactor SetBackdrop path (no
-- boxTex region at all) so the matrix still draws instead of
-- erroring on a nil DC_Cells.SHEET. W.SetBackdropG and
-- W.SOLID_BACKDROP are untouched by the sprite refactor, so this
-- is exactly what W.MakeCell did before it.
c:SetBackdrop(W.SOLID_BACKDROP)
W.SetCellBox(c, "NEUTRAL")
end
return c
end
-- Icon and FontString on demand: a cell shows one or the other, and in 1.12
-- eagerly created regions cost even while hidden.
function W.CellIcon(cell)
if not cell.icon then
cell.icon = cell:CreateTexture(nil, "ARTWORK")
cell.icon:SetPoint("CENTER", cell, "CENTER", 0, 0)
cell.icon:SetWidth(16)
cell.icon:SetHeight(16)
cell.icon:Hide()
end
return cell.icon
end
function W.CellText(cell)
if not cell.text then
cell.text = cell:CreateFontString(nil, "OVERLAY")
W.ApplyFont(cell.text, 9)
cell.text:SetPoint("CENTER", cell, "CENTER", 0, 0)
end
return cell.text
end
-- Guards on variant AND width (cell.dcBoxW, the width the box was last
-- painted at) -- NOT variant alone. ui/matrix.lua resizes a pooled cell in
-- place when its column width changes (trinket columns are 34, normal
-- cells 30: see ui/matrix.lua's column-layout path around SetWidth(col.w))
-- and only ever writes cell.dcW; it has no notion of dcBoxVariant and isn't
-- expected to invalidate it. If this guard only checked variant, a cell
-- that kept its variant across such a resize would keep a stale texcoord
-- sized for the OLD width -- the baked border would sit off the new cell
-- edge. Tracking dcBoxW here keeps the guard self-contained: correctness
-- does not depend on any caller convention.
function W.SetCellBox(cell, variant)
if not W.HasCellSheet() then
-- Fallback: same pre-refactor SetBackdrop recipe, colors pulled
-- LIVE from DC.COLORS/DC.QUALITY via W.CELL_FALLBACK_VARIANTS
-- above. Never touches cell.boxTex -- a fallback cell (see
-- W.MakeCell) does not have one. W.SetBackdropG already
-- change-guards by table reference, so no separate
-- dcBoxVariant/dcBoxW bookkeeping is needed here.
local v = W.CELL_FALLBACK_VARIANTS[variant]
if not v then
return
end
local bg = DC.COLORS[v.bg]
local border = v.border
if type(border) == "number" then
border = DC.QUALITY[border]
else
border = DC.COLORS[border]
end
if not bg or not border then
return
end
W.SetBackdropG(cell, bg, border)
return
end
if cell.dcBoxVariant == variant and cell.dcBoxW == cell.dcW then
return
end
local l, r, t, b = DC_Cells.Coords(variant, cell.dcW)
if not l then
return
end
cell.dcBoxVariant = variant
cell.dcBoxW = cell.dcW
cell.boxTex:SetTexCoord(l, r, t, b)
end
-- ------------------------------------------------------------------
-- Pill factory (ready pill, coverage pill): bordered mini frame with a
-- centered FontString; width is adjusted by the caller to fit the text.
@@ -247,6 +418,21 @@ function W.TipDouble(left, right, c, cr)
GameTooltip:AddDoubleLine(left, right, r, g, b, r2, g2, b2)
end
-- W.dcTipOwner: our own record of which of OUR frames currently owns
-- GameTooltip. WoW 1.12's GameTooltip has no "GetOwner" method (confirmed
-- against FrameXML-1.12.1: zero real hits -- the only "GetOwner" match
-- anywhere in that tree is the unrelated global function
-- GetOwnerAuctionItems). Every caller in this addon that ever needs "who
-- currently owns the tooltip" goes through W.AttachTooltip to open one in
-- the first place, so this is the single place that needs to record it.
-- Set right after SetOwner
-- (matching order: SetOwner, then the builder runs, then Show -- a builder
-- that itself re-SetOwner()s the SAME frame, like M.CellTooltip's
-- ANCHOR_LEFT override, does not change who owns it). Cleared in OnLeave,
-- guarded on identity so a late/out-of-order OnLeave can never clobber a
-- newer owner.
W.dcTipOwner = nil
function W.AttachTooltip(frame, builder, anchor)
frame.dcTipBuilder = builder
frame.dcTipAnchor = anchor or "ANCHOR_LEFT"
@@ -258,6 +444,7 @@ function W.AttachTooltip(frame, builder, anchor)
end
if GameTooltip and this.dcTipBuilder then
GameTooltip:SetOwner(this, this.dcTipAnchor)
W.dcTipOwner = this
this.dcTipBuilder(this)
GameTooltip:Show()
end
@@ -266,6 +453,9 @@ function W.AttachTooltip(frame, builder, anchor)
if this.dcOldLeave then
this.dcOldLeave()
end
if W.dcTipOwner == this then
W.dcTipOwner = nil
end
if GameTooltip then
GameTooltip:Hide()
end