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