Compare commits

...

9 Commits

Author SHA1 Message Date
Narcasung 964f88559d Add a Talents skin entry with an enable switch and per-window scale
The talent window, the Vanity and Wardrobe windows it switches between and
the tab row along its bottom are one feature to the player, so they get one
entry in the Skins tree and one master switch gating all three
Initialize calls together. Skinning one of them without the others reads as
a bug.

Scale is a slider per window, 0.5 to 1.5, applied as a multiplier on the
existing scale rather than an absolute -- the three don't ship at the same
size (the vanity store's frame runs about 9% larger than the others), and
that is the server's choice to keep. A setting of 1.0 leaves everything
exactly as it was.

The scale is set on Collections, not on the window itself. All three
windows and the tab row are its children, and it is the only one of them
with mouse enabled -- it is what the player drags. Scaling a window
directly left the drag target at full size, so a shrunk window had to be
grabbed by clicking outside itself, and the tab row, not being a child of
any window, kept whichever size the first-opened one gave it. Scaling the
container moves its hit area with it and the tabs follow as children.
2026-08-17 22:37:53 +02:00
Narcasung 766e561839 Share one skinning path across the three CoA frames
The talent, vanity and wardrobe frames are built from the same handful of
widget templates, but each module carried its own copy of the handling:
close buttons, panels, dropdown pills, tabs and the ADDON_LOADED loader
were all written out three times. The copies had drifted -- tab backdrop
insets of 3 against 2, tabs grown in one frame and not the other, the
wardrobe clearing only three of the six tab textures -- and that drift is
what reads in-game as the same control looking different depending on which
tab you're on.

Modules/Skinning.lua now owns each of those behaviours once, and the three
modules call into it. Where the copies disagreed the values are unified;
tab growth is gated on the tab actually having a label, since growing an
icon-only tab just pushes its icon off the layout, and the SetChecked hook
is guarded because hooksecurefunc errors outright on a missing method.

Titles are pinned to Arial Narrow at 13: the vanity store titled in Friz
Quadrata while the other two used Arial Narrow. Frame scale is left as the
server set it, so the vanity title still renders slightly larger than the
others.

Two things deliberately not shared: the vanity frame still skips
StripTextures, since its currency counters are regions of the frame itself,
and the talent frame's bottom-bar dropdowns stay on their own path -- they
are a different widget, a native dropdown plus a separate caret button.
2026-08-17 22:19:50 +02:00
Narcasung 083cbcbf9c Group frame skins under their own settings tab
The CoA options were a flat row of tabs, one per feature, which doesn't
survive the number of frame skins now landing. Skins get their own tab,
first in the row, laid out as a vertical tree so each skinned frame is one
entry rather than one more horizontal tab. Extra Action Button and Instance
Swap move under it.

Each skin also gets a master enable toggle. Skinning is one-way -- the
native art is stripped and replaced in place -- so turning one off can only
take effect on the next load, hence the reload prompt and the gate in
CoA:Initialize rather than an attempt to undo the skin live. Existing users
see no change: both default to on.

LayerPicker's UIDropDownMenu_AddButton wrapper is hooked at file scope,
outside that gate, so it checks the toggle itself. That one does respect
the switch immediately.
2026-08-17 22:19:39 +02:00
Narcasung dc263a2419 Skin the CoA Wardrobe tab (AppearanceWardrobeFrame)
Outer panel, close button, and the top-left portrait medallion get the
same first-pass treatment as Vanity/Talent, plus the item collection
panel: native background/border art stripped from Collection and its
InsetOverlay/ShadowOverlay decoration, the 3D model preview's vanilla
border removed, and the action buttons, search box, Filter/Order By
dropdowns, page arrows, and category tabs skinned to match.

Left native for a later pass: the icon grid contents, item slot
buttons, and character model itself.
2026-08-17 21:54:30 +02:00
Narcasung 7862871e3f Kill first-render flicker on Talent/Vanity Blizzard-frame skins
The 0.5s ScheduleRepeatingTimer poll that caught these frames couldn't
catch them before their first-ever :Show() this session, since they're
created on-demand the instant the player opens them -- native art got
one frame of paint before the poll's next tick could skin it.

Switched to a generic ADDON_LOADED listener instead: it fires
synchronously (before control returns to whatever opens the frame)
the moment the owning addon finishes loading, so skinning now lands
before the frame is ever shown. Confirmed in-game via an ADDON_LOADED
probe on the talent frame's addon before implementing.

Also fixes a regression this introduced: TalentFrame's CropBackground
was caching the background texture's "original" tex coords on its
first-ever call for later crop math. That first call now happens
pre-Show, before Blizzard's own code sets the texture's real art-tile
region, so it was permanently baking in the XML-template default
(full 0..1) as the baseline. Moved the crop to run only from OnShow,
where the real coords are already set.
2026-08-17 21:24:42 +02:00
Narcasung 62a6a2cc57 Restyle CoA talent frame tabs to match ElvUI's flat tab rows
Bigger font and plate, tighter gap between tabs, taller tabs for real
vertical padding around the label. Selection now relies on Blizzard's
own white-text-on-checked behaviour instead of an ElvUI border/fill
swap, matching the Friends/Character tab rows elsewhere in ElvUI.

Growth and frame-level bump are self-healing from the tabs' OnUpdate
hook rather than one-shot: the tab's native height isn't settled yet
when it's first skinned, and something outside our control resets the
frame level on a plain tab switch, so both need to keep correcting
themselves rather than being set once behind a guard.
2026-08-17 20:59:11 +02:00
Narcasung b14753491c Skin the CoA Vanity tab (StoreCollectionFrame)
Skins the outer window, close button, search box, Filter dropdown
(and its arrow and popup panel), Purchase/Deliver buttons, and the
grid pager arrows. Removes nothing else -- the portrait originally
suspected at the top-left corner turned out not to exist as a
separate element, and the card grid/preview panel are left native.
2026-08-17 20:17:25 +02:00
Narcasung 2736cc0065 Give each character its own settings profile
AceDB:New was passed true as defaultProfile, which the library resolves to the
literal profile name "Default", so profile selection fell back to a single
shared profile for every character instead of to the character key. Changing a
setting on one character changed it everywhere.

Dropping the argument restores AceDB's per-character fallback. Characters
already sitting on the shared profile are moved to their own on next login,
with the shared settings copied across so nothing is lost.
2026-08-17 17:20:26 +02:00
Narcasung 0b9b0c5afa Skin the CoA talent frame's Character Advancement tab
Flattens the custom talent window into the ElvUI look: frame chrome, close
buttons, tabs, bottom bar, dropdowns and their popups, the list scrollbars,
and the spec choice cards' action buttons. The talent tree itself is left
alone -- its node borders encode rank and availability.

Most of the work is in keeping the skin applied. The frames restore their
native art on refresh in three different ways, and none of them fire an
event to hook:

- The scroll arrows re-show a region StripTextures had hidden, so the arrow
  buttons re-hide it from their own update, matching by file so ElvUI's
  panel pieces (which are regions of the same button on this client) survive.
- The pooled menu rows put their hover and selection art back when a scroll
  refills them, without firing OnShow, so the strip runs from the row update
  behind a cheap texture compare.
- The talent frame's close button keeps its absolute frame level while
  TreeView rides along when the frame is raised, so the level bump re-applies
  on every show.

Row fills and the selection edge are textures on the row rather than an ElvUI
backdrop: the backdrop is a child frame and covered the row's icon and text at
every frame level available to it.
2026-08-17 17:15:29 +02:00
7 changed files with 1598 additions and 99 deletions
+4
View File
@@ -14,3 +14,7 @@ Modules\ExtraActionBar.lua
Modules\LayerPicker.lua Modules\LayerPicker.lua
Modules\DispelHighlight.lua Modules\DispelHighlight.lua
Modules\ClassResources.lua Modules\ClassResources.lua
Modules\Skinning.lua
Modules\TalentFrame.lua
Modules\VanityFrame.lua
Modules\WardrobeFrame.lua
+6 -1
View File
@@ -23,10 +23,15 @@ function CoA:UpdateInstanceButtonFont()
UpdateFont() UpdateFont()
end end
-- Hooked at file scope, so unlike the rest of the skin it stays live even when
-- the skin is off -- check the toggle here instead. Reset Position only makes
-- sense while the frame is where the server put it; once we've handed it to a
-- mover the entry does nothing useful.
do do
local orig_AddButton = UIDropDownMenu_AddButton local orig_AddButton = UIDropDownMenu_AddButton
UIDropDownMenu_AddButton = function(info, level) UIDropDownMenu_AddButton = function(info, level)
if info and info.text == "Reset Position" and UIDROPDOWNMENU_INIT_MENU == LayerPickerFrameDropDown then if CoA.db and CoA.db.profile.skins.instanceSwap
and info and info.text == "Reset Position" and UIDROPDOWNMENU_INIT_MENU == LayerPickerFrameDropDown then
return return
end end
+405
View File
@@ -0,0 +1,405 @@
local E, L, V, P, G = unpack(ElvUI)
local S = E:GetModule("Skins")
local CoA = E:GetModule("CoA")
-- Shared skinning helpers for the CoA frames (talent, vanity, wardrobe).
--
-- Those three are built from the same handful of widget templates, so every
-- element that appears in more than one of them lives here rather than being
-- copied per module. The copies had already drifted -- different backdrop
-- insets, different close button sizes, tabs grown in one frame and not the
-- other -- and the drift is exactly what reads as "the same control looks
-- different depending on which tab I'm on".
local Skin = {}
CoA.Skin = Skin
-- Every native close button on these frames is a different size (the vanity
-- store's is visibly larger than the talent frame's), and HandleCloseButton
-- keeps whatever size it's given -- it only centres a fixed 12px X inside the
-- box. Normalised here so the X lands on the same grid in all three, and the
-- click target is the same everywhere too.
local CLOSE_BUTTON_SIZE = 32
function Skin:CloseButton(close)
if not close then return end
-- Strictly once: HandleCloseButton strips the button on every call, which
-- blanks the X texture it added on the first pass, and its own guard won't
-- rebuild it because the field still points at that blanked texture.
if not close.CoASkinned then
close.CoASkinned = true
S:HandleCloseButton(close)
end
close:Size(CLOSE_BUTTON_SIZE)
end
-- Scale is a per-frame setting rather than one shared number: the three frames
-- don't ship at the same scale (the vanity store's is about 9% larger than the
-- other two, measured), and that's the server's choice, so the slider is a
-- multiplier on whatever each frame was given rather than an absolute. A
-- setting of 1 therefore leaves every frame exactly as it came.
--
-- Scale is applied to Collections, the container all three windows and the tab
-- row are children of (confirmed by probe), rather than to a window itself.
-- Collections is also the only one of them with mouse enabled -- it's what the
-- player drags -- so scaling a window directly shrank the art while leaving the
-- drag target at full size, which is why a shrunk window had to be grabbed by
-- clicking outside itself. Scaling the container moves its hit area with it,
-- and the tabs come along as its children.
--
-- The setting is still per-window: only one of them is ever open, and each
-- re-applies its own on show. The native scale is captured before anything here
-- has written one, so re-applying can't compound.
local CONTAINER_NAME = "Collections"
local scaleKey
local function GetScaleSetting(key)
local db = CoA.db and CoA.db.profile.skins.talentFrames
return (db and db[key]) or 1
end
function Skin:ApplyWindowScale(key)
local container = _G[CONTAINER_NAME]
if not container then return end
if not container.CoANativeScale then
container.CoANativeScale = container:GetScale()
end
-- Remembered so the slider can re-apply for whichever window is open.
scaleKey = key
local scale = container.CoANativeScale * GetScaleSetting(key)
if container:GetScale() ~= scale then
container:SetScale(scale)
end
end
-- Live update from the slider: a window only re-runs its own skin pass on show,
-- and a scale change should land while one is open.
function CoA:UpdateFrameScales()
if scaleKey then
Skin:ApplyWindowScale(scaleKey)
end
end
-- The talent and wardrobe frames title in Arial Narrow, the vanity store in
-- Friz Quadrata (measured live). Arial Narrow is the one that matches the rest
-- of the layout, so it's pinned here rather than left to the frames -- and
-- pinned by name through LSM rather than as a raw path, so it stays the same
-- asset ElvUI itself would resolve.
--
-- Size is written straight through, with no correction for the frames running
-- at different scales: the vanity store's is about 9% larger than the other
-- two (measured), so its title renders slightly bigger than theirs. Left as the
-- server has it for now.
--
-- FontTemplate stashes the font and size it was handed on the fontstring, so an
-- ElvUI font change later re-applies these rather than resetting them.
local TITLE_FONT = "Arial Narrow"
local TITLE_SIZE = 13
function Skin:Title(title)
if not title then return end
title:FontTemplate(E.Libs.LSM:Fetch("font", TITLE_FONT), TITLE_SIZE, "OUTLINE")
end
-- Strip first, THEN template: SetTemplate adds its own backdrop as texture
-- regions on this same frame, so stripping afterwards wipes it straight back
-- off -- which is how the wardrobe's outer panel once came out fully invisible.
--
-- keepTextures is for frames whose own regions aren't all decorative: the
-- vanity store draws its currency counters as regions of the frame itself, and
-- a blind strip blanks the counters along with the panel art.
function Skin:Panel(frame, keepTextures)
if not frame or frame.CoAPanelSkinned then return end
frame.CoAPanelSkinned = true
if not keepTextures then
frame:StripTextures()
end
frame:SetTemplate("Transparent")
end
-- Ornate art with no flat equivalent (portrait medallions, nine-slice borders,
-- shadow overlays): it goes rather than getting reskinned. Hidden as well as
-- stripped, since some of these put their own art back.
function Skin:HideArt(frame)
if not frame then return end
frame:StripTextures()
frame:Hide()
end
-- The dropdown pills on the vanity and wardrobe frames are the same widget:
-- nine anonymous "Silver-Button" slices rather than the named Left/Middle/Right
-- fields or the Normal/Pushed/Disabled set S:HandleButton knows how to clear,
-- so its own clearing can't reach them -- and a blind StripTextures would take
-- the caret and the label with them.
--
-- The native mouse-down handler re-arts one of these regions with a pressed
-- variant of the same file on every click, which is why the pill came back
-- skinless while held. SetTexture is noop'd per region after clearing.
local DROPDOWN_ART = "Silver%-Button"
local CARET_ART = "ChatFrameExpandArrow"
local CARET_SIZE = 14
local function StripDropdownArt(dropdown)
for i = 1, dropdown:GetNumRegions() do
local region = select(i, dropdown:GetRegions())
local texture = region.GetTexture and region:GetTexture()
if texture and tostring(texture):find(DROPDOWN_ART) then
region:SetTexture(nil)
region.SetTexture = E.noop
end
end
end
-- The caret is retextured after HandleButton, not before: HandleButton strips
-- the pill, and a caret replaced ahead of that gets cleared straight back off.
local function SkinDropdownCaret(dropdown)
for i = 1, dropdown:GetNumRegions() do
local region = select(i, dropdown:GetRegions())
local texture = region.GetTexture and region:GetTexture()
if texture and tostring(texture):find(CARET_ART) then
-- The caret is a plain OVERLAY texture on the dropdown itself rather
-- than a separate button, so it can't go through
-- HandleNextPrevButton and is retextured directly. No ArrowDown
-- asset exists -- every other direction in ElvUI is ArrowUp rotated.
region:SetTexture(E.Media.Textures.ArrowUp)
region:SetVertexColor(1, 1, 1)
region:SetTexCoord(0, 1, 0, 1)
region:SetRotation(S.ArrowRotation.down)
region:SetSize(CARET_SIZE, CARET_SIZE)
end
end
end
-- menu is the popout list this dropdown owns; it gets the panel only, matching
-- how the talent frame's own popup menus are treated. The option rows inside
-- are a later pass.
function Skin:Dropdown(dropdown, menu)
if not dropdown or dropdown.CoASkinned then return end
dropdown.CoASkinned = true
StripDropdownArt(dropdown)
S:HandleButton(dropdown)
SkinDropdownCaret(dropdown)
self:Panel(menu)
end
-- Tabs -----------------------------------------------------------------------
--
-- S:HandleTab can't be used on any of these: it clears the tab body by name,
-- looking for a "Middle" piece, and every CoA tab names theirs "Center", so the
-- body survives and the ElvUI backdrop just lands behind the old art.
--
-- One list covers both tab flavours: the talent frame's tabs carry the
-- Disabled variants as well, the wardrobe's category tabs don't, and the
-- lookups for the ones that don't exist simply come back nil.
local TAB_TEXTURES = {"Left", "Center", "Right", "LeftDisabled", "CenterDisabled", "RightDisabled"}
-- Blizzard's own tab code re-sets these textures both when a frame reopens and,
-- separately, on every tab switch -- and a switch never fires the owning
-- frame's OnShow, only SetChecked. So this has to be idempotent and re-run from
-- everywhere rather than skinned once behind a guard.
function Skin:StripTabArt(tab)
local name = tab:GetName()
if name then
for _, suffix in ipairs(TAB_TEXTURES) do
local tex = _G[name..suffix]
if tex then tex:SetTexture(nil) end
end
end
local highlight = tab.GetHighlightTexture and tab:GetHighlightTexture()
if highlight then highlight:SetTexture(nil) end
local checked = tab.GetCheckedTexture and tab:GetCheckedTexture()
if checked then checked:SetTexture(nil) end
end
local TAB_BACKDROP_INSET = 3
-- Native tab sizing is a retail leftover -- every other ElvUI tab row in this
-- client reads bigger -- so labelled tabs are grown, off the font's own current
-- size rather than a hardcoded number, and stay in step with the user's font
-- settings. Icon-only tabs (the wardrobe's category strip) have no label to
-- grow around and keep their native size; growing them would only push the
-- icons off their own layout.
local TAB_GROWTH = 8
local TAB_FONT_GROWTH = 3
local TAB_LABEL_OFFSET = 12
-- Grown height self-heals: on a plain /reload a tab's native height isn't
-- settled yet at skin time (Blizzard lays it out asynchronously), so a one-shot
-- SetHeight caught a stale value and left a shorter tab until something let the
-- native layout finish.
--
-- Compared by equality rather than "did it shrink": the late layout can land on
-- a height larger than the stale one this grew from, and a shrink-only check
-- would then see current > target and never regrow. GetHeight doesn't read back
-- byte-exact after SetHeight either -- UI scale rounds it to a slightly
-- different float -- so a strict ~= compare never held and this grew every tick
-- without bound. Half a pixel of slack absorbs the rounding while still
-- catching a genuine Blizzard-driven change, which is always a full tab's worth
-- of height.
local TAB_HEIGHT_EPSILON = 0.5
local function UpdateTabSize(tab)
if not tab.CoAGrowTab then return end
local height = tab:GetHeight()
if not tab.CoAGrownHeight or math.abs(height - tab.CoAGrownHeight) > TAB_HEIGHT_EPSILON then
tab.CoAGrownHeight = height + TAB_GROWTH
tab:SetHeight(tab.CoAGrownHeight)
end
end
-- Tabs that aren't children of the frame they belong to (the talent frame's
-- are separately-placed siblings) sit behind its panel art once grown, so they
-- get bumped above it. Re-applied every tick rather than on show or on select:
-- bumping from those two alone still left them behind the panel, so whatever
-- resets their level isn't either of those events.
local function BumpTabLevel(tab)
local parent = tab.CoALevelParent
if not parent then return end
tab:SetFrameStrata(parent:GetFrameStrata())
tab:SetFrameLevel(parent:GetFrameLevel() + 20)
end
-- Only the tab being switched to fires anything at all -- the ones switched
-- away from never fire OnClick/OnShow/SetChecked again, yet their native art
-- still comes back, so there's no event to catch it from. Checked from OnUpdate
-- instead: a cheap GetTexture() compare that only pays for the full strip when
-- the art has actually reappeared.
local function UpdateTabArt(tab)
BumpTabLevel(tab)
UpdateTabSize(tab)
local name = tab:GetName()
local tex = name and _G[name.."Left"]
if tex and tex:GetTexture() then
Skin:StripTabArt(tab)
end
end
-- No fill or border swap marks the open tab: the native tab code already turns
-- the label white on the checked one and leaves the rest their normal colour,
-- same as the Friends/Character tab rows. Only the art strip has to re-run.
local function OnTabChecked(tab)
Skin:StripTabArt(tab)
end
-- The tab row along the bottom of the talent window is one set of buttons
-- shared by all three windows it switches between, not one row per window,
-- which is why it lives here rather than in any single frame's module. It
-- doesn't need scaling of its own -- it's a child of Collections, so it follows
-- whatever ApplyWindowScale sets.
local COLLECTION_TAB = "CollectionsPoolFrameCollectionTabTemplate%d"
local MAX_COLLECTION_TABS = 10
function Skin:CollectionTabs(owner)
for i = 1, MAX_COLLECTION_TABS do
local tab = _G[COLLECTION_TAB:format(i)]
if not tab then break end
self:Tab(tab, owner)
end
end
-- levelParent is the frame the tab must draw above, and is only needed for tabs
-- that aren't its children (see BumpTabLevel). Pass nil for tabs parented to
-- the frame they belong to -- normal parent/child z-order already covers those.
function Skin:Tab(tab, levelParent)
if not tab then return end
-- Set on every call rather than only the first: the collection tabs are
-- shared, so the frame they have to draw above is whichever window is open.
if levelParent then
tab.CoALevelParent = levelParent
end
if not tab.CoASkinned then
tab.CoASkinned = true
self:StripTabArt(tab)
-- Default rather than Transparent: tabs sit below their frame over the
-- open world, so a see-through panel reads as washed out instead of as
-- the solid tabs the retail layout has.
--
-- The backdrop's own level is left alone. CreateBackdrop keeps it level
-- with the tab, and regions render fine on top of a same-level child;
-- forcing it a level above the tab reproduces the
-- child-frame-covers-its-parent's-own-regions quirk (see StripRowArt in
-- TalentFrame), which blanks the label.
tab:CreateBackdrop("Default")
tab.backdrop:Point("TOPLEFT", TAB_BACKDROP_INSET, -TAB_BACKDROP_INSET)
tab.backdrop:Point("BOTTOMRIGHT", -TAB_BACKDROP_INSET, TAB_BACKDROP_INSET)
tab:SetHitRectInsets(TAB_BACKDROP_INSET, TAB_BACKDROP_INSET, TAB_BACKDROP_INSET, TAB_BACKDROP_INSET)
local fontString = tab.GetFontString and tab:GetFontString()
if fontString then
tab.CoAGrowTab = true
local font, size, flags = fontString:GetFont()
if font then
fontString:SetFont(font, size + TAB_FONT_GROWTH, flags)
end
-- The label's native anchor sits right off the icon, sized for the
-- smaller native font, so grown text collides with the icon. Nudged
-- off whatever point the native layout gave it rather than a
-- hardcoded anchor that would fight that layout.
local point, relTo, relPoint, x, y = fontString:GetPoint(1)
if point then
fontString:SetPoint(point, relTo, relPoint, x + TAB_LABEL_OFFSET, y)
end
end
-- Guarded: the talent frame's tabs are CheckButtons, the wardrobe's
-- category tabs aren't guaranteed to be, and hooksecurefunc errors
-- outright on a method that doesn't exist.
if tab.SetChecked then
hooksecurefunc(tab, "SetChecked", OnTabChecked)
end
tab:HookScript("OnUpdate", UpdateTabArt)
end
self:StripTabArt(tab)
BumpTabLevel(tab)
end
-- These frames are created on-demand by their owning addon, the instant the
-- player first opens one -- a poll can't catch that before the native art gets
-- a paint. ADDON_LOADED fires (synchronously, before control returns to
-- whatever code calls :Show()) the moment that addon finishes loading, so
-- skinning from it lands before the first-ever :Show(), killing the one-frame
-- flicker a poll-based catch can't avoid. Confirmed in-game.
function Skin:OnFrameAvailable(tryHook)
if tryHook() then return end
local loader = CreateFrame("Frame")
loader:RegisterEvent("ADDON_LOADED")
loader:SetScript("OnEvent", function(self)
if tryHook() then
self:UnregisterEvent("ADDON_LOADED")
end
end)
end
+690
View File
@@ -0,0 +1,690 @@
local E, L, V, P, G = unpack(ElvUI)
local S = E:GetModule("Skins")
local CoA = E:GetModule("CoA")
local Skin = CoA.Skin
local FRAME_NAME = "CoATalentFrame"
-- The talent tree is the one part of the frame we must not touch: every node
-- is an icon button whose border/overlay textures encode rank and
-- availability, so ElvUI's generic button handling would flatten the
-- information out of them. The nodes and connectors live in pools under
-- SpecTree, with the scene art in FXFrame, so those subtrees are skipped by
-- name. TreeView itself is walked, because the bottom bar is inside it.
local EXCLUDED = {"PoolFrame", "SpecTree", "ClassTree", "FXFrame", "ShadowOverlay"}
local function IsTreeSubtree(name)
if not name then return false end
for _, pattern in ipairs(EXCLUDED) do
if name:find(pattern) then return true end
end
return false
end
local function IsDropDown(frame, name)
return name ~= nil and _G[name.."Button"] ~= nil and _G[name.."Text"] ~= nil
end
-- The bottom bar's widgets aren't at a fixed depth (some sit directly on the
-- frame, some are nested one or two containers deep), and their names aren't
-- documented anywhere, so skinning is driven by walking the frame instead of
-- by a hardcoded name list. Depth is capped so a container we didn't expect
-- can't drag us down into the tree's pooled widgets.
local MAX_DEPTH = 5
local SkinChildren
function SkinChildren(frame, depth)
if depth > MAX_DEPTH then return end
for i = 1, frame:GetNumChildren() do
local child = select(i, frame:GetChildren())
local name = child.GetName and child:GetName()
if child.GetObjectType and not IsTreeSubtree(name) then
local objType = child:GetObjectType()
if objType == "EditBox" then
S:HandleEditBox(child)
elseif objType == "Button" then
-- Close buttons are handled explicitly -- the frame's in
-- SkinFrame, where it needs an anchor and a frame level the walk
-- can't supply, and each menu's in SkinBottomBar. Matched on
-- "Close" rather than "CloseButton": the menus name theirs just
-- "...MenuClose", and it was coming out of the walk as an
-- ordinary templated square with the X stripped off it.
if not (name and name:find("Close")) then
S:HandleButton(child)
end
elseif objType == "Frame" then
if IsDropDown(child, name) then
S:HandleDropDownBox(child)
else
SkinChildren(child, depth + 1)
end
end
end
end
end
-- TreeView is a sibling drawn above the close button's own frame level, so
-- without the bump the ElvUI close texture ends up behind the tree panel and
-- the corner just looks empty. Anchored explicitly because the native corner
-- position was set relative to the NineSlice art we hide.
local function SkinCloseButton(frame)
local close = _G[FRAME_NAME.."CloseButton"]
if not close then return end
Skin:CloseButton(close)
-- Re-applied on every pass, not once at skin time. The X is drawn on the
-- close button's own frame, so it needs to outrank TreeView, and TreeView
-- rides along whenever the talent frame is raised while the close button,
-- being a sibling, keeps whatever absolute level it was given -- which is
-- how the X ends up buried behind the tree panel with the button still
-- clickable.
close:SetFrameStrata(frame:GetFrameStrata())
close:SetFrameLevel(frame:GetFrameLevel() + 20)
-- HandleCloseButton centres a 12px X inside the button's native 32px box,
-- so anchoring the box to the frame corner drops the X well below the
-- title. Centring the button on the title's own vertical midpoint puts the
-- X on the title line whatever height the bar turns out to be.
local title = _G[FRAME_NAME.."TitleText"]
local top = frame:GetTop()
local titleY = title and select(2, title:GetCenter())
close:ClearAllPoints()
if top and titleY then
close:Point("CENTER", frame, "TOPRIGHT", -16, titleY - top)
else
close:Point("TOPRIGHT", frame, "TOPRIGHT", -4, -4)
end
end
local BOTTOM_BAR = FRAME_NAME.."TreeViewBottomBar"
local BOTTOM_BAR_DROPDOWNS = {"SpecDropDown", "BuildDropDown"}
-- The two menus don't name their list alike: the spec menu's inset and scroll
-- frame hang off "List", the build creator's off "BuildList". Assuming they
-- shared the layout is why nothing on the build creator's scrollbar was ever
-- being found.
local BOTTOM_BAR_MENUS = {
{menu = "SpecializationMenu", list = "List"},
{menu = "BuildCreatorMenu", list = "BuildList"}
}
-- Rows are pooled, so this walks by index until it runs out rather than
-- tracking how many the list currently holds.
local MENU_ROW_LIMIT = 50
-- Each row's icon sits in a rounded empty-slot ring, which reads as a raised
-- bevel against flat panels. The ring goes and the icon gets the usual ElvUI
-- treatment: cropped edges and a backdrop sized to it.
local function SkinMenuRowIcon(iconFrame)
if not iconFrame or iconFrame.CoASkinned then return end
iconFrame.CoASkinned = true
for i = 1, iconFrame:GetNumRegions() do
local region = select(i, iconFrame:GetRegions())
local texture = region.GetTexture and region:GetTexture()
if texture and texture:find("EmptySlot") then
region:SetTexture(nil)
end
end
local name = iconFrame:GetName()
local icon = name and _G[name..".Icon"]
if icon then
S:HandleIcon(icon, iconFrame)
end
end
local ROW_BORDER_EDGES = {"TOP", "BOTTOM", "LEFT", "RIGHT"}
-- The active row is edged rather than filled, and the edge is four textures on
-- the row for the same reason the fill is a texture: a backdrop frame is a
-- child, so it either covers the row's art or is covered by the neighbouring
-- rows, which overlap far enough that only its bottom line came through.
local function CreateRowBorder(row)
local border = {}
for _, edge in ipairs(ROW_BORDER_EDGES) do
local line = row:CreateTexture(nil, "OVERLAY")
line:Hide()
border[edge] = line
end
border.TOP:SetPoint("TOPLEFT", row, "TOPLEFT")
border.TOP:SetPoint("TOPRIGHT", row, "TOPRIGHT")
border.TOP:SetHeight(E.mult)
border.BOTTOM:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT")
border.BOTTOM:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT")
border.BOTTOM:SetHeight(E.mult)
border.LEFT:SetPoint("TOPLEFT", row, "TOPLEFT")
border.LEFT:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT")
border.LEFT:SetWidth(E.mult)
border.RIGHT:SetPoint("TOPRIGHT", row, "TOPRIGHT")
border.RIGHT:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT")
border.RIGHT:SetWidth(E.mult)
row.CoABorder = border
end
-- Selection is read off the native overlay rather than tracked here: the row's
-- own code decides which entry is active and there's no event for it, so the
-- overlay is left in place as the signal and only its art is cleared.
local function UpdateRowSelection(row)
local border = row.CoABorder
if not border then return end
local overlay = row.CoASelected
-- Alpha as well as visibility, because it isn't known which of the two the
-- row uses to turn the overlay on.
local selected = overlay ~= nil and overlay:IsShown() and overlay:GetAlpha() > 0
if selected == row.CoASelectionShown then return end
row.CoASelectionShown = selected
local r, g, b = unpack(E.media.rgbvaluecolor)
for _, edge in ipairs(ROW_BORDER_EDGES) do
local line = border[edge]
line:SetTexture(r, g, b, 1)
if selected then line:Show() else line:Hide() end
end
end
-- The row's frame is a rounded plate from the PvP queue art, with a matching
-- background behind it. Both come off in favour of a flat ElvUI panel, and the
-- hover and selection plates have to go with them -- they're cut to the same
-- rounded shape, so they read as bevels once the plate underneath is flat.
-- Hover becomes the flat white wash ElvUI puts on list rows; selection moves
-- onto the 1px edge from CreateRowBorder, which is how the active entry is
-- marked everywhere else in ElvUI.
--
-- Hidden as well as cleared, and re-run from both the row's show and its
-- update: a single pass didn't hold, the rows put their art back as they're
-- refilled from the pool.
local function StripRowArt(row)
local name = row:GetName()
local border = name and _G[name..".Border"]
if border then
border:SetTexture(nil)
border:Hide()
end
-- ".H" is the row's highlight texture, so retexturing it is enough to
-- replace the hover state; there's no separate object to chase.
local hover = name and _G[name..".H"]
if hover and hover.SetTexture then
hover:SetTexture(1, 1, 1, 0.15)
hover:ClearAllPoints()
hover:SetInside(row)
row.CoAHover = hover
end
local selected = name and _G[name..".Selected"]
if selected and selected.SetTexture then
selected:SetTexture(nil)
row.CoASelected = selected
end
for i = 1, row:GetNumRegions() do
local region = select(i, row:GetRegions())
local texture = region.GetTexture and region:GetTexture()
if texture and texture:find("GuildFrame") then
region:SetTexture(nil)
region:Hide()
end
end
end
-- Scrolling refills the pooled rows and puts their native hover and selection
-- art back, and a pooled row doesn't fire OnShow when it's refilled, so there's
-- no event to strip on -- which is why the glow only ever appeared after a
-- scroll. The art is checked from the row's update instead. A colour-set
-- texture reads back as "SolidTexture", so the check stays a string compare
-- rather than a blind re-skin every frame.
local function UpdateRowArt(row)
local hover, selected = row.CoAHover, row.CoASelected
if (hover and hover:GetTexture() ~= "SolidTexture")
or (selected and selected:GetTexture() ~= nil) then
StripRowArt(row)
end
end
local function UpdateRow(row)
UpdateRowArt(row)
UpdateRowSelection(row)
end
local SkinMenuRow
function SkinMenuRow(row)
if not row then return end
if not row.CoASkinned then
row.CoASkinned = true
-- The fill is painted on the row itself rather than through
-- CreateBackdrop. The backdrop is a child frame, and here it covered
-- the row's name, icon and status text at every frame level tried,
-- including one below the row's own. A texture on the row's BACKGROUND
-- layer is ordered against those regions inside a single frame instead,
-- so it can't outrank them. Opaque rather than the transparent template,
-- since the menu panel behind is already see-through and a second
-- see-through layer left the rows reading as holes onto the talent scene.
local background = row:CreateTexture(nil, "BACKGROUND")
background:SetInside(row)
background:SetTexture(unpack(E.media.backdropcolor))
CreateRowBorder(row)
-- Nooped so a refill can't swap the highlight for a fresh texture
-- object, which would leave the retextured one orphaned and the check in
-- UpdateRowArt reading a state nothing draws from any more.
row.SetHighlightTexture = E.noop
row:HookScript("OnShow", SkinMenuRow)
-- No event fires when the active spec changes or when a scroll refills
-- the row, so both are resynced from the row's update. The comparisons
-- in UpdateRowArt and UpdateRowSelection make every tick that changes
-- nothing a no-op.
row:HookScript("OnUpdate", UpdateRow)
end
StripRowArt(row)
UpdateRowSelection(row)
end
-- These children are named with a literal dot ("Button1.SpecIcon"), so they
-- only come out of _G by string key, never as plain identifiers.
local function SkinMenuRows(listName)
for i = 1, MENU_ROW_LIMIT do
local rowName = listName.."ScrollFrameButton"..i
local row = _G[rowName]
if not row then break end
SkinMenuRow(row)
SkinMenuRowIcon(_G[rowName..".SpecIcon"])
end
end
-- S:HandleScrollBar can't be used on these: it assumes the thumb is a texture
-- and calls SetTexture on it, but this thumb is a button, so the call errors
-- and takes the rest of the OnShow skinning down with it.
--
-- It's a proportional scrollbar: the thumb is drawn as three slices that the
-- widget re-applies whenever it resizes, so stripping them doesn't hold. The
-- slices are made transparent instead and a value-coloured panel is stretched
-- from the first to the last, which is how ElvUI handles the same widget in
-- HandleProportionalScroll. Slices are looked up as fields and as globals,
-- since only the global names are confirmed here.
local function SkinScrollThumb(thumb, thumbName)
if not thumb or thumb.backdrop then return end
local first = thumb.Begin or _G[thumbName.."Begin"]
local last = thumb.End or _G[thumbName.."End"]
local middle = thumb.Middle or _G[thumbName.."Middle"]
if first then first:SetAlpha(0) end
if last then last:SetAlpha(0) end
if middle then middle:SetAlpha(0) end
local r, g, b = unpack(E.media.rgbvaluecolor)
thumb:CreateBackdrop("Transparent")
thumb.backdrop:SetFrameLevel(thumb:GetFrameLevel() + 1)
thumb.backdrop:SetBackdropColor(r, g, b, 0.25)
if first and last then
thumb.backdrop:Point("TOPLEFT", first)
thumb.backdrop:Point("BOTTOMRIGHT", last)
end
thumb:HookScript("OnEnter", function(self)
if self.backdrop then self.backdrop:SetBackdropColor(r, g, b, 0.75) end
end)
thumb:HookScript("OnLeave", function(self)
if self.backdrop then self.backdrop:SetBackdropColor(r, g, b, 0.25) end
end)
end
-- The scroll arrows come back as Blizzard chevrons after the list refreshes,
-- and it isn't the ElvUI arrow being overwritten -- that texture is still in
-- place underneath. The button's native art is a region StripTextures hid, and
-- the scroll frame shows it again whenever it recalculates.
--
-- Matched by file rather than as "any region that isn't one of ours": on this
-- client ElvUI's own panel and border pieces are regions of the button too, so
-- hiding everything unrecognised would take the ElvUI square with it.
--
-- Compared case-insensitively: the client hands paths back from GetTexture in
-- whatever case it stored them, not the case they were set in.
local ARROW_TEXTURE = E.Media.Textures.ArrowUp:lower()
local NATIVE_SCROLL_ART = "scrollbar"
local function RestoreArrow(button)
for i = 1, button:GetNumRegions() do
local region = select(i, button:GetRegions())
local texture = region.GetTexture and region:GetTexture()
if texture and texture:lower():find(NATIVE_SCROLL_ART) and region:IsShown() then
region:Hide()
end
end
-- The arrow itself is re-pointed as well, in case a refresh reaches the
-- state textures and not only the art it re-shows.
for _, texture in ipairs(button.CoAArrowTextures) do
local current = texture:GetTexture()
if not current or current:lower() ~= ARROW_TEXTURE then
texture:SetTexture(E.Media.Textures.ArrowUp)
texture:SetInside(button)
texture:SetTexCoord(0, 1, 0, 1)
texture:SetRotation(S.ArrowRotation[button.CoAArrowDirection])
end
end
end
local function SkinArrow(button, direction)
if not button or button.CoASkinned then return end
button.CoASkinned = true
S:HandleNextPrevButton(button, direction)
button.CoAArrowDirection = direction
button.CoAArrowTextures = {
button:GetNormalTexture(),
button:GetPushedTexture(),
button:GetDisabledTexture()
}
-- Still worth closing off: whatever refreshes these would otherwise be free
-- to swap in a fresh texture object and orphan the three above.
button.SetNormalTexture = E.noop
button.SetPushedTexture = E.noop
button.SetDisabledTexture = E.noop
button.SetHighlightTexture = E.noop
button:HookScript("OnUpdate", RestoreArrow)
end
-- The list inside each dropdown popup: a scroll frame with the framed inset
-- and overlay art around it. The arrows are named off the scroll frame rather
-- than off the scrollbar, so they're looked up here instead.
local function SkinMenuScroll(listName)
local scrollBar = _G[listName.."ScrollFrameScrollBar"]
if not scrollBar then return end
local inset = _G[listName.."Inset"]
if inset then inset:StripTextures() end
local overlay = _G[listName.."ScrollFrameArtOverlay"]
if overlay then overlay:StripTextures() end
if scrollBar.backdrop then return end
local frameLevel = scrollBar:GetFrameLevel()
scrollBar:Width(18)
scrollBar:StripTextures()
scrollBar:CreateBackdrop()
scrollBar.backdrop:SetAllPoints()
scrollBar.backdrop:SetFrameLevel(frameLevel)
local up = _G[listName.."ScrollFrameScrollUpButton"]
if up then
up:Point("BOTTOM", scrollBar, "TOP", 0, 1)
SkinArrow(up, "up")
end
local down = _G[listName.."ScrollFrameScrollDownButton"]
if down then
down:Point("TOP", scrollBar, "BOTTOM", 0, -1)
SkinArrow(down, "down")
end
local thumbName = listName.."ScrollFrameScrollBarThumb"
local thumb = (scrollBar.GetThumbTexture and scrollBar:GetThumbTexture()) or _G[thumbName]
SkinScrollThumb(thumb, thumbName)
end
-- The bar's plate art (BottomBarTexture) is the dark band under the buttons,
-- and it's the bar's only region, so a plain StripTextures clears it without
-- touching the scene art, which lives on TreeView one level up. The buttons,
-- dropdowns and search box sitting on the bar are picked up by the walk.
local function SkinBottomBar()
local bar = _G[BOTTOM_BAR]
if not bar then return end
bar:StripTextures()
-- Each dropdown's caret is a child button carrying the arrow as its own
-- art, so the walk was treating it as an ordinary button and giving it a
-- templated square. Skinned here, ahead of the walk, so it becomes the
-- ElvUI chevron; HandleNextPrevButton's isSkinned flag then makes the walk
-- leave it alone. noBackdrop keeps it a bare white arrow that takes the
-- value colour on hover, and "up" matches the way these menus open.
for _, suffix in ipairs(BOTTOM_BAR_DROPDOWNS) do
local dropdown = _G[BOTTOM_BAR..suffix]
local arrow = _G[BOTTOM_BAR..suffix.."Button"]
if arrow then
S:HandleNextPrevButton(arrow, "up", nil, true)
arrow:Size(20, 20)
end
-- These buttons are wider than they look: the label art only ever
-- covered the part up to the arrow, so templating the whole frame drew
-- a panel running under the icon buttons to their right. Skinned here
-- rather than in the walk so the backdrop can be built separately and
-- stopped at the arrow, and the dead space to its right is taken out
-- of the hit rect so it can't swallow clicks meant for those icons.
if dropdown and arrow then
S:HandleButton(dropdown, nil, nil, true)
if dropdown.backdrop then
dropdown.backdrop:ClearAllPoints()
dropdown.backdrop:Point("TOPLEFT", dropdown, "TOPLEFT", -1, 1)
dropdown.backdrop:Point("BOTTOMRIGHT", arrow, "BOTTOMRIGHT", 3, -1)
end
local dropdownRight, arrowRight = dropdown:GetRight(), arrow:GetRight()
if dropdownRight and arrowRight then
dropdown:SetHitRectInsets(0, math.max(0, dropdownRight - arrowRight - 3), 0, 0)
end
end
end
-- The two dropdown popups are plain frames, which the walk descends into
-- for their buttons but never templates, so they get their panel here.
for _, entry in ipairs(BOTTOM_BAR_MENUS) do
local menuName = BOTTOM_BAR..entry.menu
local listName = menuName..entry.list
local menu = _G[menuName]
if menu and not menu.CoASkinned then
menu.CoASkinned = true
Skin:Panel(menu)
Skin:CloseButton(_G[menuName.."Close"])
-- Rows don't exist until the menu is first opened, which happens
-- long after the talent frame's own OnShow, so they're picked up
-- on the menu's.
menu:HookScript("OnShow", function()
SkinMenuRows(listName)
end)
end
SkinMenuScroll(listName)
SkinMenuRows(listName)
end
end
-- The scene art belongs to TreeView, which runs on behind the bottom bar, so
-- clearing the bar's own plate left the art showing through under the
-- buttons. The pieces are stretched over the whole of TreeView and show their
-- slice of the file through tex coords, which occupy only part of it -- the
-- rest of the file holds other art. So the coords have to be trimmed in
-- proportion to their existing values rather than replaced outright, or
-- unrelated regions of the file scroll into view.
--
-- Geometry comes from TreeView and the bar rather than from the texture's own
-- rect, so that a re-run reads the same numbers instead of measuring a rect
-- it already cropped. Coords are kept from the first pass for the same reason.
local function CropTexture(tex, bar, keep)
if not tex.CoAOriginalCoords then
tex.CoAOriginalCoords = {tex:GetTexCoord()}
end
-- GetTexCoord yields the four corners: UL, LL, UR, LR. These are all
-- axis-aligned, so the edges come off the corners that define them.
local coords = tex.CoAOriginalCoords
local left, top, bottom, right = coords[1], coords[2], coords[4], coords[5]
tex:SetTexCoord(left, right, top, top + (bottom - top) * keep)
tex:Point("BOTTOMRIGHT", bar, "TOPRIGHT", 0, 0)
end
local function CropBackground()
local treeView = _G[FRAME_NAME.."TreeView"]
local bar = _G[BOTTOM_BAR]
if not (treeView and bar) then return end
local treeTop, treeBottom, barTop = treeView:GetTop(), treeView:GetBottom(), bar:GetTop()
if not (treeTop and treeBottom and barTop) then return end
local height = treeTop - treeBottom
if height <= 0 then return end
local keep = (treeTop - barTop) / height
if keep <= 0 or keep >= 1 then return end
-- Scoped to TreeView's own regions: earlier this looked the names up as
-- globals over a fixed range, which pulled in same-named textures owned by
-- other frames and re-anchored those too.
for i = 1, treeView:GetNumRegions() do
local region = select(i, treeView:GetRegions())
local name = region.GetName and region:GetName()
if name and name:find("Background") and region:GetObjectType() == "Texture" then
CropTexture(region, bar, keep)
end
end
end
-- The tab row is shared with the vanity and wardrobe windows and is handled in
-- Skinning.lua. The talent frame is passed as its owner while this window is
-- the open one: the tabs aren't its children, so they don't draw above its
-- panel on their own.
local function SkinTabs()
Skin:CollectionTabs(_G[FRAME_NAME])
end
local SPEC_CHOICE = FRAME_NAME.."SpecViewPoolFrameCoASpecChoiceTemplate%d"
local MAX_SPEC_CHOICES = 10
-- Only each card's action button. The cards themselves keep their art: the
-- portraits and the gold wash on the active spec are the whole point of the
-- view, and there's nothing flat that would say the same thing.
--
-- Skinned by name because the walk can't get here -- the cards hang off a
-- PoolFrame, which EXCLUDED skips so the tree's pooled nodes stay untouched.
-- Stripped as well as templated: the red plate isn't one of the Left/Middle/
-- Right pieces HandleButton clears on its own.
local function SkinSpecChoices()
for i = 1, MAX_SPEC_CHOICES do
local cardName = SPEC_CHOICE:format(i)
if not _G[cardName] then break end
S:HandleButton(_G[cardName.."SelectButton"], true)
end
end
local function SkinFrame(frame)
if not frame.CoASkinned then
frame.CoASkinned = true
Skin:HideArt(_G[FRAME_NAME.."NineSlice"])
-- The round portrait medallion overhangs the top-left corner and has
-- no flat equivalent, so it goes rather than getting reskinned.
Skin:HideArt(_G[FRAME_NAME.."PortraitFrame"])
Skin:Panel(frame)
-- The choice cards are pulled from the pool as the view opens, so the
-- talent frame's own show is too early to catch them. Run on the view's
-- show, and again a tick later in case the pool is filled after it.
local specView = _G[FRAME_NAME.."SpecView"]
if specView then
specView:HookScript("OnShow", function()
SkinSpecChoices()
E:Delay(0.1, SkinSpecChoices)
end)
end
-- Children are created lazily as tabs are visited, so the walk has to
-- run again on every show rather than once at hook time. The isSkinned
-- / backdrop guards inside ElvUI's handlers make re-runs cheap.
frame:HookScript("OnShow", function(self)
Skin:ApplyWindowScale("talentScale")
Skin:Title(_G[FRAME_NAME.."TitleText"])
SkinCloseButton(self)
SkinBottomBar()
CropBackground()
SkinChildren(self, 1)
SkinTabs()
SkinSpecChoices()
end)
end
Skin:ApplyWindowScale("talentScale")
Skin:Title(_G[FRAME_NAME.."TitleText"])
SkinCloseButton(frame)
SkinBottomBar()
-- Not CropBackground() here: this runs pre-Show now (see InitializeTalentFrame),
-- before Blizzard's own code sets the background texture's real art-tile
-- coords -- it's still XML-template default (full 0..1) at this point. Crop
-- caches "original" coords on first call and never recaptures, so cropping
-- here would permanently bake in the wrong baseline. OnShow (below) is the
-- only place this is safe to run.
SkinChildren(frame, 1)
SkinTabs()
SkinSpecChoices()
end
local function TryHook()
local frame = _G[FRAME_NAME]
if not frame then return false end
SkinFrame(frame)
return true
end
function CoA:InitializeTalentFrame()
if not E.private.skins.blizzard.enable then return end
Skin:OnFrameAvailable(TryHook)
end
+98
View File
@@ -0,0 +1,98 @@
local E, L, V, P, G = unpack(ElvUI)
local S = E:GetModule("Skins")
local CoA = E:GetModule("CoA")
local Skin = CoA.Skin
local FRAME_NAME = "StoreCollectionFrame"
-- Confirmed live by probing size/position: these two OVERLAY "Portrait2"
-- regions sit at the currency counters (aligned with SPCounterHintButton and
-- DPCounterHintButton), not at the top-left corner. An earlier version of
-- this matched them as the portrait and stripped them, which blanked the
-- counter badges instead. Whatever the real top-left portrait is hasn't been
-- identified yet -- it isn't among StoreCollectionFrame's own regions or
-- children -- so nothing removes it for now.
-- Standard UIPanelButtonTemplate art (FontString + Normal/Pushed/Disabled/
-- Highlight textures), confirmed by probe -- S:HandleButton's own texture
-- clearing handles these directly, no manual stripping needed.
local function SkinActionButtons()
S:HandleButton(_G[FRAME_NAME.."ActivateStoreButton"])
S:HandleButton(_G[FRAME_NAME.."BuyStoreButton"])
end
local function SkinSearchBox()
S:HandleEditBox(_G[FRAME_NAME.."SearchBox"])
end
-- The filter pill and its popout are the shared dropdown widget; see
-- Skinning.lua for why S:HandleButton alone can't clear it.
local function SkinDropdown()
Skin:Dropdown(_G[FRAME_NAME.."Dropdown"], _G[FRAME_NAME.."DropdownMenu"])
end
-- Neither pager button is named (both are anonymous children of
-- CollectionList), so direction is worked out from their own anchor offset
-- rather than a name match, and re-checked on every pass instead of cached --
-- cheap, and self-corrects if the two ever come back in a different order.
local function SkinPagerArrows()
local list = _G[FRAME_NAME.."CollectionList"]
if not list then return end
local a, b = select(3, list:GetChildren()), select(4, list:GetChildren())
if not (a and b and a:GetObjectType() == "Button" and b:GetObjectType() == "Button") then return end
local _, _, _, ax = a:GetPoint()
local _, _, _, bx = b:GetPoint()
local prevButton, nextButton = a, b
if bx and ax and bx < ax then
prevButton, nextButton = b, a
end
S:HandleNextPrevButton(prevButton, "left")
S:HandleNextPrevButton(nextButton, "right")
end
local function SkinContents()
-- Scale is set on the shared container while this window is the open one,
-- which carries the tab row along with it (see Skinning.lua).
Skin:ApplyWindowScale("vanityScale")
Skin:CollectionTabs(_G[FRAME_NAME])
Skin:Title(_G[FRAME_NAME.."TitleText"])
Skin:CloseButton(_G[FRAME_NAME.."CloseButton"])
SkinActionButtons()
SkinSearchBox()
SkinDropdown()
SkinPagerArrows()
end
local function SkinFrame(frame)
if not frame.CoASkinned then
frame.CoASkinned = true
-- Templated without stripping, unlike the talent and wardrobe frames:
-- the currency counters are drawn as regions of the frame itself (see
-- the note at the top of this file), so a strip blanks them.
Skin:Panel(frame, true)
frame:HookScript("OnShow", SkinContents)
end
SkinContents()
end
local function TryHook()
local frame = _G[FRAME_NAME]
if not frame then return false end
SkinFrame(frame)
return true
end
function CoA:InitializeVanityFrame()
if not E.private.skins.blizzard.enable then return end
Skin:OnFrameAvailable(TryHook)
end
+149
View File
@@ -0,0 +1,149 @@
local E, L, V, P, G = unpack(ElvUI)
local S = E:GetModule("Skins")
local CoA = E:GetModule("CoA")
local Skin = CoA.Skin
local FRAME_NAME = "AppearanceWardrobeFrame"
-- Standard UIPanelButtonTemplate art, same as Vanity's action buttons --
-- S:HandleButton's own texture clearing handles these directly.
local function SkinActionButtons()
S:HandleButton(_G[FRAME_NAME.."PlayerModelSaveOutfitButton"])
S:HandleButton(_G[FRAME_NAME.."DisableTransmogButton"])
S:HandleButton(_G[FRAME_NAME.."DisableSpellVisualsButton"])
end
local function SkinSearchBox()
S:HandleEditBox(_G[FRAME_NAME.."CollectionSearchBox"])
end
-- suffix is "Filter" or "Sorting" -- both are CollectionX buttons with a
-- matching CollectionXMenu popout (confirmed by probe), and both are the same
-- widget as the vanity frame's single dropdown, so both go through the shared
-- handler.
local function SkinCollectionDropdown(suffix)
Skin:Dropdown(_G[FRAME_NAME.."Collection"..suffix], _G[FRAME_NAME.."Collection"..suffix.."Menu"])
end
local function SkinPagerArrows()
local prevButton = _G[FRAME_NAME.."CollectionPageLeftButton"]
local nextButton = _G[FRAME_NAME.."CollectionPageRightButton"]
if prevButton then S:HandleNextPrevButton(prevButton, "left") end
if nextButton then S:HandleNextPrevButton(nextButton, "right") end
end
-- Same art naming as the talent frame's tabs (confirmed by probe), so they go
-- through the same shared handler. No level parent is passed: these are proper
-- descendants of Collection rather than separately-placed siblings, so normal
-- parent/child z-order already puts them above the panel and the frame-level
-- bump the talent tabs need doesn't apply.
--
-- Named PoolFrameAppearanceTypeTabTemplate1 through 8 (confirmed by probe),
-- not pooled/created dynamically like the talent frame's spec choices, so a
-- plain indexed loop is enough -- no OnShow hook needed to catch late pool
-- fills.
local TAB_COUNT = 8
local function SkinCategoryTabs()
for i = 1, TAB_COUNT do
Skin:Tab(_G[FRAME_NAME.."CollectionPoolFrameAppearanceTypeTabTemplate"..i])
end
end
local function SkinContents()
-- Scale is set on the shared container while this window is the open one,
-- which carries the tab row along with it (see Skinning.lua).
Skin:ApplyWindowScale("wardrobeScale")
Skin:CollectionTabs(_G[FRAME_NAME])
Skin:Title(_G[FRAME_NAME.."TitleText"])
Skin:CloseButton(_G[FRAME_NAME.."CloseButton"])
SkinActionButtons()
SkinSearchBox()
SkinCollectionDropdown("Filter")
SkinCollectionDropdown("Sorting")
SkinPagerArrows()
SkinCategoryTabs()
end
local function SkinFrame(frame)
if not frame.CoASkinned then
frame.CoASkinned = true
-- The native NineSlice panel draws its own ornate border/background on
-- top of the Transparent template's border, doubling up. Same fix as
-- the talent frame: strip and hide it.
Skin:HideArt(_G[FRAME_NAME.."NineSlice"])
-- The round medallion overhangs the top-left corner (confirmed by
-- probe: 61x61, TOPLEFT -6,8 -- matches the frame's own corner, not a
-- counter badge elsewhere, unlike Vanity's same-named "Portrait2"
-- texture). No flat equivalent, so it goes rather than getting
-- reskinned, same treatment as the talent frame's portrait.
Skin:HideArt(_G[FRAME_NAME.."PortraitFrame"])
-- The frame's own regions (confirmed by probe: 4 total, all BACKGROUND/
-- BORDER/OVERLAY textures, none of them functional -- unlike Vanity's
-- frame, nothing here doubles as a counter badge) are the wood-panel
-- background art, so this one is safe to strip before templating.
Skin:Panel(frame)
-- InsetOverlay's NineSlice and the ShadowOverlay are separate decorative
-- art layered over the item grid area (8-piece atlas borders/shadow
-- edges, confirmed via probe), stripped outright -- no functional
-- content lives on either.
Skin:HideArt(_G[FRAME_NAME.."CollectionInsetOverlayNineSlice"])
-- The actual panel behind the grid: Collection itself (confirmed via
-- /fstack -- InsetOverlay was the wrong target, its own rect doesn't
-- match the visible panel). Collection is the grid's real parent, so
-- no frame-level/strata juggling needed -- children always draw above
-- their own parent.
--
-- 16 of its own regions turned out to hold real art -- a background
-- tile plus ~14 atlas border/corner pieces (the ornate corners
-- /fstack couldn't ever pick out, since loose regions aren't frames
-- and don't show up there) -- none of it functional (the "Collected
-- 50/2744" counter is a separate FontString region, untouched by
-- StripTextures). Painting an opaque/red template over it earlier
-- just masked it; it was still there underneath, which is why
-- Transparent let it bleed back through. Strip first, then template,
-- same order as the outer frame -- SetTemplate's own WHITE8X8 backdrop
-- pieces land as regions on this same frame too, so stripping after
-- would wipe them right back off.
Skin:Panel(_G[FRAME_NAME.."Collection"])
-- The 3D model preview's vanilla border is an anonymous child (first
-- of PlayerModel's own, confirmed by probe: 8-piece "UIFrame" atlas
-- border, sized to match the model panel). No name to key off of, same
-- as Vanity's pager arrows -- picked out positionally instead. The
-- race-specific scenic backdrop texture is PlayerModel's own region,
-- not this child's, so it's untouched.
local playerModel = _G[FRAME_NAME.."PlayerModel"]
local modelBorder = playerModel and select(1, playerModel:GetChildren())
if modelBorder and modelBorder:GetObjectType() == "Frame" then
Skin:HideArt(modelBorder)
end
Skin:HideArt(_G[FRAME_NAME.."CollectionShadowOverlay"])
frame:HookScript("OnShow", SkinContents)
end
SkinContents()
end
local function TryHook()
local frame = _G[FRAME_NAME]
if not frame then return false end
SkinFrame(frame)
return true
end
function CoA:InitializeWardrobeFrame()
if not E.private.skins.blizzard.enable then return end
Skin:OnFrameAvailable(TryHook)
end
+246 -98
View File
@@ -8,11 +8,25 @@ local AddOnName = ...
BINDING_HEADER_COA = "Conquest of Azeroth" BINDING_HEADER_COA = "Conquest of Azeroth"
-- TODO: hook up server restart frame (RestartTimerFrame)
local CoA = E:NewModule("CoA", "AceEvent-3.0", "AceTimer-3.0") local CoA = E:NewModule("CoA", "AceEvent-3.0", "AceTimer-3.0")
E.CoA = CoA E.CoA = CoA
local defaults = { local defaults = {
profile = { profile = {
skins = {
extraActionButton = true,
instanceSwap = true,
talentFrames = {
enable = true,
-- Multipliers on each frame's own scale, so 1 is "as the server
-- built it" rather than a fixed size.
talentScale = 1,
vanityScale = 1,
wardrobeScale = 1,
},
},
extraActionButtonSize = 52, extraActionButtonSize = 52,
instanceButtonFont = "PT Sans Narrow", instanceButtonFont = "PT Sans Narrow",
instanceButtonFontSize = 12, instanceButtonFontSize = 12,
@@ -33,19 +47,56 @@ function CoA:RefreshConfig()
if self.UpdateInstanceButtonFont then self:UpdateInstanceButtonFont() end if self.UpdateInstanceButtonFont then self:UpdateInstanceButtonFont() end
if self.UpdateDispelHighlight then self:UpdateDispelHighlight() end if self.UpdateDispelHighlight then self:UpdateDispelHighlight() end
if self.UpdateClassResourceVisibility then self:UpdateClassResourceVisibility() end if self.UpdateClassResourceVisibility then self:UpdateClassResourceVisibility() end
if self.UpdateFrameScales then self:UpdateFrameScales() end
end end
CoA:RegisterEvent("ADDON_LOADED", function(_, addon) CoA:RegisterEvent("ADDON_LOADED", function(_, addon)
if addon ~= AddOnName then return end if addon ~= AddOnName then return end
CoA:UnregisterEvent("ADDON_LOADED") CoA:UnregisterEvent("ADDON_LOADED")
CoA.db = AceDB:New("ElvUI_CoADB", defaults, true) 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, "OnProfileChanged", "RefreshConfig")
CoA.db.RegisterCallback(CoA, "OnProfileCopied", "RefreshConfig") CoA.db.RegisterCallback(CoA, "OnProfileCopied", "RefreshConfig")
CoA.db.RegisterCallback(CoA, "OnProfileReset", "RefreshConfig") CoA.db.RegisterCallback(CoA, "OnProfileReset", "RefreshConfig")
end) 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.talentFrames[key] end,
set = function(_, value)
CoA.db.profile.skins.talentFrames[key] = value
if CoA.UpdateFrameScales then
CoA:UpdateFrameScales()
end
end,
}
end
local function getOptions() local function getOptions()
local profiles = AceDBOptions:GetOptionsTable(CoA.db) local profiles = AceDBOptions:GetOptionsTable(CoA.db)
profiles.order = 5 profiles.order = 5
@@ -56,9 +107,179 @@ local function getOptions()
childGroups = "tab", childGroups = "tab",
name = string.format("|cff1784d1%s|r", "Conquest of Azeroth"), name = string.format("|cff1784d1%s|r", "Conquest of Azeroth"),
args = { args = {
classResources = { -- 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, order = 1,
type = "group", 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.
talents = {
order = 3,
type = "group",
name = "Talents",
args = {
header = {
order = 1,
type = "header",
name = "Talents",
},
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.talentFrames.enable end,
set = function(_, value)
CoA.db.profile.skins.talentFrames.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.talentFrames.enable end,
args = {
talentScale = scaleOption(1, "Talents", "talentScale"),
vanityScale = scaleOption(2, "Vanity", "vanityScale"),
wardrobeScale = scaleOption(3, "Wardrobe", "wardrobeScale"),
},
},
},
},
},
},
classResources = {
order = 2,
type = "group",
name = "Class Resources", name = "Class Resources",
args = { args = {
header = { header = {
@@ -159,101 +380,8 @@ local function getOptions()
}, },
}, },
}, },
extraActionButton = {
order = 2,
type = "group",
name = "Extra Action Button",
args = {
header = {
order = 1,
type = "header",
name = "Extra Action Button",
},
desc = {
order = 2,
type = "description",
name = "You can move this element with Toggle Anchors.\n",
},
size = {
order = 3,
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 = 3,
type = "group",
name = "Instance Swap",
args = {
header = {
order = 1,
type = "header",
name = "Instance Swap",
},
desc = {
order = 2,
type = "description",
name = "You can move this element with Toggle Anchors.\n",
},
instanceFont = {
order = 3,
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),
},
},
},
},
dispelHighlight = { dispelHighlight = {
order = 4, order = 3,
type = "group", type = "group",
name = "Debuff Highlighting", name = "Debuff Highlighting",
args = { args = {
@@ -349,11 +477,15 @@ function CoA:Initialize()
EP:RegisterPlugin(AddOnName, getOptions) EP:RegisterPlugin(AddOnName, getOptions)
if self.InitializeExtraActionBar then -- 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() self:InitializeExtraActionBar()
end end
if self.InitializeLayerPicker then if self.InitializeLayerPicker and skins.instanceSwap then
self:InitializeLayerPicker() self:InitializeLayerPicker()
end end
@@ -364,6 +496,22 @@ function CoA:Initialize()
if self.InitializeClassResources then if self.InitializeClassResources then
self:InitializeClassResources() self:InitializeClassResources()
end 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.talentFrames.enable then
if self.InitializeTalentFrame then
self:InitializeTalentFrame()
end
if self.InitializeVanityFrame then
self:InitializeVanityFrame()
end
if self.InitializeWardrobeFrame then
self:InitializeWardrobeFrame()
end
end
end end
local function InitializeCallback() local function InitializeCallback()