Merge pull request #4942 from LLK/revert-4940-revert-4790

Revert "Revert "Merge pull request #4790 from LLK/e16n""
This commit is contained in:
picklesrus authored and GitHub committed 2019-06-20 17:53:23 -04:00
commit cf62045e26
5 files changed
+382 -21

No files matched your search

+1 -1
View File
@@ -111,7 +111,7 @@
"scratch-render": "0.1.0-prerelease.20190605151415",
"scratch-storage": "1.3.2",
"scratch-svg-renderer": "0.2.0-prerelease.20190523193400",
"scratch-vm": "0.2.0-prerelease.20190610152034",
"scratch-vm": "0.2.0-prerelease.20190619042313",
"selenium-webdriver": "3.6.0",
"startaudiocontext": "1.2.1",
"style-loader": "^0.23.0",
+38 -7
View File
@@ -17,6 +17,7 @@ import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx';
import {STAGE_DISPLAY_SIZES} from '../lib/layout-constants';
import DropAreaHOC from '../lib/drop-area-hoc.jsx';
import DragConstants from '../lib/drag-constants';
import defineDynamicBlock from '../lib/define-dynamic-block';
import {connect} from 'react-redux';
import {updateToolbox} from '../reducers/toolbox';
@@ -393,20 +394,50 @@ class Blocks extends React.Component {
// workspace to be 'undone' here.
this.workspace.clearUndo();
}
handleExtensionAdded (blocksInfo) {
// select JSON from each block info object then reject the pseudo-blocks which don't have JSON, like separators
// this actually defines blocks and MUST run regardless of the UI state
this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => blockInfo.json).filter(x => x));
handleExtensionAdded (categoryInfo) {
const defineBlocks = blockInfoArray => {
if (blockInfoArray && blockInfoArray.length > 0) {
const staticBlocksJson = [];
const dynamicBlocksInfo = [];
blockInfoArray.forEach(blockInfo => {
if (blockInfo.info && blockInfo.info.isDynamic) {
dynamicBlocksInfo.push(blockInfo);
} else if (blockInfo.json) {
staticBlocksJson.push(blockInfo.json);
}
// otherwise it's a non-block entry such as '---'
});
// Update the toolbox with new blocks
this.ScratchBlocks.defineBlocksWithJsonArray(staticBlocksJson);
dynamicBlocksInfo.forEach(blockInfo => {
// This is creating the block factory / constructor -- NOT a specific instance of the block.
// The factory should only know static info about the block: the category info and the opcode.
// Anything else will be picked up from the XML attached to the block instance.
const extendedOpcode = `${categoryInfo.id}_${blockInfo.info.opcode}`;
const blockDefinition =
defineDynamicBlock(this.ScratchBlocks, categoryInfo, blockInfo, extendedOpcode);
this.ScratchBlocks.Blocks[extendedOpcode] = blockDefinition;
});
}
};
// scratch-blocks implements a menu or custom field as a special kind of block ("shadow" block)
// these actually define blocks and MUST run regardless of the UI state
defineBlocks(
Object.getOwnPropertyNames(categoryInfo.customFieldTypes)
.map(fieldTypeName => categoryInfo.customFieldTypes[fieldTypeName].scratchBlocksDefinition));
defineBlocks(categoryInfo.menus);
defineBlocks(categoryInfo.blocks);
// Update the toolbox with new blocks if possible
const toolboxXML = this.getToolboxXML();
if (toolboxXML) {
this.props.updateToolboxState(toolboxXML);
}
}
handleBlocksInfoUpdate (blocksInfo) {
handleBlocksInfoUpdate (categoryInfo) {
// @todo Later we should replace this to avoid all the warnings from redefining blocks.
this.handleExtensionAdded(blocksInfo);
this.handleExtensionAdded(categoryInfo);
}
handleCategorySelected (categoryId) {
const extension = extensionData.find(ext => ext.extensionId === categoryId);
+108
View File
@@ -0,0 +1,108 @@
// TODO: access `BlockType` and `ArgumentType` without reaching into VM
// Should we move these into a new extension support module or something?
import ArgumentType from 'scratch-vm/src/extension-support/argument-type';
import BlockType from 'scratch-vm/src/extension-support/block-type';
/**
* Define a block using extension info which has the ability to dynamically determine (and update) its layout.
* This functionality is used for extension blocks which can change its properties based on different state
* information. For example, the `control_stop` block changes its shape based on which menu item is selected
* and a variable block changes its text to reflect the variable name without using an editable field.
* @param {object} ScratchBlocks - The ScratchBlocks name space.
* @param {object} categoryInfo - Information about this block's extension category, including any menus and icons.
* @param {object} staticBlockInfo - The base block information before any dynamic changes.
* @param {string} extendedOpcode - The opcode for the block (including the extension ID).
*/
// TODO: grow this until it can fully replace `_convertForScratchBlocks` in the VM runtime
const defineDynamicBlock = (ScratchBlocks, categoryInfo, staticBlockInfo, extendedOpcode) => ({
init: function () {
const blockJson = {
type: extendedOpcode,
inputsInline: true,
category: categoryInfo.name,
colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3
};
// There is a scratch-blocks / Blockly extension called "scratch_extension" which adjusts the styling of
// blocks to allow for an icon, a feature of Scratch extension blocks. However, Scratch "core" extension
// blocks don't have icons and so they should not use 'scratch_extension'. Adding a scratch-blocks / Blockly
// extension after `jsonInit` isn't fully supported (?), so we decide now whether there will be an icon.
if (staticBlockInfo.blockIconURI || categoryInfo.blockIconURI) {
blockJson.extensions = ['scratch_extension'];
}
// initialize the basics of the block, to be overridden & extended later by `domToMutation`
this.jsonInit(blockJson);
// initialize the cached block info used to carry block info from `domToMutation` to `mutationToDom`
this.blockInfoText = '{}';
// we need a block info update (through `domToMutation`) before we have a completely initialized block
this.needsBlockInfoUpdate = true;
},
mutationToDom: function () {
const container = document.createElement('mutation');
container.setAttribute('blockInfo', this.blockInfoText);
return container;
},
domToMutation: function (xmlElement) {
const blockInfoText = xmlElement.getAttribute('blockInfo');
if (!blockInfoText) return;
if (!this.needsBlockInfoUpdate) {
throw new Error('Attempted to update block info twice');
}
delete this.needsBlockInfoUpdate;
this.blockInfoText = blockInfoText;
const blockInfo = JSON.parse(blockInfoText);
switch (blockInfo.blockType) {
case BlockType.COMMAND:
case BlockType.CONDITIONAL:
case BlockType.LOOP:
this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE);
this.setPreviousStatement(true);
this.setNextStatement(!blockInfo.isTerminal);
break;
case BlockType.REPORTER:
this.setOutput(true);
this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_ROUND);
if (!blockInfo.disableMonitor) {
this.setCheckboxInFlyout(true);
}
break;
case BlockType.BOOLEAN:
this.setOutput(true);
this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_HEXAGONAL);
break;
case BlockType.HAT:
case BlockType.EVENT:
this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE);
this.setNextStatement(true);
break;
}
if (blockInfo.color1 || blockInfo.color2 || blockInfo.color3) {
// `setColour` handles undefined parameters by adjusting defined colors
this.setColour(blockInfo.color1, blockInfo.color2, blockInfo.color3);
}
// Layout block arguments
// TODO handle E/C Blocks
const blockText = blockInfo.text;
const args = [];
let argCount = 0;
const scratchBlocksStyleText = blockText.replace(/\[(.+?)]/g, (match, argName) => {
const arg = blockInfo.arguments[argName];
switch (arg.type) {
case ArgumentType.STRING:
args.push({type: 'input_value', name: argName});
break;
case ArgumentType.BOOLEAN:
args.push({type: 'input_value', name: argName, check: 'Boolean'});
break;
}
return `%${++argCount}`;
});
this.interpolate_(scratchBlocksStyleText, args);
}
});
export default defineDynamicBlock;
+36 -13
View File
@@ -716,13 +716,16 @@ const xmlClose = '</xml>';
/**
* @param {!boolean} isStage - Whether the toolbox is for a stage-type target.
* @param {?string} targetId - The current editing target
* @param {?string} categoriesXML - null for default toolbox, or an XML string with <category> elements.
* @param {?Array.<object>} categoriesXML - optional array of `{id,xml}` for categories. This can include both core
* and other extensions: core extensions will be placed in the normal Scratch order; others will go at the bottom.
* @property {string} id - the extension / category ID.
* @property {string} xml - the `<category>...</category>` XML for this extension / category.
* @param {?string} costumeName - The name of the default selected costume dropdown.
* @param {?string} backdropName - The name of the default selected backdrop dropdown.
* @param {?string} soundName - The name of the default selected sound dropdown.
* @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox.
*/
const makeToolboxXML = function (isStage, targetId, categoriesXML,
const makeToolboxXML = function (isStage, targetId, categoriesXML = [],
costumeName = '', backdropName = '', soundName = '') {
const gap = [categorySeparator];
@@ -730,21 +733,41 @@ const makeToolboxXML = function (isStage, targetId, categoriesXML,
backdropName = xmlEscape(backdropName);
soundName = xmlEscape(soundName);
categoriesXML = categoriesXML.slice();
const moveCategory = categoryId => {
const index = categoriesXML.findIndex(categoryInfo => categoryInfo.id === categoryId);
if (index >= 0) {
// remove the category from categoriesXML and return its XML
const [categoryInfo] = categoriesXML.splice(index, 1);
return categoryInfo.xml;
}
// return `undefined`
};
const motionXML = moveCategory('motion') || motion(isStage, targetId);
const looksXML = moveCategory('looks') || looks(isStage, targetId, costumeName, backdropName);
const soundXML = moveCategory('sound') || sound(isStage, targetId, soundName);
const eventsXML = moveCategory('event') || events(isStage, targetId);
const controlXML = moveCategory('control') || control(isStage, targetId);
const sensingXML = moveCategory('sensing') || sensing(isStage, targetId);
const operatorsXML = moveCategory('operators') || operators(isStage, targetId);
const variablesXML = moveCategory('data') || variables(isStage, targetId);
const myBlocksXML = moveCategory('procedures') || myBlocks(isStage, targetId);
const everything = [
xmlOpen,
motion(isStage, targetId), gap,
looks(isStage, targetId, costumeName, backdropName), gap,
sound(isStage, targetId, soundName), gap,
events(isStage, targetId), gap,
control(isStage, targetId), gap,
sensing(isStage, targetId), gap,
operators(isStage, targetId), gap,
variables(isStage, targetId), gap,
myBlocks(isStage, targetId)
motionXML, gap,
looksXML, gap,
soundXML, gap,
eventsXML, gap,
controlXML, gap,
sensingXML, gap,
operatorsXML, gap,
variablesXML, gap,
myBlocksXML
];
if (categoriesXML) {
everything.push(gap, categoriesXML);
for (const extensionCategory of categoriesXML) {
everything.push(gap, extensionCategory.xml);
}
everything.push(xmlClose);
+199
View File
@@ -0,0 +1,199 @@
import defineDynamicBlock from '../../../src/lib/define-dynamic-block';
import BlockType from 'scratch-vm/src/extension-support/block-type';
const MockScratchBlocks = {
OUTPUT_SHAPE_HEXAGONAL: 1,
OUTPUT_SHAPE_ROUND: 2,
OUTPUT_SHAPE_SQUARE: 3
};
const categoryInfo = {
name: 'test category',
color1: '#111',
color2: '#222',
color3: '#333'
};
const penIconURI = 'data:image/svg+xml;base64,fake_pen_icon_svg_base64_data';
const testBlockInfo = {
commandWithIcon: {
blockType: BlockType.COMMAND,
blockIconURI: penIconURI,
text: 'command with icon'
},
commandWithoutIcon: {
blockType: BlockType.COMMAND,
text: 'command without icon'
},
terminalCommand: {
blockType: BlockType.COMMAND,
isTerminal: true,
text: 'terminal command'
},
reporter: {
blockType: BlockType.REPORTER,
text: 'reporter'
},
boolean: {
blockType: BlockType.BOOLEAN,
text: 'Boolean'
},
hat: {
blockType: BlockType.HAT,
text: 'hat'
}
};
// similar to goog.mixin from the Closure library
const mixin = function (target, source) {
for (const x in source) {
target[x] = source[x];
}
};
class MockBlock {
constructor (blockInfo, extendedOpcode) {
// mimic Closure-style inheritance by mixing in `defineDynamicBlock` output as this instance's prototype
// see also the `Blockly.Block` constructor
const prototype = defineDynamicBlock(MockScratchBlocks, categoryInfo, blockInfo, extendedOpcode);
mixin(this, prototype);
this.init();
// bootstrap the mutation<->DOM cycle
this.blockInfoText = JSON.stringify(blockInfo);
const xmlElement = this.mutationToDom();
// parse blockInfo from XML to fill dynamic properties
this.domToMutation(xmlElement);
}
jsonInit (json) {
this.result = Object.assign({}, json);
}
interpolate_ () {
// TODO: add tests for this?
}
setCheckboxInFlyout (isEnabled) {
this.result.checkboxInFlyout_ = isEnabled;
}
setOutput (isEnabled) {
this.result.outputConnection = isEnabled; // Blockly calls `makeConnection_` here
}
setOutputShape (outputShape) {
this.result.outputShape_ = outputShape;
}
setNextStatement (isEnabled) {
this.result.nextConnection = isEnabled; // Blockly calls `makeConnection_` here
}
setPreviousStatement (isEnabled) {
this.result.previousConnection = isEnabled; // Blockly calls `makeConnection_` here
}
}
describe('defineDynamicBlock', () => {
test('is a function', () => {
expect(typeof defineDynamicBlock).toBe('function');
});
test('can define a command block with an icon', () => {
const extendedOpcode = 'test.commandWithIcon';
const block = new MockBlock(testBlockInfo.commandWithIcon, extendedOpcode);
expect(block.result).toEqual({
category: categoryInfo.name,
colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3,
extensions: ['scratch_extension'],
inputsInline: true,
nextConnection: true,
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE,
previousConnection: true,
type: extendedOpcode
});
});
test('can define a command block without an icon', () => {
const extendedOpcode = 'test.commandWithoutIcon';
const block = new MockBlock(testBlockInfo.commandWithoutIcon, extendedOpcode);
expect(block.result).toEqual({
category: categoryInfo.name,
colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3,
// extensions: undefined, // no icon means no extension
inputsInline: true,
nextConnection: true,
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE,
previousConnection: true,
type: extendedOpcode
});
});
test('can define a terminal command', () => {
const extendedOpcode = 'test.terminal';
const block = new MockBlock(testBlockInfo.terminalCommand, extendedOpcode);
expect(block.result).toEqual({
category: categoryInfo.name,
colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3,
// extensions: undefined, // no icon means no extension
inputsInline: true,
nextConnection: false, // terminal
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE,
previousConnection: true,
type: extendedOpcode
});
});
test('can define a reporter', () => {
const extendedOpcode = 'test.reporter';
const block = new MockBlock(testBlockInfo.reporter, extendedOpcode);
expect(block.result).toEqual({
category: categoryInfo.name,
checkboxInFlyout_: true,
colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3,
// extensions: undefined, // no icon means no extension
inputsInline: true,
// nextConnection: undefined, // reporter
outputConnection: true, // reporter
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_ROUND, // reporter
// previousConnection: undefined, // reporter
type: extendedOpcode
});
});
test('can define a Boolean', () => {
const extendedOpcode = 'test.boolean';
const block = new MockBlock(testBlockInfo.boolean, extendedOpcode);
expect(block.result).toEqual({
category: categoryInfo.name,
// checkboxInFlyout_: undefined,
colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3,
// extensions: undefined, // no icon means no extension
inputsInline: true,
// nextConnection: undefined, // reporter
outputConnection: true, // reporter
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_HEXAGONAL, // Boolean
// previousConnection: undefined, // reporter
type: extendedOpcode
});
});
test('can define a hat', () => {
const extendedOpcode = 'test.hat';
const block = new MockBlock(testBlockInfo.hat, extendedOpcode);
expect(block.result).toEqual({
category: categoryInfo.name,
colour: categoryInfo.color1,
colourSecondary: categoryInfo.color2,
colourTertiary: categoryInfo.color3,
// extensions: undefined, // no icon means no extension
inputsInline: true,
nextConnection: true,
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE,
// previousConnection: undefined, // hat
type: extendedOpcode
});
});
});