From 3667403ba0b85d01b5c37b410b7de84a0ec7cd15 Mon Sep 17 00:00:00 2001 From: Hjalte Thor Date: Wed, 18 Mar 2026 20:21:46 +0100 Subject: [PATCH] Add duration text to the cooldown manager bar glows Add optional countdown duration text for bar glows in both the preview UI and active overlays. Set default values and enable users to toggle it on and off as well as change the looks in the options. --- .../EUI_CooldownManager_Options.lua | 187 +++++++++++++++++- .../EllesmereUICdmBarGlows.lua | 62 ++++++ .../EllesmereUICooldownManager.lua | 2 +- 3 files changed, 248 insertions(+), 3 deletions(-) diff --git a/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua b/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua index 274984e3..ef2accac 100644 --- a/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua +++ b/EllesmereUICooldownManager/EUI_CooldownManager_Options.lua @@ -186,10 +186,16 @@ initFrame:SetScript("OnEvent", function(self) -- Preview glow state tracking local _bgPreviewGlowActive = {} local _bgPreviewGlowOverlays = {} + local _bgPreviewDurTickers = {} -- [pvKey] = { ticker, startTime } local _bgSpellPickerMenu EllesmereUI:RegisterOnHide(function() if _bgSpellPickerMenu then _bgSpellPickerMenu:Hide() end + -- Cancel any running preview countdown tickers + for _, info in pairs(_bgPreviewDurTickers) do + if info.ticker then info.ticker:Cancel() end + end + wipe(_bgPreviewDurTickers) end) local function ShowBarGlowSpellPicker(anchorFrame, barIdx, btnIdx, onChanged) @@ -370,6 +376,11 @@ initFrame:SetScript("OnEvent", function(self) glowColor = { r = 1, g = 0.82, b = 0.1 }, classColor = false, mode = "ACTIVE", + showDuration = false, + durationSize = 12, + durationOffsetX = 0, + durationOffsetY = 0, + durationR = 1, durationG = 1, durationB = 1, } local prefix = BAR_BUTTON_PREFIXES[barIdx] local realBtn = prefix and _G[prefix .. btnIdx] @@ -469,12 +480,16 @@ initFrame:SetScript("OnEvent", function(self) ------------------------------------------------------------------- EllesmereUI:ClearContentHeader() - -- Stop any lingering preview glows + -- Stop any lingering preview glows and countdown tickers for idx, ov in pairs(_bgPreviewGlowOverlays) do ns.StopNativeGlow(ov) end + for _, info in pairs(_bgPreviewDurTickers) do + if info.ticker then info.ticker:Cancel() end + end wipe(_bgPreviewGlowOverlays) wipe(_bgPreviewGlowActive) + wipe(_bgPreviewDurTickers) _glowHeaderBuilder = function(headerFrame, width) -- Re-read current state each build @@ -995,8 +1010,82 @@ initFrame:SetScript("OnEvent", function(self) end end - -- Helper: resolve current glow color and restart preview if active + -- Helper: format and refresh the countdown timer on the preview glow local pvKey = assignKey .. "_" .. aIdx + local function FormatPreviewDuration(remaining) + return string.format("%d", math.ceil(remaining)) + end + + local function RefreshPreviewDuration() + local ov = _bgPreviewGlowOverlays[pvKey] + if not ov or not _bgPreviewGlowActive[pvKey] then + -- Cancel any running ticker + local info = _bgPreviewDurTickers[pvKey] + if info then + if info.ticker then info.ticker:Cancel() end + _bgPreviewDurTickers[pvKey] = nil + end + return + end + local previewBtn = _glowBtnFrames[curBtn] + if not previewBtn then return end + if entry.showDuration then + if not ov._pvDurationText then + ov._pvDurationText = ov:CreateFontString(nil, "OVERLAY") + end + local fs = ov._pvDurationText + fs:SetFont(FONT_PATH, entry.durationSize or 12, "OUTLINE") + fs:SetShadowOffset(0, 0) + fs:ClearAllPoints() + fs:SetPoint("BOTTOMLEFT", previewBtn, "BOTTOMLEFT", + 3 + (entry.durationOffsetX or 0), 4 + (entry.durationOffsetY or 0)) + fs:SetTextColor(entry.durationR or 1, entry.durationG or 1, entry.durationB or 1) + -- Start or continue countdown from 12.0 + local info = _bgPreviewDurTickers[pvKey] + if not info then + local startTime = GetTime() + local DURATION = 12.0 + local ticker + ticker = C_Timer.NewTicker(0.1, function() + if not ov._pvDurationText or not _bgPreviewGlowActive[pvKey] then + ticker:Cancel() + _bgPreviewDurTickers[pvKey] = nil + return + end + local elapsed = GetTime() - startTime + local remaining = DURATION - elapsed + if remaining <= 0 then + -- Loop back to 12 + startTime = GetTime() + remaining = DURATION + local i2 = _bgPreviewDurTickers[pvKey] + if i2 then i2.startTime = startTime end + end + ov._pvDurationText:SetText(FormatPreviewDuration(remaining)) + end) + _bgPreviewDurTickers[pvKey] = { ticker = ticker, startTime = startTime } + fs:SetText(FormatPreviewDuration(DURATION)) + else + -- Ticker already running; just update font/position/color, keep countdown + local remaining = 12.0 - (GetTime() - info.startTime) + if remaining <= 0 then remaining = 12.0 end + remaining = remaining % 12.0 + if remaining <= 0 then remaining = 12.0 end + fs:SetText(FormatPreviewDuration(remaining)) + end + fs:Show() + else + -- Duration disabled: cancel ticker and hide + local info = _bgPreviewDurTickers[pvKey] + if info then + if info.ticker then info.ticker:Cancel() end + _bgPreviewDurTickers[pvKey] = nil + end + if ov._pvDurationText then ov._pvDurationText:Hide() end + end + end + + -- Helper: resolve current glow color and restart preview if active local function RefreshPreviewGlow() if not _bgPreviewGlowActive[pvKey] then return end local ov = _bgPreviewGlowOverlays[pvKey] @@ -1077,6 +1166,13 @@ initFrame:SetScript("OnEvent", function(self) if _bgPreviewGlowActive[pvKey] then ns.StopNativeGlow(ov) _bgPreviewGlowActive[pvKey] = false + -- Stop countdown ticker and hide preview duration text + local durInfo = _bgPreviewDurTickers[pvKey] + if durInfo then + if durInfo.ticker then durInfo.ticker:Cancel() end + _bgPreviewDurTickers[pvKey] = nil + end + if ov._pvDurationText then ov._pvDurationText:Hide() end -- Restore accent border if previewBtn._accentBrd then previewBtn._accentBrd:Show() end else @@ -1092,6 +1188,8 @@ initFrame:SetScript("OnEvent", function(self) _bgPreviewGlowActive[pvKey] = true -- Hide accent border so glow is visible if previewBtn._accentBrd then previewBtn._accentBrd:Hide() end + -- Show preview duration text when showDuration is on + RefreshPreviewDuration() end RefreshEye() end) @@ -1126,6 +1224,91 @@ initFrame:SetScript("OnEvent", function(self) PP.Point(glowSwatch, "RIGHT", toggle, "LEFT", -8, 0) end end + + -- Row 3: Show Duration (with swatch + cog) + local durRow + durRow, h = W:DualRow(parent, y, + { type = "toggle", text = "Show Duration", + getValue = function() return entry.showDuration == true end, + setValue = function(v) + entry.showDuration = v + Refresh() + RefreshPreviewDuration() + EllesmereUI:RefreshPage() + end, + }, + { type = "label", text = "" } + ); y = y - h + + -- Inline color swatch + cog for duration text (on left region of row 3) + do + local leftRgn = durRow._leftRegion + local ctrl = leftRgn and leftRgn._control + + -- Color swatch + local durSwatch + if ctrl and EllesmereUI.BuildColorSwatch then + durSwatch = EllesmereUI.BuildColorSwatch( + leftRgn, durRow:GetFrameLevel() + 3, + function() + return entry.durationR or 1, entry.durationG or 1, entry.durationB or 1 + end, + function(r, g, b) + entry.durationR = r; entry.durationG = g; entry.durationB = b + Refresh() + RefreshPreviewDuration() + EllesmereUI:RefreshPage() + end, + false, 20) + PP.Point(durSwatch, "RIGHT", ctrl, "LEFT", -8, 0) + end + + -- Cog: size + x/y offsets + local _, durCogShow = EllesmereUI.BuildCogPopup({ + title = "Duration Text Settings", + rows = { + { type = "slider", label = "Text Size", min = 6, max = 20, step = 1, + get = function() return entry.durationSize or 12 end, + set = function(v) entry.durationSize = v; Refresh(); RefreshPreviewDuration(); EllesmereUI:RefreshPage() end }, + { type = "slider", label = "X Offset", min = -30, max = 30, step = 1, + get = function() return entry.durationOffsetX or 0 end, + set = function(v) entry.durationOffsetX = v; Refresh(); RefreshPreviewDuration(); EllesmereUI:RefreshPage() end }, + { type = "slider", label = "Y Offset", min = -30, max = 30, step = 1, + get = function() return entry.durationOffsetY or 0 end, + set = function(v) entry.durationOffsetY = v; Refresh(); RefreshPreviewDuration(); EllesmereUI:RefreshPage() end }, + }, + }) + -- Cog button + do + local anchor = durSwatch or ctrl + if anchor and leftRgn then + local cogBtn = CreateFrame("Button", nil, leftRgn) + cogBtn:SetSize(26, 26) + cogBtn:SetPoint("RIGHT", anchor, "LEFT", -8, 0) + cogBtn:SetFrameLevel(leftRgn:GetFrameLevel() + 5) + cogBtn:SetAlpha(0.4) + local cogTex = cogBtn:CreateTexture(nil, "OVERLAY") + cogTex:SetAllPoints() + cogTex:SetTexture(EllesmereUI.RESIZE_ICON) + cogBtn:SetScript("OnEnter", function(self) self:SetAlpha(0.7) end) + cogBtn:SetScript("OnLeave", function(self) self:SetAlpha(0.4) end) + cogBtn:SetScript("OnClick", function(self) durCogShow(self) end) + end + end + + -- Blocking overlay when Show Duration is off + if durSwatch then + local swatchBlock = CreateFrame("Frame", nil, durSwatch) + swatchBlock:SetAllPoints() + swatchBlock:SetFrameLevel(durSwatch:GetFrameLevel() + 10) + swatchBlock:EnableMouse(true) + EllesmereUI.RegisterWidgetRefresh(function() + local on = entry.showDuration == true + durSwatch:SetAlpha(on and 1 or 0.3) + if on then swatchBlock:Hide() else swatchBlock:Show() end + end) + end + end end end end diff --git a/EllesmereUICooldownManager/EllesmereUICdmBarGlows.lua b/EllesmereUICooldownManager/EllesmereUICdmBarGlows.lua index 8a6ceff8..cb66e108 100644 --- a/EllesmereUICooldownManager/EllesmereUICdmBarGlows.lua +++ b/EllesmereUICooldownManager/EllesmereUICdmBarGlows.lua @@ -335,6 +335,23 @@ local function SetupOverlays() overlay:SetAlpha(1) overlay._assignEntry = entry overlay:Show() + + -- Duration text FontString (created once per overlay) + if not overlay._durationText then + local fs = overlay:CreateFontString(nil, "OVERLAY") + overlay._durationText = fs + end + do + local fs = overlay._durationText + local sz = entry.durationSize or 12 + fs:SetFont(EllesmereUI.GetFontPath("cdm"), sz, "OUTLINE") + fs:SetShadowOffset(0, 0) + fs:ClearAllPoints() + fs:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", + 3 + (entry.durationOffsetX or 0), 4 + (entry.durationOffsetY or 0)) + fs:SetTextColor(entry.durationR or 1, entry.durationG or 1, entry.durationB or 1) + fs:Hide() + end activeKeys[key] = true anyActive = true end @@ -418,6 +435,51 @@ UpdateOverlayVisuals = function() end end end + + -- Duration text: update every tick when glow is active + showDuration on + local dfs = overlay._durationText + if dfs then + if shouldGlow and entry.showDuration and mode ~= "MISSING" and spellID and spellID > 0 then + local blzChild = (ns._tickBlizzBuffChildCache and ns._tickBlizzBuffChildCache[spellID]) + or (ns._tickBlizzAllChildCache and ns._tickBlizzAllChildCache[spellID]) + local durText + if blzChild then + local auraID = blzChild.auraInstanceID + local auraUnit = blzChild.auraDataUnit or "player" + if auraID then + local ok, durObj = pcall(C_UnitAuras.GetAuraDuration, auraUnit, auraID) + if ok and durObj and durObj.GetRemainingDuration then + local rok, remaining = pcall(durObj.GetRemainingDuration, durObj) + if rok and remaining then + if issecretvalue and issecretvalue(remaining) then + -- Can't do math on secret values; %.0f rounds to + -- nearest which is close enough to ceil + local fok, fstr = pcall(string.format, "%.0f", remaining) + if fok and fstr then + durText = fstr + end + else + durText = string.format("%d", math.ceil(remaining)) + end + end + end + end + end + if durText then + local isSecret = issecretvalue and issecretvalue(durText) + if isSecret or dfs._lastText ~= durText then + dfs._lastText = not isSecret and durText or nil + dfs:SetText(durText) + end + dfs:Show() + else + dfs._lastText = nil + dfs:Hide() + end + else + dfs:Hide() + end + end end end end diff --git a/EllesmereUICooldownManager/EllesmereUICooldownManager.lua b/EllesmereUICooldownManager/EllesmereUICooldownManager.lua index 9313fc6b..5955430f 100644 --- a/EllesmereUICooldownManager/EllesmereUICooldownManager.lua +++ b/EllesmereUICooldownManager/EllesmereUICooldownManager.lua @@ -993,7 +993,7 @@ local DEFAULTS = { enabled = true, selectedBar = 1, selectedButton = nil, - assignments = {}, -- ["barIdx_btnIdx"] = { {spellID, glowStyle, glowColor, classColor, mode}, ... } + assignments = {}, -- ["barIdx_btnIdx"] = { {spellID, glowStyle, glowColor, classColor, mode, showDuration, ...}, ... } }, -- Tracked Buff Bars v2 (per-bar buff tracking with individual settings) -- Note: not in defaults -- lazy-initialized by ns.GetTrackedBuffBars()