Merge pull request #1025 from LLK/staging
Merge and freeze dependencies
This commit is contained in:
63 files changed
+5110
-44574
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
||||
language: node_js
|
||||
sudo: false
|
||||
sudo: required
|
||||
dist: trusty
|
||||
addons:
|
||||
chrome: stable
|
||||
|
||||
+21
-23
@@ -7,7 +7,7 @@
|
||||
"build": "npm run clean && webpack --progress --colors --bail",
|
||||
"clean": "rimraf ./build && mkdirp build",
|
||||
"deploy": "touch build/.nojekyll && gh-pages -t -d build -m \"Build for $(git log --pretty=format:%H -n1)\"",
|
||||
"i18n:src": "babel src > tmp.js && rimraf tmp.js && ./scripts/build-i18n-source.js ./translations/messages/ ./translations/",
|
||||
"i18n:src": "babel src > tmp.js && rimraf tmp.js && build-i18n-src ./translations/messages/ ./translations/",
|
||||
"start": "webpack-dev-server",
|
||||
"test": "npm run test:lint && npm run test:unit && NODE_ENV=production npm run build && npm run test:integration",
|
||||
"test:integration": "jest --runInBand test[\\\\/]integration",
|
||||
@@ -28,11 +28,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^7.1.3",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-core": "^6.23.1",
|
||||
"babel-eslint": "^8.0.1",
|
||||
"babel-loader": "^7.1.0",
|
||||
"babel-plugin-react-intl": "^2.3.1",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-async-to-generator": "^6.24.1",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.22.0",
|
||||
@@ -41,14 +39,14 @@
|
||||
"buffer-loader": "0.0.1",
|
||||
"chromedriver": "2.33.1",
|
||||
"classnames": "2.2.5",
|
||||
"copy-webpack-plugin": "^4.0.1",
|
||||
"copy-webpack-plugin": "^4.2.3",
|
||||
"css-loader": "^0.28.7",
|
||||
"enzyme": "^3.1.0",
|
||||
"enzyme-adapter-react-16": "1.0.3",
|
||||
"enzyme-adapter-react-16": "1.1.0",
|
||||
"eslint": "^4.7.1",
|
||||
"eslint-config-scratch": "^5.0.0",
|
||||
"eslint-plugin-import": "^2.7.0",
|
||||
"eslint-plugin-react": "^7.2.1",
|
||||
"eslint-plugin-react": "^7.5.1",
|
||||
"file-loader": "1.1.5",
|
||||
"get-float-time-domain-data": "0.1.0",
|
||||
"gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder",
|
||||
@@ -69,29 +67,29 @@
|
||||
"prop-types": "^15.5.10",
|
||||
"raf": "^3.4.0",
|
||||
"raw-loader": "0.5.1",
|
||||
"react": "16.0.0",
|
||||
"react-contextmenu": "2.8.0",
|
||||
"react-dom": "16.0.0",
|
||||
"react-draggable": "3.0.3",
|
||||
"react": "16.2.0",
|
||||
"react-contextmenu": "2.9.1",
|
||||
"react-dom": "16.2.0",
|
||||
"react-draggable": "3.0.4",
|
||||
"react-intl": "2.4.0",
|
||||
"react-intl-redux": "0.6.0",
|
||||
"react-modal": "3.0.2",
|
||||
"react-modal": "3.1.7",
|
||||
"react-redux": "5.0.6",
|
||||
"react-responsive": "3.0.0",
|
||||
"react-style-proptype": "3.0.0",
|
||||
"react-tabs": "2.1.0",
|
||||
"react-test-renderer": "16.0.0",
|
||||
"react-responsive": "4.0.3",
|
||||
"react-style-proptype": "3.1.0",
|
||||
"react-tabs": "2.1.1",
|
||||
"react-test-renderer": "16.2.0",
|
||||
"redux": "3.7.0",
|
||||
"redux-mock-store": "^1.2.3",
|
||||
"redux-throttle": "0.1.1",
|
||||
"rimraf": "^2.6.1",
|
||||
"scratch-audio": "latest",
|
||||
"scratch-blocks": "latest",
|
||||
"scratch-l10n": "^2.0.0",
|
||||
"scratch-paint": "latest",
|
||||
"scratch-render": "latest",
|
||||
"scratch-storage": "^0.3.0",
|
||||
"scratch-vm": "latest",
|
||||
"scratch-audio": "0.1.0-prerelease.1511362862",
|
||||
"scratch-blocks": "0.1.0-prerelease.1512689011",
|
||||
"scratch-l10n": "2.0.20171127165917",
|
||||
"scratch-paint": "0.1.0-prerelease.20171206013751",
|
||||
"scratch-render": "0.1.0-prerelease.1513001977",
|
||||
"scratch-storage": "0.3.0",
|
||||
"scratch-vm": "0.1.0-prerelease.1512751561-prerelease.1512751574",
|
||||
"selenium-webdriver": "3.5.0",
|
||||
"startaudiocontext": "1.2.1",
|
||||
"style-loader": "^0.19.0",
|
||||
@@ -100,7 +98,7 @@
|
||||
"wav-encoder": "1.3.0",
|
||||
"web-audio-test-api": "^0.5.2",
|
||||
"webpack": "^3.6.0",
|
||||
"webpack-dev-server": "^2.8.2",
|
||||
"webpack-dev-server": "^2.9.7",
|
||||
"xhr": "2.4.0"
|
||||
},
|
||||
"jest": {
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const glob = require('glob');
|
||||
const path = require('path');
|
||||
const mkdirp = require('mkdirp');
|
||||
|
||||
var args = process.argv.slice(2);
|
||||
|
||||
if (!args.length) {
|
||||
process.stdout.write('You must specify the messages dir generated by babel-plugin-react-intl.\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const MESSAGES_PATTERN = args.shift() + '/**/*.json';
|
||||
|
||||
if (!args.length) {
|
||||
process.stdout.write('A destination directory must be specified.\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const LANG_DIR = args.shift();
|
||||
|
||||
// Aggregates the default messages that were extracted from the example app's
|
||||
// React components via the React Intl Babel plugin. An error will be thrown if
|
||||
// there are messages in different components that use the same `id`. The result
|
||||
// is a chromei18n format collection of `id: {message: defaultMessage,
|
||||
// description: description}` pairs for the app's default locale.
|
||||
let defaultMessages = glob.sync(MESSAGES_PATTERN)
|
||||
.map((filename) => fs.readFileSync(filename, 'utf8'))
|
||||
.map((file) => JSON.parse(file))
|
||||
.reduce((collection, descriptors) => {
|
||||
descriptors.forEach(({id, defaultMessage, description}) => {
|
||||
if (collection.hasOwnProperty(id)) {
|
||||
throw new Error(`Duplicate message id: ${id}`);
|
||||
}
|
||||
|
||||
collection[id] = {message: defaultMessage, description: description};
|
||||
});
|
||||
|
||||
return collection;
|
||||
}, {});
|
||||
|
||||
mkdirp.sync(LANG_DIR);
|
||||
fs.writeFileSync(path.join(LANG_DIR, 'en.json'), JSON.stringify(defaultMessages, null, 2));
|
||||
@@ -1,4 +1,5 @@
|
||||
@import "../../css/colors.css";
|
||||
@import "../../css/units.css";
|
||||
|
||||
.wrapper {
|
||||
width: 150px;
|
||||
@@ -51,3 +52,14 @@
|
||||
min-height: 5rem;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $full-size-paint) {
|
||||
.wrapper {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
width: 4rem;
|
||||
min-height: 4rem;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import closeIcon from './icon--close.svg';
|
||||
|
||||
const CloseButton = props => (
|
||||
<div
|
||||
aria-label="Close"
|
||||
className={classNames(
|
||||
styles.closeButton,
|
||||
props.className,
|
||||
@@ -15,6 +16,8 @@ const CloseButton = props => (
|
||||
[styles.large]: props.size === CloseButton.SIZE_LARGE
|
||||
}
|
||||
)}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<img
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
@import "../../css/colors.css";
|
||||
@import "../../css/units.css";
|
||||
|
||||
.modal-content {
|
||||
width: 700px;
|
||||
}
|
||||
|
||||
.body {
|
||||
background: $ui-pane-gray;
|
||||
padding: 1.5rem 2.25rem;
|
||||
}
|
||||
|
||||
/* Blocks workspace for custom procedure declaration editor */
|
||||
.workspace {
|
||||
min-height: 200px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.workspace :global(.injectionDiv){
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.workspace :global(.blocklySvg) {
|
||||
background-color: #E7EDF2;
|
||||
}
|
||||
|
||||
/* Row of "card" buttons for modifying custom procedures */
|
||||
.options-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.option-card {
|
||||
background: white;
|
||||
border: 2px solid $form-border;
|
||||
border-radius: $space;
|
||||
padding: calc($space * 2);
|
||||
text-align: center;
|
||||
flex-grow: 1;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
flex-basis: 100px;
|
||||
}
|
||||
|
||||
.option-card:hover {
|
||||
border: 2px solid $motion-primary;
|
||||
box-shadow: 0px 0px 0px 3px $motion-transparent;
|
||||
}
|
||||
|
||||
.option-card + .option-card {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
max-height: 48px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.option-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Confirmation buttons at the bottom of the modal */
|
||||
.button-row {
|
||||
margin-top: 1rem;
|
||||
font-weight: bolder;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.button-row button {
|
||||
border: 1px solid $ui-pane-border;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: white;
|
||||
font-weight: bold;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.button-row button.ok-button {
|
||||
background: $motion-primary;
|
||||
border: $motion-primary;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.button-row button + button {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from '../modal/modal.jsx';
|
||||
import Box from '../box/box.jsx';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import booleanInputIcon from './icon--boolean-input.svg';
|
||||
import textInputIcon from './icon--text-input.svg';
|
||||
import labelIcon from './icon--label.svg';
|
||||
|
||||
import styles from './custom-procedures.css';
|
||||
|
||||
const CustomProcedures = props => (
|
||||
<Modal
|
||||
className={styles.modalContent}
|
||||
contentLabel="Create new block"
|
||||
onRequestClose={props.onCancel}
|
||||
>
|
||||
<Box
|
||||
className={styles.workspace}
|
||||
componentRef={props.componentRef}
|
||||
/>
|
||||
<Box className={styles.body}>
|
||||
<div className={styles.optionsRow}>
|
||||
<div
|
||||
className={styles.optionCard}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
onClick={props.onAddTextNumber}
|
||||
>
|
||||
<img
|
||||
className={styles.optionIcon}
|
||||
src={textInputIcon}
|
||||
/>
|
||||
<div className={styles.optionTitle}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add an input"
|
||||
description="Label for button to add a number/text input"
|
||||
id="gui.customProcedures.addAnInputNumberText"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.optionDescription}>
|
||||
<FormattedMessage
|
||||
defaultMessage="number or text"
|
||||
description="Description of the number/text input type"
|
||||
id="gui.customProcedures.numberTextType"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={styles.optionCard}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
onClick={props.onAddBoolean}
|
||||
>
|
||||
<img
|
||||
className={styles.optionIcon}
|
||||
src={booleanInputIcon}
|
||||
/>
|
||||
<div className={styles.optionTitle}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add an input"
|
||||
description="Label for button to add a boolean input"
|
||||
id="gui.customProcedures.addAnInputBoolean"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.optionDescription}>
|
||||
<FormattedMessage
|
||||
defaultMessage="boolean"
|
||||
description="Description of the boolean input type"
|
||||
id="gui.customProcedures.booleanType"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={styles.optionCard}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
onClick={props.onAddLabel}
|
||||
>
|
||||
<img
|
||||
className={styles.optionIcon}
|
||||
src={labelIcon}
|
||||
/>
|
||||
<div className={styles.optionTitle}>
|
||||
<FormattedMessage
|
||||
defaultMessage="Add a label"
|
||||
description="Label for button to add a label"
|
||||
id="gui.customProcedures.addALabel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.checkboxRow}>
|
||||
{/* @todo (#993) Implement "run without screen refresh" */}
|
||||
{/* <label><input type="checkbox" />Run without screen refresh</label> */}
|
||||
</div>
|
||||
<Box className={styles.buttonRow}>
|
||||
<button
|
||||
className={styles.cancelButton}
|
||||
onClick={props.onCancel}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Cancel"
|
||||
description="Label for button to cancel custom procedure edits"
|
||||
id="gui.customProcedures.cancel"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className={styles.okButton}
|
||||
onClick={props.onOk}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="OK"
|
||||
description="Label for button to save new custom procedure"
|
||||
id="gui.customProcedures.ok"
|
||||
/>
|
||||
</button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
CustomProcedures.propTypes = {
|
||||
componentRef: PropTypes.func.isRequired,
|
||||
onAddBoolean: PropTypes.func.isRequired,
|
||||
onAddLabel: PropTypes.func.isRequired,
|
||||
onAddTextNumber: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
onOk: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CustomProcedures;
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 57 49"><defs><style>.cls-1{opacity:0.3;}.cls-2{opacity:0.1;}.cls-3{fill:#ff6680;stroke-miterlimit:10;}.cls-3,.cls-4{stroke:#f35;}.cls-4{fill:#ff4d6a;stroke-linecap:round;stroke-linejoin:round;}</style></defs><title>R1_ C.Procedure Editble Inputs</title><g id="boolean_boolean_inputs" data-name="boolean + boolean inputs"><rect class="cls-3" x="0.5" y="0.5" width="56" height="48" rx="4" ry="4"/><path class="cls-4" d="M32.5,40.5h-8l-16-16h0l16-16h8l16,16h0Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 523 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 57 49"><defs><style>.cls-1{opacity:0.3;}.cls-2{opacity:0.1;}.cls-3{fill:#ff6680;stroke:#f35;stroke-miterlimit:10;}.cls-4{font-size:12px;fill:#fff;font-family:HelveticaNeue-Bold, Helvetica Neue;font-weight:700;letter-spacing:0em;}</style></defs><title>R1_ C.Procedure Editble Inputs</title><g id="boolean_boolean_inputs" data-name="boolean + boolean inputs"><rect class="cls-3" x="0.5" y="0.5" width="56" height="48" rx="4" ry="4"/><text class="cls-4" transform="translate(17.57 27.5)">text</text></g></svg>
|
||||
|
After Width: | Height: | Size: 559 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 57 49"><defs><style>.cls-1{opacity:0.3;}.cls-2{opacity:0.1;}.cls-3{fill:#ff6680;stroke-miterlimit:10;}.cls-3,.cls-4{stroke:#f35;}.cls-4{fill:#ff4d6a;stroke-linecap:round;stroke-linejoin:round;}</style></defs><title>R1_ C.Procedure Editble Inputs</title><g id="boolean_boolean_inputs" data-name="boolean + boolean inputs"><rect class="cls-3" x="0.5" y="0.5" width="56" height="48" rx="4" ry="4"/><rect class="cls-4" x="8.5" y="8.5" width="40" height="32" rx="16" ry="16"/></g></svg>
|
||||
|
After Width: | Height: | Size: 534 B |
@@ -68,13 +68,15 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tab.is-selected {
|
||||
z-index: 1;
|
||||
color: #40B9F5;
|
||||
background-color: #FFFFFF;
|
||||
border-bottom: white 1px solid;
|
||||
border-bottom: $ui-background-blue 1px solid;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
|
||||
@@ -11,6 +11,7 @@ const IconButton = ({
|
||||
}) => (
|
||||
<div
|
||||
className={classNames(styles.container, className)}
|
||||
role="button"
|
||||
onClick={onClick}
|
||||
>
|
||||
<img
|
||||
|
||||
@@ -22,3 +22,7 @@
|
||||
color: #3373cc;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.language-select:focus {
|
||||
border: 1px solid white;
|
||||
}
|
||||
@@ -18,6 +18,7 @@ const LanguageSelector = ({
|
||||
src={languageIcon}
|
||||
/>
|
||||
<select
|
||||
aria-label="language selector"
|
||||
className={styles.languageSelect}
|
||||
value={currentLocale}
|
||||
onChange={onChange}
|
||||
|
||||
@@ -10,15 +10,30 @@ class LibraryItem extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleBlur',
|
||||
'handleClick',
|
||||
'handleFocus',
|
||||
'handleKeyPress',
|
||||
'handleMouseEnter',
|
||||
'handleMouseLeave'
|
||||
]);
|
||||
}
|
||||
handleBlur () {
|
||||
this.props.onBlur(this.props.id);
|
||||
}
|
||||
handleFocus () {
|
||||
this.props.onFocus(this.props.id);
|
||||
}
|
||||
handleClick (e) {
|
||||
this.props.onSelect(this.props.id);
|
||||
e.preventDefault();
|
||||
}
|
||||
handleKeyPress (e) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.props.onSelect(this.props.id);
|
||||
}
|
||||
}
|
||||
handleMouseEnter () {
|
||||
this.props.onMouseEnter(this.props.id);
|
||||
}
|
||||
@@ -48,7 +63,12 @@ class LibraryItem extends React.PureComponent {
|
||||
) : (
|
||||
<Box
|
||||
className={styles.libraryItem}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
onBlur={this.handleBlur}
|
||||
onClick={this.handleClick}
|
||||
onFocus={this.handleFocus}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
@@ -73,6 +93,8 @@ LibraryItem.propTypes = {
|
||||
iconURL: PropTypes.string.isRequired,
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
onBlur: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
onMouseEnter: PropTypes.func.isRequired,
|
||||
onMouseLeave: PropTypes.func.isRequired,
|
||||
onSelect: PropTypes.func.isRequired
|
||||
|
||||
@@ -11,8 +11,10 @@ class LibraryComponent extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleBlur',
|
||||
'handleFilterChange',
|
||||
'handleFilterClear',
|
||||
'handleFocus',
|
||||
'handleMouseEnter',
|
||||
'handleMouseLeave',
|
||||
'handleSelect'
|
||||
@@ -22,6 +24,12 @@ class LibraryComponent extends React.Component {
|
||||
filterQuery: ''
|
||||
};
|
||||
}
|
||||
handleBlur (id) {
|
||||
this.handleMouseLeave(id);
|
||||
}
|
||||
handleFocus (id) {
|
||||
this.handleMouseEnter(id);
|
||||
}
|
||||
handleSelect (id) {
|
||||
this.props.onRequestClose();
|
||||
this.props.onItemSelected(this.getFilteredData()[id]);
|
||||
@@ -65,6 +73,8 @@ class LibraryComponent extends React.Component {
|
||||
id={index}
|
||||
key={`item_${index}`}
|
||||
name={dataItem.name}
|
||||
onBlur={this.handleBlur}
|
||||
onFocus={this.handleFocus}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
onSelect={this.handleSelect}
|
||||
|
||||
@@ -18,6 +18,7 @@ const MenuBar = function MenuBar () {
|
||||
>
|
||||
<div className={classNames(styles.logoWrapper, styles.menuItem)}>
|
||||
<img
|
||||
alt="Scratch"
|
||||
className={styles.scratchLogo}
|
||||
src={scratchLogo}
|
||||
/>
|
||||
|
||||
@@ -11,13 +11,13 @@ const MonitorList = props => (
|
||||
<Box
|
||||
className={styles.monitorList}
|
||||
>
|
||||
{props.monitors.valueSeq().map((monitorData, index) => (
|
||||
{props.monitors.valueSeq().map(monitorData => (
|
||||
<Monitor
|
||||
id={monitorData.id}
|
||||
index={index}
|
||||
key={monitorData.id}
|
||||
opcode={monitorData.opcode}
|
||||
params={monitorData.params}
|
||||
spriteName={monitorData.spriteName}
|
||||
value={monitorData.value}
|
||||
onDragEnd={props.onMonitorChange}
|
||||
/>
|
||||
|
||||
@@ -16,13 +16,12 @@ const MonitorComponent = props => (
|
||||
<Draggable
|
||||
bounds="parent"
|
||||
defaultClassNameDragging={styles.dragging}
|
||||
defaultPosition={{
|
||||
x: props.x,
|
||||
y: props.y
|
||||
}}
|
||||
onStop={props.onDragEnd}
|
||||
>
|
||||
<Box className={styles.monitor}>
|
||||
<Box
|
||||
className={styles.monitor}
|
||||
componentRef={props.componentRef}
|
||||
>
|
||||
<Box className={styles.label}>
|
||||
{props.label}
|
||||
</Box>
|
||||
@@ -40,17 +39,14 @@ MonitorComponent.categories = categories;
|
||||
|
||||
MonitorComponent.propTypes = {
|
||||
category: PropTypes.oneOf(Object.keys(categories)),
|
||||
componentRef: PropTypes.func.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
onDragEnd: PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number
|
||||
value: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
MonitorComponent.defaultProps = {
|
||||
category: 'data',
|
||||
x: 0,
|
||||
y: 0
|
||||
category: 'data'
|
||||
};
|
||||
|
||||
export default MonitorComponent;
|
||||
@@ -0,0 +1,57 @@
|
||||
@import "../../css/units.css";
|
||||
@import "../../css/colors.css";
|
||||
|
||||
.question-wrapper {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.question-container {
|
||||
margin: $space;
|
||||
border: 1px solid $form-border;
|
||||
border-radius: $space;
|
||||
border-width: 2px;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.question-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color: $text-primary;
|
||||
padding-bottom: $space;
|
||||
}
|
||||
|
||||
.question-input {
|
||||
display: flex; /* Keeps the input from going outside this container */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.question-submit-button {
|
||||
position: absolute;
|
||||
top: calc($space / 2);
|
||||
right: calc($space / 2);
|
||||
|
||||
width: calc(2rem - $space);
|
||||
height: calc(2rem - $space);
|
||||
|
||||
border: none;
|
||||
border-radius: 100%;
|
||||
|
||||
color: white;
|
||||
background: $motion-primary;
|
||||
}
|
||||
|
||||
/* Input overrides: width, font-weight, focus outline and padding */
|
||||
.question-input > input {
|
||||
width: 100%;
|
||||
padding: 0 2rem 0 0.75rem; /* To make room for the submit button */
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.question-input > input:focus {
|
||||
box-shadow: 0px 0px 0px 3px $motion-transparent;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import styles from './question.css';
|
||||
import Input from '../forms/input.jsx';
|
||||
|
||||
const QuestionComponent = props => {
|
||||
const {
|
||||
answer,
|
||||
question,
|
||||
onChange,
|
||||
onClick,
|
||||
onKeyPress
|
||||
} = props;
|
||||
return (
|
||||
<div className={styles.questionWrapper}>
|
||||
<div className={styles.questionContainer}>
|
||||
{question ? (
|
||||
<div className={styles.questionLabel}>{question}</div>
|
||||
) : null}
|
||||
<div className={styles.questionInput}>
|
||||
<Input
|
||||
autoFocus
|
||||
value={answer}
|
||||
onChange={onChange}
|
||||
onKeyPress={onKeyPress}
|
||||
/>
|
||||
<button
|
||||
className={styles.questionSubmitButton}
|
||||
onClick={onClick}
|
||||
>
|
||||
✔︎
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
QuestionComponent.propTypes = {
|
||||
answer: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onKeyPress: PropTypes.func.isRequired,
|
||||
question: PropTypes.string
|
||||
};
|
||||
|
||||
export default QuestionComponent;
|
||||
@@ -62,6 +62,11 @@
|
||||
border-bottom-left-radius: $form-radius;
|
||||
}
|
||||
|
||||
.radio-left:focus {
|
||||
border-color: #4c97ff;
|
||||
box-shadow: inset 0 0 0 -2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.radio-right {
|
||||
border-bottom: 1px solid $form-border;
|
||||
border-top: 1px solid $form-border;
|
||||
@@ -70,6 +75,11 @@
|
||||
border-bottom-right-radius: $form-radius;
|
||||
}
|
||||
|
||||
.radio-right:focus {
|
||||
border-color: #4c97ff;
|
||||
box-shadow: inset 0 0 0 -2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -83,3 +93,8 @@
|
||||
user-select: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.rotation-select:focus {
|
||||
border-color: #4c97ff;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
@@ -42,7 +42,7 @@ class SpriteInfo extends React.Component {
|
||||
<BufferedInput
|
||||
disabled={this.props.disabled}
|
||||
placeholder="Name"
|
||||
tabIndex="1"
|
||||
tabIndex="0"
|
||||
type="text"
|
||||
value={this.props.disabled ? '' : this.props.name}
|
||||
onSubmit={this.props.onChangeName}
|
||||
@@ -54,6 +54,7 @@ class SpriteInfo extends React.Component {
|
||||
<MediaQuery minWidth={layout.fullSizeMinWidth}>
|
||||
<div className={styles.iconWrapper}>
|
||||
<img
|
||||
aria-hidden="true"
|
||||
className={classNames(styles.xIcon, styles.icon)}
|
||||
src={xIcon}
|
||||
/>
|
||||
@@ -64,7 +65,7 @@ class SpriteInfo extends React.Component {
|
||||
small
|
||||
disabled={this.props.disabled}
|
||||
placeholder="x"
|
||||
tabIndex="2"
|
||||
tabIndex="0"
|
||||
type="text"
|
||||
value={this.props.disabled ? '' : this.props.x}
|
||||
onSubmit={this.props.onChangeX}
|
||||
@@ -76,6 +77,7 @@ class SpriteInfo extends React.Component {
|
||||
<MediaQuery minWidth={layout.fullSizeMinWidth}>
|
||||
<div className={styles.iconWrapper}>
|
||||
<img
|
||||
aria-hidden="true"
|
||||
className={classNames(styles.yIcon, styles.icon)}
|
||||
src={yIcon}
|
||||
/>
|
||||
@@ -86,7 +88,7 @@ class SpriteInfo extends React.Component {
|
||||
small
|
||||
disabled={this.props.disabled}
|
||||
placeholder="y"
|
||||
tabIndex="3"
|
||||
tabIndex="0"
|
||||
type="text"
|
||||
value={this.props.disabled ? '' : this.props.y}
|
||||
onSubmit={this.props.onChangeY}
|
||||
@@ -114,8 +116,9 @@ class SpriteInfo extends React.Component {
|
||||
[styles.isDisabled]: this.props.disabled
|
||||
}
|
||||
)}
|
||||
tabIndex="4"
|
||||
tabIndex="0"
|
||||
onClick={this.props.onClickVisible}
|
||||
onKeyPress={this.props.onPressVisible}
|
||||
>
|
||||
<img
|
||||
className={styles.icon}
|
||||
@@ -132,8 +135,9 @@ class SpriteInfo extends React.Component {
|
||||
[styles.isDisabled]: this.props.disabled
|
||||
}
|
||||
)}
|
||||
tabIndex="4"
|
||||
tabIndex="0"
|
||||
onClick={this.props.onClickNotVisible}
|
||||
onKeyPress={this.props.onPressNotVisible}
|
||||
>
|
||||
<img
|
||||
className={styles.icon}
|
||||
@@ -151,7 +155,7 @@ class SpriteInfo extends React.Component {
|
||||
small
|
||||
disabled={this.props.disabled}
|
||||
label="Direction"
|
||||
tabIndex="5"
|
||||
tabIndex="0"
|
||||
type="text"
|
||||
value={this.props.disabled ? '' : this.props.direction}
|
||||
onSubmit={this.props.onChangeDirection}
|
||||
@@ -166,6 +170,7 @@ class SpriteInfo extends React.Component {
|
||||
<select
|
||||
className={classNames(styles.selectForm, styles.rotationSelect)}
|
||||
disabled={this.props.disabled}
|
||||
tabIndex="0"
|
||||
value={this.props.rotationStyle}
|
||||
onChange={this.props.onChangeRotationStyle}
|
||||
>
|
||||
@@ -200,6 +205,8 @@ SpriteInfo.propTypes = {
|
||||
onChangeY: PropTypes.func,
|
||||
onClickNotVisible: PropTypes.func,
|
||||
onClickVisible: PropTypes.func,
|
||||
onPressNotVisible: PropTypes.func,
|
||||
onPressVisible: PropTypes.func,
|
||||
rotationStyle: PropTypes.oneOf(ROTATION_STYLES),
|
||||
visible: PropTypes.bool,
|
||||
x: PropTypes.oneOfType([
|
||||
|
||||
@@ -5,6 +5,7 @@ import classNames from 'classnames';
|
||||
import Box from '../box/box.jsx';
|
||||
import Loupe from '../loupe/loupe.jsx';
|
||||
import MonitorList from '../../containers/monitor-list.jsx';
|
||||
import Question from '../../containers/question.jsx';
|
||||
import styles from './stage.css';
|
||||
|
||||
const StageComponent = props => {
|
||||
@@ -15,6 +16,8 @@ const StageComponent = props => {
|
||||
colorInfo,
|
||||
onDeactivateColorPicker,
|
||||
isColorPicking,
|
||||
question,
|
||||
onQuestionAnswered,
|
||||
...boxProps
|
||||
} = props;
|
||||
return (
|
||||
@@ -40,6 +43,12 @@ const StageComponent = props => {
|
||||
<Loupe colorInfo={colorInfo} />
|
||||
</Box>
|
||||
) : null}
|
||||
{question === null ? null : (
|
||||
<Question
|
||||
question={question}
|
||||
onQuestionAnswered={onQuestionAnswered}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{isColorPicking ? (
|
||||
<Box
|
||||
@@ -56,6 +65,8 @@ StageComponent.propTypes = {
|
||||
height: PropTypes.number,
|
||||
isColorPicking: PropTypes.bool,
|
||||
onDeactivateColorPicker: PropTypes.func,
|
||||
onQuestionAnswered: PropTypes.func,
|
||||
question: PropTypes.string,
|
||||
width: PropTypes.number
|
||||
};
|
||||
StageComponent.defaultProps = {
|
||||
|
||||
@@ -33,7 +33,7 @@ const Waveform = props => {
|
||||
<g transform={`scale(1, -1) translate(0, -${height / 2})`}>
|
||||
<path
|
||||
className={styles.waveformPath}
|
||||
d={`M0 0${pathComponents.join()}Z`}
|
||||
d={`M0 0${pathComponents.join(' ')}Z`}
|
||||
strokeLinejoin={'round'}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
|
||||
@@ -9,11 +9,13 @@ import VM from 'scratch-vm';
|
||||
import Prompt from './prompt.jsx';
|
||||
import BlocksComponent from '../components/blocks/blocks.jsx';
|
||||
import ExtensionLibrary from './extension-library.jsx';
|
||||
import CustomProcedures from './custom-procedures.jsx';
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {updateToolbox} from '../reducers/toolbox';
|
||||
import {activateColorPicker} from '../reducers/color-picker';
|
||||
import {closeExtensionLibrary} from '../reducers/modals';
|
||||
import {activateCustomProcedures, deactivateCustomProcedures} from '../reducers/custom-procedures';
|
||||
|
||||
const addFunctionListener = (object, property, callback) => {
|
||||
const oldFn = object[property];
|
||||
@@ -35,6 +37,7 @@ class Blocks extends React.Component {
|
||||
'handlePromptStart',
|
||||
'handlePromptCallback',
|
||||
'handlePromptClose',
|
||||
'handleCustomProceduresClose',
|
||||
'onScriptGlowOn',
|
||||
'onScriptGlowOff',
|
||||
'onBlockGlowOn',
|
||||
@@ -55,6 +58,7 @@ class Blocks extends React.Component {
|
||||
}
|
||||
componentDidMount () {
|
||||
this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker;
|
||||
this.ScratchBlocks.Procedures.externalProcedureDefCallback = this.props.onActivateCustomProcedures;
|
||||
|
||||
const workspaceConfig = defaultsDeep({},
|
||||
Blocks.defaultOptions,
|
||||
@@ -74,7 +78,8 @@ class Blocks extends React.Component {
|
||||
this.state.prompt !== nextState.prompt ||
|
||||
this.props.isVisible !== nextProps.isVisible ||
|
||||
this.props.toolboxXML !== nextProps.toolboxXML ||
|
||||
this.props.extensionLibraryVisible !== nextProps.extensionLibraryVisible
|
||||
this.props.extensionLibraryVisible !== nextProps.extensionLibraryVisible ||
|
||||
this.props.customProceduresVisible !== nextProps.customProceduresVisible
|
||||
);
|
||||
}
|
||||
componentDidUpdate (prevProps) {
|
||||
@@ -172,6 +177,14 @@ class Blocks extends React.Component {
|
||||
this.workspace.reportValue(data.id, data.value);
|
||||
}
|
||||
onWorkspaceUpdate (data) {
|
||||
// When we change sprites, update the toolbox to have the new sprite's blocks
|
||||
if (this.props.vm.editingTarget) {
|
||||
const target = this.props.vm.editingTarget;
|
||||
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML();
|
||||
const toolboxXML = makeToolboxXML(target.isStage, target.id, dynamicBlocksXML);
|
||||
this.props.updateToolboxState(toolboxXML);
|
||||
}
|
||||
|
||||
if (this.props.vm.editingTarget && !this.state.workspaceMetrics[this.props.vm.editingTarget.id]) {
|
||||
this.onWorkspaceMetricsChange();
|
||||
}
|
||||
@@ -179,6 +192,8 @@ class Blocks extends React.Component {
|
||||
// Remove and reattach the workspace listener (but allow flyout events)
|
||||
this.workspace.removeChangeListener(this.props.vm.blockListener);
|
||||
const dom = this.ScratchBlocks.Xml.textToDom(data.xml);
|
||||
// @todo This line rerenders toolbox, and the change in the toolbox XML also rerenders the toolbox.
|
||||
// We should only rerender the toolbox once. See https://github.com/LLK/scratch-gui/issues/901
|
||||
this.ScratchBlocks.Xml.clearWorkspaceAndLoadFromXml(dom, this.workspace);
|
||||
this.workspace.addChangeListener(this.props.vm.blockListener);
|
||||
|
||||
@@ -193,8 +208,9 @@ class Blocks extends React.Component {
|
||||
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);
|
||||
const target = this.props.vm.editingTarget;
|
||||
const toolboxXML = makeToolboxXML(target.isStage, target.id, dynamicBlocksXML);
|
||||
this.props.updateToolboxState(toolboxXML);
|
||||
}
|
||||
handleCategorySelected (categoryName) {
|
||||
this.workspace.toolbox_.setSelectedCategoryByName(categoryName);
|
||||
@@ -212,16 +228,23 @@ class Blocks extends React.Component {
|
||||
handlePromptClose () {
|
||||
this.setState({prompt: null});
|
||||
}
|
||||
handleCustomProceduresClose (data) {
|
||||
this.props.onRequestCloseCustomProcedures(data);
|
||||
this.workspace.refreshToolboxSelection_();
|
||||
}
|
||||
render () {
|
||||
/* eslint-disable no-unused-vars */
|
||||
const {
|
||||
customProceduresVisible,
|
||||
extensionLibraryVisible,
|
||||
options,
|
||||
vm,
|
||||
isVisible,
|
||||
onActivateColorPicker,
|
||||
onExtensionAdded,
|
||||
updateToolboxState,
|
||||
onActivateCustomProcedures,
|
||||
onRequestCloseExtensionLibrary,
|
||||
onRequestCloseCustomProcedures,
|
||||
toolboxXML,
|
||||
...props
|
||||
} = this.props;
|
||||
@@ -248,16 +271,26 @@ class Blocks extends React.Component {
|
||||
onRequestClose={onRequestCloseExtensionLibrary}
|
||||
/>
|
||||
) : null}
|
||||
{customProceduresVisible ? (
|
||||
<CustomProcedures
|
||||
options={{
|
||||
media: options.media
|
||||
}}
|
||||
onRequestClose={this.handleCustomProceduresClose}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Blocks.propTypes = {
|
||||
customProceduresVisible: PropTypes.bool,
|
||||
extensionLibraryVisible: PropTypes.bool,
|
||||
isVisible: PropTypes.bool,
|
||||
onActivateColorPicker: PropTypes.func,
|
||||
onExtensionAdded: PropTypes.func,
|
||||
onActivateCustomProcedures: PropTypes.func,
|
||||
onRequestCloseCustomProcedures: PropTypes.func,
|
||||
onRequestCloseExtensionLibrary: PropTypes.func,
|
||||
options: PropTypes.shape({
|
||||
media: PropTypes.string,
|
||||
@@ -281,6 +314,7 @@ Blocks.propTypes = {
|
||||
comments: PropTypes.bool
|
||||
}),
|
||||
toolboxXML: PropTypes.string,
|
||||
updateToolboxState: PropTypes.func,
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
|
||||
@@ -317,16 +351,21 @@ Blocks.defaultProps = {
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
extensionLibraryVisible: state.modals.extensionLibrary,
|
||||
toolboxXML: state.toolbox.toolboxXML
|
||||
toolboxXML: state.toolbox.toolboxXML,
|
||||
customProceduresVisible: state.customProcedures.active
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onActivateColorPicker: callback => dispatch(activateColorPicker(callback)),
|
||||
onExtensionAdded: toolboxXML => {
|
||||
dispatch(updateToolbox(toolboxXML));
|
||||
},
|
||||
onActivateCustomProcedures: (data, callback) => dispatch(activateCustomProcedures(data, callback)),
|
||||
onRequestCloseExtensionLibrary: () => {
|
||||
dispatch(closeExtensionLibrary());
|
||||
},
|
||||
onRequestCloseCustomProcedures: data => {
|
||||
dispatch(deactivateCustomProcedures(data));
|
||||
},
|
||||
updateToolboxState: toolboxXML => {
|
||||
dispatch(updateToolbox(toolboxXML));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import bindAll from 'lodash.bindall';
|
||||
import defaultsDeep from 'lodash.defaultsdeep';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import CustomProceduresComponent from '../components/custom-procedures/custom-procedures.jsx';
|
||||
import ScratchBlocks from 'scratch-blocks';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
class CustomProcedures extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleAddLabel',
|
||||
'handleAddBoolean',
|
||||
'handleAddTextNumber',
|
||||
'handleCancel',
|
||||
'handleOk',
|
||||
'setBlocks'
|
||||
]);
|
||||
}
|
||||
componentWillUnmount () {
|
||||
if (this.workspace) {
|
||||
this.workspace.dispose();
|
||||
}
|
||||
}
|
||||
setBlocks (blocksRef) {
|
||||
if (!blocksRef) return;
|
||||
this.blocks = blocksRef;
|
||||
const workspaceConfig = defaultsDeep({},
|
||||
CustomProcedures.defaultOptions,
|
||||
this.props.options
|
||||
);
|
||||
|
||||
// @todo This is a hack to make there be no toolbox.
|
||||
const oldDefaultToolbox = ScratchBlocks.Blocks.defaultToolbox;
|
||||
ScratchBlocks.Blocks.defaultToolbox = null;
|
||||
this.workspace = ScratchBlocks.inject(this.blocks, workspaceConfig);
|
||||
ScratchBlocks.Blocks.defaultToolbox = oldDefaultToolbox;
|
||||
|
||||
// Create the procedure declaration block for editing the mutation.
|
||||
this.mutationRoot = this.workspace.newBlock('procedures_declaration');
|
||||
// Make the declaration immovable, undeletable and have no context menu
|
||||
this.mutationRoot.setMovable(false);
|
||||
this.mutationRoot.setDeletable(false);
|
||||
this.mutationRoot.contextMenu = false;
|
||||
|
||||
this.workspace.addChangeListener(() => {
|
||||
this.mutationRoot.onChangeFn();
|
||||
// Keep the block centered on the workspace
|
||||
const metrics = this.workspace.getMetrics();
|
||||
const {x, y} = this.mutationRoot.getRelativeToSurfaceXY();
|
||||
const dy = (metrics.viewHeight / 2) - (this.mutationRoot.height / 2) - y;
|
||||
let dx = (metrics.viewWidth / 2) - (this.mutationRoot.width / 2) - x;
|
||||
// If the procedure declaration is wider than the view width,
|
||||
// keep the right-hand side of the procedure in view.
|
||||
if (this.mutationRoot.width > metrics.viewWidth) {
|
||||
dx = metrics.viewWidth - this.mutationRoot.width - x;
|
||||
}
|
||||
this.mutationRoot.moveBy(dx, dy);
|
||||
});
|
||||
this.mutationRoot.domToMutation(this.props.mutator);
|
||||
this.mutationRoot.initSvg();
|
||||
this.mutationRoot.render();
|
||||
}
|
||||
handleCancel () {
|
||||
this.props.onRequestClose();
|
||||
}
|
||||
handleOk () {
|
||||
const newMutation = this.mutationRoot ? this.mutationRoot.mutationToDom() : null;
|
||||
this.props.onRequestClose(newMutation);
|
||||
}
|
||||
handleAddLabel () {
|
||||
if (this.mutationRoot) {
|
||||
this.mutationRoot.addLabelExternal();
|
||||
}
|
||||
}
|
||||
handleAddBoolean () {
|
||||
if (this.mutationRoot) {
|
||||
this.mutationRoot.addBooleanExternal();
|
||||
}
|
||||
}
|
||||
handleAddTextNumber () {
|
||||
if (this.mutationRoot) {
|
||||
this.mutationRoot.addStringNumberExternal();
|
||||
}
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<CustomProceduresComponent
|
||||
componentRef={this.setBlocks}
|
||||
onAddBoolean={this.handleAddBoolean}
|
||||
onAddLabel={this.handleAddLabel}
|
||||
onAddTextNumber={this.handleAddTextNumber}
|
||||
onCancel={this.handleCancel}
|
||||
onOk={this.handleOk}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CustomProcedures.propTypes = {
|
||||
mutator: PropTypes.instanceOf(Element),
|
||||
onRequestClose: PropTypes.func.isRequired,
|
||||
options: PropTypes.shape({
|
||||
media: PropTypes.string,
|
||||
zoom: PropTypes.shape({
|
||||
controls: PropTypes.bool,
|
||||
wheel: PropTypes.bool,
|
||||
startScale: PropTypes.number
|
||||
}),
|
||||
comments: PropTypes.bool
|
||||
})
|
||||
};
|
||||
|
||||
CustomProcedures.defaultOptions = {
|
||||
zoom: {
|
||||
controls: false,
|
||||
wheel: false,
|
||||
startScale: 0.9
|
||||
},
|
||||
comments: false,
|
||||
scrollbars: true
|
||||
};
|
||||
|
||||
CustomProcedures.defaultProps = {
|
||||
options: CustomProcedures.defaultOptions
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
mutator: state.customProcedures.mutator
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps
|
||||
)(CustomProcedures);
|
||||
@@ -1,7 +1,9 @@
|
||||
import bindAll from 'lodash.bindall';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {moveMonitorRect} from '../reducers/monitor-layout';
|
||||
|
||||
import MonitorListComponent from '../components/monitor-list/monitor-list.jsx';
|
||||
|
||||
@@ -13,7 +15,7 @@ class MonitorList extends React.Component {
|
||||
]);
|
||||
}
|
||||
handleMonitorChange (id, x, y) { // eslint-disable-line no-unused-vars
|
||||
// @todo send this event to the VM
|
||||
this.props.moveMonitorRect(id, x, y);
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
@@ -25,10 +27,15 @@ class MonitorList extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
MonitorList.propTypes = {
|
||||
moveMonitorRect: PropTypes.func.isRequired
|
||||
};
|
||||
const mapStateToProps = state => ({
|
||||
monitors: state.monitors
|
||||
});
|
||||
const mapDispatchToProps = () => ({});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
moveMonitorRect: (id, x, y) => dispatch(moveMonitorRect(id, x, y))
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
|
||||
@@ -4,25 +4,69 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import monitorAdapter from '../lib/monitor-adapter.js';
|
||||
import MonitorComponent from '../components/monitor/monitor.jsx';
|
||||
import {addMonitorRect, getInitialPosition, resizeMonitorRect, removeMonitorRect} from '../reducers/monitor-layout';
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
class Monitor extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleDragEnd'
|
||||
'handleDragEnd',
|
||||
'setElement'
|
||||
]);
|
||||
}
|
||||
componentDidMount () {
|
||||
let rect;
|
||||
// Load the VM provided position if not loaded already
|
||||
if (this.props.x && this.props.y && !this.props.monitorLayout.savedMonitorPositions[this.props.id]) {
|
||||
rect = {
|
||||
upperStart: {x: this.props.x, y: this.props.y},
|
||||
lowerEnd: {x: this.props.x + this.element.offsetWidth, y: this.props.y + this.element.offsetHeight}
|
||||
};
|
||||
this.props.addMonitorRect(this.props.id, rect, true /* savePosition */);
|
||||
} else { // Newly created user monitor
|
||||
rect = getInitialPosition(
|
||||
this.props.monitorLayout, this.props.id, this.element.offsetWidth, this.element.offsetHeight);
|
||||
this.props.addMonitorRect(this.props.id, rect);
|
||||
}
|
||||
this.element.style.top = `${rect.upperStart.y}px`;
|
||||
this.element.style.left = `${rect.upperStart.x}px`;
|
||||
}
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
if (nextState !== this.state) {
|
||||
return true;
|
||||
}
|
||||
for (const key of Object.getOwnPropertyNames(nextProps)) {
|
||||
// Don't need to rerender when other monitors are moved.
|
||||
// monitorLayout is only used during initial layout.
|
||||
if (key !== 'monitorLayout' && nextProps[key] !== this.props[key]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
componentDidUpdate () {
|
||||
this.props.resizeMonitorRect(this.props.id, this.element.offsetWidth, this.element.offsetHeight);
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this.props.removeMonitorRect(this.props.id);
|
||||
}
|
||||
handleDragEnd (e, {x, y}) {
|
||||
this.props.onDragEnd(
|
||||
this.props.id,
|
||||
x,
|
||||
y
|
||||
parseInt(this.element.style.left, 10) + x,
|
||||
parseInt(this.element.style.top, 10) + y
|
||||
);
|
||||
}
|
||||
setElement (monitorElt) {
|
||||
this.element = monitorElt;
|
||||
}
|
||||
render () {
|
||||
const monitorProps = monitorAdapter(this.props);
|
||||
return (
|
||||
<MonitorComponent
|
||||
componentRef={this.setElement}
|
||||
{...monitorProps}
|
||||
onDragEnd={this.handleDragEnd}
|
||||
/>
|
||||
@@ -31,12 +75,32 @@ class Monitor extends React.Component {
|
||||
}
|
||||
|
||||
Monitor.propTypes = {
|
||||
addMonitorRect: PropTypes.func.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired, // eslint-disable-line react/no-unused-prop-types
|
||||
monitorLayout: PropTypes.shape({
|
||||
monitors: PropTypes.object,
|
||||
savedMonitorPositions: PropTypes.object
|
||||
}).isRequired,
|
||||
onDragEnd: PropTypes.func.isRequired,
|
||||
opcode: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types
|
||||
params: PropTypes.object, // eslint-disable-line react/no-unused-prop-types, react/forbid-prop-types
|
||||
value: PropTypes.string.isRequired // eslint-disable-line react/no-unused-prop-types
|
||||
removeMonitorRect: PropTypes.func.isRequired,
|
||||
resizeMonitorRect: PropTypes.func.isRequired,
|
||||
spriteName: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
|
||||
value: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number
|
||||
};
|
||||
|
||||
export default Monitor;
|
||||
const mapStateToProps = state => ({
|
||||
monitorLayout: state.monitorLayout
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
addMonitorRect: (id, rect, savePosition) =>
|
||||
dispatch(addMonitorRect(id, rect.upperStart, rect.lowerEnd, savePosition)),
|
||||
resizeMonitorRect: (id, newWidth, newHeight) => dispatch(resizeMonitorRect(id, newWidth, newHeight)),
|
||||
removeMonitorRect: id => dispatch(removeMonitorRect(id))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Monitor);
|
||||
@@ -0,0 +1,45 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import bindAll from 'lodash.bindall';
|
||||
import QuestionComponent from '../components/question/question.jsx';
|
||||
|
||||
class Question extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleChange',
|
||||
'handleKeyPress',
|
||||
'handleSubmit'
|
||||
]);
|
||||
this.state = {
|
||||
answer: ''
|
||||
};
|
||||
}
|
||||
handleChange (e) {
|
||||
this.setState({answer: e.target.value});
|
||||
}
|
||||
handleKeyPress (event) {
|
||||
if (event.key === 'Enter') this.handleSubmit();
|
||||
}
|
||||
handleSubmit () {
|
||||
this.props.onQuestionAnswered(this.state.answer);
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<QuestionComponent
|
||||
answer={this.state.answer}
|
||||
question={this.props.question}
|
||||
onChange={this.handleChange}
|
||||
onClick={this.handleSubmit}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Question.propTypes = {
|
||||
onQuestionAnswered: PropTypes.func.isRequired,
|
||||
question: PropTypes.string
|
||||
};
|
||||
|
||||
export default Question;
|
||||
@@ -124,6 +124,7 @@ class SoundEditor extends React.Component {
|
||||
const samples = this.undoStack.pop();
|
||||
if (samples) {
|
||||
this.submitNewSamples(samples, this.props.sampleRate, true);
|
||||
this.handlePlay();
|
||||
}
|
||||
}
|
||||
handleRedo () {
|
||||
@@ -131,6 +132,7 @@ class SoundEditor extends React.Component {
|
||||
if (samples) {
|
||||
this.undoStack.push(this.props.samples.slice(0));
|
||||
this.submitNewSamples(samples, this.props.sampleRate, true);
|
||||
this.handlePlay();
|
||||
}
|
||||
}
|
||||
render () {
|
||||
|
||||
@@ -52,20 +52,18 @@ class SoundTab extends React.Component {
|
||||
|
||||
render () {
|
||||
const {
|
||||
editingTarget,
|
||||
sprites,
|
||||
stage,
|
||||
vm,
|
||||
onNewSoundFromLibraryClick,
|
||||
onNewSoundFromRecordingClick
|
||||
} = this.props;
|
||||
|
||||
const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage;
|
||||
|
||||
if (!target) {
|
||||
if (!vm.editingTarget) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sounds = target.sounds ? target.sounds.map(sound => (
|
||||
const sprite = vm.editingTarget.sprite;
|
||||
|
||||
const sounds = sprite.sounds ? sprite.sounds.map(sound => (
|
||||
{
|
||||
url: soundIcon,
|
||||
name: sound.name
|
||||
@@ -106,7 +104,7 @@ class SoundTab extends React.Component {
|
||||
onDeleteClick={this.handleDeleteSound}
|
||||
onItemClick={this.handleSelectSound}
|
||||
>
|
||||
{editingTarget && target.sounds && target.sounds.length > 0 ? (
|
||||
{sprite.sounds && sprite.sounds[this.state.selectedSoundIndex] ? (
|
||||
<SoundEditor soundIndex={this.state.selectedSoundIndex} />
|
||||
) : null}
|
||||
{this.props.soundRecorderVisible ? (
|
||||
|
||||
@@ -10,7 +10,9 @@ class SpriteInfo extends React.Component {
|
||||
bindAll(this, [
|
||||
'handleChangeRotationStyle',
|
||||
'handleClickVisible',
|
||||
'handleClickNotVisible'
|
||||
'handleClickNotVisible',
|
||||
'handlePressVisible',
|
||||
'handlePressNotVisible'
|
||||
]);
|
||||
}
|
||||
handleChangeRotationStyle (e) {
|
||||
@@ -24,6 +26,18 @@ class SpriteInfo extends React.Component {
|
||||
e.preventDefault();
|
||||
this.props.onChangeVisibility(false);
|
||||
}
|
||||
handlePressVisible (e) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.props.onChangeVisibility(true);
|
||||
}
|
||||
}
|
||||
handlePressNotVisible (e) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.props.onChangeVisibility(false);
|
||||
}
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<SpriteInfoComponent
|
||||
@@ -31,6 +45,8 @@ class SpriteInfo extends React.Component {
|
||||
onChangeRotationStyle={this.handleChangeRotationStyle}
|
||||
onClickNotVisible={this.handleClickNotVisible}
|
||||
onClickVisible={this.handleClickVisible}
|
||||
onPressNotVisible={this.handlePressNotVisible}
|
||||
onPressVisible={this.handlePressVisible}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,12 +24,14 @@ class Stage extends React.Component {
|
||||
'cancelMouseDownTimeout',
|
||||
'detachMouseEvents',
|
||||
'handleDoubleClick',
|
||||
'handleQuestionAnswered',
|
||||
'onMouseUp',
|
||||
'onMouseMove',
|
||||
'onMouseDown',
|
||||
'onStartDrag',
|
||||
'onStopDrag',
|
||||
'updateRect',
|
||||
'questionListener',
|
||||
'setCanvas'
|
||||
]);
|
||||
this.state = {
|
||||
@@ -38,7 +40,8 @@ class Stage extends React.Component {
|
||||
isDragging: false,
|
||||
dragOffset: null,
|
||||
dragId: null,
|
||||
colorInfo: null
|
||||
colorInfo: null,
|
||||
question: null
|
||||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
@@ -47,12 +50,14 @@ class Stage extends React.Component {
|
||||
this.updateRect();
|
||||
this.renderer = new Renderer(this.canvas);
|
||||
this.props.vm.attachRenderer(this.renderer);
|
||||
this.props.vm.runtime.addListener('QUESTION', this.questionListener);
|
||||
}
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
return this.props.width !== nextProps.width ||
|
||||
this.props.height !== nextProps.height ||
|
||||
this.props.isColorPicking !== nextProps.isColorPicking ||
|
||||
this.state.colorInfo !== nextState.colorInfo;
|
||||
this.state.colorInfo !== nextState.colorInfo ||
|
||||
this.state.question !== nextState.question;
|
||||
}
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.props.isColorPicking && !prevProps.isColorPicking) {
|
||||
@@ -66,6 +71,14 @@ class Stage extends React.Component {
|
||||
this.detachRectEvents();
|
||||
this.stopColorPickingLoop();
|
||||
}
|
||||
questionListener (question) {
|
||||
this.setState({question: question});
|
||||
}
|
||||
handleQuestionAnswered (answer) {
|
||||
this.setState({question: null}, () => {
|
||||
this.props.vm.runtime.emit('ANSWER', answer);
|
||||
});
|
||||
}
|
||||
startColorPickingLoop () {
|
||||
this.intervalId = setInterval(() => {
|
||||
this.setState({colorInfo: this.getColorInfo(this.pickX, this.pickY)});
|
||||
@@ -251,7 +264,9 @@ class Stage extends React.Component {
|
||||
<StageComponent
|
||||
canvasRef={this.setCanvas}
|
||||
colorInfo={this.state.colorInfo}
|
||||
question={this.state.question}
|
||||
onDoubleClick={this.handleDoubleClick}
|
||||
onQuestionAnswered={this.handleQuestionAnswered}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -9,3 +9,7 @@ $stage-menu-height: 2.75rem;
|
||||
$library-header-height: 4.375rem;
|
||||
|
||||
$form-radius: calc($space / 2);
|
||||
|
||||
/* layout contants from `layout-constants.js` */
|
||||
$full-size: 1095px;
|
||||
$full-size-paint: 1249px;
|
||||
@@ -3,7 +3,16 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="shortcut icon" href="static/favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
|
||||
<!-- Sentry error logging to help with finding bugs -->
|
||||
<script src="https://cdn.ravenjs.com/3.20.1/raven.min.js" crossorigin="anonymous"></script>
|
||||
<script>
|
||||
Raven.config('https://dcc4ed76876b411c99997f4ad5ab78a5@sentry.io/253297').install();
|
||||
</script>
|
||||
<!-- /Sentry -->
|
||||
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
import AppStateHOC from './lib/app-state-hoc.jsx';
|
||||
import GUI from './containers/gui.jsx';
|
||||
@@ -7,10 +8,17 @@ import ProjectLoaderHOC from './lib/project-loader-hoc.jsx';
|
||||
|
||||
import styles from './index.css';
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && typeof window === 'object') {
|
||||
// Warn before navigating away
|
||||
window.onbeforeunload = () => true;
|
||||
}
|
||||
|
||||
const App = AppStateHOC(ProjectLoaderHOC(GUI));
|
||||
|
||||
const appTarget = document.createElement('div');
|
||||
appTarget.className = styles.app;
|
||||
document.body.appendChild(appTarget);
|
||||
|
||||
Modal.setAppElement(appTarget);
|
||||
|
||||
ReactDOM.render(<App />, appTarget);
|
||||
+16
-7
@@ -65,6 +65,17 @@ export default function (vm) {
|
||||
return sprites;
|
||||
};
|
||||
|
||||
const cloneMenu = function () {
|
||||
if (vm.editingTarget && vm.editingTarget.isStage) {
|
||||
const menu = spriteMenu();
|
||||
if (menu.length === 0) {
|
||||
return [['', '']]; // Empty menu matches Scratch 2 behavior
|
||||
}
|
||||
return menu;
|
||||
}
|
||||
return [['myself', '_myself_']].concat(spriteMenu());
|
||||
};
|
||||
|
||||
const soundColors = ScratchBlocks.Colours.sounds;
|
||||
|
||||
const looksColors = ScratchBlocks.Colours.looks;
|
||||
@@ -99,16 +110,16 @@ export default function (vm) {
|
||||
|
||||
ScratchBlocks.Blocks.motion_goto_menu.init = function () {
|
||||
const json = jsonForMenuBlock('TO', spriteMenu, motionColors, [
|
||||
['mouse-pointer', '_mouse_'],
|
||||
['random position', '_random_']
|
||||
['random position', '_random_'],
|
||||
['mouse-pointer', '_mouse_']
|
||||
]);
|
||||
this.jsonInit(json);
|
||||
};
|
||||
|
||||
ScratchBlocks.Blocks.motion_glideto_menu.init = function () {
|
||||
const json = jsonForMenuBlock('TO', spriteMenu, motionColors, [
|
||||
['mouse-pointer', '_mouse_'],
|
||||
['random position', '_random_']
|
||||
['random position', '_random_'],
|
||||
['mouse-pointer', '_mouse_']
|
||||
]);
|
||||
this.jsonInit(json);
|
||||
};
|
||||
@@ -143,9 +154,7 @@ export default function (vm) {
|
||||
};
|
||||
|
||||
ScratchBlocks.Blocks.control_create_clone_of_menu.init = function () {
|
||||
const json = jsonForMenuBlock('CLONE_OPTION', spriteMenu, controlColors, [
|
||||
['myself', '_myself_']
|
||||
]);
|
||||
const json = jsonForMenuBlock('CLONE_OPTION', cloneMenu, controlColors, []);
|
||||
this.jsonInit(json);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,5 +3,6 @@ export default {
|
||||
fullStageHeight: 360,
|
||||
smallerStageWidth: 480 * 0.85,
|
||||
smallerStageHeight: 360 * 0.85,
|
||||
fullSizeMinWidth: 1096
|
||||
fullSizeMinWidth: 1096,
|
||||
fullSizePaintMinWidth: 1250
|
||||
};
|
||||
+33
-1290
File diff suppressed because it is too large.
Load diff
+1031
-10242
File diff suppressed because it is too large.
Load diff
@@ -1,19 +1,19 @@
|
||||
import musicImage from './music.png';
|
||||
import penImage from './pen.png';
|
||||
import wedoImage from './wedo.png';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'Music',
|
||||
extensionURL: 'music',
|
||||
iconURL: musicImage,
|
||||
description: 'Play instruments and drums.',
|
||||
featured: true
|
||||
},
|
||||
{
|
||||
name: 'Pen',
|
||||
extensionURL: 'pen',
|
||||
iconURL: penImage,
|
||||
description: 'Draw with your sprites.',
|
||||
featured: true
|
||||
},
|
||||
{
|
||||
name: 'Lego WeDo 2.0',
|
||||
extensionURL: 'wedo2',
|
||||
iconURL: wedoImage,
|
||||
description: 'Build with motors and sensors.',
|
||||
featured: true
|
||||
}
|
||||
];
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
+1648
-1004
File diff suppressed because it is too large.
Load diff
+522
-31699
File diff suppressed because it is too large.
Load diff
+168
-162
@@ -2,8 +2,12 @@ const categorySeparator = '<sep gap="36"/>';
|
||||
|
||||
const blockSeparator = '<sep gap="36"/>'; // At default scale, about 28px
|
||||
|
||||
const motion = `
|
||||
const motion = function (isStage, targetId) {
|
||||
return `
|
||||
<category name="Motion" colour="#4C97FF" secondaryColour="#3373CC">
|
||||
${isStage ? `
|
||||
<label text="Stage selected: no motion blocks"></label>
|
||||
` : `
|
||||
<block type="motion_movesteps">
|
||||
<value name="STEPS">
|
||||
<shadow type="math_number">
|
||||
@@ -120,15 +124,18 @@ const motion = `
|
||||
${blockSeparator}
|
||||
<block type="motion_setrotationstyle"/>
|
||||
${blockSeparator}
|
||||
<block id="xposition" type="motion_xposition"/>
|
||||
<block id="yposition" type="motion_yposition"/>
|
||||
<block id="direction" type="motion_direction"/>
|
||||
<block id="${targetId}_xposition" type="motion_xposition"/>
|
||||
<block id="${targetId}_yposition" type="motion_yposition"/>
|
||||
<block id="${targetId}_direction" type="motion_direction"/>`}
|
||||
${categorySeparator}
|
||||
</category>
|
||||
`;
|
||||
`;
|
||||
};
|
||||
|
||||
const looks = `
|
||||
const looks = function (isStage, targetId) {
|
||||
return `
|
||||
<category name="Looks" colour="#9966FF" secondaryColour="#774DCB">
|
||||
${isStage ? '' : `
|
||||
<block type="looks_sayforsecs">
|
||||
<value name="MESSAGE">
|
||||
<shadow type="text">
|
||||
@@ -171,23 +178,32 @@ const looks = `
|
||||
<block type="looks_show"/>
|
||||
<block type="looks_hide"/>
|
||||
${blockSeparator}
|
||||
<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>
|
||||
`}
|
||||
${isStage ? `
|
||||
<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_nextbackdrop"/>
|
||||
` : `
|
||||
<block type="looks_switchcostumeto">
|
||||
<value name="COSTUME">
|
||||
<shadow type="looks_costume"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="looks_nextcostume"/>
|
||||
<block type="looks_switchbackdropto">
|
||||
<value name="BACKDROP">
|
||||
<shadow type="looks_backdrops"/>
|
||||
</value>
|
||||
</block>
|
||||
`}
|
||||
${blockSeparator}
|
||||
<block type="looks_changeeffectby">
|
||||
<value name="CHANGE">
|
||||
@@ -205,39 +221,47 @@ const looks = `
|
||||
</block>
|
||||
<block type="looks_cleargraphiceffects"/>
|
||||
${blockSeparator}
|
||||
<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>
|
||||
${blockSeparator}
|
||||
<block type="looks_gotofront"/>
|
||||
<block type="looks_gobacklayers">
|
||||
<value name="NUM">
|
||||
<shadow type="math_integer">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
${blockSeparator}
|
||||
<block id="costumeorder" type="looks_costumeorder"/>
|
||||
<block id="backdroporder" type="looks_backdroporder"/>
|
||||
<block id="backdropname" type="looks_backdropname"/>
|
||||
<block id="size" type="looks_size"/>
|
||||
${isStage ? '' : `
|
||||
<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>
|
||||
${blockSeparator}
|
||||
<block type="looks_gotofront"/>
|
||||
<block type="looks_gobacklayers">
|
||||
<value name="NUM">
|
||||
<shadow type="math_integer">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
${blockSeparator}
|
||||
`}
|
||||
${isStage ? `
|
||||
<block id="backdroporder" type="looks_backdroporder"/>
|
||||
<block id="backdropname" type="looks_backdropname"/>
|
||||
` : `
|
||||
<block id="${targetId}_costumeorder" type="looks_costumeorder"/>
|
||||
<block id="backdropname" type="looks_backdropname"/>
|
||||
<block id="${targetId}_size" type="looks_size"/>
|
||||
`}
|
||||
${categorySeparator}
|
||||
</category>
|
||||
`;
|
||||
`;
|
||||
};
|
||||
|
||||
const sound = `
|
||||
const sound = function () {
|
||||
return `
|
||||
<category name="Sound" colour="#D65CD6" secondaryColour="#BD42BD">
|
||||
<block type="sound_play">
|
||||
<value name="SOUND_MENU">
|
||||
@@ -251,42 +275,6 @@ const sound = `
|
||||
</block>
|
||||
<block type="sound_stopallsounds"/>
|
||||
${blockSeparator}
|
||||
<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>
|
||||
${blockSeparator}
|
||||
<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>
|
||||
${blockSeparator}
|
||||
<block type="sound_changeeffectby">
|
||||
<value name="VALUE">
|
||||
<shadow type="math_number">
|
||||
@@ -318,27 +306,13 @@ const sound = `
|
||||
</value>
|
||||
</block>
|
||||
<block id="volume" type="sound_volume"/>
|
||||
${blockSeparator}
|
||||
<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 id="tempo" type="sound_tempo"/>
|
||||
${categorySeparator}
|
||||
</category>
|
||||
`;
|
||||
`;
|
||||
};
|
||||
|
||||
const events = `
|
||||
const events = function () {
|
||||
return `
|
||||
<category name="Events" colour="#FFD500" secondaryColour="#CC9900">
|
||||
<block type="event_whenflagclicked"/>
|
||||
<block type="event_whenkeypressed">
|
||||
@@ -358,20 +332,16 @@ const events = `
|
||||
<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>
|
||||
${categorySeparator}
|
||||
</category>
|
||||
`;
|
||||
`;
|
||||
};
|
||||
|
||||
const control = `
|
||||
const control = function (isStage) {
|
||||
return `
|
||||
<category name="Control" colour="#FFAB19" secondaryColour="#CF8B17">
|
||||
<block type="control_wait">
|
||||
<value name="DURATION">
|
||||
@@ -388,51 +358,72 @@ const control = `
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="control_forever"/>
|
||||
<block id="forever" type="control_forever"/>
|
||||
${blockSeparator}
|
||||
<block type="control_if"/>
|
||||
<block type="control_if_else"/>
|
||||
<block type="control_wait_until"/>
|
||||
<block type="control_repeat_until"/>
|
||||
<block id="wait_until" type="control_wait_until"/>
|
||||
<block id="repeat_until" type="control_repeat_until"/>
|
||||
${blockSeparator}
|
||||
<block type="control_stop"/>
|
||||
${blockSeparator}
|
||||
<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"/>
|
||||
${isStage ? `
|
||||
<block type="control_create_clone_of">
|
||||
<value name="CLONE_OPTION">
|
||||
<shadow type="control_create_clone_of_menu"/>
|
||||
</value>
|
||||
</block>
|
||||
` : `
|
||||
<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"/>
|
||||
`}
|
||||
${categorySeparator}
|
||||
</category>
|
||||
`;
|
||||
`;
|
||||
};
|
||||
|
||||
const sensing = `
|
||||
const sensing = function (isStage) {
|
||||
return `
|
||||
<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"/>
|
||||
${isStage ? '' : `
|
||||
<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>
|
||||
${blockSeparator}
|
||||
`}
|
||||
<block id="askandwait" type="sensing_askandwait">
|
||||
<value name="QUESTION">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">What's your name?</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block id="answer" type="sensing_answer"/>
|
||||
${blockSeparator}
|
||||
<block type="sensing_keypressed">
|
||||
<value name="KEY_OPTION">
|
||||
@@ -465,9 +456,11 @@ const sensing = `
|
||||
<block type="sensing_dayssince2000"/>
|
||||
${categorySeparator}
|
||||
</category>
|
||||
`;
|
||||
`;
|
||||
};
|
||||
|
||||
const operators = `
|
||||
const operators = function () {
|
||||
return `
|
||||
<category name="Operators" colour="#40BF4A" secondaryColour="#389438">
|
||||
<block type="operator_add">
|
||||
<value name="NUM1">
|
||||
@@ -645,33 +638,46 @@ const operators = `
|
||||
</block>
|
||||
${categorySeparator}
|
||||
</category>
|
||||
`;
|
||||
`;
|
||||
};
|
||||
|
||||
const data = `
|
||||
const data = function () {
|
||||
return `
|
||||
<category name="Data" colour="#FF8C1A" secondaryColour="#DB6E00" custom="VARIABLE">
|
||||
</category>
|
||||
`;
|
||||
`;
|
||||
};
|
||||
|
||||
const more = function () {
|
||||
return `
|
||||
<category name="More" colour="#FF6680" secondaryColour="#FF4D6A" custom="PROCEDURE">
|
||||
</category>
|
||||
`;
|
||||
};
|
||||
|
||||
const xmlOpen = '<xml style="display: none">';
|
||||
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.
|
||||
* @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox.
|
||||
*/
|
||||
const makeToolboxXML = function (categoriesXML) {
|
||||
const makeToolboxXML = function (isStage, targetId, categoriesXML) {
|
||||
const gap = [categorySeparator];
|
||||
|
||||
const everything = [
|
||||
xmlOpen,
|
||||
motion, gap,
|
||||
looks, gap,
|
||||
sound, gap,
|
||||
events, gap,
|
||||
control, gap,
|
||||
sensing, gap,
|
||||
operators, gap,
|
||||
data
|
||||
motion(isStage, targetId), gap,
|
||||
looks(isStage, targetId), gap,
|
||||
sound(isStage, targetId), gap,
|
||||
events(isStage, targetId), gap,
|
||||
control(isStage, targetId), gap,
|
||||
sensing(isStage, targetId), gap,
|
||||
operators(isStage, targetId), gap,
|
||||
data(isStage, targetId), gap,
|
||||
more(isStage, targetId)
|
||||
];
|
||||
|
||||
if (categoriesXML) {
|
||||
|
||||
+13
-15
@@ -1,35 +1,33 @@
|
||||
import OpcodeLabels from './opcode-labels.js';
|
||||
|
||||
const PADDING = 5;
|
||||
const MONITOR_HEIGHT = 23;
|
||||
|
||||
const isUndefined = a => typeof a === 'undefined';
|
||||
|
||||
/**
|
||||
* Convert monitors from VM format to what the GUI needs to render.
|
||||
* - Convert opcode to a label and a category
|
||||
* - Add missing XY position data if needed
|
||||
* @param {object} block - The monitor block
|
||||
* @param {string} block.id - The id of the monitor block
|
||||
* @param {number} block.index - The index of the monitor
|
||||
* @param {string} block.spriteName - Present only if the monitor applies only to the sprite
|
||||
* with given target ID. The name of the target sprite when the monitor was created
|
||||
* @param {string} block.opcode - The opcode of the monitor
|
||||
* @param {object} block.params - Extra params to the monitor block
|
||||
* @param {string} block.value - The monitor value
|
||||
* @param {number} x - The monitor x position
|
||||
* @param {number} y - The monitor y position
|
||||
* @return {object} The adapted monitor with label and category
|
||||
*/
|
||||
export default function ({id, index, opcode, params, value, x, y}) {
|
||||
export default function ({id, spriteName, opcode, params, value}) {
|
||||
let {label, category, labelFn} = OpcodeLabels(opcode);
|
||||
|
||||
// Use labelFn if provided for dynamic labelling (e.g. variables)
|
||||
if (!isUndefined(labelFn)) label = labelFn(params);
|
||||
|
||||
// Simple layout if x or y are undefined
|
||||
// @todo scratch2 has a more complex layout behavior we may want to adopt
|
||||
// @todo e.g. this does not work well when monitors have already been moved
|
||||
if (isUndefined(x)) x = PADDING;
|
||||
if (isUndefined(y)) y = PADDING + (index * (PADDING + MONITOR_HEIGHT));
|
||||
// Append sprite name for sprite-specific monitors
|
||||
if (spriteName) {
|
||||
label = `${spriteName}: ${label}`;
|
||||
}
|
||||
|
||||
return {id, label, category, value, x, y};
|
||||
// If value is a number, round it to six decimal places
|
||||
if (typeof value === 'number' || (typeof value === 'string' && String(parseFloat(value)) === value)) {
|
||||
value = Number(Number(value).toFixed(6));
|
||||
}
|
||||
|
||||
return {id, label, category, value};
|
||||
}
|
||||
@@ -48,6 +48,10 @@ const opcodeMap = {
|
||||
},
|
||||
|
||||
// Sensing
|
||||
sensing_answer: {
|
||||
category: 'sensing',
|
||||
label: 'answer'
|
||||
},
|
||||
sensing_loudness: {
|
||||
category: 'sensing',
|
||||
label: 'loudness'
|
||||
|
||||
@@ -25,6 +25,10 @@ class Storage extends ScratchStorage {
|
||||
[this.AssetType.ImageVector, this.AssetType.ImageBitmap, this.AssetType.Sound],
|
||||
asset => `${ASSET_SERVER}/internalapi/asset/${asset.assetId}.${asset.dataFormat}/get/`
|
||||
);
|
||||
this.addWebSource(
|
||||
[this.AssetType.Sound],
|
||||
asset => `static/extension-assets/scratch3_music/${asset.assetId}.${asset.dataFormat}`
|
||||
);
|
||||
defaultProjectAssets.forEach(asset => this.cache(
|
||||
this.AssetType[asset.assetType],
|
||||
this.DataFormat[asset.dataFormat],
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
const ACTIVATE_CUSTOM_PROCEDURES = 'scratch-gui/custom-procedures/ACTIVATE_CUSTOM_PROCEDURES';
|
||||
const DEACTIVATE_CUSTOM_PROCEDURES = 'scratch-gui/custom-procedures/DEACTIVATE_CUSTOM_PROCEDURES';
|
||||
const SET_CALLBACK = 'scratch-gui/custom-procedures/SET_CALLBACK';
|
||||
|
||||
const initialState = {
|
||||
active: false,
|
||||
mutator: null,
|
||||
callback: null
|
||||
};
|
||||
|
||||
const reducer = function (state, action) {
|
||||
if (typeof state === 'undefined') state = initialState;
|
||||
switch (action.type) {
|
||||
case ACTIVATE_CUSTOM_PROCEDURES:
|
||||
return Object.assign({}, state, {
|
||||
active: true,
|
||||
mutator: action.mutator,
|
||||
callback: action.callback
|
||||
});
|
||||
case DEACTIVATE_CUSTOM_PROCEDURES:
|
||||
// Can be called without a mutator to deactivate without new procedure
|
||||
// i.e. when clicking on the modal background
|
||||
if (action.mutator) {
|
||||
state.callback(action.mutator);
|
||||
}
|
||||
return Object.assign({}, state, {
|
||||
active: false,
|
||||
mutator: null,
|
||||
callback: null
|
||||
});
|
||||
case SET_CALLBACK:
|
||||
return Object.assign({}, state, {callback: action.callback});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Action creator to open the custom procedures modal.
|
||||
* @param {!Element} mutator The XML node of the mutator for the procedure.
|
||||
* @param {!function(!Element)} callback The function to call when done editing procedure.
|
||||
* Expect the callback to be a function that takes a new XML mutator node.
|
||||
* @returns {object} An action object with type ACTIVATE_CUSTOM_PROCEDURES.
|
||||
*/
|
||||
const activateCustomProcedures = (mutator, callback) => ({
|
||||
type: ACTIVATE_CUSTOM_PROCEDURES,
|
||||
mutator: mutator,
|
||||
callback: callback
|
||||
});
|
||||
|
||||
/**
|
||||
* Action creator to close the custom procedures modal.
|
||||
* @param {?Element} mutator The new mutator, or null if the callback should not be called.
|
||||
* @returns {object} An action object with type ACTIVATE_CUSTOM_PROCEDURES.
|
||||
*/
|
||||
const deactivateCustomProcedures = mutator => ({
|
||||
type: DEACTIVATE_CUSTOM_PROCEDURES,
|
||||
mutator: mutator
|
||||
});
|
||||
|
||||
export {
|
||||
reducer as default,
|
||||
activateCustomProcedures,
|
||||
deactivateCustomProcedures
|
||||
};
|
||||
@@ -1,8 +1,10 @@
|
||||
import {combineReducers} from 'redux';
|
||||
import colorPickerReducer from './color-picker';
|
||||
import customProceduresReducer from './custom-procedures';
|
||||
import intlReducer from './intl';
|
||||
import modalReducer from './modals';
|
||||
import monitorReducer from './monitors';
|
||||
import monitorLayoutReducer from './monitor-layout';
|
||||
import targetReducer from './targets';
|
||||
import toolboxReducer from './toolbox';
|
||||
import vmReducer from './vm';
|
||||
@@ -10,9 +12,11 @@ import {ScratchPaintReducer} from 'scratch-paint';
|
||||
|
||||
export default combineReducers({
|
||||
colorPicker: colorPickerReducer,
|
||||
customProcedures: customProceduresReducer,
|
||||
intl: intlReducer,
|
||||
modals: modalReducer,
|
||||
monitors: monitorReducer,
|
||||
monitorLayout: monitorLayoutReducer,
|
||||
targets: targetReducer,
|
||||
toolbox: toolboxReducer,
|
||||
vm: vmReducer,
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
import log from '../lib/log';
|
||||
|
||||
const ADD_MONITOR_RECT = 'scratch-gui/monitors/ADD_MONITOR_RECT';
|
||||
const MOVE_MONITOR_RECT = 'scratch-gui/monitors/MOVE_MONITOR_RECT';
|
||||
const RESIZE_MONITOR_RECT = 'scratch-gui/monitors/RESIZE_MONITOR_RECT';
|
||||
const REMOVE_MONITOR_RECT = 'scratch-gui/monitors/REMOVE_MONITOR_RECT';
|
||||
|
||||
const initialState = {
|
||||
monitors: {},
|
||||
savedMonitorPositions: {}
|
||||
};
|
||||
|
||||
// Verify that the rectangle formed by the 2 points is well-formed
|
||||
const _verifyRect = function (upperStart, lowerEnd) {
|
||||
if (isNaN(upperStart.x) || isNaN(upperStart.y) || isNaN(lowerEnd.x) || isNaN(lowerEnd.y)) {
|
||||
return false;
|
||||
}
|
||||
if (!(upperStart.x < lowerEnd.x)) {
|
||||
return false;
|
||||
}
|
||||
if (!(upperStart.y < lowerEnd.y)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const _addMonitorRect = function (state, action) {
|
||||
if (state.monitors.hasOwnProperty(action.monitorId)) {
|
||||
log.error(`Can't add monitor, monitor with id ${action.monitorId} already exists.`);
|
||||
return state;
|
||||
}
|
||||
if (!_verifyRect(action.upperStart, action.lowerEnd)) {
|
||||
log.error(`Monitor rectangle not formatted correctly`);
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
monitors: Object.assign({}, state.monitors, {
|
||||
[action.monitorId]: {
|
||||
upperStart: action.upperStart,
|
||||
lowerEnd: action.lowerEnd
|
||||
}
|
||||
}),
|
||||
savedMonitorPositions: action.savePosition ?
|
||||
Object.assign({}, state.savedMonitorPositions, {
|
||||
[action.monitorId]: {x: action.upperStart.x, y: action.upperStart.y}
|
||||
}) :
|
||||
state.savedMonitorPositions
|
||||
};
|
||||
};
|
||||
|
||||
const _moveMonitorRect = function (state, action) {
|
||||
if (!state.monitors.hasOwnProperty(action.monitorId)) {
|
||||
log.error(`Can't move monitor, monitor with id ${action.monitorId} does not exist.`);
|
||||
return state;
|
||||
}
|
||||
if (isNaN(action.newX) || isNaN(action.newY)) {
|
||||
log.error(`Monitor rectangle not formatted correctly`);
|
||||
return state;
|
||||
}
|
||||
|
||||
const oldMonitor = state.monitors[action.monitorId];
|
||||
if (oldMonitor.upperStart.x === action.newX &&
|
||||
oldMonitor.upperStart.y === action.newY) {
|
||||
// Hasn't moved
|
||||
return state;
|
||||
}
|
||||
const monitorWidth = oldMonitor.lowerEnd.x - oldMonitor.upperStart.x;
|
||||
const monitorHeight = oldMonitor.lowerEnd.y - oldMonitor.upperStart.y;
|
||||
return {
|
||||
monitors: Object.assign({}, state.monitors, {
|
||||
[action.monitorId]: {
|
||||
upperStart: {x: action.newX, y: action.newY},
|
||||
lowerEnd: {x: action.newX + monitorWidth, y: action.newY + monitorHeight}
|
||||
}
|
||||
}),
|
||||
// User generated position is saved
|
||||
savedMonitorPositions: Object.assign({}, state.savedMonitorPositions, {
|
||||
[action.monitorId]: {x: action.newX, y: action.newY}
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
const _resizeMonitorRect = function (state, action) {
|
||||
if (!state.monitors.hasOwnProperty(action.monitorId)) {
|
||||
log.error(`Can't resize monitor, monitor with id ${action.monitorId} does not exist.`);
|
||||
return state;
|
||||
}
|
||||
if (isNaN(action.newWidth) || isNaN(action.newHeight) ||
|
||||
action.newWidth <= 0 || action.newHeight <= 0) {
|
||||
log.error(`Monitor rectangle not formatted correctly`);
|
||||
return state;
|
||||
}
|
||||
|
||||
const oldMonitor = state.monitors[action.monitorId];
|
||||
const newMonitor = {
|
||||
upperStart: oldMonitor.upperStart,
|
||||
lowerEnd: {
|
||||
x: oldMonitor.upperStart.x + action.newWidth,
|
||||
y: oldMonitor.upperStart.y + action.newHeight
|
||||
}
|
||||
};
|
||||
if (newMonitor.lowerEnd.x === oldMonitor.lowerEnd.x &&
|
||||
newMonitor.lowerEnd.y === oldMonitor.lowerEnd.y) {
|
||||
// no change
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
monitors: Object.assign({}, state.monitors, {[action.monitorId]: newMonitor}),
|
||||
savedMonitorPositions: state.savedMonitorPositions
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
const _removeMonitorRect = function (state, action) {
|
||||
if (!state.monitors.hasOwnProperty(action.monitorId)) {
|
||||
log.error(`Can't remove monitor, monitor with id ${action.monitorId} does not exist.`);
|
||||
return state;
|
||||
}
|
||||
|
||||
const newMonitors = Object.assign({}, state.monitors);
|
||||
delete newMonitors[action.monitorId];
|
||||
return {
|
||||
monitors: newMonitors,
|
||||
savedMonitorPositions: state.savedMonitorPositions
|
||||
};
|
||||
};
|
||||
|
||||
const reducer = function (state, action) {
|
||||
if (typeof state === 'undefined') state = initialState;
|
||||
switch (action.type) {
|
||||
case ADD_MONITOR_RECT:
|
||||
return _addMonitorRect(state, action);
|
||||
case MOVE_MONITOR_RECT:
|
||||
return _moveMonitorRect(state, action);
|
||||
case RESIZE_MONITOR_RECT:
|
||||
return _resizeMonitorRect(state, action);
|
||||
case REMOVE_MONITOR_RECT:
|
||||
return _removeMonitorRect(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
// Init position --------------------------
|
||||
const PADDING = 5;
|
||||
// @todo fix these numbers when we fix https://github.com/LLK/scratch-gui/issues/980
|
||||
const SCREEN_WIDTH = 400;
|
||||
const SCREEN_HEIGHT = 300;
|
||||
const SCREEN_EDGE_BUFFER = 40;
|
||||
|
||||
const _rectsIntersect = function (rect1, rect2) {
|
||||
// If one rectangle is on left side of other
|
||||
if (rect1.upperStart.x >= rect2.lowerEnd.x || rect2.upperStart.x >= rect1.lowerEnd.x) return false;
|
||||
// If one rectangle is above other
|
||||
if (rect1.upperStart.y >= rect2.lowerEnd.y || rect2.upperStart.y >= rect1.lowerEnd.y) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
// We need to place a monitor with the given width and height. Return a rect defining where it should be placed.
|
||||
const getInitialPosition = function (state, monitorId, eltWidth, eltHeight) {
|
||||
// If this monitor was purposefully moved to a certain position before, put it back in that position
|
||||
if (state.savedMonitorPositions.hasOwnProperty(monitorId)) {
|
||||
const saved = state.savedMonitorPositions[monitorId];
|
||||
return {
|
||||
upperStart: saved,
|
||||
lowerEnd: {x: saved.x + eltWidth, y: saved.y + eltHeight}
|
||||
};
|
||||
}
|
||||
|
||||
// Try all starting positions for the new monitor to find one that doesn't intersect others
|
||||
const endXs = [0];
|
||||
const endYs = [0];
|
||||
let lastX = null;
|
||||
let lastY = null;
|
||||
for (const monitor in state.monitors) {
|
||||
let x = state.monitors[monitor].lowerEnd.x;
|
||||
x = Math.ceil(x / 50) * 50; // Try to choose a sensible "tab width" so more monitors line up
|
||||
endXs.push(x);
|
||||
endYs.push(Math.ceil(state.monitors[monitor].lowerEnd.y));
|
||||
}
|
||||
endXs.sort((a, b) => a - b);
|
||||
endYs.sort((a, b) => a - b);
|
||||
// We'll use plan B if the monitor doesn't fit anywhere (too long or tall)
|
||||
let planB = null;
|
||||
for (const x of endXs) {
|
||||
if (x === lastX) {
|
||||
continue;
|
||||
}
|
||||
lastX = x;
|
||||
outer:
|
||||
for (const y of endYs) {
|
||||
if (y === lastY) {
|
||||
continue;
|
||||
}
|
||||
lastY = y;
|
||||
const monitorRect = {
|
||||
upperStart: {x: x + PADDING, y: y + PADDING},
|
||||
lowerEnd: {x: x + PADDING + eltWidth, y: y + PADDING + eltHeight}
|
||||
};
|
||||
// Intersection testing rect that includes padding
|
||||
const rect = {
|
||||
upperStart: {x, y},
|
||||
lowerEnd: {x: x + eltWidth + (2 * PADDING), y: y + eltHeight + (2 * PADDING)}
|
||||
};
|
||||
for (const monitor in state.monitors) {
|
||||
if (_rectsIntersect(state.monitors[monitor], rect)) {
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
// If the rect overlaps the ends of the screen
|
||||
if (rect.lowerEnd.x > SCREEN_WIDTH || rect.lowerEnd.y > SCREEN_HEIGHT) {
|
||||
// If rect is not too close to completely off screen, set it as plan B
|
||||
if (!planB &&
|
||||
!(rect.upperStart.x + SCREEN_EDGE_BUFFER > SCREEN_WIDTH ||
|
||||
rect.upperStart.y + SCREEN_EDGE_BUFFER > SCREEN_HEIGHT)) {
|
||||
planB = monitorRect;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return monitorRect;
|
||||
}
|
||||
}
|
||||
// If the monitor is too long to fit anywhere, put it in the leftmost spot available
|
||||
// that intersects the right or bottom edge and isn't too close to the edge.
|
||||
if (planB) {
|
||||
return planB;
|
||||
}
|
||||
|
||||
// If plan B fails and there's nowhere reasonable to put it, plan C is to place the monitor randomly
|
||||
const randX = Math.ceil(Math.random() * (SCREEN_WIDTH / 2));
|
||||
const randY = Math.ceil(Math.random() * (SCREEN_HEIGHT - SCREEN_EDGE_BUFFER));
|
||||
return {
|
||||
upperStart: {
|
||||
x: randX,
|
||||
y: randY
|
||||
},
|
||||
lowerEnd: {
|
||||
x: randX + eltWidth,
|
||||
y: randY + eltHeight
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Action creators ------------------------
|
||||
/**
|
||||
* @param {!string} monitorId Id to add
|
||||
* @param {!object} upperStart upper point defining the rectangle
|
||||
* @param {!number} upperStart.x X of top point that defines the monitor location
|
||||
* @param {!number} upperStart.y Y of top point that defines the monitor location
|
||||
* @param {!object} lowerEnd lower point defining the rectangle
|
||||
* @param {!number} lowerEnd.x X of bottom point that defines the monitor location
|
||||
* @param {!number} lowerEnd.y Y of bottom point that defines the monitor location
|
||||
* @param {?boolean} savePosition True if the placement should be saved when adding the monitor
|
||||
* @returns {object} action to add a new monitor at the location
|
||||
*/
|
||||
const addMonitorRect = function (monitorId, upperStart, lowerEnd, savePosition) {
|
||||
return {
|
||||
type: ADD_MONITOR_RECT,
|
||||
monitorId: monitorId,
|
||||
upperStart: upperStart,
|
||||
lowerEnd: lowerEnd,
|
||||
savePosition: savePosition
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {!string} monitorId Id for monitor to move
|
||||
* @param {!number} newX X of top point that defines the monitor location
|
||||
* @param {!number} newY Y of top point that defines the monitor location
|
||||
* @returns {object} action to move an existing monitor to the location
|
||||
*/
|
||||
const moveMonitorRect = function (monitorId, newX, newY) {
|
||||
return {
|
||||
type: MOVE_MONITOR_RECT,
|
||||
monitorId: monitorId,
|
||||
newX: newX,
|
||||
newY: newY
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {!string} monitorId Id for monitor to resize
|
||||
* @param {!number} newWidth Width to set monitor to
|
||||
* @param {!number} newHeight Height to set monitor to
|
||||
* @returns {object} action to resize an existing monitor to the given dimensions
|
||||
*/
|
||||
const resizeMonitorRect = function (monitorId, newWidth, newHeight) {
|
||||
return {
|
||||
type: RESIZE_MONITOR_RECT,
|
||||
monitorId: monitorId,
|
||||
newWidth: newWidth,
|
||||
newHeight: newHeight
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {!string} monitorId Id for monitor to remove
|
||||
* @returns {object} action to remove an existing monitor
|
||||
*/
|
||||
const removeMonitorRect = function (monitorId) {
|
||||
return {
|
||||
type: REMOVE_MONITOR_RECT,
|
||||
monitorId: monitorId
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
reducer as default,
|
||||
addMonitorRect,
|
||||
getInitialPosition,
|
||||
moveMonitorRect,
|
||||
resizeMonitorRect,
|
||||
removeMonitorRect,
|
||||
PADDING,
|
||||
SCREEN_HEIGHT,
|
||||
SCREEN_WIDTH
|
||||
};
|
||||
@@ -1,9 +1,8 @@
|
||||
const UPDATE_TOOLBOX = 'scratch-gui/toolbox/UPDATE_TOOLBOX';
|
||||
|
||||
import makeToolboxXML from '../lib/make-toolbox-xml';
|
||||
|
||||
const initialState = {
|
||||
toolboxXML: makeToolboxXML()
|
||||
toolboxXML: makeToolboxXML(true)
|
||||
};
|
||||
|
||||
const reducer = function (state, action) {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
@@ -4,7 +4,7 @@ import bindAll from 'lodash.bindall';
|
||||
import 'chromedriver'; // register path
|
||||
import webdriver from 'selenium-webdriver';
|
||||
|
||||
const {By, until} = webdriver;
|
||||
const {By, until, Button} = webdriver;
|
||||
|
||||
class SeleniumHelper {
|
||||
constructor () {
|
||||
@@ -15,7 +15,9 @@ class SeleniumHelper {
|
||||
'findByText',
|
||||
'findByXpath',
|
||||
'getDriver',
|
||||
'getLogs'
|
||||
'getLogs',
|
||||
'loadUri',
|
||||
'rightClickText'
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -34,6 +36,21 @@ class SeleniumHelper {
|
||||
return this.findByXpath(`//body//${scope || '*'}//*[contains(text(), '${text}')]`);
|
||||
}
|
||||
|
||||
loadUri (uri) {
|
||||
const WINDOW_WIDTH = 1024;
|
||||
const WINDOW_HEIGHT = 768;
|
||||
return this.driver
|
||||
.get(`file://${uri}`)
|
||||
.then(() => (
|
||||
this.driver.executeScript('window.onbeforeunload = undefined;')
|
||||
))
|
||||
.then(() => (
|
||||
this.driver.manage()
|
||||
.window()
|
||||
.setSize(WINDOW_WIDTH, WINDOW_HEIGHT)
|
||||
));
|
||||
}
|
||||
|
||||
clickXpath (xpath) {
|
||||
return this.findByXpath(xpath).then(el => el.click());
|
||||
}
|
||||
@@ -42,6 +59,12 @@ class SeleniumHelper {
|
||||
return this.findByText(text, scope).then(el => el.click());
|
||||
}
|
||||
|
||||
rightClickText (text, scope) {
|
||||
return this.findByText(text, scope).then(el => this.driver.actions()
|
||||
.click(el, Button.RIGHT)
|
||||
.perform());
|
||||
}
|
||||
|
||||
clickButton (text) {
|
||||
return this.clickXpath(`//button[contains(text(), '${text}')]`);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ const {
|
||||
clickXpath,
|
||||
findByXpath,
|
||||
getDriver,
|
||||
getLogs
|
||||
getLogs,
|
||||
loadUri
|
||||
} = new SeleniumHelper();
|
||||
|
||||
const errorWhitelist = [
|
||||
@@ -31,7 +32,7 @@ describe('player example', () => {
|
||||
|
||||
test('Load a project by ID', async () => {
|
||||
const projectId = '96708228';
|
||||
await driver.get(`file://${uri}#${projectId}`);
|
||||
await loadUri(`${uri}#${projectId}`);
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await clickXpath('//img[@title="Go"]');
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
@@ -54,7 +55,7 @@ describe('blocks example', () => {
|
||||
|
||||
test('Load a project by ID', async () => {
|
||||
const projectId = '96708228';
|
||||
await driver.get(`file://${uri}#${projectId}`);
|
||||
await loadUri(`${uri}#${projectId}`);
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await clickXpath('//img[@title="Go"]');
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
@@ -64,7 +65,7 @@ describe('blocks example', () => {
|
||||
});
|
||||
|
||||
test('Change categories', async () => {
|
||||
await driver.get(`file://${uri}`);
|
||||
await loadUri(`${uri}`);
|
||||
await clickText('Looks');
|
||||
await clickText('Sound');
|
||||
await clickText('Events');
|
||||
|
||||
+61
-17
@@ -8,7 +8,9 @@ const {
|
||||
findByText,
|
||||
findByXpath,
|
||||
getDriver,
|
||||
getLogs
|
||||
getLogs,
|
||||
loadUri,
|
||||
rightClickText
|
||||
} = new SeleniumHelper();
|
||||
|
||||
const uri = path.resolve(__dirname, '../../build/index.html');
|
||||
@@ -34,9 +36,8 @@ describe('costumes, sounds and variables', () => {
|
||||
await driver.quit();
|
||||
});
|
||||
|
||||
|
||||
test('Blocks report when clicked in the toolbox', async () => {
|
||||
await driver.get(`file://${uri}`);
|
||||
await loadUri(uri);
|
||||
await clickText('Blocks');
|
||||
await clickText('Operators', blocksTabScope);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
|
||||
@@ -47,42 +48,50 @@ describe('costumes, sounds and variables', () => {
|
||||
});
|
||||
|
||||
test('Switching sprites updates the block menus', async () => {
|
||||
await driver.get(`file://${uri}`);
|
||||
await loadUri(uri);
|
||||
await clickText('Sound', blocksTabScope);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
|
||||
// "meow" sound block should be visible
|
||||
await findByText('meow', blocksTabScope);
|
||||
await clickText('Backdrops'); // Switch to the backdrop
|
||||
// Now "pop" sound block should be visible
|
||||
// Now "pop" sound block should be visible and motion blocks hidden
|
||||
await findByText('pop', blocksTabScope);
|
||||
await clickText('Motion', blocksTabScope);
|
||||
await findByText('Stage selected: no motion blocks');
|
||||
|
||||
const logs = await getLogs(errorWhitelist);
|
||||
await expect(logs).toEqual([]);
|
||||
});
|
||||
|
||||
test('Adding a costume', async () => {
|
||||
await driver.get(`file://${uri}`);
|
||||
await loadUri(uri);
|
||||
await clickText('Costumes');
|
||||
await clickText('Add Costume');
|
||||
const el = await findByXpath("//input[@placeholder='what are you looking for?']");
|
||||
await el.sendKeys('abb');
|
||||
await clickText('abby-a'); // Should close the modal, then click the costumes in the selector
|
||||
await clickText('Abby-a'); // Should close the modal, then click the costumes in the selector
|
||||
await clickText('costume1', costumesTabScope);
|
||||
await clickText('abby-a', costumesTabScope);
|
||||
await clickText('Abby-a', costumesTabScope);
|
||||
const logs = await getLogs(errorWhitelist);
|
||||
await expect(logs).toEqual([]);
|
||||
});
|
||||
|
||||
test('Adding a sound', async () => {
|
||||
await driver.get(`file://${uri}`);
|
||||
await loadUri(uri);
|
||||
await clickText('Sounds');
|
||||
|
||||
// Delete the sound
|
||||
await rightClickText('meow', soundsTabScope);
|
||||
await clickText('delete', soundsTabScope);
|
||||
await driver.switchTo().alert()
|
||||
.accept();
|
||||
|
||||
// Add a sound
|
||||
await clickText('Add Sound');
|
||||
const el = await findByXpath("//input[@placeholder='what are you looking for?']");
|
||||
await el.sendKeys('chom');
|
||||
await clickText('chomp'); // Should close the modal, then click the sounds in the selector
|
||||
await clickText('meow', soundsTabScope);
|
||||
await clickText('chomp', soundsTabScope);
|
||||
await clickXpath('//button[@title="Play"]');
|
||||
await clickText('meow', soundsTabScope);
|
||||
await clickText('Chomp'); // Should close the modal, then click the sounds in the selector
|
||||
await clickText('Chomp', soundsTabScope);
|
||||
await clickXpath('//button[@title="Play"]');
|
||||
|
||||
await clickText('Louder');
|
||||
@@ -99,7 +108,7 @@ describe('costumes, sounds and variables', () => {
|
||||
|
||||
test('Load a project by ID', async () => {
|
||||
const projectId = '96708228';
|
||||
await driver.get(`file://${uri}#${projectId}`);
|
||||
await loadUri(`${uri}#${projectId}`);
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await clickXpath('//img[@title="Go"]');
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
@@ -109,7 +118,7 @@ describe('costumes, sounds and variables', () => {
|
||||
});
|
||||
|
||||
test('Creating variables', async () => {
|
||||
await driver.get(`file://${uri}`);
|
||||
await loadUri(uri);
|
||||
await clickText('Blocks');
|
||||
await clickText('Data', blocksTabScope);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
|
||||
@@ -133,7 +142,7 @@ describe('costumes, sounds and variables', () => {
|
||||
});
|
||||
|
||||
test('Importing extensions', async () => {
|
||||
await driver.get(`file://${uri}`);
|
||||
await loadUri(uri);
|
||||
await clickText('Blocks');
|
||||
await clickText('Extensions');
|
||||
await clickText('Pen', modalScope); // Modal closes
|
||||
@@ -147,6 +156,41 @@ describe('costumes, sounds and variables', () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
|
||||
await clickText('stamp', blocksTabScope); // Would fail if didn't scroll back
|
||||
|
||||
// Make sure switching sprites doesn't clear extensions
|
||||
await clickText('Backdrops'); // Switch to the backdrop
|
||||
await findByText('Pen', blocksTabScope); // Pen extension should still be loaded
|
||||
|
||||
const logs = await getLogs(errorWhitelist);
|
||||
await expect(logs).toEqual([]);
|
||||
});
|
||||
|
||||
test('Deleting only sprite does not crash', async () => {
|
||||
const spriteTileContext = '*[starts-with(@class,"react-contextmenu-wrapper")]';
|
||||
await loadUri(uri);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
|
||||
await rightClickText('Sprite1', spriteTileContext);
|
||||
await clickText('delete', spriteTileContext);
|
||||
await driver.switchTo().alert()
|
||||
.accept();
|
||||
// Confirm that the stage has been switched to
|
||||
await findByText('Stage selected: no motion blocks');
|
||||
const logs = await getLogs(errorWhitelist);
|
||||
await expect(logs).toEqual([]);
|
||||
});
|
||||
|
||||
test('Custom procedures', async () => {
|
||||
await loadUri(uri);
|
||||
await clickText('More');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
|
||||
await clickText('Make a Block...');
|
||||
// Click on the "add an input" buttons
|
||||
await clickText('number or text', modalScope);
|
||||
await clickText('boolean', modalScope);
|
||||
await clickText('Add a label', modalScope);
|
||||
await clickText('OK', modalScope);
|
||||
|
||||
// Make sure a "define" block has been added to the workspace
|
||||
await findByText('define', blocksTabScope);
|
||||
|
||||
const logs = await getLogs(errorWhitelist);
|
||||
await expect(logs).toEqual([]);
|
||||
|
||||
@@ -4,6 +4,7 @@ exports[`IconButtonComponent matches snapshot 1`] = `
|
||||
<div
|
||||
className="custom-class-name"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
>
|
||||
<img
|
||||
className={undefined}
|
||||
|
||||
@@ -99,7 +99,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
|
||||
>
|
||||
<path
|
||||
className={undefined}
|
||||
d="M0 0Q0 0 60 45,Q120 90 180 135,Q240 180 300 225,Q360 270 420 135,Q480 0 480 0,Q480 0 420 -135,Q360 -270 300 -225,Q240 -180 180 -135,Q120 -90 60 -45,Q0 0 0 0Z"
|
||||
d="M0 0Q0 0 60 45 Q120 90 180 135 Q240 180 300 225 Q360 270 420 135 Q480 0 480 0 Q480 0 420 -135 Q360 -270 300 -225 Q240 -180 180 -135 Q120 -90 60 -45 Q0 0 0 0Z"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
@@ -341,6 +341,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
|
||||
<div
|
||||
className=""
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
>
|
||||
<img
|
||||
className={undefined}
|
||||
@@ -357,6 +358,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
|
||||
<div
|
||||
className=""
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
>
|
||||
<img
|
||||
className={undefined}
|
||||
@@ -373,6 +375,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
|
||||
<div
|
||||
className=""
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
>
|
||||
<img
|
||||
className={undefined}
|
||||
@@ -389,6 +392,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
|
||||
<div
|
||||
className=""
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
>
|
||||
<img
|
||||
className={undefined}
|
||||
@@ -405,6 +409,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
|
||||
<div
|
||||
className=""
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
>
|
||||
<img
|
||||
className={undefined}
|
||||
@@ -421,6 +426,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
|
||||
<div
|
||||
className=""
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
>
|
||||
<img
|
||||
className={undefined}
|
||||
@@ -437,6 +443,7 @@ exports[`Sound Editor Component matches snapshot 1`] = `
|
||||
<div
|
||||
className=""
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
>
|
||||
<img
|
||||
className={undefined}
|
||||
|
||||
@@ -12,8 +12,11 @@ exports[`SpriteSelectorItemComponent matches snapshot when selected 1`] = `
|
||||
onTouchStart={[Function]}
|
||||
>
|
||||
<div
|
||||
aria-label="Close"
|
||||
className=""
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
<img
|
||||
className={undefined}
|
||||
|
||||
@@ -224,7 +224,7 @@ describe('Sound Editor Container', () => {
|
||||
expect(mockAudioEffects.instance.process).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('undo/redo functionality', () => {
|
||||
test('undo/redo stack state', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<SoundEditor
|
||||
soundIndex={soundIndex}
|
||||
@@ -270,4 +270,34 @@ describe('Sound Editor Container', () => {
|
||||
component = wrapper.find(SoundEditorComponent);
|
||||
expect(component.prop('canRedo')).toEqual(false);
|
||||
});
|
||||
|
||||
test('undo and redo submit new samples and play the sound', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<SoundEditor
|
||||
soundIndex={soundIndex}
|
||||
store={store}
|
||||
/>
|
||||
);
|
||||
let component = wrapper.find(SoundEditorComponent);
|
||||
|
||||
// Set up an undoable state
|
||||
component.props().onActivateTrim(); // Activate trimming
|
||||
component.props().onActivateTrim(); // Submit new samples by calling again
|
||||
wrapper.update();
|
||||
component = wrapper.find(SoundEditorComponent);
|
||||
|
||||
// Undo should update the sound buffer and play the new samples
|
||||
component.props().onUndo();
|
||||
expect(mockAudioBufferPlayer.instance.play).toHaveBeenCalled();
|
||||
expect(vm.updateSoundBuffer).toHaveBeenCalled();
|
||||
|
||||
// Clear the mocks call history to assert again for redo.
|
||||
vm.updateSoundBuffer.mockClear();
|
||||
mockAudioBufferPlayer.instance.play.mockClear();
|
||||
|
||||
// Undo should update the sound buffer and play the new samples
|
||||
component.props().onRedo();
|
||||
expect(mockAudioBufferPlayer.instance.play).toHaveBeenCalled();
|
||||
expect(vm.updateSoundBuffer).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,301 @@
|
||||
/* eslint-env jest */
|
||||
import monitorLayoutReducer from '../../../src/reducers/monitor-layout';
|
||||
import {addMonitorRect, moveMonitorRect} from '../../../src/reducers/monitor-layout';
|
||||
import {resizeMonitorRect, removeMonitorRect} from '../../../src/reducers/monitor-layout';
|
||||
import {getInitialPosition, PADDING, SCREEN_WIDTH, SCREEN_HEIGHT} from '../../../src/reducers/monitor-layout';
|
||||
|
||||
test('initialState', () => {
|
||||
let defaultState;
|
||||
|
||||
expect(monitorLayoutReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined();
|
||||
expect(monitorLayoutReducer(defaultState /* state */, {type: 'anything'} /* action */).monitors).toBeDefined();
|
||||
expect(monitorLayoutReducer(defaultState /* state */, {type: 'anything'} /* action */).savedMonitorPositions)
|
||||
.toBeDefined();
|
||||
});
|
||||
|
||||
test('addMonitorRect', () => {
|
||||
let defaultState;
|
||||
const monitorId = 1;
|
||||
const monitorId2 = 2;
|
||||
const upperStart = {x: 100, y: 100};
|
||||
const lowerEnd = {x: 200, y: 200};
|
||||
|
||||
// Add a monitor rect
|
||||
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId, upperStart, lowerEnd));
|
||||
expect(reduxState.monitors[monitorId]).toBeDefined();
|
||||
expect(reduxState.monitors[monitorId].upperStart).toEqual(upperStart);
|
||||
expect(reduxState.monitors[monitorId].lowerEnd).toEqual(lowerEnd);
|
||||
// Add monitor rect doesn't save position
|
||||
expect(reduxState.savedMonitorPositions[monitorId]).toBeUndefined();
|
||||
const reduxState2 = monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 0, 0));
|
||||
|
||||
// Add a second monitor rect
|
||||
const reduxState3 = monitorLayoutReducer(reduxState2, addMonitorRect(monitorId2, upperStart, lowerEnd));
|
||||
expect(reduxState3.monitors[monitorId]).toBeDefined();
|
||||
expect(reduxState3.monitors[monitorId2]).toBeDefined();
|
||||
expect(reduxState3.monitors[monitorId2].upperStart).toEqual(upperStart);
|
||||
expect(reduxState3.monitors[monitorId2].lowerEnd).toEqual(lowerEnd);
|
||||
// Saved positions aren't changed by adding monitor
|
||||
expect(reduxState3.savedMonitorPositions).toEqual(reduxState2.savedMonitorPositions);
|
||||
});
|
||||
|
||||
test('addMonitorRectWithSavedPosition', () => {
|
||||
let defaultState;
|
||||
const monitorId = 1;
|
||||
const upperStart = {x: 100, y: 100};
|
||||
const lowerEnd = {x: 200, y: 200};
|
||||
|
||||
// Add a monitor rect
|
||||
const reduxState = monitorLayoutReducer(defaultState,
|
||||
addMonitorRect(monitorId, upperStart, lowerEnd, true /* savePosition */));
|
||||
expect(reduxState.monitors[monitorId]).toBeDefined();
|
||||
expect(reduxState.monitors[monitorId].upperStart).toEqual(upperStart);
|
||||
expect(reduxState.monitors[monitorId].lowerEnd).toEqual(lowerEnd);
|
||||
// Save position
|
||||
expect(reduxState.savedMonitorPositions[monitorId].x).toEqual(upperStart.x);
|
||||
expect(reduxState.savedMonitorPositions[monitorId].y).toEqual(upperStart.y);
|
||||
});
|
||||
|
||||
test('invalidRect', () => {
|
||||
let defaultState;
|
||||
const reduxState = monitorLayoutReducer(defaultState /* state */, {type: 'initialize'} /* action */);
|
||||
|
||||
// Problem: x end is before x start
|
||||
expect(
|
||||
monitorLayoutReducer(reduxState,
|
||||
addMonitorRect(1, {x: 100, y: 100}, {x: 10, y: 200})))
|
||||
.toEqual(reduxState);
|
||||
|
||||
// Problem: y end is before y start
|
||||
expect(
|
||||
monitorLayoutReducer(reduxState,
|
||||
addMonitorRect(1, {x: 100, y: 100}, {x: 200, y: 10})))
|
||||
.toEqual(reduxState);
|
||||
});
|
||||
|
||||
test('invalidAddMonitorRect', () => {
|
||||
let defaultState;
|
||||
const monitorId = 1;
|
||||
const upperStart = {x: 100, y: 100};
|
||||
const lowerEnd = {x: 200, y: 200};
|
||||
|
||||
// Add a monitor rect
|
||||
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId, upperStart, lowerEnd));
|
||||
// Try to add the same one
|
||||
expect(monitorLayoutReducer(reduxState, addMonitorRect(monitorId, upperStart, lowerEnd)))
|
||||
.toEqual(reduxState);
|
||||
});
|
||||
|
||||
test('moveMonitorRect', () => {
|
||||
let defaultState;
|
||||
const monitorId = 1;
|
||||
const monitorId2 = 2;
|
||||
const width = 102;
|
||||
const height = 101;
|
||||
const upperStart = {x: 100, y: 100};
|
||||
const lowerEnd = {x: upperStart.x + width, y: upperStart.y + height};
|
||||
const movedToPosition = {x: 0, y: 0};
|
||||
const movedToPosition2 = {x: 543, y: 2};
|
||||
|
||||
// Add a monitor rect and move it. Expect it to be in monitors state and saved positions.
|
||||
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId, upperStart, lowerEnd));
|
||||
const reduxState2 = monitorLayoutReducer(reduxState,
|
||||
moveMonitorRect(monitorId, movedToPosition.x, movedToPosition.y));
|
||||
expect(reduxState2.monitors[monitorId]).toBeDefined();
|
||||
expect(reduxState2.monitors[monitorId].upperStart).toEqual(movedToPosition);
|
||||
expect(reduxState2.monitors[monitorId].lowerEnd.x).toEqual(movedToPosition.x + width);
|
||||
expect(reduxState2.monitors[monitorId].lowerEnd.y).toEqual(movedToPosition.y + height);
|
||||
expect(reduxState2.savedMonitorPositions[monitorId]).toBeDefined();
|
||||
expect(reduxState2.savedMonitorPositions[monitorId].x).toEqual(movedToPosition.x);
|
||||
expect(reduxState2.savedMonitorPositions[monitorId].y).toEqual(movedToPosition.y);
|
||||
|
||||
// Add a second monitor rect and move it. Expect there to now be 2 saved positions.
|
||||
const reduxState3 = monitorLayoutReducer(reduxState2, addMonitorRect(monitorId2, upperStart, lowerEnd));
|
||||
const reduxState4 = monitorLayoutReducer(reduxState3,
|
||||
moveMonitorRect(monitorId2, movedToPosition2.x, movedToPosition2.y));
|
||||
expect(reduxState4.savedMonitorPositions[monitorId]).toEqual(reduxState2.savedMonitorPositions[monitorId]);
|
||||
expect(reduxState4.savedMonitorPositions[monitorId2].x).toEqual(movedToPosition2.x);
|
||||
expect(reduxState4.savedMonitorPositions[monitorId2].y).toEqual(movedToPosition2.y);
|
||||
});
|
||||
|
||||
test('invalidMoveMonitorRect', () => {
|
||||
let defaultState;
|
||||
let reduxState = monitorLayoutReducer(defaultState, {type: 'initialize'} /* action */);
|
||||
const monitorId = 1;
|
||||
|
||||
// Try to move a monitor rect that doesn't exist
|
||||
expect(monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 1 /* newX */, 1 /* newY */)))
|
||||
.toEqual(reduxState);
|
||||
|
||||
// Add the monitor to move
|
||||
reduxState = monitorLayoutReducer(reduxState, addMonitorRect(monitorId, {x: 100, y: 100}, {x: 200, y: 200}));
|
||||
|
||||
// Invalid newX
|
||||
expect(monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 'Oregon' /* newX */, 1 /* newY */)))
|
||||
.toEqual(reduxState);
|
||||
|
||||
// Invalid newY
|
||||
expect(monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 1 /* newX */)))
|
||||
.toEqual(reduxState);
|
||||
});
|
||||
|
||||
test('resizeMonitorRect', () => {
|
||||
let defaultState;
|
||||
const monitorId = 1;
|
||||
const upperStart = {x: 100, y: 100};
|
||||
const newWidth = 10;
|
||||
const newHeight = 20;
|
||||
|
||||
// Add a monitor rect and resize it
|
||||
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId, upperStart, {x: 200, y: 200}));
|
||||
const reduxState2 = monitorLayoutReducer(reduxState,
|
||||
resizeMonitorRect(monitorId, newWidth, newHeight));
|
||||
expect(reduxState2.monitors[monitorId]).toBeDefined();
|
||||
expect(reduxState2.monitors[monitorId].upperStart).toEqual(upperStart);
|
||||
expect(reduxState2.monitors[monitorId].lowerEnd.x).toEqual(upperStart.x + newWidth);
|
||||
expect(reduxState2.monitors[monitorId].lowerEnd.y).toEqual(upperStart.y + newHeight);
|
||||
// Saved positions aren't changed by resizing monitor
|
||||
expect(reduxState2.savedMonitorPositions).toEqual(reduxState.savedMonitorPositions);
|
||||
});
|
||||
|
||||
test('invalidResizeMonitorRect', () => {
|
||||
let defaultState;
|
||||
let reduxState = monitorLayoutReducer(defaultState, {type: 'initialize'} /* action */);
|
||||
const monitorId = 1;
|
||||
|
||||
// Try to resize a monitor rect that doesn't exist
|
||||
expect(monitorLayoutReducer(reduxState, resizeMonitorRect(monitorId, 1 /* newWidth */, 1 /* newHeight */)))
|
||||
.toEqual(reduxState);
|
||||
|
||||
// Add the monitor to resize
|
||||
reduxState = monitorLayoutReducer(reduxState, addMonitorRect(monitorId, {x: 100, y: 100}, {x: 200, y: 200}));
|
||||
|
||||
// Invalid newWidth
|
||||
expect(monitorLayoutReducer(reduxState, resizeMonitorRect(monitorId, 'Oregon' /* newWidth */, 1 /* newHeight */)))
|
||||
.toEqual(reduxState);
|
||||
|
||||
// Invalid newHeight
|
||||
expect(monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 1 /* newWidth */)))
|
||||
.toEqual(reduxState);
|
||||
|
||||
// newWidth < 0
|
||||
expect(monitorLayoutReducer(reduxState, resizeMonitorRect(monitorId, -1 /* newWidth */, 1 /* newHeight */)))
|
||||
.toEqual(reduxState);
|
||||
|
||||
// newHeight < 0
|
||||
expect(monitorLayoutReducer(reduxState, resizeMonitorRect(monitorId, 1 /* newWidth */, -1 /* newHeight */)))
|
||||
.toEqual(reduxState);
|
||||
});
|
||||
|
||||
test('removeMonitorRect', () => {
|
||||
let defaultState;
|
||||
const monitorId = 1;
|
||||
|
||||
// Add a monitor rect, move it, and remove it
|
||||
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(monitorId,
|
||||
{x: 100, y: 100},
|
||||
{x: 200, y: 200}
|
||||
));
|
||||
const reduxState2 = monitorLayoutReducer(reduxState, moveMonitorRect(monitorId, 0, 0));
|
||||
const reduxState3 = monitorLayoutReducer(reduxState2, removeMonitorRect(monitorId));
|
||||
expect(reduxState3.monitors[monitorId]).toBeUndefined();
|
||||
// Check that saved positions aren't changed by removing monitor
|
||||
expect(reduxState3.savedMonitorPositions).toEqual(reduxState2.savedMonitorPositions);
|
||||
});
|
||||
|
||||
test('invalidRemoveMonitorRect', () => {
|
||||
let defaultState;
|
||||
const reduxState = monitorLayoutReducer(defaultState, {type: 'initialize'} /* action */);
|
||||
|
||||
// Try to remove a monitor rect that doesn't exist
|
||||
expect(monitorLayoutReducer(reduxState, resizeMonitorRect(1)))
|
||||
.toEqual(reduxState);
|
||||
});
|
||||
|
||||
test('getInitialPosition_lineUpTopLeft', () => {
|
||||
let defaultState;
|
||||
const width = 100;
|
||||
const height = 200;
|
||||
// Add monitors to right and bottom, but there is a space in the top left
|
||||
let reduxState = monitorLayoutReducer(defaultState, addMonitorRect(1,
|
||||
{x: width + PADDING, y: 0},
|
||||
{x: 100, y: height}
|
||||
));
|
||||
reduxState = monitorLayoutReducer(defaultState, addMonitorRect(2,
|
||||
{x: 0, y: height + PADDING},
|
||||
{x: width, y: 100}
|
||||
));
|
||||
|
||||
// Check that the added monitor appears in the space
|
||||
const rect = getInitialPosition(reduxState, 3, width, height);
|
||||
expect(rect.upperStart).toBeDefined();
|
||||
expect(rect.lowerEnd).toBeDefined();
|
||||
expect(rect.lowerEnd.x - rect.upperStart.x).toEqual(width);
|
||||
expect(rect.lowerEnd.y - rect.upperStart.y).toEqual(height);
|
||||
expect(rect.upperStart.x).toEqual(PADDING);
|
||||
expect(rect.upperStart.y).toEqual(PADDING);
|
||||
});
|
||||
|
||||
test('getInitialPosition_savedPosition', () => {
|
||||
const monitorId = 1;
|
||||
const savedX = 100;
|
||||
const savedY = 200;
|
||||
const width = 7;
|
||||
const height = 8;
|
||||
const reduxState = {
|
||||
monitors: {},
|
||||
savedMonitorPositions: {[monitorId]: {x: savedX, y: savedY}}
|
||||
};
|
||||
|
||||
// Check that initial position uses saved state
|
||||
const rect = getInitialPosition(reduxState, monitorId, width, height);
|
||||
expect(rect.upperStart).toBeDefined();
|
||||
expect(rect.lowerEnd).toBeDefined();
|
||||
expect(rect.lowerEnd.x - rect.upperStart.x).toEqual(width);
|
||||
expect(rect.lowerEnd.y - rect.upperStart.y).toEqual(height);
|
||||
expect(rect.upperStart.x).toEqual(savedX);
|
||||
expect(rect.upperStart.y).toEqual(savedY);
|
||||
});
|
||||
|
||||
test('getInitialPosition_lineUpLeft', () => {
|
||||
let defaultState;
|
||||
const monitor1EndY = 60;
|
||||
// Add a monitor that takes up the upper left corner
|
||||
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(1, {x: 0, y: 0}, {x: 100, y: monitor1EndY}));
|
||||
|
||||
// Check that added monitor is under it and lines up left
|
||||
const rect = getInitialPosition(reduxState, 2, 20 /* width */, 20 /* height */);
|
||||
expect(rect.upperStart.y >= monitor1EndY + PADDING).toBeTruthy();
|
||||
});
|
||||
|
||||
test('getInitialPosition_lineUpTop', () => {
|
||||
let defaultState;
|
||||
const monitor1EndX = 100;
|
||||
// Add a monitor that takes up the whole left side
|
||||
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(1,
|
||||
{x: 0, y: 0},
|
||||
{x: monitor1EndX, y: SCREEN_HEIGHT}
|
||||
));
|
||||
|
||||
// Check that added monitor is to the right of it and lines up top
|
||||
const rect = getInitialPosition(reduxState, 2, 20 /* width */, 20 /* height */);
|
||||
expect(rect.upperStart.y).toEqual(PADDING);
|
||||
expect(rect.upperStart.x >= monitor1EndX + PADDING).toBeTruthy();
|
||||
});
|
||||
|
||||
test('getInitialPosition_noRoom', () => {
|
||||
let defaultState;
|
||||
const width = 7;
|
||||
const height = 8;
|
||||
// Add a monitor that takes up the whole screen
|
||||
const reduxState = monitorLayoutReducer(defaultState, addMonitorRect(1,
|
||||
{x: 0, y: 0},
|
||||
{x: SCREEN_WIDTH, y: SCREEN_HEIGHT}
|
||||
));
|
||||
|
||||
// Check that added monitor exists somewhere (we don't care where)
|
||||
const rect = getInitialPosition(reduxState, 2, width, height);
|
||||
expect(rect.upperStart).toBeDefined();
|
||||
expect(rect.lowerEnd.x - rect.upperStart.x).toEqual(width);
|
||||
expect(rect.lowerEnd.y - rect.upperStart.y).toEqual(height);
|
||||
});
|
||||
@@ -103,10 +103,18 @@ module.exports = {
|
||||
filename: 'player.html',
|
||||
title: 'Scratch 3.0 GUI: Player Example'
|
||||
}),
|
||||
new CopyWebpackPlugin([{
|
||||
from: 'static',
|
||||
to: 'static'
|
||||
}]),
|
||||
new CopyWebpackPlugin([{
|
||||
from: 'node_modules/scratch-blocks/media',
|
||||
to: 'static/blocks-media'
|
||||
}]),
|
||||
new CopyWebpackPlugin([{
|
||||
from: 'node_modules/scratch-vm/dist/node/assets',
|
||||
to: 'static/extension-assets'
|
||||
}]),
|
||||
new CopyWebpackPlugin([{
|
||||
from: 'extensions/**',
|
||||
to: 'static',
|
||||
|
||||
Reference in new issue
Block a user