Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 9 additions & 127 deletions src/actions/action_menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {
ASTNode,
ContextMenu,
ContextMenuRegistry,
ShortcutRegistry,
utils as BlocklyUtils,
WidgetDiv,
} from 'blockly';
import {ShortcutRegistry, utils as BlocklyUtils, WidgetDiv} from 'blockly';
import * as Constants from '../constants';
import type {BlockSvg, RenderedConnection, WorkspaceSvg} from 'blockly';
import type {WorkspaceSvg} from 'blockly';
import {Navigation} from '../navigation';

const KeyCodes = BlocklyUtils.KeyCodes;
Expand Down Expand Up @@ -85,50 +78,20 @@
* @param workspace The workspace.
*/
private openActionMenu(workspace: WorkspaceSvg): boolean {
let rtl: boolean;

// TODO(#362): Pass this through the precondition and callback instead of making it up.
const menuOpenEvent = new KeyboardEvent('keydown');

const cursor = workspace.getCursor();
if (!cursor) throw new Error('workspace has no cursor');
const node = cursor.getCurNode();
if (!node) return false;
const nodeType = node.getType();
switch (nodeType) {
case ASTNode.types.BLOCK: {
const block = node.getLocation() as BlockSvg;
block.showContextMenu(menuOpenEvent);
break;
}

// case Blockly.ASTNode.types.INPUT:
case ASTNode.types.NEXT:
case ASTNode.types.PREVIOUS:
case ASTNode.types.INPUT: {
const connection = node.getLocation() as RenderedConnection;
rtl = connection.getSourceBlock().RTL;

const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions(
{focusedNode: connection},
menuOpenEvent,
);
// If no valid options, don't show a menu
if (!menuOptions?.length) return true;
const location = this.calculateLocationForConnectionMenu(connection);
ContextMenu.show(menuOpenEvent, menuOptions, rtl, workspace, location);
break;
}

case ASTNode.types.WORKSPACE: {
const workspace = node.getLocation() as WorkspaceSvg;
workspace.showContextMenu(menuOpenEvent);
break;
}

default:
console.info(`No action menu for ASTNode of type ${nodeType}`);
return false;
// TODO(google/blockly#8847): Add typeguard for IContextMenu in core when this moves over
const location = node.getLocation() as any;

Check warning on line 89 in src/actions/action_menu.ts

View workflow job for this annotation

GitHub Actions / Eslint check

Unexpected any. Specify a different type
if (location.showContextMenu) {
location.showContextMenu(menuOpenEvent);
} else {
console.info(`No action menu for ASTNode of type ${node.getType()}`);
return false;
}

setTimeout(() => {
Expand All @@ -147,85 +110,4 @@
}, 10);
return true;
}

/**
* Find a context menu action, throwing an `Error` if it is not present or
* not an action. This usefully narrows the type to `ActionRegistryItem`
* which is not exported from Blockly.
*
* @param id The id of the action.
* @returns the action.
*/
private getContextMenuAction(id: string) {
const item = ContextMenuRegistry.registry.getItem(id);
if (!item) {
throw new Error(`can't find context menu item ${id}`);
}
if (!item?.callback) {
throw new Error(`context menu item unexpectedly not action ${id}`);
}
return item;
}

/**
* Create a fake PointerEvent for opening the action menu on the specified
* block.
*
* @param block The block to open the action menu for.
* @returns screen coordinates of where to show a menu for a block
*/
private calculateLocationOfBlock(block: BlockSvg): BlocklyUtils.Coordinate {
// Get the location of the top-left corner of the block in
// screen coordinates.
const blockCoords = BlocklyUtils.svgMath.wsToScreenCoordinates(
block.workspace,
block.getRelativeToSurfaceXY(),
);

// Prefer a y position below the first field in the block.
const fieldBoundingClientRect = block.inputList
.filter((input) => input.isVisible())
.flatMap((input) => input.fieldRow)
.filter((f) => f.isVisible())[0]
?.getSvgRoot()
?.getBoundingClientRect();

const y =
fieldBoundingClientRect && fieldBoundingClientRect.height
? fieldBoundingClientRect.y + fieldBoundingClientRect.height
: blockCoords.y + block.height;

return new BlocklyUtils.Coordinate(blockCoords.x + 5, y + 5);
}

/**
* Create a fake PointerEvent for opening the action menu for the
* given connection.
*
* For now this just puts the action menu in the same place as the
* context menu for the source block.
*
* @param connection The node to open the action menu for.
* @returns Screen coordinates of where to show menu for a connection node.
*/
private calculateLocationForConnectionMenu(
connection: RenderedConnection,
): BlocklyUtils.Coordinate {
const block = connection.getSourceBlock() as BlockSvg;
const workspace = block.workspace as WorkspaceSvg;

if (typeof connection.x !== 'number') {
// No coordinates for connection? Fall back to the parent block.
return this.calculateLocationOfBlock(block);
}
const connectionWSCoords = new BlocklyUtils.Coordinate(
connection.x,
connection.y,
);
const connectionScreenCoords = BlocklyUtils.svgMath.wsToScreenCoordinates(
workspace,
connectionWSCoords,
);
return connectionScreenCoords.translate(5, 5);
}
}