Compare commits

..

17 Commits

Author SHA1 Message Date
Narcasung e66a7a6354 Always skin the options the CoA client adds
That pass only fills the gaps ElvUI's own Blizzard options skin leaves,
so a switch of its own was never a meaningful choice: it follows
E.private.skins.blizzard.enable, the same gate ElvUI checks.
2026-08-22 16:59:59 +02:00
Narcasung 63e179e96e Keep the scroll arrows skinned when the list refreshes
The scroll frame re-shows the button's own native art whenever it
recalculates, and one skinning pass can't hold against it: ElvUI's
HandleNextPrevButton guards on isSkinned, so a second call does nothing.

The talent frame already carried the answer -- re-hide the art by file
from OnUpdate and pin the three state textures -- so it moves into the
shared layer as Skin:ScrollArrow and the Challenges list scrolls take it
too. Routing the talent frame through it also picks up the
disabled-hover fix every other button already has.
2026-08-22 16:59:59 +02:00
Narcasung 013fe665ab Skin the Challenges frame's Gamemodes tab
The Trials shape a third time, and again suffix for suffix: the search
box and list container are named off the tab exactly as Trials names
its own, the list's only region is the same store divider, and the
scroll arrows hang off the scroll frame rather than off the bar. So the
divider constant is shared rather than copied and the three calls go in
unchanged.

The one difference is the filter, which this tab does not have. Its two
children are the search box and the list and neither is a menu, so
there is nothing for a Dropdown call to be handed. That is written into
the comment rather than left implicit: with the same function sitting
three times over in the file, a missing fourth line otherwise reads as
an oversight.

The tab owns no regions of its own at all, so nothing here needs the
keepTextures care the Rewards tab's PageText took. Both children exist
on a cold reload, before the tab has ever been opened, and the counts
are unchanged after a click, so it rides SkinContents with the rest
rather than needing a hook on its own OnShow.

Everything reuses an existing helper, so the shared layer is unchanged.
2026-08-22 16:50:05 +02:00
Narcasung d9476af220 Skin the Challenges frame's Challenges tab
The tab is the Trials tab again in every detail that matters: the same
three children under the same suffixes, the same store divider as its
list container's only region, the same silver-button filter, and its
scroll arrows named off the scroll frame rather than off the bar. So it
takes the same four calls, sharing the divider constant rather than
copying it.

Everything exists before the tab has ever been opened, so it goes
through SkinContents with the rest and needs no hook of its own.
2026-08-22 16:47:04 +02:00
Narcasung cafc3ce28f Skin the Challenges frame's Rewards tab
The tab's twenty-nine children all exist on a cold reload, before it has
ever been opened, so this goes through SkinContents with the rest of the
window rather than needing a hook on the tab's own OnShow -- which a tab
switch would have wanted, since switching tabs fires nothing.

None of the Trials tab's naming carries over. Its search box is
TrialsTabSearch and its filter a FilterDropDown, where these are
StoreTabSearchBox and Filter, so every suffix was confirmed rather than
copied and the divergence is written down for the four tabs still to do.

The Filter is the one widget that had to be read twice. Structurally it
is a plain Button -- no Middle texture, no Button child, no Text
fontstring -- so the test the Interface Options sweep settled on says it
is not a dropdown. By art it plainly is: the same nine Silver-Button
slices and ChatFrameExpandArrow caret the Trials filter wears. Dropdown
matches on the art rather than on the naming, so it takes it unchanged;
the structural test is only ever a shortcut to that question and here it
answers it wrongly.

The grid is a fixed pool of twenty-one cards built up front, not one
built per page, so a single pass covers every card the tab will ever
show and nothing hangs off a page-change hook. The icon crop is the
exception and is re-applied from a hook on the texture's SetTexture: the
store refills the pool in place on every search, filter and page change,
and SetTexture resets a texture's coords, so a one-shot crop survives
exactly until the first page turn and then the native icon borders come
back on every card at once. The slot plate underneath needs no such
watch -- StripArtByFile noops its SetTexture, so a refill that re-arts it
writes nothing.

Cards take HandleButton's useCreateBackdrop path rather than its default
SetTemplate one, which is what makes their padding possible: a template
is drawn as regions of the button and can only ever match its rect, and
the rect is tight -- 140x32 around a 32px icon, so the icon met the top
and bottom edges and the name ran to the right one. A backdrop is a
child frame that can be set outside it. Same reasoning as the window
panel's, one level down. The hover colouring follows either way, since
it resolves button.backdrop before the button itself.

The two headings above the grid are Buttons by type but captions by
function, so their atlas art comes off and nothing goes back: a backdrop
would draw a box around a caption, and the HIGHLIGHT layer would light
it up under a cursor with nothing behind it. They are walked off the
layout rather than named, the set being one per reward vendor.

Left alone deliberately: the Currency button, which carries the trophy
icon and its counter as its only two regions and no frame art at all,
and PageText, which is a region of the tab itself -- the same trap as
the vanity store's counters. Nothing here blind-strips the tab. No
scroll bar is wired either; the grid pages rather than scrolls and has
no scroll frame to hang one off.

Everything reuses an existing helper, so the shared layer is unchanged.
2026-08-22 16:43:00 +02:00
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
Narcasung 1a62b4ed1f Skin the options the CoA client adds to Interface, Video and Audio
ElvUI skins the stock options windows from four hardcoded name lists, so
every widget the client added since upstream last synced stays native --
53 of them here, 39 in four panels upstream never enumerated at all
(Battlenet, RaidEffects, Stereo, Voice). Reported cases were the Auto
Accept Trades checkbox and the Spell Queue Window slider.

Sweep the three panel containers on show and skin by object type instead
of carrying a parallel name list that would go stale the same way.

Panels belonging to other addons are skipped by matching frames against
INTERFACEOPTIONS_ADDONCATEGORIES, rebuilt on every sweep so a
load-on-demand addon that registers late is still caught.

Two guards worth naming. Sliders are gated on frame.template because
HandleSliderFrame has no idempotence check of its own and HookScript
stacks, so a second pass over one ElvUI already skinned would run its
OnDisable/OnEnable handlers twice. Dropdowns are detected structurally,
by a Middle texture plus a Button, because HandleDropDownBox uses that
button unguarded and a false positive is a Lua error rather than a
cosmetic miss.

No Button branch: nothing needs one, and it would flatten other addons'
panel tabs into ElvUI pill buttons.
2026-08-22 15:57:21 +02:00
Narcasung 13f36e7817 Skin the Clear invalid slots button and grey the dead ones
A third button sits on the wardrobe model alongside Apply and Cancel and
appears under the same condition: Clear invalid slots, shown with the warning
that an appearance has not been unlocked yet. It was simply not in the list,
so it kept its native green pill against the two now-flat buttons beside it.

It is drawn from the same atlas as Apply -- the green variant of the gold file
-- so Skin.RedButtonArt already matches it and the existing path covers it
whole; probing its regions after the pass comes back nil for all of them. With
three names the list is read in a loop rather than repeated call by call.

Apply is disabled for as long as an invalid slot is pending, and with the pill
gone nothing said so any more: the ElvUI panel is drawn the same either way,
and the atlas was the only thing carrying the distinction. The label is greyed
instead, the same yellow/grey pair the specialization menu's Activate button
uses. There is no event for the state change -- clearing the invalid slots
enables Apply on the spot -- so it is driven from the button's update and
compared against the cached state, again as Activate does it, and the border
goes with it: Apply comes alive under the very cursor that just clicked Clear
invalid slots, and OnEnter has been and gone by then.
2026-08-18 16:44:09 +02:00
Narcasung 919feed322 Strip the native hover glow from every skinned button
HandleButton clears a button's own Normal/Highlight/Pushed/Disabled textures
and the named Left/Middle/Right pieces, so a native highlight drawn as a plain
region of the button survives it and lights up under the cursor against the
flat backdrop. Clearing the wardrobe Cancel button's by file fixed that one
button and nothing else: probing the HIGHLIGHT layer across everything the
plugin skins turned up Save outfit wearing the same red file, and Disable
transmog and Disable spell visuals wearing the dialog-box glow, none of which
any clear-by-file call site was ever going to reach.

The whole layer goes rather than named files. On these frames it only ever
carries native hover art -- every button probed has exactly one HIGHLIGHT
region, blank on the ones that already looked right, so there is nothing else
there to lose. ElvUI's own hover treatment is untouched: it is the backdrop
border swap HandleButton hooks, and a backdrop is drawn through SetBackdrop
plus two child border frames rather than as a region of the button.

Cleared from Skin:Button, so every call site in the plugin inherits it, and
unguarded so a region added after the first skin pass is caught too. SetTexture
is noop'd per region as StripArtByFile does, or a state change that re-arts the
button would bring the glow straight back.
2026-08-18 16:43:31 +02:00
Narcasung d2e55eb3cb Stop disabled buttons taking the hover border colour
S:HandleButton ends with an unconditional OnEnter/OnLeave pair that swaps
the border to the value colour and back, and neither handler looks at
IsEnabled. A disabled button still fires both scripts on this client, so
the greyed Activate button, Save changes with nothing pending and Purchase
with nothing selected all lit up under the cursor and read as clickable.

HookScript can't be undone, but handlers fire in registration order, so a
hook registered after HandleButton runs last and has the final say on the
colour. Skin:Button wraps HandleButton with that hook, and every call site
in the plugin now goes through it, so the correction is inherited rather
than repeated per module.

The Activate button carries the border from its own update as well:
activating a spec disables it under the very cursor that just clicked it,
and OnEnter has been and gone by then, so nothing else would put the border
back until the pointer moved off.

Skin:IsEnabled hoists the 0/1-to-boolean normalisation this client needs
out of UpdateActivateState, which held the only copy of it.
2026-08-18 16:30:27 +02:00
Narcasung cddfc7e7af Skin the Apply and Cancel buttons on the wardrobe model
Both only exist while a transmog change is pending, which is why every
earlier pass over the frame missed them -- it was always skinned in its idle
state -- so they still wore their native green and red pills against an
otherwise flat window.

They are the same widget as the specialization menu's Activate button: no
Normal/Pushed texture for HandleButton to clear, the pill drawn across plain
regions from an atlas. Two files, not one -- Apply takes the green variant of
128GoldRedButton, Cancel is drawn from 128RedButton, HIGHLIGHT region and all,
which is what left a red glow under the cursor once only the gold file was
being cleared. The shared "RedButton" suffix matches both. Their labels take
ElvUI's yellow rather than carrying the atlas's green/red split over: the two
sit side by side and read apart by their text.

The buttons exist from the start rather than being created on the first
pending change, so the normal skin pass reaches them, but the art is only
there to be cleared once a change is pending -- and nothing fires the frame's
OnShow at that point, since the window is already open. The buttons' own
OnShow catches it instead.

The clear-by-file loop the Activate button and the dropdown pills had a copy
of each now lives in Skinning.lua as Skin:StripArtByFile, with the atlas name
alongside it.
2026-08-18 16:29:12 +02:00
Narcasung 58fae4418a Skin the Activate button in the specialization menu
The one button in the Character Advancement frame the skin never reached:
the Activate button inside an expanded specialization row still wore its
native pill against otherwise flat rows.

It has no Normal/Pushed/Disabled textures for HandleButton to clear -- the
pill is three BACKGROUND regions plus a HIGHLIGHT one, all drawn from the
128GoldRedButton atlas, switching between the green and grey variant by tex
coord with the vertex colour left white throughout. So the art is cleared by
file and the regions' SetTexture noop'd, as the dropdown pills already are,
and the enabled/disabled distinction the atlas was carrying is re-created on
the label: ElvUI's yellow when live, grey when dead.

The expanded content exists from the moment the menu is built rather than
being created when its row is expanded, so it is picked up with the rest of
the row.
2026-08-18 16:15:05 +02:00
Narcasung cc6cd4028b Create movers eagerly so they always show in Toggle Anchors
The Extra Action Button, Instance Swap, and class resource frame
movers only got built once their native frame had already appeared
on screen with a resolvable position -- so if the frame never showed
up in a session (e.g. a class resource bar the current class doesn't
use), Toggle Anchors had nothing to display for it. Movers are now
created up front from a default size/position, and just get resized
and anchored once the real frame shows up. Also fixes each mover's
right-click config shortcut, which pointed at a pre-restructure
options path.
2026-08-18 16:00:49 +02:00
Narcasung 6bf3864891 Refresh the dispel talents when the specialization changes
Switching to another saved specialization never confirms a build, so
CHARACTER_ADVANCEMENT_UPDATE_ENTRIES_RESULT doesn't fire for it and the
cached talent state stayed stale for the rest of the session. That path
only fires PENDING_BUILD_UPDATED and SUGGESTIONS_UPDATED, so listen to
both. PENDING_BUILD_UPDATED also fires on every click inside the talent
UI, so the handler recomputes the cached rank and rebuilds the
unitframes only when the answer actually changed.

Also fall back to matching the talent by name among the learned entries
when the entry ID reports nothing, so an ID gone stale after a server
rebalance doesn't silently switch the feature off.
2026-08-18 15:59:29 +02:00
Narcasung d2005c4725 Detect the dispel talents instead of asking for them
The three talents that grant an extra dispel type were gated behind
manual checkboxes because the talent state looked unreadable on this
server. It isn't: C_CharacterAdvancement.GetTalentRankByID reports a
live rank for the player, entry IDs are shared across every class tree
so querying a foreign one just reports rank 0, and
CHARACTER_ADVANCEMENT_UPDATE_ENTRIES_RESULT fires when a build is
confirmed.

Key the talents to their entry IDs and read them instead. The lookup is
cached, since PostUpdate_DebuffHighlight runs once per aura per frame,
and refreshed on build confirmation plus the existing delayed refresh
after login. Every call is guarded, so a missing API reads as "talent
not taken" and behaves as it did before.

The three checkboxes are gone, replaced by a single Detect Talents
switch that turns the whole lookup off.
2026-08-18 15:41:13 +02:00
Narcasung 76413b6828 update readme for the talent tab implementation 2026-08-18 13:29:23 +02:00
Narcasung 01f1f5c7c3 Centre wardrobe tab labels and name the skin for what it covers
The label nudge in the shared tab handler exists to clear the icon on the
talent window's tabs, where the native anchor sits tight against it and
grown text would collide. The wardrobe's category tabs carry no icon and
centre their label, so the same nudge pushed every one of them off to the
right. It's now applied only to tabs that actually have a visible icon.

The settings entry covers three windows, so it's named for all three:
Advancement/Vanity/Wardrobe. The keys behind it move with it -- the db
table is skins.collections, after Collections, the container the three
windows and the tab row all hang off, and talentScale becomes
advancementScale. Nothing here has been released, so there's no stored
setting to migrate.
2026-08-17 22:41:25 +02:00
13 changed files with 1521 additions and 245 deletions
+2
View File
@@ -18,3 +18,5 @@ Modules\Skinning.lua
Modules\TalentFrame.lua
Modules\VanityFrame.lua
Modules\WardrobeFrame.lua
Modules\ChallengesFrame.lua
Modules\InterfaceOptions.lua
+437
View File
@@ -0,0 +1,437 @@
local E, L, V, P, G = unpack(ElvUI)
local CoA = E:GetModule("CoA")
local Skin = CoA.Skin
local FRAME_NAME = "ChallengesFrame"
-- Unlike the talent/vanity/wardrobe trio, this window is not one of the
-- Collections container's children: it's a HIGH-strata frame parented straight
-- to UIParent, and it's mouse-enabled itself rather than being dragged by a
-- container (probed). So it deliberately doesn't go through
-- Skin:ApplyWindowScale or Skin: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, which this window has no part in. A
-- scale slider for it later would set the scale on the frame itself, and the
-- drag target moves with it because the frame is its own drag target.
-- Stock Blizzard panel tabs, unlike every other tab row the plugin skins: plain
-- Buttons rather than CheckButtons, and their art is named Left/Middle/Right
-- (plus the Disabled set Blizzard swaps in for the selected tab) rather than
-- the server's Left/Center/Right -- both flavours are covered by the shared
-- strip in Skinning.lua. They're real children of the frame, so no level parent
-- is needed: normal parent/child z-order already puts them above its panel.
--
-- Six exist today, two of them gated behind content an account may not have, so
-- the row is walked until the first missing index rather than against a
-- hardcoded count -- same shape as Skin:CollectionTabs.
--
-- This is the one row in the plugin that interlocks: every tab past the first is
-- anchored 16px back into the one before it (probed) so the native end caps
-- overlap, which reads as a single merged bar once the art is gone. Hence the
-- unoverlap flag; the server-authored rows sit apart already and don't take it.
local MAX_TABS = 10
local function SkinTabs()
for i = 1, MAX_TABS do
local tab = _G[FRAME_NAME.."Tab"..i]
if not tab then break end
Skin:Tab(tab, nil, true)
end
end
-- The row hangs 6px clear of the frame's bottom edge (probed), which the native
-- art spanned with the tabs' own ornate tops -- gone with the art, so the row
-- reads as detached from the window it belongs to. Only the first tab carries a
-- vertical offset at all, the rest being anchored off its edge, so moving it
-- moves the row.
--
-- Set to the backdrop inset rather than to zero: the tab's flat backdrop starts
-- that far inside the tab, so a tab flush with the frame still leaves its
-- visible edge floating. Written as an absolute rather than as an adjustment to
-- what's there, which keeps it idempotent across the repeated skin passes.
local function AnchorTabRow()
local tab = _G[FRAME_NAME.."Tab1"]
if not tab then return end
local point, relativeTo, relativePoint, x = tab:GetPoint(1)
if not point then return end
tab:SetPoint(point, relativeTo, relativePoint, x, Skin.TabBackdropInset)
end
-- The nine-slice draws the ornate purple border and the teal inner panel over
-- the top of the frame, doubling up with the Transparent template's own border
-- -- but unlike the talent and wardrobe frames' nine-slices it isn't purely
-- decorative: the window's title is one of its regions (probed: nine BORDER
-- textures, one ARTWORK texture, and the title FontString). So it's stripped
-- rather than run through Skin:HideArt, whose Hide() would take the title down
-- with the border. StripTextures only touches Textures, so the art goes and the
-- FontString stays.
local function StripBorderArt()
local nineSlice = _G[FRAME_NAME.."NineSlice"]
if not nineSlice then return end
nineSlice:StripTextures()
end
-- That title has no name of its own -- there is no ChallengesFrameTitleText,
-- and none of the other usual suffixes resolve either (probed) -- so it's
-- picked out by type, the same way the vanity frame's pager arrows are picked
-- out by position. The nine-slice owns exactly one FontString.
local function GetTitle()
local nineSlice = _G[FRAME_NAME.."NineSlice"]
if not nineSlice then return end
for i = 1, nineSlice:GetNumRegions() do
local region = select(i, nineSlice:GetRegions())
if region:GetObjectType() == "FontString" then return region end
end
end
-- Vertical slack left above the title inside the widened panel.
local TITLE_PADDING = 6
-- The title is drawn above the frame's own top edge -- the native art carries a
-- banner up there that the frame's rect doesn't include -- so a panel sized to
-- the frame leaves the title floating over the open world. The panel is built
-- as a backdrop rather than through SetTemplate for exactly this reason: a
-- template's backdrop is drawn as regions of the frame itself and can only ever
-- match its rect, while a backdrop is a child frame with points of its own, so
-- its top edge can be pushed up to take the title in.
--
-- The distance is measured live rather than hardcoded: it's whatever gap the
-- server's own layout left between the title and the frame, so this stays right
-- if that layout changes. Re-measured on every show for the same reason the tab
-- heights are (see Skinning.lua) -- on a cold /reload the layout isn't settled
-- at skin time, and a one-shot measurement catches a stale value.
--
-- The hit rect is grown to match. The frame is its own drag target (probed), and
-- without this the widened panel would have a band along its top that looks part
-- of the window but can't be grabbed.
local function UpdatePanelTop(frame)
local backdrop = frame.backdrop
local title = GetTitle()
if not (backdrop and title) then return end
local frameTop, titleTop = frame:GetTop(), title:GetTop()
if not (frameTop and titleTop) then return end
local offset = titleTop - frameTop + TITLE_PADDING
if offset < 0 then offset = 0 end
backdrop:Point("TOPLEFT", frame, "TOPLEFT", 0, offset)
backdrop:Point("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)
-- Negative insets grow the hit rect outwards rather than shrinking it.
frame:SetHitRectInsets(0, 0, -offset, 0)
end
-- Per-tab contents. Each of the six tabs owns its own copy of these widgets
-- rather than sharing one set, so they're skinned per tab; Trials, Rewards,
-- Challenges and Gamemodes are done, and the two left follow the same shape.
--
-- Nothing about one tab's naming carries to the next: the Trials search box is
-- "...TrialsTabSearch" and its filter a "...FilterDropDown", where the Rewards
-- pair are "...StoreTabSearchBox" and "...Filter" (probed). Every suffix on a
-- new tab is worth confirming rather than copying.
local TRIALS_TAB = FRAME_NAME.."TrialsTab"
local TRIALS_LIST = TRIALS_TAB.."Challenges"
local TRIALS_SCROLL = TRIALS_LIST.."ScrollFrame"
-- The list container's one and only region: a 2px rule drawn across the bottom
-- of the panel from the store art (probed). It's the native panel's own divider,
-- and with the panel around it flat there's nothing left for it to divide.
local LIST_DIVIDER_ART = "perksactivities"
local function SkinTrialsTab()
Skin:SearchBox(_G[TRIALS_TAB.."Search"])
-- Cleared by file rather than stripped: the container is the scroll frame's
-- own parent, so a blind strip is a blunter instrument than one known region
-- needs, and StripArtByFile's noop holds if the list ever re-arts it.
Skin:StripArtByFile(_G[TRIALS_LIST], LIST_DIVIDER_ART)
-- The same nine "UI-Silver-Button" slices and ChatFrameExpandArrow caret as
-- the vanity and wardrobe dropdowns (probed), so it takes the same handler.
-- Nothing is passed for its popout: the Trials tab has exactly three children
-- and none of them is a menu (probed), so wherever the list is built it isn't
-- there. Skin:Dropdown skips that half when it's handed nil.
Skin:Dropdown(_G[TRIALS_TAB.."FilterDropDown"])
-- The arrows are named off the scroll frame rather than off the bar, which is
-- the other reason S:HandleScrollBar can't find them on its own.
Skin:ScrollBar(
_G[TRIALS_SCROLL.."ScrollBar"],
_G[TRIALS_SCROLL.."ScrollBarThumb"],
_G[TRIALS_SCROLL.."ScrollUpButton"],
_G[TRIALS_SCROLL.."ScrollDownButton"]
)
end
local STORE_TAB = FRAME_NAME.."StoreTab"
local STORE_LAYOUT = STORE_TAB.."StoreButtonLayout"
-- Walked to the first missing index rather than against the twenty-one that
-- exist today, the same shape as the tab row: this count is a page size, and a
-- page size is exactly the sort of thing a content patch moves.
local MAX_SLOTS = 40
-- The bevelled empty-slot plate every bag and auction square on this client
-- draws, here behind each reward card (probed). The dashes are escaped because
-- StripArtByFile matches with find, where an unescaped dash is a pattern range.
local SLOT_ART = "UI%-AuctionFrame%-ItemSlot"
-- The card's rect is drawn tight around its contents: 140x32 with a 32px icon,
-- so the icon meets the top and bottom edges exactly and the name runs to the
-- right one. The native plate hid that by bleeding a bevel outwards; a flat
-- backdrop on the same rect just looks cramped. The grid leaves far more room
-- than this between cards -- tens of pixels each way -- so the box is grown
-- rather than the contents moved in, which would fight the store's own layout.
local CARD_PADDING = 4
-- The headings above the grid are drawn out of the quest log's atlas by tex
-- coord (probed: three slices plus the button's own NormalTexture, all one
-- file). Cleared by file rather than stripped so the label survives.
local HEADING_ART = "questmaplogatlas"
-- A row rather than an item square: 140x32 with a 32px icon flush left, then
-- the name, then a SimpleHTML cost (probed). The icon is a region of the button
-- itself rather than sitting in a frame of its own, unlike the talent menu's
-- rows, so it takes the crop but not a backdrop of its own.
--
-- The card takes HandleButton's useCreateBackdrop path rather than its default
-- SetTemplate one, which is what makes the padding possible at all: a template
-- is drawn as regions of the button and can only ever match its rect, while a
-- backdrop is a child frame that can be set outside it. Same reasoning as the
-- window panel's, one level down. Skin:Button's hover colouring follows either
-- way -- it resolves button.backdrop before the button itself.
--
-- The native hover square needs nothing by name: it's a plain region on the
-- HIGHLIGHT layer (probed), which is the whole layer Skin:Button's
-- StripHighlightArt takes.
--
-- The crop is re-applied from a hook rather than set once. The grid is a fixed
-- pool the store refills in place on every search, filter and page change, and
-- SetTexture resets a texture's coords -- so a one-shot crop would survive
-- exactly until the first page turn, and the native icon borders would come
-- back on every card at once. The plate underneath needs no such watch:
-- StripArtByFile noops its SetTexture, so a refill that re-arts it writes
-- nothing.
local function SkinStoreCard(slot)
if not slot then return end
Skin:StripArtByFile(slot, SLOT_ART)
Skin:Button(slot, nil, nil, true)
-- Re-set on every pass rather than once: SetOutside writes fixed points, so
-- a UI scale change that moves what a pixel is leaves them stale.
if slot.backdrop then
slot.backdrop:SetOutside(slot, CARD_PADDING, CARD_PADDING)
end
local name = slot:GetName()
local icon = name and _G[name.."Icon"]
if not icon then return end
icon:SetTexCoord(unpack(E.TexCoords))
-- Guarded separately from Skin:Button's own flag, and for the same reason it
-- guards its own: this runs again on every show, and hooksecurefunc stacks
-- handlers rather than replacing them.
if not slot.CoAIconHooked then
slot.CoAIconHooked = true
hooksecurefunc(icon, "SetTexture", function(texture)
texture:SetTexCoord(unpack(E.TexCoords))
end)
end
end
-- "Trial Master's Rewards" and its sibling for the build vendor are Buttons by
-- type, but they aren't buttons: they're the grid's section headings, drawn as
-- a stylised plaque and doing nothing when clicked. So they get their art taken
-- off and nothing put back -- no backdrop, which would draw a box around a
-- caption and read as clickable, and no hover border for the same reason. The
-- HIGHLIGHT layer goes too, or the caption lights up under the cursor with
-- nothing behind it.
--
-- Walked off the layout rather than named, since the set is one per reward
-- vendor and grows with content. Same reasoning as the tab row.
local function SkinStoreHeadings()
local layout = _G[STORE_LAYOUT]
if not layout then return end
for i = 1, layout:GetNumChildren() do
local heading = select(i, layout:GetChildren())
Skin:StripArtByFile(heading, HEADING_ART)
Skin:StripHighlightArt(heading)
end
end
-- Everything on this tab exists before it has ever been opened (probed: the
-- same twenty-nine children on a cold reload as after a click), so it goes
-- through SkinContents with the rest rather than needing a hook on the tab's
-- own OnShow.
--
-- Two things here are deliberately left alone. The Currency button carries the
-- trophy icon and the "Trial Master's Trophy: 0" counter as its only two
-- regions and no frame art at all, so there's nothing to strip and a backdrop
-- would only make a label look clickable. PageText is a FontString region of
-- the tab itself -- the same trap as the vanity store's counters, and the
-- reason Skin:Panel grew keepTextures -- so nothing here blind-strips the tab.
--
-- No scroll bar is wired: the grid pages rather than scrolls, and there's no
-- scroll frame anywhere under the tab to hang one off (probed).
local function SkinStoreTab()
Skin:SearchBox(_G[STORE_TAB.."SearchBox"])
-- Not a dropdown by structure -- no Middle texture, no Button child, no Text
-- fontstring, just a plain Button (probed) -- but it is one by art: the same
-- nine "UI-Silver-Button" slices and ChatFrameExpandArrow caret the Trials
-- filter wears. Skin:Dropdown matches on the art rather than on the naming,
-- so it takes this one too; the Middle-plus-Button structural test is only
-- ever a shortcut to that question, and here it answers it wrongly.
Skin:Dropdown(_G[STORE_TAB.."Filter"])
-- Stock "UI-SquareButton" arrows (probed), unlike the hand-built bar on the
-- Trials list, so these go through the ElvUI handler unaided.
Skin:NextPrevButton(_G[STORE_TAB.."PreviousPageButton"], "left")
Skin:NextPrevButton(_G[STORE_TAB.."NextPageButton"], "right")
SkinStoreHeadings()
for i = 1, MAX_SLOTS do
local slot = _G[STORE_TAB.."Slot"..i]
if not slot then break end
SkinStoreCard(slot)
end
end
local CHALLENGES_TAB = FRAME_NAME.."ChallengesTab"
local CHALLENGES_LIST = CHALLENGES_TAB.."Challenges"
local CHALLENGES_SCROLL = CHALLENGES_LIST.."ScrollFrame"
-- The one tab whose naming does carry over from another: the same three
-- children as Trials, under the same suffixes, with the same store divider as
-- its list container's only region, the same nine "UI-Silver-Button" slices and
-- caret on its filter, and its arrows named off the scroll frame rather than
-- off the bar (probed). So it takes the Trials treatment call for call -- see
-- SkinTrialsTab above for why each of the four is the call it is.
--
-- The divider constant is shared rather than copied: it is the same 2px rule out
-- of the same file, not a second one that happens to match today.
local function SkinChallengesTab()
Skin:SearchBox(_G[CHALLENGES_TAB.."Search"])
Skin:StripArtByFile(_G[CHALLENGES_LIST], LIST_DIVIDER_ART)
Skin:Dropdown(_G[CHALLENGES_TAB.."FilterDropDown"])
Skin:ScrollBar(
_G[CHALLENGES_SCROLL.."ScrollBar"],
_G[CHALLENGES_SCROLL.."ScrollBarThumb"],
_G[CHALLENGES_SCROLL.."ScrollUpButton"],
_G[CHALLENGES_SCROLL.."ScrollDownButton"]
)
end
local GAMEMODES_TAB = FRAME_NAME.."GamemodesTab"
local GAMEMODES_LIST = GAMEMODES_TAB.."Challenges"
local GAMEMODES_SCROLL = GAMEMODES_LIST.."ScrollFrame"
-- The Trials/Challenges shape a third time, and again suffix for suffix: the
-- same search box and list names, the same store divider as its list
-- container's only region, the same arrows named off the scroll frame rather
-- than off the bar (probed). See SkinTrialsTab above for why each call is the
-- call it is.
--
-- What differs is the filter: there isn't one, and that's the tab's own shape
-- rather than something missed here. It has exactly two children, the search
-- box and the list, and neither of them is a menu (probed), so there is nothing
-- for a Skin:Dropdown line to be handed.
--
-- It owns no regions of its own either (probed), so nothing here needs the
-- keepTextures care the Rewards tab's PageText took. And everything under it
-- exists before the tab has ever been opened -- two children and no regions on
-- a cold reload, unchanged after a click -- so it rides SkinContents like the
-- other three rather than needing a hook on the tab's own OnShow.
local function SkinGamemodesTab()
Skin:SearchBox(_G[GAMEMODES_TAB.."Search"])
Skin:StripArtByFile(_G[GAMEMODES_LIST], LIST_DIVIDER_ART)
Skin:ScrollBar(
_G[GAMEMODES_SCROLL.."ScrollBar"],
_G[GAMEMODES_SCROLL.."ScrollBarThumb"],
_G[GAMEMODES_SCROLL.."ScrollUpButton"],
_G[GAMEMODES_SCROLL.."ScrollDownButton"]
)
end
-- The close button is anchored inside the frame's own top-right corner, which
-- stopped being the corner the player sees once the panel grew up over the title
-- band -- it ends up floating a title's height below the top edge. Re-anchored
-- to the panel instead, and re-run on every pass so it follows the panel's top
-- whenever that gets re-measured.
--
-- No offset of its own: Skin:CloseButton normalises the button to a 32px box
-- with a 12px X centred in it, so the margin off the corner is already built in.
local function AnchorCloseButton(frame)
local close = _G[FRAME_NAME.."CloseButton"]
if not (close and frame.backdrop) then return end
close:ClearAllPoints()
close:SetPoint("TOPRIGHT", frame.backdrop, "TOPRIGHT", 0, 0)
end
-- Nothing here re-runs on a tab switch, and nothing needs to: switching tabs
-- doesn't fire the frame's OnShow, and the one thing a switch does disturb is
-- the tab art, which Skin:Tab watches from its own OnUpdate. The title
-- fontstring is reused across tabs rather than swapped for a per-tab one -- its
-- text changes, the object doesn't -- so it only needs the one pass too.
local function SkinContents()
StripBorderArt()
Skin:Title(GetTitle())
UpdatePanelTop(_G[FRAME_NAME])
Skin:CloseButton(_G[FRAME_NAME.."CloseButton"])
AnchorCloseButton(_G[FRAME_NAME])
SkinTabs()
AnchorTabRow()
SkinTrialsTab()
SkinStoreTab()
SkinGamemodesTab()
SkinChallengesTab()
end
local function SkinFrame(frame)
if not frame.CoASkinned then
frame.CoASkinned = true
-- A single BACKGROUND texture is all the frame owns (probed), and it's
-- decorative -- nothing functional is drawn as a region of this frame,
-- unlike the vanity store, whose currency counters are. So it's stripped
-- outright. Stripped before the backdrop is made, not after: a backdrop
-- built first would be a child frame and survive, but the order is kept
-- the same as Skin:Panel's for one less rule to remember.
frame:StripTextures()
frame:CreateBackdrop("Transparent")
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:InitializeChallengesFrame()
if not E.private.skins.blizzard.enable then return end
Skin:OnFrameAvailable(TryHook)
end
+44 -25
View File
@@ -1,13 +1,18 @@
local E, L, V, P, G = unpack(ElvUI)
local CoA = E:GetModule("CoA")
-- defaultY staggers the movers' first-run default position (LEFT of screen,
-- spaced vertically) so they don't stack on top of each other before the
-- player has ever dragged them.
local FRAMES = {
{name = "CoAResourceSegmentBar", moverText = "Resource Segment Bar", hideKey = "hideResourceSegmentBar"},
{name = "CoAResourceOrb", moverText = "Resource Orb", hideKey = "hideResourceOrb"},
{name = "CoAResourceBar", moverText = "Resource Bar", hideKey = "hideResourceBar"},
{name = "CoAMultiCastActionBarFrame", moverText = "Multi Cast Action Bar", hideKey = "hideMultiCastActionBar"},
{name = "CoAResourceSegmentBar", moverText = "Resource Segment Bar", hideKey = "hideResourceSegmentBar", defaultY = 40},
{name = "CoAResourceOrb", moverText = "Resource Orb", hideKey = "hideResourceOrb", defaultY = 100},
{name = "CoAResourceBar", moverText = "Resource Bar", hideKey = "hideResourceBar", defaultY = 160},
{name = "CoAMultiCastActionBarFrame", moverText = "Multi Cast Action Bar", hideKey = "hideMultiCastActionBar", defaultY = 220},
}
local DEFAULT_WIDTH, DEFAULT_HEIGHT = 150, 30
local FRAME_NAMES = {}
for _, def in ipairs(FRAMES) do
FRAME_NAMES[def.name] = true
@@ -179,30 +184,42 @@ local function LockPosition(frame, holder)
end)
end
local function SetupMover(frame, name, moverText)
if frame.CoAMoverCreated then return true end
-- Created eagerly (independent of the native frame ever showing up -- several
-- of these are class-specific and may never appear for a given character) so
-- every mover always appears in Toggle Anchors. The default point is only
-- used until the player drags it once; after that E:CreateMover restores the
-- saved position from E.db.movers.
local function CreateMoverHolder(def)
if CoA.classResourceMoversCreated and CoA.classResourceMoversCreated[def.name] then return end
CoA.classResourceMoversCreated = CoA.classResourceMoversCreated or {}
CoA.classResourceMoversCreated[def.name] = true
local holder = CreateFrame("Frame", "CoA_"..def.name.."Holder", E.UIParent)
holder:Size(DEFAULT_WIDTH, DEFAULT_HEIGHT)
holder:Point("LEFT", E.UIParent, "LEFT", 150, def.defaultY)
E:CreateMover(holder, "CoA_"..def.name.."Mover", def.moverText, nil, nil, nil, "ALL,COA", nil, "CoA,classResources")
holder:SetAllPoints(_G["CoA_"..def.name.."Mover"])
end
local function SyncHolderSize(frame, name)
local holder = _G["CoA_"..name.."Holder"]
if not holder then return end
local width, height = frame:GetSize()
if width == 0 or height == 0 then return false end
if width > 0 and height > 0 then
holder:Size(width, height)
end
end
local left, bottom = frame:GetLeft(), frame:GetBottom()
if not left or not bottom then return false end
frame.CoAMoverCreated = true
local holder = CreateFrame("Frame", "CoA_"..name.."Holder", E.UIParent)
holder:Size(width, height)
holder:Point("BOTTOMLEFT", E.UIParent, "BOTTOMLEFT", left, bottom)
E:CreateMover(holder, "CoA_"..name.."Mover", moverText, nil, nil, nil, "ALL,COA", nil, "CoA,skin,classResources")
holder:SetAllPoints(_G["CoA_"..name.."Mover"])
local function AnchorFrame(frame, name)
local holder = _G["CoA_"..name.."Holder"]
if not holder then return end
QueueAnchor(function()
AnchorToHolder(frame, holder)
LockPosition(frame, holder)
end)
return true
end
-- The server never Show()s a frame the current class doesn't use, so its
@@ -259,12 +276,10 @@ local function TryHookAll()
DisableDrag(frame)
SetupVisibility(frame, def.hideKey)
FixTooltip(frame)
SyncHolderSize(frame, def.name)
AnchorFrame(frame, def.name)
if SetupMover(frame, def.name, def.moverText) then
hooked[def.name] = true
else
allHooked = false
end
hooked[def.name] = true
else
allHooked = false
end
@@ -275,6 +290,10 @@ local function TryHookAll()
end
function CoA:InitializeClassResources()
for _, def in ipairs(FRAMES) do
CreateMoverHolder(def)
end
if TryHookAll() then return end
self.classResourcesTimer = self:ScheduleRepeatingTimer(function()
+90 -11
View File
@@ -17,15 +17,65 @@ local CLASS_DISPEL_TYPES = {
PYROMANCER = {},
}
-- Extra dispel types unlocked by a talent choice. There's no reliable way to
-- auto-detect the talent on this server, so these are gated by a manual
-- checkbox in the options panel instead.
-- Extra dispel types unlocked by a talent choice, keyed to the Character
-- Advancement entry that grants them. The IDs are stable and shared across
-- every class tree, so querying one from a class that can't take it simply
-- reports rank 0 rather than erroring.
local TALENT_DISPEL_TYPES = {
PROPHET = {flag = "hasBlightAntidote", types = {Curse = true}},
CULTIST = {flag = "hasDevourCurse", types = {Curse = true}},
PYROMANCER = {flag = "hasBurnImpurities", types = {Magic = true, Disease = true, Bleed = true}},
PROPHET = {talentID = 6324, talentName = "Blight Antidote", types = {Curse = true}},
CULTIST = {talentID = 12982, talentName = "Devour Curse", types = {Curse = true}},
PYROMANCER = {talentID = 31276, talentName = "Burn Impurities", types = {Magic = true, Disease = true, Bleed = true}},
}
-- Cached result of the talent lookup for the player's current build.
-- PostUpdate_DebuffHighlight runs once per aura per frame, so the query is
-- hoisted out of that path and refreshed only when the build can change.
local hasTalentDispel = false
-- C_CharacterAdvancement is a server addition with no API documentation, so
-- every entry point is guarded and a failed call reads as "talent not taken",
-- which degrades to the same behaviour as before auto-detection existed.
local function HasTalent(talent)
local api = C_CharacterAdvancement
if not api then return false end
if type(api.GetTalentRankByID) == "function" then
local ok, rank = pcall(api.GetTalentRankByID, talent.talentID)
if ok and type(rank) == "number" and rank > 0 then return true end
end
-- Fall back to matching the talent by name, so an entry ID gone stale after
-- a server rebalance doesn't silently switch the feature off. Names are the
-- more fragile key of the two, but GetKnownTalentEntries only ever lists
-- entries the player has actually learned, which makes this a genuinely
-- independent check rather than a second opinion on the same ID. The list
-- has been seen with holes in it, so it's walked with pairs.
if type(api.GetKnownTalentEntries) == "function" then
local ok, entries = pcall(api.GetKnownTalentEntries)
if ok and type(entries) == "table" then
for _, entry in pairs(entries) do
if type(entry) == "table" and entry.Name == talent.talentName then return true end
end
end
end
return false
end
-- Returns whether the talent state actually moved, so callers reacting to the
-- chattier advancement events can skip the unitframe rebuild when it didn't.
local function RefreshTalentState()
local _, class = UnitClass("player")
local talent = TALENT_DISPEL_TYPES[class]
local hasTalent = (talent and HasTalent(talent)) or false
if hasTalent == hasTalentDispel then return false end
hasTalentDispel = hasTalent
return true
end
function CoA:CanDispel(debuffType)
local _, class = UnitClass("player")
local baseTypes = CLASS_DISPEL_TYPES[class]
@@ -34,7 +84,9 @@ function CoA:CanDispel(debuffType)
if baseTypes and baseTypes[debuffType] then return true end
local talent = TALENT_DISPEL_TYPES[class]
if talent and CoA.db.profile[talent.flag] and talent.types[debuffType] then return true end
if talent and hasTalentDispel and CoA.db.profile.dispelHighlightDetectTalents and talent.types[debuffType] then
return true
end
return false
end
@@ -69,18 +121,45 @@ hooksecurefunc(UF, "Configure_DebuffHighlight", function(_, frame)
end)
function CoA:UpdateDispelHighlight()
RefreshTalentState()
UF:Update_AllFrames()
end
function CoA:RefreshDispelTalents()
if RefreshTalentState() then
UF:Update_AllFrames()
end
end
-- On Ascension, UnitClass("player")'s second return only reliably reports the
-- real custom class (CULTIST, PYROMANCER, ...) a short while after login --
-- immediately at ADDON_LOADED/PLAYER_LOGIN it can still read back the generic
-- "HERO" base class. If a debuff highlight gets evaluated before that data
-- syncs, CoA:CanDispel wrongly returns false and the highlight stays wrongly
-- suppressed until the next aura change. Force one extra refresh shortly
-- after entering the world so the very first debuff isn't judged too early.
-- "HERO" base class, and the advancement data lags in the same way. If a
-- debuff highlight gets evaluated before that data syncs, CoA:CanDispel
-- wrongly returns false and the highlight stays wrongly suppressed until the
-- next aura change. Force one extra refresh shortly after entering the world
-- so the very first debuff isn't judged too early.
--
-- CHARACTER_ADVANCEMENT_UPDATE_ENTRIES_RESULT fires when a build is confirmed
-- in the talent UI, but switching to another saved specialization doesn't
-- confirm anything -- that path only fires PENDING_BUILD_UPDATED and
-- SUGGESTIONS_UPDATED, so both are needed or the ranks read stale for the rest
-- of the session. PENDING_BUILD_UPDATED also fires on every click inside the
-- talent UI and SUGGESTIONS_UPDATED is only incidentally related, which is why
-- they go through RefreshDispelTalents: it costs one rank lookup and rebuilds
-- the unitframes only when the answer changed.
function CoA:InitializeDispelHighlight()
CoA:RegisterEvent("PLAYER_ENTERING_WORLD", function()
CoA:ScheduleTimer("UpdateDispelHighlight", 2)
end)
for _, event in ipairs({
"CHARACTER_ADVANCEMENT_UPDATE_ENTRIES_RESULT",
"CHARACTER_ADVANCEMENT_PENDING_BUILD_UPDATED",
"CHARACTER_ADVANCEMENT_SUGGESTIONS_UPDATED",
}) do
CoA:RegisterEvent(event, function()
CoA:RefreshDispelTalents()
end)
end
end
+33 -18
View File
@@ -53,6 +53,11 @@ end
function CoA:UpdateExtraActionButtonSize()
UpdateSize()
local holder = _G["CoA_ExtraActionBarHolder"]
if holder then
holder:Size(CoA.db.profile.extraActionButtonSize or 52)
end
end
local function UpdateHotkeyText(button)
@@ -157,26 +162,34 @@ local function SkinButton(button, container)
SkinGlow(button, name)
end
local function SetupMover(container, button)
if CoA.extraActionBarMoverCreated then return true end
local width, height = button:GetSize()
if width == 0 or height == 0 then return false end
local left, bottom = button:GetLeft(), button:GetBottom()
if not left or not bottom then return false end
-- Created eagerly (independent of the native ExtraActionBar ever showing up)
-- so the mover always appears in Toggle Anchors. The default point below is
-- only used until the player drags it once; after that E:CreateMover restores
-- the saved position from E.db.movers.
local function CreateMoverHolder()
if CoA.extraActionBarMoverCreated then return end
CoA.extraActionBarMoverCreated = true
local size = CoA.db.profile.extraActionButtonSize or 52
local holder = CreateFrame("Frame", "CoA_ExtraActionBarHolder", E.UIParent)
holder:Size(size, size)
holder:Point("BOTTOM", E.UIParent, "BOTTOM", 0, 260)
E:CreateMover(holder, "CoA_ExtraActionBarMover", "Extra Action Bar", nil, nil, nil, "ALL,COA", nil, "CoA,skins,extraActionButton")
holder:SetAllPoints(_G["CoA_ExtraActionBarMover"])
end
local function SetupContainer(container)
if container.CoAContainerSetup then return end
container.CoAContainerSetup = true
container:StripTextures(true)
container:EnableMouse(false)
end
local holder = CreateFrame("Frame", "CoA_ExtraActionBarHolder", E.UIParent)
holder:Size(width, height)
holder:Point("BOTTOMLEFT", E.UIParent, "BOTTOMLEFT", left, bottom)
E:CreateMover(holder, "CoA_ExtraActionBarMover", "Extra Action Bar", nil, nil, nil, "ALL,COA", nil, "CoA,skin,extraActionBar")
holder:SetAllPoints(_G["CoA_ExtraActionBarMover"])
local function AnchorContainer(container)
local holder = _G["CoA_ExtraActionBarHolder"]
if not holder then return end
local function Anchor()
container:ClearAllPoints()
@@ -191,8 +204,6 @@ local function SetupMover(container, button)
else
Anchor()
end
return true
end
local function TryHook()
@@ -201,13 +212,17 @@ local function TryHook()
if container and button then
SkinButton(button, container)
return SetupMover(container, button)
SetupContainer(container)
AnchorContainer(container)
return true
end
return false
end
function CoA:InitializeExtraActionBar()
CreateMoverHolder()
if TryHook() then return end
self.extraActionBarTimer = self:ScheduleRepeatingTimer(function()
+142
View File
@@ -0,0 +1,142 @@
local E, L, V, P, G = unpack(ElvUI)
local S = E:GetModule("Skins")
local CoA = E:GetModule("CoA")
local _G = _G
local ipairs, pairs, select, type = ipairs, pairs, select, type
-- Generic sweep over the stock options windows.
--
-- ElvUI skins those windows from four hardcoded name lists (checkboxes,
-- sliders, buttons, dropdowns) in Modules/Skins/Blizzard/BlizzardOptions.lua,
-- so anything the client added since upstream last synced stays native --
-- 53 widgets on this server, 39 of them in four panels upstream never
-- enumerated at all (Battlenet, RaidEffects, Stereo, Voice). Rather than
-- carry a parallel name list that goes stale the same way, this walks the
-- panel containers and skins by object type.
local MAX_DEPTH = 3
-- Every panel is parented at load (probed: the Interface container reports the
-- same child count before and after opening the window), so the containers can
-- be walked without waiting for a category to be displayed.
local ROOTS = {
"InterfaceOptionsFramePanelContainer",
"VideoOptionsFramePanelContainer",
"AudioOptionsFramePanelContainer",
}
-- Hooked on all three windows rather than only the Interface one: the Video and
-- Audio windows are separate frames reachable straight from the game menu, and
-- a player who opens one of those first would otherwise see it unskinned. Each
-- show sweeps every root, which costs nothing -- the pass is idempotent.
local TRIGGERS = {
"InterfaceOptionsFrame",
"VideoOptionsFrame",
"AudioOptionsFrame",
}
-- Panels owned by other addons, keyed by frame. Rebuilt on every sweep rather
-- than cached at init: load-on-demand addons register their category late, and
-- a set built once would let their panels through on later shows.
local addonPanels = {}
local function RebuildAddOnPanels()
for panel in pairs(addonPanels) do
addonPanels[panel] = nil
end
local categories = _G.INTERFACEOPTIONS_ADDONCATEGORIES
if type(categories) ~= "table" then return end
-- The entries are the panel frames themselves, not descriptors (probed).
-- Matching on the frame is why this doesn't need a name prefix whitelist:
-- an addon can name its panel anything, and the registry is authoritative.
for _, panel in ipairs(categories) do
if type(panel) == "table" then
addonPanels[panel] = true
end
end
end
-- Structural test, not a name test. S:HandleDropDownBox uses _G[name.."Button"]
-- unguarded when anchoring its backdrop, so a false positive is a Lua error
-- rather than a cosmetic miss; it also force-sets the frame's width, which
-- mangles a group box even when it doesn't error. A DropDown name suffix was
-- considered and rejected -- ElvUI's own dropdown list carries
-- CompactUnitFrameProfilesProfileSelector (no suffix) and ...SortByDropdown
-- (lowercase d), so suffix matching misses real dropdowns and adds nothing.
local function IsDropDown(frame)
local name = frame.GetName and frame:GetName()
if not name then return false end
local middle = _G[name.."Middle"]
if not middle or not middle.IsObjectType or not middle:IsObjectType("Texture") then return false end
return _G[name.."Button"] ~= nil
end
-- depth is the depth of the children being visited, counting the container's
-- own children as 1. Three is enough for the deepest real case: the Controls
-- panel's anonymous "Looting Options" group box and the compact raid frame
-- profile dialogs both hold their widgets a level below the panel.
local function Sweep(frame, depth)
for i = 1, frame:GetNumChildren() do
local child = select(i, frame:GetChildren())
if child and not addonPanels[child] then
local objectType = child:GetObjectType()
if objectType == "CheckButton" then
S:HandleCheckBox(child)
elseif objectType == "Slider" then
-- The gate is load-bearing, not an optimisation:
-- HandleSliderFrame has no idempotence guard of its own and
-- HookScript stacks, so a second pass over a slider ElvUI
-- already skinned would run its OnDisable/OnEnable handlers
-- twice. E:SetTemplate sets frame.template, so that field is
-- the "already skinned" marker for sliders.
if not child.template then
S:HandleSliderFrame(child)
end
elseif objectType == "Frame" and IsDropDown(child) then
S:HandleDropDownBox(child)
elseif depth < MAX_DEPTH then
-- Deliberately no Button branch: nothing in the sweep needs one,
-- and it would flatten other addons' panel tabs -- object type
-- Button, but tabs -- into ElvUI pill buttons.
Sweep(child, depth + 1)
end
end
end
end
local function SweepOptions()
RebuildAddOnPanels()
for _, name in ipairs(ROOTS) do
local root = _G[name]
if root then
Sweep(root, 1)
end
end
end
function CoA:InitializeInterfaceOptions()
-- The same two switches that gate ElvUI's own pass: if the user turned the
-- stock options skin off, the gap this fills isn't a gap any more.
if not E.private.skins.blizzard.enable then return end
if not E.private.skins.blizzard.BlizzardOptions then return end
-- Skinning on show rather than at init puts the sweep after ElvUI's
-- Skin_BlizzardOptions callback with no ordering work, and re-running it on
-- every show catches panels that only appear once their addon loads.
for _, name in ipairs(TRIGGERS) do
local frame = _G[name]
if frame then
frame:HookScript("OnShow", SweepOptions)
end
end
end
+28 -15
View File
@@ -19,8 +19,17 @@ local function UpdateFont(button)
)
end
local function SyncHolderSize(button)
local holder = _G["CoA_LayerPickerHolder"]
button = button or _G[BUTTON_NAME]
if holder and button then
holder:Size(button:GetSize())
end
end
function CoA:UpdateInstanceButtonFont()
UpdateFont()
SyncHolderSize()
end
-- Hooked at file scope, so unlike the rest of the skin it stays live even when
@@ -69,24 +78,26 @@ local function DisableDrag(button)
end)
end
local function SetupMover(button)
if CoA.layerPickerMoverCreated then return true end
local width, height = button:GetSize()
if width == 0 or height == 0 then return false end
local left, bottom = button:GetLeft(), button:GetBottom()
if not left or not bottom then return false end
-- Created eagerly (independent of the native LayerPickerFrame ever showing
-- up) so the mover always appears in Toggle Anchors. The default point below
-- is only used until the player drags it once; after that E:CreateMover
-- restores the saved position from E.db.movers.
local function CreateMoverHolder()
if CoA.layerPickerMoverCreated then return end
CoA.layerPickerMoverCreated = true
local holder = CreateFrame("Frame", "CoA_LayerPickerHolder", E.UIParent)
holder:Size(width, height)
holder:Point("BOTTOMLEFT", E.UIParent, "BOTTOMLEFT", left, bottom)
holder:Size(MIN_WIDTH, MIN_HEIGHT)
holder:Point("TOPLEFT", E.UIParent, "TOPLEFT", 250, -250)
E:CreateMover(holder, "CoA_LayerPickerMover", "Instance", nil, nil, nil, "ALL,COA", nil, "CoA,skin,instance")
E:CreateMover(holder, "CoA_LayerPickerMover", "Instance", nil, nil, nil, "ALL,COA", nil, "CoA,skins,instanceSwap")
holder:SetAllPoints(_G["CoA_LayerPickerMover"])
_G["CoA_LayerPickerMover"]:SetFrameStrata("FULLSCREEN")
end
local function AnchorButton(button)
local holder = _G["CoA_LayerPickerHolder"]
if not holder then return end
local function Anchor()
button:ClearAllPoints()
@@ -101,8 +112,6 @@ local function SetupMover(button)
else
Anchor()
end
return true
end
local function SkinButton(button)
@@ -122,13 +131,17 @@ local function TryHook()
if button then
DisableDrag(button)
SkinButton(button)
return SetupMover(button)
SyncHolderSize(button)
AnchorButton(button)
return true
end
return false
end
function CoA:InitializeLayerPicker()
CreateMoverHolder()
if TryHook() then return end
self.layerPickerTimer = self:ScheduleRepeatingTimer(function()
+468 -39
View File
@@ -57,7 +57,7 @@ local CONTAINER_NAME = "Collections"
local scaleKey
local function GetScaleSetting(key)
local db = CoA.db and CoA.db.profile.skins.talentFrames
local db = CoA.db and CoA.db.profile.skins.collections
return (db and db[key]) or 1
end
@@ -138,31 +138,166 @@ function Skin:HideArt(frame)
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.
-- Buttons ---------------------------------------------------------------------
--
-- 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
-- IsEnabled hands back 0 and 1 on this client rather than false and true
-- (probed), and 0 is truthy in Lua, so every enabled check has to normalise
-- what it gets back before comparing it.
function Skin:IsEnabled(button)
local state = button.IsEnabled and button:IsEnabled()
local function StripDropdownArt(dropdown)
for i = 1, dropdown:GetNumRegions() do
local region = select(i, dropdown:GetRegions())
return state ~= nil and state ~= false and state ~= 0
end
-- The target is resolved the way ElvUI's own SetModifiedBackdrop and
-- SetOriginalBackdrop resolve it -- the backdrop when the button has one, the
-- button itself otherwise -- or the colour lands on something that isn't the
-- object drawing the border.
local function SetButtonBorder(button, lit)
local backdrop = button.backdrop or button
if not backdrop.SetBackdropBorderColor then return end
backdrop:SetBackdropBorderColor(unpack(lit and E.media.rgbvaluecolor or E.media.bordercolor))
end
-- S:HandleButton ends with an unconditional pair of OnEnter/OnLeave hooks that
-- swap the border to the value colour and back, and neither of them looks at
-- IsEnabled. A disabled button still fires both scripts on this client, so a
-- dead button lights up under the cursor and reads as clickable -- the greyed
-- Activate button, Save changes with nothing pending, Purchase with nothing
-- selected. HookScript can't be undone, but handlers fire in registration
-- order, so a hook registered after HandleButton runs last and has the final
-- say on the colour.
--
-- This one deliberately doesn't consult IsMouseOver: OnEnter can fire a pixel
-- before IsMouseOver turns true (ElvUI's own aura code pads around the same
-- quirk), and reading it here would drop the highlight off a live button.
local function OnButtonEnter(button)
if Skin:IsEnabled(button) then return end
SetButtonBorder(button, false)
end
-- For a button that can be disabled while the cursor is already on it: OnEnter
-- has been and gone by then, so nothing corrects the border until the pointer
-- leaves. Only worth wiring up where something already watches the button's
-- state -- it does not justify an OnUpdate of its own.
function Skin:RefreshButtonBorder(button)
SetButtonBorder(button, self:IsEnabled(button) and button:IsMouseOver())
end
-- Every S:HandleButton call site in the plugin goes through here, so the
-- disabled-hover correction is inherited rather than repeated per module. The
-- extra arguments are HandleButton's own (strip, isDeclineButton,
-- useCreateBackdrop, noSetTemplate) and are passed straight through.
function Skin:Button(button, ...)
if not button then return end
S:HandleButton(button, ...)
-- Unguarded: a pass over a handful of regions is cheap, and re-running it
-- catches a region added to the button after the first skin pass.
self:StripHighlightArt(button)
-- Guarded separately from HandleButton's own isSkinned flag: these frames
-- re-run their skin pass on every show, and HookScript stacks handlers
-- rather than replacing them, so an unguarded re-hook adds another copy
-- per show for the life of the session.
if not button.CoAButtonSkinned then
button.CoAButtonSkinned = true
button:HookScript("OnEnter", OnButtonEnter)
end
end
-- S:HandleNextPrevButton routes through S:HandleButton, so it inherits exactly
-- the lit-border-on-a-dead-button behaviour OnButtonEnter is here to correct,
-- and scroll arrows spend a good deal of their life disabled at one end of their
-- list or the other. Same separate guard as Skin:Button, and for the same
-- reason: these frames re-run their skin passes, and HookScript stacks.
function Skin:NextPrevButton(button, direction)
if not button then return end
S:HandleNextPrevButton(button, direction)
if not button.CoAButtonSkinned then
button.CoAButtonSkinned = true
button:HookScript("OnEnter", OnButtonEnter)
end
end
-- Several of these controls draw their art as anonymous regions of one atlas
-- file rather than through 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 or the
-- label with it. Those are cleared by file instead.
--
-- SetTexture is noop'd per region rather than just cleared: whatever re-arts
-- the control on a state change (a mouse-down swapping in the pressed variant,
-- a tex coord swap between two variants of the same file) is free to re-set the
-- file as well, and a cleared texture would come straight back.
function Skin:StripArtByFile(frame, pattern)
if not frame then return end
for i = 1, frame:GetNumRegions() do
local region = select(i, frame:GetRegions())
local texture = region.GetTexture and region:GetTexture()
if texture and tostring(texture):find(DROPDOWN_ART) then
if texture and tostring(texture):find(pattern) then
region:SetTexture(nil)
region.SetTexture = E.noop
end
end
end
-- S:HandleButton clears the button's own Normal/Highlight/Pushed/Disabled
-- textures and the named Left/Middle/Right pieces, and nothing else -- so a
-- native highlight drawn as a plain region of the button survives it and lights
-- up under the cursor against the flat ElvUI backdrop. That was the wardrobe
-- Cancel button's red glow, and clearing it by file there fixed exactly that
-- one button: probing the layer across everything the plugin skins turned up
-- the Save outfit button wearing the same red file, and Disable transmog and
-- Disable spell visuals wearing the dialog-box glow, none of which any
-- clear-by-file call site was ever going to reach.
--
-- The whole layer goes rather than named files, because on these frames it only
-- ever carries native hover art. Every button probed has exactly one HIGHLIGHT
-- region, and on the ones that already looked right it is blank -- so there is
-- nothing else living there to lose. ElvUI's own hover treatment isn't caught:
-- it's the backdrop border swap HandleButton hooks, and a backdrop is drawn
-- through SetBackdrop plus two child border frames, not as a region of the
-- button at all.
--
-- Noop'd rather than only cleared, as in StripArtByFile: a state change that
-- re-arts the button would otherwise bring the glow straight back.
function Skin:StripHighlightArt(frame)
if not frame then return end
for i = 1, frame:GetNumRegions() do
local region = select(i, frame:GetRegions())
if region:GetObjectType() == "Texture" and region:GetDrawLayer() == "HIGHLIGHT" then
region:SetTexture(nil)
region.SetTexture = E.noop
end
end
end
-- The pill behind the talent frame's Activate button and the wardrobe's Apply
-- and Cancel buttons. Two files rather than one: "128GoldRedButton" carries the
-- green and grey variants as tex coords, and Cancel alone is drawn from
-- "128RedButton" (probed). The shared suffix matches both, and nothing else on
-- these frames uses either. Only the pill itself is this pattern's job now --
-- the hover art on both files goes with the rest of the HIGHLIGHT layer.
Skin.RedButtonArt = "RedButton"
-- The dropdown pills on the vanity and wardrobe frames are the same widget:
-- nine anonymous "Silver-Button" slices.
local DROPDOWN_ART = "Silver%-Button"
local CARET_ART = "ChatFrameExpandArrow"
local CARET_SIZE = 14
-- 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)
@@ -191,23 +326,242 @@ function Skin:Dropdown(dropdown, menu)
if not dropdown or dropdown.CoASkinned then return end
dropdown.CoASkinned = true
StripDropdownArt(dropdown)
S:HandleButton(dropdown)
self:StripArtByFile(dropdown, DROPDOWN_ART)
self:Button(dropdown)
SkinDropdownCaret(dropdown)
self:Panel(menu)
end
-- The CoA search boxes draw their border and magnifier from the stock
-- "CommonSearch" file, tinted to the server's own colour, as anonymous regions
-- -- so S:HandleEditBox can't reach them: it only alpha-0s the named Left/
-- Middle/Right pieces a stock Blizzard edit box has.
--
-- Cleared by file rather than by a blind strip, which is what a first pass did:
-- an EditBox draws its own caret and its selection highlight as plain solid
-- textures of its own (probed: four of them, sub-pixel wide and text-high, shown
-- only while the box has focus), and StripTextures takes those with the border,
-- leaving a search box you can type in with no cursor in it.
local SEARCH_BOX_ART = "CommonSearch"
function Skin:SearchBox(box)
if not box or box.CoASearchSkinned then return end
box.CoASearchSkinned = true
self:StripArtByFile(box, SEARCH_BOX_ART)
S:HandleEditBox(box)
end
-- Scroll bars ------------------------------------------------------------------
--
-- S:HandleScrollBar can't be used on these: the CoA bars are hand-built rather
-- than the stock Slider widget, so the thumb is a Button with art of its own
-- (probed) where a stock bar has a thumb *texture*, and HandleScrollBar reaches
-- for the thumb through GetThumbTexture and then calls SetTexture on what it
-- finds. It also looks the arrow buttons up as children of the bar, and these
-- are named off the scroll frame instead. The treatment below is the same one it
-- applies, only with each piece handed in by the caller.
--
-- The art on both pieces is applied lazily, and that is what defeated two
-- earlier passes at this: at skin time the thumb's textures read back nil, so
-- matching them by file found nothing to clear, and the scroll code arts them a
-- moment later when it first measures the list. Clearing, noop'ing the regions'
-- SetTexture, and alpha'ing them all failed the same way -- not because the art
-- came back, but because it was never there yet to be taken.
--
-- So it's handled from OnUpdate, the same answer the tab art needed and for the
-- same reason: nothing fires when it happens, and the check is a three-region
-- alpha compare that only writes when something has actually re-arted the
-- widget. Blanket rather than matched by file: neither the track nor the thumb
-- carries anything but its own art -- no label, no counter -- which is the only
-- thing StripArtByFile's file matching is there to protect.
local function HideScrollArt(frame)
for i = 1, frame:GetNumRegions() do
local region = select(i, frame:GetRegions())
if region:GetObjectType() == "Texture" and region:GetAlpha() ~= 0 then
region:SetAlpha(0)
end
end
end
-- Colours and states taken from ElvUI's own proportional scroll bars
-- (S:HandleProportionalScroll), which is what every other scroll bar in this UI
-- looks like: the value colour at a quarter alpha, three quarters while hovered
-- or held. Its handlers are locals in ElvUI's file, so the four scripts are
-- written out here rather than reached for.
--
-- The active flag is what keeps a thumb lit while it's being dragged: the
-- pointer routinely leaves a thin thumb mid-drag, and OnLeave would otherwise
-- dim it while the list is still moving under it.
local SCROLL_BUTTON_GAP = 1
local SCROLL_THUMB_ALPHA = 0.25
local SCROLL_THUMB_ACTIVE_ALPHA = 0.75
local function SetThumbColor(thumb, alpha)
if not thumb.backdrop then return end
local r, g, b = unpack(E.media.rgbvaluecolor)
thumb.backdrop:SetBackdropColor(r, g, b, alpha)
end
local function ThumbOnEnter(thumb)
SetThumbColor(thumb, SCROLL_THUMB_ACTIVE_ALPHA)
end
local function ThumbOnLeave(thumb)
if thumb.CoAThumbActive then return end
SetThumbColor(thumb, SCROLL_THUMB_ALPHA)
end
local function ThumbOnMouseDown(thumb)
thumb.CoAThumbActive = true
SetThumbColor(thumb, SCROLL_THUMB_ACTIVE_ALPHA)
end
local function ThumbOnMouseUp(thumb)
thumb.CoAThumbActive = nil
SetThumbColor(thumb, SCROLL_THUMB_ALPHA)
end
-- The arrows re-art themselves the same way the track and thumb do, and one
-- pass can't hold against it: S:HandleNextPrevButton guards on its own
-- isSkinned flag, so calling it a second time does nothing at all. What comes
-- back isn't the ElvUI arrow being overwritten either -- that texture is still
-- in place underneath (probed: the native "scrollbarproportional" region shown
-- alongside ArrowUp). It's the button's own art, which StripTextures only hid,
-- and the scroll frame shows it again whenever it recalculates.
--
-- So the native art is re-hidden by file from OnUpdate, the same answer the
-- track, the thumb and the tab art all needed and for the same reason: nothing
-- fires when it happens. The three state textures are captured and their
-- setters noop'd as well -- still worth closing off, since whatever refreshes
-- these is free to swap in a fresh texture object and orphan the three.
--
-- Matched by file rather than as "any region that isn't one of ours": 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 tostring(texture):lower():find(NATIVE_SCROLL_ART) and region:IsShown() then
region:Hide()
end
end
for _, texture in ipairs(button.CoAArrowTextures) do
local current = texture:GetTexture()
if not current or tostring(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
function Skin:ScrollArrow(button, direction)
if not button or button.CoAArrowSkinned then return end
button.CoAArrowSkinned = true
self:NextPrevButton(button, direction)
button.CoAArrowDirection = direction
button.CoAArrowTextures = {
button:GetNormalTexture(),
button:GetPushedTexture(),
button:GetDisabledTexture(),
}
button.SetNormalTexture = E.noop
button.SetPushedTexture = E.noop
button.SetDisabledTexture = E.noop
button.SetHighlightTexture = E.noop
button:HookScript("OnUpdate", RestoreArrow)
end
function Skin:ScrollBar(bar, thumb, up, down)
if not bar or bar.CoASkinned then return end
bar.CoASkinned = true
-- The track gets no backdrop of its own, only its art taken off, which is
-- what ElvUI's own proportional scroll bars do: against these panels an empty
-- channel reads cleaner than a second filled block behind the thumb, and the
-- thumb is legible on its own at the value colour.
bar:HookScript("OnUpdate", HideScrollArt)
if thumb then
thumb:HookScript("OnUpdate", HideScrollArt)
-- Filling the thumb rather than inset inside it: the thumb is already
-- narrower than the track it runs in, so an inset backdrop reads as a thin
-- line rather than as a grip. The track's own gutter is the separation.
--
-- Levelled off the thumb rather than off the track: the thumb sits a level
-- above the track already, so a backdrop built at the track's level would
-- land underneath the thumb's own regions.
thumb:CreateBackdrop("Transparent")
thumb.backdrop:SetFrameLevel(thumb:GetFrameLevel() + 1)
thumb.backdrop:SetAllPoints()
SetThumbColor(thumb, SCROLL_THUMB_ALPHA)
thumb:HookScript("OnEnter", ThumbOnEnter)
thumb:HookScript("OnLeave", ThumbOnLeave)
thumb:HookScript("OnMouseDown", ThumbOnMouseDown)
thumb:HookScript("OnMouseUp", ThumbOnMouseUp)
end
-- Re-anchored onto the track's ends after skinning, and cleared first rather
-- than pointed on top of what's there: HandleNextPrevButton resizes them to
-- ElvUI's own 18px, so whatever the native layout anchored them by no longer
-- lines up with the track, and a second point left in place would stretch the
-- button between the two.
if up then
self:ScrollArrow(up, "up")
up:ClearAllPoints()
up:Point("BOTTOM", bar, "TOP", 0, SCROLL_BUTTON_GAP)
end
if down then
self:ScrollArrow(down, "down")
down:ClearAllPoints()
down:Point("TOP", bar, "BOTTOM", 0, -SCROLL_BUTTON_GAP)
end
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.
-- S:HandleTab still isn't used on any of these. On the server-authored tabs it
-- can't be: it clears the tab body by name, looking for a "Middle" piece, and
-- those name theirs "Center", so the body survives and the ElvUI backdrop just
-- lands behind the old art. The Challenges window's tabs are stock Blizzard
-- ones that do use "Middle" (probed), so HandleTab would reach them -- they go
-- through the same strip anyway, because one code path is what keeps every tab
-- row in this UI looking alike.
--
-- 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"}
-- One list covers all three tab flavours: the talent frame's tabs carry the
-- Disabled variants as well, the wardrobe's category tabs don't, the Challenges
-- tabs use Blizzard's Left/Middle/Right plus the Disabled set Blizzard swaps in
-- to draw the selected tab, and the lookups for the ones that don't exist
-- simply come back nil.
local TAB_TEXTURES = {
"Left", "Center", "Middle", "Right",
"LeftDisabled", "CenterDisabled", "MiddleDisabled", "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
@@ -230,7 +584,11 @@ function Skin:StripTabArt(tab)
if checked then checked:SetTexture(nil) end
end
-- Exposed: a module that re-anchors a tab row against the frame it belongs to
-- has to know how far inside its tab the backdrop actually starts, or it lines
-- the tab up flush and leaves the visible edge floating by this much.
local TAB_BACKDROP_INSET = 3
Skin.TabBackdropInset = TAB_BACKDROP_INSET
-- 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
@@ -254,20 +612,59 @@ local TAB_LABEL_OFFSET = 12
-- 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
-- of height. The same slack covers the width below.
local TAB_SIZE_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
if not tab.CoAGrownHeight or math.abs(height - tab.CoAGrownHeight) > TAB_SIZE_EPSILON then
tab.CoAGrownHeight = height + TAB_GROWTH
tab:SetHeight(tab.CoAGrownHeight)
end
end
-- Native panel tabs interlock: each is anchored back into the one before it by
-- the width of the art's end cap, so the ornate ends overlap rather than butting
-- together. Strip the art, put a flat backdrop in its place, and that overlap
-- has nothing left to hide it -- the row reads as one merged bar, and the tabs'
-- hit rects overlap along with their backdrops.
--
-- The tab is narrowed by exactly what its anchor was pulled back by, rather than
-- being pushed along to make room: pushing grows the row by the overlap times
-- the number of tabs, which on the Challenges window overhangs the frame by four
-- times the slack the row has. Absorbing it instead leaves the row's total
-- extent exactly where the native layout put it.
--
-- Opt-in per row rather than detected from the anchor: a tab anchored onto its
-- neighbour with a negative offset is the signature of an interlocking row, but
-- it would also match any row that merely sits tight, and silently resizing the
-- server-authored rows on that guess isn't worth the convenience.
local function UnoverlapTab(tab)
local point, relativeTo, relativePoint, x, y = tab:GetPoint(1)
if not (point and x) or x >= 0 then return end
tab.CoATabOverlap = -x
tab:SetPoint(point, relativeTo, relativePoint, 0, y)
end
-- Re-checked every tick with the same self-healing shape as the grown height,
-- and for the same reason: the native tab code re-measures a tab off its own
-- label, and a width written once would be handed straight back.
local function UpdateTabWidth(tab)
local overlap = tab.CoATabOverlap
if not overlap then return end
local width = tab:GetWidth()
if not tab.CoAUnoverlappedWidth or math.abs(width - tab.CoAUnoverlappedWidth) > TAB_SIZE_EPSILON then
tab.CoAUnoverlappedWidth = width - overlap
tab:SetWidth(tab.CoAUnoverlappedWidth)
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:
@@ -286,15 +683,29 @@ end
-- 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.
--
-- Two probes rather than one: a stock Blizzard tab draws its selected state by
-- hiding the Left/Middle/Right set and showing the Disabled set in its place,
-- so watching only the inactive half would miss art returning on whichever tab
-- is currently open. The three pieces of a set are always re-arted together, so
-- one probe per set is enough.
local TAB_ART_PROBES = {"Left", "LeftDisabled"}
local function UpdateTabArt(tab)
BumpTabLevel(tab)
UpdateTabSize(tab)
UpdateTabWidth(tab)
local name = tab:GetName()
local tex = name and _G[name.."Left"]
if not name then return end
if tex and tex:GetTexture() then
Skin:StripTabArt(tab)
for _, suffix in ipairs(TAB_ART_PROBES) do
local tex = _G[name..suffix]
if tex and tex:GetTexture() then
Skin:StripTabArt(tab)
return
end
end
end
@@ -325,7 +736,11 @@ 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)
--
-- unoverlap is for rows of interlocking native tabs (see UnoverlapTab). Only the
-- tabs anchored onto a neighbour take it; the first tab in a row has nothing to
-- pull back from and is left alone by the offset check.
function Skin:Tab(tab, levelParent, unoverlap)
if not tab then return end
-- Set on every call rather than only the first: the collection tabs are
@@ -339,6 +754,10 @@ function Skin:Tab(tab, levelParent)
self:StripTabArt(tab)
if unoverlap then
UnoverlapTab(tab)
end
-- 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.
@@ -362,13 +781,23 @@ function Skin:Tab(tab, levelParent)
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)
-- Only for tabs that actually carry an icon. On those (the talent
-- window's) the label's native anchor sits right off the icon, sized
-- for the smaller native font, so grown text collides with it, and
-- the label is nudged off whatever point the native layout gave it
-- rather than a hardcoded anchor that would fight that layout. The
-- wardrobe's category tabs have no icon and centre their label, so
-- the same nudge just pushed every one of them off-centre to the
-- right.
local name = tab:GetName()
local icon = name and _G[name.."Icon"]
if icon and icon.GetTexture and icon:GetTexture() and icon:IsShown() then
local point, relTo, relPoint, x, y = fontString:GetPoint(1)
if point then
fontString:SetPoint(point, relTo, relPoint, x + TAB_LABEL_OFFSET, y)
end
end
end
+113 -68
View File
@@ -56,7 +56,7 @@ function SkinChildren(frame, depth)
-- "...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)
Skin:Button(child)
end
elseif objType == "Frame" then
if IsDropDown(child, name) then
@@ -311,6 +311,111 @@ function SkinMenuRow(row)
UpdateRowSelection(row)
end
-- The Activate button inside an expanded specialization row draws its pill from
-- a single atlas across three BACKGROUND regions plus a HIGHLIGHT one, and
-- switches between the green and the grey variant by tex coord. There are no
-- Normal/Pushed/Disabled textures for HandleButton to clear (probed: both come
-- back nil, and every region's vertex colour is white in either state), so the
-- art is cleared by file the way the dropdown pills are (Skin:StripArtByFile,
-- which noops SetTexture per region as well -- whatever swaps the tex coord on
-- a state change is free to re-art the region), and the enabled/disabled
-- distinction the atlas was carrying has to be re-created on the label.
-- GetFontString covers the templated case; the scan is for a label that was
-- added as a plain region rather than set as the button's own font string.
local function ActivateLabel(button)
if button.CoALabel then return button.CoALabel end
local text = button.GetFontString and button:GetFontString()
if not text then
for i = 1, button:GetNumRegions() do
local region = select(i, button:GetRegions())
if region:GetObjectType() == "FontString" then
text = region
break
end
end
end
button.CoALabel = text
return text
end
-- Yellow live, grey dead: the same pair ElvUI's own templated buttons use, so
-- these read as buttons rather than as labels on a panel.
local ACTIVATE_COLOR = {1, 0.82, 0}
local ACTIVATE_DISABLED_COLOR = {0.55, 0.55, 0.55}
-- The active spec's button is disabled, and with the pill gone nothing else
-- says so -- the ElvUI panel is drawn the same either way. The label is greyed
-- instead, which is how ElvUI marks a dead button everywhere else.
--
-- The enabled state is compared against the cached one rather than written
-- blind because this runs from the button's update: there's no event for a spec
-- becoming active. Skin:IsEnabled does the 0/1-to-boolean normalisation this
-- client needs (see Skinning.lua).
local function UpdateActivateState(button)
local enabled = Skin:IsEnabled(button)
if enabled == button.CoAActivateEnabled then return end
button.CoAActivateEnabled = enabled
-- Activating a spec disables this button under the very cursor that just
-- clicked it, so its hover border would stay lit until the pointer moved
-- off. This update already watches the state, so it carries the border too.
Skin:RefreshButtonBorder(button)
local text = ActivateLabel(button)
if not text then return end
text:SetTextColor(unpack(enabled and ACTIVATE_COLOR or ACTIVATE_DISABLED_COLOR))
end
-- Stripped before templating, as with the dropdown pills: HandleButton adds its
-- backdrop as regions of this same button, so a strip afterwards takes the
-- backdrop with the pill.
local function SkinActivateButton(button)
if not button then return end
if not button.CoASkinned then
button.CoASkinned = true
Skin:StripArtByFile(button, Skin.RedButtonArt)
Skin:Button(button)
button:HookScript("OnUpdate", UpdateActivateState)
end
UpdateActivateState(button)
end
-- Unlike the row's icon these are named without a literal dot
-- ("Button1ExpandedContent", "...ExpandedContentActivateButton"), read off the
-- frame stack.
--
-- The expanded content exists from the moment the menu is built rather than
-- being created when its row is expanded (probed: present, hidden, before any
-- row had been opened), so it's picked up with the rest of the row. The show
-- hook is there for a row whose content is filled in later than this first pass.
local function SkinRowExpansion(rowName)
local content = _G[rowName.."ExpandedContent"]
if not content then return end
local buttonName = rowName.."ExpandedContentActivateButton"
SkinActivateButton(_G[buttonName])
if content.CoASkinned then return end
content.CoASkinned = true
content:HookScript("OnShow", function()
SkinActivateButton(_G[buttonName])
end)
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)
@@ -321,6 +426,7 @@ local function SkinMenuRows(listName)
SkinMenuRow(row)
SkinMenuRowIcon(_G[rowName..".SpecIcon"])
SkinRowExpansion(rowName)
end
end
@@ -365,67 +471,6 @@ local function SkinScrollThumb(thumb, thumbName)
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.
@@ -452,13 +497,13 @@ local function SkinMenuScroll(listName)
local up = _G[listName.."ScrollFrameScrollUpButton"]
if up then
up:Point("BOTTOM", scrollBar, "TOP", 0, 1)
SkinArrow(up, "up")
Skin:ScrollArrow(up, "up")
end
local down = _G[listName.."ScrollFrameScrollDownButton"]
if down then
down:Point("TOP", scrollBar, "BOTTOM", 0, -1)
SkinArrow(down, "down")
Skin:ScrollArrow(down, "down")
end
local thumbName = listName.."ScrollFrameScrollBarThumb"
@@ -499,7 +544,7 @@ local function SkinBottomBar()
-- 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)
Skin:Button(dropdown, nil, nil, true)
if dropdown.backdrop then
dropdown.backdrop:ClearAllPoints()
@@ -617,7 +662,7 @@ local function SkinSpecChoices()
local cardName = SPEC_CHOICE:format(i)
if not _G[cardName] then break end
S:HandleButton(_G[cardName.."SelectButton"], true)
Skin:Button(_G[cardName.."SelectButton"], true)
end
end
@@ -648,7 +693,7 @@ local function SkinFrame(frame)
-- 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:ApplyWindowScale("advancementScale")
Skin:Title(_G[FRAME_NAME.."TitleText"])
SkinCloseButton(self)
SkinBottomBar()
@@ -659,7 +704,7 @@ local function SkinFrame(frame)
end)
end
Skin:ApplyWindowScale("talentScale")
Skin:ApplyWindowScale("advancementScale")
Skin:Title(_G[FRAME_NAME.."TitleText"])
SkinCloseButton(frame)
SkinBottomBar()
+2 -2
View File
@@ -17,8 +17,8 @@ local FRAME_NAME = "StoreCollectionFrame"
-- 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"])
Skin:Button(_G[FRAME_NAME.."ActivateStoreButton"])
Skin:Button(_G[FRAME_NAME.."BuyStoreButton"])
end
local function SkinSearchBox()
+89 -3
View File
@@ -8,9 +8,94 @@ 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"])
Skin:Button(_G[FRAME_NAME.."PlayerModelSaveOutfitButton"])
Skin:Button(_G[FRAME_NAME.."DisableTransmogButton"])
Skin:Button(_G[FRAME_NAME.."DisableSpellVisualsButton"])
end
-- Apply, Cancel and Clear invalid slots only exist while a transmog change is
-- pending, which is why they were missed for so long -- but they exist from the
-- start rather than being created on the first change (probed: they are
-- Buttons, hidden, with nothing pending), so the normal skin pass reaches them.
--
-- Same widget as the talent frame's Activate button: the pill is drawn across
-- plain regions, with no Normal/Pushed texture for HandleButton to clear
-- (probed: they come back nil). Apply and Clear invalid slots take the green
-- variant of the gold atlas, Cancel is drawn from the red file instead --
-- Skin.RedButtonArt matches both files, and Cancel's HIGHLIGHT region goes with
-- the rest of its art.
--
-- The labels don't carry the atlas's green/red split over: the buttons sit in a
-- row and read apart by their text, and they take the same yellow every other
-- templated button on these frames uses -- the talent frame's live Activate
-- label included -- so a pending change doesn't get its own colour scheme. The
-- grey is the disabled half of that same pair.
local LABEL_COLOR = {1, 0.82, 0}
local DISABLED_LABEL_COLOR = {0.55, 0.55, 0.55}
-- Apply is disabled for as long as an invalid slot is pending, and with the
-- pill gone nothing else says so -- the ElvUI panel is drawn the same either
-- way. The label is greyed instead, exactly as the talent frame does it for the
-- active spec's Activate button.
--
-- Driven from the button's update because there's no event for the state
-- change: clearing the invalid slots enables Apply on the spot. The cached
-- state is what keeps that cheap -- the colours are only written when the state
-- actually flips.
local function UpdatePendingButtonState(button)
local enabled = Skin:IsEnabled(button)
if enabled == button.CoAPendingEnabled then return end
button.CoAPendingEnabled = enabled
-- Clearing the invalid slots enables Apply under the very cursor that just
-- clicked Clear invalid slots, so its hover border would stay dark until
-- the pointer moved off. This update already watches the state, so it
-- carries the border too.
Skin:RefreshButtonBorder(button)
local text = button.GetFontString and button:GetFontString()
if text then text:SetTextColor(unpack(enabled and LABEL_COLOR or DISABLED_LABEL_COLOR)) end
end
-- The strip runs on every pass rather than once behind the skinned guard: the
-- art is only there to be cleared once a change is pending, and nothing fires
-- the frame's OnShow at that point -- the window is already open. The buttons'
-- own OnShow is what catches it, since that is exactly when they appear.
local function SkinPendingButton(name)
local button = _G[name]
if not button then return end
Skin:StripArtByFile(button, Skin.RedButtonArt)
if not button.CoASkinned then
button.CoASkinned = true
-- Stripped before templating: HandleButton adds its backdrop as regions
-- of this same button, so a strip afterwards takes the backdrop with the
-- pill.
Skin:Button(button)
button:HookScript("OnShow", function(self)
Skin:StripArtByFile(self, Skin.RedButtonArt)
end)
button:HookScript("OnUpdate", UpdatePendingButtonState)
end
UpdatePendingButtonState(button)
end
local PENDING_BUTTONS = {
"PlayerModelApplyButton",
"PlayerModelCancelButton",
"PlayerModelClearInvalidButton",
}
local function SkinPendingButtons()
for _, suffix in ipairs(PENDING_BUTTONS) do
SkinPendingButton(FRAME_NAME..suffix)
end
end
local function SkinSearchBox()
@@ -59,6 +144,7 @@ local function SkinContents()
Skin:Title(_G[FRAME_NAME.."TitleText"])
Skin:CloseButton(_G[FRAME_NAME.."CloseButton"])
SkinActionButtons()
SkinPendingButtons()
SkinSearchBox()
SkinCollectionDropdown("Filter")
SkinCollectionDropdown("Sorting")
+4 -1
View File
@@ -4,10 +4,13 @@ ElvUI plugin that hides and skins the custom CoA (3.3.5) frames not covered by s
## Implemented
- Character advancement, vanity, wardrobe frames: skin, scale
- Challenges frame: skin
- Class resource frames: hiding
- Extra action button frame: skin, anchor, size
- Instance swap frame: skin, anchor, font settings
- Only highlight unitframes if dispellable by your CoA class, with manual talent switch.
- Interface/Video/Audio options: skin the widgets ElvUI's own pass misses
- Only highlight unitframes if dispellable by your CoA class.
## Installation
+69 -63
View File
@@ -18,23 +18,26 @@ local defaults = {
skins = {
extraActionButton = true,
instanceSwap = true,
talentFrames = {
collections = {
enable = true,
-- Multipliers on each frame's own scale, so 1 is "as the server
-- built it" rather than a fixed size.
talentScale = 1,
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,
},
extraActionButtonSize = 52,
instanceButtonFont = "PT Sans Narrow",
instanceButtonFontSize = 12,
instanceButtonFontOutline = "OUTLINE",
dispelHighlightOnlyMine = false,
hasBlightAntidote = false,
hasDevourCurse = false,
hasBurnImpurities = false,
dispelHighlightDetectTalents = true,
hideResourceSegmentBar = false,
hideResourceOrb = false,
hideResourceBar = false,
@@ -86,9 +89,9 @@ local function scaleOption(order, name, key)
min = 0.5,
max = 1.5,
step = 0.01,
get = function() return CoA.db.profile.skins.talentFrames[key] end,
get = function() return CoA.db.profile.skins.collections[key] end,
set = function(_, value)
CoA.db.profile.skins.talentFrames[key] = value
CoA.db.profile.skins.collections[key] = value
if CoA.UpdateFrameScales then
CoA:UpdateFrameScales()
@@ -239,25 +242,26 @@ local function getOptions()
-- 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 = {
-- 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 = "Talents",
name = "Advancement/Vanity/Wardrobe",
args = {
header = {
order = 1,
type = "header",
name = "Talents",
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.talentFrames.enable end,
get = function() return CoA.db.profile.skins.collections.enable end,
set = function(_, value)
CoA.db.profile.skins.talentFrames.enable = value
CoA.db.profile.skins.collections.enable = value
E:StaticPopup_Show("CONFIG_RL")
end,
},
@@ -266,15 +270,38 @@ local function getOptions()
type = "group",
inline = true,
name = "Scale",
disabled = function() return not CoA.db.profile.skins.talentFrames.enable end,
disabled = function() return not CoA.db.profile.skins.collections.enable end,
args = {
talentScale = scaleOption(1, "Talents", "talentScale"),
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,
},
},
},
},
},
classResources = {
@@ -404,55 +431,20 @@ local function getOptions()
end
end,
},
talents = {
detectTalents = {
order = 3,
type = "group",
inline = true,
name = "Talents",
args = {
hasBlightAntidote = {
order = 1,
type = "toggle",
name = string.format("Blight Antidote (%s)", LOCALIZED_CLASS_NAMES_MALE.PROPHET),
desc = "Grants Curse dispel.",
get = function() return CoA.db.profile.hasBlightAntidote end,
set = function(_, value)
CoA.db.profile.hasBlightAntidote = value
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,
},
hasDevourCurse = {
order = 2,
type = "toggle",
name = string.format("Devour Curse (%s)", LOCALIZED_CLASS_NAMES_MALE.CULTIST),
desc = "Grants Curse dispel.",
get = function() return CoA.db.profile.hasDevourCurse end,
set = function(_, value)
CoA.db.profile.hasDevourCurse = value
if CoA.UpdateDispelHighlight then
CoA:UpdateDispelHighlight()
end
end,
},
hasBurnImpurities = {
order = 3,
type = "toggle",
name = string.format("Burn Impurities (%s)", LOCALIZED_CLASS_NAMES_MALE.PYROMANCER),
desc = "Grants Magic, Disease, and Bleed dispel.",
get = function() return CoA.db.profile.hasBurnImpurities end,
set = function(_, value)
CoA.db.profile.hasBurnImpurities = value
if CoA.UpdateDispelHighlight then
CoA:UpdateDispelHighlight()
end
end,
},
},
if CoA.UpdateDispelHighlight then
CoA:UpdateDispelHighlight()
end
end,
},
},
},
@@ -499,7 +491,7 @@ function CoA:Initialize()
-- 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 skins.collections.enable then
if self.InitializeTalentFrame then
self:InitializeTalentFrame()
end
@@ -512,6 +504,20 @@ function CoA:Initialize()
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
-- No switch of its own: this one only fills the gaps ElvUI's own Blizzard
-- options pass leaves, so it's on wherever that pass is, and its own gate on
-- E.private.skins.blizzard.enable is the same one ElvUI checks.
if self.InitializeInterfaceOptions then
self:InitializeInterfaceOptions()
end
end
local function InitializeCallback()