Merge branch 'develop' into enhancement/1929

This commit is contained in:
Eric Rosenbaum authored and GitHub committed 2019-06-19 11:09:14 -04:00
commit a115f6dde5
22 files changed
+956 -523

No files matched your search

+132 -1
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
```js
@@ -152,6 +270,10 @@ class SomeBlocks {
// Will be used as the extension's namespace.
id: 'someBlocks',
// Core extensions only: override the default extension block colors.
color1: '#FF8C1A',
color2: '#DB6E00',
// Optional: the human-readable name of this extension as string.
// This and any other string to be displayed in the Scratch UI may either be
// a string or a call to `formatMessage`; a plain string will not be
@@ -307,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)
+7 -1
View File
@@ -1116,8 +1116,14 @@ class Blocks {
let mutationString = `<${mutation.tagName}`;
for (const prop in mutation) {
if (prop === 'children' || prop === 'tagName') continue;
const mutationValue = (typeof mutation[prop] === 'string') ?
let mutationValue = (typeof mutation[prop] === 'string') ?
xmlEscape(mutation[prop]) : mutation[prop];
// Handle dynamic extension blocks
if (prop === 'blockInfo') {
mutationValue = xmlEscape(JSON.stringify(mutation[prop]));
}
mutationString += ` ${prop}="${mutationValue}"`;
}
mutationString += '>';
+6
View File
@@ -13,6 +13,12 @@ const mutatorTagToObject = function (dom) {
for (const prop in dom.attribs) {
if (prop === 'xmlns') continue;
obj[prop] = decodeHtml(dom.attribs[prop]);
// Note: the capitalization of block info in the following lines is important.
// The lowercase is read in from xml which normalizes case. The VM uses camel case everywhere else.
if (prop === 'blockinfo') {
obj.blockInfo = JSON.parse(obj.blockinfo);
delete obj.blockinfo;
}
}
for (let i = 0; i < dom.children.length; i++) {
obj.children.push(
+124 -64
View File
@@ -42,6 +42,8 @@ const defaultBlockPackages = {
scratch3_procedures: require('../blocks/scratch3_procedures')
};
const defaultExtensionColors = ['#0FBD8C', '#0DA57A', '#0B8E69'];
/**
* Information used for converting Scratch argument types into scratch-blocks data.
* @type {object.<ArgumentType, {shadowType: string, fieldType: string}>}
@@ -509,6 +511,14 @@ class Runtime extends EventEmitter {
return 'PROJECT_CHANGED';
}
/**
* Event name for report that a change was made to an extension in the toolbox.
* @const {string}
*/
static get TOOLBOX_EXTENSIONS_NEED_UPDATE () {
return 'TOOLBOX_EXTENSIONS_NEED_UPDATE';
}
/**
* Event name for targets update report.
* @const {string}
@@ -776,24 +786,26 @@ class Runtime extends EventEmitter {
name: maybeFormatMessage(extensionInfo.name),
showStatusButton: extensionInfo.showStatusButton,
blockIconURI: extensionInfo.blockIconURI,
menuIconURI: extensionInfo.menuIconURI,
color1: extensionInfo.colour || '#0FBD8C',
color2: extensionInfo.colourSecondary || '#0DA57A',
color3: extensionInfo.colourTertiary || '#0B8E69',
customFieldTypes: {},
blocks: [],
menus: []
menuIconURI: extensionInfo.menuIconURI
};
if (extensionInfo.color1) {
categoryInfo.color1 = extensionInfo.color1;
categoryInfo.color2 = extensionInfo.color2;
categoryInfo.color3 = extensionInfo.color3;
} else {
categoryInfo.color1 = defaultExtensionColors[0];
categoryInfo.color2 = defaultExtensionColors[1];
categoryInfo.color3 = defaultExtensionColors[2];
}
this._blockInfo.push(categoryInfo);
this._fillExtensionCategory(categoryInfo, extensionInfo);
const fieldTypeDefinitionsForScratch = [];
for (const fieldTypeName in categoryInfo.customFieldTypes) {
if (extensionInfo.customFieldTypes.hasOwnProperty(fieldTypeName)) {
const fieldTypeInfo = categoryInfo.customFieldTypes[fieldTypeName];
fieldTypeDefinitionsForScratch.push(fieldTypeInfo.scratchBlocksDefinition);
// Emit events for custom field types from extension
this.emit(Runtime.EXTENSION_FIELD_ADDED, {
@@ -803,9 +815,7 @@ class Runtime extends EventEmitter {
}
}
const allBlocks = fieldTypeDefinitionsForScratch.concat(categoryInfo.blocks).concat(categoryInfo.menus);
this.emit(Runtime.EXTENSION_ADDED, allBlocks);
this.emit(Runtime.EXTENSION_ADDED, categoryInfo);
}
/**
@@ -814,33 +824,34 @@ class Runtime extends EventEmitter {
* @private
*/
_refreshExtensionPrimitives (extensionInfo) {
let extensionBlocks = [];
for (const categoryInfo of this._blockInfo) {
if (extensionInfo.id === categoryInfo.id) {
categoryInfo.name = maybeFormatMessage(extensionInfo.name);
categoryInfo.blocks = [];
categoryInfo.menus = [];
this._fillExtensionCategory(categoryInfo, extensionInfo);
extensionBlocks = extensionBlocks.concat(categoryInfo.blocks, categoryInfo.menus);
}
}
const categoryInfo = this._blockInfo.find(info => info.id === extensionInfo.id);
if (categoryInfo) {
categoryInfo.name = maybeFormatMessage(extensionInfo.name);
this._fillExtensionCategory(categoryInfo, extensionInfo);
this.emit(Runtime.BLOCKSINFO_UPDATE, extensionBlocks);
this.emit(Runtime.BLOCKSINFO_UPDATE, categoryInfo);
}
}
/**
* 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) {
@@ -880,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':
@@ -906,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',
@@ -915,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
}
]
}
@@ -1011,8 +1034,7 @@ class Runtime extends EventEmitter {
category: categoryInfo.name,
colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3,
extensions: ['scratch_extension']
colourTertiary: categoryInfo.color3
};
const context = {
// TODO: store this somewhere so that we can map args appropriately after translation.
@@ -1032,6 +1054,7 @@ class Runtime extends EventEmitter {
const iconURI = blockInfo.blockIconURI || categoryInfo.blockIconURI;
if (iconURI) {
blockJSON.extensions = ['scratch_extension'];
blockJSON.message0 = '%1 %2';
const iconJSON = {
type: 'field_image',
@@ -1135,7 +1158,9 @@ class Runtime extends EventEmitter {
++outLineNum;
}
const blockXML = `<block type="${extendedOpcode}">${context.inputList.join('')}</block>`;
const mutation = blockInfo.isDynamic ? `<mutation blockInfo="${xmlEscape(JSON.stringify(blockInfo))}"/>` : '';
const inputs = context.inputList.join('');
const blockXML = `<block type="${extendedOpcode}">${mutation}${inputs}</block>`;
return {
info: context.blockInfo,
@@ -1215,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;
}
// <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.
// Boolean inputs don't need to specify a shadow in the XML.
if (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
// text input and therefore don't need a field element.
if (fieldType) {
context.inputList.push(`<field name="${fieldType}">${defaultValue}</field>`);
}
// A <field> displays a dynamic value: a user-editable text field, a drop-down menu, etc.
if (fieldName) {
context.inputList.push(`<field name="${fieldName}">${defaultValue}</field>`);
}
if (shadowType) {
context.inputList.push('</shadow>');
}
context.inputList.push('</value>');
if (valueName) {
context.inputList.push('</value>');
}
const argsName = `args${context.outLineNum}`;
const blockArgs = (context.blockJSON[argsName] = context.blockJSON[argsName] || []);
@@ -1249,11 +1296,12 @@ class Runtime extends EventEmitter {
}
/**
* @returns {string} scratch-blocks XML description for all dynamic blocks, wrapped in <category> elements.
* @returns {Array.<object>} scratch-blocks XML for each category of extension blocks, in category order.
* @property {string} id - the category / extension ID
* @property {string} xml - the XML text for this category, starting with `<category>` and ending with `</category>`
*/
getBlocksXML () {
const xmlParts = [];
for (const categoryInfo of this._blockInfo) {
return this._blockInfo.map(categoryInfo => {
const {name, color1, color2} = categoryInfo;
const paletteBlocks = categoryInfo.blocks.filter(block => !block.info.hideFromPalette);
const colorXML = `colour="${color1}" secondaryColour="${color2}"`;
@@ -1274,12 +1322,12 @@ class Runtime extends EventEmitter {
statusButtonXML = 'showStatusButton="true"';
}
xmlParts.push(`<category name="${name}" id="${categoryInfo.id}"
${statusButtonXML} ${colorXML} ${menuIconXML}>`);
xmlParts.push.apply(xmlParts, paletteBlocks.map(block => block.xml));
xmlParts.push('</category>');
}
return xmlParts.join('\n');
return {
id: categoryInfo.id,
xml: `<category name="${name}" id="${categoryInfo.id}" ${statusButtonXML} ${colorXML} ${menuIconXML}>${
paletteBlocks.map(block => block.xml).join('')}</category>`
};
});
}
/**
@@ -1981,10 +2029,15 @@ class Runtime extends EventEmitter {
* @param {!Target} editingTarget New editing target.
*/
setEditingTarget (editingTarget) {
const oldEditingTarget = this._editingTarget;
this._editingTarget = editingTarget;
// Script glows must be cleared.
this._scriptGlowsPreviousFrame = [];
this._updateGlows();
if (oldEditingTarget !== this._editingTarget) {
this.requestToolboxExtensionsUpdate();
}
}
/**
@@ -2402,12 +2455,19 @@ class Runtime extends EventEmitter {
}
/**
* Emit an event that indicate that the blocks on the workspace need updating.
* Emit an event that indicates that the blocks on the workspace need updating.
*/
requestBlocksUpdate () {
this.emit(Runtime.BLOCKS_NEED_UPDATE);
}
/**
* Emit an event that indicates that the toolbox extension blocks need updating.
*/
requestToolboxExtensionsUpdate () {
this.emit(Runtime.TOOLBOX_EXTENSIONS_NEED_UPDATE);
}
/**
* Set up timers to repeatedly step in a browser.
*/
+48 -26
View File
@@ -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;
}
@@ -389,27 +398,40 @@ class ExtensionManager {
log.warn(`Ignoring opcode "${blockInfo.opcode}" for button with text: ${blockInfo.text}`);
}
break;
default:
default: {
if (!blockInfo.opcode) {
throw new Error('Missing opcode for block');
}
blockInfo.func = blockInfo.func ? this._sanitizeID(blockInfo.func) : blockInfo.opcode;
const funcName = blockInfo.func ? this._sanitizeID(blockInfo.func) : blockInfo.opcode;
// Avoid promise overhead if possible
if (dispatch._isRemoteService(serviceName)) {
blockInfo.func = dispatch.call.bind(dispatch, serviceName, blockInfo.func);
} else {
const serviceObject = dispatch.services[serviceName];
const func = serviceObject[blockInfo.func];
if (func) {
blockInfo.func = func.bind(serviceObject);
} else {
throw new Error(`Could not find extension block function called ${blockInfo.func}`);
const getBlockInfo = blockInfo.isDynamic ?
args => args && args.mutation && args.mutation.blockInfo :
() => blockInfo;
const callBlockFunc = (() => {
if (dispatch._isRemoteService(serviceName)) {
return (args, util, realBlockInfo) =>
dispatch.call(serviceName, funcName, args, util, realBlockInfo);
}
}
// avoid promise latency if we can call direct
const serviceObject = dispatch.services[serviceName];
if (!serviceObject[funcName]) {
// The function might show up later as a dynamic property of the service object
log.warn(`Could not find extension block function called ${funcName}`);
}
return (args, util, realBlockInfo) =>
serviceObject[funcName](args, util, realBlockInfo);
})();
blockInfo.func = (args, util) => {
const realBlockInfo = getBlockInfo(args);
// TODO: filter args using the keys of realBlockInfo.arguments? maybe only if sandboxed?
return callBlockFunc(args, util, realBlockInfo);
};
break;
}
}
return blockInfo;
}
+221 -200
View File
@@ -1424,206 +1424,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
}
]
}
}
};
}
+8 -2
View File
@@ -1142,8 +1142,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)
}
}
};
}
+31 -16
View File
@@ -134,7 +134,7 @@ class GdxFor {
* @type {BLE}
* @private
*/
this._scratchLinkSocket = null;
this._ble = null;
/**
* An @vernier/godirect Device
@@ -181,11 +181,11 @@ class GdxFor {
* Called by the runtime when user wants to scan for a peripheral.
*/
scan () {
if (this._scratchLinkSocket) {
this._scratchLinkSocket.disconnect();
if (this._ble) {
this._ble.disconnect();
}
this._scratchLinkSocket = new BLE(this._runtime, this._extensionId, {
this._ble = new BLE(this._runtime, this._extensionId, {
filters: [
{namePrefix: 'GDX-FOR'}
],
@@ -200,8 +200,8 @@ class GdxFor {
* @param {number} id - the id of the peripheral to connect to.
*/
connect (id) {
if (this._scratchLinkSocket) {
this._scratchLinkSocket.connectPeripheral(id);
if (this._ble) {
this._ble.connectPeripheral(id);
}
}
@@ -247,8 +247,8 @@ class GdxFor {
*/
isConnected () {
let connected = false;
if (this._scratchLinkSocket) {
connected = this._scratchLinkSocket.isConnected();
if (this._ble) {
connected = this._ble.isConnected();
}
return connected;
}
@@ -258,7 +258,7 @@ class GdxFor {
* @private
*/
_onConnect () {
const adapter = new ScratchLinkDeviceAdapter(this._scratchLinkSocket, BLEUUID);
const adapter = new ScratchLinkDeviceAdapter(this._ble, BLEUUID);
godirect.createDevice(adapter, {open: true, startMeasurements: false}).then(device => {
// Setup device
this._device = device;
@@ -278,7 +278,7 @@ class GdxFor {
});
});
this._timeoutID = window.setInterval(
() => this._scratchLinkSocket.handleDisconnectError(BLEDataStoppedError),
() => this._ble.handleDisconnectError(BLEDataStoppedError),
BLETimeout
);
});
@@ -322,7 +322,7 @@ class GdxFor {
// cancel disconnect timeout and start a new one
window.clearInterval(this._timeoutID);
this._timeoutID = window.setInterval(
() => this._scratchLinkSocket.handleDisconnectError(BLEDataStoppedError),
() => this._ble.handleDisconnectError(BLEDataStoppedError),
BLETimeout
);
}
@@ -773,11 +773,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
}
}
};
}
@@ -4,8 +4,8 @@ const Base64Util = require('../../util/base64-util');
* Adapter class
*/
class ScratchLinkDeviceAdapter {
constructor (scratchLinkSocket, {service, commandChar, responseChar}) {
this.scratchLinkSocket = scratchLinkSocket;
constructor (socket, {service, commandChar, responseChar}) {
this.socket = socket;
this._service = service;
this._commandChar = commandChar;
@@ -21,13 +21,13 @@ class ScratchLinkDeviceAdapter {
writeCommand (commandBuffer) {
const data = Base64Util.uint8ArrayToBase64(commandBuffer);
return this.scratchLinkSocket
return this.socket
.write(this._service, this._commandChar, data, 'base64');
}
setup ({onResponse}) {
this._deviceOnResponse = onResponse;
return this.scratchLinkSocket
return this.socket
.startNotifications(this._service, this._responseChar, this._onResponse);
// TODO:
+55 -49
View File
@@ -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)
}
}
};
}
+24 -6
View File
@@ -761,12 +761,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']
}
}
};
}
+8 -2
View File
@@ -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)
}
}
};
}
+4 -1
View File
@@ -477,7 +477,10 @@ class Scratch3PenBlocks {
}
],
menus: {
colorParam: this._initColorParam()
colorParam: {
acceptReporters: true,
items: this._initColorParam()
}
}
};
}
+8 -2
View File
@@ -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()
}
}
};
}
+4 -1
View File
@@ -146,7 +146,10 @@ class Scratch3TranslateBlocks {
}
],
menus: {
languages: this._supportedLanguages
languages: {
acceptReporters: true,
items: this._supportedLanguages
}
}
};
}
+12 -3
View File
@@ -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)
}
}
};
}
+151 -134
View File
@@ -297,7 +297,6 @@ class WeDo2Motor {
/**
* Start active braking on this motor. After a short time, the motor will turn off.
* // TODO: rename this to coastAfter?
*/
startBraking () {
if (this._power === 0) return;
@@ -1145,139 +1144,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: ['<', '>']
}
}
};
}
+7 -4
View File
@@ -109,18 +109,21 @@ class VirtualMachine extends EventEmitter {
this.runtime.on(Runtime.BLOCK_DRAG_END, (blocks, topBlockId) => {
this.emit(Runtime.BLOCK_DRAG_END, blocks, topBlockId);
});
this.runtime.on(Runtime.EXTENSION_ADDED, blocksInfo => {
this.emit(Runtime.EXTENSION_ADDED, blocksInfo);
this.runtime.on(Runtime.EXTENSION_ADDED, categoryInfo => {
this.emit(Runtime.EXTENSION_ADDED, categoryInfo);
});
this.runtime.on(Runtime.EXTENSION_FIELD_ADDED, (fieldName, fieldImplementation) => {
this.emit(Runtime.EXTENSION_FIELD_ADDED, fieldName, fieldImplementation);
});
this.runtime.on(Runtime.BLOCKSINFO_UPDATE, blocksInfo => {
this.emit(Runtime.BLOCKSINFO_UPDATE, blocksInfo);
this.runtime.on(Runtime.BLOCKSINFO_UPDATE, categoryInfo => {
this.emit(Runtime.BLOCKSINFO_UPDATE, categoryInfo);
});
this.runtime.on(Runtime.BLOCKS_NEED_UPDATE, () => {
this.emitWorkspaceUpdate();
});
this.runtime.on(Runtime.TOOLBOX_EXTENSIONS_NEED_UPDATE, () => {
this.extensionManager.refreshBlocks();
});
this.runtime.on(Runtime.PERIPHERAL_LIST_UPDATE, info => {
this.emit(Runtime.PERIPHERAL_LIST_UPDATE, info);
});
+18 -2
View File
@@ -1,6 +1,8 @@
const test = require('tap').test;
const Worker = require('tiny-worker');
const BlockType = require('../../src/extension-support/block-type');
const dispatch = require('../../src/dispatch/central-dispatch');
const VirtualMachine = require('../../src/virtual-machine');
@@ -33,8 +35,9 @@ class TestInternalExtension {
};
}
go () {
go (args, util, blockInfo) {
this.status.goCalled = true;
return blockInfo;
}
_buildAMenu () {
@@ -62,9 +65,22 @@ test('internal extension', t => {
t.type(func, 'function');
t.notOk(extension.status.goCalled);
func();
const goBlockInfo = func();
t.ok(extension.status.goCalled);
// The 'go' block returns its own blockInfo. Make sure it matches the expected info.
// Note that the extension parser fills in missing fields so there are more fields here than in `getInfo`.
const expectedBlockInfo = {
arguments: {},
blockAllThreads: false,
blockType: BlockType.COMMAND,
func: goBlockInfo.func, // Cheat since we don't have a good way to ensure we generate the same function
opcode: 'go',
terminal: false,
text: 'go'
};
t.deepEqual(goBlockInfo, expectedBlockInfo);
// There should be 2 menus - one is an array, one is the function to call.
t.equal(vm.runtime._blockInfo[0].menus.length, 2);
// First menu has 3 items.
+20 -1
View File
@@ -25,7 +25,7 @@ test('spec', t => {
t.type(b.getNextBlock, 'function');
t.type(b.getBranch, 'function');
t.type(b.getOpcode, 'function');
t.type(b.mutationToXML, 'function');
t.end();
});
@@ -239,6 +239,25 @@ test('getOpcode', t => {
t.end();
});
test('mutationToXML', t => {
const b = new Blocks(new Runtime());
const testStringRaw = '"arbitrary" & \'complicated\' test string';
const testStringEscaped = '\\&quot;arbitrary\\&quot; &amp; &apos;complicated&apos; test string';
const mutation = {
tagName: 'mutation',
children: [],
blockInfo: {
text: testStringRaw
}
};
const xml = b.mutationToXML(mutation);
t.equals(
xml,
`<mutation blockInfo="{&quot;text&quot;:&quot;${testStringEscaped}&quot;}"></mutation>`
);
t.end();
});
// Block events tests
test('create', t => {
const b = new Blocks(new Runtime());
+26
View File
@@ -0,0 +1,26 @@
const test = require('tap').test;
const mutationAdapter = require('../../src/engine/mutation-adapter');
test('spec', t => {
t.type(mutationAdapter, 'function');
t.end();
});
test('convert DOM to Scratch object', t => {
const testStringRaw = '"arbitrary" & \'complicated\' test string';
const testStringEscaped = '\\&quot;arbitrary\\&quot; &amp; &apos;complicated&apos; test string';
const xml = `<mutation blockInfo="{&quot;text&quot;:&quot;${testStringEscaped}&quot;}"></mutation>`;
const expectedMutation = {
tagName: 'mutation',
children: [],
blockInfo: {
text: testStringRaw
}
};
// TODO: do we want to test passing a DOM node to `mutationAdapter`? Node.js doesn't have built-in DOM support...
const mutationFromString = mutationAdapter(xml);
t.deepEqual(mutationFromString, expectedMutation);
t.end();
});
+38 -4
View File
@@ -11,6 +11,9 @@ const ScratchBlocksConstants = require('../../src/engine/scratch-blocks-constant
const testExtensionInfo = {
id: 'test',
name: 'fake test extension',
color1: '#111111',
color2: '#222222',
color3: '#333333',
blocks: [
{
func: 'MAKE_A_VARIABLE',
@@ -20,7 +23,8 @@ const testExtensionInfo = {
{
opcode: 'reporter',
blockType: BlockType.REPORTER,
text: 'simple text'
text: 'simple text',
blockIconURI: 'invalid icon URI' // trigger the 'scratch_extension' path
},
'---', // separator between groups of blocks in an extension
{
@@ -63,6 +67,14 @@ const testExtensionInfo = {
]
};
const testCategoryInfo = function (t, block) {
t.equal(block.json.category, 'fake test extension');
t.equal(block.json.colour, '#111111');
t.equal(block.json.colourSecondary, '#222222');
t.equal(block.json.colourTertiary, '#333333');
t.equal(block.json.inputsInline, true);
};
const testButton = function (t, button) {
t.same(button.json, null); // should be null or undefined
t.equal(button.xml, '<button text="this is a button" callbackKey="MAKE_A_VARIABLE"></button>');
@@ -70,13 +82,28 @@ const testButton = function (t, button) {
const testReporter = function (t, reporter) {
t.equal(reporter.json.type, 'test_reporter');
testCategoryInfo(t, reporter);
t.equal(reporter.json.checkboxInFlyout, true);
t.equal(reporter.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_ROUND);
t.equal(reporter.json.output, 'String');
t.notOk(reporter.json.hasOwnProperty('previousStatement'));
t.notOk(reporter.json.hasOwnProperty('nextStatement'));
t.equal(reporter.json.message0, 'simple text');
t.same(reporter.json.extensions, ['scratch_extension']);
t.equal(reporter.json.message0, '%1 %2simple text'); // "%1 %2" from the block icon
t.notOk(reporter.json.hasOwnProperty('message1'));
t.notOk(reporter.json.hasOwnProperty('args0'));
t.same(reporter.json.args0, [
// %1 in message0: the block icon
{
type: 'field_image',
src: 'invalid icon URI',
width: 40,
height: 40
},
// %2 in message0: separator between icon and text (only added when there's also an icon)
{
type: 'field_vertical_separator'
}
]);
t.notOk(reporter.json.hasOwnProperty('args1'));
t.equal(reporter.xml, '<block type="test_reporter"></block>');
};
@@ -88,9 +115,11 @@ const testSeparator = function (t, separator) {
const testCommand = function (t, command) {
t.equal(command.json.type, 'test_command');
testCategoryInfo(t, command);
t.equal(command.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE);
t.assert(command.json.hasOwnProperty('previousStatement'));
t.assert(command.json.hasOwnProperty('nextStatement'));
t.notOk(command.json.extensions && command.json.extensions.length); // OK if it's absent or empty
t.equal(command.json.message0, 'text with %1');
t.notOk(command.json.hasOwnProperty('message1'));
t.strictSame(command.json.args0[0], {
@@ -105,9 +134,11 @@ const testCommand = function (t, command) {
const testConditional = function (t, conditional) {
t.equal(conditional.json.type, 'test_ifElse');
testCategoryInfo(t, conditional);
t.equal(conditional.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE);
t.ok(conditional.json.hasOwnProperty('previousStatement'));
t.ok(conditional.json.hasOwnProperty('nextStatement'));
t.notOk(conditional.json.extensions && conditional.json.extensions.length); // OK if it's absent or empty
t.equal(conditional.json.message0, 'test if %1 is spiffy and if so then');
t.equal(conditional.json.message1, '%1'); // placeholder for substack #1
t.equal(conditional.json.message2, 'or elsewise');
@@ -133,9 +164,11 @@ const testConditional = function (t, conditional) {
const testLoop = function (t, loop) {
t.equal(loop.json.type, 'test_loop');
testCategoryInfo(t, loop);
t.equal(loop.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE);
t.ok(loop.json.hasOwnProperty('previousStatement'));
t.notOk(loop.json.hasOwnProperty('nextStatement')); // isTerminal is set on this block
t.notOk(loop.json.extensions && loop.json.extensions.length); // OK if it's absent or empty
t.equal(loop.json.message0, 'loopty %1 loops');
t.equal(loop.json.message1, '%1'); // placeholder for substack
t.equal(loop.json.message2, '%1'); // placeholder for loop arrow
@@ -160,7 +193,8 @@ const testLoop = function (t, loop) {
test('registerExtensionPrimitives', t => {
const runtime = new Runtime();
runtime.on(Runtime.EXTENSION_ADDED, blocksInfo => {
runtime.on(Runtime.EXTENSION_ADDED, categoryInfo => {
const blocksInfo = categoryInfo.blocks;
t.equal(blocksInfo.length, testExtensionInfo.blocks.length);
blocksInfo.forEach(blockInfo => {