From 1a62b4ed1fffae3b97124c5c3fdb4f360533bd87 Mon Sep 17 00:00:00 2001 From: Narcasung Date: Sat, 22 Aug 2026 15:57:05 +0200 Subject: [PATCH] Skin the options the CoA client adds to Interface, Video and Audio ElvUI skins the stock options windows from four hardcoded name lists, so every widget the client added since upstream last synced stays native -- 53 of them here, 39 in four panels upstream never enumerated at all (Battlenet, RaidEffects, Stereo, Voice). Reported cases were the Auto Accept Trades checkbox and the Spell Queue Window slider. Sweep the three panel containers on show and skin by object type instead of carrying a parallel name list that would go stale the same way. Panels belonging to other addons are skipped by matching frames against INTERFACEOPTIONS_ADDONCATEGORIES, rebuilt on every sweep so a load-on-demand addon that registers late is still caught. Two guards worth naming. Sliders are gated on frame.template because HandleSliderFrame has no idempotence check of its own and HookScript stacks, so a second pass over one ElvUI already skinned would run its OnDisable/OnEnable handlers twice. Dropdowns are detected structurally, by a Middle texture plus a Button, because HandleDropDownBox uses that button unguarded and a false positive is a Lua error rather than a cosmetic miss. No Button branch: nothing needs one, and it would flatten other addons' panel tabs into ElvUI pill buttons. --- ElvUI_CoA.toc | 1 + Modules/InterfaceOptions.lua | 142 +++++++++++++++++++++++++++++++++++ README.md | 1 + core.lua | 28 +++++++ 4 files changed, 172 insertions(+) create mode 100644 Modules/InterfaceOptions.lua diff --git a/ElvUI_CoA.toc b/ElvUI_CoA.toc index 6855703..fc802ac 100644 --- a/ElvUI_CoA.toc +++ b/ElvUI_CoA.toc @@ -18,3 +18,4 @@ Modules\Skinning.lua Modules\TalentFrame.lua Modules\VanityFrame.lua Modules\WardrobeFrame.lua +Modules\InterfaceOptions.lua diff --git a/Modules/InterfaceOptions.lua b/Modules/InterfaceOptions.lua new file mode 100644 index 0000000..3e93312 --- /dev/null +++ b/Modules/InterfaceOptions.lua @@ -0,0 +1,142 @@ +local E, L, V, P, G = unpack(ElvUI) +local S = E:GetModule("Skins") +local CoA = E:GetModule("CoA") + +local _G = _G +local ipairs, pairs, select, type = ipairs, pairs, select, type + +-- Generic sweep over the stock options windows. +-- +-- ElvUI skins those windows from four hardcoded name lists (checkboxes, +-- sliders, buttons, dropdowns) in Modules/Skins/Blizzard/BlizzardOptions.lua, +-- so anything the client added since upstream last synced stays native -- +-- 53 widgets on this server, 39 of them in four panels upstream never +-- enumerated at all (Battlenet, RaidEffects, Stereo, Voice). Rather than +-- carry a parallel name list that goes stale the same way, this walks the +-- panel containers and skins by object type. + +local MAX_DEPTH = 3 + +-- Every panel is parented at load (probed: the Interface container reports the +-- same child count before and after opening the window), so the containers can +-- be walked without waiting for a category to be displayed. +local ROOTS = { + "InterfaceOptionsFramePanelContainer", + "VideoOptionsFramePanelContainer", + "AudioOptionsFramePanelContainer", +} + +-- Hooked on all three windows rather than only the Interface one: the Video and +-- Audio windows are separate frames reachable straight from the game menu, and +-- a player who opens one of those first would otherwise see it unskinned. Each +-- show sweeps every root, which costs nothing -- the pass is idempotent. +local TRIGGERS = { + "InterfaceOptionsFrame", + "VideoOptionsFrame", + "AudioOptionsFrame", +} + +-- Panels owned by other addons, keyed by frame. Rebuilt on every sweep rather +-- than cached at init: load-on-demand addons register their category late, and +-- a set built once would let their panels through on later shows. +local addonPanels = {} + +local function RebuildAddOnPanels() + for panel in pairs(addonPanels) do + addonPanels[panel] = nil + end + + local categories = _G.INTERFACEOPTIONS_ADDONCATEGORIES + if type(categories) ~= "table" then return end + + -- The entries are the panel frames themselves, not descriptors (probed). + -- Matching on the frame is why this doesn't need a name prefix whitelist: + -- an addon can name its panel anything, and the registry is authoritative. + for _, panel in ipairs(categories) do + if type(panel) == "table" then + addonPanels[panel] = true + end + end +end + +-- Structural test, not a name test. S:HandleDropDownBox uses _G[name.."Button"] +-- unguarded when anchoring its backdrop, so a false positive is a Lua error +-- rather than a cosmetic miss; it also force-sets the frame's width, which +-- mangles a group box even when it doesn't error. A DropDown name suffix was +-- considered and rejected -- ElvUI's own dropdown list carries +-- CompactUnitFrameProfilesProfileSelector (no suffix) and ...SortByDropdown +-- (lowercase d), so suffix matching misses real dropdowns and adds nothing. +local function IsDropDown(frame) + local name = frame.GetName and frame:GetName() + if not name then return false end + + local middle = _G[name.."Middle"] + if not middle or not middle.IsObjectType or not middle:IsObjectType("Texture") then return false end + + return _G[name.."Button"] ~= nil +end + +-- depth is the depth of the children being visited, counting the container's +-- own children as 1. Three is enough for the deepest real case: the Controls +-- panel's anonymous "Looting Options" group box and the compact raid frame +-- profile dialogs both hold their widgets a level below the panel. +local function Sweep(frame, depth) + for i = 1, frame:GetNumChildren() do + local child = select(i, frame:GetChildren()) + + if child and not addonPanels[child] then + local objectType = child:GetObjectType() + + if objectType == "CheckButton" then + S:HandleCheckBox(child) + elseif objectType == "Slider" then + -- The gate is load-bearing, not an optimisation: + -- HandleSliderFrame has no idempotence guard of its own and + -- HookScript stacks, so a second pass over a slider ElvUI + -- already skinned would run its OnDisable/OnEnable handlers + -- twice. E:SetTemplate sets frame.template, so that field is + -- the "already skinned" marker for sliders. + if not child.template then + S:HandleSliderFrame(child) + end + elseif objectType == "Frame" and IsDropDown(child) then + S:HandleDropDownBox(child) + elseif depth < MAX_DEPTH then + -- Deliberately no Button branch: nothing in the sweep needs one, + -- and it would flatten other addons' panel tabs -- object type + -- Button, but tabs -- into ElvUI pill buttons. + Sweep(child, depth + 1) + end + end + end +end + +local function SweepOptions() + RebuildAddOnPanels() + + for _, name in ipairs(ROOTS) do + local root = _G[name] + + if root then + Sweep(root, 1) + end + end +end + +function CoA:InitializeInterfaceOptions() + -- The same two switches that gate ElvUI's own pass: if the user turned the + -- stock options skin off, the gap this fills isn't a gap any more. + if not E.private.skins.blizzard.enable then return end + if not E.private.skins.blizzard.BlizzardOptions then return end + + -- Skinning on show rather than at init puts the sweep after ElvUI's + -- Skin_BlizzardOptions callback with no ordering work, and re-running it on + -- every show catches panels that only appear once their addon loads. + for _, name in ipairs(TRIGGERS) do + local frame = _G[name] + + if frame then + frame:HookScript("OnShow", SweepOptions) + end + end +end diff --git a/README.md b/README.md index e629a5c..00854b0 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ ElvUI plugin that hides and skins the custom CoA (3.3.5) frames not covered by s - Class resource frames: hiding - Extra action button frame: skin, anchor, size - Instance swap frame: skin, anchor, font settings +- Interface/Video/Audio options: skin the widgets ElvUI's own pass misses - Only highlight unitframes if dispellable by your CoA class. ## Installation diff --git a/core.lua b/core.lua index e88f96e..2e4a8ef 100644 --- a/core.lua +++ b/core.lua @@ -26,6 +26,7 @@ local defaults = { vanityScale = 1, wardrobeScale = 1, }, + interfaceOptions = true, }, extraActionButtonSize = 52, instanceButtonFont = "PT Sans Narrow", @@ -274,6 +275,29 @@ local function getOptions() }, }, }, + interfaceOptions = { + order = 5, + type = "group", + name = "Interface Options", + args = { + header = { + order = 1, + type = "header", + name = "Interface Options", + }, + enable = { + order = 2, + type = "toggle", + name = "Enable", + desc = "Skin the options the CoA client adds to the stock Interface, Video and Audio windows, which ElvUI's own pass doesn't cover. Requires a UI reload.", + get = function() return CoA.db.profile.skins.interfaceOptions end, + set = function(_, value) + CoA.db.profile.skins.interfaceOptions = value + E:StaticPopup_Show("CONFIG_RL") + end, + }, + }, + }, }, }, classResources = { @@ -476,6 +500,10 @@ function CoA:Initialize() self:InitializeWardrobeFrame() end end + + if self.InitializeInterfaceOptions and skins.interfaceOptions then + self:InitializeInterfaceOptions() + end end local function InitializeCallback()