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(),