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