From d57ed1fa59c90f0a6385c7ba1120ad092d94c07f Mon Sep 17 00:00:00 2001 From: RubyJ Date: Sat, 14 Mar 2026 13:56:08 -0400 Subject: [PATCH 1/7] feat: in-game settings panel for cooldown duration configuration --- CooldownTracker.toc | 3 +- Core.lua | 5 + Data.lua | 130 ++++++++++++++------------ Settings.lua | 221 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 298 insertions(+), 61 deletions(-) create mode 100644 Settings.lua diff --git a/CooldownTracker.toc b/CooldownTracker.toc index 08b870c..0f2f2c0 100644 --- a/CooldownTracker.toc +++ b/CooldownTracker.toc @@ -2,9 +2,10 @@ ## Title: Healer Cooldown Tracker ## Notes: Manually track healer cooldowns for raid leaders. Click to start a timer when a healer calls out on comms. ## Author: Reube -## Version: 1.0.0 +## Version: 1.1.0 ## SavedVariables: CooldownTrackerDB Data.lua UI.lua +Settings.lua Core.lua diff --git a/Core.lua b/Core.lua index f230beb..88ae64a 100644 --- a/Core.lua +++ b/Core.lua @@ -6,6 +6,7 @@ -- Slash commands: -- /cdt — toggle the tracker window -- /cdt reset — reset all running timers +-- /cdt settings — open the settings panel -------------------------------------------------------------------------------- local AddonName, CT = ... @@ -25,6 +26,8 @@ eventFrame:SetScript("OnEvent", function(_, event, name) -- Build the UI (defined in UI.lua) then restore the saved position CT:BuildUI() CT:RestorePosition() + -- Register the settings panel (defined in Settings.lua) + CT:InitSettings() end end) @@ -40,6 +43,8 @@ SlashCmdList["COOLDOWNTRACKER"] = function(msg) CT.activeTimers[cd.id] = nil end print("|cffaaddff[CooldownTracker]|r All timers reset.") + elseif cmd == "settings" then + CT:OpenSettings() else if CT.mainFrame:IsShown() then CT.mainFrame:Hide() diff --git a/Data.lua b/Data.lua index 8a451f1..ce19d6b 100644 --- a/Data.lua +++ b/Data.lua @@ -2,12 +2,13 @@ -- Data.lua -- Cooldown definitions. Add new healer abilities here — no other file needs -- to change. Each entry requires: --- id - unique string key --- class - class name (display only) --- name - ability name (display only) --- duration - cooldown length in seconds --- icon - WoW texture path (Interface\Icons\...) --- r, g, b - accent colour using WoW class colours (0-1 range) +-- id - unique string key +-- class - class name (display only) +-- name - ability name (display only) +-- duration - cooldown length in seconds (may be overridden by user) +-- defaultDuration - original duration used by the Reset button in settings +-- icon - WoW texture path (Interface\Icons\...) +-- r, g, b - accent colour using WoW class colours (0-1 range) -------------------------------------------------------------------------------- local AddonName, CT = ... @@ -17,95 +18,104 @@ CT.COOLDOWNS = { -- Druid (class colour: orange) -- ------------------------------------------------------------------------- { - id = "druid_convoke", - class = "Druid", - name = "Convoke the Spirits", - duration = 60, - icon = "Interface\\Icons\\ability_ardenweald_druid", - r = 1.0, g = 0.49, b = 0.04, + id = "druid_convoke", + class = "Druid", + name = "Convoke the Spirits", + duration = 60, + defaultDuration = 60, + icon = "Interface\\Icons\\ability_ardenweald_druid", + r = 1.0, g = 0.49, b = 0.04, }, { - id = "druid_tranquility", - class = "Druid", - name = "Tranquility", - duration = 180, - icon = "Interface\\Icons\\spell_nature_tranquility", - r = 1.0, g = 0.49, b = 0.04, + id = "druid_tranquility", + class = "Druid", + name = "Tranquility", + duration = 180, + defaultDuration = 180, + icon = "Interface\\Icons\\spell_nature_tranquility", + r = 1.0, g = 0.49, b = 0.04, }, -- ------------------------------------------------------------------------- -- Paladin (class colour: pink) -- ------------------------------------------------------------------------- { - id = "paladin_avenging_wrath", - class = "Paladin", - name = "Avenging Wrath", - duration = 120, - icon = "Interface\\Icons\\spell_holy_avenginewrath", - r = 0.96, g = 0.55, b = 0.73, + id = "paladin_avenging_wrath", + class = "Paladin", + name = "Avenging Wrath", + duration = 120, + defaultDuration = 120, + icon = "Interface\\Icons\\spell_holy_avenginewrath", + r = 0.96, g = 0.55, b = 0.73, }, { - id = "paladin_aura_mastery", - class = "Paladin", - name = "Aura Mastery", - duration = 120, - icon = "Interface\\Icons\\spell_holy_auramastery", - r = 0.96, g = 0.55, b = 0.73, + id = "paladin_aura_mastery", + class = "Paladin", + name = "Aura Mastery", + duration = 120, + defaultDuration = 120, + icon = "Interface\\Icons\\spell_holy_auramastery", + r = 0.96, g = 0.55, b = 0.73, }, -- ------------------------------------------------------------------------- -- Shaman (class colour: blue) -- ------------------------------------------------------------------------- { - id = "shaman_ascendance", - class = "Shaman", - name = "Ascendance", - duration = 180, - icon = "Interface\\Icons\\spell_fire_elementaldevastation", - r = 0.0, g = 0.44, b = 0.87, + id = "shaman_ascendance", + class = "Shaman", + name = "Ascendance", + duration = 180, + defaultDuration = 180, + icon = "Interface\\Icons\\spell_fire_elementaldevastation", + r = 0.0, g = 0.44, b = 0.87, }, { - id = "shaman_spirit_link", - class = "Shaman", - name = "Spirit Link Totem", - duration = 180, - icon = "Interface\\Icons\\spell_shaman_spiritlink", - r = 0.0, g = 0.44, b = 0.87, + id = "shaman_spirit_link", + class = "Shaman", + name = "Spirit Link Totem", + duration = 180, + defaultDuration = 180, + icon = "Interface\\Icons\\spell_shaman_spiritlink", + r = 0.0, g = 0.44, b = 0.87, }, -- ------------------------------------------------------------------------- -- Warrior (class colour: tan/gold) -- ------------------------------------------------------------------------- { - id = "warrior_rallying_cry", - class = "Warrior", - name = "Rallying Cry", - duration = 180, - icon = "Interface\\Icons\\ability_warrior_rallyingcry", - r = 0.78, g = 0.61, b = 0.23, + id = "warrior_rallying_cry", + class = "Warrior", + name = "Rallying Cry", + duration = 180, + defaultDuration = 180, + icon = "Interface\\Icons\\ability_warrior_rallyingcry", + r = 0.78, g = 0.61, b = 0.23, }, -- ------------------------------------------------------------------------- -- Evoker (class colour: teal) -- ------------------------------------------------------------------------- { - id = "evoker_zephyr", - class = "Evoker", - name = "Zephyr", - duration = 120, - icon = "Interface\\Icons\\ability_evoker_hoverblack", - r = 0.2, g = 0.58, b = 0.5, + id = "evoker_zephyr", + class = "Evoker", + name = "Zephyr", + duration = 120, + defaultDuration = 120, + icon = "Interface\\Icons\\ability_evoker_hoverblack", + r = 0.2, g = 0.58, b = 0.5, }, -- ------------------------------------------------------------------------- -- Death Knight (class colour: red) -- ------------------------------------------------------------------------- { - id = "dk_amz", - class = "Death Knight", - name = "Anti-Magic Zone", - duration = 120, - icon = "Interface\\Icons\\spell_deathknight_antimagiczone", - r = 0.77, g = 0.12, b = 0.23, + id = "dk_amz", + class = "Death Knight", + name = "Anti-Magic Zone", + duration = 120, + defaultDuration = 120, + icon = "Interface\\Icons\\spell_deathknight_antimagiczone", + r = 0.77, g = 0.12, b = 0.23, }, } diff --git a/Settings.lua b/Settings.lua new file mode 100644 index 0000000..f674a11 --- /dev/null +++ b/Settings.lua @@ -0,0 +1,221 @@ +-------------------------------------------------------------------------------- +-- Settings.lua +-- In-game configuration panel for CooldownTracker, registered with WoW's +-- modern Settings API (Escape -> Options -> AddOns -> Healer Cooldown Tracker). +-- +-- Exposes: +-- CT:InitSettings() - called once on ADDON_LOADED to apply saved values +-- and register the panel +-- CT:OpenSettings() - programmatically opens the panel (slash command) +-------------------------------------------------------------------------------- + +local AddonName, CT = ... + +-- --------------------------------------------------------------------------- +-- Helpers +-- --------------------------------------------------------------------------- + +-- Returns the effective duration for a cooldown: saved custom value or default. +local function GetEffectiveDuration(cd) + local custom = CooldownTrackerDB and CooldownTrackerDB.customDurations + return (custom and custom[cd.id]) or cd.defaultDuration +end + +-- Applies all saved custom durations to the live CT.COOLDOWNS table. +local function ApplyCustomDurations() + local custom = CooldownTrackerDB.customDurations or {} + for _, cd in ipairs(CT.COOLDOWNS) do + cd.duration = custom[cd.id] or cd.defaultDuration + end +end + +-- --------------------------------------------------------------------------- +-- Panel construction +-- --------------------------------------------------------------------------- + +local PANEL_ROW_HEIGHT = 42 +local PANEL_ROW_PAD = 2 +local ICON_SIZE = 28 +local EDIT_WIDTH = 64 + +local function CreateSettingsPanel() + -- WoW hands this frame to the Settings system; it should be unsized here. + local panel = CreateFrame("Frame") + panel.name = "Healer Cooldown Tracker" + + -- ----- Header ----------------------------------------------------------- + local title = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge") + title:SetPoint("TOPLEFT", 16, -16) + title:SetText("|cffaaddffHealer Cooldown Tracker|r — Settings") + + local desc = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + desc:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6) + desc:SetText("Override cooldown durations to match your raiders' current talents.\nPress Enter in a duration box to confirm. Changes apply immediately.") + desc:SetJustifyH("LEFT") + + -- Column header labels + local hdrAbility = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + hdrAbility:SetPoint("TOPLEFT", desc, "BOTTOMLEFT", ICON_SIZE + 12, -10) + hdrAbility:SetText("Ability") + hdrAbility:SetTextColor(0.7, 0.7, 0.7) + + local hdrDur = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + hdrDur:SetPoint("LEFT", hdrAbility, "LEFT", 260, 0) + hdrDur:SetText("Cooldown (seconds)") + hdrDur:SetTextColor(0.7, 0.7, 0.7) + + local divider = panel:CreateTexture(nil, "ARTWORK") + divider:SetHeight(1) + divider:SetPoint("TOPLEFT", desc, "BOTTOMLEFT", 0, -26) + divider:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -16, -26 - desc:GetHeight() - title:GetHeight() - 22) + -- Fix: anchor relative to a known point + divider:SetPoint("TOPLEFT", hdrAbility, "BOTTOMLEFT", -ICON_SIZE - 12, -4) + divider:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -16, 0) + divider:SetColorTexture(0.3, 0.3, 0.4, 0.6) + + -- ----- Scroll frame ----------------------------------------------------- + local scrollFrame = CreateFrame("ScrollFrame", "CTSettingsScrollFrame", panel, "UIPanelScrollFrameTemplate") + scrollFrame:SetPoint("TOPLEFT", divider, "BOTTOMLEFT", 0, -6) + scrollFrame:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -28, 50) + + local contentHeight = #CT.COOLDOWNS * (PANEL_ROW_HEIGHT + PANEL_ROW_PAD) + local content = CreateFrame("Frame", nil, scrollFrame) + content:SetSize(scrollFrame:GetWidth() or 560, contentHeight) + scrollFrame:SetScrollChild(content) + + -- Track edit boxes so Reset All can update them + local editBoxes = {} + + for i, cd in ipairs(CT.COOLDOWNS) do + local yOff = -((i - 1) * (PANEL_ROW_HEIGHT + PANEL_ROW_PAD)) + + local row = CreateFrame("Frame", nil, content) + row:SetSize(content:GetWidth(), PANEL_ROW_HEIGHT) + row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, yOff) + + -- Alternating background + local bg = row:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints(row) + if i % 2 == 0 then + bg:SetColorTexture(0.12, 0.12, 0.14, 0.4) + else + bg:SetColorTexture(0.07, 0.07, 0.09, 0.25) + end + + -- Accent left strip + local strip = row:CreateTexture(nil, "BACKGROUND") + strip:SetSize(3, PANEL_ROW_HEIGHT) + strip:SetPoint("LEFT", row, "LEFT", 0, 0) + strip:SetColorTexture(cd.r, cd.g, cd.b, 0.9) + + -- Icon + local icon = row:CreateTexture(nil, "ARTWORK") + icon:SetSize(ICON_SIZE, ICON_SIZE) + icon:SetPoint("LEFT", row, "LEFT", 8, 0) + icon:SetTexture(cd.icon) + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + + -- Ability name + local nameLabel = row:CreateFontString(nil, "OVERLAY", "GameFontNormal") + nameLabel:SetPoint("TOPLEFT", icon, "TOPRIGHT", 8, -2) + nameLabel:SetText(cd.name) + nameLabel:SetTextColor(1, 1, 1) + + -- Class tag + local classLabel = row:CreateFontString(nil, "OVERLAY") + classLabel:SetPoint("BOTTOMLEFT", icon, "BOTTOMRIGHT", 8, 2) + classLabel:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE") + classLabel:SetText(cd.class) + classLabel:SetTextColor(cd.r, cd.g, cd.b) + + -- Duration edit box + local editBox = CreateFrame("EditBox", "CTSettingsEdit_" .. cd.id, row, "InputBoxTemplate") + editBox:SetSize(EDIT_WIDTH, 24) + editBox:SetPoint("LEFT", row, "LEFT", 280, 0) + editBox:SetAutoFocus(false) + editBox:SetNumeric(true) + editBox:SetMaxLetters(4) + editBox:SetText(tostring(GetEffectiveDuration(cd))) + + local function CommitEdit() + local val = tonumber(editBox:GetText()) + if val and val > 0 then + CooldownTrackerDB.customDurations = CooldownTrackerDB.customDurations or {} + CooldownTrackerDB.customDurations[cd.id] = (val ~= cd.defaultDuration) and val or nil + cd.duration = val + else + editBox:SetText(tostring(GetEffectiveDuration(cd))) + end + editBox:ClearFocus() + end + editBox:SetScript("OnEnterPressed", CommitEdit) + editBox:SetScript("OnEscapePressed", function() + editBox:SetText(tostring(GetEffectiveDuration(cd))) + editBox:ClearFocus() + end) + editBox:SetScript("OnEditFocusLost", CommitEdit) + + editBoxes[cd.id] = editBox + + -- "Default" reset button + local resetBtn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate") + resetBtn:SetSize(70, 22) + resetBtn:SetPoint("LEFT", editBox, "RIGHT", 10, 0) + resetBtn:SetText("Default") + resetBtn:SetScript("OnClick", function() + CooldownTrackerDB.customDurations = CooldownTrackerDB.customDurations or {} + CooldownTrackerDB.customDurations[cd.id] = nil + cd.duration = cd.defaultDuration + editBox:SetText(tostring(cd.defaultDuration)) + end) + + -- Tooltip on the edit box + editBox:SetScript("OnEnter", function() + GameTooltip:SetOwner(editBox, "ANCHOR_RIGHT") + GameTooltip:SetText("Cooldown Duration") + GameTooltip:AddLine("Enter the cooldown in seconds.", 1, 1, 1) + GameTooltip:AddLine(string.format("Default: %d seconds", cd.defaultDuration), 0.8, 0.8, 0.8) + GameTooltip:AddLine("Press Enter to confirm, Escape to cancel.", 0.6, 0.6, 0.6) + GameTooltip:Show() + end) + editBox:SetScript("OnLeave", function() GameTooltip:Hide() end) + end + + -- ----- Reset All button ------------------------------------------------- + local resetAllBtn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate") + resetAllBtn:SetSize(110, 26) + resetAllBtn:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", 16, 16) + resetAllBtn:SetText("Reset All Defaults") + resetAllBtn:SetScript("OnClick", function() + CooldownTrackerDB.customDurations = {} + for _, cd in ipairs(CT.COOLDOWNS) do + cd.duration = cd.defaultDuration + if editBoxes[cd.id] then + editBoxes[cd.id]:SetText(tostring(cd.defaultDuration)) + end + end + print("|cffaaddff[CooldownTracker]|r All durations reset to defaults.") + end) + + return panel +end + +-- --------------------------------------------------------------------------- +-- Public API +-- --------------------------------------------------------------------------- + +function CT:InitSettings() + CooldownTrackerDB.customDurations = CooldownTrackerDB.customDurations or {} + ApplyCustomDurations() + + local panel = CreateSettingsPanel() + local category = Settings.RegisterCanvasLayoutCategory(panel, "Healer Cooldown Tracker") + Settings.RegisterAddOnCategory(category) + CT.settingsCategory = category +end + +function CT:OpenSettings() + if CT.settingsCategory then + Settings.OpenToCategory(CT.settingsCategory:GetID()) + end +end From aad752547cd66dae5723bc4de1359588bcc918f9 Mon Sep 17 00:00:00 2001 From: RubyJ Date: Sat, 14 Mar 2026 14:00:44 -0400 Subject: [PATCH 2/7] fix: settings panel rows invisible due to zero-width scroll content --- Settings.lua | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/Settings.lua b/Settings.lua index f674a11..5e22a82 100644 --- a/Settings.lua +++ b/Settings.lua @@ -37,9 +37,9 @@ local PANEL_ROW_HEIGHT = 42 local PANEL_ROW_PAD = 2 local ICON_SIZE = 28 local EDIT_WIDTH = 64 +local CONTENT_WIDTH = 520 -- fixed width; the Settings canvas is ~550px local function CreateSettingsPanel() - -- WoW hands this frame to the Settings system; it should be unsized here. local panel = CreateFrame("Frame") panel.name = "Healer Cooldown Tracker" @@ -50,6 +50,7 @@ local function CreateSettingsPanel() local desc = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") desc:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6) + desc:SetWidth(CONTENT_WIDTH) desc:SetText("Override cooldown durations to match your raiders' current talents.\nPress Enter in a duration box to confirm. Changes apply immediately.") desc:SetJustifyH("LEFT") @@ -66,21 +67,18 @@ local function CreateSettingsPanel() local divider = panel:CreateTexture(nil, "ARTWORK") divider:SetHeight(1) - divider:SetPoint("TOPLEFT", desc, "BOTTOMLEFT", 0, -26) - divider:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -16, -26 - desc:GetHeight() - title:GetHeight() - 22) - -- Fix: anchor relative to a known point - divider:SetPoint("TOPLEFT", hdrAbility, "BOTTOMLEFT", -ICON_SIZE - 12, -4) - divider:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -16, 0) + divider:SetPoint("TOPLEFT", hdrAbility, "BOTTOMLEFT", -ICON_SIZE - 12, -4) + divider:SetWidth(CONTENT_WIDTH) divider:SetColorTexture(0.3, 0.3, 0.4, 0.6) -- ----- Scroll frame ----------------------------------------------------- local scrollFrame = CreateFrame("ScrollFrame", "CTSettingsScrollFrame", panel, "UIPanelScrollFrameTemplate") - scrollFrame:SetPoint("TOPLEFT", divider, "BOTTOMLEFT", 0, -6) + scrollFrame:SetPoint("TOPLEFT", divider, "BOTTOMLEFT", 0, -6) scrollFrame:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", -28, 50) local contentHeight = #CT.COOLDOWNS * (PANEL_ROW_HEIGHT + PANEL_ROW_PAD) local content = CreateFrame("Frame", nil, scrollFrame) - content:SetSize(scrollFrame:GetWidth() or 560, contentHeight) + content:SetSize(CONTENT_WIDTH, contentHeight) scrollFrame:SetScrollChild(content) -- Track edit boxes so Reset All can update them @@ -90,8 +88,9 @@ local function CreateSettingsPanel() local yOff = -((i - 1) * (PANEL_ROW_HEIGHT + PANEL_ROW_PAD)) local row = CreateFrame("Frame", nil, content) - row:SetSize(content:GetWidth(), PANEL_ROW_HEIGHT) - row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, yOff) + row:SetHeight(PANEL_ROW_HEIGHT) + row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, yOff) + row:SetPoint("TOPRIGHT", content, "TOPRIGHT", 0, yOff) -- Alternating background local bg = row:CreateTexture(nil, "BACKGROUND") From a546b1e295cb2966b27ab61cff51e9c223343f46 Mon Sep 17 00:00:00 2001 From: RubyJ Date: Sat, 14 Mar 2026 14:04:12 -0400 Subject: [PATCH 3/7] fix: settings edit boxes blank on open due to OnEditFocusLost during init --- Settings.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Settings.lua b/Settings.lua index 5e22a82..5ce5828 100644 --- a/Settings.lua +++ b/Settings.lua @@ -152,7 +152,6 @@ local function CreateSettingsPanel() editBox:SetText(tostring(GetEffectiveDuration(cd))) editBox:ClearFocus() end) - editBox:SetScript("OnEditFocusLost", CommitEdit) editBoxes[cd.id] = editBox @@ -196,6 +195,16 @@ local function CreateSettingsPanel() print("|cffaaddff[CooldownTracker]|r All durations reset to defaults.") end) + -- Refresh all edit boxes every time the panel is shown, ensuring they + -- are always populated (avoids blank boxes from init-time focus events). + panel:SetScript("OnShow", function() + for _, cd in ipairs(CT.COOLDOWNS) do + if editBoxes[cd.id] then + editBoxes[cd.id]:SetText(tostring(GetEffectiveDuration(cd))) + end + end + end) + return panel end From 5ce300dbda76f4e441329d2697b18e963a3e5dc6 Mon Sep 17 00:00:00 2001 From: RubyJ Date: Sat, 14 Mar 2026 14:11:19 -0400 Subject: [PATCH 4/7] fix: edit boxes blank due to SetNumeric(true) incompatibility with SetText --- Settings.lua | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/Settings.lua b/Settings.lua index 5ce5828..a3f0214 100644 --- a/Settings.lua +++ b/Settings.lua @@ -84,6 +84,16 @@ local function CreateSettingsPanel() -- Track edit boxes so Reset All can update them local editBoxes = {} + -- Populates every edit box from the current effective duration. + -- Called on creation and via OnShow. + local function RefreshAllEditBoxes() + for _, cd in ipairs(CT.COOLDOWNS) do + if editBoxes[cd.id] then + editBoxes[cd.id]:SetText(tostring(GetEffectiveDuration(cd))) + end + end + end + for i, cd in ipairs(CT.COOLDOWNS) do local yOff = -((i - 1) * (PANEL_ROW_HEIGHT + PANEL_ROW_PAD)) @@ -132,8 +142,8 @@ local function CreateSettingsPanel() editBox:SetSize(EDIT_WIDTH, 24) editBox:SetPoint("LEFT", row, "LEFT", 280, 0) editBox:SetAutoFocus(false) - editBox:SetNumeric(true) editBox:SetMaxLetters(4) + -- Plain text mode — SetNumeric causes SetText to be unreliable editBox:SetText(tostring(GetEffectiveDuration(cd))) local function CommitEdit() @@ -195,15 +205,11 @@ local function CreateSettingsPanel() print("|cffaaddff[CooldownTracker]|r All durations reset to defaults.") end) - -- Refresh all edit boxes every time the panel is shown, ensuring they - -- are always populated (avoids blank boxes from init-time focus events). - panel:SetScript("OnShow", function() - for _, cd in ipairs(CT.COOLDOWNS) do - if editBoxes[cd.id] then - editBoxes[cd.id]:SetText(tostring(GetEffectiveDuration(cd))) - end - end - end) + -- Refresh on show (belt-and-suspenders alongside the direct call below) + panel:SetScript("OnShow", RefreshAllEditBoxes) + + -- Populate all boxes immediately at creation time + RefreshAllEditBoxes() return panel end From ac457d7f4da9dd86177906601219a8e1ef69ec44 Mon Sep 17 00:00:00 2001 From: RubyJ Date: Sat, 14 Mar 2026 14:13:05 -0400 Subject: [PATCH 5/7] fix: replace InputBoxTemplate with manual EditBox + HookScript OnShow --- Settings.lua | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/Settings.lua b/Settings.lua index a3f0214..f9816ac 100644 --- a/Settings.lua +++ b/Settings.lua @@ -137,14 +137,31 @@ local function CreateSettingsPanel() classLabel:SetText(cd.class) classLabel:SetTextColor(cd.r, cd.g, cd.b) - -- Duration edit box - local editBox = CreateFrame("EditBox", "CTSettingsEdit_" .. cd.id, row, "InputBoxTemplate") + -- Duration edit box — built manually (no template) to avoid + -- InputBoxTemplate's OnShow clearing our text. + local editBox = CreateFrame("EditBox", "CTSettingsEdit_" .. cd.id, row, "BackdropTemplate") editBox:SetSize(EDIT_WIDTH, 24) editBox:SetPoint("LEFT", row, "LEFT", 280, 0) editBox:SetAutoFocus(false) editBox:SetMaxLetters(4) - -- Plain text mode — SetNumeric causes SetText to be unreliable + editBox:SetFontObject("ChatFontNormal") + editBox:SetJustifyH("CENTER") + if editBox.SetBackdrop then + editBox:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + editBox:SetBackdropColor(0.1, 0.1, 0.1, 0.8) + editBox:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.8) + end + editBox:SetTextInsets(4, 4, 2, 2) editBox:SetText(tostring(GetEffectiveDuration(cd))) + -- Re-populate after any inherited show handlers fire + editBox:HookScript("OnShow", function(self) + self:SetText(tostring(GetEffectiveDuration(cd))) + end) local function CommitEdit() local val = tonumber(editBox:GetText()) From 0e0fa50f66b6b7bcc280db8356a736750b675abf Mon Sep 17 00:00:00 2001 From: RubyJ Date: Sat, 14 Mar 2026 14:24:33 -0400 Subject: [PATCH 6/7] fix: use C_Timer.After(0) to populate edit boxes after Settings canvas init --- Settings.lua | 47 ++++++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/Settings.lua b/Settings.lua index f9816ac..9d38cd6 100644 --- a/Settings.lua +++ b/Settings.lua @@ -36,8 +36,8 @@ end local PANEL_ROW_HEIGHT = 42 local PANEL_ROW_PAD = 2 local ICON_SIZE = 28 -local EDIT_WIDTH = 64 -local CONTENT_WIDTH = 520 -- fixed width; the Settings canvas is ~550px +local EDIT_WIDTH = 80 +local CONTENT_WIDTH = 520 local function CreateSettingsPanel() local panel = CreateFrame("Frame") @@ -51,7 +51,7 @@ local function CreateSettingsPanel() local desc = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") desc:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6) desc:SetWidth(CONTENT_WIDTH) - desc:SetText("Override cooldown durations to match your raiders' current talents.\nPress Enter in a duration box to confirm. Changes apply immediately.") + desc:SetText("Override cooldown durations to match your raiders' current talents.\nType a new value and press Enter to confirm. Changes apply immediately.") desc:SetJustifyH("LEFT") -- Column header labels @@ -81,17 +81,22 @@ local function CreateSettingsPanel() content:SetSize(CONTENT_WIDTH, contentHeight) scrollFrame:SetScrollChild(content) - -- Track edit boxes so Reset All can update them + -- Track edit boxes so Reset All / refresh can update them local editBoxes = {} - -- Populates every edit box from the current effective duration. - -- Called on creation and via OnShow. + -- Populates every edit box. Uses C_Timer.After(0) to defer to next frame, + -- because WoW's Settings canvas clears EditBox text during its own show + -- sequence, so we must run AFTER that completes. local function RefreshAllEditBoxes() - for _, cd in ipairs(CT.COOLDOWNS) do - if editBoxes[cd.id] then - editBoxes[cd.id]:SetText(tostring(GetEffectiveDuration(cd))) + C_Timer.After(0, function() + for _, cd in ipairs(CT.COOLDOWNS) do + local eb = editBoxes[cd.id] + if eb then + eb:SetText(tostring(GetEffectiveDuration(cd))) + eb:SetCursorPosition(0) + end end - end + end) end for i, cd in ipairs(CT.COOLDOWNS) do @@ -137,13 +142,11 @@ local function CreateSettingsPanel() classLabel:SetText(cd.class) classLabel:SetTextColor(cd.r, cd.g, cd.b) - -- Duration edit box — built manually (no template) to avoid - -- InputBoxTemplate's OnShow clearing our text. + -- Duration edit box — built manually to avoid template init issues local editBox = CreateFrame("EditBox", "CTSettingsEdit_" .. cd.id, row, "BackdropTemplate") editBox:SetSize(EDIT_WIDTH, 24) editBox:SetPoint("LEFT", row, "LEFT", 280, 0) editBox:SetAutoFocus(false) - editBox:SetMaxLetters(4) editBox:SetFontObject("ChatFontNormal") editBox:SetJustifyH("CENTER") if editBox.SetBackdrop then @@ -156,12 +159,10 @@ local function CreateSettingsPanel() editBox:SetBackdropColor(0.1, 0.1, 0.1, 0.8) editBox:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.8) end - editBox:SetTextInsets(4, 4, 2, 2) - editBox:SetText(tostring(GetEffectiveDuration(cd))) - -- Re-populate after any inherited show handlers fire - editBox:HookScript("OnShow", function(self) - self:SetText(tostring(GetEffectiveDuration(cd))) - end) + editBox:SetTextInsets(6, 6, 2, 2) + + -- Text is set by RefreshAllEditBoxes via C_Timer.After — not here, + -- because anything set here gets wiped by the Settings canvas. local function CommitEdit() local val = tonumber(editBox:GetText()) @@ -198,9 +199,8 @@ local function CreateSettingsPanel() editBox:SetScript("OnEnter", function() GameTooltip:SetOwner(editBox, "ANCHOR_RIGHT") GameTooltip:SetText("Cooldown Duration") - GameTooltip:AddLine("Enter the cooldown in seconds.", 1, 1, 1) + GameTooltip:AddLine("Type a duration in seconds and press Enter.", 1, 1, 1) GameTooltip:AddLine(string.format("Default: %d seconds", cd.defaultDuration), 0.8, 0.8, 0.8) - GameTooltip:AddLine("Press Enter to confirm, Escape to cancel.", 0.6, 0.6, 0.6) GameTooltip:Show() end) editBox:SetScript("OnLeave", function() GameTooltip:Hide() end) @@ -222,12 +222,9 @@ local function CreateSettingsPanel() print("|cffaaddff[CooldownTracker]|r All durations reset to defaults.") end) - -- Refresh on show (belt-and-suspenders alongside the direct call below) + -- When the Settings canvas shows our panel, wait one frame then fill boxes panel:SetScript("OnShow", RefreshAllEditBoxes) - -- Populate all boxes immediately at creation time - RefreshAllEditBoxes() - return panel end From 560719310c6fa29c01ff7d2d79f0bf7405ee572a Mon Sep 17 00:00:00 2001 From: RubyJ Date: Sat, 14 Mar 2026 14:40:50 -0400 Subject: [PATCH 7/7] feat: auto-commit duration on every keystroke via OnTextChanged --- Settings.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Settings.lua b/Settings.lua index 9d38cd6..6c5cad9 100644 --- a/Settings.lua +++ b/Settings.lua @@ -170,12 +170,13 @@ local function CreateSettingsPanel() CooldownTrackerDB.customDurations = CooldownTrackerDB.customDurations or {} CooldownTrackerDB.customDurations[cd.id] = (val ~= cd.defaultDuration) and val or nil cd.duration = val - else - editBox:SetText(tostring(GetEffectiveDuration(cd))) end - editBox:ClearFocus() end - editBox:SetScript("OnEnterPressed", CommitEdit) + editBox:SetScript("OnTextChanged", function(self, userInput) + -- Only commit on actual keystrokes, not programmatic SetText + if userInput then CommitEdit() end + end) + editBox:SetScript("OnEnterPressed", function() editBox:ClearFocus() end) editBox:SetScript("OnEscapePressed", function() editBox:SetText(tostring(GetEffectiveDuration(cd))) editBox:ClearFocus()