From bed54bae1ff7231da6fa614574b1ef91f16cd2ea Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Mon, 1 Apr 2019 13:10:19 -0700 Subject: [PATCH] Allow extensions to make buttons --- src/engine/runtime.js | 92 ++++++++++++++++------ src/extension-support/block-type.js | 5 ++ src/extension-support/extension-manager.js | 25 +++--- 3 files changed, 89 insertions(+), 33 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index c2e37f3c..f26d7cdb 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -123,16 +123,6 @@ const cloudDataManager = () => { }; }; -/** - * Predefined "Converted block info" for a separator between blocks in a block category - * @type {ConvertedBlockInfo} - */ -const ConvertedSeparator = { - info: {}, - json: null, - xml: '' -}; - /** * Numeric ID for Runtime._step in Profiler instances. * @type {number} @@ -866,22 +856,20 @@ class Runtime extends EventEmitter { } for (const blockInfo of extensionInfo.blocks) { - if (blockInfo === '---') { - categoryInfo.blocks.push(ConvertedSeparator); - continue; - } try { const convertedBlock = this._convertForScratchBlocks(blockInfo, categoryInfo); - const opcode = convertedBlock.json.type; categoryInfo.blocks.push(convertedBlock); - if (blockInfo.blockType !== BlockType.EVENT) { - this._primitives[opcode] = convertedBlock.info.func; - } - if (blockInfo.blockType === BlockType.EVENT || blockInfo.blockType === BlockType.HAT) { - this._hats[opcode] = { - edgeActivated: blockInfo.isEdgeActivated, - restartExistingThreads: blockInfo.shouldRestartExistingThreads - }; + if (convertedBlock.json) { + const opcode = convertedBlock.json.type; + if (blockInfo.blockType !== BlockType.EVENT) { + this._primitives[opcode] = convertedBlock.info.func; + } + if (blockInfo.blockType === BlockType.EVENT || blockInfo.blockType === BlockType.HAT) { + this._hats[opcode] = { + edgeActivated: blockInfo.isEdgeActivated, + restartExistingThreads: blockInfo.shouldRestartExistingThreads + }; + } } } catch (e) { log.error('Error parsing block: ', {block: blockInfo, error: e}); @@ -986,6 +974,25 @@ class Runtime extends EventEmitter { }; } + /** + * Convert ExtensionBlockMetadata into data ready for scratch-blocks. + * @param {ExtensionBlockMetadata} blockInfo - the block info to convert + * @param {CategoryInfo} categoryInfo - the category for this block + * @returns {ConvertedBlockInfo} - the converted & original block information + * @private + */ + _convertForScratchBlocks (blockInfo, categoryInfo) { + if (blockInfo === '---') { + return this._convertSeparatorForScratchBlocks(); + } + + if (blockInfo.blockType === BlockType.BUTTON) { + return this._convertButtonForScratchBlocks(blockInfo); + } + + return this._convertBlockForScratchBlocks(blockInfo, categoryInfo); + } + /** * Convert ExtensionBlockMetadata into scratch-blocks JSON & XML, and generate a proxy function. * @param {ExtensionBlockMetadata} blockInfo - the block to convert @@ -993,7 +1000,7 @@ class Runtime extends EventEmitter { * @returns {ConvertedBlockInfo} - the converted & original block information * @private */ - _convertForScratchBlocks (blockInfo, categoryInfo) { + _convertBlockForScratchBlocks (blockInfo, categoryInfo) { const extendedOpcode = `${categoryInfo.id}_${blockInfo.opcode}`; const blockJSON = { @@ -1135,6 +1142,43 @@ class Runtime extends EventEmitter { }; } + /** + * Generate a separator between blocks categories or sub-categories. + * @param {ExtensionBlockMetadata} blockInfo - the block to convert + * @param {CategoryInfo} categoryInfo - the category for this block + * @returns {ConvertedBlockInfo} - the converted & original block information + * @private + */ + _convertSeparatorForScratchBlocks (blockInfo) { + return { + info: blockInfo, + xml: '' + }; + } + + /** + * Convert a button for scratch-blocks. A button has no opcode but specifies a callback name in the `func` field. + * @param {ExtensionBlockMetadata} buttonInfo - the button to convert + * @property {string} func - the callback name + * @param {CategoryInfo} categoryInfo - the category for this button + * @returns {ConvertedBlockInfo} - the converted & original button information + * @private + */ + _convertButtonForScratchBlocks (buttonInfo) { + // for now we only support these pre-defined callbacks handled in scratch-blocks + const supportedCallbackKeys = ['CREATE_LIST', 'CREATE_PROCEDURE', 'CREATE_VARIABLE']; + if (supportedCallbackKeys.indexOf(buttonInfo.func) < 0) { + log.error(`Custom button callbacks not supported yet: ${buttonInfo.func}`); + } + + const extensionMessageContext = this.makeMessageContextForTarget(); + const buttonText = maybeFormatMessage(buttonInfo.text, extensionMessageContext); + return { + info: buttonInfo, + xml: `` + }; + } + /** * Helper for _convertForScratchBlocks which handles linearization of argument placeholders. Called as a callback * from string#replace. In addition to the return value the JSON and XML items in the context will be filled. diff --git a/src/extension-support/block-type.js b/src/extension-support/block-type.js index 3b38c051..e7f0b18d 100644 --- a/src/extension-support/block-type.js +++ b/src/extension-support/block-type.js @@ -8,6 +8,11 @@ const BlockType = { */ BOOLEAN: 'Boolean', + /** + * A button (not an actual block) for some special action, like making a variable + */ + BUTTON: 'button', + /** * Command block */ diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index 99fa83bf..30bc6d50 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -375,16 +375,24 @@ class ExtensionManager { blockAllThreads: false, arguments: {} }, blockInfo); - blockInfo.opcode = this._sanitizeID(blockInfo.opcode); + blockInfo.opcode = blockInfo.opcode && this._sanitizeID(blockInfo.opcode); blockInfo.text = blockInfo.text || blockInfo.opcode; - if (blockInfo.blockType !== BlockType.EVENT) { + switch (blockInfo.blockType) { + case BlockType.EVENT: + if (blockInfo.func) { + log.warn(`Ignoring function "${blockInfo.func}" for event block ${blockInfo.opcode}`); + } + break; + case BlockType.BUTTON: + if (blockInfo.opcode) { + log.warn(`Ignoring opcode "${blockInfo.opcode}" for button with text: ${blockInfo.text}`); + } + break; + default: blockInfo.func = blockInfo.func ? this._sanitizeID(blockInfo.func) : blockInfo.opcode; - /** - * This is only here because the VM performs poorly when blocks return promises. - * @TODO make it possible for the VM to resolve a promise and continue during the same Scratch "tick" - */ + // Avoid promise overhead if possible if (dispatch._isRemoteService(serviceName)) { blockInfo.func = dispatch.call.bind(dispatch, serviceName, blockInfo.func); } else { @@ -392,12 +400,11 @@ class ExtensionManager { const func = serviceObject[blockInfo.func]; if (func) { blockInfo.func = func.bind(serviceObject); - } else if (blockInfo.blockType !== BlockType.EVENT) { + } else { throw new Error(`Could not find extension block function called ${blockInfo.func}`); } } - } else if (blockInfo.func) { - log.warn(`Ignoring function "${blockInfo.func}" for event block ${blockInfo.opcode}`); + break; } return blockInfo;