Release 3.8.5: UI polish and initialization cleanup

This commit is contained in:
2026-11-06 11:40:25 -04:00
parent a81ca54e0c
commit 9101a0b32f
16 changed files with 704 additions and 69 deletions
+9
View File
@@ -4,6 +4,15 @@ 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.5 — 2026-11-06 — UI polish and initialization cleanup
- Replace the theme checkboxes with one **UI:** dropdown below Search: Default, Darkened or Golden, preserving existing choices.
- Add a version button beside Defaults in Advanced Options, opening a copyable Gitea project link.
- Match equipped-item comparison backgrounds to the source tooltip in Darkened UI.
- Preserve native mail, quest-log and NPC quest/gossip parchment colors and torn edges in both themes, including empty quest-log artwork. Restore Golden UI's darker NPC title strip.
- Keep item-rarity borders and formatted Golden titles consistent when windows update or are recolored again.
- Consolidate deferred initialization, batch duplicate delayed recolors and bound the texture-classification cache while preserving frame exclusions.
## 3.8.4 — 2026-11-06 — Action-bar proc highlights
- Add **Proc & Reactive Spell Highlights** (Action Bar, off): event-driven pale light-blue glows for supported spells and macros, with native pulsing animation where available. Central proc alerts remain with dedicated addons such as Proc Doc.
+2 -3
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.4.** What changed in each update: see the [changelog](CHANGELOG.md).
**Version 3.8.5.** 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.
@@ -253,8 +253,7 @@ Nameplate Scale, Class Colors and Castbar were removed in 3.8 because enabling t
### Interface
- Darkened UI *(off)*
- Golden UI *(off)*: gold borders, tooltips and dialogs like the WoW Forever client. Darkened UI and Golden UI also recolor the borders of ShaguDPS, Bagshui, Outfitter, SoulstoneTracker, OctoBus, RollFor and VCB, and of addon minimap buttons; Golden UI retains the default text colors. TrinketMenu keeps its own styling in every UI mode.
- **UI:** dropdown below the Advanced Options search bar, with **Default**, **Darkened** and **Golden** (Default initially). Select one mode and press Okay to apply/reload. Existing theme selections carry over; old saves with both enabled retain Darkened priority. Both themed modes recolor supported addon borders and minimap buttons. Golden uses gold borders, tooltips and dialogs like WoW Forever while retaining default text colors. TrinketMenu keeps its own styling.
Golden and Darkened UI keep the original NPC quest/gossip parchment, including its torn edges, while styling the outer frame. No stationery overlay is used on those panels.
+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.4
## Version: 3.8.5
## Dependencies: !!!ClassicAPI
## SavedVariables: ShaguTweaks_player_cache, ShaguTweaks_social_cache
## SavedVariablesPerCharacter: ShaguTweaks_config, ShaguTweaks_cache
+108 -7
View File
@@ -4,6 +4,19 @@ local GetExpansion = ShaguTweaks.GetExpansion
local mod = math.mod or mod
local current_config = {}
local darkKey, goldKey = T["Darkened UI"], T["Golden UI"]
local modeKey = T["UI:"]
local modeLabels = { T["Default"], T["Darkened"], T["Golden"] }
local modeOption = { title = modeKey, description = T["UI mode: Default, Darkened UI or Golden UI."], category = T["Interface"] }
local function SelectedMode()
return current_config[darkKey] == 1 and 2 or current_config[goldKey] == 1 and 3 or 1
end
local function SelectMode(mode, dropdown)
current_config[darkKey] = mode == 2 and 1 or 0
current_config[goldKey] = mode == 3 and 1 or 0
UIDropDownMenu_SetSelectedID(dropdown, mode)
UIDropDownMenu_SetText(modeLabels[mode], dropdown)
end
local max_width = 500
local max_height = 680
@@ -35,7 +48,7 @@ settings:SetBackdrop({
settings.scrollframe = CreateFrame('ScrollFrame', 'AdvancedSettingsGUIScrollframe', settings, 'UIPanelScrollFrameTemplate')
settings.scrollframe:SetWidth(max_width - 50)
settings.scrollframe:SetPoint('CENTER', settings, -16, -5)
settings.scrollframe:SetPoint('CENTER', settings, -16, -21)
settings.scrollframe:Hide()
settings.container = CreateFrame("Frame", "AdvancedSettingsGUIContainer", settings)
@@ -114,6 +127,52 @@ settings.defaults:SetScript("OnClick", function()
settings:defaults()
end)
settings.version = CreateFrame("Button", "AdvancedSettingsGUIVersion", settings, "GameMenuButtonTemplate")
settings.version:SetWidth(80)
settings.version:SetPoint("LEFT", settings.defaults, "RIGHT", 4, 0)
settings.version:SetText("v" .. (GetAddOnMetadata("ShaguTweaks", "Version") or "unknown"))
settings.projectLink = CreateFrame("Frame", "ShaguTweaksProjectLink", settings)
local projectLink = settings.projectLink
projectLink:SetWidth(420)
projectLink:SetHeight(110)
projectLink:SetPoint("CENTER", settings, "CENTER", 0, 0)
projectLink:SetFrameStrata("DIALOG")
projectLink:SetFrameLevel(settings:GetFrameLevel() + 10)
projectLink:EnableMouse(true)
projectLink:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true, tileSize = 32, edgeSize = 32,
insets = { left = 11, right = 12, top = 12, bottom = 11 },
})
projectLink:Hide()
table.insert(UISpecialFrames, "ShaguTweaksProjectLink")
projectLink.label = projectLink:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
projectLink.label:SetPoint("TOP", projectLink, "TOP", 0, -16)
projectLink.label:SetText(T["Press Ctrl+C to copy the link."])
projectLink.edit = CreateFrame("EditBox", "ShaguTweaksProjectLinkEdit", projectLink, "InputBoxTemplate")
projectLink.edit:SetWidth(370)
projectLink.edit:SetHeight(24)
projectLink.edit:SetPoint("TOP", projectLink, "TOP", 0, -33)
projectLink.edit:SetAutoFocus(false)
projectLink.edit:SetScript("OnEscapePressed", function() projectLink:Hide() end)
projectLink.edit:SetScript("OnEnterPressed", function() projectLink:Hide() end)
projectLink:SetScript("OnHide", function() projectLink.edit:ClearFocus() end)
projectLink.close = CreateFrame("Button", nil, projectLink, "GameMenuButtonTemplate")
projectLink.close:SetWidth(90)
projectLink.close:SetPoint("BOTTOM", projectLink, "BOTTOM", 0, 12)
projectLink.close:SetText(CLOSE)
projectLink.close:SetScript("OnClick", function() projectLink:Hide() end)
settings.version:SetScript("OnClick", function()
local color = ShaguTweaks.UIBorder
if color then projectLink:SetBackdropBorderColor(color.r, color.g, color.b, 1) end
projectLink:Show()
projectLink.edit:SetText("https://octowow.st/git/OEVG/ShaguTweaks-ClassicAPI")
projectLink.edit:SetFocus()
projectLink.edit:HighlightText()
end)
local searchIndex = {}
local function MatchesSearch(module, title, category, query)
if query == "" then return true end
@@ -183,7 +242,7 @@ settings.load = function(self)
-- update window sizing according to screen
settings:SetHeight(max_height)
settings.scrollframe:SetHeight(max_height - 120)
settings.scrollframe:SetHeight(max_height - 152)
settings.container:SetHeight(max_height - 30)
settings.entries = settings.entries or {}
@@ -199,7 +258,7 @@ settings.load = function(self)
-- sort all configs into categories
local gui = {}
for title, module in pairs(ShaguTweaks.mods) do
if module.expansions[expansion] then
if title ~= darkKey and title ~= goldKey and module.expansions[expansion] then
local category = module.category or T["General"]
if MatchesSearch(module, title, category, settings.query or "") then
gui[category] = gui[category] or {}
@@ -208,6 +267,44 @@ settings.load = function(self)
end
end
local hasMode = expansion == "vanilla" and (ShaguTweaks.mods[darkKey] or ShaguTweaks.mods[goldKey])
if hasMode then
local holder = settings.entries["ui-mode"]
if not holder then
holder = CreateFrame("Frame", nil, settings)
holder:SetWidth(160); holder:SetHeight(32)
holder.label = holder:CreateFontString(nil, "OVERLAY", "GameFontNormal")
holder.label:SetPoint("LEFT", holder, "LEFT", 0, 0)
holder.label:SetText(modeKey)
local dropdown = CreateFrame("Frame", "AdvancedSettingsGUIUIMode", holder, "UIDropDownMenuTemplate")
dropdown:SetPoint("LEFT", holder, "LEFT", 22, -2)
UIDropDownMenu_SetWidth(90, dropdown)
UIDropDownMenu_Initialize(dropdown, function()
for i, label in ipairs(modeLabels) do
local choice = i
local info = { text = label, checked = SelectedMode() == i,
func = function() SelectMode(choice, dropdown) end }
UIDropDownMenu_AddButton(info)
end
end)
settings.modeDropdown = dropdown
settings.entries["ui-mode"] = holder
holder:SetScript("OnHide", function() if CloseDropDownMenus then CloseDropDownMenus() end end)
end
holder:ClearAllPoints()
holder:SetPoint("TOP", settings, "TOP", 0, -56)
holder:Show()
UIDropDownMenu_SetSelectedID(settings.modeDropdown, SelectedMode())
UIDropDownMenu_SetText(modeLabels[SelectedMode()], settings.modeDropdown)
local color = ShaguTweaks.UIBorder
if color then
for _, suffix in ipairs({"Left", "Middle", "Right"}) do
local texture = _G["AdvancedSettingsGUIUIMode" .. suffix]
if texture then texture:SetVertexColor(color.r, color.g, color.b, 1) end
end
end
end
local topspace = 10
local required_height = topspace
local entrysize = 22
@@ -444,19 +541,21 @@ settings.load = function(self)
settings:UpdateOptionAvailability()
-- set container size to required height
if not next(gui) then
settings.empty:Show()
if hasMode and MatchesSearch(modeOption, modeKey, modeOption.category, settings.query or "" ) then
settings.empty:Hide()
else settings.empty:Show() end
required_height = 64
else
settings.empty:Hide()
end
settings.container:SetHeight(required_height)
if required_height + 100 <= max_height then
if required_height + 132 <= max_height then
-- reduce base frame if possible
settings:SetHeight(required_height + 100)
settings:SetHeight(required_height + 132)
settings.container:SetParent(settings)
settings.container:ClearAllPoints()
settings.container:SetPoint("TOPLEFT", settings, "TOPLEFT", 10, -54)
settings.container:SetPoint("TOPLEFT", settings, "TOPLEFT", 10, -86)
settings.container:SetWidth(max_width - 20)
settings.scrollframe:Hide()
@@ -496,6 +595,8 @@ settings:SetScript("OnShow", function()
for k, v in pairs(ShaguTweaks_config) do
current_config[k] = v
end
-- Existing saves may contain both old toggles; retain Darkened precedence.
if current_config[darkKey] == 1 then current_config[goldKey] = 0 end
-- copy overwrites into a separate table so Cancel discards slider changes
current_config.overwrites = {}
for k, v in pairs(ShaguTweaks_config.overwrites or {}) do
+51 -12
View File
@@ -76,18 +76,57 @@ ShaguTweaks.HookScript = function(f, script, func)
end)
end
ShaguTweaks.HookAddonOrVariable = function(addon, func)
local lurker = CreateFrame("Frame", nil)
lurker.func = func
lurker:RegisterEvent("ADDON_LOADED")
lurker:RegisterEvent("VARIABLES_LOADED")
lurker:RegisterEvent("PLAYER_ENTERING_WORLD")
lurker:SetScript("OnEvent",function()
if IsAddOnLoaded(addon) or _G[addon] then
this:func()
this:UnregisterAllEvents()
-- One event listener for deferred addon/global initialization. Deliberately
-- retain the old three triggers rather than invoking already-ready hooks inline.
do
local listener, draining
local pending, ready, checked = {}, {}, {}
local function Drain()
if draining then return end
draining = true
local previousThis, previousEvent = this, event
local a1,a2,a3,a4,a5,a6,a7,a8,a9 = arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9
local remaining = 0
local count = table.getn(pending)
for i = 1, count do
local entry = pending[i]
local available = checked[entry.name]
if available == nil then
available = (IsAddOnLoaded(entry.name) or _G[entry.name]) and true or false
checked[entry.name] = available
end
if available then table.insert(ready, entry)
else remaining = remaining + 1; pending[remaining] = entry end
end
end)
for i = count, remaining + 1, -1 do pending[i] = nil end
table.setn(pending, remaining) -- Lua 5.0 caches lengths used by table.insert.
for name in pairs(checked) do checked[name] = nil end
-- Dispatch only the snapshot: callbacks may register more work/load addons.
for i = 1, table.getn(ready) do
local callback = ready[i].callback
ready[i] = nil
this, event = listener, previousEvent
arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9 = a1,a2,a3,a4,a5,a6,a7,a8,a9
listener.func = callback
local ok, failure = pcall(callback, listener)
listener.func = nil
if not ok then pcall(geterrorhandler(), failure) end
end
table.setn(ready, 0)
this, event = previousThis, previousEvent
arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9 = a1,a2,a3,a4,a5,a6,a7,a8,a9
draining = nil
if not next(pending) then listener:UnregisterAllEvents() end
end
ShaguTweaks.HookAddonOrVariable = function(addon, func)
if not listener then listener = CreateFrame("Frame", nil); listener:SetScript("OnEvent", Drain) end
if not next(pending) then
listener:RegisterEvent("ADDON_LOADED")
listener:RegisterEvent("VARIABLES_LOADED")
listener:RegisterEvent("PLAYER_ENTERING_WORLD")
end
table.insert(pending, { name = addon, callback = func })
end
end
ShaguTweaks.hooksecurefunc = function(tbl, name, func, prepend)
@@ -452,4 +491,4 @@ ShaguTweaks.GetItemLinkByName = function(name)
return CacheItemLink(name, hyperLink)
end
end
end
end
+174 -36
View File
@@ -75,7 +75,6 @@ local regionskips = {
local backgrounds = {
["^SpellBookFrame$"] = { 325, 355, 17, -74 },
["^ItemTextFrame$"] = { 300, 355, 24, -74 },
["^QuestLogDetailScrollFrame$"] = { QuestLogDetailScrollChildFrame:GetWidth(), QuestLogDetailScrollChildFrame:GetHeight(), 0, 0 },
}
local borders = {
@@ -157,30 +156,51 @@ local showframes = {
["RollForLootFrameHeader"] = true,
}
local PATH_CACHE_LIMIT = 256
local pathCache, pathKeys, pathCursor = {}, {}, 0
local function ClassifyPath(path)
local result = pathCache[path]
local capture = ShaguTweaks.RecolorCapture
if capture then
if result then capture.cacheHits = capture.cacheHits + 1
else capture.cacheMisses = capture.cacheMisses + 1 end
end
if result then return result end
local lower = string.lower(path)
result = 3 -- Candidate: name exclusions must still be evaluated per region.
if string.find(lower, "interface\\questframe\\ui-questlog-empty-", 1, true)
or string.find(lower, "interface\\stationery\\", 1, true)
or string.find(lower, "interface\\mailframe\\mailitemborder", 1, true) then
result = 2 -- Paper/insets always remain native.
else
for i = 1, table.getn(whitelist) do
if string.find(lower, whitelist[i], 1, true) then result = 1; break end
end
if result == 3 then
for entry in pairs(blacklist) do
if string.find(path, entry, 1) then result = 2; break end
end
end
end
pathCursor = math.mod(pathCursor, PATH_CACHE_LIMIT) + 1
local previous = pathKeys[pathCursor]
if previous then pathCache[previous] = nil end
pathKeys[pathCursor], pathCache[path] = path, result
return result
end
local function IsBlacklisted(texture)
local path = texture:GetTexture()
if type(path) ~= "string" then return true end
local name = texture:GetName()
local texture = texture:GetTexture()
if not texture then return true end
local lower = string.lower(texture)
if string.find(lower, "interface\\mailframe\\mailitemborder", 1, true) then
-- This includes the brown inset of each inbox slot, not just its edge.
return true
end
for i = 1, table.getn(whitelist) do
if string.find(lower, whitelist[i], 1, true) then return nil end
end
if name and string.find(name, "StationeryBackground", 1, true) then return true end
local result = ClassifyPath(path)
if result == 1 then return nil end -- Explicit artwork whitelist wins over names.
if result == 2 then return true end
if name then
for entry in pairs(blacklist) do
if string.find(name, entry, 1) then return true end
end
end
for entry in pairs(blacklist) do
if string.find(texture, entry, 1) then return true end
end
return nil
end
@@ -189,7 +209,7 @@ local function AddSpecialBackground(frame, w, h, x, y)
frame.Material:SetTexture("Interface\\Stationery\\StationeryTest1")
frame.Material:SetWidth(w)
frame.Material:SetHeight(h)
frame.Material:SetPoint("TOPLEFT", frame, x, y)
frame.Material:SetPoint("TOPLEFT", frame, "TOPLEFT", x, y)
frame.Material:SetVertexColor(.8, .8, .8)
end
@@ -271,7 +291,9 @@ local function RestoreQuestParchment(frame)
-- Native 256px tiles contain both the frame and torn parchment.
-- Copy only the paper area; leave the palette on the outer artwork.
local x1, x2 = left and 22 or 0, left and 256 or 67
local y1, y2 = top and 74 or 0, top and 256 or 148
-- Native paper starts below the header and includes the torn lower
-- edge through row 159. Stopping at 148 leaves a tinted bottom band.
local y1, y2 = top and 82 or 0, top and 256 or 160
-- Use a higher layer than both the native BACKGROUND tiles and
-- BORDER material textures. Equal-layer texture batching can draw
-- tinted native tiles over only some of our restored paper pieces.
@@ -291,10 +313,63 @@ local function RestoreQuestParchment(frame)
end
end
local function DarkenFrame(frame, r, g, b, a)
local function RestoreQuestLogParchment(frame)
if not frame.GetName or frame:GetName() ~= "QuestLogFrame" then return end
for _, source in pairs({frame:GetRegions()}) do
local path = source.GetTexture and source:GetTexture()
if type(path) == "string" and not source.stQuestLogCrops and not source.stOriginalQuestPaper then
local lower = string.lower(path)
local crops, tileWidth
-- Turtle's two native tiles, verified against its shipped artwork.
-- Keep the list surface and both parchment halves; exclude metal rims.
if string.find(lower, "interface\\questframe\\ui-questlog-left", 1, true)
and string.find(lower, "ui%-questlog%-left$") then
crops, tileWidth = {{19,74,320,436},{348,74,512,456}}, 512
elseif string.find(lower, "interface\\questframe\\ui-questlog-right", 1, true)
and string.find(lower, "ui%-questlog%-right$") then
crops, tileWidth = {{0,74,136,456}}, 256
end
if crops then
local w,h = source:GetWidth(), source:GetHeight()
if w and h and w > 0 and h > 0 then
source.stQuestLogCrops = {}
for _, rect in ipairs(crops) do
local paper = frame:CreateTexture(nil, "OVERLAY")
paper.stOriginalQuestPaper = true
paper:SetTexture(path)
paper:SetTexCoord(rect[1]/tileWidth,rect[3]/tileWidth,rect[2]/512,rect[4]/512)
paper:SetWidth((rect[3]-rect[1])*w/tileWidth)
paper:SetHeight((rect[4]-rect[2])*h/512)
paper:SetPoint("TOPLEFT",source,"TOPLEFT",rect[1]*w/tileWidth,-rect[2]*h/512)
paper:SetVertexColor(1,1,1,1)
table.insert(source.stQuestLogCrops,paper)
end
end
end
end
end
end
local function DarkenFrame(frame, r, g, b, a, scan)
-- dont't do anything if neither Darkened UI nor Golden UI is enabled
if not tint then return end
if IsExcludedFrame(frame) then return end
local capture = ShaguTweaks.RecolorCapture
if capture and frame then
if not scan then
scan = capture.rootIndex[frame]
if not scan then
scan = { name = frame.GetName and frame:GetName() or tostring(frame), calls = 0, frames = 0, regions = 0, tables = 0 }
capture.rootIndex[frame] = scan
table.insert(capture.roots, scan)
end
scan.calls = scan.calls + 1
end
scan.frames = scan.frames + 1
capture.frameVisits = capture.frameVisits + 1
if capture.seen[frame] then capture.repeatVisits = capture.repeatVisits + 1
else capture.seen[frame] = true; capture.uniqueFrames = capture.uniqueFrames + 1 end
end
-- set defaults
if not r and not g and not b then
@@ -303,18 +378,23 @@ local function DarkenFrame(frame, r, g, b, a)
-- iterate through all subframes
if frame and frame.GetChildren then
if capture then scan.tables = scan.tables + 1 end
for _, frame in pairs({frame:GetChildren()}) do
DarkenFrame(frame, r, g, b, a)
DarkenFrame(frame, r, g, b, a, scan)
end
end
-- set vertex on all regions
if frame and frame.GetRegions then
if capture then scan.tables = scan.tables + 1 end
-- read name
local name = frame.GetName and frame:GetName()
-- set the palette's backdrop border color everywhere
frame:SetBackdropBorderColor(edge.r, edge.g, edge.b, edge.a)
if not frame.stPreserveRarityColor then
if capture then capture.borderWrites = capture.borderWrites + 1 end
frame:SetBackdropBorderColor(edge.r, edge.g, edge.b, edge.a)
end
-- add special backgrounds to quests and such
for pattern, inset in pairs(backgrounds) do
@@ -330,6 +410,7 @@ local function DarkenFrame(frame, r, g, b, a)
-- scan through all regions (textures)
for id, region in pairs({frame:GetRegions()}) do
if capture then scan.regions = scan.regions + 1 end
if region.SetVertexColor and region:GetObjectType() == "Texture" then
if region:GetTexture() and string.find(region:GetTexture(), "UI%-Panel%-Button%-Up") then
-- monochrome buttons
@@ -352,12 +433,14 @@ local function DarkenFrame(frame, r, g, b, a)
elseif IsBlacklisted(region) then
-- skip blacklisted texture names
else
if capture then capture.textureWrites = capture.textureWrites + 1 end
region:SetVertexColor(r,g,b,a)
end
end
end
end
if frame and frame.GetRegions then RestoreQuestParchment(frame) end
if frame and frame.GetRegions then RestoreQuestLogParchment(frame) end
end
-- register darken frame to global
@@ -453,8 +536,11 @@ end
local function LockTree(frame)
if not frame then return end
if IsExcludedFrame(frame) then return end
local capture = ShaguTweaks.RecolorCapture
if capture then capture.lockVisits = capture.lockVisits + 1 end
LockBorder(frame)
if frame.GetRegions then
if capture then capture.lockTables = capture.lockTables + 1 end
local regions = { frame:GetRegions() }
for i = 1, table.getn(regions) do
local region = regions[i]
@@ -465,6 +551,7 @@ local function LockTree(frame)
end
end
if frame.GetChildren then
if capture then capture.lockTables = capture.lockTables + 1 end
local children = { frame:GetChildren() }
for i = 1, table.getn(children) do LockTree(children[i]) end
end
@@ -472,6 +559,11 @@ end
-- find the addon frames that exist already, anywhere below UIParent
local function FindAddonFrames(frame)
local capture = ShaguTweaks.RecolorCapture
if capture then
capture.discoveryVisits = capture.discoveryVisits + 1
capture.discoveryTables = capture.discoveryTables + 1
end
local children = { frame:GetChildren() }
for i = 1, table.getn(children) do
local child = children[i]
@@ -504,10 +596,16 @@ local function RecolorOnShow(frame)
if not frame or frame.stRecolorOnShow then return end
frame.stRecolorOnShow = true
if golden.rollforLoot and frame:GetName() == "RollForLootFrameHeader" then golden.rollforLoot(frame) end
local pending
local function Trailing()
pending = nil
DarkenFrame(frame)
end
ShaguTweaks.HookScript(frame, "OnShow", function()
DarkenFrame(frame)
if _G.C_Timer and _G.C_Timer.After then
_G.C_Timer.After(0, function() DarkenFrame(frame) end)
if not pending and _G.C_Timer and _G.C_Timer.After then
pending = true
_G.C_Timer.After(0, Trailing)
end
end)
if frame:IsShown() then DarkenFrame(frame) end
@@ -824,7 +922,14 @@ local function RedrawTitle(holder, title)
local setText = title.SetText
title.SetText = function(self, text)
setText(self, text)
copy:SetText(text)
copy:SetText(self:GetText())
end
local setFormattedText = title.SetFormattedText
if type(setFormattedText) == "function" then
title.SetFormattedText = function(self, ...)
setFormattedText(self, unpack(arg))
copy:SetText(self:GetText())
end
end
return copy
end
@@ -855,8 +960,15 @@ end
-- Send Mail and TurtleMail Log tabs. Matching the original pixels preserves the torn
-- edges and tile seams without replacing the parchment with stationery.
local function RestoreMailParchment()
if not InboxFrame or InboxFrame.stOriginalParchment then return end
InboxFrame.stOriginalParchment = true
if not InboxFrame then return end
if not InboxFrame.stParchmentRetryHook then
InboxFrame.stParchmentRetryHook = true
ShaguTweaks.HookScript(InboxFrame, "OnShow", RestoreMailParchment)
end
if InboxFrame.stOriginalParchment then return end
InboxFrame.stParchmentTiles = InboxFrame.stParchmentTiles or {}
local copies = InboxFrame.stParchmentTiles
local complete = true
local tiles = {
{ "MailFrameTopLeft", 22, 74, 256, 256 },
{ "MailFrameTopRight", 0, 74, 80, 256 },
@@ -866,17 +978,22 @@ local function RestoreMailParchment()
for i = 1, table.getn(tiles) do
local tile = tiles[i]
local source = _G[tile[1]]
if source and source:GetTexture() then
if not copies[i] and source and source:GetTexture() then
local w, h = source:GetWidth(), source:GetHeight()
local paper = InboxFrame:CreateTexture(nil, "BACKGROUND")
paper:SetTexture(source:GetTexture())
paper:SetVertexColor(1, 1, 1, 1)
paper:SetTexCoord(tile[2] / w, tile[4] / w, tile[3] / h, tile[5] / h)
paper:SetWidth(tile[4] - tile[2])
paper:SetHeight(tile[5] - tile[3])
paper:SetPoint("TOPLEFT", source, "TOPLEFT", tile[2], -tile[3])
if w and h and w > 0 and h > 0 then
local paper = InboxFrame:CreateTexture(nil, "BACKGROUND")
paper:SetTexture(source:GetTexture())
paper:SetVertexColor(1, 1, 1, 1)
paper:SetTexCoord(tile[2] / w, tile[4] / w, tile[3] / h, tile[5] / h)
paper:SetWidth(tile[4] - tile[2])
paper:SetHeight(tile[5] - tile[3])
paper:SetPoint("TOPLEFT", source, "TOPLEFT", tile[2], -tile[3])
copies[i] = paper
end
end
if not copies[i] then complete = false end
end
InboxFrame.stOriginalParchment = complete or nil
end
module.enable = function(self)
@@ -885,6 +1002,18 @@ module.enable = function(self)
RestoreMailParchment()
end
local function ShadeNpcTitleBar(frame, nameFrame, panels, color)
if not frame or not nameFrame or frame.stTitleBar then return end
local level = frame:GetFrameLevel() + 1
for _, panel in pairs(panels) do
if panel then level = math.max(level, panel:GetFrameLevel() + 1) end
end
-- NPC names already live in a separate frame. Keep that native title above
-- both the panel artwork and shade, without creating another text copy.
if nameFrame:GetFrameLevel() <= level then nameFrame:SetFrameLevel(level + 1) end
ShadeTitleBar(frame, {72,-22,-60,-39}, nil, color, level)
end
local function ShadeTitleBars(color)
-- world map: its strip is 3 to 21 px below the top of its art (384 above
-- the frame center), from the left corner to the window buttons
@@ -914,6 +1043,9 @@ local function ShadeTitleBars(color)
ShadeTitleBar(MerchantFrame, STRIP_B, MerchantNameText, color)
ShadeTitleBar(SpellBookFrame, STRIP_A, SpellBookTitleText, color)
ShadeTitleBar(FriendsFrame, STRIP_B, FriendsFrameTitleText, color)
ShadeNpcTitleBar(QuestFrame, _G.QuestNpcNameFrame,
{ _G.QuestFrameGreetingPanel, _G.QuestFrameDetailPanel, _G.QuestFrameProgressPanel, _G.QuestFrameRewardPanel }, color)
ShadeNpcTitleBar(GossipFrame, _G.GossipNpcNameFrame, { _G.GossipFrameGreetingPanel }, color)
-- the quest log is wider: its strip ends before the close button
ShadeTitleBar(QuestLogFrame, { 72, -16, -121, -34 }, QuestLogTitleText, color)
@@ -995,3 +1127,9 @@ golden.enable = function(self)
golden.rollforLoot = function(frame) ShadeRollForLoot(frame, golden.config["goldmode.bar"]) end
if _G.RollForLootFrameHeader then golden.rollforLoot(_G.RollForLootFrameHeader) end
end
-- Function references for the optional companion; no profiling starts here.
ShaguTweaks.RecolorProfileFunctions = {
DarkenFrame = DarkenFrame, LockTree = LockTree,
FindAddonFrames = FindAddonFrames, ClassifyPath = ClassifyPath,
}
+13
View File
@@ -10,6 +10,15 @@ local module = ShaguTweaks:register({
enabled = true,
})
local function MatchBackground(compare, source)
if not ShaguTweaks_config or ShaguTweaks_config[T["Darkened UI"]] ~= 1
or not source.GetBackdropColor or not compare.SetBackdropColor then return end
local r,g,b,a = source:GetBackdropColor()
if not r then return end
local cr,cg,cb,ca = compare:GetBackdropColor()
if cr ~= r or cg ~= g or cb ~= b or ca ~= a then compare:SetBackdropColor(r,g,b,a) end
end
local function EnableNativeCompare(self)
if not API.tooltipsetitem or not API.modifierstate or not API.timer
or not ShoppingTooltip1 or not ShoppingTooltip2
@@ -43,6 +52,7 @@ local function EnableNativeCompare(self)
if point then compare:SetPoint(point, relative, relativePoint, x, y) end
if compare:SetHyperlinkCompareItem(link, i, true, tooltip) then compare:Show() end
end
MatchBackground(compare, tooltip)
end
end
building = false
@@ -75,6 +85,7 @@ local function EnableNativeCompare(self)
-- selects filled ring/weapon slots and supplies the equipped header.
if compare:SetHyperlinkCompareItem(link, i, true, tooltip) then
compare:Show()
MatchBackground(compare, tooltip)
previous = compare
active = tooltip
else compare:Hide() end
@@ -293,6 +304,7 @@ module.enable = function(self)
ShoppingTooltip1:SetInventoryItem("player", slotID)
ShoppingTooltip1:Show()
AddHeader(ShoppingTooltip1)
MatchBackground(ShoppingTooltip1, tooltip)
if slotNames[2] then
local secondSlotID = GetInventorySlotInfo(slotNames[2])
@@ -306,6 +318,7 @@ module.enable = function(self)
ShoppingTooltip2:SetInventoryItem("player", secondSlotID)
ShoppingTooltip2:Show()
AddHeader(ShoppingTooltip2)
MatchBackground(ShoppingTooltip2, tooltip)
end
activeTooltip = tooltip
+11 -1
View File
@@ -9,7 +9,7 @@ sh tests/run_all.sh ../tools/lua5.0/lua5.0
The interpreter defaults to `lua5.0` if omitted. The runner discovers all
`tests/test_*.lua` files, reports every failure, prints a final count, and exits
unsuccessfully if any suite fails. It is developer tooling, is not loaded by the
addon, and has no in-game overhead. The current suite contains 27 test files.
addon, and has no in-game overhead. The current suite contains 31 test files.
Individual checks can also be run from the addon root:
@@ -109,3 +109,13 @@ rebuild; a non-item or persistently hidden source removes comparisons after the
deferred lifecycle check.
Proc-highlight checks cover capability detection, pre-existing procs, direct spells/macros, paging, hidden buttons, event-only updates and native-animation/static paths.
Initialization-hook checks exercise one deferred listener, duplicate dependency checks, callback ordering/context, nested registration, reentrant events, errors, teardown and reactivation using Lua 5.0.
Recolor-audit regressions cover marked rarity preservation during recursive passes, formatted Golden titles, and partial/zero-size mail tile retries without duplicate crops.
Quest-log checks use the inspected Turtle native 512/256-pixel tiles, preserve list and parchment crops, exclude empty-page art, and reject replacement stationery overlays.
Recolor scheduling checks verify immediate/trailing behavior, one pending callback per frame, callback reuse, late children and FIFO cache limits with dynamic name/path changes.
Recolor profiling tests verify opt-in traversal counts, recursive root attribution, unique/repeat visits and that stopped captures no longer receive updates.
+6
View File
@@ -11,6 +11,8 @@ local function frame(name)
end
function methods:SetOwner(owner) self.owner=owner;self.nativeContent=nil end
function methods:GetOwner() return self.owner end
function methods:GetBackdropColor() return unpack(self.background or {0,0,0,.5}) end
function methods:SetBackdropColor(r,g,b,a) self.background={r,g,b,a} end
function methods:ClearAllPoints() self.point=nil end
function methods:SetPoint(...) self.point=arg end
function methods:GetPoint() if self.point then return unpack(self.point) end return 'TOPLEFT',UIParent end
@@ -56,10 +58,14 @@ local function flush()
for _,callback in ipairs(queue) do callback() end
assert(table.getn(pending)==0,'Comparison construction scheduled a recursive refresh')
end
ShaguTweaks_config={['Darkened UI']=1}
GameTooltip.background={.2,.2,.2,1}
GameTooltip:SetOwner(hover);GameTooltip.link='item:123:enchant:randomsuffix';GameTooltip.two=true;GameTooltip:Show()
GameTooltip.scripts.OnTooltipSetItem();GameTooltip.scripts.OnTooltipSetItem();assert(table.getn(pending)==1)
flush();assert(compared[1]==GameTooltip.link and compared[2]==GameTooltip.link)
assert(ShoppingTooltip1:IsShown() and ShoppingTooltip2:IsShown())
assert(ShoppingTooltip1.background[1]==.2 and ShoppingTooltip1.background[4]==1)
assert(ShoppingTooltip2.background[1]==.2 and ShoppingTooltip2.background[4]==1)
assert(ShoppingTooltip2.point[2]==ShoppingTooltip1)
assert(ShoppingTooltip1:GetOwner()==hover and ShoppingTooltip2:GetOwner()==hover)
local beforeBuild,beforeHide=builds,ShoppingTooltip1.hides
+2
View File
@@ -25,6 +25,7 @@ function methods:GetFontObject() return 'DefaultGoldFont' end
function methods:GetJustifyH() return 'CENTER' end
function methods:SetJustifyH(v) self.justify=v end
function methods:SetText(v) self.text=v end
function methods:SetFormattedText(format,...) self.text=string.format(format,unpack(arg)) end
function methods:GetText() return self.text end
function methods:CreateTexture() return object() end
function methods:CreateFontString() return object() end
@@ -50,6 +51,7 @@ assert(bar.texture.path=='Interface\\PaperDollInfoFrame\\UI-Character-General-To
assert(bar.texture.color[4]==1 and math.abs(bar.texture.color[2]-.78*.63)<.00001)
assert(bar.title.font=='DefaultGoldFont' and title.alpha==0 and bar.title.text=='First target')
title:SetText('Next target');assert(bar.title.text=='Next target')
title:SetFormattedText('%s %d','Formatted target',2);assert(bar.title.text=='Formatted target 2')
local count=made;apply(target);assert(made==count)
assert(bar.visible)
isPlayer=true;bar.OnEvent();assert(not bar.visible and title.alpha==1)
+64
View File
@@ -0,0 +1,64 @@
local root=(arg[1] or '.')..'/'
local file=assert(io.open(root..'helpers.lua'));local source=file:read('*a');file:close()
assert(loadstring(source))
local first=assert(string.find(source,'-- One event listener for deferred',1,true))
local last=assert(string.find(source,'ShaguTweaks.hooksecurefunc =',first,true))
local frames,loaded,checks,errors={},{},{},{}
ShaguTweaks={}
function CreateFrame()
local frame={events={},scripts={}}
function frame:RegisterEvent(e) self.events[e]=true end
function frame:UnregisterAllEvents() self.events={} end
function frame:SetScript(k,v) self.scripts[k]=v end
table.insert(frames,frame);return frame
end
function IsAddOnLoaded(name) checks[name]=(checks[name] or 0)+1;return loaded[name] end
function geterrorhandler() return function(message) table.insert(errors,message) end end
local chunk=assert(loadstring('local _G=getfenv(0)\n'..string.sub(source,first,last-1)))
chunk()
assert(table.getn(frames)==0)
local hook=ShaguTweaks.HookAddonOrVariable
local calls={}
local function emit(name,payload)
local oldThis,oldEvent,oldArg=this,event,arg1
this,event,arg1=frames[1],name,payload
frames[1].scripts.OnEvent()
assert(this==frames[1] and event==name and arg1==payload)
this,event,arg1=oldThis,oldEvent,oldArg
end
loaded.Already=true
hook('Already',function(frame)
assert(frame==this and frame.func and event=='ADDON_LOADED' and arg1=='Payload')
table.insert(calls,'first');arg1='changed';event='changed';this=nil
hook('Nested',function() table.insert(calls,'nested') end)
-- Reentrant events may not drain/overwrite the active callback snapshot.
emit('ADDON_LOADED','Inner')
end)
hook('Already',function() assert(arg1=='Payload');table.insert(calls,'second') end)
_G.ExistingGlobal={}
hook('ExistingGlobal',function() table.insert(calls,'global') end)
hook('Missing',function() table.insert(calls,'missing') end)
loaded.Nested=true
assert(table.getn(frames)==1 and table.getn(calls)==0 and not frames[1].scripts.OnUpdate)
emit('ADDON_LOADED','Payload')
assert(table.concat(calls,',')=='first,second,global')
assert(checks.Already==1 and checks.ExistingGlobal==1 and checks.Missing==1)
emit('VARIABLES_LOADED')
assert(calls[4]=='nested' and frames[1].events.ADDON_LOADED)
loaded.Missing=true
emit('PLAYER_ENTERING_WORLD')
assert(calls[5]=='missing' and not next(frames[1].events))
emit('ADDON_LOADED');assert(table.getn(calls)==5)
-- Reactivate the same listener; one failed callback must not block the next.
hook('Already',function() error('expected initialization failure') end)
hook('Already',function() table.insert(calls,'after-error') end)
assert(frames[1].events.VARIABLES_LOADED and table.getn(frames)==1)
emit('ADDON_LOADED')
assert(table.getn(errors)==1 and calls[6]=='after-error' and not next(frames[1].events))
emit('ADDON_LOADED');assert(table.getn(errors)==1 and table.getn(calls)==6)
-- Global truthiness preserves the old contract, including numeric zero.
_G.LaterGlobal=false;hook('LaterGlobal',function() table.insert(calls,'later') end)
emit('VARIABLES_LOADED');assert(table.getn(calls)==6)
_G.LaterGlobal=0;emit('PLAYER_ENTERING_WORLD');assert(calls[7]=='later')
assert(not next(frames[1].events) and table.getn(frames)==1)
print('Initialization hooks: one lazy listener, deferred readiness, deduplicated checks, ordered callbacks, nested/reentrant registration, context, error isolation, teardown and rearming passed')
+55 -7
View File
@@ -9,9 +9,15 @@ function methods:Hide() self.visible=false end
function methods:IsShown() return self.visible end
function methods:SetWidth(v) self.width=v end
function methods:SetHeight(v) self.height=v end
function methods:SetPoint(...) self.point=arg end
function methods:GetWidth() return self.width or 500 end
function methods:GetHeight() return self.height or 900 end
function methods:GetScale() return 1 end
function methods:GetFrameLevel() return 1 end
function methods:SetFocus() self.focused=true end
function methods:ClearFocus() self.focused=false end
function methods:HighlightText() self.selected=true end
function methods:SetVertexColor(r,g,b,a) self.color={r,g,b,a} end
function methods:SetChecked(v) self.checked=v end
function methods:GetChecked() return self.checked end
function methods:Enable() self.enabled=true end
@@ -26,10 +32,13 @@ function CreateFrame(kind,name,parent,template)
local f=setmetatable({scripts={},visible=true},{__index=function(t,k) if methods[k] then return methods[k] elseif string.find(k,'^[A-Z]') then return noop end end})
if name then _G[name]=f end
if name and template=='OptionsCheckButtonTemplate' then _G[name..'Text']=CreateFrame() end
if name and template=='UIDropDownMenuTemplate' then
for _,suffix in ipairs({'Left','Middle','Right'}) do _G[name..suffix]=CreateFrame() end
end
return f
end
UIParent=CreateFrame(); GameMenuFrame=CreateFrame(); GameMenuButtonUIOptions=CreateFrame(); GameMenuButtonKeybindings=CreateFrame(); GameTooltip=CreateFrame()
UISpecialFrames={}; DEFAULTS='Defaults'; OKAY='Okay'; CANCEL='Cancel'
UISpecialFrames={}; DEFAULTS='Defaults'; OKAY='Okay'; CANCEL='Cancel'; CLOSE='Close'
ShowUIPanel=noop; HideUIPanel=noop; UpdateMicroButtons=noop
local reloaded=false
function ReloadUI() reloaded=true end
@@ -45,6 +54,8 @@ local function add(title,category,description,sliders)
ShaguTweaks.mods[title]={title=title,category=category,description=description,sliders=sliders,expansions={vanilla=true}}
end
add('Golden UI','Interface','Colors borders gold')
add('Darkened UI','Interface','Darkens borders')
add('Other Interface','Interface','Another interface feature')
add('Hide Player Frame Resources','Unit Frames','Hides native resources')
add('Hide Player Name','Unit Frames','Hides native name')
ShaguTweaks.mods['Hide Player Name'].requires={'Hide Player Frame Resources'}
@@ -52,8 +63,23 @@ add('Item Rarity Borders','Tooltip & Items','Colors mail and trade items')
add('Range Color','Action Bar','Shows distance',{{key='range.delay',label='Update interval',min=1,max=10,default=3}})
for i=1,40 do add('Other '..i,'Other','Another feature') end
ShaguTweaks_config={overwrites={}}
GetAddOnMetadata=function(addon,key) assert(addon=='ShaguTweaks' and key=='Version');return '3.8.5' end
local menu={}
function UIDropDownMenu_Initialize(dropdown,callback) dropdown.initialize=callback end
function UIDropDownMenu_SetWidth(width,dropdown) dropdown.width=width end
function UIDropDownMenu_SetSelectedID(dropdown,id) dropdown.selected=id end
function UIDropDownMenu_SetText(text,dropdown) dropdown.text=text end
function UIDropDownMenu_AddButton(info) table.insert(menu,info) end
local function choose(dropdown,id)
menu={};dropdown.initialize();assert(table.getn(menu)==3);menu[id].func()
end
dofile(root..'config.lua')
local gui=AdvancedSettingsGUI
assert(gui.version:GetText()=='v3.8.5')
this=gui.version;gui.version.scripts.OnClick();this=nil
assert(gui.projectLink.visible and gui.projectLink.edit.focused and gui.projectLink.edit.selected)
assert(gui.projectLink.edit:GetText()=='https://octowow.st/git/OEVG/ShaguTweaks-ClassicAPI')
gui.projectLink.close.scripts.OnClick();assert(not gui.projectLink.visible)
this=gui; gui.scripts.OnShow(); this=nil
assert(gui.scrollframe.visible)
local hideName=gui.entries['Hide Player Name']
@@ -67,20 +93,42 @@ gui.search:SetText('')
hideResources:SetChecked(false);this=hideResources;hideResources.scripts.OnClick()
assert(not hideName.enabled and hideName.checked) -- retain dependent choice while dormant
local firstCount=made
local checkbox=gui.entries['Golden UI']; checkbox:SetChecked(true); this=checkbox; checkbox.scripts.OnClick(); this=nil
local checkbox=gui.entries['ui-mode'];local dropdown=gui.modeDropdown
assert(checkbox.point[2]==gui and checkbox.point[5]==-56)
assert(not gui.entries['Golden UI'] and not gui.entries['Darkened UI'])
assert(dropdown.selected==1)
choose(dropdown,2);assert(dropdown.text=='Darkened')
choose(dropdown,3);assert(dropdown.text=='Golden')
-- Collapse a category before searching: matches must still be visible.
local collapse=gui.category.Interface.button; this=collapse; collapse.scripts.OnClick(); this=nil
assert(not checkbox.visible)
assert(checkbox.visible and not gui.entries['Other Interface'].visible)
gui.search:SetText(' GOLDEN ')
assert(checkbox.visible and checkbox.checked and not gui.entries['Range Color'].visible)
assert(checkbox.visible and dropdown.selected==3 and not gui.entries['Range Color'].visible)
assert(not gui.scrollframe.visible and not gui.empty.visible)
assert(not ShaguTweaks_config['Golden UI'])
gui.search:SetText('mail'); assert(gui.entries['Item Rarity Borders'].visible and not checkbox.visible)
gui.search:SetText('mail'); assert(gui.entries['Item Rarity Borders'].visible and checkbox.visible)
gui.search:SetText('update interval'); assert(gui.entries['Range Color'].visible and gui.entries['slider:range.delay'].visible)
gui.search:SetText('[.*'); assert(gui.empty.visible) -- literal, not Lua pattern matching
this=gui.searchClear; gui.searchClear.scripts.OnClick(); this=nil
assert(not gui.empty.visible and checkbox.visible and checkbox.checked and gui.scrollframe.visible)
assert(not gui.empty.visible and checkbox.visible and dropdown.selected==3 and gui.scrollframe.visible)
assert(made==firstCount) -- subsequent searches reuse all widgets
this=gui.okay; gui.okay.scripts.OnClick(); this=nil
assert(ShaguTweaks_config['Golden UI']==1 and reloaded)
assert(ShaguTweaks_config['Golden UI']==1 and ShaguTweaks_config['Darkened UI']==0 and reloaded)
-- Defaults and Cancel change only the temporary selection.
this=gui;gui.scripts.OnShow();this=nil
gui:defaults();assert(dropdown.selected==1 and ShaguTweaks_config['Golden UI']==1)
this=gui.cancel;gui.cancel.scripts.OnClick();this=nil
this=gui;gui.scripts.OnShow();this=nil;assert(dropdown.selected==3)
ShaguTweaks_config['Darkened UI']=1
this=gui;gui.scripts.OnShow();this=nil;assert(dropdown.selected==2)
assert(ShaguTweaks_config['Golden UI']==1) -- opening does not alter saved config
choose(dropdown,1);this=gui.okay;gui.okay.scripts.OnClick();this=nil
assert(ShaguTweaks_config['Golden UI']==0 and ShaguTweaks_config['Darkened UI']==0)
for _,color in ipairs({{r=.3,g=.3,b=.3},{r=.8,g=.62,b=.32}}) do
ShaguTweaks.UIBorder=color;gui:load()
for _,suffix in ipairs({'Left','Middle','Right'}) do
local c=_G['AdvancedSettingsGUIUIMode'..suffix].color
assert(c[1]==color.r and c[2]==color.g and c[3]==color.b and c[4]==1)
end
end
print('Search: literal/case-insensitive matching, descriptions/sliders, collapsed categories, empty/clear, widget reuse, scroll layouts and unsaved settings passed')
+18 -2
View File
@@ -25,9 +25,10 @@ for _,name in ipairs({'GossipFrameGreetingPanel','QuestFrameGreetingPanel','Ques
function panel:CreateTexture(_,layer) assert(layer=='ARTWORK');local t=texture();table.insert(regions,t);return t end
restore(panel);assert(table.getn(regions)==9)
local left,right=regions[1].stQuestPaper,regions[2].stQuestPaper
assert(left.width==234 and left.height==182 and left.color[1]==1)
assert(left.width==234 and left.height==174 and left.color[1]==1)
assert(right.width==67 and right.uv[2]==67/128)
assert(regions[4].stQuestPaper.height==148)
assert(left.point[5]==-82 and regions[3].stQuestPaper.uv[4]==160/256)
assert(regions[4].stQuestPaper.height==160)
assert(not regions[5].stQuestPaper) -- the bottom patch is not a 256px tile
restore(panel);assert(table.getn(regions)==9)
end
@@ -36,3 +37,18 @@ assert(not string.find(source,'["^QuestFrame(.+)Panel$"]',1,true))
local f=assert(io.open(root..'mods/reduced-actionbar.lua','r'));local reduced=f:read('*a');f:close()
assert(loadstring(reduced));assert(string.find(reduced,'ShaguTweaks.EnableActionBarMovers()',1,true))
print('Native quest/gossip parchment: four original crops, right-tile dimensions, no stationery overlay, idempotence; Reduced Actionbar enables movement')
local start=assert(string.find(source,'local function ShadeNpcTitleBar',1,true))
local finish=assert(string.find(source,'local function ShadeTitleBars',start,true))
local shade=assert(loadstring(string.sub(source,start,finish-1)..'return ShadeNpcTitleBar'))()
local rootFrame={GetFrameLevel=function() return 1 end}
local nameFrame={level=2,GetFrameLevel=function(self) return self.level end,SetFrameLevel=function(self,v) self.level=v end}
local calls=0
function ShadeTitleBar(frame,strip,title,color,level)
calls=calls+1;assert(frame==rootFrame and title==nil and level==61)
assert(strip[2]==-22 and strip[4]==-39)
frame.stTitleBar={}
end
shade(rootFrame,nameFrame,{[1]={GetFrameLevel=function() return 57 end},[3]={GetFrameLevel=function() return 60 end}}, {})
assert(nameFrame.level==62 and calls==1)
shade(rootFrame,nameFrame,{},{});assert(calls==1)
print('NPC Golden title shade stays above all panels and below the native name, without duplicate text or shade')
+113
View File
@@ -0,0 +1,113 @@
local root=(arg[1] or '.')..'/'
local f=assert(io.open(root..'mods/dark-ui-elements.lua'));local source=f:read('*a');f:close()
assert(loadstring(source))
local function Extract(first,last,suffix,prefix)
local a=assert(string.find(source,first,1,true))
local b=assert(string.find(source,last,a,true))
return assert(loadstring((prefix or '')..string.sub(source,a,b-1)..suffix))()
end
local palette='local tint={r=.3,g=.3,b=.3,a=.9};local edge={r=.3,g=.3,b=.3,a=.9};'
ShaguTweaks={}
local darken=Extract('local function IsExcludedFrame','local ADDON_PREFIXES =','return DarkenFrame',
palette..'local backgrounds,borders,regionskips={},{},{};')
local lock=Extract('local ADDON_PREFIXES =','local function LockTextureBorder','return LockBorder',palette)
local frame={stPreserveRarityColor=true,color={0,1,0,1}}
function frame:GetName() return 'MailAttachmentAudit' end
function frame:GetChildren() end
function frame:GetRegions() end
function frame:SetBackdropBorderColor(r,g,b,a) self.color={r,g,b,a} end
function frame:GetBackdropBorderColor() return unpack(self.color) end
lock(frame);assert(frame.color[2]==1)
darken(frame);assert(frame.color[1]==0 and frame.color[2]==1)
print('Recursive recoloring preserves marked rarity borders')
local redraw=Extract('local function RedrawTitle','local function ShadeTitleBar','return RedrawTitle')
local title={text='Old title'}
function title:GetText() return self.text end
function title:SetText(text) self.text=text end
function title:SetFormattedText(format,value) self.text=string.format(format,value) end
function title:SetAlpha(value) self.alpha=value end
local holder={}
function holder:CreateFontString()
return {SetAllPoints=function() end,SetText=function(self,text) self.text=text end}
end
local copy=redraw(holder,title)
title:SetFormattedText('New %s','title')
assert(title.text=='New title' and copy.text=='New title' and title.alpha==0)
title:SetText('Plain again');assert(copy.text=='Plain again')
print('Golden title copies track plain and formatted text')
local restore=Extract('local function RestoreMailParchment','module.enable =','return RestoreMailParchment')
local made=0
function ShaguTweaks.HookScript(object,event,callback) object[event]=callback end
local function noop() end
InboxFrame={CreateTexture=function()
made=made+1
return {SetTexture=noop,SetVertexColor=noop,SetTexCoord=noop,SetWidth=noop,SetHeight=noop,SetPoint=noop}
end}
MailFrameTopLeft=nil;MailFrameTopRight=nil;MailFrameBotLeft=nil;MailFrameBotRight=nil
restore();assert(not InboxFrame.stOriginalParchment and made==0 and InboxFrame.OnShow)
local width=0
MailFrameTopLeft={GetTexture=function() return 'LateTile' end,GetWidth=function() return width end,GetHeight=function() return 256 end}
local function tile() return {GetTexture=function() return 'NativeTile' end,GetWidth=function() return 256 end,GetHeight=function() return 256 end} end
MailFrameTopRight=tile();MailFrameBotLeft=tile();MailFrameBotRight=tile()
InboxFrame.OnShow();assert(made==3 and not InboxFrame.stOriginalParchment)
local right=InboxFrame.stParchmentTiles[2]
width=256;InboxFrame.OnShow();assert(made==4 and InboxFrame.stOriginalParchment)
assert(InboxFrame.stParchmentTiles[2]==right)
restore();InboxFrame.OnShow();assert(made==4)
-- Transparent/common rarity borders must retain their exact color and alpha.
frame.color={1,1,1,.4};darken(frame);assert(frame.color[1]==1 and frame.color[4]==.4)
frame.stPreserveRarityColor=nil;darken(frame);assert(frame.color[1]==.3)
print('Mail parchment: absent/zero-size tiles retry on show, partial pieces reused, completed initialization idempotent')
local blocked=Extract('local PATH_CACHE_LIMIT =','local function AddSpecialBackground','return IsBlacklisted',
'local blacklist={StationeryTest=true};local whitelist={};')
for _,path in ipairs({'Interface\\Stationery\\Stationery_Illidan','Interface\\Stationery\\StationeryTest1','interface\\stationery\\custom'}) do
local paper={GetName=function() return 'Paper' end,GetTexture=function() return path end}
assert(blocked(paper))
end
assert(blocked({GetName=function() return 'StationeryBackgroundRight' end,GetTexture=function() return 'CustomPaper' end}))
-- Exercise the real traversal with an already-assigned stationery texture.
IsBlacklisted=blocked
IsSlotArtwork=Extract('local function IsSlotArtwork','local function TintSlotBorder','return IsSlotArtwork')
local paper={GetName=function() return 'StationeryBackgroundLeft' end,
GetTexture=function() return 'Interface\\Stationery\\Stationery_Default' end,
GetObjectType=function() return 'Texture' end,GetBlendMode=function() return 'BLEND' end,
SetVertexColor=function() error('Recursive recoloring must not tint stationery') end}
frame.GetRegions=function() return paper end
darken(frame);darken(frame)
for _,path in ipairs({'Interface\\QuestFrame\\UI-QuestLog-Empty-TopLeft','Interface\\QuestFrame\\UI-QuestLog-Empty-BotRight'}) do
assert(blocked({GetName=function() return nil end,GetTexture=function() return path end}))
end
assert(not string.find(source,'["^QuestLogDetailScrollFrame$"]',1,true))
assert(not string.find(source,'["^EmptyQuestLogFrame$"]',1,true))
print('Send-mail stationery and empty quest-log artwork are excluded from tinting; replacement quest-log stationery removed')
local restoreLog=Extract('local function RestoreQuestLogParchment','local function DarkenFrame','return RestoreQuestLogParchment')
local regions={}
local textureMethods={}
function textureMethods:GetTexture() return self.path end
function textureMethods:GetWidth() return self.width end
function textureMethods:GetHeight() return self.height end
function textureMethods:SetTexture(v) self.path=v end
function textureMethods:SetWidth(v) self.width=v end
function textureMethods:SetHeight(v) self.height=v end
function textureMethods:SetPoint(...) self.point=arg end
function textureMethods:SetTexCoord(...) self.coords=arg end
function textureMethods:SetVertexColor(...) self.color=arg end
local function native(path,width,height)
local t=setmetatable({path=path,width=width,height=height},{__index=textureMethods})
table.insert(regions,t);return t
end
local left=native('Interface\\QuestFrame\\UI-QuestLog-Left',512,512)
local right=native('Interface\\QuestFrame\\UI-QuestLog-Right',256,512)
local log={GetName=function() return 'QuestLogFrame' end,GetRegions=function() return unpack(regions) end}
function log:CreateTexture(_,layer) assert(layer=='OVERLAY');return native() end
restoreLog(log)
assert(table.getn(regions)==5 and table.getn(left.stQuestLogCrops)==2 and table.getn(right.stQuestLogCrops)==1)
local paper=left.stQuestLogCrops[2]
assert(paper.path==left.path and paper.width==164 and paper.height==382 and paper.point[4]==348)
assert(paper.coords[1]==348/512 and paper.color[4]==1 and paper.stOriginalQuestPaper)
assert(right.stQuestLogCrops[1].width==136 and right.stQuestLogCrops[1].height==382)
assert(left.stQuestLogCrops[1].width==301 and left.stQuestLogCrops[1].height==362)
restoreLog(log);assert(table.getn(regions)==5)
print('Native Turtle quest log: original list/paper textures, torn-edge crops, white opaque fills and idempotent reuse')
+21
View File
@@ -0,0 +1,21 @@
local root=(arg[1] or '.')..'/'
local f=assert(io.open(root..'mods/dark-ui-elements.lua'));local source=f:read('*a');f:close();assert(loadstring(source))
local a=assert(string.find(source,'local function IsExcludedFrame',1,true))
local b=assert(string.find(source,'local ADDON_PREFIXES =',a,true))
ShaguTweaks={}
local prefix='local tint={r=.3,g=.3,b=.3,a=.9};local edge=tint;local backgrounds,borders,regionskips={},{},{};'
local darken=assert(loadstring(prefix..string.sub(source,a,b-1)..'return DarkenFrame'))()
local function frame(name)
return {GetName=function() return name end,GetChildren=function() end,GetRegions=function() end,
SetBackdropBorderColor=function() end}
end
local parent,child=frame('Parent'),frame('Child')
parent.GetChildren=function() return child end
darken(parent);assert(not ShaguTweaks.RecolorCapture)
local c={roots={},rootIndex={},seen={},frameVisits=0,uniqueFrames=0,repeatVisits=0,borderWrites=0,textureWrites=0}
ShaguTweaks.RecolorCapture=c
darken(parent);darken(parent)
assert(c.frameVisits==4 and c.uniqueFrames==2 and c.repeatVisits==2 and c.borderWrites==4)
assert(table.getn(c.roots)==1 and c.roots[1].calls==2 and c.roots[1].frames==4 and c.roots[1].tables==8)
ShaguTweaks.RecolorCapture=nil;darken(parent);assert(c.frameVisits==4)
print('Recolor diagnostics: opt-in counters, root versus recursive calls, unique/repeat visits, traversal table counts and inactive cleanup passed')
+56
View File
@@ -0,0 +1,56 @@
local root=(arg[1] or '.')..'/'
local f=assert(io.open(root..'mods/dark-ui-elements.lua'));local source=f:read('*a');f:close()
assert(loadstring(source))
local function Slice(first,last)
local a=assert(string.find(source,first,1,true));local b=assert(string.find(source,last,a,true))
return string.sub(source,a,b-1)
end
local lowerCalls=0
local environment=setmetatable({ShaguTweaks={},CountLower=function() lowerCalls=lowerCalls+1 end},{__index=getfenv(0)})
local chunk=assert(loadstring('local originalString=string;local string={find=string.find,lower=function(path) CountLower();return originalString.lower(path) end};'
..Slice('local blacklist =','local regionskips =')
..Slice('local whitelist =','local showframes =')
..Slice('local PATH_CACHE_LIMIT =','local function AddSpecialBackground')
..'return IsBlacklisted,pathCache,pathKeys,PATH_CACHE_LIMIT'))
setfenv(chunk,environment)
local blocked,cache,keys,limit=chunk()
local texture={path='Interface\\FrameXML\\CustomFrameArt',name='FrameArt'}
function texture:GetName() return self.name end
function texture:GetTexture() return self.path end
assert(not blocked(texture) and lowerCalls==1)
texture.name='ActionIcon';assert(blocked(texture) and lowerCalls==1)
texture.name='FrameArt';assert(not blocked(texture) and lowerCalls==1)
texture.path='Interface\\AddOns\\Outfitter\\Textures\\OutfitterFrame';texture.name='Icon'
assert(not blocked(texture)) -- explicit whitelist precedes ordinary name exclusions
texture.name='StationeryBackgroundLeft';assert(blocked(texture))
texture.path='Interface\\MailFrame\\MailItemBorder';texture.name='FrameArt';assert(blocked(texture))
texture.path='Interface\\Stationery\\CustomPaper';assert(blocked(texture))
texture.path='Interface\\QuestFrame\\UI-QuestLog-Empty-BotLeft';assert(blocked(texture))
texture.path='Interface\\Icons\\Test';assert(blocked(texture))
texture.path=nil;assert(blocked(texture))
for i=1,300 do texture.path='Interface\\FrameXML\\CustomArt'..i;assert(not blocked(texture)) end
local count=0;for _ in pairs(cache) do count=count+1 end
assert(count==limit and table.getn(keys)==limit and not cache['Interface\\FrameXML\\CustomFrameArt'])
texture.path='Interface\\FrameXML\\CustomFrameArt';local previous=lowerCalls
assert(not blocked(texture) and lowerCalls==previous+1) -- evicted entries recompute safely
local queue,passes,hooks={},{},0
local env=setmetatable({},{__index=getfenv(0)})
env._G=env
env.C_Timer={After=function(delay,fn) assert(delay==0);table.insert(queue,fn) end}
env.ShaguTweaks={HookScript=function(frame,event,fn) hooks=hooks+1;frame[event]=fn end}
env.golden={}
env.DarkenFrame=function(frame) table.insert(passes,frame.children) end
local showChunk=assert(loadstring(Slice('local function RecolorOnShow','local function LockAddonFrames')..'return RecolorOnShow'))
setfenv(showChunk,env);local install=showChunk()
local frame={children=0,GetName=function() return 'RollForLootFrameHeader' end,IsShown=function() return false end}
install(frame);install(frame);assert(hooks==1 and table.getn(passes)==0)
frame.OnShow();frame.OnShow();frame.OnShow()
assert(table.getn(passes)==3 and table.getn(queue)==1)
frame.children=10 -- late-created rows must be included by the trailing pass
local callback=table.remove(queue,1);callback()
assert(passes[4]==10)
frame.OnShow();assert(table.getn(queue)==1 and queue[1]==callback) -- reuse callback/rearm
queue[1]();queue={}
env.C_Timer=nil;frame.OnShow();assert(table.getn(queue)==0)
print('Recolors: bounded path cache, live name exclusions, whitelist/paper rules, eviction; immediate passes retained, one trailing callback, late children and rearming passed')