diff --git a/docs/extensions.md b/docs/extensions.md index c4ba6070..5bafb51b 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -120,6 +120,123 @@ 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. +* 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 @@ -315,8 +432,8 @@ class SomeBlocks { // The examples above are shorthand for setting only the `items` property in this full form: menuC: { - // This flag makes a "non-droppable" menu: the menu will not accept reporters. - rejectReporters: true, + // 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' diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 7ed8ecad..ac6d005a 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -894,7 +894,7 @@ class Runtime extends EventEmitter { * @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} [rejectReporters] - if true, prevent dropping reporters onto this menu + * @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 @@ -927,8 +927,8 @@ class Runtime extends EventEmitter { colour: categoryInfo.color1, colourSecondary: categoryInfo.color2, colourTertiary: categoryInfo.color3, - outputShape: menuInfo.rejectReporters ? - ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE : ScratchBlocksConstants.OUTPUT_SHAPE_ROUND, + outputShape: menuInfo.acceptReporters ? + ScratchBlocksConstants.OUTPUT_SHAPE_ROUND : ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE, args0: [ { type: 'field_dropdown', @@ -1235,16 +1235,16 @@ class Runtime extends EventEmitter { let fieldName; if (argInfo.menu) { const menuInfo = argInfo.menu && context.categoryInfo.menuInfo[argInfo.menu]; - if (menuInfo.rejectReporters) { + if (menuInfo.acceptReporters) { + valueName = placeholder; + shadowType = this._makeExtensionMenuId(argInfo.menu, context.categoryInfo.id); + fieldName = argInfo.menu; + } else { argJSON.type = 'field_dropdown'; argJSON.options = menuInfo.items; valueName = null; shadowType = null; fieldName = placeholder; - } else { - valueName = placeholder; - shadowType = this._makeExtensionMenuId(argInfo.menu, context.categoryInfo.id); - fieldName = argInfo.menu; } } else { valueName = placeholder;