From adc8c77ebecf6ebe1bc0033495eecadbaa4644f0 Mon Sep 17 00:00:00 2001 From: ChrisBeWithYou Date: Sun, 5 Jul 2026 15:02:48 -0500 Subject: [PATCH] feat(editor): add tempo map sync point actions --- screen.js | 30 ++++++++++++++++++++++++++++-- tests/tempo_sync_inspector.test.js | 8 ++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/screen.js b/screen.js index 6cb37559..17bdd658 100644 --- a/screen.js +++ b/screen.js @@ -11639,6 +11639,9 @@ function _tempoSyncInspectorStatePure(measures, selectedIndex) { numeratorValue: '', denominatorValue: '4', signatureDisabled: true, + canInsert: rows.length > 0, + canDelete: false, + deleteTitle: 'Select an interior sync point to delete it', hint: 'Select a sync point on the Tempo Map grid.', }; } @@ -11647,6 +11650,8 @@ function _tempoSyncInspectorStatePure(measures, selectedIndex) { ? Number(selected.denominator) : 4; const hasBpm = !selected.isLast && Number(selected.bpm) > 0; + const selectedOrdinal = rows.indexOf(selected); + const canDelete = selectedOrdinal > 0 && selectedOrdinal < rows.length - 1; return { label: `Measure ${selected.measure}`, bpmValue: hasBpm ? Number(selected.bpm).toFixed(2) : '', @@ -11657,6 +11662,11 @@ function _tempoSyncInspectorStatePure(measures, selectedIndex) { numeratorValue: String(numerator), denominatorValue: String(denominator), signatureDisabled: false, + canInsert: true, + canDelete, + deleteTitle: canDelete + ? 'Delete selected sync point' + : 'First and final sync points cannot be deleted', hint: hasBpm ? `${numerator}/${denominator}` : `${numerator}/${denominator} - final measure BPM needs a closing downbeat.`, @@ -11674,7 +11684,13 @@ function _ensureTempoSyncInspector() { el.className = 'hidden items-center gap-1.5 px-2 py-0.5 rounded border border-gray-700 bg-dark-700/60 text-xs'; el.innerHTML = 'Sync point:' + 'No selection' - + ''; + + '' + + '' + + ''; + const insertBtn = el.querySelector('#editor-tempo-sync-insert'); + const deleteBtn = el.querySelector('#editor-tempo-sync-delete'); + if (insertBtn) insertBtn.onclick = () => _tempoInsertSyncPoint(S.cursorTime); + if (deleteBtn) deleteBtn.onclick = () => { if (S.tempoSel >= 0) _tempoDeleteSyncPoint(S.tempoSel); }; bpm.parentNode.insertBefore(el, bpm.previousElementSibling || bpm); return el; } @@ -11685,11 +11701,13 @@ function _refreshTempoSyncInspector() { const bpmEl = document.getElementById('editor-bpm'); const numEl = document.getElementById('editor-tempo-sig'); const denEl = document.getElementById('editor-tempo-sig-den'); + const insertBtn = document.getElementById('editor-tempo-sync-insert'); + const deleteBtn = document.getElementById('editor-tempo-sync-delete'); if (!el) return; const hasGrid = !!(S.beats && S.beats.length >= 2); const visible = !!S.tempoMapMode && hasGrid; const state = _tempoSyncInspectorStatePure(visible ? _tempoMeasures() : [], visible ? S.tempoSel : -1); - const sig = `${visible}|${S.tempoSel}|${state.label}|${state.bpmValue}|${state.bpmDisabled}|${state.numeratorValue}|${state.denominatorValue}|${state.signatureDisabled}|${state.hint}`; + const sig = `${visible}|${S.tempoSel}|${state.label}|${state.bpmValue}|${state.bpmDisabled}|${state.numeratorValue}|${state.denominatorValue}|${state.signatureDisabled}|${state.canInsert}|${state.canDelete}|${state.hint}`; _tempoSyncInspectorState = sig; el.classList.toggle('hidden', !visible); el.classList.toggle('inline-flex', visible); @@ -11734,6 +11752,14 @@ function _refreshTempoSyncInspector() { denEl.style.opacity = ''; } } + if (insertBtn && visible) { + insertBtn.disabled = !state.canInsert; + insertBtn.title = 'Insert a sync point at the playhead'; + } + if (deleteBtn && visible) { + deleteBtn.disabled = !state.canDelete; + deleteBtn.title = state.deleteTitle; + } } function _ensureTempoSignatureControl() { let wrap = document.getElementById('editor-tempo-sig-wrap'); diff --git a/tests/tempo_sync_inspector.test.js b/tests/tempo_sync_inspector.test.js index 2d65ca52..e2681293 100644 --- a/tests/tempo_sync_inspector.test.js +++ b/tests/tempo_sync_inspector.test.js @@ -38,6 +38,8 @@ t('asks for a selected sync point before enabling edits', () => { assert.strictEqual(state.bpmDisabled, true); assert.strictEqual(state.signatureDisabled, true); assert.strictEqual(state.bpmValue, ''); + assert.strictEqual(state.canInsert, true); + assert.strictEqual(state.canDelete, false); }); t('shows editable BPM and signature for a selected non-final measure', () => { @@ -49,6 +51,9 @@ t('shows editable BPM and signature for a selected non-final measure', () => { assert.strictEqual(state.denominatorValue, '8'); assert.strictEqual(state.signatureDisabled, false); assert.strictEqual(state.hint, '7/8'); + assert.strictEqual(state.canInsert, true); + assert.strictEqual(state.canDelete, true); + assert.strictEqual(state.deleteTitle, 'Delete selected sync point'); }); t('keeps signature editable but disables BPM for the final measure', () => { @@ -59,6 +64,9 @@ t('keeps signature editable but disables BPM for the final measure', () => { assert.strictEqual(state.signatureDisabled, false); assert.ok(state.bpmTitle.includes('closing downbeat')); assert.ok(state.hint.includes('final measure BPM')); + assert.strictEqual(state.canInsert, true); + assert.strictEqual(state.canDelete, false); + assert.ok(state.deleteTitle.includes('cannot be deleted')); }); console.log(`\n${pass} passed, ${fail} failed`);