diff --git a/docs/extensions.md b/docs/extensions.md index f2a8779ac..add901889 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -120,6 +120,124 @@ class SomeBlocks { } ``` +### Defining a Menu + +To display a drop-down menu for a block argument, specify the `menu` property of that argument and a matching item in +the `menus` section of your extension's definition: + +```js +return { + // ... + blocks: [ + { + // ... + arguments: { + FOO: { + type: ArgumentType.NUMBER, + menu: 'fooMenu' + } + } + } + ], + menus: { + fooMenu: { + items: ['a', 'b', 'c'] + } + } +} +``` + +The items in a menu may be specified with an array or with the name of a function which returns an array. The two +simplest forms for menu definitions are: + +```js +getInfo () { + return { + menus: { + staticMenu: ['static 1', 'static 2', 'static 3'], + dynamicMenu: 'getDynamicMenuItems' + } + }; +} +// this member function will be called each time the menu opens +getDynamicMenuItems () { + return ['dynamic 1', 'dynamic 2', 'dynamic 3']; +} +``` + +The examples above are shorthand for these equivalent definitions: + +```js +getInfo () { + return { + menus: { + staticMenu: { + items: ['static 1', 'static 2', 'static 3'] + }, + dynamicMenu: { + items: 'getDynamicMenuItems' + } + } + }; +} +// this member function will be called each time the menu opens +getDynamicMenuItems () { + return ['dynamic 1', 'dynamic 2', 'dynamic 3']; +} +``` + +If a menu item needs a label that doesn't match its value -- for example, if the label needs to be displayed in the +user's language but the value needs to stay constant -- the menu item may be an object instead of a string. This works +for both static and dynamic menu items: + +```js +menus: { + staticMenu: [ + { + text: formatMessage(/* ... */), + value: 42 + } + ] +} +``` + +#### Accepting reporters ("droppable" menus) + +By default it is not possible to specify the value of a dropdown menu by inserting a reporter block. While we +encourage extension authors to make their menus accept reporters when possible, doing so requires careful +consideration to avoid confusion and frustration on the part of those using the extension. + +A few of these considerations include: + +* The valid values for the menu should not change when the user changes the Scratch language setting. + * In particular, changing languages should never break a working project. +* The average Scratch user should be able to figure out the valid values for this input without referring to extension + documentation. + * One way to ensure this is to make an item's text match or include the item's value. For example, the official Music + extension contains menu items with names like "(1) Piano" with value 1, "(8) Cello" with value 8, and so on. +* The block should accept any value as input, even "invalid" values. + * Scratch has no concept of a runtime error! + * For a command block, sometimes the best option is to do nothing. + * For a reporter, returning zero or the empty string might make sense. +* The block should be forgiving in its interpretation of inputs. + * For example, if the block expects a string and receives a number it may make sense to interpret the number as a + string instead of treating it as invalid input. + +The `acceptReporters` flag indicates that the user can drop a reporter onto the menu input: + +```js +menus: { + staticMenu: { + acceptReporters: true, + items: [/*...*/] + }, + dynamicMenu: { + acceptReporters: true, + items: 'getDynamicMenuItems' + } +} +``` + ## Annotated Example ```js @@ -311,7 +429,16 @@ class SomeBlocks { // Dynamic menu: returns an array as above. // Called each time the menu is opened. - menuB: 'getItemsForMenuB' + menuB: 'getItemsForMenuB', + + // The examples above are shorthand for setting only the `items` property in this full form: + menuC: { + // This flag makes a "droppable" menu: the menu will allow dropping a reporter in for the input. + acceptReporters: true, + + // The `item` property may be an array or function name as in previous menu examples. + items: [/*...*/] || 'getItemsForMenuC' + } }, // Optional: translations (UNSTABLE - NOT YET SUPPORTED) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 792455c91..a3976e0e3 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -786,10 +786,7 @@ class Runtime extends EventEmitter { name: maybeFormatMessage(extensionInfo.name), showStatusButton: extensionInfo.showStatusButton, blockIconURI: extensionInfo.blockIconURI, - menuIconURI: extensionInfo.menuIconURI, - customFieldTypes: {}, - blocks: [], - menus: [] + menuIconURI: extensionInfo.menuIconURI }; if (extensionInfo.color1) { @@ -830,8 +827,6 @@ class Runtime extends EventEmitter { const categoryInfo = this._blockInfo.find(info => info.id === extensionInfo.id); if (categoryInfo) { categoryInfo.name = maybeFormatMessage(extensionInfo.name); - categoryInfo.blocks = []; - categoryInfo.menus = []; this._fillExtensionCategory(categoryInfo, extensionInfo); this.emit(Runtime.BLOCKSINFO_UPDATE, categoryInfo); @@ -839,18 +834,24 @@ class Runtime extends EventEmitter { } /** - * Read extension information, convert menus, blocks and custom field types + * Read extension information, convert menus, blocks and custom field types * and store the results in the provided category object. * @param {CategoryInfo} categoryInfo - the category to be filled * @param {ExtensionMetadata} extensionInfo - the extension metadata to read * @private */ _fillExtensionCategory (categoryInfo, extensionInfo) { + categoryInfo.blocks = []; + categoryInfo.customFieldTypes = {}; + categoryInfo.menus = []; + categoryInfo.menuInfo = {}; + for (const menuName in extensionInfo.menus) { if (extensionInfo.menus.hasOwnProperty(menuName)) { - const menuItems = extensionInfo.menus[menuName]; - const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuItems, categoryInfo); + const menuInfo = extensionInfo.menus[menuName]; + const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuInfo, categoryInfo); categoryInfo.menus.push(convertedMenu); + categoryInfo.menuInfo[menuName] = menuInfo; } } for (const fieldTypeName in extensionInfo.customFieldTypes) { @@ -890,21 +891,16 @@ class Runtime extends EventEmitter { } /** - * 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} menuItems - the list of items for this menu - * @param {CategoryInfo} categoryInfo - the category for this block - * @returns {object} - a JSON-esque object ready for scratch-blocks' consumption + * Convert the given extension menu items into the scratch-blocks style of list of pairs. + * If the menu is dynamic (e.g. the passed in argument is a function), return the input unmodified. + * @param {object} menuItems - an array of menu items or a function to retrieve such an array + * @returns {object} - an array of 2 element arrays or the original input function * @private */ - _buildMenuForScratchBlocks (menuName, menuItems, categoryInfo) { - const menuId = this._makeExtensionMenuId(menuName, categoryInfo.id); - let options = null; - if (typeof menuItems === 'function') { - options = menuItems; - } else { + _convertMenuItems (menuItems) { + if (typeof menuItems !== 'function') { const extensionMessageContext = this.makeMessageContextForTarget(); - options = menuItems.map(item => { + return menuItems.map(item => { const formattedItem = maybeFormatMessage(item, extensionMessageContext); switch (typeof formattedItem) { case 'string': @@ -916,6 +912,22 @@ class Runtime extends EventEmitter { } }); } + return menuItems; + } + + /** + * 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 {object} menuInfo - a description of this menu and its items + * @property {*} items - an array of menu items or a function to retrieve such an array + * @property {boolean} [acceptReporters] - if true, allow dropping reporters onto this menu + * @param {CategoryInfo} categoryInfo - the category for this block + * @returns {object} - a JSON-esque object ready for scratch-blocks' consumption + * @private + */ + _buildMenuForScratchBlocks (menuName, menuInfo, categoryInfo) { + const menuId = this._makeExtensionMenuId(menuName, categoryInfo.id); + const menuItems = this._convertMenuItems(menuInfo.items); return { json: { message0: '%1', @@ -925,12 +937,13 @@ class Runtime extends EventEmitter { colour: categoryInfo.color1, colourSecondary: categoryInfo.color2, colourTertiary: categoryInfo.color3, - outputShape: ScratchBlocksConstants.OUTPUT_SHAPE_ROUND, + outputShape: menuInfo.acceptReporters ? + ScratchBlocksConstants.OUTPUT_SHAPE_ROUND : ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE, args0: [ { type: 'field_dropdown', name: menuName, - options: options + options: menuItems } ] } @@ -1227,29 +1240,51 @@ class Runtime extends EventEmitter { argJSON.check = argTypeInfo.check; } - const shadowType = (argInfo.menu ? - this._makeExtensionMenuId(argInfo.menu, context.categoryInfo.id) : - argTypeInfo.shadowType); - const fieldType = argInfo.menu || argTypeInfo.fieldType; + let valueName; + let shadowType; + let fieldName; + if (argInfo.menu) { + const menuInfo = context.categoryInfo.menuInfo[argInfo.menu]; + if (menuInfo.acceptReporters) { + valueName = placeholder; + shadowType = this._makeExtensionMenuId(argInfo.menu, context.categoryInfo.id); + fieldName = argInfo.menu; + } else { + argJSON.type = 'field_dropdown'; + argJSON.options = this._convertMenuItems(menuInfo.items); + valueName = null; + shadowType = null; + fieldName = placeholder; + } + } else { + valueName = placeholder; + shadowType = argTypeInfo.shadowType; + fieldName = argTypeInfo.fieldType; + } // is the ScratchBlocks name for a block input. - context.inputList.push(``); + if (valueName) { + context.inputList.push(``); + } // The is a placeholder for a reporter and is visible when there's no reporter in this input. // Boolean inputs don't need to specify a shadow in the XML. if (shadowType) { context.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 (fieldType) { - context.inputList.push(`${defaultValue}`); - } + // A displays a dynamic value: a user-editable text field, a drop-down menu, etc. + if (fieldName) { + context.inputList.push(`${defaultValue}`); + } + if (shadowType) { context.inputList.push(''); } - context.inputList.push(''); + if (valueName) { + context.inputList.push(''); + } const argsName = `args${context.outLineNum}`; const blockArgs = (context.blockJSON[argsName] = context.blockJSON[argsName] || []); diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index 71eb5f126..7cb556c5d 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -294,7 +294,7 @@ class ExtensionManager { } return results; }, []); - extensionInfo.menus = extensionInfo.menus || []; + extensionInfo.menus = extensionInfo.menus || {}; extensionInfo.menus = this._prepareMenuInfo(serviceName, extensionInfo.menus); return extensionInfo; } @@ -309,15 +309,24 @@ class ExtensionManager { _prepareMenuInfo (serviceName, menus) { const menuNames = Object.getOwnPropertyNames(menus); for (let i = 0; i < menuNames.length; i++) { - const item = menuNames[i]; - // If the value is a string, it should be the name of a function in the - // extension object to call to populate the menu whenever it is opened. - // Set up the binding for the function object here so - // we can use it later when converting the menu for Scratch Blocks. - if (typeof menus[item] === 'string') { + const menuName = menuNames[i]; + let menuInfo = menus[menuName]; + + // If the menu description is in short form (items only) then normalize it to general form: an object with + // its items listed in an `items` property. + if (!menuInfo.items) { + menuInfo = { + items: menuInfo + }; + menus[menuName] = menuInfo; + } + // If `items` is a string, it should be the name of a function in the extension object. Calling the + // function should return an array of items to populate the menu when it is opened. + if (typeof menuInfo.items === 'string') { + const menuItemFunctionName = menuInfo.items; const serviceObject = dispatch.services[serviceName]; - const menuName = menus[item]; - menus[item] = this._getExtensionMenuItems.bind(this, serviceObject, menuName); + // Bind the function here so we can pass a simple item generation function to Scratch Blocks later. + menuInfo.items = this._getExtensionMenuItems.bind(this, serviceObject, menuItemFunctionName); } } return menus; @@ -326,11 +335,11 @@ class ExtensionManager { /** * Fetch the items for a particular extension menu, providing the target ID for context. * @param {object} extensionObject - the extension object providing the menu. - * @param {string} menuName - the name of the menu function to call. + * @param {string} menuItemFunctionName - the name of the menu function to call. * @returns {Array} menu items ready for scratch-blocks. * @private */ - _getExtensionMenuItems (extensionObject, menuName) { + _getExtensionMenuItems (extensionObject, menuItemFunctionName) { // Fetch the items appropriate for the target currently being edited. This assumes that menus only // collect items when opened by the user while editing a particular target. const editingTarget = this.runtime.getEditingTarget() || this.runtime.getTargetForStage(); @@ -338,7 +347,7 @@ class ExtensionManager { const extensionMessageContext = this.runtime.makeMessageContextForTarget(editingTarget); // TODO: Fix this to use dispatch.call when extensions are running in workers. - const menuFunc = extensionObject[menuName]; + const menuFunc = extensionObject[menuItemFunctionName]; const menuItems = menuFunc.call(extensionObject, editingTargetID).map( item => { item = maybeFormatMessage(item, extensionMessageContext); @@ -356,7 +365,7 @@ class ExtensionManager { }); if (!menuItems || menuItems.length < 1) { - throw new Error(`Extension menu returned no items: ${menuName}`); + throw new Error(`Extension menu returned no items: ${menuItemFunctionName}`); } return menuItems; } diff --git a/src/extensions/scratch3_boost/index.js b/src/extensions/scratch3_boost/index.js index 854ef7b71..00323c89d 100644 --- a/src/extensions/scratch3_boost/index.js +++ b/src/extensions/scratch3_boost/index.js @@ -1413,206 +1413,227 @@ class Scratch3BoostBlocks { } ], menus: { - MOTOR_ID: [ - { - text: 'A', - value: BoostMotorLabel.A - }, - { - text: 'B', - value: BoostMotorLabel.B - }, - { - text: 'C', - value: BoostMotorLabel.C - }, - { - text: 'D', - value: BoostMotorLabel.D - }, - { - text: 'AB', - value: BoostMotorLabel.AB - }, - { - text: 'ABCD', - value: BoostMotorLabel.ALL - } - ], - MOTOR_REPORTER_ID: [ - { - text: 'A', - value: BoostMotorLabel.A - }, - { - text: 'B', - value: BoostMotorLabel.B - }, - { - text: 'C', - value: BoostMotorLabel.C - }, - { - text: 'D', - value: BoostMotorLabel.D - } - ], - MOTOR_DIRECTION: [ - { - text: formatMessage({ - id: 'boost.motorDirection.forward', - default: 'this way', - description: 'label for forward element in motor direction menu for LEGO Boost extension' - }), - value: BoostMotorDirection.FORWARD - }, - { - text: formatMessage({ - id: 'boost.motorDirection.backward', - default: 'that way', - description: 'label for backward element in motor direction menu for LEGO Boost extension' - }), - value: BoostMotorDirection.BACKWARD - }, - { - text: formatMessage({ - id: 'boost.motorDirection.reverse', - default: 'reverse', - description: 'label for reverse element in motor direction menu for LEGO Boost extension' - }), - value: BoostMotorDirection.REVERSE - } - ], - TILT_DIRECTION: [ - { - text: formatMessage({ - id: 'boost.tiltDirection.up', - default: 'up', - description: 'label for up element in tilt direction menu for LEGO Boost extension' - }), - value: BoostTiltDirection.UP - }, - { - text: formatMessage({ - id: 'boost.tiltDirection.down', - default: 'down', - description: 'label for down element in tilt direction menu for LEGO Boost extension' - }), - value: BoostTiltDirection.DOWN - }, - { - text: formatMessage({ - id: 'boost.tiltDirection.left', - default: 'left', - description: 'label for left element in tilt direction menu for LEGO Boost extension' - }), - value: BoostTiltDirection.LEFT - }, - { - text: formatMessage({ - id: 'boost.tiltDirection.right', - default: 'right', - description: 'label for right element in tilt direction menu for LEGO Boost extension' - }), - value: BoostTiltDirection.RIGHT - } - ], - TILT_DIRECTION_ANY: [ - { - text: formatMessage({ - id: 'boost.tiltDirection.up', - default: 'up' - }), - value: BoostTiltDirection.UP - }, - { - text: formatMessage({ - id: 'boost.tiltDirection.down', - default: 'down' - }), - value: BoostTiltDirection.DOWN - }, - { - text: formatMessage({ - id: 'boost.tiltDirection.left', - default: 'left' - }), - value: BoostTiltDirection.LEFT - }, - { - text: formatMessage({ - id: 'boost.tiltDirection.right', - default: 'right' - }), - value: BoostTiltDirection.RIGHT - }, - { - text: formatMessage({ - id: 'boost.tiltDirection.any', - default: 'any', - description: 'label for any element in tilt direction menu for LEGO Boost extension' - }), - value: BoostTiltDirection.ANY - } - ], - COLOR: [ - { - text: formatMessage({ - id: 'boost.color.red', - default: 'red', - description: 'the color red' - }), - value: BoostColor.RED - }, - { - text: formatMessage({ - id: 'boost.color.blue', - default: 'blue', - description: 'the color blue' - }), - value: BoostColor.BLUE - }, - { - text: formatMessage({ - id: 'boost.color.green', - default: 'green', - description: 'the color green' - }), - value: BoostColor.GREEN - }, - { - text: formatMessage({ - id: 'boost.color.yellow', - default: 'yellow', - description: 'the color yellow' - }), - value: BoostColor.YELLOW - }, - { - text: formatMessage({ - id: 'boost.color.white', - default: 'white', - desription: 'the color white' - }), - value: BoostColor.WHITE - }, - { - text: formatMessage({ - id: 'boost.color.black', - default: 'black', - description: 'the color black' - }), - value: BoostColor.BLACK - }, - { - text: formatMessage({ - id: 'boost.color.any', - default: 'any color', - description: 'any color' - }), - value: BoostColor.ANY - } - ] + MOTOR_ID: { + acceptReporters: true, + items: [ + { + text: 'A', + value: BoostMotorLabel.A + }, + { + text: 'B', + value: BoostMotorLabel.B + }, + { + text: 'C', + value: BoostMotorLabel.C + }, + { + text: 'D', + value: BoostMotorLabel.D + }, + { + text: 'AB', + value: BoostMotorLabel.AB + }, + { + text: 'ABCD', + value: BoostMotorLabel.ALL + } + ] + }, + MOTOR_REPORTER_ID: { + acceptReporters: true, + items: [ + { + text: 'A', + value: BoostMotorLabel.A + }, + { + text: 'B', + value: BoostMotorLabel.B + }, + { + text: 'C', + value: BoostMotorLabel.C + }, + { + text: 'D', + value: BoostMotorLabel.D + } + ] + }, + MOTOR_DIRECTION: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'boost.motorDirection.forward', + default: 'this way', + description: + 'label for forward element in motor direction menu for LEGO Boost extension' + }), + value: BoostMotorDirection.FORWARD + }, + { + text: formatMessage({ + id: 'boost.motorDirection.backward', + default: 'that way', + description: + 'label for backward element in motor direction menu for LEGO Boost extension' + }), + value: BoostMotorDirection.BACKWARD + }, + { + text: formatMessage({ + id: 'boost.motorDirection.reverse', + default: 'reverse', + description: + 'label for reverse element in motor direction menu for LEGO Boost extension' + }), + value: BoostMotorDirection.REVERSE + } + ] + }, + TILT_DIRECTION: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'boost.tiltDirection.up', + default: 'up', + description: 'label for up element in tilt direction menu for LEGO Boost extension' + }), + value: BoostTiltDirection.UP + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.down', + default: 'down', + description: 'label for down element in tilt direction menu for LEGO Boost extension' + }), + value: BoostTiltDirection.DOWN + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.left', + default: 'left', + description: 'label for left element in tilt direction menu for LEGO Boost extension' + }), + value: BoostTiltDirection.LEFT + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.right', + default: 'right', + description: 'label for right element in tilt direction menu for LEGO Boost extension' + }), + value: BoostTiltDirection.RIGHT + } + ] + }, + TILT_DIRECTION_ANY: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'boost.tiltDirection.up', + default: 'up' + }), + value: BoostTiltDirection.UP + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.down', + default: 'down' + }), + value: BoostTiltDirection.DOWN + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.left', + default: 'left' + }), + value: BoostTiltDirection.LEFT + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.right', + default: 'right' + }), + value: BoostTiltDirection.RIGHT + }, + { + text: formatMessage({ + id: 'boost.tiltDirection.any', + default: 'any', + description: 'label for any element in tilt direction menu for LEGO Boost extension' + }), + value: BoostTiltDirection.ANY + } + ] + }, + COLOR: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'boost.color.red', + default: 'red', + description: 'the color red' + }), + value: BoostColor.RED + }, + { + text: formatMessage({ + id: 'boost.color.blue', + default: 'blue', + description: 'the color blue' + }), + value: BoostColor.BLUE + }, + { + text: formatMessage({ + id: 'boost.color.green', + default: 'green', + description: 'the color green' + }), + value: BoostColor.GREEN + }, + { + text: formatMessage({ + id: 'boost.color.yellow', + default: 'yellow', + description: 'the color yellow' + }), + value: BoostColor.YELLOW + }, + { + text: formatMessage({ + id: 'boost.color.white', + default: 'white', + desription: 'the color white' + }), + value: BoostColor.WHITE + }, + { + text: formatMessage({ + id: 'boost.color.black', + default: 'black', + description: 'the color black' + }), + value: BoostColor.BLACK + }, + { + text: formatMessage({ + id: 'boost.color.any', + default: 'any color', + description: 'any color' + }), + value: BoostColor.ANY + } + ] + } } }; } diff --git a/src/extensions/scratch3_ev3/index.js b/src/extensions/scratch3_ev3/index.js index 971f0662c..80053a2b5 100644 --- a/src/extensions/scratch3_ev3/index.js +++ b/src/extensions/scratch3_ev3/index.js @@ -1137,8 +1137,14 @@ class Scratch3Ev3Blocks { } ], menus: { - motorPorts: this._formatMenu(Ev3MotorMenu), - sensorPorts: this._formatMenu(Ev3SensorMenu) + motorPorts: { + acceptReporters: true, + items: this._formatMenu(Ev3MotorMenu) + }, + sensorPorts: { + acceptReporters: true, + items: this._formatMenu(Ev3SensorMenu) + } } }; } diff --git a/src/extensions/scratch3_gdx_for/index.js b/src/extensions/scratch3_gdx_for/index.js index 173b1c93e..ff1ce18bb 100644 --- a/src/extensions/scratch3_gdx_for/index.js +++ b/src/extensions/scratch3_gdx_for/index.js @@ -757,11 +757,26 @@ class Scratch3GdxForBlocks { } ], menus: { - pushPullOptions: this.PUSH_PULL_MENU, - gestureOptions: this.GESTURE_MENU, - axisOptions: this.AXIS_MENU, - tiltOptions: this.TILT_MENU, - tiltAnyOptions: this.TILT_MENU_ANY + pushPullOptions: { + acceptReporters: true, + items: this.PUSH_PULL_MENU + }, + gestureOptions: { + acceptReporters: true, + items: this.GESTURE_MENU + }, + axisOptions: { + acceptReporters: true, + items: this.AXIS_MENU + }, + tiltOptions: { + acceptReporters: true, + items: this.TILT_MENU + }, + tiltAnyOptions: { + acceptReporters: true, + items: this.TILT_MENU_ANY + } } }; } diff --git a/src/extensions/scratch3_makeymakey/index.js b/src/extensions/scratch3_makeymakey/index.js index ed925ac92..a83ca6616 100644 --- a/src/extensions/scratch3_makeymakey/index.js +++ b/src/extensions/scratch3_makeymakey/index.js @@ -207,55 +207,61 @@ class Scratch3MakeyMakeyBlocks { } ], menus: { - KEY: [ - { - text: formatMessage({ - id: 'makeymakey.spaceKey', - default: 'space', - description: 'The space key on a computer keyboard.' - }), - value: KEY_ID_SPACE - }, - { - text: formatMessage({ - id: 'makeymakey.upArrow', - default: 'up arrow', - description: 'The up arrow key on a computer keyboard.' - }), - value: KEY_ID_UP - }, - { - text: formatMessage({ - id: 'makeymakey.downArrow', - default: 'down arrow', - description: 'The down arrow key on a computer keyboard.' - }), - value: KEY_ID_DOWN - }, - { - text: formatMessage({ - id: 'makeymakey.rightArrow', - default: 'right arrow', - description: 'The right arrow key on a computer keyboard.' - }), - value: KEY_ID_RIGHT - }, - { - text: formatMessage({ - id: 'makeymakey.leftArrow', - default: 'left arrow', - description: 'The left arrow key on a computer keyboard.' - }), - value: KEY_ID_LEFT - }, - {text: 'w', value: 'w'}, - {text: 'a', value: 'a'}, - {text: 's', value: 's'}, - {text: 'd', value: 'd'}, - {text: 'f', value: 'f'}, - {text: 'g', value: 'g'} - ], - SEQUENCE: this.buildSequenceMenu(this.DEFAULT_SEQUENCES) + KEY: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'makeymakey.spaceKey', + default: 'space', + description: 'The space key on a computer keyboard.' + }), + value: KEY_ID_SPACE + }, + { + text: formatMessage({ + id: 'makeymakey.upArrow', + default: 'up arrow', + description: 'The up arrow key on a computer keyboard.' + }), + value: KEY_ID_UP + }, + { + text: formatMessage({ + id: 'makeymakey.downArrow', + default: 'down arrow', + description: 'The down arrow key on a computer keyboard.' + }), + value: KEY_ID_DOWN + }, + { + text: formatMessage({ + id: 'makeymakey.rightArrow', + default: 'right arrow', + description: 'The right arrow key on a computer keyboard.' + }), + value: KEY_ID_RIGHT + }, + { + text: formatMessage({ + id: 'makeymakey.leftArrow', + default: 'left arrow', + description: 'The left arrow key on a computer keyboard.' + }), + value: KEY_ID_LEFT + }, + {text: 'w', value: 'w'}, + {text: 'a', value: 'a'}, + {text: 's', value: 's'}, + {text: 'd', value: 'd'}, + {text: 'f', value: 'f'}, + {text: 'g', value: 'g'} + ] + }, + SEQUENCE: { + acceptReporters: true, + items: this.buildSequenceMenu(this.DEFAULT_SEQUENCES) + } } }; } diff --git a/src/extensions/scratch3_microbit/index.js b/src/extensions/scratch3_microbit/index.js index 29b5d790a..81476fa05 100644 --- a/src/extensions/scratch3_microbit/index.js +++ b/src/extensions/scratch3_microbit/index.js @@ -746,12 +746,30 @@ class Scratch3MicroBitBlocks { } ], menus: { - buttons: this.BUTTONS_MENU, - gestures: this.GESTURES_MENU, - pinState: this.PIN_STATE_MENU, - tiltDirection: this.TILT_DIRECTION_MENU, - tiltDirectionAny: this.TILT_DIRECTION_ANY_MENU, - touchPins: ['0', '1', '2'] + buttons: { + acceptReporters: true, + items: this.BUTTONS_MENU + }, + gestures: { + acceptReporters: true, + items: this.GESTURES_MENU + }, + pinState: { + acceptReporters: true, + items: this.PIN_STATE_MENU + }, + tiltDirection: { + acceptReporters: true, + items: this.TILT_DIRECTION_MENU + }, + tiltDirectionAny: { + acceptReporters: true, + items: this.TILT_DIRECTION_ANY_MENU + }, + touchPins: { + acceptReporters: true, + items: ['0', '1', '2'] + } } }; } diff --git a/src/extensions/scratch3_music/index.js b/src/extensions/scratch3_music/index.js index e1693e405..cd43b87b9 100644 --- a/src/extensions/scratch3_music/index.js +++ b/src/extensions/scratch3_music/index.js @@ -911,8 +911,14 @@ class Scratch3MusicBlocks { } ], menus: { - DRUM: this._buildMenu(this.DRUM_INFO), - INSTRUMENT: this._buildMenu(this.INSTRUMENT_INFO) + DRUM: { + acceptReporters: true, + items: this._buildMenu(this.DRUM_INFO) + }, + INSTRUMENT: { + acceptReporters: true, + items: this._buildMenu(this.INSTRUMENT_INFO) + } } }; } diff --git a/src/extensions/scratch3_pen/index.js b/src/extensions/scratch3_pen/index.js index 2132d10a4..78705a5f5 100644 --- a/src/extensions/scratch3_pen/index.js +++ b/src/extensions/scratch3_pen/index.js @@ -477,7 +477,10 @@ class Scratch3PenBlocks { } ], menus: { - colorParam: this._initColorParam() + colorParam: { + acceptReporters: true, + items: this._initColorParam() + } } }; } diff --git a/src/extensions/scratch3_text2speech/index.js b/src/extensions/scratch3_text2speech/index.js index c4aa0b423..747912322 100644 --- a/src/extensions/scratch3_text2speech/index.js +++ b/src/extensions/scratch3_text2speech/index.js @@ -469,8 +469,14 @@ class Scratch3Text2SpeechBlocks { } ], menus: { - voices: this.getVoiceMenu(), - languages: this.getLanguageMenu() + voices: { + acceptReporters: true, + items: this.getVoiceMenu() + }, + languages: { + acceptReporters: true, + items: this.getLanguageMenu() + } } }; } diff --git a/src/extensions/scratch3_translate/index.js b/src/extensions/scratch3_translate/index.js index 0a5bf6140..3c1d4cd19 100644 --- a/src/extensions/scratch3_translate/index.js +++ b/src/extensions/scratch3_translate/index.js @@ -146,7 +146,10 @@ class Scratch3TranslateBlocks { } ], menus: { - languages: this._supportedLanguages + languages: { + acceptReporters: true, + items: this._supportedLanguages + } } }; } diff --git a/src/extensions/scratch3_video_sensing/index.js b/src/extensions/scratch3_video_sensing/index.js index 76d5a2c12..bbdf3c308 100644 --- a/src/extensions/scratch3_video_sensing/index.js +++ b/src/extensions/scratch3_video_sensing/index.js @@ -488,9 +488,18 @@ class Scratch3VideoSensingBlocks { } ], menus: { - ATTRIBUTE: this._buildMenu(this.ATTRIBUTE_INFO), - SUBJECT: this._buildMenu(this.SUBJECT_INFO), - VIDEO_STATE: this._buildMenu(this.VIDEO_STATE_INFO) + ATTRIBUTE: { + acceptReporters: true, + items: this._buildMenu(this.ATTRIBUTE_INFO) + }, + SUBJECT: { + acceptReporters: true, + items: this._buildMenu(this.SUBJECT_INFO) + }, + VIDEO_STATE: { + acceptReporters: true, + items: this._buildMenu(this.VIDEO_STATE_INFO) + } } }; } diff --git a/src/extensions/scratch3_wedo2/index.js b/src/extensions/scratch3_wedo2/index.js index d7a69e489..9fd0b6616 100644 --- a/src/extensions/scratch3_wedo2/index.js +++ b/src/extensions/scratch3_wedo2/index.js @@ -1133,139 +1133,157 @@ class Scratch3WeDo2Blocks { } ], menus: { - MOTOR_ID: [ - { - text: formatMessage({ - id: 'wedo2.motorId.default', - default: 'motor', - description: 'label for motor element in motor menu for LEGO WeDo 2 extension' - }), - value: WeDo2MotorLabel.DEFAULT - }, - { - text: formatMessage({ - id: 'wedo2.motorId.a', - default: 'motor A', - description: 'label for motor A element in motor menu for LEGO WeDo 2 extension' - }), - value: WeDo2MotorLabel.A - }, - { - text: formatMessage({ - id: 'wedo2.motorId.b', - default: 'motor B', - description: 'label for motor B element in motor menu for LEGO WeDo 2 extension' - }), - value: WeDo2MotorLabel.B - }, - { - text: formatMessage({ - id: 'wedo2.motorId.all', - default: 'all motors', - description: 'label for all motors element in motor menu for LEGO WeDo 2 extension' - }), - value: WeDo2MotorLabel.ALL - } - ], - MOTOR_DIRECTION: [ - { - text: formatMessage({ - id: 'wedo2.motorDirection.forward', - default: 'this way', - description: 'label for forward element in motor direction menu for LEGO WeDo 2 extension' - }), - value: WeDo2MotorDirection.FORWARD - }, - { - text: formatMessage({ - id: 'wedo2.motorDirection.backward', - default: 'that way', - description: 'label for backward element in motor direction menu for LEGO WeDo 2 extension' - }), - value: WeDo2MotorDirection.BACKWARD - }, - { - text: formatMessage({ - id: 'wedo2.motorDirection.reverse', - default: 'reverse', - description: 'label for reverse element in motor direction menu for LEGO WeDo 2 extension' - }), - value: WeDo2MotorDirection.REVERSE - } - ], - TILT_DIRECTION: [ - { - text: formatMessage({ - id: 'wedo2.tiltDirection.up', - default: 'up', - description: 'label for up element in tilt direction menu for LEGO WeDo 2 extension' - }), - value: WeDo2TiltDirection.UP - }, - { - text: formatMessage({ - id: 'wedo2.tiltDirection.down', - default: 'down', - description: 'label for down element in tilt direction menu for LEGO WeDo 2 extension' - }), - value: WeDo2TiltDirection.DOWN - }, - { - text: formatMessage({ - id: 'wedo2.tiltDirection.left', - default: 'left', - description: 'label for left element in tilt direction menu for LEGO WeDo 2 extension' - }), - value: WeDo2TiltDirection.LEFT - }, - { - text: formatMessage({ - id: 'wedo2.tiltDirection.right', - default: 'right', - description: 'label for right element in tilt direction menu for LEGO WeDo 2 extension' - }), - value: WeDo2TiltDirection.RIGHT - } - ], - TILT_DIRECTION_ANY: [ - { - text: formatMessage({ - id: 'wedo2.tiltDirection.up', - default: 'up' - }), - value: WeDo2TiltDirection.UP - }, - { - text: formatMessage({ - id: 'wedo2.tiltDirection.down', - default: 'down' - }), - value: WeDo2TiltDirection.DOWN - }, - { - text: formatMessage({ - id: 'wedo2.tiltDirection.left', - default: 'left' - }), - value: WeDo2TiltDirection.LEFT - }, - { - text: formatMessage({ - id: 'wedo2.tiltDirection.right', - default: 'right' - }), - value: WeDo2TiltDirection.RIGHT - }, - { - text: formatMessage({ - id: 'wedo2.tiltDirection.any', - default: 'any', - description: 'label for any element in tilt direction menu for LEGO WeDo 2 extension' - }), - value: WeDo2TiltDirection.ANY - } - ], - OP: ['<', '>'] + MOTOR_ID: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'wedo2.motorId.default', + default: 'motor', + description: 'label for motor element in motor menu for LEGO WeDo 2 extension' + }), + value: WeDo2MotorLabel.DEFAULT + }, + { + text: formatMessage({ + id: 'wedo2.motorId.a', + default: 'motor A', + description: 'label for motor A element in motor menu for LEGO WeDo 2 extension' + }), + value: WeDo2MotorLabel.A + }, + { + text: formatMessage({ + id: 'wedo2.motorId.b', + default: 'motor B', + description: 'label for motor B element in motor menu for LEGO WeDo 2 extension' + }), + value: WeDo2MotorLabel.B + }, + { + text: formatMessage({ + id: 'wedo2.motorId.all', + default: 'all motors', + description: 'label for all motors element in motor menu for LEGO WeDo 2 extension' + }), + value: WeDo2MotorLabel.ALL + } + ] + }, + MOTOR_DIRECTION: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'wedo2.motorDirection.forward', + default: 'this way', + description: + 'label for forward element in motor direction menu for LEGO WeDo 2 extension' + }), + value: WeDo2MotorDirection.FORWARD + }, + { + text: formatMessage({ + id: 'wedo2.motorDirection.backward', + default: 'that way', + description: + 'label for backward element in motor direction menu for LEGO WeDo 2 extension' + }), + value: WeDo2MotorDirection.BACKWARD + }, + { + text: formatMessage({ + id: 'wedo2.motorDirection.reverse', + default: 'reverse', + description: + 'label for reverse element in motor direction menu for LEGO WeDo 2 extension' + }), + value: WeDo2MotorDirection.REVERSE + } + ] + }, + TILT_DIRECTION: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'wedo2.tiltDirection.up', + default: 'up', + description: 'label for up element in tilt direction menu for LEGO WeDo 2 extension' + }), + value: WeDo2TiltDirection.UP + }, + { + text: formatMessage({ + id: 'wedo2.tiltDirection.down', + default: 'down', + description: 'label for down element in tilt direction menu for LEGO WeDo 2 extension' + }), + value: WeDo2TiltDirection.DOWN + }, + { + text: formatMessage({ + id: 'wedo2.tiltDirection.left', + default: 'left', + description: 'label for left element in tilt direction menu for LEGO WeDo 2 extension' + }), + value: WeDo2TiltDirection.LEFT + }, + { + text: formatMessage({ + id: 'wedo2.tiltDirection.right', + default: 'right', + description: 'label for right element in tilt direction menu for LEGO WeDo 2 extension' + }), + value: WeDo2TiltDirection.RIGHT + } + ] + }, + TILT_DIRECTION_ANY: { + acceptReporters: true, + items: [ + { + text: formatMessage({ + id: 'wedo2.tiltDirection.up', + default: 'up' + }), + value: WeDo2TiltDirection.UP + }, + { + text: formatMessage({ + id: 'wedo2.tiltDirection.down', + default: 'down' + }), + value: WeDo2TiltDirection.DOWN + }, + { + text: formatMessage({ + id: 'wedo2.tiltDirection.left', + default: 'left' + }), + value: WeDo2TiltDirection.LEFT + }, + { + text: formatMessage({ + id: 'wedo2.tiltDirection.right', + default: 'right' + }), + value: WeDo2TiltDirection.RIGHT + }, + { + text: formatMessage({ + id: 'wedo2.tiltDirection.any', + default: 'any', + description: 'label for any element in tilt direction menu for LEGO WeDo 2 extension' + }), + value: WeDo2TiltDirection.ANY + } + ] + }, + OP: { + acceptReporters: true, + items: ['<', '>'] + } } }; }