Skip to content

Commit 1233f57

Browse files
fix: stale curNode when clicking the workspace
Due to an unintentially commented out line, clicking the workspace wouldn't clear the current node so e.g. delete would act on a block that didn't have a visual selection. Re-enabling that required revisiting shadow blocks to prevent getCurNode unexpectedly clearing based on a null selection. We now always set/use the selection for shadow blocks and take care not to move the cursor unexpectedly as it can be in two positions for a shadow block selection.
1 parent c2e7d4b commit 1233f57

File tree

1 file changed

+101
-57
lines changed

1 file changed

+101
-57
lines changed

src/line_cursor.ts

Lines changed: 101 additions & 57 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 {getWorkspaceElement, scrollBoundsIntoView} from './workspace_utilities';
18+
import {scrollBoundsIntoView} from './workspace_utilities';
1919

2020
/** Options object for LineCursor instances. */
2121
export type CursorOptions = {
@@ -61,7 +61,7 @@ export class LineCursor extends Marker {
6161
) {
6262
super();
6363
// Bind selectListener to facilitate future install/uninstall.
64-
this.selectListener = this.selectListener.bind(this);
64+
this.changeListener = this.changeListener.bind(this);
6565
// Regularise options and apply defaults.
6666
this.options = {...defaultOptions, ...options};
6767

@@ -81,7 +81,7 @@ export class LineCursor extends Marker {
8181
markerManager.setCursor(this);
8282
const oldCursorNode = this.oldCursor?.getCurNode();
8383
if (oldCursorNode) this.setCurNode(oldCursorNode);
84-
this.workspace.addChangeListener(this.selectListener);
84+
this.workspace.addChangeListener(this.changeListener);
8585
this.installed = true;
8686
}
8787

@@ -92,7 +92,7 @@ export class LineCursor extends Marker {
9292
*/
9393
uninstall() {
9494
if (!this.installed) throw new Error('LineCursor not yet installed');
95-
this.workspace.removeChangeListener(this.selectListener.bind(this));
95+
this.workspace.removeChangeListener(this.changeListener.bind(this));
9696
if (this.oldCursor) {
9797
this.workspace.getMarkerManager().setCursor(this.oldCursor);
9898
}
@@ -479,36 +479,14 @@ export class LineCursor extends Marker {
479479
* Get the current location of the cursor.
480480
*
481481
* Overrides normal Marker getCurNode to update the current node from the selected
482-
* block. We typically update the node from the selection via a listener but
483-
* that is not called immediately when `Gesture` calls `Blockly.common.setSelected`.
482+
* block. This typically happens via the selection listener but that is not called
483+
* immediately when `Gesture` calls `Blockly.common.setSelected`.
484484
* In particular the listener runs after showing the context menu.
485485
*
486486
* @returns The current field, connection, or block the cursor is on.
487487
*/
488-
override getCurNode(): ASTNode | null {
489-
const curNode = super.getCurNode();
490-
let selected = Blockly.common.getSelected();
491-
if (
492-
selected === null &&
493-
curNode?.getType() === Blockly.ASTNode.types.BLOCK
494-
) {
495-
// Selection says our curNode is not selected anymore.
496-
// this.setCurNode(null as never, true);
497-
return super.getCurNode();
498-
}
499-
if (selected?.workspace !== this.workspace) return curNode;
500-
// Selection has a block in our workspace.
501-
if (selected instanceof Blockly.BlockSvg) {
502-
if (selected.isShadow()) {
503-
// Although the shadow block is selected it's the parent that has the
504-
// visual selection.
505-
selected = selected.getParent();
506-
}
507-
if (selected) {
508-
this.setCurNode(new ASTNode(ASTNode.types.BLOCK, selected), true);
509-
}
510-
}
511-
488+
override getCurNode(): Blockly.ASTNode | null {
489+
this.updateCurNodeFromSelection();
512490
return super.getCurNode();
513491
}
514492

@@ -555,25 +533,9 @@ export class LineCursor extends Marker {
555533
* @param newNode The new location of the cursor.
556534
* @param selectionUpToDate If false (the default) we'll update the selection too.
557535
*/
558-
override setCurNode(newNode: ASTNode, selectionUpToDate = false) {
536+
override setCurNode(newNode: ASTNode | null, selectionUpToDate = false) {
559537
if (!selectionUpToDate) {
560-
if (
561-
newNode?.getType() === ASTNode.types.BLOCK &&
562-
// For shadow blocks we need to clear the selection that's drawn on their parent.
563-
!(newNode.getLocation() as Blockly.BlockSvg).isShadow()
564-
) {
565-
if (Blockly.common.getSelected() !== newNode.getLocation()) {
566-
Blockly.Events.disable();
567-
Blockly.common.setSelected(newNode.getLocation() as Blockly.BlockSvg);
568-
Blockly.Events.enable();
569-
}
570-
} else {
571-
if (Blockly.common.getSelected()) {
572-
Blockly.Events.disable();
573-
Blockly.common.setSelected(null);
574-
Blockly.Events.enable();
575-
}
576-
}
538+
this.updateSelectionFromNode(newNode);
577539
}
578540

579541
super.setCurNode(newNode);
@@ -652,6 +614,7 @@ export class LineCursor extends Marker {
652614
// Selection should already be in sync.
653615
} else {
654616
block.addSelect();
617+
block.getParent()?.removeSelect();
655618
}
656619
}
657620

@@ -707,18 +670,99 @@ export class LineCursor extends Marker {
707670
}
708671

709672
/**
710-
* Event listener that syncs the cursor location to the selected
711-
* block on SELECTED events.
673+
* Event listener that syncs the cursor location to the selected block on
674+
* SELECTED events.
675+
*
676+
* This does not run early enough in all cases so `getCurNode()` also updates
677+
* the node from the selection.
712678
*
713679
* @param event The `Selected` event.
714680
*/
715-
private selectListener(event: Blockly.Events.Abstract) {
716-
if (event.type !== Blockly.Events.SELECTED) return;
717-
const selectedEvent = event as Blockly.Events.Selected;
718-
if (selectedEvent.workspaceId !== this.workspace.id) return;
719-
// This runs too late so the logic to update the selection is in
720-
// `getCurNode`.
721-
this.getCurNode();
681+
private changeListener(event: Blockly.Events.Abstract) {
682+
switch (event.type) {
683+
case Blockly.Events.SELECTED:
684+
this.updateCurNodeFromSelection();
685+
break;
686+
case Blockly.Events.CLICK: {
687+
const click = event as Blockly.Events.Click;
688+
if (
689+
click.workspaceId === this.workspace.id &&
690+
click.targetType === Blockly.Events.ClickTarget.WORKSPACE
691+
) {
692+
this.setCurNode(null);
693+
}
694+
}
695+
}
696+
}
697+
698+
/**
699+
* Updates the current node to match the selection.
700+
*
701+
* Clears the current node if it's on a block but the selection is null.
702+
* Sets the node to a block if selected for our workspace.
703+
* For shadow blocks selections the parent is used by default (unless we're
704+
* already on the shadow block via keyboard) as that's where the visual
705+
* selection is.
706+
*/
707+
private updateCurNodeFromSelection() {
708+
const curNode = super.getCurNode();
709+
const selected = Blockly.common.getSelected();
710+
711+
if (
712+
selected === null &&
713+
curNode?.getType() === Blockly.ASTNode.types.BLOCK
714+
) {
715+
this.setCurNode(null, true);
716+
return;
717+
}
718+
if (selected?.workspace !== this.workspace) {
719+
return;
720+
}
721+
if (selected instanceof Blockly.BlockSvg) {
722+
let block: Blockly.BlockSvg | null = selected;
723+
if (selected.isShadow()) {
724+
// OK if the current node is on the parent OR the shadow block.
725+
// The former happens for clicks, the latter for keyboard nav.
726+
if (
727+
curNode &&
728+
(curNode.getLocation() === block ||
729+
curNode.getLocation() === block.getParent())
730+
) {
731+
return;
732+
}
733+
block = block.getParent();
734+
}
735+
if (block) {
736+
this.setCurNode(Blockly.ASTNode.createBlockNode(block)!, true);
737+
}
738+
}
739+
}
740+
741+
/**
742+
* Updates the selection from the node.
743+
*
744+
* Clears the selection for non-block nodes.
745+
* Clears the selection for shadow blocks as the selection is drawn on
746+
* the parent but the cursor will be drawn on the shadow block itself.
747+
* We need to take care not to later clear the current node due to that null
748+
* selection, so we track the latest selection we're in sync with.
749+
*
750+
* @param newNode The new node.
751+
*/
752+
private updateSelectionFromNode(newNode: Blockly.ASTNode | null) {
753+
if (newNode?.getType() === ASTNode.types.BLOCK) {
754+
if (Blockly.common.getSelected() !== newNode.getLocation()) {
755+
Blockly.Events.disable();
756+
Blockly.common.setSelected(newNode.getLocation() as Blockly.BlockSvg);
757+
Blockly.Events.enable();
758+
}
759+
} else {
760+
if (Blockly.common.getSelected()) {
761+
Blockly.Events.disable();
762+
Blockly.common.setSelected(null);
763+
Blockly.Events.enable();
764+
}
765+
}
722766
}
723767
}
724768

0 commit comments

Comments
 (0)