Release 3.8.7: unit frame colors and theme fixes

This commit is contained in:
2026-11-08 19:32:00 -04:00
parent e43d93a524
commit 6fc566feaf
10 changed files with 306 additions and 23 deletions
+7
View File
@@ -4,6 +4,13 @@ Changes in OEVG/ShaguTweaks-ClassicAPI, newest first. Entries summarize released
Based on [Dusk/ShaguTweaks-ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-ClassicAPI) at `4391564` (2026-09-08). Earlier history is available upstream.
## 3.8.7 — 2026-11-08 — Unit frames and minimap buttons
- Add optional owner class colors for pet health bars, including targeted pets with identifiable owners.
- Add an option to remove the class-colored background behind your player name, including on the detached name frame.
- Apply Darkened and Golden UI tints to the Outfitter, TrinketMenu and Atlas-CFM minimap buttons.
- Preserve the Spellbook's original parchment and torn edges in Darkened and Golden UI.
## 3.8.6 — 2026-11-06 — Module fixes and native timers
- Prevent recursive merchant-click hooks when Buy Em All runs alongside another merchant addon.
+1 -1
View File
@@ -7,7 +7,7 @@ English-only fork of [Dusk/ShaguTweaks-ClassicAPI](https://octowow.st/git/Dusk/S
Focused on **stability, compatibility, customization, and performance** while keeping the original ShaguTweaks experience.
**Version 3.8.6.** What changed in each update: see the [changelog](CHANGELOG.md).
**Version 3.8.7.** What changed in each update: see the [changelog](CHANGELOG.md).
> **Upgrading to 3.7:** the modules of the separate *ShaguTweaks Extras ClassicAPI* addon are now built into ShaguTweaks. **Disable or delete `ShaguTweaks-extras`** after updating, otherwise its older copies replace the built-in ones. Your on/off settings carry over. Raid Frames, Bag Item Click, Bag Search Bar and Auction Alt-Buy were not merged. See [Docs/EXTRAS_MERGE.md](Docs/EXTRAS_MERGE.md) for where each module went and why some were left out. See the [migration guide](Docs/EXTRAS_MERGE.md#removed-later) for removed modules.
+1 -1
View File
@@ -2,7 +2,7 @@
## Title: |cffffcc00Shagu|cffffffffTweaks |cffcccccc[ClassicAPI]
## Author: Shagu
## Notes: A collection of tweaks and enhancements to the Default UI
## Version: 3.8.6
## Version: 3.8.7
## Dependencies: !!!ClassicAPI
## SavedVariables: ShaguTweaks_player_cache, ShaguTweaks_social_cache
## SavedVariablesPerCharacter: ShaguTweaks_config, ShaguTweaks_cache
+65 -5
View File
@@ -73,7 +73,6 @@ local regionskips = {
}
local backgrounds = {
["^SpellBookFrame$"] = { 325, 355, 17, -74 },
["^ItemTextFrame$"] = { 300, 355, 24, -74 },
}
@@ -276,6 +275,40 @@ local function IsExcludedFrame(frame)
return name and string.find(name, "TrinketMenu", 1, true) == 1
end
-- The Spellbook's metal frame and torn paper share four 256px-high tiles.
local spellbookPaper = {
topleft = {22, 74, 256, 256, 256},
topright = {0, 74, 85, 256, 128},
botleft = {22, 0, 256, 175, 256},
botright = {0, 0, 85, 175, 128},
}
local function RestoreSpellbookParchment(frame, source)
if source.stSpellbookPaper then return end
local path = source:GetTexture()
if type(path) ~= "string" then return end
local _, _, side = string.find(string.lower(path), "^interface\\spellbook\\ui%-spellbookpanel%-(%a+)$")
local crop = spellbookPaper[side]
if not crop then return end
local width, height = source:GetWidth(), source:GetHeight()
if not width or width <= 0 or not height or height <= 0 then return end
-- Keep the art below the paper and the page number above it.
source:SetDrawLayer("BORDER")
if _G.SpellBookPageText then _G.SpellBookPageText:SetDrawLayer("OVERLAY") end
local paper = frame:CreateTexture(nil, "ARTWORK")
paper.stOriginalQuestPaper = true
paper:SetTexture(path)
paper:SetVertexColor(1, 1, 1, 1)
local x1, y1, x2, y2, tileWidth = unpack(crop)
local sx, sy = width / tileWidth, height / 256
paper:SetTexCoord(x1 / tileWidth, x2 / tileWidth, y1 / 256, y2 / 256)
paper:SetWidth((x2 - x1) * sx)
paper:SetHeight((y2 - y1) * sy)
paper:SetPoint("TOPLEFT", source, "TOPLEFT", x1 * sx, -y1 * sy)
source.stSpellbookPaper = paper
end
local function RestoreQuestParchment(frame)
local name = frame.GetName and frame:GetName()
if not name or not (string.find(name, "^QuestFrame.*Panel$")
@@ -435,6 +468,7 @@ local function DarkenFrame(frame, r, g, b, a, scan)
else
if capture then capture.textureWrites = capture.textureWrites + 1 end
region:SetVertexColor(r,g,b,a)
if name == "SpellBookFrame" then RestoreSpellbookParchment(frame, region) end
end
end
end
@@ -577,10 +611,24 @@ local function FindAddonFrames(frame)
end
end
-- The round border of addon minimap buttons (most use the stock tracking
-- border texture). Those buttons are often made after login.
-- These buttons need explicit discovery when created by XML or nested below
-- a minimap child frame. Outfitter and TrinketMenu bake the rim into their art.
local addonMinimapButtons = {
OutfitterMinimapButton = true,
TrinketMenu_IconFrame = true,
AtlasCFMMinimapButton = true,
}
local function TintMinimapButton(frame)
if not frame or not frame.GetRegions then return end
if not tint or not frame or not frame.GetRegions then return end
local name = frame.GetName and frame:GetName()
if name == "OutfitterMinimapButton" or name == "TrinketMenu_IconFrame" then
local normal = frame:GetNormalTexture()
local pushed = frame:GetPushedTexture()
if normal then normal:SetVertexColor(tint.r, tint.g, tint.b, 1) end
if pushed then pushed:SetVertexColor(tint.r, tint.g, tint.b, 1) end
return
end
if IsExcludedFrame(frame) then return end
local regions = { frame:GetRegions() }
for i = 1, table.getn(regions) do
@@ -592,6 +640,10 @@ local function TintMinimapButton(frame)
end
end
local function TintLoadedMinimapButton(name)
HookAddonOrVariable(name, function() TintMinimapButton(_G[name]) end)
end
local function RecolorOnShow(frame)
if not frame or frame.stRecolorOnShow then return end
frame.stRecolorOnShow = true
@@ -620,6 +672,14 @@ local function LockAddonFrames()
local buttons = { Minimap:GetChildren() }
for i = 1, table.getn(buttons) do TintMinimapButton(buttons[i]) end
end
for name in pairs(addonMinimapButtons) do
local button = _G[name]
if not button then
TintLoadedMinimapButton(name)
elseif button:GetParent() ~= Minimap then
TintMinimapButton(button)
end
end
-- frames those addons create later: named after the addon, or created
-- inside one of their frames (templates bring their children along)
@@ -637,7 +697,7 @@ local function LockAddonFrames()
end
end
-- minimap buttons get their border texture right after they are made
if frame and a3 and a3 == Minimap then
if frame and ((a3 and a3 == Minimap) or addonMinimapButtons[a2]) then
if _G.C_Timer and _G.C_Timer.After then
_G.C_Timer.After(0, function() TintMinimapButton(frame) end)
end
+93
View File
@@ -19,6 +19,95 @@ local function GetUnitClassColor(unit)
return palette[class] or FALLBACK_CLASS_COLOR
end
local playerNameOption = ShaguTweaks:register({
title = T["Player's Name No Class Color"],
description = T["Removes the class-colored strip behind your player name, including the separate name frame."],
expansions = { ["vanilla"] = true },
category = T["Unit Frames"],
enable = function() end, -- Read by both player name layouts at startup.
})
local petColors = ShaguTweaks:register({
title = T["Pets Use Owner Class Color"],
description = T["Colors pet health bars by their owners' classes, including targeted pets whose owners can be identified."],
expansions = { ["vanilla"] = true },
category = T["Unit Frames"],
})
petColors.enable = function()
local refresh = {}
local function AddPetBar(bar, pet, owner)
if not bar then return end
local setColor = bar.SetStatusBarColor
local function Apply()
if not UnitExists(pet) or not UnitExists(owner) then return end
local color = GetUnitClassColor(owner)
setColor(bar, color.r, color.g, color.b, 1)
end
refresh[owner] = Apply
ShaguTweaks.hooksecurefunc(bar, "SetStatusBarColor", Apply)
Apply()
end
AddPetBar(PetFrameHealthBar, "pet", "player")
for id = 1, MAX_PARTY_MEMBERS do
AddPetBar(_G["PartyMemberFrame" .. id .. "PetFrameHealthBar"], "partypet" .. id, "party" .. id)
end
local function GetTargetOwner()
if not UnitExists("target") then return end
if UnitIsUnit("target", "pet") then return "player" end
for id = 1, MAX_PARTY_MEMBERS do
if UnitIsUnit("target", "partypet" .. id) then return "party" .. id end
end
local api = ShaguTweaks.API
if api and api.UnitTokenFromGUID and _G.UnitIsPet and _G.UnitOwnerGUID and _G.UnitIsPet("target") then
local guid = _G.UnitOwnerGUID("target")
local owner = guid and api.UnitTokenFromGUID(guid)
if owner and UnitIsPlayer(owner) then return owner end
end
end
local function RefreshTarget() end
if TargetFrameHealthBar then
local bar = TargetFrameHealthBar
local setColor = bar.SetStatusBarColor
local r, g, b, a = bar:GetStatusBarColor()
local targetColor
RefreshTarget = function()
local owner = GetTargetOwner()
local previous = targetColor
targetColor = owner and GetUnitClassColor(owner)
if targetColor then
setColor(bar, targetColor.r, targetColor.g, targetColor.b, 1)
elseif previous then
setColor(bar, r, g, b, a or 1)
end
end
-- Big Health installs its setter hook later. Calls through the captured
-- setter still reach this hook, so the owner color wins over reaction blue.
ShaguTweaks.hooksecurefunc(bar, "SetStatusBarColor", function(_, red, green, blue, alpha)
r, g, b, a = red, green, blue, alpha
if targetColor then setColor(bar, targetColor.r, targetColor.g, targetColor.b, 1) end
end)
ShaguTweaks.hooksecurefunc("TargetFrame_CheckFaction", RefreshTarget)
RefreshTarget()
end
local events = CreateFrame("Frame")
events:RegisterEvent("UNIT_PET")
events:RegisterEvent("PARTY_MEMBERS_CHANGED")
events:RegisterEvent("PLAYER_ENTERING_WORLD")
events:SetScript("OnEvent", function()
if event == "UNIT_PET" then
local apply = refresh[arg1]
if apply then apply() end
else
for _, apply in pairs(refresh) do apply() end
end
RefreshTarget()
end)
end
local function SetTextColorIfChanged(fontString, r, g, b, a)
local cr, cg, cb, ca = fontString:GetTextColor()
if cr ~= r or cg ~= g or cb ~= b or ca ~= a then
@@ -74,6 +163,10 @@ module.enable = function(self)
PlayerFrameNameBackground:SetHeight(19)
PlayerFrameNameBackground:SetPoint("TOPLEFT", 106, -22)
PlayerFrameNameBackground:SetVertexColor(playerColor.r, playerColor.g, playerColor.b, 1)
-- Big Health still reads this texture's class tint when its artwork is hidden.
if ShaguTweaks_config and ShaguTweaks_config[playerNameOption.title] == 1 then
PlayerFrameNameBackground:Hide()
end
local wait = CreateFrame("Frame")
wait:RegisterEvent("PLAYER_ENTERING_WORLD")
+16 -14
View File
@@ -202,20 +202,22 @@ module.enable = function(self)
playerName:ClearAllPoints()
playerName:SetPoint("CENTER", name, "CENTER", 0, 0)
end
-- Big Health deliberately hides the stock name background. Keep its
-- color source for that module, but draw a separate class strip here.
name.classBackground = name:CreateTexture(nil, "BACKGROUND")
name.classBackground:SetPoint("TOPLEFT", name, "TOPLEFT", 4, -3)
name.classBackground:SetPoint("BOTTOMRIGHT", name, "BOTTOMRIGHT", -4, 3)
local healthTexture = PlayerFrameHealthBar:GetStatusBarTexture()
name.classBackground:SetTexture(healthTexture and healthTexture:GetTexture()
or "Interface\\TargetingFrame\\UI-StatusBar")
local _, class = UnitClass("player")
local palette = ShaguTweaks.ModernClassColors or RAID_CLASS_COLORS
local classColor = palette and palette[class]
local cr, cg, cb = PlayerFrameHealthBar:GetStatusBarColor()
name.classBackground:SetVertexColor(classColor and classColor.r or cr,
classColor and classColor.g or cg, classColor and classColor.b or cb, 1)
if ShaguTweaks_config[T["Player's Name No Class Color"]] ~= 1 then
-- Big Health deliberately hides the stock name background. Keep its
-- color source for that module, but draw a separate class strip here.
name.classBackground = name:CreateTexture(nil, "BACKGROUND")
name.classBackground:SetPoint("TOPLEFT", name, "TOPLEFT", 4, -3)
name.classBackground:SetPoint("BOTTOMRIGHT", name, "BOTTOMRIGHT", -4, 3)
local healthTexture = PlayerFrameHealthBar:GetStatusBarTexture()
name.classBackground:SetTexture(healthTexture and healthTexture:GetTexture()
or "Interface\\TargetingFrame\\UI-StatusBar")
local _, class = UnitClass("player")
local palette = ShaguTweaks.ModernClassColors or RAID_CLASS_COLORS
local classColor = palette and palette[class]
local cr, cg, cb = PlayerFrameHealthBar:GetStatusBarColor()
name.classBackground:SetVertexColor(classColor and classColor.r or cr,
classColor and classColor.g or cg, classColor and classColor.b or cb, 1)
end
if ShaguTweaks_config[T["Hide Player Name"]] == 1 then name:Hide() end
-- Keep native bar objects and events alive. Copies may still mirror them.
+6 -1
View File
@@ -1,5 +1,5 @@
local root=(arg[1] or '.') .. '/'
local function Check(big,timer,hideResources,druid)
local function Check(big,timer,hideResources,druid,noNameColor)
local modules,frames,pending={},{},{}
local methods={}
local function noop() end
@@ -112,6 +112,7 @@ local function Check(big,timer,hideResources,druid)
function methods:StartSizing(direction) self.sizing=direction end
ShaguTweaks.Abbreviate=function(v) return tostring(v) end
ShaguTweaks_config={['Hide Player Frame Resources']=hideResources and 1 or 0,
["Player's Name No Class Color"]=noNameColor and 1 or 0,
['Unit Frame Big Health for Player']=big and 1 or 0,
PersonalResourceDisplay={Health={x=420,y=520,width=180,height=40}}}
local nativeHeight=PlayerFrameHealthBar:GetHeight()
@@ -121,6 +122,9 @@ local function Check(big,timer,hideResources,druid)
local ready=frames[table.getn(frames)]
fire(ready,'OnEvent');fire(ready,'OnUpdate')
if timer then pending[1]();pending={} else fire(ready,'OnUpdate') end
local name=ShaguTweaks.PlayerFrameResourcesLayout.name
assert((name.classBackground~=nil)==not noNameColor)
assert(PlayerFrame.name:GetParent()==name)
end
local parent=PlayerFrameHealthBar:GetParent()
local alpha=PlayerFrameHealthBar.alpha
@@ -198,6 +202,7 @@ end
Check(false,true,false);Check(true,true,false);Check(false,true,true);Check(true,true,true)
Check(false,false,false);Check(true,false,true)
Check(false,true,false,true);Check(true,true,true,true)
Check(true,true,true,true,true)
-- Migration preserves explicit new choices and old health/power positions.
local f=assert(io.open(root..'main.lua'));local s=f:read('*a');f:close()
local a=assert(string.find(s,' -- Replace detached native bars',1,true))
+71 -1
View File
@@ -5,7 +5,11 @@ ShaguTweaks={T=setmetatable({},{__index=function(t,k) return k end})}
function ShaguTweaks.GetGlobalEnv() return _G end
function ShaguTweaks:register(module) modules[module.title]=module; return module end
function ShaguTweaks.hooksecurefunc(object,name,callback)
if type(object)~='table' then return end
if type(object)=='string' then
local original=_G[object]
_G[object]=function(...) original(unpack(arg));name(unpack(arg)) end
return
end
local original=object[name]; object[name]=function(self,a,b,c,d) original(self,a,b,c,d); callback(self,a,b,c,d) end
end
MAX_PARTY_MEMBERS=0
@@ -24,6 +28,72 @@ this=deferred.frame; deferred.callback(); this=nil
local expected='Interface\\AddOns\\ShaguTweaks\\img\\BlizzardRetailBar'
assert(PlayerFrameNameBackground.path==expected and TargetFrameNameBackground.path==expected and PlayerFrameHealthBar.path==expected)
print('Lua 5.0: matching modern name/health artwork passed')
local function healthBar()
return { SetStatusBarColor=function(self,r,g,b) self.color={r,g,b} end }
end
local units={player='WARLOCK', pet='WARRIOR', party1='HUNTER', partypet1='WARRIOR'}
function UnitClass(unit) return units[unit],units[unit] end
local targetUnit
function UnitExists(unit) return units[unit=='target' and targetUnit or unit]~=nil end
function UnitIsUnit(first,second) return first=='target' and targetUnit==second end
function UnitIsPlayer(unit) return unit=='party1' end
function UnitIsPet(unit) return unit=='target' and targetUnit=='remotePet' end
function UnitOwnerGUID() return 'owner-guid' end
ShaguTweaks.API={UnitTokenFromGUID=function(guid) if guid=='owner-guid' then return 'party1' end end}
PetFrameHealthBar=healthBar()
PartyMemberFrame1PetFrameHealthBar=healthBar()
TargetFrameHealthBar=healthBar()
TargetFrameHealthBar:SetStatusBarColor(0,1,0)
function TargetFrameHealthBar:GetStatusBarColor() return unpack(self.color) end
MAX_PARTY_MEMBERS=1
local scripts={}
function CreateFrame() return {RegisterEvent=function() end, SetScript=function(_,k,v) scripts[k]=v end} end
modules['Pets Use Owner Class Color']:enable()
assert(PetFrameHealthBar.color[3]==.93 and PartyMemberFrame1PetFrameHealthBar.color[1]==.67)
PetFrameHealthBar:SetStatusBarColor(0,1,0)
assert(PetFrameHealthBar.color[3]==.93)
targetUnit='pet';TargetFrame_CheckFaction()
assert(TargetFrameHealthBar.color[3]==.93)
-- Big Health captures the setter and applies the faction tint through it.
local bigHealthSetter=TargetFrameHealthBar.SetStatusBarColor
bigHealthSetter(TargetFrameHealthBar,0,0,1,1)
assert(TargetFrameHealthBar.color[3]==.93)
targetUnit='partypet1';TargetFrame_CheckFaction()
assert(TargetFrameHealthBar.color[1]==.67)
units.remotePet='WARRIOR'
targetUnit='remotePet';TargetFrame_CheckFaction()
assert(TargetFrameHealthBar.color[1]==.67)
TargetFrameHealthBar:SetStatusBarColor(0,1,0)
units.npc='WARRIOR'
targetUnit='npc';TargetFrame_CheckFaction()
assert(TargetFrameHealthBar.color[2]==1 and TargetFrameHealthBar.color[3]==0)
units.party1='WARLOCK'
event,arg1='UNIT_PET','party1';scripts.OnEvent()
assert(PartyMemberFrame1PetFrameHealthBar.color[3]==.93)
units.partypet1=nil
PartyMemberFrame1PetFrameHealthBar:SetStatusBarColor(0,1,0)
assert(PartyMemberFrame1PetFrameHealthBar.color[2]==1)
function UnitReaction() return 5 end
function UnitIsPlayer() return false end
local name={GetFont=function() return 'font',12 end, SetFont=function() end}
PlayerFrame={name=name,CreateTexture=function()
local strip=texture()
strip.SetWidth=function() end;strip.SetHeight=function() end;strip.SetPoint=function() end
strip.Hide=function(self) self.hidden=true end
return strip
end}
TargetFrame={name=name}
TargetFrameNameBackground.Hide=function() end
for _,hide in ipairs({0,1}) do
ShaguTweaks_config={["Player's Name No Class Color"]=hide}
modules['Unit Frame Class Colors']:enable()
assert((PlayerFrameNameBackground.hidden or false)==(hide==1))
assert(PlayerFrameNameBackground.r==.53)
end
print('Player name and pet options: hidden name strip keeps its class tint for Big Health, pet bars follow their owners')
-- Hiding the player name never affects the grouped player frame.
dofile(root..'mods/unitframes-hide-player-name.lua')
local nameHidden=false
+26
View File
@@ -14,7 +14,33 @@ function methods:SetHeight(v) self.height=v end
function methods:SetPoint(...) self.point=arg end
function methods:SetTexCoord(...) self.uv=arg end
function methods:SetVertexColor(...) self.color=arg end
function methods:SetDrawLayer(layer) self.layer=layer end
local function texture(path,w,h) return setmetatable({path=path,width=w,height=h},{__index=methods}) end
local spellbookStart=assert(string.find(source,'local spellbookPaper =',1,true))
local restoreSpellbook=assert(loadstring(string.sub(source,spellbookStart,a-1)..'return RestoreSpellbookParchment'))()
SpellBookPageText=texture()
local spellbook={copies=0}
function spellbook:CreateTexture(_,layer)
self.copies=self.copies+1
local paper=texture();paper:SetDrawLayer(layer);return paper
end
for _,suffix in ipairs({'TopLeft','TopRight','BotLeft','BotRight'}) do
local left=string.find(suffix,'Left',1,true)
local top=string.find(suffix,'Top',1,true)
local tile=texture('Interface\\Spellbook\\UI-SpellbookPanel-'..suffix,left and 256 or 128,256)
restoreSpellbook(spellbook,tile)
restoreSpellbook(spellbook,tile)
local paper=assert(tile.stSpellbookPaper)
assert(paper.path==tile.path and paper.color[1]==1 and paper.color[4]==1)
assert(paper.width==(left and 234 or 85) and paper.height==(top and 182 or 175))
assert(paper.point[2]==tile and paper.point[4]==(left and 22 or 0) and paper.point[5]==(top and -74 or 0))
assert(tile.layer=='BORDER' and paper.layer=='ARTWORK' and SpellBookPageText.layer=='OVERLAY')
end
restoreSpellbook(spellbook,texture('Interface\\Spellbook\\Spellbook-Icon',58,58))
assert(spellbook.copies==4)
assert(not string.find(source,'["^SpellBookFrame$"]',1,true))
for _,name in ipairs({'GossipFrameGreetingPanel','QuestFrameGreetingPanel','QuestFrameDetailPanel','QuestFrameProgressPanel','QuestFrameRewardPanel'}) do
local regions={}
for _,suffix in ipairs({'TopLeft','TopRight','BotLeft','BotRight'}) do
+20
View File
@@ -61,3 +61,23 @@ helpers[3](parent)
helpers[4](probe)
assert(not matches('TrinketMenu_MainFrame'))
print('Active palettes: TrinketMenu recursive, persistent-hook and minimap recoloring paths are excluded')
local function texture(path)
return { GetTexture = function() return path end,
SetVertexColor = function(self, r, g, b) self.color = {r, g, b} end }
end
for _, name in ipairs({"TrinketMenu_IconFrame", "OutfitterMinimapButton"}) do
local normal, pressed = texture(), texture()
helpers[4]({
GetName = function() return name end,
GetNormalTexture = function() return normal end,
GetPushedTexture = function() return pressed end,
GetRegions = function() error("Only the baked button artwork should be tinted") end,
})
assert(normal.color[2] == .78 and pressed.color[3] == .4)
end
local atlasIcon = texture("Interface\\WorldMap\\WorldMap-Icon")
local atlasRim = texture("Interface\\Minimap\\MiniMap-TrackingBorder")
helpers[4]({GetName = function() return "AtlasCFMMinimapButton" end,
GetRegions = function() return atlasIcon, atlasRim end})
assert(not atlasIcon.color and atlasRim.color[2] == .78)