Skip to content

Commit 545dfe5

Browse files
cpcallenmicrobit-matt-hillsdonrachel-fenichel
authored
feat: Edit option in block context menu (#274)
* refactor: Rename shortcuts to match arrow keys Rename the main directional shortcuts to match the names of corresponding keys. Not renaming the corresponding methods on LineCursor yet as that seems like a riskier change. * chore: Sort inputs in navigation_controller.ts * feat: Edit action * feat: Only show Edit option when useful If there are no inline nodes to the right of the cursor on the 'current line' of the program, hide the Edit option. This means the menu item is shown any time the cursor is on a block and not in the rightmost position 'on the current line'; consequently sometimes the description ("Edit block contents") is possibly misleading, because it might not be the contents of the _current_ block that's being navigated to, but rather that of a sibling or parent block. * fix: Adjust weights to put edit between insert and delete * fix: typo in src/constants.ts Co-authored-by: Matt Hillsdon <[email protected]> * fix: Revert f05269f for PR #274. * fix: JSDoc tweaks for PR #274 * fix: remove dead import --------- Co-authored-by: Matt Hillsdon <[email protected]> Co-authored-by: Rachel Fenichel <[email protected]>
1 parent 219eb37 commit 545dfe5

File tree

5 files changed

+115
-6
lines changed

5 files changed

+115
-6
lines changed

src/actions/clipboard.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const createSerializedKey = ShortcutRegistry.registry.createSerializedKey.bind(
3030
* menu; changing individual weights relative to base weight can change
3131
* the order within the clipboard group.
3232
*/
33-
const BASE_WEIGHT = 11;
33+
const BASE_WEIGHT = 12;
3434

3535
/**
3636
* Logic and state for cut/copy/paste actions as both keyboard shortcuts

src/actions/delete.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export class DeleteAction {
132132
},
133133
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
134134
id: 'blockDeleteFromContextMenu',
135-
weight: 10,
135+
weight: 11,
136136
};
137137

138138
ContextMenuRegistry.registry.register(deleteItem);

src/actions/edit.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {
8+
Connection,
9+
ContextMenuRegistry,
10+
ShortcutRegistry,
11+
comments,
12+
utils as BlocklyUtils,
13+
} from 'blockly';
14+
import * as Constants from '../constants';
15+
import type {BlockSvg, WorkspaceSvg} from 'blockly';
16+
import {LineCursor} from '../line_cursor';
17+
import {NavigationController} from '../navigation_controller';
18+
19+
const KeyCodes = BlocklyUtils.KeyCodes;
20+
21+
/**
22+
* Action to edit a block. This just moves the cursor to the first
23+
* field or input (if there is one), and exists as an aid to
24+
* navigational discoverability:
25+
*
26+
* Any time there is a cursor position that can be accessed by
27+
* pressing the right-arrow key, which isn't accessible by pressing
28+
* the down-arrow key (these positions are typically fields and value
29+
* inputs), a context menu item "Edit Block contents (→︎)" will be
30+
* shown in the block context menu.
31+
*
32+
* N.B.: This item is shown any time the cursor is on a block and not
33+
* in the rightmost position 'on the current line'; that means that
34+
* sometimes the label ("Edit block contents") is possibly misleading,
35+
* because it might not be the contents of the _current_ block that's
36+
* being edited, but rather that of a sibling or parent block.
37+
*
38+
* This action registers itself only as a context menu item, as there
39+
* is already a corresponding "right" shortcut item.
40+
*/
41+
export class EditAction {
42+
constructor(private canCurrentlyNavigate: (ws: WorkspaceSvg) => boolean) {}
43+
44+
/**
45+
* Install this action as a context menu item.
46+
*/
47+
install() {
48+
this.registerContextMenuAction();
49+
}
50+
51+
/**
52+
* Uninstall this action as both a keyboard shortcut and a context menu item.
53+
* Reinstall the original context menu action if possible.
54+
*/
55+
uninstall() {
56+
ContextMenuRegistry.registry.unregister('edit');
57+
}
58+
59+
/**
60+
* Register the edit block action as a context menu item on blocks.
61+
*/
62+
private registerContextMenuAction() {
63+
const editAboveItem: ContextMenuRegistry.RegistryItem = {
64+
displayText: 'Edit Block contents (→︎)',
65+
preconditionFn: (scope: ContextMenuRegistry.Scope) => {
66+
const workspace = scope.block?.workspace;
67+
if (!workspace || !this.canCurrentlyNavigate(workspace)) {
68+
return 'disabled';
69+
}
70+
const cursor = workspace.getCursor() as LineCursor | null;
71+
if (!cursor) return 'disabled';
72+
return cursor.atEndOfLine() ? 'hidden' : 'enabled';
73+
},
74+
callback: (scope: ContextMenuRegistry.Scope) => {
75+
const workspace = scope.block?.workspace;
76+
if (!workspace) return false;
77+
workspace.getCursor()?.in();
78+
return true;
79+
},
80+
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
81+
id: 'edit',
82+
weight: 10,
83+
};
84+
85+
ContextMenuRegistry.registry.register(editAboveItem);
86+
}
87+
}

src/line_cursor.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,22 @@ export class LineCursor extends Marker {
175175
return newNode;
176176
}
177177

178+
/**
179+
* Returns true iff the node to which we would navigate if in() were
180+
* called, which will be a validInLineNode, is also a validLineNode
181+
* - in effect, if the LineCursor is at the end of the 'current
182+
* line' of the program.
183+
*/
184+
public atEndOfLine(): boolean {
185+
const curNode = this.getCurNode();
186+
if (!curNode) return false;
187+
const rightNode = this.getNextNode(
188+
curNode,
189+
this.validInLineNode.bind(this),
190+
);
191+
return this.validLineNode(rightNode);
192+
}
193+
178194
/**
179195
* Returns true iff the given node represents the "beginning of a
180196
* new line of code" (and thus can be visited by pressing the

src/navigation_controller.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ import {
2323
} from 'blockly/core';
2424

2525
import * as Constants from './constants';
26-
import {Navigation} from './navigation';
27-
import {LineCursor} from './line_cursor';
28-
import {ShortcutDialog} from './shortcut_dialog';
26+
import {Clipboard} from './actions/clipboard';
2927
import {DeleteAction} from './actions/delete';
28+
import {EditAction} from './actions/edit';
3029
import {InsertAction} from './actions/insert';
31-
import {Clipboard} from './actions/clipboard';
30+
import {LineCursor} from './line_cursor';
31+
import {Navigation} from './navigation';
32+
import {ShortcutDialog} from './shortcut_dialog';
3233
import {WorkspaceMovement} from './actions/ws_movement';
3334
import {ArrowNavigation} from './actions/arrow_navigation';
3435
import {ExitAction} from './actions/exit';
@@ -64,6 +65,9 @@ export class NavigationController {
6465
this.canCurrentlyEdit.bind(this),
6566
);
6667

68+
/** Context menu and keyboard action for deletion. */
69+
editAction: EditAction = new EditAction(this.canCurrentlyEdit.bind(this));
70+
6771
/** Context menu and keyboard action for insertion. */
6872
insertAction: InsertAction = new InsertAction(
6973
this.navigation,
@@ -351,6 +355,7 @@ export class NavigationController {
351355
ShortcutRegistry.registry.register(shortcut);
352356
}
353357
this.deleteAction.install();
358+
this.editAction.install();
354359
this.insertAction.install();
355360
this.workspaceMovement.install();
356361
this.arrowNavigation.install();
@@ -377,6 +382,7 @@ export class NavigationController {
377382
}
378383

379384
this.deleteAction.uninstall();
385+
this.editAction.uninstall();
380386
this.insertAction.uninstall();
381387
this.disconnectAction.uninstall();
382388
this.clipboard.uninstall();

0 commit comments

Comments
 (0)