mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-24 23:12:24 -05:00
Merge pull request #700 from cwillisf/extension-menus
Extensions: implement drop-down menus
This commit is contained in:
commit
a8ae7bc5b4
3 changed files with 106 additions and 20 deletions
|
@ -32,6 +32,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'
|
||||||
};
|
};
|
||||||
|
@ -43,9 +47,8 @@ const ArgumentTypeMap = (() => {
|
||||||
shadowType: 'text',
|
shadowType: 'text',
|
||||||
fieldType: 'TEXT'
|
fieldType: 'TEXT'
|
||||||
};
|
};
|
||||||
// @TODO: talk to Rachel & co. to figure out what goes here. Make it OK to not have a field. Add `check` support.
|
|
||||||
map[ArgumentType.BOOLEAN] = {
|
map[ArgumentType.BOOLEAN] = {
|
||||||
shadowType: ''
|
check: 'Boolean'
|
||||||
};
|
};
|
||||||
return map;
|
return map;
|
||||||
})();
|
})();
|
||||||
|
@ -398,6 +401,17 @@ 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) {
|
||||||
|
return `${extensionId}.menu.${escapeHtml(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.)
|
||||||
|
@ -410,11 +424,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;
|
||||||
|
@ -422,7 +444,54 @@ 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} 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
|
||||||
|
* @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) */
|
||||||
|
if (typeof menuItems === 'string') {
|
||||||
|
throw new Error(`Dynamic extension menus are not yet supported. Menu name: ${menuName}`);
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -457,14 +526,10 @@ class Runtime extends EventEmitter {
|
||||||
// Sanitize the placeholder to ensure valid XML
|
// Sanitize the placeholder to ensure valid XML
|
||||||
placeholder = placeholder.replace(/[<"&]/, '_');
|
placeholder = placeholder.replace(/[<"&]/, '_');
|
||||||
|
|
||||||
blockJSON.args0.push({
|
const argJSON = {
|
||||||
type: 'input_value',
|
type: 'input_value',
|
||||||
name: placeholder
|
name: placeholder
|
||||||
});
|
};
|
||||||
|
|
||||||
// scratch-blocks uses 1-based argument indexing
|
|
||||||
const argNum = blockJSON.args0.length;
|
|
||||||
argsMap[placeholder] = argNum;
|
|
||||||
|
|
||||||
const argInfo = blockInfo.arguments[placeholder] || {};
|
const argInfo = blockInfo.arguments[placeholder] || {};
|
||||||
const argTypeInfo = ArgumentTypeMap[argInfo.type] || {};
|
const argTypeInfo = ArgumentTypeMap[argInfo.type] || {};
|
||||||
|
@ -472,18 +537,38 @@ class Runtime extends EventEmitter {
|
||||||
'' :
|
'' :
|
||||||
escapeHtml(argInfo.defaultValue.toString()));
|
escapeHtml(argInfo.defaultValue.toString()));
|
||||||
|
|
||||||
|
if (argTypeInfo.check) {
|
||||||
|
argJSON.check = argTypeInfo.check;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
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.
|
||||||
inputList.push(`<value name="${placeholder}"><shadow type="${argTypeInfo.shadowType}">`);
|
// Boolean inputs don't need to specify a shadow in the XML.
|
||||||
|
if (shadowType) {
|
||||||
|
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
|
// <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>');
|
||||||
|
}
|
||||||
|
|
||||||
|
inputList.push('</value>');
|
||||||
|
|
||||||
|
// scratch-blocks uses 1-based argument indexing
|
||||||
|
blockJSON.args0.push(argJSON);
|
||||||
|
const argNum = blockJSON.args0.length;
|
||||||
|
argsMap[placeholder] = argNum;
|
||||||
return `%${argNum}`;
|
return `%${argNum}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const ArgumentType = {
|
const ArgumentType = {
|
||||||
|
ANGLE: 'angle',
|
||||||
BOOLEAN: 'Boolean',
|
BOOLEAN: 'Boolean',
|
||||||
COLOR: 'color',
|
COLOR: 'color',
|
||||||
NUMBER: 'number',
|
NUMBER: 'number',
|
||||||
|
|
|
@ -174,7 +174,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