Merge pull request #2143 from cwillisf/non-droppable-extension-menus

support non-droppable menus in extensions
This commit is contained in:
Chris Willis-Ford 2019-06-18 21:18:10 -07:00 committed by GitHub
commit c6b63a8f09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 734 additions and 452 deletions

View file

@ -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 ## Annotated Example
```js ```js
@ -311,7 +429,16 @@ class SomeBlocks {
// Dynamic menu: returns an array as above. // Dynamic menu: returns an array as above.
// Called each time the menu is opened. // 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) // Optional: translations (UNSTABLE - NOT YET SUPPORTED)

View file

@ -786,10 +786,7 @@ class Runtime extends EventEmitter {
name: maybeFormatMessage(extensionInfo.name), name: maybeFormatMessage(extensionInfo.name),
showStatusButton: extensionInfo.showStatusButton, showStatusButton: extensionInfo.showStatusButton,
blockIconURI: extensionInfo.blockIconURI, blockIconURI: extensionInfo.blockIconURI,
menuIconURI: extensionInfo.menuIconURI, menuIconURI: extensionInfo.menuIconURI
customFieldTypes: {},
blocks: [],
menus: []
}; };
if (extensionInfo.color1) { if (extensionInfo.color1) {
@ -830,8 +827,6 @@ class Runtime extends EventEmitter {
const categoryInfo = this._blockInfo.find(info => info.id === extensionInfo.id); const categoryInfo = this._blockInfo.find(info => info.id === extensionInfo.id);
if (categoryInfo) { if (categoryInfo) {
categoryInfo.name = maybeFormatMessage(extensionInfo.name); categoryInfo.name = maybeFormatMessage(extensionInfo.name);
categoryInfo.blocks = [];
categoryInfo.menus = [];
this._fillExtensionCategory(categoryInfo, extensionInfo); this._fillExtensionCategory(categoryInfo, extensionInfo);
this.emit(Runtime.BLOCKSINFO_UPDATE, categoryInfo); 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. * and store the results in the provided category object.
* @param {CategoryInfo} categoryInfo - the category to be filled * @param {CategoryInfo} categoryInfo - the category to be filled
* @param {ExtensionMetadata} extensionInfo - the extension metadata to read * @param {ExtensionMetadata} extensionInfo - the extension metadata to read
* @private * @private
*/ */
_fillExtensionCategory (categoryInfo, extensionInfo) { _fillExtensionCategory (categoryInfo, extensionInfo) {
categoryInfo.blocks = [];
categoryInfo.customFieldTypes = {};
categoryInfo.menus = [];
categoryInfo.menuInfo = {};
for (const menuName in extensionInfo.menus) { for (const menuName in extensionInfo.menus) {
if (extensionInfo.menus.hasOwnProperty(menuName)) { if (extensionInfo.menus.hasOwnProperty(menuName)) {
const menuItems = extensionInfo.menus[menuName]; const menuInfo = extensionInfo.menus[menuName];
const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuItems, categoryInfo); const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuInfo, categoryInfo);
categoryInfo.menus.push(convertedMenu); categoryInfo.menus.push(convertedMenu);
categoryInfo.menuInfo[menuName] = menuInfo;
} }
} }
for (const fieldTypeName in extensionInfo.customFieldTypes) { 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. * Convert the given extension menu items into the scratch-blocks style of list of pairs.
* @param {string} menuName - the name of the menu * If the menu is dynamic (e.g. the passed in argument is a function), return the input unmodified.
* @param {array} menuItems - the list of items for this menu * @param {object} menuItems - an array of menu items or a function to retrieve such an array
* @param {CategoryInfo} categoryInfo - the category for this block * @returns {object} - an array of 2 element arrays or the original input function
* @returns {object} - a JSON-esque object ready for scratch-blocks' consumption
* @private * @private
*/ */
_buildMenuForScratchBlocks (menuName, menuItems, categoryInfo) { _convertMenuItems (menuItems) {
const menuId = this._makeExtensionMenuId(menuName, categoryInfo.id); if (typeof menuItems !== 'function') {
let options = null;
if (typeof menuItems === 'function') {
options = menuItems;
} else {
const extensionMessageContext = this.makeMessageContextForTarget(); const extensionMessageContext = this.makeMessageContextForTarget();
options = menuItems.map(item => { return menuItems.map(item => {
const formattedItem = maybeFormatMessage(item, extensionMessageContext); const formattedItem = maybeFormatMessage(item, extensionMessageContext);
switch (typeof formattedItem) { switch (typeof formattedItem) {
case 'string': 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 { return {
json: { json: {
message0: '%1', message0: '%1',
@ -925,12 +937,13 @@ class Runtime extends EventEmitter {
colour: categoryInfo.color1, colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2, colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3, colourTertiary: categoryInfo.color3,
outputShape: ScratchBlocksConstants.OUTPUT_SHAPE_ROUND, outputShape: menuInfo.acceptReporters ?
ScratchBlocksConstants.OUTPUT_SHAPE_ROUND : ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE,
args0: [ args0: [
{ {
type: 'field_dropdown', type: 'field_dropdown',
name: menuName, name: menuName,
options: options options: menuItems
} }
] ]
} }
@ -1227,29 +1240,51 @@ class Runtime extends EventEmitter {
argJSON.check = argTypeInfo.check; argJSON.check = argTypeInfo.check;
} }
const shadowType = (argInfo.menu ? let valueName;
this._makeExtensionMenuId(argInfo.menu, context.categoryInfo.id) : let shadowType;
argTypeInfo.shadowType); let fieldName;
const fieldType = argInfo.menu || argTypeInfo.fieldType; 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;
}
// <value> is the ScratchBlocks name for a block input. // <value> is the ScratchBlocks name for a block input.
context.inputList.push(`<value name="${placeholder}">`); if (valueName) {
context.inputList.push(`<value name="${placeholder}">`);
}
// The <shadow> is a placeholder for a reporter and is visible when there's no reporter in this input. // The <shadow> 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. // Boolean inputs don't need to specify a shadow in the XML.
if (shadowType) { if (shadowType) {
context.inputList.push(`<shadow type="${shadowType}">`); context.inputList.push(`<shadow type="${shadowType}">`);
}
// <field> is a text field that the user can type into. Some shadows, like the color picker, don't allow // A <field> displays a dynamic value: a user-editable text field, a drop-down menu, etc.
// text input and therefore don't need a field element. if (fieldName) {
if (fieldType) { context.inputList.push(`<field name="${fieldName}">${defaultValue}</field>`);
context.inputList.push(`<field name="${fieldType}">${defaultValue}</field>`); }
}
if (shadowType) {
context.inputList.push('</shadow>'); context.inputList.push('</shadow>');
} }
context.inputList.push('</value>'); if (valueName) {
context.inputList.push('</value>');
}
const argsName = `args${context.outLineNum}`; const argsName = `args${context.outLineNum}`;
const blockArgs = (context.blockJSON[argsName] = context.blockJSON[argsName] || []); const blockArgs = (context.blockJSON[argsName] = context.blockJSON[argsName] || []);

View file

@ -294,7 +294,7 @@ class ExtensionManager {
} }
return results; return results;
}, []); }, []);
extensionInfo.menus = extensionInfo.menus || []; extensionInfo.menus = extensionInfo.menus || {};
extensionInfo.menus = this._prepareMenuInfo(serviceName, extensionInfo.menus); extensionInfo.menus = this._prepareMenuInfo(serviceName, extensionInfo.menus);
return extensionInfo; return extensionInfo;
} }
@ -309,15 +309,24 @@ class ExtensionManager {
_prepareMenuInfo (serviceName, menus) { _prepareMenuInfo (serviceName, menus) {
const menuNames = Object.getOwnPropertyNames(menus); const menuNames = Object.getOwnPropertyNames(menus);
for (let i = 0; i < menuNames.length; i++) { for (let i = 0; i < menuNames.length; i++) {
const item = menuNames[i]; const menuName = menuNames[i];
// If the value is a string, it should be the name of a function in the let menuInfo = menus[menuName];
// extension object to call to populate the menu whenever it is opened.
// Set up the binding for the function object here so // If the menu description is in short form (items only) then normalize it to general form: an object with
// we can use it later when converting the menu for Scratch Blocks. // its items listed in an `items` property.
if (typeof menus[item] === 'string') { 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 serviceObject = dispatch.services[serviceName];
const menuName = menus[item]; // Bind the function here so we can pass a simple item generation function to Scratch Blocks later.
menus[item] = this._getExtensionMenuItems.bind(this, serviceObject, menuName); menuInfo.items = this._getExtensionMenuItems.bind(this, serviceObject, menuItemFunctionName);
} }
} }
return menus; return menus;
@ -326,11 +335,11 @@ class ExtensionManager {
/** /**
* Fetch the items for a particular extension menu, providing the target ID for context. * Fetch the items for a particular extension menu, providing the target ID for context.
* @param {object} extensionObject - the extension object providing the menu. * @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. * @returns {Array} menu items ready for scratch-blocks.
* @private * @private
*/ */
_getExtensionMenuItems (extensionObject, menuName) { _getExtensionMenuItems (extensionObject, menuItemFunctionName) {
// Fetch the items appropriate for the target currently being edited. This assumes that menus only // 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. // collect items when opened by the user while editing a particular target.
const editingTarget = this.runtime.getEditingTarget() || this.runtime.getTargetForStage(); const editingTarget = this.runtime.getEditingTarget() || this.runtime.getTargetForStage();
@ -338,7 +347,7 @@ class ExtensionManager {
const extensionMessageContext = this.runtime.makeMessageContextForTarget(editingTarget); const extensionMessageContext = this.runtime.makeMessageContextForTarget(editingTarget);
// TODO: Fix this to use dispatch.call when extensions are running in workers. // 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( const menuItems = menuFunc.call(extensionObject, editingTargetID).map(
item => { item => {
item = maybeFormatMessage(item, extensionMessageContext); item = maybeFormatMessage(item, extensionMessageContext);
@ -356,7 +365,7 @@ class ExtensionManager {
}); });
if (!menuItems || menuItems.length < 1) { 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; return menuItems;
} }

View file

@ -1413,206 +1413,227 @@ class Scratch3BoostBlocks {
} }
], ],
menus: { menus: {
MOTOR_ID: [ MOTOR_ID: {
{ acceptReporters: true,
text: 'A', items: [
value: BoostMotorLabel.A {
}, text: 'A',
{ value: BoostMotorLabel.A
text: 'B', },
value: BoostMotorLabel.B {
}, text: 'B',
{ value: BoostMotorLabel.B
text: 'C', },
value: BoostMotorLabel.C {
}, text: 'C',
{ value: BoostMotorLabel.C
text: 'D', },
value: BoostMotorLabel.D {
}, text: 'D',
{ value: BoostMotorLabel.D
text: 'AB', },
value: BoostMotorLabel.AB {
}, text: 'AB',
{ value: BoostMotorLabel.AB
text: 'ABCD', },
value: BoostMotorLabel.ALL {
} text: 'ABCD',
], value: BoostMotorLabel.ALL
MOTOR_REPORTER_ID: [ }
{ ]
text: 'A', },
value: BoostMotorLabel.A MOTOR_REPORTER_ID: {
}, acceptReporters: true,
{ items: [
text: 'B', {
value: BoostMotorLabel.B text: 'A',
}, value: BoostMotorLabel.A
{ },
text: 'C', {
value: BoostMotorLabel.C text: 'B',
}, value: BoostMotorLabel.B
{ },
text: 'D', {
value: BoostMotorLabel.D text: 'C',
} value: BoostMotorLabel.C
], },
MOTOR_DIRECTION: [ {
{ text: 'D',
text: formatMessage({ value: BoostMotorLabel.D
id: 'boost.motorDirection.forward', }
default: 'this way', ]
description: 'label for forward element in motor direction menu for LEGO Boost extension' },
}), MOTOR_DIRECTION: {
value: BoostMotorDirection.FORWARD acceptReporters: true,
}, items: [
{ {
text: formatMessage({ text: formatMessage({
id: 'boost.motorDirection.backward', id: 'boost.motorDirection.forward',
default: 'that way', default: 'this way',
description: 'label for backward element in motor direction menu for LEGO Boost extension' description:
}), 'label for forward element in motor direction menu for LEGO Boost extension'
value: BoostMotorDirection.BACKWARD }),
}, value: BoostMotorDirection.FORWARD
{ },
text: formatMessage({ {
id: 'boost.motorDirection.reverse', text: formatMessage({
default: 'reverse', id: 'boost.motorDirection.backward',
description: 'label for reverse element in motor direction menu for LEGO Boost extension' default: 'that way',
}), description:
value: BoostMotorDirection.REVERSE 'label for backward element in motor direction menu for LEGO Boost extension'
} }),
], value: BoostMotorDirection.BACKWARD
TILT_DIRECTION: [ },
{ {
text: formatMessage({ text: formatMessage({
id: 'boost.tiltDirection.up', id: 'boost.motorDirection.reverse',
default: 'up', default: 'reverse',
description: 'label for up element in tilt direction menu for LEGO Boost extension' description:
}), 'label for reverse element in motor direction menu for LEGO Boost extension'
value: BoostTiltDirection.UP }),
}, value: BoostMotorDirection.REVERSE
{ }
text: formatMessage({ ]
id: 'boost.tiltDirection.down', },
default: 'down', TILT_DIRECTION: {
description: 'label for down element in tilt direction menu for LEGO Boost extension' acceptReporters: true,
}), items: [
value: BoostTiltDirection.DOWN {
}, text: formatMessage({
{ id: 'boost.tiltDirection.up',
text: formatMessage({ default: 'up',
id: 'boost.tiltDirection.left', description: 'label for up element in tilt direction menu for LEGO Boost extension'
default: 'left', }),
description: 'label for left element in tilt direction menu for LEGO Boost extension' value: BoostTiltDirection.UP
}), },
value: BoostTiltDirection.LEFT {
}, text: formatMessage({
{ id: 'boost.tiltDirection.down',
text: formatMessage({ default: 'down',
id: 'boost.tiltDirection.right', description: 'label for down element in tilt direction menu for LEGO Boost extension'
default: 'right', }),
description: 'label for right element in tilt direction menu for LEGO Boost extension' value: BoostTiltDirection.DOWN
}), },
value: BoostTiltDirection.RIGHT {
} text: formatMessage({
], id: 'boost.tiltDirection.left',
TILT_DIRECTION_ANY: [ default: 'left',
{ description: 'label for left element in tilt direction menu for LEGO Boost extension'
text: formatMessage({ }),
id: 'boost.tiltDirection.up', value: BoostTiltDirection.LEFT
default: 'up' },
}), {
value: BoostTiltDirection.UP text: formatMessage({
}, id: 'boost.tiltDirection.right',
{ default: 'right',
text: formatMessage({ description: 'label for right element in tilt direction menu for LEGO Boost extension'
id: 'boost.tiltDirection.down', }),
default: 'down' value: BoostTiltDirection.RIGHT
}), }
value: BoostTiltDirection.DOWN ]
}, },
{ TILT_DIRECTION_ANY: {
text: formatMessage({ acceptReporters: true,
id: 'boost.tiltDirection.left', items: [
default: 'left' {
}), text: formatMessage({
value: BoostTiltDirection.LEFT id: 'boost.tiltDirection.up',
}, default: 'up'
{ }),
text: formatMessage({ value: BoostTiltDirection.UP
id: 'boost.tiltDirection.right', },
default: 'right' {
}), text: formatMessage({
value: BoostTiltDirection.RIGHT id: 'boost.tiltDirection.down',
}, default: 'down'
{ }),
text: formatMessage({ value: BoostTiltDirection.DOWN
id: 'boost.tiltDirection.any', },
default: 'any', {
description: 'label for any element in tilt direction menu for LEGO Boost extension' text: formatMessage({
}), id: 'boost.tiltDirection.left',
value: BoostTiltDirection.ANY default: 'left'
} }),
], value: BoostTiltDirection.LEFT
COLOR: [ },
{ {
text: formatMessage({ text: formatMessage({
id: 'boost.color.red', id: 'boost.tiltDirection.right',
default: 'red', default: 'right'
description: 'the color red' }),
}), value: BoostTiltDirection.RIGHT
value: BoostColor.RED },
}, {
{ text: formatMessage({
text: formatMessage({ id: 'boost.tiltDirection.any',
id: 'boost.color.blue', default: 'any',
default: 'blue', description: 'label for any element in tilt direction menu for LEGO Boost extension'
description: 'the color blue' }),
}), value: BoostTiltDirection.ANY
value: BoostColor.BLUE }
}, ]
{ },
text: formatMessage({ COLOR: {
id: 'boost.color.green', acceptReporters: true,
default: 'green', items: [
description: 'the color green' {
}), text: formatMessage({
value: BoostColor.GREEN id: 'boost.color.red',
}, default: 'red',
{ description: 'the color red'
text: formatMessage({ }),
id: 'boost.color.yellow', value: BoostColor.RED
default: 'yellow', },
description: 'the color yellow' {
}), text: formatMessage({
value: BoostColor.YELLOW id: 'boost.color.blue',
}, default: 'blue',
{ description: 'the color blue'
text: formatMessage({ }),
id: 'boost.color.white', value: BoostColor.BLUE
default: 'white', },
desription: 'the color white' {
}), text: formatMessage({
value: BoostColor.WHITE id: 'boost.color.green',
}, default: 'green',
{ description: 'the color green'
text: formatMessage({ }),
id: 'boost.color.black', value: BoostColor.GREEN
default: 'black', },
description: 'the color black' {
}), text: formatMessage({
value: BoostColor.BLACK id: 'boost.color.yellow',
}, default: 'yellow',
{ description: 'the color yellow'
text: formatMessage({ }),
id: 'boost.color.any', value: BoostColor.YELLOW
default: 'any color', },
description: 'any color' {
}), text: formatMessage({
value: BoostColor.ANY 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
}
]
}
} }
}; };
} }

View file

@ -1137,8 +1137,14 @@ class Scratch3Ev3Blocks {
} }
], ],
menus: { menus: {
motorPorts: this._formatMenu(Ev3MotorMenu), motorPorts: {
sensorPorts: this._formatMenu(Ev3SensorMenu) acceptReporters: true,
items: this._formatMenu(Ev3MotorMenu)
},
sensorPorts: {
acceptReporters: true,
items: this._formatMenu(Ev3SensorMenu)
}
} }
}; };
} }

View file

@ -757,11 +757,26 @@ class Scratch3GdxForBlocks {
} }
], ],
menus: { menus: {
pushPullOptions: this.PUSH_PULL_MENU, pushPullOptions: {
gestureOptions: this.GESTURE_MENU, acceptReporters: true,
axisOptions: this.AXIS_MENU, items: this.PUSH_PULL_MENU
tiltOptions: this.TILT_MENU, },
tiltAnyOptions: this.TILT_MENU_ANY 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
}
} }
}; };
} }

View file

@ -207,55 +207,61 @@ class Scratch3MakeyMakeyBlocks {
} }
], ],
menus: { menus: {
KEY: [ KEY: {
{ acceptReporters: true,
text: formatMessage({ items: [
id: 'makeymakey.spaceKey', {
default: 'space', text: formatMessage({
description: 'The space key on a computer keyboard.' id: 'makeymakey.spaceKey',
}), default: 'space',
value: KEY_ID_SPACE description: 'The space key on a computer keyboard.'
}, }),
{ value: KEY_ID_SPACE
text: formatMessage({ },
id: 'makeymakey.upArrow', {
default: 'up arrow', text: formatMessage({
description: 'The up arrow key on a computer keyboard.' id: 'makeymakey.upArrow',
}), default: 'up arrow',
value: KEY_ID_UP description: 'The up arrow key on a computer keyboard.'
}, }),
{ value: KEY_ID_UP
text: formatMessage({ },
id: 'makeymakey.downArrow', {
default: 'down arrow', text: formatMessage({
description: 'The down arrow key on a computer keyboard.' id: 'makeymakey.downArrow',
}), default: 'down arrow',
value: KEY_ID_DOWN description: 'The down arrow key on a computer keyboard.'
}, }),
{ value: KEY_ID_DOWN
text: formatMessage({ },
id: 'makeymakey.rightArrow', {
default: 'right arrow', text: formatMessage({
description: 'The right arrow key on a computer keyboard.' id: 'makeymakey.rightArrow',
}), default: 'right arrow',
value: KEY_ID_RIGHT description: 'The right arrow key on a computer keyboard.'
}, }),
{ value: KEY_ID_RIGHT
text: formatMessage({ },
id: 'makeymakey.leftArrow', {
default: 'left arrow', text: formatMessage({
description: 'The left arrow key on a computer keyboard.' id: 'makeymakey.leftArrow',
}), default: 'left arrow',
value: KEY_ID_LEFT description: 'The left arrow key on a computer keyboard.'
}, }),
{text: 'w', value: 'w'}, value: KEY_ID_LEFT
{text: 'a', value: 'a'}, },
{text: 's', value: 's'}, {text: 'w', value: 'w'},
{text: 'd', value: 'd'}, {text: 'a', value: 'a'},
{text: 'f', value: 'f'}, {text: 's', value: 's'},
{text: 'g', value: 'g'} {text: 'd', value: 'd'},
], {text: 'f', value: 'f'},
SEQUENCE: this.buildSequenceMenu(this.DEFAULT_SEQUENCES) {text: 'g', value: 'g'}
]
},
SEQUENCE: {
acceptReporters: true,
items: this.buildSequenceMenu(this.DEFAULT_SEQUENCES)
}
} }
}; };
} }

View file

@ -746,12 +746,30 @@ class Scratch3MicroBitBlocks {
} }
], ],
menus: { menus: {
buttons: this.BUTTONS_MENU, buttons: {
gestures: this.GESTURES_MENU, acceptReporters: true,
pinState: this.PIN_STATE_MENU, items: this.BUTTONS_MENU
tiltDirection: this.TILT_DIRECTION_MENU, },
tiltDirectionAny: this.TILT_DIRECTION_ANY_MENU, gestures: {
touchPins: ['0', '1', '2'] 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']
}
} }
}; };
} }

View file

@ -911,8 +911,14 @@ class Scratch3MusicBlocks {
} }
], ],
menus: { menus: {
DRUM: this._buildMenu(this.DRUM_INFO), DRUM: {
INSTRUMENT: this._buildMenu(this.INSTRUMENT_INFO) acceptReporters: true,
items: this._buildMenu(this.DRUM_INFO)
},
INSTRUMENT: {
acceptReporters: true,
items: this._buildMenu(this.INSTRUMENT_INFO)
}
} }
}; };
} }

View file

@ -477,7 +477,10 @@ class Scratch3PenBlocks {
} }
], ],
menus: { menus: {
colorParam: this._initColorParam() colorParam: {
acceptReporters: true,
items: this._initColorParam()
}
} }
}; };
} }

View file

@ -469,8 +469,14 @@ class Scratch3Text2SpeechBlocks {
} }
], ],
menus: { menus: {
voices: this.getVoiceMenu(), voices: {
languages: this.getLanguageMenu() acceptReporters: true,
items: this.getVoiceMenu()
},
languages: {
acceptReporters: true,
items: this.getLanguageMenu()
}
} }
}; };
} }

View file

@ -146,7 +146,10 @@ class Scratch3TranslateBlocks {
} }
], ],
menus: { menus: {
languages: this._supportedLanguages languages: {
acceptReporters: true,
items: this._supportedLanguages
}
} }
}; };
} }

View file

@ -488,9 +488,18 @@ class Scratch3VideoSensingBlocks {
} }
], ],
menus: { menus: {
ATTRIBUTE: this._buildMenu(this.ATTRIBUTE_INFO), ATTRIBUTE: {
SUBJECT: this._buildMenu(this.SUBJECT_INFO), acceptReporters: true,
VIDEO_STATE: this._buildMenu(this.VIDEO_STATE_INFO) 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)
}
} }
}; };
} }

View file

@ -1133,139 +1133,157 @@ class Scratch3WeDo2Blocks {
} }
], ],
menus: { menus: {
MOTOR_ID: [ MOTOR_ID: {
{ acceptReporters: true,
text: formatMessage({ items: [
id: 'wedo2.motorId.default', {
default: 'motor', text: formatMessage({
description: 'label for motor element in motor menu for LEGO WeDo 2 extension' id: 'wedo2.motorId.default',
}), default: 'motor',
value: WeDo2MotorLabel.DEFAULT 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', text: formatMessage({
description: 'label for motor A element in motor menu for LEGO WeDo 2 extension' id: 'wedo2.motorId.a',
}), default: 'motor A',
value: WeDo2MotorLabel.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', text: formatMessage({
description: 'label for motor B element in motor menu for LEGO WeDo 2 extension' id: 'wedo2.motorId.b',
}), default: 'motor B',
value: WeDo2MotorLabel.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', text: formatMessage({
description: 'label for all motors element in motor menu for LEGO WeDo 2 extension' id: 'wedo2.motorId.all',
}), default: 'all motors',
value: WeDo2MotorLabel.ALL 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', MOTOR_DIRECTION: {
default: 'this way', acceptReporters: true,
description: 'label for forward element in motor direction menu for LEGO WeDo 2 extension' items: [
}), {
value: WeDo2MotorDirection.FORWARD text: formatMessage({
}, id: 'wedo2.motorDirection.forward',
{ default: 'this way',
text: formatMessage({ description:
id: 'wedo2.motorDirection.backward', 'label for forward element in motor direction menu for LEGO WeDo 2 extension'
default: 'that way', }),
description: 'label for backward element in motor direction menu for LEGO WeDo 2 extension' value: WeDo2MotorDirection.FORWARD
}), },
value: WeDo2MotorDirection.BACKWARD {
}, text: formatMessage({
{ id: 'wedo2.motorDirection.backward',
text: formatMessage({ default: 'that way',
id: 'wedo2.motorDirection.reverse', description:
default: 'reverse', 'label for backward element in motor direction menu for LEGO WeDo 2 extension'
description: 'label for reverse element in motor direction menu for LEGO WeDo 2 extension' }),
}), value: WeDo2MotorDirection.BACKWARD
value: WeDo2MotorDirection.REVERSE },
} {
], text: formatMessage({
TILT_DIRECTION: [ id: 'wedo2.motorDirection.reverse',
{ default: 'reverse',
text: formatMessage({ description:
id: 'wedo2.tiltDirection.up', 'label for reverse element in motor direction menu for LEGO WeDo 2 extension'
default: 'up', }),
description: 'label for up element in tilt direction menu for LEGO WeDo 2 extension' value: WeDo2MotorDirection.REVERSE
}), }
value: WeDo2TiltDirection.UP ]
}, },
{ TILT_DIRECTION: {
text: formatMessage({ acceptReporters: true,
id: 'wedo2.tiltDirection.down', items: [
default: 'down', {
description: 'label for down element in tilt direction menu for LEGO WeDo 2 extension' text: formatMessage({
}), id: 'wedo2.tiltDirection.up',
value: WeDo2TiltDirection.DOWN default: 'up',
}, description: 'label for up element in tilt direction menu for LEGO WeDo 2 extension'
{ }),
text: formatMessage({ value: WeDo2TiltDirection.UP
id: 'wedo2.tiltDirection.left', },
default: 'left', {
description: 'label for left element in tilt direction menu for LEGO WeDo 2 extension' text: formatMessage({
}), id: 'wedo2.tiltDirection.down',
value: WeDo2TiltDirection.LEFT default: 'down',
}, description: 'label for down element in tilt direction menu for LEGO WeDo 2 extension'
{ }),
text: formatMessage({ value: WeDo2TiltDirection.DOWN
id: 'wedo2.tiltDirection.right', },
default: 'right', {
description: 'label for right element in tilt direction menu for LEGO WeDo 2 extension' text: formatMessage({
}), id: 'wedo2.tiltDirection.left',
value: WeDo2TiltDirection.RIGHT default: 'left',
} description: 'label for left element in tilt direction menu for LEGO WeDo 2 extension'
], }),
TILT_DIRECTION_ANY: [ value: WeDo2TiltDirection.LEFT
{ },
text: formatMessage({ {
id: 'wedo2.tiltDirection.up', text: formatMessage({
default: 'up' id: 'wedo2.tiltDirection.right',
}), default: 'right',
value: WeDo2TiltDirection.UP description: 'label for right element in tilt direction menu for LEGO WeDo 2 extension'
}, }),
{ value: WeDo2TiltDirection.RIGHT
text: formatMessage({ }
id: 'wedo2.tiltDirection.down', ]
default: 'down' },
}), TILT_DIRECTION_ANY: {
value: WeDo2TiltDirection.DOWN acceptReporters: true,
}, items: [
{ {
text: formatMessage({ text: formatMessage({
id: 'wedo2.tiltDirection.left', id: 'wedo2.tiltDirection.up',
default: 'left' default: 'up'
}), }),
value: WeDo2TiltDirection.LEFT value: WeDo2TiltDirection.UP
}, },
{ {
text: formatMessage({ text: formatMessage({
id: 'wedo2.tiltDirection.right', id: 'wedo2.tiltDirection.down',
default: 'right' default: 'down'
}), }),
value: WeDo2TiltDirection.RIGHT value: WeDo2TiltDirection.DOWN
}, },
{ {
text: formatMessage({ text: formatMessage({
id: 'wedo2.tiltDirection.any', id: 'wedo2.tiltDirection.left',
default: 'any', default: 'left'
description: 'label for any element in tilt direction menu for LEGO WeDo 2 extension' }),
}), value: WeDo2TiltDirection.LEFT
value: WeDo2TiltDirection.ANY },
} {
], text: formatMessage({
OP: ['<', '>'] 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: ['<', '>']
}
} }
}; };
} }