diff --git a/AGENTS.md b/AGENTS.md index d95b8469..9c4d0cf0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -103,7 +103,10 @@ All Lua files start with: - Test load order mirrors TOC load order. Test files mirror source paths; library tests live under `Libs//Tests/`. - Test production code directly. Do not mirror production logic in specs or preserve table-attached helpers just so specs can call them. - Prefer testing the real public flow, or the actual shared owner of the logic. +- Tests and shared stubs must model the supported Retail runtime strictly. Do not add fallback fields, compatibility aliases, or helper behavior that production code does not receive. +- Shared test helpers default to the current Retail API shape. If a test needs a non-Retail shape to cover a compatibility branch or malformed input, keep that stub local to the test and document why. - Stub the canonical function, not a wrapper or alias. If a stub diverges from real behavior, fix the stub. +- If a test double masked a production bug, fix the shared helper first and add a regression that exercises the real runtime shape before considering the bug closed. - Do not guard production APIs only to satisfy tests. If an API exists in the supported runtime/load order, tests must stub it. - Reuse `Tests/TestHelpers.lua` before creating new shared helpers. - `StaticPopup_Show` stubs forward `(name, text1, text2, data)` and call `OnAccept(self, data)`. diff --git a/ClassUtil.lua b/ClassUtil.lua index 6cffd5b4..5f34410b 100644 --- a/ClassUtil.lua +++ b/ClassUtil.lua @@ -64,6 +64,12 @@ function ClassUtil.GetPlayerResourceType() return ClassUtil.GetResourceType(class, GetSpecialization(), GetShapeshiftForm()) end +--- Returns whether the player is a Death Knight. +function ClassUtil.IsDeathKnight() + local _, class = UnitClass("player") + return class == "DEATHKNIGHT" +end + --- Gets the max Maelstrom value that can diff based on talents local function getMaelstromWeaponMax() if C_SpellBook.IsSpellKnown(C.RESOURCEBAR_RAGING_MAELSTROM_SPELLID) then @@ -135,6 +141,7 @@ function ClassUtil.GetCurrentMaxResourceValues(resourceType) if resourceType then local max = UnitPowerMax("player", resourceType) local current = UnitPower("player", resourceType) + ---@type number|nil local safeMax = max if issecretvalue(max) then safeMax = nil diff --git a/ColorUtil.lua b/ColorUtil.lua index df07b357..eb715f0f 100644 --- a/ColorUtil.lua +++ b/ColorUtil.lua @@ -7,20 +7,6 @@ local _, ns = ... local ColorUtil = {} ns.ColorUtil = ColorUtil ---- Compares two ECM_Color tables for equality. ----@param c1 ECM_Color|nil ----@param c2 ECM_Color|nil ----@return boolean -function ColorUtil.AreEqual(c1, c2) - if c1 == c2 then - return true - end - if not c1 or not c2 then - return false - end - return c1.r == c2.r and c1.g == c2.g and c1.b == c2.b and c1.a == c2.a -end - local function clamp(v, minV, maxV) return math.max(minV, math.min(maxV, v)) end diff --git a/Constants.lua b/Constants.lua index 913be60e..f9af1da0 100644 --- a/Constants.lua +++ b/Constants.lua @@ -177,19 +177,19 @@ local constants = { --- Predefined icon stacks resolved at runtime by stackKey. --- Each entry defines an icon kind and its candidate sources. -local BUILTIN_STACKS = { +constants.BUILTIN_STACKS = { trinket1 = { kind = "equipSlot", slotId = 13, label = "Trinket 1" }, trinket2 = { kind = "equipSlot", slotId = 14, label = "Trinket 2" }, } --- Default display order for builtin stack keys (matches default viewers.utility order). -local BUILTIN_STACK_ORDER = { "trinket1", "trinket2" } +constants.BUILTIN_STACK_ORDER = { "trinket1", "trinket2" } -local DRACTHYR_WING_BUFFET_IDS = { 357214, 368970 } -- Base and enhanced evoker variants. +local dracthyrWingBuffetIds = { 357214, 368970 } -- Base and enhanced evoker variants. --- Racial ability lookup keyed by UnitRace("player") raceFileName. --- One primary active racial per race. -local RACIAL_ABILITIES = { +constants.RACIAL_ABILITIES = { Human = { spellId = 59752 }, -- Every Man for Himself Orc = { spellId = 33697 }, -- Blood Fury Dwarf = { spellId = 20594 }, -- Stoneform @@ -213,25 +213,25 @@ local RACIAL_ABILITIES = { Vulpera = { spellId = 312411 }, -- Bag of Tricks MagharOrc = { spellId = 274738 }, -- Ancestral Call Mechagnome = { spellId = 312924 }, -- Hyper Organic Light Originator - Dracthyr = { spellIds = DRACTHYR_WING_BUFFET_IDS }, -- Wing Buffet + Dracthyr = { spellIds = dracthyrWingBuffetIds }, -- 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, +constants.RACIAL_SPELL_ALIASES = { + [357214] = dracthyrWingBuffetIds, + [368970] = dracthyrWingBuffetIds, } -local BLIZZARD_FRAMES = { +constants.BLIZZARD_FRAMES = { "EssentialCooldownViewer", "UtilityCooldownViewer", "BuffIconCooldownViewer", "BuffBarCooldownViewer", } -local CLASS_COLORS = { +constants.CLASS_COLORS = { DEATHKNIGHT = "C41F3B", DEMONHUNTER = "A330C9", DRUID = "FF7D0A", @@ -249,19 +249,19 @@ local CLASS_COLORS = { -- Resource types that support a separate color when at maximum value. -- Code-level gate; user toggle is stored in the profile (maxColorsEnabled). -local resourceBarMaxColorTypes = { +constants.RESOURCEBAR_MAX_COLOR_TYPES = { [constants.RESOURCEBAR_TYPE_ICICLES] = true, [constants.RESOURCEBAR_TYPE_DEVOURER_META] = true, [constants.RESOURCEBAR_TYPE_DEVOURER_NORMAL] = true, } -local resourceBarCastableMaxColorSpells = { +constants.RESOURCEBAR_CASTABLE_MAX_COLOR_SPELLS = { [constants.RESOURCEBAR_TYPE_DEVOURER_META] = constants.SPELLID_COLLAPSING_STAR, [constants.RESOURCEBAR_TYPE_DEVOURER_NORMAL] = constants.SPELLID_VOID_META, } ---- Authoritative mapping from module name to its profile config key. -local moduleConfigKeys = { +--- Maps modules to their respective profile config key. +constants.MODULE_CONFIG_KEYS = { [constants.POWERBAR] = "powerBar", [constants.RESOURCEBAR] = "resourceBar", [constants.RUNEBAR] = "runeBar", @@ -273,18 +273,20 @@ local moduleConfigKeys = { --- Returns the profile config key for a module name. --- Uses the authoritative lookup; falls back to lowercasing the first character. function constants.ConfigKeyForModule(name) - return moduleConfigKeys[name] or (name:sub(1, 1):lower() .. name:sub(2)) + return constants.MODULE_CONFIG_KEYS[name] or (name:sub(1, 1):lower() .. name:sub(2)) end -local chainOrder = { +-- Defines the ordering of modules for chained anchoring. +constants.CHAIN_ORDER = { constants.POWERBAR, constants.RESOURCEBAR, constants.RUNEBAR, constants.BUFFBARS, constants.EXTERNALBARS, } -constants.CHAIN_ORDER = chainOrder -constants.MODULE_ORDER = { + +-- Controls the order that modules are loaded in. +constants.MODULE_LOAD_ORDER = { constants.POWERBAR, constants.RESOURCEBAR, constants.RUNEBAR, @@ -292,14 +294,5 @@ constants.MODULE_ORDER = { constants.EXTERNALBARS, constants.EXTRAICONS, } -constants.MODULE_CONFIG_KEYS = moduleConfigKeys -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 ns.Constants = constants diff --git a/ECM.lua b/ECM.lua index 762c898a..92c1d474 100644 --- a/ECM.lua +++ b/ECM.lua @@ -51,12 +51,6 @@ function ns.AreWarningsEnabled() return not gc or gc.warnings ~= false end ---- Returns whether the player is a Death Knight. -function ns.IsDeathKnight() - local _, class = UnitClass("player") - return class == "DEATHKNIGHT" -end - local function getAddonVersion() return C_AddOns.GetAddOnMetadata(ADDON_NAME, C.ADDON_METADATA_VERSION_KEY) end diff --git a/EnhancedCooldownManager.code-workspace b/EnhancedCooldownManager.code-workspace index 239ff677..7a11d98a 100644 --- a/EnhancedCooldownManager.code-workspace +++ b/EnhancedCooldownManager.code-workspace @@ -97,7 +97,8 @@ "SettingsPanel", "SettingsSliderControlMixin", "CreateSettingsListSectionHeaderInitializer", - "Round" + "Round", + "CLASS_COLORS" ], "Lua.diagnostics.disable": [ "assign-type-mismatch" diff --git a/EnhancedCooldownManager.toc b/EnhancedCooldownManager.toc index 958627ce..6a1d2e1f 100644 --- a/EnhancedCooldownManager.toc +++ b/EnhancedCooldownManager.toc @@ -2,7 +2,7 @@ ## Title: Enhanced Cooldown Manager |cff9c9c9cby|r |cffa855f7A|r|cff7a84f7r|r|cff4cc9f0g|r|cff22c55ei|r ## Notes: Add resource, power and buff bars, new spells and item icons to the built-in Cooldown Manager. ## Author: Argi -## Version: v0.8.5 +## Version: v0.8.6 ## SavedVariables: EnhancedCooldownManagerDB ## Category-enUS: User Interface ## IconTexture: Interface\AddOns\EnhancedCooldownManager\Media\icon diff --git a/Libs/LibSettingsBuilder/Interop/Enhancements.lua b/Libs/LibSettingsBuilder/Interop/Enhancements.lua index 4b9662d2..63206443 100644 --- a/Libs/LibSettingsBuilder/Interop/Enhancements.lua +++ b/Libs/LibSettingsBuilder/Interop/Enhancements.lua @@ -451,52 +451,71 @@ local function getCategoryDefaultsButton() return header and header.DefaultsButton or nil end -function interop.installCategoryDefaultsOverride(onClick, enabledPredicate, confirmDefaults, pageName) +local function isPageDefaultsEnabled(cbs) + return (cbs.onDefault or cbs.defaultSettings and #cbs.defaultSettings > 0) + and (not cbs.onDefaultEnabled or cbs.onDefaultEnabled()) +end + +local function resetPageDefaults(cbs) + if cbs.onDefault then + cbs.onDefault() + return + end + + for _, setting in ipairs(cbs.defaultSettings or {}) do + setting:SetValue(setting._lsbDefaultValue) + end +end + +local function getPageDefaultsConfirmText(cbs) + local text = cbs.defaultsConfirmText + if not text:find("%%s") then return text end + return string.format(text, tostring(cbs.pageName or ""):lower()) +end + +function interop.installCategoryDefaultsHide() local button = getCategoryDefaultsButton() if not button then return function() end end - local originalOnClick = button:GetScript("OnClick") - local originalEnabled = button:IsEnabled() + local wasShown = button:IsShown() + button:Hide() + interop.refreshVisibleSettingsFrames() - local function applyEnabled() - if enabledPredicate then - button:SetEnabled(enabledPredicate() and true or false) - elseif not onClick then - button:SetEnabled(originalEnabled) - else - button:SetEnabled(true) + return function() + if wasShown then + button:Show() + interop.refreshVisibleSettingsFrames() end end +end + +function interop.installCategoryDefaultsOverride(cbs) + local button = getCategoryDefaultsButton() + if not button then + return function() end + end - button:SetScript("OnClick", function(self) - if enabledPredicate and not enabledPredicate() then + local originalScript = button:GetScript("OnClick") + local wasEnabled = button:IsEnabled() + button:SetEnabled(isPageDefaultsEnabled(cbs)) + button:SetScript("OnClick", function() + if not isPageDefaultsEnabled(cbs) then return end - local function reset() - if onClick then - onClick() - applyEnabled() - elseif originalOnClick then - originalOnClick(self) - end - end - - if confirmDefaults then - confirmDefaults(pageName, reset) - else - reset() - end + interop.showConfirmDialog(cbs.defaultsDialogName, getPageDefaultsConfirmText(cbs), { + onAccept = function() + resetPageDefaults(cbs) + interop.refreshVisibleSettingsFrames() + end, + }) end) - applyEnabled() return function() - if button:GetScript("OnClick") then - button:SetScript("OnClick", originalOnClick) - end - button:SetEnabled(originalEnabled) + button:SetScript("OnClick", originalScript) + button:SetEnabled(wasEnabled) end end @@ -505,9 +524,13 @@ local function notifyLifecycleHidden(category) if not cbs then return end - if cbs._defaultsRestore then - cbs._defaultsRestore() - cbs._defaultsRestore = nil + if cbs._defaultsHideRestore then + cbs._defaultsHideRestore() + cbs._defaultsHideRestore = nil + end + if cbs._defaultsOverrideRestore then + cbs._defaultsOverrideRestore() + cbs._defaultsOverrideRestore = nil end if cbs.onHide then cbs.onHide() @@ -533,13 +556,10 @@ function interop.installPageLifecycleHooks() lib._activeLifecycleCategory = category local cbs = category and lib._pageLifecycleCallbacks[category] or nil if cbs then - if cbs.onDefault or cbs.confirmDefaults then - cbs._defaultsRestore = interop.installCategoryDefaultsOverride( - cbs.onDefault, - cbs.onDefaultEnabled, - cbs.confirmDefaults, - cbs.pageName - ) + if cbs.hideDefaults then + cbs._defaultsHideRestore = interop.installCategoryDefaultsHide() + elseif cbs.onDefault or cbs.defaultSettings and #cbs.defaultSettings > 0 then + cbs._defaultsOverrideRestore = interop.installCategoryDefaultsOverride(cbs) end if cbs.onShow then cbs.onShow() @@ -583,12 +603,12 @@ function interop.refreshVisibleSettingsFrames() interop.forEachVisibleSettingsFrame(interop.refreshSettingsFrame) end -function interop.ensureConfirmDialog(name) +function interop.ensureConfirmDialog(name, button1, button2) if not StaticPopupDialogs[name] then StaticPopupDialogs[name] = { text = "%s", - button1 = YES, - button2 = NO, + button1 = button1 or YES, + button2 = button2 or NO, OnAccept = function(_, data) if data and data.onAccept then data.onAccept() diff --git a/Libs/LibSettingsBuilder/Interop/ListRows.lua b/Libs/LibSettingsBuilder/Interop/ListRows.lua index 85af7642..3406b848 100644 --- a/Libs/LibSettingsBuilder/Interop/ListRows.lua +++ b/Libs/LibSettingsBuilder/Interop/ListRows.lua @@ -168,7 +168,8 @@ local function applyHeaderFrame(frame, data) ensureHeaderRowWidgets(frame) local settingsHeader = data.attachToCategoryHeader and getSettingsListHeader() or nil local actionParent = settingsHeader or frame - local rightAnchor = settingsHeader and settingsHeader.DefaultsButton or nil + local defaultsButton = settingsHeader and settingsHeader.DefaultsButton or nil + local rightAnchor = defaultsButton and defaultsButton:IsShown() and defaultsButton or nil if frame._lsbHeaderTitle then frame._lsbHeaderTitle:ClearAllPoints() diff --git a/Libs/LibSettingsBuilder/README.md b/Libs/LibSettingsBuilder/README.md index ce7813ed..5aa44239 100644 --- a/Libs/LibSettingsBuilder/README.md +++ b/Libs/LibSettingsBuilder/README.md @@ -25,6 +25,11 @@ LSB.New({ name = "My Addon", store = MyAddonDB, defaults = defaults, + defaultsConfirmation = { + text = "Reset %s to defaults?", + button1 = "Reset", + button2 = "Don't reset", + }, onChanged = function() MyAddon:Refresh() end, @@ -49,6 +54,23 @@ LSB.New({ }) ``` +Pages with path-bound rows can use Blizzard's category **Defaults** button when `defaults` and `defaultsConfirmation` are configured. The confirmation `text` may include `%s`, which is replaced with the lowercase page name. + +```lua +page = { + key = "main", + defaultsConfirmText = "Reset ALL settings on this page? This cannot be undone.", + onDefaultEnabled = function() + return MyAddon:CanResetMainPage() + end, + rows = { + -- path-bound rows reset to values from defaults + }, +} +``` + +Page definitions may also provide `onDefault` for custom reset logic, or `hideDefaults = true` to hide the category Defaults button on pages where reset behavior does not apply. + ## AceDB Profile ```lua @@ -69,6 +91,11 @@ LSB.New({ return MyAddon.db.profile end, defaults = defaults.profile, + defaultsConfirmation = { + text = "Reset %s to defaults?", + button1 = "Reset", + button2 = "Don't reset", + }, onChanged = function() MyAddon:Refresh() end, diff --git a/Libs/LibSettingsBuilder/Registry/CoreState.lua b/Libs/LibSettingsBuilder/Registry/CoreState.lua index a2fe5c47..b434f2af 100644 --- a/Libs/LibSettingsBuilder/Registry/CoreState.lua +++ b/Libs/LibSettingsBuilder/Registry/CoreState.lua @@ -33,7 +33,7 @@ ---@field onChanged LibSettingsBuilderChangedCallback Gets the callback fired after a row setter completes. ---@field store table|(fun(): table)|nil Gets the store table or lazy provider used by path-bound rows. ---@field defaults table|(fun(): table)|nil Gets the defaults table or lazy provider used by path-bound rows. ----@field defaultsConfirmation fun(pageName: string, onAccept: fun())|nil Gets the optional confirmation hook shown before any category-header `Defaults` reset. +---@field defaultsConfirmation table|nil Gets localized strings for page Defaults confirmations: `text`, `button1`, and `button2`. ---@field getNestedValue LibSettingsBuilderGetNestedValue|nil Gets the custom nested-path reader used by path-bound rows. ---@field setNestedValue LibSettingsBuilderSetNestedValue|nil Gets the custom nested-path writer used by path-bound rows. ---@field page LibSettingsBuilderPageConfig|nil Gets the optional root-owned page definition. @@ -239,6 +239,9 @@ function registry.makeProxySetting(self, spec, varType, defaultFallback, binding end setting = interop.registerProxySetting(category, variable, varType, spec.name, defaultValue, getter, setter) + if spec.path ~= nil then + setting._lsbDefaultValue = defaultValue + end setting.SetValueNoCallback = setValueNoCallback return setting, category @@ -261,15 +264,18 @@ function registry.makeColorSetting(self, spec) registry.postSet(self, spec, value, setting) end + local defaultValue = foundation.colorTableToHex(binding.default or {}) + setting = interop.registerProxySetting( category, variable, interop.getVarTypeString(), spec.name, - foundation.colorTableToHex(binding.default or {}), + defaultValue, getter, setter ) + setting._lsbDefaultValue = defaultValue return setting, category end diff --git a/Libs/LibSettingsBuilder/Registry/Runtime.lua b/Libs/LibSettingsBuilder/Registry/Runtime.lua index 71e6a50d..43ba1c67 100644 --- a/Libs/LibSettingsBuilder/Registry/Runtime.lua +++ b/Libs/LibSettingsBuilder/Registry/Runtime.lua @@ -81,8 +81,9 @@ ---@field rows LibSettingsBuilderRowConfig[] Gets the declarative row array registered on the page. ---@field onShow LibSettingsBuilderPageLifecycleCallback|nil Gets the callback fired when Blizzard shows this page. ---@field onHide LibSettingsBuilderPageLifecycleCallback|nil Gets the callback fired when Blizzard hides this page. ----@field onDefault fun()|nil Gets the callback invoked when the user clicks the Blizzard category-header `Defaults` button while this page is active. When supplied, the library replaces the button's default reset behavior for the duration the page is shown. ----@field onDefaultEnabled fun(): boolean|nil Gets the predicate that controls whether the `Defaults` button is enabled while this page is active. Defaults to always-enabled when `onDefault` is supplied. +---@field onDefault fun()|nil Gets the callback invoked after confirming the category-header `Defaults` button for this page. +---@field onDefaultEnabled fun(): boolean|nil Gets the predicate that controls whether the custom defaults callback is enabled. Defaults to always-enabled when `onDefault` is supplied. +---@field hideDefaults boolean|nil Gets whether the Blizzard category-header `Defaults` button is hidden entirely while this page is active. Mutually exclusive with `onDefault`; the button is restored when the page is hidden. ---@field disabled LibSettingsBuilderPredicate|nil Gets the page-level disabled predicate propagated to child rows. ---@field hidden LibSettingsBuilderPredicate|nil Gets the page-level hidden predicate propagated to child rows. ---@field order number|nil Gets the sort order used when a section declares multiple pages. @@ -380,6 +381,24 @@ local function ensureConfirmDialog(builder) return interop.ensureConfirmDialog(builder._confirmDialogName) end +local function getDefaultsConfirmation(builder) + local confirmation = builder._config.defaultsConfirmation + assert(type(confirmation) == "table", "LibSettingsBuilder: defaultsConfirmation is required when page defaults are enabled") + assert(type(confirmation.text) == "string" and confirmation.text ~= "", "LibSettingsBuilder: defaultsConfirmation.text is required") + assert(type(confirmation.button1) == "string" and confirmation.button1 ~= "", "LibSettingsBuilder: defaultsConfirmation.button1 is required") + assert(type(confirmation.button2) == "string" and confirmation.button2 ~= "", "LibSettingsBuilder: defaultsConfirmation.button2 is required") + return confirmation +end + +local function ensureDefaultsConfirmDialog(builder, confirmation) + if builder._defaultsConfirmDialogName then + return builder._defaultsConfirmDialogName + end + + builder._defaultsConfirmDialogName = builder._config.varPrefix .. "_" .. MAJOR:gsub("[%-%.]", "_") .. "_DefaultsConfirm" + return interop.ensureConfirmDialog(builder._defaultsConfirmDialogName, confirmation.button1, confirmation.button2) +end + local function prepareButtonClick(builder, spec) local callbackContext = registry.createCallbackContext(builder, spec) local originalClick = spec.onClick @@ -478,6 +497,12 @@ end local rowRegistration = {} +local function registerDefaultSetting(page, setting) + if setting and setting._lsbDefaultValue ~= nil then + page._defaultSettings[#page._defaultSettings + 1] = setting + end +end + local function registerBuiltRow(sourceName, page, row, created) local spec = prepareRow(sourceName, page, row) local builder = page._builder @@ -488,6 +513,7 @@ local function registerBuiltRow(sourceName, page, row, created) end local initializer, setting = registry.applyBuildResult(builder, spec, build(spec)) + registerDefaultSetting(page, setting) if row.id then created[row.id] = { initializer = initializer, setting = setting } end @@ -736,15 +762,20 @@ local function assertPageMutable(page, sourceName) end local function bindPageLifecycle(page) - local confirmDefaults = page._builder._config.defaultsConfirmation - if page._onShow or page._onHide or page._onDefault or confirmDefaults then + local hasSettingDefaults = #page._defaultSettings > 0 and page._builder._config.defaultsConfirmation ~= nil + if page._onShow or page._onHide or page._onDefault or page._hideDefaults or hasSettingDefaults then + local hasDefaults = page._onDefault or hasSettingDefaults + local defaultsConfirmation = hasDefaults and getDefaultsConfirmation(page._builder) or nil lib._pageLifecycleCallbacks[page._category] = { onShow = page._onShow, onHide = page._onHide, onDefault = page._onDefault, onDefaultEnabled = page._onDefaultEnabled, - confirmDefaults = confirmDefaults, - pageName = page._name, + defaultSettings = hasSettingDefaults and page._defaultSettings or nil, + defaultsConfirmText = page._defaultsConfirmText or (defaultsConfirmation and defaultsConfirmation.text) or nil, + defaultsDialogName = defaultsConfirmation and ensureDefaultsConfirmDialog(page._builder, defaultsConfirmation) or nil, + hideDefaults = page._hideDefaults, + pageName = page._name or page._key, } interop.installPageLifecycleHooks() end @@ -758,7 +789,6 @@ end local function materializePage(page, category) assert(not page._registered, "materializePage: page is already registered") page._category = category - bindPageLifecycle(page) -- Create the handle before row operations so ctx.page is available in callbacks -- registered during those operations (e.g. onClick, onSet). @@ -776,6 +806,7 @@ local function materializePage(page, category) for _, operation in ipairs(page._operations) do operation(created) end + bindPageLifecycle(page) page._registered = true return page @@ -806,6 +837,9 @@ local function createPage(owner, key, rows, opts) _onHide = opts.onHide, _onDefault = opts.onDefault, _onDefaultEnabled = opts.onDefaultEnabled, + _hideDefaults = opts.hideDefaults, + _defaultsConfirmText = opts.defaultsConfirmText, + _defaultSettings = {}, _operations = {}, _rowIDs = {}, _registered = false, @@ -976,6 +1010,8 @@ local function registerPageDefinition(owner, pageDef, defaultName) onHide = pageDef.onHide, onDefault = pageDef.onDefault, onDefaultEnabled = pageDef.onDefaultEnabled, + hideDefaults = pageDef.hideDefaults, + defaultsConfirmText = pageDef.defaultsConfirmText, disabled = schema.normalizePredicate(pageDef.disabled), hidden = schema.normalizePredicate(pageDef.hidden), order = pageDef.order, diff --git a/Libs/LibSettingsBuilder/Schema/Rows.lua b/Libs/LibSettingsBuilder/Schema/Rows.lua index fc17c81f..d0bf8708 100644 --- a/Libs/LibSettingsBuilder/Schema/Rows.lua +++ b/Libs/LibSettingsBuilder/Schema/Rows.lua @@ -203,4 +203,7 @@ function schema.validatePageDefinition(sourceName, pageDef) assert(type(pageDef.rows) == "table", sourceName .. ": page definition requires rows") assertBooleanOrCallback(sourceName, "disabled", pageDef.disabled) assertBooleanOrCallback(sourceName, "hidden", pageDef.hidden) + if pageDef.hideDefaults ~= nil then + assert(type(pageDef.hideDefaults) == "boolean", sourceName .. ": hideDefaults must be a boolean") + end end diff --git a/Libs/LibSettingsBuilder/Tests/Builder_spec.lua b/Libs/LibSettingsBuilder/Tests/Builder_spec.lua index 6e4b02ad..023817ad 100644 --- a/Libs/LibSettingsBuilder/Tests/Builder_spec.lua +++ b/Libs/LibSettingsBuilder/Tests/Builder_spec.lua @@ -53,12 +53,77 @@ describe("LibSettingsBuilder Builder", function() defaults = function() return defaults end, + defaultsConfirmation = { + text = "Localized reset %s?", + button1 = "Localized reset", + button2 = "Localized don't reset", + }, onChanged = function() end, page = config and config.page or nil, sections = config and config.sections or nil, }), profile, defaults end + local function createDefaultsButton() + return { + _enabled = true, + _script = function(self) + self._nativeResetCalls = (self._nativeResetCalls or 0) + 1 + end, + GetScript = function(self) + return self._script + end, + IsEnabled = function(self) + return self._enabled + end, + SetEnabled = function(self, enabled) + self._enabled = enabled + end, + SetScript = function(self, _, script) + self._script = script + end, + } + end + + local function installDefaultsButton(button) + local currentCategory + _G.hooksecurefunc = function(tbl, method, hook) + local original = tbl[method] + tbl[method] = function(...) + original(...) + hook(...) + end + end + rawset(SettingsPanel, "DisplayCategory", function(_, category) + currentCategory = category + end) + rawset(SettingsPanel, "GetCurrentCategory", function() + return currentCategory + end) + rawset(SettingsPanel, "GetSettingsList", function() + return { Header = { DefaultsButton = button } } + end) + rawset(SettingsPanel, "HookScript", function() end) + end + + local function recordPopupAutoAccept() + local originalShow = StaticPopup_Show + local shown + local text + _G.StaticPopup_Show = function(name, text1, text2, data) + shown = name + text = text1 + originalShow(name, text1, text2, data) + end + local function getShown() + return shown + end + local function getText() + return text + end + return getShown, getText + end + it("registers root and section pages through LSB.New", function() local sb = createBuilder({ page = { @@ -93,6 +158,227 @@ describe("LibSettingsBuilder Builder", function() assert.is_true(sb:HasCategory(generalPage._category)) end) + it("binds a hideDefaults page into the lifecycle callbacks", function() + local sb = createBuilder({ + sections = { + { + key = "profile", + name = "Profile", + pages = { + { + key = "main", + hideDefaults = true, + rows = { + { type = "info", name = "Version", value = "1.0" }, + }, + }, + }, + }, + }, + }) + + local page = assert(sb:GetPage("profile", "main")) + local lsb = LibStub("LibSettingsBuilder-1.0") + local cbs = assert(lsb._pageLifecycleCallbacks[page._category]) + + assert.is_true(cbs.hideDefaults) + assert.is_nil(cbs.onDefault) + end) + + it("binds custom page defaults into lifecycle callbacks", function() + local resetCalls = 0 + local defaultEnabled = true + local sb = createBuilder({ + sections = { + { + key = "general", + name = "General", + pages = { + { + key = "native", + name = "Native", + rows = { + { type = "info", name = "Version", value = "1.0" }, + }, + }, + { + key = "custom", + name = "Custom", + onDefault = function() + resetCalls = resetCalls + 1 + end, + onDefaultEnabled = function() + return defaultEnabled + end, + rows = { + { type = "info", name = "Version", value = "1.0" }, + }, + }, + }, + }, + }, + }) + + local lsb = LibStub("LibSettingsBuilder-1.0") + local nativePage = assert(sb:GetPage("general", "native")) + local customPage = assert(sb:GetPage("general", "custom")) + local cbs = assert(lsb._pageLifecycleCallbacks[customPage._category]) + + assert.is_nil(lsb._pageLifecycleCallbacks[nativePage._category]) + assert.are.equal("Custom", cbs.pageName) + assert.is_function(cbs.onDefault) + assert.is_true(cbs.onDefaultEnabled()) + + cbs.onDefault() + + assert.are.equal(1, resetCalls) + + defaultEnabled = false + + assert.is_false(cbs.onDefaultEnabled()) + end) + + it("resets current page settings through a custom Defaults confirmation", function() + local button = createDefaultsButton() + installDefaultsButton(button) + local getPopup, getPopupText = recordPopupAutoAccept() + + local sb, profile = createBuilder({ + sections = { + { + key = "general", + name = "General", + pages = { + { + key = "main", + name = "General Page", + rows = { + { type = "checkbox", path = "general.enabled", name = "Enabled" }, + }, + }, + }, + }, + }, + }) + + local page = assert(sb:GetPage("general", "main")) + SettingsPanel:DisplayCategory(page._category) + button:GetScript("OnClick")(button) + + assert.are.equal(0, button._nativeResetCalls or 0) + assert.are.equal("BS_LibSettingsBuilder_1_0_DefaultsConfirm", getPopup()) + assert.are.equal("Localized reset general page?", getPopupText()) + assert.are.equal("Localized reset", StaticPopupDialogs.BS_LibSettingsBuilder_1_0_DefaultsConfirm.button1) + assert.are.equal("Localized don't reset", StaticPopupDialogs.BS_LibSettingsBuilder_1_0_DefaultsConfirm.button2) + assert.is_false(profile.general.enabled) + end) + + it("uses per-page defaultsConfirmText when provided", function() + local button = createDefaultsButton() + installDefaultsButton(button) + local _, getPopupText = recordPopupAutoAccept() + + local sb = createBuilder({ + sections = { + { + key = "general", + name = "General", + pages = { + { + key = "main", + defaultsConfirmText = "Custom confirm text", + onDefault = function() end, + rows = { + { type = "info", name = "Version", value = "1.0" }, + }, + }, + }, + }, + }, + }) + + local page = assert(sb:GetPage("general", "main")) + SettingsPanel:DisplayCategory(page._category) + button:GetScript("OnClick")(button) + + assert.are.equal("Custom confirm text", getPopupText()) + end) + + it("runs custom page defaults instead of generic setting resets", function() + local resetCalls = 0 + local button = createDefaultsButton() + installDefaultsButton(button) + recordPopupAutoAccept() + + local sb, profile = createBuilder({ + sections = { + { + key = "general", + name = "General", + pages = { + { + key = "main", + onDefault = function() + resetCalls = resetCalls + 1 + end, + rows = { + { type = "checkbox", path = "general.enabled", name = "Enabled" }, + }, + }, + }, + }, + }, + }) + + local page = assert(sb:GetPage("general", "main")) + SettingsPanel:DisplayCategory(page._category) + button:GetScript("OnClick")(button) + + assert.are.equal(1, resetCalls) + assert.is_true(profile.general.enabled) + end) + + it("disables custom page defaults when the page predicate returns false", function() + local resetCalls = 0 + local popupShown = false + local button = createDefaultsButton() + installDefaultsButton(button) + _G.StaticPopup_Show = function() + popupShown = true + end + + local sb = createBuilder({ + sections = { + { + key = "general", + name = "General", + pages = { + { + key = "main", + onDefault = function() + resetCalls = resetCalls + 1 + end, + onDefaultEnabled = function() + return false + end, + rows = { + { type = "info", name = "Version", value = "1.0" }, + }, + }, + }, + }, + }, + }) + + local page = assert(sb:GetPage("general", "main")) + SettingsPanel:DisplayCategory(page._category) + button:GetScript("OnClick")(button) + + assert.is_false(button:IsEnabled()) + assert.is_false(popupShown) + assert.are.equal(0, resetCalls) + end) + it("returns nil for missing section-page lookups", function() local sb = createBuilder({ sections = { diff --git a/Libs/LibSettingsBuilder/Tests/Controls_spec.lua b/Libs/LibSettingsBuilder/Tests/Controls_spec.lua index 4873b3a1..82db1973 100644 --- a/Libs/LibSettingsBuilder/Tests/Controls_spec.lua +++ b/Libs/LibSettingsBuilder/Tests/Controls_spec.lua @@ -299,6 +299,7 @@ describe("LibSettingsBuilder Controls", function() _G.SettingsListElementMixin = {} _G.SettingsDropdownControlMixin = {} _G.SettingsSliderControlMixin = {} + _G.SettingsPanel.HookScript = function() end _G.CreateFrame = function() return createScriptableFrame() end @@ -722,6 +723,71 @@ describe("LibSettingsBuilder Controls", function() assert.are.equal(frame, clickedFrame) end) + it("anchors attached page action buttons to the header edge when Defaults is hidden", function() + TestHelpers.SetupLibStub() + TestHelpers.SetupSettingsStubs() + _G.hooksecurefunc = function() end + _G.SettingsListElementMixin = {} + _G.SettingsDropdownControlMixin = {} + _G.SettingsSliderControlMixin = {} + + local settingsHeader = createScriptableFrame() + settingsHeader.DefaultsButton = createScriptableFrame() + settingsHeader.DefaultsButton:Hide() + _G.SettingsPanel = { + IsShown = function() return true end, + GetCurrentCategory = function() return nil end, + GetSettingsList = function() + return { Header = settingsHeader } + end, + } + _G.CreateFrame = function() + return createScriptableFrame() + end + + TestHelpers.LoadLibSettingsBuilder() + + local builder = LibStub("LibSettingsBuilder-1.0").New({ + name = "Attached Actions", + store = function() return { general = {} } end, + defaults = function() return { general = {} } end, + onChanged = function() end, + sections = { + { + key = "general", + name = "General", + pages = { + { + key = "main", + rows = { + { + type = "pageActions", + attachToCategoryHeader = true, + actions = { + { text = "Run" }, + }, + }, + }, + }, + }, + }, + }, + }) + + local initializer = builder:GetPage("general", "main")._category:GetLayout()._initializers[1] + local frame = createScriptableFrame() + initializer:InitFrame(frame) + + local button = assert(frame._lsbHeaderActionButtons[1]) + local point, relativeTo, relativePoint, x, y = button:GetPoint(1) + + assert.are.equal("RIGHT", point) + assert.are.equal(settingsHeader, relativeTo) + assert.are.equal("RIGHT", relativePoint) + assert.are.equal(-20, x) + assert.are.equal(0, y) + end) + it("reevaluates dynamic button disabled predicates after handler-backed settings change", function() TestHelpers.SetupLibStub() TestHelpers.SetupSettingsStubs() @@ -998,6 +1064,7 @@ describe("LibSettingsBuilder Controls", function() _G.SettingsListElementMixin = {} _G.SettingsDropdownControlMixin = {} _G.SettingsSliderControlMixin = {} + _G.SettingsPanel.HookScript = function() end _G.CreateFrame = function() return createScriptableFrame() end diff --git a/Libs/LibSettingsBuilder/docs/API_REFERENCE.md b/Libs/LibSettingsBuilder/docs/API_REFERENCE.md index fe1fd155..d24605b6 100644 --- a/Libs/LibSettingsBuilder/docs/API_REFERENCE.md +++ b/Libs/LibSettingsBuilder/docs/API_REFERENCE.md @@ -23,7 +23,9 @@ Documented surface: - `page:GetId()` - `page:Refresh()` - `config.page` and `config.sections` +- `config.defaultsConfirmation` - raw row tables in `rows = { ... }` +- page defaults fields: `onDefault`, `onDefaultEnabled`, `hideDefaults`, and `defaultsConfirmText` The runtime returned by `LSB.New(...)` is intentionally narrow. Row helper constructors are not available on `lsb` instances, and deprecated transition namespaces like `LSBDeprecated` are not part of the documented public API. @@ -53,6 +55,7 @@ Optional fields: - `store` — table or function returning the live store used by path-bound rows - `defaults` — table or function returning default values for path-bound rows +- `defaultsConfirmation` — table of localized strings for page Defaults confirmations; required when page defaults are enabled by path-bound defaultable rows or `onDefault` - `getNestedValue` - `setNestedValue` - `page` @@ -60,6 +63,12 @@ Optional fields: Returns an `lsb` runtime instance bound to one category tree. +`defaultsConfirmation` fields: + +- `text` — confirmation prompt; `%s`, when present, is replaced with the lowercase page name +- `button1` — accept button text +- `button2` — decline button text + ### `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`. @@ -82,6 +91,10 @@ Root page definition fields: - `name` (optional; defaults to the root name) - `onShow` - `onHide` +- `onDefault` +- `onDefaultEnabled` +- `hideDefaults` +- `defaultsConfirmText` - `disabled` - `hidden` - `order` @@ -103,6 +116,10 @@ Page definition fields inside `pages`: - `rows` - `onShow` - `onHide` +- `onDefault` +- `onDefaultEnabled` +- `hideDefaults` +- `defaultsConfirmText` - `disabled` - `hidden` - `order` @@ -118,6 +135,21 @@ Notes: Declarative root registration is the only supported page-construction API. +### Page defaults behavior + +LibSettingsBuilder can override Blizzard's category **Defaults** button for a visible page. + +The button is active when the page has resettable path-bound rows or provides `onDefault`. Pages with resettable path-bound rows reset those rows to values from `config.defaults`; pages with `onDefault` run that callback instead. + +Page defaults fields: + +- `onDefault(ctx)` — custom reset callback for the page. When present, it replaces generic path-bound setting resets for that page. +- `onDefaultEnabled(ctx)` — optional predicate for enabling the Defaults button. When omitted, the button is enabled. +- `hideDefaults = true` — hides the category Defaults button while the page is visible. Use this for informational pages or pages where reset behavior does not apply. +- `defaultsConfirmText` — page-specific confirmation prompt. If omitted, `config.defaultsConfirmation.text` is used. `%s`, when present, is replaced with the lowercase page name; prompts without `%s` are shown verbatim. + +`config.defaultsConfirmation` is required for pages that expose Defaults behavior through resettable path-bound rows or `onDefault`. Its `button1` and `button2` labels are shared by all page defaults confirmations for that runtime. + ### Lookup and page operations - `lsb:GetSection(key)` — registered section metadata or `nil` diff --git a/Libs/LibSettingsBuilder/docs/QUICK_START.md b/Libs/LibSettingsBuilder/docs/QUICK_START.md index 82d14ae0..0ec300e0 100644 --- a/Libs/LibSettingsBuilder/docs/QUICK_START.md +++ b/Libs/LibSettingsBuilder/docs/QUICK_START.md @@ -24,6 +24,11 @@ local lsb = LSB.New({ name = "My Addon", store = MyAddonDB.profile, defaults = MyAddonDefaults.profile, + defaultsConfirmation = { + text = "Reset %s to defaults?", + button1 = "Reset", + button2 = "Don't reset", + }, onChanged = function(ctx) MyAddon:Refresh() end, @@ -72,6 +77,8 @@ local lsb = LSB.New({ `name` and `onChanged` are required when you register a root page or section tree. `store` enables path mode; use handler mode when your values do not live in a dot-path table. +Provide `defaultsConfirmation` when pages should expose Blizzard's category **Defaults** button for path-bound rows or custom `onDefault` reset logic. Pages can hide the button with `hideDefaults = true`, disable it with `onDefaultEnabled`, or override the prompt with `defaultsConfirmText`. + Declarative pages can mix persisted controls and layout-only rows freely, so it is normal to combine `checkbox`, `slider`, `input`, `header`, `subheader`, `info`, `button`, `pageActions`, `list`, `sectionList`, and `canvas` entries on one page. ## Handler mode diff --git a/Locales/en.lua b/Locales/en.lua index 94939e02..f3e0dc04 100644 --- a/Locales/en.lua +++ b/Locales/en.lua @@ -304,6 +304,10 @@ L["DISABLE_TOOLTIP"] = "Disable" L["REMOVE_TOOLTIP"] = "Remove" L["CREATE"] = "Create" L["DONT_CREATE"] = "Don't create" +L["RESET"] = "Reset" +L["DONT_RESET"] = "Don't reset" +L["RESET_PAGE_TO_DEFAULTS_CONFIRM"] = "Reset %s to defaults?" +L["RESET_PROFILE_TO_DEFAULTS_CONFIRM"] = "Reset ALL settings on this profile to default? This cannot be undone." L["RENAME"] = "Rename" L["DONT_RENAME"] = "Don't rename" L["DONT_DELETE"] = "Don't delete" @@ -375,14 +379,6 @@ L["DELETE_PROFILE_SELECT_DESC"] = "Select a profile to delete." L["DELETE"] = "Delete" L["DELETE_DESC"] = "Delete the selected profile. The active profile cannot be deleted." L["DELETE_PROFILE_CONFIRM"] = "Are you sure you want to delete the profile '%s'?" -L["RESET"] = "Reset" -L["RESET_PAGE_CONFIRM"] = "Are you sure you want to reset settings on this page?" -L["DONT_RESET"] = "Don't reset" -L["RESET_PAGE_SETTINGS"] = "Reset %s settings" -L["RESET_PROFILE"] = "Reset current profile to defaults" -L["RESET_PROFILE_BUTTON"] = "Reset Profile" -L["RESET_PROFILE_DESC"] = "Reset the current profile back to default settings. This cannot be undone." -L["RESET_PROFILE_CONFIRM"] = "Are you sure you want to reset the current profile to defaults?" L["IMPORT_EXPORT"] = "Import / Export" L["IMPORT_PROFILE"] = "Import profile from clipboard" L["IMPORT"] = "Import" diff --git a/Locales/ru.lua b/Locales/ru.lua index 5e38d4bd..dc1ba338 100644 --- a/Locales/ru.lua +++ b/Locales/ru.lua @@ -302,6 +302,10 @@ L["DISABLE_TOOLTIP"] = "Выключить" L["REMOVE_TOOLTIP"] = "Удалить" L["CREATE"] = "Создать" L["DONT_CREATE"] = "Не создавать" +L["RESET"] = "Сбросить" +L["DONT_RESET"] = "Не сбрасывать" +L["RESET_PAGE_TO_DEFAULTS_CONFIRM"] = "Сбросить %s на настройки по умолчанию?" +L["RESET_PROFILE_TO_DEFAULTS_CONFIRM"] = "Сбросить ВСЕ настройки этого профиля? Это действие нельзя отменить." L["RENAME"] = "Переименовать" L["DONT_RENAME"] = "Не переименовывать" L["DONT_DELETE"] = "Не удалять" @@ -373,14 +377,6 @@ L["DELETE_PROFILE_SELECT_DESC"] = "Выберите профиль для уда L["DELETE"] = "Удалить" L["DELETE_DESC"] = "Удалить выбранный профиль. Активный профиль удалить нельзя." L["DELETE_PROFILE_CONFIRM"] = "Вы уверены, что хотите удалить профиль «%s»?" -L["RESET"] = "Сброс" -L["RESET_PAGE_CONFIRM"] = "Вы уверены, что хотите сбросить настройки на этой странице?" -L["DONT_RESET"] = "Не сбрасывать" -L["RESET_PAGE_SETTINGS"] = "Сбросить настройки: %s" -L["RESET_PROFILE"] = "Сбросить текущий профиль до стандартных" -L["RESET_PROFILE_BUTTON"] = "Сбросить профиль" -L["RESET_PROFILE_DESC"] = "Сбросить текущий профиль к настройкам по умолчанию. Это действие нельзя отменить." -L["RESET_PROFILE_CONFIRM"] = "Вы уверены, что хотите сбросить текущий профиль до настроек по умолчанию?" L["IMPORT_EXPORT"] = "Импорт / Экспорт" L["IMPORT_PROFILE"] = "Импортировать профиль из буфера обмена" L["IMPORT"] = "Импорт" diff --git a/Modules/RuneBar.lua b/Modules/RuneBar.lua index 1d5cea0c..3e9504b4 100644 --- a/Modules/RuneBar.lua +++ b/Modules/RuneBar.lua @@ -293,7 +293,7 @@ function RuneBar:CreateFrame() end function RuneBar:ShouldShow() - return ns.IsDeathKnight() and ns.BarMixin.FrameProto.ShouldShow(self) + return ns.ClassUtil.IsDeathKnight() and ns.BarMixin.FrameProto.ShouldShow(self) end function RuneBar:Refresh(why, force) @@ -359,7 +359,7 @@ function RuneBar:OnInitialize() end function RuneBar:OnEnable() - if not ns.IsDeathKnight() then + if not ns.ClassUtil.IsDeathKnight() then return end diff --git a/Runtime.lua b/Runtime.lua index 4bea7baa..7ebbd891 100644 --- a/Runtime.lua +++ b/Runtime.lua @@ -694,7 +694,7 @@ end function Runtime.Enable(addon) local profile = addon.db and addon.db.profile - for _, moduleName in ipairs(C.MODULE_ORDER) do + for _, moduleName in ipairs(C.MODULE_LOAD_ORDER) do local configKey = C.MODULE_CONFIG_KEYS[moduleName] local moduleConfig = profile and profile[configKey] local shouldEnable = (not moduleConfig) or (moduleConfig.enabled ~= false) diff --git a/Tests/ClassUtil_spec.lua b/Tests/ClassUtil_spec.lua index 2c6699cd..b9d04fda 100644 --- a/Tests/ClassUtil_spec.lua +++ b/Tests/ClassUtil_spec.lua @@ -71,6 +71,24 @@ describe("ClassUtil", function() end end) + describe("IsDeathKnight", function() + it("returns true when the player's class token is DEATHKNIGHT", function() + _G.UnitClass = function() + return "Death Knight", "DEATHKNIGHT", 6 + end + + assert.is_true(ns.ClassUtil.IsDeathKnight()) + end) + + it("returns false when the player's class token is not DEATHKNIGHT", function() + _G.UnitClass = function() + return "Warrior", "WARRIOR", 1 + end + + assert.is_false(ns.ClassUtil.IsDeathKnight()) + end) + end) + describe("GetResourceType", function() local function setAvailablePowerType(powerType) UnitStub.Reset() diff --git a/Tests/ColorUtil_spec.lua b/Tests/ColorUtil_spec.lua index 061aed23..aa8447f6 100644 --- a/Tests/ColorUtil_spec.lua +++ b/Tests/ColorUtil_spec.lua @@ -19,21 +19,12 @@ describe("ColorUtil", function() end) before_each(function() - ns = {} + ns = {} TestHelpers.LoadChunk("ColorUtil.lua", "Unable to load ColorUtil.lua")(nil, ns) ColorUtil = assert(ns.ColorUtil, "ColorUtil did not initialize") end) - it("AreEqual handles identical, nil, and distinct colors", function() - local color = { r = 1, g = 0.5, b = 0.25, a = 1 } - - assert.is_true(ColorUtil.AreEqual(color, color)) - assert.is_true(ColorUtil.AreEqual(nil, nil)) - assert.is_false(ColorUtil.AreEqual(color, nil)) - assert.is_false(ColorUtil.AreEqual(color, { r = 1, g = 0.5, b = 0.25, a = 0.5 })) - end) - it("ColorToHex converts normalized RGB values to lowercase hex", function() local hex = ColorUtil.ColorToHex({ r = 1, g = 0.5, b = 0, a = 1 }) assert.are.equal("ff8000", hex) diff --git a/Tests/FrameUtil_spec.lua b/Tests/FrameUtil_spec.lua index c5b60683..1fc606be 100644 --- a/Tests/FrameUtil_spec.lua +++ b/Tests/FrameUtil_spec.lua @@ -45,16 +45,6 @@ describe("FrameUtil", function() secretValues = {} ns = {} - ns.ColorUtil = {} - ns.ColorUtil.AreEqual = function(a, b) - if a == nil and b == nil then - return true - end - if a == nil or b == nil then - return false - end - return a.r == b.r and a.g == b.g and a.b == b.b and a.a == b.a - end ns.DebugAssert = function(condition, message) if not condition then error(message or "ECM.DebugAssert failed") diff --git a/Tests/Modules/RuneBar_spec.lua b/Tests/Modules/RuneBar_spec.lua index 0b2ef1a6..e1263db9 100644 --- a/Tests/Modules/RuneBar_spec.lua +++ b/Tests/Modules/RuneBar_spec.lua @@ -68,9 +68,11 @@ describe("RuneBar real source", function() target.EnsureFrame = target.EnsureFrame or function() end end, }, - IsDeathKnight = function() - return isDeathKnight - end, + ClassUtil = { + IsDeathKnight = function() + return isDeathKnight + end, + }, Runtime = { RegisterFrame = function() registerFrameCalls = registerFrameCalls + 1 diff --git a/Tests/TestHelpers.lua b/Tests/TestHelpers.lua index 785c9312..32a6453d 100644 --- a/Tests/TestHelpers.lua +++ b/Tests/TestHelpers.lua @@ -414,11 +414,9 @@ function TestHelpers.SetupSettingsStubs() local dialog = _G.StaticPopupDialogs[name] if dialog and dialog.OnAccept then 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, SetEnabled = function() end } } - if dialog.OnShow then dialog.OnShow(self) end - dialog.OnAccept(self, data) + local popupFrame = TestHelpers.MakeRetailStaticPopupFrame({ which = name, data = data }) + if dialog.OnShow then dialog.OnShow(popupFrame, data) end + dialog.OnAccept(popupFrame, data) else dialog.OnAccept(nil, data) end @@ -1851,11 +1849,12 @@ function TestHelpers.SetupOptionsEnv(profile, defaults) ns.GetGlobalConfig = function() return mod.db.profile and mod.db.profile.global end - ns.IsDeathKnight = function() - local _, classToken = UnitClass("player") - return classToken == "DEATHKNIGHT" - end - ns.ClassUtil = {} + ns.ClassUtil = { + IsDeathKnight = function() + local _, classToken = UnitClass("player") + return classToken == "DEATHKNIGHT" + end, + } TestHelpers.LoadChunk("UI/OptionUtil.lua", "Unable to load UI/OptionUtil.lua")(nil, ns) TestHelpers.LoadChunk("UI/ExtraIconsShared.lua", "Unable to load UI/ExtraIconsShared.lua")(nil, ns) @@ -1951,6 +1950,75 @@ function TestHelpers.FindButtonInitializer(initializers, buttonText) return nil end +--- Find a pageActions action by its text from a declarative rows list. +--- @param rows table +--- @param text string +--- @return table|nil +function TestHelpers.FindPageAction(rows, text) + for _, row in ipairs(rows or {}) do + if row.type == "pageActions" then + for _, action in ipairs(row.actions or {}) do + if action.text == text then + return action + end + end + end + end + return nil +end + +--- Create a Retail-style StaticPopup frame whose text input is exposed via `EditBox`. +--- @param opts table|nil +--- @return table popupFrame +--- @return table editBox +function TestHelpers.MakeRetailStaticPopupFrame(opts) + opts = opts or {} + + local text = opts.text or "" + local highlighted = false + local buttonEnabled = opts.buttonEnabled ~= false + local popupFrame + local editBox = { + GetText = function() + return text + end, + SetText = function(_, value) + text = value + highlighted = false + end, + HighlightText = function() + highlighted = true + end, + IsTextHighlighted = function() + return highlighted + end, + } + + popupFrame = { + EditBox = editBox, + which = opts.which, + data = opts.data, + _hidden = false, + button1 = { + IsEnabled = function() + return buttonEnabled + end, + SetEnabled = function(_, enabled) + buttonEnabled = enabled == true + end, + }, + Hide = function(self) + self._hidden = true + end, + } + + function editBox:GetParent() + return popupFrame + end + + return popupFrame, editBox +end + --- Override StaticPopup_Show to capture the popup key and auto-accept it. --- For edit-box dialogs, optionally sets provided text before OnAccept. --- @param editText string|nil @@ -1965,29 +2033,10 @@ function TestHelpers.InstallPopupAutoAccept(editText) end if dialog.hasEditBox then - local text = "" - local editBox = { - GetText = function() - return text - end, - SetText = function(_, value) - text = value - end, - HighlightText = function() end, - } - local popupFrame = { - EditBox = editBox, - editBox = editBox, - button1 = { - IsEnabled = function() - return true - end, - SetEnabled = function() end, - }, - } + local popupFrame, editBox = TestHelpers.MakeRetailStaticPopupFrame({ which = name, data = data }) if dialog.OnShow then - dialog.OnShow(popupFrame) + dialog.OnShow(popupFrame, data) end if editText ~= nil then editBox:SetText(editText) diff --git a/Tests/UI/BuffBarsOptions_spec.lua b/Tests/UI/BuffBarsOptions_spec.lua index 61d267e4..87c8717b 100644 --- a/Tests/UI/BuffBarsOptions_spec.lua +++ b/Tests/UI/BuffBarsOptions_spec.lua @@ -597,16 +597,19 @@ describe("BuffBarsOptions", function() assert.are.equal(ns.L["SPELL_COLORS_SECRET_NAMES_DESC"], confirmText) end) - it("shared reset clears all editable spell color sections", function() - local buffKey = SpellColors.MakeKey("Buff Keep", 111, 222, 333) + it("registers shared spell color defaults for the category Defaults button", function() + local spellColorsSpec = registerSpellColorsSpec() + + assert.is_function(spellColorsSpec.onDefault) + assert.is_function(spellColorsSpec.onDefaultEnabled) + end) + + it("shared spell color defaults reset all editable sections", function() + local buffKey = SpellColors.MakeKey("Buff Reset", 111, 222, 333) local externalKey = SpellColors.MakeKey("External Reset", 444, 555, 666) - local buffDefaultColor = { r = 0.2, g = 0.3, b = 0.4, a = 1 } - local buffResetDefaultColor = ns.Constants.BUFFBARS_DEFAULT_COLOR - local externalResetDefaultColor = ns.Constants.BUFFBARS_DEFAULT_COLOR - local externalCustomDefaultColor = { r = 0.9, g = 0.8, b = 0.7, a = 1 } - BuffSpellColors:SetDefaultColor(buffDefaultColor) - ExternalSpellColors:SetDefaultColor(externalCustomDefaultColor) + BuffSpellColors:SetDefaultColor({ r = 0.2, g = 0.3, b = 0.4, a = 1 }) + ExternalSpellColors:SetDefaultColor({ r = 0.9, g = 0.8, b = 0.7, a = 1 }) BuffSpellColors:SetColorByKey(buffKey, { r = 0.3, g = 0.4, b = 0.5, a = 1 }) ExternalSpellColors:SetColorByKey(externalKey, { r = 0.6, g = 0.5, b = 0.4, a = 1 }) @@ -614,8 +617,8 @@ describe("BuffBarsOptions", function() spellColorsSpec.onDefault() - assert.are.same(buffResetDefaultColor, BuffSpellColors:GetDefaultColor()) - assert.are.same(externalResetDefaultColor, ExternalSpellColors:GetDefaultColor()) + assert.are.same(ns.Constants.BUFFBARS_DEFAULT_COLOR, BuffSpellColors:GetDefaultColor()) + assert.are.same(ns.Constants.BUFFBARS_DEFAULT_COLOR, ExternalSpellColors:GetDefaultColor()) assert.is_nil(BuffSpellColors:GetColorByKey(buffKey)) assert.is_nil(ExternalSpellColors:GetColorByKey(externalKey)) assert.are.same({ ns.L["SPELL_COLORS_SUBCAT"] }, refreshCalls) @@ -664,7 +667,6 @@ describe("BuffBarsOptions", function() local buffItems = getSpellColorCollectionItems(spellColorsSpec, "buffBars") local externalItems = getSpellColorCollectionItems(spellColorsSpec, "externalBars") - assert.is_true(spellColorsSpec.onDefaultEnabled()) assert.is_false(buffHeader.disabled()) assert.is_true(externalHeader.disabled()) assert.is_true(buffItems[1].color.enabled()) @@ -802,7 +804,7 @@ describe("BuffBarsOptions", function() assert.is_false(actions[2].enabled()) end) - it("header actions disable all three actions while spell color editing is locked", function() + it("header actions disable while spell color editing is locked", function() BuffSpellColors:SetColorByKey(SpellColors.MakeKey("Immolation Aura", 258920, nil, nil), { r = 0.2, g = 0.3, b = 0.4, a = 1, }) @@ -966,7 +968,6 @@ describe("BuffBarsOptions", function() actions[1].onClick() actions[2].onClick() - spellColorsSpec.onDefault() assert.is_nil(confirmText) assert.is_nil(popupKey) diff --git a/Tests/UI/ItemStacksOptions_spec.lua b/Tests/UI/ItemStacksOptions_spec.lua index 54b80587..e59da0cf 100644 --- a/Tests/UI/ItemStacksOptions_spec.lua +++ b/Tests/UI/ItemStacksOptions_spec.lua @@ -147,6 +147,34 @@ describe("ItemStacksOptions settings page", function() assert.are.same({ "OptionsChanged" }, scheduledReasons) end) + it("creates item stacks from Retail StaticPopup frames that expose only EditBox", function() + local shown + + _G.StaticPopup_Show = function(name, _text1, _text2, data) + shown = name + local dialog = assert(_G.StaticPopupDialogs[name]) + local popupFrame, editBox = TestHelpers.MakeRetailStaticPopupFrame({ which = name, data = data }) + + assert.has_no.errors(function() + if dialog.OnShow then + dialog.OnShow(popupFrame, data) + end + end) + + editBox:SetText("Potions") + dialog.OnAccept(popupFrame, data) + end + + getRow("createItemStack").onClick({ page = registeredPage }) + + assert.are.equal("ECM_CREATE_ITEM_STACK", shown) + assert.are.equal(1, profile.extraIcons.itemStacks.order[1]) + assert.are.equal(2, profile.extraIcons.itemStacks.nextId) + assert.are.equal("Potions", profile.extraIcons.itemStacks.byId[1].name) + assert.are.same({ "OptionsChanged" }, scheduledReasons) + assert.are.same({ registeredPage._category }, refreshCalls) + end) + it("renames an item stack without changing viewer references", function() createStack("Potions") profile.extraIcons.viewers.utility = { { kind = "itemStack", itemStackId = 1 } } @@ -397,4 +425,23 @@ describe("ItemStacksOptions settings page", function() assert.is_false(profile.extraIcons.itemStacks.byId.combatPotions.hideInInstances) assert.is_true(profile.extraIcons.itemStacks.byId.combatPotions.hideInRatedPvp) end) + + it("exposes onDefault handler on the page table", function() + assert.is_function(page.onDefault) + end) + + it("resets item stacks to defaults via onDefault", function() + ns.Addon.db.defaults = { profile = TestHelpers.deepClone(defaults) } + profile.extraIcons.itemStacks = TestHelpers.deepClone(defaults.extraIcons.itemStacks) + + createStack("Custom Stack") + assert.is_not_nil(profile.extraIcons.itemStacks.byId[1]) + + page.onDefault() + + assert.is_nil(profile.extraIcons.itemStacks.byId[1]) + assert.is_not_nil(profile.extraIcons.itemStacks.byId.combatPotions) + assert.is_not_nil(profile.extraIcons.itemStacks.byId.healthPotions) + assert.is_not_nil(profile.extraIcons.itemStacks.byId.healthstones) + end) end) diff --git a/Tests/UI/Options_spec.lua b/Tests/UI/Options_spec.lua index 0fbc4df9..8638a24b 100644 --- a/Tests/UI/Options_spec.lua +++ b/Tests/UI/Options_spec.lua @@ -181,6 +181,10 @@ describe("OptionUtil", function() assert.is_table(registeredPage) assert.are.equal(ns.L["ADDON_NAME"], registeredPage:GetId()) end) + + it("hides the defaults button", function() + assert.is_true(ns.AboutPage.hideDefaults) + end) end) describe("CreateModuleEnabledHandler", function() @@ -519,13 +523,9 @@ describe("OptionUtil", function() assert.are.equal(profileCategory:GetID(), openedCategory) end) - it("confirms native page defaults before invoking the header reset", function() + it("does not override Defaults when the page has no reset behavior", function() local nativeResetCalls = 0 - local popupKey - local popupText - local acceptText - local cancelText - local acceptFn + local popupShown = false local button = { _script = function() nativeResetCalls = nativeResetCalls + 1 @@ -548,27 +548,16 @@ describe("OptionUtil", function() rawset(SettingsPanel, "GetSettingsList", function() return { Header = { DefaultsButton = button } } end) - ns.Addon.ShowConfirmDialog = function(_, key, text, button1, button2, onAccept) - popupKey = key - popupText = text - acceptText = button1 - cancelText = button2 - acceptFn = onAccept + ns.Addon.ShowConfirmDialog = function() + popupShown = true end SettingsPanel:SetCurrentCategory(generalCategory) SettingsPanel:DisplayCategory(generalCategory) button:GetScript("OnClick")(button) - assert.are.equal(0, nativeResetCalls) - assert.are.equal("ECM_CONFIRM_RESET_SETTINGS_PAGE", popupKey) - assert.are.equal("Are you sure you want to reset settings on this page?", popupText) - assert.are.equal("Reset General settings", acceptText) - assert.are.equal("Don't reset", cancelText) - - acceptFn() - assert.are.equal(1, nativeResetCalls) + assert.is_false(popupShown) end) end) end) diff --git a/Tests/UI/ProfileOptions_spec.lua b/Tests/UI/ProfileOptions_spec.lua index 43ed021c..53dbac47 100644 --- a/Tests/UI/ProfileOptions_spec.lua +++ b/Tests/UI/ProfileOptions_spec.lua @@ -41,8 +41,8 @@ describe("ProfileOptions getters/setters/defaults", function() assert.are.equal("profile", ns.ProfileOptions.key) assert.are.equal("Other", settings.ECM_ProfileCopy:GetValue()) assert.is_not_nil(profileCategory) + assert.is_nil(ns.ProfileOptions.pages[1].hideDefaults) assert.is_function(ns.ProfileOptions.pages[1].onDefault) - assert.is_false(ns.ProfileOptions.pages[1].onDefaultEnabled()) end) before_each(function() @@ -76,9 +76,10 @@ describe("ProfileOptions getters/setters/defaults", function() end describe("switch profile", function() - it("disables the category defaults button for profile actions", function() + it("uses the category Defaults button", function() + assert.is_nil(ns.ProfileOptions.pages[1].hideDefaults) assert.is_function(ns.ProfileOptions.pages[1].onDefault) - assert.is_false(ns.ProfileOptions.pages[1].onDefaultEnabled()) + assert.is_nil(ns.ProfileOptions.pages[1].onDefaultEnabled) end) it("getter returns current profile", function() @@ -90,6 +91,14 @@ describe("ProfileOptions getters/setters/defaults", function() getSetting("ProfileSwitch"):SetValue("Other") assert.are.equal("Other", called) end) + it("ignores blank profile defaults from direct setting resets", function() + local called = false + ns.Addon.db.SetProfile = function() called = true end + + getSetting("ProfileSwitch"):SetValue("") + + assert.is_false(called) + end) it("setter refreshes the category", function() getSetting("ProfileSwitch"):SetValue("Other") assert.are.same({ profileCategory }, refreshCalls) @@ -111,6 +120,36 @@ describe("ProfileOptions getters/setters/defaults", function() assert.are.equal("MyCustomProfile", switched) assert.are.same({ profileCategory }, refreshCalls) end) + + it("supports Retail StaticPopup frames that expose only EditBox", function() + local switched + local shown + ns.Addon.db.SetProfile = function(_, value) switched = value end + + _G.StaticPopup_Show = function(name, _text1, _text2, data) + shown = name + local dialog = assert(_G.StaticPopupDialogs[name]) + local popupFrame, editBox = TestHelpers.MakeRetailStaticPopupFrame({ which = name, data = data }) + + assert.has_no.errors(function() + if dialog.OnShow then + dialog.OnShow(popupFrame) + end + end) + + assert.are.equal("TestPlayer - 120000", editBox:GetText()) + assert.is_true(editBox:IsTextHighlighted()) + + editBox:SetText("RetailPopupProfile") + dialog.OnAccept(popupFrame, data) + end + + TestHelpers.FindButtonInitializer(initializers, ns.L["NEW_PROFILE"])._onClick() + + assert.are.equal("ECM_NEW_PROFILE", shown) + assert.are.equal("RetailPopupProfile", switched) + assert.are.same({ profileCategory }, refreshCalls) + end) end) describe("copy profile picker", function() @@ -221,14 +260,71 @@ describe("ProfileOptions getters/setters/defaults", function() end) end) - describe("reset profile", function() - it("calls db:ResetProfile", function() + local function getPageActions() + for _, row in ipairs(ns.ProfileOptions.pages[1].rows) do + if row.type == "pageActions" then + return row.actions + end + end + end + + local function getPageAction(text) + return TestHelpers.FindPageAction(ns.ProfileOptions.pages[1].rows, text) + end + + describe("profile page actions", function() + it("leaves defaults to the category header button", function() + local actions = assert(getPageActions()) + + assert.are.equal(2, #actions) + assert.are.equal(ns.L["IMPORT"], actions[1].text) + assert.are.equal(ns.L["EXPORT"], actions[2].text) + end) + + it("resets the current profile through the page defaults hook", function() local reset = false ns.Addon.db.ResetProfile = function() reset = true end - TestHelpers.FindButtonInitializer(initializers, ns.L["RESET_PROFILE_BUTTON"])._onClick() + ns.ProfileOptions.pages[1].onDefault() assert.is_true(reset) + assert.are.same({ profileCategory }, refreshCalls) + end) + + it("resets the current profile through the custom Defaults button", function() + local nativeReset = false + local reset = false + local button = { + _enabled = true, + _script = function() + nativeReset = true + end, + GetScript = function(self) + return self._script + end, + IsEnabled = function(self) + return self._enabled + end, + SetEnabled = function(self, enabled) + self._enabled = enabled + end, + SetScript = function(self, _, script) + self._script = script + end, + } + + rawset(SettingsPanel, "GetSettingsList", function() + return { Header = { DefaultsButton = button } } + end) + ns.Addon.db.ResetProfile = function() reset = true end + + SettingsPanel:SetCurrentCategory(profileCategory) + SettingsPanel:DisplayCategory(profileCategory) + button:GetScript("OnClick")(button) + + assert.is_false(nativeReset) + assert.is_true(reset) + assert.are.same({ profileCategory }, refreshCalls) end) end) @@ -238,7 +334,7 @@ describe("ProfileOptions getters/setters/defaults", function() local opened = false ns.Addon.ShowImportDialog = function() opened = true end - TestHelpers.FindButtonInitializer(initializers, ns.L["IMPORT"])._onClick() + assert(getPageAction(ns.L["IMPORT"])).onClick() assert.is_true(opened) end) @@ -250,7 +346,7 @@ describe("ProfileOptions getters/setters/defaults", function() ns.Addon.ShowImportDialog = function() opened = true end ns.Print = function(msg) printed = msg end - TestHelpers.FindButtonInitializer(initializers, ns.L["IMPORT"])._onClick() + assert(getPageAction(ns.L["IMPORT"])).onClick() assert.is_false(opened) assert.are.equal(ns.L["CANNOT_IMPORT_IN_COMBAT"], printed) @@ -262,7 +358,7 @@ describe("ProfileOptions getters/setters/defaults", function() local exportedWith ns.Addon.ShowExportDialog = function(_, str) exportedWith = str end - TestHelpers.FindButtonInitializer(initializers, ns.L["EXPORT"])._onClick() + assert(getPageAction(ns.L["EXPORT"])).onClick() assert.are.equal("exported_string", exportedWith) end) @@ -272,7 +368,7 @@ describe("ProfileOptions getters/setters/defaults", function() local printed ns.Print = function(msg) printed = msg end - TestHelpers.FindButtonInitializer(initializers, ns.L["EXPORT"])._onClick() + assert(getPageAction(ns.L["EXPORT"])).onClick() assert.are.equal(string.format(ns.L["EXPORT_FAILED"], "codec broke"), printed) end) @@ -282,7 +378,7 @@ describe("ProfileOptions getters/setters/defaults", function() local printed ns.Print = function(msg) printed = msg end - TestHelpers.FindButtonInitializer(initializers, ns.L["EXPORT"])._onClick() + assert(getPageAction(ns.L["EXPORT"])).onClick() assert.are.equal(string.format(ns.L["EXPORT_FAILED"], "Unknown error"), printed) end) diff --git a/UI/AboutOptions.lua b/UI/AboutOptions.lua index 13c30b41..2c630a94 100644 --- a/UI/AboutOptions.lua +++ b/UI/AboutOptions.lua @@ -14,6 +14,7 @@ end ns.AboutPage = { key = "about", + hideDefaults = true, rows = { { type = "info", diff --git a/UI/ExtraIconsOptions.lua b/UI/ExtraIconsOptions.lua index d999551e..2c4a161f 100644 --- a/UI/ExtraIconsOptions.lua +++ b/UI/ExtraIconsOptions.lua @@ -950,7 +950,6 @@ ExtraIconsOptions.pages = { footerSpacing = 4, disabled = isDisabled, sections = ExtraIconsOptions.BuildSections, - onDefault = ExtraIconsOptions.ResetToDefaults, }, }, }, diff --git a/UI/ItemStacksOptions.lua b/UI/ItemStacksOptions.lua index a6e9d808..9bb75118 100644 --- a/UI/ItemStacksOptions.lua +++ b/UI/ItemStacksOptions.lua @@ -449,7 +449,16 @@ function ItemStacksOptions.OnInitialize() ItemStacksOptions.EnsureItemLoadFrame() end +local function resetItemStacksToDefaults() + local defaultStacks = ns.Addon.db.defaults.profile.extraIcons.itemStacks + getProfile().extraIcons.itemStacks = ns.CloneValue(defaultStacks) + ItemStacksOptions._selectedStackId = nil + ItemStacksOptions._draftState.idText = nil + doAction() +end + ItemStacksOptions.page = { + onDefault = resetItemStacksToDefaults, key = "itemStacks", name = L["ITEM_STACKS"], onShow = function() diff --git a/UI/OptionUtil.lua b/UI/OptionUtil.lua index a2181989..c1181ee6 100644 --- a/UI/OptionUtil.lua +++ b/UI/OptionUtil.lua @@ -604,7 +604,7 @@ function OptionUtil.MakeConfirmDialog(text, button1, button2) } end -local function getPopupEditBox(frame) return frame and frame.editBox end +local function getPopupEditBox(frame) return frame and (frame.EditBox or frame.editBox) end local function trimDialogText(text) return strtrim(text or "") @@ -668,31 +668,3 @@ function OptionUtil.MakeTextInputDialog(text, button1, button2) } end -local function formatResetPageButton(pageName) - local format = L["RESET_PAGE_SETTINGS"] - if type(format) ~= "string" or not format:find("%%s") then - format = "Reset %s settings" - end - return format:format(pageName or "") -end - -function OptionUtil.ConfirmPageDefaultsReset(pageName, onAccept) - local addon = ns.Addon - if addon and addon.ShowConfirmDialog then - addon:ShowConfirmDialog( - "ECM_CONFIRM_RESET_SETTINGS_PAGE", - L["RESET_PAGE_CONFIRM"], - formatResetPageButton(pageName), - L["DONT_RESET"], - onAccept - ) - return - end - - StaticPopupDialogs["ECM_CONFIRM_RESET_SETTINGS_PAGE"] = OptionUtil.MakeConfirmDialog( - L["RESET_PAGE_CONFIRM"], - formatResetPageButton(pageName), - L["DONT_RESET"] - ) - StaticPopup_Show("ECM_CONFIRM_RESET_SETTINGS_PAGE", nil, nil, { onAccept = onAccept }) -end diff --git a/UI/Options.lua b/UI/Options.lua index eeb93b27..a4b50724 100644 --- a/UI/Options.lua +++ b/UI/Options.lua @@ -16,6 +16,11 @@ local LSB = LibStub("LibSettingsBuilder-1.0") ns.Settings = LSB:New({ name = L["ADDON_NAME"], + defaultsConfirmation = { + text = L["RESET_PAGE_TO_DEFAULTS_CONFIRM"], + button1 = L["RESET"], + button2 = L["DONT_RESET"], + }, store = function() return ns.Addon.db and ns.Addon.db.profile end, @@ -27,9 +32,6 @@ ns.Settings = LSB:New({ ns.Runtime.ScheduleLayoutUpdate(0, "OptionsChanged") end end, - defaultsConfirmation = function(pageName, onAccept) - ns.OptionUtil.ConfirmPageDefaultsReset(pageName, onAccept) - end, }) ns.SettingsBuilder = ns.Settings diff --git a/UI/ProfileOptions.lua b/UI/ProfileOptions.lua index 6364f52f..b83e385b 100644 --- a/UI/ProfileOptions.lua +++ b/UI/ProfileOptions.lua @@ -5,20 +5,27 @@ local _, ns = ... local L = ns.L +local function getPopupEditBox(frame) return frame and (frame.EditBox or frame.editBox) end + StaticPopupDialogs["ECM_NEW_PROFILE"] = { text = L["NEW_PROFILE_PROMPT"], button1 = L["CREATE"], button2 = L["DONT_CREATE"], hasEditBox = true, OnAccept = function(self, data) - local name = strtrim(self.editBox:GetText()) + local editBox = getPopupEditBox(self) + local name = strtrim(editBox and editBox:GetText() or "") if name ~= "" and data and data.onAccept then data.onAccept(name) end end, OnShow = function(self) - self.editBox:SetText(UnitName("player") .. " - " .. date("%H%M%S")) - self.editBox:HighlightText() + local editBox = getPopupEditBox(self) + if not editBox then + return + end + editBox:SetText(UnitName("player") .. " - " .. date("%H%M%S")) + editBox:HighlightText() end, EditBoxOnEnterPressed = function(self) local parent = self:GetParent() @@ -134,15 +141,18 @@ local deleteProfileRow, getDeleteProfile, resetDeleteProfile = createProfilePick otherProfilesGenerator ) +local function resetCurrentProfile() + ns.Addon.db:ResetProfile() + ns.Settings:GetPage("profile", "main"):Refresh() +end + ProfileOptions.key = "profile" ProfileOptions.name = L["PROFILES"] ProfileOptions.pages = { { key = "main", - onDefault = function() end, - onDefaultEnabled = function() - return false - end, + onDefault = resetCurrentProfile, + defaultsConfirmText = L["RESET_PROFILE_TO_DEFAULTS_CONFIRM"], rows = { { type = "header", name = L["ACTIVE_PROFILE"] }, { @@ -163,7 +173,9 @@ ProfileOptions.pages = { return ns.Addon.db:GetCurrentProfile() end, set = function(value) - ns.Addon.db:SetProfile(value) + if value and value ~= "" then + ns.Addon.db:SetProfile(value) + end end, onSet = function(ctx) ctx.page:Refresh() @@ -224,45 +236,34 @@ ProfileOptions.pages = { }) end, }, - { type = "header", name = L["RESET"] }, { - type = "button", - name = L["RESET_PROFILE"], - buttonText = L["RESET_PROFILE_BUTTON"], - tooltip = L["RESET_PROFILE_DESC"], - confirm = L["RESET_PROFILE_CONFIRM"], - onClick = function(ctx) - ns.Addon.db:ResetProfile() - ctx.page:Refresh() - end, - }, - { type = "header", name = L["IMPORT_EXPORT"] }, - { - type = "button", - name = L["IMPORT_PROFILE"], - buttonText = L["IMPORT"], - tooltip = L["IMPORT_DESC"], - onClick = function() - if InCombatLockdown() then - ns.Print(L["CANNOT_IMPORT_IN_COMBAT"]) - return - end - ns.Addon:ShowImportDialog() - end, - }, - { - type = "button", - name = L["EXPORT_PROFILE"], - buttonText = L["EXPORT"], - tooltip = L["EXPORT_DESC"], - onClick = function() - local exportString, err = ns.ImportExport.ExportCurrentProfile() - if not exportString then - ns.Print(string.format(L["EXPORT_FAILED"], err or "Unknown error")) - return - end - ns.Addon:ShowExportDialog(exportString) - end, + type = "pageActions", + attachToCategoryHeader = true, + actions = { + { + text = L["IMPORT"], + tooltip = L["IMPORT_DESC"], + onClick = function() + if InCombatLockdown() then + ns.Print(L["CANNOT_IMPORT_IN_COMBAT"]) + return + end + ns.Addon:ShowImportDialog() + end, + }, + { + text = L["EXPORT"], + tooltip = L["EXPORT_DESC"], + onClick = function() + local exportString, err = ns.ImportExport.ExportCurrentProfile() + if not exportString then + ns.Print(string.format(L["EXPORT_FAILED"], err or "Unknown error")) + return + end + ns.Addon:ShowExportDialog(exportString) + end, + }, + }, }, }, }, diff --git a/UI/ResourceBarOptions.lua b/UI/ResourceBarOptions.lua index 9ca546c3..a0d4ae2f 100644 --- a/UI/ResourceBarOptions.lua +++ b/UI/ResourceBarOptions.lua @@ -130,7 +130,7 @@ rows[#rows + 1] = { ResourceBarOptions.key = "resourceBar" ResourceBarOptions.name = L["RESOURCE_BAR"] -ResourceBarOptions.disabled = ns.IsDeathKnight +ResourceBarOptions.disabled = ns.ClassUtil.IsDeathKnight ResourceBarOptions.pages = { { key = "main", diff --git a/UI/RuneBarOptions.lua b/UI/RuneBarOptions.lua index 768beaa0..4703d2b0 100644 --- a/UI/RuneBarOptions.lua +++ b/UI/RuneBarOptions.lua @@ -30,7 +30,7 @@ for _, row in ipairs(ns.OptionUtil.CreateBarRows(isDisabled, { showText = false, rows[#rows + 1] = row end -if not ns.IsDeathKnight() then +if not ns.ClassUtil.IsDeathKnight() then table.insert(rows, 1, { type = "subheader", name = L["DK_ONLY_WARNING"], @@ -79,7 +79,7 @@ rows[#rows + 1] = { RuneBarOptions.key = "runeBar" RuneBarOptions.name = L["RUNE_BAR"] RuneBarOptions.disabled = function() - return not ns.IsDeathKnight() + return not ns.ClassUtil.IsDeathKnight() end RuneBarOptions.pages = { { diff --git a/UI/SpellColorsPage.lua b/UI/SpellColorsPage.lua index 0b5fb574..42bdeb5f 100644 --- a/UI/SpellColorsPage.lua +++ b/UI/SpellColorsPage.lua @@ -3,7 +3,6 @@ -- Licensed under the GNU General Public License v3.0 local _, ns = ... -local C = ns.Constants local L = ns.L local REMOVE_STALE_SPELL_COLORS_POPUP = "ECM_CONFIRM_REMOVE_STALE_SPELL_COLORS" @@ -29,7 +28,7 @@ end local function getScopeDefaultColor(scope) local defaults = ns.defaults and ns.defaults.profile and ns.defaults.profile[scope] local color = defaults and defaults.colors and defaults.colors.defaultColor - return color or C.BUFFBARS_DEFAULT_COLOR + return color or ns.Constants.BUFFBARS_DEFAULT_COLOR end ---@param entries { key: ECM_SpellColorKey }[]|nil @@ -301,13 +300,6 @@ function SpellColorsPage:OnInitialize() addon:RegisterEvent("PLAYER_REGEN_ENABLED", combatRefreshCallback) end ----@param section table -local function resetSpellColorSection(section) - local spellColors = getSpellColors(section.scope) - spellColors:ClearCurrentSpecColors() - spellColors:SetDefaultColor(getScopeDefaultColor(section.scope)) -end - local function reconcileSpellColors() ns.Addon:ConfirmReloadUI(L["SPELL_COLORS_SECRET_NAMES_DESC"]) end @@ -430,6 +422,13 @@ local function canMaintainAnySpellColorSection() end) end +---@param section table +local function resetSpellColorSection(section) + local spellColors = getSpellColors(section.scope) + spellColors:ClearCurrentSpecColors() + spellColors:SetDefaultColor(getScopeDefaultColor(section.scope)) +end + ---@param refreshPage fun() local function resetAllSpellColors(refreshPage) local didReset = false