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.
This commit is contained in:
2026-08-22 15:57:05 +02:00
parent 13f36e7817
commit 1a62b4ed1f
4 changed files with 172 additions and 0 deletions
+1
View File
@@ -18,3 +18,4 @@ Modules\Skinning.lua
Modules\TalentFrame.lua Modules\TalentFrame.lua
Modules\VanityFrame.lua Modules\VanityFrame.lua
Modules\WardrobeFrame.lua Modules\WardrobeFrame.lua
Modules\InterfaceOptions.lua
+142
View File
@@ -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
+1
View File
@@ -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 - Class resource frames: hiding
- Extra action button frame: skin, anchor, size - Extra action button frame: skin, anchor, size
- Instance swap frame: skin, anchor, font settings - 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. - Only highlight unitframes if dispellable by your CoA class.
## Installation ## Installation
+28
View File
@@ -26,6 +26,7 @@ local defaults = {
vanityScale = 1, vanityScale = 1,
wardrobeScale = 1, wardrobeScale = 1,
}, },
interfaceOptions = true,
}, },
extraActionButtonSize = 52, extraActionButtonSize = 52,
instanceButtonFont = "PT Sans Narrow", 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 = { classResources = {
@@ -476,6 +500,10 @@ function CoA:Initialize()
self:InitializeWardrobeFrame() self:InitializeWardrobeFrame()
end end
end end
if self.InitializeInterfaceOptions and skins.interfaceOptions then
self:InitializeInterfaceOptions()
end
end end
local function InitializeCallback() local function InitializeCallback()