From e0b471c354891d4e816c3d004bcb92a876ff75f4 Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Sun, 27 Sep 2026 12:27:34 +0200 Subject: [PATCH] Open the add menu with a right-click in the asset panel (#244) New assets could only be added through the + button. A right-click on a category heading or on the empty space below the rows now opens the same menu at the pointer. - ListView gets the callback onListMenu. It fires on a right-click on empty space or on a heading. Only the asset panel has headings, so other lists are not affected. - AssetPanel opens its + menu from there. - A right-click on a heading does not fold or unfold the category. A right-click on empty space clears the selection like a left-click. - A right-click on an asset still opens the menu of that asset. - The empty list now says "No assets - press + or right-click". Tests: - ./check.sh: 178 files, 2573 tests, CHECK EXIT 0 - e2e 1-4: ALL PASSED, 134 checks - 3 new checks in e2e 04 with real right-clicks: menu on the "Scenes" heading (21 types, heading stays folded), menu on empty space, and "Behavior" from that menu creates one, undone again. - Counter-probe: without the menu both right-click checks fail. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- src/editor/panels/AssetPanel.ts | 3 +- src/ui/widgets/ListView.ts | 7 ++++- tools/e2e/04-assets.mjs | 49 +++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1c5aeb5b..6a21746b 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, 1452 checks (GM_STEPS=20 for one step) +node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1455 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/panels/AssetPanel.ts b/src/editor/panels/AssetPanel.ts index 0874b322..03fd41f3 100644 --- a/src/editor/panels/AssetPanel.ts +++ b/src/editor/panels/AssetPanel.ts @@ -80,10 +80,11 @@ export class AssetPanel extends Panel { add.onClick = () => new Menu(this.addMenuEntries()).openBelow(add); this.header.add(add); - this.list.emptyText = 'No assets – press +'; + this.list.emptyText = 'No assets – press + or right-click'; this.list.onSelect = (id) => state.select(id ? { kind: 'asset', assetId: id } : null); this.list.onActivate = (id) => this.activate(id); this.list.onContextMenu = (id, x, y) => this.openContextMenu(id, x, y); + this.list.onListMenu = (x, y) => new Menu(this.addMenuEntries()).open(x, y); this.list.onDragOut = (id) => this.startPlacementDrag(id); this.list.onToggle = (id, expand) => this.setFolded(id.slice(HEADING.length) as AssetType, !expand); this.scroll.setContent(this.list); diff --git a/src/ui/widgets/ListView.ts b/src/ui/widgets/ListView.ts index 92306bcc..94367577 100644 --- a/src/ui/widgets/ListView.ts +++ b/src/ui/widgets/ListView.ts @@ -91,6 +91,8 @@ export class ListView extends Widget { onActivate?: (id: string) => void; /** Returning the opened menu keeps its row marked for as long as it is up. */ onContextMenu?: (id: string, globalX: number, globalY: number) => Popup | void; + /** A right-click on the empty space below the rows or on a heading, which have no menu of their own. */ + onListMenu?: (globalX: number, globalY: number) => void; /** Called when a dragged row is released somewhere that could take it. */ onDrop?: (draggedId: string, spot: DropSpot) => void; /** @@ -121,7 +123,9 @@ export class ListView extends Widget { this.pingMarker.visible = false; this.addChild(this.emptyLabel, this.pingMarker, this.dropMarker); this.on('pointerdown', (e: FederatedPointerEvent) => { - if (e.target === this) this.select(null); + if (e.target !== this) return; + this.select(null); + if (e.button === 2) this.onListMenu?.(e.global.x, e.global.y); }); this.on('globalpointermove', (e: FederatedPointerEvent) => this.onDragMove(e)); this.onPointerRelease(() => this.onDragEnd()); @@ -294,6 +298,7 @@ export class ListView extends Widget { const modifiers: PickModifiers = { ctrl: e.ctrlKey || e.metaKey, shift: e.shiftKey }; if (item.heading) { if (e.button === 0) this.onToggle?.(item.id, !item.expanded); + else if (e.button === 2) this.onListMenu?.(e.global.x, e.global.y); return; } // The chevron only folds the row – it never selects, so folding keeps the selection put. diff --git a/tools/e2e/04-assets.mjs b/tools/e2e/04-assets.mjs index 08947406..db1d5744 100644 --- a/tools/e2e/04-assets.mjs +++ b/tools/e2e/04-assets.mjs @@ -32,6 +32,8 @@ export async function assets(t) { await shot('11-context-menu'); await press('Escape'); + await addMenuOnRightClick(t); + // 4b. Drag the Crate asset from the panel into the scene view. Driven step by step rather than // with `drag()`, because the preview only exists while the button is still down. const from = await json("__gm.assetRow('crate')"); @@ -443,3 +445,50 @@ export async function assets(t) { JSON.stringify({ first, typeOfFirst, rowAt, iconAt, ink, typeWords }), ); } + +/** + * 4a-ii. Where no menu of its own opens – on a group heading or on the empty space below the rows – + * a right-click brings up the + menu (#244). The heading stays folded as it was. + */ +async function addMenuOnRightClick(t) { + const { check, json, shot, click, clickText, press, undo, showTab } = t; + const addRows = (rows) => + Array.isArray(rows) && rows.filter(Boolean).length === 21 && rows.includes('Sprite'); + const heading = await json("__gm.assetRowShown('group:scene')"); + const sceneFolded = async () => + !(await json('__gm.assetGroups()')).find((g) => g.type === 'scene').expanded; + const foldedBefore = await sceneFolded(); + await click(heading.x, heading.y, 'right'); + const onHeading = await json('__gm.menuRows()'); + check( + 'a right-click on a group heading opens the + menu and leaves the group as it was', + addRows(onHeading) && (await sceneFolded()) === foldedBefore, + JSON.stringify({ onHeading, foldedBefore }), + ); + await press('Escape'); + const groups = await json('__gm.assetGroups()'); + const lastGroup = groups.at(-1); + const lastShown = await json( + `__gm.assetRowShown(${JSON.stringify(lastGroup.expanded ? lastGroup.ids.at(-1) : `group:${lastGroup.type}`)})`, + ); + const panel = await json('__gm.assetsRect()'); + const empty = { x: panel.x + panel.w / 2, y: panel.y + panel.h - 12 }; + await click(empty.x, empty.y, 'right'); + const onEmpty = await json('__gm.menuRows()'); + check( + 'a right-click on the empty space below the rows opens the + menu', + empty.y > lastShown.y + 20 && addRows(onEmpty), + JSON.stringify({ empty, lastShown, onEmpty }), + ); + await shot('11c-add-menu-on-right-click'); + const before = (await json('__gm.state.project.assets')).length; + await clickText('Behavior'); + const fromMenu = await json('__gm.state.project.assets.at(-1)'); + check( + 'an entry of that menu creates the asset like the + button does', + (await json('__gm.state.project.assets')).length === before + 1 && fromMenu.type === 'behavior', + JSON.stringify(fromMenu && { type: fromMenu.type, name: fromMenu.name }), + ); + await undo(); + await showTab('scene'); +}