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 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-26 22:43:36 +02:00
1 parent 0c5ac32b59
commit 89b95aa8ac
4 files changed
+36 -11

No files matched your search

+1 -1
View File
@@ -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
+18 -1
View File
@@ -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);
}
-9
View File
@@ -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. */
+17
View File
@@ -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.