Skip to content

Commit cda9d83

Browse files
committed
feat: Introduce Mover class, moving mode
Introduce the Mover class, with shortcuts that will put a workspace into (and exit from) moving mode. While in moving mode, cursor navigation via the cursor keys is disabled.
1 parent 955e1fd commit cda9d83

File tree

3 files changed

+276
-5
lines changed

3 files changed

+276
-5
lines changed

src/actions/mover.ts

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import * as Constants from '../constants';
8+
import {ASTNode, ShortcutRegistry, utils as BlocklyUtils} from 'blockly';
9+
import type {Block, WorkspaceSvg} from 'blockly';
10+
import {Navigation} from '../navigation';
11+
12+
const KeyCodes = BlocklyUtils.KeyCodes;
13+
const createSerializedKey = ShortcutRegistry.registry.createSerializedKey.bind(
14+
ShortcutRegistry.registry,
15+
);
16+
17+
/**
18+
* Actions for moving blocks with keyboard shortcuts.
19+
*/
20+
export class Mover {
21+
/**
22+
* Function provided by the navigation controller to say whether editing
23+
* is allowed.
24+
*/
25+
protected canCurrentlyEdit: (ws: WorkspaceSvg) => boolean;
26+
27+
/**
28+
* Map of moves in progress.
29+
*
30+
* An entry for a given workspace in this map means that the this
31+
* Mover is moving a block on that workspace, and will disable
32+
* normal cursor movement until the move is complete.
33+
*/
34+
protected moves: Map<WorkspaceSvg, MoveInfo> = new Map();
35+
36+
constructor(
37+
protected navigation: Navigation,
38+
canEdit: (ws: WorkspaceSvg) => boolean,
39+
) {
40+
this.canCurrentlyEdit = canEdit;
41+
}
42+
43+
private shortcuts: ShortcutRegistry.KeyboardShortcut[] = [
44+
// Begin and end move.
45+
{
46+
name: 'Start move',
47+
preconditionFn: (workspace) => this.canMove(workspace),
48+
callback: (workspace) => this.startMove(workspace),
49+
keyCodes: [KeyCodes.M],
50+
allowCollision: true, // TODO: remove once #309 has been merged.
51+
},
52+
{
53+
name: 'Finish move',
54+
preconditionFn: (workspace) => this.isMoving(workspace),
55+
callback: (workspace) => this.finishMove(workspace),
56+
keyCodes: [KeyCodes.ENTER],
57+
allowCollision: true,
58+
},
59+
{
60+
name: 'Abort move',
61+
preconditionFn: (workspace) => this.isMoving(workspace),
62+
callback: (workspace) => this.abortMove(workspace),
63+
keyCodes: [KeyCodes.ESC],
64+
allowCollision: true,
65+
},
66+
67+
// Constrained moves.
68+
{
69+
name: 'Move left, constrained',
70+
preconditionFn: (workspace) => this.isMoving(workspace),
71+
callback: (workspace) => this.moveConstrained(workspace /* , ...*/),
72+
keyCodes: [KeyCodes.LEFT],
73+
allowCollision: true,
74+
},
75+
{
76+
name: 'Move right unconstraind',
77+
preconditionFn: (workspace) => this.isMoving(workspace),
78+
callback: (workspace) => this.moveConstrained(workspace /* , ... */),
79+
keyCodes: [KeyCodes.RIGHT],
80+
allowCollision: true,
81+
},
82+
{
83+
name: 'Move up, constrained',
84+
preconditionFn: (workspace) => this.isMoving(workspace),
85+
callback: (workspace) => this.moveConstrained(workspace /* , ... */),
86+
keyCodes: [KeyCodes.UP],
87+
allowCollision: true,
88+
},
89+
{
90+
name: 'Move down constrained',
91+
preconditionFn: (workspace) => this.isMoving(workspace),
92+
callback: (workspace) => this.moveConstrained(workspace /* , ... */),
93+
keyCodes: [KeyCodes.DOWN],
94+
allowCollision: true,
95+
},
96+
97+
// Unconstrained moves.
98+
{
99+
name: 'Move left, unconstrained',
100+
preconditionFn: (workspace) => this.isMoving(workspace),
101+
callback: (workspace) => this.moveUnconstrained(workspace, -1, 0),
102+
keyCodes: [createSerializedKey(KeyCodes.LEFT, [KeyCodes.ALT])],
103+
},
104+
{
105+
name: 'Move right, unconstraind',
106+
preconditionFn: (workspace) => this.isMoving(workspace),
107+
callback: (workspace) => this.moveUnconstrained(workspace, 1, 0),
108+
keyCodes: [createSerializedKey(KeyCodes.RIGHT, [KeyCodes.ALT])],
109+
},
110+
{
111+
name: 'Move up unconstrained',
112+
preconditionFn: (workspace) => this.isMoving(workspace),
113+
callback: (workspace) => this.moveUnconstrained(workspace, 0, -1),
114+
keyCodes: [createSerializedKey(KeyCodes.UP, [KeyCodes.ALT])],
115+
},
116+
{
117+
name: 'Move down, unconstrained',
118+
preconditionFn: (workspace) => this.isMoving(workspace),
119+
callback: (workspace) => this.moveUnconstrained(workspace, 0, 1),
120+
keyCodes: [createSerializedKey(KeyCodes.DOWN, [KeyCodes.ALT])],
121+
},
122+
];
123+
124+
/**
125+
* Install the actions as both keyboard shortcuts and (where
126+
* applicable) context menu items.
127+
*/
128+
install() {
129+
for (const shortcut of this.shortcuts) {
130+
ShortcutRegistry.registry.register(shortcut);
131+
}
132+
}
133+
134+
/**
135+
* Uninstall these actions.
136+
*/
137+
uninstall() {
138+
for (const shortcut of this.shortcuts) {
139+
ShortcutRegistry.registry.unregister(shortcut.name);
140+
}
141+
}
142+
143+
/**
144+
* Returns true iff we are able to begin moving a block on the given
145+
* workspace.
146+
*
147+
* @param workspace The workspace to move on.
148+
* @returns True iff we can beign a move.
149+
*/
150+
canMove(workspace: WorkspaceSvg) {
151+
// TODO: also check if current block is movable.
152+
return (
153+
this.navigation.getState(workspace) === Constants.STATE.WORKSPACE &&
154+
this.canCurrentlyEdit(workspace) &&
155+
!this.moves.has(workspace)
156+
);
157+
}
158+
159+
/**
160+
* Returns true iff we are currently moving a block on the given
161+
* workspace.
162+
*
163+
* @param workspace The workspace we might be moving on.
164+
* @returns True iff we are moving.
165+
*/
166+
isMoving(workspace: WorkspaceSvg) {
167+
return this.canCurrentlyEdit(workspace) && this.moves.has(workspace);
168+
}
169+
170+
/**
171+
* Start moving the currently-focused item on workspace, if
172+
* possible.
173+
*
174+
* Should only be called if canMove has returned true.
175+
*
176+
* @param workspace The workspace we might be moving on.
177+
* @returns True iff a move has successfully begun.
178+
*/
179+
startMove(workspace: WorkspaceSvg) {
180+
const cursor = workspace?.getCursor();
181+
const curNode = cursor?.getCurNode();
182+
const block = curNode?.getSourceBlock();
183+
if (!block || !block.isMovable()) return false;
184+
185+
console.log('startMove');
186+
187+
this.moves.set(workspace, {});
188+
return true;
189+
}
190+
191+
/**
192+
* Finish moving the currently-focused item on workspace.
193+
*
194+
* Should only be called if isMoving has returned true.
195+
*
196+
* @param workspace The workspace on which we are moving.
197+
* @returns True iff move successfully finished.
198+
*/
199+
finishMove(workspace: WorkspaceSvg) {
200+
if (!workspace) return false;
201+
202+
console.log('finishMove');
203+
204+
this.moves.delete(workspace);
205+
return true;
206+
}
207+
208+
/**
209+
* Abort moving the currently-focused item on workspace.
210+
*
211+
* Should only be called if isMoving has returned true.
212+
*
213+
* @param workspace The workspace on which we are moving.
214+
* @returns True iff move successfully aborted.
215+
*/
216+
abortMove(workspace: WorkspaceSvg) {
217+
if (!workspace) return false;
218+
219+
console.log('abortMove');
220+
221+
this.moves.delete(workspace);
222+
return true;
223+
}
224+
225+
/**
226+
* Action to move the item being moved in the given direction,
227+
* constrained to valid attachment points (if any).
228+
*
229+
* @param workspace The workspace to move on.
230+
* @returns True iff this action applies and has been performed.
231+
*/
232+
moveConstrained(
233+
workspace: WorkspaceSvg,
234+
/* ... */
235+
) {
236+
console.log('moveConstrained');
237+
// Not yet implemented. Absorb keystroke to avoid moving cursor.
238+
return true;
239+
}
240+
241+
/**
242+
* Action to move the item being moved in the given direction,
243+
* without constraint.
244+
*
245+
* @param workspace The workspace to move on.
246+
* @param xDirection -1 to move left. 1 to move right.
247+
* @param yDirection -1 to move up. 1 to move down.
248+
* @returns True iff this action applies and has been performed.
249+
*/
250+
moveUnconstrained(
251+
workspace: WorkspaceSvg,
252+
xDirection: number,
253+
yDirection: number,
254+
): boolean {
255+
console.log('moveUnconstrained');
256+
// Not yet implemented. Absorb keystroke to avoid moving cursor.
257+
return true;
258+
}
259+
}
260+
261+
/**
262+
* Information about the currently in-progress move for a given
263+
* Workspace.
264+
*/
265+
type MoveInfo = {};

src/actions/ws_movement.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export class WorkspaceMovement {
5555
callback: (workspace) => this.moveWSCursor(workspace, 0, -1),
5656
keyCodes: [createSerializedKey(KeyCodes.W, [KeyCodes.SHIFT])],
5757
},
58-
58+
5959
/** Move the cursor on the workspace down. */
6060
{
6161
name: Constants.SHORTCUT_NAMES.MOVE_WS_CURSOR_DOWN,

src/navigation_controller.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {ExitAction} from './actions/exit';
3434
import {EnterAction} from './actions/enter';
3535
import {DisconnectAction} from './actions/disconnect';
3636
import {ActionMenu} from './actions/action_menu';
37+
import {Mover} from './actions/mover';
3738

3839
const KeyCodes = BlocklyUtils.KeyCodes;
3940
const createSerializedKey = ShortcutRegistry.registry.createSerializedKey.bind(
@@ -99,8 +100,12 @@ export class NavigationController {
99100
this.canCurrentlyNavigate.bind(this),
100101
);
101102

103+
mover = new Mover(this.navigation, this.canCurrentlyEdit.bind(this));
104+
102105
navigationFocus: NAVIGATION_FOCUS_MODE = NAVIGATION_FOCUS_MODE.NONE;
103106

107+
hasNavigationFocus: boolean = false;
108+
104109
/**
105110
* Original Toolbox.prototype.onShortcut method, saved by
106111
* addShortcutHandlers.
@@ -504,6 +509,7 @@ export class NavigationController {
504509
this.actionMenu.install();
505510

506511
this.clipboard.install();
512+
this.mover.install();
507513
this.shortcutDialog.install();
508514

509515
// Initialize the shortcut modal with available shortcuts. Needs
@@ -516,10 +522,7 @@ export class NavigationController {
516522
* Removes all the keyboard navigation shortcuts.
517523
*/
518524
dispose() {
519-
for (const shortcut of Object.values(this.shortcuts)) {
520-
ShortcutRegistry.registry.unregister(shortcut.name);
521-
}
522-
525+
this.mover.install();
523526
this.deleteAction.uninstall();
524527
this.insertAction.uninstall();
525528
this.disconnectAction.uninstall();
@@ -530,6 +533,9 @@ export class NavigationController {
530533
this.actionMenu.uninstall();
531534
this.shortcutDialog.uninstall();
532535

536+
for (const shortcut of Object.values(this.shortcuts)) {
537+
ShortcutRegistry.registry.unregister(shortcut.name);
538+
}
533539
this.removeShortcutHandlers();
534540
this.navigation.dispose();
535541
}

0 commit comments

Comments
 (0)