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