From 50f168172e7aad2b69306422ccab8faf1918e1af Mon Sep 17 00:00:00 2001
From: Paul Kaplan <pkaplan@media.mit.edu>
Date: Thu, 17 Aug 2017 17:21:12 -0400
Subject: [PATCH] Add controls container for turbo mode indicator

---
 src/components/controls/controls.css          |   3 +
 src/components/controls/controls.jsx          |  53 +++++++++++++
 src/components/green-flag/green-flag.css      |  17 +++--
 src/components/green-flag/icon-green-flag.svg | Bin 816 -> 0 bytes
 src/components/gui/gui.css                    |   6 +-
 src/components/gui/gui.jsx                    |   9 +--
 src/components/stop-all/stop-all.css          |  19 +++--
 src/components/turbo-mode/icon--turbo.svg     | Bin 0 -> 1244 bytes
 src/components/turbo-mode/turbo-mode.css      |  20 +++++
 src/components/turbo-mode/turbo-mode.jsx      |  24 ++++++
 src/containers/controls.jsx                   |  70 ++++++++++++++++++
 src/containers/green-flag.jsx                 |  14 +---
 src/css/colors.css                            |   2 +
 src/examples/blocks-only.css                  |   9 +--
 src/examples/blocks-only.jsx                  |   9 +--
 src/examples/player.jsx                       |  13 +---
 test/unit/components/controls.test.jsx        |  40 ++++++++++
 17 files changed, 248 insertions(+), 60 deletions(-)
 create mode 100644 src/components/controls/controls.css
 create mode 100644 src/components/controls/controls.jsx
 delete mode 100644 src/components/green-flag/icon-green-flag.svg
 create mode 100644 src/components/turbo-mode/icon--turbo.svg
 create mode 100644 src/components/turbo-mode/turbo-mode.css
 create mode 100644 src/components/turbo-mode/turbo-mode.jsx
 create mode 100644 src/containers/controls.jsx
 create mode 100644 test/unit/components/controls.test.jsx

diff --git a/src/components/controls/controls.css b/src/components/controls/controls.css
new file mode 100644
index 000000000..7e60ae334
--- /dev/null
+++ b/src/components/controls/controls.css
@@ -0,0 +1,3 @@
+.controls-container {
+    display: flex;
+}
diff --git a/src/components/controls/controls.jsx b/src/components/controls/controls.jsx
new file mode 100644
index 000000000..8fec705b0
--- /dev/null
+++ b/src/components/controls/controls.jsx
@@ -0,0 +1,53 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import GreenFlag from '../green-flag/green-flag.jsx';
+import StopAll from '../stop-all/stop-all.jsx';
+import TurboMode from '../turbo-mode/turbo-mode.jsx';
+
+import styles from './controls.css';
+
+const Controls = function (props) {
+    const {
+        active,
+        className,
+        onGreenFlagClick,
+        onStopAllClick,
+        turbo,
+        ...componentProps
+    } = props;
+    return (
+        <div
+            className={classNames(styles.controlsContainer, className)}
+            {...componentProps}
+        >
+            <GreenFlag
+                active={active}
+                onClick={onGreenFlagClick}
+            />
+            <StopAll
+                active={active}
+                onClick={onStopAllClick}
+            />
+            {turbo ? (
+                <TurboMode />
+            ) : null}
+        </div>
+    );
+};
+
+Controls.propTypes = {
+    active: PropTypes.bool,
+    className: PropTypes.string,
+    onGreenFlagClick: PropTypes.func.isRequired,
+    onStopAllClick: PropTypes.func.isRequired,
+    turbo: PropTypes.bool
+};
+
+Controls.defaultProps = {
+    active: false,
+    turbo: false
+};
+
+export default Controls;
diff --git a/src/components/green-flag/green-flag.css b/src/components/green-flag/green-flag.css
index 3fda3a207..f348896f7 100644
--- a/src/components/green-flag/green-flag.css
+++ b/src/components/green-flag/green-flag.css
@@ -1,13 +1,18 @@
 .green-flag {
-    width: 1.1rem;
-    height: 1.1rem;
-    opacity: 0.5;
+    box-sizing: content-box;
+    width: 1.25rem;
+    height: 1.25rem;
+    padding: 0.375rem;
+    border-radius: 0.25rem;
     user-select: none;
     cursor: pointer;
-    transition: opacity 0.2s ease-out; /* @todo: standardize with var */
+    transition: 0.2s ease-out;
 }
 
-.green-flag.is-active,
 .green-flag:hover {
-    opacity: 1;
+    transform: scale(1.2);
+}
+
+.green-flag.is-active {
+    background-color: rgba(0, 0, 0, 0.1);
 }
diff --git a/src/components/green-flag/icon-green-flag.svg b/src/components/green-flag/icon-green-flag.svg
deleted file mode 100644
index 4bd2528f9be697e4b8a35158c217fffe81c8309b..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 816
zcmb_aU2EGg6n##9#hv;zuw*GsGFypP8W^RpL1EC>;n=F9MU{=E*a@TmeOGGMWw3`m
z?CDD9ocnQfwZ1t%8yHmYbZZMD7$=}?)z;cJ1v&U9nvt82)Ac(_;Z9lAm%i;K+}7<@
z!M!mD=OY9nXPBJ8*T*~fbnM#R!-F|A^xguC;Nxo#2}{N~Y!2Ggz~2K<isjwVTn-@!
z=(-@E%aiKA;RR>3t{^+_JDH~AII{7YwSAM$IOi!QpC$5WwB7$rDDpf{Ba704@a{<9
z^ctrV7_=HUZNPzpxMmrMIa?5L{$x}^lr6VL(QR4no4!5RT3R)t@<O|c6G+#U6()4O
za;hH`3D!al@TU%OuZ}*goI`xVoaaBDv@!B(vDn-$vK2mMyI1mRzRdIa;(Vp(NL6Jg
zW8+Kkd#mlA=(F~!HyVQ!eDxB0h=1uZ>e}z5VE?1(I*pFed4n$3UFmmFnbP6@&@lnr
z>le<lM6j$9>;n~Sp5SB0xMZ`08=mmMpdvCcMrO#^yh355C?*Apz9mg3lgNfrfsHY;
mBn_j4m;60S_*Mcp&NBQ7cnE7pX!Z*_A24&Bq7$?=VE75%zVjmh

diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css
index 6a063a166..c8a9285a2 100644
--- a/src/components/gui/gui.css
+++ b/src/components/gui/gui.css
@@ -101,10 +101,6 @@
     position: relative;
 }
 
-.green-flag {
-    margin: 0.25rem 0.6rem;
-}
-
 .stage-and-target-wrapper {
     /*
         Makes rows for children:
@@ -130,7 +126,7 @@
     display: flex;
     flex-grow: 1;
     flex-basis: 0;
-    
+
     padding-top: $space;
     padding-left: $space;
     padding-right: $space;
diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx
index 8c8c9f96d..1eb0e6505 100644
--- a/src/components/gui/gui.jsx
+++ b/src/components/gui/gui.jsx
@@ -8,11 +8,10 @@ import VM from 'scratch-vm';
 
 import Blocks from '../../containers/blocks.jsx';
 import CostumeTab from '../../containers/costume-tab.jsx';
-import GreenFlag from '../../containers/green-flag.jsx';
+import Controls from '../../containers/controls.jsx';
 import TargetPane from '../../containers/target-pane.jsx';
 import SoundTab from '../../containers/sound-tab.jsx';
 import Stage from '../../containers/stage.jsx';
-import StopAll from '../../containers/stop-all.jsx';
 
 import Box from '../box/box.jsx';
 import MenuBar from '../menu-bar/menu-bar.jsx';
@@ -90,11 +89,7 @@ const GUIComponent = props => {
 
                     <Box className={styles.stageAndTargetWrapper}>
                         <Box className={styles.stageMenuWrapper}>
-                            <GreenFlag
-                                className={styles.greenFlag}
-                                vm={vm}
-                            />
-                            <StopAll vm={vm} />
+                            <Controls vm={vm} />
                         </Box>
                         <Box className={styles.stageWrapper}>
                             <MediaQuery minWidth={layout.fullSizeMinWidth}>{isFullSize => (
diff --git a/src/components/stop-all/stop-all.css b/src/components/stop-all/stop-all.css
index 95a633771..99ebee892 100644
--- a/src/components/stop-all/stop-all.css
+++ b/src/components/stop-all/stop-all.css
@@ -1,13 +1,22 @@
 .stop-all {
-    width: 1.1rem;
-    height: 1.1rem;
-    opacity: 0.5;
+    box-sizing: content-box;
+    width: 1.25rem;
+    height: 1.25rem;
+    padding: 0.375rem;
+    border-radius: 0.25rem;
     user-select: none;
     cursor: pointer;
-    transition: opacity 0.2s ease-out; /* @todo: standardize with var */ 
+    transition: 0.2s ease-out;
+}
+
+.stop-all {
+    opacity: 0.5;
 }
 
-.stop-all.is-active,
 .stop-all:hover {
+    transform: scale(1.2);
+}
+
+.stop-all.is-active {
     opacity: 1;
 }
diff --git a/src/components/turbo-mode/icon--turbo.svg b/src/components/turbo-mode/icon--turbo.svg
new file mode 100644
index 0000000000000000000000000000000000000000..64fd1665fc03cb694262fc1a67b1adc958e29f4b
GIT binary patch
literal 1244
zcmZuxO>f&e5WM$S@UfQ$uqb|WH?Uo_2J$X0P@wIlH^*jdA&L}8YMh_np+AtKs35@0
z;qL70i1hs9>r*RU@-Q}iw~$6@DRQ^%cTKln$RFSDDw1NHHr;O1_FZ1cu9qK|@8;vn
zUL2d<bU;mfewE^ooBd(Bu3nma{M^4Tq!wD3h*$pE#7Kd*-MEm4X?nh`>*MjLj!yN%
zzP4KH8uXWx+t;?~9{-hSn7Kaf%H=zOXZ~MRiT`t#ht1Rvx8mDlp0)?!owDMCV;%WM
zRN{}0_x&M1HJi)0YTG~6<GH4)E|gO<wRt)H8t(h*zHg_h+4kMMKEvm(yF6}}t08Zu
zyn`px;nG;m>(kaX>tS3L1V3kFFPhy#e%<VI1wY1V=pRr1cKN!ht`1A_(6ntazigJc
z8h*9N&M&#^ce{%LClk)Je<{8)F68^WJ6f4fSH|n|tf>%`Xa2mw7*O{mD#M0t3^P`o
zv^O>;v5LwunG_Puj8>j(Vs;`b^1)b+1q{IiZ%8B%_z<=C1vQ2O*&x<Q`DncN1l~l6
zwJE$wktTK?6s%lG&PVowly~0R5Is;!!Gk*jrLrHR0U8}AAFL5;V^pLVpus5Clv40w
zWo+q;<qVMxDd-RcLZ_XEpn_tsz}gj5bF@i^!VrzM&Vv>zJzIm)Sx_L=&Icz}tb%u#
zBU->V+F;pO!Jr7_eJUu%*CsiFU`Qlw3Wx@d;E-5jxP(+<peS-Fu~?BZkmlK^8LWmQ
zj>y2upkbw_2o&~(z?6XM7@S2GP@|KLOjfKt=7e?)hn*rv)^Twdv0=g|uz=AuX!a}|
z41{1P0vfG$me>F#Z}Gxm4?$=hfUf^}B!0@%X?{C)QcT098z1`NX(3Nxiwp80DUR$o
x%%B_JFKD_ELqC<R2-{B2&AIfHU%eF|yjDouq;f>xPVKzjU#?XN=H(A$`5Ta?JG%e?

literal 0
HcmV?d00001

diff --git a/src/components/turbo-mode/turbo-mode.css b/src/components/turbo-mode/turbo-mode.css
new file mode 100644
index 000000000..d12699cee
--- /dev/null
+++ b/src/components/turbo-mode/turbo-mode.css
@@ -0,0 +1,20 @@
+@import "../../css/colors.css";
+
+.turbo-container {
+    display: flex;
+    align-items: center;
+    padding: 0.25rem;
+    user-select: none;
+}
+
+.turbo-icon {
+    margin: 0.25rem;
+}
+
+.turbo-label {
+    font-size: 0.625rem;
+    font-weight: bold;
+    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+    color: $control-primary;
+    white-space: nowrap;
+}
diff --git a/src/components/turbo-mode/turbo-mode.jsx b/src/components/turbo-mode/turbo-mode.jsx
new file mode 100644
index 000000000..724af24d1
--- /dev/null
+++ b/src/components/turbo-mode/turbo-mode.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import {FormattedMessage} from 'react-intl';
+
+import turboIcon from './icon--turbo.svg';
+
+import styles from './turbo-mode.css';
+
+const TurboMode = () => (
+    <div className={styles.turboContainer}>
+        <img
+            className={styles.turboIcon}
+            src={turboIcon}
+        />
+        <div className={styles.turboLabel}>
+            <FormattedMessage
+                defaultMessage="Turbo Mode"
+                description="Label indicating turbo mode is active"
+                id="controls.turboMode"
+            />
+        </div>
+    </div>
+);
+
+export default TurboMode;
diff --git a/src/containers/controls.jsx b/src/containers/controls.jsx
new file mode 100644
index 000000000..18dd44e5e
--- /dev/null
+++ b/src/containers/controls.jsx
@@ -0,0 +1,70 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+import VM from 'scratch-vm';
+
+import ControlsComponent from '../components/controls/controls.jsx';
+
+class Controls extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+            'handleGreenFlagClick',
+            'handleStopAllClick',
+            'onProjectRunStart',
+            'onProjectRunStop'
+        ]);
+        this.state = {
+            projectRunning: false,
+            turbo: 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});
+    }
+    handleGreenFlagClick (e) {
+        e.preventDefault();
+        if (e.shiftKey) {
+            this.setState({turbo: !this.state.turbo});
+            this.props.vm.setTurboMode(!this.state.turbo);
+        } else {
+            this.props.vm.greenFlag();
+        }
+    }
+    handleStopAllClick (e) {
+        e.preventDefault();
+        this.props.vm.stopAll();
+    }
+    render () {
+        const {
+            vm, // eslint-disable-line no-unused-vars
+            ...props
+        } = this.props;
+        return (
+            <ControlsComponent
+                {...props}
+                active={this.state.projectRunning}
+                turbo={this.state.turbo}
+                onGreenFlagClick={this.handleGreenFlagClick}
+                onStopAllClick={this.handleStopAllClick}
+            />
+        );
+    }
+}
+
+Controls.propTypes = {
+    vm: PropTypes.instanceOf(VM)
+};
+
+export default Controls;
diff --git a/src/containers/green-flag.jsx b/src/containers/green-flag.jsx
index b1f6acabf..3ca971fe4 100644
--- a/src/containers/green-flag.jsx
+++ b/src/containers/green-flag.jsx
@@ -11,8 +11,6 @@ class GreenFlag extends React.Component {
         super(props);
         bindAll(this, [
             'handleClick',
-            'handleKeyDown',
-            'handleKeyUp',
             'onProjectRunStart',
             'onProjectRunStop'
         ]);
@@ -21,14 +19,10 @@ class GreenFlag extends React.Component {
     componentDidMount () {
         this.props.vm.addListener('PROJECT_RUN_START', this.onProjectRunStart);
         this.props.vm.addListener('PROJECT_RUN_STOP', this.onProjectRunStop);
-        document.addEventListener('keydown', this.handleKeyDown);
-        document.addEventListener('keyup', this.handleKeyUp);
     }
     componentWillUnmount () {
         this.props.vm.removeListener('PROJECT_RUN_START', this.onProjectRunStart);
         this.props.vm.removeListener('PROJECT_RUN_STOP', this.onProjectRunStop);
-        document.removeEventListener('keydown', this.handleKeyDown);
-        document.removeEventListener('keyup', this.handleKeyUp);
     }
     onProjectRunStart () {
         this.setState({projectRunning: true});
@@ -36,15 +30,9 @@ class GreenFlag extends React.Component {
     onProjectRunStop () {
         this.setState({projectRunning: false});
     }
-    handleKeyDown (e) {
-        this.setState({shiftKeyDown: e.shiftKey});
-    }
-    handleKeyUp (e) {
-        this.setState({shiftKeyDown: e.shiftKey});
-    }
     handleClick (e) {
         e.preventDefault();
-        if (this.state.shiftKeyDown) {
+        if (e.shiftKey) {
             this.props.vm.setTurboMode(!this.props.vm.runtime.turboMode);
         } else {
             this.props.vm.greenFlag();
diff --git a/src/css/colors.css b/src/css/colors.css
index ea3f5936f..d524795cd 100644
--- a/src/css/colors.css
+++ b/src/css/colors.css
@@ -14,4 +14,6 @@ $red-tertiary: #E64D00;
 $sound-primary: #CF63CF;
 $sound-tertiary: #A63FA6;
 
+$control-primary: #FFAB19;
+
 $form-border: #E9EEF2;
diff --git a/src/examples/blocks-only.css b/src/examples/blocks-only.css
index 971361388..0f3ac07f8 100644
--- a/src/examples/blocks-only.css
+++ b/src/examples/blocks-only.css
@@ -1,11 +1,4 @@
-.green-flag {
-    position: absolute;
-    z-index: 2;
-    top: 10px;
-    right: 50px;
-}
-
-.stop-all {
+.controls {
     position: absolute;
     z-index: 2;
     top: 10px;
diff --git a/src/examples/blocks-only.jsx b/src/examples/blocks-only.jsx
index c97444d9e..8d5159e82 100644
--- a/src/examples/blocks-only.jsx
+++ b/src/examples/blocks-only.jsx
@@ -3,8 +3,7 @@ import ReactDOM from 'react-dom';
 import {connect} from 'react-redux';
 
 import AppStateHOC from '../lib/app-state-hoc.jsx';
-import GreenFlag from '../containers/green-flag.jsx';
-import StopAll from '../containers/stop-all.jsx';
+import Controls from '../containers/controls.jsx';
 import Blocks from '../containers/blocks.jsx';
 import GUI from '../containers/gui.jsx';
 import ProjectLoaderHOC from '../lib/project-loader-hoc.jsx';
@@ -14,8 +13,7 @@ import styles from './blocks-only.css';
 const mapStateToProps = state => ({vm: state.vm});
 
 const VMBlocks = connect(mapStateToProps)(Blocks);
-const VMGreenFlag = connect(mapStateToProps)(GreenFlag);
-const VMStopAll = connect(mapStateToProps)(StopAll);
+const VMControls = connect(mapStateToProps)(Controls);
 
 const BlocksOnly = props => (
     <GUI {...props}>
@@ -25,8 +23,7 @@ const BlocksOnly = props => (
                 media: `static/blocks-media/`
             }}
         />
-        <VMGreenFlag className={styles.greenFlag} />
-        <VMStopAll className={styles.stopAll} />
+        <VMControls className={styles.controls} />
     </GUI>
 );
 
diff --git a/src/examples/player.jsx b/src/examples/player.jsx
index 56c7a7360..ab50527cd 100644
--- a/src/examples/player.jsx
+++ b/src/examples/player.jsx
@@ -3,8 +3,7 @@ import ReactDOM from 'react-dom';
 import {connect} from 'react-redux';
 
 import AppStateHOC from '../lib/app-state-hoc.jsx';
-import GreenFlag from '../containers/green-flag.jsx';
-import StopAll from '../containers/stop-all.jsx';
+import Controls from '../containers/controls.jsx';
 import Stage from '../containers/stage.jsx';
 import Box from '../components/box/box.jsx';
 import GUI from '../containers/gui.jsx';
@@ -15,8 +14,7 @@ import './player.css';
 const mapStateToProps = state => ({vm: state.vm});
 
 const VMStage = connect(mapStateToProps)(Stage);
-const VMGreenFlag = connect(mapStateToProps)(GreenFlag);
-const VMStopAll = connect(mapStateToProps)(StopAll);
+const VMControls = connect(mapStateToProps)(Controls);
 
 class Player extends React.Component {
     constructor (props) {
@@ -55,17 +53,12 @@ class Player extends React.Component {
                 width={width}
             >
                 <Box height={40}>
-                    <VMGreenFlag
+                    <VMControls
                         style={{
                             marginRight: 10,
                             height: 40
                         }}
                     />
-                    <VMStopAll
-                        style={{
-                            height: 40
-                        }}
-                    />
                 </Box>
                 <VMStage
                     height={height}
diff --git a/test/unit/components/controls.test.jsx b/test/unit/components/controls.test.jsx
new file mode 100644
index 000000000..fcf22208d
--- /dev/null
+++ b/test/unit/components/controls.test.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import {shallowWithIntl} from '../../helpers/intl-helpers.jsx';
+import Controls from '../../../src/components/controls/controls';
+import TurboMode from '../../../src/components/turbo-mode/turbo-mode';
+
+describe('Controls component', () => {
+    const defaultProps = () => ({
+        active: false,
+        greenFlagTitle: 'Go',
+        onGreenFlagClick: jest.fn(),
+        onStopAllClick: jest.fn(),
+        stopAllTitle: 'Stop',
+        turbo: false
+    });
+
+    test('shows turbo mode when in turbo mode', () => {
+        const component = shallowWithIntl(
+            <Controls
+                {...defaultProps()}
+            />
+        );
+        expect(component.find(TurboMode).exists()).toEqual(false);
+        component.setProps({turbo: true});
+        expect(component.find(TurboMode).exists()).toEqual(true);
+    });
+
+    test('triggers the right callbacks when clicked', () => {
+        const props = defaultProps();
+        const component = shallowWithIntl(
+            <Controls
+                {...props}
+            />
+        );
+        component.find('[title="Go"]').simulate('click');
+        expect(props.onGreenFlagClick).toHaveBeenCalled();
+
+        component.find('[title="Stop"]').simulate('click');
+        expect(props.onStopAllClick).toHaveBeenCalled();
+    });
+});
-- 
GitLab