Skip to content

Commit

Permalink
add copy/paste to context menu and restrict tutorial pasting (#10379)
Browse files Browse the repository at this point in the history
  • Loading branch information
riknoll authored Feb 10, 2025
1 parent d3b668f commit ec58c8c
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 28 deletions.
5 changes: 3 additions & 2 deletions pxtblocks/contextMenu/blockItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import * as Blockly from "blockly";
import { openHelpUrl } from "../external";

// Lower weight is higher in context menu
enum BlockContextWeight {
export enum BlockContextWeight {
Duplicate = 10,
Copy = 15,
AddComment = 20,
ExpandCollapse = 30,
DeleteBlock = 40,
Help = 50
Help = 50,
}

export function registerBlockitems() {
Expand Down
3 changes: 2 additions & 1 deletion pxtblocks/contextMenu/workspaceItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { flow, screenshotAsync, screenshotEnabled, setCollapsedAll } from "../la
import { openWorkspaceSearch } from "../external";

// Lower weight is higher in context menu
enum WorkspaceContextWeight {
export enum WorkspaceContextWeight {
Paste = 10,
DeleteAll = 20,
FormatCode = 30,
CollapseBlocks = 40,
Expand Down
133 changes: 117 additions & 16 deletions pxtblocks/copyPaste.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as Blockly from "blockly";
import { getCopyPasteHandlers } from "./external";
import { BlockContextWeight } from "./contextMenu/blockItems";
import { WorkspaceContextWeight } from "./contextMenu/workspaceItems";

let oldCopy: Blockly.ShortcutRegistry.KeyboardShortcut;
let oldCut: Blockly.ShortcutRegistry.KeyboardShortcut;
Expand All @@ -21,6 +23,8 @@ export function initCopyPaste() {
registerCopy();
registerCut();
registerPaste();
registerCopyContextMenu();
registerPasteContextMenu();
}

function registerCopy() {
Expand All @@ -29,15 +33,7 @@ function registerCopy() {
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);
},
callback: copy,
// 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]],
Expand Down Expand Up @@ -72,17 +68,122 @@ function registerPaste() {
preconditionFn(workspace) {
return oldPaste.preconditionFn(workspace);
},
callback(workspace, e, shortcut) {
const handler = getCopyPasteHandlers()?.paste;
callback: paste,
keyCodes: [oldPaste.keyCodes[0], oldPaste.keyCodes[1], oldPaste.keyCodes[2]],
};

if (handler) {
return handler(workspace, e);
Blockly.ShortcutRegistry.registry.register(pasteShortcut);
}

function registerCopyContextMenu() {
const copyOption: Blockly.ContextMenuRegistry.RegistryItem = {
displayText: () => lf("Copy"),
preconditionFn: scope => {
const block = scope.block;
if (block.isInFlyout || !block.isMovable() || !block.isEditable()) {
return "hidden";
}

const handlers = getCopyPasteHandlers();

if (handlers) {
return handlers.copyPrecondition(scope);
}

return oldPaste.callback(workspace, e, shortcut);
return "enabled";
},
keyCodes: [oldPaste.keyCodes[0], oldPaste.keyCodes[1], oldPaste.keyCodes[2]],
callback: function (scope: Blockly.ContextMenuRegistry.Scope, e: PointerEvent): void {
const block = scope.block;

if (!block) return;

block.select();
copy(block.workspace, e);
},
scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK,
weight: BlockContextWeight.Copy,
id: "makecode-copy-block"
};

Blockly.ShortcutRegistry.registry.register(pasteShortcut);
const copyCommentOption: Blockly.ContextMenuRegistry.RegistryItem = {
displayText: () => lf("Copy"),
preconditionFn: scope => {
const comment = scope.comment;
if (!comment.isMovable() || !comment.isEditable()) {
return "hidden";
}

const handlers = getCopyPasteHandlers();

if (handlers) {
return handlers.copyPrecondition(scope);
}

return "enabled";
},
callback: function (scope: Blockly.ContextMenuRegistry.Scope, e: PointerEvent): void {
const comment = scope.comment;

if (!comment) return;

comment.select();
copy(comment.workspace, e);
},
scopeType: Blockly.ContextMenuRegistry.ScopeType.COMMENT,
weight: BlockContextWeight.Copy,
id: "makecode-copy-comment"
};

Blockly.ContextMenuRegistry.registry.register(copyOption);
Blockly.ContextMenuRegistry.registry.register(copyCommentOption);
}

function registerPasteContextMenu() {
const pasteOption: Blockly.ContextMenuRegistry.RegistryItem = {
displayText: () => lf("Paste"),
preconditionFn: scope => {
if (pxt.shell.isReadOnly() || scope.workspace.options.readOnly) {
return "hidden";
}

const handlers = getCopyPasteHandlers();

if (handlers) {
return handlers.pastePrecondition(scope);
}

return "enabled";
},
callback: function (scope: Blockly.ContextMenuRegistry.Scope, e: PointerEvent): void {
const workspace = scope.workspace;

if (!workspace) return;
paste(workspace, e);
},
scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE,
weight: WorkspaceContextWeight.Paste,
id: "makecode-paste"
};

Blockly.ContextMenuRegistry.registry.register(pasteOption);
}

const copy = (workspace: Blockly.WorkspaceSvg, e: Event, shortcut?: Blockly.ShortcutRegistry.KeyboardShortcut) => {
const handler = getCopyPasteHandlers()?.copy;

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

return oldCopy.callback(workspace, e, shortcut);
}

const paste = (workspace: Blockly.WorkspaceSvg, e: Event, shortcut?: Blockly.ShortcutRegistry.KeyboardShortcut) => {
const handler = getCopyPasteHandlers()?.paste;

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

return oldPaste.callback(workspace, e, shortcut);
}
11 changes: 9 additions & 2 deletions pxtblocks/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,23 +91,30 @@ export function openWorkspaceSearch() {
}

type ShortcutHandler = (workspace: Blockly.Workspace, e: Event) => boolean;
type PreconditionFn = (scope: Blockly.ContextMenuRegistry.Scope) => "enabled" | "disabled" | "hidden";

let _handleCopy: ShortcutHandler;
let _handleCut: ShortcutHandler;
let _handlePaste: ShortcutHandler;
let _copyPre: PreconditionFn;
let _pastePre: PreconditionFn;

export function setCopyPaste(copy: ShortcutHandler, cut: ShortcutHandler, paste: ShortcutHandler) {
export function setCopyPaste(copy: ShortcutHandler, cut: ShortcutHandler, paste: ShortcutHandler, copyPrecondition: PreconditionFn, pastePrecondition: PreconditionFn ) {
_handleCopy = copy;
_handleCut = cut;
_handlePaste = paste;
_copyPre = copyPrecondition;
_pastePre = pastePrecondition;
}

export function getCopyPasteHandlers() {
if (_handleCopy) {
return {
copy: _handleCopy,
cut: _handleCut,
paste: _handlePaste
paste: _handlePaste,
copyPrecondition: _copyPre,
pastePrecondition: _pastePre,
};
}
return null;
Expand Down
58 changes: 51 additions & 7 deletions webapp/src/blocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ interface CopyDataEntry {
coord: Blockly.utils.Coordinate;
workspaceId: string;
targetVersion: string;
headerId: string;
}


Expand Down Expand Up @@ -477,7 +478,7 @@ export class Editor extends toolboxeditor.ToolboxEditor {
pxtblockly.external.setPromptTranslateBlock(dialogs.promptTranslateBlock);
}

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

private initBlocklyToolbox() {
Expand Down Expand Up @@ -1949,7 +1950,7 @@ export class Editor extends toolboxeditor.ToolboxEditor {

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

this.pasteAsync(data);
return true;
Expand Down Expand Up @@ -2033,6 +2034,44 @@ export class Editor extends toolboxeditor.ToolboxEditor {

doPaste();
}

protected copyPrecondition = (scope: Blockly.ContextMenuRegistry.Scope) => {
const workspace = scope.block?.workspace || scope.comment?.workspace;

if (!workspace || workspace !== this.editor) {
return "hidden";
}

return "enabled";
}

protected pastePrecondition = (scope: Blockly.ContextMenuRegistry.Scope) => {
if (scope.workspace !== this.editor) return "hidden";

const data = getCopyData();

if (!data || !this.canPasteData(data)) {
return "disabled";
}

return "enabled";
}

protected canPasteData(data: CopyDataEntry): boolean {
const header = pkg.mainEditorPkg().header;

if (!header) {
return false;
}
else if (data.headerId === header.id) {
return true;
}
else if (header.tutorial && !header.tutorialCompleted) {
return false;
}

return true;
}
}

function forEachImageField(workspace: Blockly.Workspace, cb: (asset: pxtblockly.FieldAssetEditor<any, any>) => void) {
Expand Down Expand Up @@ -2147,7 +2186,8 @@ function copy(workspace: Blockly.WorkspaceSvg, e: Event) {
saveCopyData(
copyData,
copyCoords,
copyWorkspace
copyWorkspace,
pkg.mainEditorPkg().header.id
);
}

Expand All @@ -2165,7 +2205,8 @@ function cut(workspace: Blockly.WorkspaceSvg, e: Event) {
saveCopyData(
copyData,
copyCoords,
copyWorkspace
copyWorkspace,
pkg.mainEditorPkg().header.id
);
selected.checkAndDelete();
return true;
Expand All @@ -2182,7 +2223,8 @@ function cut(workspace: Blockly.WorkspaceSvg, e: Event) {
saveCopyData(
copyData,
copyCoords,
copyWorkspace
copyWorkspace,
pkg.mainEditorPkg().header.id
);
selected.dispose();
return true;
Expand All @@ -2193,14 +2235,16 @@ function cut(workspace: Blockly.WorkspaceSvg, e: Event) {
function saveCopyData(
data: Blockly.ICopyData,
coord: Blockly.utils.Coordinate,
workspace: Blockly.Workspace
workspace: Blockly.Workspace,
headerId: string
) {
const entry: CopyDataEntry = {
version: 1,
data,
coord,
workspaceId: workspace.id,
targetVersion: pxt.appTarget.versions.target
targetVersion: pxt.appTarget.versions.target,
headerId
};

pxt.storage.setLocal(
Expand Down

0 comments on commit ec58c8c

Please sign in to comment.