Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
231 changes: 231 additions & 0 deletions src/actions/enter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {ASTNode, ShortcutRegistry, utils as BlocklyUtils} from 'blockly/core';

import type {
Block,
BlockSvg,
Field,
FlyoutButton,
WorkspaceSvg,
} from 'blockly/core';

import * as Constants from '../constants';
import type {Navigation} from '../navigation';

const KeyCodes = BlocklyUtils.KeyCodes;

/**
* Class for registering a shortcut for the enter action.
*/
export class EnterAction {
constructor(
private navigation: Navigation,
private canCurrentlyEdit: (ws: WorkspaceSvg) => boolean,
) {}

/**
* Adds the enter action shortcut to the registry.
*/
install() {
/**
* 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.
*/
ShortcutRegistry.registry.register({
name: Constants.SHORTCUT_NAMES.EDIT_OR_CONFIRM,
preconditionFn: (workspace) => this.canCurrentlyEdit(workspace),
callback: (workspace, event) => {
event.preventDefault();

let flyoutCursor;
let curNode;
let nodeType;

switch (this.navigation.getState(workspace)) {
case Constants.STATE.WORKSPACE:
this.handleEnterForWS(workspace);
return true;
case Constants.STATE.FLYOUT:
flyoutCursor = this.navigation.getFlyoutCursor(workspace);
if (!flyoutCursor) {
return false;
}
curNode = flyoutCursor.getCurNode();
nodeType = curNode.getType();

switch (nodeType) {
case ASTNode.types.STACK:
this.insertFromFlyout(workspace);
break;
case ASTNode.types.BUTTON:
this.triggerButtonCallback(workspace);
break;
}

return true;
default:
return false;
}
},
keyCodes: [KeyCodes.ENTER, KeyCodes.SPACE],
});
}

/**
* Handles hitting the enter key on the workspace.
*
* @param workspace The workspace.
*/
private handleEnterForWS(workspace: WorkspaceSvg) {
const cursor = workspace.getCursor();
if (!cursor) return;
const curNode = cursor.getCurNode();
const nodeType = curNode.getType();
if (nodeType === ASTNode.types.FIELD) {
(curNode.getLocation() as Field).showEditor();
} else if (nodeType === ASTNode.types.BLOCK) {
const block = curNode.getLocation() as Block;
if (!this.tryShowFullBlockFieldEditor(block)) {
const metaKey = navigator.platform.startsWith('Mac') ? 'Cmd' : 'Ctrl';
const canMoveInHint = `Press right arrow to move in or ${metaKey} + Enter for more options`;
const genericHint = `Press ${metaKey} + Enter for options`;
const hint =
curNode.in()?.getSourceBlock() === block
? canMoveInHint
: genericHint;
alert(hint);
}
} else if (curNode.isConnection() || nodeType === ASTNode.types.WORKSPACE) {
this.navigation.openToolboxOrFlyout(workspace);
} else if (nodeType === ASTNode.types.STACK) {
console.warn('Cannot mark a stack.');
}
}

/**
* Inserts a block from the flyout.
* Tries to find a connection on the block to connect to the marked
* location. If no connection has been marked, or there is not a compatible
* connection then the block is placed on the workspace.
*
* @param workspace The main workspace. The workspace
* the block will be placed on.
*/
private insertFromFlyout(workspace: WorkspaceSvg) {
const stationaryNode = workspace.getCursor()?.getCurNode();
const newBlock = this.createNewBlock(workspace);
if (!newBlock) return;
if (stationaryNode) {
if (
!this.navigation.tryToConnectNodes(
workspace,
stationaryNode,
ASTNode.createBlockNode(newBlock)!,
)
) {
console.warn(
'Something went wrong while inserting a block from the flyout.',
);
}
}

this.navigation.focusWorkspace(workspace);
workspace.getCursor()!.setCurNode(ASTNode.createBlockNode(newBlock)!);
this.navigation.removeMark(workspace);
}

/**
* Triggers a flyout button's callback.
*
* @param workspace The main workspace. The workspace
* containing a flyout with a button.
*/
private triggerButtonCallback(workspace: WorkspaceSvg) {
const button = this.navigation
.getFlyoutCursor(workspace)!
.getCurNode()
.getLocation() as FlyoutButton;
const buttonCallback = (workspace as any).flyoutButtonCallbacks.get(
(button as any).callbackKey,
);
if (typeof buttonCallback === 'function') {
buttonCallback(button);
} else if (!button.isLabel()) {
throw new Error('No callback function found for flyout button.');
}
}

/**
* If this block has a full block field then show its editor.
*
* @param block A block.
* @returns True if we showed the editor, false otherwise.
*/
private tryShowFullBlockFieldEditor(block: Block): boolean {
if (block.isSimpleReporter()) {
for (const input of block.inputList) {
for (const field of input.fieldRow) {
// @ts-expect-error isFullBlockField is a protected method.
if (field.isClickable() && field.isFullBlockField()) {
field.showEditor();
return true;
}
}
}
}
return false;
}

/**
* Creates a new block based on the current block the flyout cursor is on.
*
* @param workspace The main workspace. The workspace
* the block will be placed on.
* @returns The newly created block.
*/
private createNewBlock(workspace: WorkspaceSvg): BlockSvg | null {
const flyout = workspace.getFlyout();
if (!flyout || !flyout.isVisible()) {
console.warn(
'Trying to insert from the flyout when the flyout does not ' +
' exist or is not visible',
);
return null;
}

const curBlock = this.navigation
.getFlyoutCursor(workspace)!
.getCurNode()
.getLocation() as BlockSvg;
if (!curBlock.isEnabled()) {
console.warn("Can't insert a disabled block.");
return null;
}

const newBlock = flyout.createBlock(curBlock);
// Render to get the sizing right.
newBlock.render();
// Connections are not tracked when the block is first created. Normally
// there's enough time for them to become tracked in the user's mouse
// movements, but not here.
newBlock.setConnectionTracking(true);
return newBlock;
}

/**
* Removes the enter action shortcut.
*/
uninstall() {
ShortcutRegistry.registry.unregister(
Constants.SHORTCUT_NAMES.EDIT_OR_CONFIRM,
);
}
}
143 changes: 0 additions & 143 deletions src/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,74 +540,6 @@ export class Navigation {
return cursor as FlyoutCursor;
}

/**
* Inserts a block from the flyout.
* Tries to find a connection on the block to connect to the marked
* location. If no connection has been marked, or there is not a compatible
* connection then the block is placed on the workspace.
*
* @param workspace The main workspace. The workspace
* the block will be placed on.
*/
insertFromFlyout(workspace: Blockly.WorkspaceSvg) {
const newBlock = this.createNewBlock(workspace);
if (!newBlock) return;
if (this.markedNode) {
if (
!this.tryToConnectNodes(
workspace,
this.markedNode,
Blockly.ASTNode.createBlockNode(newBlock)!,
)
) {
this.warn(
'Something went wrong while inserting a block from the flyout.',
);
}
}

this.focusWorkspace(workspace);
workspace
.getCursor()!
.setCurNode(Blockly.ASTNode.createBlockNode(newBlock)!);
this.removeMark(workspace);
}

/**
* Creates a new block based on the current block the flyout cursor is on.
*
* @param workspace The main workspace. The workspace
* the block will be placed on.
* @returns The newly created block.
*/
createNewBlock(workspace: Blockly.WorkspaceSvg): Blockly.BlockSvg | null {
const flyout = workspace.getFlyout();
if (!flyout || !flyout.isVisible()) {
this.warn(
'Trying to insert from the flyout when the flyout does not ' +
' exist or is not visible',
);
return null;
}

const curBlock = this.getFlyoutCursor(workspace)!
.getCurNode()
.getLocation() as Blockly.BlockSvg;
if (!curBlock.isEnabled()) {
this.warn("Can't insert a disabled block.");
return null;
}

const newBlock = flyout.createBlock(curBlock);
// Render to get the sizing right.
newBlock.render();
// Connections are not tracked when the block is first created. Normally
// there's enough time for them to become tracked in the user's mouse
// movements, but not here.
newBlock.setConnectionTracking(true);
return newBlock;
}

/**
* Hides the flyout cursor and optionally hides the flyout.
*
Expand Down Expand Up @@ -1168,40 +1100,6 @@ export class Navigation {
console.error(msg);
}

/**
* Handles hitting the enter key on the workspace.
*
* @param workspace The workspace.
*/
handleEnterForWS(workspace: Blockly.WorkspaceSvg) {
const cursor = workspace.getCursor();
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) {
const block = curNode.getLocation() as Blockly.Block;
if (!tryShowFullBlockFieldEditor(block)) {
const metaKey = navigator.platform.startsWith('Mac') ? 'Cmd' : 'Ctrl';
const canMoveInHint = `Press right arrow to move in or ${metaKey} + Enter for more options`;
const genericHint = `Press ${metaKey} + Enter for options`;
const hint =
curNode.in()?.getSourceBlock() === block
? canMoveInHint
: genericHint;
alert(hint);
}
} else if (
curNode.isConnection() ||
nodeType === Blockly.ASTNode.types.WORKSPACE
) {
this.openToolboxOrFlyout(workspace);
} else if (nodeType === Blockly.ASTNode.types.STACK) {
this.warn('Cannot mark a stack.');
}
}

/**
* Save the current cursor location and open the toolbox or flyout
* to select and insert a block.
Expand Down Expand Up @@ -1355,26 +1253,6 @@ export class Navigation {
return false;
}

/**
* Triggers a flyout button's callback.
*
* @param workspace The main workspace. The workspace
* containing a flyout with a button.
*/
triggerButtonCallback(workspace: Blockly.WorkspaceSvg) {
const button = this.getFlyoutCursor(workspace)!
.getCurNode()
.getLocation() as Blockly.FlyoutButton;
const buttonCallback = (workspace as any).flyoutButtonCallbacks.get(
(button as any).callbackKey,
);
if (typeof buttonCallback === 'function') {
buttonCallback(button);
} else if (!button.isLabel()) {
throw new Error('No callback function found for flyout button.');
}
}

/**
* Removes the change listeners on all registered workspaces.
*/
Expand Down Expand Up @@ -1486,24 +1364,3 @@ function fakeEventForConnectionNode(node: Blockly.ASTNode): PointerEvent {
clientY: connectionScreenCoords.y + 5,
});
}

/**
* If this block has a full block field then show its editor.
*
* @param block A block.
* @returns True if we showed the editor, false otherwise.
*/
function tryShowFullBlockFieldEditor(block: Blockly.Block): boolean {
if (block.isSimpleReporter()) {
for (const input of block.inputList) {
for (const field of input.fieldRow) {
// @ts-expect-error isFullBlockField is a protected method.
if (field.isClickable() && field.isFullBlockField()) {
field.showEditor();
return true;
}
}
}
}
return false;
}
Loading