DopingControl 0.7.0
This commit is contained in:
@@ -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
|
||||
+4078
File diff suppressed because it is too large
Load Diff
+1919
File diff suppressed because it is too large
Load Diff
+447
@@ -0,0 +1,447 @@
|
||||
-- DopingControl ui/report.lua
|
||||
-- Report preview overlay + raid-chat send queue.
|
||||
--
|
||||
-- Pure part (offline-testable):
|
||||
-- DC_Report.SendDisabled(source) -> bool (true iff source == "sim")
|
||||
-- DC_Report.QueueCreate(lines) -> queue (independent copy of lines)
|
||||
-- DC_Report.QueueTick(q, now, src) -> line-to-send-now | nil
|
||||
-- DC_Report.QueueLabel(q) -> "sending k/n" button label
|
||||
-- DC_Report.BuildLines(store, db, tab) -> lines, vm (via DC_Model)
|
||||
--
|
||||
-- Queue semantics:
|
||||
-- * First line goes out immediately on Send; every further line waits
|
||||
-- >= SEND_INTERVAL (0.6 s) measured from the PREVIOUS ACTUAL SEND
|
||||
-- (the client chat throttle is ~10 msgs/10 s --
|
||||
-- 0.6 s spacing stays safely under it even with other addons chatting).
|
||||
-- * ABORT: if store.source flips to "sim" mid-drain, the very next tick
|
||||
-- aborts the queue without sending (synthetic names must
|
||||
-- never reach real chat). An aborted queue never resumes.
|
||||
-- * In sim mode the Send button never activates in the first place
|
||||
-- (SendDisabled predicate), with tooltip "disabled in test mode".
|
||||
--
|
||||
-- WoW wiring (behind `if CreateFrame then`): overlay frame
|
||||
-- DopingControlReportFrame, DIALOG strata, anchored over the matrix frame,
|
||||
-- scrollable preview FontString showing DC_Model.reportLines output EXACTLY
|
||||
-- as it will be sent, buttons "Send to raid chat" + "Close".
|
||||
-- SendChatMessage(line, "RAID") only ever fires from the queue drainer here
|
||||
-- (single-sender rule).
|
||||
--
|
||||
-- Deliberate resolutions:
|
||||
-- * DC_Report.Open() is exposed with OPTIONAL (vm, tab) arguments: the
|
||||
-- matrix "Report..." button may pass its already-built vm; with no
|
||||
-- arguments the report builds its own vm from DC.store via
|
||||
-- DC_Model/DC_Roles and the current tab (DC_Matrix.GetTab()/.tab if
|
||||
-- the matrix exposes it, else DC.currentTab, else "CONSUMABLES").
|
||||
-- * The matrix frame global name is not fixed; anchoring
|
||||
-- tries DC_Matrix.frame, then DopingControlFrame /
|
||||
-- DopingControlMatrixFrame, else UIParent.
|
||||
-- * Closing the overlay (Close / ESC) while draining cancels the rest of
|
||||
-- the queue -- hidden frames get no OnUpdate on 1.12, so a hidden
|
||||
-- "paused" queue would silently resume minutes later; cancel is the
|
||||
-- predictable behavior.
|
||||
|
||||
DopingControl = DopingControl or {}
|
||||
local DC = DopingControl
|
||||
|
||||
DC_Report = DC_Report or {}
|
||||
local R = DC_Report
|
||||
|
||||
-- ==================================================================
|
||||
-- Pure queue core
|
||||
-- ==================================================================
|
||||
|
||||
R.SEND_INTERVAL = 0.6 -- seconds between two sent lines
|
||||
R.CHAT_TYPE = "RAID"
|
||||
|
||||
-- Send is dead exactly in test mode. nil source (no scan
|
||||
-- yet) is NOT sim: sending a "coverage 0/0" report is pointless but safe.
|
||||
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)
|
||||
local q = {
|
||||
lines = {},
|
||||
n = 0,
|
||||
sent = 0,
|
||||
nextAt = 0, -- absolute time the next send is allowed
|
||||
done = false,
|
||||
aborted = false,
|
||||
}
|
||||
local n = table.getn(lines)
|
||||
for i = 1, n do
|
||||
table.insert(q.lines, lines[i])
|
||||
end
|
||||
q.n = n
|
||||
if n == 0 then
|
||||
q.done = true
|
||||
end
|
||||
return q
|
||||
end
|
||||
|
||||
-- One drain step. now = current time (GetTime() in-game), source = current
|
||||
-- store source ("scan"/"sim"/nil). Returns the line to send NOW, or nil.
|
||||
-- Sets q.done when the last line went out, q.aborted on a sim flip.
|
||||
function R.QueueTick(q, now, source)
|
||||
if not q or q.done or q.aborted then
|
||||
return nil
|
||||
end
|
||||
if source == "sim" then
|
||||
q.aborted = true -- never resumes
|
||||
return nil
|
||||
end
|
||||
if now < q.nextAt then
|
||||
return nil
|
||||
end
|
||||
q.sent = q.sent + 1
|
||||
local line = q.lines[q.sent]
|
||||
q.nextAt = now + R.SEND_INTERVAL
|
||||
if q.sent >= q.n then
|
||||
q.done = true
|
||||
end
|
||||
return line
|
||||
end
|
||||
|
||||
-- Button label while a queue exists.
|
||||
function R.QueueLabel(q)
|
||||
if not q then
|
||||
return nil
|
||||
end
|
||||
if q.aborted then
|
||||
return "aborted"
|
||||
end
|
||||
return string.format("sending %d/%d", q.sent, q.n)
|
||||
end
|
||||
|
||||
-- ==================================================================
|
||||
-- Line building (pure given a store; uses DC_Model + DC_Roles)
|
||||
-- ==================================================================
|
||||
|
||||
-- "14:04"-style timestamp: WoW 1.12 exposes a global date(); plain
|
||||
-- Lua 5.0 has os.date.
|
||||
function R.TimeStr()
|
||||
if type(date) == "function" then
|
||||
return date("%H:%M")
|
||||
end
|
||||
if os and type(os.date) == "function" then
|
||||
return os.date("%H:%M")
|
||||
end
|
||||
return "--:--"
|
||||
end
|
||||
|
||||
-- Build the report lines for a store + db + tab. Returns lines, vm.
|
||||
function R.BuildLines(store, dbt, tab)
|
||||
store = store or DC.store or { players = {} }
|
||||
tab = tab or "CONSUMABLES"
|
||||
-- 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
|
||||
local vm = DC_Model.build(store, roleOf, expect, tab)
|
||||
local label = (DC.TAB_LABEL and DC.TAB_LABEL[tab]) or tab
|
||||
return DC_Model.reportLines(vm, label, R.TimeStr()), vm
|
||||
end
|
||||
|
||||
-- ==================================================================
|
||||
-- WoW wiring
|
||||
-- ==================================================================
|
||||
if CreateFrame then
|
||||
|
||||
local frame = nil -- DopingControlReportFrame (lazy)
|
||||
local previewText = nil -- the preview FontString
|
||||
local scrollChild = nil
|
||||
local infoText = nil
|
||||
local sendBtn = nil
|
||||
local sendBlocker = nil -- invisible tooltip overlay while disabled
|
||||
local lines = nil -- current preview lines (array)
|
||||
local queue = nil -- active drain queue or nil
|
||||
|
||||
local function db()
|
||||
return DopingControlDB
|
||||
end
|
||||
|
||||
local function currentSource()
|
||||
return DC.store and DC.store.source
|
||||
end
|
||||
|
||||
local function currentTab()
|
||||
if DC_Matrix then
|
||||
if DC_Matrix.GetTab then
|
||||
return DC_Matrix.GetTab()
|
||||
end
|
||||
if DC_Matrix.tab then
|
||||
return DC_Matrix.tab
|
||||
end
|
||||
end
|
||||
return DC.currentTab or "CONSUMABLES"
|
||||
end
|
||||
|
||||
-- The matrix frame global name is not guaranteed; try the likely
|
||||
-- handles, fall back safely.
|
||||
local function anchorFrame()
|
||||
if DC_Matrix and DC_Matrix.frame then
|
||||
return DC_Matrix.frame
|
||||
end
|
||||
local m = getglobal("DopingControlFrame")
|
||||
or getglobal("DopingControlMatrixFrame")
|
||||
return m or UIParent
|
||||
end
|
||||
|
||||
local function panelBackdrop()
|
||||
if DC_Widgets and DC_Widgets.PANEL_BACKDROP then
|
||||
return DC_Widgets.PANEL_BACKDROP
|
||||
end
|
||||
if DC.PANEL_BACKDROP then
|
||||
return DC.PANEL_BACKDROP
|
||||
end
|
||||
return {
|
||||
bgFile = "Interface\\AddOns\\DopingControl\\textures\\panel_bg",
|
||||
edgeFile = "Interface\\AddOns\\DopingControl\\textures\\panel_border",
|
||||
tile = true, tileSize = 128, edgeSize = 16,
|
||||
insets = { left = 5, right = 5, top = 5, bottom = 5 },
|
||||
}
|
||||
end
|
||||
|
||||
-- Send button + blocker state machine: draining -> disabled "sending
|
||||
-- k/n"; sim -> disabled with tooltip blocker; else enabled.
|
||||
local function UpdateSendState()
|
||||
if not sendBtn then
|
||||
return
|
||||
end
|
||||
if queue and not queue.done and not queue.aborted then
|
||||
sendBtn:Disable()
|
||||
sendBtn:SetText(R.QueueLabel(queue))
|
||||
sendBlocker:Hide()
|
||||
elseif R.SendDisabled(currentSource()) then
|
||||
sendBtn:Disable()
|
||||
sendBtn:SetText("Send to raid chat")
|
||||
sendBlocker:Show()
|
||||
else
|
||||
sendBtn:Enable()
|
||||
sendBtn:SetText("Send to raid chat")
|
||||
sendBlocker:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
-- OnUpdate drainer (installed on the frame only while a queue exists;
|
||||
-- no allocation on the idle path).
|
||||
local function OnUpdateDrain()
|
||||
if not queue then
|
||||
this:SetScript("OnUpdate", nil)
|
||||
return
|
||||
end
|
||||
local line = R.QueueTick(queue, GetTime(), currentSource())
|
||||
if line then
|
||||
SendChatMessage(R.ChatSafe(line), R.CHAT_TYPE)
|
||||
UpdateSendState()
|
||||
end
|
||||
if queue.done or queue.aborted then
|
||||
queue = nil
|
||||
this:SetScript("OnUpdate", nil)
|
||||
UpdateSendState()
|
||||
end
|
||||
end
|
||||
|
||||
local function OnSendClick()
|
||||
if queue then
|
||||
return
|
||||
end
|
||||
if not lines or table.getn(lines) == 0 then
|
||||
return
|
||||
end
|
||||
if R.SendDisabled(currentSource()) then
|
||||
return
|
||||
end
|
||||
queue = R.QueueCreate(lines)
|
||||
frame:SetScript("OnUpdate", OnUpdateDrain)
|
||||
UpdateSendState()
|
||||
end
|
||||
|
||||
local function BuildReportFrame()
|
||||
if frame then
|
||||
return frame
|
||||
end
|
||||
local f = CreateFrame("Frame", "DopingControlReportFrame", UIParent)
|
||||
f:SetWidth(460)
|
||||
f:SetHeight(340)
|
||||
f:SetFrameStrata("DIALOG")
|
||||
f:SetPoint("CENTER", anchorFrame(), "CENTER", 0, 0)
|
||||
f:SetBackdrop(panelBackdrop())
|
||||
f:SetBackdropColor(1, 1, 1, 0.97)
|
||||
f:SetMovable(true)
|
||||
f:EnableMouse(true)
|
||||
f:RegisterForDrag("LeftButton")
|
||||
f:SetScript("OnDragStart", function() this:StartMoving() end)
|
||||
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
|
||||
f:SetClampedToScreen(true)
|
||||
|
||||
-- title (gold)
|
||||
local title = f:CreateFontString("DopingControlReportTitle",
|
||||
"ARTWORK", "GameFontNormal")
|
||||
title:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -14)
|
||||
local g = DC.COLORS and DC.COLORS.goldHi
|
||||
if g then
|
||||
title:SetTextColor(g.r, g.g, g.b)
|
||||
else
|
||||
title:SetTextColor(0.85, 0.66, 0.31)
|
||||
end
|
||||
title:SetText("Report preview")
|
||||
|
||||
-- info line (line count + pacing note)
|
||||
infoText = f:CreateFontString("DopingControlReportInfo",
|
||||
"ARTWORK", "GameFontNormalSmall")
|
||||
infoText:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -30)
|
||||
infoText:SetTextColor(0.6, 0.6, 0.6)
|
||||
infoText:SetText("")
|
||||
|
||||
-- close X
|
||||
local closeX = CreateFrame("Button", "DopingControlReportCloseX",
|
||||
f, "UIPanelCloseButton")
|
||||
closeX:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -8)
|
||||
|
||||
-- scrollable preview: shows reportLines output EXACTLY as sent
|
||||
local scroll = CreateFrame("ScrollFrame", "DopingControlReportScroll",
|
||||
f, "UIPanelScrollFrameTemplate")
|
||||
scroll:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -48)
|
||||
scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -36, 44)
|
||||
|
||||
scrollChild = CreateFrame("Frame", "DopingControlReportScrollChild",
|
||||
scroll)
|
||||
scrollChild:SetWidth(400)
|
||||
scrollChild:SetHeight(1)
|
||||
scroll:SetScrollChild(scrollChild)
|
||||
|
||||
previewText = scrollChild:CreateFontString("DopingControlReportText",
|
||||
"ARTWORK", "GameFontHighlightSmall")
|
||||
previewText:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, 0)
|
||||
previewText:SetWidth(390)
|
||||
previewText:SetJustifyH("LEFT")
|
||||
previewText:SetJustifyV("TOP")
|
||||
-- mono-ish: 1.12 ships no true monospace; the condensed number
|
||||
-- font keeps the indented name lists readable
|
||||
previewText:SetFont("Fonts\\ARIALN.TTF", 12)
|
||||
|
||||
-- buttons
|
||||
sendBtn = CreateFrame("Button", "DopingControlReportSendButton",
|
||||
f, "UIPanelButtonTemplate")
|
||||
sendBtn:SetWidth(150)
|
||||
sendBtn:SetHeight(22)
|
||||
sendBtn:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 16, 14)
|
||||
sendBtn:SetText("Send to raid chat")
|
||||
sendBtn:SetScript("OnClick", OnSendClick)
|
||||
|
||||
local closeBtn = CreateFrame("Button", "DopingControlReportCloseButton",
|
||||
f, "UIPanelButtonTemplate")
|
||||
closeBtn:SetWidth(80)
|
||||
closeBtn:SetHeight(22)
|
||||
closeBtn:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -16, 14)
|
||||
closeBtn:SetText("Close")
|
||||
closeBtn:SetScript("OnClick", function() f:Hide() end)
|
||||
|
||||
if pfUI and pfUI.api then
|
||||
if pfUI.api.SkinButton then
|
||||
pfUI.api.SkinButton(sendBtn)
|
||||
pfUI.api.SkinButton(closeBtn)
|
||||
end
|
||||
if pfUI.api.SkinCloseButton then
|
||||
pfUI.api.SkinCloseButton(closeX)
|
||||
end
|
||||
end
|
||||
|
||||
-- invisible overlay for the disabled-in-test-mode tooltip
|
||||
-- (disabled buttons receive no mouse events on 1.12)
|
||||
sendBlocker = CreateFrame("Frame", "DopingControlReportSendBlocker", f)
|
||||
sendBlocker:SetAllPoints(sendBtn)
|
||||
sendBlocker:SetFrameLevel(sendBtn:GetFrameLevel() + 2)
|
||||
sendBlocker:EnableMouse(true)
|
||||
sendBlocker:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText("disabled in test mode", 1, 1, 1)
|
||||
GameTooltip:AddLine("Simulated names never reach real chat.", 0.8, 0.8, 0.8)
|
||||
GameTooltip:AddLine("The preview stays fully usable.", 0.8, 0.8, 0.8)
|
||||
GameTooltip:Show()
|
||||
end)
|
||||
sendBlocker:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
sendBlocker:Hide()
|
||||
|
||||
-- closing cancels a running drain (see header)
|
||||
f:SetScript("OnHide", function()
|
||||
if queue then
|
||||
queue = nil
|
||||
this:SetScript("OnUpdate", nil)
|
||||
end
|
||||
end)
|
||||
|
||||
-- ESC closes
|
||||
table.insert(UISpecialFrames, "DopingControlReportFrame")
|
||||
|
||||
f:Hide()
|
||||
frame = f
|
||||
return f
|
||||
end
|
||||
|
||||
-- Rebuild the preview content. vm/tab optional (see header).
|
||||
function R.Refresh(vm, tab)
|
||||
BuildReportFrame()
|
||||
tab = tab or currentTab()
|
||||
if vm then
|
||||
local label = (DC.TAB_LABEL and DC.TAB_LABEL[tab]) or tab
|
||||
lines = DC_Model.reportLines(vm, label, R.TimeStr())
|
||||
else
|
||||
lines = R.BuildLines(DC.store, db(), tab)
|
||||
end
|
||||
local n = table.getn(lines)
|
||||
previewText:SetText(table.concat(lines, "\n"))
|
||||
scrollChild:SetHeight(n * 14 + 24)
|
||||
infoText:SetText(string.format(
|
||||
"%d line%s - sent one per %.1f s", n, (n == 1) and "" or "s",
|
||||
R.SEND_INTERVAL))
|
||||
UpdateSendState()
|
||||
end
|
||||
|
||||
-- Public entry point (matrix "Report..." button, /dc report).
|
||||
function R.Open(vm, tab)
|
||||
local f = BuildReportFrame()
|
||||
R.Refresh(vm, tab)
|
||||
f:Show()
|
||||
end
|
||||
|
||||
function R.Toggle(vm, tab)
|
||||
local f = BuildReportFrame()
|
||||
if f:IsShown() then
|
||||
f:Hide()
|
||||
else
|
||||
R.Open(vm, tab)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
+463
@@ -0,0 +1,463 @@
|
||||
-- DopingControl ui/widgets.lua
|
||||
-- Shared UI building blocks: panel backdrop, pooled-widget factories
|
||||
-- (cell / pill / badge / button), tooltip helpers, pfUI-aware font helper,
|
||||
-- change-guarded setters (only touch a region when the value changed).
|
||||
--
|
||||
-- Factory + pooling patterns follow common 1.12 addon practice
|
||||
-- (pfUI-style WHITE8X8 backdrops, pooled regions, save-and-call hooks).
|
||||
--
|
||||
-- Pure Lua 5.0 dofile-loadable: top level only defines tables/functions;
|
||||
-- CreateFrame & friends are only referenced INSIDE factory functions, which
|
||||
-- are never called offline.
|
||||
|
||||
DopingControl = DopingControl or {}
|
||||
local DC = DopingControl
|
||||
|
||||
DC_Widgets = DC_Widgets or {}
|
||||
local W = DC_Widgets
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Texture / font constants
|
||||
-- ------------------------------------------------------------------
|
||||
W.SOLID_TEX = "Interface\\Buttons\\WHITE8X8" -- 1px-border backdrop base
|
||||
W.FONT = "Fonts\\FRIZQT__.TTF" -- default client font
|
||||
W.CHECK_TEX = "Interface\\Buttons\\UI-CheckBox-Check" -- HAS glyph (tinted green)
|
||||
W.CROSS_TEX = "Interface\\Buttons\\UI-GroupLoot-Pass-Up" -- MISSING glyph (tinted red)
|
||||
|
||||
-- Main window backdrop (textures shipped in DopingControl\textures\).
|
||||
W.PANEL_BACKDROP = {
|
||||
bgFile = "Interface\\AddOns\\DopingControl\\textures\\panel_bg",
|
||||
edgeFile = "Interface\\AddOns\\DopingControl\\textures\\panel_border",
|
||||
tile = true, tileSize = 128, edgeSize = 16,
|
||||
insets = { left = 5, right = 5, top = 5, bottom = 5 },
|
||||
}
|
||||
|
||||
-- Solid 1px-border backdrop for cells/pills/badges/buttons (pfUI-style
|
||||
-- WHITE8X8 recipe; bg + border colored via SetBackdropColor /
|
||||
-- SetBackdropBorderColor).
|
||||
W.SOLID_BACKDROP = {
|
||||
bgFile = W.SOLID_TEX,
|
||||
edgeFile = W.SOLID_TEX,
|
||||
tile = false, edgeSize = 1,
|
||||
insets = { left = 1, right = 1, top = 1, bottom = 1 },
|
||||
}
|
||||
|
||||
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
|
||||
-- FAMILY when pfUI is present; forcing pfUI's global font size would break
|
||||
-- the 30x23 cell grid).
|
||||
-- ------------------------------------------------------------------
|
||||
function W.ApplyFont(fs, size)
|
||||
size = size or 10
|
||||
if pfUI and pfUI.font_default then
|
||||
fs:SetFont(pfUI.font_default, size)
|
||||
else
|
||||
fs:SetFont(W.FONT, size)
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Change-guarded setters (convention: cache last-applied value on the
|
||||
-- region as dcLast*; only call the real setter when the value changed.
|
||||
-- Color guards compare by TABLE REFERENCE -- callers always pass the
|
||||
-- shared DC.COLORS / DC.QUALITY tables, so this is exact and zero-alloc).
|
||||
-- ------------------------------------------------------------------
|
||||
function W.SetTextG(fs, text)
|
||||
if fs.dcLastText ~= text then
|
||||
fs.dcLastText = text
|
||||
fs:SetText(text)
|
||||
end
|
||||
end
|
||||
|
||||
function W.SetTextColorG(fs, c)
|
||||
if fs.dcLastColor ~= c then
|
||||
fs.dcLastColor = c
|
||||
fs:SetTextColor(c.r, c.g, c.b)
|
||||
end
|
||||
end
|
||||
|
||||
function W.SetBackdropG(f, bg, border)
|
||||
if f.dcLastBg ~= bg then
|
||||
f.dcLastBg = bg
|
||||
f:SetBackdropColor(bg.r, bg.g, bg.b, bg.a or 1)
|
||||
end
|
||||
if f.dcLastBorder ~= border then
|
||||
f.dcLastBorder = border
|
||||
f:SetBackdropBorderColor(border.r, border.g, border.b, 1)
|
||||
end
|
||||
end
|
||||
|
||||
function W.SetTextureG(tex, path)
|
||||
if tex.dcLastTex ~= path then
|
||||
tex.dcLastTex = path
|
||||
tex:SetTexture(path)
|
||||
end
|
||||
end
|
||||
|
||||
function W.SetVertexG(tex, c)
|
||||
if tex.dcLastVertex ~= c then
|
||||
tex.dcLastVertex = c
|
||||
tex:SetVertexColor(c.r, c.g, c.b)
|
||||
end
|
||||
end
|
||||
|
||||
function W.SetAlphaG(region, a)
|
||||
if region.dcLastAlpha ~= a then
|
||||
region.dcLastAlpha = a
|
||||
region:SetAlpha(a)
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- FontString factory
|
||||
-- ------------------------------------------------------------------
|
||||
function W.MakeText(parent, size, color, layer)
|
||||
local fs = parent:CreateFontString(nil, layer or "OVERLAY")
|
||||
W.ApplyFont(fs, size)
|
||||
if color then
|
||||
W.SetTextColorG(fs, color)
|
||||
end
|
||||
return fs
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Cell factory: 30x23 state cell (the 30x23 size is part of the fixed
|
||||
-- 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:EnableMouse(true)
|
||||
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.
|
||||
-- ------------------------------------------------------------------
|
||||
function W.MakePill(parent, name)
|
||||
local p = CreateFrame("Frame", name, parent)
|
||||
p:SetWidth(70)
|
||||
p:SetHeight(16)
|
||||
p:SetBackdrop(W.SOLID_BACKDROP)
|
||||
p:EnableMouse(true)
|
||||
p.text = p:CreateFontString(nil, "OVERLAY")
|
||||
W.ApplyFont(p.text, 9)
|
||||
p.text:SetPoint("CENTER", p, "CENTER", 0, 0)
|
||||
return p
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Role badge factory: clickable bordered button with centered label.
|
||||
-- Suggested roles render with a "?" suffix + dimmer border (matrix paints
|
||||
-- that -- 1.12 has no dashed borders, see the adaptation note there).
|
||||
-- ------------------------------------------------------------------
|
||||
function W.MakeBadge(parent, name)
|
||||
local b = CreateFrame("Button", name, parent)
|
||||
b:SetWidth(56)
|
||||
b:SetHeight(16)
|
||||
b:SetBackdrop(W.SOLID_BACKDROP)
|
||||
b.text = b:CreateFontString(nil, "OVERLAY")
|
||||
W.ApplyFont(b.text, 8)
|
||||
b.text:SetPoint("CENTER", b, "CENTER", 0, 0)
|
||||
b:RegisterForClicks("LeftButtonUp")
|
||||
return b
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Dark gold-trim button (titlebar Scan/Report/Options/X).
|
||||
-- ------------------------------------------------------------------
|
||||
function W.MakeButton(parent, name, label, width, height)
|
||||
local C = DC.COLORS
|
||||
local b = CreateFrame("Button", name, parent)
|
||||
b:SetWidth(width or 64)
|
||||
b:SetHeight(height or 19)
|
||||
b:SetBackdrop(W.SOLID_BACKDROP)
|
||||
W.SetBackdropG(b, C.panel2, C.goldSoft)
|
||||
b.text = b:CreateFontString(nil, "OVERLAY")
|
||||
W.ApplyFont(b.text, 10)
|
||||
b.text:SetPoint("CENTER", b, "CENTER", 0, 0)
|
||||
W.SetTextColorG(b.text, C.goldHi)
|
||||
b.text:SetText(label)
|
||||
b:SetScript("OnEnter", function()
|
||||
W.SetBackdropG(this, C.panel3, C.gold)
|
||||
end)
|
||||
b:SetScript("OnLeave", function()
|
||||
W.SetBackdropG(this, C.panel2, C.goldSoft)
|
||||
end)
|
||||
return b
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Tooltip helpers. Convention: the builder only ADDS
|
||||
-- lines; AttachTooltip calls SetOwner before and Show after -- AddLine
|
||||
-- never calls Show itself.
|
||||
-- AttachTooltip CHAINS any existing OnEnter/OnLeave (no HookScript on
|
||||
-- 1.12 -- standard save-and-call pattern).
|
||||
-- ------------------------------------------------------------------
|
||||
function W.TipLine(text, c, wrap)
|
||||
if not GameTooltip then
|
||||
return
|
||||
end
|
||||
local r, g, b = 1, 1, 1
|
||||
if c then
|
||||
r, g, b = c.r, c.g, c.b
|
||||
end
|
||||
if wrap then
|
||||
GameTooltip:AddLine(text, r, g, b, 1)
|
||||
else
|
||||
GameTooltip:AddLine(text, r, g, b)
|
||||
end
|
||||
end
|
||||
|
||||
-- Two-column tooltip line: label left, value flush RIGHT. This is what
|
||||
-- makes a breakdown read like an addition on paper -- the values line up
|
||||
-- in a column instead of trailing their labels at ragged positions. 1.12
|
||||
-- has no monospace font and no tab stops; AddDoubleLine is the only way to
|
||||
-- get a right-aligned column, and it exists in this client's FrameXML.
|
||||
-- `cr` defaults to the label color when omitted.
|
||||
function W.TipDouble(left, right, c, cr)
|
||||
if not GameTooltip then
|
||||
return
|
||||
end
|
||||
local r, g, b = 1, 1, 1
|
||||
if c then
|
||||
r, g, b = c.r, c.g, c.b
|
||||
end
|
||||
local r2, g2, b2 = r, g, b
|
||||
if cr then
|
||||
r2, g2, b2 = cr.r, cr.g, cr.b
|
||||
end
|
||||
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"
|
||||
frame.dcOldEnter = frame:GetScript("OnEnter")
|
||||
frame.dcOldLeave = frame:GetScript("OnLeave")
|
||||
frame:SetScript("OnEnter", function()
|
||||
if this.dcOldEnter then
|
||||
this.dcOldEnter()
|
||||
end
|
||||
if GameTooltip and this.dcTipBuilder then
|
||||
GameTooltip:SetOwner(this, this.dcTipAnchor)
|
||||
W.dcTipOwner = this
|
||||
this.dcTipBuilder(this)
|
||||
GameTooltip:Show()
|
||||
end
|
||||
end)
|
||||
frame:SetScript("OnLeave", function()
|
||||
if this.dcOldLeave then
|
||||
this.dcOldLeave()
|
||||
end
|
||||
if W.dcTipOwner == this then
|
||||
W.dcTipOwner = nil
|
||||
end
|
||||
if GameTooltip then
|
||||
GameTooltip:Hide()
|
||||
end
|
||||
end)
|
||||
end
|
||||
Reference in New Issue
Block a user