diff --git a/src/navigation.ts b/src/navigation.ts index 137406d..7595dd2 100644 --- a/src/navigation.ts +++ b/src/navigation.ts @@ -1231,13 +1231,13 @@ export class Navigation { */ handleEnterForWS(workspace: Blockly.WorkspaceSvg) { const cursor = workspace.getCursor(); - if (!cursor) { - return; - } + if (!cursor) return; const curNode = cursor.getCurNode(); const nodeType = curNode.getType(); if (nodeType == Blockly.ASTNode.types.FIELD) { (curNode.getLocation() as Blockly.Field).showEditor(); + } else if (nodeType == Blockly.ASTNode.types.BLOCK) { + this.openActionMenu(curNode); } else if ( curNode.isConnection() || nodeType == Blockly.ASTNode.types.WORKSPACE @@ -1248,13 +1248,63 @@ export class Navigation { } else { this.focusFlyout(workspace); } - } else if (nodeType == Blockly.ASTNode.types.BLOCK) { - this.warn('Cannot mark a block.'); } else if (nodeType == Blockly.ASTNode.types.STACK) { this.warn('Cannot mark a stack.'); } } + /** + * Show the action menu for a given node. + * + * The action menu will contain entries for relevant actions for the + * node's location. If the location is a block, this will include + * the contents of the block's context menu (if any). + */ + openActionMenu(node: Blockly.ASTNode) { + const fakeEvent = fakeEventForNode(node); + (node.getLocation() as Blockly.BlockSvg).showContextMenu(fakeEvent); + + let menuOptions: Array< + | Blockly.ContextMenuRegistry.ContextMenuOption + | Blockly.ContextMenuRegistry.LegacyContextMenuOption + > | null = null; + let rtl: boolean; + let workspace: Blockly.WorkspaceSvg; + + const nodeType = node.getType(); + switch (nodeType) { + case Blockly.ASTNode.types.BLOCK: + const block = node.getLocation() as Blockly.BlockSvg; + workspace = block.workspace as Blockly.WorkspaceSvg; + rtl = block.RTL; + + // Reimplement BlockSvg.prototype.generateContextMenu as that + // method is protected. + if (!workspace.options.readOnly && !block.contextMenu) { + menuOptions = + Blockly.ContextMenuRegistry.registry.getContextMenuOptions( + Blockly.ContextMenuRegistry.ScopeType.BLOCK, + {block}, + ); + + // Allow the block to add or modify menuOptions. + if (block.customContextMenu) { + block.customContextMenu(menuOptions); + } + } + // End reimplement. + break; + default: + throw new TypeError( + `unable to show action menu for ASTNode of type ${nodeType}`, + ); + } + + if (!menuOptions || !menuOptions.length) return; + + Blockly.ContextMenu.show(fakeEvent, menuOptions, rtl, workspace); + } + /** * Pastes the copied block to the marked location if possible or * onto the workspace otherwise. @@ -1333,3 +1383,32 @@ export class Navigation { } } } + +/** + * Create a fake PointerEvent for opening the action menu for the + * given ASTNode. + * + * Currently only works for block nodes. + * + * @param node The node to open the action menu for. + * @returns A synthetic pointerdown PointerEvent. + */ +function fakeEventForNode(node: Blockly.ASTNode): PointerEvent { + if (node.getType() !== Blockly.ASTNode.types.BLOCK) { + throw new TypeError('can only create PointerEvents for BLOCK nodes'); + } + + // Get the location of the top-left corner of the block in + // screen coordinates. + const block = node.getLocation() as Blockly.BlockSvg; + const coords = Blockly.utils.svgMath.wsToScreenCoordinates( + block.workspace, + block.getRelativeToSurfaceXY(), + ); + + // Create a fake event for the action menu code to work from. + return new PointerEvent('pointerdown', { + clientX: coords.x, + clientY: coords.y, + }); +} diff --git a/src/navigation_controller.ts b/src/navigation_controller.ts index 0b2a87b..cfba0f8 100644 --- a/src/navigation_controller.ts +++ b/src/navigation_controller.ts @@ -397,11 +397,14 @@ export class NavigationController { }, /** - * Mark the current location, insert a block from the flyout at - * the marked location, or press a flyout button. + * Enter key: + * + * - On the flyout: press a button or choose a block to place. + * - On a stack: open a block's context menu or field's editor. + * - On the workspace: open the context menu. */ - mark: { - name: Constants.SHORTCUT_NAMES.MARK, + enter: { + name: Constants.SHORTCUT_NAMES.MARK, // FIXME preconditionFn: (workspace) => this.canCurrentlyEdit(workspace), callback: (workspace) => { let flyoutCursor;