Context menu registry, initial version. (#3930)

* initial version of registry for context menu options
* Registers workspace-level context menu options
This commit is contained in:
Maribeth Bottorff
2020-07-07 10:41:53 -07:00
committed by GitHub
parent 02d5f6e4e9
commit 5e55d89d3b
5 changed files with 453 additions and 132 deletions

View File

@@ -40,6 +40,8 @@ goog.addDependency('../../core/connection.js', ['Blockly.Connection'], ['Blockly
goog.addDependency('../../core/connection_db.js', ['Blockly.ConnectionDB'], ['Blockly.RenderedConnection'], {});
goog.addDependency('../../core/constants.js', ['Blockly.constants'], [], {});
goog.addDependency('../../core/contextmenu.js', ['Blockly.ContextMenu'], ['Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Menu', 'Blockly.MenuItem', 'Blockly.Msg', 'Blockly.Xml', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Rect', 'Blockly.utils.dom', 'Blockly.utils.userAgent'], {});
goog.addDependency('../../core/contextmenu_items.js', ['Blockly.ContextMenuItems'], [], {});
goog.addDependency('../../core/contextmenu_registry.js', ['Blockly.ContextMenuRegistry'], ['Blockly.ContextMenuItems'], {'lang': 'es5'});
goog.addDependency('../../core/css.js', ['Blockly.Css'], [], {'lang': 'es5'});
goog.addDependency('../../core/dropdowndiv.js', ['Blockly.DropDownDiv'], ['Blockly.utils.dom', 'Blockly.utils.math', 'Blockly.utils.style'], {});
goog.addDependency('../../core/events.js', ['Blockly.Events'], ['Blockly.utils'], {});
@@ -188,7 +190,7 @@ goog.addDependency('../../core/workspace_comment_svg.js', ['Blockly.WorkspaceCom
goog.addDependency('../../core/workspace_drag_surface_svg.js', ['Blockly.WorkspaceDragSurfaceSvg'], ['Blockly.utils', 'Blockly.utils.dom'], {});
goog.addDependency('../../core/workspace_dragger.js', ['Blockly.WorkspaceDragger'], ['Blockly.utils.Coordinate'], {});
goog.addDependency('../../core/workspace_events.js', ['Blockly.Events.FinishedLoading'], ['Blockly.Events', 'Blockly.Events.Ui', 'Blockly.utils.object'], {'lang': 'es5'});
goog.addDependency('../../core/workspace_svg.js', ['Blockly.WorkspaceSvg'], ['Blockly.BlockSvg', 'Blockly.ConnectionDB', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Gesture', 'Blockly.Grid', 'Blockly.MarkerManager', 'Blockly.Msg', 'Blockly.Options', 'Blockly.ThemeManager', 'Blockly.Themes.Classic', 'Blockly.TouchGesture', 'Blockly.Workspace', 'Blockly.WorkspaceAudio', 'Blockly.WorkspaceDragSurfaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.constants', 'Blockly.navigation', 'Blockly.registry', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Metrics', 'Blockly.utils.Rect', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.toolbox'], {});
goog.addDependency('../../core/workspace_svg.js', ['Blockly.WorkspaceSvg'], ['Blockly.BlockSvg', 'Blockly.ConnectionDB', 'Blockly.ContextMenuRegistry', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Gesture', 'Blockly.Grid', 'Blockly.MarkerManager', 'Blockly.Msg', 'Blockly.Options', 'Blockly.ThemeManager', 'Blockly.Themes.Classic', 'Blockly.TouchGesture', 'Blockly.Workspace', 'Blockly.WorkspaceAudio', 'Blockly.WorkspaceDragSurfaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.constants', 'Blockly.navigation', 'Blockly.registry', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Metrics', 'Blockly.utils.Rect', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.toolbox'], {});
goog.addDependency('../../core/ws_comment_events.js', ['Blockly.Events.CommentBase', 'Blockly.Events.CommentChange', 'Blockly.Events.CommentCreate', 'Blockly.Events.CommentDelete', 'Blockly.Events.CommentMove'], ['Blockly.Events', 'Blockly.Events.Abstract', 'Blockly.utils.Coordinate', 'Blockly.utils.object', 'Blockly.utils.xml'], {});
goog.addDependency('../../core/xml.js', ['Blockly.Xml'], ['Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.FinishedLoading', 'Blockly.Events.VarCreate', 'Blockly.utils', 'Blockly.utils.dom', 'Blockly.utils.global', 'Blockly.utils.xml'], {});
goog.addDependency('../../core/zoom_controls.js', ['Blockly.ZoomControls'], ['Blockly.Css', 'Blockly.Scrollbar', 'Blockly.Touch', 'Blockly.utils.dom'], {'lang': 'es5'});

View File

@@ -89,7 +89,7 @@ Blockly.ContextMenu.populate_ = function(options, rtl) {
var actionHandler = function(_menuItem) {
var option = this;
Blockly.ContextMenu.hide();
option.callback();
option.callback(option.scope);
};
menuItem.onAction(actionHandler, option);
}

281
core/contextmenu_items.js Normal file
View File

@@ -0,0 +1,281 @@
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Registers default context menu items.
* @author maribethb@google.com (Maribeth Bottorff)
*/
'use strict';
/**
* @name Blockly.ContextMenuItems
* @namespace
*/
goog.provide('Blockly.ContextMenuItems');
goog.requireType('Blockly.BlockSvg');
/** Option to undo previous action. */
Blockly.ContextMenuItems.registerUndo = function() {
var undoOption = {};
undoOption.displayText = function() {
return Blockly.Msg['UNDO'];
};
undoOption.preconditionFn = function(scope) {
if (scope.workspace.undoStack_.length > 0) {
return 'enabled';
}
return 'disabled';
};
undoOption.callback = function(scope) {
scope.workspace.undo(false);
};
undoOption.scopeType = Blockly.ContextMenuRegistry.ScopeType.WORKSPACE;
undoOption.id = 'undoWorkspace';
undoOption.weight = 0;
Blockly.ContextMenuRegistry.registry.register(undoOption);
};
/** Option to redo previous action. */
Blockly.ContextMenuItems.registerRedo = function() {
var redoOption = {};
redoOption.displayText = function() { return Blockly.Msg['REDO']; };
redoOption.preconditionFn = function(scope) {
if (scope.workspace.redoStack_.length > 0) {
return 'enabled';
}
return 'disabled';
};
redoOption.callback = function(scope) {
scope.workspace.undo(true);
};
redoOption.scopeType = Blockly.ContextMenuRegistry.ScopeType.WORKSPACE;
redoOption.id = 'redoWorkspace';
redoOption.weight = 0;
Blockly.ContextMenuRegistry.registry.register(redoOption);
};
/** Option to clean up blocks. */
Blockly.ContextMenuItems.registerCleanup = function() {
var cleanOption = {};
cleanOption.displayText = function() {
return Blockly.Msg['CLEAN_UP'];
};
cleanOption.preconditionFn = function(scope) {
if (scope.workspace.isMovable()) {
if (scope.workspace.getTopBlocks(false).length > 1) {
return 'enabled';
}
return 'disabled';
}
return 'hidden';
};
cleanOption.callback = function(scope) {
scope.workspace.cleanUp();
};
cleanOption.scopeType = Blockly.ContextMenuRegistry.ScopeType.WORKSPACE;
cleanOption.id = 'cleanWorkspace';
cleanOption.weight = 0;
Blockly.ContextMenuRegistry.registry.register(cleanOption);
};
/**
* Creates a callback to collapse or expand top blocks.
* @param {boolean} shouldCollapse Whether a block should collapse.
* @param {!Array<Blockly.BlockSvg>} topBlocks Top blocks in the workspace.
* @private
*/
Blockly.ContextMenuItems.toggleOption_ = function(shouldCollapse, topBlocks) {
var DELAY = 10;
var ms = 0;
for (var i = 0; i < topBlocks.length; i++) {
var block = topBlocks[i];
while (block) {
setTimeout(block.setCollapsed.bind(block, shouldCollapse), ms);
block = block.getNextBlock();
ms += DELAY;
}
}
};
/** Option to collapse all blocks. */
Blockly.ContextMenuItems.registerCollapse = function() {
var collapseOption = {};
collapseOption.displayText = function() {
return Blockly.Msg['COLLAPSE_ALL'];
};
collapseOption.preconditionFn = function(scope) {
if (scope.workspace.options.collapse) {
var topBlocks = scope.workspace.getTopBlocks(false);
for (var i = 0; i < topBlocks.length; i++) {
var block = topBlocks[i];
while (block) {
if (!block.isCollapsed()) {
return 'enabled';
}
block = block.getNextBlock();
}
}
return 'disabled';
}
return 'hidden';
};
collapseOption.callback = function(scope) {
Blockly.ContextMenuItems.toggleOption_(true, scope.workspace.getTopBlocks(true));
};
collapseOption.scopeType = Blockly.ContextMenuRegistry.ScopeType.WORKSPACE;
collapseOption.id = 'collapseWorkspace';
collapseOption.weight = 0;
Blockly.ContextMenuRegistry.registry.register(collapseOption);
};
/** Option to expand all blocks. */
Blockly.ContextMenuItems.registerExpand = function() {
var expandOption = {};
expandOption.displayText = function() {
return Blockly.Msg['EXPAND_ALL'];
};
expandOption.preconditionFn = function(scope) {
if (scope.workspace.options.collapse) {
var topBlocks = scope.workspace.getTopBlocks(false);
for (var i = 0; i < topBlocks.length; i++) {
var block = topBlocks[i];
while (block) {
if (block.isCollapsed()) {
return 'enabled';
}
block = block.getNextBlock();
}
}
return 'disabled';
}
return 'hidden';
};
expandOption.callback = function(scope) {
Blockly.ContextMenuItems.toggleOption_(false, scope.workspace.getTopBlocks(true));
};
expandOption.scopeType = Blockly.ContextMenuRegistry.ScopeType.WORKSPACE;
expandOption.id = 'toggleWorkspace';
expandOption.weight = 0;
Blockly.ContextMenuRegistry.registry.register(expandOption);
};
/**
* Adds a block and its children to a list of deletable blocks.
* @param {!Blockly.BlockSvg} block to delete.
* @param {!Array.<!Blockly.BlockSvg>} deleteList list of blocks that can be deleted. This will be
* modifed in place with the given block and its descendants.
* @private
*/
Blockly.ContextMenuItems.addDeletableBlocks_ = function(block, deleteList) {
if (block.isDeletable()) {
Array.prototype.push.apply(deleteList, block.getDescendants(false));
} else {
var children = /** @type !Array.<!Blockly.BlockSvg> */ (block.getChildren(false));
for (var i = 0; i < children.length; i++) {
Blockly.ContextMenuItems.addDeletableBlocks_(children[i], deleteList);
}
}
};
/**
* Constructs a list of blocks that can be deleted in the given workspace.
* @param {!Blockly.WorkspaceSvg} workspace to delete all blocks from.
* @return {!Array.<!Blockly.BlockSvg>} list of blocks to delete.
* @private
*/
Blockly.ContextMenuItems.getDeletableBlocks_ = function(workspace) {
var deleteList = [];
var topBlocks = workspace.getTopBlocks(true);
for (var i = 0; i < topBlocks.length; i++) {
Blockly.ContextMenuItems.addDeletableBlocks_(topBlocks[i], deleteList);
}
return deleteList;
};
/** Deletes the given blocks. Used to delete all blocks in the workspace.
* @param {!Array.<!Blockly.BlockSvg>} deleteList list of blocks to delete.
* @param {string} eventGroup event group id with which all delete events should be associated.
* @private
*/
Blockly.ContextMenuItems.deleteNext_ = function(deleteList, eventGroup) {
var DELAY = 10;
Blockly.Events.setGroup(eventGroup);
var block = deleteList.shift();
if (block) {
if (block.workspace) {
block.dispose(false, true);
setTimeout(Blockly.ContextMenuItems.deleteNext_, DELAY, deleteList, eventGroup);
} else {
Blockly.ContextMenuItems.deleteNext_(deleteList, eventGroup);
}
}
Blockly.Events.setGroup(false);
};
/** Option to delete all blocks. */
Blockly.ContextMenuItems.registerDeleteAll = function() {
var deleteOption = {};
deleteOption.displayText = function(scope) {
var deletableBlocksLength =
Blockly.ContextMenuItems.getDeletableBlocks_(scope.workspace).length;
if (deletableBlocksLength == 1) {
return Blockly.Msg['DELETE_BLOCK'];
} else {
return Blockly.Msg['DELETE_X_BLOCKS'].replace('%1', String(deletableBlocksLength));
}
};
deleteOption.preconditionFn = function(scope) {
var deletableBlocksLength =
Blockly.ContextMenuItems.getDeletableBlocks_(scope.workspace).length;
return deletableBlocksLength > 0 ? 'enabled' : 'disabled';
};
deleteOption.callback = function(scope) {
if (scope.workspace.currentGesture_) {
scope.workspace.currentGesture_.cancel();
}
var deletableBlocks = Blockly.ContextMenuItems.getDeletableBlocks_(scope.workspace);
var eventGroup = Blockly.utils.genUid();
if (deletableBlocks.length < 2) {
Blockly.ContextMenuItems.deleteNext_(deletableBlocks, eventGroup);
} else {
Blockly.confirm(
Blockly.Msg['DELETE_ALL_BLOCKS'].replace('%1', deletableBlocks.length),
function(ok) {
if (ok) {
Blockly.ContextMenuItems.deleteNext_(deletableBlocks, eventGroup);
}
});
}
};
deleteOption.scopeType = Blockly.ContextMenuRegistry.ScopeType.WORKSPACE;
deleteOption.id = 'workspaceDelete';
deleteOption.weight = 0;
Blockly.ContextMenuRegistry.registry.register(deleteOption);
};
/**
* Registers all workspace-scoped context menu items.
* @private
*/
Blockly.ContextMenuItems.registerWorkspaceOptions_ = function() {
Blockly.ContextMenuItems.registerUndo();
Blockly.ContextMenuItems.registerRedo();
Blockly.ContextMenuItems.registerCleanup();
Blockly.ContextMenuItems.registerCollapse();
Blockly.ContextMenuItems.registerExpand();
Blockly.ContextMenuItems.registerDeleteAll();
};
/**
* Registers all default context menu items. This should be called once per instance of
* ContextMenuRegistry.
* @package
*/
Blockly.ContextMenuItems.registerDefaultOptions = function() {
Blockly.ContextMenuItems.registerWorkspaceOptions_();
};

View File

@@ -0,0 +1,165 @@
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Registry for context menu option items.
* @author maribethb@google.com (Maribeth Bottorff)
*/
'use strict';
/**
* @name Blockly.ContextMenuRegistry
* @namespace
*/
goog.provide('Blockly.ContextMenuRegistry');
goog.require('Blockly.ContextMenuItems');
/**
* Class for the registry of context menu items. This is intended to be a singleton. You should
* not create a new instance, and only access this class from Blockly.ContextMenuRegistry.registry.
* @constructor
*/
Blockly.ContextMenuRegistry = function() {
// Singleton instance should be registered once.
Blockly.ContextMenuRegistry.registry = this;
/**
* Registry of all registered RegistryItems, keyed by id.
* @type {!Object<string, Blockly.ContextMenuRegistry.RegistryItem>}
* @private
*/
this.registry_ = {};
Blockly.ContextMenuItems.registerDefaultOptions();
};
/**
* Where this menu item should be rendered. If the menu item should be rendered in multiple
* scopes, e.g. on both a block and a workspace, it should be registered for each scope.
* @enum {string}
*/
Blockly.ContextMenuRegistry.ScopeType = {
BLOCK: 'block',
WORKSPACE: 'workspace',
};
/**
* The actual workspace/block where the menu is being rendered. This is passed to callback and
* displayText functions that depend on this information.
* @typedef {{
* block: (Blockly.BlockSvg|undefined),
* workspace: (Blockly.WorkspaceSvg|undefined),
* }}
*/
Blockly.ContextMenuRegistry.Scope;
/**
* A menu item as entered in the registry.
* @typedef {{
* callback: function(!Blockly.ContextMenuRegistry.Scope),
* scopeType: !Blockly.ContextMenuRegistry.ScopeType,
* displayText: ((function(!Blockly.ContextMenuRegistry.Scope):string)|string),
* preconditionFn: function(!Blockly.ContextMenuRegistry.Scope):string,
* weight: number,
* id: string,
* }}
*/
Blockly.ContextMenuRegistry.RegistryItem;
/**
* A menu item as presented to contextmenu.js.
* @typedef {{
* text: string,
* enabled: boolean,
* callback: function(!Blockly.ContextMenuRegistry.Scope),
* scope: !Blockly.ContextMenuRegistry.Scope,
* weight: number,
* }}
*/
Blockly.ContextMenuRegistry.ContextMenuOption;
/**
* Singleton instance of this class. All interactions with this class should be done on this object.
* @type {?Blockly.ContextMenuRegistry}
*/
Blockly.ContextMenuRegistry.registry = null;
/**
* Registers a RegistryItem.
* @param {!Blockly.ContextMenuRegistry.RegistryItem} item Context menu item to register.
* @throws {Error} if an item with the given id already exists.
*/
Blockly.ContextMenuRegistry.prototype.register = function(item) {
if (this.registry_[item.id]) {
throw Error('Menu item with id "' + item.id + '" is already registered.');
}
this.registry_[item.id] = item;
};
/**
* Unregisters a RegistryItem with the given id.
* @param {string} id The id of the RegistryItem to remove.
* @throws {Error} if an item with the given id does not exist.
*/
Blockly.ContextMenuRegistry.prototype.unregister = function(id) {
if (this.registry_[id]) {
delete this.registry_[id];
} else {
throw new Error('Menu item with id "' + id + '" not found.');
}
};
/**
* @param {string} id The id of the RegistryItem to get.
* @returns {?Blockly.ContextMenuRegistry.RegistryItem} RegistryItem or null if not found
*/
Blockly.ContextMenuRegistry.prototype.getItem = function(id) {
if (this.registry_[id]) {
return this.registry_[id];
}
return null;
};
/**
* Gets the valid context menu options for the given scope type (e.g. block or workspace) and scope.
* Blocks are only shown if the preconditionFn shows they should not be hidden.
* @param {!Blockly.ContextMenuRegistry.ScopeType} scopeType Type of scope where menu should be
* shown (e.g. on a block or on a workspace)
* @param {!Blockly.ContextMenuRegistry.Scope} scope Current scope of context menu
* (i.e., the exact workspace or block being clicked on)
* @returns {!Array.<!Blockly.ContextMenuRegistry.ContextMenuOption>} the list of ContextMenuOptions
*/
Blockly.ContextMenuRegistry.prototype.getContextMenuOptions = function(scopeType, scope) {
var menuOptions = [];
var registry = this.registry_;
Object.keys(registry).forEach(function(id) {
var item = registry[id];
if (scopeType == item.scopeType) {
var precondition = item.preconditionFn(scope);
if (precondition != 'hidden') {
var displayText = typeof item.displayText == 'function' ?
item.displayText(scope) : item.displayText;
/** @type {!Blockly.ContextMenuRegistry.ContextMenuOption} */
var menuOption = {
text: displayText,
enabled: (precondition == 'enabled'),
callback: item.callback,
scope: scope,
weight: item.weight,
};
menuOptions.push(menuOption);
}
}
});
menuOptions.sort(function(a, b) {
return a.weight - b.weight;
});
return menuOptions;
};
// Creates and assigns the singleton instance.
new Blockly.ContextMenuRegistry();

View File

@@ -16,6 +16,7 @@ goog.require('Blockly.BlockSvg');
goog.require('Blockly.blockRendering');
goog.require('Blockly.ConnectionDB');
goog.require('Blockly.constants');
goog.require('Blockly.ContextMenuRegistry');
goog.require('Blockly.Events');
goog.require('Blockly.Events.BlockCreate');
goog.require('Blockly.Gesture');
@@ -1689,136 +1690,8 @@ Blockly.WorkspaceSvg.prototype.showContextMenu = function(e) {
if (this.options.readOnly || this.isFlyout) {
return;
}
var menuOptions = [];
var topBlocks = this.getTopBlocks(true);
var eventGroup = Blockly.utils.genUid();
var ws = this;
// Options to undo/redo previous action.
var undoOption = {};
undoOption.text = Blockly.Msg['UNDO'];
undoOption.enabled = this.undoStack_.length > 0;
undoOption.callback = this.undo.bind(this, false);
menuOptions.push(undoOption);
var redoOption = {};
redoOption.text = Blockly.Msg['REDO'];
redoOption.enabled = this.redoStack_.length > 0;
redoOption.callback = this.undo.bind(this, true);
menuOptions.push(redoOption);
// Option to clean up blocks.
if (this.isMovable()) {
var cleanOption = {};
cleanOption.text = Blockly.Msg['CLEAN_UP'];
cleanOption.enabled = topBlocks.length > 1;
cleanOption.callback = this.cleanUp.bind(this);
menuOptions.push(cleanOption);
}
// Add a little animation to collapsing and expanding.
var DELAY = 10;
if (this.options.collapse) {
var hasCollapsedBlocks = false;
var hasExpandedBlocks = false;
for (var i = 0; i < topBlocks.length; i++) {
var block = topBlocks[i];
while (block) {
if (block.isCollapsed()) {
hasCollapsedBlocks = true;
} else {
hasExpandedBlocks = true;
}
block = block.getNextBlock();
}
}
/**
* Option to collapse or expand top blocks.
* @param {boolean} shouldCollapse Whether a block should collapse.
* @private
*/
var toggleOption = function(shouldCollapse) {
var ms = 0;
for (var i = 0; i < topBlocks.length; i++) {
var block = topBlocks[i];
while (block) {
setTimeout(block.setCollapsed.bind(block, shouldCollapse), ms);
block = block.getNextBlock();
ms += DELAY;
}
}
};
// Option to collapse top blocks.
var collapseOption = {enabled: hasExpandedBlocks};
collapseOption.text = Blockly.Msg['COLLAPSE_ALL'];
collapseOption.callback = function() {
toggleOption(true);
};
menuOptions.push(collapseOption);
// Option to expand top blocks.
var expandOption = {enabled: hasCollapsedBlocks};
expandOption.text = Blockly.Msg['EXPAND_ALL'];
expandOption.callback = function() {
toggleOption(false);
};
menuOptions.push(expandOption);
}
// Option to delete all blocks.
// Count the number of blocks that are deletable.
var deleteList = [];
function addDeletableBlocks(block) {
if (block.isDeletable()) {
deleteList = deleteList.concat(block.getDescendants(false));
} else {
var children = block.getChildren(false);
for (var i = 0; i < children.length; i++) {
addDeletableBlocks(children[i]);
}
}
}
for (var i = 0; i < topBlocks.length; i++) {
addDeletableBlocks(topBlocks[i]);
}
function deleteNext() {
Blockly.Events.setGroup(eventGroup);
var block = deleteList.shift();
if (block) {
if (block.workspace) {
block.dispose(false, true);
setTimeout(deleteNext, DELAY);
} else {
deleteNext();
}
}
Blockly.Events.setGroup(false);
}
var deleteOption = {
text: deleteList.length == 1 ? Blockly.Msg['DELETE_BLOCK'] :
Blockly.Msg['DELETE_X_BLOCKS'].replace('%1', String(deleteList.length)),
enabled: deleteList.length > 0,
callback: function() {
if (ws.currentGesture_) {
ws.currentGesture_.cancel();
}
if (deleteList.length < 2 ) {
deleteNext();
} else {
Blockly.confirm(
Blockly.Msg['DELETE_ALL_BLOCKS'].replace('%1', deleteList.length),
function(ok) {
if (ok) {
deleteNext();
}
});
}
}
};
menuOptions.push(deleteOption);
var menuOptions = Blockly.ContextMenuRegistry.registry.getContextMenuOptions(
Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, {workspace: this});
// Allow the developer to add or modify menuOptions.
if (this.configureContextMenu) {