diff --git a/src/components/stage/stage.css b/src/components/stage/stage.css
index 9264c094e..7e0a59c2e 100644
--- a/src/components/stage/stage.css
+++ b/src/components/stage/stage.css
@@ -89,7 +89,10 @@ to adjust for the border using a different method */
padding-bottom: calc($stage-full-screen-stage-padding + $stage-full-screen-border-width);
}
-.monitor-wrapper, .color-picker-wrapper, .frame-wrapper {
+.monitor-wrapper,
+.color-picker-wrapper,
+.frame-wrapper,
+.green-flag-overlay-wrapper {
position: absolute;
top: 0;
left: 0;
@@ -138,6 +141,36 @@ to adjust for the border using a different method */
animation-fill-mode: forwards; /* Leave at 0 opacity after animation */
}
+.green-flag-overlay-wrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: rgba(0,0,0,0.25);
+ border-radius: 0.5rem;
+}
+
+.green-flag-overlay {
+ padding: 1rem;
+ border-radius: 100%;
+ background: rgba(255,255,255,0.75);
+ border: 3px solid $ui-white;
+ pointer-events: all;
+ cursor: pointer;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ box-sizing: border-box;
+ width: 5rem;
+ height: 5rem;
+}
+
+.green-flag-overlay > img {
+ width: 100%;
+ object-fit: contain;
+}
+
+
+
@keyframes flash {
0% { opacity: 1; }
100% { opacity: 0; }
diff --git a/src/components/stage/stage.jsx b/src/components/stage/stage.jsx
index ede5048ea..6d81443f8 100644
--- a/src/components/stage/stage.jsx
+++ b/src/components/stage/stage.jsx
@@ -7,6 +7,7 @@ import DOMElementRenderer from '../../containers/dom-element-renderer.jsx';
import Loupe from '../loupe/loupe.jsx';
import MonitorList from '../../containers/monitor-list.jsx';
import TargetHighlight from '../../containers/target-highlight.jsx';
+import GreenFlagOverlay from '../../containers/green-flag-overlay.jsx';
import Question from '../../containers/question.jsx';
import MicIndicator from '../mic-indicator/mic-indicator.jsx';
import {STAGE_DISPLAY_SIZES} from '../../lib/layout-constants.js';
@@ -71,6 +72,11 @@ const StageComponent = props => {
stageWidth={stageDimensions.width}
/>
+
+
+
{isColorPicking && colorInfo ? (
diff --git a/src/containers/controls.jsx b/src/containers/controls.jsx
index 2869a047a..db052b494 100644
--- a/src/containers/controls.jsx
+++ b/src/containers/controls.jsx
@@ -20,6 +20,9 @@ class Controls extends React.Component {
if (e.shiftKey) {
this.props.vm.setTurboMode(!this.props.turbo);
} else {
+ if (!this.props.isStarted) {
+ this.props.vm.start();
+ }
this.props.vm.greenFlag();
analytics.event({
category: 'general',
@@ -38,6 +41,7 @@ class Controls extends React.Component {
render () {
const {
vm, // eslint-disable-line no-unused-vars
+ isStarted, // eslint-disable-line no-unused-vars
projectRunning,
turbo,
...props
@@ -55,12 +59,14 @@ class Controls extends React.Component {
}
Controls.propTypes = {
+ isStarted: PropTypes.bool.isRequired,
projectRunning: PropTypes.bool.isRequired,
turbo: PropTypes.bool.isRequired,
vm: PropTypes.instanceOf(VM)
};
const mapStateToProps = state => ({
+ isStarted: state.scratchGui.vmStatus.running,
projectRunning: state.scratchGui.vmStatus.running,
turbo: state.scratchGui.vmStatus.turbo
});
diff --git a/src/containers/green-flag-overlay.jsx b/src/containers/green-flag-overlay.jsx
new file mode 100644
index 000000000..2d0e4135f
--- /dev/null
+++ b/src/containers/green-flag-overlay.jsx
@@ -0,0 +1,55 @@
+import bindAll from 'lodash.bindall';
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import {connect} from 'react-redux';
+import VM from 'scratch-vm';
+import greenFlag from '../components/green-flag/icon--green-flag.svg';
+
+class GreenFlagOverlay extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleClick'
+ ]);
+ }
+
+ handleClick () {
+ this.props.vm.start();
+ this.props.vm.greenFlag();
+ }
+
+ render () {
+ if (this.props.isStarted) return null;
+
+ return (
+
+

+
+ );
+ }
+}
+
+GreenFlagOverlay.propTypes = {
+ className: PropTypes.string,
+ isStarted: PropTypes.bool,
+ vm: PropTypes.instanceOf(VM)
+};
+
+const mapStateToProps = state => ({
+ isStarted: state.scratchGui.vmStatus.started,
+ vm: state.scratchGui.vm
+});
+
+const mapDispatchToProps = () => ({});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(GreenFlagOverlay);
diff --git a/src/containers/green-flag.jsx b/src/containers/green-flag.jsx
deleted file mode 100644
index a725b7e1b..000000000
--- a/src/containers/green-flag.jsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import bindAll from 'lodash.bindall';
-import PropTypes from 'prop-types';
-import React from 'react';
-
-import VM from 'scratch-vm';
-
-import GreenFlagComponent from '../components/green-flag/green-flag.jsx';
-
-class GreenFlag extends React.Component {
- constructor (props) {
- super(props);
- bindAll(this, [
- 'handleClick',
- 'onProjectRunStart',
- 'onProjectRunStop'
- ]);
- this.state = {projectRunning: false};
- }
- componentDidMount () {
- this.props.vm.addListener('PROJECT_RUN_START', this.onProjectRunStart);
- this.props.vm.addListener('PROJECT_RUN_STOP', this.onProjectRunStop);
- }
- componentWillUnmount () {
- this.props.vm.removeListener('PROJECT_RUN_START', this.onProjectRunStart);
- this.props.vm.removeListener('PROJECT_RUN_STOP', this.onProjectRunStop);
- }
- onProjectRunStart () {
- this.setState({projectRunning: true});
- }
- onProjectRunStop () {
- this.setState({projectRunning: false});
- }
- handleClick (e) {
- e.preventDefault();
- if (e.shiftKey) {
- this.props.vm.setTurboMode(!this.props.vm.runtime.turboMode);
- } else {
- this.props.vm.greenFlag();
- }
- }
- render () {
- const {
- vm, // eslint-disable-line no-unused-vars
- ...props
- } = this.props;
- return (
-
- );
- }
-}
-
-GreenFlag.propTypes = {
- vm: PropTypes.instanceOf(VM)
-};
-
-export default GreenFlag;
diff --git a/src/containers/stage.jsx b/src/containers/stage.jsx
index 888dfa1c6..077006fcc 100644
--- a/src/containers/stage.jsx
+++ b/src/containers/stage.jsx
@@ -59,6 +59,12 @@ class Stage extends React.Component {
this.canvas = document.createElement('canvas');
this.renderer = new Renderer(this.canvas);
this.props.vm.attachRenderer(this.renderer);
+
+ // Calling draw a single time before any project is loaded just makes
+ // the canvas white instead of solid black–needed because it is not
+ // possible to use CSS to style the canvas to have a different
+ // default color
+ this.props.vm.renderer.draw();
}
this.props.vm.attachV2SVGAdapter(new V2SVGAdapter());
this.props.vm.attachV2BitmapAdapter(new V2BitmapAdapter());
diff --git a/src/containers/stop-all.jsx b/src/containers/stop-all.jsx
deleted file mode 100644
index 0f7641a20..000000000
--- a/src/containers/stop-all.jsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import bindAll from 'lodash.bindall';
-import PropTypes from 'prop-types';
-import React from 'react';
-import VM from 'scratch-vm';
-
-import StopAllComponent from '../components/stop-all/stop-all.jsx';
-
-class StopAll extends React.Component {
- constructor (props) {
- super(props);
- bindAll(this, [
- 'handleClick',
- 'onProjectRunStart',
- 'onProjectRunStop'
- ]);
- this.state = {projectRunning: false};
- }
- componentDidMount () {
- this.props.vm.addListener('PROJECT_RUN_START', this.onProjectRunStart);
- this.props.vm.addListener('PROJECT_RUN_STOP', this.onProjectRunStop);
- }
- componentWillUnmount () {
- this.props.vm.removeListener('PROJECT_RUN_START', this.onProjectRunStart);
- this.props.vm.removeListener('PROJECT_RUN_STOP', this.onProjectRunStop);
- }
- onProjectRunStart () {
- this.setState({projectRunning: true});
- }
- onProjectRunStop () {
- this.setState({projectRunning: false});
- }
- handleClick (e) {
- e.preventDefault();
- this.props.vm.stopAll();
- }
- render () {
- const {
- vm, // eslint-disable-line no-unused-vars
- ...props
- } = this.props;
- return (
-
- );
- }
-}
-
-StopAll.propTypes = {
- vm: PropTypes.instanceOf(VM)
-};
-
-export default StopAll;
diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx
index 9dee35d75..58f45e79f 100644
--- a/src/lib/vm-listener-hoc.jsx
+++ b/src/lib/vm-listener-hoc.jsx
@@ -8,7 +8,7 @@ import {connect} from 'react-redux';
import {updateTargets} from '../reducers/targets';
import {updateBlockDrag} from '../reducers/block-drag';
import {updateMonitors} from '../reducers/monitors';
-import {setRunningState, setTurboState} from '../reducers/vm-status';
+import {setRunningState, setTurboState, setStartedState} from '../reducers/vm-status';
import {showExtensionAlert} from '../reducers/alerts';
import {updateMicIndicator} from '../reducers/mic-indicator';
@@ -39,6 +39,7 @@ const vmListenerHOC = function (WrappedComponent) {
this.props.vm.on('TURBO_MODE_OFF', this.props.onTurboModeOff);
this.props.vm.on('PROJECT_RUN_START', this.props.onProjectRunStart);
this.props.vm.on('PROJECT_RUN_STOP', this.props.onProjectRunStop);
+ this.props.vm.on('RUNTIME_STARTED', this.props.onRuntimeStarted);
this.props.vm.on('PERIPHERAL_DISCONNECT_ERROR', this.props.onShowExtensionAlert);
this.props.vm.on('MIC_LISTENING', this.props.onMicListeningUpdate);
@@ -110,6 +111,7 @@ const vmListenerHOC = function (WrappedComponent) {
onTargetsUpdate,
onProjectRunStart,
onProjectRunStop,
+ onRuntimeStarted,
onTurboModeOff,
onTurboModeOn,
onShowExtensionAlert,
@@ -128,6 +130,7 @@ const vmListenerHOC = function (WrappedComponent) {
onMonitorsUpdate: PropTypes.func.isRequired,
onProjectRunStart: PropTypes.func.isRequired,
onProjectRunStop: PropTypes.func.isRequired,
+ onRuntimeStarted: PropTypes.func.isRequired,
onShowExtensionAlert: PropTypes.func.isRequired,
onTargetsUpdate: PropTypes.func.isRequired,
onTurboModeOff: PropTypes.func.isRequired,
@@ -158,6 +161,7 @@ const vmListenerHOC = function (WrappedComponent) {
},
onProjectRunStart: () => dispatch(setRunningState(true)),
onProjectRunStop: () => dispatch(setRunningState(false)),
+ onRuntimeStarted: () => dispatch(setStartedState(true)),
onTurboModeOn: () => dispatch(setTurboState(true)),
onTurboModeOff: () => dispatch(setTurboState(false)),
onShowExtensionAlert: data => {
diff --git a/src/lib/vm-manager-hoc.jsx b/src/lib/vm-manager-hoc.jsx
index 404b7e3c8..981fe8326 100644
--- a/src/lib/vm-manager-hoc.jsx
+++ b/src/lib/vm-manager-hoc.jsx
@@ -27,12 +27,15 @@ const vmManagerHOC = function (WrappedComponent) {
]);
}
componentDidMount () {
- if (this.props.vm.initialized) return;
- this.audioEngine = new AudioEngine();
- this.props.vm.attachAudioEngine(this.audioEngine);
- this.props.vm.setCompatibilityMode(true);
- this.props.vm.start();
- this.props.vm.initialized = true;
+ if (!this.props.vm.initialized) {
+ this.audioEngine = new AudioEngine();
+ this.props.vm.attachAudioEngine(this.audioEngine);
+ this.props.vm.setCompatibilityMode(true);
+ this.props.vm.initialized = true;
+ }
+ if (!this.props.isPlayerOnly && !this.props.isStarted) {
+ this.props.vm.start();
+ }
}
componentDidUpdate (prevProps) {
// if project is in loading state, AND fonts are loaded,
@@ -41,11 +44,26 @@ const vmManagerHOC = function (WrappedComponent) {
(!prevProps.isLoadingWithId || !prevProps.fontsLoaded)) {
this.loadProject();
}
+ // Start the VM if entering editor mode with an unstarted vm
+ if (!this.props.isPlayerOnly && !this.props.isStarted) {
+ this.props.vm.start();
+ }
}
loadProject () {
return this.props.vm.loadProject(this.props.projectData)
.then(() => {
this.props.onLoadedProject(this.props.loadingState, this.props.canSave);
+
+ // If the vm is not running, call draw on the renderer manually
+ // This draws the state of the loaded project with no blocks running
+ // which closely matches the 2.0 behavior, except for monitors–
+ // 2.0 runs monitors and shows updates (e.g. timer monitor)
+ // before the VM starts running other hat blocks.
+ if (!this.props.isStarted) {
+ // Wrap in a setTimeout because skin loading in
+ // the renderer can be async.
+ setTimeout(() => this.props.vm.renderer.draw());
+ }
})
.catch(e => {
this.props.onError(e);
@@ -56,6 +74,7 @@ const vmManagerHOC = function (WrappedComponent) {
/* eslint-disable no-unused-vars */
fontsLoaded,
loadingState,
+ isStarted,
onError: onErrorProp,
onLoadedProject: onLoadedProjectProp,
projectData,
@@ -79,6 +98,7 @@ const vmManagerHOC = function (WrappedComponent) {
cloudHost: PropTypes.string,
fontsLoaded: PropTypes.bool,
isLoadingWithId: PropTypes.bool,
+ isPlayerOnly: PropTypes.bool,
loadingState: PropTypes.oneOf(LoadingStates),
onError: PropTypes.func,
onLoadedProject: PropTypes.func,
@@ -94,7 +114,9 @@ const vmManagerHOC = function (WrappedComponent) {
isLoadingWithId: getIsLoadingWithId(loadingState),
projectData: state.scratchGui.projectState.projectData,
projectId: state.scratchGui.projectState.projectId,
- loadingState: loadingState
+ loadingState: loadingState,
+ isPlayerOnly: state.scratchGui.mode.isPlayerOnly,
+ isStarted: state.scratchGui.vmStatus.started
};
};
diff --git a/src/reducers/vm-status.js b/src/reducers/vm-status.js
index 8e9651992..e99b80368 100644
--- a/src/reducers/vm-status.js
+++ b/src/reducers/vm-status.js
@@ -1,14 +1,20 @@
const SET_RUNNING_STATE = 'scratch-gui/vm-status/SET_RUNNING_STATE';
const SET_TURBO_STATE = 'scratch-gui/vm-status/SET_TURBO_STATE';
+const SET_STARTED_STATE = 'scratch-gui/vm-status/SET_STARTED_STATE';
const initialState = {
running: false,
+ started: false,
turbo: false
};
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
+ case SET_STARTED_STATE:
+ return Object.assign({}, state, {
+ started: action.started
+ });
case SET_RUNNING_STATE:
return Object.assign({}, state, {
running: action.running
@@ -22,6 +28,14 @@ const reducer = function (state, action) {
}
};
+const setStartedState = function (started) {
+ return {
+ type: SET_STARTED_STATE,
+ started: started
+ };
+};
+
+
const setRunningState = function (running) {
return {
type: SET_RUNNING_STATE,
@@ -40,5 +54,6 @@ export {
reducer as default,
initialState as vmStatusInitialState,
setRunningState,
+ setStartedState,
setTurboState
};
diff --git a/test/unit/containers/__snapshots__/green-flag.test.jsx.snap b/test/unit/containers/__snapshots__/green-flag.test.jsx.snap
deleted file mode 100644
index f8dbdbf82..000000000
--- a/test/unit/containers/__snapshots__/green-flag.test.jsx.snap
+++ /dev/null
@@ -1,21 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`GreenFlag Container renders active state 1`] = `
-
-`;
-
-exports[`GreenFlag Container renders inactive state 1`] = `
-
-`;
diff --git a/test/unit/containers/green-flag.test.jsx b/test/unit/containers/green-flag.test.jsx
deleted file mode 100644
index 4744ebaab..000000000
--- a/test/unit/containers/green-flag.test.jsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from 'react';
-import {shallow} from 'enzyme';
-import GreenFlag from '../../../src/containers/green-flag';
-import renderer from 'react-test-renderer';
-import VM from 'scratch-vm';
-
-describe('GreenFlag Container', () => {
- let vm;
- beforeEach(() => {
- vm = new VM();
- });
-
- test('renders active state', () => {
- const component = renderer.create(
-
- );
- expect(component.toJSON()).toMatchSnapshot();
- });
-
- test('renders inactive state', () => {
- const component = renderer.create(
-
- );
- expect(component.toJSON()).toMatchSnapshot();
- });
-
- test('triggers onClick when active', () => {
- const onClick = jest.fn();
- const componentShallowWrapper = shallow(
-
- );
- componentShallowWrapper.simulate('click');
- expect(onClick).toHaveBeenCalled();
- });
-
- // @todo: Test for handles key events.
- // @todo: Test project run start.
- // @todo: Test project run stop.
-});
diff --git a/test/unit/util/vm-manager-hoc.test.jsx b/test/unit/util/vm-manager-hoc.test.jsx
index 8c065d83d..798e3a7e4 100644
--- a/test/unit/util/vm-manager-hoc.test.jsx
+++ b/test/unit/util/vm-manager-hoc.test.jsx
@@ -16,7 +16,9 @@ describe('VMManagerHOC', () => {
beforeEach(() => {
store = mockStore({
scratchGui: {
- projectState: {}
+ projectState: {},
+ mode: {},
+ vmStatus: {}
}
});
vm = new VM();
@@ -24,34 +26,95 @@ describe('VMManagerHOC', () => {
vm.setCompatibilityMode = jest.fn();
vm.start = jest.fn();
});
- test('when it mounts, the vm is initialized', () => {
+ test('when it mounts in player mode, the vm is initialized but not started', () => {
const Component = () => ();
const WrappedComponent = vmManagerHOC(Component);
mount(
);
expect(vm.attachAudioEngine.mock.calls.length).toBe(1);
expect(vm.setCompatibilityMode.mock.calls.length).toBe(1);
- expect(vm.start.mock.calls.length).toBe(1);
expect(vm.initialized).toBe(true);
+
+ // But vm should not be started automatically
+ expect(vm.start).not.toHaveBeenCalled();
});
- test('if it mounts with an initialized vm, it does not reinitialize the vm', () => {
+ test('when it mounts in editor mode, the vm is initialized and started', () => {
+ const Component = () => ();
+ const WrappedComponent = vmManagerHOC(Component);
+ mount(
+
+ );
+ expect(vm.attachAudioEngine.mock.calls.length).toBe(1);
+ expect(vm.setCompatibilityMode.mock.calls.length).toBe(1);
+ expect(vm.initialized).toBe(true);
+
+ expect(vm.start).toHaveBeenCalled();
+ });
+ test('if it mounts with an initialized vm, it does not reinitialize the vm but will start it', () => {
const Component = () => ;
const WrappedComponent = vmManagerHOC(Component);
vm.initialized = true;
mount(
);
expect(vm.attachAudioEngine.mock.calls.length).toBe(0);
expect(vm.setCompatibilityMode.mock.calls.length).toBe(0);
- expect(vm.start.mock.calls.length).toBe(0);
expect(vm.initialized).toBe(true);
+
+ expect(vm.start).toHaveBeenCalled();
+ });
+
+ test('if it mounts without starting the VM, it can be started by switching to editor mode', () => {
+ const Component = () => ;
+ const WrappedComponent = vmManagerHOC(Component);
+ vm.initialized = true;
+ const mounted = mount(
+
+ );
+ expect(vm.start).not.toHaveBeenCalled();
+ mounted.setProps({
+ isPlayerOnly: false
+ });
+ expect(vm.start).toHaveBeenCalled();
+ });
+ test('if it mounts with an initialized and started VM, it does not start again', () => {
+ const Component = () => ;
+ const WrappedComponent = vmManagerHOC(Component);
+ vm.initialized = true;
+ const mounted = mount(
+
+ );
+ expect(vm.start).not.toHaveBeenCalled();
+ mounted.setProps({
+ isPlayerOnly: false
+ });
+ expect(vm.start).not.toHaveBeenCalled();
});
test('if the isLoadingWithId prop becomes true, it loads project data into the vm', () => {
vm.loadProject = jest.fn(() => Promise.resolve());