From dc263a2419fecf412fc9af2d3a4054420665b2e2 Mon Sep 17 00:00:00 2001 From: Narcasung Date: Mon, 17 Aug 2026 21:54:30 +0200 Subject: [PATCH] Skin the CoA Wardrobe tab (AppearanceWardrobeFrame) Outer panel, close button, and the top-left portrait medallion get the same first-pass treatment as Vanity/Talent, plus the item collection panel: native background/border art stripped from Collection and its InsetOverlay/ShadowOverlay decoration, the 3D model preview's vanilla border removed, and the action buttons, search box, Filter/Order By dropdowns, page arrows, and category tabs skinned to match. Left native for a later pass: the icon grid contents, item slot buttons, and character model itself. --- ElvUI_CoA.toc | 1 + Modules/WardrobeFrame.lua | 300 ++++++++++++++++++++++++++++++++++++++ core.lua | 4 + 3 files changed, 305 insertions(+) create mode 100644 Modules/WardrobeFrame.lua diff --git a/ElvUI_CoA.toc b/ElvUI_CoA.toc index 10396af..5a3f7c0 100644 --- a/ElvUI_CoA.toc +++ b/ElvUI_CoA.toc @@ -16,3 +16,4 @@ Modules\DispelHighlight.lua Modules\ClassResources.lua Modules\TalentFrame.lua Modules\VanityFrame.lua +Modules\WardrobeFrame.lua diff --git a/Modules/WardrobeFrame.lua b/Modules/WardrobeFrame.lua new file mode 100644 index 0000000..5c91dc1 --- /dev/null +++ b/Modules/WardrobeFrame.lua @@ -0,0 +1,300 @@ +local E, L, V, P, G = unpack(ElvUI) +local S = E:GetModule("Skins") +local CoA = E:GetModule("CoA") + +local FRAME_NAME = "AppearanceWardrobeFrame" + +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. + if not close.CoASkinned then + close.CoASkinned = true + + S:HandleCloseButton(close) + end +end + +-- 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 + +-- Filter and Order By share Vanity's exact dropdown art -- same nine +-- anonymous "Silver-Button" slices, confirmed by probe, that S:HandleButton +-- can't reach on its own, plus the same native mouse-down re-art problem on +-- click (SetTexture noop'd per region after clearing, same trick). +local DROPDOWN_ART_PATTERNS = {"Silver%-Button"} + +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 then + for _, pattern in ipairs(DROPDOWN_ART_PATTERNS) do + if tostring(texture):find(pattern) then + region:SetTexture(nil) + region.SetTexture = E.noop + break + end + end + end + end +end + +-- suffix is "Filter" or "Sorting" -- both are CollectionX buttons with a +-- matching CollectionXMenu popout (confirmed by probe), same shape as +-- Vanity's single dropdown. +local function SkinCollectionDropdown(suffix) + local dropdown = _G[FRAME_NAME.."Collection"..suffix] + if not dropdown or dropdown.CoASkinned then return end + dropdown.CoASkinned = true + + StripDropdownArt(dropdown) + + S:HandleButton(dropdown) + + -- The caret is a plain OVERLAY texture on the dropdown itself, not a + -- separate button, so it can't go through HandleNextPrevButton -- it's + -- retextured directly instead. + 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("ChatFrameExpandArrow") then + -- No ArrowDown asset exists -- every other direction in ElvUI is + -- ArrowUp rotated, so this matches that convention. + region:SetTexture(E.Media.Textures.ArrowUp) + region:SetVertexColor(1, 1, 1) + region:SetTexCoord(0, 1, 0, 1) + region:SetRotation(S.ArrowRotation.down) + region:SetSize(14, 14) + end + end + + local menu = _G[FRAME_NAME.."Collection"..suffix.."Menu"] + if menu and not menu.CoASkinned then + menu.CoASkinned = true + + -- Panel only, matching how Vanity's own popup menu is treated -- the + -- option rows inside are a later pass. + menu:StripTextures() + menu:SetTemplate("Transparent") + end +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 Left/Center/Right art naming as the talent frame's tabs (confirmed by +-- probe), but these are proper descendants of Collection rather than +-- separately-placed siblings, so the frame-level bump Talent needs to clear +-- its own frame's regions doesn't apply here -- normal parent/child z-order +-- already puts them on top. +local TAB_TEXTURES = {"Left", "Center", "Right"} + +local function StripTab(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 + +-- Only the tab being switched to gets any kind of hook fired -- the ones +-- switched away from never fire OnClick/OnShow/SetChecked again, but their +-- native art still comes back, so there's no event to catch it from. Same +-- fix as the talent frame's tabs: a cheap OnUpdate texture check, only pays +-- for the full strip when the native art has actually reappeared. +local function UpdateTabArt(tab) + local name = tab:GetName() + local tex = name and _G[name.."Left"] + + if tex and tex:GetTexture() then + StripTab(tab) + end +end + +local function SkinTab(tab) + if not tab.CoASkinned then + tab.CoASkinned = true + + tab:CreateBackdrop("Default") + tab.backdrop:Point("TOPLEFT", 2, -2) + tab.backdrop:Point("BOTTOMRIGHT", -2, 2) + + tab:HookScript("OnUpdate", UpdateTabArt) + end + + StripTab(tab) +end + +-- 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 + local tab = _G[FRAME_NAME.."CollectionPoolFrameAppearanceTypeTabTemplate"..i] + if tab then SkinTab(tab) end + end +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. + local nineSlice = _G[FRAME_NAME.."NineSlice"] + if nineSlice then + nineSlice:StripTextures() + nineSlice:Hide() + end + + -- 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. + local portrait = _G[FRAME_NAME.."PortraitFrame"] + if portrait then + portrait:StripTextures() + portrait:Hide() + end + + -- 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. Strip it first, THEN apply the template -- SetTemplate + -- adds its own backdrop as real texture regions on this same frame, so + -- stripping afterward wiped the backdrop right back off (outer panel + -- came out fully invisible). + frame:StripTextures() + frame:SetTemplate("Transparent") + + -- 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. + local insetOverlayNineSlice = _G[FRAME_NAME.."CollectionInsetOverlayNineSlice"] + if insetOverlayNineSlice then + insetOverlayNineSlice:StripTextures() + insetOverlayNineSlice:Hide() + end + + -- 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. + local collection = _G[FRAME_NAME.."Collection"] + if collection then + collection:StripTextures() + collection:SetTemplate("Transparent") + end + + -- 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 + modelBorder:StripTextures() + modelBorder:Hide() + end + + local shadowOverlay = _G[FRAME_NAME.."CollectionShadowOverlay"] + if shadowOverlay then + shadowOverlay:StripTextures() + shadowOverlay:Hide() + end + + frame:HookScript("OnShow", function(self) + SkinCloseButton(self) + SkinActionButtons() + SkinSearchBox() + SkinCollectionDropdown("Filter") + SkinCollectionDropdown("Sorting") + SkinPagerArrows() + SkinCategoryTabs() + end) + end + + SkinCloseButton(frame) + SkinActionButtons() + SkinSearchBox() + SkinCollectionDropdown("Filter") + SkinCollectionDropdown("Sorting") + SkinPagerArrows() + SkinCategoryTabs() +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 + if TryHook() then return end + + -- Frame is created on-demand by its owning addon, the instant the player + -- first opens it -- a poll can't catch that before 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 here lands before the first-ever :Show(). Confirmed in-game on + -- the talent frame's identical pattern; see TalentFrame.lua. + local loader = CreateFrame("Frame") + loader:RegisterEvent("ADDON_LOADED") + loader:SetScript("OnEvent", function(self) + if TryHook() then + self:UnregisterEvent("ADDON_LOADED") + end + end) +end diff --git a/core.lua b/core.lua index c34155f..6b86945 100644 --- a/core.lua +++ b/core.lua @@ -386,6 +386,10 @@ function CoA:Initialize() if self.InitializeVanityFrame then self:InitializeVanityFrame() end + + if self.InitializeWardrobeFrame then + self:InitializeWardrobeFrame() + end end local function InitializeCallback()