diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 532a35388..08a3e95a0 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -556,6 +556,14 @@ class Runtime extends EventEmitter { return 'EXTENSION_ADDED'; } + /** + * Event name for reporting that an extension as asked for a custom field to be added + * @const {string} + */ + static get EXTENSION_FIELD_ADDED () { + return 'EXTENSION_FIELD_ADDED'; + } + /** * Event name for updating the available set of peripheral devices. * This causes the peripheral connection modal to update a list of @@ -779,6 +787,7 @@ class Runtime extends EventEmitter { color1: extensionInfo.colour || '#0FBD8C', color2: extensionInfo.colourSecondary || '#0DA57A', color3: extensionInfo.colourTertiary || '#0B8E69', + customFieldTypes: {}, blocks: [], menus: [] }; @@ -787,7 +796,23 @@ class Runtime extends EventEmitter { this._fillExtensionCategory(categoryInfo, extensionInfo); - this.emit(Runtime.EXTENSION_ADDED, categoryInfo.blocks.concat(categoryInfo.menus)); + const fieldTypeDefinitionsForScratch = []; + for (const fieldTypeName in categoryInfo.customFieldTypes) { + if (extensionInfo.customFieldTypes.hasOwnProperty(fieldTypeName)) { + const fieldTypeInfo = categoryInfo.customFieldTypes[fieldTypeName]; + fieldTypeDefinitionsForScratch.push(fieldTypeInfo.scratchBlocksDefinition); + + // Emit events for custom field types from extension + this.emit(Runtime.EXTENSION_FIELD_ADDED, { + name: `field_${fieldTypeInfo.extendedName}`, + implementation: fieldTypeInfo.fieldImplementation + }); + } + } + + const allBlocks = fieldTypeDefinitionsForScratch.concat(categoryInfo.blocks).concat(categoryInfo.menus); + + this.emit(Runtime.EXTENSION_ADDED, allBlocks); } /** @@ -811,7 +836,8 @@ class Runtime extends EventEmitter { } /** - * Read extension information, convert menus and blocks, and store the results in the provided category object. + * Read extension information, convert menus, blocks and custom field types + * and store the results in the provided category object. * @param {CategoryInfo} categoryInfo - the category to be filled * @param {ExtensionMetadata} extensionInfo - the extension metadata to read * @private @@ -824,6 +850,19 @@ class Runtime extends EventEmitter { categoryInfo.menus.push(convertedMenu); } } + for (const fieldTypeName in extensionInfo.customFieldTypes) { + if (extensionInfo.customFieldTypes.hasOwnProperty(fieldTypeName)) { + const fieldType = extensionInfo.customFieldTypes[fieldTypeName]; + const fieldTypeInfo = this._buildCustomFieldInfo( + fieldTypeName, + fieldType, + extensionInfo.id, + categoryInfo + ); + + categoryInfo.customFieldTypes[fieldTypeName] = fieldTypeInfo; + } + } for (const blockInfo of extensionInfo.blocks) { if (blockInfo === '---') { @@ -897,6 +936,55 @@ class Runtime extends EventEmitter { }; } + _buildCustomFieldInfo (fieldName, fieldInfo, extensionId, categoryInfo) { + const extendedName = `${extensionId}_${fieldName}`; + return { + fieldName: fieldName, + extendedName: extendedName, + argumentTypeInfo: { + shadowType: extendedName, + fieldType: `field_${extendedName}` + }, + scratchBlocksDefinition: this._buildCustomFieldTypeForScratchBlocks( + extendedName, + fieldInfo.output, + fieldInfo.outputShape, + categoryInfo + ), + fieldImplementation: fieldInfo.implementation + }; + } + + /** + * Build the scratch-blocks JSON needed for a fieldType. + * Custom field types need to be namespaced to the extension so that extensions can't interfere with each other + * @param {string} fieldName - The name of the field + * @param {string} output - The output of the field + * @param {number} outputShape - Shape of the field (from ScratchBlocksConstants) + * @param {object} categoryInfo - The category the field belongs to (Used to set its colors) + * @returns {object} - Object to be inserted into scratch-blocks + */ + _buildCustomFieldTypeForScratchBlocks (fieldName, output, outputShape, categoryInfo) { + return { + json: { + type: fieldName, + message0: '%1', + inputsInline: true, + output: output, + colour: categoryInfo.color1, + colourSecondary: categoryInfo.color2, + colourTertiary: categoryInfo.color3, + outputShape: outputShape, + args0: [ + { + name: `field_${fieldName}`, + type: `field_${fieldName}` + } + ] + } + }; + } + /** * Convert ExtensionBlockMetadata into scratch-blocks JSON & XML, and generate a proxy function. * @param {ExtensionBlockMetadata} blockInfo - the block to convert @@ -1065,10 +1153,16 @@ class Runtime extends EventEmitter { }; const argInfo = context.blockInfo.arguments[placeholder] || {}; - const argTypeInfo = ArgumentTypeMap[argInfo.type] || {}; - const defaultValue = (typeof argInfo.defaultValue === 'undefined' ? - '' : - escapeHtml(maybeFormatMessage(argInfo.defaultValue, this.makeMessageContextForTarget()).toString())); + let argTypeInfo = ArgumentTypeMap[argInfo.type] || {}; + + // Field type not a standard field type, see if extension has registered custom field type + if (!ArgumentTypeMap[argInfo.type] && context.categoryInfo.customFieldTypes[argInfo.type]) { + argTypeInfo = context.categoryInfo.customFieldTypes[argInfo.type].argumentTypeInfo; + } + + const defaultValue = + typeof argInfo.defaultValue === 'undefined' ? '' : + escapeHtml(maybeFormatMessage(argInfo.defaultValue, this.makeMessageContextForTarget()).toString()); if (argTypeInfo.check) { argJSON.check = argTypeInfo.check; @@ -1103,6 +1197,7 @@ class Runtime extends EventEmitter { blockArgs.push(argJSON); const argNum = blockArgs.length; context.argsMap[placeholder] = argNum; + return `%${argNum}`; } diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 7763f4ac1..e762ce196 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -110,6 +110,9 @@ class VirtualMachine extends EventEmitter { this.runtime.on(Runtime.EXTENSION_ADDED, blocksInfo => { this.emit(Runtime.EXTENSION_ADDED, blocksInfo); }); + this.runtime.on(Runtime.EXTENSION_FIELD_ADDED, (fieldName, fieldImplementation) => { + this.emit(Runtime.EXTENSION_FIELD_ADDED, fieldName, fieldImplementation); + }); this.runtime.on(Runtime.BLOCKSINFO_UPDATE, blocksInfo => { this.emit(Runtime.BLOCKSINFO_UPDATE, blocksInfo); });