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

+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. */