Skip to content

Commit 8d5654c

Browse files
authored
feat: Add flag to enable/disable cursor visiting stack nodes (#197)
* feat: Add options object for LineCursor Add a CursorOptions type and add options objects of that type to LineCursor and its constructor. The only available (but not yet implemented) option is stackConnections, defaulting to true. * refactor: Ensure predicates can access this Use bind when passing isValidLineNode and isValidInlineNode as arguments to getNextNode and getPreviousNode, so that those methods can access this.options. * refactor: Use protected instead of @Protected * fix: have isValidInlineNode use isValidLineNode This ensures that all valid line nodes are also valid inline nodes, which was the intention (but had accidentally been broken by PR #162, and could easily be further broken by this PR without sufficient care). * feat: Implement the stackConnections: false option Prevent stack connections, except for unconnected 'next' connections (and unconnected next statement inputs), from being visited if stackConnections option is false. * feat: Plumb CursorOptions through KeyboardNavigation Add a NavigationOptions type and use it for an options object for the KeyboardNavigation constructor. For now it just has a single property of type CursorOptions, but I anticipate adding flags for different test scenarios (e.g. which key opens context menu, on which nodes various options are presented). Will plumb this through the test code in the next RP.
1 parent fb7c7b8 commit 8d5654c

File tree

2 files changed

+75
-18
lines changed

2 files changed

+75
-18
lines changed

src/index.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,17 @@
66

77
import * as Blockly from 'blockly/core';
88
import {NavigationController} from './navigation_controller';
9-
import {LineCursor} from './line_cursor';
9+
import {CursorOptions, LineCursor} from './line_cursor';
10+
11+
/** Options object for KeyboardNavigation instances. */
12+
export type NavigationOptions = {
13+
cursor: Partial<CursorOptions>;
14+
};
15+
16+
/** Default options for LineCursor instances. */
17+
const defaultOptions: NavigationOptions = {
18+
cursor: {},
19+
};
1020

1121
/** Plugin for keyboard navigation. */
1222
export class KeyboardNavigation {
@@ -39,9 +49,15 @@ export class KeyboardNavigation {
3949
* @param workspace The workspace that the plugin will
4050
* be added to.
4151
*/
42-
constructor(workspace: Blockly.WorkspaceSvg) {
52+
constructor(
53+
workspace: Blockly.WorkspaceSvg,
54+
options: Partial<NavigationOptions>,
55+
) {
4356
this.workspace = workspace;
4457

58+
// Regularise options and apply defaults.
59+
options = {...defaultOptions, ...options};
60+
4561
this.navigationController = new NavigationController();
4662
this.navigationController.init();
4763
this.navigationController.addWorkspace(workspace);
@@ -51,7 +67,7 @@ export class KeyboardNavigation {
5167
this.originalTheme = workspace.getTheme();
5268
this.setGlowTheme();
5369

54-
this.cursor = new LineCursor(workspace);
70+
this.cursor = new LineCursor(workspace, options.cursor);
5571
this.cursor.install();
5672

5773
// Ensure that only the root SVG G (group) has a tab index.

src/line_cursor.ts

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,29 @@
1616
import * as Blockly from 'blockly/core';
1717
import {ASTNode, Marker} from 'blockly/core';
1818

19+
/** Options object for LineCursor instances. */
20+
export type CursorOptions = {
21+
/**
22+
* Can the cursor visit all stack connections (next/previous), or
23+
* (if false) only unconnected next connections?
24+
*/
25+
stackConnections: boolean;
26+
};
27+
28+
/** Default options for LineCursor instances. */
29+
const defaultOptions: CursorOptions = {
30+
stackConnections: true,
31+
};
32+
1933
/**
2034
* Class for a line cursor.
2135
*/
2236
export class LineCursor extends Marker {
2337
override type = 'cursor';
2438

39+
/** Options for this line cursor. */
40+
private readonly options: CursorOptions;
41+
2542
/** Has the cursor been installed in a workspace's marker manager? */
2643
private installed = false;
2744

@@ -31,10 +48,15 @@ export class LineCursor extends Marker {
3148
/**
3249
* @param workspace The workspace this cursor belongs to.
3350
*/
34-
constructor(public readonly workspace: Blockly.WorkspaceSvg) {
51+
constructor(
52+
public readonly workspace: Blockly.WorkspaceSvg,
53+
options?: Partial<CursorOptions>,
54+
) {
3555
super();
3656
// Bind selectListener to facilitate future install/uninstall.
3757
this.selectListener = this.selectListener.bind(this);
58+
// Regularise options and apply defaults.
59+
this.options = {...defaultOptions, ...options};
3860
}
3961

4062
/**
@@ -79,7 +101,7 @@ export class LineCursor extends Marker {
79101
if (!curNode) {
80102
return null;
81103
}
82-
let newNode = this.getNextNode(curNode, this.validLineNode);
104+
let newNode = this.getNextNode(curNode, this.validLineNode.bind(this));
83105

84106
if (newNode) {
85107
this.setCurNode(newNode);
@@ -99,7 +121,7 @@ export class LineCursor extends Marker {
99121
if (!curNode) {
100122
return null;
101123
}
102-
const newNode = this.getNextNode(curNode, this.validInLineNode);
124+
const newNode = this.getNextNode(curNode, this.validInLineNode.bind(this));
103125

104126
if (newNode) {
105127
this.setCurNode(newNode);
@@ -118,7 +140,7 @@ export class LineCursor extends Marker {
118140
if (!curNode) {
119141
return null;
120142
}
121-
let newNode = this.getPreviousNode(curNode, this.validLineNode);
143+
let newNode = this.getPreviousNode(curNode, this.validLineNode.bind(this));
122144

123145
if (newNode) {
124146
this.setCurNode(newNode);
@@ -138,7 +160,10 @@ export class LineCursor extends Marker {
138160
if (!curNode) {
139161
return null;
140162
}
141-
const newNode = this.getPreviousNode(curNode, this.validInLineNode);
163+
const newNode = this.getPreviousNode(
164+
curNode,
165+
this.validInLineNode.bind(this),
166+
);
142167

143168
if (newNode) {
144169
this.setCurNode(newNode);
@@ -158,11 +183,18 @@ export class LineCursor extends Marker {
158183
* This is to facilitate connecting additional blocks to a
159184
* stack/substack.
160185
*
186+
* If options.stackConnections is true (the default) then allow the
187+
* cursor to visit all (useful) stack connection by additionally
188+
* returning true for:
189+
*
190+
* - Any next statement input
191+
* - Any 'next' connection.
192+
* - An unconnected previous statement input.
193+
*
161194
* @param node The AST node to check.
162195
* @returns True if the node should be visited, false otherwise.
163-
* @protected
164196
*/
165-
validLineNode(node: ASTNode | null): boolean {
197+
protected validLineNode(node: ASTNode | null): boolean {
166198
if (!node) return false;
167199
const location = node.getLocation();
168200
const type = node && node.getType();
@@ -171,38 +203,47 @@ export class LineCursor extends Marker {
171203
return !(location as Blockly.Block).outputConnection?.isConnected();
172204
case ASTNode.types.INPUT:
173205
const connection = location as Blockly.Connection;
174-
return connection.type === Blockly.NEXT_STATEMENT;
206+
return (
207+
connection.type === Blockly.NEXT_STATEMENT &&
208+
(this.options.stackConnections || !connection.isConnected())
209+
);
175210
case ASTNode.types.NEXT:
176-
return true;
211+
return (
212+
this.options.stackConnections ||
213+
!(location as Blockly.Connection).isConnected()
214+
);
177215
case ASTNode.types.PREVIOUS:
178-
return !(location as Blockly.Connection).isConnected();
216+
return (
217+
this.options.stackConnections &&
218+
!(location as Blockly.Connection).isConnected()
219+
);
179220
default:
180221
return false;
181222
}
182223
}
183224

184225
/**
185226
* Returns true iff the given node can be visited by the cursor when
186-
* using the left/right arrow keys. Specifically, if the node is for:
227+
* using the left/right arrow keys. Specifically, if the node is
228+
* for any node for which valideLineNode would return true, plus:
187229
*
188230
* - Any block.
189-
* - Any field.
231+
* - Any field that is not a full block field.
190232
* - Any unconnected next or input connection. This is to
191233
* facilitate connecting additional blocks.
192234
*
193235
* @param node The AST node to check whether it is valid.
194236
* @returns True if the node should be visited, false otherwise.
195-
* @protected
196237
*/
197-
validInLineNode(node: ASTNode | null): boolean {
238+
protected validInLineNode(node: ASTNode | null): boolean {
198239
if (!node) return false;
240+
if (this.validLineNode(node)) return true;
199241
const location = node.getLocation();
200242
const type = node && node.getType();
201243
switch (type) {
202244
case ASTNode.types.BLOCK:
203245
return true;
204246
case ASTNode.types.INPUT:
205-
case ASTNode.types.NEXT:
206247
return !(location as Blockly.Connection).isConnected();
207248
case ASTNode.types.FIELD:
208249
// @ts-expect-error isFullBlockField is a protected method.

0 commit comments

Comments
 (0)