mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-22 22:12:28 -05:00
support non-droppable menus in extensions
This commit is contained in:
parent
ab715901a6
commit
e7bf49c8df
3 changed files with 80 additions and 37 deletions
|
@ -311,7 +311,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 "non-droppable" menu: the menu will not accept reporters.
|
||||
rejectReporters: true,
|
||||
|
||||
// The `item` property may be an array or function name as in previous menu examples.
|
||||
items: [/*...*/] || 'getItemsForMenuC'
|
||||
}
|
||||
},
|
||||
|
||||
// Optional: translations (UNSTABLE - NOT YET SUPPORTED)
|
||||
|
|
|
@ -784,10 +784,7 @@ class Runtime extends EventEmitter {
|
|||
name: maybeFormatMessage(extensionInfo.name),
|
||||
showStatusButton: extensionInfo.showStatusButton,
|
||||
blockIconURI: extensionInfo.blockIconURI,
|
||||
menuIconURI: extensionInfo.menuIconURI,
|
||||
customFieldTypes: {},
|
||||
blocks: [],
|
||||
menus: []
|
||||
menuIconURI: extensionInfo.menuIconURI
|
||||
};
|
||||
|
||||
if (extensionInfo.color1) {
|
||||
|
@ -828,8 +825,6 @@ class Runtime extends EventEmitter {
|
|||
for (const categoryInfo of this._blockInfo) {
|
||||
if (extensionInfo.id === categoryInfo.id) {
|
||||
categoryInfo.name = maybeFormatMessage(extensionInfo.name);
|
||||
categoryInfo.blocks = [];
|
||||
categoryInfo.menus = [];
|
||||
this._fillExtensionCategory(categoryInfo, extensionInfo);
|
||||
|
||||
this.emit(Runtime.BLOCKSINFO_UPDATE, categoryInfo);
|
||||
|
@ -845,11 +840,17 @@ class Runtime extends EventEmitter {
|
|||
* @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 menuValue = extensionInfo.menus[menuName];
|
||||
const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuValue, categoryInfo);
|
||||
categoryInfo.menus.push(convertedMenu);
|
||||
categoryInfo.menuInfo[menuName] = menuValue;
|
||||
}
|
||||
}
|
||||
for (const fieldTypeName in extensionInfo.customFieldTypes) {
|
||||
|
@ -891,19 +892,21 @@ 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 {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
|
||||
* @param {CategoryInfo} categoryInfo - the category for this block
|
||||
* @returns {object} - a JSON-esque object ready for scratch-blocks' consumption
|
||||
* @private
|
||||
*/
|
||||
_buildMenuForScratchBlocks (menuName, menuItems, categoryInfo) {
|
||||
_buildMenuForScratchBlocks (menuName, menuInfo, categoryInfo) {
|
||||
const menuId = this._makeExtensionMenuId(menuName, categoryInfo.id);
|
||||
let options = null;
|
||||
if (typeof menuItems === 'function') {
|
||||
options = menuItems;
|
||||
let menuItems = null;
|
||||
if (typeof menuInfo.items === 'function') {
|
||||
menuItems = menuInfo.items;
|
||||
} else {
|
||||
const extensionMessageContext = this.makeMessageContextForTarget();
|
||||
options = menuItems.map(item => {
|
||||
menuItems = menuInfo.items.map(item => {
|
||||
const formattedItem = maybeFormatMessage(item, extensionMessageContext);
|
||||
switch (typeof formattedItem) {
|
||||
case 'string':
|
||||
|
@ -924,12 +927,13 @@ class Runtime extends EventEmitter {
|
|||
colour: categoryInfo.color1,
|
||||
colourSecondary: categoryInfo.color2,
|
||||
colourTertiary: categoryInfo.color3,
|
||||
outputShape: ScratchBlocksConstants.OUTPUT_SHAPE_ROUND,
|
||||
outputShape: menuInfo.rejectReporters ?
|
||||
ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE : ScratchBlocksConstants.OUTPUT_SHAPE_ROUND,
|
||||
args0: [
|
||||
{
|
||||
type: 'field_dropdown',
|
||||
name: menuName,
|
||||
options: options
|
||||
options: menuItems
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1226,29 +1230,52 @@ 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 = argInfo.menu && context.categoryInfo.menuInfo[argInfo.menu];
|
||||
if (menuInfo.rejectReporters) {
|
||||
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;
|
||||
shadowType = argTypeInfo.shadowType;
|
||||
fieldName = argTypeInfo.fieldType;
|
||||
}
|
||||
|
||||
// <value> is the ScratchBlocks name for a block input.
|
||||
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>`);
|
||||
if (fieldName) {
|
||||
context.inputList.push(`<field name="${fieldName}">${defaultValue}</field>`);
|
||||
}
|
||||
|
||||
if (shadowType) {
|
||||
context.inputList.push('</shadow>');
|
||||
}
|
||||
|
||||
if (valueName) {
|
||||
context.inputList.push('</value>');
|
||||
}
|
||||
|
||||
const argsName = `args${context.outLineNum}`;
|
||||
const blockArgs = (context.blockJSON[argsName] = context.blockJSON[argsName] || []);
|
||||
|
|
|
@ -310,15 +310,22 @@ class ExtensionManager {
|
|||
_prepareMenuInfo (serviceName, menus) {
|
||||
const menuNames = Object.getOwnPropertyNames(menus);
|
||||
for (let i = 0; i < menuNames.length; i++) {
|
||||
const item = menuNames[i];
|
||||
const menuName = menuNames[i];
|
||||
let menuInfo = menus[menuName];
|
||||
if (!menuInfo.items) {
|
||||
menuInfo = {
|
||||
items: menuInfo
|
||||
};
|
||||
menus[menuName] = menuInfo;
|
||||
}
|
||||
// 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') {
|
||||
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);
|
||||
menuInfo.items = this._getExtensionMenuItems.bind(this, serviceObject, menuItemFunctionName);
|
||||
}
|
||||
}
|
||||
return menus;
|
||||
|
@ -327,11 +334,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();
|
||||
|
@ -339,7 +346,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);
|
||||
|
@ -357,7 +364,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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue