Skip to content

Commit b86c6c7

Browse files
authored
feat: Make cursor arrow keys behave as per #129 (#143)
* feat: Make cursor arrow keys behave as per #129 Modify LineCursor so that movement via arrow keys behaves as described in #129. * fix: Remove debug logging * fix: typos
1 parent 2d401e5 commit b86c6c7

File tree

1 file changed

+76
-71
lines changed

1 file changed

+76
-71
lines changed

src/line_cursor.ts

Lines changed: 76 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -110,60 +110,66 @@ export class LineCursor extends Marker {
110110
}
111111

112112
/**
113-
* Decides if the previous and next methods should traverse the given node.
114-
* The previous and next method only traverse previous connections, next
115-
* connections and blocks.
113+
* Returns true iff the given node represents the "beginning of a
114+
* new line of code" (and thus can be visited by pressing the
115+
* up/down arrow keys). Specifically, if the node is for:
116+
*
117+
* - Any block that is not a value block.
118+
* - A top-level value block (one that is unconnected).
119+
* - An unconnected next statement input.
120+
* - An unconnected 'next' connection - the "blank line at the end".
121+
* This is to facilitate connecting additional blocks to a
122+
* stack/substack.
116123
*
117124
* @param node The AST node to check.
118125
* @returns True if the node should be visited, false otherwise.
119126
* @protected
120127
*/
121128
validLineNode(node: ASTNode | null): boolean {
122-
if (!node) {
123-
return false;
124-
}
125-
let isValid = false;
129+
if (!node) return false;
126130
const location = node.getLocation();
127131
const type = node && node.getType();
128-
if (type == ASTNode.types.BLOCK) {
129-
if ((location as Blockly.Block).outputConnection === null) {
130-
isValid = true;
131-
}
132-
} else if (
133-
type == ASTNode.types.INPUT &&
134-
(location as Blockly.Connection).type == Blockly.NEXT_STATEMENT
135-
) {
136-
isValid = true;
137-
} else if (type == ASTNode.types.NEXT) {
138-
isValid = true;
132+
switch (type) {
133+
case ASTNode.types.BLOCK:
134+
return !((location as Blockly.Block).outputConnection?.isConnected());
135+
case ASTNode.types.INPUT:
136+
const connection = (location as Blockly.Connection);
137+
return connection.type === Blockly.NEXT_STATEMENT && !connection.isConnected();
138+
case ASTNode.types.NEXT:
139+
return !((location as Blockly.Connection).isConnected());
140+
default:
141+
return false;
139142
}
140-
return isValid;
141143
}
142144

143145
/**
144-
* Decides if the in and out methods should traverse the given node.
145-
* The in and out method only traverse fields and input connections.
146+
* Returns true iff the given node can be visited by the cursor when
147+
* using the left/right arrow keys. Specifically, if the node is for:
148+
*
149+
* - Any block.
150+
* - Any field.
151+
* - Any unconnected next or input connection. This is to
152+
* facilitate connecting additional blocks.
146153
*
147154
* @param node The AST node to check whether it is valid.
148155
* @returns True if the node should be visited, false otherwise.
149156
* @protected
150157
*/
151158
validInLineNode(node: ASTNode | null): boolean {
152-
if (!node) {
153-
return false;
154-
}
155-
let isValid = false;
159+
if (!node) return false;
156160
const location = node.getLocation();
157161
const type = node && node.getType();
158-
if (type == ASTNode.types.FIELD) {
159-
isValid = true;
160-
} else if (
161-
type == ASTNode.types.INPUT &&
162-
(location as Blockly.Connection).type == Blockly.INPUT_VALUE
163-
) {
164-
isValid = true;
162+
switch (type) {
163+
case ASTNode.types.BLOCK:
164+
return true;
165+
case ASTNode.types.INPUT:
166+
case ASTNode.types.NEXT:
167+
return !((location as Blockly.Connection).isConnected());
168+
case ASTNode.types.FIELD:
169+
return true;
170+
default:
171+
return false;
165172
}
166-
return isValid;
167173
}
168174

169175
/**
@@ -395,60 +401,59 @@ export class LineCursor extends Marker {
395401
}
396402

397403
/**
398-
* Set the location of the marker and call the update method.
399-
* Setting isStack to true will only work if the newLocation is the top most
400-
* output or previous connection on a stack.
404+
* Set the location of the marker and draw it.
401405
*
402406
* Overrides drawing logic to call `setSelected` if the location is
403-
* a block, for testing on October 28 2024.
407+
* a block, or `addSelect` if it's a shadow block (since shadow
408+
* blocks can't be selected).
409+
*
410+
* TODO(#142): The selection and fake-selection code was originally
411+
* a hack added for testing on October 28 2024, because the default
412+
* drawer behaviour was to draw a box around the block and all
413+
* attached child blocks, which was confusing when navigating
414+
* stacks.
415+
*
416+
* Since then we have decided that we probably _do_ in most cases
417+
* want navigating to a block to select the block, but more
418+
* particularly that we want navigation to move _focus_. Replace
419+
* this selection hack with non-hacky changing of focus once that's
420+
* possible.
404421
*
405422
* @param newNode The new location of the marker.
406423
*/
407-
setCurNode(newNode: ASTNode) {
408-
const oldNode = (this as any).curNode;
409-
(this as any).curNode = newNode;
410-
const drawer = (this as any).drawer;
424+
override setCurNode(newNode: ASTNode) {
425+
const oldNode = this.getCurNode();
426+
super.setCurNode(newNode);
427+
const drawer = this.getDrawer();
428+
411429
if (!drawer) {
412430
console.error('could not find a drawer');
413431
return;
414432
}
415433

416-
const newNodeIsFieldColour =
417-
newNode?.getType() == ASTNode.types.FIELD &&
418-
(newNode.getLocation() as Blockly.Field) instanceof FieldColour;
419-
const oldNodeIsFieldColour =
420-
oldNode?.getType() == ASTNode.types.FIELD &&
421-
(oldNode.getLocation() as Blockly.Field) instanceof FieldColour;
422-
423-
if (newNode?.getType() == ASTNode.types.BLOCK) {
424-
drawer.hide();
425-
const block = newNode.getLocation() as Blockly.BlockSvg;
426-
Blockly.common.setSelected(block);
427-
} else if (newNodeIsFieldColour) {
428-
drawer.hide();
429-
430-
if (oldNode?.getType() == ASTNode.types.BLOCK) {
434+
// If old node was a block, unselect it or remove fake selection.
435+
if (oldNode?.getType() === ASTNode.types.BLOCK) {
436+
const block = oldNode.getLocation() as Blockly.BlockSvg;
437+
if (!block.isShadow()) {
431438
Blockly.common.setSelected(null);
432-
} else if (oldNodeIsFieldColour) {
433-
const field = oldNode.getLocation() as FieldColour;
434-
const block = field.getSourceBlock() as Blockly.BlockSvg;
439+
} else {
435440
block.removeSelect();
436441
}
442+
}
437443

438-
const field = newNode.getLocation() as FieldColour;
439-
const block = field.getSourceBlock() as Blockly.BlockSvg;
440-
block.addSelect();
441-
} else {
442-
if (oldNode?.getType() == ASTNode.types.BLOCK) {
443-
Blockly.common.setSelected(null);
444-
} else if (oldNodeIsFieldColour) {
445-
const field = oldNode.getLocation() as FieldColour;
446-
const block = field.getSourceBlock() as Blockly.BlockSvg;
447-
block.removeSelect();
444+
// If new node is a block, select it or make it look selected.
445+
if (newNode?.getType() === ASTNode.types.BLOCK) {
446+
drawer.hide();
447+
const block = newNode.getLocation() as Blockly.BlockSvg;
448+
if (!block.isShadow()) {
449+
Blockly.common.setSelected(block);
450+
} else {
451+
block.addSelect();
448452
}
449-
450-
drawer.draw(oldNode, newNode);
453+
return;
451454
}
455+
456+
drawer.draw(oldNode, newNode);
452457
}
453458
}
454459

0 commit comments

Comments
 (0)