Skip to content

Commit 3727db5

Browse files
fix: take care making a selection when focusing the workspace
This can interfere with in-progress gestures causing them to act in unexpected ways. We now clear the current node when focus moves away. Avoid altering the selection in cursor.draw to prevent similar poorly timed selections. If the cursor position is lost, e.g. by click in the workspace, the arrow keys now default it if needed. Tabbing to the workspace always shows a cursor. Fixes: - Click in workspace blank space now doesn't select a block on mouse down - The mini-workspace for "if" and similar now behaves correctly again (no keyboard nav still though) - Also fixes #287
1 parent cf36b60 commit 3727db5

File tree

6 files changed

+114
-83
lines changed

6 files changed

+114
-83
lines changed

src/actions/arrow_navigation.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ export class ArrowNavigation {
6464
case Constants.STATE.WORKSPACE:
6565
isHandled = this.fieldShortcutHandler(workspace, shortcut);
6666
if (!isHandled && workspace) {
67-
workspace.getCursor()?.in();
67+
if (!this.navigation.defaultCursorPositionIfNeeded(workspace)) {
68+
workspace.getCursor()?.in();
69+
}
6870
isHandled = true;
6971
}
7072
return isHandled;
@@ -95,7 +97,9 @@ export class ArrowNavigation {
9597
case Constants.STATE.WORKSPACE:
9698
isHandled = this.fieldShortcutHandler(workspace, shortcut);
9799
if (!isHandled && workspace) {
98-
workspace.getCursor()?.out();
100+
if (!this.navigation.defaultCursorPositionIfNeeded(workspace)) {
101+
workspace.getCursor()?.out();
102+
}
99103
isHandled = true;
100104
}
101105
return isHandled;
@@ -125,7 +129,9 @@ export class ArrowNavigation {
125129
case Constants.STATE.WORKSPACE:
126130
isHandled = this.fieldShortcutHandler(workspace, shortcut);
127131
if (!isHandled && workspace) {
128-
workspace.getCursor()?.next();
132+
if (!this.navigation.defaultCursorPositionIfNeeded(workspace)) {
133+
workspace.getCursor()?.next();
134+
}
129135
isHandled = true;
130136
}
131137
return isHandled;
@@ -158,7 +164,14 @@ export class ArrowNavigation {
158164
case Constants.STATE.WORKSPACE:
159165
isHandled = this.fieldShortcutHandler(workspace, shortcut);
160166
if (!isHandled) {
161-
workspace.getCursor()?.prev();
167+
if (
168+
!this.navigation.defaultCursorPositionIfNeeded(
169+
workspace,
170+
'last',
171+
)
172+
) {
173+
workspace.getCursor()?.prev();
174+
}
162175
isHandled = true;
163176
}
164177
return isHandled;

src/actions/enter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export class EnterAction {
125125
* the block will be placed on.
126126
*/
127127
private insertFromFlyout(workspace: WorkspaceSvg) {
128-
const stationaryNode = workspace.getCursor()?.getCurNode();
128+
const stationaryNode = this.navigation.getStationaryNode(workspace);
129129
const newBlock = this.createNewBlock(workspace);
130130
if (!newBlock) return;
131131
if (stationaryNode) {

src/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class KeyboardNavigation {
2828
private focusListener: (e: Event) => void;
2929

3030
/** Event handler run when the workspace loses focus. */
31-
private blurListener: () => void;
31+
private blurListener: (e: Event) => void;
3232

3333
/** Event handler run when the toolbox gains focus. */
3434
private toolboxFocusListener: () => void;
@@ -129,8 +129,14 @@ export class KeyboardNavigation {
129129
this.navigationController.handleFocusWorkspace(workspace);
130130
}
131131
};
132-
this.blurListener = () => {
133-
this.navigationController.handleBlurWorkspace(workspace);
132+
this.blurListener = (e: Event) => {
133+
const relatedTarget = (e as FocusEvent).relatedTarget;
134+
if (
135+
relatedTarget !== this.workspace.getParentSvg() &&
136+
relatedTarget !== this.workspace.getSvgGroup()
137+
) {
138+
this.navigationController.handleBlurWorkspace(workspace);
139+
}
134140
};
135141

136142
workspace.getSvgGroup().addEventListener('focus', this.focusListener);

src/line_cursor.ts

Lines changed: 39 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import * as Blockly from 'blockly/core';
1717
import {ASTNode, Marker} from 'blockly/core';
18-
import {scrollBoundsIntoView} from './workspace_utilities';
18+
import {getWorkspaceElement, scrollBoundsIntoView} from './workspace_utilities';
1919

2020
/** Options object for LineCursor instances. */
2121
export type CursorOptions = {
@@ -474,47 +474,6 @@ export class LineCursor extends Marker {
474474
throw new Error('no valid nodes in this.potentialNodes');
475475
}
476476

477-
/**
478-
* Get the current location of the cursor.
479-
*
480-
* Overrides superclass implementation to add a hack that attempts
481-
* to detect if the user has moved focus by selecting a block and,
482-
* if so, update the cursor location (and any highlighting) to
483-
* match.
484-
*
485-
* Doing this only when getCurNode would naturally be called works
486-
* reasonably well but has some glitches, most notably that if the
487-
* cursor was not on a block (e.g. it was on a connection or the
488-
* workspace) when the user selected a block then it will remain
489-
* visible in its previous location until some keyboard navigation occurs.
490-
*
491-
* To ameliorate this, the LineCursor constructor adds an event
492-
* listener that calls getCurNode in response to SELECTED events.
493-
*
494-
* Remove this hack once Blockly is modified to update the
495-
* cursor/focus itself.
496-
*
497-
* @returns The current field, connection, or block the cursor is on.
498-
*/
499-
override getCurNode(): ASTNode {
500-
const curNode = super.getCurNode();
501-
const selected = Blockly.common.getSelected();
502-
if (selected?.workspace !== this.workspace) return curNode;
503-
504-
// Selected item is on workspace that this cursor belongs to.
505-
const curLocation = curNode?.getLocation();
506-
if (curLocation === selected) return curNode;
507-
508-
// Selected item is not where cursor is. Try to move cursor.
509-
if (!(selected instanceof Blockly.Block)) {
510-
console.error('Selected item is not a block. Ignoring');
511-
return curNode;
512-
}
513-
const newNode = new ASTNode(ASTNode.types.BLOCK, selected);
514-
this.setCurNode(newNode);
515-
return newNode;
516-
}
517-
518477
/**
519478
* Set the location of the cursor and draw it.
520479
*
@@ -523,7 +482,25 @@ export class LineCursor extends Marker {
523482
*
524483
* @param newNode The new location of the cursor.
525484
*/
526-
override setCurNode(newNode: ASTNode) {
485+
override setCurNode(newNode: ASTNode, selectionInSync = false) {
486+
if (newNode?.getLocation() === this.getCurNode()?.getLocation()) {
487+
return;
488+
}
489+
if (!selectionInSync) {
490+
if (
491+
newNode?.getType() === ASTNode.types.BLOCK &&
492+
!(newNode.getLocation() as Blockly.BlockSvg).isShadow()
493+
) {
494+
if (Blockly.common.getSelected() !== newNode.getLocation()) {
495+
Blockly.common.setSelected(newNode.getLocation() as Blockly.BlockSvg);
496+
}
497+
} else {
498+
if (Blockly.common.getSelected()) {
499+
Blockly.common.setSelected(null);
500+
}
501+
}
502+
}
503+
527504
const oldNode = super.getCurNode();
528505
// Kludge: we can't set this.curNode directly, so we have to call
529506
// super.setCurNode(...) to do it for us - but that would call
@@ -533,6 +510,7 @@ export class LineCursor extends Marker {
533510
this.setDrawer(null as any); // Cast required since param is not nullable.
534511
super.setCurNode(newNode);
535512
this.setDrawer(drawer);
513+
536514
// Draw this marker the way we want to.
537515
this.drawMarker(oldNode, newNode);
538516
// Try to scroll cursor into view.
@@ -545,22 +523,6 @@ export class LineCursor extends Marker {
545523
}
546524
}
547525

548-
override hide(): void {
549-
super.hide();
550-
551-
// If there's a block currently selected, remove the selection since the
552-
// cursor should now be hidden.
553-
const curNode = this.getCurNode();
554-
if (curNode && curNode.getType() === ASTNode.types.BLOCK) {
555-
const block = curNode.getLocation() as Blockly.BlockSvg;
556-
if (!block.isShadow()) {
557-
Blockly.common.setSelected(null);
558-
} else {
559-
block.removeSelect();
560-
}
561-
}
562-
}
563-
564526
/**
565527
* Redraw the current marker.
566528
*
@@ -605,7 +567,7 @@ export class LineCursor extends Marker {
605567
if (oldNode?.getType() === ASTNode.types.BLOCK) {
606568
const block = oldNode.getLocation() as Blockly.BlockSvg;
607569
if (!block.isShadow()) {
608-
Blockly.common.setSelected(null);
570+
// Selection should already be in sync.
609571
} else {
610572
block.removeSelect();
611573
}
@@ -633,7 +595,7 @@ export class LineCursor extends Marker {
633595
} else if (curNodeType === ASTNode.types.BLOCK) {
634596
const block = curNode.getLocation() as Blockly.BlockSvg;
635597
if (!block.isShadow()) {
636-
Blockly.common.setSelected(block);
598+
// Selection should already be in sync.
637599
} else {
638600
block.addSelect();
639601
}
@@ -698,7 +660,22 @@ export class LineCursor extends Marker {
698660
if (event.type !== Blockly.Events.SELECTED) return;
699661
const selectedEvent = event as Blockly.Events.Selected;
700662
if (selectedEvent.workspaceId !== this.workspace.id) return;
701-
this.getCurNode();
663+
if (selectedEvent.newElementId) {
664+
const block = this.workspace.getBlockById(selectedEvent.newElementId);
665+
if (block) {
666+
const node = ASTNode.createBlockNode(block);
667+
if (node) {
668+
this.setCurNode(node, true);
669+
}
670+
}
671+
} else if (
672+
this.getCurNode()?.getType() === ASTNode.types.BLOCK &&
673+
!(this.getCurNode().getLocation() as Blockly.BlockSvg).isShadow()
674+
) {
675+
// This does mean other cursor types remain if you click on the workspace
676+
// background. Ideally gesture would be handling this with more context.
677+
this.setCurNode(null as never, true);
678+
}
702679
}
703680
}
704681

src/navigation.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,18 @@ export class Navigation {
142142
return this.workspaceStates[workspace.id];
143143
}
144144

145+
/**
146+
* Gets the node to use as context for insert operations.
147+
*
148+
* @param workspace The main workspace.
149+
*/
150+
getStationaryNode(workspace: Blockly.WorkspaceSvg) {
151+
return (
152+
this.passiveFocusIndicator.getCurNode() ??
153+
workspace.getCursor()?.getCurNode()
154+
);
155+
}
156+
145157
/**
146158
* Adds all event listeners and cursors to the flyout that are needed for
147159
* keyboard navigation to work.
@@ -386,13 +398,19 @@ export class Navigation {
386398
this.setState(workspace, Constants.STATE.WORKSPACE);
387399
if (!Blockly.Gesture.inProgress()) {
388400
workspace.hideChaff();
401+
// This will make a selection which would interfere with any gesture.
402+
this.defaultCursorPositionIfNeeded(workspace);
389403
}
390-
this.setCursorOnWorkspaceFocus(workspace, true);
391404

392405
const cursor = workspace.getCursor();
393406
if (cursor) {
407+
const passiveFocusNode = this.passiveFocusIndicator.getCurNode();
394408
this.passiveFocusIndicator.hide();
395-
cursor.draw();
409+
// If there's a gesture then it will either set the node or be a click
410+
// that should not set one.
411+
if (!Blockly.Gesture.inProgress() && passiveFocusNode) {
412+
cursor.setCurNode(passiveFocusNode);
413+
}
396414
}
397415
}
398416

@@ -405,10 +423,11 @@ export class Navigation {
405423
this.setState(workspace, Constants.STATE.NOWHERE);
406424
const cursor = workspace.getCursor();
407425
if (cursor) {
408-
cursor.hide();
409426
if (cursor.getCurNode()) {
410427
this.passiveFocusIndicator.show(cursor.getCurNode());
411428
}
429+
// It's initially null so this is a valid state despite the types.
430+
cursor.setCurNode(null as never);
412431
}
413432
}
414433

@@ -546,43 +565,46 @@ export class Navigation {
546565
/**
547566
* Sets the cursor location when focusing the workspace.
548567
* Tries the following, in order, stopping after the first success:
549-
* - Resume editing by putting the cursor at the marker location, if any.
550-
* - Resume editing by returning the cursor to its previous location, if any.
568+
* - Resume editing by returning the cursor to its previous location, if valid.
551569
* - Move the cursor to the top connection point on on the first top block.
552570
* - Move the cursor to the default location on the workspace.
553571
*
554572
* @param workspace The main Blockly workspace.
555-
* @param keepPosition Whether to retain the cursor's previous position.
573+
* @return true if the cursor location was defaulted.
556574
*/
557-
setCursorOnWorkspaceFocus(
575+
defaultCursorPositionIfNeeded(
558576
workspace: Blockly.WorkspaceSvg,
559-
keepPosition: boolean,
577+
prefer: 'first' | 'last' = 'first',
560578
) {
561579
const topBlocks = workspace.getTopBlocks(true);
562580
const cursor = workspace.getCursor();
563581
if (!cursor) {
564582
return;
565583
}
566-
567584
const disposed = cursor.getCurNode()?.getSourceBlock()?.disposed;
568-
if (cursor.getCurNode() && !disposed && keepPosition) {
585+
if (cursor.getCurNode() && !disposed) {
569586
// Retain the cursor's previous position since it's set, but only if not
570587
// disposed (which can happen when blocks are reloaded).
571-
return;
588+
return false;
572589
}
573590
const wsCoordinates = new Blockly.utils.Coordinate(
574591
this.DEFAULT_WS_COORDINATE.x / workspace.scale,
575592
this.DEFAULT_WS_COORDINATE.y / workspace.scale,
576593
);
577594
if (topBlocks.length > 0) {
578-
cursor.setCurNode(Blockly.ASTNode.createTopNode(topBlocks[0])!);
595+
cursor.setCurNode(
596+
Blockly.ASTNode.createTopNode(
597+
topBlocks[prefer === 'first' ? 0 : topBlocks.length - 1],
598+
)!,
599+
);
579600
} else {
580601
const wsNode = Blockly.ASTNode.createWorkspaceNode(
581602
workspace,
582603
wsCoordinates,
583604
);
584605
cursor.setCurNode(wsNode!);
585606
}
607+
return true;
586608
}
587609

588610
/**

src/passive_focus.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ export class PassiveFocus {
3030
this.nextConnectionIndicator = this.createNextIndicator();
3131
}
3232

33+
/**
34+
* Get the current passive focus node.
35+
*
36+
* @returns the node or null.
37+
*/
38+
getCurNode(): ASTNode | null {
39+
return this.curNode;
40+
}
41+
3342
/** Dispose of this indicator. Do any necessary cleanup. */
3443
dispose() {
3544
this.hide();
@@ -104,7 +113,11 @@ export class PassiveFocus {
104113
*/
105114
hideAtBlock(node: ASTNode) {
106115
const block = node.getLocation() as BlockSvg;
107-
utils.dom.removeClass(block.pathObject.svgPath, 'passiveBlockFocus');
116+
// When a block is selected we can end up with a duplicate svgPath.
117+
const svgPaths = block.getSvgRoot().querySelectorAll('.passiveBlockFocus');
118+
svgPaths.forEach((svgPath) =>
119+
utils.dom.removeClass(svgPath, 'passiveBlockFocus'),
120+
);
108121
}
109122

110123
/**

0 commit comments

Comments
 (0)