Files
ElvUI_CoA/core.lua
T
Narcasung a8541fa1ee Skin the Challenges window, its tab row and the Trials tab
ChallengesFrame is a HIGH-strata window parented straight to UIParent and
mouse-enabled itself, not one of the three that hang off the Collections
container -- so it gets its own module and its own enable switch rather
than joining the collections group, and it deliberately skips
ApplyWindowScale and CollectionTabs: the first scales Collections, which
would resize the other three windows and not this one, and the second
skins the tab row those three share.

The panel is built as a backdrop instead of through SetTemplate. A
template's backdrop is drawn as regions of the frame and can only match
its rect, and the title sits above that rect -- the native art carries a
banner up there -- so a panel sized to the frame left the title floating
over the open world. A backdrop has points of its own, so its top edge is
pushed up over the title, measured live rather than hardcoded, with the
hit rect and the close button following it. The nine-slice is stripped
rather than run through HideArt for a related reason: the title is one of
its regions, and Hide() would take it down with the border.

The tab row is the first stock Blizzard flavour the plugin skins, so the
shared layer grows to cover it. TAB_TEXTURES picks up the Middle pieces
those name their body after, and the OnUpdate art re-check watches the
Disabled set as well -- that pair is how a stock tab draws its selected
state, so watching only the inactive half missed art returning on
whichever tab was open. Native panel tabs also interlock, each anchored
16px back into the one before it so the ornate end caps overlap, which
reads as one merged bar once the art is gone; the overlap is absorbed by
narrowing each tab rather than by spacing the row out, which would have
grown it 80px past a frame with 16px of slack. The row is pulled up flush
against the panel it belongs to, since the native tops that spanned that
gap went with the art.

Inside the Trials tab: the search box, the filter dropdown and the scroll
bar. The dropdown is the same widget as the vanity and wardrobe ones and
takes the existing handler unchanged. The search box needs its border
cleared by file, not stripped -- an EditBox draws its caret and selection
highlight as plain textures of its own, and a blind strip leaves a box
you can type in with no cursor in it.

The scroll bar needed a handler of its own. HandleScrollBar reaches for
the thumb through GetThumbTexture and then calls SetTexture on it, but
these bars are hand-built: the thumb is a Button with art of its own, and
the arrows are named off the scroll frame rather than off the bar, so it
finds neither. Both pieces are also arted lazily -- at skin time their
textures read back nil, which is why clearing them, noop'ing their
SetTexture and alpha'ing them all failed in turn -- so the art is
suppressed from OnUpdate, the same answer the tab art needed. Colours
follow ElvUI's own proportional scroll bars, and the track is left empty
rather than given a backdrop.
2026-08-22 16:32:37 +02:00

549 lines
16 KiB
Lua

local E, L, V, P, G = unpack(ElvUI)
local EP = E.Libs.EP
local ACH = LibStub("LibAceConfigHelper")
local AceDB = LibStub("AceDB-3.0")
local AceDBOptions = LibStub("AceDBOptions-3.0")
local AddOnName = ...
BINDING_HEADER_COA = "Conquest of Azeroth"
-- TODO: hook up server restart frame (RestartTimerFrame)
local CoA = E:NewModule("CoA", "AceEvent-3.0", "AceTimer-3.0")
E.CoA = CoA
local defaults = {
profile = {
skins = {
extraActionButton = true,
instanceSwap = true,
collections = {
enable = true,
-- Multipliers on each frame's own scale, so 1 is "as the server
-- built it" rather than a fixed size.
advancementScale = 1,
vanityScale = 1,
wardrobeScale = 1,
},
-- Deliberately not inside the collections group: that switch covers
-- the three windows that share the Collections container and its tab
-- row, and this one is a separate window on UIParent with a tab row
-- of its own.
challenges = true,
interfaceOptions = true,
},
extraActionButtonSize = 52,
instanceButtonFont = "PT Sans Narrow",
instanceButtonFontSize = 12,
instanceButtonFontOutline = "OUTLINE",
dispelHighlightOnlyMine = false,
dispelHighlightDetectTalents = true,
hideResourceSegmentBar = false,
hideResourceOrb = false,
hideResourceBar = false,
hideMultiCastActionBar = false,
},
}
function CoA:RefreshConfig()
if self.UpdateExtraActionButtonSize then self:UpdateExtraActionButtonSize() end
if self.UpdateInstanceButtonFont then self:UpdateInstanceButtonFont() end
if self.UpdateDispelHighlight then self:UpdateDispelHighlight() end
if self.UpdateClassResourceVisibility then self:UpdateClassResourceVisibility() end
if self.UpdateFrameScales then self:UpdateFrameScales() end
end
CoA:RegisterEvent("ADDON_LOADED", function(_, addon)
if addon ~= AddOnName then return end
CoA:UnregisterEvent("ADDON_LOADED")
CoA.db = AceDB:New("ElvUI_CoADB", defaults)
-- One-time migration: earlier versions pinned every character to a single
-- shared "Default" profile. Move each character to its own name-based
-- profile (copying over the settings it already had) the first time it logs in.
if not CoA.db.char.migratedSharedProfile then
local charProfile = CoA.db.keys.char
if CoA.db:GetCurrentProfile() == "Default" and charProfile ~= "Default" then
CoA.db:SetProfile(charProfile)
CoA.db:CopyProfile("Default", true)
end
CoA.db.char.migratedSharedProfile = true
end
CoA.db.RegisterCallback(CoA, "OnProfileChanged", "RefreshConfig")
CoA.db.RegisterCallback(CoA, "OnProfileCopied", "RefreshConfig")
CoA.db.RegisterCallback(CoA, "OnProfileReset", "RefreshConfig")
end)
-- One slider per window rather than a single shared one: the three don't ship
-- at the same scale, so a shared value would only line them up by flattening
-- the difference the server built in.
local function scaleOption(order, name, key)
return {
order = order,
type = "range",
name = name,
min = 0.5,
max = 1.5,
step = 0.01,
get = function() return CoA.db.profile.skins.collections[key] end,
set = function(_, value)
CoA.db.profile.skins.collections[key] = value
if CoA.UpdateFrameScales then
CoA:UpdateFrameScales()
end
end,
}
end
local function getOptions()
local profiles = AceDBOptions:GetOptionsTable(CoA.db)
profiles.order = 5
local options = {
order = 55,
type = "group",
childGroups = "tab",
name = string.format("|cff1784d1%s|r", "Conquest of Azeroth"),
args = {
-- Frame skins live under their own tab, laid out as a vertical tree
-- rather than more horizontal tabs: one entry per skinned frame, and
-- there are a lot more of those coming.
skins = {
order = 1,
type = "group",
childGroups = "tree",
name = "Skins",
args = {
extraActionButton = {
order = 1,
type = "group",
name = "Extra Action Button",
args = {
header = {
order = 1,
type = "header",
name = "Extra Action Button",
},
-- Skinning is one-way: the native art is stripped and
-- replaced in place, so turning a skin off can only take
-- effect on the next load. Hence the reload prompt.
enable = {
order = 2,
type = "toggle",
name = "Enable",
desc = "Skin the Extra Action Button. Requires a UI reload.",
get = function() return CoA.db.profile.skins.extraActionButton end,
set = function(_, value)
CoA.db.profile.skins.extraActionButton = value
E:StaticPopup_Show("CONFIG_RL")
end,
},
desc = {
order = 3,
type = "description",
name = "You can move this element with Toggle Anchors.\n",
},
size = {
order = 4,
disabled = function() return not CoA.db.profile.skins.extraActionButton end,
type = "range",
name = "Size",
desc = "Adjust the width/height of the Extra Action Button, in pixels.",
min = 30,
max = 100,
step = 1,
get = function() return CoA.db.profile.extraActionButtonSize end,
set = function(_, value)
CoA.db.profile.extraActionButtonSize = value
if CoA.UpdateExtraActionButtonSize then
CoA:UpdateExtraActionButtonSize()
end
end,
},
},
},
instanceSwap = {
order = 2,
type = "group",
name = "Instance Swap",
args = {
header = {
order = 1,
type = "header",
name = "Instance Swap",
},
enable = {
order = 2,
type = "toggle",
name = "Enable",
desc = "Skin the Instance Swap button. Requires a UI reload.",
get = function() return CoA.db.profile.skins.instanceSwap end,
set = function(_, value)
CoA.db.profile.skins.instanceSwap = value
E:StaticPopup_Show("CONFIG_RL")
end,
},
desc = {
order = 3,
type = "description",
name = "You can move this element with Toggle Anchors.\n",
},
instanceFont = {
order = 4,
disabled = function() return not CoA.db.profile.skins.instanceSwap end,
type = "group",
inline = true,
name = "Instance Font",
args = {
font = ACH:SharedMediaFont("Font", nil, 1, nil,
function() return CoA.db.profile.instanceButtonFont end,
function(_, value)
CoA.db.profile.instanceButtonFont = value
if CoA.UpdateInstanceButtonFont then
CoA:UpdateInstanceButtonFont()
end
end),
fontSize = {
order = 2,
type = "range",
name = "Font Size",
min = 8,
max = 32,
step = 1,
get = function() return CoA.db.profile.instanceButtonFontSize end,
set = function(_, value)
CoA.db.profile.instanceButtonFontSize = value
if CoA.UpdateInstanceButtonFont then
CoA:UpdateInstanceButtonFont()
end
end,
},
fontOutline = ACH:FontFlags("Font Outline", nil, 3, nil,
function() return CoA.db.profile.instanceButtonFontOutline end,
function(_, value)
CoA.db.profile.instanceButtonFontOutline = value
if CoA.UpdateInstanceButtonFont then
CoA:UpdateInstanceButtonFont()
end
end),
},
},
},
},
-- One entry for the whole talent window: its own frame, the
-- tab row along its bottom, and the Vanity and Wardrobe
-- windows those tabs open. They're separate frames but one
-- feature to the player, and they're skinned as a set --
-- named for Collections, the container they all hang off.
collections = {
order = 3,
type = "group",
name = "Advancement/Vanity/Wardrobe",
args = {
header = {
order = 1,
type = "header",
name = "Advancement/Vanity/Wardrobe",
},
enable = {
order = 2,
type = "toggle",
name = "Enable",
desc = "Skin the talent window, its tabs, and the Vanity and Wardrobe windows. Requires a UI reload.",
get = function() return CoA.db.profile.skins.collections.enable end,
set = function(_, value)
CoA.db.profile.skins.collections.enable = value
E:StaticPopup_Show("CONFIG_RL")
end,
},
scale = {
order = 3,
type = "group",
inline = true,
name = "Scale",
disabled = function() return not CoA.db.profile.skins.collections.enable end,
args = {
advancementScale = scaleOption(1, "Advancement", "advancementScale"),
vanityScale = scaleOption(2, "Vanity", "vanityScale"),
wardrobeScale = scaleOption(3, "Wardrobe", "wardrobeScale"),
},
},
},
},
challenges = {
order = 4,
type = "group",
name = "Challenges",
args = {
header = {
order = 1,
type = "header",
name = "Challenges",
},
enable = {
order = 2,
type = "toggle",
name = "Enable",
desc = "Skin the Challenges window and its tabs. Requires a UI reload.",
get = function() return CoA.db.profile.skins.challenges end,
set = function(_, value)
CoA.db.profile.skins.challenges = value
E:StaticPopup_Show("CONFIG_RL")
end,
},
},
},
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 = {
order = 2,
type = "group",
name = "Class Resources",
args = {
header = {
order = 1,
type = "header",
name = "Class Resources",
},
desc = {
order = 2,
type = "description",
name = "You can move these elements with Toggle Anchors.\n",
},
hideResourceSegmentBar = {
order = 3,
type = "toggle",
name = "Hide CoAResourceSegmentBar",
width = "full",
get = function() return CoA.db.profile.hideResourceSegmentBar end,
set = function(_, value)
CoA.db.profile.hideResourceSegmentBar = value
if CoA.UpdateClassResourceVisibility then
CoA:UpdateClassResourceVisibility()
end
end,
},
hideResourceOrb = {
order = 4,
type = "toggle",
name = "Hide CoAResourceOrb",
width = "full",
get = function() return CoA.db.profile.hideResourceOrb end,
set = function(_, value)
CoA.db.profile.hideResourceOrb = value
if CoA.UpdateClassResourceVisibility then
CoA:UpdateClassResourceVisibility()
end
end,
},
hideResourceBar = {
order = 5,
type = "toggle",
name = "Hide CoAResourceBar",
width = "full",
get = function() return CoA.db.profile.hideResourceBar end,
set = function(_, value)
CoA.db.profile.hideResourceBar = value
if CoA.UpdateClassResourceVisibility then
CoA:UpdateClassResourceVisibility()
end
end,
},
hideMultiCastActionBar = {
order = 6,
type = "toggle",
name = "Hide CoAMultiCastActionBarFrame",
width = "full",
get = function() return CoA.db.profile.hideMultiCastActionBar end,
set = function(_, value)
CoA.db.profile.hideMultiCastActionBar = value
if CoA.UpdateClassResourceVisibility then
CoA:UpdateClassResourceVisibility()
end
end,
},
hideAll = {
order = 7,
type = "execute",
name = "Hide All",
func = function()
CoA.db.profile.hideResourceSegmentBar = true
CoA.db.profile.hideResourceOrb = true
CoA.db.profile.hideResourceBar = true
CoA.db.profile.hideMultiCastActionBar = true
if CoA.UpdateClassResourceVisibility then
CoA:UpdateClassResourceVisibility()
end
end,
},
showAll = {
order = 8,
type = "execute",
name = "Show All",
func = function()
CoA.db.profile.hideResourceSegmentBar = false
CoA.db.profile.hideResourceOrb = false
CoA.db.profile.hideResourceBar = false
CoA.db.profile.hideMultiCastActionBar = false
if CoA.UpdateClassResourceVisibility then
CoA:UpdateClassResourceVisibility()
end
end,
},
},
},
dispelHighlight = {
order = 3,
type = "group",
name = "Debuff Highlighting",
args = {
header = {
order = 1,
type = "header",
name = "Debuff Highlighting",
},
onlyMine = {
order = 2,
type = "toggle",
name = "Only Highlight If Dispellable By Me",
desc = "Suppress the debuff highlight on unitframes for debuff types your class cannot dispel.",
get = function() return CoA.db.profile.dispelHighlightOnlyMine end,
set = function(_, value)
CoA.db.profile.dispelHighlightOnlyMine = value
if CoA.UpdateDispelHighlight then
CoA:UpdateDispelHighlight()
end
end,
},
detectTalents = {
order = 3,
type = "toggle",
name = "Detect Talents",
desc = "Read the talents that grant extra dispel types from your current build. Turn this off to fall back to what your class can dispel without any talent.",
disabled = function() return not CoA.db.profile.dispelHighlightOnlyMine end,
get = function() return CoA.db.profile.dispelHighlightDetectTalents end,
set = function(_, value)
CoA.db.profile.dispelHighlightDetectTalents = value
if CoA.UpdateDispelHighlight then
CoA:UpdateDispelHighlight()
end
end,
},
},
},
profiles = profiles,
},
}
E.Options.args.CoA = options
end
local function RegisterMoverCategory()
for _, layout in ipairs(E.ConfigModeLayouts) do
if layout == "COA" then return end
end
tinsert(E.ConfigModeLayouts, "COA")
E.ConfigModeLocalizedStrings.COA = "CoA"
end
function CoA:Initialize()
RegisterMoverCategory()
EP:RegisterPlugin(AddOnName, getOptions)
-- ADDON_LOADED normally beats module init, but fall back to the defaults
-- rather than error out if a skin gets initialized before the DB exists.
local skins = self.db and self.db.profile.skins or defaults.profile.skins
if self.InitializeExtraActionBar and skins.extraActionButton then
self:InitializeExtraActionBar()
end
if self.InitializeLayerPicker and skins.instanceSwap then
self:InitializeLayerPicker()
end
if self.InitializeDispelHighlight then
self:InitializeDispelHighlight()
end
if self.InitializeClassResources then
self:InitializeClassResources()
end
-- One switch for all three: the vanity and wardrobe windows are the talent
-- frame's own tabs, so skinning one without the others reads as a bug.
if skins.collections.enable then
if self.InitializeTalentFrame then
self:InitializeTalentFrame()
end
if self.InitializeVanityFrame then
self:InitializeVanityFrame()
end
if self.InitializeWardrobeFrame then
self:InitializeWardrobeFrame()
end
end
-- Its own switch rather than a place in the group above: the Challenges
-- window isn't reachable from the talent frame's tabs, so turning it off
-- doesn't leave a half-skinned set behind.
if self.InitializeChallengesFrame and skins.challenges then
self:InitializeChallengesFrame()
end
if self.InitializeInterfaceOptions and skins.interfaceOptions then
self:InitializeInterfaceOptions()
end
end
local function InitializeCallback()
CoA:Initialize()
end
E:RegisterModule(CoA:GetName(), InitializeCallback)