Skip to content

Commit 8bc2ef8

Browse files
authored
support copy/pasting between tabs and projects (#10366)
* support pasting between tabs and projects * pr feedback
1 parent d874027 commit 8bc2ef8

File tree

4 files changed

+307
-0
lines changed

4 files changed

+307
-0
lines changed

pxtblocks/copyPaste.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import * as Blockly from "blockly";
2+
import { getCopyPasteHandlers } from "./external";
3+
4+
let oldCopy: Blockly.ShortcutRegistry.KeyboardShortcut;
5+
let oldCut: Blockly.ShortcutRegistry.KeyboardShortcut;
6+
let oldPaste: Blockly.ShortcutRegistry.KeyboardShortcut;
7+
8+
export function initCopyPaste() {
9+
if (oldCopy) return;
10+
11+
const shortcuts = Blockly.ShortcutRegistry.registry.getRegistry()
12+
13+
oldCopy = { ...shortcuts[Blockly.ShortcutItems.names.COPY] };
14+
oldCut = { ...shortcuts[Blockly.ShortcutItems.names.CUT] };
15+
oldPaste = { ...shortcuts[Blockly.ShortcutItems.names.PASTE] };
16+
17+
Blockly.ShortcutRegistry.registry.unregister(Blockly.ShortcutItems.names.COPY);
18+
Blockly.ShortcutRegistry.registry.unregister(Blockly.ShortcutItems.names.CUT);
19+
Blockly.ShortcutRegistry.registry.unregister(Blockly.ShortcutItems.names.PASTE);
20+
21+
registerCopy();
22+
registerCut();
23+
registerPaste();
24+
}
25+
26+
function registerCopy() {
27+
const copyShortcut: Blockly.ShortcutRegistry.KeyboardShortcut = {
28+
name: Blockly.ShortcutItems.names.COPY,
29+
preconditionFn(workspace) {
30+
return oldCopy.preconditionFn(workspace);
31+
},
32+
callback(workspace, e, shortcut) {
33+
const handler = getCopyPasteHandlers()?.copy;
34+
35+
if (handler) {
36+
return handler(workspace, e);
37+
}
38+
39+
return oldCopy.callback(workspace, e, shortcut);
40+
},
41+
// the registered shortcut from blockly isn't an array, it's some sort
42+
// of serialized object so we have to convert it back to an array
43+
keyCodes: [oldCopy.keyCodes[0], oldCopy.keyCodes[1], oldCopy.keyCodes[2]],
44+
};
45+
Blockly.ShortcutRegistry.registry.register(copyShortcut);
46+
}
47+
48+
function registerCut() {
49+
const cutShortcut: Blockly.ShortcutRegistry.KeyboardShortcut = {
50+
name: Blockly.ShortcutItems.names.CUT,
51+
preconditionFn(workspace) {
52+
return oldCut.preconditionFn(workspace);
53+
},
54+
callback(workspace, e, shortcut) {
55+
const handler = getCopyPasteHandlers()?.cut;
56+
57+
if (handler) {
58+
return handler(workspace, e);
59+
}
60+
61+
return oldCut.callback(workspace, e, shortcut);
62+
},
63+
keyCodes: [oldCut.keyCodes[0], oldCut.keyCodes[1], oldCut.keyCodes[2]],
64+
};
65+
66+
Blockly.ShortcutRegistry.registry.register(cutShortcut);
67+
}
68+
69+
function registerPaste() {
70+
const pasteShortcut: Blockly.ShortcutRegistry.KeyboardShortcut = {
71+
name: Blockly.ShortcutItems.names.PASTE,
72+
preconditionFn(workspace) {
73+
return oldPaste.preconditionFn(workspace);
74+
},
75+
callback(workspace, e, shortcut) {
76+
const handler = getCopyPasteHandlers()?.paste;
77+
78+
if (handler) {
79+
return handler(workspace, e);
80+
}
81+
82+
return oldPaste.callback(workspace, e, shortcut);
83+
},
84+
keyCodes: [oldPaste.keyCodes[0], oldPaste.keyCodes[1], oldPaste.keyCodes[2]],
85+
};
86+
87+
Blockly.ShortcutRegistry.registry.register(pasteShortcut);
88+
}

pxtblocks/external.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,27 @@ export function openWorkspaceSearch() {
8888
if (_openWorkspaceSearch) {
8989
_openWorkspaceSearch();
9090
}
91+
}
92+
93+
type ShortcutHandler = (workspace: Blockly.Workspace, e: Event) => boolean;
94+
95+
let _handleCopy: ShortcutHandler;
96+
let _handleCut: ShortcutHandler;
97+
let _handlePaste: ShortcutHandler;
98+
99+
export function setCopyPaste(copy: ShortcutHandler, cut: ShortcutHandler, paste: ShortcutHandler) {
100+
_handleCopy = copy;
101+
_handleCut = cut;
102+
_handlePaste = paste;
103+
}
104+
105+
export function getCopyPasteHandlers() {
106+
if (_handleCopy) {
107+
return {
108+
copy: _handleCopy,
109+
cut: _handleCut,
110+
paste: _handlePaste
111+
};
112+
}
113+
return null;
91114
}

pxtblocks/loader.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { renderCodeCard } from "./codecardRenderer";
2525
import { FieldDropdown } from "./fields/field_dropdown";
2626
import { setDraggableShadowBlocks, setDuplicateOnDrag, setDuplicateOnDragStrategy } from "./plugins/duplicateOnDrag";
2727
import { applyPolyfills } from "./polyfills";
28+
import { initCopyPaste } from "./copyPaste";
2829

2930

3031
interface BlockDefinition {
@@ -607,6 +608,7 @@ function init(blockInfo: pxtc.BlocksInfo) {
607608
initText();
608609
initComments();
609610
initTooltip();
611+
initCopyPaste();
610612
}
611613

612614

webapp/src/blocks.tsx

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ import { DuplicateOnDragConnectionChecker } from "../../pxtblocks/plugins/duplic
3737
import { PathObject } from "../../pxtblocks/plugins/renderer/pathObject";
3838
import { Measurements } from "./constants";
3939

40+
interface CopyDataEntry {
41+
version: 1;
42+
data: Blockly.ICopyData;
43+
coord: Blockly.utils.Coordinate;
44+
workspaceId: string;
45+
targetVersion: string;
46+
}
47+
4048

4149
export class Editor extends toolboxeditor.ToolboxEditor {
4250
editor: Blockly.WorkspaceSvg;
@@ -470,6 +478,8 @@ export class Editor extends toolboxeditor.ToolboxEditor {
470478
if (pxt.Util.isTranslationMode()) {
471479
pxtblockly.external.setPromptTranslateBlock(dialogs.promptTranslateBlock);
472480
}
481+
482+
pxtblockly.external.setCopyPaste(copy, cut, this.pasteCallback);
473483
}
474484

475485
private initBlocklyToolbox() {
@@ -1938,6 +1948,93 @@ export class Editor extends toolboxeditor.ToolboxEditor {
19381948
this.removeBreakpointFromEvent(block.id)
19391949
}
19401950
}
1951+
1952+
protected pasteCallback = () => {
1953+
const data = getCopyData();
1954+
if (!data?.data || !this.editor) return false;
1955+
1956+
this.pasteAsync(data);
1957+
return true;
1958+
}
1959+
1960+
protected async pasteAsync(data: CopyDataEntry) {
1961+
const copyData = data.data;
1962+
const copyWorkspace = this.editor;
1963+
const copyCoords = copyWorkspace.id === data.workspaceId ? data.coord : undefined;
1964+
1965+
// this pasting code is adapted from Blockly/core/shortcut_items.ts
1966+
const doPaste = () => {
1967+
if (!copyCoords) {
1968+
// If we don't have location data about the original copyable, let the
1969+
// paster determine position.
1970+
return !!Blockly.clipboard.paste(copyData, copyWorkspace);
1971+
}
1972+
1973+
const { left, top, width, height } = copyWorkspace
1974+
.getMetricsManager()
1975+
.getViewMetrics(true);
1976+
const viewportRect = new Blockly.utils.Rect(
1977+
top,
1978+
top + height,
1979+
left,
1980+
left + width
1981+
);
1982+
1983+
if (viewportRect.contains(copyCoords.x, copyCoords.y)) {
1984+
// If the original copyable is inside the viewport, let the paster
1985+
// determine position.
1986+
return !!Blockly.clipboard.paste(copyData, copyWorkspace);
1987+
}
1988+
1989+
// Otherwise, paste in the middle of the viewport.
1990+
const centerCoords = new Blockly.utils.Coordinate(
1991+
left + width / 2,
1992+
top + height / 2
1993+
);
1994+
return !!Blockly.clipboard.paste(copyData, copyWorkspace, centerCoords);
1995+
};
1996+
1997+
if (data.version !== 1) {
1998+
await core.confirmAsync({
1999+
header: lf("Paste Error"),
2000+
body: lf("The code you are pasting comes from an incompatible version of the editor."),
2001+
hideCancel: true
2002+
});
2003+
2004+
return;
2005+
}
2006+
2007+
if (copyData.paster === Blockly.clipboard.BlockPaster.TYPE) {
2008+
const typeCounts: {[index: string]: number} = (copyData as any).typeCounts;
2009+
2010+
for (const blockType of Object.keys(typeCounts)) {
2011+
if (!Blockly.Blocks[blockType]) {
2012+
await core.confirmAsync({
2013+
header: lf("Paste Error"),
2014+
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."),
2015+
hideCancel: true
2016+
});
2017+
2018+
return;
2019+
}
2020+
}
2021+
}
2022+
2023+
if (data.targetVersion !== pxt.appTarget.versions.target) {
2024+
const result = await core.confirmAsync({
2025+
header: lf("Paste Warning"),
2026+
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?"),
2027+
agreeLbl: lf("Paste Anyway"),
2028+
agreeClass: "red"
2029+
});
2030+
2031+
if (result !== 1) {
2032+
return;
2033+
}
2034+
}
2035+
2036+
doPaste();
2037+
}
19412038
}
19422039

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

20302127
return undefined;
2128+
}
2129+
2130+
// adapted from Blockly/core/shortcut_items.ts
2131+
function copy(workspace: Blockly.WorkspaceSvg, e: Event) {
2132+
// Prevent the default copy behavior, which may beep or otherwise indicate
2133+
// an error due to the lack of a selection.
2134+
e.preventDefault();
2135+
workspace.hideChaff();
2136+
const selected = Blockly.common.getSelected();
2137+
if (!selected || !Blockly.isCopyable(selected)) return false;
2138+
2139+
const copyData = selected.toCopyData();
2140+
const copyWorkspace =
2141+
selected.workspace instanceof Blockly.WorkspaceSvg
2142+
? selected.workspace
2143+
: workspace;
2144+
const copyCoords = Blockly.isDraggable(selected)
2145+
? selected.getRelativeToSurfaceXY()
2146+
: null;
2147+
2148+
if (copyData) {
2149+
saveCopyData(
2150+
copyData,
2151+
copyCoords,
2152+
copyWorkspace
2153+
);
2154+
}
2155+
2156+
return !!copyData;
2157+
}
2158+
2159+
// adapted from Blockly/core/shortcut_items.ts
2160+
function cut(workspace: Blockly.WorkspaceSvg, e: Event) {
2161+
const selected = Blockly.common.getSelected();
2162+
2163+
if (selected instanceof Blockly.BlockSvg) {
2164+
const copyData = selected.toCopyData();
2165+
const copyWorkspace = workspace;
2166+
const copyCoords = selected.getRelativeToSurfaceXY();
2167+
saveCopyData(
2168+
copyData,
2169+
copyCoords,
2170+
copyWorkspace
2171+
);
2172+
selected.checkAndDelete();
2173+
return true;
2174+
} else if (
2175+
Blockly.isDeletable(selected) &&
2176+
selected.isDeletable() &&
2177+
Blockly.isCopyable(selected)
2178+
) {
2179+
const copyData = selected.toCopyData();
2180+
const copyWorkspace = workspace;
2181+
const copyCoords = Blockly.isDraggable(selected)
2182+
? selected.getRelativeToSurfaceXY()
2183+
: null;
2184+
saveCopyData(
2185+
copyData,
2186+
copyCoords,
2187+
copyWorkspace
2188+
);
2189+
selected.dispose();
2190+
return true;
2191+
}
2192+
return false;
2193+
}
2194+
2195+
function saveCopyData(
2196+
data: Blockly.ICopyData,
2197+
coord: Blockly.utils.Coordinate,
2198+
workspace: Blockly.Workspace
2199+
) {
2200+
const entry: CopyDataEntry = {
2201+
version: 1,
2202+
data,
2203+
coord,
2204+
workspaceId: workspace.id,
2205+
targetVersion: pxt.appTarget.versions.target
2206+
};
2207+
2208+
pxt.storage.setLocal(
2209+
copyDataKey(),
2210+
JSON.stringify(entry)
2211+
);
2212+
}
2213+
2214+
function getCopyData(): CopyDataEntry | undefined {
2215+
const data = pxt.storage.getLocal(copyDataKey());
2216+
2217+
if (data) {
2218+
return pxt.U.jsonTryParse(data);
2219+
}
2220+
return undefined;
2221+
}
2222+
2223+
function copyDataKey() {
2224+
return "copyData";
20312225
}

0 commit comments

Comments
 (0)