Merge pull request #1025 from LLK/staging

Merge and freeze dependencies
This commit is contained in:
Paul Kaplan authored and GitHub committed 2017-12-11 15:57:56 -05:00
commit 8dcb84a7af
63 files changed
+5110 -44574

No files matched your search

+1 -1
View File
@@ -1,5 +1,5 @@
language: node_js
sudo: false
sudo: required
dist: trusty
addons:
chrome: stable
+21 -23
View File
@@ -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": {
-45
View File
@@ -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));
+12
View File
@@ -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

+3 -1
View File
@@ -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
+10
View File
@@ -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}
+1
View File
@@ -18,6 +18,7 @@ const MenuBar = function MenuBar () {
>
<div className={classNames(styles.logoWrapper, styles.menuItem)}>
<img
alt="Scratch"
className={styles.scratchLogo}
src={scratchLogo}
/>
+2 -2
View File
@@ -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}
/>
+7 -11
View File
@@ -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;
+57
View File
@@ -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;
}
+47
View File
@@ -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);
}
+13 -6
View File
@@ -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([
+11
View File
@@ -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 = {
+1 -1
View File
@@ -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}
/>
+48 -9
View File
@@ -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));
}
});
+135
View File
@@ -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);
+9 -2
View File
@@ -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,
+71 -7
View File
@@ -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);
+45
View File
@@ -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;
+2
View File
@@ -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 () {
+6 -8
View File
@@ -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 ? (
+17 -1
View File
@@ -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}
/>
);
}
+17 -2
View File
@@ -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}
/>
);
+4
View File
@@ -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;
+9
View File
@@ -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>
+8
View File
@@ -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
View File
@@ -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);
};
+2 -1
View File
@@ -3,5 +3,6 @@ export default {
fullStageHeight: 360,
smallerStageWidth: 480 * 0.85,
smallerStageHeight: 360 * 0.85,
fullSizeMinWidth: 1096
fullSizeMinWidth: 1096,
fullSizePaintMinWidth: 1250
};
File diff suppressed because it is too large. Load diff
+1031 -10242
View File
File diff suppressed because it is too large. Load diff
+8 -8
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+168 -162
View File
@@ -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
View File
@@ -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};
}
+4
View File
@@ -48,6 +48,10 @@ const opcodeMap = {
},
// Sensing
sensing_answer: {
category: 'sensing',
label: 'answer'
},
sensing_loudness: {
category: 'sensing',
label: 'loudness'
+4
View File
@@ -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],
+65
View File
@@ -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
};
+4
View File
@@ -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,
+318
View File
@@ -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 -2
View File
@@ -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

+25 -2
View File
@@ -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}')]`);
}
+5 -4
View File
@@ -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
View File
@@ -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}
+31 -1
View File
@@ -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);
});
+8
View File
@@ -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',