diff --git a/src/actions/drag_mover.ts b/src/actions/drag_mover.ts deleted file mode 100644 index f0a34046..00000000 --- a/src/actions/drag_mover.ts +++ /dev/null @@ -1,206 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - ASTNode, - BlockSvg, - WorkspaceSvg, - common, - registry, - utils, -} from 'blockly'; -import type {Block, IDragger} from 'blockly'; -import {Mover, MoveInfo} from './mover'; - -/** - * The distance to move an item, in workspace coordinates, when - * making an unconstrained move. - */ -const UNCONSTRAINED_MOVE_DISTANCE = 20; - -/** - * An experimental implementation of Mover that uses a dragger to - * perform unconstrained movement. - */ -export class DragMover extends Mover { - /** - * Map of moves in progress. - * - * An entry for a given workspace in this map means that the this - * Mover is moving a block on that workspace, and will disable - * normal cursor movement until the move is complete. - */ - protected declare moves: Map; - - /** - * Start moving the currently-focused item on workspace, if - * possible. - * - * Should only be called if canMove has returned true. - * - * @param workspace The workspace we might be moving on. - * @returns True iff a move has successfully begun. - */ - override startMove(workspace: WorkspaceSvg) { - const cursor = workspace?.getCursor(); - const block = this.getCurrentBlock(workspace); - if (!cursor || !block) throw new Error('precondition failure'); - - // Select and focus block. - common.setSelected(block); - cursor.setCurNode(ASTNode.createBlockNode(block)); - // Begin dragging block. - const DraggerClass = registry.getClassFromOptions( - registry.Type.BLOCK_DRAGGER, - workspace.options, - true, - ); - if (!DraggerClass) throw new Error('no Dragger registered'); - const dragger = new DraggerClass(block, workspace); - // Record that a move is in progress and start dragging. - const info = new DragMoveInfo(block, dragger); - this.moves.set(workspace, info); - // Begin drag. - dragger.onDragStart(info.fakePointerEvent('pointerdown')); - return true; - } - - /** - * Finish moving the currently-focused item on workspace. - * - * Should only be called if isMoving has returned true. - * - * @param workspace The workspace on which we are moving. - * @returns True iff move successfully finished. - */ - override finishMove(workspace: WorkspaceSvg) { - const info = this.moves.get(workspace); - if (!info) throw new Error('no move info for workspace'); - - info.dragger.onDragEnd( - info.fakePointerEvent('pointerup'), - new utils.Coordinate(0, 0), - ); - - this.moves.delete(workspace); - return true; - } - - /** - * Abort moving the currently-focused item on workspace. - * - * Should only be called if isMoving has returned true. - * - * @param workspace The workspace on which we are moving. - * @returns True iff move successfully aborted. - */ - override abortMove(workspace: WorkspaceSvg) { - const info = this.moves.get(workspace); - if (!info) throw new Error('no move info for workspace'); - - // Monkey patch dragger to trigger call to draggable.revertDrag. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (info.dragger as any).shouldReturnToStart = () => true; - const blockSvg = info.block as BlockSvg; - - // Explicitly call `hidePreview` because it is not called in revertDrag. - // @ts-expect-error Access to private property dragStrategy. - blockSvg.dragStrategy.connectionPreviewer.hidePreview(); - info.dragger.onDragEnd( - info.fakePointerEvent('pointerup'), - new utils.Coordinate(0, 0), - ); - - this.moves.delete(workspace); - return true; - } - - /** - * Action to move the item being moved in the given direction, - * constrained to valid attachment points (if any). - * - * @param workspace The workspace to move on. - * @returns True iff this action applies and has been performed. - */ - override moveConstrained( - workspace: WorkspaceSvg, - /* ... */ - ) { - // Not yet implemented. Absorb keystroke to avoid moving cursor. - alert(`Constrained movement not implemented. - -Use ctrl+arrow or alt+arrow (option+arrow on macOS) for unconstrained move. -Use enter to complete the move, or escape to abort.`); - return true; - } - - /** - * Action to move the item being moved in the given direction, - * without constraint. - * - * @param workspace The workspace to move on. - * @param xDirection -1 to move left. 1 to move right. - * @param yDirection -1 to move up. 1 to move down. - * @returns True iff this action applies and has been performed. - */ - override moveUnconstrained( - workspace: WorkspaceSvg, - xDirection: number, - yDirection: number, - ): boolean { - if (!workspace) return false; - const info = this.moves.get(workspace); - if (!info) throw new Error('no move info for workspace'); - - info.totalDelta.x += - xDirection * UNCONSTRAINED_MOVE_DISTANCE * workspace.scale; - info.totalDelta.y += - yDirection * UNCONSTRAINED_MOVE_DISTANCE * workspace.scale; - - info.dragger.onDrag(info.fakePointerEvent('pointermove'), info.totalDelta); - return true; - } -} - -/** - * Information about the currently in-progress move for a given - * Workspace. - */ -class DragMoveInfo extends MoveInfo { - /** Total distance moved, in screen pixels */ - totalDelta = new utils.Coordinate(0, 0); - - constructor( - readonly block: Block, - readonly dragger: IDragger, - ) { - super(block); - } - - /** - * Create a fake pointer event for dragging. - * - * @param type Which type of pointer event to create. - * @returns A synthetic PointerEvent that can be consumed by Blockly's - * dragging code. - */ - fakePointerEvent(type: string): PointerEvent { - const workspace = this.block.workspace; - if (!(workspace instanceof WorkspaceSvg)) throw new TypeError(); - - const blockCoords = utils.svgMath.wsToScreenCoordinates( - workspace, - new utils.Coordinate( - this.startLocation.x + this.totalDelta.x, - this.startLocation.y + this.totalDelta.y, - ), - ); - return new PointerEvent(type, { - clientX: blockCoords.x, - clientY: blockCoords.y, - }); - } -} diff --git a/src/actions/mover.ts b/src/actions/mover.ts index 63ec0b7f..746717df 100644 --- a/src/actions/mover.ts +++ b/src/actions/mover.ts @@ -10,10 +10,12 @@ import { Connection, ContextMenuRegistry, ShortcutRegistry, + WorkspaceSvg, common, + registry, utils, } from 'blockly'; -import type {Block, BlockSvg, WorkspaceSvg} from 'blockly'; +import type {Block, BlockSvg, IDragger} from 'blockly'; import {Navigation} from '../navigation'; const KeyCodes = utils.KeyCodes; @@ -21,6 +23,12 @@ const createSerializedKey = ShortcutRegistry.registry.createSerializedKey.bind( ShortcutRegistry.registry, ); +/** + * The distance to move an item, in workspace coordinates, when + * making an unconstrained move. + */ +const UNCONSTRAINED_MOVE_DISTANCE = 20; + /** * Actions for moving blocks with keyboard shortcuts. */ @@ -34,6 +42,12 @@ export class Mover { */ protected moves: Map = new Map(); + /** + * The stashed isDragging function, which is replaced at the beginning + * of a keyboard drag and reset at the end of a keyboard drag. + */ + oldIsDragging: (() => boolean) | null = null; + constructor( protected navigation: Navigation, protected canEdit: (ws: WorkspaceSvg) => boolean, @@ -222,10 +236,20 @@ export class Mover { common.setSelected(block); cursor.setCurNode(ASTNode.createBlockNode(block)); - // Additional implementation goes here. - console.log('startMove'); - - this.moves.set(workspace, new MoveInfo(block)); + this.patchIsDragging(workspace); + // Begin dragging block. + const DraggerClass = registry.getClassFromOptions( + registry.Type.BLOCK_DRAGGER, + workspace.options, + true, + ); + if (!DraggerClass) throw new Error('no Dragger registered'); + const dragger = new DraggerClass(block, workspace); + // Record that a move is in progress and start dragging. + const info = new MoveInfo(block, dragger); + this.moves.set(workspace, info); + // Begin drag. + dragger.onDragStart(info.fakePointerEvent('pointerdown')); return true; } @@ -241,9 +265,12 @@ export class Mover { const info = this.moves.get(workspace); if (!info) throw new Error('no move info for workspace'); - // Additional implementation goes here. - console.log('finishMove'); + info.dragger.onDragEnd( + info.fakePointerEvent('pointerup'), + new utils.Coordinate(0, 0), + ); + this.unpatchIsDragging(workspace); this.moves.delete(workspace); return true; } @@ -260,9 +287,20 @@ export class Mover { const info = this.moves.get(workspace); if (!info) throw new Error('no move info for workspace'); - // Additional implementation goes here. - console.log('abortMove'); + // Monkey patch dragger to trigger call to draggable.revertDrag. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (info.dragger as any).shouldReturnToStart = () => true; + const blockSvg = info.block as BlockSvg; + // Explicitly call `hidePreview` because it is not called in revertDrag. + // @ts-expect-error Access to private property dragStrategy. + blockSvg.dragStrategy.connectionPreviewer.hidePreview(); + info.dragger.onDragEnd( + info.fakePointerEvent('pointerup'), + new utils.Coordinate(0, 0), + ); + + this.unpatchIsDragging(workspace); this.moves.delete(workspace); return true; } @@ -278,8 +316,11 @@ export class Mover { workspace: WorkspaceSvg, /* ... */ ) { - console.log('moveConstrained'); // Not yet implemented. Absorb keystroke to avoid moving cursor. + alert(`Constrained movement not implemented. + +Use ctrl+arrow or alt+arrow (option+arrow on macOS) for unconstrained move. +Use enter to complete the move, or escape to abort.`); return true; } @@ -297,8 +338,16 @@ export class Mover { xDirection: number, yDirection: number, ): boolean { - console.log('moveUnconstrained'); - // Not yet implemented. Absorb keystroke to avoid moving cursor. + if (!workspace) return false; + const info = this.moves.get(workspace); + if (!info) throw new Error('no move info for workspace'); + + info.totalDelta.x += + xDirection * UNCONSTRAINED_MOVE_DISTANCE * workspace.scale; + info.totalDelta.y += + yDirection * UNCONSTRAINED_MOVE_DISTANCE * workspace.scale; + + info.dragger.onDrag(info.fakePointerEvent('pointermove'), info.totalDelta); return true; } @@ -315,6 +364,30 @@ export class Mover { const curNode = cursor?.getCurNode(); return (curNode?.getSourceBlock() as BlockSvg) ?? undefined; } + + /** + * Monkeypatch over workspace.isDragging to return whether a keyboard + * drag is in progress. + * + * @param workspace The workspace to patch. + */ + private patchIsDragging(workspace: WorkspaceSvg) { + this.oldIsDragging = workspace.isDragging; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (workspace as any).isDragging = () => this.isMoving(workspace); + } + + /** + * Remove the monkeypatch on workspace.isDragging. + * + * @param workspace The workspace to unpatch. + */ + private unpatchIsDragging(workspace: WorkspaceSvg) { + if (this.oldIsDragging) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (workspace as any).isDragging = this.oldIsDragging; + } + } } /** @@ -322,13 +395,42 @@ export class Mover { * Workspace. */ export class MoveInfo { + /** Total distance moved, in screen pixels */ + totalDelta = new utils.Coordinate(0, 0); readonly parentNext: Connection | null; readonly parentInput: Connection | null; readonly startLocation: utils.Coordinate; - constructor(readonly block: Block) { + constructor( + readonly block: Block, + readonly dragger: IDragger, + ) { this.parentNext = block.previousConnection?.targetConnection ?? null; this.parentInput = block.outputConnection?.targetConnection ?? null; this.startLocation = block.getRelativeToSurfaceXY(); } + + /** + * Create a fake pointer event for dragging. + * + * @param type Which type of pointer event to create. + * @returns A synthetic PointerEvent that can be consumed by Blockly's + * dragging code. + */ + fakePointerEvent(type: string): PointerEvent { + const workspace = this.block.workspace; + if (!(workspace instanceof WorkspaceSvg)) throw new TypeError(); + + const blockCoords = utils.svgMath.wsToScreenCoordinates( + workspace, + new utils.Coordinate( + this.startLocation.x + this.totalDelta.x, + this.startLocation.y + this.totalDelta.y, + ), + ); + return new PointerEvent(type, { + clientX: blockCoords.x, + clientY: blockCoords.y, + }); + } } diff --git a/src/navigation_controller.ts b/src/navigation_controller.ts index 546a4045..bf6a1e8f 100644 --- a/src/navigation_controller.ts +++ b/src/navigation_controller.ts @@ -34,7 +34,7 @@ import {ExitAction} from './actions/exit'; import {EnterAction} from './actions/enter'; import {DisconnectAction} from './actions/disconnect'; import {ActionMenu} from './actions/action_menu'; -import {DragMover as Mover} from './actions/drag_mover'; +import {Mover} from './actions/mover'; const KeyCodes = BlocklyUtils.KeyCodes;