From 8c281ce410d0f89444ad919771af4721ba8113f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:51:37 +0000 Subject: [PATCH 01/15] Implement combo point settings icon alignment Agent-Logs-Url: https://github.com/argium/EnhancedCooldownManager/sessions/b3fb4382-8c9f-4a2f-8075-ef0b6e12e1e0 Co-authored-by: argium <15852038+argium@users.noreply.github.com> --- ARCHITECTURE.md | 2 +- Tests/UI/ResourceBarOptions_spec.lua | 25 ++++++++-------- UI/ResourceBarOptions.lua | 45 +++++++++++++++++++--------- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4d5fa523..8fb43e2a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -191,7 +191,7 @@ LibEditMode detects WoW's Edit Mode enter/exit. On enter, all modules are forced ### Options UI -Setting changes flow through LibSettingsBuilder's `onChange` → `Runtime.ScheduleLayoutUpdate(0, "OptionsChanged")`. +Setting changes flow through LibSettingsBuilder's `onChange` → `Runtime.ScheduleLayoutUpdate(0, "OptionsChanged")`. Resource bar color rows prepend a fixed two-icon prefix so shared resources like Combo Points can show multiple class icons while keeping the icon column right-aligned. ### Watchdog Ticker diff --git a/Tests/UI/ResourceBarOptions_spec.lua b/Tests/UI/ResourceBarOptions_spec.lua index 9abbef66..5d91164f 100644 --- a/Tests/UI/ResourceBarOptions_spec.lua +++ b/Tests/UI/ResourceBarOptions_spec.lua @@ -8,6 +8,7 @@ local TestHelpers = describe("ResourceBarOptions getters/setters/defaults", function() local originalGlobals local profile, defaults, SB, ns, settings, capturedTable + local emptyIcon = "|TInterface\\Buttons\\WHITE8X8:14:14:0:0:8:8:0:0:0:0|t" setup(function() originalGlobals = TestHelpers.CaptureGlobals(TestHelpers.OPTIONS_GLOBALS) @@ -153,15 +154,15 @@ describe("ResourceBarOptions getters/setters/defaults", function() defsByKey[def.key] = def.name end - assert.are.equal("|A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DH"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_VENGEANCE_SOULS]) - assert.are.equal("|A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DEVOURER"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_DEVOURER_NORMAL]) - assert.are.equal("|A:classicon-mage:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MAGE .. ns.L["RESOURCE_ICICLES"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_ICICLES]) - assert.are.equal("|A:classicon-monk:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MONK .. ns.L["RESOURCE_CHI"] .. "|r", defsByKey[Enum.PowerType.Chi]) - assert.are.equal("|A:classicon-rogue:14:14|a |cff" .. ns.Constants.CLASS_COLORS.ROGUE .. ns.L["RESOURCE_COMBO_POINTS"] .. "|r", defsByKey[Enum.PowerType.ComboPoints]) - assert.are.equal("|A:classicon-evoker:14:14|a |cff" .. ns.Constants.CLASS_COLORS.EVOKER .. ns.L["RESOURCE_ESSENCE"] .. "|r", defsByKey[Enum.PowerType.Essence]) - assert.are.equal("|A:classicon-paladin:14:14|a |cff" .. ns.Constants.CLASS_COLORS.PALADIN .. ns.L["RESOURCE_HOLY_POWER"] .. "|r", defsByKey[Enum.PowerType.HolyPower]) - assert.are.equal("|A:classicon-shaman:14:14|a |cff" .. ns.Constants.CLASS_COLORS.SHAMAN .. ns.L["RESOURCE_MAELSTROM_WEAPON"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_MAELSTROM_WEAPON]) - assert.are.equal("|A:classicon-warlock:14:14|a |cff" .. ns.Constants.CLASS_COLORS.WARLOCK .. ns.L["RESOURCE_SOUL_SHARDS"] .. "|r", defsByKey[Enum.PowerType.SoulShards]) + assert.are.equal(emptyIcon .. "|A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DH"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_VENGEANCE_SOULS]) + assert.are.equal(emptyIcon .. "|A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DEVOURER"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_DEVOURER_NORMAL]) + assert.are.equal(emptyIcon .. "|A:classicon-mage:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MAGE .. ns.L["RESOURCE_ICICLES"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_ICICLES]) + assert.are.equal(emptyIcon .. "|A:classicon-monk:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MONK .. ns.L["RESOURCE_CHI"] .. "|r", defsByKey[Enum.PowerType.Chi]) + assert.are.equal("|A:classicon-druid:14:14|a|A:classicon-rogue:14:14|a |cff" .. ns.Constants.CLASS_COLORS.ROGUE .. ns.L["RESOURCE_COMBO_POINTS"] .. "|r", defsByKey[Enum.PowerType.ComboPoints]) + assert.are.equal(emptyIcon .. "|A:classicon-evoker:14:14|a |cff" .. ns.Constants.CLASS_COLORS.EVOKER .. ns.L["RESOURCE_ESSENCE"] .. "|r", defsByKey[Enum.PowerType.Essence]) + assert.are.equal(emptyIcon .. "|A:classicon-paladin:14:14|a |cff" .. ns.Constants.CLASS_COLORS.PALADIN .. ns.L["RESOURCE_HOLY_POWER"] .. "|r", defsByKey[Enum.PowerType.HolyPower]) + assert.are.equal(emptyIcon .. "|A:classicon-shaman:14:14|a |cff" .. ns.Constants.CLASS_COLORS.SHAMAN .. ns.L["RESOURCE_MAELSTROM_WEAPON"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_MAELSTROM_WEAPON]) + assert.are.equal(emptyIcon .. "|A:classicon-warlock:14:14|a |cff" .. ns.Constants.CLASS_COLORS.WARLOCK .. ns.L["RESOURCE_SOUL_SHARDS"] .. "|r", defsByKey[Enum.PowerType.SoulShards]) assert.is_nil(defsByKey[Enum.PowerType.ArcaneCharges]) end) end) @@ -206,15 +207,15 @@ describe("ResourceBarOptions getters/setters/defaults", function() end assert.are.equal( - "|A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DEVOURER"] .. "|r", + emptyIcon .. "|A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DEVOURER"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_DEVOURER_NORMAL] ) assert.are.equal( - "|A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_VOID_FRAGMENTS_DEVOURER"] .. "|r", + emptyIcon .. "|A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_VOID_FRAGMENTS_DEVOURER"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_DEVOURER_META] ) assert.are.equal( - "|A:classicon-mage:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MAGE .. ns.L["RESOURCE_ICICLES"] .. "|r", + emptyIcon .. "|A:classicon-mage:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MAGE .. ns.L["RESOURCE_ICICLES"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_ICICLES] ) end) diff --git a/UI/ResourceBarOptions.lua b/UI/ResourceBarOptions.lua index f1a6a89f..17307459 100644 --- a/UI/ResourceBarOptions.lua +++ b/UI/ResourceBarOptions.lua @@ -6,29 +6,46 @@ local _, ns = ... local C = ns.Constants local L = ns.L local COLOR_WHITE_HEX = C.COLOR_WHITE_HEX or "FFFFFF" +local RESOURCE_ICON_SIZE = 14 +local RESOURCE_ICON_SLOTS = 2 +local EMPTY_RESOURCE_ICON = "|TInterface\\Buttons\\WHITE8X8:14:14:0:0:8:8:0:0:0:0|t" -local function createResourceColorName(className, label) - local color = (C.CLASS_COLORS and C.CLASS_COLORS[className]) or COLOR_WHITE_HEX - local icon = className and ("|A:classicon-" .. string.lower(className) .. ":14:14|a ") or "" - return icon .. "|cff" .. color .. label .. "|r" +local function createResourceClassIcon(className) + return "|A:classicon-" .. string.lower(className) .. ":" .. RESOURCE_ICON_SIZE .. ":" .. RESOURCE_ICON_SIZE .. "|a" +end + +local function createResourceColorName(colorClassName, label, iconClasses) + local color = (C.CLASS_COLORS and C.CLASS_COLORS[colorClassName]) or COLOR_WHITE_HEX + local prefix = "" + local padding = RESOURCE_ICON_SLOTS - #iconClasses + + for _ = 1, padding do + prefix = prefix .. EMPTY_RESOURCE_ICON + end + + for _, className in ipairs(iconClasses) do + prefix = prefix .. createResourceClassIcon(className) + end + + return prefix .. " |cff" .. color .. label .. "|r" end local RESOURCE_COLOR_DEFS = { { key = C.RESOURCEBAR_TYPE_VENGEANCE_SOULS, - name = createResourceColorName("DEMONHUNTER", L["RESOURCE_SOUL_FRAGMENTS_DH"]), + name = createResourceColorName("DEMONHUNTER", L["RESOURCE_SOUL_FRAGMENTS_DH"], { "DEMONHUNTER" }), }, { key = C.RESOURCEBAR_TYPE_DEVOURER_NORMAL, - name = createResourceColorName("DEMONHUNTER", L["RESOURCE_SOUL_FRAGMENTS_DEVOURER"]), + name = createResourceColorName("DEMONHUNTER", L["RESOURCE_SOUL_FRAGMENTS_DEVOURER"], { "DEMONHUNTER" }), }, { key = C.RESOURCEBAR_TYPE_DEVOURER_META, - name = createResourceColorName("DEMONHUNTER", L["RESOURCE_VOID_FRAGMENTS_DEVOURER"]), + name = createResourceColorName("DEMONHUNTER", L["RESOURCE_VOID_FRAGMENTS_DEVOURER"], { "DEMONHUNTER" }), }, { key = C.RESOURCEBAR_TYPE_ICICLES, - name = createResourceColorName("MAGE", L["RESOURCE_ICICLES"]), + name = createResourceColorName("MAGE", L["RESOURCE_ICICLES"], { "MAGE" }), }, -- { -- -- Secret 2026/03 @@ -37,27 +54,27 @@ local RESOURCE_COLOR_DEFS = { -- }, { key = Enum.PowerType.Chi, - name = createResourceColorName("MONK", L["RESOURCE_CHI"]), + name = createResourceColorName("MONK", L["RESOURCE_CHI"], { "MONK" }), }, { key = Enum.PowerType.ComboPoints, - name = createResourceColorName("ROGUE", L["RESOURCE_COMBO_POINTS"]), + name = createResourceColorName("ROGUE", L["RESOURCE_COMBO_POINTS"], { "DRUID", "ROGUE" }), }, { key = Enum.PowerType.Essence, - name = createResourceColorName("EVOKER", L["RESOURCE_ESSENCE"]), + name = createResourceColorName("EVOKER", L["RESOURCE_ESSENCE"], { "EVOKER" }), }, { key = Enum.PowerType.HolyPower, - name = createResourceColorName("PALADIN", L["RESOURCE_HOLY_POWER"]), + name = createResourceColorName("PALADIN", L["RESOURCE_HOLY_POWER"], { "PALADIN" }), }, { key = C.RESOURCEBAR_TYPE_MAELSTROM_WEAPON, - name = createResourceColorName("SHAMAN", L["RESOURCE_MAELSTROM_WEAPON"]), + name = createResourceColorName("SHAMAN", L["RESOURCE_MAELSTROM_WEAPON"], { "SHAMAN" }), }, { key = Enum.PowerType.SoulShards, - name = createResourceColorName("WARLOCK", L["RESOURCE_SOUL_SHARDS"]), + name = createResourceColorName("WARLOCK", L["RESOURCE_SOUL_SHARDS"], { "WARLOCK" }), }, } From d89636062850773635927518dd95f6e78c80344c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 23:40:59 +0000 Subject: [PATCH 02/15] Address ResourceBar icon prefix review comments Agent-Logs-Url: https://github.com/argium/EnhancedCooldownManager/sessions/62c09ac2-c5ff-49db-b34a-bfc321e73876 Co-authored-by: argium <15852038+argium@users.noreply.github.com> --- Tests/UI/ResourceBarOptions_spec.lua | 2 +- UI/ResourceBarOptions.lua | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Tests/UI/ResourceBarOptions_spec.lua b/Tests/UI/ResourceBarOptions_spec.lua index e20f4fcc..f80f64e7 100644 --- a/Tests/UI/ResourceBarOptions_spec.lua +++ b/Tests/UI/ResourceBarOptions_spec.lua @@ -8,7 +8,7 @@ local TestHelpers = describe("ResourceBarOptions getters/setters/defaults", function() local originalGlobals local profile, defaults, SB, ns, settings, capturedPage - local emptyIcon = "|TInterface\\Buttons\\WHITE8X8:14:14:0:0:8:8:0:0:0:0|t" + local emptyIcon = "|TInterface\\Buttons\\WHITE8X8:14:14:0:0:8:8:0:0:0:0:0:0:0:0|t" local function getPageRow(path) for _, row in ipairs(capturedPage.rows) do diff --git a/UI/ResourceBarOptions.lua b/UI/ResourceBarOptions.lua index 8b497e0e..8b4390fe 100644 --- a/UI/ResourceBarOptions.lua +++ b/UI/ResourceBarOptions.lua @@ -8,7 +8,12 @@ local L = ns.L local COLOR_WHITE_HEX = C.COLOR_WHITE_HEX or "FFFFFF" local RESOURCE_ICON_SIZE = 14 local RESOURCE_ICON_SLOTS = 2 -local EMPTY_RESOURCE_ICON = "|TInterface\\Buttons\\WHITE8X8:14:14:0:0:8:8:0:0:0:0|t" +local EMPTY_RESOURCE_ICON = + "|TInterface\\Buttons\\WHITE8X8:" + .. RESOURCE_ICON_SIZE + .. ":" + .. RESOURCE_ICON_SIZE + .. ":0:0:8:8:0:0:0:0:0:0:0:0|t" local function createResourceClassIcon(className) return "|A:classicon-" .. string.lower(className) .. ":" .. RESOURCE_ICON_SIZE .. ":" .. RESOURCE_ICON_SIZE .. "|a" @@ -17,14 +22,16 @@ end local function createResourceColorName(colorClassName, label, iconClasses) local color = (C.CLASS_COLORS and C.CLASS_COLORS[colorClassName]) or COLOR_WHITE_HEX local prefix = "" - local padding = RESOURCE_ICON_SLOTS - #iconClasses + local iconCount = math.min(#iconClasses, RESOURCE_ICON_SLOTS) + local padding = math.max(0, RESOURCE_ICON_SLOTS - iconCount) + local startIndex = math.max(1, #iconClasses - RESOURCE_ICON_SLOTS + 1) for _ = 1, padding do prefix = prefix .. EMPTY_RESOURCE_ICON end - for _, className in ipairs(iconClasses) do - prefix = prefix .. createResourceClassIcon(className) + for i = startIndex, #iconClasses do + prefix = prefix .. createResourceClassIcon(iconClasses[i]) end return prefix .. " |cff" .. color .. label .. "|r" @@ -50,7 +57,7 @@ local RESOURCE_COLOR_DEFS = { -- { -- -- Secret 2026/03 -- key = Enum.PowerType.ArcaneCharges, - -- name = createResourceColorName("MAGE", L["RESOURCE_ARCANE_CHARGES"]), + -- name = createResourceColorName("MAGE", L["RESOURCE_ARCANE_CHARGES"], { "MAGE" }), -- }, { key = Enum.PowerType.Chi, From 66867c21f4c125a351134785b49daea5c08d6a61 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 23:41:53 +0000 Subject: [PATCH 03/15] Inline ResourceBar icon prefix helper Agent-Logs-Url: https://github.com/argium/EnhancedCooldownManager/sessions/62c09ac2-c5ff-49db-b34a-bfc321e73876 Co-authored-by: argium <15852038+argium@users.noreply.github.com> --- UI/ResourceBarOptions.lua | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/UI/ResourceBarOptions.lua b/UI/ResourceBarOptions.lua index 8b4390fe..9d9bb3ed 100644 --- a/UI/ResourceBarOptions.lua +++ b/UI/ResourceBarOptions.lua @@ -15,9 +15,6 @@ local EMPTY_RESOURCE_ICON = .. RESOURCE_ICON_SIZE .. ":0:0:8:8:0:0:0:0:0:0:0:0|t" -local function createResourceClassIcon(className) - return "|A:classicon-" .. string.lower(className) .. ":" .. RESOURCE_ICON_SIZE .. ":" .. RESOURCE_ICON_SIZE .. "|a" -end local function createResourceColorName(colorClassName, label, iconClasses) local color = (C.CLASS_COLORS and C.CLASS_COLORS[colorClassName]) or COLOR_WHITE_HEX @@ -31,7 +28,7 @@ local function createResourceColorName(colorClassName, label, iconClasses) end for i = startIndex, #iconClasses do - prefix = prefix .. createResourceClassIcon(iconClasses[i]) + prefix = prefix .. "|A:classicon-" .. string.lower(iconClasses[i]) .. ":" .. RESOURCE_ICON_SIZE .. ":" .. RESOURCE_ICON_SIZE .. "|a" end return prefix .. " |cff" .. color .. label .. "|r" From a76c7699f9f8a117ab3912147caeec78684e9edc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 23:42:48 +0000 Subject: [PATCH 04/15] Document ResourceBar transparent icon spacer Agent-Logs-Url: https://github.com/argium/EnhancedCooldownManager/sessions/62c09ac2-c5ff-49db-b34a-bfc321e73876 Co-authored-by: argium <15852038+argium@users.noreply.github.com> --- UI/ResourceBarOptions.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/UI/ResourceBarOptions.lua b/UI/ResourceBarOptions.lua index 9d9bb3ed..be41d12e 100644 --- a/UI/ResourceBarOptions.lua +++ b/UI/ResourceBarOptions.lua @@ -8,6 +8,7 @@ local L = ns.L local COLOR_WHITE_HEX = C.COLOR_WHITE_HEX or "FFFFFF" local RESOURCE_ICON_SIZE = 14 local RESOURCE_ICON_SLOTS = 2 +-- Transparent texture slot for right-aligning rows with fewer class icons. local EMPTY_RESOURCE_ICON = "|TInterface\\Buttons\\WHITE8X8:" .. RESOURCE_ICON_SIZE From dab0c8c6ba52cfd0bfc5322043a14d3d61b68b07 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 23:44:02 +0000 Subject: [PATCH 05/15] Remove stale Arcane Charges comment block Agent-Logs-Url: https://github.com/argium/EnhancedCooldownManager/sessions/62c09ac2-c5ff-49db-b34a-bfc321e73876 Co-authored-by: argium <15852038+argium@users.noreply.github.com> --- UI/ResourceBarOptions.lua | 5 ----- 1 file changed, 5 deletions(-) diff --git a/UI/ResourceBarOptions.lua b/UI/ResourceBarOptions.lua index be41d12e..2be09d04 100644 --- a/UI/ResourceBarOptions.lua +++ b/UI/ResourceBarOptions.lua @@ -52,11 +52,6 @@ local RESOURCE_COLOR_DEFS = { key = C.RESOURCEBAR_TYPE_ICICLES, name = createResourceColorName("MAGE", L["RESOURCE_ICICLES"], { "MAGE" }), }, - -- { - -- -- Secret 2026/03 - -- key = Enum.PowerType.ArcaneCharges, - -- name = createResourceColorName("MAGE", L["RESOURCE_ARCANE_CHARGES"], { "MAGE" }), - -- }, { key = Enum.PowerType.Chi, name = createResourceColorName("MONK", L["RESOURCE_CHI"], { "MONK" }), From 454e35222b55ecba0479807824137ed273c0bf9d Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 09:31:51 +1000 Subject: [PATCH 06/15] Remove gap between buff bars and external defensives when some buff bars are hidden (#100) Remove gap between buff bars and external defensives when some buff bars are hidden. Hidden bars still reserved space in the viewer leading to a visible gap. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: argium <15852038+argium@users.noreply.github.com> --- Modules/BuffBars.lua | 8 ++++++++ Tests/Modules/BuffBars_spec.lua | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/Modules/BuffBars.lua b/Modules/BuffBars.lua index 61cfc575..13f93fc8 100644 --- a/Modules/BuffBars.lua +++ b/Modules/BuffBars.lua @@ -302,10 +302,12 @@ function BuffBars:UpdateLayout(why) -- Style all visible children (lazy setters make redundant calls no-ops) self._editLocked = false + local shownChildCount = 0 local ok, err = pcall(function() for _, entry in ipairs(visibleChildren) do hookChildFrame(entry.frame, self) StyleChildBar(self, entry.frame, cfg, globalConfig, spellColors) + if entry.frame:IsShown() then shownChildCount = shownChildCount + 1 end end layoutBars(visibleChildren, viewer, growsUp, verticalSpacing) @@ -317,6 +319,12 @@ function BuffBars:UpdateLayout(why) end ns.DebugAssert(ok, "Error styling buff bars: " .. tostring(err)) + local barHeight = (cfg and cfg.height) or globalConfig.barHeight or 0 + + -- Size the viewer from shown rows so hidden buff bars do not reserve chain space. + local totalHeight = shownChildCount * barHeight + math.max(0, shownChildCount - 1) * verticalSpacing + FrameUtil.LazySetHeight(viewer, totalHeight) + viewer:Show() ns.Log(ns.Constants.BUFFBARS, "UpdateLayout (" .. (why or "") .. ")", { mode = position and position.mode or "free", diff --git a/Tests/Modules/BuffBars_spec.lua b/Tests/Modules/BuffBars_spec.lua index a578ef3c..413d14fb 100644 --- a/Tests/Modules/BuffBars_spec.lua +++ b/Tests/Modules/BuffBars_spec.lua @@ -448,6 +448,7 @@ describe("BuffBars real source", function() ns.Runtime.DetachedAnchor = detachedAnchor local originalCalculateLayoutParams = ns.BarMixin.FrameProto.CalculateLayoutParams local originalLazySetAnchors = ns.FrameUtil.LazySetAnchors + local originalLazySetHeight = ns.FrameUtil.LazySetHeight ns.BarMixin.FrameProto.CalculateLayoutParams = function(self) local gc = self:GetGlobalConfig() @@ -465,6 +466,9 @@ describe("BuffBars real source", function() frame.__ecmAnchorCache = anchors frame.__anchors = anchors end + ns.FrameUtil.LazySetHeight = function(frame, value) + frame.__height = value + end BuffBars.GetModuleConfig = function() return { @@ -496,6 +500,7 @@ describe("BuffBars real source", function() ns.Runtime.DetachedAnchor = nil ns.BarMixin.FrameProto.CalculateLayoutParams = originalCalculateLayoutParams ns.FrameUtil.LazySetAnchors = originalLazySetAnchors + ns.FrameUtil.LazySetHeight = originalLazySetHeight end) it("free mode sets width from baseBarWidth*barWidthScale without touching viewer anchors", function() @@ -505,11 +510,15 @@ describe("BuffBars real source", function() appliedWidths[#appliedWidths + 1] = { frame = frame, value = value } end local originalLazySetAnchors = ns.FrameUtil.LazySetAnchors + local originalLazySetHeight = ns.FrameUtil.LazySetHeight ns.FrameUtil.LazySetAnchors = function(frame, anchors) anchorCalls[#anchorCalls + 1] = frame frame.__ecmAnchorCache = anchors frame.__anchors = anchors end + ns.FrameUtil.LazySetHeight = function(frame, value) + frame.__height = value + end BuffBarCooldownViewer.baseBarWidth = 180 BuffBarCooldownViewer.barWidthScale = 1.25 @@ -543,6 +552,7 @@ describe("BuffBars real source", function() end ns.FrameUtil.LazySetAnchors = originalLazySetAnchors + ns.FrameUtil.LazySetHeight = originalLazySetHeight end) it("free mode does not clobber Blizzard-managed viewer position across edit mode cycles", function() @@ -909,6 +919,7 @@ describe("BuffBars real source", function() function BuffBars:GetModuleConfig() return { anchorMode = ns.Constants.ANCHORMODE_CHAIN, + height = 21, showIcon = false, showSpellName = true, showDuration = true, @@ -931,6 +942,7 @@ describe("BuffBars real source", function() assert.is_true(second.__ecmHooked) assert.are.equal(2, #appliedTextures) assert.are.equal(2, #appliedColors) + assert.are.equal(21, BuffBarCooldownViewer.__height) assert.is_true(BuffBarCooldownViewer:IsShown()) end) From de61ede5be6b6cee635b4f65b537a2541fa5129a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 21:43:58 +1000 Subject: [PATCH 07/15] Stabilize ExtraIcons layout anchoring (#101) ExtraIcons could repeatedly clear and reapply the same anchors while laying out its appended cooldown icons, which made the flicker-sensitive layout path harder to keep stable. --- Constants.lua | 12 +- ECM.lua | 11 + Modules/ExtraIcons.lua | 262 ++++++--------------- Tests/Modules/ExtraIcons_spec.lua | 370 +++++++----------------------- 4 files changed, 180 insertions(+), 475 deletions(-) diff --git a/Constants.lua b/Constants.lua index 06bbf5b4..1f3bd565 100644 --- a/Constants.lua +++ b/Constants.lua @@ -184,6 +184,8 @@ local BUILTIN_STACKS = { --- Default display order for builtin stack keys (matches default viewers.utility order). local BUILTIN_STACK_ORDER = { "trinket1", "trinket2" } +local DRACTHYR_WING_BUFFET_IDS = { 357214, 368970 } -- Base and enhanced evoker variants. + --- Racial ability lookup keyed by UnitRace("player") raceFileName. --- One primary active racial per race. local RACIAL_ABILITIES = { @@ -210,10 +212,17 @@ local RACIAL_ABILITIES = { Vulpera = { spellId = 312411 }, -- Bag of Tricks MagharOrc = { spellId = 274738 }, -- Ancestral Call Mechagnome = { spellId = 312924 }, -- Hyper Organic Light Originator - Dracthyr = { spellIds = { 357214, 368970 } }, -- Tail Swipe + Dracthyr = { spellIds = DRACTHYR_WING_BUFFET_IDS }, -- Wing Buffet EarthenDwarf = { spellId = 436717 }, -- Azerite Surge } +--- Some racial abilities have different spell IDs. For example, Dracthyr evokers +--- have a more potent wing buffet compared to other classes. +local RACIAL_SPELL_ALIASES = { + [357214] = DRACTHYR_WING_BUFFET_IDS, + [368970] = DRACTHYR_WING_BUFFET_IDS, +} + local BLIZZARD_FRAMES = { "EssentialCooldownViewer", "UtilityCooldownViewer", @@ -287,6 +296,7 @@ constants.BLIZZARD_FRAMES = BLIZZARD_FRAMES constants.BUILTIN_STACKS = BUILTIN_STACKS constants.BUILTIN_STACK_ORDER = BUILTIN_STACK_ORDER constants.RACIAL_ABILITIES = RACIAL_ABILITIES +constants.RACIAL_SPELL_ALIASES = RACIAL_SPELL_ALIASES constants.RESOURCEBAR_CASTABLE_MAX_COLOR_SPELLS = resourceBarCastableMaxColorSpells constants.CLASS_COLORS = CLASS_COLORS constants.RESOURCEBAR_MAX_COLOR_TYPES = resourceBarMaxColorTypes diff --git a/ECM.lua b/ECM.lua index b41e7f2e..af351342 100644 --- a/ECM.lua +++ b/ECM.lua @@ -115,6 +115,17 @@ function ns.CloneValue(value) return copy end +--- Safely calls a frame method for diagnostics and returns nil on error. +function ns.GetFrameValue(frame, methodName) + if not frame or type(frame[methodName]) ~= "function" then + return nil + end + + local ok, value = pcall(frame[methodName], frame) + if ok then return value end + return nil +end + ns.Print = LibConsole:NewPrinter(function(message) print(ns.ColorUtil.Sparkle(L["ADDON_ABRV"] .. ":") .. " " .. message) end) diff --git a/Modules/ExtraIcons.lua b/Modules/ExtraIcons.lua index 6ba6e066..fab24fc4 100644 --- a/Modules/ExtraIcons.lua +++ b/Modules/ExtraIcons.lua @@ -9,28 +9,19 @@ local ExtraIcons = ns.Addon:NewModule("ExtraIcons") ns.Addon.ExtraIcons = ExtraIcons local BUILTIN_STACKS = ns.Constants.BUILTIN_STACKS -local RACIAL_ABILITIES = ns.Constants.RACIAL_ABILITIES +local RACIAL_SPELL_ALIASES = ns.Constants.RACIAL_SPELL_ALIASES local DEFAULT_SIZE = ns.Constants.DEFAULT_EXTRA_ICON_SIZE local MAIN_BORDER_SCALE = ns.Constants.EXTRA_ICON_MAIN_BORDER_SCALE local UTILITY_BORDER_SCALE = ns.Constants.EXTRA_ICON_UTILITY_BORDER_SCALE local canAccessTable = _G.canaccesstable -local RACIAL_SPELL_ALIASES = {} -for _, racial in pairs(RACIAL_ABILITIES) do - local spellIds = racial.spellIds or { racial.spellId } - for _, spellId in ipairs(spellIds) do - RACIAL_SPELL_ALIASES[spellId] = spellIds - end -end - --- Ordered viewer keys mapped to their Blizzard frame globals. +local MAIN_VIEWER_KEY = "EssentialCooldownViewer" +local UTILITY_VIEWER_KEY = "UtilityCooldownViewer" local VIEWERS = { - { key = "main", blizzKey = "EssentialCooldownViewer", borderScale = { MAIN_BORDER_SCALE, MAIN_BORDER_SCALE }, ownsAnchor = true }, + { key = "main", blizzKey = MAIN_VIEWER_KEY, borderScale = { MAIN_BORDER_SCALE, MAIN_BORDER_SCALE }, ownsAnchor = true }, -- Utility icon frames render square; keep the overlay square so extras do not look short. - { key = "utility", blizzKey = "UtilityCooldownViewer", borderScale = { UTILITY_BORDER_SCALE, UTILITY_BORDER_SCALE } }, + { key = "utility", blizzKey = UTILITY_VIEWER_KEY, borderScale = { UTILITY_BORDER_SCALE, UTILITY_BORDER_SCALE } }, } -local BLIZZ_KEY = {} -for _, v in ipairs(VIEWERS) do BLIZZ_KEY[v.key] = v.blizzKey end -------------------------------------------------------------------------------- -- Shared horizontal centering @@ -42,66 +33,6 @@ local function cachePoint(vs, blizzFrame) vs.originalPoint = { point, relativeTo, relativePoint, x or 0, y or 0 } end -local function applyPoint(vs, blizzFrame, offsetX) - local p = vs and vs.originalPoint - if not p or not blizzFrame then return end - blizzFrame:ClearAllPoints() - blizzFrame:SetPoint(p[1], p[2], p[3], p[4] + (offsetX or 0), p[5]) -end - -local function horizontalBounds(point, width) - if point == "LEFT" or point == "TOPLEFT" or point == "BOTTOMLEFT" then - return 0, width - elseif point == "RIGHT" or point == "TOPRIGHT" or point == "BOTTOMRIGHT" then - return -width, 0 - end - local h = width / 2 - return -h, h -end - ---- Computes a per-viewer horizontal offset that re-centers both viewers as a ---- single stacked group when they share the same original anchor. -local function getSharedOffsets(viewers) - local offsets = { main = 0, utility = 0 } - local mainState, utilState = viewers.main, viewers.utility - if not mainState or not utilState then return offsets end - - local mainFrame = _G[BLIZZ_KEY.main] - local utilFrame = _G[BLIZZ_KEY.utility] - cachePoint(mainState, mainFrame) - cachePoint(utilState, utilFrame) - - local mp, up = mainState.originalPoint, utilState.originalPoint - if not mp or not up then return offsets end - if mainFrame and up[2] == mainFrame then return offsets end - if up[1] ~= mp[1] or up[2] ~= mp[2] or up[3] ~= mp[3] or up[4] ~= mp[4] then - return offsets - end - - local sharedLeft, sharedRight - for _, v in ipairs(VIEWERS) do - local frame = _G[v.blizzKey] - local p = viewers[v.key].originalPoint - if frame and frame:IsShown() and p then - local l, r = horizontalBounds(p[1], frame:GetWidth() or 0) - sharedLeft = sharedLeft and math.min(sharedLeft, l) or l - sharedRight = sharedRight and math.max(sharedRight, r) or r - end - end - if not sharedLeft then return offsets end - local center = (sharedLeft + sharedRight) / 2 - - for _, v in ipairs(VIEWERS) do - local frame = _G[v.blizzKey] - local p = viewers[v.key].originalPoint - if frame and frame:IsShown() and p then - local l, r = horizontalBounds(p[1], frame:GetWidth() or 0) - offsets[v.key] = center - ((l + r) / 2) - end - end - return offsets -end - local function updateMainViewerAnchor(vs, blizzFrame, rightFrame) local anchor = vs and vs.anchorFrame if not anchor then return end @@ -124,7 +55,7 @@ end -- Entry resolution -------------------------------------------------------------------------------- -local function resolveEquipSlot(slotId) +local function getEquipSlotIconData(slotId) local itemId = GetInventoryItemID("player", slotId) if not itemId then return nil end local _, spellId = C_Item.GetItemSpell(itemId) @@ -134,7 +65,7 @@ local function resolveEquipSlot(slotId) return { itemId = itemId, texture = texture, slotId = slotId } end -local function resolveItem(ids, showIfMissing) +local function resolveFirstItem(ids, showIfMissing) local missingData for _, entry in ipairs(ids) do @@ -151,24 +82,24 @@ local function resolveItem(ids, showIfMissing) return missingData end -local function resolveKnownSpell(spellId) +local function getKnownSpellData(spellId) if spellId and C_SpellBook.IsSpellKnown(spellId) then local texture = C_Spell.GetSpellTexture(spellId) if texture then return { spellId = spellId, texture = texture } end end end -local function resolveSpell(ids) +local function resolveFirstKnownSpell(ids) for _, entry in ipairs(ids) do local spellId = type(entry) == "table" and entry.spellId or entry - local data = resolveKnownSpell(spellId) + local data = getKnownSpellData(spellId) if data then return data end local aliases = RACIAL_SPELL_ALIASES[spellId] if aliases then for _, aliasSpellId in ipairs(aliases) do if aliasSpellId ~= spellId then - data = resolveKnownSpell(aliasSpellId) + data = getKnownSpellData(aliasSpellId) if data then return data end end end @@ -176,24 +107,20 @@ local function resolveSpell(ids) end end -local function isNonPvpInstance() +local function shouldShowItemStack(itemStack) + if not itemStack then return false end + local hiddenInRatedPvp = itemStack.hideInRatedPvp and C_PvP.IsRatedMap() local inInstance, instanceType = IsInInstance() - return inInstance and instanceType ~= "pvp" and instanceType ~= "arena" + local hiddenInInstance = itemStack.hideInInstances and inInstance and instanceType ~= "pvp" and instanceType ~= "arena" + return not hiddenInRatedPvp and not hiddenInInstance end -local function shouldSuppressItemStack(itemStack) - if not itemStack then return true end - if itemStack.hideInRatedPvp and C_PvP.IsRatedMap() then return true end - if itemStack.hideInInstances and isNonPvpInstance() then return true end - return false -end - -local function resolveItemStack(entry, moduleConfig) +local function getConfiguredItemStackData(entry, moduleConfig) local itemStacks = moduleConfig and moduleConfig.itemStacks local itemStack = itemStacks and itemStacks.byId and itemStacks.byId[entry.itemStackId] - return not shouldSuppressItemStack(itemStack) + return shouldShowItemStack(itemStack) and itemStack.ids - and resolveItem(itemStack.ids, itemStack.showIfMissing == true) + and resolveFirstItem(itemStack.ids, itemStack.showIfMissing == true) or nil end @@ -206,10 +133,10 @@ local function resolveEntry(entry, moduleConfig) else kind, slotId, ids = entry.kind, entry.slotId, entry.ids end - if kind == "equipSlot" then return resolveEquipSlot(slotId) end - if kind == "item" then return ids and resolveItem(ids) end - if kind == "itemStack" then return resolveItemStack(entry, moduleConfig) end - if kind == "spell" then return ids and resolveSpell(ids) end + if kind == "equipSlot" then return getEquipSlotIconData(slotId) end + if kind == "item" then return ids and resolveFirstItem(ids) end + if kind == "itemStack" then return getConfiguredItemStackData(entry, moduleConfig) end + if kind == "spell" then return ids and resolveFirstKnownSpell(ids) end end local _resolved = {} @@ -304,7 +231,7 @@ local function updateIconCooldown(icon) end end -local function setIconCountText(icon, text) +local function applyIconCountText(icon, text) if text ~= nil then icon.Count:SetText(tostring(text)) icon.Count:Show() @@ -321,7 +248,7 @@ local function updateIconCountText(icon, globalConfig, config) if icon.itemId and (not config or config.showStackCount ~= false) then local count = C_Item.GetItemCount(icon.itemId) if count and count > 1 then - setIconCountText(icon, count) + applyIconCountText(icon, count) return end end @@ -329,45 +256,12 @@ local function updateIconCountText(icon, globalConfig, config) if icon.spellId and (not config or config.showCharges ~= false) then local charges = C_Spell.GetSpellCharges(icon.spellId) if charges and charges.maxCharges and charges.maxCharges > 1 and charges.currentCharges ~= nil then - setIconCountText(icon, charges.currentCharges) + applyIconCountText(icon, charges.currentCharges) return end end - setIconCountText(icon, nil) -end - ---- Caches and returns the cooldown number font from a sibling Blizzard icon. -local function getSiblingFont(viewer) - local cached = viewer.__ecmCDFont - if cached then return cached[1], cached[2], cached[3] end - - for _, child in ipairs({ viewer:GetChildren() }) do - local cooldown = child.Cooldown - if cooldown and cooldown.GetRegions then - local region = cooldown:GetRegions() - if region and region.IsObjectType and region:IsObjectType("FontString") and region.GetFont then - local fp, fs, ff = region:GetFont() - if fp and fs then - viewer.__ecmCDFont = { fp, fs, ff } - return fp, fs, ff - end - end - end - end -end - -local function getFrameValue(frame, methodName) - if not frame or type(frame[methodName]) ~= "function" then - return nil - end - - local ok, value = pcall(frame[methodName], frame) - if ok then - return value - end - - return nil + applyIconCountText(icon, nil) end local function getItemFramesCount(itemFrames) @@ -384,49 +278,39 @@ local function getItemFramesCount(itemFrames) return ok and count or nil end -local function getCombatState() - if type(InCombatLockdown) ~= "function" then - return nil - end - - local ok, inCombat = pcall(InCombatLockdown) - return ok and inCombat == true or nil -end - -local function getViewerDiagnostics(blizzFrame, viewerKey, why, itemFrames) +local function getViewerDiagnostics(blizzFrame, viewerConfig, why, itemFrames) local itemFramesAccessible = nil if type(itemFrames) == "table" then itemFramesAccessible = canAccessTable(itemFrames) end return { - viewerKey = viewerKey, - blizzardFrameKey = BLIZZ_KEY[viewerKey], + viewerKey = viewerConfig.key, + blizzardFrameKey = viewerConfig.blizzKey, reason = why, viewerExists = blizzFrame ~= nil, - viewerName = getFrameValue(blizzFrame, "GetName"), - viewerShown = getFrameValue(blizzFrame, "IsShown"), - viewerWidth = getFrameValue(blizzFrame, "GetWidth"), - viewerHeight = getFrameValue(blizzFrame, "GetHeight"), - viewerAlpha = getFrameValue(blizzFrame, "GetAlpha"), - viewerNumPoints = getFrameValue(blizzFrame, "GetNumPoints"), + viewerName = ns.GetFrameValue(blizzFrame, "GetName"), + viewerShown = ns.GetFrameValue(blizzFrame, "IsShown"), + viewerWidth = ns.GetFrameValue(blizzFrame, "GetWidth"), + viewerHeight = ns.GetFrameValue(blizzFrame, "GetHeight"), + viewerAlpha = ns.GetFrameValue(blizzFrame, "GetAlpha"), + viewerNumPoints = ns.GetFrameValue(blizzFrame, "GetNumPoints"), viewerIconScale = blizzFrame and blizzFrame.iconScale or nil, viewerChildXPadding = blizzFrame and blizzFrame.childXPadding or nil, viewerHasGetItemFrames = blizzFrame ~= nil and type(blizzFrame.GetItemFrames) == "function", itemFramesType = type(itemFrames), itemFramesAccessible = itemFramesAccessible, itemFramesArrayCount = getItemFramesCount(itemFrames), - inCombatLockdown = getCombatState(), } end -local function getAccessibleItemFrames(blizzFrame, viewerKey, why) +local function getAccessibleItemFrames(blizzFrame, viewerConfig, why) local ok, itemFrames = pcall(blizzFrame.GetItemFrames, blizzFrame) if not ok then - local data = getViewerDiagnostics(blizzFrame, viewerKey, why, nil) + local data = getViewerDiagnostics(blizzFrame, viewerConfig, why, nil) data.error = tostring(itemFrames) - ns.ErrorLogOnce("ExtraIcons", "GetItemFrames:" .. viewerKey, - "Unable to read cooldown viewer item frames for " .. viewerKey .. " during " + ns.ErrorLogOnce("ExtraIcons", "GetItemFrames:" .. viewerConfig.key, + "Unable to read cooldown viewer item frames for " .. viewerConfig.key .. " during " .. tostring(why or "unknown") .. ": " .. tostring(itemFrames), data) return nil end @@ -436,9 +320,9 @@ local function getAccessibleItemFrames(blizzFrame, viewerKey, why) end if not canAccessTable(itemFrames) then - ns.ErrorLogOnce("ExtraIcons", "InaccessibleItemFrames:" .. viewerKey, - "Cooldown viewer item frames are inaccessible for " .. viewerKey .. " during " - .. tostring(why or "unknown"), getViewerDiagnostics(blizzFrame, viewerKey, why, itemFrames)) + ns.ErrorLogOnce("ExtraIcons", "InaccessibleItemFrames:" .. viewerConfig.key, + "Cooldown viewer item frames are inaccessible for " .. viewerConfig.key .. " during " + .. tostring(why or "unknown"), getViewerDiagnostics(blizzFrame, viewerConfig, why, itemFrames)) return nil end @@ -460,10 +344,13 @@ function ExtraIcons:CreateFrame() container:SetFrameStrata("MEDIUM") container:SetSize(1, 1) - local anchor = CreateFrame("Frame", "ECMExtraIcons_" .. v.key .. "Anchor", parent) - anchor:SetFrameStrata("MEDIUM") - anchor:SetSize(1, 1) - anchor:Hide() + local anchor + if v.ownsAnchor then + anchor = CreateFrame("Frame", "ECMExtraIcons_" .. v.key .. "Anchor", parent) + anchor:SetFrameStrata("MEDIUM") + anchor:SetSize(1, 1) + anchor:Hide() + end self._viewers[v.key] = { anchorFrame = anchor, @@ -492,22 +379,24 @@ function ExtraIcons:GetMainViewerAnchor() local vs = self._viewers and self._viewers.main local anchor = vs and vs.anchorFrame if anchor and anchor:IsShown() then return anchor end - return _G[BLIZZ_KEY.main] + return _G[MAIN_VIEWER_KEY] end -function ExtraIcons:_updateSingleViewer(viewerConfig, entries, isEditing, sharedOffsetX, moduleConfig, why) +function ExtraIcons:_updateSingleViewer(viewerConfig, entries, isEditing, moduleConfig, why) local blizzFrame = _G[viewerConfig.blizzKey] local vs = self._viewers[viewerConfig.key] if not vs then return false end local container = vs.container - sharedOffsetX = sharedOffsetX or 0 cachePoint(vs, blizzFrame) local items = (not blizzFrame or not blizzFrame:IsShown() or isEditing or #entries == 0) and {} or resolveEntries(entries, moduleConfig) if #items == 0 then - applyPoint(vs, blizzFrame, sharedOffsetX) + local p = vs.originalPoint + if p and blizzFrame then + FrameUtil.LazySetAnchors(blizzFrame, { { p[1], p[2], p[3], p[4], p[5] } }) + end if isEditing then vs.originalPoint = nil end container:Hide() if viewerConfig.ownsAnchor then updateMainViewerAnchor(vs, blizzFrame, nil) end @@ -519,27 +408,26 @@ function ExtraIcons:_updateSingleViewer(viewerConfig, entries, isEditing, shared vs.iconPool[i] = createIcon(container, DEFAULT_SIZE, viewerConfig.borderScale) end - local fontPath, fontSize, fontFlags = getSiblingFont(blizzFrame) local globalConfig = self:GetGlobalConfig() local iconSize = DEFAULT_SIZE local viewerScale = blizzFrame.iconScale or 1.0 local spacing = blizzFrame.childXPadding or 0 local lastActive = nil - local itemFrames = getAccessibleItemFrames(blizzFrame, viewerConfig.key, why) + local itemFrames = getAccessibleItemFrames(blizzFrame, viewerConfig, why) if itemFrames then local ok, err = pcall(function() for _, itemFrame in ipairs(itemFrames) do if itemFrame.isActive then iconSize = itemFrame:GetWidth() or iconSize - lastActive = itemFrame + if itemFrame:IsShown() then lastActive = itemFrame end end end end) if not ok then iconSize = DEFAULT_SIZE lastActive = nil - local data = getViewerDiagnostics(blizzFrame, viewerConfig.key, why, itemFrames) + local data = getViewerDiagnostics(blizzFrame, viewerConfig, why, itemFrames) data.error = tostring(err) ns.ErrorLogOnce("ExtraIcons", "IterateItemFrames:" .. viewerConfig.key, "Unable to iterate cooldown viewer item frames for " .. viewerConfig.key .. " during " @@ -557,7 +445,10 @@ function ExtraIcons:_updateSingleViewer(viewerConfig, entries, isEditing, shared -- on-screen centre already coincides with the original anchor; we only -- need to absorb the on-screen width of the gap + extra group. local extraOnScreen = (spacing + totalWidth) * viewerScale - applyPoint(vs, blizzFrame, sharedOffsetX - extraOnScreen / 2) + local p = vs.originalPoint + if p and blizzFrame then + FrameUtil.LazySetAnchors(blizzFrame, { { p[1], p[2], p[3], p[4] - extraOnScreen / 2, p[5] } }) + end local xOffset = 0 for i, data in ipairs(items) do @@ -572,25 +463,16 @@ function ExtraIcons:_updateSingleViewer(viewerConfig, entries, isEditing, shared icon.Icon:SetTexture(data.texture) icon.Icon:SetDesaturated(data.missing == true) - icon:ClearAllPoints() - icon:SetPoint("LEFT", container, "LEFT", xOffset, 0) + FrameUtil.LazySetAnchors(icon, { { "LEFT", container, "LEFT", xOffset, 0 } }) icon:Show() updateIconCooldown(icon) updateIconCountText(icon, globalConfig, moduleConfig) - if fontPath and fontSize then - local region = icon.Cooldown:GetRegions() - if region and region.IsObjectType and region:IsObjectType("FontString") and region.SetFont then - region:SetFont(fontPath, fontSize, fontFlags) - end - end - xOffset = xOffset + iconSize + spacing end - container:ClearAllPoints() - container:SetPoint("LEFT", lastActive or blizzFrame, "RIGHT", spacing, 0) + FrameUtil.LazySetAnchors(container, { { "LEFT", lastActive or blizzFrame, "RIGHT", spacing, 0 } }) container:Show() if viewerConfig.ownsAnchor then updateMainViewerAnchor(vs, blizzFrame, container) end @@ -611,12 +493,11 @@ function ExtraIcons:UpdateLayout(why) -- When hidden, leave viewers nil so each call gets empty entries, which -- restores Blizzard viewer positions and hides extra-icon containers. local viewers = shouldShow and moduleConfig and moduleConfig.viewers - local offsets = getSharedOffsets(self._viewers) local anyPlaced = false for _, v in ipairs(VIEWERS) do local entries = viewers and viewers[v.key] or {} - if self:_updateSingleViewer(v, entries, isEditing, offsets[v.key], moduleConfig, why) then + if self:_updateSingleViewer(v, entries, isEditing, moduleConfig, why) then anyPlaced = true end end @@ -732,10 +613,9 @@ function ExtraIcons:HookEditMode() end) end -function ExtraIcons:_hookViewer(viewerKey) - local blizzKey = BLIZZ_KEY[viewerKey] - local blizzFrame = _G[blizzKey] - local vs = self._viewers and self._viewers[viewerKey] +function ExtraIcons:_hookViewer(viewerConfig) + local blizzFrame = _G[viewerConfig.blizzKey] + local vs = self._viewers and self._viewers[viewerConfig.key] if not blizzFrame or not vs or vs.hooked then return end vs.hooked = true @@ -760,7 +640,7 @@ function ExtraIcons:_hookViewer(viewerKey) ns.Runtime.RequestLayout("ExtraIcons:OnSizeChanged") end) - ns.Log(self.Name, "Hooked " .. blizzKey) + ns.Log(self.Name, "Hooked " .. viewerConfig.blizzKey) end -------------------------------------------------------------------------------- @@ -792,7 +672,7 @@ function ExtraIcons:OnEnable() end self:HookEditMode() - for _, v in ipairs(VIEWERS) do self:_hookViewer(v.key) end + for _, v in ipairs(VIEWERS) do self:_hookViewer(v) end ns.Runtime.RequestLayout("ExtraIcons:OnEnable") end) end diff --git a/Tests/Modules/ExtraIcons_spec.lua b/Tests/Modules/ExtraIcons_spec.lua index 2ad34ebf..1583bdfe 100644 --- a/Tests/Modules/ExtraIcons_spec.lua +++ b/Tests/Modules/ExtraIcons_spec.lua @@ -235,6 +235,7 @@ describe("ExtraIcons real source", function() "C_Item", "C_PvP", "canaccesstable", + "issecretvalue", "ipairs", }) end) @@ -299,21 +300,30 @@ describe("ExtraIcons real source", function() UnregisterFrame = function() end, RequestLayout = function() end, }, - FrameUtil = { - ApplyFont = function(fontString, appliedGlobalConfig, moduleConfig) - applyFontCalls[#applyFontCalls + 1] = { - fontString = fontString, - globalConfig = appliedGlobalConfig, - moduleConfig = moduleConfig, - } - end, - }, GetGlobalConfig = function() return globalConfig end, + -- ECM.lua is not loaded in this isolated source spec. + GetFrameValue = function(frame, methodName) + if not frame or type(frame[methodName]) ~= "function" then + return nil + end + local ok, value = pcall(frame[methodName], frame) + if ok then return value end + return nil + end, } TestHelpers.LoadChunk("Constants.lua", "Unable to load Constants.lua")(nil, ns) TestHelpers.LoadChunk("Locales/en.lua", "Unable to load Locales/en.lua")(nil, ns) + _G.issecretvalue = function() return false end + TestHelpers.LoadChunk("FrameUtil.lua", "Unable to load FrameUtil.lua")(nil, ns) + ns.FrameUtil.ApplyFont = function(fontString, appliedGlobalConfig, moduleConfig) + applyFontCalls[#applyFontCalls + 1] = { + fontString = fontString, + globalConfig = appliedGlobalConfig, + moduleConfig = moduleConfig, + } + end _G.UIParent = TestHelpers.makeFrame({ name = "UIParent" }) EditModeManagerFrame = TestHelpers.makeHookableFrame(false) UtilityCooldownViewer = TestHelpers.makeHookableFrame(true) @@ -529,124 +539,6 @@ describe("ExtraIcons real source", function() } end - local function makeActiveFrames(count, width) - local frames = {} - for i = 1, count do - local frame = TestHelpers.makeFrame({ shown = true, width = width, height = width }) - frame.isActive = true - frames[i] = frame - end - return frames - end - - local function getPointAnchorOffset(point, width) - if point == "LEFT" or point == "TOPLEFT" or point == "BOTTOMLEFT" then - return 0 - elseif point == "RIGHT" or point == "TOPRIGHT" or point == "BOTTOMRIGHT" then - return width - elseif point == "CENTER" or point == "TOP" or point == "BOTTOM" then - return width / 2 - end - - error("Unsupported point " .. tostring(point)) - end - - local function getFrameLeft(frame) - local point, relativeTo, relativePoint, x = frame:GetPoint(1) - local width = frame:GetWidth() or 0 - local anchorX = x or 0 - - if relativeTo and relativeTo ~= UIParent then - local relativeLeft = getFrameLeft(relativeTo) - local relativeWidth = relativeTo:GetWidth() or 0 - anchorX = relativeLeft + getPointAnchorOffset(relativePoint, relativeWidth) + (x or 0) - end - - return anchorX - getPointAnchorOffset(point, width) - end - - local function getViewerRowCenter(viewerFrame, container) - local rowWidth = viewerFrame:GetWidth() or 0 - if container and container:IsShown() then - rowWidth = rowWidth + (viewerFrame.childXPadding or 0) + ((container:GetWidth() or 0) * (container.__scale or 1)) - end - return getFrameLeft(viewerFrame) + (rowWidth / 2) - end - - local function resolveAnchorTarget(anchorTarget) - if anchorTarget == "UIParent" then - return UIParent - elseif anchorTarget == "main" then - return EssentialCooldownViewer - elseif anchorTarget == "utility" then - return UtilityCooldownViewer - end - return nil - end - - local function assertViewerRowsStayCenteredAfterMove(args) - UtilityCooldownViewer.childXPadding = 4 - UtilityCooldownViewer.iconScale = 1.0 - UtilityCooldownViewer:SetWidth(args.utilityViewerWidth) - UtilityCooldownViewer.GetItemFrames = function() - return makeActiveFrames(args.utilityActiveCount, 22) - end - UtilityCooldownViewer:SetPoint( - args.utilityAnchor.point, - resolveAnchorTarget(args.utilityAnchor.relativeTo), - args.utilityAnchor.relativePoint, - args.utilityAnchor.x, - args.utilityAnchor.y - ) - - EssentialCooldownViewer.childXPadding = 4 - EssentialCooldownViewer.iconScale = 1.0 - EssentialCooldownViewer:SetWidth(args.mainViewerWidth) - EssentialCooldownViewer.GetItemFrames = function() - return makeActiveFrames(args.mainActiveCount, 22) - end - EssentialCooldownViewer:SetPoint( - args.mainAnchor.point, - resolveAnchorTarget(args.mainAnchor.relativeTo), - args.mainAnchor.relativePoint, - args.mainAnchor.x, - args.mainAnchor.y - ) - - inventoryItemBySlot[13] = 101 - inventoryTextureBySlot[13] = "trinket-1" - inventorySpellByItem[101] = 9001 - inventoryItemBySlot[14] = 102 - inventoryTextureBySlot[14] = "trinket-2" - inventorySpellByItem[102] = 9002 - itemCounts[HEALTHSTONE_ID] = 1 - itemIconsByID[HEALTHSTONE_ID] = "healthstone" - - ExtraIcons.InnerFrame = ExtraIcons:CreateFrame() - - local config = makeViewersConfig(args.beforeUtility, args.beforeMain) - ExtraIcons.GetModuleConfig = function() - return config - end - - assert.is_true(ExtraIcons:UpdateLayout("before-move")) - - config.viewers.utility = args.afterUtility - config.viewers.main = args.afterMain - - assert.is_true(ExtraIcons:UpdateLayout("after-move")) - - local utilityCenter = getViewerRowCenter(UtilityCooldownViewer, ExtraIcons._viewers.utility.container) - local mainCenter = getViewerRowCenter(EssentialCooldownViewer, ExtraIcons._viewers.main.container) - - assert.are.equal(mainCenter, utilityCenter) - - if args.expectUtilityRelativeTo then - local _, relativeTo = UtilityCooldownViewer:GetPoint(1) - assert.are.equal(resolveAnchorTarget(args.expectUtilityRelativeTo), relativeTo) - end - end - it("requires at least one viewer to be visible in ShouldShow", function() assert.is_true(ExtraIcons:ShouldShow()) @@ -713,8 +605,9 @@ describe("ExtraIcons real source", function() it("hooks viewers only once", function() ExtraIcons.InnerFrame = ExtraIcons:CreateFrame() - ExtraIcons:_hookViewer("utility") - ExtraIcons:_hookViewer("utility") + local utilityHookConfig = { key = "utility", blizzKey = "UtilityCooldownViewer" } + ExtraIcons:_hookViewer(utilityHookConfig) + ExtraIcons:_hookViewer(utilityHookConfig) assert.is_true(ExtraIcons._viewers.utility.hooked) assert.are.equal(1, UtilityCooldownViewer:GetHookCount("OnShow")) @@ -784,7 +677,7 @@ describe("ExtraIcons real source", function() return enabled end - ExtraIcons:_hookViewer("utility") + ExtraIcons:_hookViewer({ key = "utility", blizzKey = "UtilityCooldownViewer" }) UtilityCooldownViewer._hooks.OnShow[1]() UtilityCooldownViewer._hooks.OnHide[1]() UtilityCooldownViewer._hooks.OnSizeChanged[1]() @@ -1017,10 +910,6 @@ describe("ExtraIcons real source", function() assert.are.equal(14, vs.iconPool[2].slotId) assert.are.equal(COMBAT_POTION_ID, vs.iconPool[3].itemId) assert.are.equal(HEALTHSTONE_ID, vs.iconPool[4].itemId) - assert.same( - { "Fonts\\FRIZQT__.TTF", 17, "OUTLINE" }, - vs.iconPool[1].Cooldown.__fontRegion.__font - ) local point, relativeTo, relativePoint, x, y = UtilityCooldownViewer:GetPoint(1) assert.are.equal("CENTER", point) @@ -1060,6 +949,70 @@ describe("ExtraIcons real source", function() assert.are.equal(87, x) end) + it("anchors to the viewer when active item frames are hidden", function() + local activeFrame = TestHelpers.makeFrame({ shown = false, width = 22, height = 22 }) + activeFrame.isActive = true + UtilityCooldownViewer.childXPadding = 4 + UtilityCooldownViewer.iconScale = 1.0 + UtilityCooldownViewer:SetWidth(22) + UtilityCooldownViewer.GetItemFrames = function() + return { activeFrame } + end + UtilityCooldownViewer:SetPoint("CENTER", UIParent, "CENTER", 100, 0) + + itemCounts[HEALTHSTONE_ID] = 1 + itemIconsByID[HEALTHSTONE_ID] = "healthstone" + + ExtraIcons.InnerFrame = ExtraIcons:CreateFrame() + ExtraIcons.GetModuleConfig = function() + return makeViewersConfig({ { kind = "itemStack", itemStackId = "healthstones" } }) + end + + assert.is_true(ExtraIcons:UpdateLayout("hidden-active-frame")) + + local _, anchorFrame = ExtraIcons._viewers.utility.container:GetPoint(1) + assert.are.equal(UtilityCooldownViewer, anchorFrame) + end) + + it("does not reapply unchanged anchors on repeated layouts", function() + local activeFrame = TestHelpers.makeFrame({ shown = true, width = 22, height = 22 }) + activeFrame.isActive = true + UtilityCooldownViewer.childXPadding = 4 + UtilityCooldownViewer.iconScale = 1.0 + UtilityCooldownViewer:SetWidth(22) + UtilityCooldownViewer.GetItemFrames = function() + return { activeFrame } + end + UtilityCooldownViewer:SetPoint("CENTER", UIParent, "CENTER", 100, 0) + + itemCounts[HEALTHSTONE_ID] = 1 + itemIconsByID[HEALTHSTONE_ID] = "healthstone" + + ExtraIcons.InnerFrame = ExtraIcons:CreateFrame() + ExtraIcons.GetModuleConfig = function() + return makeViewersConfig({ { kind = "itemStack", itemStackId = "healthstones" } }) + end + + assert.is_true(ExtraIcons:UpdateLayout("initial")) + + local vs = ExtraIcons._viewers.utility + local viewerSetPoints = TestHelpers.getCalls(UtilityCooldownViewer, "SetPoint") + local viewerClears = TestHelpers.getCalls(UtilityCooldownViewer, "ClearAllPoints") + local containerSetPoints = TestHelpers.getCalls(vs.container, "SetPoint") + local containerClears = TestHelpers.getCalls(vs.container, "ClearAllPoints") + local iconSetPoints = TestHelpers.getCalls(vs.iconPool[1], "SetPoint") + local iconClears = TestHelpers.getCalls(vs.iconPool[1], "ClearAllPoints") + + assert.is_true(ExtraIcons:UpdateLayout("unchanged")) + + assert.are.equal(viewerSetPoints, TestHelpers.getCalls(UtilityCooldownViewer, "SetPoint")) + assert.are.equal(viewerClears, TestHelpers.getCalls(UtilityCooldownViewer, "ClearAllPoints")) + assert.are.equal(containerSetPoints, TestHelpers.getCalls(vs.container, "SetPoint")) + assert.are.equal(containerClears, TestHelpers.getCalls(vs.container, "ClearAllPoints")) + assert.are.equal(iconSetPoints, TestHelpers.getCalls(vs.iconPool[1], "SetPoint")) + assert.are.equal(iconClears, TestHelpers.getCalls(vs.iconPool[1], "ClearAllPoints")) + end) + it("does not iterate inaccessible GetItemFrames results", function() local inaccessibleFrames = {} UtilityCooldownViewer.childXPadding = 4 @@ -1335,155 +1288,6 @@ describe("ExtraIcons real source", function() assert.is_true(ExtraIcons._viewers.main.container:IsShown()) end) - for _, case in ipairs({ - { - name = "keeps same-parent viewer rows centered when utility becomes empty", - utilityViewerWidth = 22, - utilityActiveCount = 1, - mainViewerWidth = 48, - mainActiveCount = 2, - utilityAnchor = { point = "LEFT", relativeTo = "UIParent", relativePoint = "LEFT", x = 100, y = 0 }, - mainAnchor = { point = "LEFT", relativeTo = "UIParent", relativePoint = "LEFT", x = 100, y = 40 }, - beforeUtility = { - { kind = "itemStack", itemStackId = "healthstones" }, - }, - beforeMain = { - { stackKey = "trinket1" }, - }, - afterUtility = {}, - afterMain = { - { stackKey = "trinket1" }, - { kind = "itemStack", itemStackId = "healthstones" }, - }, - }, - { - name = "keeps same-parent viewer rows centered when main becomes empty", - utilityViewerWidth = 22, - utilityActiveCount = 1, - mainViewerWidth = 48, - mainActiveCount = 2, - utilityAnchor = { point = "LEFT", relativeTo = "UIParent", relativePoint = "LEFT", x = 100, y = 0 }, - mainAnchor = { point = "LEFT", relativeTo = "UIParent", relativePoint = "LEFT", x = 100, y = 40 }, - beforeUtility = { - { kind = "itemStack", itemStackId = "healthstones" }, - }, - beforeMain = { - { stackKey = "trinket1" }, - }, - afterUtility = { - { kind = "itemStack", itemStackId = "healthstones" }, - { stackKey = "trinket1" }, - }, - afterMain = {}, - }, - { - name = "keeps same-parent viewer rows centered when both viewers still have different ECM counts", - utilityViewerWidth = 22, - utilityActiveCount = 1, - mainViewerWidth = 48, - mainActiveCount = 2, - utilityAnchor = { point = "LEFT", relativeTo = "UIParent", relativePoint = "LEFT", x = 100, y = 0 }, - mainAnchor = { point = "LEFT", relativeTo = "UIParent", relativePoint = "LEFT", x = 100, y = 40 }, - beforeUtility = { - { kind = "itemStack", itemStackId = "healthstones" }, - { stackKey = "trinket1" }, - }, - beforeMain = { - { stackKey = "trinket2" }, - }, - afterUtility = { - { kind = "itemStack", itemStackId = "healthstones" }, - }, - afterMain = { - { stackKey = "trinket2" }, - { stackKey = "trinket1" }, - }, - }, - { - name = "keeps same-parent viewer rows centered for center anchors when utility becomes empty", - utilityViewerWidth = 22, - utilityActiveCount = 1, - mainViewerWidth = 48, - mainActiveCount = 2, - utilityAnchor = { point = "CENTER", relativeTo = "UIParent", relativePoint = "CENTER", x = 100, y = 0 }, - mainAnchor = { point = "CENTER", relativeTo = "UIParent", relativePoint = "CENTER", x = 100, y = 40 }, - beforeUtility = { - { kind = "itemStack", itemStackId = "healthstones" }, - }, - beforeMain = { - { stackKey = "trinket1" }, - }, - afterUtility = {}, - afterMain = { - { stackKey = "trinket1" }, - { kind = "itemStack", itemStackId = "healthstones" }, - }, - }, - { - name = "keeps same-parent viewer rows centered for top-left anchors when utility becomes empty", - utilityViewerWidth = 22, - utilityActiveCount = 1, - mainViewerWidth = 48, - mainActiveCount = 2, - utilityAnchor = { point = "TOPLEFT", relativeTo = "UIParent", relativePoint = "TOPLEFT", x = 100, y = -100 }, - mainAnchor = { point = "TOPLEFT", relativeTo = "UIParent", relativePoint = "TOPLEFT", x = 100, y = -60 }, - beforeUtility = { - { kind = "itemStack", itemStackId = "healthstones" }, - }, - beforeMain = { - { stackKey = "trinket1" }, - }, - afterUtility = {}, - afterMain = { - { stackKey = "trinket1" }, - { kind = "itemStack", itemStackId = "healthstones" }, - }, - }, - { - name = "keeps same-parent viewer rows centered for right anchors when utility becomes empty", - utilityViewerWidth = 22, - utilityActiveCount = 1, - mainViewerWidth = 48, - mainActiveCount = 2, - utilityAnchor = { point = "RIGHT", relativeTo = "UIParent", relativePoint = "RIGHT", x = -100, y = 0 }, - mainAnchor = { point = "RIGHT", relativeTo = "UIParent", relativePoint = "RIGHT", x = -100, y = 40 }, - beforeUtility = { - { kind = "itemStack", itemStackId = "healthstones" }, - }, - beforeMain = { - { stackKey = "trinket1" }, - }, - afterUtility = {}, - afterMain = { - { stackKey = "trinket1" }, - { kind = "itemStack", itemStackId = "healthstones" }, - }, - }, - { - name = "keeps a utility viewer anchored to main centered without same-parent coupling", - utilityViewerWidth = 22, - utilityActiveCount = 1, - mainViewerWidth = 48, - mainActiveCount = 2, - utilityAnchor = { point = "LEFT", relativeTo = "main", relativePoint = "LEFT", x = 26, y = -40 }, - mainAnchor = { point = "LEFT", relativeTo = "UIParent", relativePoint = "LEFT", x = 100, y = 40 }, - beforeUtility = { - { kind = "itemStack", itemStackId = "healthstones" }, - }, - beforeMain = {}, - afterUtility = {}, - afterMain = { - { kind = "itemStack", itemStackId = "healthstones" }, - }, - expectUtilityRelativeTo = "main", - }, - }) do - local currentCase = case - it(currentCase.name, function() - assertViewerRowsStayCenteredAfterMove(currentCase) - end) - end - it("prefers demonic healthstone over the legacy healthstone", function() local utilityIconChild = TestHelpers.makeFrame({ shown = true, width = 18, height = 18 }) utilityIconChild.GetSpellID = function() return 1 end From e73c4862279370c70314512eae96b16a403a859b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 22:40:21 +1000 Subject: [PATCH 08/15] Fix timeless buff bar background using wrong texture (#99) Update the background texture more frequently. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: argium <15852038+argium@users.noreply.github.com> --- BarStyle.lua | 7 ++- Modules/BuffBars.lua | 11 +++++ Tests/Modules/BuffBars_spec.lua | 80 +++++++++++++++++++++++++++++++++ docs/BuffBars.md | 2 +- 4 files changed, 97 insertions(+), 3 deletions(-) diff --git a/BarStyle.lua b/BarStyle.lua index 663d5d33..6e2b2491 100644 --- a/BarStyle.lua +++ b/BarStyle.lua @@ -224,7 +224,6 @@ local function styleEmptyStatusBarBackground(bar, barBG, config, globalConfig) if texture and type(barBG.SetTexture) == "function" then barBG:SetTexture(texture) end - local ok, r, g, b, a = pcall(bar.GetStatusBarColor, bar) if ok then barBG:SetVertexColor(r, g, b, a or 1) @@ -326,7 +325,11 @@ local function styleChildBar(module, frame, config, globalConfig, spellColors) local barBG = FrameUtil.GetBarBackground(bar) styleBarBackground(frame, barBG, config, globalConfig) styleBarColor(module, frame, bar, globalConfig, spellColors, 0) - styleEmptyStatusBarBackground(bar, barBG, config, globalConfig) + -- Only fill the background solid for bars with an active aura. cooldownID + -- identifies the configured slot, including inactive visible slots. + if frame.auraInstanceID then + styleEmptyStatusBarBackground(bar, barBG, config, globalConfig) + end FrameUtil.ApplyFont(bar.Name, globalConfig, config) FrameUtil.ApplyFont(bar.Duration, globalConfig, config) diff --git a/Modules/BuffBars.lua b/Modules/BuffBars.lua index 13f93fc8..d8470512 100644 --- a/Modules/BuffBars.lua +++ b/Modules/BuffBars.lua @@ -422,6 +422,17 @@ function BuffBars:OnEnable() self:RegisterEvent("ZONE_CHANGED", function(_, ...) self:OnZoneChanged(...) end) self:RegisterEvent("ZONE_CHANGED_INDOORS", function(_, ...) self:OnZoneChanged(...) end) self:RegisterEvent("PLAYER_ENTERING_WORLD", function(_, ...) self:OnZoneChanged(...) end) + -- Blizzard updates each child's auraInstanceID synchronously inside its own + -- UNIT_AURA handler but does not always re-fire SetPoint/OnShow/OnHide on + -- bars whose layout order is unchanged (e.g. a configured slot whose aura + -- toggles on/off). Defer a layout pass so StyleChildBar re-evaluates the + -- active-aura background after Blizzard's update completes. + self:RegisterEvent("UNIT_AURA", function(_, _, unit) + if unit ~= "player" then + return + end + ns.Runtime.RequestLayout("BuffBars:UNIT_AURA") + end) C_Timer.After(0.1, function() self:HookViewer() diff --git a/Tests/Modules/BuffBars_spec.lua b/Tests/Modules/BuffBars_spec.lua index 413d14fb..93f83dea 100644 --- a/Tests/Modules/BuffBars_spec.lua +++ b/Tests/Modules/BuffBars_spec.lua @@ -138,6 +138,7 @@ describe("BuffBars real source", function() } child.cooldownInfo = { spellID = layoutIndex } child.cooldownID = 1000 + layoutIndex + child.auraInstanceID = 3000 + layoutIndex child.iconTextureFileID = 2000 + layoutIndex child.RefreshCooldownInfo = function() end return child @@ -838,6 +839,27 @@ describe("BuffBars real source", function() assert.is_true(BuffBars._viewerHooked) end) + it("registers UNIT_AURA on enable and requests layout only for player auras", function() + local captured = {} + function BuffBars:RegisterEvent(event, cb) + captured[event] = cb + end + local reasons = {} + ns.Runtime.RequestLayout = function(reason) + reasons[#reasons + 1] = reason + end + + BuffBars:OnInitialize() + BuffBars:OnEnable() + + local cb = assert(captured["UNIT_AURA"], "expected UNIT_AURA registration") + -- LibEvent dispatches cb(target, event, ...wowArgs) + cb(BuffBars, "UNIT_AURA", "target") + cb(BuffBars, "UNIT_AURA", "player") + + assert.same({ "BuffBars:UNIT_AURA" }, reasons) + end) + it("unregisters on disable", function() function BuffBars:UnregisterAllEvents() end @@ -1126,12 +1148,15 @@ describe("BuffBars real source", function() child.Bar.__minMax = { 0, 0 } child.Bar.__value = 0 local bgRegion = makeBarBackground() + local expectedTexture = "ExpectedStatusBarTexture" layoutSingleChild(child, defaultModule(), defaultGlobal(), function() ns.FrameUtil.GetBarBackground = function() return bgRegion end + ns.FrameUtil.GetTexture = function() return expectedTexture end end) assert.same({ 0.4, 0.5, 0.6, 1.0 }, bgRegion.__vcolor) + assert.are.equal(expectedTexture, bgRegion.__texture) end) it("does not compare secret status bar values when checking for timeless auras", function() @@ -1156,19 +1181,71 @@ describe("BuffBars real source", function() child.Bar.__minMax = { 0, 0 } child.Bar.__value = 0 local bgRegion = makeBarBackground() + local expectedTexture = "ExpectedStatusBarTexture" child.RefreshCooldownInfo = function(self) self.Bar.__value = 0 end layoutSingleChild(child, defaultModule(), defaultGlobal(), function() ns.FrameUtil.GetBarBackground = function() return bgRegion end + ns.FrameUtil.GetTexture = function() return expectedTexture end end) child:RefreshCooldownInfo() assert.same({ 0.4, 0.5, 0.6, 1.0 }, bgRegion.__vcolor) + assert.are.equal(expectedTexture, bgRegion.__texture) assert.are.equal(0, child.Bar.__value) end) + it("restores the normal background after a timeless aura expires", function() + -- When a timeless aura expires, Blizzard clears auraInstanceID on the + -- child frame and re-runs the SetPoint/OnShow/layout hooks that drive + -- StyleChildBar. The unconditional styleBarBackground call at the top + -- of styleChildBar must reset the background texture/color so the + -- empty-bar fill applied during the active phase does not stick. + local child = makeStyledChild("Expiring", true, 1) + child.Bar.__minMax = { 0, 0 } + child.Bar.__value = 0 + local bgRegion = makeBarBackground() + local expectedTexture = "ExpectedStatusBarTexture" + local function configure() + ns.FrameUtil.GetBarBackground = function() return bgRegion end + ns.FrameUtil.GetTexture = function() return expectedTexture end + end + + layoutSingleChild(child, defaultModule(), defaultGlobal(), configure) + assert.same({ 0.4, 0.5, 0.6, 1.0 }, bgRegion.__vcolor) + assert.are.equal(expectedTexture, bgRegion.__texture) + + child.auraInstanceID = nil + layoutSingleChild(child, defaultModule(), defaultGlobal(), configure) + + assert.are.equal(ns.Constants.FALLBACK_TEXTURE, bgRegion.__texture) + assert.same({ 0, 0, 0, 0.8 }, bgRegion.__vcolor) + end) + + it("keeps the normal background for an empty bar slot when hide-when-inactive is off", function() + -- When Blizzard's "hide when inactive" is disabled, bar frames remain + -- visible even when the configured aura is not yet active. Their status + -- bar values are 0/0 like a timeless aura, and the spell name IS set + -- (the slot is configured for the spell). cooldownID still identifies + -- the configured slot, so auraInstanceID is the reliable active-aura signal. + -- The timeless background treatment must not apply; the configured + -- background color must be kept. + local child = makeStyledChild("Slot", true, 1) + child.Bar.__minMax = { 0, 0 } + child.Bar.__value = 0 + child.auraInstanceID = nil + local bgRegion = makeBarBackground() + + layoutSingleChild(child, defaultModule(), defaultGlobal(), function() + ns.FrameUtil.GetBarBackground = function() return bgRegion end + end) + + assert.are.equal(ns.Constants.FALLBACK_TEXTURE, bgRegion.__texture) + assert.same({ 0, 0, 0, 0.8 }, bgRegion.__vcolor) + end) + it("does not call Blizzard cooldown data methods while styling", function() local child = makeStyledChild("Infinite", true, 1) child.Bar.__minMax = { 0, 0 } @@ -1177,12 +1254,15 @@ describe("BuffBars real source", function() error("GetCooldownValues must not be called from addon styling") end local bgRegion = makeBarBackground() + local expectedTexture = "ExpectedStatusBarTexture" layoutSingleChild(child, defaultModule(), defaultGlobal(), function() ns.FrameUtil.GetBarBackground = function() return bgRegion end + ns.FrameUtil.GetTexture = function() return expectedTexture end end) assert.same({ 0.4, 0.5, 0.6, 1.0 }, bgRegion.__vcolor) + assert.are.equal(expectedTexture, bgRegion.__texture) end) end) diff --git a/docs/BuffBars.md b/docs/BuffBars.md index b23b38c7..2e59426a 100644 --- a/docs/BuffBars.md +++ b/docs/BuffBars.md @@ -8,7 +8,7 @@ | **Description** | Mirrors Blizzard's `BuffBarCooldownViewer` area into ECM-styled aura bars. ECM repositions and restyles Blizzard-owned child bars instead of creating its own aura rows. | | **Source file** | [`Modules/BuffBars.lua`](../Modules/BuffBars.lua) | | **Mixin** | `BarMixin.AddFrameMixin(self, "BuffBars")` using `BarMixin.FrameProto` methods such as `EnsureFrame()`, `GetModuleConfig()`, `ShouldShow()`, and `CalculateLayoutParams()`. | -| **Events listened to** | - `ZONE_CHANGED_NEW_AREA` — refreshes zone-specific Blizzard aura bars and requests layout.
- `ZONE_CHANGED` — refreshes zone changes that can alter the viewer's child set.
- `ZONE_CHANGED_INDOORS` — refreshes indoor/outdoor aura transitions.
- `PLAYER_ENTERING_WORLD` — catches initial world entry and reload/login transitions. | +| **Events listened to** | - `ZONE_CHANGED_NEW_AREA` — refreshes zone-specific Blizzard aura bars and requests layout.
- `ZONE_CHANGED` — refreshes zone changes that can alter the viewer's child set.
- `ZONE_CHANGED_INDOORS` — refreshes indoor/outdoor aura transitions.
- `PLAYER_ENTERING_WORLD` — catches initial world entry and reload/login transitions.
- `UNIT_AURA` (player only) — requests a deferred layout so `StyleChildBar` re-evaluates the active-aura background after Blizzard updates each child's `auraInstanceID`. | | **Hooks** | - `BuffBarCooldownViewer:OnShow` — requests a layout pass when Blizzard re-shows the viewer.
- `BuffBarCooldownViewer:OnSizeChanged` — requests a second-pass layout when Blizzard changes viewer width/size.
- `child:SetPoint` — restores ECM's cached anchors, restyles the child, and queues a second-pass layout.
- `child:OnShow` — reapplies ECM styling and queues a second-pass layout.
- `child:OnHide` — queues a second-pass layout so the remaining bars restack cleanly. | | **Dependencies** | - `ns.BarMixin` / `BarMixin.FrameProto` — frame-module lifecycle, config access, anchor calculation.
- `ns.Runtime` — frame registration plus `RequestLayout()` / layout execution.
- `ns.BarStyle.StyleChildBar` — applies ECM visuals to Blizzard child bars.
- `ns.FrameUtil` — lazy anchors, width snapshots, icon texture lookup.
- `ns.SpellColors.Get("buffBars")` — scoped spell-color discovery, lookup, and cache clearing.
- `ns.Constants` / `ns.defaults` — scope name, anchor-mode semantics, default colors/config.
- Blizzard `BuffBarCooldownViewer` and its child aura-bar frames — source viewer and mirrored rows.
- `C_Timer.After(0.1)` — deferred hook install so the Blizzard viewer exists before BuffBars attaches hooks. | | **Options file(s)** | [`UI/BuffBarsOptions.lua`](../UI/BuffBarsOptions.lua), plus BuffBars' section registration into [`UI/SpellColorsPage.lua`](../UI/SpellColorsPage.lua) | From df44fbb62b63b1f3a6d9963c358cbd7b208c9d87 Mon Sep 17 00:00:00 2001 From: Argi <15852038+argium@users.noreply.github.com> Date: Tue, 19 May 2026 13:32:19 +1000 Subject: [PATCH 09/15] Significant cleanup. --- .luacheckrc | 1 - AGENTS.md | 1 + BarStyle.lua | 30 ++--- ECM.lua | 13 +-- FrameUtil.lua | 21 ++-- Libs/LibEvent/LibEvent.lua | 27 ++--- Libs/LibEvent/README.md | 6 +- Libs/LibEvent/Tests/LibEvent_spec.lua | 8 +- .../LibLSMSettingsWidgets.lua | 24 ++-- .../Interop/CollectionFrames.lua | 10 +- .../Interop/Enhancements.lua | 40 ++----- Libs/LibSettingsBuilder/Interop/ListRows.lua | 36 ++---- Libs/LibSettingsBuilder/Interop/Widgets.lua | 34 ++---- .../LibSettingsBuilder/Registry/CoreState.lua | 60 ---------- Libs/LibSettingsBuilder/Registry/Runtime.lua | 2 - Libs/LibSettingsBuilder/docs/API_REFERENCE.md | 4 - .../docs/TROUBLESHOOTING.md | 10 -- Migration.lua | 4 - Modules/BuffBars.lua | 29 ++--- Modules/ExternalBars.lua | 104 +++++++----------- Modules/ExtraIcons.lua | 3 +- Modules/PowerBar.lua | 6 +- Modules/RuneBar.lua | 4 - Runtime.lua | 4 - Tests/FrameUtil_spec.lua | 21 ---- Tests/Migration_spec.lua | 11 +- Tests/Modules/BuffBars_spec.lua | 6 + Tests/TestHelpers.lua | 33 +++++- Tests/UI/ItemStacksOptions_spec.lua | 3 +- UI/ExtraIconsOptions.lua | 6 +- UI/ItemStacksOptions.lua | 33 ++---- UI/OptionUtil.lua | 41 ++----- UI/Options.lua | 26 +---- UI/ProfileOptions.lua | 21 +--- 34 files changed, 214 insertions(+), 468 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index 3bd3a386..a95b15aa 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -31,7 +31,6 @@ files = { } globals = { - "LSB_DEBUG", "LibLSMSettingsWidgets_FontPickerMixin", "LibLSMSettingsWidgets_TexturePickerMixin", "SlashCmdList", diff --git a/AGENTS.md b/AGENTS.md index 02a51a13..b5da1f03 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,6 +68,7 @@ All Lua files start with: ## Architecture - Prefer the simplest production code for current supported runtimes. No fallback paths, compatibility branches, defensive adapters, or built-in shims without a concrete supported environment that needs them. +- Do not nil-guard methods, fields, or globals that are guaranteed to exist on current Retail (e.g., `Frame:GetRegions`, `Region:IsObjectType`, `Texture:GetMaskTexture`/`RemoveMaskTexture`, `:Hide`, `YES`/`NO`, `C_EditMode`). Call them directly. Guard only against optional third-party addons (e.g., `DevTool`), genuinely polymorphic shapes, or runtime data that may be absent. - Keep one owner for shared state, derived values, utility functions, style metrics, and widget rendering details. - Use loose coupling through events, hooks, callbacks, or messages. - Do not add trivial passthrough wrappers, fixed-literal indirection, or single-caller abstractions without an independently testable contract. diff --git a/BarStyle.lua b/BarStyle.lua index 6e2b2491..e331414f 100644 --- a/BarStyle.lua +++ b/BarStyle.lua @@ -27,25 +27,19 @@ local function applySquareIconStyle(iconFrame, iconTexture, iconOverlay, debuffB iconTexture:SetTexCoord(0, 1, 0, 1) -- Remove circular masks from the icon texture - if iconTexture.GetNumMaskTextures and iconTexture.RemoveMaskTexture and iconTexture.GetMaskTexture then - for i = (iconTexture:GetNumMaskTextures() or 0), 1, -1 do - local mask = iconTexture:GetMaskTexture(i) - if mask then - iconTexture:RemoveMaskTexture(mask) - if mask.Hide then mask:Hide() end - end + for i = (iconTexture:GetNumMaskTextures() or 0), 1, -1 do + local mask = iconTexture:GetMaskTexture(i) + if mask then + iconTexture:RemoveMaskTexture(mask) + mask:Hide() end - elseif iconTexture.SetMask then - pcall(iconTexture.SetMask, iconTexture, nil) end -- Remove mask regions from the icon frame - if iconFrame.GetRegions and iconTexture.RemoveMaskTexture then - for _, region in ipairs({ iconFrame:GetRegions() }) do - if region and region.IsObjectType and region:IsObjectType("MaskTexture") then - pcall(iconTexture.RemoveMaskTexture, iconTexture, region) - if region.Hide then region:Hide() end - end + for _, region in ipairs({ iconFrame:GetRegions() }) do + if region:IsObjectType("MaskTexture") then + pcall(iconTexture.RemoveMaskTexture, iconTexture, region) + region:Hide() end end @@ -181,7 +175,7 @@ local function styleBarColor(module, frame, bar, globalConfig, spellColors, retr end local function isEmptyStatusBar(bar) - if not bar or type(bar.GetMinMaxValues) ~= "function" or type(bar.GetValue) ~= "function" then + if not bar then return false end @@ -207,7 +201,7 @@ local function isEmptyStatusBar(bar) end local function styleEmptyStatusBarBackground(bar, barBG, config, globalConfig) - if not barBG or type(bar.GetStatusBarColor) ~= "function" or type(barBG.SetVertexColor) ~= "function" then + if not barBG then return end @@ -221,7 +215,7 @@ local function styleEmptyStatusBarBackground(bar, barBG, config, globalConfig) -- when set, otherwise fall back to the global texture. local textureName = (config and config.texture) or (globalConfig and globalConfig.texture) local texture = FrameUtil.GetTexture(textureName) - if texture and type(barBG.SetTexture) == "function" then + if texture then barBG:SetTexture(texture) end local ok, r, g, b, a = pcall(bar.GetStatusBarColor, bar) diff --git a/ECM.lua b/ECM.lua index af351342..a60c6e97 100644 --- a/ECM.lua +++ b/ECM.lua @@ -115,15 +115,12 @@ function ns.CloneValue(value) return copy end ---- Safely calls a frame method for diagnostics and returns nil on error. +--- Safely calls a frame method for diagnostics and returns nil when the frame is missing. function ns.GetFrameValue(frame, methodName) - if not frame or type(frame[methodName]) ~= "function" then + if not frame then return nil end - - local ok, value = pcall(frame[methodName], frame) - if ok then return value end - return nil + return frame[methodName](frame) end ns.Print = LibConsole:NewPrinter(function(message) @@ -283,8 +280,8 @@ function mod:ConfirmReloadUI(text, onAccept, onCancel) if not StaticPopupDialogs[C.POPUP_CONFIRM_RELOAD_UI] then StaticPopupDialogs[C.POPUP_CONFIRM_RELOAD_UI] = { text = L["RELOAD_UI_PROMPT"], - button1 = YES or "Yes", - button2 = NO or "No", + button1 = YES, + button2 = NO, OnAccept = function(_, data) if data and data.onAccept then data.onAccept() diff --git a/FrameUtil.lua b/FrameUtil.lua index f48cd922..3dfafaa6 100644 --- a/FrameUtil.lua +++ b/FrameUtil.lua @@ -17,7 +17,7 @@ local function tryGetRegion(frame, index, regionType) end local region = select(index, frame:GetRegions()) - if region and region.IsObjectType and region:IsObjectType(regionType) then + if region and region:IsObjectType(regionType) then return region end @@ -51,15 +51,17 @@ end ---@param statusBar StatusBar|nil ---@return Texture|nil function FrameUtil.GetBarBackground(statusBar) - if not statusBar or not statusBar.GetRegions then + if not statusBar then return nil end local cached = statusBar.__ecmBarBG - if cached and cached.IsObjectType and cached:IsObjectType("Texture") then + if cached and cached:IsObjectType("Texture") then + ---@cast cached Texture return cached end for _, region in ipairs({ statusBar:GetRegions() }) do - if region and region.IsObjectType and region:IsObjectType("Texture") then + if region:IsObjectType("Texture") then + ---@cast region Texture local atlas = region.GetAtlas and region:GetAtlas() if atlas == "UI-HUD-CoolDownManager-Bar-BG" or atlas == "UI-HUD-CooldownManager-Bar-BG" then statusBar.__ecmBarBG = region @@ -130,15 +132,10 @@ end ---@param parent Frame|nil ---@return number, number function FrameUtil.GetParentSize(parent) - if parent and parent.GetSize then - local width, height = parent:GetSize() - if width and height then - return width, height - end + if not parent then + return 0, 0 end - local width = (parent and parent.GetWidth and parent:GetWidth()) or 0 - local height = (parent and parent.GetHeight and parent:GetHeight()) or 0 - return width, height + return parent:GetSize() end --- Returns the offset from the frame's center to the specified anchor point, diff --git a/Libs/LibEvent/LibEvent.lua b/Libs/LibEvent/LibEvent.lua index 0c804c35..29a5539c 100644 --- a/Libs/LibEvent/LibEvent.lua +++ b/Libs/LibEvent/LibEvent.lua @@ -3,7 +3,7 @@ -- Licensed under the GNU General Public License v3.0 ---@class LibEvent ----@field embeds table, _stats: table|nil }> Stores embedded event instances by target table. +---@field embeds table }> Stores embedded event instances by target table. local MAJOR, MINOR = "LibEvent-1.0", 3 local LibEvent = LibStub:NewLibrary(MAJOR, MINOR) @@ -15,9 +15,7 @@ end local ipairs = ipairs local pairs = pairs local type = type -local wipe = wipe -local METRICS_DEBUG_ENABLED = false local EMPTY_STATS = {} LibEvent.embeds = LibEvent.embeds or {} @@ -92,17 +90,15 @@ function LibEvent:UnregisterAllEvents() end end ----Gets event invocation stats when metrics are enabled. ----@return table A table mapping event names to their fire counts, or an empty table when metrics are disabled. +---Gets event invocation stats. +---@return table Empty stats table; metrics collection is not enabled in normal runtime. function LibEvent:GetEventStats() - return getInstance(self)._stats or EMPTY_STATS + getInstance(self) + return EMPTY_STATS end ----Resets event invocation stats when metrics are enabled. -function LibEvent:ResetEventStats() - local stats = getInstance(self)._stats - if stats then wipe(stats) end -end +---Resets event invocation stats. +function LibEvent:ResetEventStats() getInstance(self) end local function createInstance(target) local instance = LibEvent.embeds[target] @@ -110,10 +106,6 @@ local function createInstance(target) instance = { _events = {} } end - if METRICS_DEBUG_ENABLED and not instance._stats then - instance._stats = {} - end - instance._events = instance._events or {} instance.frame = instance.frame or CreateFrame("Frame") @@ -126,10 +118,6 @@ local function createInstance(target) if not cbs then return end - if METRICS_DEBUG_ENABLED then - instance._stats[event] = (instance._stats[event] or 0) + 1 - end - instance._dispatching = true local i = 1 while i <= #cbs do local cb = cbs[i] @@ -139,7 +127,6 @@ local function createInstance(target) i = i + 1 end end - instance._dispatching = false end) LibEvent.embeds[target] = instance diff --git a/Libs/LibEvent/README.md b/Libs/LibEvent/README.md index 07185eb2..391e7010 100644 --- a/Libs/LibEvent/README.md +++ b/Libs/LibEvent/README.md @@ -9,7 +9,7 @@ Distributed via [LibStub](https://www.wowace.com/projects/libstub). - Embed into any table to give it event registration capabilities. - Zero-allocation dispatch loop — no snapshot copies per fire. - Idempotent embedding — safe to re-embed on library upgrades. -- Metrics hooks via `GetEventStats` / `ResetEventStats`; metrics are disabled by default, so these APIs return no counts in normal runtime. +- No-op stats hooks via `GetEventStats` / `ResetEventStats` for callers that need a stable API surface. ## Quick start @@ -33,8 +33,8 @@ end) | `UnregisterEvent(event, callback)` | Remove a specific callback. | | `UnregisterAllEvents()` | Remove all callbacks and unregister the hidden frame. | | `Fire(event, ...)` | Manually fire an event on the target. | -| `GetEventStats()` | Returns event fire counts when metrics are enabled, otherwise an empty table. | -| `ResetEventStats()` | Clears accumulated stats when metrics are enabled. | +| `GetEventStats()` | Returns an empty table. | +| `ResetEventStats()` | No-op retained for API stability. | ## Testing diff --git a/Libs/LibEvent/Tests/LibEvent_spec.lua b/Libs/LibEvent/Tests/LibEvent_spec.lua index b2edff1a..d550d57c 100644 --- a/Libs/LibEvent/Tests/LibEvent_spec.lua +++ b/Libs/LibEvent/Tests/LibEvent_spec.lua @@ -351,13 +351,13 @@ describe("LibEvent", function() assert.same({ "stable" }, calls) end) - it("does not initialize _stats when metrics debug is disabled", function() + it("does not initialize stats storage", function() local target = {} LibEvent:Embed(target) assert.is_nil(LibEvent.embeds[target]._stats) end) - it("does not increment _stats when metrics debug is disabled", function() + it("does not increment stats during dispatch", function() local target = { TEST_EVENT = function() end } LibEvent:Embed(target) @@ -370,7 +370,7 @@ describe("LibEvent", function() assert.same({}, target:GetEventStats()) end) - it("GetEventStats returns an empty table when metrics debug is disabled", function() + it("GetEventStats returns an empty table", function() local target = { TEST_EVENT = function() end } LibEvent:Embed(target) @@ -382,7 +382,7 @@ describe("LibEvent", function() assert.is_nil(stats.TEST_EVENT) end) - it("ResetEventStats is a no-op when metrics debug is disabled", function() + it("ResetEventStats is a no-op", function() local target = { EVENT_A = function() end, EVENT_B = function() end, diff --git a/Libs/LibLSMSettingsWidgets/LibLSMSettingsWidgets.lua b/Libs/LibLSMSettingsWidgets/LibLSMSettingsWidgets.lua index 98ee8394..8607e8a9 100644 --- a/Libs/LibLSMSettingsWidgets/LibLSMSettingsWidgets.lua +++ b/Libs/LibLSMSettingsWidgets/LibLSMSettingsWidgets.lua @@ -20,9 +20,7 @@ local function invalidateMediaCache() wipe(mediaCache) end -if LSM and LSM.RegisterCallback then - LSM:RegisterCallback("LibSharedMedia_Registered", invalidateMediaCache) -end +LSM:RegisterCallback("LibSharedMedia_Registered", invalidateMediaCache) local function getSortedMediaNames(mediaType, fallback) local cached = mediaCache[mediaType] @@ -31,10 +29,8 @@ local function getSortedMediaNames(mediaType, fallback) end local sorted = {} - if LSM and LSM.List then - for _, name in ipairs(LSM:List(mediaType)) do - sorted[#sorted + 1] = name - end + for _, name in ipairs(LSM:List(mediaType)) do + sorted[#sorted + 1] = name end if #sorted == 0 then @@ -69,17 +65,17 @@ end -------------------------------------------------------------------------------- local function setPickerEnabled(self, enabled) - if self.DropDown.SetEnabled then self.DropDown:SetEnabled(enabled) end - if self.DropDown.EnableMouse then self.DropDown:EnableMouse(enabled) end - if self.DropDownHost.SetEnabled then self.DropDownHost:SetEnabled(enabled) end - if self.DropDownHost.EnableMouse then self.DropDownHost:EnableMouse(enabled) end + self.DropDown:SetEnabled(enabled) + self.DropDown:EnableMouse(enabled) + self.DropDownHost:SetEnabled(enabled) + self.DropDownHost:EnableMouse(enabled) self.Preview[enabled and "Show" or "Hide"](self.Preview) end local function createDropDown(self) local host = CreateFrame("Frame", nil, self, "SettingsDropdownWithButtonsTemplate") - if host.DecrementButton then host.DecrementButton:Hide() end - if host.IncrementButton then host.IncrementButton:Hide() end + host.DecrementButton:Hide() + host.IncrementButton:Hide() local dropdown = host.Dropdown or host dropdown:SetPoint("LEFT", self.Text, "RIGHT", 10, 0) @@ -140,7 +136,7 @@ local function updateDropdownText(self, mediaType) if not self.setting then return nil, nil end local currentName = self.setting:GetValue() - local mediaPath = LSM and LSM.Fetch and LSM:Fetch(mediaType, currentName) + local mediaPath = LSM:Fetch(mediaType, currentName) if self.DropDown and self.DropDown.OverrideText then self.DropDown:OverrideText(currentName or "") diff --git a/Libs/LibSettingsBuilder/Interop/CollectionFrames.lua b/Libs/LibSettingsBuilder/Interop/CollectionFrames.lua index af5fff7a..ba61d3c2 100644 --- a/Libs/LibSettingsBuilder/Interop/CollectionFrames.lua +++ b/Libs/LibSettingsBuilder/Interop/CollectionFrames.lua @@ -182,7 +182,7 @@ local function bindCollectionRowTooltip(row, item) setCollectionRowHighlight(row, false) if item.onLeave then item.onLeave(self, item) - elseif GameTooltip_Hide then + else GameTooltip_Hide() end end) @@ -722,12 +722,8 @@ local function setupModeInputDropdown(row, trailer, sectionData, enabled) dropdown:SetText(label) end - if dropdown.SetEnabled then - dropdown:SetEnabled(enabled) - end - if row._dropdownHost.SetEnabled then - row._dropdownHost:SetEnabled(enabled) - end + dropdown:SetEnabled(enabled) + row._dropdownHost:SetEnabled(enabled) if dropdown.SetupMenu then dropdown:SetupMenu(function(_, rootDescription) diff --git a/Libs/LibSettingsBuilder/Interop/Enhancements.lua b/Libs/LibSettingsBuilder/Interop/Enhancements.lua index d2d6e519..4b9662d2 100644 --- a/Libs/LibSettingsBuilder/Interop/Enhancements.lua +++ b/Libs/LibSettingsBuilder/Interop/Enhancements.lua @@ -117,7 +117,7 @@ local function configureDropdownFrame(frame, initializer, data) frame:InitDropdown() end -if not lib._scrollDropdownHookInstalled and hooksecurefunc and SettingsDropdownControlMixin then +if not lib._scrollDropdownHookInstalled then hooksecurefunc(SettingsDropdownControlMixin, "Init", function(frame, initializer) local data = getInitializerData(initializer) if not data or (data._lsbKind ~= "dropdown" and data._lsbKind ~= "scrollDropdown") then @@ -156,10 +156,6 @@ local function getSliderStepCount(minValue, maxValue, step) end local function createInlineSliderFormatters() - if not MinimalSliderWithSteppersMixin or not MinimalSliderWithSteppersMixin.Label then - return nil - end - return { [MinimalSliderWithSteppersMixin.Label.Right] = function() return "" @@ -233,7 +229,7 @@ local function attachInlineSliderEditor(slider, textLabel, editBoxWidth) valueButton:SetScript("OnClick", function() editBox:SetText(textLabel and textLabel.GetText and textLabel:GetText() or "") - if textLabel and textLabel.Hide then + if textLabel then textLabel:Hide() end editBox:Show() @@ -295,7 +291,7 @@ local function configureInlineSlider(slider, textLabel, field, onValueChanged, r end end - if slider.RegisterCallback and MinimalSliderWithSteppersMixin and MinimalSliderWithSteppersMixin.Event then + if slider.RegisterCallback then slider:RegisterCallback(MinimalSliderWithSteppersMixin.Event.OnValueChanged, handleValueChanged, slider) else slider:HookScript("OnValueChanged", handleValueChanged) @@ -308,10 +304,6 @@ interop.configureInlineSlider = configureInlineSlider if not lib._sliderHookInstalled then local function setupSliderEditableValue() - if not SettingsSliderControlMixin then - return - end - local function findValueLabel(sliderWithSteppers) if sliderWithSteppers._label then return sliderWithSteppers._label @@ -454,7 +446,7 @@ if not lib._sliderHookInstalled then end local function getCategoryDefaultsButton() - local settingsList = SettingsPanel and SettingsPanel.GetSettingsList and SettingsPanel:GetSettingsList() + local settingsList = SettingsPanel:GetSettingsList() local header = settingsList and settingsList.Header return header and header.DefaultsButton or nil end @@ -527,26 +519,10 @@ function interop.installPageLifecycleHooks() return end - if type(SettingsPanel) ~= "table" or type(SettingsPanel.DisplayCategory) ~= "function" then - if lib._pageLifecycleDeferred or type(CreateFrame) ~= "function" then - return - end - lib._pageLifecycleDeferred = true - local f = CreateFrame("Frame") - f:RegisterEvent("ADDON_LOADED") - f:SetScript("OnEvent", function(self) - if type(SettingsPanel) == "table" and type(SettingsPanel.DisplayCategory) == "function" then - self:UnregisterAllEvents() - interop.installPageLifecycleHooks() - end - end) - return - end - lib._pageLifecycleHooked = true hooksecurefunc(SettingsPanel, "DisplayCategory", function(panel) - local category = panel.GetCurrentCategory and panel:GetCurrentCategory() or nil + local category = panel:GetCurrentCategory() local old = lib._activeLifecycleCategory if old == category then return @@ -578,15 +554,15 @@ function interop.installPageLifecycleHooks() end function interop.getCurrentSettingsCategory() - return SettingsPanel and SettingsPanel.GetCurrentCategory and SettingsPanel:GetCurrentCategory() or nil + return SettingsPanel:GetCurrentCategory() end function interop.isSettingsPanelShown() - return SettingsPanel and SettingsPanel.IsShown and SettingsPanel:IsShown() + return SettingsPanel:IsShown() end function interop.forEachVisibleSettingsFrame(callback) - local settingsList = SettingsPanel and SettingsPanel.GetSettingsList and SettingsPanel:GetSettingsList() + local settingsList = SettingsPanel:GetSettingsList() local scrollBox = settingsList and settingsList.ScrollBox if scrollBox and scrollBox.ForEachFrame then scrollBox:ForEachFrame(callback) diff --git a/Libs/LibSettingsBuilder/Interop/ListRows.lua b/Libs/LibSettingsBuilder/Interop/ListRows.lua index 4573b02c..08a351d9 100644 --- a/Libs/LibSettingsBuilder/Interop/ListRows.lua +++ b/Libs/LibSettingsBuilder/Interop/ListRows.lua @@ -37,16 +37,9 @@ local function resetListElement(frame) end local function hideListElementObjects(frame, getterName) - if not frame or not frame[getterName] then - return - end - local objects = { frame[getterName](frame) } for i = 1, #objects do - local object = objects[i] - if object and object.Hide then - object:Hide() - end + objects[i]:Hide() end end @@ -101,7 +94,7 @@ local function ensureHeaderRowWidgets(frame) end local function getSettingsListHeader() - local settingsList = SettingsPanel and SettingsPanel.GetSettingsList and SettingsPanel:GetSettingsList() + local settingsList = SettingsPanel:GetSettingsList() return settingsList and settingsList.Header or nil end @@ -336,7 +329,7 @@ local function scheduleInputPreview(frame, immediate) end local delay = immediate and 0 or (data.debounce or 0) - if delay > 0 and C_Timer and C_Timer.NewTimer then + if delay > 0 then frame._lsbInputPreviewTimer = C_Timer.NewTimer(delay, function() frame._lsbInputPreviewTimer = nil resolveInputPreview(frame) @@ -379,12 +372,8 @@ local function applyButtonRowEnabledState(frame, enabled) if not button then return end - if button.SetEnabled then - button:SetEnabled(enabled) - end - if button.EnableMouse then - button:EnableMouse(enabled) - end + button:SetEnabled(enabled) + button:EnableMouse(enabled) end local function ensureColorRowWidgets(frame) @@ -623,17 +612,12 @@ local function installColorPickerHooks() end session.reopening = true - if C_Timer and C_Timer.After then - C_Timer.After(0, function() - session.reopening = nil - if colorPickerSession == session and not session.committed and not session.cancelled then - showColorPickerSession(session) - end - end) - else + C_Timer.After(0, function() session.reopening = nil - showColorPickerSession(session) - end + if colorPickerSession == session and not session.committed and not session.cancelled then + showColorPickerSession(session) + end + end) end) end diff --git a/Libs/LibSettingsBuilder/Interop/Widgets.lua b/Libs/LibSettingsBuilder/Interop/Widgets.lua index 9dcc7284..9b0b8c83 100644 --- a/Libs/LibSettingsBuilder/Interop/Widgets.lua +++ b/Libs/LibSettingsBuilder/Interop/Widgets.lua @@ -48,14 +48,10 @@ function interop.setCanvasInteractive(frame, enabled) if frame.SetEnabled then frame:SetEnabled(enabled) end - if frame.EnableMouse then - frame:EnableMouse(enabled) - end - if frame.GetChildren then - local children = { frame:GetChildren() } - for i = 1, #children do - interop.setCanvasInteractive(children[i], enabled) - end + frame:EnableMouse(enabled) + local children = { frame:GetChildren() } + for i = 1, #children do + interop.setCanvasInteractive(children[i], enabled) end end @@ -115,13 +111,13 @@ function interop.refreshSettingsFrame(frame) end function interop.showFrame(frame) - if frame and frame.Show then + if frame then frame:Show() end end function interop.setTextureValue(texture, value) - if not texture or not texture.SetTexture then + if not texture then return end @@ -155,19 +151,13 @@ function interop.setSimpleTooltip(owner, text) end owner:SetScript("OnEnter", function(self) - if not GameTooltip then - return - end - GameTooltip:SetOwner(self, "ANCHOR_RIGHT") GameTooltip:ClearLines() interop.setGameTooltipText(text, true) GameTooltip:Show() end) owner:SetScript("OnLeave", function() - if GameTooltip_Hide then - GameTooltip_Hide() - end + GameTooltip_Hide() end) end @@ -255,13 +245,9 @@ local function setButtonTextureState(button, setterName, getterName, value, blen return end - if texture.ClearAllPoints then - texture:ClearAllPoints() - end - if texture.SetAllPoints then - texture:SetAllPoints(button) - end - if alpha ~= nil and texture.SetAlpha then + texture:ClearAllPoints() + texture:SetAllPoints(button) + if alpha ~= nil then texture:SetAlpha(alpha) end end diff --git a/Libs/LibSettingsBuilder/Registry/CoreState.lua b/Libs/LibSettingsBuilder/Registry/CoreState.lua index 59dbe59c..a2fe5c47 100644 --- a/Libs/LibSettingsBuilder/Registry/CoreState.lua +++ b/Libs/LibSettingsBuilder/Registry/CoreState.lua @@ -51,41 +51,6 @@ local interop = internal.interop local registry = internal.registry lib._runtimeApi = lib._runtimeApi or {} -local COMMON_SPEC_FIELDS = { - path = true, - name = true, - tooltip = true, - category = true, - onSet = true, - getTransform = true, - setTransform = true, - disabled = true, - hidden = true, - layout = true, - type = true, - desc = true, - get = true, - set = true, - key = true, - default = true, -} - -local EXTRA_FIELDS_BY_TYPE = { - checkbox = {}, - slider = { min = true, max = true, step = true, formatter = true }, - dropdown = { values = true, scrollHeight = true }, - color = {}, - input = { - debounce = true, - maxLetters = true, - numeric = true, - onTextChanged = true, - resolveText = true, - width = true, - }, - custom = { template = true, varType = true }, -} - local function defaultGetNestedValue(tbl, path) local current = tbl for segment in path:gmatch("[^.]+") do @@ -309,31 +274,6 @@ function registry.makeColorSetting(self, spec) return setting, category end -function registry.validateSpecFields(_, controlType, spec) - if not LSB_DEBUG then - return - end - - local allowed = EXTRA_FIELDS_BY_TYPE[controlType] - if not allowed then - return - end - - for key in pairs(spec) do - if not COMMON_SPEC_FIELDS[key] and not allowed[key] then - print( - "|cffFF8800LibSettingsBuilder WARNING:|r Unknown spec field '" - .. tostring(key) - .. "' on " - .. controlType - .. " control '" - .. tostring(spec.name or spec.path) - .. "'" - ) - end - end -end - function registry.isParentEnabled(self, spec) if not spec._parentInitializer then return true diff --git a/Libs/LibSettingsBuilder/Registry/Runtime.lua b/Libs/LibSettingsBuilder/Registry/Runtime.lua index 76049955..26a8788b 100644 --- a/Libs/LibSettingsBuilder/Registry/Runtime.lua +++ b/Libs/LibSettingsBuilder/Registry/Runtime.lua @@ -395,8 +395,6 @@ local function prepareButtonClick(builder, spec) end local function prepareProxyRow(builder, rowType, spec) - registry.validateSpecFields(builder, rowType, spec) - local setting, category if rowType == "checkbox" then setting, category = registry.makeProxySetting(builder, spec, interop.getVarTypeBoolean(), false) diff --git a/Libs/LibSettingsBuilder/docs/API_REFERENCE.md b/Libs/LibSettingsBuilder/docs/API_REFERENCE.md index 0bbfac87..42d6f8f1 100644 --- a/Libs/LibSettingsBuilder/docs/API_REFERENCE.md +++ b/Libs/LibSettingsBuilder/docs/API_REFERENCE.md @@ -483,7 +483,3 @@ Row builders still fall into the same public families: `input` is implemented as a built-in custom list row on `SettingsListElementTemplate`. It creates an `InputBoxTemplate` edit box at runtime, subscribes to watched proxy settings through callback handles, and optionally debounces preview refreshes. That gives it built-in-row behavior without requiring a separate XML template. `canvas` rows stay on the current lifecycle path. The documented public canvas API is the `canvas` row type; older canvas-layout helpers live under internal implementation details and are not part of the public surface documented here. - -## Debugging - -Set `LSB_DEBUG = true` to warn about unknown spec fields while developing new settings definitions. diff --git a/Libs/LibSettingsBuilder/docs/TROUBLESHOOTING.md b/Libs/LibSettingsBuilder/docs/TROUBLESHOOTING.md index 4a09e3df..8f25cf7e 100644 --- a/Libs/LibSettingsBuilder/docs/TROUBLESHOOTING.md +++ b/Libs/LibSettingsBuilder/docs/TROUBLESHOOTING.md @@ -102,13 +102,3 @@ If debugging slider behavior: - give the row an explicit `height`, or make sure the frame reports a stable height, - prefer built-in rows, `list`, or `sectionList` when you want Blizzard-style settings layout instead of a bespoke frame, - use `type = "custom"` for XML-backed row widgets rather than a full embedded canvas when you only need one custom control. - -## Debugging spec mistakes - -Set `LSB_DEBUG = true` during development to warn about unknown spec fields. - -This helps catch typos like: - -- `paht` -- `tooltipp` -- `valeus` diff --git a/Migration.lua b/Migration.lua index 651bb060..60d93850 100644 --- a/Migration.lua +++ b/Migration.lua @@ -202,10 +202,6 @@ local EDIT_MODE_BUILTIN_NAMES = { "Modern", "Classic" } ---@return string[]|nil names All layout names, or nil if unavailable. local function resolveAllLayoutNames() - if type(C_EditMode) ~= "table" or type(C_EditMode.GetLayouts) ~= "function" then - return nil - end - local layoutInfo = C_EditMode.GetLayouts() if not layoutInfo then return nil diff --git a/Modules/BuffBars.lua b/Modules/BuffBars.lua index d8470512..7f7d3e8d 100644 --- a/Modules/BuffBars.lua +++ b/Modules/BuffBars.lua @@ -16,30 +16,17 @@ local function getSpellColors() return ns.SpellColors.Get(SPELL_COLOR_SCOPE) end -local function getFrameValue(frame, methodName) - if not frame or type(frame[methodName]) ~= "function" then - return nil - end - - local ok, value = pcall(frame[methodName], frame) - if ok then - return value - end - - return nil -end - local function getViewerDiagnostics(viewer, why) return { reason = why, viewerExists = viewer ~= nil, - viewerName = getFrameValue(viewer, "GetName"), - viewerShown = getFrameValue(viewer, "IsShown"), - viewerWidth = getFrameValue(viewer, "GetWidth"), - viewerHeight = getFrameValue(viewer, "GetHeight"), - viewerAlpha = getFrameValue(viewer, "GetAlpha"), - viewerNumPoints = getFrameValue(viewer, "GetNumPoints"), - viewerHasGetChildren = viewer ~= nil and type(viewer.GetChildren) == "function", + viewerName = ns.GetFrameValue(viewer, "GetName"), + viewerShown = ns.GetFrameValue(viewer, "IsShown"), + viewerWidth = ns.GetFrameValue(viewer, "GetWidth"), + viewerHeight = ns.GetFrameValue(viewer, "GetHeight"), + viewerAlpha = ns.GetFrameValue(viewer, "GetAlpha"), + viewerNumPoints = ns.GetFrameValue(viewer, "GetNumPoints"), + viewerHasGetChildren = viewer ~= nil, inCombatLockdown = InCombatLockdown(), } end @@ -292,7 +279,7 @@ function BuffBars:UpdateLayout(why) growsUp = position.point == "BOTTOMLEFT" else -- In free mode, infer from the viewer's current (Blizzard-managed) anchor. - local currentPoint = viewer.GetPoint and viewer:GetPoint(1) + local currentPoint = viewer:GetPoint(1) growsUp = currentPoint == "BOTTOMLEFT" or currentPoint == "BOTTOM" or currentPoint == "BOTTOMRIGHT" end diff --git a/Modules/ExternalBars.lua b/Modules/ExternalBars.lua index 2d0dfbc3..ea8f1319 100644 --- a/Modules/ExternalBars.lua +++ b/Modules/ExternalBars.lua @@ -49,70 +49,54 @@ local function getViewer() return _G["ExternalDefensivesFrame"] end ----@param tbl table|nil ----@param logKey string|nil ----@param reason string|nil ----@return number|nil -local function countAccessibleArray(tbl, logKey, reason) - if type(tbl) ~= "table" or not canAccessTable(tbl) then - return nil - end - - local count = 0 - local ok, err = pcall(function() - for index in ipairs(tbl) do - count = index - end - end) - if ok then - return count - end - +local function logAccessibleCountError(tbl, logKey, reason, operation, err) if logKey then ns.ErrorLogOnce("ExternalBars", logKey, "ExternalBars diagnostics could not iterate " .. logKey - .. " with ipairs during " .. tostring(reason or "unknown") .. ": " .. tostring(err), { + .. " with " .. operation .. " during " .. tostring(reason or "unknown") .. ": " .. tostring(err), { reason = reason, - operation = "ipairs", + operation = operation, error = tostring(err), tableType = type(tbl), tableAccessible = type(tbl) == "table" and canAccessTable(tbl) or nil, inCombatLockdown = InCombatLockdown(), }) end - return nil end ---@param tbl table|nil ---@param logKey string|nil ---@param reason string|nil ----@return number|nil -local function countAccessibleKeys(tbl, logKey, reason) +---@return number|nil, number|nil +local function countAccessibleEntries(tbl, logKey, reason) if type(tbl) ~= "table" or not canAccessTable(tbl) then - return nil + return nil, nil end - local count = 0 + local arrayCount = nil local ok, err = pcall(function() + local count = 0 + for index in ipairs(tbl) do + count = index + end + arrayCount = count + end) + if not ok then + logAccessibleCountError(tbl, logKey, reason, "ipairs", err) + end + + local keyCount = nil + ok, err = pcall(function() + local count = 0 for _ in pairs(tbl) do count = count + 1 end + keyCount = count end) - if ok then - return count + if not ok then + logAccessibleCountError(tbl, logKey, reason, "pairs", err) end - if logKey then - ns.ErrorLogOnce("ExternalBars", logKey, "ExternalBars diagnostics could not iterate " .. logKey - .. " with pairs during " .. tostring(reason or "unknown") .. ": " .. tostring(err), { - reason = reason, - operation = "pairs", - error = tostring(err), - tableType = type(tbl), - tableAccessible = type(tbl) == "table" and canAccessTable(tbl) or nil, - inCombatLockdown = InCombatLockdown(), - }) - end - return nil + return arrayCount, keyCount end ---@param reason string|nil @@ -131,8 +115,8 @@ local function getAuraInfoErrorData(reason, viewer, auraInfo) auraInfoType = type(auraInfo), canAccessAuraInfo = canAccessAuraInfo, viewerExists = viewer ~= nil, - viewerShown = viewer and viewer.IsShown and viewer:IsShown() or nil, - viewerAlpha = viewer and viewer.GetAlpha and viewer:GetAlpha() or nil, + viewerShown = viewer and viewer:IsShown() or nil, + viewerAlpha = viewer and viewer:GetAlpha() or nil, inCombatLockdown = InCombatLockdown(), instanceName = instanceName, instanceType = instanceType, @@ -347,33 +331,29 @@ function ExternalBars:_GetDiagnostics(viewer, auraInfo, reason) if moduleConfig then moduleConfigEnabled = moduleConfig.enabled ~= false end - - local viewerHasUpdateAuras = false - if viewer then - viewerHasUpdateAuras = type(viewer.UpdateAuras) == "function" - end + local auraInfoArrayCount, auraInfoKeyCount = countAccessibleEntries(auraInfo, "AuraInfoDiagnosticsFailed", reason) + local auraFramesArrayCount, auraFramesKeyCount = countAccessibleEntries(auraFrames, "AuraFramesDiagnosticsFailed", reason) return { moduleEnabled = self.IsEnabled and self:IsEnabled() or nil, moduleConfigEnabled = moduleConfigEnabled, moduleHidden = self.IsHidden == true, frameCreated = frame ~= nil, - frameShown = frame and frame.IsShown and frame:IsShown() or nil, - frameWidth = frame and frame.GetWidth and frame:GetWidth() or nil, - frameHeight = frame and frame.GetHeight and frame:GetHeight() or nil, + frameShown = frame and frame:IsShown() or nil, + frameWidth = frame and frame:GetWidth() or nil, + frameHeight = frame and frame:GetHeight() or nil, viewerExists = viewer ~= nil, - viewerShown = viewer and viewer.IsShown and viewer:IsShown() or nil, - viewerAlpha = viewer and viewer.GetAlpha and viewer:GetAlpha() or nil, + viewerShown = viewer and viewer:IsShown() or nil, + viewerAlpha = viewer and viewer:GetAlpha() or nil, viewerHooked = self._viewerHooked == true, - viewerHasUpdateAuras = viewerHasUpdateAuras, originalIconsHidden = self._originalIconsHidden == true, activeAuraCount = self._activeAuraCount or 0, auraInfoType = type(auraInfo), - auraInfoArrayCount = countAccessibleArray(auraInfo, "AuraInfoDiagnosticsFailed", reason), - auraInfoKeyCount = countAccessibleKeys(auraInfo, "AuraInfoDiagnosticsFailed", reason), + auraInfoArrayCount = auraInfoArrayCount, + auraInfoKeyCount = auraInfoKeyCount, auraFramesType = type(auraFrames), - auraFramesArrayCount = countAccessibleArray(auraFrames, "AuraFramesDiagnosticsFailed", reason), - auraFramesKeyCount = countAccessibleKeys(auraFrames, "AuraFramesDiagnosticsFailed", reason), + auraFramesArrayCount = auraFramesArrayCount, + auraFramesKeyCount = auraFramesKeyCount, } end @@ -449,11 +429,11 @@ function ExternalBars:_GetBarDiagnostics(index, bar, auraState) canShowDurationText = canShowDurationText, canUpdateDurationBar = canUpdateDurationBar, barExists = bar ~= nil, - barShown = bar and bar.IsShown and bar:IsShown() or nil, - barWidth = bar and bar.GetWidth and bar:GetWidth() or nil, - barHeight = bar and bar.GetHeight and bar:GetHeight() or nil, - iconShown = bar and bar.Icon and bar.Icon.IsShown and bar.Icon:IsShown() or nil, - iconTexture = iconTexture and iconTexture.GetTexture and iconTexture:GetTexture() or nil, + barShown = bar and bar:IsShown() or nil, + barWidth = bar and bar:GetWidth() or nil, + barHeight = bar and bar:GetHeight() or nil, + iconShown = bar and bar.Icon and bar.Icon:IsShown() or nil, + iconTexture = iconTexture and iconTexture:GetTexture() or nil, cooldownSpellID = bar and bar.cooldownInfo and bar.cooldownInfo.spellID or nil, } end diff --git a/Modules/ExtraIcons.lua b/Modules/ExtraIcons.lua index fab24fc4..607aa7a7 100644 --- a/Modules/ExtraIcons.lua +++ b/Modules/ExtraIcons.lua @@ -297,7 +297,6 @@ local function getViewerDiagnostics(blizzFrame, viewerConfig, why, itemFrames) viewerNumPoints = ns.GetFrameValue(blizzFrame, "GetNumPoints"), viewerIconScale = blizzFrame and blizzFrame.iconScale or nil, viewerChildXPadding = blizzFrame and blizzFrame.childXPadding or nil, - viewerHasGetItemFrames = blizzFrame ~= nil and type(blizzFrame.GetItemFrames) == "function", itemFramesType = type(itemFrames), itemFramesAccessible = itemFramesAccessible, itemFramesArrayCount = getItemFramesCount(itemFrames), @@ -588,8 +587,8 @@ function ExtraIcons:_rebuildTrackedSlots() end function ExtraIcons:HookEditMode() + if self._editModeHooked then return end local mgr = _G.EditModeManagerFrame - if not mgr or self._editModeHooked then return end self._editModeHooked = true self._isEditModeActive = mgr:IsShown() diff --git a/Modules/PowerBar.lua b/Modules/PowerBar.lua index 295f46e2..356a4afe 100644 --- a/Modules/PowerBar.lua +++ b/Modules/PowerBar.lua @@ -11,9 +11,8 @@ ns.Addon.PowerBar = PowerBar --- Elemental Shamans use Maelstrom while other Shaman specs use Mana. local function getCurrentPowerType() local _, class = UnitClass("player") - local specIndex = GetSpecialization() - if class == "SHAMAN" and specIndex then - if specIndex == C.SHAMAN_ELEMENTAL_SPEC_INDEX then + if class == "SHAMAN" then + if GetSpecialization() == C.SHAMAN_ELEMENTAL_SPEC_INDEX then return Enum.PowerType.Maelstrom end return Enum.PowerType.Mana @@ -35,7 +34,6 @@ function PowerBar:GetTickSpec() local classID = select(3, UnitClass("player")) local specIndex = GetSpecialization() - if not classID or not specIndex then return nil end local classMappings = ticksCfg.mappings[classID] local ticks = classMappings and classMappings[specIndex] diff --git a/Modules/RuneBar.lua b/Modules/RuneBar.lua index 04cbc461..77e7f822 100644 --- a/Modules/RuneBar.lua +++ b/Modules/RuneBar.lua @@ -105,10 +105,6 @@ end ---@param moduleConfig table ---@param globalConfig table local function updateFragmentedRuneDisplay(bar, maxRunes, moduleConfig, globalConfig) - if not GetRuneCooldown then - return - end - if not bar.FragmentedBars then return end diff --git a/Runtime.lua b/Runtime.lua index 622d47c6..60eb89fa 100644 --- a/Runtime.lua +++ b/Runtime.lua @@ -468,10 +468,6 @@ local function getRequestDiagnostics(opts) end local function getRequestDebugStack() - if type(debugstack) ~= "function" then - return nil - end - local ok, stack = pcall(debugstack, 3, 8, 8) if ok then return stack diff --git a/Tests/FrameUtil_spec.lua b/Tests/FrameUtil_spec.lua index b26bce35..64f43258 100644 --- a/Tests/FrameUtil_spec.lua +++ b/Tests/FrameUtil_spec.lua @@ -677,27 +677,6 @@ describe("FrameUtil", function() assert.are.equal(600, h) end) - it("falls back to GetWidth/GetHeight", function() - local p = { - GetWidth = function() return 1024 end, - GetHeight = function() return 768 end, - } - local w, h = FrameUtil.GetParentSize(p) - assert.are.equal(1024, w) - assert.are.equal(768, h) - end) - - it("falls back to GetWidth/GetHeight when GetSize returns nil", function() - local p = { - GetSize = function() return nil, nil end, - GetWidth = function() return 640 end, - GetHeight = function() return 480 end, - } - local w, h = FrameUtil.GetParentSize(p) - assert.are.equal(640, w) - assert.are.equal(480, h) - end) - it("returns 0, 0 for nil parent", function() local w, h = FrameUtil.GetParentSize(nil) assert.are.equal(0, w) diff --git a/Tests/Migration_spec.lua b/Tests/Migration_spec.lua index 7cf3ab56..ba9cb0e1 100644 --- a/Tests/Migration_spec.lua +++ b/Tests/Migration_spec.lua @@ -114,6 +114,9 @@ describe("Migration", function() end end _G.UIParent = { + GetSize = function() + return 1920, 1080 + end, GetWidth = function() return 1920 end, @@ -951,8 +954,12 @@ describe("Migration", function() assert.same(expected, profile.powerBar.editModePositions.MyCustomLayout) end) - it("V11 advances schema even when the active layout name cannot be resolved", function() - _G.C_EditMode = nil + it("V11 advances schema when Edit Mode layout data is unavailable", function() + _G.C_EditMode = { + GetLayouts = function() + return nil + end, + } local profile = { schemaVersion = 10, diff --git a/Tests/Modules/BuffBars_spec.lua b/Tests/Modules/BuffBars_spec.lua index 93f83dea..335465dc 100644 --- a/Tests/Modules/BuffBars_spec.lua +++ b/Tests/Modules/BuffBars_spec.lua @@ -166,6 +166,12 @@ describe("BuffBars real source", function() ErrorLogOnce = function(module, key, message, data) errorLogs[#errorLogs + 1] = { module = module, key = key, message = message, data = data } end, + GetFrameValue = function(frame, methodName) + if not frame then + return nil + end + return frame[methodName](frame) + end, DebugAssert = function() end, IsDebugEnabled = function() return false end, Constants = nil, diff --git a/Tests/TestHelpers.lua b/Tests/TestHelpers.lua index 8b873cb0..5f6d1d4d 100644 --- a/Tests/TestHelpers.lua +++ b/Tests/TestHelpers.lua @@ -212,6 +212,10 @@ function TestHelpers.SetupSettingsStubs() "ECM_DeepEquals", "CreateFromMixins", "SettingsListElementInitializer", + "hooksecurefunc", + "SettingsDropdownControlMixin", + "SettingsSliderControlMixin", + "SettingsPanel", } local function makeLayout() @@ -382,6 +386,17 @@ function TestHelpers.SetupSettingsStubs() Event = { OnValueChanged = "OnValueChanged" }, } + _G.hooksecurefunc = _G.hooksecurefunc or function() end + _G.SettingsDropdownControlMixin = _G.SettingsDropdownControlMixin or {} + _G.SettingsSliderControlMixin = _G.SettingsSliderControlMixin or {} + _G.SettingsPanel = _G.SettingsPanel or { + DisplayCategory = function() end, + GetCurrentCategory = function() return nil end, + GetSettingsList = function() return nil end, + HookScript = function() end, + IsShown = function() return false end, + } + _G.CreateColor = function(r, g, b, a) return { r = r, g = g, b = b, a = a or 1 } end @@ -401,7 +416,7 @@ function TestHelpers.SetupSettingsStubs() if dialog.hasEditBox then local text = "" local editBox = { GetText = function() return text end, SetText = function(_, t) text = t end, HighlightText = function() end } - local self = { editBox = editBox, button1 = { IsEnabled = function() return true end } } + local self = { editBox = editBox, button1 = { IsEnabled = function() return true end, SetEnabled = function() end } } if dialog.OnShow then dialog.OnShow(self) end dialog.OnAccept(self, data) else @@ -629,6 +644,21 @@ function TestHelpers.makeTexture(opts) return self.__desaturated == true end + texture.__masks = {} + function texture:GetNumMaskTextures() + return #self.__masks + end + function texture:GetMaskTexture(index) + return self.__masks[index] + end + function texture:RemoveMaskTexture(mask) + for i = #self.__masks, 1, -1 do + if self.__masks[i] == mask then + table.remove(self.__masks, i) + end + end + end + return texture end @@ -1945,6 +1975,7 @@ function TestHelpers.InstallPopupAutoAccept(editText) IsEnabled = function() return true end, + SetEnabled = function() end, }, } diff --git a/Tests/UI/ItemStacksOptions_spec.lua b/Tests/UI/ItemStacksOptions_spec.lua index 655dac84..54b80587 100644 --- a/Tests/UI/ItemStacksOptions_spec.lua +++ b/Tests/UI/ItemStacksOptions_spec.lua @@ -370,8 +370,7 @@ describe("ItemStacksOptions settings page", function() end) it("protects default stacks and reverts them to defaults", function() - ns.Addon.db.defaults = { profile = { extraIcons = { itemStacks = { byId = {} } } } } - ns.defaults = { profile = defaults } + ns.Addon.db.defaults = { profile = TestHelpers.deepClone(defaults) } profile.extraIcons.itemStacks = TestHelpers.deepClone(defaults.extraIcons.itemStacks) profile.extraIcons.itemStacks.byId.combatPotions.name = "Custom Combat" profile.extraIcons.itemStacks.byId.combatPotions.ids = { { itemID = 999 } } diff --git a/UI/ExtraIconsOptions.lua b/UI/ExtraIconsOptions.lua index 317a8ae6..d999551e 100644 --- a/UI/ExtraIconsOptions.lua +++ b/UI/ExtraIconsOptions.lua @@ -97,13 +97,11 @@ local function doActionAndUpdateLayout(fn) end local function getSpellName(spellId) - local api = type(C_Spell) == "table" and C_Spell or nil - return spellId and api and api.GetSpellName and api.GetSpellName(spellId) or nil + return spellId and C_Spell.GetSpellName(spellId) or nil end local function getSpellTexture(spellId) - local api = type(C_Spell) == "table" and C_Spell or nil - return spellId and api and api.GetSpellTexture and api.GetSpellTexture(spellId) or nil + return spellId and C_Spell.GetSpellTexture(spellId) or nil end local function isDisabledBuiltinEntry(entry) return entry and entry.stackKey and entry.disabled and BUILTIN_STACK_SET[entry.stackKey] == true end diff --git a/UI/ItemStacksOptions.lua b/UI/ItemStacksOptions.lua index ff322c8c..a6e9d808 100644 --- a/UI/ItemStacksOptions.lua +++ b/UI/ItemStacksOptions.lua @@ -18,8 +18,6 @@ StaticPopupDialogs["ECM_CONFIRM_REMOVE_ITEM_STACK_ITEM"] = OptionUtil.MakeConfirmDialog(L["ITEM_STACK_REMOVE_ITEM_CONFIRM"], L["REMOVE"], L["DONT_REMOVE"]) StaticPopupDialogs["ECM_CONFIRM_REVERT_ITEM_STACK"] = ns.OptionUtil.MakeConfirmDialog(L["ITEM_STACK_REVERT_CONFIRM"], L["REVERT"], L["DONT_REVERT"]) -StaticPopupDialogs["ECM_CONFIRM_REVERT_ITEM_STACK"] = - ns.OptionUtil.MakeConfirmDialog(L["ITEM_STACK_REVERT_CONFIRM"], L["REVERT"], L["DONT_REVERT"]) local ITEM_STACK_ROW_HEIGHT = 22 local ITEM_STACK_COLLECTION_HEIGHT = 240 @@ -39,13 +37,6 @@ local function getProfile() return ns.Addon.db.profile end local function getDefaultStack(stackId) local defaults = ns.Addon.db.defaults and ns.Addon.db.defaults.profile local defaultStacks = defaults and defaults.extraIcons and defaults.extraIcons.itemStacks - local defaultStack = defaultStacks and defaultStacks.byId and defaultStacks.byId[stackId] - if defaultStack then - return defaultStack - end - - defaults = ns.defaults and ns.defaults.profile - defaultStacks = defaults and defaults.extraIcons and defaults.extraIcons.itemStacks return defaultStacks and defaultStacks.byId and defaultStacks.byId[stackId] or nil end @@ -56,16 +47,12 @@ local function refreshPage() end end -local function doAction(fn, page) +local function doAction(fn) if fn then fn() end ns.Runtime.ScheduleLayoutUpdate(0, "OptionsChanged") - if registeredPage then - refreshPage() - elseif page then - page:Refresh() - end + refreshPage() end local function getItemStacks() return EnsureItemStacks(getProfile()) end @@ -382,18 +369,18 @@ local function disableManagedStackActions() return stackId == nil or isDefaultStackId(stackId) end -local function openCreateDialog(ctx) +local function openCreateDialog() StaticPopup_Show("ECM_CREATE_ITEM_STACK", nil, nil, { popupKey = "ECM_CREATE_ITEM_STACK", onAccept = function(name) doAction(function() createStack(getProfile(), name) - end, ctx and ctx.page or registeredPage) + end) end, }) end -local function openRenameDialog(ctx) +local function openRenameDialog() local stackId = getSelectedStackId() local itemStack = getSelectedStack() if not itemStack or isDefaultStackId(stackId) then @@ -405,12 +392,12 @@ local function openRenameDialog(ctx) onAccept = function(name) doAction(function() renameStack(getProfile(), stackId, name) - end, ctx and ctx.page or registeredPage) + end) end, }) end -local function openDeleteDialog(ctx) +local function openDeleteDialog() local stackId = getSelectedStackId() local itemStack = getSelectedStack() if not itemStack or isDefaultStackId(stackId) then @@ -420,12 +407,12 @@ local function openDeleteDialog(ctx) onAccept = function() doAction(function() deleteStack(getProfile(), stackId) - end, ctx and ctx.page or registeredPage) + end) end, }) end -local function revertSelectedStack(ctx) +local function revertSelectedStack() local stackId = getSelectedStackId() local itemStack = getSelectedStack() if not itemStack or not isDefaultStackId(stackId) then @@ -435,7 +422,7 @@ local function revertSelectedStack(ctx) onAccept = function() doAction(function() revertStackToDefault(getProfile(), stackId) - end, ctx and ctx.page or registeredPage) + end) end, }) end diff --git a/UI/OptionUtil.lua b/UI/OptionUtil.lua index a54ce75a..0b553b4f 100644 --- a/UI/OptionUtil.lua +++ b/UI/OptionUtil.lua @@ -65,9 +65,7 @@ end local function createPreviewBlock(parent, width, height, point, relativeTo, relativePoint, x, y, color) local block = parent:CreateTexture(nil, "ARTWORK") - if type(block.SetColorTexture) == "function" then - block:SetColorTexture(color[1], color[2], color[3], color[4] or 1) - end + block:SetColorTexture(color[1], color[2], color[3], color[4] or 1) block:SetSize(width, height) block:SetPoint(point, relativeTo, relativePoint, x, y) return block @@ -80,10 +78,6 @@ local function createPreviewBars(parent, positions) end function OptionUtil.CreatePositioningExamplesCanvas() - if type(CreateFrame) ~= "function" then - return {} - end - local frame = CreateFrame("Frame") frame:SetHeight(C.POSITION_MODE_EXPLAINER_HEIGHT) @@ -153,9 +147,7 @@ function OptionUtil.CreatePositioningExamplesCanvas() local previewBg = preview:CreateTexture(nil, "BACKGROUND") previewBg:SetAllPoints(preview) - if type(previewBg.SetColorTexture) == "function" then - previewBg:SetColorTexture(0.08, 0.08, 0.08, 1) - end + previewBg:SetColorTexture(0.08, 0.08, 0.08, 1) column.build(preview) @@ -368,11 +360,11 @@ function OptionUtil.CreateFontOverrideRow(isDisabled) path = "", disabled = isDisabled, fontValues = function() - return LSMW and LSMW.GetFontValues and LSMW.GetFontValues() or {} + return LSMW.GetFontValues() end, fontFallback = getGlobalFont, fontSizeFallback = getGlobalFontSize, - fontTemplate = LSMW and LSMW.FONT_PICKER_TEMPLATE or nil, + fontTemplate = LSMW.FONT_PICKER_TEMPLATE, } end @@ -617,25 +609,15 @@ function OptionUtil.MakeConfirmDialog(text, button1, button2) } end -local function getPopupEditBox(frame) return frame and (frame.EditBox or frame.editBox) end +local function getPopupEditBox(frame) return frame and frame.editBox end local function trimDialogText(text) - if strtrim then - return strtrim(text or "") - end - return tostring(text or ""):match("^%s*(.-)%s*$") + return strtrim(text or "") end local function setDialogAcceptEnabled(frame, enabled) - local button = frame and (frame.button1 or (frame.Buttons and frame.Buttons[1])) - if not button then - return - end - if enabled and button.Enable then - button:Enable() - elseif not enabled and button.Disable then - button:Disable() - elseif button.SetEnabled then + local button = frame and frame.button1 + if button then button:SetEnabled(enabled) end end @@ -675,13 +657,12 @@ function OptionUtil.MakeTextInputDialog(text, button1, button2) if not parent then return end - local button1Frame = parent and (parent.button1 or (parent.Buttons and parent.Buttons[1])) - if button1Frame and button1Frame.IsEnabled and not button1Frame:IsEnabled() then + if parent.button1 and not parent.button1:IsEnabled() then return end parent:Hide() - local dialog = parent and parent.which and StaticPopupDialogs[parent.which] - dialog = dialog or (parent and parent.data and parent.data.popupKey and StaticPopupDialogs[parent.data.popupKey]) + local dialog = parent.which and StaticPopupDialogs[parent.which] + dialog = dialog or (parent.data and parent.data.popupKey and StaticPopupDialogs[parent.data.popupKey]) if dialog and dialog.OnAccept then dialog.OnAccept(parent, parent.data) end diff --git a/UI/Options.lua b/UI/Options.lua index 0aaa3ec0..6065925d 100644 --- a/UI/Options.lua +++ b/UI/Options.lua @@ -39,7 +39,7 @@ local function isTrackedECMCategory(category) end local function getCategoryOpenToken(category) - if category and type(category.GetID) == "function" then + if category then return category:GetID() end end @@ -70,28 +70,9 @@ function Options:InstallCategoryTracking() return end - if type(SettingsPanel) ~= "table" or type(SettingsPanel.DisplayCategory) ~= "function" then - if self._categoryTrackingDeferred or type(CreateFrame) ~= "function" then - return - end - - self._categoryTrackingDeferred = true - local tracker = CreateFrame("Frame") - tracker:RegisterEvent("ADDON_LOADED") - tracker:SetScript("OnEvent", function(frame) - if type(SettingsPanel) == "table" and type(SettingsPanel.DisplayCategory) == "function" then - self._categoryTrackingDeferred = nil - self:InstallCategoryTracking() - frame:UnregisterAllEvents() - end - end) - return - end - self._categoryTrackingInstalled = true hooksecurefunc(SettingsPanel, "DisplayCategory", function(panel) - local category = panel.GetCurrentCategory and panel:GetCurrentCategory() or nil - rememberTrackedCategory(self, category) + rememberTrackedCategory(self, panel:GetCurrentCategory()) end) end @@ -142,8 +123,7 @@ end function Options:OpenOptions() self:InstallCategoryTracking() - local currentCategory = SettingsPanel and SettingsPanel.GetCurrentCategory and SettingsPanel:GetCurrentCategory() or nil - rememberTrackedCategory(self, currentCategory) + rememberTrackedCategory(self, SettingsPanel:GetCurrentCategory()) local categoryToken = self._lastOpenedCategoryToken or getDefaultOptionsCategoryToken() if categoryToken then diff --git a/UI/ProfileOptions.lua b/UI/ProfileOptions.lua index 856434d9..6364f52f 100644 --- a/UI/ProfileOptions.lua +++ b/UI/ProfileOptions.lua @@ -11,32 +11,21 @@ StaticPopupDialogs["ECM_NEW_PROFILE"] = { button2 = L["DONT_CREATE"], hasEditBox = true, OnAccept = function(self, data) - local editBox = self and (self.EditBox or self.editBox) - if not editBox then - return - end - local name = strtrim(editBox:GetText()) + local name = strtrim(self.editBox:GetText()) if name ~= "" and data and data.onAccept then data.onAccept(name) end end, OnShow = function(self) - local editBox = self.EditBox or self.editBox - if not editBox then - return - end - editBox:SetText(UnitName("player") .. " - " .. date("%H%M%S")) - editBox:HighlightText() + self.editBox:SetText(UnitName("player") .. " - " .. date("%H%M%S")) + self.editBox:HighlightText() end, EditBoxOnEnterPressed = function(self) local parent = self:GetParent() - local button1 = parent and (parent.button1 or (parent.Buttons and parent.Buttons[1])) - if not button1 or button1:IsEnabled() then + if parent.button1:IsEnabled() then parent:Hide() local dialog = StaticPopupDialogs["ECM_NEW_PROFILE"] - if dialog.OnAccept then - dialog.OnAccept(parent, parent.data) - end + dialog.OnAccept(parent, parent.data) end end, timeout = 0, From 5eaca44e56811ec80cf1132d29e3ea8935d841df Mon Sep 17 00:00:00 2001 From: Argi <15852038+argium@users.noreply.github.com> Date: Tue, 19 May 2026 17:00:46 +1000 Subject: [PATCH 10/15] linter cleanup pass --- .luacheckrc | 1 + BarMixin.lua | 181 ++++++++++++------ BarStyle.lua | 30 ++- Defaults.lua | 3 + ECM.lua | 47 +---- EnhancedCooldownManager.code-workspace | 10 +- .../Interop/CollectionFrames.lua | 9 +- Modules/BuffBars.lua | 30 +-- Modules/ExternalBars.lua | 14 +- Modules/ExtraIcons.lua | 6 +- Modules/RuneBar.lua | 30 ++- Runtime.lua | 14 +- SpellColors.lua | 62 ++++-- Tests/ECM_Runtime_spec.lua | 21 +- Tests/ECM_spec.lua | 24 +-- UI/Options.lua | 3 + UI/SpellColorsPage.lua | 2 +- 17 files changed, 265 insertions(+), 222 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index a95b15aa..211e78a5 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -63,6 +63,7 @@ read_globals = {'C_PlayerInfo','DEFAULT_CHAT_FRAME', 'MenuUtil', 'GameTooltip', "CreateScrollBoxListLinearView", "CreateSettingsButtonInitializer", "CreateSettingsListSectionHeaderInitializer", + "CooldownViewerSettings", "CurveConstants", "debugstack", "DevTool", diff --git a/BarMixin.lua b/BarMixin.lua index 0166c276..d7a69709 100644 --- a/BarMixin.lua +++ b/BarMixin.lua @@ -2,6 +2,67 @@ -- Author: Argium -- Licensed under the GNU General Public License v3.0 +---@alias AnchorPoint string + +---@class ECM_EditModeFrame : Frame Frame registered with LibEditMode. +---@field editModeName string|nil Edit Mode registration name. + +---@class ECM_EditModeFrameOptions Options used to register a frame with LibEditMode. +---@field name string Edit Mode display name. +---@field defaultPosition ECM_EditModePosition|nil Default frame position. +---@field onPositionChanged fun(layoutName: string, point: string, x: number, y: number) Callback invoked when the frame is moved. +---@field hideSelection fun(): boolean|nil Predicate that hides the LibEditMode selection frame when true. +---@field settings table[]|nil LibEditMode settings entries. + +---@class FrameProto Frame mixin that owns visibility, positioning, Edit Mode registration, and config access. +---@field _configKey string|nil Config key for this frame's section. +---@field _editModeRegisteredFrame Frame|nil Frame already registered with Edit Mode. +---@field _lastUpdate number|nil Timestamp of the most recent throttled refresh. +---@field IsHidden boolean|nil Whether the frame is currently hidden. +---@field InnerFrame Frame|nil Inner WoW frame owned by this mixin. +---@field Name string Name of the frame. +---@field ChainRightPoint fun(point: string|nil, fallback: string): string Gets the matching right-side anchor point. +---@field NormalizeGrowDirection fun(direction: string|nil): string Gets a supported grow direction. +---@field GetGlobalConfig fun(self: FrameProto): ECM_GlobalConfig Gets the live global configuration. +---@field RegisterEvent fun(self: FrameProto, event: string, callback: function) Registers an event callback. +---@field UnregisterEvent fun(self: FrameProto, event: string) Unregisters an event callback. +---@field UnregisterAllEvents fun(self: FrameProto) Unregisters all event callbacks. +---@field GetModuleConfig fun(self: FrameProto): table|nil Gets the live module configuration. +---@field GetNextChainAnchor fun(self: FrameProto, frameName: string|nil, anchorMode: string|nil): Frame, boolean Gets the previous frame in the module chain. +---@field EnsureFrame fun(self: FrameProto) Ensures the inner frame exists and is registered with Edit Mode. +---@field IsEnabled fun(self: FrameProto): boolean Gets whether the module is enabled. +---@field ShouldShow fun(self: FrameProto): boolean Gets whether the frame should currently be shown. +---@field ShouldRegisterEditMode fun(self: FrameProto): boolean Gets whether the frame should be registered with Edit Mode. +---@field CreateFrame fun(self: FrameProto): Frame Creates the inner frame. +---@field CalculateLayoutParams fun(self: FrameProto): table Gets layout parameters for the current anchoring mode. +---@field ApplyFramePosition fun(self: FrameProto): table|nil Applies the current frame position. +---@field UpdateLayout fun(self: FrameProto, why: string|nil): boolean Updates frame layout. +---@field SetHidden fun(self: FrameProto, hidden: boolean) Sets whether the frame is hidden. +---@field Refresh fun(self: FrameProto, why: string|nil, force: boolean|nil): boolean Refreshes frame state. +---@field ThrottledRefresh fun(self: FrameProto, why: string|nil, immediate: boolean|nil): boolean Refreshes frame state subject to throttling. +---@field IsReady fun(self: FrameProto): boolean Gets whether the module is ready for layout updates. +---@field _SaveEditModePosition fun(self: FrameProto, layoutName: string, point: string, x: number, y: number) Saves an Edit Mode position. +---@field _RegisterEditMode fun(self: FrameProto) Registers the frame with Edit Mode. + +---@class BarInnerFrame : Frame Inner frame for bar modules with StatusBar and tick regions. +---@field StatusBar StatusBar Value display bar. +---@field TicksFrame Frame Container for tick mark textures. +---@field TextValue FontString|nil Text overlay for value display. +---@field TextFrame Frame|nil Container frame for text overlay. +---@field SetText fun(self: BarInnerFrame, text: string|number|nil) Sets the displayed text value. +---@field SetTextVisible fun(self: BarInnerFrame, shown: boolean) Sets whether the displayed text is visible. + +---@class BarProto : FrameProto Status bar layer with ticks, text, and value refresh. +---@field InnerFrame BarInnerFrame|nil Inner frame with StatusBar and tick children. +---@field tickPool table|nil Default pool of tick mark textures. +---@field EnsureTicks fun(self: BarProto, count: number, parentFrame: Frame, poolKey: string|nil) Ensures a tick texture pool has enough visible ticks. +---@field HideAllTicks fun(self: BarProto, poolKey: string|nil) Hides all ticks in a tick texture pool. +---@field LayoutResourceTicks fun(self: BarProto, maxResources: number, color: ECM_Color|table|nil, tickWidth: number|nil, poolKey: string|nil) Positions ticks evenly as resource dividers. +---@field LayoutValueTicks fun(self: BarProto, statusBar: StatusBar, ticks: table, maxValue: number, defaultColor: ECM_Color, defaultWidth: number, poolKey: string|nil) Positions ticks at specific resource values. +---@field GetStatusBarValues fun(self: BarProto): number|nil, number|nil, number|nil, boolean Gets the current bar value details. +---@field GetStatusBarColor fun(self: BarProto): ECM_Color Gets the current status bar color. +---@field GetTickSpec fun(self: BarProto): table|nil Gets tick layout details for the current refresh. + local _, ns = ... local C = ns.Constants local L = ns.L @@ -17,6 +78,7 @@ EditMode.Lib = LibEditMode ns.EditMode = EditMode --- Gets the active Edit Mode layout name. +---@return string|nil layoutName function EditMode.GetActiveLayoutName() return LibEditMode:GetActiveLayoutName() end @@ -42,6 +104,7 @@ function EditMode.GetPosition(positions, layoutName) return { point = C.EDIT_MODE_DEFAULT_POINT, x = 0, y = 0 }, activeLayoutName end +--- Saves an Edit Mode position for a layout. ---@param container table|nil ---@param fieldName string ---@param layoutName string @@ -60,8 +123,8 @@ function EditMode.SavePosition(container, fieldName, layoutName, point, x, y) container[fieldName][layoutName] = { point = point, x = x, y = y } end ----@param frame Frame|nil ----@param options table +---@param frame ECM_EditModeFrame|nil +---@param options ECM_EditModeFrameOptions function EditMode.RegisterFrame(frame, options) if not frame then return @@ -113,14 +176,6 @@ end) -- FrameProto — base frame layer (positioning, visibility, edit mode, config) -------------------------------------------------------------------------------- ----@alias AnchorPoint string - ----@class FrameProto : AceModule Frame mixin that owns visibility and config access. ----@field _configKey string|nil Config key for this frame's section. ----@field IsHidden boolean|nil Whether the frame is currently hidden. ----@field InnerFrame Frame|nil Inner WoW frame owned by this mixin. ----@field Name string Name of the frame. - local FrameProto = {} --- Returns the effective root anchor for chained modules. @@ -142,10 +197,10 @@ local function getPrimaryChainAnchor() end --- Determine the correct anchor for this specific frame in the fixed order. ---- @param frameName string|nil The name of the current frame, or nil if first in chain. ---- @param anchorMode string|nil The anchor mode to filter by (defaults to ANCHORMODE_CHAIN). ---- @return Frame The frame to anchor to. ---- @return boolean isFirst True if this is the first frame in the chain. +---@param frameName string|nil The name of the current frame, or nil if first in chain. +---@param anchorMode string|nil The anchor mode to filter by (defaults to ANCHORMODE_CHAIN). +---@return Frame anchor The frame to anchor to. +---@return boolean isFirst True if this is the first frame in the chain. function FrameProto:GetNextChainAnchor(frameName, anchorMode) anchorMode = anchorMode or C.ANCHORMODE_CHAIN @@ -182,11 +237,12 @@ function FrameProto:GetNextChainAnchor(frameName, anchorMode) return getPrimaryChainAnchor(), true end -function FrameProto:SetHidden(hide) - self.IsHidden = hide +---@param hidden boolean +function FrameProto:SetHidden(hidden) + self.IsHidden = hidden if self.InnerFrame then -- Hide immediately, but defer showing until the next layout pass to ensure proper anchoring. - if hide then + if hidden then self.InnerFrame:Hide() else ns.Runtime.RequestLayout("SetHidden") @@ -207,6 +263,7 @@ function FrameProto:ShouldRegisterEditMode() return true end +---@return Frame frame function FrameProto:CreateFrame() local globalConfig = self:GetGlobalConfig() local moduleConfig = self:GetModuleConfig() @@ -274,7 +331,7 @@ local function getStackedLayoutParams(self, globalConfig, moduleConfig, mode) local anchor, isFirst = self:GetNextChainAnchor(self.Name, mode) local directionKey = isDetached and "detachedGrowDirection" or "moduleGrowDirection" - local growsUp = self.NormalizeGrowDirection(globalConfig and globalConfig[directionKey]) == C.GROW_DIRECTION_UP + local growsUp = FrameProto.NormalizeGrowDirection(globalConfig and globalConfig[directionKey]) == C.GROW_DIRECTION_UP local gap if isDetached then @@ -305,8 +362,8 @@ end --- Modules with custom positioning (e.g. BuffBars) override this. ---@return table params Layout parameters: mode, anchor, isFirst, anchorPoint, anchorRelativePoint, offsetX, offsetY, width, height function FrameProto:CalculateLayoutParams() - local globalConfig = self:GetGlobalConfig() - local moduleConfig = self:GetModuleConfig() + local globalConfig = assert(self:GetGlobalConfig(), "global config required") + local moduleConfig = assert(self:GetModuleConfig(), "module config required") local mode = moduleConfig.anchorMode or C.ANCHORMODE_CHAIN if mode == C.ANCHORMODE_FREE then local pos = EditMode.GetPosition(moduleConfig and moduleConfig.editModePositions) @@ -330,7 +387,7 @@ end --- Handles ShouldShow check, layout calculation, and anchor positioning. ---@return table|nil params Layout params if shown, nil if hidden function FrameProto:ApplyFramePosition() - local frame = self.InnerFrame + local frame = assert(self.InnerFrame, "InnerFrame required") if not self:ShouldShow() then frame:Hide() return nil @@ -370,9 +427,9 @@ end ---@param why string|nil ---@return boolean function FrameProto:UpdateLayout(why) - local globalConfig = self:GetGlobalConfig() - local moduleConfig = self:GetModuleConfig() - local frame = self.InnerFrame + local globalConfig = assert(self:GetGlobalConfig(), "global config required") + local moduleConfig = assert(self:GetModuleConfig(), "module config required") + local frame = assert(self.InnerFrame, "InnerFrame required") local borderConfig = moduleConfig.border local params = self:ApplyFramePosition() @@ -404,17 +461,17 @@ function FrameProto:UpdateLayout(why) end --- Handles common refresh logic for FrameProto-derived frames. ---- @param why string|nil Optional debug string for why the refresh was triggered. ---- @param force boolean|nil Whether to force a refresh, even if the bar is hidden. ---- @return boolean continue True if the frame should continue refreshing, false to skip. +---@param why string|nil Optional debug string for why the refresh was triggered. +---@param force boolean|nil Whether to force a refresh, even if the bar is hidden. +---@return boolean continue True if the frame should continue refreshing, false to skip. function FrameProto:Refresh(why, force) return force or self:ShouldShow() end --- Rate-limited refresh. Skips if called within updateFrequency window unless immediate is true. ---- @param why string|nil Optional debug string for why the refresh was triggered. ---- @param immediate boolean|nil Whether to bypass the rate limit without bypassing ShouldShow. ---- @return boolean refreshed True if Refresh() was called +---@param why string|nil Optional debug string for why the refresh was triggered. +---@param immediate boolean|nil Whether to bypass the rate limit without bypassing ShouldShow. +---@return boolean refreshed True if Refresh() was called function FrameProto:ThrottledRefresh(why, immediate) local globalConfig = self:GetGlobalConfig() local freq = (globalConfig and globalConfig.updateFrequency) or C.DEFAULT_REFRESH_FREQUENCY @@ -427,7 +484,7 @@ function FrameProto:ThrottledRefresh(why, immediate) end --- Checks if the module is ready for layout updates. ---- @return boolean ready True if the module is ready for updates. +---@return boolean ready True if the module is ready for updates. function FrameProto:IsReady() return self:IsEnabled() and self.InnerFrame ~= nil @@ -453,6 +510,7 @@ function FrameProto:_RegisterEditMode() if not frame or self._editModeRegisteredFrame == frame then return end + ---@cast frame ECM_EditModeFrame local module = self EditMode.RegisterFrame(frame, { @@ -497,7 +555,7 @@ function FrameProto:_RegisterEditMode() end --- Returns this module's config section (live from AceDB profile). ----@return table|nil +---@return table|nil config function FrameProto:GetModuleConfig() return ns.Addon.db and ns.Addon.db.profile and ns.Addon.db.profile[self._configKey] end @@ -505,12 +563,10 @@ end -------------------------------------------------------------------------------- -- BarProto — status bar layer (StatusBar, ticks, text, refresh) -------------------------------------------------------------------------------- - local BarProto = setmetatable({}, { __index = FrameProto }) --- Ensures the tick pool has the required number of ticks. --- Creates new ticks as needed, shows required ticks, hides extras. ----@param self BarProto ---@param count number Number of ticks needed ---@param parentFrame Frame Frame to create ticks on (e.g., bar.StatusBar or bar.TicksFrame) ---@param poolKey string|nil Key for tick pool on bar (default "tickPool") @@ -541,7 +597,6 @@ function BarProto:EnsureTicks(count, parentFrame, poolKey) end --- Hides all ticks in the pool. ----@param self BarProto ---@param poolKey string|nil Key for tick pool (default "tickPool") function BarProto:HideAllTicks(poolKey) local pool = self[poolKey or "tickPool"] @@ -556,7 +611,6 @@ end --- Positions ticks evenly as resource dividers. --- Used by ResourceBar to show divisions between resources. ----@param self BarProto ---@param maxResources number Number of resources (ticks = maxResources - 1) ---@param color ECM_Color|table|nil RGBA color (default black) ---@param tickWidth number|nil Width of each tick (default 1) @@ -568,7 +622,7 @@ function BarProto:LayoutResourceTicks(maxResources, color, tickWidth, poolKey) return end - local frame = self.InnerFrame + local frame = assert(self.InnerFrame, "InnerFrame required") local barWidth = frame:GetWidth() local barHeight = frame:GetHeight() if barWidth <= 0 or barHeight <= 0 then @@ -600,7 +654,6 @@ end --- Positions ticks at specific resource values. --- Used by PowerBar for breakpoint markers (e.g., energy thresholds). ----@param self BarProto ---@param statusBar StatusBar StatusBar to position ticks on ---@param ticks table Array of tick definitions { { value = number, color = ECM_Color, width = number }, ... } ---@param maxValue number Maximum resource value @@ -617,7 +670,7 @@ function BarProto:LayoutValueTicks(statusBar, ticks, maxValue, defaultColor, def return end - local frame = self.InnerFrame + local frame = assert(self.InnerFrame, "InnerFrame required") local barWidth = statusBar:GetWidth() local barHeight = frame:GetHeight() if barWidth <= 0 or barHeight <= 0 then @@ -660,7 +713,7 @@ end ---@return number|nil current ---@return number|nil max ---@return number|nil displayValue ----@return boolean isFraction valueType +---@return boolean isFraction function BarProto:GetStatusBarValues() ns.DebugAssert(false, "GetStatusBarValues not implemented in derived class") return -1, -1, -1, false @@ -676,17 +729,19 @@ function BarProto:GetStatusBarColor() end --- Refreshes the bar frame layout and values. ---- @param why string|nil Reason for refresh (for logging/debugging). ---- @param force boolean|nil If true, forces a refresh even if not needed. ---- @return boolean continue True if refresh completed, false if skipped +---@param why string|nil Reason for refresh (for logging/debugging). +---@param force boolean|nil If true, forces a refresh even if not needed. +---@return boolean continue True if refresh completed, false if skipped +---@diagnostic disable-next-line: duplicate-set-field function BarProto:Refresh(why, force) if not FrameProto.Refresh(self, why, force) then return false end - local frame = self.InnerFrame + local frame = assert(self.InnerFrame, "InnerFrame required") + ---@cast frame BarInnerFrame local globalConfig = self:GetGlobalConfig() - local moduleConfig = self:GetModuleConfig() + local moduleConfig = assert(self:GetModuleConfig(), "module config required") -- Values: apply min/max before value so startup/transient states do not -- render full when current is zero. @@ -749,8 +804,11 @@ function BarProto:Refresh(why, force) return true end +---@return BarInnerFrame frame +---@diagnostic disable-next-line: duplicate-set-field function BarProto:CreateFrame() local frame = FrameProto.CreateFrame(self) + ---@cast frame BarInnerFrame -- StatusBar for value display frame.StatusBar = CreateFrame("StatusBar", nil, frame) @@ -795,6 +853,7 @@ BarMixin.FrameProto = FrameProto BarMixin.BarProto = BarProto setmetatable(BarMixin, { __index = BarProto }) +---@param target table function BarMixin.AssertValid(target) assert(target and type(target) == "table", "target is not a table") assert(target.Name, "target is missing a Name") @@ -804,8 +863,8 @@ end --- Applies frame-only mixin (positioning, visibility, edit mode, config access). --- Used by modules that manage their own inner content (e.g. BuffBars, ExtraIcons). --- Idempotent — safe to call more than once (no-op after first application). ---- @param target table table to apply the mixin to. ---- @param name string the module name. must be unique. +---@param target table table to apply the mixin to. +---@param name string the module name. must be unique. function BarMixin.AddFrameMixin(target, name) assert(target, "target required") assert(name, "name required") @@ -843,24 +902,26 @@ end --- Applies bar mixin (frame + StatusBar, ticks, text, refresh). --- Used by bar modules (PowerBar, ResourceBar, RuneBar). --- Idempotent — safe to call more than once (no-op after first application). -function BarMixin.AddBarMixin(module, name) - assert(module, "target required") +---@param target table table to apply the mixin to. +---@param name string the module name. must be unique. +function BarMixin.AddBarMixin(target, name) + assert(target, "target required") assert(name, "name required") - if module._mixinApplied then + if target._mixinApplied then return end - local existingMt = getmetatable(module) + local existingMt = getmetatable(target) local existingIndex = existingMt and existingMt.__index - setmetatable(module, { + setmetatable(target, { __index = function(_, k) local v = BarProto[k] if v ~= nil then return v end if type(existingIndex) == "function" then - return existingIndex(module, k) + return existingIndex(target, k) end if type(existingIndex) == "table" then return existingIndex[k] @@ -868,12 +929,12 @@ function BarMixin.AddBarMixin(module, name) end, }) - module.Name = name - module._configKey = C.ConfigKeyForModule(name) - if not module.GetGlobalConfig then - module.GetGlobalConfig = ns.GetGlobalConfig + target.Name = name + target._configKey = C.ConfigKeyForModule(name) + if not target.GetGlobalConfig then + target.GetGlobalConfig = ns.GetGlobalConfig end - module.IsHidden = false - module._mixinApplied = true - module._lastUpdate = GetTime() + target.IsHidden = false + target._mixinApplied = true + target._lastUpdate = GetTime() end diff --git a/BarStyle.lua b/BarStyle.lua index e331414f..bbc64f56 100644 --- a/BarStyle.lua +++ b/BarStyle.lua @@ -2,6 +2,15 @@ -- Author: Argium -- Licensed under the GNU General Public License v3.0 +---@class ECM_BuffBarStatusBar : StatusBar Buff/external bar StatusBar with text child regions. +---@field Name FontString|nil Spell name text. +---@field Duration FontString|nil Duration text. +---@field Pip Texture|nil Pip/spark texture. + +---@class ECM_BuffIconFrame : Frame Icon frame on a buff bar with application count. +---@field Applications FontString|nil Stack count text. +---@field __ecmSquareStyled boolean|nil One-time square-style flag. + local _, ns = ... local FrameUtil = ns.FrameUtil @@ -15,7 +24,7 @@ local FrameUtil = ns.FrameUtil --- Strips circular masks and hides overlay/border to produce a square icon. --- The heavy cleanup (mask removal, pcalls, region iteration) is cached on the --- frame via `__ecmSquareStyled` so it only runs once per icon frame. ----@param iconFrame Frame|nil +---@param iconFrame ECM_BuffIconFrame|nil ---@param iconTexture Texture|nil ---@param iconOverlay Texture|nil ---@param debuffBorder Texture|nil @@ -49,9 +58,9 @@ local function applySquareIconStyle(iconFrame, iconTexture, iconOverlay, debuffB iconFrame.__ecmSquareStyled = true end ----@param frame Frame ----@param bar StatusBar ----@param iconFrame Frame|nil +---@param frame ECM_BuffBarMixin +---@param bar ECM_BuffBarStatusBar +---@param iconFrame ECM_BuffIconFrame|nil ---@param config table|nil ---@param globalConfig table|nil local function styleBarHeight(frame, bar, iconFrame, config, globalConfig) @@ -86,6 +95,7 @@ local function styleBarBackground(frame, barBG, config, globalConfig) -- so Blizzard cannot override our anchors. SetAllPoints does not fire -- SetPoint hooks, so no re-entrancy guard is needed. if not barBG.__ecmBGHooked then + ---@diagnostic disable-next-line: inject-field barBG.__ecmBGHooked = true barBG:SetParent(frame) hooksecurefunc(barBG, "SetPoint", function() @@ -108,7 +118,7 @@ end --- Returns true if the module's _editLocked flag was set by this call. ---@param module table ---@param frame ECM_BuffBarMixin|Frame ----@param bar StatusBar +---@param bar ECM_BuffBarStatusBar ---@param globalConfig table|nil ---@param spellColors ECM_SpellColorStore ---@param retryCount number|nil @@ -224,8 +234,8 @@ local function styleEmptyStatusBarBackground(bar, barBG, config, globalConfig) end end ----@param frame Frame ----@param iconFrame Frame|nil +---@param frame ECM_BuffBarMixin +---@param iconFrame ECM_BuffIconFrame|nil ---@param config table|nil local function styleBarIcon(frame, iconFrame, config) assert(frame ~= nil, "BarStyle.styleBarIcon requires a frame") @@ -254,9 +264,9 @@ local function styleBarIcon(frame, iconFrame, config) end end ----@param frame Frame ----@param bar StatusBar ----@param iconFrame Frame|nil +---@param frame ECM_BuffBarMixin +---@param bar ECM_BuffBarStatusBar +---@param iconFrame ECM_BuffIconFrame|nil ---@param config table|nil local function styleBarAnchors(frame, bar, iconFrame, config) assert(frame ~= nil, "BarStyle.styleBarAnchors requires a frame") diff --git a/Defaults.lua b/Defaults.lua index 55ef63a4..6ae46836 100644 --- a/Defaults.lua +++ b/Defaults.lua @@ -56,10 +56,13 @@ local _, ns = ... ---@class ECM_GlobalConfig Global configuration. ---@field debug boolean Whether debug logging is enabled. ---@field errorLogging boolean Whether targeted error logging is enabled. +---@field debugToChat boolean Whether debug output is also printed to chat. +---@field releasePopupSeenVersion string|nil Last release popup version acknowledged by the player. ---@field hideWhenMounted boolean Whether to hide when mounted or in a vehicle. ---@field hideOutOfCombatInRestAreas boolean Whether to hide out of combat in rest areas. ---@field updateFrequency number Update frequency in seconds. ---@field barHeight number Default bar height. +---@field barWidth number|nil Legacy global free-mode bar width fallback. ---@field barBgColor ECM_Color Default bar background color. ---@field offsetY number Global vertical offset. ---@field moduleSpacing number Vertical gap between chained modules. diff --git a/ECM.lua b/ECM.lua index a60c6e97..dfe9aa31 100644 --- a/ECM.lua +++ b/ECM.lua @@ -23,11 +23,11 @@ local C = ns.Constants local L = ns.L --- Returns the global config section. Standalone accessor for non-module callers. ----@return table|nil +---@return ECM_GlobalConfig function ns.GetGlobalConfig() local db = ns.Addon and ns.Addon.db local profile = db and db.profile - return profile and profile[C.CONFIG_SECTION_GLOBAL] + return profile and profile[C.CONFIG_SECTION_GLOBAL] or {} end --- Returns whether debug mode is enabled. @@ -227,45 +227,6 @@ function ns.Log(module, message, data) end end -local function getSecureVariableStatus(owner, key) - local ok, secure, taint - if key == nil then - ok, secure, taint = pcall(_G.issecurevariable, owner) - else - ok, secure, taint = pcall(_G.issecurevariable, owner, key) - end - if not ok then - return nil - end - return secure, taint -end - -local function logTaint(key, reason, source) - local sourceName = source or "unknown" - ns.ErrorLogOnce("Taint", key, key .. " is tainted by " .. tostring(sourceName) - .. " during " .. tostring(reason or "unknown"), { - reason = reason, - source = sourceName, - }) -end - -function ns._CheckChatTaint(reason) - local secure, taint = getSecureVariableStatus("ChatFrameUtil") - if secure == false then - logTaint("ChatFrameUtil", reason, taint) - end - - secure, taint = getSecureVariableStatus(_G.ChatFrameUtil, "SetLastTellTarget") - if secure == false then - logTaint("ChatFrameUtil.SetLastTellTarget", reason, taint) - end - - secure, taint = getSecureVariableStatus(_G.ChatFrameMixin, "MessageEventHandler") - if secure == false then - logTaint("ChatFrameMixin.MessageEventHandler", reason, taint) - end -end - --- Shows a confirmation popup and reloads the UI on accept. --- ReloadUI is blocked in combat. ---@param text string @@ -378,8 +339,8 @@ function mod:ChatCommand(input) local optionsModule = self:GetModule("Options", true) if optionsModule then + ---@cast optionsModule ECM_OptionsModule optionsModule:OpenOptions() - ns._CheckChatTaint("ChatCommand:options") end return end @@ -478,6 +439,7 @@ function mod:HandleOpenOptionsAfterCombat() local optionsModule = self:GetModule("Options", true) if optionsModule then + ---@cast optionsModule ECM_OptionsModule optionsModule:OpenOptions() end end @@ -556,7 +518,6 @@ function mod:OnEnable() end self:ShowReleasePopup() - ns._CheckChatTaint("OnEnable") end --- Re-evaluates module enable/disable states after a profile change and refreshes layout. diff --git a/EnhancedCooldownManager.code-workspace b/EnhancedCooldownManager.code-workspace index 3d59080e..d18834a6 100644 --- a/EnhancedCooldownManager.code-workspace +++ b/EnhancedCooldownManager.code-workspace @@ -22,7 +22,13 @@ "~\\scoop\\apps\\luarocks\\current\\rocks\\share\\lua\\5.4\\busted", "~\\.vscode\\extensions\\ketho.wow-api-0.22.2\\Annotations\\Core" ], + "Lua.workspace.ignoreDir": [ + "Tests", + "Libs/*/Tests" + ], + "Lua.diagnostics.ignoredFiles": "Disable", "Lua.diagnostics.globals": [ + "DevTool", "DEFAULT", "SlashCmdList", "STANDARD_TEXT_FONT", @@ -63,6 +69,7 @@ "ButtonStateBehaviorMixin", "DefaultTooltipMixin", "MenuConstants", + "CurveConstants", "class", "DRUID_CAT_FORM", "UIParent", @@ -77,7 +84,8 @@ "GameFontNormalSmall", "ChatFontNormal", "GRAY_FONT_COLOR", - "coroutine" + "coroutine", + "canaccesstable" ], "Lua.diagnostics.disable": [ "assign-type-mismatch" diff --git a/Libs/LibSettingsBuilder/Interop/CollectionFrames.lua b/Libs/LibSettingsBuilder/Interop/CollectionFrames.lua index ba61d3c2..9370ed40 100644 --- a/Libs/LibSettingsBuilder/Interop/CollectionFrames.lua +++ b/Libs/LibSettingsBuilder/Interop/CollectionFrames.lua @@ -8,9 +8,6 @@ if not lib or not lib._loadState or not lib._loadState.open then return end -local ADD = _G.ADD -local REMOVE = _G.REMOVE - local internal = lib._internal local foundation = internal.foundation local interop = internal.interop @@ -379,7 +376,7 @@ local function refreshEditorCollectionRow(row, item) row._removeButton:ClearAllPoints() row._removeButton:SetPoint("LEFT", row._swatch, "RIGHT", 8, 0) row._removeButton:SetSize((item.remove and item.remove.width) or 70, 22) - row._removeButton:SetText((item.remove and item.remove.text) or REMOVE or "Remove") + row._removeButton:SetText((item.remove and item.remove.text) or REMOVE) row._removeButton:SetScript("OnClick", function() if item.remove and item.remove.onClick then item.remove.onClick(item, row) @@ -631,7 +628,7 @@ local function ensureModeInputRow(row) row._submitButton = CreateFrame("Button", nil, row, "UIPanelButtonTemplate") row._submitButton:SetPoint("RIGHT", row, "RIGHT", 0, 0) row._submitButton:SetSize(44, 22) - row._submitButton:SetText(ADD or "Add") + row._submitButton:SetText(ADD) row._previewLabel:SetPoint("RIGHT", row._submitButton, "LEFT", -6, 0) @@ -860,7 +857,7 @@ local function refreshModeInputRow(row, trailer, sectionData) activeRow._previewLabel:Hide() end - activeRow._submitButton:SetText(submitText or ADD or "Add") + activeRow._submitButton:SetText(submitText or ADD) setSimpleTooltip(activeRow._submitButton, submitTooltip) activeRow._submitButton:SetScript("OnClick", function() if currentTrailer.onSubmit and canSubmit then diff --git a/Modules/BuffBars.lua b/Modules/BuffBars.lua index 7f7d3e8d..4cef909b 100644 --- a/Modules/BuffBars.lua +++ b/Modules/BuffBars.lua @@ -7,6 +7,10 @@ local BarMixin = ns.BarMixin local FrameUtil = ns.FrameUtil local ChainRightPoint = BarMixin.FrameProto.ChainRightPoint local StyleChildBar = ns.BarStyle.StyleChildBar + +---@class BuffBars : FrameProto Buff bars module with LibEvent embedding. +---@field RegisterEvent fun(self: BuffBars, event: string, callback: function) +---@field UnregisterEvent fun(self: BuffBars, event: string) local BuffBars = ns.Addon:NewModule("BuffBars") ns.Addon.BuffBars = BuffBars @@ -48,13 +52,15 @@ end ---@class ECM_BuffBarMixin : Frame ---@field __ecmHooked boolean ----@field Bar StatusBar ----@field DebuffBorder any ----@field Icon Frame +---@field Bar ECM_BuffBarStatusBar +---@field DebuffBorder Texture|nil +---@field Icon Frame|nil ---@field ignoreInLayout boolean|nil ---@field layoutIndex number|nil ---@field cooldownID number|nil ---@field cooldownInfo { spellID: number|nil }|nil +---@field auraInstanceID number|nil +---@field _ecmColorRetryTimer FunctionContainer|nil local function getChildrenOrdered(viewer, why) local children = collectViewerChildren(viewer, why) @@ -199,7 +205,7 @@ function BuffBars:ShouldRegisterEditMode() end function BuffBars:CreateFrame() - return _G["BuffBarCooldownViewer"] + return BuffBarCooldownViewer end function BuffBars:IsReady() @@ -207,7 +213,7 @@ function BuffBars:IsReady() return false end - local viewer = _G["BuffBarCooldownViewer"] + local viewer = BuffBarCooldownViewer if not viewer then return false end @@ -217,7 +223,7 @@ end --- Override UpdateLayout to position the BuffBarViewer and apply styling to children. function BuffBars:UpdateLayout(why) - local viewer = _G["BuffBarCooldownViewer"] + local viewer = BuffBarCooldownViewer local globalConfig = self:GetGlobalConfig() local cfg = self:GetModuleConfig() local spellColors = getSpellColors() @@ -330,7 +336,7 @@ end --- Bars where all identifying keys are secret/nil are skipped. ---@return ECM_SpellColorKey[] function BuffBars:GetActiveSpellData() - local viewer = _G["BuffBarCooldownViewer"] + local viewer = BuffBarCooldownViewer if not viewer then return {} end @@ -359,7 +365,7 @@ end --- Hooks the BuffBarCooldownViewer for automatic updates. function BuffBars:HookViewer() - local viewer = _G["BuffBarCooldownViewer"] + local viewer = BuffBarCooldownViewer if not viewer or self._viewerHooked then return end @@ -405,10 +411,10 @@ function BuffBars:OnEnable() self:EnsureFrame() ns.Runtime.RegisterFrame(self) - self:RegisterEvent("ZONE_CHANGED_NEW_AREA", function(_, ...) self:OnZoneChanged(...) end) - self:RegisterEvent("ZONE_CHANGED", function(_, ...) self:OnZoneChanged(...) end) - self:RegisterEvent("ZONE_CHANGED_INDOORS", function(_, ...) self:OnZoneChanged(...) end) - self:RegisterEvent("PLAYER_ENTERING_WORLD", function(_, ...) self:OnZoneChanged(...) end) + self:RegisterEvent("ZONE_CHANGED_NEW_AREA", function(_, ...) self:OnZoneChanged() end) + self:RegisterEvent("ZONE_CHANGED", function(_, ...) self:OnZoneChanged() end) + self:RegisterEvent("ZONE_CHANGED_INDOORS", function(_, ...) self:OnZoneChanged() end) + self:RegisterEvent("PLAYER_ENTERING_WORLD", function(_, ...) self:OnZoneChanged() end) -- Blizzard updates each child's auraInstanceID synchronously inside its own -- UNIT_AURA handler but does not always re-fire SetPoint/OnShow/OnHide on -- bars whose layout order is unchanged (e.g. a configured slot whose aura diff --git a/Modules/ExternalBars.lua b/Modules/ExternalBars.lua index ea8f1319..13144142 100644 --- a/Modules/ExternalBars.lua +++ b/Modules/ExternalBars.lua @@ -11,12 +11,8 @@ local C = ns.Constants local ExternalBars = ns.Addon:NewModule("ExternalBars") ns.Addon.ExternalBars = ExternalBars -local PLAYER_UNIT = "player" -local SPELL_COLOR_SCOPE = C.SCOPE_EXTERNALBARS -local canAccessTable = _G.canaccesstable - local function getSpellColors() - return ns.SpellColors.Get(SPELL_COLOR_SCOPE) + return ns.SpellColors.Get(C.SCOPE_EXTERNALBARS) end ---@class ECM_ExternalAuraState Cached external aura data keyed by Blizzard's aura array position. @@ -104,7 +100,7 @@ end ---@param auraInfo table|nil ---@return table local function getAuraInfoErrorData(reason, viewer, auraInfo) - local instanceName, instanceType, difficultyID, difficultyName, maxPlayers, _, _, instanceID = _G.GetInstanceInfo() + local instanceName, instanceType, difficultyID, difficultyName, maxPlayers, _, _, instanceID = GetInstanceInfo() local canAccessAuraInfo = nil if type(auraInfo) == "table" then canAccessAuraInfo = canAccessTable(auraInfo) @@ -137,7 +133,7 @@ local function buildAuraState(index, info, auraState, includeDiagnostics) local auraInstanceID = info.auraInstanceID local auraName = nil local spellID = nil - local auraData = C_UnitAuras.GetAuraDataByAuraInstanceID(PLAYER_UNIT, auraInstanceID) + local auraData = C_UnitAuras.GetAuraDataByAuraInstanceID("player", auraInstanceID) local accessibleAuraData = canAccessTable(auraData) and auraData or nil if accessibleAuraData then local auraDataName = accessibleAuraData.name @@ -153,7 +149,7 @@ local function buildAuraState(index, info, auraState, includeDiagnostics) local duration = info.duration local expirationTime = info.expirationTime - local durationObject = C_UnitAuras.GetAuraDuration(PLAYER_UNIT, auraInstanceID) + local durationObject = C_UnitAuras.GetAuraDuration("player", auraInstanceID) local canUpdateDurationBar = durationObject ~= nil auraState.index = index @@ -363,7 +359,7 @@ end ---@param activeAuraCount number ---@return table function ExternalBars:_GetLayoutRequestDiagnostics(reason, viewer, auraInfo, activeAuraCount) - local instanceName, instanceType, difficultyID, difficultyName, maxPlayers, _, _, instanceID = _G.GetInstanceInfo() + local instanceName, instanceType, difficultyID, difficultyName, maxPlayers, _, _, instanceID = GetInstanceInfo() local moduleConfig = self:GetModuleConfig() local globalConfig = self:GetGlobalConfig() local diagnostics = self:_GetDiagnostics(viewer, auraInfo, reason) diff --git a/Modules/ExtraIcons.lua b/Modules/ExtraIcons.lua index 607aa7a7..7ead758f 100644 --- a/Modules/ExtraIcons.lua +++ b/Modules/ExtraIcons.lua @@ -13,7 +13,6 @@ local RACIAL_SPELL_ALIASES = ns.Constants.RACIAL_SPELL_ALIASES local DEFAULT_SIZE = ns.Constants.DEFAULT_EXTRA_ICON_SIZE local MAIN_BORDER_SCALE = ns.Constants.EXTRA_ICON_MAIN_BORDER_SCALE local UTILITY_BORDER_SCALE = ns.Constants.EXTRA_ICON_UTILITY_BORDER_SCALE -local canAccessTable = _G.canaccesstable local MAIN_VIEWER_KEY = "EssentialCooldownViewer" local UTILITY_VIEWER_KEY = "UtilityCooldownViewer" @@ -173,6 +172,7 @@ local function createIcon(parent, size, borderScale) icon.Cooldown:SetDrawSwipe(true) icon.Cooldown:SetHideCountdownNumbers(false) icon.Cooldown:SetSwipeTexture([[Interface\HUD\UI-HUD-CoolDownManager-Icon-Swipe]], 0, 0, 0, 0.2) + ---@diagnostic disable-next-line: missing-parameter, param-type-mismatch icon.Cooldown:SetEdgeTexture([[Interface\Cooldown\UI-HUD-ActionBar-SecondaryCooldown]]) icon.Border = icon:CreateTexture(nil, "OVERLAY") @@ -485,7 +485,7 @@ function ExtraIcons:UpdateLayout(why) local moduleConfig = self:GetModuleConfig() local isEditing = self._isEditModeActive if isEditing == nil then - local mgr = _G.EditModeManagerFrame + local mgr = EditModeManagerFrame isEditing = mgr and mgr:IsShown() or false end @@ -588,7 +588,7 @@ end function ExtraIcons:HookEditMode() if self._editModeHooked then return end - local mgr = _G.EditModeManagerFrame + local mgr = EditModeManagerFrame self._editModeHooked = true self._isEditModeActive = mgr:IsShown() diff --git a/Modules/RuneBar.lua b/Modules/RuneBar.lua index 77e7f822..1d5cea0c 100644 --- a/Modules/RuneBar.lua +++ b/Modules/RuneBar.lua @@ -2,7 +2,27 @@ -- Author: Argium -- Licensed under the GNU General Public License v3.0 +---@diagnostic disable: need-check-nil, redundant-parameter + +---@class RuneBarFrame : BarInnerFrame Rune bar inner frame with fragmented sub-bars and animation state. +---@field FragmentedBars StatusBar[]|nil Array of StatusBar fragments, one per rune. +---@field _maxResources number|nil Maximum rune count. +---@field _readySet table|nil Per-frame ready state lookup (reused). +---@field _cdLookup table|nil Per-frame cooldown lookup (reused). +---@field _lastReadySet table|nil Snapshot of previous ready states. +---@field _lastBarWidth number|nil Cached bar width for change detection. +---@field _lastBarHeight number|nil Cached bar height for change detection. +---@field _displayOrder table|nil Reused display order buffer. +---@field _cdSortBuf table|nil Reused cooldown sort buffer. +---@field _lastValueUpdate number|nil Timestamp of last value update. + local _, ns = ... + +---@class RuneBar : BarProto Rune bar module for Death Knight rune display. +---@field InnerFrame RuneBarFrame|nil +---@field EnsureTicks fun(self: RuneBar, count: number, parentFrame: Frame, poolKey: string|nil) Ensures rune tick textures exist. +---@field LayoutResourceTicks fun(self: RuneBar, maxResources: number, color: ECM_Color|table|nil, tickWidth: number|nil, poolKey: string|nil) Positions rune divider ticks. +---@field RegisterEvent fun(self: RuneBar, event: string, callback: function) local RuneBar = ns.Addon:NewModule("RuneBar") local C = ns.Constants local FrameUtil = ns.FrameUtil @@ -65,7 +85,7 @@ local function applyRuneFragmentVisual(frag, isReady, cd, color) end --- Creates or returns fragmented sub-bars for runes. ----@param bar Frame +---@param bar RuneBarFrame ---@param maxResources number ---@param tex string Texture path local function ensureFragmentedBars(bar, maxResources, tex) @@ -100,7 +120,7 @@ end --- Updates fragmented rune display (individual bars per rune). --- Only repositions bars when rune ready states change to avoid flickering. ----@param bar Frame +---@param bar RuneBarFrame ---@param maxRunes number ---@param moduleConfig table ---@param globalConfig table @@ -201,7 +221,7 @@ end --- Triggers a full layout refresh when rune ready/CD states change. --- Self-stops the animation ticker when all runes are ready. ---@param self RuneBar ----@param frame Frame +---@param frame RuneBarFrame local function updateRuneValues(self, frame) local frags = frame.FragmentedBars if not frags then @@ -283,7 +303,7 @@ function RuneBar:Refresh(why, force) local cfg = self:GetModuleConfig() local globalConfig = self:GetGlobalConfig() - local frame = self.InnerFrame + local frame = assert(self.InnerFrame, "InnerFrame required") local maxRunes = C.RUNEBAR_MAX_RUNES if frame._maxResources ~= maxRunes then @@ -321,7 +341,7 @@ function RuneBar:_StartAnimationTicker() end self._valueTicker = C_Timer.NewTicker(C.DEFAULT_REFRESH_FREQUENCY, function() if self:IsEnabled() and self.InnerFrame and self.InnerFrame:IsShown() then - updateRuneValues(self, self.InnerFrame) + updateRuneValues(self, self.InnerFrame --[[@as RuneBarFrame]]) end end) end diff --git a/Runtime.lua b/Runtime.lua index 60eb89fa..e56eca7e 100644 --- a/Runtime.lua +++ b/Runtime.lua @@ -40,12 +40,6 @@ local LAYOUT_EVENTS = { CVAR_UPDATE = { delay = 0, arg1 = "cooldownViewerEnabled" }, } -local CHAT_TAINT_ZONE_EVENTS = { - ZONE_CHANGED_NEW_AREA = true, - ZONE_CHANGED = true, - ZONE_CHANGED_INDOORS = true, -} - local _modules = {} local _globallyHidden = false local _desiredAlpha = 1 @@ -395,7 +389,7 @@ local function hookCooldownViewerSettings() return end - local settingsFrame = _G.CooldownViewerSettings + local settingsFrame = CooldownViewerSettings if not settingsFrame then return end @@ -608,7 +602,7 @@ end -------------------------------------------------------------------------------- --- Handles a layout-triggering event, updating combat state and scheduling layout. ----@param addon table The AceAddon instance (self from the event handler) +---@param _addon table The AceAddon instance (self from the event handler) ---@param event string ---@param arg1 any local function handleLayoutEvent(_addon, event, arg1) @@ -622,10 +616,6 @@ local function handleLayoutEvent(_addon, event, arg1) return end - if CHAT_TAINT_ZONE_EVENTS[event] then - ns._CheckChatTaint(event) - end - if config.combatChange then _inCombat = (event == "PLAYER_REGEN_DISABLED") end diff --git a/SpellColors.lua b/SpellColors.lua index cd8e28a8..a538311d 100644 --- a/SpellColors.lua +++ b/SpellColors.lua @@ -7,16 +7,44 @@ -- by name, spell ID, cooldown ID, or texture file ID. Includes persistence and -- public APIs for per-spell color customization. +---@alias ECM_SpellColorKeyField "spellName"|"spellID"|"cooldownID"|"textureFileID" + +---@class ECM_SpellColorKey Spell color identity assembled from one or more supported key fields. +---@field keyType ECM_SpellColorKeyField Primary key field type. +---@field primaryKey string|number Primary key value. +---@field spellName string|nil Spell name key. +---@field spellID number|nil Spell ID key. +---@field cooldownID number|nil Cooldown ID key. +---@field textureFileID number|nil Texture file ID key. +---@field Matches fun(self: ECM_SpellColorKey, other: ECM_SpellColorKey|table|nil): boolean Gets whether another key identifies the same logical spell color entry. +---@field Merge fun(self: ECM_SpellColorKey, other: ECM_SpellColorKey|table|nil): ECM_SpellColorKey|nil Merges another matching key into this key. +---@field ToString fun(self: ECM_SpellColorKey): string Gets a debug string for the key. +---@field ToArray fun(self: ECM_SpellColorKey): table Gets key values in tier order. + +---@class ECM_SpellColorKeyType : ECM_SpellColorKey Runtime metatable for spell color keys. + +---@class ECM_SpellColorStore Spell color storage facade for one options scope. +---@field _scope string Store scope name. +---@field _configAccessor (fun(): table|nil)|nil Optional config accessor override. +---@field _discoveredKeys ECM_SpellColorKey[] Runtime-discovered keys from visible bars. +---@field _SetConfigAccessor fun(self: ECM_SpellColorStore, accessor: fun(): table|nil) Sets the config accessor used by this store. +---@field GetColorByKey fun(self: ECM_SpellColorStore, key: ECM_SpellColorKey|table|nil): ECM_Color|nil Gets the configured color for a key. +---@field GetAllColorEntries fun(self: ECM_SpellColorStore): table[] Gets deduplicated color entries for the current class/spec. +---@field SetColorByKey fun(self: ECM_SpellColorStore, key: ECM_SpellColorKey|table|nil, color: ECM_Color) Sets the configured color for a key. +---@field SetDefaultColor fun(self: ECM_SpellColorStore, color: ECM_Color) Sets the default color for this store scope. +---@field ResetColorByKey fun(self: ECM_SpellColorStore, key: ECM_SpellColorKey|table|nil): boolean, boolean, boolean, boolean Resets the configured color for a key. +---@field ReconcileAllKeys fun(self: ECM_SpellColorStore, keys: ECM_SpellColorKey[]|nil): number Reconciles and repairs persisted key metadata. +---@field RemoveEntriesByKeys fun(self: ECM_SpellColorStore, keys: (ECM_SpellColorKey|table)[]): ECM_SpellColorKey[] Removes persisted and discovered entries matching keys. +---@field DiscoverBar fun(self: ECM_SpellColorStore, frame: ECM_BuffBarMixin) Registers a visible bar in the discovered key cache. +---@field ClearDiscoveredKeys fun(self: ECM_SpellColorStore) Clears the runtime discovered key cache. +---@field ClearCurrentSpecColors fun(self: ECM_SpellColorStore): number Clears persisted colors for the current class/spec. +---@field GetColorForBar fun(self: ECM_SpellColorStore, frame: ECM_BuffBarMixin|Frame): table|nil Gets the configured color for a bar frame. +---@field GetDefaultColor fun(self: ECM_SpellColorStore): table Gets the default color for this store scope. + local _, ns = ... local C = ns.Constants ----@class ECM_SpellColorStore ----@field _scope string ----@field _configAccessor (fun(): table|nil)|nil ----@field _discoveredKeys ECM_SpellColorKey[] ----@field _SetConfigAccessor fun(self: ECM_SpellColorStore, accessor: fun(): table|nil) - local SpellColors = {} ns.SpellColors = SpellColors local SpellColorStore = {} @@ -61,18 +89,9 @@ end -- SpellColorKeyType class --------------------------------------------------------------------------- ----@class ECM_SpellColorKeyType : ECM_SpellColorKey local SpellColorKeyType = {} SpellColorKeyType.__index = SpellColorKeyType ----@class ECM_SpellColorKey ----@field keyType "spellName"|"spellID"|"cooldownID"|"textureFileID" ----@field primaryKey string|number ----@field spellName string|nil ----@field spellID number|nil ----@field cooldownID number|nil ----@field textureFileID number|nil - ---@param spellName string|nil ---@param spellID number|nil ---@param cooldownID number|nil @@ -222,6 +241,7 @@ function SpellColorKeyType:ToString() ) end +---@return table values Key values in tier order. function SpellColorKeyType:ToArray() return { self.spellName, self.spellID, self.cooldownID, self.textureFileID } end @@ -236,6 +256,9 @@ SpellColors.NormalizeKey = normalizeKey --- Returns true when two keys identify the same logical spell-color entry. --- Both operands are normalized first so callers can pass either a --- `ECM_SpellColorKey` or a raw payload accepted by `NormalizeKey`. +---@param left ECM_SpellColorKey|table|nil +---@param right ECM_SpellColorKey|table|nil +---@return boolean function SpellColors.KeysMatch(left, right) return keysMatch(normalizeKey(left), normalizeKey(right)) end @@ -243,11 +266,15 @@ end --- Merges identifiers from two matching keys into a single normalized key. --- Both operands are normalized first so callers can pass either a --- `ECM_SpellColorKey` or a raw payload accepted by `NormalizeKey`. +---@param base ECM_SpellColorKey|table|nil +---@param other ECM_SpellColorKey|table|nil +---@return ECM_SpellColorKey|nil function SpellColors.MergeKeys(base, other) return mergeKeys(normalizeKey(base), normalizeKey(other)) end -- WoW uses Lua 5.1 (global `unpack`), busted tests use Lua 5.3+ (`table.unpack`). +---@diagnostic disable-next-line: undefined-field local unpack = _G.unpack or table.unpack --------------------------------------------------------------------------- @@ -353,7 +380,7 @@ local function normalizeEntryMetadata(entry, normalized) end local changed = scrubLegacyColorMetadata(entry.value) - local desired = buildEntryMeta(normalized) + local desired = assert(buildEntryMeta(normalized), "entry metadata required") local current = type(entry.meta) == "table" and entry.meta or nil if not current @@ -488,6 +515,7 @@ function SpellColors.Get(scope) end -- Not used by production code; retained for tests that need to swap config sources after construction. +---@param accessor fun(): table|nil function SpellColorStore:_SetConfigAccessor(accessor) self._configAccessor = accessor end @@ -866,7 +894,7 @@ function SpellColorStore:GetColorForBar(frame) if not (frame and frame.__ecmHooked) then ns.Log("SpellColors", "GetColorForBar - invalid bar frame", { frame = frame, - nameExists = frame and type(frame.Name) == "table" and type(frame.Name.GetText) == "function", + nameExists = frame and frame.Bar and type(frame.Bar.Name) == "table" and type(frame.Bar.Name.GetText) == "function", iconExists = frame and type(frame.Icon) == "table" and type(frame.Icon.GetRegions) == "function", }) return nil diff --git a/Tests/ECM_Runtime_spec.lua b/Tests/ECM_Runtime_spec.lua index 3061d1e2..79051b77 100644 --- a/Tests/ECM_Runtime_spec.lua +++ b/Tests/ECM_Runtime_spec.lua @@ -256,7 +256,7 @@ describe("ECM.Runtime layout system", function() printedMessages[#printedMessages + 1] = message end _G.StaticPopupDialogs = {} - _G.StaticPopup_Show = function() end + _G.StaticPopup_Show = function(...) end _G.YES = "Yes" _G.NO = "No" _G.DevTool = nil @@ -694,25 +694,6 @@ describe("ECM.Runtime layout system", function() assert.same({ "ZONE_CHANGED" }, reasons) end) - it("checks chat taint immediately on zone change events", function() - local taintReasons = {} - local libEvent = LibStub("LibEvent-1.0") - ns._CheckChatTaint = function(reason) - taintReasons[#taintReasons + 1] = reason - end - - ns.Runtime.Enable(fakeAddon) - - local addonFrame = assert(libEvent.embeds[fakeAddon].frame) - local handler = assert(addonFrame.__scripts and addonFrame.__scripts["OnEvent"]) - - handler(addonFrame, "ZONE_CHANGED") - handler(addonFrame, "ZONE_CHANGED_INDOORS") - handler(addonFrame, "PLAYER_ENTERING_WORLD") - - assert.same({ "ZONE_CHANGED", "ZONE_CHANGED_INDOORS" }, taintReasons) - end) - it("ScheduleLayoutUpdate with a shorter delay supersedes a pending longer-delay timer", function() local mod = makeRegisteredModule() local reasons = {} diff --git a/Tests/ECM_spec.lua b/Tests/ECM_spec.lua index ae4ff154..191e637f 100644 --- a/Tests/ECM_spec.lua +++ b/Tests/ECM_spec.lua @@ -141,7 +141,7 @@ describe("ECM layout system", function() printedMessages[#printedMessages + 1] = message end _G.StaticPopupDialogs = {} - _G.StaticPopup_Show = function() end + _G.StaticPopup_Show = function(...) end _G.YES = "Yes" _G.NO = "No" _G.DevTool = nil @@ -459,28 +459,6 @@ describe("ECM layout system", function() assert.is_truthy(printedMessages[2]:find("errorKey=other-key", 1, true)) end) - it("reports tainted chat reply helpers once during enable", function() - _G._testDB.profile.global.errorLogging = true - _G.ChatFrameUtil = { SetLastTellTarget = function() end } - _G.issecurevariable = function(owner, key) - if owner == _G.ChatFrameUtil and key == "SetLastTellTarget" then - return false, "EnhancedCooldownManager" - end - return true - end - - fakeAddon:OnEnable() - fakeAddon:OnEnable() - - local taintMessages = 0 - for _, message in ipairs(printedMessages) do - if message:find("ChatFrameUtil.SetLastTellTarget is tainted", 1, true) then - assert.is_truthy(message:find("by EnhancedCooldownManager during OnEnable", 1, true)) - taintMessages = taintMessages + 1 - end - end - assert.are.equal(1, taintMessages) - end) end) describe("release popup", function() diff --git a/UI/Options.lua b/UI/Options.lua index 6065925d..eeb93b27 100644 --- a/UI/Options.lua +++ b/UI/Options.lua @@ -2,6 +2,9 @@ -- Author: Argium -- Licensed under the GNU General Public License v3.0 +---@class ECM_OptionsModule : AceModule Options module that opens and tracks ECM settings categories. +---@field OpenOptions fun(self: ECM_OptionsModule) Opens the options UI to the last tracked category. + local _, ns = ... local L = ns.L diff --git a/UI/SpellColorsPage.lua b/UI/SpellColorsPage.lua index c871237e..0b5fb574 100644 --- a/UI/SpellColorsPage.lua +++ b/UI/SpellColorsPage.lua @@ -85,7 +85,7 @@ end ---@return boolean local function isSpellColorsReconcileRestricted() - return _G.UnitAffectingCombat("player") or InCombatLockdown() or IsInInstance() + return UnitAffectingCombat("player") or InCombatLockdown() or IsInInstance() end ---@param rows { key: ECM_SpellColorKey }[]|nil From 34f3313db2c4b2669fa8b5b5685c4f740cdb1a42 Mon Sep 17 00:00:00 2001 From: Argi <15852038+argium@users.noreply.github.com> Date: Tue, 19 May 2026 17:19:42 +1000 Subject: [PATCH 11/15] Fix luacheck warnings --- .luacheckrc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.luacheckrc b/.luacheckrc index 211e78a5..984d971c 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -51,7 +51,10 @@ read_globals = {'C_PlayerInfo','DEFAULT_CHAT_FRAME', 'MenuUtil', 'GameTooltip', -- Externals "AddonCompartmentFrame", + "BuffBarCooldownViewer", "C_AddOns", "C_CVar", "C_EditMode", "C_Item", "C_PartyInfo", "C_PvP", "C_Spell", "C_SpellBook", "C_Timer", "C_TradeSkillUI", "C_UnitAuras", + "canAccessTable", + "CANCEL", "CreateAtlasMarkup", "ColorPickerFrame", @@ -90,7 +93,10 @@ read_globals = {'C_PlayerInfo','DEFAULT_CHAT_FRAME', 'MenuUtil', 'GameTooltip', "StaticPopup_Show", "UIParent", "UnitCanAssist", "UnitCanAttack", "UnitClass", "UnitExists", "UnitIsPlayer", "UnitName", "UnitInVehicle", "UnitOnTaxi", "UnitIsDead", "UnitName", "UnitRace", - "UnitPower", "UnitPowerMax", "UnitPowerPercent", "UnitPowerType", + "UnitPower", "UnitPowerMax", "UnitPowerPercent", "UnitPowerType", "UnitAffectingCombat", "YES", + "ADD", + "REMOVE", "MinimalSliderWithSteppersMixin", + "GetInstanceInfo" } From 6a62e6f39d59b1027fa8cff35220cb46d76ceeba Mon Sep 17 00:00:00 2001 From: Argi <15852038+argium@users.noreply.github.com> Date: Tue, 19 May 2026 17:29:23 +1000 Subject: [PATCH 12/15] fix canAccessTable typo --- .codex | 0 .gitignore | 3 +-- Modules/ExternalBars.lua | 12 ++++++------ Modules/ExtraIcons.lua | 6 +++--- 4 files changed, 10 insertions(+), 11 deletions(-) delete mode 100644 .codex diff --git a/.codex b/.codex deleted file mode 100644 index e69de29b..00000000 diff --git a/.gitignore b/.gitignore index 36ea80aa..1d762acb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,11 +3,10 @@ TODO.md PLAN-*.md plan-*.md CHAT-*.json -/Annotations -/wt luacov.report.html luacov.report.out luacov.stats.out +test-results # External libraries fetched via .pkgmeta externals /Libs/AceAddon-3.0/ diff --git a/Modules/ExternalBars.lua b/Modules/ExternalBars.lua index 13144142..193ff432 100644 --- a/Modules/ExternalBars.lua +++ b/Modules/ExternalBars.lua @@ -53,7 +53,7 @@ local function logAccessibleCountError(tbl, logKey, reason, operation, err) operation = operation, error = tostring(err), tableType = type(tbl), - tableAccessible = type(tbl) == "table" and canAccessTable(tbl) or nil, + tableAccessible = type(tbl) == "table" and canaccesstable(tbl) or nil, inCombatLockdown = InCombatLockdown(), }) end @@ -64,7 +64,7 @@ end ---@param reason string|nil ---@return number|nil, number|nil local function countAccessibleEntries(tbl, logKey, reason) - if type(tbl) ~= "table" or not canAccessTable(tbl) then + if type(tbl) ~= "table" or not canaccesstable(tbl) then return nil, nil end @@ -103,7 +103,7 @@ local function getAuraInfoErrorData(reason, viewer, auraInfo) local instanceName, instanceType, difficultyID, difficultyName, maxPlayers, _, _, instanceID = GetInstanceInfo() local canAccessAuraInfo = nil if type(auraInfo) == "table" then - canAccessAuraInfo = canAccessTable(auraInfo) + canAccessAuraInfo = canaccesstable(auraInfo) end return { @@ -134,7 +134,7 @@ local function buildAuraState(index, info, auraState, includeDiagnostics) local auraName = nil local spellID = nil local auraData = C_UnitAuras.GetAuraDataByAuraInstanceID("player", auraInstanceID) - local accessibleAuraData = canAccessTable(auraData) and auraData or nil + local accessibleAuraData = canaccesstable(auraData) and auraData or nil if accessibleAuraData then local auraDataName = accessibleAuraData.name if not issecretvalue(auraDataName) and auraDataName ~= nil and auraDataName ~= "" then @@ -822,13 +822,13 @@ function ExternalBars:OnExternalAurasUpdated(reason) ns.ErrorLogOnce("ExternalBars", logKey, message, data) end - if type(auraInfo) == "table" and not canAccessTable(auraInfo) then + if type(auraInfo) == "table" and not canaccesstable(auraInfo) then abortAuraSync(self, "AuraInfoInaccessible", "Blizzard external aura info is inaccessible during " .. tostring(reason or "unknown")) elseif type(auraInfo) == "table" then local ok, err = pcall(function() for index, info in ipairs(auraInfo) do - if type(info) ~= "table" or not canAccessTable(info) then + if type(info) ~= "table" or not canaccesstable(info) then error("inaccessible external aura info entry at index " .. tostring(index)) end diff --git a/Modules/ExtraIcons.lua b/Modules/ExtraIcons.lua index 7ead758f..cf33f40b 100644 --- a/Modules/ExtraIcons.lua +++ b/Modules/ExtraIcons.lua @@ -265,7 +265,7 @@ local function updateIconCountText(icon, globalConfig, config) end local function getItemFramesCount(itemFrames) - if type(itemFrames) ~= "table" or not canAccessTable(itemFrames) then + if type(itemFrames) ~= "table" or not canaccesstable(itemFrames) then return nil end @@ -281,7 +281,7 @@ end local function getViewerDiagnostics(blizzFrame, viewerConfig, why, itemFrames) local itemFramesAccessible = nil if type(itemFrames) == "table" then - itemFramesAccessible = canAccessTable(itemFrames) + itemFramesAccessible = canaccesstable(itemFrames) end return { @@ -318,7 +318,7 @@ local function getAccessibleItemFrames(blizzFrame, viewerConfig, why) return nil end - if not canAccessTable(itemFrames) then + if not canaccesstable(itemFrames) then ns.ErrorLogOnce("ExtraIcons", "InaccessibleItemFrames:" .. viewerConfig.key, "Cooldown viewer item frames are inaccessible for " .. viewerConfig.key .. " during " .. tostring(why or "unknown"), getViewerDiagnostics(blizzFrame, viewerConfig, why, itemFrames)) From 245f9adf176f91508c63626ed8b382603a688cf8 Mon Sep 17 00:00:00 2001 From: Argi <15852038+argium@users.noreply.github.com> Date: Tue, 19 May 2026 17:35:58 +1000 Subject: [PATCH 13/15] Update canaccesstable in luacheckrc, too. --- .luacheckrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.luacheckrc b/.luacheckrc index 984d971c..19264619 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -53,7 +53,7 @@ read_globals = {'C_PlayerInfo','DEFAULT_CHAT_FRAME', 'MenuUtil', 'GameTooltip', "AddonCompartmentFrame", "BuffBarCooldownViewer", "C_AddOns", "C_CVar", "C_EditMode", "C_Item", "C_PartyInfo", "C_PvP", "C_Spell", "C_SpellBook", "C_Timer", "C_TradeSkillUI", "C_UnitAuras", - "canAccessTable", + "canaccesstable", "CANCEL", "CreateAtlasMarkup", From a65d395c44494feeda15d635b1d67f5b6ae982a8 Mon Sep 17 00:00:00 2001 From: Argi <15852038+argium@users.noreply.github.com> Date: Tue, 19 May 2026 18:03:23 +1000 Subject: [PATCH 14/15] Fix transparent. --- Tests/UI/ResourceBarOptions_spec.lua | 26 +++++++++++++------------- UI/ResourceBarOptions.lua | 12 ++++++------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Tests/UI/ResourceBarOptions_spec.lua b/Tests/UI/ResourceBarOptions_spec.lua index f80f64e7..070fcae9 100644 --- a/Tests/UI/ResourceBarOptions_spec.lua +++ b/Tests/UI/ResourceBarOptions_spec.lua @@ -8,7 +8,7 @@ local TestHelpers = describe("ResourceBarOptions getters/setters/defaults", function() local originalGlobals local profile, defaults, SB, ns, settings, capturedPage - local emptyIcon = "|TInterface\\Buttons\\WHITE8X8:14:14:0:0:8:8:0:0:0:0:0:0:0:0|t" + local emptyIcon = "|TInterface\\Common\\spacer:14:14|t" local function getPageRow(path) for _, row in ipairs(capturedPage.rows) do @@ -162,15 +162,15 @@ describe("ResourceBarOptions getters/setters/defaults", function() defsByKey[def.key] = def.name end - assert.are.equal(emptyIcon .. "|A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DH"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_VENGEANCE_SOULS]) - assert.are.equal(emptyIcon .. "|A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DEVOURER"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_DEVOURER_NORMAL]) - assert.are.equal(emptyIcon .. "|A:classicon-mage:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MAGE .. ns.L["RESOURCE_ICICLES"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_ICICLES]) - assert.are.equal(emptyIcon .. "|A:classicon-monk:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MONK .. ns.L["RESOURCE_CHI"] .. "|r", defsByKey[Enum.PowerType.Chi]) - assert.are.equal("|A:classicon-druid:14:14|a|A:classicon-rogue:14:14|a |cff" .. ns.Constants.CLASS_COLORS.ROGUE .. ns.L["RESOURCE_COMBO_POINTS"] .. "|r", defsByKey[Enum.PowerType.ComboPoints]) - assert.are.equal(emptyIcon .. "|A:classicon-evoker:14:14|a |cff" .. ns.Constants.CLASS_COLORS.EVOKER .. ns.L["RESOURCE_ESSENCE"] .. "|r", defsByKey[Enum.PowerType.Essence]) - assert.are.equal(emptyIcon .. "|A:classicon-paladin:14:14|a |cff" .. ns.Constants.CLASS_COLORS.PALADIN .. ns.L["RESOURCE_HOLY_POWER"] .. "|r", defsByKey[Enum.PowerType.HolyPower]) - assert.are.equal(emptyIcon .. "|A:classicon-shaman:14:14|a |cff" .. ns.Constants.CLASS_COLORS.SHAMAN .. ns.L["RESOURCE_MAELSTROM_WEAPON"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_MAELSTROM_WEAPON]) - assert.are.equal(emptyIcon .. "|A:classicon-warlock:14:14|a |cff" .. ns.Constants.CLASS_COLORS.WARLOCK .. ns.L["RESOURCE_SOUL_SHARDS"] .. "|r", defsByKey[Enum.PowerType.SoulShards]) + assert.are.equal(emptyIcon .. " |A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DH"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_VENGEANCE_SOULS]) + assert.are.equal(emptyIcon .. " |A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DEVOURER"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_DEVOURER_NORMAL]) + assert.are.equal(emptyIcon .. " |A:classicon-mage:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MAGE .. ns.L["RESOURCE_ICICLES"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_ICICLES]) + assert.are.equal(emptyIcon .. " |A:classicon-monk:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MONK .. ns.L["RESOURCE_CHI"] .. "|r", defsByKey[Enum.PowerType.Chi]) + assert.are.equal("|A:classicon-druid:14:14|a |A:classicon-rogue:14:14|a |cff" .. ns.Constants.CLASS_COLORS.ROGUE .. ns.L["RESOURCE_COMBO_POINTS"] .. "|r", defsByKey[Enum.PowerType.ComboPoints]) + assert.are.equal(emptyIcon .. " |A:classicon-evoker:14:14|a |cff" .. ns.Constants.CLASS_COLORS.EVOKER .. ns.L["RESOURCE_ESSENCE"] .. "|r", defsByKey[Enum.PowerType.Essence]) + assert.are.equal(emptyIcon .. " |A:classicon-paladin:14:14|a |cff" .. ns.Constants.CLASS_COLORS.PALADIN .. ns.L["RESOURCE_HOLY_POWER"] .. "|r", defsByKey[Enum.PowerType.HolyPower]) + assert.are.equal(emptyIcon .. " |A:classicon-shaman:14:14|a |cff" .. ns.Constants.CLASS_COLORS.SHAMAN .. ns.L["RESOURCE_MAELSTROM_WEAPON"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_MAELSTROM_WEAPON]) + assert.are.equal(emptyIcon .. " |A:classicon-warlock:14:14|a |cff" .. ns.Constants.CLASS_COLORS.WARLOCK .. ns.L["RESOURCE_SOUL_SHARDS"] .. "|r", defsByKey[Enum.PowerType.SoulShards]) assert.is_nil(defsByKey[Enum.PowerType.ArcaneCharges]) end) end) @@ -215,15 +215,15 @@ describe("ResourceBarOptions getters/setters/defaults", function() end assert.are.equal( - emptyIcon .. "|A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DEVOURER"] .. "|r", + emptyIcon .. " |A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DEVOURER"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_DEVOURER_NORMAL] ) assert.are.equal( - emptyIcon .. "|A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_VOID_FRAGMENTS_DEVOURER"] .. "|r", + emptyIcon .. " |A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_VOID_FRAGMENTS_DEVOURER"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_DEVOURER_META] ) assert.are.equal( - emptyIcon .. "|A:classicon-mage:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MAGE .. ns.L["RESOURCE_ICICLES"] .. "|r", + emptyIcon .. " |A:classicon-mage:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MAGE .. ns.L["RESOURCE_ICICLES"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_ICICLES] ) end) diff --git a/UI/ResourceBarOptions.lua b/UI/ResourceBarOptions.lua index 2be09d04..cbadaa4b 100644 --- a/UI/ResourceBarOptions.lua +++ b/UI/ResourceBarOptions.lua @@ -10,29 +10,29 @@ local RESOURCE_ICON_SIZE = 14 local RESOURCE_ICON_SLOTS = 2 -- Transparent texture slot for right-aligning rows with fewer class icons. local EMPTY_RESOURCE_ICON = - "|TInterface\\Buttons\\WHITE8X8:" + "|TInterface\\Common\\spacer:" .. RESOURCE_ICON_SIZE .. ":" .. RESOURCE_ICON_SIZE - .. ":0:0:8:8:0:0:0:0:0:0:0:0|t" + .. "|t" local function createResourceColorName(colorClassName, label, iconClasses) local color = (C.CLASS_COLORS and C.CLASS_COLORS[colorClassName]) or COLOR_WHITE_HEX - local prefix = "" + local icons = {} local iconCount = math.min(#iconClasses, RESOURCE_ICON_SLOTS) local padding = math.max(0, RESOURCE_ICON_SLOTS - iconCount) local startIndex = math.max(1, #iconClasses - RESOURCE_ICON_SLOTS + 1) for _ = 1, padding do - prefix = prefix .. EMPTY_RESOURCE_ICON + icons[#icons + 1] = EMPTY_RESOURCE_ICON end for i = startIndex, #iconClasses do - prefix = prefix .. "|A:classicon-" .. string.lower(iconClasses[i]) .. ":" .. RESOURCE_ICON_SIZE .. ":" .. RESOURCE_ICON_SIZE .. "|a" + icons[#icons + 1] = "|A:classicon-" .. string.lower(iconClasses[i]) .. ":" .. RESOURCE_ICON_SIZE .. ":" .. RESOURCE_ICON_SIZE .. "|a" end - return prefix .. " |cff" .. color .. label .. "|r" + return table.concat(icons, " ") .. " |cff" .. color .. label .. "|r" end local RESOURCE_COLOR_DEFS = { From bcbcd5350059a385529ea33a27a49315ad9ccac8 Mon Sep 17 00:00:00 2001 From: Argi <15852038+argium@users.noreply.github.com> Date: Tue, 19 May 2026 18:37:25 +1000 Subject: [PATCH 15/15] cleanup --- Tests/UI/ResourceBarOptions_spec.lua | 61 ++++++++++++++++++++++------ UI/ResourceBarOptions.lua | 4 +- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/Tests/UI/ResourceBarOptions_spec.lua b/Tests/UI/ResourceBarOptions_spec.lua index 070fcae9..e28c2895 100644 --- a/Tests/UI/ResourceBarOptions_spec.lua +++ b/Tests/UI/ResourceBarOptions_spec.lua @@ -10,6 +10,14 @@ describe("ResourceBarOptions getters/setters/defaults", function() local profile, defaults, SB, ns, settings, capturedPage local emptyIcon = "|TInterface\\Common\\spacer:14:14|t" + local function classIcon(className) + return "|A:classicon-" .. className .. ":14:14|a" + end + + local function resourceLabel(prefix, colorClass, labelKey) + return prefix .. " |cff" .. ns.Constants.CLASS_COLORS[colorClass] .. ns.L[labelKey] .. "|r" + end + local function getPageRow(path) for _, row in ipairs(capturedPage.rows) do if row.path == path then @@ -158,19 +166,47 @@ describe("ResourceBarOptions getters/setters/defaults", function() end) it("prefixes each resource label with its class icon and color", function() local defsByKey = {} + local demonHunterIcon = emptyIcon .. " " .. classIcon("demonhunter") for _, def in ipairs(assert(getPageRow("colors")).defs) do defsByKey[def.key] = def.name end - assert.are.equal(emptyIcon .. " |A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DH"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_VENGEANCE_SOULS]) - assert.are.equal(emptyIcon .. " |A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DEVOURER"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_DEVOURER_NORMAL]) - assert.are.equal(emptyIcon .. " |A:classicon-mage:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MAGE .. ns.L["RESOURCE_ICICLES"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_ICICLES]) - assert.are.equal(emptyIcon .. " |A:classicon-monk:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MONK .. ns.L["RESOURCE_CHI"] .. "|r", defsByKey[Enum.PowerType.Chi]) - assert.are.equal("|A:classicon-druid:14:14|a |A:classicon-rogue:14:14|a |cff" .. ns.Constants.CLASS_COLORS.ROGUE .. ns.L["RESOURCE_COMBO_POINTS"] .. "|r", defsByKey[Enum.PowerType.ComboPoints]) - assert.are.equal(emptyIcon .. " |A:classicon-evoker:14:14|a |cff" .. ns.Constants.CLASS_COLORS.EVOKER .. ns.L["RESOURCE_ESSENCE"] .. "|r", defsByKey[Enum.PowerType.Essence]) - assert.are.equal(emptyIcon .. " |A:classicon-paladin:14:14|a |cff" .. ns.Constants.CLASS_COLORS.PALADIN .. ns.L["RESOURCE_HOLY_POWER"] .. "|r", defsByKey[Enum.PowerType.HolyPower]) - assert.are.equal(emptyIcon .. " |A:classicon-shaman:14:14|a |cff" .. ns.Constants.CLASS_COLORS.SHAMAN .. ns.L["RESOURCE_MAELSTROM_WEAPON"] .. "|r", defsByKey[ns.Constants.RESOURCEBAR_TYPE_MAELSTROM_WEAPON]) - assert.are.equal(emptyIcon .. " |A:classicon-warlock:14:14|a |cff" .. ns.Constants.CLASS_COLORS.WARLOCK .. ns.L["RESOURCE_SOUL_SHARDS"] .. "|r", defsByKey[Enum.PowerType.SoulShards]) + assert.are.equal( + resourceLabel(demonHunterIcon, "DEMONHUNTER", "RESOURCE_SOUL_FRAGMENTS_DH"), + defsByKey[ns.Constants.RESOURCEBAR_TYPE_VENGEANCE_SOULS] + ) + assert.are.equal( + resourceLabel(demonHunterIcon, "DEMONHUNTER", "RESOURCE_SOUL_FRAGMENTS_DEVOURER"), + defsByKey[ns.Constants.RESOURCEBAR_TYPE_DEVOURER_NORMAL] + ) + assert.are.equal( + resourceLabel(emptyIcon .. " " .. classIcon("mage"), "MAGE", "RESOURCE_ICICLES"), + defsByKey[ns.Constants.RESOURCEBAR_TYPE_ICICLES] + ) + assert.are.equal( + resourceLabel(emptyIcon .. " " .. classIcon("monk"), "MONK", "RESOURCE_CHI"), + defsByKey[Enum.PowerType.Chi] + ) + assert.are.equal( + resourceLabel(classIcon("druid") .. " " .. classIcon("rogue"), "ROGUE", "RESOURCE_COMBO_POINTS"), + defsByKey[Enum.PowerType.ComboPoints] + ) + assert.are.equal( + resourceLabel(emptyIcon .. " " .. classIcon("evoker"), "EVOKER", "RESOURCE_ESSENCE"), + defsByKey[Enum.PowerType.Essence] + ) + assert.are.equal( + resourceLabel(emptyIcon .. " " .. classIcon("paladin"), "PALADIN", "RESOURCE_HOLY_POWER"), + defsByKey[Enum.PowerType.HolyPower] + ) + assert.are.equal( + resourceLabel(emptyIcon .. " " .. classIcon("shaman"), "SHAMAN", "RESOURCE_MAELSTROM_WEAPON"), + defsByKey[ns.Constants.RESOURCEBAR_TYPE_MAELSTROM_WEAPON] + ) + assert.are.equal( + resourceLabel(emptyIcon .. " " .. classIcon("warlock"), "WARLOCK", "RESOURCE_SOUL_SHARDS"), + defsByKey[Enum.PowerType.SoulShards] + ) assert.is_nil(defsByKey[Enum.PowerType.ArcaneCharges]) end) end) @@ -210,20 +246,21 @@ describe("ResourceBarOptions getters/setters/defaults", function() end) it("reuses the icon-prefixed names for capped resource rows", function() local defsByKey = {} + local demonHunterIcon = emptyIcon .. " " .. classIcon("demonhunter") for _, def in ipairs(assert(getPageRow("maxColors")).defs) do defsByKey[def.key] = def.name end assert.are.equal( - emptyIcon .. " |A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_SOUL_FRAGMENTS_DEVOURER"] .. "|r", + resourceLabel(demonHunterIcon, "DEMONHUNTER", "RESOURCE_SOUL_FRAGMENTS_DEVOURER"), defsByKey[ns.Constants.RESOURCEBAR_TYPE_DEVOURER_NORMAL] ) assert.are.equal( - emptyIcon .. " |A:classicon-demonhunter:14:14|a |cff" .. ns.Constants.CLASS_COLORS.DEMONHUNTER .. ns.L["RESOURCE_VOID_FRAGMENTS_DEVOURER"] .. "|r", + resourceLabel(demonHunterIcon, "DEMONHUNTER", "RESOURCE_VOID_FRAGMENTS_DEVOURER"), defsByKey[ns.Constants.RESOURCEBAR_TYPE_DEVOURER_META] ) assert.are.equal( - emptyIcon .. " |A:classicon-mage:14:14|a |cff" .. ns.Constants.CLASS_COLORS.MAGE .. ns.L["RESOURCE_ICICLES"] .. "|r", + resourceLabel(emptyIcon .. " " .. classIcon("mage"), "MAGE", "RESOURCE_ICICLES"), defsByKey[ns.Constants.RESOURCEBAR_TYPE_ICICLES] ) end) diff --git a/UI/ResourceBarOptions.lua b/UI/ResourceBarOptions.lua index cbadaa4b..9ca546c3 100644 --- a/UI/ResourceBarOptions.lua +++ b/UI/ResourceBarOptions.lua @@ -16,7 +16,6 @@ local EMPTY_RESOURCE_ICON = .. RESOURCE_ICON_SIZE .. "|t" - local function createResourceColorName(colorClassName, label, iconClasses) local color = (C.CLASS_COLORS and C.CLASS_COLORS[colorClassName]) or COLOR_WHITE_HEX local icons = {} @@ -29,7 +28,8 @@ local function createResourceColorName(colorClassName, label, iconClasses) end for i = startIndex, #iconClasses do - icons[#icons + 1] = "|A:classicon-" .. string.lower(iconClasses[i]) .. ":" .. RESOURCE_ICON_SIZE .. ":" .. RESOURCE_ICON_SIZE .. "|a" + local iconClass = string.lower(iconClasses[i]) + icons[#icons + 1] = "|A:classicon-" .. iconClass .. ":" .. RESOURCE_ICON_SIZE .. ":" .. RESOURCE_ICON_SIZE .. "|a" end return table.concat(icons, " ") .. " |cff" .. color .. label .. "|r"