Skip to content
Snippets Groups Projects
Unverified Commit 616d4fa1 authored by Paul Kaplan's avatar Paul Kaplan Committed by GitHub
Browse files

Merge pull request #3882 from paulkaplan/selective-auto-start

Green flag overlay to start in player mode
parents fba12418 4d74ed03
No related branches found
No related tags found
No related merge requests found
......@@ -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; }
......
......@@ -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}
/>
</Box>
<Box className={styles.greenFlagOverlayWrapper}>
<GreenFlagOverlay
className={styles.greenFlagOverlay}
/>
</Box>
{isColorPicking && colorInfo ? (
<Box className={styles.colorPickerWrapper}>
<Loupe colorInfo={colorInfo} />
......
......@@ -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
});
......
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 (
<div
className={this.props.className}
onClick={this.handleClick}
>
<img
draggable={false}
src={greenFlag}
/>
</div>
);
}
}
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);
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 (
<GreenFlagComponent
active={this.state.projectRunning}
onClick={this.handleClick}
{...props}
/>
);
}
}
GreenFlag.propTypes = {
vm: PropTypes.instanceOf(VM)
};
export default GreenFlag;
......@@ -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());
......
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 (
<StopAllComponent
active={!this.state.projectRunning}
onClick={this.handleClick}
{...props}
/>
);
}
}
StopAll.propTypes = {
vm: PropTypes.instanceOf(VM)
};
export default StopAll;
......@@ -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 => {
......
......@@ -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
};
};
......
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
};
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GreenFlag Container renders active state 1`] = `
<img
className="undefined"
draggable={false}
onClick={[Function]}
src="test-file-stub"
title="Go"
/>
`;
exports[`GreenFlag Container renders inactive state 1`] = `
<img
className=""
draggable={false}
onClick={[Function]}
src="test-file-stub"
title="Go"
/>
`;
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(
<GreenFlag
active
vm={vm}
/>
);
expect(component.toJSON()).toMatchSnapshot();
});
test('renders inactive state', () => {
const component = renderer.create(
<GreenFlag
active={false}
vm={vm}
/>
);
expect(component.toJSON()).toMatchSnapshot();
});
test('triggers onClick when active', () => {
const onClick = jest.fn();
const componentShallowWrapper = shallow(
<GreenFlag
active
vm={vm}
onClick={onClick}
/>
);
componentShallowWrapper.simulate('click');
expect(onClick).toHaveBeenCalled();
});
// @todo: Test for handles key events.
// @todo: Test project run start.
// @todo: Test project run stop.
});
......@@ -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 = () => (<div />);
const WrappedComponent = vmManagerHOC(Component);
mount(
<WrappedComponent
isPlayerOnly
isStarted={false}
store={store}
vm={vm}
/>
);
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('when it mounts in editor mode, the vm is initialized and started', () => {
const Component = () => (<div />);
const WrappedComponent = vmManagerHOC(Component);
mount(
<WrappedComponent
isPlayerOnly={false}
isStarted={false}
store={store}
vm={vm}
/>
);
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', () => {
test('if it mounts with an initialized vm, it does not reinitialize the vm but will start it', () => {
const Component = () => <div />;
const WrappedComponent = vmManagerHOC(Component);
vm.initialized = true;
mount(
<WrappedComponent
isPlayerOnly={false}
isStarted={false}
store={store}
vm={vm}
/>
);
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 = () => <div />;
const WrappedComponent = vmManagerHOC(Component);
vm.initialized = true;
const mounted = mount(
<WrappedComponent
isPlayerOnly
isStarted={false}
store={store}
vm={vm}
/>
);
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 = () => <div />;
const WrappedComponent = vmManagerHOC(Component);
vm.initialized = true;
const mounted = mount(
<WrappedComponent
isPlayerOnly
isStarted
store={store}
vm={vm}
/>
);
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());
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment