From e4bbd451a37644696f144ca08b2922b8a80f1abe Mon Sep 17 00:00:00 2001 From: Maribeth Bottorff Date: Fri, 10 Jul 2020 19:11:48 -0700 Subject: [PATCH] Use Context Menu registry for block-level menu options (#4032) Use registry for block-level context menu items. --- core/block_svg.js | 84 +------ core/contextmenu.js | 92 ------- core/contextmenu_items.js | 501 ++++++++++++++++++++++++++++---------- core/workspace.js | 18 ++ 4 files changed, 398 insertions(+), 297 deletions(-) diff --git a/core/block_svg.js b/core/block_svg.js index 9cb76f0a6..ab2958368 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -17,6 +17,7 @@ goog.require('Blockly.Block'); goog.require('Blockly.blockAnimations'); goog.require('Blockly.blockRendering.IPathObject'); goog.require('Blockly.ContextMenu'); +goog.require('Blockly.ContextMenuRegistry'); goog.require('Blockly.Events'); goog.require('Blockly.Events.Ui'); goog.require('Blockly.Events.BlockMove'); @@ -716,87 +717,8 @@ Blockly.BlockSvg.prototype.generateContextMenu = function() { if (this.workspace.options.readOnly || !this.contextMenu) { return null; } - // Save the current block in a variable for use in closures. - var block = this; - var menuOptions = []; - - if (!this.isInFlyout) { - if (this.isDeletable() && this.isMovable()) { - menuOptions.push(Blockly.ContextMenu.blockDuplicateOption(block)); - } - - if (this.workspace.options.comments && !this.collapsed_ && - this.isEditable()) { - menuOptions.push(Blockly.ContextMenu.blockCommentOption(block)); - } - - if (this.isMovable()) { - if (!this.collapsed_) { - // Option to make block inline. - for (var i = 1; i < this.inputList.length; i++) { - if (this.inputList[i - 1].type != Blockly.NEXT_STATEMENT && - this.inputList[i].type != Blockly.NEXT_STATEMENT) { - // Only display this option if there are two value or dummy inputs - // next to each other. - var inlineOption = {enabled: true}; - var isInline = this.getInputsInline(); - inlineOption.text = isInline ? - Blockly.Msg['EXTERNAL_INPUTS'] : Blockly.Msg['INLINE_INPUTS']; - inlineOption.callback = function() { - block.setInputsInline(!isInline); - }; - menuOptions.push(inlineOption); - break; - } - } - // Option to collapse block - if (this.workspace.options.collapse) { - var collapseOption = {enabled: true}; - collapseOption.text = Blockly.Msg['COLLAPSE_BLOCK']; - collapseOption.callback = function() { - block.setCollapsed(true); - }; - menuOptions.push(collapseOption); - } - } else { - // Option to expand block. - if (this.workspace.options.collapse) { - var expandOption = {enabled: true}; - expandOption.text = Blockly.Msg['EXPAND_BLOCK']; - expandOption.callback = function() { - block.setCollapsed(false); - }; - menuOptions.push(expandOption); - } - } - } - - if (this.workspace.options.disable && this.isEditable()) { - // Option to disable/enable block. - var disableOption = { - text: this.isEnabled() ? - Blockly.Msg['DISABLE_BLOCK'] : Blockly.Msg['ENABLE_BLOCK'], - enabled: !this.getInheritedDisabled(), - callback: function() { - var group = Blockly.Events.getGroup(); - if (!group) { - Blockly.Events.setGroup(true); - } - block.setEnabled(!block.isEnabled()); - if (!group) { - Blockly.Events.setGroup(false); - } - } - }; - menuOptions.push(disableOption); - } - - if (this.isDeletable()) { - menuOptions.push(Blockly.ContextMenu.blockDeleteOption(block)); - } - } - - menuOptions.push(Blockly.ContextMenu.blockHelpOption(block)); + var menuOptions = Blockly.ContextMenuRegistry.registry.getContextMenuOptions( + Blockly.ContextMenuRegistry.ScopeType.BLOCK, {block: this}); // Allow the block to add or modify menuOptions. if (this.customContextMenu) { diff --git a/core/contextmenu.js b/core/contextmenu.js index ebac436e8..8cd454100 100644 --- a/core/contextmenu.js +++ b/core/contextmenu.js @@ -203,98 +203,6 @@ Blockly.ContextMenu.callbackFactory = function(block, xml) { // Helper functions for creating context menu options. -/** - * Make a context menu option for deleting the current block. - * @param {!Blockly.BlockSvg} block The block where the right-click originated. - * @return {!Object} A menu option, containing text, enabled, and a callback. - * @package - */ -Blockly.ContextMenu.blockDeleteOption = function(block) { - // Option to delete this block but not blocks lower in the stack. - // Count the number of blocks that are nested in this block. - var descendantCount = block.getDescendants(false).length; - var nextBlock = block.getNextBlock(); - if (nextBlock) { - // Blocks in the current stack would survive this block's deletion. - descendantCount -= nextBlock.getDescendants(false).length; - } - var deleteOption = { - text: descendantCount == 1 ? Blockly.Msg['DELETE_BLOCK'] : - Blockly.Msg['DELETE_X_BLOCKS'].replace('%1', String(descendantCount)), - enabled: true, - callback: function() { - Blockly.Events.setGroup(true); - block.dispose(true, true); - Blockly.Events.setGroup(false); - } - }; - return deleteOption; -}; - -/** - * Make a context menu option for showing help for the current block. - * @param {!Blockly.BlockSvg} block The block where the right-click originated. - * @return {!Object} A menu option, containing text, enabled, and a callback. - * @package - */ -Blockly.ContextMenu.blockHelpOption = function(block) { - var url = (typeof block.helpUrl == 'function') ? - block.helpUrl() : block.helpUrl; - var helpOption = { - enabled: !!url, - text: Blockly.Msg['HELP'], - callback: function() { - block.showHelp(); - } - }; - return helpOption; -}; - -/** - * Make a context menu option for duplicating the current block. - * @param {!Blockly.BlockSvg} block The block where the right-click originated. - * @return {!Object} A menu option, containing text, enabled, and a callback. - * @package - */ -Blockly.ContextMenu.blockDuplicateOption = function(block) { - var enabled = block.isDuplicatable(); - var duplicateOption = { - text: Blockly.Msg['DUPLICATE_BLOCK'], - enabled: enabled, - callback: function() { - Blockly.duplicate(block); - } - }; - return duplicateOption; -}; - -/** - * Make a context menu option for adding or removing comments on the current - * block. - * @param {!Blockly.BlockSvg} block The block where the right-click originated. - * @return {!Object} A menu option, containing text, enabled, and a callback. - * @package - */ -Blockly.ContextMenu.blockCommentOption = function(block) { - var commentOption = { - enabled: !Blockly.utils.userAgent.IE - }; - // If there's already a comment, add an option to delete it. - if (block.getCommentIcon()) { - commentOption.text = Blockly.Msg['REMOVE_COMMENT']; - commentOption.callback = function() { - block.setCommentText(null); - }; - } else { - // If there's no comment, add an option to create a comment. - commentOption.text = Blockly.Msg['ADD_COMMENT']; - commentOption.callback = function() { - block.setCommentText(''); - }; - } - return commentOption; -}; - /** * Make a context menu option for deleting the current workspace comment. * @param {!Blockly.WorkspaceCommentSvg} comment The workspace comment where the diff --git a/core/contextmenu_items.js b/core/contextmenu_items.js index 13532d5dd..26228ca56 100644 --- a/core/contextmenu_items.js +++ b/core/contextmenu_items.js @@ -20,65 +20,71 @@ goog.requireType('Blockly.BlockSvg'); /** Option to undo previous action. */ Blockly.ContextMenuItems.registerUndo = function() { - var undoOption = {}; - undoOption.displayText = function() { - return Blockly.Msg['UNDO']; + /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ + var undoOption = { + displayText: function() { + return Blockly.Msg['UNDO']; + }, + preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + if (scope.workspace.getUndoStack().length > 0) { + return 'enabled'; + } + return 'disabled'; + }, + callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + scope.workspace.undo(false); + }, + scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, + id: 'undoWorkspace', + weight: 0, }; - 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'; + /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ + var redoOption = { + displayText: function() { return Blockly.Msg['REDO']; }, + preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + if (scope.workspace.getRedoStack().length > 0) { + return 'enabled'; + } + return 'disabled'; + }, + callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + scope.workspace.undo(true); + }, + scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, + id: 'redoWorkspace', + weight: 0, }; - 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'; + /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ + var cleanOption = { + displayText: function() { + return Blockly.Msg['CLEAN_UP']; + }, + preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + if (scope.workspace.isMovable()) { + if (scope.workspace.getTopBlocks(false).length > 1) { + return 'enabled'; + } + return 'disabled'; } - return 'disabled'; - } - return 'hidden'; + return 'hidden'; + }, + callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + scope.workspace.cleanUp(); + }, + scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, + id: 'cleanWorkspace', + weight: 0, }; - cleanOption.callback = function(scope) { - scope.workspace.cleanUp(); - }; - cleanOption.scopeType = Blockly.ContextMenuRegistry.ScopeType.WORKSPACE; - cleanOption.id = 'cleanWorkspace'; - cleanOption.weight = 0; Blockly.ContextMenuRegistry.registry.register(cleanOption); }; @@ -103,63 +109,67 @@ Blockly.ContextMenuItems.toggleOption_ = function(shouldCollapse, topBlocks) { /** 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'; + /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ + var collapseOption = { + displayText: function() { + return Blockly.Msg['COLLAPSE_ALL']; + }, + preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ 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(); } - block = block.getNextBlock(); } + return 'disabled'; } - return 'disabled'; - } - return 'hidden'; + return 'hidden'; + }, + callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + Blockly.ContextMenuItems.toggleOption_(true, scope.workspace.getTopBlocks(true)); + }, + scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, + id: 'collapseWorkspace', + weight: 0, }; - 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'; + /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ + var expandOption = { + displayText: function() { + return Blockly.Msg['EXPAND_ALL']; + }, + preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ 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(); } - block = block.getNextBlock(); } + return 'disabled'; } - return 'disabled'; - } - return 'hidden'; + return 'hidden'; + }, + callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + Blockly.ContextMenuItems.toggleOption_(false, scope.workspace.getTopBlocks(true)); + }, + scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, + id: 'toggleWorkspace', + weight: 0, }; - 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); }; @@ -218,42 +228,51 @@ Blockly.ContextMenuItems.deleteNext_ = function(deleteList, eventGroup) { /** 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)); - } + /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ + var deleteOption = { + displayText: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + if (!scope.workspace) { + return; + } + 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)); + } + }, + preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + if (!scope.workspace) { + return; + } + var deletableBlocksLength = + Blockly.ContextMenuItems.getDeletableBlocks_(scope.workspace).length; + return deletableBlocksLength > 0 ? 'enabled' : 'disabled'; + }, + callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + if (!scope.workspace) { + return; + } + scope.workspace.cancelCurrentGesture(); + 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); + } + }); + } + }, + scopeType: Blockly.ContextMenuRegistry.ScopeType.WORKSPACE, + id: 'workspaceDelete', + weight: 0, }; - 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); }; @@ -270,6 +289,239 @@ Blockly.ContextMenuItems.registerWorkspaceOptions_ = function() { Blockly.ContextMenuItems.registerDeleteAll(); }; +/** Option to duplicate a block. */ +Blockly.ContextMenuItems.registerDuplicate = function() { + /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ + var duplicateOption = { + displayText: function() { + return Blockly.Msg['DUPLICATE_BLOCK']; + }, + preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + var block = scope.block; + if (!block.isInFlyout && block.isDeletable() && block.isMovable()) { + if (block.isDuplicatable()) { + return 'enabled'; + } + return 'disabled'; + } + return 'hidden'; + }, + callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + if (scope.block) { + Blockly.duplicate(scope.block); + } + }, + scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, + id: 'blockDuplicate', + weight: 0, + }; + Blockly.ContextMenuRegistry.registry.register(duplicateOption); +}; + +/** Option to add or remove block-level comment. */ +Blockly.ContextMenuItems.registerComment = function() { + /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ + var commentOption = { + displayText: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + if (scope.block.getCommentIcon()) { + // If there's already a comment, option is to remove. + return Blockly.Msg['REMOVE_COMMENT']; + } + // If there's no comment yet, option is to add. + return Blockly.Msg['ADD_COMMENT']; + }, + preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + var block = scope.block; + // IE doesn't support necessary features for comment editing. + if (!Blockly.utils.userAgent.IE && !block.isInFlyout && block.workspace.options.comments && + !block.isCollapsed() && block.isEditable()) { + return 'enabled'; + } + return 'hidden'; + }, + callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + var block = scope.block; + if (block.getCommentIcon()) { + block.setCommentText(null); + } else { + block.setCommentText(''); + } + }, + scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, + id: 'blockComment', + weight: 0, + }; + Blockly.ContextMenuRegistry.registry.register(commentOption); +}; + +/** Option to inline variables. */ +Blockly.ContextMenuItems.registerInline = function() { + /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ + var inlineOption = { + displayText: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + return (scope.block.getInputsInline()) ? + Blockly.Msg['EXTERNAL_INPUTS'] : Blockly.Msg['INLINE_INPUTS']; + }, + preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + var block = scope.block; + if (!block.isInFlyout && block.isMovable() && !block.isCollapsed()) { + for (var i = 1; i < block.inputList.length; i++) { + // Only display this option if there are two value or dummy inputs next to each other. + if (block.inputList[i - 1].type != Blockly.NEXT_STATEMENT && + block.inputList[i].type != Blockly.NEXT_STATEMENT) { + return 'enabled'; + } + } + } + return 'hidden'; + }, + callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + scope.block.setInputsInline(!scope.block.getInputsInline()); + }, + scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, + id: 'blockInline', + weight: 0, + }; + Blockly.ContextMenuRegistry.registry.register(inlineOption); +}; + +/** Option to collapse or expand a block. */ +Blockly.ContextMenuItems.registerCollapseExpandBlock = function() { + /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ + var collapseExpandOption = { + displayText: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + if (scope.block.isCollapsed()) { + return Blockly.Msg['EXPAND_BLOCK']; + } + return Blockly.Msg['COLLAPSE_BLOCK']; + }, + preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + var block = scope.block; + if (!block.isInFlyout && block.isMovable()) { + return 'enabled'; + } + return 'hidden'; + }, + callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + scope.block.setCollapsed(!scope.block.isCollapsed()); + }, + scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, + id: 'blockCollapseExpand', + weight: 0, + }; + Blockly.ContextMenuRegistry.registry.register(collapseExpandOption); +}; + +/** Option to disable or enable a block. */ +Blockly.ContextMenuItems.registerDisable = function() { + /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ + var disableOption = { + displayText: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + return (scope.block.isEnabled()) ? + Blockly.Msg['DISABLE_BLOCK'] : Blockly.Msg['ENABLE_BLOCK']; + }, + preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + var block = scope.block; + if (!block.isInFlyout && block.workspace.options.disable && block.isEditable()) { + if (block.getInheritedDisabled()) { + return 'disabled'; + } + return 'enabled'; + } + return 'hidden'; + }, + callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + var block = scope.block; + var group = Blockly.Events.getGroup(); + if (!group) { + Blockly.Events.setGroup(true); + } + block.setEnabled(!block.isEnabled()); + if (!group) { + Blockly.Events.setGroup(false); + } + }, + scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, + id: 'blockDisable', + weight: 0, + }; + Blockly.ContextMenuRegistry.registry.register(disableOption); +}; + +/** Option to delete a block. */ +Blockly.ContextMenuItems.registerDelete = function() { + /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ + var deleteOption = { + displayText: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + var block = scope.block; + // Count the number of blocks that are nested in this block. + var descendantCount = block.getDescendants(false).length; + var nextBlock = block.getNextBlock(); + if (nextBlock) { + // Blocks in the current stack would survive this block's deletion. + descendantCount -= nextBlock.getDescendants(false).length; + } + return (descendantCount == 1) ? Blockly.Msg['DELETE_BLOCK'] : + Blockly.Msg['DELETE_X_BLOCKS'].replace('%1', String(descendantCount)); + }, + preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + if (!scope.block.isInFlyout && scope.block.isDeletable()) { + return 'enabled'; + } + return 'hidden'; + }, + callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + Blockly.Events.setGroup(true); + scope.block.dispose(true, true); + Blockly.Events.setGroup(false); + }, + scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, + id: 'blockDelete', + weight: 0, + }; + Blockly.ContextMenuRegistry.registry.register(deleteOption); +}; + +/** Option to open help for a block. */ +Blockly.ContextMenuItems.registerHelp = function() { + /** @type {!Blockly.ContextMenuRegistry.RegistryItem} */ + var helpOption = { + displayText: function() { + return Blockly.Msg['HELP']; + }, + preconditionFn: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + var block = scope.block; + var url = (typeof block.helpUrl == 'function') ? + block.helpUrl() : block.helpUrl; + if (url) { + return 'enabled'; + } + return 'hidden'; + }, + callback: function(/** @type {!Blockly.ContextMenuRegistry.Scope} */ scope) { + scope.block.showHelp(); + }, + scopeType: Blockly.ContextMenuRegistry.ScopeType.BLOCK, + id: 'blockHelp', + weight: 0, + }; + Blockly.ContextMenuRegistry.registry.register(helpOption); +}; + +/** + * Registers all block-scoped context menu items. + * @private + */ +Blockly.ContextMenuItems.registerBlockOptions_ = function() { + Blockly.ContextMenuItems.registerDuplicate(); + Blockly.ContextMenuItems.registerComment(); + Blockly.ContextMenuItems.registerInline(); + Blockly.ContextMenuItems.registerCollapseExpandBlock(); + Blockly.ContextMenuItems.registerDisable(); + Blockly.ContextMenuItems.registerDelete(); + Blockly.ContextMenuItems.registerHelp(); +}; + /** * Registers all default context menu items. This should be called once per instance of * ContextMenuRegistry. @@ -277,5 +529,6 @@ Blockly.ContextMenuItems.registerWorkspaceOptions_ = function() { */ Blockly.ContextMenuItems.registerDefaultOptions = function() { Blockly.ContextMenuItems.registerWorkspaceOptions_(); + Blockly.ContextMenuItems.registerBlockOptions_(); }; diff --git a/core/workspace.js b/core/workspace.js index 591fbcdf3..2c6d2ece3 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -558,6 +558,24 @@ Blockly.Workspace.prototype.hasBlockLimits = function() { return this.options.maxBlocks != Infinity || !!this.options.maxInstances; }; +/** + * Gets the undo stack for workplace. + * @return {!Array.} undo stack + * @package + */ +Blockly.Workspace.prototype.getUndoStack = function() { + return this.undoStack_; +}; + +/** + * Gets the redo stack for workplace. + * @return {!Array.} redo stack + * @package + */ +Blockly.Workspace.prototype.getRedoStack = function() { + return this.redoStack_; +}; + /** * Undo or redo the previous action. * @param {boolean} redo False if undo, true if redo.