Skip to content

Commit

Permalink
support copy/pasting between tabs and projects (#10366)
Browse files Browse the repository at this point in the history
* support pasting between tabs and projects

* pr feedback
  • Loading branch information
riknoll authored Jan 31, 2025
1 parent d874027 commit 8bc2ef8
Show file tree
Hide file tree
Showing 4 changed files with 307 additions and 0 deletions.
88 changes: 88 additions & 0 deletions pxtblocks/copyPaste.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import * as Blockly from "blockly";
import { getCopyPasteHandlers } from "./external";

let oldCopy: Blockly.ShortcutRegistry.KeyboardShortcut;
let oldCut: Blockly.ShortcutRegistry.KeyboardShortcut;
let oldPaste: Blockly.ShortcutRegistry.KeyboardShortcut;

export function initCopyPaste() {
if (oldCopy) return;

const shortcuts = Blockly.ShortcutRegistry.registry.getRegistry()

oldCopy = { ...shortcuts[Blockly.ShortcutItems.names.COPY] };
oldCut = { ...shortcuts[Blockly.ShortcutItems.names.CUT] };
oldPaste = { ...shortcuts[Blockly.ShortcutItems.names.PASTE] };

Blockly.ShortcutRegistry.registry.unregister(Blockly.ShortcutItems.names.COPY);
Blockly.ShortcutRegistry.registry.unregister(Blockly.ShortcutItems.names.CUT);
Blockly.ShortcutRegistry.registry.unregister(Blockly.ShortcutItems.names.PASTE);

registerCopy();
registerCut();
registerPaste();
}

function registerCopy() {
const copyShortcut: Blockly.ShortcutRegistry.KeyboardShortcut = {
name: Blockly.ShortcutItems.names.COPY,
preconditionFn(workspace) {
return oldCopy.preconditionFn(workspace);
},
callback(workspace, e, shortcut) {
const handler = getCopyPasteHandlers()?.copy;

if (handler) {
return handler(workspace, e);
}

return oldCopy.callback(workspace, e, shortcut);
},
// the registered shortcut from blockly isn't an array, it's some sort
// of serialized object so we have to convert it back to an array
keyCodes: [oldCopy.keyCodes[0], oldCopy.keyCodes[1], oldCopy.keyCodes[2]],
};
Blockly.ShortcutRegistry.registry.register(copyShortcut);
}

function registerCut() {
const cutShortcut: Blockly.ShortcutRegistry.KeyboardShortcut = {
name: Blockly.ShortcutItems.names.CUT,
preconditionFn(workspace) {
return oldCut.preconditionFn(workspace);
},
callback(workspace, e, shortcut) {
const handler = getCopyPasteHandlers()?.cut;

if (handler) {
return handler(workspace, e);
}

return oldCut.callback(workspace, e, shortcut);
},
keyCodes: [oldCut.keyCodes[0], oldCut.keyCodes[1], oldCut.keyCodes[2]],
};

Blockly.ShortcutRegistry.registry.register(cutShortcut);
}

function registerPaste() {
const pasteShortcut: Blockly.ShortcutRegistry.KeyboardShortcut = {
name: Blockly.ShortcutItems.names.PASTE,
preconditionFn(workspace) {
return oldPaste.preconditionFn(workspace);
},
callback(workspace, e, shortcut) {
const handler = getCopyPasteHandlers()?.paste;

if (handler) {
return handler(workspace, e);
}

return oldPaste.callback(workspace, e, shortcut);
},
keyCodes: [oldPaste.keyCodes[0], oldPaste.keyCodes[1], oldPaste.keyCodes[2]],
};

Blockly.ShortcutRegistry.registry.register(pasteShortcut);
}
23 changes: 23 additions & 0 deletions pxtblocks/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,27 @@ export function openWorkspaceSearch() {
if (_openWorkspaceSearch) {
_openWorkspaceSearch();
}
}

type ShortcutHandler = (workspace: Blockly.Workspace, e: Event) => boolean;

let _handleCopy: ShortcutHandler;
let _handleCut: ShortcutHandler;
let _handlePaste: ShortcutHandler;

export function setCopyPaste(copy: ShortcutHandler, cut: ShortcutHandler, paste: ShortcutHandler) {
_handleCopy = copy;
_handleCut = cut;
_handlePaste = paste;
}

export function getCopyPasteHandlers() {
if (_handleCopy) {
return {
copy: _handleCopy,
cut: _handleCut,
paste: _handlePaste
};
}
return null;
}
2 changes: 2 additions & 0 deletions pxtblocks/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { renderCodeCard } from "./codecardRenderer";
import { FieldDropdown } from "./fields/field_dropdown";
import { setDraggableShadowBlocks, setDuplicateOnDrag, setDuplicateOnDragStrategy } from "./plugins/duplicateOnDrag";
import { applyPolyfills } from "./polyfills";
import { initCopyPaste } from "./copyPaste";


interface BlockDefinition {
Expand Down Expand Up @@ -607,6 +608,7 @@ function init(blockInfo: pxtc.BlocksInfo) {
initText();
initComments();
initTooltip();
initCopyPaste();
}


Expand Down
194 changes: 194 additions & 0 deletions webapp/src/blocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ import { DuplicateOnDragConnectionChecker } from "../../pxtblocks/plugins/duplic
import { PathObject } from "../../pxtblocks/plugins/renderer/pathObject";
import { Measurements } from "./constants";

interface CopyDataEntry {
version: 1;
data: Blockly.ICopyData;
coord: Blockly.utils.Coordinate;
workspaceId: string;
targetVersion: string;
}


export class Editor extends toolboxeditor.ToolboxEditor {
editor: Blockly.WorkspaceSvg;
Expand Down Expand Up @@ -470,6 +478,8 @@ export class Editor extends toolboxeditor.ToolboxEditor {
if (pxt.Util.isTranslationMode()) {
pxtblockly.external.setPromptTranslateBlock(dialogs.promptTranslateBlock);
}

pxtblockly.external.setCopyPaste(copy, cut, this.pasteCallback);
}

private initBlocklyToolbox() {
Expand Down Expand Up @@ -1938,6 +1948,93 @@ export class Editor extends toolboxeditor.ToolboxEditor {
this.removeBreakpointFromEvent(block.id)
}
}

protected pasteCallback = () => {
const data = getCopyData();
if (!data?.data || !this.editor) return false;

this.pasteAsync(data);
return true;
}

protected async pasteAsync(data: CopyDataEntry) {
const copyData = data.data;
const copyWorkspace = this.editor;
const copyCoords = copyWorkspace.id === data.workspaceId ? data.coord : undefined;

// this pasting code is adapted from Blockly/core/shortcut_items.ts
const doPaste = () => {
if (!copyCoords) {
// If we don't have location data about the original copyable, let the
// paster determine position.
return !!Blockly.clipboard.paste(copyData, copyWorkspace);
}

const { left, top, width, height } = copyWorkspace
.getMetricsManager()
.getViewMetrics(true);
const viewportRect = new Blockly.utils.Rect(
top,
top + height,
left,
left + width
);

if (viewportRect.contains(copyCoords.x, copyCoords.y)) {
// If the original copyable is inside the viewport, let the paster
// determine position.
return !!Blockly.clipboard.paste(copyData, copyWorkspace);
}

// Otherwise, paste in the middle of the viewport.
const centerCoords = new Blockly.utils.Coordinate(
left + width / 2,
top + height / 2
);
return !!Blockly.clipboard.paste(copyData, copyWorkspace, centerCoords);
};

if (data.version !== 1) {
await core.confirmAsync({
header: lf("Paste Error"),
body: lf("The code you are pasting comes from an incompatible version of the editor."),
hideCancel: true
});

return;
}

if (copyData.paster === Blockly.clipboard.BlockPaster.TYPE) {
const typeCounts: {[index: string]: number} = (copyData as any).typeCounts;

for (const blockType of Object.keys(typeCounts)) {
if (!Blockly.Blocks[blockType]) {
await core.confirmAsync({
header: lf("Paste Error"),
body: lf("The code that you're trying to paste contains blocks that aren't available in the current project. If pasting from another project, make sure that you have installed all of the necessary extensions and try again."),
hideCancel: true
});

return;
}
}
}

if (data.targetVersion !== pxt.appTarget.versions.target) {
const result = await core.confirmAsync({
header: lf("Paste Warning"),
body: lf("The code you're trying to paste is from a different version of Microsoft MakeCode. Pasting it may cause issues with your current project. Are you sure you want to continue?"),
agreeLbl: lf("Paste Anyway"),
agreeClass: "red"
});

if (result !== 1) {
return;
}
}

doPaste();
}
}

function forEachImageField(workspace: Blockly.Workspace, cb: (asset: pxtblockly.FieldAssetEditor<any, any>) => void) {
Expand Down Expand Up @@ -2028,4 +2125,101 @@ function resolveLocalizedMarkdown(url: string) {
}

return undefined;
}

// adapted from Blockly/core/shortcut_items.ts
function copy(workspace: Blockly.WorkspaceSvg, e: Event) {
// Prevent the default copy behavior, which may beep or otherwise indicate
// an error due to the lack of a selection.
e.preventDefault();
workspace.hideChaff();
const selected = Blockly.common.getSelected();
if (!selected || !Blockly.isCopyable(selected)) return false;

const copyData = selected.toCopyData();
const copyWorkspace =
selected.workspace instanceof Blockly.WorkspaceSvg
? selected.workspace
: workspace;
const copyCoords = Blockly.isDraggable(selected)
? selected.getRelativeToSurfaceXY()
: null;

if (copyData) {
saveCopyData(
copyData,
copyCoords,
copyWorkspace
);
}

return !!copyData;
}

// adapted from Blockly/core/shortcut_items.ts
function cut(workspace: Blockly.WorkspaceSvg, e: Event) {
const selected = Blockly.common.getSelected();

if (selected instanceof Blockly.BlockSvg) {
const copyData = selected.toCopyData();
const copyWorkspace = workspace;
const copyCoords = selected.getRelativeToSurfaceXY();
saveCopyData(
copyData,
copyCoords,
copyWorkspace
);
selected.checkAndDelete();
return true;
} else if (
Blockly.isDeletable(selected) &&
selected.isDeletable() &&
Blockly.isCopyable(selected)
) {
const copyData = selected.toCopyData();
const copyWorkspace = workspace;
const copyCoords = Blockly.isDraggable(selected)
? selected.getRelativeToSurfaceXY()
: null;
saveCopyData(
copyData,
copyCoords,
copyWorkspace
);
selected.dispose();
return true;
}
return false;
}

function saveCopyData(
data: Blockly.ICopyData,
coord: Blockly.utils.Coordinate,
workspace: Blockly.Workspace
) {
const entry: CopyDataEntry = {
version: 1,
data,
coord,
workspaceId: workspace.id,
targetVersion: pxt.appTarget.versions.target
};

pxt.storage.setLocal(
copyDataKey(),
JSON.stringify(entry)
);
}

function getCopyData(): CopyDataEntry | undefined {
const data = pxt.storage.getLocal(copyDataKey());

if (data) {
return pxt.U.jsonTryParse(data);
}
return undefined;
}

function copyDataKey() {
return "copyData";
}

0 comments on commit 8bc2ef8

Please sign in to comment.