diff --git a/core/block_svg.ts b/core/block_svg.ts index 9fb809ade..161170242 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -602,7 +602,7 @@ export class BlockSvg * @param e Mouse event. * @internal */ - showContextMenu(e: Event) { + showContextMenu(e: PointerEvent) { const menuOptions = this.generateContextMenu(); if (menuOptions && menuOptions.length) { diff --git a/core/blockly.ts b/core/blockly.ts index 711eec907..510b5b8ed 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -399,23 +399,6 @@ WorkspaceSvg.newTrashcan = function (workspace: WorkspaceSvg): Trashcan { return new Trashcan(workspace); }; -WorkspaceCommentSvg.prototype.showContextMenu = function ( - this: WorkspaceCommentSvg, - e: Event, -) { - if (this.workspace.options.readOnly) { - return; - } - const menuOptions = []; - - if (this.isDeletable() && this.isMovable()) { - menuOptions.push(ContextMenu.commentDuplicateOption(this)); - menuOptions.push(ContextMenu.commentDeleteOption(this)); - } - - ContextMenu.show(e, menuOptions, this.RTL); -}; - MiniWorkspaceBubble.prototype.newWorkspaceSvg = function ( options: Options, ): WorkspaceSvg { diff --git a/core/comments/rendered_workspace_comment.ts b/core/comments/rendered_workspace_comment.ts index 9d9ccc572..6fb8b9b66 100644 --- a/core/comments/rendered_workspace_comment.ts +++ b/core/comments/rendered_workspace_comment.ts @@ -25,6 +25,9 @@ import { WorkspaceCommentPaster, WorkspaceCommentCopyData, } from '../clipboard/workspace_comment_paster.js'; +import {IContextMenu} from '../interfaces/i_contextmenu.js'; +import * as contextMenu from '../contextmenu.js'; +import {ContextMenuRegistry} from '../contextmenu_registry.js'; export class RenderedWorkspaceComment extends WorkspaceComment @@ -34,7 +37,8 @@ export class RenderedWorkspaceComment IDraggable, ISelectable, IDeletable, - ICopyable + ICopyable, + IContextMenu { /** The class encompassing the svg elements making up the workspace comment. */ private view: CommentView; @@ -178,11 +182,6 @@ export class RenderedWorkspaceComment } } - /** Returns whether this comment is deletable or not. */ - isDeletable(): boolean { - return !this.workspace.options.readOnly; - } - /** Visually indicates that this comment would be deleted if dropped. */ setDeleteStyle(wouldDelete: boolean): void { if (wouldDelete) { @@ -239,4 +238,13 @@ export class RenderedWorkspaceComment }), }; } + + /** Show a context menu for this comment. */ + showContextMenu(e: PointerEvent): void { + const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions( + ContextMenuRegistry.ScopeType.COMMENT, + {comment: this}, + ); + contextMenu.show(e, menuOptions, this.workspace.RTL); + } } diff --git a/core/contextmenu.ts b/core/contextmenu.ts index d1d5f5e4a..939477b3c 100644 --- a/core/contextmenu.ts +++ b/core/contextmenu.ts @@ -9,7 +9,6 @@ import type {Block} from './block.js'; import type {BlockSvg} from './block_svg.js'; import * as browserEvents from './browser_events.js'; -import * as clipboard from './clipboard.js'; import {config} from './config.js'; import * as dom from './utils/dom.js'; import type { @@ -19,15 +18,11 @@ import type { import * as eventUtils from './events/utils.js'; import {Menu} from './menu.js'; import {MenuItem} from './menuitem.js'; -import {Msg} from './msg.js'; import * as aria from './utils/aria.js'; -import {Coordinate} from './utils/coordinate.js'; import {Rect} from './utils/rect.js'; import * as serializationBlocks from './serialization/blocks.js'; import * as svgMath from './utils/svg_math.js'; import * as WidgetDiv from './widgetdiv.js'; -import {WorkspaceCommentSvg} from './workspace_comment_svg.js'; -import type {WorkspaceSvg} from './workspace_svg.js'; import * as Xml from './xml.js'; import * as common from './common.js'; @@ -69,7 +64,7 @@ let menu_: Menu | null = null; * @param rtl True if RTL, false if LTR. */ export function show( - e: Event, + e: PointerEvent, options: (ContextMenuOption | LegacyContextMenuOption)[], rtl: boolean, ) { @@ -78,7 +73,7 @@ export function show( hide(); return; } - const menu = populate_(options, rtl); + const menu = populate_(options, rtl, e); menu_ = menu; position_(menu, e, rtl); @@ -95,11 +90,13 @@ export function show( * * @param options Array of menu options. * @param rtl True if RTL, false if LTR. + * @param e The event that triggered the context menu to open. * @returns The menu that will be shown on right click. */ function populate_( options: (ContextMenuOption | LegacyContextMenuOption)[], rtl: boolean, + e: PointerEvent, ): Menu { /* Here's what one option object looks like: {text: 'Make It So', @@ -124,7 +121,7 @@ function populate_( // will not be expecting a scope parameter, so there should be // no problems. Just assume it is a ContextMenuOption and we'll // pass undefined if it's not. - option.callback((option as ContextMenuOption).scope); + option.callback((option as ContextMenuOption).scope, e); }, 0); }); }; @@ -266,125 +263,3 @@ export function callbackFactory( return newBlock; }; } - -// Helper functions for creating context menu options. - -/** - * Make a context menu option for deleting the current workspace comment. - * - * @param comment The workspace comment where the - * right-click originated. - * @returns A menu option, - * containing text, enabled, and a callback. - * @internal - */ -export function commentDeleteOption( - comment: WorkspaceCommentSvg, -): LegacyContextMenuOption { - const deleteOption = { - text: Msg['REMOVE_COMMENT'], - enabled: true, - callback: function () { - eventUtils.setGroup(true); - comment.dispose(); - eventUtils.setGroup(false); - }, - }; - return deleteOption; -} - -/** - * Make a context menu option for duplicating the current workspace comment. - * - * @param comment The workspace comment where the - * right-click originated. - * @returns A menu option, - * containing text, enabled, and a callback. - * @internal - */ -export function commentDuplicateOption( - comment: WorkspaceCommentSvg, -): LegacyContextMenuOption { - const duplicateOption = { - text: Msg['DUPLICATE_COMMENT'], - enabled: true, - callback: function () { - const data = comment.toCopyData(); - if (!data) return; - clipboard.paste(data, comment.workspace); - }, - }; - return duplicateOption; -} - -/** - * Make a context menu option for adding a comment on the workspace. - * - * @param ws The workspace where the right-click - * originated. - * @param e The right-click mouse event. - * @returns A menu option, containing text, enabled, and a callback. - * comments are not bundled in. - * @internal - */ -export function workspaceCommentOption( - ws: WorkspaceSvg, - e: Event, -): ContextMenuOption { - /** - * Helper function to create and position a comment correctly based on the - * location of the mouse event. - */ - function addWsComment() { - const comment = new WorkspaceCommentSvg( - ws, - Msg['WORKSPACE_COMMENT_DEFAULT_TEXT'], - WorkspaceCommentSvg.DEFAULT_SIZE, - WorkspaceCommentSvg.DEFAULT_SIZE, - ); - - const injectionDiv = ws.getInjectionDiv(); - // Bounding rect coordinates are in client coordinates, meaning that they - // are in pixels relative to the upper left corner of the visible browser - // window. These coordinates change when you scroll the browser window. - const boundingRect = injectionDiv.getBoundingClientRect(); - - // The client coordinates offset by the injection div's upper left corner. - const mouseEvent = e as MouseEvent; - const clientOffsetPixels = new Coordinate( - mouseEvent.clientX - boundingRect.left, - mouseEvent.clientY - boundingRect.top, - ); - - // The offset in pixels between the main workspace's origin and the upper - // left corner of the injection div. - const mainOffsetPixels = ws.getOriginOffsetInPixels(); - - // The position of the new comment in pixels relative to the origin of the - // main workspace. - const finalOffset = Coordinate.difference( - clientOffsetPixels, - mainOffsetPixels, - ); - // The position of the new comment in main workspace coordinates. - finalOffset.scale(1 / ws.scale); - - const commentX = finalOffset.x; - const commentY = finalOffset.y; - comment.moveBy(commentX, commentY); - if (ws.rendered) { - comment.initSvg(); - comment.render(); - common.setSelected(comment); - } - } - - const wsCommentOption = { - enabled: true, - } as ContextMenuOption; - wsCommentOption.text = Msg['ADD_COMMENT']; - wsCommentOption.callback = function () { - addWsComment(); - }; - return wsCommentOption; -} diff --git a/core/contextmenu_items.ts b/core/contextmenu_items.ts index a540a1341..42349cb5f 100644 --- a/core/contextmenu_items.ts +++ b/core/contextmenu_items.ts @@ -8,6 +8,7 @@ import type {BlockSvg} from './block_svg.js'; import * as clipboard from './clipboard.js'; +import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js'; import { ContextMenuRegistry, RegistryItem, @@ -19,7 +20,9 @@ import * as eventUtils from './events/utils.js'; import {CommentIcon} from './icons/comment_icon.js'; import {Msg} from './msg.js'; import {StatementInput} from './renderers/zelos/zelos.js'; +import {Coordinate} from './utils/coordinate.js'; import type {WorkspaceSvg} from './workspace_svg.js'; +import * as common from './common.js'; /** * Option to undo previous action. @@ -554,6 +557,106 @@ export function registerHelp() { ContextMenuRegistry.registry.register(helpOption); } +/** Registers an option for deleting a workspace comment. */ +export function registerCommentDelete() { + const deleteOption: RegistryItem = { + displayText: () => Msg['REMOVE_COMMENT'], + preconditionFn(scope: Scope) { + return scope.comment?.isDeletable() ? 'enabled' : 'hidden'; + }, + callback(scope: Scope) { + eventUtils.setGroup(true); + scope.comment?.dispose(); + eventUtils.setGroup(false); + }, + scopeType: ContextMenuRegistry.ScopeType.COMMENT, + id: 'commentDelete', + weight: 6, + }; + ContextMenuRegistry.registry.register(deleteOption); +} + +/** Registers an option for duplicating a workspace comment. */ +export function registerCommentDuplicate() { + const duplicateOption: RegistryItem = { + displayText: () => Msg['DUPLICATE_COMMENT'], + preconditionFn(scope: Scope) { + return scope.comment?.isMovable() ? 'enabled' : 'hidden'; + }, + callback(scope: Scope) { + if (!scope.comment) return; + const data = scope.comment.toCopyData(); + if (!data) return; + clipboard.paste(data, scope.comment.workspace); + }, + scopeType: ContextMenuRegistry.ScopeType.COMMENT, + id: 'commentDuplicate', + weight: 1, + }; + ContextMenuRegistry.registry.register(duplicateOption); +} + +/** Registers an option for adding a workspace comment to the workspace. */ +export function registerCommentCreate() { + const createOption: RegistryItem = { + displayText: () => Msg['ADD_COMMENT'], + preconditionFn: () => 'enabled', + callback: (scope: Scope, e: PointerEvent) => { + const workspace = scope.workspace; + if (!workspace) return; + eventUtils.setGroup(true); + const comment = new RenderedWorkspaceComment(workspace); + comment.setText(Msg['WORKSPACE_COMMENT_DEFAULT_TEXT']); + comment.moveTo( + pixelsToWorkspaceCoords( + new Coordinate(e.clientX, e.clientY), + workspace, + ), + ); + common.setSelected(comment); + eventUtils.setGroup(false); + }, + scopeType: ContextMenuRegistry.ScopeType.WORKSPACE, + id: 'commentCreate', + weight: 8, + }; + ContextMenuRegistry.registry.register(createOption); +} + +/** + * Converts pixel coordinates (relative to the window) to workspace coordinates. + */ +function pixelsToWorkspaceCoords( + pixelCoord: Coordinate, + workspace: WorkspaceSvg, +): Coordinate { + const injectionDiv = workspace.getInjectionDiv(); + // Bounding rect coordinates are in client coordinates, meaning that they + // are in pixels relative to the upper left corner of the visible browser + // window. These coordinates change when you scroll the browser window. + const boundingRect = injectionDiv.getBoundingClientRect(); + + // The client coordinates offset by the injection div's upper left corner. + const clientOffsetPixels = new Coordinate( + pixelCoord.x - boundingRect.left, + pixelCoord.y - boundingRect.top, + ); + + // The offset in pixels between the main workspace's origin and the upper + // left corner of the injection div. + const mainOffsetPixels = workspace.getOriginOffsetInPixels(); + + // The position of the new comment in pixels relative to the origin of the + // main workspace. + const finalOffset = Coordinate.difference( + clientOffsetPixels, + mainOffsetPixels, + ); + // The position of the new comment in main workspace coordinates. + finalOffset.scale(1 / workspace.scale); + return finalOffset; +} + /** Registers all block-scoped context menu items. */ function registerBlockOptions_() { registerDuplicate(); @@ -565,6 +668,13 @@ function registerBlockOptions_() { registerHelp(); } +/** Registers all workspace comment related menu items. */ +export function registerCommentOptions() { + registerCommentDuplicate(); + registerCommentDelete(); + registerCommentCreate(); +} + /** * Registers all default context menu items. This should be called once per * instance of ContextMenuRegistry. diff --git a/core/contextmenu_registry.ts b/core/contextmenu_registry.ts index c1183ca97..abbd0f975 100644 --- a/core/contextmenu_registry.ts +++ b/core/contextmenu_registry.ts @@ -12,6 +12,7 @@ // Former goog.module ID: Blockly.ContextMenuRegistry import type {BlockSvg} from './block_svg.js'; +import {RenderedWorkspaceComment} from './comments/rendered_workspace_comment.js'; import type {WorkspaceSvg} from './workspace_svg.js'; /** @@ -119,6 +120,7 @@ export namespace ContextMenuRegistry { export enum ScopeType { BLOCK = 'block', WORKSPACE = 'workspace', + COMMENT = 'comment', } /** @@ -128,13 +130,20 @@ export namespace ContextMenuRegistry { export interface Scope { block?: BlockSvg; workspace?: WorkspaceSvg; + comment?: RenderedWorkspaceComment; } /** * A menu item as entered in the registry. */ export interface RegistryItem { - callback: (p1: Scope) => void; + /** + * @param scope Object that provides a reference to the thing that had its + * context menu opened. + * @param e The original event that triggered the context menu to open. Not + * the event that triggered the click on the option. + */ + callback: (scope: Scope, e: PointerEvent) => void; scopeType: ScopeType; displayText: ((p1: Scope) => string | HTMLElement) | string | HTMLElement; preconditionFn: (p1: Scope) => string; @@ -148,7 +157,13 @@ export namespace ContextMenuRegistry { export interface ContextMenuOption { text: string | HTMLElement; enabled: boolean; - callback: (p1: Scope) => void; + /** + * @param scope Object that provides a reference to the thing that had its + * context menu opened. + * @param e The original event that triggered the context menu to open. Not + * the event that triggered the click on the option. + */ + callback: (scope: Scope, e: PointerEvent) => void; scope: Scope; weight: number; } diff --git a/core/gesture.ts b/core/gesture.ts index 1109ecfce..7970ed006 100644 --- a/core/gesture.ts +++ b/core/gesture.ts @@ -721,6 +721,9 @@ export class Gesture { this.targetBlock.showContextMenu(e); } else if (this.startBubble) { this.startBubble.showContextMenu(e); + } else if (this.startComment) { + this.startComment.workspace.hideChaff(); + this.startComment.showContextMenu(e); } else if (this.startWorkspace_ && !this.flyout) { this.startWorkspace_.hideChaff(); this.startWorkspace_.showContextMenu(e); diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts index 611a6a717..589062876 100644 --- a/core/workspace_svg.ts +++ b/core/workspace_svg.ts @@ -1673,7 +1673,7 @@ export class WorkspaceSvg extends Workspace implements IASTNodeLocationSvg { * @param e Mouse event. * @internal */ - showContextMenu(e: Event) { + showContextMenu(e: PointerEvent) { if (this.options.readOnly || this.isFlyout) { return; } diff --git a/tests/playground.html b/tests/playground.html index 43106bb79..b06ce8efb 100644 --- a/tests/playground.html +++ b/tests/playground.html @@ -69,7 +69,10 @@ }, }); initToolbox(workspace); + workspace.configureContextMenu = configureContextMenu; + Blockly.ContextMenuItems.registerCommentOptions(); + // Restore previously displayed text. if (sessionStorage) { var text = sessionStorage.getItem('textarea'); @@ -265,11 +268,6 @@ }, }; menuOptions.push(screenshotOption); - - // Adds a default-sized workspace comment to the workspace. - menuOptions.push( - Blockly.ContextMenu.workspaceCommentOption(workspace, e), - ); } function logger(e) {