From 283b4c15936d4210b078e0eb7b282244aebd24e2 Mon Sep 17 00:00:00 2001 From: RubyJ Date: Sat, 14 Mar 2026 16:58:07 -0400 Subject: [PATCH 1/2] feat: configurable grid layout (columns setting) with card/wide row modes --- Core.lua | 10 ++ Settings.lua | 75 ++++++++++++++- UI.lua | 253 ++++++++++++++++++++++++++++++++++++--------------- 3 files changed, 261 insertions(+), 77 deletions(-) diff --git a/Core.lua b/Core.lua index 88ae64a..c2bc897 100644 --- a/Core.lua +++ b/Core.lua @@ -7,6 +7,7 @@ -- /cdt — toggle the tracker window -- /cdt reset — reset all running timers -- /cdt settings — open the settings panel +-- /cdt columns N — set grid columns (1-9) -------------------------------------------------------------------------------- local AddonName, CT = ... @@ -45,6 +46,15 @@ SlashCmdList["COOLDOWNTRACKER"] = function(msg) print("|cffaaddff[CooldownTracker]|r All timers reset.") elseif cmd == "settings" then CT:OpenSettings() + elseif cmd:match("^columns%s+(%d+)$") then + local n = tonumber(cmd:match("^columns%s+(%d+)$")) + if n and n >= 1 and n <= 9 then + CooldownTrackerDB.columns = n + CT:LayoutRows() + print("|cffaaddff[CooldownTracker]|r Columns set to " .. n .. ".") + else + print("|cffaaddff[CooldownTracker]|r Usage: /cdt columns <1-9>") + end else if CT.mainFrame:IsShown() then CT.mainFrame:Hide() diff --git a/Settings.lua b/Settings.lua index 6c5cad9..748d181 100644 --- a/Settings.lua +++ b/Settings.lua @@ -51,12 +51,73 @@ 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.\nType a new value and press Enter to confirm. Changes apply immediately.") + desc:SetText("Configure the layout and cooldown durations.") desc:SetJustifyH("LEFT") - -- Column header labels + -- ----- Layout section --------------------------------------------------- + local layoutSection = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + layoutSection:SetPoint("TOPLEFT", desc, "BOTTOMLEFT", 0, -12) + layoutSection:SetText("|cffaaddffLayout|r") + + local colLabel = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlight") + colLabel:SetPoint("TOPLEFT", layoutSection, "BOTTOMLEFT", 0, -6) + colLabel:SetText("Columns:") + + local colBox = CreateFrame("EditBox", "CTSettingsColBox", panel, "BackdropTemplate") + colBox:SetSize(50, 24) + colBox:SetPoint("LEFT", colLabel, "RIGHT", 8, 0) + colBox:SetAutoFocus(false) + colBox:SetFontObject("ChatFontNormal") + colBox:SetJustifyH("CENTER") + if colBox.SetBackdrop then + colBox: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 }, + }) + colBox:SetBackdropColor(0.1, 0.1, 0.1, 0.8) + colBox:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.8) + end + colBox:SetTextInsets(4, 4, 2, 2) + + local colHint = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + colHint:SetPoint("LEFT", colBox, "RIGHT", 10, 0) + colHint:SetText("1 = vertical stack, 2-9 = grid layout") + colHint:SetTextColor(0.6, 0.6, 0.6) + + colBox:SetScript("OnTextChanged", function(self, userInput) + if not userInput then return end + local val = tonumber(self:GetText()) + if val and val >= 1 and val <= 9 then + CooldownTrackerDB.columns = val + if CT.LayoutRows then CT:LayoutRows() end + end + end) + colBox:SetScript("OnEnterPressed", function(self) self:ClearFocus() end) + colBox:SetScript("OnEscapePressed", function(self) + self:SetText(tostring(CooldownTrackerDB.columns or 1)) + self:ClearFocus() + end) + colBox:SetScript("OnEnter", function() + GameTooltip:SetOwner(colBox, "ANCHOR_RIGHT") + GameTooltip:SetText("Grid Columns") + GameTooltip:AddLine("1 = single vertical column (default)", 1, 1, 1) + GameTooltip:AddLine("2-9 = compact card grid with that many columns", 0.8, 0.8, 0.8) + GameTooltip:AddLine("Changes apply instantly to the tracker window.", 0.6, 0.6, 0.6) + GameTooltip:Show() + end) + colBox:SetScript("OnLeave", function() GameTooltip:Hide() end) + + local layoutDivider = panel:CreateTexture(nil, "ARTWORK") + layoutDivider:SetHeight(1) + layoutDivider:SetPoint("TOPLEFT", colLabel, "BOTTOMLEFT", 0, -10) + layoutDivider:SetWidth(CONTENT_WIDTH) + layoutDivider:SetColorTexture(0.3, 0.3, 0.4, 0.4) + + -- ----- Cooldown duration headers ---------------------------------------- local hdrAbility = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") - hdrAbility:SetPoint("TOPLEFT", desc, "BOTTOMLEFT", ICON_SIZE + 12, -10) + hdrAbility:SetPoint("TOPLEFT", layoutDivider, "BOTTOMLEFT", ICON_SIZE + 12, -8) hdrAbility:SetText("Ability") hdrAbility:SetTextColor(0.7, 0.7, 0.7) @@ -71,6 +132,13 @@ local function CreateSettingsPanel() divider:SetWidth(CONTENT_WIDTH) divider:SetColorTexture(0.3, 0.3, 0.4, 0.6) + -- refresh colBox on panel show + local origOnShow = panel:GetScript("OnShow") + panel:SetScript("OnShow", function(self) + colBox:SetText(tostring(CooldownTrackerDB.columns or 1)) + if origOnShow then origOnShow(self) end + end) + -- ----- Scroll frame ----------------------------------------------------- local scrollFrame = CreateFrame("ScrollFrame", "CTSettingsScrollFrame", panel, "UIPanelScrollFrameTemplate") scrollFrame:SetPoint("TOPLEFT", divider, "BOTTOMLEFT", 0, -6) @@ -235,6 +303,7 @@ end function CT:InitSettings() CooldownTrackerDB.customDurations = CooldownTrackerDB.customDurations or {} + CooldownTrackerDB.columns = CooldownTrackerDB.columns or 1 ApplyCustomDurations() local panel = CreateSettingsPanel() diff --git a/UI.lua b/UI.lua index 22f7cd7..b24e0d5 100644 --- a/UI.lua +++ b/UI.lua @@ -1,8 +1,11 @@ -------------------------------------------------------------------------------- -- UI.lua --- Builds and manages all UI widgets: the main draggable frame, per-cooldown --- rows (icon, labels, progress bar, button), and the per-frame update loop. --- Exposes: CT:BuildUI(), CT:UpdateAllRows(), CT:RestorePosition() +-- Builds and manages the main window. Supports a configurable grid layout: +-- columns = 1 → wide vertical rows (current look) +-- columns = N → N-column card grid (compact square cards) +-- +-- Exposes: CT:BuildUI(), CT:UpdateAllRows(), CT:RestorePosition(), +-- CT:LayoutRows() -------------------------------------------------------------------------------- local AddonName, CT = ... @@ -10,13 +13,21 @@ local AddonName, CT = ... -- --------------------------------------------------------------------------- -- Layout constants -- --------------------------------------------------------------------------- -local ROW_HEIGHT = 36 -local ROW_PADDING = 6 -local FRAME_WIDTH = 300 -local ICON_SIZE = 28 -local BUTTON_WIDTH = 52 local TITLE_HEIGHT = 24 local BOTTOM_PAD = 8 +local ROW_PADDING = 6 + +-- Wide-row mode (columns = 1) +local WIDE_ROW_H = 36 +local WIDE_W = 300 +local ICON_SIZE = 28 +local BUTTON_W = 52 + +-- Card mode (columns ≥ 2) +local CARD_W = 100 +local CARD_H = 72 +local CARD_PAD = 6 +local CARD_ICON = 26 -- --------------------------------------------------------------------------- -- Local helpers @@ -25,11 +36,8 @@ local function FormatTime(seconds) if seconds <= 0 then return "Ready" end local m = math.floor(seconds / 60) local s = math.floor(seconds % 60) - if m > 0 then - return string.format("%d:%02d", m, s) - else - return string.format("0:%02d", s) - end + if m > 0 then return string.format("%d:%02d", m, s) + else return string.format("0:%02d", s) end end local function SetBtnColor(btn, r, g, b) @@ -47,7 +55,88 @@ local function SavePosition(frame) end -- --------------------------------------------------------------------------- --- Row update (called every OnUpdate tick for active timers) +-- Per-row internal layout: wide (1-col) vs card (multi-col) +-- These functions reanchor all child widgets inside an existing row frame. +-- --------------------------------------------------------------------------- +local function ApplyWideLayout(row) + local cd = row.cd + local rowW = WIDE_W - 16 + + row:SetSize(rowW, WIDE_ROW_H) + + row.strip:Show() + row.strip:ClearAllPoints() + row.strip:SetSize(3, WIDE_ROW_H) + row.strip:SetPoint("LEFT", row, "LEFT", 0, 0) + + row.iconTex:ClearAllPoints() + row.iconTex:SetSize(ICON_SIZE, ICON_SIZE) + row.iconTex:SetPoint("LEFT", row, "LEFT", 7, 0) + + row.nameLabel:Show() + row.nameLabel:ClearAllPoints() + row.nameLabel:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE") + row.nameLabel:SetPoint("LEFT", row.iconTex, "RIGHT", 6, 4) + + row.classLabel:Show() + row.classLabel:ClearAllPoints() + row.classLabel:SetPoint("LEFT", row.iconTex, "RIGHT", 6, -6) + + row.timerLabel:ClearAllPoints() + row.timerLabel:SetFont("Fonts\\FRIZQT__.TTF", 13, "OUTLINE") + row.timerLabel:SetPoint("RIGHT", row, "RIGHT", -(BUTTON_W + 8), 0) + + row.bar:Show() + row.bar:ClearAllPoints() + row.bar:SetSize(math.max(4, rowW - ICON_SIZE - BUTTON_W - 30), 3) + row.bar:SetPoint("BOTTOMLEFT", row.iconTex, "BOTTOMRIGHT", 6, 2) + + row.barFill:Show() + row.barFill:ClearAllPoints() + row.barFill:SetSize(row.bar:GetWidth(), 3) + row.barFill:SetPoint("LEFT", row.bar, "LEFT", 0, 0) + + row.button:ClearAllPoints() + row.button:SetSize(BUTTON_W, 22) + row.button:SetPoint("RIGHT", row, "RIGHT", -2, 0) + + row.isWide = true +end + +local function ApplyCardLayout(row, cW, cH) + row:SetSize(cW, cH) + + -- Colour accent becomes a top strip + row.strip:Show() + row.strip:ClearAllPoints() + row.strip:SetSize(cW, 3) + row.strip:SetPoint("TOP", row, "TOP", 0, 0) + + local cd = row.cd + row.iconTex:ClearAllPoints() + row.iconTex:SetSize(CARD_ICON, CARD_ICON) + row.iconTex:SetPoint("TOP", row, "TOP", 0, -6) + + row.nameLabel:Hide() -- not enough room + row.classLabel:Hide() + + row.timerLabel:ClearAllPoints() + row.timerLabel:SetFont("Fonts\\FRIZQT__.TTF", 12, "OUTLINE") + row.timerLabel:SetPoint("CENTER", row, "CENTER", 0, 4) + + row.bar:Hide() + row.barFill:Hide() + + local btnW = math.max(40, cW - 12) + row.button:ClearAllPoints() + row.button:SetSize(btnW, 20) + row.button:SetPoint("BOTTOM", row, "BOTTOM", 0, 4) + + row.isWide = false +end + +-- --------------------------------------------------------------------------- +-- Row update (called every OnUpdate tick) -- --------------------------------------------------------------------------- local function UpdateRow(row, now) local cd = row.cd @@ -56,110 +145,85 @@ local function UpdateRow(row, now) if endTime then local remaining = endTime - now if remaining <= 0 then - -- Timer just expired CT.activeTimers[cd.id] = nil row.timerLabel:SetText("|cff00ff00Ready|r") - row.barFill:SetWidth(row.bar:GetWidth()) - row.barFill:SetVertexColor(0.2, 0.9, 0.2) + if row.isWide then + row.barFill:SetWidth(row.bar:GetWidth()) + row.barFill:SetVertexColor(0.2, 0.9, 0.2) + end row.button:SetText("Used") SetBtnColor(row.button, 0.3, 0.85, 0.3) row.iconTex:SetAlpha(1) PlaySound(SOUNDKIT.ALARM_CLOCK_WARNING_3) else - -- Counting down local frac = remaining / cd.duration - local barW = math.max(2, row.bar:GetWidth() * frac) - row.barFill:SetWidth(barW) - if frac < 0.25 then - row.barFill:SetVertexColor(0.9, 0.2, 0.2) - elseif frac < 0.5 then - row.barFill:SetVertexColor(0.9, 0.7, 0.1) - else - row.barFill:SetVertexColor(cd.r, cd.g, cd.b) + if row.isWide then + local barW = math.max(2, row.bar:GetWidth() * frac) + row.barFill:SetWidth(barW) + if frac < 0.25 then row.barFill:SetVertexColor(0.9, 0.2, 0.2) + elseif frac < 0.5 then row.barFill:SetVertexColor(0.9, 0.7, 0.1) + else row.barFill:SetVertexColor(cd.r, cd.g, cd.b) end end row.timerLabel:SetText("|cffff8040" .. FormatTime(remaining) .. "|r") row.button:SetText("Reset") SetBtnColor(row.button, 0.8, 0.3, 0.3) end else - -- Ready state row.timerLabel:SetText("|cff00ff00Ready|r") - row.barFill:SetWidth(row.bar:GetWidth()) - row.barFill:SetVertexColor(0.2, 0.9, 0.2) + if row.isWide then + row.barFill:SetWidth(row.bar:GetWidth()) + row.barFill:SetVertexColor(0.2, 0.9, 0.2) + end row.button:SetText("Used") SetBtnColor(row.button, 0.3, 0.85, 0.3) end end -- --------------------------------------------------------------------------- --- Row construction +-- Row construction (creates all child frames; layout applied separately) -- --------------------------------------------------------------------------- -local function CreateRow(parent, cd, index) - local yOffset = -(TITLE_HEIGHT + ROW_PADDING + (index - 1) * (ROW_HEIGHT + ROW_PADDING)) - +local function CreateRow(parent, cd) local row = CreateFrame("Frame", nil, parent) - row:SetSize(FRAME_WIDTH - 16, ROW_HEIGHT) - row:SetPoint("TOPLEFT", parent, "TOPLEFT", 8, yOffset) row.cd = cd - -- Background local bg = row:CreateTexture(nil, "BACKGROUND") bg:SetAllPoints(row) bg:SetColorTexture(0, 0, 0, 0.35) + row.bg = bg - -- Accent left strip local strip = row:CreateTexture(nil, "BACKGROUND") - strip:SetSize(3, ROW_HEIGHT) - strip:SetPoint("LEFT", row, "LEFT", 0, 0) strip:SetColorTexture(cd.r, cd.g, cd.b, 0.9) + row.strip = strip - -- Spell icon local icon = row:CreateTexture(nil, "ARTWORK") - icon:SetSize(ICON_SIZE, ICON_SIZE) - icon:SetPoint("LEFT", row, "LEFT", 7, 0) icon:SetTexture(cd.icon) icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) row.iconTex = icon - -- Ability name local nameLabel = row:CreateFontString(nil, "OVERLAY", "GameFontNormal") - nameLabel:SetPoint("LEFT", icon, "RIGHT", 6, 4) nameLabel:SetText(cd.name) nameLabel:SetTextColor(1, 1, 1) - nameLabel:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE") + row.nameLabel = nameLabel - -- Class tag local classLabel = row:CreateFontString(nil, "OVERLAY") - classLabel:SetPoint("LEFT", icon, "RIGHT", 6, -6) classLabel:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE") classLabel:SetText(cd.class) classLabel:SetTextColor(cd.r, cd.g, cd.b) + row.classLabel = classLabel - -- Timer label local timerLabel = row:CreateFontString(nil, "OVERLAY") - timerLabel:SetFont("Fonts\\FRIZQT__.TTF", 13, "OUTLINE") - timerLabel:SetPoint("RIGHT", row, "RIGHT", -(BUTTON_WIDTH + 8), 0) timerLabel:SetText("|cff00ff00Ready|r") row.timerLabel = timerLabel - -- Progress bar background local bar = row:CreateTexture(nil, "BORDER") - bar:SetSize(row:GetWidth() - ICON_SIZE - BUTTON_WIDTH - 26, 3) - bar:SetPoint("BOTTOMLEFT", icon, "BOTTOMRIGHT", 6, 2) bar:SetColorTexture(0.15, 0.15, 0.15, 0.8) row.bar = bar - -- Progress bar fill local fill = row:CreateTexture(nil, "ARTWORK") - fill:SetSize(bar:GetWidth(), 3) - fill:SetPoint("LEFT", bar, "LEFT", 0, 0) fill:SetColorTexture(cd.r, cd.g, cd.b) row.barFill = fill - -- Used / Reset button local btn = CreateFrame("Button", nil, row, "UIPanelButtonTemplate") - btn:SetSize(BUTTON_WIDTH, 22) - btn:SetPoint("RIGHT", row, "RIGHT", -2, 0) btn:SetText("Used") SetBtnColor(btn, 0.3, 0.85, 0.3) btn:SetScript("OnClick", function() @@ -172,7 +236,6 @@ local function CreateRow(parent, cd, index) end) row.button = btn - -- Hover highlight + tooltip row:SetScript("OnEnter", function() bg:SetColorTexture(cd.r * 0.15, cd.g * 0.15, cd.b * 0.15, 0.5) GameTooltip:SetOwner(row, "ANCHOR_RIGHT") @@ -183,7 +246,7 @@ local function CreateRow(parent, cd, index) if m > 0 and s > 0 then GameTooltip:AddLine(string.format("Cooldown: %dm %ds", m, s), 0.8, 0.8, 0.8) elseif m > 0 then - GameTooltip:AddLine(string.format("Cooldown: %d minute%s", m, m > 1 and "s" or ""), 0.8, 0.8, 0.8) + GameTooltip:AddLine(string.format("Cooldown: %d min", m), 0.8, 0.8, 0.8) else GameTooltip:AddLine(string.format("Cooldown: %ds", s), 0.8, 0.8, 0.8) end @@ -205,7 +268,54 @@ local function CreateRow(parent, cd, index) end -- --------------------------------------------------------------------------- --- Public: CT:UpdateAllRows() — called from OnUpdate +-- Public: CT:LayoutRows() +-- Repositions all row frames based on CooldownTrackerDB.columns. +-- Safe to call any time (e.g. from settings panel after columns change). +-- --------------------------------------------------------------------------- +function CT:LayoutRows() + local cols = math.max(1, math.min(#CT.COOLDOWNS, CooldownTrackerDB.columns or 1)) + local n = #CT.COOLDOWNS + local f = CT.mainFrame + + if cols == 1 then + -- Fully vertical: wide rows + local frameH = TITLE_HEIGHT + ROW_PADDING + + n * (WIDE_ROW_H + ROW_PADDING) + + BOTTOM_PAD + f:SetSize(WIDE_W, frameH) + + for i, cd in ipairs(CT.COOLDOWNS) do + local row = CT.rows[cd.id] + row:ClearAllPoints() + row:SetPoint("TOPLEFT", f, "TOPLEFT", + 8, + -(TITLE_HEIGHT + ROW_PADDING + (i - 1) * (WIDE_ROW_H + ROW_PADDING))) + ApplyWideLayout(row) + end + else + -- Grid: compact cards + local numRowsGrid = math.ceil(n / cols) + local frameW = cols * (CARD_W + CARD_PAD) + CARD_PAD + local frameH = TITLE_HEIGHT + CARD_PAD + + numRowsGrid * (CARD_H + CARD_PAD) + + BOTTOM_PAD + f:SetSize(frameW, frameH) + + for i, cd in ipairs(CT.COOLDOWNS) do + local col = (i - 1) % cols + local row = math.floor((i - 1) / cols) + local r = CT.rows[cd.id] + r:ClearAllPoints() + r:SetPoint("TOPLEFT", f, "TOPLEFT", + CARD_PAD + col * (CARD_W + CARD_PAD), + -(TITLE_HEIGHT + CARD_PAD + row * (CARD_H + CARD_PAD))) + ApplyCardLayout(r, CARD_W, CARD_H) + end + end +end + +-- --------------------------------------------------------------------------- +-- Public: CT:UpdateAllRows() -- --------------------------------------------------------------------------- function CT:UpdateAllRows() local now = GetTime() @@ -227,17 +337,12 @@ function CT:RestorePosition() end -- --------------------------------------------------------------------------- --- Public: CT:BuildUI() — called once from Core.lua on ADDON_LOADED +-- Public: CT:BuildUI() -- --------------------------------------------------------------------------- function CT:BuildUI() CT.rows = {} - local totalRows = #CT.COOLDOWNS - local frameHeight = TITLE_HEIGHT + ROW_PADDING + totalRows * (ROW_HEIGHT + ROW_PADDING) + BOTTOM_PAD - - -- Main frame local f = CreateFrame("Frame", "CooldownTrackerFrame", UIParent, "BackdropTemplate") - f:SetSize(FRAME_WIDTH, frameHeight) f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) f:SetMovable(true) f:EnableMouse(true) @@ -250,7 +355,6 @@ function CT:BuildUI() f:SetFrameStrata("MEDIUM") f:SetClampedToScreen(true) - -- Backdrop if f.SetBackdrop then f:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", @@ -288,14 +392,15 @@ function CT:BuildUI() divider:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -TITLE_HEIGHT) divider:SetColorTexture(0.3, 0.3, 0.5, 0.6) - -- Build one row per cooldown - for i, cd in ipairs(CT.COOLDOWNS) do - CreateRow(f, cd, i) + -- Create all rows (layout applied below) + for _, cd in ipairs(CT.COOLDOWNS) do + CreateRow(f, cd) end - -- Per-frame update loop f:SetScript("OnUpdate", function() CT:UpdateAllRows() end) - f:Hide() CT.mainFrame = f + + -- Apply layout (reads CooldownTrackerDB.columns, defaults to 1) + CT:LayoutRows() end From 11c7d54f60dc7e1fedf753d9d5da4f8980c7a5a7 Mon Sep 17 00:00:00 2001 From: RubyJ Date: Sun, 15 Mar 2026 13:02:52 -0400 Subject: [PATCH 2/2] fix: timerLabel missing font before SetText in CreateRow --- UI.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/UI.lua b/UI.lua index b24e0d5..e168d89 100644 --- a/UI.lua +++ b/UI.lua @@ -212,6 +212,7 @@ local function CreateRow(parent, cd) row.classLabel = classLabel local timerLabel = row:CreateFontString(nil, "OVERLAY") + timerLabel:SetFont("Fonts\\FRIZQT__.TTF", 13, "OUTLINE") timerLabel:SetText("|cff00ff00Ready|r") row.timerLabel = timerLabel