diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ab3abb..fb22307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 87c99bf..cc2e4be 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/ShaguTweaks.toc b/ShaguTweaks.toc index c3f7142..275763d 100644 --- a/ShaguTweaks.toc +++ b/ShaguTweaks.toc @@ -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 diff --git a/config.lua b/config.lua index 67e6779..d725736 100644 --- a/config.lua +++ b/config.lua @@ -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 diff --git a/helpers.lua b/helpers.lua index e35c713..02693bd 100644 --- a/helpers.lua +++ b/helpers.lua @@ -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 \ No newline at end of file +end diff --git a/mods/dark-ui-elements.lua b/mods/dark-ui-elements.lua index 19e2d38..c6ab64a 100644 --- a/mods/dark-ui-elements.lua +++ b/mods/dark-ui-elements.lua @@ -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, +} diff --git a/mods/equip-compare.lua b/mods/equip-compare.lua index f0a42dc..a9686a7 100644 --- a/mods/equip-compare.lua +++ b/mods/equip-compare.lua @@ -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 diff --git a/tests/README.md b/tests/README.md index f4f51a7..3a95a34 100644 --- a/tests/README.md +++ b/tests/README.md @@ -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. diff --git a/tests/test_equip_compare_native.lua b/tests/test_equip_compare_native.lua index 242e743..efd7948 100644 --- a/tests/test_equip_compare_native.lua +++ b/tests/test_equip_compare_native.lua @@ -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 diff --git a/tests/test_golden_unit_names.lua b/tests/test_golden_unit_names.lua index ea12e2a..49bfa34 100644 --- a/tests/test_golden_unit_names.lua +++ b/tests/test_golden_unit_names.lua @@ -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) diff --git a/tests/test_initialization_hooks.lua b/tests/test_initialization_hooks.lua new file mode 100644 index 0000000..8fe428d --- /dev/null +++ b/tests/test_initialization_hooks.lua @@ -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') diff --git a/tests/test_options_search.lua b/tests/test_options_search.lua index 8ff865f..d511015 100644 --- a/tests/test_options_search.lua +++ b/tests/test_options_search.lua @@ -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') diff --git a/tests/test_quest_parchment.lua b/tests/test_quest_parchment.lua index bb467b2..4eee592 100644 --- a/tests/test_quest_parchment.lua +++ b/tests/test_quest_parchment.lua @@ -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') diff --git a/tests/test_recolor_audit_regressions.lua b/tests/test_recolor_audit_regressions.lua new file mode 100644 index 0000000..4d88bf4 --- /dev/null +++ b/tests/test_recolor_audit_regressions.lua @@ -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') diff --git a/tests/test_recolor_profile.lua b/tests/test_recolor_profile.lua new file mode 100644 index 0000000..6b96f74 --- /dev/null +++ b/tests/test_recolor_profile.lua @@ -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') diff --git a/tests/test_recolor_scheduling.lua b/tests/test_recolor_scheduling.lua new file mode 100644 index 0000000..6ba1be0 --- /dev/null +++ b/tests/test_recolor_scheduling.lua @@ -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')