Options: the era AddOns page, one checkbox per installed addon, then Load out of date AddOns

The era's tenth category was a button in the tree that went nowhere. It now opens onto the
era's own per-addon enable list, rebuilt on show from the AddOn API: one checkbox per
installed addon carrying its `GetAddOnInfo` enabled state, then the Load out of date AddOns
row under it (`checkAddonVersion`, inverted, so checked means "0"). A toggle calls
`EnableAddOn`/`DisableAddOn`, which the host persists to `AddOns.txt` at logout — the
reference's own contract for this screen, where the change lands on the next world entry.

`the_defaults_button_is_armed_by_rows_not_by_a_category` is the invariant that every
category leads somewhere and that Defaults is live on every page with a CVar behind it, and
AddOns is the second category to run its own page after Keybindings. It is exempted from the
`OPTIONS_PAGE_ROWS` half for that reason, and from the Defaults half because an addon's
enabled state is not a CVar: there is nothing there for Defaults to write back.
This commit is contained in:
chairmanpow
2026-10-02 13:36:51 +01:00
parent 28cac71578
commit 0b71937624
2 changed files with 116 additions and 12 deletions
+92 -3
View File
@@ -86,7 +86,7 @@
-- that keeps the three shadow rows it inherits one row away from where they used to be.
OPTIONS_CATEGORY_KEYS = {
"Controls", "Interface", "ActionBars", "Combat", "Chat", "Keybindings", "Nameplates",
"Graphics", "AdvancedGraphics", "Audio",
"Graphics", "AdvancedGraphics", "Audio", "AddOns",
}
-- The era UIParent draws smaller than our 768-unit canvas; a chosen value, not the
@@ -148,13 +148,22 @@
BenillaOptionsFrameContainerScroll:SetVerticalScroll(0)
-- Leaving Keybindings disarms a pending capture, which must never outlive the page.
local kb = getglobal("BenillaOptionsFrameContainerBodyKeybindings")
local addons = getglobal("BenillaOptionsFrameContainerBodyAddOns")
if key == "Keybindings" then
kb:Show()
addons:Hide()
KeyBindingsPage_Refresh()
BenillaOptionsFrameContainerUnbind:Show()
BenillaOptionsFrameContainerDefaults:Enable()
elseif key == "AddOns" then
kb:Hide()
addons:Show()
OptionsAddOns_Refresh()
BenillaOptionsFrameContainerUnbind:Hide()
BenillaOptionsFrameContainerDefaults:Disable()
else
kb:Hide()
addons:Hide()
KeyBindings_SetSelected(nil)
KeyBindings_Output("")
BenillaOptionsFrameContainerUnbind:Hide()
@@ -252,6 +261,11 @@
for i = 1, KEYBINDINGS_SEARCH_ROWS do
take("BenillaOptionsFrameContainerBodyKeybindSearch" .. i)
end
-- The AddOns page's dynamic rows: row 0 is the Load out of date AddOns row.
for i = 0, table.getn(OptionsAddOns_Rows) do
local row = OptionsAddOns_Rows[i]
if row then take(row:GetName()) end
end
return bottom
end
@@ -313,6 +327,8 @@
for i = 1, KEYBINDINGS_SEARCH_ROWS do
getglobal("BenillaOptionsFrameContainerBodyKeybindSearch" .. i):Hide()
end
-- The AddOns page is not in OPTIONS_PAGE_ROWS; hide it and its rows with it.
BenillaOptionsFrameContainerBodyAddOns:Hide()
KeyBindingsPage.searchPainted = nil
KeyBindings_SetSelected(nil)
BenillaOptionsFrameContainerUnbind:Hide()
@@ -1472,6 +1488,67 @@
OptionsApply_Update()
end
-- ── The AddOns page: the era's per-addon enable list ──
-- Rows are built on show from the AddOn API: one checkbox per installed addon (its
-- `GetAddOnInfo` enabled state), then the Load out of date AddOns row. A toggle calls
-- `EnableAddOn`/`DisableAddOn`, which the host persists to `AddOns.txt` at logout, so the
-- change applies on the next world entry — the reference's own contract for the AddOns
-- screen. Row 0 is the Load out of date AddOns row.
OptionsAddOns_Rows = {}
function OptionsAddOns_GetRow(i)
local row = OptionsAddOns_Rows[i]
if row then return row end
row = CreateFrame("Frame", "BenillaOptionsFrameContainerBodyAddOnsRow" .. i,
BenillaOptionsFrameContainerBodyAddOns, "OptionsCheckboxRowTemplate")
row.index = i
OptionsAddOns_Rows[i] = row
return row
end
function OptionsAddOns_OnClick(box)
local row = box:GetParent()
local checked = box:GetChecked()
PlaySound(checked and "igMainMenuOptionCheckBoxOn" or "igMainMenuOptionCheckBoxOff")
if checked then
EnableAddOn(row.index)
else
DisableAddOn(row.index)
end
end
function OptionsAddOns_Refresh()
local n = GetNumAddOns()
local prev = nil
for i = 1, n do
local name, title, notes, enabled = GetAddOnInfo(i)
local row = OptionsAddOns_GetRow(i)
row:Show()
local label = getglobal(row:GetName() .. "Label")
label:SetText(title or name)
row.searchTag = string.upper(title or name)
local box = getglobal(row:GetName() .. "Check")
box:SetChecked(enabled)
box:SetScript("OnClick", OptionsAddOns_OnClick)
OptionsSearch_Place(row:GetName(), prev)
prev = row:GetName()
end
for i = n + 1, table.getn(OptionsAddOns_Rows) do
OptionsAddOns_Rows[i]:Hide()
end
-- The Load out of date AddOns row, under the list; inverted, `checkAddonVersion`
-- "1" enforces the check (`0x402c3b`), so checked means "0".
local lod = OptionsAddOns_GetRow(0)
if not lod.cvar then
OptionsRow_OnLoad(lod, "checkAddonVersion", "Load out of date AddOns")
lod.invert = 1
end
lod:Show()
getglobal(lod:GetName() .. "Check"):SetChecked(
OptionsRow_Get(lod) == "0" and 1 or nil)
OptionsSearch_Place(lod:GetName(), prev)
end
-- ── MONKEY (advanced graphics): the page that has to WATCH its settings ─────────────
-- Every other page in this window owns its rows outright: nothing but a click on the page
-- moves the CVars behind it while it is open, so reading them once on select is the whole
@@ -2101,9 +2178,8 @@
<NormalFont inherits="GameFontNormalSmall"/>
<HighlightFont inherits="GameFontNormalSmall"/>
<DisabledFont inherits="GameFontNormalSmall"/>
<!-- No addon list behind it: disabled, in the era's unselected gold. -->
<!-- The AddOns page below: the era's per-addon enable list. -->
<Scripts>
<OnLoad>self:Disable()</OnLoad>
<OnShow>OptionsTab_FitWidth(self)</OnShow>
</Scripts>
</Button>
@@ -2177,6 +2253,10 @@
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentRowAdvancedGraphics" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>OptionsCategoryRow_OnLoad(self, "Audio", "Audio")</OnLoad></Scripts>
</Button>
<Button name="$parentRowAddOns" inherits="OptionsCategoryRowTemplate">
<Anchors><Anchor point="TOPLEFT" relativeTo="$parentRowAudio" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
<Scripts><OnLoad>OptionsCategoryRow_OnLoad(self, "AddOns", "AddOns")</OnLoad></Scripts>
</Button>
</Frames>
</Frame>
@@ -2564,6 +2644,15 @@
</Frame>
</Frames>
</Frame>
<!-- The era AddOns page: one checkbox per installed addon (enabled
state), then the Load out of date AddOns row; the rows are
built by OptionsAddOns_Refresh on show. -->
<Frame name="$parentAddOns" hidden="true">
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT"/>
</Anchors>
</Frame>
<!-- No useUiScale checkbox, which 1.12 has: benilla registers
useUiScale but never reads it, so the UI scale always
applies. -->
@@ -4073,8 +4073,8 @@ fn the_buff_durations_row_repitches_the_bar_and_the_pitch_survives_a_restart() {
);
}
/// Every category has rows but Keybindings, which runs its own page, and each arms Defaults; a key
/// with no rows behind it leaves Defaults dead.
/// Every category has rows but Keybindings and AddOns, which run their own pages, and each
/// row-backed category arms Defaults; a key with no rows behind it leaves Defaults dead.
#[test]
fn the_defaults_button_is_armed_by_rows_not_by_a_category() {
benilla_formats::wow_data_or_skip!();
@@ -4087,16 +4087,20 @@ fn the_defaults_button_is_armed_by_rows_not_by_a_category() {
.split(',')
.map(str::to_string)
.collect();
// MONKEY (advanced graphics): ten, not nine — the era's own tree has no Advanced Graphics
// page, because the era client has no dynamic light and shadow system to put on one. It is
// benilla's own category, seated under Graphics in the System group, and it is held to the
// same bar as the era's nine: it opens onto rows, and Defaults is live on it.
// MONKEY (advanced graphics): ten of the eleven, not nine — the era's own tree has no Advanced
// Graphics page, because the era client has no dynamic light and shadow system to put on one. It
// is benilla's own category, seated under Graphics in the System group, and it is held to the
// same bar as the era's nine: it opens onto rows, and Defaults is live on it. The eleventh is
// AddOns, the era's own per-addon enable list, rebuilt on show from the AddOn API.
assert_eq!(
keys.len(),
10,
"the nine 1.15.9 categories + ours: {keys:?}"
11,
"the nine 1.15.9 categories + AdvancedGraphics + AddOns: {keys:?}"
);
for key in &keys {
// Keybindings and AddOns own their pages: Keybindings builds its search rows on show and
// AddOns builds one row per installed addon, so neither is a fixed OPTIONS_PAGE_ROWS list.
let owns_its_page = matches!(key.as_str(), "Keybindings" | "AddOns");
let has_rows = s
.eval::<bool>(&format!(
"return OPTIONS_PAGE_ROWS[\"{key}\"] ~= nil and \
@@ -4104,11 +4108,22 @@ fn the_defaults_button_is_armed_by_rows_not_by_a_category() {
))
.unwrap();
assert!(
has_rows || key == "Keybindings",
has_rows || owns_its_page,
"{key} opens onto nothing — every category leads somewhere"
);
s.run(&format!("BenillaOptionsFrameCategoryListRow{key}:Click()"))
.unwrap();
// Defaults resets the CVars behind a page. The AddOns page holds none: an addon's enabled
// state is EnableAddOn/DisableAddOn, which the host persists to `AddOns.txt` at logout, so
// there is nothing there for Defaults to write back — the button stays dead on it.
if key == "AddOns" {
assert!(
!s.eval::<bool>("return BenillaOptionsFrameContainerDefaults:IsEnabled() ~= 0")
.unwrap(),
"AddOns resets nothing, so Defaults is dead there"
);
continue;
}
assert!(
s.eval::<bool>("return BenillaOptionsFrameContainerDefaults:IsEnabled() ~= 0")
.unwrap(),