The `/errors` window wears the ClassTrainer panel art, but its Clear and Close buttons sat at y −455, below the art, and its detail pane ran to −442, into the button sockets. On that art the class trainer lays out (`Blizzard_TrainerUI.xml`, `ClassTrainer_SetToClassTrainer`): - a 296×184 list at (−67, −96) from the top right, with 11 rows; - a 119-tall detail pane 8 below the list, ending at −407; - Train and Exit, 80×22, centred at (224, −420) and (305, −420). The log now uses those frames: Clear and Close sit in the sockets, the detail ends above them, and the list shows 11 rows, not 13. The new test lays out the stock trainer and the log in one UI and checks that each log frame equals the trainer's, with the buttons inside the window. It fails on the old layout. Fixes #698 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
367 lines
21 KiB
XML
367 lines
21 KiB
XML
<Ui>
|
|
<!--
|
|
The script error log, which 1.12 does not have, so every name carries the `Benilla` prefix;
|
|
the stock `ScriptErrors` dialog shows one message, this keeps the log. Laid out on the
|
|
ClassTrainer panel art as the class trainer lays itself out (Blizzard_TrainerUI.xml:214-356
|
|
and :481-513, Blizzard_TrainerUI.lua:469-474): its rows, its list, its detail under the
|
|
list and its two buttons in the art's sockets. It collects as a 1.12 error-log addon does:
|
|
it wraps the handler `geterrorhandler` holds, the stock `_ERRORMESSAGE`
|
|
(BasicControls.xml:16), which still runs after it. An addon that later calls
|
|
`seterrorhandler` replaces the chain, as in 1.12.
|
|
-->
|
|
<Script><![CDATA[
|
|
-- A pushable above every stock panel's, so a game window opening pushes the log aside.
|
|
UIPanelWindows["BenillaScriptLogFrame"] = { area = "left", pushable = 9, whileDead = 1 }
|
|
|
|
-- The class trainer's rows (`CLASS_TRAINER_SKILLS_DISPLAYED`, Blizzard_TrainerUI.lua:470).
|
|
BENILLA_SCRIPTLOG_ROWS = 11
|
|
BENILLA_SCRIPTLOG_ROW_HEIGHT = 16
|
|
-- A wrapped row would break the faux kit's fixed rows.
|
|
BENILLA_SCRIPTLOG_ROW_CHARS = 52
|
|
|
|
-- A `seq`, not an index: indices shift when the cap evicts.
|
|
BenillaScriptLogSelected = nil
|
|
|
|
-- Distinct messages kept before the oldest is evicted.
|
|
BENILLA_SCRIPTLOG_CAP = 256
|
|
-- Rows oldest first, each { seq, message, count }; `total` is the last `seq` handed out,
|
|
-- kept apart from the row count so an eviction or a clear never renumbers.
|
|
BenillaScriptLog = { rows = {}, total = 0 }
|
|
|
|
-- A repeat bumps its row's count and keeps its place.
|
|
function BenillaScriptLog_Record(message)
|
|
if type(message) ~= "string" then message = tostring(message) end
|
|
local log = BenillaScriptLog
|
|
local found = nil
|
|
for i = 1, table.getn(log.rows) do
|
|
if log.rows[i].message == message then found = log.rows[i] end
|
|
end
|
|
if found then
|
|
found.count = found.count + 1
|
|
else
|
|
log.total = log.total + 1
|
|
if table.getn(log.rows) >= BENILLA_SCRIPTLOG_CAP then table.remove(log.rows, 1) end
|
|
table.insert(log.rows, { seq = log.total, message = message, count = 1 })
|
|
end
|
|
if BenillaScriptLogFrame and BenillaScriptLogFrame:IsVisible() then
|
|
BenillaScriptLog_Update()
|
|
end
|
|
end
|
|
|
|
-- The chain: the log first, guarded so a fault in it cannot stop the stock handler.
|
|
local benillaStockErrorHandler = geterrorhandler()
|
|
seterrorhandler(function(message)
|
|
pcall(BenillaScriptLog_Record, message)
|
|
return benillaStockErrorHandler(message)
|
|
end)
|
|
|
|
-- `strsub` counts bytes, so the cut backs up over UTF-8 continuation bytes (0x80..0xBF).
|
|
function BenillaScriptLog_RowText(message)
|
|
local cut = strfind(message, "\n", 1, 1)
|
|
local head = message
|
|
if cut then head = strsub(message, 1, cut - 1) end
|
|
if strlen(head) > BENILLA_SCRIPTLOG_ROW_CHARS then
|
|
local last = BENILLA_SCRIPTLOG_ROW_CHARS - 3
|
|
while last > 0 do
|
|
local b = strbyte(head, last + 1)
|
|
if not b or b < 128 or b >= 192 then break end
|
|
last = last - 1
|
|
end
|
|
head = strsub(head, 1, last) .. "..."
|
|
end
|
|
return head
|
|
end
|
|
|
|
function BenillaScriptLog_Update()
|
|
local shown = table.getn(BenillaScriptLog.rows)
|
|
local total = BenillaScriptLog.total
|
|
|
|
local summary
|
|
if total == 0 then
|
|
summary = "No problems this session."
|
|
elseif total > shown then
|
|
summary = format("%d problems (%d older not kept)", total, total - shown)
|
|
elseif total == 1 then
|
|
summary = "1 problem"
|
|
else
|
|
summary = format("%d problems", total)
|
|
end
|
|
getglobal("BenillaScriptLogSummary"):SetText(summary)
|
|
|
|
local scrollFrame = getglobal("BenillaScriptLogListScrollFrame")
|
|
FauxScrollFrame_Update(scrollFrame, shown, BENILLA_SCRIPTLOG_ROWS, BENILLA_SCRIPTLOG_ROW_HEIGHT)
|
|
BenillaScrollTrough_Follow("BenillaScriptLogListScrollFrame")
|
|
local offset = FauxScrollFrame_GetOffset(scrollFrame)
|
|
|
|
local selectedRow = nil
|
|
for i = 1, BENILLA_SCRIPTLOG_ROWS do
|
|
local button = getglobal("BenillaScriptLogRow" .. i)
|
|
local index = offset + i
|
|
local row = BenillaScriptLog.rows[index]
|
|
if row then
|
|
button.seq = row.seq
|
|
button.message = row.message
|
|
-- `#12 x1113`: one deduplicated failure that happened 1,113 times.
|
|
local label = "#" .. row.seq
|
|
if row.count > 1 then label = label .. " x" .. row.count end
|
|
button.index:SetText(label)
|
|
button.label:SetText(BenillaScriptLog_RowText(row.message))
|
|
button:Show()
|
|
if row.seq == BenillaScriptLogSelected then selectedRow = button end
|
|
else
|
|
button.seq = nil
|
|
button.message = nil
|
|
button:Hide()
|
|
end
|
|
end
|
|
|
|
-- Hides when the row scrolls away, as the reference's CraftFrame does.
|
|
local highlight = getglobal("BenillaScriptLogHighlightFrame")
|
|
if selectedRow then
|
|
highlight:SetPoint("TOPLEFT", selectedRow:GetName(), "TOPLEFT", 0, 0)
|
|
highlight:Show()
|
|
else
|
|
highlight:Hide()
|
|
end
|
|
|
|
BenillaScriptLog_UpdateDetail()
|
|
end
|
|
|
|
-- The selected message in full, traceback and all.
|
|
function BenillaScriptLog_UpdateDetail()
|
|
local text = ""
|
|
if BenillaScriptLogSelected then
|
|
for _, row in ipairs(BenillaScriptLog.rows) do
|
|
if row.seq == BenillaScriptLogSelected then
|
|
text = row.message
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if text == "" then
|
|
text = "Select a row above to read it in full."
|
|
end
|
|
getglobal("BenillaScriptLogDetailText"):SetText(text)
|
|
end
|
|
|
|
-- Text rects resolve after Lua runs, so the detail child is re-measured every frame.
|
|
function BenillaScriptLogFrame_OnUpdate()
|
|
BenillaScroll_ResizeChild("BenillaScriptLogDetailScroll", { "BenillaScriptLogDetailText" })
|
|
end
|
|
|
|
function BenillaScriptLogRow_OnClick(button)
|
|
if not button.seq then return end
|
|
if BenillaScriptLogSelected == button.seq then
|
|
BenillaScriptLogSelected = nil
|
|
else
|
|
BenillaScriptLogSelected = button.seq
|
|
end
|
|
BenillaScriptLog_Update()
|
|
end
|
|
|
|
function BenillaScriptLogFrame_OnLoad()
|
|
-- Masks the square icon to the portrait ring.
|
|
SetPortraitToTexture("BenillaScriptLogFramePortrait",
|
|
"Interface\\Icons\\INV_Misc_Note_01")
|
|
end
|
|
|
|
-- The list frame's OnVerticalScroll repaints.
|
|
function BenillaScriptLogFrame_OnMouseWheel(delta)
|
|
BenillaFauxScrollFrame_OnMouseWheel(getglobal("BenillaScriptLogListScrollFrame"), delta)
|
|
end
|
|
|
|
-- ── Entry points ──────────────────────────────────────────────────────────────────────
|
|
|
|
function BenillaScriptLog_Toggle()
|
|
local frame = getglobal("BenillaScriptLogFrame")
|
|
if frame:IsVisible() then
|
|
HideUIPanel(frame)
|
|
else
|
|
ShowUIPanel(frame)
|
|
end
|
|
end
|
|
|
|
-- `/errors clear`: keeps the numbering, so a number never names two failures.
|
|
function BenillaScriptLog_Clear()
|
|
BenillaScriptLog.rows = {}
|
|
BenillaScriptLogSelected = nil
|
|
if getglobal("BenillaScriptLogFrame"):IsVisible() then
|
|
BenillaScriptLog_Update()
|
|
end
|
|
DEFAULT_CHAT_FRAME:AddMessage("Script log cleared.", 1.0, 1.0, 0.0)
|
|
end
|
|
]]></Script>
|
|
|
|
<!-- One list row: the `#N xCount` column, then the message head. -->
|
|
<Button name="BenillaScriptLogRowTemplate" virtual="true" hidden="true">
|
|
<Size><AbsDimension x="293" y="16"/></Size>
|
|
<Layers>
|
|
<Layer level="ARTWORK">
|
|
<FontString name="$parentIndex" inherits="GameFontNormalSmall" justifyH="LEFT">
|
|
<Size><AbsDimension x="56" y="16"/></Size>
|
|
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor></Anchors>
|
|
</FontString>
|
|
<FontString name="$parentLabel" inherits="GameFontNormalSmall" justifyH="LEFT">
|
|
<Size><AbsDimension x="229" y="16"/></Size>
|
|
<Anchors><Anchor point="LEFT" relativePoint="LEFT"><Offset><AbsDimension x="62" y="0"/></Offset></Anchor></Anchors>
|
|
</FontString>
|
|
</Layer>
|
|
</Layers>
|
|
<HighlightTexture file="Interface\Buttons\UI-Listbox-Highlight2" alphaMode="ADD"/>
|
|
<Scripts>
|
|
<OnLoad>
|
|
this.index = getglobal(this:GetName() .. "Index")
|
|
this.label = getglobal(this:GetName() .. "Label")
|
|
</OnLoad>
|
|
<OnClick>BenillaScriptLogRow_OnClick(this)</OnClick>
|
|
</Scripts>
|
|
</Button>
|
|
|
|
<Frame name="BenillaScriptLogFrame" hidden="true">
|
|
<!-- The standard left-slot panel size. No <Anchors>: ShowUIPanel seats it. -->
|
|
<Size><AbsDimension x="384" y="512"/></Size>
|
|
<Layers>
|
|
<Layer level="BACKGROUND">
|
|
<!-- OnLoad masks the square icon, whose corners would show through the ring. -->
|
|
<Texture name="BenillaScriptLogFramePortrait" file="Interface\Icons\INV_Misc_Note_01">
|
|
<Size><AbsDimension x="60" y="60"/></Size>
|
|
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="7" y="-6"/></Offset></Anchor></Anchors>
|
|
</Texture>
|
|
</Layer>
|
|
<!-- The ClassTrainer quadrants, the one profession-free set in the 1.12 art. -->
|
|
<Layer level="BORDER">
|
|
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-TopLeft"><Size><AbsDimension x="256" y="256"/></Size><Anchors><Anchor point="TOPLEFT"/></Anchors></Texture>
|
|
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-TopRight"><Size><AbsDimension x="128" y="256"/></Size><Anchors><Anchor point="TOPRIGHT"/></Anchors></Texture>
|
|
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-BotLeft"><Size><AbsDimension x="256" y="256"/></Size><Anchors><Anchor point="BOTTOMLEFT"/></Anchors></Texture>
|
|
<Texture file="Interface\ClassTrainerFrame\UI-ClassTrainer-BotRight"><Size><AbsDimension x="128" y="256"/></Size><Anchors><Anchor point="BOTTOMRIGHT"/></Anchors></Texture>
|
|
<FontString name="BenillaScriptLogTitle" inherits="GameFontNormal" justifyH="CENTER" text="Script Errors">
|
|
<Anchors><Anchor point="TOP" relativePoint="TOP"><Offset><AbsDimension x="0" y="-17"/></Offset></Anchor></Anchors>
|
|
</FontString>
|
|
<FontString name="BenillaScriptLogSummary" inherits="GameFontHighlightSmall" justifyH="LEFT" text="">
|
|
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="26" y="-76"/></Offset></Anchor></Anchors>
|
|
</FontString>
|
|
</Layer>
|
|
</Layers>
|
|
<Frames>
|
|
<Button name="BenillaScriptLogCloseButton" inherits="UIPanelCloseButton">
|
|
<Anchors><Anchor point="TOPRIGHT"><Offset><AbsDimension x="-29" y="-8"/></Offset></Anchor></Anchors>
|
|
</Button>
|
|
|
|
<!-- Declared before the rows so their text draws over the glow. -->
|
|
<Frame name="BenillaScriptLogHighlightFrame" hidden="true">
|
|
<Size><AbsDimension x="293" y="16"/></Size>
|
|
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
|
|
<Layers><Layer level="ARTWORK">
|
|
<Texture file="Interface\Buttons\UI-Listbox-Highlight2">
|
|
<Anchors><Anchor point="TOPLEFT"/><Anchor point="BOTTOMRIGHT"/></Anchors>
|
|
</Texture>
|
|
</Layer></Layers>
|
|
</Frame>
|
|
|
|
<!-- The class trainer's first row (ClassTrainerSkill1, Blizzard_TrainerUI.xml:214-222). -->
|
|
<Button name="BenillaScriptLogRow1" inherits="BenillaScriptLogRowTemplate">
|
|
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="22" y="-100"/></Offset></Anchor></Anchors>
|
|
</Button>
|
|
<Button name="BenillaScriptLogRow2" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow1" relativePoint="BOTTOMLEFT"/></Anchors></Button>
|
|
<Button name="BenillaScriptLogRow3" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow2" relativePoint="BOTTOMLEFT"/></Anchors></Button>
|
|
<Button name="BenillaScriptLogRow4" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow3" relativePoint="BOTTOMLEFT"/></Anchors></Button>
|
|
<Button name="BenillaScriptLogRow5" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow4" relativePoint="BOTTOMLEFT"/></Anchors></Button>
|
|
<Button name="BenillaScriptLogRow6" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow5" relativePoint="BOTTOMLEFT"/></Anchors></Button>
|
|
<Button name="BenillaScriptLogRow7" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow6" relativePoint="BOTTOMLEFT"/></Anchors></Button>
|
|
<Button name="BenillaScriptLogRow8" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow7" relativePoint="BOTTOMLEFT"/></Anchors></Button>
|
|
<Button name="BenillaScriptLogRow9" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow8" relativePoint="BOTTOMLEFT"/></Anchors></Button>
|
|
<Button name="BenillaScriptLogRow10" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow9" relativePoint="BOTTOMLEFT"/></Anchors></Button>
|
|
<Button name="BenillaScriptLogRow11" inherits="BenillaScriptLogRowTemplate"><Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogRow10" relativePoint="BOTTOMLEFT"/></Anchors></Button>
|
|
|
|
<!-- The class trainer's list frame (Blizzard_TrainerUI.xml:313-322, 184 tall as its .lua:471
|
|
sets it). Only the faux kit's right-hand Slider renders. -->
|
|
<ScrollFrame name="BenillaScriptLogListScrollFrame" inherits="FauxScrollFrameTemplate">
|
|
<Size><AbsDimension x="296" y="184"/></Size>
|
|
<Anchors><Anchor point="TOPRIGHT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="-67" y="-96"/></Offset></Anchor></Anchors>
|
|
<Scripts>
|
|
<!-- The stock kit has no `updateFunc`, so the repaint is wired here. -->
|
|
<OnVerticalScroll>FauxScrollFrame_OnVerticalScroll(BENILLA_SCRIPTLOG_ROW_HEIGHT, BenillaScriptLog_Update)</OnVerticalScroll>
|
|
</Scripts>
|
|
</ScrollFrame>
|
|
|
|
<!-- The class trainer's detail pane, 8 under its list and 119 tall (Blizzard_TrainerUI.xml:
|
|
346-356, .lua:472), so it ends above the buttons' sockets. The real-scroll kit: a
|
|
message with its traceback has variable height. -->
|
|
<ScrollFrame name="BenillaScriptLogDetailScroll">
|
|
<Size><AbsDimension x="296" y="119"/></Size>
|
|
<Anchors><Anchor point="TOPLEFT" relativeTo="BenillaScriptLogListScrollFrame" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-8"/></Offset></Anchor></Anchors>
|
|
<Scripts>
|
|
<OnLoad>this:SetScrollChild(getglobal(this:GetName() .. "Child"))</OnLoad>
|
|
<OnMouseWheel>BenillaScroll_OnMouseWheel(this, arg1)</OnMouseWheel>
|
|
<OnVerticalScroll>BenillaScroll_SyncBar(this)</OnVerticalScroll>
|
|
<OnScrollRangeChanged>BenillaScroll_SyncBar(this)</OnScrollRangeChanged>
|
|
</Scripts>
|
|
<Frames>
|
|
<Slider name="$parentBar" inherits="BenillaScrollBarTemplate" hidden="true">
|
|
<Anchors>
|
|
<Anchor point="TOPLEFT" relativePoint="TOPRIGHT"><Offset><AbsDimension x="6" y="-16"/></Offset></Anchor>
|
|
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="6" y="16"/></Offset></Anchor>
|
|
</Anchors>
|
|
<Scripts><OnValueChanged>BenillaScrollBar_OnValueChanged(this, arg1)</OnValueChanged></Scripts>
|
|
</Slider>
|
|
<Frame name="$parentChild">
|
|
<Size><AbsDimension x="296" y="119"/></Size>
|
|
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"/></Anchors>
|
|
<Layers>
|
|
<Layer level="ARTWORK">
|
|
<!-- 270 wide so the traceback wraps rather than clips. -->
|
|
<FontString name="BenillaScriptLogDetailText" inherits="GameFontHighlightSmall" justifyH="LEFT" justifyV="TOP" text="">
|
|
<Size><AbsDimension x="270" y="0"/></Size>
|
|
<Anchors><Anchor point="TOPLEFT" relativePoint="TOPLEFT"><Offset><AbsDimension x="6" y="-6"/></Offset></Anchor></Anchors>
|
|
</FontString>
|
|
</Layer>
|
|
</Layers>
|
|
</Frame>
|
|
</Frames>
|
|
</ScrollFrame>
|
|
|
|
<!-- The art's two button sockets, where the class trainer's Train and Exit sit
|
|
(Blizzard_TrainerUI.xml:481-513). -->
|
|
<Button name="BenillaScriptLogClearButton" inherits="UIPanelButtonTemplate" text="Clear">
|
|
<Size><AbsDimension x="80" y="22"/></Size>
|
|
<Anchors><Anchor point="CENTER" relativePoint="TOPLEFT"><Offset><AbsDimension x="224" y="-420"/></Offset></Anchor></Anchors>
|
|
<Scripts><OnClick>BenillaScriptLog_Clear()</OnClick></Scripts>
|
|
</Button>
|
|
<Button name="BenillaScriptLogCloseBottomButton" inherits="UIPanelButtonTemplate" text="Close">
|
|
<Size><AbsDimension x="80" y="22"/></Size>
|
|
<Anchors><Anchor point="CENTER" relativePoint="TOPLEFT"><Offset><AbsDimension x="305" y="-420"/></Offset></Anchor></Anchors>
|
|
<Scripts><OnClick>HideUIPanel(getglobal("BenillaScriptLogFrame"))</OnClick></Scripts>
|
|
</Button>
|
|
</Frames>
|
|
<Scripts>
|
|
<OnLoad>BenillaScriptLogFrame_OnLoad()</OnLoad>
|
|
<!-- A spin bubbles up the parent chain; the detail pane's own handler wins there. -->
|
|
<OnMouseWheel>BenillaScriptLogFrame_OnMouseWheel(arg1)</OnMouseWheel>
|
|
<OnShow>BenillaScriptLog_Update()</OnShow>
|
|
<OnUpdate>BenillaScriptLogFrame_OnUpdate()</OnUpdate>
|
|
</Scripts>
|
|
</Frame>
|
|
<!-- The player's commands, as an addon registers them; after the stock ChatFrame.lua, which
|
|
declares SlashCmdList. -->
|
|
<Script><![CDATA[
|
|
-- Deviation: 1.12 has no `/reload` (later clients added it; 1.12 reloads through
|
|
-- `/console reloadui`); kept so a player can reload the interface after toggling addons.
|
|
SLASH_BENILLA_RELOAD1 = "/reload"
|
|
SlashCmdList["BENILLA_RELOAD"] = function()
|
|
ReloadUI()
|
|
end
|
|
|
|
-- `/errors` toggles the log and `/errors clear` empties it; `/err` is the short form.
|
|
SLASH_BENILLA_ERRORS1 = "/errors"
|
|
SLASH_BENILLA_ERRORS2 = "/err"
|
|
SlashCmdList["BENILLA_ERRORS"] = function(msg)
|
|
local arg = string.lower((string.gsub(msg or "", "^%s*(.-)%s*$", "%1")))
|
|
if arg == "clear" then
|
|
BenillaScriptLog_Clear()
|
|
else
|
|
BenillaScriptLog_Toggle()
|
|
end
|
|
end
|
|
]]></Script>
|
|
</Ui>
|