From 0b9b0c5afaadc9f7e7d5b54cefe22a490cb1fadf Mon Sep 17 00:00:00 2001 From: Narcasung Date: Mon, 17 Aug 2026 17:15:29 +0200 Subject: [PATCH] Skin the CoA talent frame's Character Advancement tab Flattens the custom talent window into the ElvUI look: frame chrome, close buttons, tabs, bottom bar, dropdowns and their popups, the list scrollbars, and the spec choice cards' action buttons. The talent tree itself is left alone -- its node borders encode rank and availability. Most of the work is in keeping the skin applied. The frames restore their native art on refresh in three different ways, and none of them fire an event to hook: - The scroll arrows re-show a region StripTextures had hidden, so the arrow buttons re-hide it from their own update, matching by file so ElvUI's panel pieces (which are regions of the same button on this client) survive. - The pooled menu rows put their hover and selection art back when a scroll refills them, without firing OnShow, so the strip runs from the row update behind a cheap texture compare. - The talent frame's close button keeps its absolute frame level while TreeView rides along when the frame is raised, so the level bump re-applies on every show. Row fills and the selection edge are textures on the row rather than an ElvUI backdrop: the backdrop is a child frame and covered the row's icon and text at every frame level available to it. --- ElvUI_CoA.toc | 1 + Modules/TalentFrame.lua | 762 ++++++++++++++++++++++++++++++++++++++++ core.lua | 4 + 3 files changed, 767 insertions(+) create mode 100644 Modules/TalentFrame.lua diff --git a/ElvUI_CoA.toc b/ElvUI_CoA.toc index 6aa2a2f..46e309e 100644 --- a/ElvUI_CoA.toc +++ b/ElvUI_CoA.toc @@ -14,3 +14,4 @@ Modules\ExtraActionBar.lua Modules\LayerPicker.lua Modules\DispelHighlight.lua Modules\ClassResources.lua +Modules\TalentFrame.lua diff --git a/Modules/TalentFrame.lua b/Modules/TalentFrame.lua new file mode 100644 index 0000000..6a94c11 --- /dev/null +++ b/Modules/TalentFrame.lua @@ -0,0 +1,762 @@ +local E, L, V, P, G = unpack(ElvUI) +local S = E:GetModule("Skins") +local CoA = E:GetModule("CoA") + +local FRAME_NAME = "CoATalentFrame" +local TAB_NAME = "CollectionsPoolFrameCollectionTabTemplate%d" +local MAX_TABS = 10 + +-- 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 + + -- Strictly once: HandleCloseButton strips the button 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 + + -- 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 + + menu:StripTextures() + menu:SetTemplate("Transparent") + + -- Once only: HandleCloseButton strips the button on every call, + -- which blanks the X it added on the first pass. + local close = _G[menuName.."Close"] + if close then + S:HandleCloseButton(close) + end + + -- 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 + +-- S:HandleTab can't be used here: it clears the tab background by name, +-- looking for a "Middle" piece, and these tabs name theirs "Center", so the +-- body of the tab survives and the ElvUI backdrop just lands behind the old +-- art. The unnamed ARTWORK region is the tab's icon and is left alone. +local TAB_TEXTURES = {"Left", "Center", "Right", "LeftDisabled", "CenterDisabled", "RightDisabled"} + +-- These are CheckButtons, and clearing the checked texture along with the +-- rest of the art leaves nothing to mark the open tab, so selection is shown +-- with the ElvUI value colour on the backdrop border instead. +local function UpdateTabSelection(tab) + if not tab.backdrop then return end + + if tab:GetChecked() then + tab.backdrop:SetBackdropBorderColor(unpack(E.media.rgbvaluecolor)) + else + tab.backdrop:SetBackdropBorderColor(unpack(E.media.bordercolor)) + end +end + +local function SkinTab(tab) + if tab.CoASkinned then return end + tab.CoASkinned = true + + 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 + + -- Default rather than Transparent: these sit below the frame over open + -- world, so a see-through panel reads as washed out instead of as the + -- solid tabs the retail layout has. + tab:CreateBackdrop("Default") + tab.backdrop:Point("TOPLEFT", 6, -3) + tab.backdrop:Point("BOTTOMRIGHT", -6, 3) + tab:SetHitRectInsets(6, 6, 3, 3) + + hooksecurefunc(tab, "SetChecked", UpdateTabSelection) + UpdateTabSelection(tab) +end + +local function SkinTabs() + for i = 1, MAX_TABS do + local tab = _G[TAB_NAME:format(i)] + if not tab then break end + + SkinTab(tab) + end +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 + + frame:StripTextures() + + local nineSlice = _G[FRAME_NAME.."NineSlice"] + if nineSlice then + nineSlice:StripTextures() + nineSlice:Hide() + end + + -- The round portrait medallion overhangs the top-left corner and has + -- no flat equivalent, so it goes rather than getting reskinned. + local portrait = _G[FRAME_NAME.."PortraitFrame"] + if portrait then + portrait:StripTextures() + portrait:Hide() + end + + frame:SetTemplate("Transparent") + + -- 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) + SkinCloseButton(self) + SkinBottomBar() + CropBackground() + SkinChildren(self, 1) + SkinTabs() + SkinSpecChoices() + end) + end + + SkinCloseButton(frame) + SkinBottomBar() + CropBackground() + 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 + if TryHook() then return end + + self.talentFrameTimer = self:ScheduleRepeatingTimer(function() + if TryHook() then + self:CancelTimer(self.talentFrameTimer) + self.talentFrameTimer = nil + end + end, 0.5) +end diff --git a/core.lua b/core.lua index 6bfccec..98bc907 100644 --- a/core.lua +++ b/core.lua @@ -364,6 +364,10 @@ function CoA:Initialize() if self.InitializeClassResources then self:InitializeClassResources() end + + if self.InitializeTalentFrame then + self:InitializeTalentFrame() + end end local function InitializeCallback()