mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-24 23:12:24 -05:00
Implement drop-down menus for extension blocks
Also, add `ANGLE` argument type (like `NUMBER` but adds an angle picker)
This commit is contained in:
parent
e4830dfe51
commit
c1681e54d5
3 changed files with 80 additions and 6 deletions
|
@ -33,6 +33,10 @@ const defaultBlockPackages = {
|
||||||
*/
|
*/
|
||||||
const ArgumentTypeMap = (() => {
|
const ArgumentTypeMap = (() => {
|
||||||
const map = {};
|
const map = {};
|
||||||
|
map[ArgumentType.ANGLE] = {
|
||||||
|
shadowType: 'math_angle',
|
||||||
|
fieldType: 'NUM'
|
||||||
|
};
|
||||||
map[ArgumentType.COLOR] = {
|
map[ArgumentType.COLOR] = {
|
||||||
shadowType: 'colour_picker'
|
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.
|
* Register the primitives provided by an extension.
|
||||||
* @param {ExtensionInfo} extensionInfo - information about the extension (id, blocks, etc.)
|
* @param {ExtensionInfo} extensionInfo - information about the extension (id, blocks, etc.)
|
||||||
|
@ -400,11 +416,19 @@ class Runtime extends EventEmitter {
|
||||||
color1: '#FF6680',
|
color1: '#FF6680',
|
||||||
color2: '#FF4D6A',
|
color2: '#FF4D6A',
|
||||||
color3: '#FF3355',
|
color3: '#FF3355',
|
||||||
blocks: []
|
blocks: [],
|
||||||
|
menus: []
|
||||||
};
|
};
|
||||||
|
|
||||||
this._blockInfo.push(categoryInfo);
|
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) {
|
for (const blockInfo of extensionInfo.blocks) {
|
||||||
const convertedBlock = this._convertForScratchBlocks(blockInfo, categoryInfo);
|
const convertedBlock = this._convertForScratchBlocks(blockInfo, categoryInfo);
|
||||||
const opcode = convertedBlock.json.type;
|
const opcode = convertedBlock.json.type;
|
||||||
|
@ -412,7 +436,51 @@ class Runtime extends EventEmitter {
|
||||||
this._primitives[opcode] = convertedBlock.info.func;
|
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 argTypeInfo = ArgumentTypeMap[argInfo.type] || {};
|
||||||
const defaultValue = (typeof argInfo.defaultValue === 'undefined' ? '' : argInfo.defaultValue.toString());
|
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.
|
// <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.
|
// 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
|
// <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.
|
// text input and therefore don't need a field element.
|
||||||
if (argTypeInfo.fieldType) {
|
if (fieldType) {
|
||||||
inputList.push(`<field name="${argTypeInfo.fieldType}">${defaultValue}</field>`);
|
inputList.push(`<field name="${fieldType}">${defaultValue}</field>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
inputList.push('</shadow></value>');
|
inputList.push('</shadow></value>');
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const ArgumentType = {
|
const ArgumentType = {
|
||||||
|
ANGLE: 'angle',
|
||||||
BOOLEAN: 'Boolean',
|
BOOLEAN: 'Boolean',
|
||||||
COLOR: 'color',
|
COLOR: 'color',
|
||||||
NUMBER: 'number',
|
NUMBER: 'number',
|
||||||
|
|
|
@ -151,7 +151,7 @@ class ExtensionManager {
|
||||||
result.push(this._prepareBlockInfo(serviceName, blockInfo));
|
result.push(this._prepareBlockInfo(serviceName, blockInfo));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// TODO: more meaningful error reporting
|
// 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;
|
return result;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
Loading…
Reference in a new issue