mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-07-08 20:14:00 -04: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.
|
// Dynamic menu: returns an array as above.
|
||||||
// Called each time the menu is opened.
|
// 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)
|
// Optional: translations (UNSTABLE - NOT YET SUPPORTED)
|
||||||
|
|
|
@ -784,10 +784,7 @@ class Runtime extends EventEmitter {
|
||||||
name: maybeFormatMessage(extensionInfo.name),
|
name: maybeFormatMessage(extensionInfo.name),
|
||||||
showStatusButton: extensionInfo.showStatusButton,
|
showStatusButton: extensionInfo.showStatusButton,
|
||||||
blockIconURI: extensionInfo.blockIconURI,
|
blockIconURI: extensionInfo.blockIconURI,
|
||||||
menuIconURI: extensionInfo.menuIconURI,
|
menuIconURI: extensionInfo.menuIconURI
|
||||||
customFieldTypes: {},
|
|
||||||
blocks: [],
|
|
||||||
menus: []
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (extensionInfo.color1) {
|
if (extensionInfo.color1) {
|
||||||
|
@ -828,8 +825,6 @@ class Runtime extends EventEmitter {
|
||||||
for (const categoryInfo of this._blockInfo) {
|
for (const categoryInfo of this._blockInfo) {
|
||||||
if (extensionInfo.id === categoryInfo.id) {
|
if (extensionInfo.id === categoryInfo.id) {
|
||||||
categoryInfo.name = maybeFormatMessage(extensionInfo.name);
|
categoryInfo.name = maybeFormatMessage(extensionInfo.name);
|
||||||
categoryInfo.blocks = [];
|
|
||||||
categoryInfo.menus = [];
|
|
||||||
this._fillExtensionCategory(categoryInfo, extensionInfo);
|
this._fillExtensionCategory(categoryInfo, extensionInfo);
|
||||||
|
|
||||||
this.emit(Runtime.BLOCKSINFO_UPDATE, categoryInfo);
|
this.emit(Runtime.BLOCKSINFO_UPDATE, categoryInfo);
|
||||||
|
@ -845,11 +840,17 @@ class Runtime extends EventEmitter {
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_fillExtensionCategory (categoryInfo, extensionInfo) {
|
_fillExtensionCategory (categoryInfo, extensionInfo) {
|
||||||
|
categoryInfo.blocks = [];
|
||||||
|
categoryInfo.customFieldTypes = {};
|
||||||
|
categoryInfo.menus = [];
|
||||||
|
categoryInfo.menuInfo = {};
|
||||||
|
|
||||||
for (const menuName in extensionInfo.menus) {
|
for (const menuName in extensionInfo.menus) {
|
||||||
if (extensionInfo.menus.hasOwnProperty(menuName)) {
|
if (extensionInfo.menus.hasOwnProperty(menuName)) {
|
||||||
const menuItems = extensionInfo.menus[menuName];
|
const menuValue = extensionInfo.menus[menuName];
|
||||||
const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuItems, categoryInfo);
|
const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuValue, categoryInfo);
|
||||||
categoryInfo.menus.push(convertedMenu);
|
categoryInfo.menus.push(convertedMenu);
|
||||||
|
categoryInfo.menuInfo[menuName] = menuValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const fieldTypeName in extensionInfo.customFieldTypes) {
|
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.
|
* 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 {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
|
* @param {CategoryInfo} categoryInfo - the category for this block
|
||||||
* @returns {object} - a JSON-esque object ready for scratch-blocks' consumption
|
* @returns {object} - a JSON-esque object ready for scratch-blocks' consumption
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_buildMenuForScratchBlocks (menuName, menuItems, categoryInfo) {
|
_buildMenuForScratchBlocks (menuName, menuInfo, categoryInfo) {
|
||||||
const menuId = this._makeExtensionMenuId(menuName, categoryInfo.id);
|
const menuId = this._makeExtensionMenuId(menuName, categoryInfo.id);
|
||||||
let options = null;
|
let menuItems = null;
|
||||||
if (typeof menuItems === 'function') {
|
if (typeof menuInfo.items === 'function') {
|
||||||
options = menuItems;
|
menuItems = menuInfo.items;
|
||||||
} else {
|
} else {
|
||||||
const extensionMessageContext = this.makeMessageContextForTarget();
|
const extensionMessageContext = this.makeMessageContextForTarget();
|
||||||
options = menuItems.map(item => {
|
menuItems = menuInfo.items.map(item => {
|
||||||
const formattedItem = maybeFormatMessage(item, extensionMessageContext);
|
const formattedItem = maybeFormatMessage(item, extensionMessageContext);
|
||||||
switch (typeof formattedItem) {
|
switch (typeof formattedItem) {
|
||||||
case 'string':
|
case 'string':
|
||||||
|
@ -924,12 +927,13 @@ class Runtime extends EventEmitter {
|
||||||
colour: categoryInfo.color1,
|
colour: categoryInfo.color1,
|
||||||
colourSecondary: categoryInfo.color2,
|
colourSecondary: categoryInfo.color2,
|
||||||
colourTertiary: categoryInfo.color3,
|
colourTertiary: categoryInfo.color3,
|
||||||
outputShape: ScratchBlocksConstants.OUTPUT_SHAPE_ROUND,
|
outputShape: menuInfo.rejectReporters ?
|
||||||
|
ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE : ScratchBlocksConstants.OUTPUT_SHAPE_ROUND,
|
||||||
args0: [
|
args0: [
|
||||||
{
|
{
|
||||||
type: 'field_dropdown',
|
type: 'field_dropdown',
|
||||||
name: menuName,
|
name: menuName,
|
||||||
options: options
|
options: menuItems
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1226,29 +1230,52 @@ class Runtime extends EventEmitter {
|
||||||
argJSON.check = argTypeInfo.check;
|
argJSON.check = argTypeInfo.check;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shadowType = (argInfo.menu ?
|
let valueName;
|
||||||
this._makeExtensionMenuId(argInfo.menu, context.categoryInfo.id) :
|
let shadowType;
|
||||||
argTypeInfo.shadowType);
|
let fieldName;
|
||||||
const fieldType = argInfo.menu || argTypeInfo.fieldType;
|
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.
|
// <value> is the ScratchBlocks name for a block input.
|
||||||
|
if (valueName) {
|
||||||
context.inputList.push(`<value name="${placeholder}">`);
|
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.
|
// 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.
|
// Boolean inputs don't need to specify a shadow in the XML.
|
||||||
if (shadowType) {
|
if (shadowType) {
|
||||||
context.inputList.push(`<shadow type="${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
|
// <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 (fieldType) {
|
if (fieldName) {
|
||||||
context.inputList.push(`<field name="${fieldType}">${defaultValue}</field>`);
|
context.inputList.push(`<field name="${fieldName}">${defaultValue}</field>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shadowType) {
|
||||||
context.inputList.push('</shadow>');
|
context.inputList.push('</shadow>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (valueName) {
|
||||||
context.inputList.push('</value>');
|
context.inputList.push('</value>');
|
||||||
|
}
|
||||||
|
|
||||||
const argsName = `args${context.outLineNum}`;
|
const argsName = `args${context.outLineNum}`;
|
||||||
const blockArgs = (context.blockJSON[argsName] = context.blockJSON[argsName] || []);
|
const blockArgs = (context.blockJSON[argsName] = context.blockJSON[argsName] || []);
|
||||||
|
|
|
@ -310,15 +310,22 @@ class ExtensionManager {
|
||||||
_prepareMenuInfo (serviceName, menus) {
|
_prepareMenuInfo (serviceName, menus) {
|
||||||
const menuNames = Object.getOwnPropertyNames(menus);
|
const menuNames = Object.getOwnPropertyNames(menus);
|
||||||
for (let i = 0; i < menuNames.length; i++) {
|
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
|
// 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.
|
// extension object to call to populate the menu whenever it is opened.
|
||||||
// Set up the binding for the function object here so
|
// Set up the binding for the function object here so
|
||||||
// we can use it later when converting the menu for Scratch Blocks.
|
// 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 serviceObject = dispatch.services[serviceName];
|
||||||
const menuName = menus[item];
|
menuInfo.items = this._getExtensionMenuItems.bind(this, serviceObject, menuItemFunctionName);
|
||||||
menus[item] = this._getExtensionMenuItems.bind(this, serviceObject, menuName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return menus;
|
return menus;
|
||||||
|
@ -327,11 +334,11 @@ class ExtensionManager {
|
||||||
/**
|
/**
|
||||||
* Fetch the items for a particular extension menu, providing the target ID for context.
|
* Fetch the items for a particular extension menu, providing the target ID for context.
|
||||||
* @param {object} extensionObject - the extension object providing the menu.
|
* @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.
|
* @returns {Array} menu items ready for scratch-blocks.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_getExtensionMenuItems (extensionObject, menuName) {
|
_getExtensionMenuItems (extensionObject, menuItemFunctionName) {
|
||||||
// Fetch the items appropriate for the target currently being edited. This assumes that menus only
|
// 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.
|
// collect items when opened by the user while editing a particular target.
|
||||||
const editingTarget = this.runtime.getEditingTarget() || this.runtime.getTargetForStage();
|
const editingTarget = this.runtime.getEditingTarget() || this.runtime.getTargetForStage();
|
||||||
|
@ -339,7 +346,7 @@ class ExtensionManager {
|
||||||
const extensionMessageContext = this.runtime.makeMessageContextForTarget(editingTarget);
|
const extensionMessageContext = this.runtime.makeMessageContextForTarget(editingTarget);
|
||||||
|
|
||||||
// TODO: Fix this to use dispatch.call when extensions are running in workers.
|
// 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(
|
const menuItems = menuFunc.call(extensionObject, editingTargetID).map(
|
||||||
item => {
|
item => {
|
||||||
item = maybeFormatMessage(item, extensionMessageContext);
|
item = maybeFormatMessage(item, extensionMessageContext);
|
||||||
|
@ -357,7 +364,7 @@ class ExtensionManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!menuItems || menuItems.length < 1) {
|
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;
|
return menuItems;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue