From c1681e54d50089677ff0856c7bde08559b76b83e Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Fri, 6 Oct 2017 13:37:59 -0700 Subject: [PATCH] Implement drop-down menus for extension blocks Also, add `ANGLE` argument type (like `NUMBER` but adds an angle picker) --- src/engine/runtime.js | 83 ++++++++++++++++++++-- src/extension-support/argument-type.js | 1 + src/extension-support/extension-manager.js | 2 +- 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 6d8ce1dd1..85babe5d9 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -33,6 +33,10 @@ const defaultBlockPackages = { */ const ArgumentTypeMap = (() => { const map = {}; + map[ArgumentType.ANGLE] = { + shadowType: 'math_angle', + fieldType: 'NUM' + }; map[ArgumentType.COLOR] = { shadowType: 'colour_picker' }; @@ -388,6 +392,18 @@ class Runtime extends EventEmitter { } } + /** + * Generate an extension-specific menu ID. + * @param {string} menuName - the name of the menu. + * @param {string} extensionId - the ID of the extension hosting the menu. + * @returns {string} - the constructed ID. + * @private + */ + _makeExtensionMenuId (menuName, extensionId) { + /** @TODO: protect against XML characters in menu name */ + return `${extensionId}.menu.${menuName}`; + } + /** * Register the primitives provided by an extension. * @param {ExtensionInfo} extensionInfo - information about the extension (id, blocks, etc.) @@ -400,11 +416,19 @@ class Runtime extends EventEmitter { color1: '#FF6680', color2: '#FF4D6A', color3: '#FF3355', - blocks: [] + blocks: [], + menus: [] }; this._blockInfo.push(categoryInfo); + for (const menuName in extensionInfo.menus) { + if (extensionInfo.menus.hasOwnProperty(menuName)) { + const menuItems = extensionInfo.menus[menuName]; + const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuItems, extensionInfo); + categoryInfo.menus.push(convertedMenu); + } + } for (const blockInfo of extensionInfo.blocks) { const convertedBlock = this._convertForScratchBlocks(blockInfo, categoryInfo); const opcode = convertedBlock.json.type; @@ -412,7 +436,51 @@ class Runtime extends EventEmitter { this._primitives[opcode] = convertedBlock.info.func; } - this.emit(Runtime.EXTENSION_ADDED, categoryInfo.blocks); + this.emit(Runtime.EXTENSION_ADDED, categoryInfo.blocks.concat(categoryInfo.menus)); + } + + /** + * Build the scratch-blocks JSON for a menu. Note that scratch-blocks treats menus as a special kind of block. + * @param {string} menuName - the name of the menu + * @param {array|string} menuItems - the list of menu items, or the name of an extension method to collect them. + * @param {CategoryInfo} categoryInfo - the category for this block + * @returns {object} - a JSON-esque object ready for scratch-blocks' consumption + * @private + */ + _buildMenuForScratchBlocks (menuName, menuItems, categoryInfo) { + const menuId = this._makeExtensionMenuId(menuName, categoryInfo.id); + + /** @TODO: support dynamic menus when 'menuItems' is a method name string (see extension spec) */ + const options = menuItems.map(item => { + switch (typeof item) { + case 'string': + return [item, item]; + case 'object': + return [item.text, item.value]; + default: + throw new Error(`Can't interpret menu item: ${item}`); + } + }); + + return { + json: { + message0: '%1', + type: menuId, + inputsInline: true, + output: 'String', + colour: categoryInfo.color1, + colourSecondary: categoryInfo.color2, + colourTertiary: categoryInfo.color3, + outputShape: ScratchBlocksConstants.OUTPUT_SHAPE_ROUND, + args0: [ + { + type: 'field_dropdown', + name: menuName, + options: options + } + ] + } + }; } /** @@ -460,14 +528,19 @@ class Runtime extends EventEmitter { const argTypeInfo = ArgumentTypeMap[argInfo.type] || {}; const defaultValue = (typeof argInfo.defaultValue === 'undefined' ? '' : argInfo.defaultValue.toString()); + const shadowType = (argInfo.menu ? + this._makeExtensionMenuId(argInfo.menu, categoryInfo.id) : + argTypeInfo.shadowType); + const fieldType = argInfo.menu || argTypeInfo.fieldType; + // is the ScratchBlocks name for a block input. // The is a placeholder for a reporter and is visible when there's no reporter in this input. - inputList.push(``); + inputList.push(``); // is a text field that the user can type into. Some shadows, like the color picker, don't allow // text input and therefore don't need a field element. - if (argTypeInfo.fieldType) { - inputList.push(`${defaultValue}`); + if (fieldType) { + inputList.push(`${defaultValue}`); } inputList.push(''); diff --git a/src/extension-support/argument-type.js b/src/extension-support/argument-type.js index 70381c84b..0589e9fb6 100644 --- a/src/extension-support/argument-type.js +++ b/src/extension-support/argument-type.js @@ -1,4 +1,5 @@ const ArgumentType = { + ANGLE: 'angle', BOOLEAN: 'Boolean', COLOR: 'color', NUMBER: 'number', diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index 827bc491d..55ac9fdb7 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -151,7 +151,7 @@ class ExtensionManager { result.push(this._prepareBlockInfo(serviceName, blockInfo)); } catch (e) { // TODO: more meaningful error reporting - log.error(`Skipping malformed block: ${JSON.stringify(e)}`); + log.error(`Error processing block: ${e.message}, Block:\n${JSON.stringify(blockInfo)}`); } return result; }, []);