Compare commits
17 Commits
964f88559d
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e66a7a6354 | |||
| 63e179e96e | |||
| 013fe665ab | |||
| d9476af220 | |||
| cafc3ce28f | |||
| a8541fa1ee | |||
| 1a62b4ed1f | |||
| 13f36e7817 | |||
| 919feed322 | |||
| d2e55eb3cb | |||
| cddfc7e7af | |||
| 58fae4418a | |||
| cc6cd4028b | |||
| 6bf3864891 | |||
| d2005c4725 | |||
| 76413b6828 | |||
| 01f1f5c7c3 |
@@ -18,3 +18,5 @@ Modules\Skinning.lua
|
||||
Modules\TalentFrame.lua
|
||||
Modules\VanityFrame.lua
|
||||
Modules\WardrobeFrame.lua
|
||||
Modules\ChallengesFrame.lua
|
||||
Modules\InterfaceOptions.lua
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user