diff --git a/src/actions/clipboard.ts b/src/actions/clipboard.ts index 8a815175..58323e0d 100644 --- a/src/actions/clipboard.ts +++ b/src/actions/clipboard.ts @@ -30,7 +30,7 @@ const createSerializedKey = ShortcutRegistry.registry.createSerializedKey.bind( * menu; changing individual weights relative to base weight can change * the order within the clipboard group. */ -const BASE_WEIGHT = 11; +const BASE_WEIGHT = 12; /** * Logic and state for cut/copy/paste actions as both keyboard shortcuts diff --git a/src/actions/delete.ts b/src/actions/delete.ts index ef5e7f31..7b6f2a9d 100644 --- a/src/actions/delete.ts +++ b/src/actions/delete.ts @@ -132,7 +132,7 @@ export class DeleteAction { }, scopeType: ContextMenuRegistry.ScopeType.BLOCK, id: 'blockDeleteFromContextMenu', - weight: 10, + weight: 11, }; ContextMenuRegistry.registry.register(deleteItem); diff --git a/src/actions/edit.ts b/src/actions/edit.ts new file mode 100644 index 00000000..9d6f7254 --- /dev/null +++ b/src/actions/edit.ts @@ -0,0 +1,87 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + Connection, + ContextMenuRegistry, + ShortcutRegistry, + comments, + utils as BlocklyUtils, +} from 'blockly'; +import * as Constants from '../constants'; +import type {BlockSvg, WorkspaceSvg} from 'blockly'; +import {LineCursor} from '../line_cursor'; +import {NavigationController} from '../navigation_controller'; + +const KeyCodes = BlocklyUtils.KeyCodes; + +/** + * Action to edit a block. This just moves the cursor to the first + * field or input (if there is one), and exists as an aid to + * navigational discoverability: + * + * Any time there is a cursor position that can be accessed by + * pressing the right-arrow key, which isn't accessible by pressing + * the down-arrow key (these positions are typically fields and value + * inputs), a context menu item "Edit Block contents (→︎)" will be + * shown in the block context menu. + * + * N.B.: This item is shown any time the cursor is on a block and not + * in the rightmost position 'on the current line'; that means that + * sometimes the label ("Edit block contents") is possibly misleading, + * because it might not be the contents of the _current_ block that's + * being edited, but rather that of a sibling or parent block. + * + * This action registers itself only as a context menu item, as there + * is already a corresponding "right" shortcut item. + */ +export class EditAction { + constructor(private canCurrentlyNavigate: (ws: WorkspaceSvg) => boolean) {} + + /** + * Install this action as a context menu item. + */ + install() { + this.registerContextMenuAction(); + } + + /** + * Uninstall this action as both a keyboard shortcut and a context menu item. + * Reinstall the original context menu action if possible. + */ + uninstall() { + ContextMenuRegistry.registry.unregister('edit'); + } + + /** + * Register the edit block action as a context menu item on blocks. + */ + private registerContextMenuAction() { + const editAboveItem: ContextMenuRegistry.RegistryItem = { + displayText: 'Edit Block contents (→︎)', + preconditionFn: (scope: ContextMenuRegistry.Scope) => { + const workspace = scope.block?.workspace; + if (!workspace || !this.canCurrentlyNavigate(workspace)) { + return 'disabled'; + } + const cursor = workspace.getCursor() as LineCursor | null; + if (!cursor) return 'disabled'; + return cursor.atEndOfLine() ? 'hidden' : 'enabled'; + }, + callback: (scope: ContextMenuRegistry.Scope) => { + const workspace = scope.block?.workspace; + if (!workspace) return false; + workspace.getCursor()?.in(); + return true; + }, + scopeType: ContextMenuRegistry.ScopeType.BLOCK, + id: 'edit', + weight: 10, + }; + + ContextMenuRegistry.registry.register(editAboveItem); + } +} diff --git a/src/line_cursor.ts b/src/line_cursor.ts index b86dd6d1..e01e3096 100644 --- a/src/line_cursor.ts +++ b/src/line_cursor.ts @@ -175,6 +175,22 @@ export class LineCursor extends Marker { return newNode; } + /** + * Returns true iff the node to which we would navigate if in() were + * called, which will be a validInLineNode, is also a validLineNode + * - in effect, if the LineCursor is at the end of the 'current + * line' of the program. + */ + public atEndOfLine(): boolean { + const curNode = this.getCurNode(); + if (!curNode) return false; + const rightNode = this.getNextNode( + curNode, + this.validInLineNode.bind(this), + ); + return this.validLineNode(rightNode); + } + /** * Returns true iff the given node represents the "beginning of a * new line of code" (and thus can be visited by pressing the diff --git a/src/navigation_controller.ts b/src/navigation_controller.ts index cd5464a8..739cb185 100644 --- a/src/navigation_controller.ts +++ b/src/navigation_controller.ts @@ -23,12 +23,13 @@ import { } from 'blockly/core'; import * as Constants from './constants'; -import {Navigation} from './navigation'; -import {LineCursor} from './line_cursor'; -import {ShortcutDialog} from './shortcut_dialog'; +import {Clipboard} from './actions/clipboard'; import {DeleteAction} from './actions/delete'; +import {EditAction} from './actions/edit'; import {InsertAction} from './actions/insert'; -import {Clipboard} from './actions/clipboard'; +import {LineCursor} from './line_cursor'; +import {Navigation} from './navigation'; +import {ShortcutDialog} from './shortcut_dialog'; import {WorkspaceMovement} from './actions/ws_movement'; import {ArrowNavigation} from './actions/arrow_navigation'; import {ExitAction} from './actions/exit'; @@ -64,6 +65,9 @@ export class NavigationController { this.canCurrentlyEdit.bind(this), ); + /** Context menu and keyboard action for deletion. */ + editAction: EditAction = new EditAction(this.canCurrentlyEdit.bind(this)); + /** Context menu and keyboard action for insertion. */ insertAction: InsertAction = new InsertAction( this.navigation, @@ -351,6 +355,7 @@ export class NavigationController { ShortcutRegistry.registry.register(shortcut); } this.deleteAction.install(); + this.editAction.install(); this.insertAction.install(); this.workspaceMovement.install(); this.arrowNavigation.install(); @@ -377,6 +382,7 @@ export class NavigationController { } this.deleteAction.uninstall(); + this.editAction.uninstall(); this.insertAction.uninstall(); this.disconnectAction.uninstall(); this.clipboard.uninstall();