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"