Implement drop-down menus for extension blocks

Also, add `ANGLE` argument type (like `NUMBER` but adds an angle picker)
This commit is contained in:
Christopher Willis-Ford 2017-10-06 13:37:59 -07:00
parent e4830dfe51
commit c1681e54d5
3 changed files with 80 additions and 6 deletions

View file

@ -33,6 +33,10 @@ const defaultBlockPackages = {
*/
const ArgumentTypeMap = (() => {
const map = {};
map[ArgumentType.ANGLE] = {
shadowType: 'math_angle',
fieldType: 'NUM'
};
map[ArgumentType.COLOR] = {
shadowType: 'colour_picker'
};
@ -388,6 +392,18 @@ class Runtime extends EventEmitter {
}
}
/**
* Generate an extension-specific menu ID.
* @param {string} menuName - the name of the menu.
* @param {string} extensionId - the ID of the extension hosting the menu.
* @returns {string} - the constructed ID.
* @private
*/
_makeExtensionMenuId (menuName, extensionId) {
/** @TODO: protect against XML characters in menu name */
return `${extensionId}.menu.${menuName}`;
}
/**
* Register the primitives provided by an extension.
* @param {ExtensionInfo} extensionInfo - information about the extension (id, blocks, etc.)
@ -400,11 +416,19 @@ class Runtime extends EventEmitter {
color1: '#FF6680',
color2: '#FF4D6A',
color3: '#FF3355',
blocks: []
blocks: [],
menus: []
};
this._blockInfo.push(categoryInfo);
for (const menuName in extensionInfo.menus) {
if (extensionInfo.menus.hasOwnProperty(menuName)) {
const menuItems = extensionInfo.menus[menuName];
const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuItems, extensionInfo);
categoryInfo.menus.push(convertedMenu);
}
}
for (const blockInfo of extensionInfo.blocks) {
const convertedBlock = this._convertForScratchBlocks(blockInfo, categoryInfo);
const opcode = convertedBlock.json.type;
@ -412,7 +436,51 @@ class Runtime extends EventEmitter {
this._primitives[opcode] = convertedBlock.info.func;
}
this.emit(Runtime.EXTENSION_ADDED, categoryInfo.blocks);
this.emit(Runtime.EXTENSION_ADDED, categoryInfo.blocks.concat(categoryInfo.menus));
}
/**
* Build the scratch-blocks JSON for a menu. Note that scratch-blocks treats menus as a special kind of block.
* @param {string} menuName - the name of the menu
* @param {array|string} menuItems - the list of menu items, or the name of an extension method to collect them.
* @param {CategoryInfo} categoryInfo - the category for this block
* @returns {object} - a JSON-esque object ready for scratch-blocks' consumption
* @private
*/
_buildMenuForScratchBlocks (menuName, menuItems, categoryInfo) {
const menuId = this._makeExtensionMenuId(menuName, categoryInfo.id);
/** @TODO: support dynamic menus when 'menuItems' is a method name string (see extension spec) */
const options = menuItems.map(item => {
switch (typeof item) {
case 'string':
return [item, item];
case 'object':
return [item.text, item.value];
default:
throw new Error(`Can't interpret menu item: ${item}`);
}
});
return {
json: {
message0: '%1',
type: menuId,
inputsInline: true,
output: 'String',
colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3,
outputShape: ScratchBlocksConstants.OUTPUT_SHAPE_ROUND,
args0: [
{
type: 'field_dropdown',
name: menuName,
options: options
}
]
}
};
}
/**
@ -460,14 +528,19 @@ class Runtime extends EventEmitter {
const argTypeInfo = ArgumentTypeMap[argInfo.type] || {};
const defaultValue = (typeof argInfo.defaultValue === 'undefined' ? '' : argInfo.defaultValue.toString());
const shadowType = (argInfo.menu ?
this._makeExtensionMenuId(argInfo.menu, categoryInfo.id) :
argTypeInfo.shadowType);
const fieldType = argInfo.menu || argTypeInfo.fieldType;
// <value> is the ScratchBlocks name for a block input.
// The <shadow> is a placeholder for a reporter and is visible when there's no reporter in this input.
inputList.push(`<value name="${placeholder}"><shadow type="${argTypeInfo.shadowType}">`);
inputList.push(`<value name="${placeholder}"><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 (argTypeInfo.fieldType) {
inputList.push(`<field name="${argTypeInfo.fieldType}">${defaultValue}</field>`);
if (fieldType) {
inputList.push(`<field name="${fieldType}">${defaultValue}</field>`);
}
inputList.push('</shadow></value>');

View file

@ -1,4 +1,5 @@
const ArgumentType = {
ANGLE: 'angle',
BOOLEAN: 'Boolean',
COLOR: 'color',
NUMBER: 'number',

View file

@ -151,7 +151,7 @@ class ExtensionManager {
result.push(this._prepareBlockInfo(serviceName, blockInfo));
} catch (e) {
// TODO: more meaningful error reporting
log.error(`Skipping malformed block: ${JSON.stringify(e)}`);
log.error(`Error processing block: ${e.message}, Block:\n${JSON.stringify(blockInfo)}`);
}
return result;
}, []);