Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/blockly/core/clipboard/block_paster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class BlockPaster implements IPaster<BlockCopyData, BlockSvg> {
// However, the algorithm for deciding where to paste a block depends on
// the starting position of the copied block, so we'll pass those coordinates along
const initialCoordinates =
coordinate ||
coordinate ??
new Coordinate(
copyData.blockState['x'] || 0,
copyData.blockState['y'] || 0,
Expand Down
4 changes: 4 additions & 0 deletions packages/blockly/core/keyboard_nav/navigators/navigator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import {BlockSvg} from '../../block_svg.js';
import {CommentEditor} from '../../comments/comment_editor.js';
import {Field} from '../../field.js';
import {getFocusManager} from '../../focus_manager.js';
import {Icon} from '../../icons/icon.js';
Expand Down Expand Up @@ -499,6 +500,9 @@ export class Navigator {
return node.getSourceBlock();
} else if (node instanceof Icon) {
return node.getSourceBlock() as BlockSvg;
} else if (node instanceof CommentEditor) {
const parent = node.getParent();
return parent instanceof BlockSvg ? parent : null;
}

return null;
Expand Down
12 changes: 12 additions & 0 deletions packages/blockly/core/shortcut_items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ export function registerPaste() {
},
callback(workspace: WorkspaceSvg, e: Event) {
const copyData = clipboard.getLastCopiedData();
const focusedNode = getFocusManager().getFocusedNode();
if (!copyData) return false;

const copyWorkspace = clipboard.getLastCopiedWorkspace();
Expand All @@ -355,6 +356,17 @@ export function registerPaste() {
return !!clipboard.paste(copyData, targetWorkspace, mouseCoords);
}

// If the focused node is a block, or part of a block (connection, field, etc.),
// paste relative to that block's position.
const block = targetWorkspace
.getNavigator()
.getSourceBlockFromNode(focusedNode);
const pasteOrigin = block?.getRelativeToSurfaceXY();
if (pasteOrigin) {
return !!clipboard.paste(copyData, targetWorkspace, pasteOrigin);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be super cool if when:

  1. a connection is focused
  2. the copied block is compatible with that connection

we connected the block to the connection automatically

but that could be a follow up feature too :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that was discussed in RaspberryPiFoundation/blockly-keyboard-experimentation#480. There were concerns that auto-connecting on paste could have surprising effects (popping out blocks, unexpected nesting, etc.), and that people often use copy/paste to reuse a block elsewhere rather than insert it at the current location.


// No spatial focus target (e.g. workspace root) — use copy-location behavior.
const copyCoords = clipboard.getLastCopiedLocation();
if (!copyCoords) {
// If we don't have location data about the original copyable, let the
Expand Down
73 changes: 73 additions & 0 deletions packages/blockly/tests/mocha/shortcut_items_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,79 @@ suite('Keyboard Shortcut Items', function () {
sinon.assert.calledWith(toastSpy, this.workspace, 'copiedHint');
toastSpy.restore();
});

test('Pastes near focused block instead of copy origin', function () {
this.workspace.clear();
const blockA = setSelectedBlock(this.workspace);

this.injectionDiv.dispatchEvent(
createKeyDownEvent(Blockly.utils.KeyCodes.C, [
Blockly.utils.KeyCodes.CTRL_CMD,
]),
);

const blockB = Blockly.serialization.blocks.append(
{type: 'stack_block', x: 300, y: 300},
this.workspace,
);
Blockly.getFocusManager().focusNode(blockB);

this.injectionDiv.dispatchEvent(
createKeyDownEvent(Blockly.utils.KeyCodes.V, [
Blockly.utils.KeyCodes.CTRL_CMD,
]),
);

const pastedBlock = this.workspace
.getAllBlocks(false)
.find((b) => ![blockA, blockB].includes(b));
assert.isDefined(pastedBlock);

const pastedXY = pastedBlock.getRelativeToSurfaceXY();
// Check that the pasted block is closer to blockB than blockA, which means
// it used the focus location instead of the copy origin.
assert.isBelow(
Blockly.utils.Coordinate.distance(
pastedXY,
blockB.getRelativeToSurfaceXY(),
),
Blockly.utils.Coordinate.distance(
pastedXY,
blockA.getRelativeToSurfaceXY(),
),
);
});

test('Uses copy origin when workspace has focus', function () {
const blockA = setSelectedBlock(this.workspace);
this.injectionDiv.dispatchEvent(
createKeyDownEvent(Blockly.utils.KeyCodes.C, [
Blockly.utils.KeyCodes.CTRL_CMD,
]),
);

Blockly.getFocusManager().focusNode(this.workspace);
this.injectionDiv.dispatchEvent(
createKeyDownEvent(Blockly.utils.KeyCodes.V, [
Blockly.utils.KeyCodes.CTRL_CMD,
]),
);

const pastedBlock = this.workspace
.getAllBlocks(false)
.find((b) => b.id !== blockA.id);
assert.isDefined(pastedBlock);

const copyOrigin = blockA.getRelativeToSurfaceXY();
const pastedXY = pastedBlock.getRelativeToSurfaceXY();
assert.isBelow(
Blockly.utils.Coordinate.distance(pastedXY, copyOrigin),
Blockly.utils.Coordinate.distance(
pastedXY,
new Blockly.utils.Coordinate(300, 300),
),
);
});
});

suite('Undo', function () {
Expand Down