From 89b95aa8acfc61975e022eb87049400e4c657f62 Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Sat, 26 Sep 2026 22:43:36 +0200 Subject: [PATCH] Move "Delete row" from the inspector into the timeline menu The context menu of the timeline is where the other row and key actions live. A separate button in the inspector was easy to miss and sat apart from them. - The timeline menu gets "Delete row" as a danger entry after a separator. It deletes the row under the pointer, whether the click hits the row header or a cell. Below the last row or on the ruler it is disabled. - A right-click on a row header now also selects that row, like a right-click on a key selects the key. A right-click in an empty cell keeps the selection so chosen keys stay available for Copy. - The "Delete row" button in the "Timeline row" inspector is gone. "Delete keys" for a multi-key selection stays. Tests: - ./check.sh: 172 files, 2498 tests, CHECK EXIT 0 - e2e 25: ALL PASSED, 30 checks. New check in 25g deletes a row via a real right-click and restores it with all three keys via Undo. - No counter-probe. No check covers the right-click into a cell. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- src/editor/animation/TimelineCanvas.ts | 19 ++++++++++++++++++- src/editor/inspector/trackSection.ts | 9 --------- tools/e2e/25-animation.mjs | 17 +++++++++++++++++ 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8d752acd..d3fd258f 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh The browser suite and the three example games need a machine with a GPU: ```bash -node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1416 checks (GM_STEPS=20 for one step) +node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1417 checks (GM_STEPS=20 for one step) # every tool signs in first – see the Accounts page node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs diff --git a/src/editor/animation/TimelineCanvas.ts b/src/editor/animation/TimelineCanvas.ts index 31d5e256..372c0f66 100644 --- a/src/editor/animation/TimelineCanvas.ts +++ b/src/editor/animation/TimelineCanvas.ts @@ -4,6 +4,7 @@ import { copyAnimationKeys, MoveAnimationKeysCommand, PasteAnimationKeysCommand, + RemoveAnimationTracksCommand, SetAnimationKeyCommand, ShiftAnimationFramesCommand, shiftedFrame, @@ -278,13 +279,20 @@ export class TimelineCanvas extends Widget { ); } - /** A right-click on a key picks it first, the way a node's menu does, so Copy speaks about it. */ + /** + * A right-click on a key picks it first, the way a node's menu does, so Copy speaks about it; one + * on a row's header picks the row. Delete row always means the row under the pointer. + */ private openMenu(asset: AnimationAsset, local: Vec2, global: Vec2): void { const hit = local.x >= HEADER_WIDTH && local.y >= RULER_HEIGHT ? keyAt(this.view, asset.tracks, { x: local.x - HEADER_WIDTH, y: local.y }) : null; if (hit && !hasKey(this.state.selectedKeys, hit)) this.state.select(trackSelection(asset.id, [hit])); + const row = local.y >= RULER_HEIGHT ? rowAt(local.y, asset.tracks.length) : -1; + const track = row >= 0 ? asset.tracks[row] : undefined; + if (track && local.x < HEADER_WIDTH && this.state.selectedTrackId !== track.id) + this.state.select(trackSelection(asset.id, [], track.id)); const picked = this.state.selectedKeys.length; const at = this.playheadFrame; new Menu([ @@ -301,6 +309,15 @@ export class TimelineCanvas extends Widget { { separator: true }, { label: `Insert frames at ${at}…`, onSelect: () => this.openFramesDialog('insert') }, { label: `Remove frames from ${at}…`, danger: true, onSelect: () => this.openFramesDialog('remove') }, + { separator: true }, + { + label: 'Delete row', + danger: true, + disabled: !track, + onSelect: () => { + if (track) this.state.execute(new RemoveAnimationTracksCommand(asset.id, [track.id])); + }, + }, ]).open(global.x, global.y); } diff --git a/src/editor/inspector/trackSection.ts b/src/editor/inspector/trackSection.ts index 1b7cab3b..0211c273 100644 --- a/src/editor/inspector/trackSection.ts +++ b/src/editor/inspector/trackSection.ts @@ -1,6 +1,5 @@ import { RemoveAnimationKeysCommand, - RemoveAnimationTracksCommand, SetAnimationKeyCommand, SetKeyInterpolationCommand, SetTrackRelativeCommand, @@ -112,14 +111,6 @@ export function buildTrackSection( }), ); } - - addGap(host); - host.body.add( - new Button('Delete row', { - variant: 'danger', - onClick: () => host.state.execute(new RemoveAnimationTracksCommand(assetId, [trackId])), - }), - ); } /** The value of the one picked key, editable in the widget its kind calls for. */ diff --git a/tools/e2e/25-animation.mjs b/tools/e2e/25-animation.mjs index bc374195..2193dc6f 100644 --- a/tools/e2e/25-animation.mjs +++ b/tools/e2e/25-animation.mjs @@ -375,6 +375,23 @@ export async function animation(t) { JSON.stringify(await keysOf()), ); + // Right-click on the row's header → Delete row: the row goes, the animation stays picked; undo. + const rowHeader = await json(`__gm.animation.headerAt(${JSON.stringify(trackId)})`); + await click(rowHeader.x, rowHeader.y, 'right'); + const deleteItem = await find('Delete row', true); + await click(deleteItem.x, deleteItem.y); + const rowsGone = (await walk()).tracks.length; + const afterDelete = await json('__gm.state.selection'); + await undo(); + check( + 'the menu on a row header deletes that row, and undo brings it back with its keys', + rowsGone === 0 && + afterDelete?.kind === 'asset' && + (await walk()).tracks.length === 1 && + JSON.stringify(await keysOf()) === three, + JSON.stringify({ rowsGone, afterDelete, keys: await keysOf() }), + ); + // 25h. Undo all the way: the key at 5, the value edit, the keys at 10 and 0, then the row. Exactly // that many – one more would take back a command of an earlier step, so the count is checked // on the way: after four the row is still there, empty.