Merge pull request #655 from cwillisf/feature/extensions
Feature/extensions
This commit is contained in:
commit
e79cec4a08
11 files changed
+1024
-3
No files matched your search
@@ -148,3 +148,18 @@
|
||||
height: $stage-menu-height;
|
||||
padding: $space;
|
||||
}
|
||||
|
||||
.extension-button {
|
||||
font-size: 0.55rem;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
bottom: 0.75rem;
|
||||
left: 1rem;
|
||||
border: 1px solid lightgray;
|
||||
background: white;
|
||||
z-index: 50; /** Force extension button above the ScratchBlocks flyout. */
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -12,18 +12,31 @@ import Controls from '../../containers/controls.jsx';
|
||||
import TargetPane from '../../containers/target-pane.jsx';
|
||||
import SoundTab from '../../containers/sound-tab.jsx';
|
||||
import Stage from '../../containers/stage.jsx';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import Box from '../box/box.jsx';
|
||||
import IconButton from '../icon-button/icon-button.jsx';
|
||||
import MenuBar from '../menu-bar/menu-bar.jsx';
|
||||
|
||||
import layout from '../../lib/layout-constants.js';
|
||||
import styles from './gui.css';
|
||||
import addExtensionIcon from '../sprite-selector/icon--sprite.svg';
|
||||
|
||||
const addExtensionMessage = (
|
||||
<FormattedMessage
|
||||
defaultMessage="Add Extension"
|
||||
description="Button to add an extension in the target pane"
|
||||
id="targetPane.addExtension"
|
||||
/>
|
||||
);
|
||||
|
||||
const GUIComponent = props => {
|
||||
const {
|
||||
basePath,
|
||||
children,
|
||||
enableExtensions,
|
||||
vm,
|
||||
onAddExtensionClick,
|
||||
onTabSelect,
|
||||
tabIndex,
|
||||
...componentProps
|
||||
@@ -77,6 +90,14 @@ const GUIComponent = props => {
|
||||
vm={vm}
|
||||
/>
|
||||
</Box>
|
||||
<IconButton
|
||||
className={classNames(styles.extensionButton, {
|
||||
[styles.hidden]: !enableExtensions
|
||||
})}
|
||||
img={addExtensionIcon}
|
||||
title={addExtensionMessage}
|
||||
onClick={onAddExtensionClick}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel className={tabClassNames.tabPanel}>
|
||||
{tabIndex === 1 ? <CostumeTab vm={vm} /> : null}
|
||||
@@ -115,6 +136,8 @@ const GUIComponent = props => {
|
||||
GUIComponent.propTypes = {
|
||||
basePath: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
enableExtensions: PropTypes.bool,
|
||||
onAddExtensionClick: PropTypes.func,
|
||||
onTabSelect: PropTypes.func,
|
||||
tabIndex: PropTypes.number,
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import bindAll from 'lodash.bindall';
|
||||
import debounce from 'lodash.debounce';
|
||||
import defaultsDeep from 'lodash.defaultsdeep';
|
||||
import makeToolboxXML from '../lib/make-toolbox-xml';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import VMScratchBlocks from '../lib/blocks';
|
||||
@@ -8,6 +9,9 @@ import VM from 'scratch-vm';
|
||||
import Prompt from './prompt.jsx';
|
||||
import BlocksComponent from '../components/blocks/blocks.jsx';
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {updateToolbox} from '../reducers/toolbox';
|
||||
|
||||
const addFunctionListener = (object, property, callback) => {
|
||||
const oldFn = object[property];
|
||||
object[property] = function () {
|
||||
@@ -31,6 +35,7 @@ class Blocks extends React.Component {
|
||||
'onScriptGlowOff',
|
||||
'onBlockGlowOn',
|
||||
'onBlockGlowOff',
|
||||
'handleExtensionAdded',
|
||||
'onTargetsUpdate',
|
||||
'onVisualReport',
|
||||
'onWorkspaceUpdate',
|
||||
@@ -55,9 +60,20 @@ class Blocks extends React.Component {
|
||||
this.attachVM();
|
||||
}
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
return this.state.prompt !== nextState.prompt || this.props.isVisible !== nextProps.isVisible;
|
||||
return (
|
||||
this.state.prompt !== nextState.prompt ||
|
||||
this.props.isVisible !== nextProps.isVisible ||
|
||||
this.props.toolboxXML !== nextProps.toolboxXML
|
||||
);
|
||||
}
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.toolboxXML !== this.props.toolboxXML) {
|
||||
const selectedCategoryName = this.workspace.toolbox_.getSelectedItem().name_;
|
||||
this.workspace.updateToolbox(this.props.toolboxXML);
|
||||
// Blockly throws if we don't select a category after updating the toolbox.
|
||||
/** @TODO Find a way to avoid the exception without accessing private properties. */
|
||||
this.setToolboxSelectedItemByName(selectedCategoryName);
|
||||
}
|
||||
if (this.props.isVisible === prevProps.isVisible) {
|
||||
return;
|
||||
}
|
||||
@@ -77,6 +93,20 @@ class Blocks extends React.Component {
|
||||
this.detachVM();
|
||||
this.workspace.dispose();
|
||||
}
|
||||
/**
|
||||
* Select a particular category in the toolbox by specifying the category name.
|
||||
* This is a workaround for a bug: @see {@link componentDidUpdate} above.
|
||||
* @TODO Remove this or reimplement using only public APIs.
|
||||
* @param {string} name - the name of the category to select.
|
||||
*/
|
||||
setToolboxSelectedItemByName (name) {
|
||||
const categories = this.workspace.toolbox_.categoryMenu_.categories_;
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
if (categories[i].name_ === name) {
|
||||
this.workspace.toolbox_.setSelectedItem(categories[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
attachVM () {
|
||||
this.workspace.addChangeListener(this.props.vm.blockListener);
|
||||
this.flyoutWorkspace = this.workspace
|
||||
@@ -91,6 +121,7 @@ class Blocks extends React.Component {
|
||||
this.props.vm.addListener('VISUAL_REPORT', this.onVisualReport);
|
||||
this.props.vm.addListener('workspaceUpdate', this.onWorkspaceUpdate);
|
||||
this.props.vm.addListener('targetsUpdate', this.onTargetsUpdate);
|
||||
this.props.vm.addListener('EXTENSION_ADDED', this.handleExtensionAdded);
|
||||
}
|
||||
detachVM () {
|
||||
this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn);
|
||||
@@ -100,6 +131,7 @@ class Blocks extends React.Component {
|
||||
this.props.vm.removeListener('VISUAL_REPORT', this.onVisualReport);
|
||||
this.props.vm.removeListener('workspaceUpdate', this.onWorkspaceUpdate);
|
||||
this.props.vm.removeListener('targetsUpdate', this.onTargetsUpdate);
|
||||
this.props.vm.removeListener('EXTENSION_ADDED', this.handleExtensionAdded);
|
||||
}
|
||||
updateToolboxBlockValue (id, value) {
|
||||
const block = this.workspace
|
||||
@@ -167,6 +199,12 @@ class Blocks extends React.Component {
|
||||
this.workspace.resize();
|
||||
}
|
||||
}
|
||||
handleExtensionAdded (blocksInfo) {
|
||||
this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => blockInfo.json));
|
||||
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML();
|
||||
const toolboxXML = makeToolboxXML(dynamicBlocksXML);
|
||||
this.props.onExtensionAdded(toolboxXML);
|
||||
}
|
||||
setBlocks (blocks) {
|
||||
this.blocks = blocks;
|
||||
}
|
||||
@@ -185,6 +223,8 @@ class Blocks extends React.Component {
|
||||
options, // eslint-disable-line no-unused-vars
|
||||
vm, // eslint-disable-line no-unused-vars
|
||||
isVisible, // eslint-disable-line no-unused-vars
|
||||
onExtensionAdded, // eslint-disable-line no-unused-vars
|
||||
toolboxXML, // eslint-disable-line no-unused-vars
|
||||
...props
|
||||
} = this.props;
|
||||
return (
|
||||
@@ -209,6 +249,7 @@ class Blocks extends React.Component {
|
||||
|
||||
Blocks.propTypes = {
|
||||
isVisible: PropTypes.bool,
|
||||
onExtensionAdded: PropTypes.func,
|
||||
options: PropTypes.shape({
|
||||
media: PropTypes.string,
|
||||
zoom: PropTypes.shape({
|
||||
@@ -230,6 +271,7 @@ Blocks.propTypes = {
|
||||
}),
|
||||
comments: PropTypes.bool
|
||||
}),
|
||||
toolboxXML: PropTypes.string,
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
|
||||
@@ -264,4 +306,17 @@ Blocks.defaultProps = {
|
||||
options: Blocks.defaultOptions
|
||||
};
|
||||
|
||||
export default Blocks;
|
||||
const mapStateToProps = state => ({
|
||||
toolboxXML: state.toolbox.toolboxXML
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onExtensionAdded: toolboxXML => {
|
||||
dispatch(updateToolbox(toolboxXML));
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Blocks);
|
||||
@@ -12,6 +12,7 @@ class GUI extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleAddExtensionClick',
|
||||
'handleTabSelect'
|
||||
]);
|
||||
this.state = {tabIndex: 0};
|
||||
@@ -34,6 +35,10 @@ class GUI extends React.Component {
|
||||
handleTabSelect (tabIndex) {
|
||||
this.setState({tabIndex});
|
||||
}
|
||||
handleAddExtensionClick () {
|
||||
/** @TODO open modal for the extension library instead */
|
||||
this.props.vm.extensionManager.loadExtensionURL('static/extensions/example-extension.js');
|
||||
}
|
||||
render () {
|
||||
const {
|
||||
children,
|
||||
@@ -43,8 +48,10 @@ class GUI extends React.Component {
|
||||
} = this.props;
|
||||
return (
|
||||
<GUIComponent
|
||||
enableExtensions={window.location.search.includes('extensions')}
|
||||
tabIndex={this.state.tabIndex}
|
||||
vm={vm}
|
||||
onAddExtensionClick={this.handleAddExtensionClick}
|
||||
onTabSelect={this.handleTabSelect}
|
||||
{...componentProps}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
extends: ['scratch'], // no ES6
|
||||
env: {
|
||||
worker: true
|
||||
},
|
||||
globals: {
|
||||
Scratch: true
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,230 @@
|
||||
var ExampleExtension = function () {
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {object} This extension's metadata.
|
||||
*/
|
||||
ExampleExtension.prototype.getInfo = function () {
|
||||
return {
|
||||
// Required: the machine-readable name of this extension.
|
||||
// Will be used as the extension's namespace. Must not contain a '.' character.
|
||||
id: 'someBlocks',
|
||||
|
||||
// Optional: the human-readable name of this extension as string.
|
||||
// This and any other string to be displayed in the Scratch UI may either be
|
||||
// a string or a call to `intlDefineMessage`; a plain string will not be
|
||||
// translated whereas a call to `intlDefineMessage` will connect the string
|
||||
// to the translation map (see below). The `intlDefineMessage` call is
|
||||
// similar to `defineMessages` from `react-intl` in form, but will actually
|
||||
// call some extension support code to do its magic. For example, we will
|
||||
// internally namespace the messages such that two extensions could have
|
||||
// messages with the same ID without colliding.
|
||||
// See also: https://github.com/yahoo/react-intl/wiki/API#definemessages
|
||||
name: 'Some Blocks',
|
||||
|
||||
// Optional: URI for an icon for this extension. Data URI OK.
|
||||
// If not present, use a generic icon.
|
||||
// TODO: what file types are OK? All web images? Just PNG?
|
||||
iconURI: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAAAAACyOJm3AAAAFklEQVQYV2P4DwMMEMgAI/+DE' +
|
||||
'UIMBgAEWB7i7uidhAAAAABJRU5ErkJggg==',
|
||||
|
||||
// Optional: Link to documentation content for this extension.
|
||||
// If not present, offer no link.
|
||||
docsURI: 'https://....',
|
||||
|
||||
// Required: the list of blocks implemented by this extension,
|
||||
// in the order intended for display.
|
||||
blocks: [
|
||||
{
|
||||
opcode: 'example-noop',
|
||||
blockType: Scratch.BlockType.COMMAND,
|
||||
blockAllThreads: false,
|
||||
text: 'do nothing',
|
||||
func: 'noop'
|
||||
},
|
||||
{
|
||||
opcode: 'example-conditional',
|
||||
blockType: Scratch.BlockType.CONDITIONAL,
|
||||
branchCount: 4,
|
||||
isTerminal: true,
|
||||
blockAllThreads: false,
|
||||
text: 'choose [BRANCH]',
|
||||
arguments: {
|
||||
BRANCH: {
|
||||
type: Scratch.ArgumentType.NUMBER,
|
||||
defaultValue: 1
|
||||
}
|
||||
},
|
||||
func: 'noop'
|
||||
},
|
||||
{
|
||||
// Required: the machine-readable name of this operation.
|
||||
// This will appear in project JSON. Must not contain a '.' character.
|
||||
opcode: 'myReporter', // becomes 'someBlocks.myReporter'
|
||||
|
||||
// Required: the kind of block we're defining, from a predefined list:
|
||||
// 'command' - a normal command block, like "move {} steps"
|
||||
// 'reporter' - returns a value, like "direction"
|
||||
// 'Boolean' - same as 'reporter' but returns a Boolean value
|
||||
// 'hat' - starts a stack if its value is truthy
|
||||
// 'conditional' - control flow, like "if {}" or "repeat {}"
|
||||
// A 'conditional' block may return the one-based index of a branch
|
||||
// to run, or it may return zero/falsy to run no branch. Each time a
|
||||
// child branch finishes, the block is called again. This is only a
|
||||
// slight change to the current model for control flow blocks, and is
|
||||
// also compatible with returning true/false for an "if" or "repeat"
|
||||
// block.
|
||||
// TODO: Consider Blockly-like nextStatement, previousStatement, and
|
||||
// output attributes as an alternative. Those are more flexible, but
|
||||
// allow bad combinations.
|
||||
blockType: Scratch.BlockType.REPORTER,
|
||||
|
||||
// Required for conditional blocks, ignored for others: the number of
|
||||
// child branches this block controls. An "if" or "repeat" block would
|
||||
// specify a branch count of 1; an "if-else" block would specify a
|
||||
// branch count of 2.
|
||||
// TODO: should we support dynamic branch count for "switch"-likes?
|
||||
branchCount: 0,
|
||||
|
||||
// Optional, default false: whether or not this block ends a stack.
|
||||
// The "forever" and "stop all" blocks would specify true here.
|
||||
isTerminal: true,
|
||||
|
||||
// Optional, default false: whether or not to block all threads while
|
||||
// this block is busy. This is for things like the "touching color"
|
||||
// block in compatibility mode, and is only needed if the VM runs in a
|
||||
// worker. We might even consider omitting it from extension docs...
|
||||
blockAllThreads: false,
|
||||
|
||||
// Required: the human-readable text on this block, including argument
|
||||
// placeholders. Argument placeholders should be in [MACRO_CASE] and
|
||||
// must be [ENCLOSED_WITHIN_SQUARE_BRACKETS].
|
||||
text: 'letter [LETTER_NUM] of [TEXT]',
|
||||
|
||||
// Required: describe each argument.
|
||||
// Note that this is an array: the order of arguments will be used
|
||||
arguments: {
|
||||
// Required: the ID of the argument, which will be the name in the
|
||||
// args object passed to the implementation function.
|
||||
LETTER_NUM: {
|
||||
// Required: type of the argument / shape of the block input
|
||||
type: Scratch.ArgumentType.NUMBER,
|
||||
|
||||
// Optional: the default value of the argument
|
||||
defaultValue: 1
|
||||
},
|
||||
|
||||
// Required: the ID of the argument, which will be the name in the
|
||||
// args object passed to the implementation function.
|
||||
TEXT: {
|
||||
// Required: type of the argument / shape of the block input
|
||||
type: Scratch.ArgumentType.STRING,
|
||||
|
||||
// Optional: the default value of the argument
|
||||
defaultValue: 'text'
|
||||
}
|
||||
},
|
||||
|
||||
// Optional: a string naming the function implementing this block.
|
||||
// If this is omitted, use the opcode string.
|
||||
func: 'myReporter',
|
||||
|
||||
// Optional: list of target types for which this block should appear.
|
||||
// If absent, assume it applies to all builtin targets -- that is:
|
||||
// ['sprite', 'stage']
|
||||
filter: ['someBlocks.wedo2', 'sprite', 'stage']
|
||||
},
|
||||
{
|
||||
opcode: 'example-Boolean',
|
||||
blockType: Scratch.BlockType.BOOLEAN,
|
||||
text: 'return true',
|
||||
func: 'returnTrue'
|
||||
},
|
||||
{
|
||||
opcode: 'example-hat',
|
||||
blockType: Scratch.BlockType.HAT,
|
||||
text: 'after forever',
|
||||
func: 'returnFalse'
|
||||
},
|
||||
{
|
||||
// Another block...
|
||||
}
|
||||
],
|
||||
|
||||
// Optional: define extension-specific menus here.
|
||||
menus: {
|
||||
// Required: an identifier for this menu, unique within this extension.
|
||||
menuA: [
|
||||
// Static menu: list items which should appear in the menu.
|
||||
{
|
||||
// Required: the value of the menu item when it is chosen.
|
||||
value: 'itemId1',
|
||||
|
||||
// Optional: the human-readable label for this item.
|
||||
// Use `value` as the text if this is absent.
|
||||
text: 'Item One'
|
||||
},
|
||||
|
||||
// The simplest form of a list item is a string which will be used as
|
||||
// both value and text.
|
||||
'itemId2'
|
||||
],
|
||||
|
||||
// Dynamic menu: a string naming a function which returns an array as above.
|
||||
// Called each time the menu is opened.
|
||||
menuB: 'getItemsForMenuB'
|
||||
},
|
||||
|
||||
// Optional: translations
|
||||
translation_map: {
|
||||
de: {
|
||||
'extensionName': 'Einige Blöcke',
|
||||
'myReporter': 'Buchstabe [LETTER_NUM] von [TEXT]',
|
||||
'myReporter.TEXT_default': 'Text',
|
||||
'menuA_item1': 'Artikel eins',
|
||||
|
||||
// Dynamic menus can be translated too
|
||||
'menuB_example': 'Beispiel',
|
||||
|
||||
// This message contains ICU placeholders (see `myReporter()` below)
|
||||
'myReporter.result': 'Buchstabe {LETTER_NUM} von {TEXT} ist {LETTER}.'
|
||||
},
|
||||
it: {
|
||||
// ...
|
||||
}
|
||||
},
|
||||
|
||||
// Optional: list new target type(s) provided by this extension.
|
||||
targetTypes: [
|
||||
'wedo2', // automatically transformed to 'someBlocks.wedo2'
|
||||
'speech' // automatically transformed to 'someBlocks.speech'
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Implement myReporter.
|
||||
* @param {object} args - the block's arguments.
|
||||
* @property {number} LETTER_NUM - the string value of the argument.
|
||||
* @property {string} TEXT - the string value of the argument.
|
||||
* @returns {string} a string which includes the block argument value.
|
||||
*/
|
||||
ExampleExtension.prototype.myReporter = function (args) {
|
||||
// Note: this implementation is not Unicode-clean; it's just here as an example.
|
||||
const result = args.TEXT.charAt(args.LETTER_NUM);
|
||||
|
||||
return ['Letter ', args.LETTER_NUM, ' of ', args.TEXT, ' is ', result, '.'].join('');
|
||||
};
|
||||
|
||||
ExampleExtension.prototype.noop = function () {
|
||||
};
|
||||
|
||||
ExampleExtension.prototype.returnTrue = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
ExampleExtension.prototype.returnFalse = function () {
|
||||
return false;
|
||||
};
|
||||
|
||||
Scratch.extensions.register(new ExampleExtension());
|
||||
@@ -0,0 +1,640 @@
|
||||
const separator = '<sep gap="45"/>';
|
||||
|
||||
const motion = `
|
||||
<category name="Motion" colour="#4C97FF" secondaryColour="#3373CC">
|
||||
<block type="motion_movesteps">
|
||||
<value name="STEPS">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_turnright">
|
||||
<value name="DEGREES">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">15</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_turnleft">
|
||||
<value name="DEGREES">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">15</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_pointindirection">
|
||||
<value name="DIRECTION">
|
||||
<shadow type="math_angle">
|
||||
<field name="NUM">90</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_pointtowards">
|
||||
<value name="TOWARDS">
|
||||
<shadow type="motion_pointtowards_menu">
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_gotoxy">
|
||||
<value name="X">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="Y">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_goto">
|
||||
<value name="TO">
|
||||
<shadow type="motion_goto_menu">
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_glidesecstoxy">
|
||||
<value name="SECS">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="X">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="Y">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_changexby">
|
||||
<value name="DX">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_setx">
|
||||
<value name="X">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_changeyby">
|
||||
<value name="DY">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_sety">
|
||||
<value name="Y">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_ifonedgebounce"/>
|
||||
<block type="motion_setrotationstyle"/>
|
||||
<block type="motion_xposition"/>
|
||||
<block type="motion_yposition"/>
|
||||
<block type="motion_direction"/>
|
||||
</category>
|
||||
`;
|
||||
|
||||
const looks = `
|
||||
<category name="Looks" colour="#9966FF" secondaryColour="#774DCB">
|
||||
<block type="looks_show"/>
|
||||
<block type="looks_hide"/>
|
||||
<block type="looks_switchcostumeto">
|
||||
<value name="COSTUME">
|
||||
<shadow type="looks_costume"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="looks_nextcostume"/>
|
||||
<block type="looks_nextbackdrop"/>
|
||||
<block type="looks_switchbackdropto">
|
||||
<value name="BACKDROP">
|
||||
<shadow type="looks_backdrops"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="looks_switchbackdroptoandwait">
|
||||
<value name="BACKDROP">
|
||||
<shadow type="looks_backdrops"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="looks_changeeffectby">
|
||||
<value name="CHANGE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="looks_seteffectto">
|
||||
<value name="VALUE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="looks_cleargraphiceffects"/>
|
||||
<block type="looks_changesizeby">
|
||||
<value name="CHANGE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="looks_setsizeto">
|
||||
<value name="SIZE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">100</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="looks_gotofront"/>
|
||||
<block type="looks_gobacklayers">
|
||||
<value name="NUM">
|
||||
<shadow type="math_integer">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="looks_costumeorder"/>
|
||||
<block type="looks_backdroporder"/>
|
||||
<block type="looks_backdropname"/>
|
||||
<block type="looks_size"/>
|
||||
</category>
|
||||
`;
|
||||
|
||||
const sound = `
|
||||
<category name="Sound" colour="#D65CD6" secondaryColour="#BD42BD">
|
||||
<block type="sound_play">
|
||||
<value name="SOUND_MENU">
|
||||
<shadow type="sound_sounds_menu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_playuntildone">
|
||||
<value name="SOUND_MENU">
|
||||
<shadow type="sound_sounds_menu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_stopallsounds"/>
|
||||
<block type="sound_playdrumforbeats">
|
||||
<value name="DRUM">
|
||||
<shadow type="sound_drums_menu"/>
|
||||
</value>
|
||||
<value name="BEATS">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0.25</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_restforbeats">
|
||||
<value name="BEATS">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0.25</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_playnoteforbeats">
|
||||
<value name="NOTE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">60</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="BEATS">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0.5</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_setinstrumentto">
|
||||
<value name="INSTRUMENT">
|
||||
<shadow type="sound_instruments_menu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_changeeffectby">
|
||||
<value name="VALUE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_seteffectto">
|
||||
<value name="VALUE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">100</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_cleareffects"/>
|
||||
<block type="sound_changevolumeby">
|
||||
<value name="VOLUME">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">-10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_setvolumeto">
|
||||
<value name="VOLUME">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">100</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_volume"/>
|
||||
<block type="sound_changetempoby">
|
||||
<value name="TEMPO">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">20</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_settempotobpm">
|
||||
<value name="TEMPO">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">60</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_tempo"/>
|
||||
</category>
|
||||
`;
|
||||
|
||||
const pen = `
|
||||
<category name="Pen" colour="#00B295" secondaryColour="#0B8E69">
|
||||
<block type="pen_clear"/>
|
||||
<block type="pen_stamp"/>
|
||||
<block type="pen_pendown"/>
|
||||
<block type="pen_penup"/>
|
||||
<block type="pen_setpencolortocolor">
|
||||
<value name="COLOR">
|
||||
<shadow type="colour_picker">
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="pen_changepencolorby">
|
||||
<value name="COLOR">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="pen_setpencolortonum">
|
||||
<value name="COLOR">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="pen_changepenshadeby">
|
||||
<value name="SHADE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="pen_setpenshadeto">
|
||||
<value name="SHADE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">50</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="pen_changepensizeby">
|
||||
<value name="SIZE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="pen_setpensizeto">
|
||||
<value name="SIZE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
</category>
|
||||
`;
|
||||
|
||||
const events = `
|
||||
<category name="Events" colour="#FFD500" secondaryColour="#CC9900">
|
||||
<block type="event_whenflagclicked"/>
|
||||
<block type="event_whenkeypressed">
|
||||
</block>
|
||||
<block type="event_whenthisspriteclicked"/>
|
||||
<block type="event_whenbackdropswitchesto">
|
||||
</block>
|
||||
<block type="event_whengreaterthan">
|
||||
<value name="VALUE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="event_whenbroadcastreceived">
|
||||
</block>
|
||||
<block type="event_broadcast">
|
||||
<value name="BROADCAST_OPTION">
|
||||
<shadow type="event_broadcast_menu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="event_broadcastandwait">
|
||||
<value name="BROADCAST_OPTION">
|
||||
<shadow type="event_broadcast_menu"/>
|
||||
</value>
|
||||
</block>
|
||||
</category>
|
||||
`;
|
||||
|
||||
const control = `
|
||||
<category name="Control" colour="#FFAB19" secondaryColour="#CF8B17">
|
||||
<block type="control_wait">
|
||||
<value name="DURATION">
|
||||
<shadow type="math_positive_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="control_repeat">
|
||||
<value name="TIMES">
|
||||
<shadow type="math_whole_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="control_forever"/>
|
||||
<block type="control_if"/>
|
||||
<block type="control_if_else"/>
|
||||
<block type="control_wait_until"/>
|
||||
<block type="control_repeat_until"/>
|
||||
<block type="control_stop"/>
|
||||
<block type="control_start_as_clone"/>
|
||||
<block type="control_create_clone_of">
|
||||
<value name="CLONE_OPTION">
|
||||
<shadow type="control_create_clone_of_menu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="control_delete_this_clone"/>
|
||||
</category>
|
||||
`;
|
||||
|
||||
const sensing = `
|
||||
<category name="Sensing" colour="#4CBFE6" secondaryColour="#2E8EB8">
|
||||
<block type="sensing_touchingobject">
|
||||
<value name="TOUCHINGOBJECTMENU">
|
||||
<shadow type="sensing_touchingobjectmenu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sensing_touchingcolor">
|
||||
<value name="COLOR">
|
||||
<shadow type="colour_picker"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sensing_coloristouchingcolor">
|
||||
<value name="COLOR">
|
||||
<shadow type="colour_picker"/>
|
||||
</value>
|
||||
<value name="COLOR2">
|
||||
<shadow type="colour_picker"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sensing_distanceto">
|
||||
<value name="DISTANCETOMENU">
|
||||
<shadow type="sensing_distancetomenu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sensing_keypressed">
|
||||
<value name="KEY_OPTION">
|
||||
<shadow type="sensing_keyoptions"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sensing_mousedown"/>
|
||||
<block type="sensing_mousex"/>
|
||||
<block type="sensing_mousey"/>
|
||||
<block type="sensing_loudness"/>
|
||||
<block type="sensing_timer"/>
|
||||
<block type="sensing_resettimer"/>
|
||||
<block type="sensing_of">
|
||||
<value name="PROPERTY">
|
||||
<shadow type="sensing_of_property_menu"/>
|
||||
</value>
|
||||
<value name="OBJECT">
|
||||
<shadow type="sensing_of_object_menu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sensing_current">
|
||||
<value name="CURRENTMENU">
|
||||
<shadow type="sensing_currentmenu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sensing_dayssince2000"/>
|
||||
</category>
|
||||
`;
|
||||
|
||||
const operators = `
|
||||
<category name="Operators" colour="#40BF4A" secondaryColour="#389438">
|
||||
<block type="operator_add">
|
||||
<value name="NUM1">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="NUM2">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_subtract">
|
||||
<value name="NUM1">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="NUM2">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_multiply">
|
||||
<value name="NUM1">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="NUM2">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_divide">
|
||||
<value name="NUM1">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="NUM2">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_random">
|
||||
<value name="FROM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="TO">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_lt">
|
||||
<value name="OPERAND1">
|
||||
<shadow type="text">
|
||||
<field name="TEXT"/>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="OPERAND2">
|
||||
<shadow type="text">
|
||||
<field name="TEXT"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_equals">
|
||||
<value name="OPERAND1">
|
||||
<shadow type="text">
|
||||
<field name="TEXT"/>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="OPERAND2">
|
||||
<shadow type="text">
|
||||
<field name="TEXT"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_gt">
|
||||
<value name="OPERAND1">
|
||||
<shadow type="text">
|
||||
<field name="TEXT"/>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="OPERAND2">
|
||||
<shadow type="text">
|
||||
<field name="TEXT"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_and"/>
|
||||
<block type="operator_or"/>
|
||||
<block type="operator_not"/>
|
||||
<block type="operator_join">
|
||||
<value name="STRING1">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">hello</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="STRING2">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">world</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_letter_of">
|
||||
<value name="LETTER">
|
||||
<shadow type="math_whole_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="STRING">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">world</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_length">
|
||||
<value name="STRING">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">world</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_mod">
|
||||
<value name="NUM1">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="NUM2">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_round">
|
||||
<value name="NUM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_mathop">
|
||||
<value name="NUM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
</category>
|
||||
`;
|
||||
|
||||
const data = `
|
||||
<category name="Data" colour="#FF8C1A" secondaryColour="#DB6E00" custom="VARIABLE">
|
||||
</category>
|
||||
`;
|
||||
|
||||
const xmlOpen = '<xml style="display: none">';
|
||||
const xmlClose = '</xml>';
|
||||
|
||||
/**
|
||||
* @param {string?} categoriesXML - null for default toolbox, or an XML string with <category> elements.
|
||||
* @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox.
|
||||
*/
|
||||
const makeToolboxXML = function (categoriesXML) {
|
||||
const gap = [separator];
|
||||
|
||||
const everything = [
|
||||
xmlOpen,
|
||||
motion, gap,
|
||||
looks, gap,
|
||||
sound, gap,
|
||||
pen, gap,
|
||||
data, gap,
|
||||
events, gap,
|
||||
control, gap,
|
||||
sensing, gap,
|
||||
operators
|
||||
];
|
||||
|
||||
if (categoriesXML) {
|
||||
everything.push(gap, categoriesXML);
|
||||
}
|
||||
|
||||
everything.push(xmlClose);
|
||||
|
||||
return everything.join('\n');
|
||||
};
|
||||
|
||||
export default makeToolboxXML;
|
||||
@@ -3,6 +3,7 @@ import intlReducer from './intl';
|
||||
import modalReducer from './modals';
|
||||
import monitorReducer from './monitors';
|
||||
import targetReducer from './targets';
|
||||
import toolboxReducer from './toolbox';
|
||||
import vmReducer from './vm';
|
||||
|
||||
|
||||
@@ -11,5 +12,6 @@ export default combineReducers({
|
||||
modals: modalReducer,
|
||||
monitors: monitorReducer,
|
||||
targets: targetReducer,
|
||||
toolbox: toolboxReducer,
|
||||
vm: vmReducer
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
const UPDATE_TOOLBOX = 'scratch-gui/toolbox/UPDATE_TOOLBOX';
|
||||
|
||||
import makeToolboxXML from '../lib/make-toolbox-xml';
|
||||
|
||||
const initialState = {
|
||||
toolboxXML: makeToolboxXML()
|
||||
};
|
||||
|
||||
const reducer = function (state, action) {
|
||||
if (typeof state === 'undefined') state = initialState;
|
||||
switch (action.type) {
|
||||
case UPDATE_TOOLBOX:
|
||||
return Object.assign({}, state, {
|
||||
toolboxXML: action.toolboxXML
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const updateToolbox = function (toolboxXML) {
|
||||
return {
|
||||
type: UPDATE_TOOLBOX,
|
||||
toolboxXML: toolboxXML
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
reducer as default,
|
||||
updateToolbox
|
||||
};
|
||||
@@ -33,7 +33,7 @@ class SeleniumHelper {
|
||||
}
|
||||
|
||||
clickText (text) {
|
||||
return this.clickXpath(`//*[contains(text(), '${text}')]`);
|
||||
return this.clickXpath(`//body//*[contains(text(), '${text}')]`);
|
||||
}
|
||||
|
||||
clickButton (text) {
|
||||
|
||||
@@ -99,6 +99,15 @@ module.exports = {
|
||||
new CopyWebpackPlugin([{
|
||||
from: 'node_modules/scratch-blocks/media',
|
||||
to: 'static/blocks-media'
|
||||
}]),
|
||||
new CopyWebpackPlugin([{
|
||||
from: 'extensions/**',
|
||||
to: 'static',
|
||||
context: 'src/examples'
|
||||
}]),
|
||||
new CopyWebpackPlugin([{
|
||||
from: 'extension-worker.{js,js.map}',
|
||||
context: 'node_modules/scratch-vm/dist/web'
|
||||
}])
|
||||
].concat(process.env.NODE_ENV === 'production' ? [
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
|
||||
Reference in new issue
Block a user