diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml
index f3bca6ed..157b4cf2 100644
--- a/crates/benilla-app/assets/ui/OptionsFrame.xml
+++ b/crates/benilla-app/assets/ui/OptionsFrame.xml
@@ -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 @@
-
+
- self:Disable()OptionsTab_FitWidth(self)
@@ -2177,6 +2253,10 @@
OptionsCategoryRow_OnLoad(self, "Audio", "Audio")
+
@@ -2564,6 +2644,15 @@
+
+
+
+
+
+
+
diff --git a/crates/benilla-app/src/ui_script/options_tests.rs b/crates/benilla-app/src/ui_script/options_tests.rs
index 8e2ce3bb..e367c337 100644
--- a/crates/benilla-app/src/ui_script/options_tests.rs
+++ b/crates/benilla-app/src/ui_script/options_tests.rs
@@ -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::(&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::("return BenillaOptionsFrameContainerDefaults:IsEnabled() ~= 0")
+ .unwrap(),
+ "AddOns resets nothing, so Defaults is dead there"
+ );
+ continue;
+ }
assert!(
s.eval::("return BenillaOptionsFrameContainerDefaults:IsEnabled() ~= 0")
.unwrap(),