From b2c31945609ace49500babe9ce26ad5082ed3ee1 Mon Sep 17 00:00:00 2001 From: Thales <> Date: Wed, 23 Sep 2026 14:11:43 +0100 Subject: [PATCH 1/4] Stop the click's panel looking live while the click is off With the click track switched off, two things in its own panel went on saying otherwise (#655). Grid stayed lit with the beat grid editor still open, and the count-in kept the tint that marks it as armed, though a count-in counts into the click and does not play without it. The reporter read both as settings that would still take effect, which is the fair reading. The count-in tint now requires the click to be on as well as a length to be chosen. Only the tint follows the click: the length stays in the select, so switching the click back on brings the count-in back as it was. The grid editor is a click-track tool. It edits the beats the click plays, the live click and the exported one, and it is only available while there is a metronome. So switching the click off closes it, from the button or the K shortcut, and opening it with the click off switches the click on, the same way choosing a vocal mode switches vocals on. Closing the editor leaves the click alone: Done means finished editing, not stop the click. beatgridUi already imports from transport, so the editor subscribes to the toggle through onMetronomeToggle rather than transport importing back into it. toggleMetronome is the one path every on and off goes through. The existing count-in test asserted the armed tint with the click off, which is the state #655 reports as wrong. It now switches the click on first. The five new tests fail against main and pass here. --- static/js/beatgridUi.js | 11 +++- static/js/transport.js | 30 ++++++++++- tests/e2e/click-track.spec.mjs | 92 +++++++++++++++++++++++++++++++++- 3 files changed, 130 insertions(+), 3 deletions(-) diff --git a/static/js/beatgridUi.js b/static/js/beatgridUi.js index da6ab59d..ea3b655f 100644 --- a/static/js/beatgridUi.js +++ b/static/js/beatgridUi.js @@ -13,7 +13,7 @@ import { undoBeatGrid, redoBeatGrid, resetBeatGrid, canUndo, canRedo, barLengthAt, setBarLengthAt, getBeats, } from "./beatgrid.js"; -import { applyMetronomeAccent } from "./transport.js"; +import { applyMetronomeAccent, toggleMetronome, onMetronomeToggle } from "./transport.js"; const TOOL_BTNS = [ ["bg-tool-move", "move", "Drag a beat to retime the region. Alt-drag moves one beat."], @@ -56,6 +56,12 @@ export function toggleBeatGridEditor(force) { if (!bgToolbar) return; const next = force === undefined ? !isBeatGridEditing() : !!force; if (next && !_available) return; + // The editor is a click-track tool: it edits the beats the click plays, and + // the way to check an edit is to hear it. So opening it switches the click + // on, the same way choosing a vocal mode switches vocals on, and switching + // the click off closes it (see wireBeatGridUi). Otherwise Grid could stay + // lit inside the panel of a feature that is off (#655). + if (next) toggleMetronome(true); setBeatGridEditing(next); bgToolbar.classList.toggle("hidden", !next); // The Grid button is a press-to-open toggle like the click and count-in @@ -101,6 +107,9 @@ export function wireBeatGridUi() { }); metroEditBtn?.addEventListener("click", () => toggleBeatGridEditor()); + onMetronomeToggle((on) => { + if (!on && isBeatGridEditing()) toggleBeatGridEditor(false); + }); bgUndoBtn?.addEventListener("click", () => { undoBeatGrid(); syncBeatGridButtons(); }); bgRedoBtn?.addEventListener("click", () => { redoBeatGrid(); syncBeatGridButtons(); }); diff --git a/static/js/transport.js b/static/js/transport.js index 3b610a9c..9d99cbba 100644 --- a/static/js/transport.js +++ b/static/js/transport.js @@ -1239,7 +1239,25 @@ function _renderCountIn() { metroCountInEl.value = String(metronomeCountInBars); // The wrap carries the "on" tint the toggle button used to, so the panel // still shows at a glance that a count-in is armed. - metroCountInEl.parentElement?.classList.toggle("active", metronomeCountInBars > 0); + // + // Armed means it will play, and a count-in counts into the click, so with + // the click off it will not (#655). The length the user chose stays in the + // select, so switching the click back on brings the count-in back as it was; + // only the tint that claims it is live follows the click. + metroCountInEl.parentElement?.classList.toggle( + "active", + metronomeEnabled && metronomeCountInBars > 0, + ); +} + +// Things that have to follow the click on and off but live in modules this +// one cannot import without a cycle: the beat grid editor imports from here. +const _metronomeListeners = new Set(); + +/** Calls `fn(on)` whenever the click track is switched on or off. */ +export function onMetronomeToggle(fn) { + _metronomeListeners.add(fn); + return () => _metronomeListeners.delete(fn); } export function toggleMetronome(force) { @@ -1250,6 +1268,16 @@ export function toggleMetronome(force) { metroBtn.setAttribute("aria-pressed", on ? "true" : "false"); metronome?.setEnabled(on); _saveMetroPrefs(); + // Everything in the click's own panel that shows a live state has to agree + // with the click. The count-in is here; the grid editor listens. + _renderCountIn(); + for (const fn of _metronomeListeners) { + try { + fn(on); + } catch (e) { + console.warn("[metronome] toggle listener failed:", e); + } + } } /** diff --git a/tests/e2e/click-track.spec.mjs b/tests/e2e/click-track.spec.mjs index 75d6772a..cbf9a31f 100644 --- a/tests/e2e/click-track.spec.mjs +++ b/tests/e2e/click-track.spec.mjs @@ -7,7 +7,7 @@ // though it covered them. The first test is the one that would have caught it. import { test, expect } from "@playwright/test"; -import { openStudio, waitForClickTrack } from "./helpers.mjs"; +import { openStudio, waitForClickTrack, openClickOptions } from "./helpers.mjs"; const metro = (page) => ({ toggle: page.locator("#t-metro"), @@ -60,6 +60,8 @@ test.describe("click track", () => { // moved to the wrapper with it. await metro(page).countIn.selectOption("3"); await expect(metro(page).countIn).toHaveValue("3"); + // Armed means it will play, and it only plays into the click (#655). + await metro(page).toggle.click(); await expect(page.locator("#t-metro-countin").locator("xpath=..")).toHaveClass(/active/); await openStudio(page, { tauri: true }); @@ -174,6 +176,94 @@ test.describe("click track", () => { await expect(ui.note).toContainText("accenting 4/4 from the detected downbeat"); }); + // #655: with the click off, nothing in its panel may look live. The count-in + // and the grid editor were both still lit after the click was switched off, + // which read as though they would do something they would not. + test("the count-in stops looking armed while the click is off", async ({ page }) => { + await openStudio(page, { tauri: true }); + await waitForClickTrack(page); + const ui = metro(page); + const wrap = page.locator("#t-metro-countin").locator("xpath=.."); + + // The toggle sits outside the options popover, so pressing it closes the + // popover when the options are folded; open it again before reaching in. + await ui.toggle.click(); + await openClickOptions(page); + await ui.countIn.selectOption("2"); + await expect(wrap).toHaveClass(/active/); + + await ui.toggle.click(); + await expect(wrap).not.toHaveClass(/active/); + // Only the tint follows the click. The length is a setting and stays, so + // switching the click back on brings the count-in back as it was. + await expect(ui.countIn).toHaveValue("2"); + + await ui.toggle.click(); + await expect(wrap).toHaveClass(/active/); + }); + + test("switching the click off closes the grid editor", async ({ page }) => { + await openStudio(page, { tauri: true }); + await waitForClickTrack(page); + const ui = metro(page); + const toolbar = page.locator("#beatgrid-toolbar"); + + await ui.grid.click(); + await expect(toolbar).not.toHaveClass(/hidden/); + + await ui.toggle.click(); + await expect(ui.toggle).toHaveAttribute("aria-pressed", "false"); + await expect(toolbar).toHaveClass(/hidden/); + await expect(ui.grid).not.toHaveClass(/active/); + await expect(ui.grid).toHaveAttribute("aria-pressed", "false"); + }); + + test("the K shortcut closes it too", async ({ page }) => { + // The shortcut and the button are two ways into the same toggle, and the + // editor has to follow both. + await openStudio(page, { tauri: true }); + await waitForClickTrack(page); + const ui = metro(page); + const toolbar = page.locator("#beatgrid-toolbar"); + + await ui.grid.click(); + await expect(toolbar).not.toHaveClass(/hidden/); + + await page.locator("body").press("k"); + await expect(ui.toggle).toHaveAttribute("aria-pressed", "false"); + await expect(toolbar).toHaveClass(/hidden/); + }); + + test("opening the grid editor with the click off switches the click on", async ({ page }) => { + // The editor edits the beats the click plays, and the way to check an edit + // is to hear it. Opening it while the click is off would light Grid inside + // the panel of a feature that is off, so it asks for the click as well. + await openStudio(page, { tauri: true }); + await waitForClickTrack(page); + const ui = metro(page); + await expect(ui.toggle).toHaveAttribute("aria-pressed", "false"); + + await ui.grid.click(); + + await expect(page.locator("#beatgrid-toolbar")).not.toHaveClass(/hidden/); + await expect(ui.toggle).toHaveAttribute("aria-pressed", "true"); + await expect(ui.grid).toHaveClass(/active/); + }); + + test("closing the grid editor leaves the click as it was", async ({ page }) => { + // Only the click can close the editor, not the other way round: Done is + // "finished editing", not "stop the click". + await openStudio(page, { tauri: true }); + await waitForClickTrack(page); + const ui = metro(page); + + await ui.grid.click(); + await page.locator("#bg-done").click(); + + await expect(ui.grid).not.toHaveClass(/active/); + await expect(ui.toggle).toHaveAttribute("aria-pressed", "true"); + }); + test("Grid opens and closes the beat-grid editor", async ({ page }) => { await openStudio(page, { tauri: true }); await waitForClickTrack(page); From 4c7e0b4d0a2b0f5f0944c537bedd33bd01583969 Mon Sep 17 00:00:00 2001 From: Thales <> Date: Wed, 23 Sep 2026 15:24:02 +0100 Subject: [PATCH 2/4] Show the click track toggle as its icon alone The on/off button carried a fixed label, ON, in every language. It never changed, so with the click off it still said ON and only the missing gold highlight said otherwise (#681). It turned up in the screenshots taken to check #655, which is the same complaint about a different control. Making it say OFF would have put the state on the button twice, in the word and in the highlight, and the word is the less reliable of the two: on a label "ON" reads as a state and as a command to switch on equally well. The highlight is already unambiguous and is how the rest of that row shows state. The #655 reporter suggested exactly this, a metronome icon alone, and it gives width back to a row that already folds its options away when narrow. The button keeps its accessible name and tooltip, "Click track (K)", so a screen reader and a hover lose nothing. It is square like the volume button beside it rather than a narrower pill. The Grid button shares the class and keeps its label, so the change is a modifier on this button only. The click.on key has nothing left to label and is gone from all ten tables. --- static/css/daw.css | 7 +++++++ static/index.html | 7 +++++-- static/js/i18n.js | 10 ---------- tests/e2e/click-track.spec.mjs | 21 +++++++++++++++++++++ 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/static/css/daw.css b/static/css/daw.css index 71ab4f1c..f0f16e67 100644 --- a/static/css/daw.css +++ b/static/css/daw.css @@ -2877,6 +2877,13 @@ input, textarea { font-family: inherit; } background: rgba(232,200,64,0.18); border-color: rgba(232,200,64,0.55); color: var(--accent); } .click-metro-icon { flex-shrink: 0; } +/* The on/off toggle is the icon alone (#681), square like the volume button + beside it. The Grid button shares the class above and keeps its label. */ +.click-toggle-btn.click-toggle-icon { + width: 34px; + padding: 0; + justify-content: center; +} /* Beat grid editor: canvas overlay + toolbar */ .beatgrid-canvas { diff --git a/static/index.html b/static/index.html index 42640ef5..fbe7695c 100644 --- a/static/index.html +++ b/static/index.html @@ -851,11 +851,14 @@