From 8564cfeac1aa84583900c1a08fecbe7865988f59 Mon Sep 17 00:00:00 2001
From: Argi <15852038+argium@users.noreply.github.com>
Date: Wed, 20 May 2026 13:24:21 +1000
Subject: [PATCH 1/7] Add extensibility API for LSB that allows
LSMSettingsWidgets to register custom types for declarative controls. Fixed
XML registration conflict when the lib is embedded in more than one addon.
---
EnhancedCooldownManager.toc | 2 +-
.../LibLSMSettingsWidgets.lua | 291 ++++++++-------
.../LibLSMSettingsWidgets.xml | 20 --
Libs/LibLSMSettingsWidgets/README.md | 14 +-
.../Tests/LibLSMSettingsWidgets_spec.lua | 332 +++++++-----------
Libs/LibSettingsBuilder/Builders/Rows.lua | 15 +-
Libs/LibSettingsBuilder/Core.lua | 16 +
Libs/LibSettingsBuilder/Interop/ListRows.lua | 5 +-
Libs/LibSettingsBuilder/Registry/Runtime.lua | 43 ++-
Libs/LibSettingsBuilder/Schema/Rows.lua | 4 -
.../Tests/Architecture_spec.lua | 1 +
.../Tests/Controls_spec.lua | 8 +-
Libs/LibSettingsBuilder/docs/API_REFERENCE.md | 30 +-
Libs/LibSettingsBuilder/docs/INSTALLATION.md | 10 +-
.../docs/MIGRATION_GUIDE.md | 4 +-
Libs/LibSettingsBuilder/docs/QUICK_START.md | 2 +-
.../docs/TROUBLESHOOTING.md | 2 +-
Tests/TestHelpers.lua | 11 +-
UI/GeneralOptions.lua | 7 +-
UI/OptionUtil.lua | 5 -
20 files changed, 385 insertions(+), 437 deletions(-)
delete mode 100644 Libs/LibLSMSettingsWidgets/LibLSMSettingsWidgets.xml
diff --git a/EnhancedCooldownManager.toc b/EnhancedCooldownManager.toc
index f1957b88..ae52e737 100644
--- a/EnhancedCooldownManager.toc
+++ b/EnhancedCooldownManager.toc
@@ -22,7 +22,7 @@ Libs\LibDeflate\lib.xml
Libs\LibEditMode\embed.xml
Libs\LibSharedMedia-3.0\lib.xml
Libs\LibSettingsBuilder\embed.xml
-Libs\LibLSMSettingsWidgets\LibLSMSettingsWidgets.xml
+Libs\LibLSMSettingsWidgets\LibLSMSettingsWidgets.lua
Constants.lua
Locales\en.lua
diff --git a/Libs/LibLSMSettingsWidgets/LibLSMSettingsWidgets.lua b/Libs/LibLSMSettingsWidgets/LibLSMSettingsWidgets.lua
index 8607e8a9..deb68091 100644
--- a/Libs/LibLSMSettingsWidgets/LibLSMSettingsWidgets.lua
+++ b/Libs/LibLSMSettingsWidgets/LibLSMSettingsWidgets.lua
@@ -1,19 +1,16 @@
--- LibLSMSettingsWidgets: LibSharedMedia picker widgets for the WoW Settings API.
--- Provides font and texture picker templates with live previews.
+-- Enhanced Cooldown Manager addon for World of Warcraft
+-- Author: Argium
+-- Licensed under the GNU General Public License v3.0
-local MAJOR, MINOR = "LibLSMSettingsWidgets-1.0", 2
+-- LibLSMSettingsWidgets: LibSharedMedia picker widgets for LibSettingsBuilder rows.
+
+local MAJOR, MINOR = "LibLSMSettingsWidgets-1.0", 3
local lib = LibStub:NewLibrary(MAJOR, MINOR)
if not lib then return end
local LSM = LibStub("LibSharedMedia-3.0")
-lib.FONT_PICKER_TEMPLATE = "LibLSMSettingsWidgets_FontPickerTemplate"
-lib.TEXTURE_PICKER_TEMPLATE = "LibLSMSettingsWidgets_TexturePickerTemplate"
-
---------------------------------------------------------------------------------
--- Media value helpers
---------------------------------------------------------------------------------
-
+local FONT_PREVIEW_TEXT = "AaBbCcDd 1234"
local mediaCache = {}
local function invalidateMediaCache()
@@ -42,179 +39,207 @@ local function getSortedMediaNames(mediaType, fallback)
return sorted
end
-function lib.GetFontValues()
- local names = getSortedMediaNames("font", "Expressway")
+local function getMediaValues(mediaType, fallback)
local values = {}
- for _, name in ipairs(names) do
+ for _, name in ipairs(getSortedMediaNames(mediaType, fallback)) do
values[name] = name
end
return values
end
+function lib.GetFontValues()
+ return getMediaValues("font", "Expressway")
+end
+
function lib.GetStatusbarValues()
- local names = getSortedMediaNames("statusbar", "Blizzard")
- local values = {}
- for _, name in ipairs(names) do
- values[name] = name
+ return getMediaValues("statusbar", "Blizzard")
+end
+
+local function createDropDown(frame)
+ local host = CreateFrame("Frame", nil, frame, "SettingsDropdownWithButtonsTemplate")
+ if host.DecrementButton then
+ host.DecrementButton:Hide()
end
- return values
+ if host.IncrementButton then
+ host.IncrementButton:Hide()
+ end
+
+ local dropdown = host.Dropdown or host
+ dropdown:SetWidth(200)
+ return host, dropdown
end
---------------------------------------------------------------------------------
--- Shared picker helpers
---------------------------------------------------------------------------------
+local function ensurePicker(frame, kind)
+ local picker = frame._lsmwPicker
+ if not picker then
+ local host, dropdown = createDropDown(frame)
+ picker = { host = host, dropdown = dropdown }
+ frame._lsmwPicker = picker
+ end
+
+ if picker.kind ~= kind then
+ if picker.preview then
+ picker.preview:Hide()
+ end
+
+ if kind == "font" then
+ picker.preview = picker.fontPreview or frame:CreateFontString(nil, "OVERLAY")
+ picker.fontPreview = picker.preview
+ picker.preview:SetFontObject(GameFontHighlight)
+ picker.preview:SetJustifyH("LEFT")
+ else
+ picker.preview = picker.texturePreview or frame:CreateTexture(nil, "ARTWORK")
+ picker.texturePreview = picker.preview
+ picker.preview:SetSize(120, 16)
+ picker.preview:SetVertexColor(0.4, 0.6, 0.9, 1)
+ end
+ picker.kind = kind
+ end
-local function setPickerEnabled(self, enabled)
- 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)
+ return picker
end
-local function createDropDown(self)
- local host = CreateFrame("Frame", nil, self, "SettingsDropdownWithButtonsTemplate")
- host.DecrementButton:Hide()
- host.IncrementButton:Hide()
+local function setPickerEnabled(frame, enabled)
+ local picker = frame._lsmwPicker
+ if not picker then return end
- local dropdown = host.Dropdown or host
- dropdown:SetPoint("LEFT", self.Text, "RIGHT", 10, 0)
- dropdown:SetWidth(200)
- self.DropDownHost = host
- return dropdown
+ picker.dropdown:SetEnabled(enabled)
+ picker.dropdown:EnableMouse(enabled)
+ picker.host:SetEnabled(enabled)
+ picker.host:EnableMouse(enabled)
+ picker.preview[enabled and "Show" or "Hide"](picker.preview)
end
-local function initPicker(self, initializer)
- SettingsListElementMixin.Init(self, initializer)
+local function anchorPicker(frame, picker)
+ if frame.Text then
+ frame.Text:ClearAllPoints()
+ frame.Text:SetPoint("LEFT", frame, "LEFT", 37, 0)
+ frame.Text:SetPoint("RIGHT", frame, "CENTER", -85, 0)
+ frame.Text:SetJustifyV("MIDDLE")
+ frame.Text:Show()
+ end
- local data = initializer:GetData() or {}
- self.setting = data.setting or (initializer.GetSetting and initializer:GetSetting()) or nil
+ picker.host:ClearAllPoints()
+ if frame.Text then
+ picker.host:SetPoint("LEFT", frame.Text, "RIGHT", 10, 0)
+ else
+ picker.host:SetPoint("LEFT", frame, "CENTER", -80, 0)
+ end
+ picker.host:Show()
- if data.name and self.Text then
- self.Text:SetText(data.name)
+ picker.preview:ClearAllPoints()
+ picker.preview:SetPoint("LEFT", picker.dropdown, "RIGHT", 10, 0)
+ picker.preview:SetPoint("RIGHT", frame, "RIGHT", -20, 0)
+ picker.preview:Show()
+end
+
+local function updateDropdownText(picker, setting, mediaType)
+ local currentName = setting and setting.GetValue and setting:GetValue() or nil
+
+ if picker.dropdown.OverrideText then
+ picker.dropdown:OverrideText(currentName or "")
end
- self:SetupDropdown()
- self:UpdatePreview()
+ if not currentName then
+ return nil, nil
+ end
- local frame = self
- local oldSetEnabled = initializer.SetEnabled
- initializer.SetEnabled = function(init, enabled)
- if oldSetEnabled then
- oldSetEnabled(init, enabled)
- end
- frame:SetEnabled(enabled)
+ return currentName, LSM:Fetch(mediaType, currentName)
+end
+
+local function updateFontPreview(picker, setting)
+ local _, fontPath = updateDropdownText(picker, setting, "font")
+ if fontPath then
+ picker.preview:SetFont(fontPath, 14, "")
+ picker.preview:SetText(FONT_PREVIEW_TEXT)
+ else
+ picker.preview:SetFontObject(GameFontHighlight)
+ picker.preview:SetText("")
end
end
-local function setupMediaDropdown(self, mediaType, fallback)
- local setting = self.setting
- local picker = self
+local function updateTexturePreview(picker, setting)
+ local _, texturePath = updateDropdownText(picker, setting, "statusbar")
+ picker.preview:SetTexture(texturePath)
+end
+local function setupMediaDropdown(frame, picker, setting, mediaType, fallback, updatePreview)
if not setting then return end
- self.DropDown:SetupMenu(function(_, rootDescription)
+ picker.dropdown:SetupMenu(function(_, rootDescription)
rootDescription:SetScrollMode(200)
- local sorted = getSortedMediaNames(mediaType, fallback)
-
- for _, name in ipairs(sorted) do
+ for _, name in ipairs(getSortedMediaNames(mediaType, fallback)) do
rootDescription:CreateRadio(name,
function() return setting:GetValue() == name end,
function()
setting:SetValue(name)
- picker:UpdatePreview()
+ updatePreview(picker, setting)
+ if frame.RefreshDropdownText then
+ frame:RefreshDropdownText()
+ end
end)
end
end)
end
---- Updates the dropdown label and fetches the media path for the current value.
----@return string|nil currentName
----@return string|nil mediaPath
-local function updateDropdownText(self, mediaType)
- if not self.setting then return nil, nil end
-
- local currentName = self.setting:GetValue()
- local mediaPath = LSM:Fetch(mediaType, currentName)
+local function applyPickerRow(frame, data, initializer, kind, mediaType, fallback, updatePreview)
+ local picker = ensurePicker(frame, kind)
+ local setting = data.setting
- if self.DropDown and self.DropDown.OverrideText then
- self.DropDown:OverrideText(currentName or "")
+ frame._lsmwPickerSetting = setting
+ if frame.Text then
+ frame.Text:SetText(data.name or "")
end
- return currentName, mediaPath
-end
-
---------------------------------------------------------------------------------
--- Font Picker Mixin
---------------------------------------------------------------------------------
+ anchorPicker(frame, picker)
+ setupMediaDropdown(frame, picker, setting, mediaType, fallback, updatePreview)
+ updatePreview(picker, setting)
-LibLSMSettingsWidgets_FontPickerMixin = {}
-
-function LibLSMSettingsWidgets_FontPickerMixin:OnLoad()
- SettingsListElementMixin.OnLoad(self)
-
- self.DropDown = createDropDown(self)
- self.Preview = self:CreateFontString(nil, "OVERLAY")
- self.Preview:SetFontObject(GameFontHighlight)
- self.Preview:SetPoint("LEFT", self.DropDown, "RIGHT", 10, 0)
- self.Preview:SetPoint("RIGHT", self, "RIGHT", -20, 0)
- self.Preview:SetJustifyH("LEFT")
- self.Preview:SetText("AaBbCcDd 1234")
+ initializer.SetEnabled = function(_, enabled)
+ setPickerEnabled(frame, enabled)
+ end
end
-LibLSMSettingsWidgets_FontPickerMixin.CreateDropDown = createDropDown
-LibLSMSettingsWidgets_FontPickerMixin.Init = initPicker
-LibLSMSettingsWidgets_FontPickerMixin.SetEnabled = setPickerEnabled
-
-function LibLSMSettingsWidgets_FontPickerMixin:SetupDropdown()
- setupMediaDropdown(self, "font", "Expressway")
+function lib.ApplyFontPickerRow(frame, data, initializer)
+ applyPickerRow(frame, data, initializer, "font", "font", "Expressway", updateFontPreview)
end
-function LibLSMSettingsWidgets_FontPickerMixin:UpdatePreview()
- local _, fontPath = updateDropdownText(self, "font")
- if self.Preview then
- if fontPath then
- self.Preview:SetFont(fontPath, 14, "")
- self.Preview:SetText("AaBbCcDd 1234")
- else
- self.Preview:SetFontObject(GameFontHighlight)
- self.Preview:SetText("")
- end
- end
+function lib.ApplyTexturePickerRow(frame, data, initializer)
+ applyPickerRow(frame, data, initializer, "texture", "statusbar", "Blizzard", updateTexturePreview)
end
---------------------------------------------------------------------------------
--- Texture Picker Mixin
---------------------------------------------------------------------------------
+function lib.ResetPickerRow(frame)
+ local picker = frame._lsmwPicker
+ if not picker then return end
-LibLSMSettingsWidgets_TexturePickerMixin = {}
-
-function LibLSMSettingsWidgets_TexturePickerMixin:OnLoad()
- SettingsListElementMixin.OnLoad(self)
-
- self.DropDown = createDropDown(self)
- self.Preview = self:CreateTexture(nil, "ARTWORK")
- self.Preview:SetPoint("LEFT", self.DropDown, "RIGHT", 10, 0)
- self.Preview:SetSize(120, 16)
- self.Preview:SetVertexColor(0.4, 0.6, 0.9, 1)
+ picker.host:Hide()
+ if picker.fontPreview then
+ picker.fontPreview:Hide()
+ end
+ if picker.texturePreview then
+ picker.texturePreview:Hide()
+ end
+ frame._lsmwPickerSetting = nil
end
-LibLSMSettingsWidgets_TexturePickerMixin.CreateDropDown = createDropDown
-LibLSMSettingsWidgets_TexturePickerMixin.Init = initPicker
-LibLSMSettingsWidgets_TexturePickerMixin.SetEnabled = setPickerEnabled
-
-function LibLSMSettingsWidgets_TexturePickerMixin:SetupDropdown()
- setupMediaDropdown(self, "statusbar", "Blizzard")
+function lib.Register(settingsBuilder)
+ if not settingsBuilder or not settingsBuilder.RegisterRowType then return end
+
+ settingsBuilder:RegisterRowType("font", {
+ varType = "string",
+ defaultValue = "",
+ extent = 26,
+ applyFrame = lib.ApplyFontPickerRow,
+ resetFrame = lib.ResetPickerRow,
+ })
+ settingsBuilder:RegisterRowType("texture", {
+ varType = "string",
+ defaultValue = "",
+ extent = 26,
+ applyFrame = lib.ApplyTexturePickerRow,
+ resetFrame = lib.ResetPickerRow,
+ })
end
-function LibLSMSettingsWidgets_TexturePickerMixin:UpdatePreview()
- local _, texturePath = updateDropdownText(self, "statusbar")
- if self.Preview then
- if texturePath then
- self.Preview:SetTexture(texturePath)
- else
- self.Preview:SetTexture(nil)
- end
- end
-end
+lib.Register(LibStub("LibSettingsBuilder-1.0", true))
diff --git a/Libs/LibLSMSettingsWidgets/LibLSMSettingsWidgets.xml b/Libs/LibLSMSettingsWidgets/LibLSMSettingsWidgets.xml
deleted file mode 100644
index ebf294c2..00000000
--- a/Libs/LibLSMSettingsWidgets/LibLSMSettingsWidgets.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Libs/LibLSMSettingsWidgets/README.md b/Libs/LibLSMSettingsWidgets/README.md
index 3ca7a858..bbfce884 100644
--- a/Libs/LibLSMSettingsWidgets/README.md
+++ b/Libs/LibLSMSettingsWidgets/README.md
@@ -1,12 +1,12 @@
# LibLSMSettingsWidgets-1.0
-LibSharedMedia picker widgets for the WoW Settings API. Provides font and texture picker templates with live previews.
+LibSharedMedia picker widgets for LibSettingsBuilder. Provides pure-Lua `font` and `texture` declarative row types with live previews.
Distributed via [LibStub](https://www.wowace.com/projects/libstub).
## Features
-- Drop-in font and texture picker templates for `Settings.CreateControlTextContainer`.
+- Drop-in `type = "font"` and `type = "texture"` rows for LibSettingsBuilder declaratives.
- Live preview of the selected font or texture in the dropdown.
- Cached and sorted media name lists, auto-invalidated when new media is registered.
- Graceful fallback when media fetch fails.
@@ -15,12 +15,16 @@ Distributed via [LibStub](https://www.wowace.com/projects/libstub).
```lua
local LSW = LibStub("LibLSMSettingsWidgets-1.0")
+LSW.Register(LibStub("LibSettingsBuilder-1.0"))
--- Use the template name when creating a Settings dropdown:
--- LSW.FONT_PICKER_TEMPLATE
--- LSW.TEXTURE_PICKER_TEMPLATE
+rows = {
+ { type = "font", path = "font", name = "Font" },
+ { type = "texture", path = "texture", name = "Texture" },
+}
```
+When loaded after LibSettingsBuilder, the library registers these row types automatically. Calling `Register` is safe and idempotent for explicit setup.
+
## Testing
Tests live in `Tests/` and use [busted](https://olivinelabs.com/busted/). Run from the **host addon root** (the directory containing `.busted`):
diff --git a/Libs/LibLSMSettingsWidgets/Tests/LibLSMSettingsWidgets_spec.lua b/Libs/LibLSMSettingsWidgets/Tests/LibLSMSettingsWidgets_spec.lua
index aca3b46c..f77bea35 100644
--- a/Libs/LibLSMSettingsWidgets/Tests/LibLSMSettingsWidgets_spec.lua
+++ b/Libs/LibLSMSettingsWidgets/Tests/LibLSMSettingsWidgets_spec.lua
@@ -11,12 +11,58 @@ describe("LibLSMSettingsWidgets", function()
local originalGlobals
local lsm
+ local function makeSetting(value)
+ return {
+ GetValue = function()
+ return value
+ end,
+ SetValue = function(_, nextValue)
+ value = nextValue
+ end,
+ }
+ end
+
+ local function makeDropdownHost()
+ local host = TestHelpers.makeFrame()
+ host.DecrementButton = TestHelpers.makeFrame()
+ host.IncrementButton = TestHelpers.makeFrame()
+ host.Dropdown = TestHelpers.makeFrame()
+ host.Dropdown.SetupMenu = function(self, callback)
+ self.__menuCallback = callback
+ end
+ host.Dropdown.OverrideText = function(self, text)
+ self.__overrideText = text
+ end
+ return host
+ end
+
+ local function makePickerFrame()
+ local frame = TestHelpers.makeFrame()
+ frame.Text = frame:CreateFontString()
+ return frame
+ end
+
+ local function buildMenu(dropdown)
+ local menu = { entries = {} }
+ function menu:SetScrollMode(height)
+ self.height = height
+ end
+ function menu:CreateRadio(label, isChecked, onClick)
+ self.entries[#self.entries + 1] = {
+ label = label,
+ isChecked = isChecked,
+ onClick = onClick,
+ }
+ end
+ dropdown.__menuCallback(nil, menu)
+ return menu
+ end
+
setup(function()
originalGlobals = TestHelpers.CaptureGlobals({
+ "CreateFrame",
+ "GameFontHighlight",
"LibStub",
- "SettingsListElementMixin",
- "LibLSMSettingsWidgets_FontPickerMixin",
- "LibLSMSettingsWidgets_TexturePickerMixin",
"wipe",
})
end)
@@ -27,15 +73,15 @@ describe("LibLSMSettingsWidgets", function()
before_each(function()
TestHelpers.SetupLibStub()
+ _G.GameFontHighlight = {}
_G.wipe = function(tbl)
for key in pairs(tbl) do
tbl[key] = nil
end
end
- _G.SettingsListElementMixin = {
- OnLoad = function() end,
- Init = function() end,
- }
+ _G.CreateFrame = function()
+ return makeDropdownHost()
+ end
lsm = LibStub:NewLibrary("LibSharedMedia-3.0", 1)
if lsm then
@@ -79,214 +125,92 @@ describe("LibLSMSettingsWidgets", function()
assert.are.same({ Beta = "Beta" }, lib.GetFontValues())
end)
- it("font dropdown radio selections update the setting and preview", function()
- local selected = "Alpha"
- local previewUpdates = 0
- local radioEntries = {}
+ it("applies a font picker row and updates the setting from menu selection", function()
lsm.List = function()
return { "Beta", "Alpha" }
end
+ lsm.Fetch = function(_, mediaType, name)
+ return mediaType == "font" and "/fonts/" .. name .. ".ttf" or nil
+ end
- local picker = {
- setting = {
- GetValue = function()
- return selected
- end,
- SetValue = function(_, value)
- selected = value
- end,
- },
- DropDown = {
- SetupMenu = function(_, callback)
- callback(nil, {
- SetScrollMode = function(self, height)
- self.height = height
- end,
- CreateRadio = function(_, label, isChecked, onClick)
- radioEntries[#radioEntries + 1] = {
- label = label,
- isChecked = isChecked,
- onClick = onClick,
- }
- end,
- })
- end,
- },
- UpdatePreview = function()
- previewUpdates = previewUpdates + 1
- end,
- }
-
- LibLSMSettingsWidgets_FontPickerMixin.SetupDropdown(picker)
-
- assert.are.equal("Alpha", radioEntries[1].label)
- assert.is_true(radioEntries[1].isChecked())
- radioEntries[2].onClick()
- assert.are.equal("Beta", selected)
- assert.are.equal(1, previewUpdates)
- end)
-
- it("SetupDropdown is a no-op until Init provides a setting", function()
- local setupMenuCalled = false
- local picker = {
- DropDown = {
- SetupMenu = function()
- setupMenuCalled = true
- end,
- },
- }
-
- LibLSMSettingsWidgets_TexturePickerMixin.SetupDropdown(picker)
-
- assert.is_false(setupMenuCalled)
+ local lib = LibStub("LibLSMSettingsWidgets-1.0")
+ local setting = makeSetting("Alpha")
+ local frame = makePickerFrame()
+ local initializer = {}
+
+ lib.ApplyFontPickerRow(frame, { name = "Font", setting = setting }, initializer)
+
+ local picker = frame._lsmwPicker
+ assert.are.equal("Font", frame.Text:GetText())
+ assert.are.equal("Alpha", picker.dropdown.__overrideText)
+ assert.are.same({ "/fonts/Alpha.ttf", 14, "" }, { picker.preview:GetFont() })
+ assert.are.equal("AaBbCcDd 1234", picker.preview:GetText())
+
+ local menu = buildMenu(picker.dropdown)
+ assert.are.equal(200, menu.height)
+ assert.are.equal("Alpha", menu.entries[1].label)
+ assert.is_true(menu.entries[1].isChecked())
+ menu.entries[2].onClick()
+ assert.are.equal("Beta", setting:GetValue())
+ assert.are.equal("Beta", picker.dropdown.__overrideText)
end)
- it("updates font and texture previews from LibSharedMedia fetch results", function()
- local fetched = {
- font = "/fonts/alpha.ttf",
- statusbar = "/textures/bar",
- }
- lsm.Fetch = function(_, mediaType)
- return fetched[mediaType]
+ it("rebinds a recycled row from font to texture and reset hides picker children", function()
+ lsm.Fetch = function(_, mediaType, name)
+ if mediaType == "font" then
+ return "/fonts/" .. name .. ".ttf"
+ end
+ return "/textures/" .. name
end
- local fontPicker = {
- setting = { GetValue = function() return "Alpha" end },
- DropDown = {
- OverrideText = function(self, text)
- self.text = text
- end,
- },
- Preview = {
- SetFont = function(self, path, size, flags)
- self.font = { path, size, flags }
- end,
- SetText = function(self, text)
- self.text = text
- end,
- },
- }
- LibLSMSettingsWidgets_FontPickerMixin.UpdatePreview(fontPicker)
- assert.are.equal("Alpha", fontPicker.DropDown.text)
- assert.are.same({ "/fonts/alpha.ttf", 14, "" }, fontPicker.Preview.font)
- assert.are.equal("AaBbCcDd 1234", fontPicker.Preview.text)
-
- local texturePicker = {
- setting = { GetValue = function() return "Bar" end },
- DropDown = {
- OverrideText = function(self, text)
- self.text = text
- end,
- },
- Preview = {
- SetTexture = function(self, texture)
- self.texture = texture
- end,
- },
- }
- LibLSMSettingsWidgets_TexturePickerMixin.UpdatePreview(texturePicker)
- assert.are.equal("Bar", texturePicker.DropDown.text)
- assert.are.equal("/textures/bar", texturePicker.Preview.texture)
+ local lib = LibStub("LibLSMSettingsWidgets-1.0")
+ local frame = makePickerFrame()
+ local initializer = {}
+
+ lib.ApplyFontPickerRow(frame, { name = "Font", setting = makeSetting("Alpha") }, initializer)
+ local fontPreview = frame._lsmwPicker.fontPreview
+ assert.is_true(fontPreview:IsShown())
+
+ lib.ApplyTexturePickerRow(frame, { name = "Texture", setting = makeSetting("Smooth") }, initializer)
+ local picker = frame._lsmwPicker
+ assert.is_false(fontPreview:IsShown())
+ assert.are.equal("/textures/Smooth", picker.texturePreview:GetTexture())
+ assert.are.equal("Texture", frame.Text:GetText())
+
+ lib.ResetPickerRow(frame)
+ assert.is_false(picker.host:IsShown())
+ assert.is_false(picker.texturePreview:IsShown())
end)
- local pickerCases = {
- { name = "FontPicker", global = "LibLSMSettingsWidgets_FontPickerMixin" },
- { name = "TexturePicker", global = "LibLSMSettingsWidgets_TexturePickerMixin" },
- }
-
- for _, case in ipairs(pickerCases) do
- it(case.name .. " SetEnabled disables dropdown and hides preview", function()
- local dropdownEnabled, dropdownMouse
- local hostEnabled, hostMouse
- local previewShown = true
-
- local picker = {
- DropDown = {
- SetEnabled = function(_, enabled) dropdownEnabled = enabled end,
- EnableMouse = function(_, enabled) dropdownMouse = enabled end,
- },
- DropDownHost = {
- SetEnabled = function(_, enabled) hostEnabled = enabled end,
- EnableMouse = function(_, enabled) hostMouse = enabled end,
- },
- Preview = {
- Show = function() previewShown = true end,
- Hide = function() previewShown = false end,
- },
- }
-
- local mixin = _G[case.global]
- mixin.SetEnabled(picker, false)
- assert.is_false(dropdownEnabled)
- assert.is_false(dropdownMouse)
- assert.is_false(hostEnabled)
- assert.is_false(hostMouse)
- assert.is_false(previewShown)
-
- mixin.SetEnabled(picker, true)
- assert.is_true(dropdownEnabled)
- assert.is_true(dropdownMouse)
- assert.is_true(hostEnabled)
- assert.is_true(hostMouse)
- assert.is_true(previewShown)
- end)
-
- it(case.name .. " Init bridges initializer.SetEnabled to frame", function()
- local dropdownEnabled, previewShown
-
- local setting = {
- GetValue = function() return "TestFont" end,
- SetValue = function() end,
- }
- local staleSetting = {
- GetValue = function() return "chain" end,
- SetValue = function() end,
- }
-
- local initializer = {
- GetData = function() return { name = "Test", setting = setting } end,
- GetSetting = function() return staleSetting end,
- }
-
- local picker = {
- Text = { SetText = function() end },
- DropDown = {
- SetupMenu = function() end,
- OverrideText = function() end,
- SetEnabled = function(_, enabled) dropdownEnabled = enabled end,
- EnableMouse = function() end,
- },
- DropDownHost = {
- SetEnabled = function() end,
- EnableMouse = function() end,
- },
- Preview = {
- SetFont = function() end,
- SetText = function() end,
- Show = function() previewShown = true end,
- Hide = function() previewShown = false end,
- },
- SetupDropdown = function() end,
- UpdatePreview = function() end,
- }
-
- local mixin = _G[case.global]
- picker.SetEnabled = mixin.SetEnabled
- mixin.Init(picker, initializer)
-
- assert.are.same(setting, picker.setting)
+ it("bridges initializer enabled state to the active picker frame", function()
+ local lib = LibStub("LibLSMSettingsWidgets-1.0")
+ local frame = makePickerFrame()
+ local initializer = {}
+
+ lib.ApplyTexturePickerRow(frame, { name = "Texture", setting = makeSetting("Smooth") }, initializer)
+ initializer:SetEnabled(false)
+
+ local picker = frame._lsmwPicker
+ assert.is_false(picker.dropdown:IsEnabled())
+ assert.is_false(picker.dropdown:IsMouseEnabled())
+ assert.is_false(picker.host:IsEnabled())
+ assert.is_false(picker.host:IsMouseEnabled())
+ assert.is_false(picker.preview:IsShown())
+ end)
- -- Init should have bridged SetEnabled onto the initializer
- assert.is_function(initializer.SetEnabled)
+ it("registers declarative font and texture row types with LibSettingsBuilder", function()
+ local registered = {}
+ local lib = LibStub("LibLSMSettingsWidgets-1.0")
- -- Calling initializer:SetEnabled propagates to the frame
- initializer:SetEnabled(false)
- assert.is_false(dropdownEnabled)
- assert.is_false(previewShown)
+ lib.Register({
+ RegisterRowType = function(_, name, descriptor)
+ registered[name] = descriptor
+ end,
+ })
- initializer:SetEnabled(true)
- assert.is_true(dropdownEnabled)
- assert.is_true(previewShown)
- end)
- end
+ assert.is_function(registered.font.applyFrame)
+ assert.is_function(registered.texture.applyFrame)
+ assert.are.equal("string", registered.font.varType)
+ assert.are.equal("string", registered.texture.varType)
+ end)
end)
diff --git a/Libs/LibSettingsBuilder/Builders/Rows.lua b/Libs/LibSettingsBuilder/Builders/Rows.lua
index e9cd9e95..1c0ed37b 100644
--- a/Libs/LibSettingsBuilder/Builders/Rows.lua
+++ b/Libs/LibSettingsBuilder/Builders/Rows.lua
@@ -84,17 +84,20 @@ function builders.input(spec)
return { initializer = initializer, setting = spec.setting, registration = "category" }
end
---- Creates a proxy setting backed by a custom frame template.
---- The template's Init receives initializer data containing {setting, name, tooltip}.
-function builders.custom(spec)
- assert(spec.template, "Custom: spec.template is required")
+function builders.registered(spec)
+ local descriptor = lib._registeredRowTypes[spec.type]
+ assert(descriptor, "Registered row type '" .. tostring(spec.type) .. "' is not available")
- local initializer = interop.createElementInitializer(spec.template, {
+ local initializer = interop.createCustomListRowInitializer("SettingsListElementTemplate", {
name = spec.name,
setting = spec.setting,
+ settingVariable = interop.getSettingVariable(spec.setting),
tooltip = spec.tooltip,
- })
+ }, descriptor.extent or 26, descriptor.applyFrame, descriptor.resetFrame)
interop.setInitializerSetting(initializer, spec.setting)
+ if descriptor.configureInitializer then
+ descriptor.configureInitializer(initializer, spec.setting, spec)
+ end
return { initializer = initializer, setting = spec.setting, registration = "category" }
end
diff --git a/Libs/LibSettingsBuilder/Core.lua b/Libs/LibSettingsBuilder/Core.lua
index 9bc4152f..8d8e2688 100644
--- a/Libs/LibSettingsBuilder/Core.lua
+++ b/Libs/LibSettingsBuilder/Core.lua
@@ -19,5 +19,21 @@ lib._internal = {
builders = {},
registry = {},
}
+lib._registeredRowTypes = {}
lib._pageLifecycleCallbacks = {}
lib._pageLifecycleHooked = false
+
+function lib:RegisterRowType(name, descriptor)
+ assert(type(name) == "string" and name ~= "", "RegisterRowType: name is required")
+ assert(type(descriptor) == "table", "RegisterRowType: descriptor is required")
+ assert(type(descriptor.applyFrame) == "function", "RegisterRowType: descriptor.applyFrame is required")
+
+ descriptor.name = name
+ self._registeredRowTypes[name] = descriptor
+
+ local schema = self._internal and self._internal.schema
+ if schema and schema.VALID_ROW_TYPES then
+ schema.VALID_ROW_TYPES[name] = true
+ schema.PROXY_ROW_TYPES[name] = true
+ end
+end
diff --git a/Libs/LibSettingsBuilder/Interop/ListRows.lua b/Libs/LibSettingsBuilder/Interop/ListRows.lua
index 08a351d9..85af7642 100644
--- a/Libs/LibSettingsBuilder/Interop/ListRows.lua
+++ b/Libs/LibSettingsBuilder/Interop/ListRows.lua
@@ -861,7 +861,7 @@ local function initializerIsEnabled(initializer)
return true
end
-local function createCustomListRowInitializer(template, data, extent, initFrame)
+local function createCustomListRowInitializer(template, data, extent, initFrame, resetFrame)
local initializer = interop.createElementInitializer(template, data)
setInitializerExtent(initializer, extent)
@@ -916,6 +916,9 @@ local function createCustomListRowInitializer(template, data, extent, initFrame)
frame._lsbCanvas:Hide()
frame._lsbCanvas = nil
end
+ if resetFrame then
+ resetFrame(frame)
+ end
if self._lsbActiveFrame == frame then
self._lsbActiveFrame = nil
end
diff --git a/Libs/LibSettingsBuilder/Registry/Runtime.lua b/Libs/LibSettingsBuilder/Registry/Runtime.lua
index 26a8788b..9180fb2c 100644
--- a/Libs/LibSettingsBuilder/Registry/Runtime.lua
+++ b/Libs/LibSettingsBuilder/Registry/Runtime.lua
@@ -38,7 +38,6 @@
---| "checkboxList"
---| "color"
---| "colorList"
----| "custom"
---| "dropdown"
---| "fontOverride"
---| "header"
@@ -193,11 +192,6 @@
---@field resolveText LibSettingsBuilderInputResolveTextCallback|nil Gets the preview-text resolver shown beneath the edit box.
---@field width number|nil Gets the edit-box width.
----@class LibSettingsBuilderCustomRowConfig: LibSettingsBuilderBindableRowBase
----@field type "custom" Gets the XML-template-backed custom row kind.
----@field template string Gets the XML template name registered with Blizzard's Settings API.
----@field varType any Gets the optional `Settings.VarType` override.
-
---@class LibSettingsBuilderButtonRowConfig: LibSettingsBuilderRowBase
---@field type "button" Gets the button row kind.
---@field buttonText string|nil Gets the button label; defaults to `name`.
@@ -269,9 +263,7 @@
---@field enabledTooltip string|nil Gets the override toggle tooltip.
---@field fontName string|nil Gets the font-row label.
---@field fontTooltip string|nil Gets the font-row tooltip.
----@field fontValues (fun(): table)|nil Gets the optional dropdown value provider for the font row.
---@field fontFallback (fun(): string|nil)|nil Gets the fallback font name used when no override is stored.
----@field fontTemplate string|nil Gets the optional custom template used instead of the built-in dropdown.
---@field sizeName string|nil Gets the font-size row label.
---@field sizeTooltip string|nil Gets the font-size row tooltip.
---@field sizeMin number|nil Gets the minimum font size.
@@ -291,7 +283,6 @@
---| LibSettingsBuilderDropdownRowConfig
---| LibSettingsBuilderColorRowConfig
---| LibSettingsBuilderInputRowConfig
----| LibSettingsBuilderCustomRowConfig
---| LibSettingsBuilderButtonRowConfig
---| LibSettingsBuilderHeaderRowConfig
---| LibSettingsBuilderSubheaderRowConfig
@@ -334,10 +325,26 @@ local ROW_BUILDERS = {
subheader = builders.subheader,
color = builders.color,
input = builders.input,
- custom = builders.custom,
}
local PROXY_ROW_TYPES = schema.PROXY_ROW_TYPES
+local function getRegisteredRowType(rowType)
+ return lib._registeredRowTypes and lib._registeredRowTypes[rowType]
+end
+
+local function getRegisteredRowVarType(descriptor)
+ if descriptor.varType == "boolean" then
+ return interop.getVarTypeBoolean()
+ end
+ if descriptor.varType == "number" then
+ return interop.getVarTypeNumber()
+ end
+ if type(descriptor.varType) == "function" then
+ return descriptor.varType()
+ end
+ return interop.getVarTypeString()
+end
+
local function refreshCategory(builder, category)
if not category then
return
@@ -417,8 +424,14 @@ local function prepareProxyRow(builder, rowType, spec)
setting, category = registry.makeColorSetting(builder, spec)
elseif rowType == "input" then
setting, category = registry.makeProxySetting(builder, spec, interop.getVarTypeString(), "")
- elseif rowType == "custom" then
- setting, category = registry.makeProxySetting(builder, spec, spec.varType or interop.getVarTypeString(), "")
+ elseif getRegisteredRowType(rowType) then
+ local descriptor = getRegisteredRowType(rowType)
+ setting, category = registry.makeProxySetting(
+ builder,
+ spec,
+ getRegisteredRowVarType(descriptor),
+ descriptor.defaultValue or ""
+ )
end
spec.setting = setting
@@ -469,7 +482,7 @@ local function registerBuiltRow(sourceName, page, row, created)
local spec = prepareRow(sourceName, page, row)
local builder = page._builder
local rowType = spec.type
- local build = ROW_BUILDERS[rowType]
+ local build = ROW_BUILDERS[rowType] or (getRegisteredRowType(rowType) and builders.registered)
if not build then
error(sourceName .. ": unknown row type '" .. tostring(rowType) .. "'")
end
@@ -565,12 +578,10 @@ local function registerFontOverride(sourceName, page, row, created)
end
local fontSpec = {
- type = spec.fontTemplate and "custom" or "dropdown",
+ type = "font",
path = sectionPath .. ".font",
name = spec.fontName or "Font",
tooltip = spec.fontTooltip,
- values = spec.fontValues,
- template = spec.fontTemplate,
disabled = isOverrideDisabled,
getTransform = function(value)
if value then
diff --git a/Libs/LibSettingsBuilder/Schema/Rows.lua b/Libs/LibSettingsBuilder/Schema/Rows.lua
index 55a43e21..fc17c81f 100644
--- a/Libs/LibSettingsBuilder/Schema/Rows.lua
+++ b/Libs/LibSettingsBuilder/Schema/Rows.lua
@@ -18,7 +18,6 @@ schema.PROXY_ROW_TYPES = {
dropdown = true,
color = true,
input = true,
- custom = true,
}
schema.COMPOSITE_ROW_TYPES = {
@@ -37,7 +36,6 @@ schema.VALID_ROW_TYPES = {
checkboxList = true,
color = true,
colorList = true,
- custom = true,
dropdown = true,
fontOverride = true,
header = true,
@@ -157,8 +155,6 @@ function schema.validateRow(sourceName, builder, row)
assert(type(row.onClick) == "function", sourceName .. ": button row '" .. rowLabel .. "' requires onClick")
elseif rowType == "canvas" then
assert(row.canvas, sourceName .. ": canvas row '" .. rowLabel .. "' requires canvas")
- elseif rowType == "custom" then
- assert(row.template, sourceName .. ": custom row '" .. rowLabel .. "' requires template")
elseif rowType == "dropdown" then
assert(row.values ~= nil, sourceName .. ": dropdown row '" .. rowLabel .. "' requires values")
elseif rowType == "list" then
diff --git a/Libs/LibSettingsBuilder/Tests/Architecture_spec.lua b/Libs/LibSettingsBuilder/Tests/Architecture_spec.lua
index 2bedf53d..784a9846 100644
--- a/Libs/LibSettingsBuilder/Tests/Architecture_spec.lua
+++ b/Libs/LibSettingsBuilder/Tests/Architecture_spec.lua
@@ -116,6 +116,7 @@ describe("LibSettingsBuilder architecture", function()
it("keeps lib table exports public-only", function()
local allowed = {
New = true,
+ RegisterRowType = true,
GetSection = true,
GetRootPage = true,
GetPage = true,
diff --git a/Libs/LibSettingsBuilder/Tests/Controls_spec.lua b/Libs/LibSettingsBuilder/Tests/Controls_spec.lua
index c39b3650..4873b3a1 100644
--- a/Libs/LibSettingsBuilder/Tests/Controls_spec.lua
+++ b/Libs/LibSettingsBuilder/Tests/Controls_spec.lua
@@ -528,7 +528,7 @@ describe("LibSettingsBuilder Controls", function()
assert.are.equal("Potions", displayedText)
end)
- it("passes custom row settings through initializer data", function()
+ it("passes registered row settings through initializer data", function()
TestHelpers.SetupLibStub()
TestHelpers.SetupSettingsStubs()
_G.hooksecurefunc = function() end
@@ -540,6 +540,9 @@ describe("LibSettingsBuilder Controls", function()
end
TestHelpers.LoadLibSettingsBuilder()
+ LibStub("LibSettingsBuilder-1.0"):RegisterRowType("testPicker", {
+ applyFrame = function() end,
+ })
local profile = { general = { font = "Expressway" } }
local defaults = { general = { font = "Expressway" } }
@@ -561,10 +564,9 @@ describe("LibSettingsBuilder Controls", function()
key = "main",
rows = {
{
- type = "custom",
+ type = "testPicker",
path = "font",
name = "Font",
- template = "TestFontPickerTemplate",
},
},
},
diff --git a/Libs/LibSettingsBuilder/docs/API_REFERENCE.md b/Libs/LibSettingsBuilder/docs/API_REFERENCE.md
index 42d6f8f1..e5d8d9b0 100644
--- a/Libs/LibSettingsBuilder/docs/API_REFERENCE.md
+++ b/Libs/LibSettingsBuilder/docs/API_REFERENCE.md
@@ -15,6 +15,7 @@
Documented surface:
- `LSB.New(config)`
+- `LSB:RegisterRowType(name, descriptor)`
- `lsb:GetSection(sectionKey)`
- `lsb:GetRootPage()`
- `lsb:GetPage(sectionKey, pageKey)`
@@ -59,6 +60,12 @@ Optional fields:
Returns an `lsb` runtime instance bound to one category tree.
+### `LSB:RegisterRowType(name, descriptor)`
+
+Registers a reusable Lua-backed proxy row type. The descriptor must provide `applyFrame(frame, data, initializer)` and may provide `resetFrame(frame)`, `extent`, `varType`, and `defaultValue`.
+
+Registered rows render on Blizzard's `SettingsListElementTemplate`; no XML template is required.
+
## Registration tree
`LSB.New(config)` accepts and registers the full declarative tree.
@@ -195,19 +202,6 @@ Notes:
`debounceMilliseconds` is still normalized to `debounce / 1000` for compatibility.
-### `custom` row
-
-Additional fields:
-
-- `template`
-- `varType`
-
-Notes:
-
-- use this for XML-backed widgets that are not covered by the built-in controls,
-- the template must already be loaded by the time you register settings,
-- unlike `input`, `custom` does not create its frame structure in Lua.
-
### `button` row
Additional fields:
@@ -386,9 +380,7 @@ Fields:
- `enabledTooltip`
- `fontName`
- `fontTooltip`
-- `fontValues`
- `fontFallback`
-- `fontTemplate`
- `sizeName`
- `sizeTooltip`
- `sizeMin`
@@ -398,8 +390,7 @@ Fields:
Notes:
-- expands to an override checkbox, a font selector, and a size slider,
-- when `fontTemplate` is present, the font selector uses `type = "custom"` instead of the built-in dropdown.
+- expands to an override checkbox, a `type = "font"` selector, and a size slider.
### `border`
@@ -446,7 +437,6 @@ Supported canonical row types:
- `dropdown`
- `input`
- `color`
-- `custom`
- `button`
- `header`
- `subheader`
@@ -473,11 +463,11 @@ The public API is declarative and intentionally narrow. Internally, the library
- **Schema** normalizes and validates raw row tables.
- **Registry** materializes root pages, sections, page handles, refresh behavior, lifecycle callbacks, store/default bindings, callback contexts, and composite child rows.
- **Builders** translate prepared row specs into simple primitive operations. Builders do not receive the runtime object or call Registry.
-- **Interop** is the only layer that calls Blizzard Settings/UI APIs, creates frames, installs hooks, or renders custom row widgets.
+- **Interop** is the only layer that calls Blizzard Settings/UI APIs, creates frames, installs hooks, or renders registered row widgets.
Row builders still fall into the same public families:
-- **proxy controls** — persisted values backed by proxy settings (`checkbox`, `slider`, `dropdown`, `color`, `input`, `custom`),
+- **proxy controls** — persisted values backed by proxy settings (`checkbox`, `slider`, `dropdown`, `color`, `input`, and registered row types),
- **layout rows** — structural/display rows without stored values (`header`, `subheader`, `info`, `button`, `canvas`, `pageActions`),
- **composites** — declarative specs expanded by Registry into multiple child rows (`border`, `fontOverride`, `heightOverride`, `colorList`, `checkboxList`).
diff --git a/Libs/LibSettingsBuilder/docs/INSTALLATION.md b/Libs/LibSettingsBuilder/docs/INSTALLATION.md
index afcc0a12..6ae93a3d 100644
--- a/Libs/LibSettingsBuilder/docs/INSTALLATION.md
+++ b/Libs/LibSettingsBuilder/docs/INSTALLATION.md
@@ -53,7 +53,7 @@ When you use `input` rows with `debounce` / `resolveText`, the library also uses
Those hooks are part of the library's behavior and should be considered when debugging conflicts with heavily customized Settings UI code.
-## Built-in controls vs custom templates
+## Built-in controls and registered row types
Most library features are available with no extra XML:
@@ -61,13 +61,7 @@ Most library features are available with no extra XML:
- layout rows like `header`, `subheader`, `info`, `button`, `pageActions`, and `canvas`,
- composite builders like `border`, `fontOverride`, and `heightOverride`.
-`input` is a built-in row type implemented entirely in Lua on top of `SettingsListElementTemplate` plus a runtime-created `InputBoxTemplate` edit box.
-
-Only `type = "custom"` rows require you to supply your own template. In that case:
-
-1. define the template in XML,
-2. load that XML from your TOC before calling `LSB.New({ ... })`, and
-3. pass the template name through `spec.template`.
+`input` is a built-in row type implemented entirely in Lua on top of `SettingsListElementTemplate` plus a runtime-created `InputBoxTemplate` edit box. Other libraries can add pure-Lua row types with `LSB:RegisterRowType(name, descriptor)`.
## Canvas support
diff --git a/Libs/LibSettingsBuilder/docs/MIGRATION_GUIDE.md b/Libs/LibSettingsBuilder/docs/MIGRATION_GUIDE.md
index 9b1dd612..779291cd 100644
--- a/Libs/LibSettingsBuilder/docs/MIGRATION_GUIDE.md
+++ b/Libs/LibSettingsBuilder/docs/MIGRATION_GUIDE.md
@@ -84,9 +84,9 @@ Declarative pages use canonical row types only:
- specialized row templates,
- genuinely bespoke embedded frames.
-If you only need text or numeric entry, use the built-in `input` type first. Reach for `type = "custom"` only when you need a genuinely different widget.
+If you only need text or numeric entry, use the built-in `input` type first. Reach for a registered row type only when you need a genuinely different widget.
-If you need an ordered list, grouped editor, or add/remove workflow, prefer `type = "list"` or `type = "sectionList"` before reaching for `type = "custom"` or `type = "canvas"`.
+If you need an ordered list, grouped editor, or add/remove workflow, prefer `type = "list"` or `type = "sectionList"` before reaching for a registered row type or `type = "canvas"`.
## Migrating AceConfig input fields
diff --git a/Libs/LibSettingsBuilder/docs/QUICK_START.md b/Libs/LibSettingsBuilder/docs/QUICK_START.md
index 8fb53537..82d14ae0 100644
--- a/Libs/LibSettingsBuilder/docs/QUICK_START.md
+++ b/Libs/LibSettingsBuilder/docs/QUICK_START.md
@@ -132,4 +132,4 @@ Handler rows require `get`, `set`, and a stable `key` (or `id`).
- Use composites for repeated patterns like borders, font overrides, and height overrides.
- Prefer declarative root registration for large standard settings pages.
- Look up registered page handles with `lsb:GetRootPage()` or `lsb:GetPage(...)`, then call `page:Refresh()` for async or transient redraws.
-- Reach for `type = "custom"` or `type = "canvas"` only when built-ins like `input`, `list`, and `sectionList` stop fitting.
+- Reach for registered row types or `type = "canvas"` only when built-ins like `input`, `list`, and `sectionList` stop fitting.
diff --git a/Libs/LibSettingsBuilder/docs/TROUBLESHOOTING.md b/Libs/LibSettingsBuilder/docs/TROUBLESHOOTING.md
index 8f25cf7e..f4792eb6 100644
--- a/Libs/LibSettingsBuilder/docs/TROUBLESHOOTING.md
+++ b/Libs/LibSettingsBuilder/docs/TROUBLESHOOTING.md
@@ -101,4 +101,4 @@ 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.
+- use a registered row type for reusable Lua-backed widgets rather than a full embedded canvas when you only need one custom control.
diff --git a/Tests/TestHelpers.lua b/Tests/TestHelpers.lua
index 5f6d1d4d..785c9312 100644
--- a/Tests/TestHelpers.lua
+++ b/Tests/TestHelpers.lua
@@ -1799,8 +1799,15 @@ function TestHelpers.SetupLibSettingsBuilder()
lsmw.GetStatusbarValues = function()
return { Solid = "Solid" }
end
- lsmw.FONT_PICKER_TEMPLATE = "TestFontPickerTemplate"
- lsmw.TEXTURE_PICKER_TEMPLATE = "TestTexturePickerTemplate"
+ lsmw.Register = function(lsb)
+ lsb:RegisterRowType("font", {
+ applyFrame = function() end,
+ })
+ lsb:RegisterRowType("texture", {
+ applyFrame = function() end,
+ })
+ end
+ lsmw.Register(LibStub("LibSettingsBuilder-1.0"))
return lsmw
end
diff --git a/UI/GeneralOptions.lua b/UI/GeneralOptions.lua
index 285261b4..f860650f 100644
--- a/UI/GeneralOptions.lua
+++ b/UI/GeneralOptions.lua
@@ -4,7 +4,6 @@
local _, ns = ...
local L = ns.L
-local LSMW = LibStub("LibLSMSettingsWidgets-1.0")
local function isFadeDisabled()
local gc = ns.GetGlobalConfig()
local fade = gc and gc.outOfCombatFade
@@ -72,18 +71,16 @@ local GeneralOptions = {
-- Appearance
{ type = "header", name = L["APPEARANCE"] },
{
- type = "custom",
+ type = "texture",
path = "texture",
name = L["BAR_TEXTURE"],
tooltip = L["BAR_TEXTURE_DESC"],
- template = LSMW.TEXTURE_PICKER_TEMPLATE,
},
{
- type = "custom",
+ type = "font",
path = "font",
name = L["FONT"],
tooltip = L["FONT_DESC"],
- template = LSMW.FONT_PICKER_TEMPLATE,
},
{
type = "slider",
diff --git a/UI/OptionUtil.lua b/UI/OptionUtil.lua
index 0b553b4f..a2181989 100644
--- a/UI/OptionUtil.lua
+++ b/UI/OptionUtil.lua
@@ -9,7 +9,6 @@ local _, ns = ...
local C = ns.Constants
local L = ns.L
local OptionUtil = ns.OptionUtil or {}
-local LSMW = LibStub("LibLSMSettingsWidgets-1.0", true)
ns.OptionUtil = OptionUtil
@@ -359,12 +358,8 @@ function OptionUtil.CreateFontOverrideRow(isDisabled)
type = "fontOverride",
path = "",
disabled = isDisabled,
- fontValues = function()
- return LSMW.GetFontValues()
- end,
fontFallback = getGlobalFont,
fontSizeFallback = getGlobalFontSize,
- fontTemplate = LSMW.FONT_PICKER_TEMPLATE,
}
end
From 00b0d071a51ac86a8b36430ce15cd961896884aa Mon Sep 17 00:00:00 2001
From: Argi <15852038+argium@users.noreply.github.com>
Date: Thu, 21 May 2026 15:05:47 +1000
Subject: [PATCH 2/7] Update workspace.
---
EnhancedCooldownManager.code-workspace | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/EnhancedCooldownManager.code-workspace b/EnhancedCooldownManager.code-workspace
index d18834a6..091f4375 100644
--- a/EnhancedCooldownManager.code-workspace
+++ b/EnhancedCooldownManager.code-workspace
@@ -20,7 +20,7 @@
},
"Lua.workspace.library": [
"~\\scoop\\apps\\luarocks\\current\\rocks\\share\\lua\\5.4\\busted",
- "~\\.vscode\\extensions\\ketho.wow-api-0.22.2\\Annotations\\Core"
+ "~\\.vscode\\extensions\\ketho.wow-api-0.22.3\\Annotations\\Core"
],
"Lua.workspace.ignoreDir": [
"Tests",
@@ -85,7 +85,11 @@
"ChatFontNormal",
"GRAY_FONT_COLOR",
"coroutine",
- "canaccesstable"
+ "canaccesstable",
+ "ADD",
+ "REMOVE",
+ "BuffBarCooldownViewer",
+ "UISpecialFrames"
],
"Lua.diagnostics.disable": [
"assign-type-mismatch"
From 68e1cf2d9e6053b53428e844ddaa16dd3ba32cf6 Mon Sep 17 00:00:00 2001
From: Argi <15852038+argium@users.noreply.github.com>
Date: Sat, 23 May 2026 10:37:29 +1000
Subject: [PATCH 3/7] descriptor.defaultValue or "" will coerce valid falsy
defaults (e.g. false or 0) into the empty-string fallback. Use an explicit
nil check (e.g. descriptor.defaultValue ~= nil and descriptor.defaultValue or
"") so registered row types can safely use boolean/number defaults.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
Libs/LibSettingsBuilder/Registry/Runtime.lua | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Libs/LibSettingsBuilder/Registry/Runtime.lua b/Libs/LibSettingsBuilder/Registry/Runtime.lua
index 9180fb2c..ff274701 100644
--- a/Libs/LibSettingsBuilder/Registry/Runtime.lua
+++ b/Libs/LibSettingsBuilder/Registry/Runtime.lua
@@ -430,7 +430,7 @@ local function prepareProxyRow(builder, rowType, spec)
builder,
spec,
getRegisteredRowVarType(descriptor),
- descriptor.defaultValue or ""
+ descriptor.defaultValue ~= nil and descriptor.defaultValue or ""
)
end
From 6476effaf9cd9c8127eb2580071d05e686d4542d Mon Sep 17 00:00:00 2001
From: Argi <15852038+argium@users.noreply.github.com>
Date: Sat, 23 May 2026 11:11:34 +1000
Subject: [PATCH 4/7] fontOverride composite falls back without LSM
---
EnhancedCooldownManager.code-workspace | 9 +-
Libs/LibSettingsBuilder/Registry/Runtime.lua | 16 ++-
.../LibSettingsBuilder/Tests/Builder_spec.lua | 115 ++++++++++++++++++
Libs/LibSettingsBuilder/docs/API_REFERENCE.md | 5 +-
4 files changed, 142 insertions(+), 3 deletions(-)
diff --git a/EnhancedCooldownManager.code-workspace b/EnhancedCooldownManager.code-workspace
index 091f4375..f6a1f25d 100644
--- a/EnhancedCooldownManager.code-workspace
+++ b/EnhancedCooldownManager.code-workspace
@@ -89,7 +89,14 @@
"ADD",
"REMOVE",
"BuffBarCooldownViewer",
- "UISpecialFrames"
+ "UISpecialFrames",
+ "ScrollUtil",
+ "CreateScrollBoxListLinearView",
+ "CreateDataProvider",
+ "GameTooltip_Hide",
+ "SettingsPanel",
+ "SettingsSliderControlMixin",
+ "CreateSettingsListSectionHeaderInitializer"
],
"Lua.diagnostics.disable": [
"assign-type-mismatch"
diff --git a/Libs/LibSettingsBuilder/Registry/Runtime.lua b/Libs/LibSettingsBuilder/Registry/Runtime.lua
index ff274701..71e6a50d 100644
--- a/Libs/LibSettingsBuilder/Registry/Runtime.lua
+++ b/Libs/LibSettingsBuilder/Registry/Runtime.lua
@@ -554,6 +554,17 @@ local function registerHeightOverride(sourceName, page, row, created)
return initializer, setting
end
+local function getFontOverrideDropdownValues(spec)
+ if spec.fontValues then
+ return spec.fontValues
+ end
+
+ return function()
+ local font = spec.fontFallback and spec.fontFallback()
+ return font and { [font] = font } or {}
+ end
+end
+
local function registerFontOverride(sourceName, page, row, created)
local spec = prepareRow(sourceName, page, row)
local sectionPath = schema.resolvePagePath(page.path or "", spec.path)
@@ -578,7 +589,7 @@ local function registerFontOverride(sourceName, page, row, created)
end
local fontSpec = {
- type = "font",
+ type = getRegisteredRowType("font") and "font" or "dropdown",
path = sectionPath .. ".font",
name = spec.fontName or "Font",
tooltip = spec.fontTooltip,
@@ -593,6 +604,9 @@ local function registerFontOverride(sourceName, page, row, created)
return nil
end,
}
+ if fontSpec.type == "dropdown" then
+ fontSpec.values = getFontOverrideDropdownValues(spec)
+ end
schema.propagateModifiers(fontSpec, spec)
rowRegistration.register(sourceName, page, fontSpec, created)
diff --git a/Libs/LibSettingsBuilder/Tests/Builder_spec.lua b/Libs/LibSettingsBuilder/Tests/Builder_spec.lua
index 30a1526a..6e4b02ad 100644
--- a/Libs/LibSettingsBuilder/Tests/Builder_spec.lua
+++ b/Libs/LibSettingsBuilder/Tests/Builder_spec.lua
@@ -187,6 +187,121 @@ describe("LibSettingsBuilder Builder", function()
assert.is_nil(profile.general.height)
end)
+ it("falls back to a dropdown for fontOverride without a registered font row", function()
+ local settings = TestHelpers.CollectSettings(function()
+ createBuilder({
+ sections = {
+ {
+ key = "general",
+ name = "General",
+ pages = {
+ {
+ key = "main",
+ rows = {
+ {
+ type = "fontOverride",
+ path = "",
+ fontFallback = function()
+ return "Fallback Font"
+ end,
+ fontValues = function()
+ return {
+ ["Fallback Font"] = "Fallback Font",
+ ["Other Font"] = "Other Font",
+ }
+ end,
+ },
+ },
+ },
+ },
+ },
+ },
+ })
+ end)
+
+ local fontSetting = assert(settings["BS_general_font"])
+ local fontOptions = fontSetting._optionsGen()
+
+ assert.is_not_nil(settings["BS_general_overrideFont"])
+ assert.is_not_nil(settings["BS_general_fontSize"])
+ assert.is_function(fontSetting._optionsGen)
+ assert.are.equal("Fallback Font", fontSetting:GetValue())
+ assert.are.equal("Fallback Font", fontOptions[1].value)
+ assert.are.equal("Other Font", fontOptions[2].value)
+ end)
+
+ it("uses fontFallback as the default fontOverride dropdown value source", function()
+ local settings = TestHelpers.CollectSettings(function()
+ createBuilder({
+ sections = {
+ {
+ key = "general",
+ name = "General",
+ pages = {
+ {
+ key = "main",
+ rows = {
+ {
+ type = "fontOverride",
+ path = "",
+ fontFallback = function()
+ return "Fallback Font"
+ end,
+ },
+ },
+ },
+ },
+ },
+ },
+ })
+ end)
+
+ local fontSetting = assert(settings["BS_general_font"])
+ local fontOptions = fontSetting._optionsGen()
+
+ assert.are.equal("Fallback Font", fontSetting:GetValue())
+ assert.are.equal(1, #fontOptions)
+ assert.are.equal("Fallback Font", fontOptions[1].value)
+ assert.are.equal("Fallback Font", fontOptions[1].label)
+ end)
+
+ it("keeps registered font rows for fontOverride when available", function()
+ LibStub("LibSettingsBuilder-1.0"):RegisterRowType("font", {
+ applyFrame = function() end,
+ })
+
+ local settings = TestHelpers.CollectSettings(function()
+ createBuilder({
+ sections = {
+ {
+ key = "general",
+ name = "General",
+ pages = {
+ {
+ key = "main",
+ rows = {
+ {
+ type = "fontOverride",
+ path = "",
+ fontValues = {
+ ["Fallback Font"] = "Fallback Font",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ })
+ end)
+
+ local fontSetting = assert(settings["BS_general_font"])
+
+ assert.is_not_nil(settings["BS_general_overrideFont"])
+ assert.is_not_nil(settings["BS_general_fontSize"])
+ assert.is_nil(fontSetting._optionsGen)
+ end)
+
it("rejects deprecated desc fields at registration time", function()
local ok, err = pcall(function()
createBuilder({
diff --git a/Libs/LibSettingsBuilder/docs/API_REFERENCE.md b/Libs/LibSettingsBuilder/docs/API_REFERENCE.md
index e5d8d9b0..fe1fd155 100644
--- a/Libs/LibSettingsBuilder/docs/API_REFERENCE.md
+++ b/Libs/LibSettingsBuilder/docs/API_REFERENCE.md
@@ -380,6 +380,7 @@ Fields:
- `enabledTooltip`
- `fontName`
- `fontTooltip`
+- `fontValues`
- `fontFallback`
- `sizeName`
- `sizeTooltip`
@@ -390,7 +391,9 @@ Fields:
Notes:
-- expands to an override checkbox, a `type = "font"` selector, and a size slider.
+- expands to an override checkbox, a font selector, and a size slider.
+- uses a registered `type = "font"` row when available; otherwise uses a built-in `dropdown` selector.
+- `fontValues` may provide the fallback dropdown values as a table or function; when omitted, the fallback dropdown lists the current `fontFallback()` value when available.
### `border`
From b3f2b455c05a21301e28a6f2e1c552afbde47b4f Mon Sep 17 00:00:00 2001
From: Argi <15852038+argium@users.noreply.github.com>
Date: Sat, 23 May 2026 11:18:11 +1000
Subject: [PATCH 5/7] Add debug tracing for extra icons
---
Libs/LibSettingsBuilder/Core.lua | 6 +-
Modules/ExtraIcons.lua | 158 +++++++++++++++++++++++++++++--
2 files changed, 155 insertions(+), 9 deletions(-)
diff --git a/Libs/LibSettingsBuilder/Core.lua b/Libs/LibSettingsBuilder/Core.lua
index 8d8e2688..6193dff4 100644
--- a/Libs/LibSettingsBuilder/Core.lua
+++ b/Libs/LibSettingsBuilder/Core.lua
@@ -19,9 +19,9 @@ lib._internal = {
builders = {},
registry = {},
}
-lib._registeredRowTypes = {}
-lib._pageLifecycleCallbacks = {}
-lib._pageLifecycleHooked = false
+lib._registeredRowTypes = lib._registeredRowTypes or {}
+lib._pageLifecycleCallbacks = lib._pageLifecycleCallbacks or {}
+lib._pageLifecycleHooked = lib._pageLifecycleHooked or false
function lib:RegisterRowType(name, descriptor)
assert(type(name) == "string" and name ~= "", "RegisterRowType: name is required")
diff --git a/Modules/ExtraIcons.lua b/Modules/ExtraIcons.lua
index cf33f40b..3b8029a0 100644
--- a/Modules/ExtraIcons.lua
+++ b/Modules/ExtraIcons.lua
@@ -264,6 +264,47 @@ local function updateIconCountText(icon, globalConfig, config)
applyIconCountText(icon, nil)
end
+local function formatDebugValue(value)
+ if value == nil then return "nil" end
+ if issecretvalue(value) then return "[secret]" end
+ return tostring(value)
+end
+
+local function formatDebugFrame(frame)
+ if not frame then return "nil" end
+ if type(frame.GetName) == "function" then
+ local ok, name = pcall(frame.GetName, frame)
+ if ok and name then return formatDebugValue(name) end
+ end
+ return formatDebugValue(frame)
+end
+
+local function formatDebugPoint(frame)
+ if not frame then return "nil" end
+ if type(frame.GetPoint) ~= "function" then return "no-GetPoint" end
+ local ok, point, relativeTo, relativePoint, x, y = pcall(frame.GetPoint, frame, 1)
+ if not ok then return "error:" .. formatDebugValue(point) end
+ if not point then return "none" end
+ return formatDebugValue(point) .. "," .. formatDebugFrame(relativeTo) .. ","
+ .. formatDebugValue(relativePoint) .. "," .. formatDebugValue(x or 0) .. "," .. formatDebugValue(y or 0)
+end
+
+local function formatDebugAnchor(anchor)
+ if not anchor then return "nil" end
+ return formatDebugValue(anchor[1]) .. "," .. formatDebugFrame(anchor[2]) .. ","
+ .. formatDebugValue(anchor[3]) .. "," .. formatDebugValue(anchor[4] or 0) .. ","
+ .. formatDebugValue(anchor[5] or 0)
+end
+
+local function debugField(name, value)
+ return name .. "=" .. formatDebugValue(value)
+end
+
+local function debugLog(moduleName, event, fields)
+ table.insert(fields, 1, "[ExtraIconsDebug:" .. event .. "]")
+ ns.Log(moduleName, table.concat(fields, " "))
+end
+
local function getItemFramesCount(itemFrames)
if type(itemFrames) ~= "table" or not canaccesstable(itemFrames) then
return nil
@@ -387,18 +428,50 @@ function ExtraIcons:_updateSingleViewer(viewerConfig, entries, isEditing, module
if not vs then return false end
local container = vs.container
cachePoint(vs, blizzFrame)
+ local debugEnabled = ns.IsDebugEnabled()
+ if debugEnabled then
+ local p = vs.originalPoint
+ debugLog(self.Name, "viewer-start", {
+ debugField("trigger", why or ""),
+ debugField("viewer", viewerConfig.key),
+ debugField("blizzKey", viewerConfig.blizzKey),
+ debugField("blizzFrame", formatDebugFrame(blizzFrame)),
+ debugField("blizzShown", blizzFrame and blizzFrame:IsShown() or false),
+ debugField("entries", #entries),
+ debugField("isEditing", isEditing == true),
+ debugField("originalPoint", p and formatDebugAnchor(p) or "nil"),
+ debugField("livePoint", formatDebugPoint(blizzFrame)),
+ debugField("containerShown", container:IsShown()),
+ debugField("containerPoint", formatDebugPoint(container)),
+ })
+ end
local items = (not blizzFrame or not blizzFrame:IsShown() or isEditing or #entries == 0)
and {} or resolveEntries(entries, moduleConfig)
if #items == 0 then
local p = vs.originalPoint
+ local restoredAnchor = false
if p and blizzFrame then
- FrameUtil.LazySetAnchors(blizzFrame, { { p[1], p[2], p[3], p[4], p[5] } })
+ restoredAnchor = 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
+ if debugEnabled then
+ debugLog(self.Name, "viewer-empty", {
+ debugField("trigger", why or ""),
+ debugField("viewer", viewerConfig.key),
+ debugField("items", #items),
+ debugField("blizzExists", blizzFrame ~= nil),
+ debugField("blizzShown", blizzFrame and blizzFrame:IsShown() or false),
+ debugField("entries", #entries),
+ debugField("isEditing", isEditing == true),
+ debugField("restoredAnchor", restoredAnchor),
+ debugField("finalBlizzPoint", formatDebugPoint(blizzFrame)),
+ debugField("containerShown", container:IsShown()),
+ })
+ end
return false
end
@@ -412,14 +485,22 @@ function ExtraIcons:_updateSingleViewer(viewerConfig, entries, isEditing, module
local viewerScale = blizzFrame.iconScale or 1.0
local spacing = blizzFrame.childXPadding or 0
local lastActive = nil
+ local itemFramesCount = nil
+ local activeItemFrames = 0
+ local shownActiveItemFrames = 0
local itemFrames = getAccessibleItemFrames(blizzFrame, viewerConfig, why)
if itemFrames then
+ itemFramesCount = getItemFramesCount(itemFrames)
local ok, err = pcall(function()
for _, itemFrame in ipairs(itemFrames) do
if itemFrame.isActive then
+ activeItemFrames = activeItemFrames + 1
iconSize = itemFrame:GetWidth() or iconSize
- if itemFrame:IsShown() then lastActive = itemFrame end
+ if itemFrame:IsShown() then
+ shownActiveItemFrames = shownActiveItemFrames + 1
+ lastActive = itemFrame
+ end
end
end
end)
@@ -445,11 +526,15 @@ function ExtraIcons:_updateSingleViewer(viewerConfig, entries, isEditing, module
-- need to absorb the on-screen width of the gap + extra group.
local extraOnScreen = (spacing + totalWidth) * viewerScale
local p = vs.originalPoint
+ local viewerAnchor = nil
+ local viewerAnchorChanged = false
if p and blizzFrame then
- FrameUtil.LazySetAnchors(blizzFrame, { { p[1], p[2], p[3], p[4] - extraOnScreen / 2, p[5] } })
+ viewerAnchor = { p[1], p[2], p[3], p[4] - extraOnScreen / 2, p[5] }
+ viewerAnchorChanged = FrameUtil.LazySetAnchors(blizzFrame, { viewerAnchor })
end
local xOffset = 0
+ local iconAnchorChanges = 0
for i, data in ipairs(items) do
local icon = vs.iconPool[i]
icon:SetSize(iconSize, iconSize)
@@ -462,7 +547,9 @@ function ExtraIcons:_updateSingleViewer(viewerConfig, entries, isEditing, module
icon.Icon:SetTexture(data.texture)
icon.Icon:SetDesaturated(data.missing == true)
- FrameUtil.LazySetAnchors(icon, { { "LEFT", container, "LEFT", xOffset, 0 } })
+ if FrameUtil.LazySetAnchors(icon, { { "LEFT", container, "LEFT", xOffset, 0 } }) then
+ iconAnchorChanges = iconAnchorChanges + 1
+ end
icon:Show()
updateIconCooldown(icon)
@@ -471,15 +558,51 @@ function ExtraIcons:_updateSingleViewer(viewerConfig, entries, isEditing, module
xOffset = xOffset + iconSize + spacing
end
- FrameUtil.LazySetAnchors(container, { { "LEFT", lastActive or blizzFrame, "RIGHT", spacing, 0 } })
+ local containerAnchor = { "LEFT", lastActive or blizzFrame, "RIGHT", spacing, 0 }
+ local containerAnchorChanged = FrameUtil.LazySetAnchors(container, { containerAnchor })
container:Show()
if viewerConfig.ownsAnchor then updateMainViewerAnchor(vs, blizzFrame, container) end
+ if debugEnabled then
+ debugLog(self.Name, "viewer-placed", {
+ debugField("trigger", why or ""),
+ debugField("viewer", viewerConfig.key),
+ debugField("items", #items),
+ debugField("entries", #entries),
+ debugField("itemFrames", itemFramesCount),
+ debugField("activeFrames", activeItemFrames),
+ debugField("shownActiveFrames", shownActiveItemFrames),
+ debugField("lastActive", formatDebugFrame(lastActive)),
+ debugField("iconSize", iconSize),
+ debugField("viewerScale", viewerScale),
+ debugField("spacing", spacing),
+ debugField("totalWidth", totalWidth),
+ debugField("extraOnScreen", extraOnScreen),
+ debugField("viewerAnchor", viewerAnchor and formatDebugAnchor(viewerAnchor) or "nil"),
+ debugField("viewerAnchorChanged", viewerAnchorChanged),
+ debugField("containerAnchor", formatDebugAnchor(containerAnchor)),
+ debugField("containerAnchorChanged", containerAnchorChanged),
+ debugField("iconAnchorChanges", iconAnchorChanges),
+ debugField("finalBlizzPoint", formatDebugPoint(blizzFrame)),
+ debugField("finalContainerPoint", formatDebugPoint(container)),
+ debugField("containerSize", container:GetWidth() .. "x" .. container:GetHeight()),
+ })
+ end
return true
end
function ExtraIcons:UpdateLayout(why)
- if not self.InnerFrame or not self._viewers then return false end
+ local debugEnabled = ns.IsDebugEnabled()
+ if not self.InnerFrame or not self._viewers then
+ if debugEnabled then
+ debugLog(self.Name, "layout-skipped", {
+ debugField("trigger", why or ""),
+ debugField("hasInnerFrame", self.InnerFrame ~= nil),
+ debugField("hasViewers", self._viewers ~= nil),
+ })
+ end
+ return false
+ end
local shouldShow = self:ShouldShow()
local moduleConfig = self:GetModuleConfig()
@@ -494,6 +617,21 @@ function ExtraIcons:UpdateLayout(why)
local viewers = shouldShow and moduleConfig and moduleConfig.viewers
local anyPlaced = false
+ if debugEnabled then
+ local mainEntries = viewers and viewers.main or {}
+ local utilityEntries = viewers and viewers.utility or {}
+ debugLog(self.Name, "layout-start", {
+ debugField("trigger", why or ""),
+ debugField("shouldShow", shouldShow),
+ debugField("isEditing", isEditing == true),
+ debugField("hasModuleConfig", moduleConfig ~= nil),
+ debugField("hasViewersConfig", viewers ~= nil),
+ debugField("mainEntries", #mainEntries),
+ debugField("utilityEntries", #utilityEntries),
+ debugField("innerShown", self.InnerFrame:IsShown()),
+ })
+ end
+
for _, v in ipairs(VIEWERS) do
local entries = viewers and viewers[v.key] or {}
if self:_updateSingleViewer(v, entries, isEditing, moduleConfig, why) then
@@ -514,6 +652,14 @@ function ExtraIcons:UpdateLayout(why)
self:ThrottledRefresh("UpdateLayout")
end
+ if debugEnabled then
+ debugLog(self.Name, "layout-finish", {
+ debugField("trigger", why or ""),
+ debugField("anyPlaced", anyPlaced),
+ debugField("innerShown", self.InnerFrame:IsShown()),
+ })
+ end
+
return anyPlaced
end
From 58ebbce47e0aa9224d5a15d6e86d2b54e9b0ad55 Mon Sep 17 00:00:00 2001
From: Argi <15852038+argium@users.noreply.github.com>
Date: Sat, 23 May 2026 16:26:09 +1000
Subject: [PATCH 6/7] Fix: initializer.SetEnabled is rebound to a closure over
the current frame, but reactive reevaluation calls SetEnabled on initializers
regardless of whether they still own that frame. After Blizzard recycles list
frames, this can toggle enabled/disabled state (and hide/show preview) on the
wrong visible row, because the old initializer still points at a frame now
used by another initializer.
---
.../LibLSMSettingsWidgets.lua | 22 +++++++++++++---
.../Tests/LibLSMSettingsWidgets_spec.lua | 26 +++++++++++++++++++
2 files changed, 44 insertions(+), 4 deletions(-)
diff --git a/Libs/LibLSMSettingsWidgets/LibLSMSettingsWidgets.lua b/Libs/LibLSMSettingsWidgets/LibLSMSettingsWidgets.lua
index deb68091..0b4b8a0c 100644
--- a/Libs/LibLSMSettingsWidgets/LibLSMSettingsWidgets.lua
+++ b/Libs/LibLSMSettingsWidgets/LibLSMSettingsWidgets.lua
@@ -110,6 +110,20 @@ local function setPickerEnabled(frame, enabled)
picker.preview[enabled and "Show" or "Hide"](picker.preview)
end
+local function configurePickerInitializer(initializer)
+ if initializer._lsmwHasEnabledBridge then return end
+
+ initializer._lsmwEnabled = true
+ initializer._lsmwHasEnabledBridge = true
+ initializer.SetEnabled = function(controlInitializer, enabled)
+ controlInitializer._lsmwEnabled = enabled
+ local frame = controlInitializer._lsbActiveFrame
+ if frame and (not frame._lsbInitializer or frame._lsbInitializer == controlInitializer) then
+ setPickerEnabled(frame, enabled)
+ end
+ end
+end
+
local function anchorPicker(frame, picker)
if frame.Text then
frame.Text:ClearAllPoints()
@@ -184,6 +198,9 @@ local function setupMediaDropdown(frame, picker, setting, mediaType, fallback, u
end
local function applyPickerRow(frame, data, initializer, kind, mediaType, fallback, updatePreview)
+ configurePickerInitializer(initializer)
+ initializer._lsbActiveFrame = frame
+
local picker = ensurePicker(frame, kind)
local setting = data.setting
@@ -195,10 +212,7 @@ local function applyPickerRow(frame, data, initializer, kind, mediaType, fallbac
anchorPicker(frame, picker)
setupMediaDropdown(frame, picker, setting, mediaType, fallback, updatePreview)
updatePreview(picker, setting)
-
- initializer.SetEnabled = function(_, enabled)
- setPickerEnabled(frame, enabled)
- end
+ setPickerEnabled(frame, initializer._lsmwEnabled ~= false)
end
function lib.ApplyFontPickerRow(frame, data, initializer)
diff --git a/Libs/LibLSMSettingsWidgets/Tests/LibLSMSettingsWidgets_spec.lua b/Libs/LibLSMSettingsWidgets/Tests/LibLSMSettingsWidgets_spec.lua
index f77bea35..2dfc6045 100644
--- a/Libs/LibLSMSettingsWidgets/Tests/LibLSMSettingsWidgets_spec.lua
+++ b/Libs/LibLSMSettingsWidgets/Tests/LibLSMSettingsWidgets_spec.lua
@@ -198,6 +198,32 @@ describe("LibLSMSettingsWidgets", function()
assert.is_false(picker.preview:IsShown())
end)
+ it("ignores stale initializer enabled updates after a picker frame is recycled", function()
+ local lib = LibStub("LibLSMSettingsWidgets-1.0")
+ local frame = makePickerFrame()
+ local oldInitializer = {}
+ local newInitializer = {}
+
+ lib.ApplyTexturePickerRow(frame, { name = "Old Texture", setting = makeSetting("Smooth") }, oldInitializer)
+ frame._lsbInitializer = oldInitializer
+ oldInitializer:SetEnabled(false)
+
+ lib.ApplyTexturePickerRow(frame, { name = "New Texture", setting = makeSetting("Blizzard") }, newInitializer)
+ frame._lsbInitializer = newInitializer
+ newInitializer:SetEnabled(true)
+
+ local picker = frame._lsmwPicker
+ assert.is_true(picker.dropdown:IsEnabled())
+ assert.is_true(picker.host:IsEnabled())
+ assert.is_true(picker.preview:IsShown())
+
+ oldInitializer:SetEnabled(false)
+
+ assert.is_true(picker.dropdown:IsEnabled())
+ assert.is_true(picker.host:IsEnabled())
+ assert.is_true(picker.preview:IsShown())
+ end)
+
it("registers declarative font and texture row types with LibSettingsBuilder", function()
local registered = {}
local lib = LibStub("LibLSMSettingsWidgets-1.0")
From 879de226bc0109827b4ddeb65c7e185ab12f1b78 Mon Sep 17 00:00:00 2001
From: Argi <15852038+argium@users.noreply.github.com>
Date: Sat, 23 May 2026 17:00:06 +1000
Subject: [PATCH 7/7] Update the threshold for event storm
---
Constants.lua | 3 ++-
ECM.lua | 2 +-
Tests/ECM_spec.lua | 4 ++--
3 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/Constants.lua b/Constants.lua
index 1f3bd565..913be60e 100644
--- a/Constants.lua
+++ b/Constants.lua
@@ -10,6 +10,7 @@ local constants = {
ADDON_METADATA_VERSION_KEY = "Version",
DEBUG_COLOR = "F17934",
ERROR_COLOR = "ff4040",
+ WARNING_COLOR = "ffd100",
RELEASE_POPUP_VERSION = "v0.8.3",
VERSION_TAG_BETA = "beta",
@@ -122,7 +123,7 @@ local constants = {
-- Runtime timing and debug limits
LAYOUT_COMBAT_END_DELAY = 0.1,
LAYOUT_ENTERING_WORLD_DELAY = 0.4,
- LAYOUT_STORM_COUNT = 20,
+ LAYOUT_STORM_COUNT = 60,
LAYOUT_STORM_WINDOW = 2,
LAYOUT_ZONE_CHANGE_DELAY = 0.1,
LIFECYCLE_SECOND_PASS_DELAY = 0.05,
diff --git a/ECM.lua b/ECM.lua
index dfe9aa31..a6cd239c 100644
--- a/ECM.lua
+++ b/ECM.lua
@@ -174,7 +174,7 @@ function ns.ErrorLog(module, message, data)
local messageStr = ns.ToString(message)
local payload = makeErrorData(module, nil, data)
local dataStr = ns.ToString(payload)
- local coloredPrefix = "|cff" .. C.ERROR_COLOR .. "[" .. L["ADDON_ABRV"] .. " Error"
+ local coloredPrefix = "|cff" .. C.WARNING_COLOR .. "[" .. L["ADDON_ABRV"] .. " Warning"
.. (module and (" " .. module) or "") .. "]|r "
print(coloredPrefix .. messageStr .. "\n" .. dataStr)
diff --git a/Tests/ECM_spec.lua b/Tests/ECM_spec.lua
index 191e637f..90c1c077 100644
--- a/Tests/ECM_spec.lua
+++ b/Tests/ECM_spec.lua
@@ -397,7 +397,7 @@ describe("ECM layout system", function()
assert.is_false(ns.IsErrorLoggingEnabled())
end)
- it("prints targeted errors to chat and DevTool", function()
+ it("prints targeted warnings to chat and DevTool", function()
local devToolCalls = {}
_G._testDB.profile.global.errorLogging = true
_G.DevTool = {
@@ -409,7 +409,7 @@ describe("ECM layout system", function()
ns.ErrorLog("Taint", "ChatFrameUtil.SetLastTellTarget is tainted", { source = "EnhancedCooldownManager" })
assert.are.equal(1, #printedMessages)
- assert.is_truthy(printedMessages[1]:find("%[ECM Error Taint%]"))
+ assert.is_truthy(printedMessages[1]:find("%[ECM Warning Taint%]"))
assert.is_truthy(printedMessages[1]:find("ChatFrameUtil.SetLastTellTarget is tainted", 1, true))
assert.is_truthy(printedMessages[1]:find("source=EnhancedCooldownManager", 1, true))
assert.is_truthy(printedMessages[1]:find("module=Taint", 1, true))