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:
1 parent
0c5ac32b59
commit
89b95aa8ac
4 files changed
+36
-11
No files matched your search
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user