From bd0e3295687cd3fc932bc6c73fd139e50027a6fc Mon Sep 17 00:00:00 2001
From: Paul Kaplan <pkaplan@media.mit.edu>
Date: Wed, 1 Aug 2018 15:41:10 -0400
Subject: [PATCH] Add direction popover with dial and rotation style changing

---
 src/components/direction-picker/dial.css      |  40 +++++
 src/components/direction-picker/dial.jsx      | 156 ++++++++++++++++++
 .../direction-picker/direction-picker.css     |  31 ++++
 .../direction-picker/direction-picker.jsx     | 142 ++++++++++++++++
 .../direction-picker/icon--all-around.svg     | Bin 0 -> 1310 bytes
 .../direction-picker/icon--dial.svg           | Bin 0 -> 2117 bytes
 .../direction-picker/icon--dont-rotate.svg    | Bin 0 -> 2157 bytes
 .../direction-picker/icon--handle.svg         | Bin 0 -> 508 bytes
 .../direction-picker/icon--left-right.svg     | Bin 0 -> 3772 bytes
 src/components/sprite-info/sprite-info.jsx    |  33 ++--
 .../sprite-selector/sprite-selector.jsx       |   4 +
 src/components/target-pane/target-pane.jsx    |   3 +
 src/containers/direction-picker.jsx           |  62 +++++++
 src/containers/target-pane.jsx                |   5 +
 14 files changed, 455 insertions(+), 21 deletions(-)
 create mode 100644 src/components/direction-picker/dial.css
 create mode 100644 src/components/direction-picker/dial.jsx
 create mode 100644 src/components/direction-picker/direction-picker.css
 create mode 100644 src/components/direction-picker/direction-picker.jsx
 create mode 100644 src/components/direction-picker/icon--all-around.svg
 create mode 100644 src/components/direction-picker/icon--dial.svg
 create mode 100644 src/components/direction-picker/icon--dont-rotate.svg
 create mode 100644 src/components/direction-picker/icon--handle.svg
 create mode 100644 src/components/direction-picker/icon--left-right.svg
 create mode 100644 src/containers/direction-picker.jsx

diff --git a/src/components/direction-picker/dial.css b/src/components/direction-picker/dial.css
new file mode 100644
index 000000000..f8fac8b45
--- /dev/null
+++ b/src/components/direction-picker/dial.css
@@ -0,0 +1,40 @@
+@import "../../css/colors.css";
+
+.container {
+    padding: 1rem;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.dial-container {
+    position: relative;
+}
+
+.dial-face, .dial-handle, .gauge {
+    position: absolute;
+    top: 0;
+    left: 0;
+    overflow: visible;
+}
+
+.dial-face {
+    width: 100%;
+}
+
+$dial-size: 40px;
+
+.dial-handle {
+    cursor: pointer;
+    width: $dial-size;
+    height: $dial-size;
+    /* Use margin to make positioning via top/left easier */
+    margin-left: calc($dial-size / -2);
+    margin-top: calc($dial-size / -2);
+}
+
+.gauge-path {
+    fill: $motion-transparent;
+    stroke: $motion-primary;
+    stroke-width: 1px;
+}
diff --git a/src/components/direction-picker/dial.jsx b/src/components/direction-picker/dial.jsx
new file mode 100644
index 000000000..69d170537
--- /dev/null
+++ b/src/components/direction-picker/dial.jsx
@@ -0,0 +1,156 @@
+import PropTypes from 'prop-types';
+import bindAll from 'lodash.bindall';
+import React from 'react';
+import {getEventXY} from '../../lib/touch-utils';
+
+import styles from './dial.css';
+
+import dialFace from './icon--dial.svg';
+import dialHandle from './icon--handle.svg';
+
+class Dial extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+            'handleMouseDown',
+            'handleMouseMove',
+            'containerRef',
+            'handleRef',
+            'unbindMouseEvents'
+        ]);
+    }
+
+    componentDidMount () {
+        // Manually add touch/mouse handlers so that preventDefault can be used
+        // to prevent scrolling on touch.
+        // Tracked as a react issue https://github.com/facebook/react/issues/6436
+        this.handleElement.addEventListener('mousedown', this.handleMouseDown);
+        this.handleElement.addEventListener('touchstart', this.handleMouseDown);
+    }
+
+    componentWillUnmount () {
+        this.unbindMouseEvents();
+        this.handleElement.removeEventListener('mousedown', this.handleMouseDown);
+        this.handleElement.removeEventListener('touchstart', this.handleMouseDown);
+    }
+
+    /**
+     * Get direction from dial center to mouse move event.
+     * @param {Event} e - Mouse move event.
+     * @returns {number} Direction in degrees, clockwise, 90=horizontal.
+     */
+    directionToMouseEvent (e) {
+        const {x: mx, y: my} = getEventXY(e);
+        const bbox = this.containerElement.getBoundingClientRect();
+        const cy = bbox.top + (bbox.height / 2);
+        const cx = bbox.left + (bbox.width / 2);
+        const angle = Math.atan2(my - cy, mx - cx);
+        const degrees = angle * (180 / Math.PI);
+        return degrees + 90; // To correspond with scratch coordinate system
+    }
+
+    /**
+     * Create SVG path data string for the dial "gauge", the overlaid arc slice.
+     * @param {number} radius - The radius of the dial.
+     * @param {number} direction - Direction in degrees, clockwise, 90=horizontal.
+     * @returns {string} Path data string for the gauge.
+     */
+    gaugePath (radius, direction) {
+        const rads = (direction) * (Math.PI / 180)
+        const path = [];
+        path.push(`M ${radius} 0`);
+        path.push(`L ${radius} ${radius}`);
+        path.push(`L ${radius + radius * Math.sin(rads)} ${radius - radius * Math.cos(rads)}`);
+        path.push(`A ${radius} ${radius} 0 0 ${direction < 0 ? 1 : 0} ${radius} 0`);
+        path.push(`Z`);
+        return path.join(' ');
+    }
+
+    handleMouseMove (e) {
+        this.props.onChange(this.directionToMouseEvent(e) + this.directionOffset);
+        e.preventDefault();
+    }
+
+    unbindMouseEvents () {
+        window.removeEventListener('mousemove', this.handleMouseMove);
+        window.removeEventListener('mouseup', this.unbindMouseEvents);
+        window.removeEventListener('touchmove', this.handleMouseMove);
+        window.removeEventListener('touchend', this.unbindMouseEvents);
+    }
+
+    handleMouseDown (e) {
+        // Because the drag handle is not a single point, there is some initial
+        // difference between the current sprite direction and the direction to the mouse
+        // Store this offset to prevent jumping when the mouse is moved.
+        this.directionOffset = this.props.direction - this.directionToMouseEvent(e);
+        window.addEventListener('mousemove', this.handleMouseMove);
+        window.addEventListener('mouseup', this.unbindMouseEvents);
+        window.addEventListener('touchmove', this.handleMouseMove);
+        window.addEventListener('touchend', this.unbindMouseEvents);
+        e.preventDefault();
+    }
+
+    containerRef (el) {
+        this.containerElement = el;
+    }
+
+    handleRef (el) {
+        this.handleElement = el;
+    }
+
+    render () {
+        const {direction, radius} = this.props;
+        return (
+            <div className={styles.container}>
+                <div
+                    className={styles.dialContainer}
+                    style={{
+                        width: `${radius * 2}px`,
+                        height: `${radius * 2}px`,
+                    }}
+                    ref={this.containerRef}
+                >
+                    <img
+                        src={dialFace}
+                        className={styles.dialFace}
+                        draggable={false}
+                    />
+                    <svg
+                        width={radius * 2}
+                        height={radius * 2}
+                        className={styles.gauge}
+                    >
+                        <path
+                            className={styles.gaugePath}
+                            d={this.gaugePath(radius, direction)}
+                        />
+                    </svg>
+                    <img
+                        src={dialHandle}
+                        className={styles.dialHandle}
+                        draggable={false}
+                        style={{
+                            top: `${radius - radius * Math.cos(direction * (Math.PI / 180))}px`,
+                            left: `${radius + radius * Math.sin(direction * (Math.PI / 180))}px`,
+                            transform: `rotate(${direction}deg)`
+                        }}
+                        ref={this.handleRef}
+                    />
+                </div>
+            </div>
+        );
+    }
+}
+
+Dial.propTypes = {
+    direction: PropTypes.number,
+    onChange: PropTypes.func.isRequired,
+    radius: PropTypes.number
+};
+
+Dial.defaultProps = {
+    radius: 56, // px
+    direction: 90 // degrees
+};
+
+export default Dial;
diff --git a/src/components/direction-picker/direction-picker.css b/src/components/direction-picker/direction-picker.css
new file mode 100644
index 000000000..ed70f8ab4
--- /dev/null
+++ b/src/components/direction-picker/direction-picker.css
@@ -0,0 +1,31 @@
+@import "../../css/colors.css";
+
+.button-row {
+    display: flex;
+    flex-direction: row;
+    justify-content: center;
+
+}
+
+.icon-button {
+    margin: 0.25rem;
+    border: none;
+    background: none;
+    outline: none;
+    cursor: pointer;
+}
+
+.icon-button:active > img {
+    width: 20px;
+    height: 20px;
+    transform: scale(1.15);
+}
+
+.icon-button > img {
+    transition: transform 0.1s;
+    filter: grayscale(100%);
+}
+
+.icon-button.active > img {
+    filter: none;
+}
diff --git a/src/components/direction-picker/direction-picker.jsx b/src/components/direction-picker/direction-picker.jsx
new file mode 100644
index 000000000..36ba8ec6c
--- /dev/null
+++ b/src/components/direction-picker/direction-picker.jsx
@@ -0,0 +1,142 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import Popover from 'react-popover';
+import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
+
+import Label from '../forms/label.jsx';
+import Input from '../forms/input.jsx';
+import BufferedInputHOC from '../forms/buffered-input-hoc.jsx';
+import Dial from './dial.jsx';
+
+import styles from './direction-picker.css';
+
+import allAroundIcon from './icon--all-around.svg';
+import leftRightIcon from './icon--left-right.svg';
+import dontRotateIcon from './icon--dont-rotate.svg';
+
+const BufferedInput = BufferedInputHOC(Input);
+
+const directionLabel = (
+    <FormattedMessage
+        defaultMessage="Direction"
+        description="Sprite info direction label"
+        id="gui.SpriteInfo.direction"
+    />
+);
+
+const RotationStyles = {
+    ALL_AROUND: 'all around',
+    LEFT_RIGHT: 'left-right',
+    DONT_ROTATE: "don't rotate"
+};
+
+const messages = defineMessages({
+    allAround: {
+        id: 'gui.directionPicker.rotationStyles.allAround',
+        description: 'Button to change to the all around rotation style',
+        defaultMessage: 'All Around'
+    },
+    leftRight: {
+        id: 'gui.directionPicker.rotationStyles.leftRight',
+        description: 'Button to change to the left-right rotation style',
+        defaultMessage: 'Left/Right'
+    },
+    dontRotate: {
+        id: 'gui.directionPicker.rotationStyles.dontRotate',
+        description: 'Button to change to the dont rotate rotation style',
+        defaultMessage: 'Do not rotate'
+    }
+});
+
+const DirectionPicker = props => (
+    <Label
+        secondary
+        text={directionLabel}
+    >
+        <Popover
+            body={
+                <div>
+                    <Dial
+                        direction={props.direction}
+                        onChange={props.onChangeDirection}
+                    />
+                    <div className={styles.buttonRow}>
+                        <button
+                            className={classNames(styles.iconButton, {
+                                [styles.active]: props.rotationStyle === RotationStyles.ALL_AROUND
+                            })}
+                            title={props.intl.formatMessage(messages.allAround)}
+                            onClick={props.onClickAllAround}
+                        >
+                            <img
+                                draggable={false}
+                                src={allAroundIcon}
+                            />
+                        </button>
+                        <button
+                            className={classNames(styles.iconButton, {
+                                [styles.active]: props.rotationStyle === RotationStyles.LEFT_RIGHT
+                            })}
+                            title={props.intl.formatMessage(messages.leftRight)}
+                            onClick={props.onClickLeftRight}
+                        >
+                            <img
+                                draggable={false}
+                                src={leftRightIcon}
+                            />
+                        </button>
+                        <button
+                            className={classNames(styles.iconButton, {
+                                [styles.active]: props.rotationStyle === RotationStyles.DONT_ROTATE
+                            })}
+                            title={props.intl.formatMessage(messages.dontRotate)}
+                            onClick={props.onClickDontRotate}
+                        >
+                            <img
+                                draggable={false}
+                                src={dontRotateIcon}
+                            />
+                        </button>
+                    </div>
+                </div>
+            }
+            isOpen={props.popoverOpen}
+            preferPlace="above"
+            onOuterAction={props.onClosePopover}
+        >
+            <BufferedInput
+                small
+                disabled={props.disabled}
+                label={directionLabel}
+                tabIndex="0"
+                type="text"
+                value={props.disabled ? '' : props.direction}
+                onFocus={props.onOpenPopover}
+                onSubmit={props.onChangeDirection}
+            />
+        </Popover>
+    </Label>
+
+);
+
+DirectionPicker.propTypes = {
+    direction: PropTypes.number.isRequired,
+    disabled: PropTypes.bool.isRequired,
+    intl: intlShape,
+    onChangeDirection: PropTypes.func.isRequired,
+    onClickAllAround: PropTypes.func.isRequired,
+    onClickDontRotate: PropTypes.func.isRequired,
+    onClickLeftRight: PropTypes.func.isRequired,
+    onClosePopover: PropTypes.func.isRequired,
+    onOpenPopover: PropTypes.func.isRequired,
+    popoverOpen: PropTypes.bool.isRequired,
+    rotationStyle: PropTypes.string
+};
+
+const WrappedDirectionPicker = injectIntl(DirectionPicker);
+
+export {
+    WrappedDirectionPicker as default,
+    RotationStyles
+};
diff --git a/src/components/direction-picker/icon--all-around.svg b/src/components/direction-picker/icon--all-around.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2412c0b2327c3db488418d978ad382fa72335702
GIT binary patch
literal 1310
zcmZux+iu%141M2MXx_^P#Ih(+lvE@sFazC7_Oi<dWU-nUb?w4&lYIW@#936Vfq}?J
zM~BoQ>G}QXxe<rh@9VBzWFS>0V!P?8y4^0a-@hJmpWQD%%=d?_IM&s$TVz_joHDVC
z^>#O0-wt&=e(g?+ObI3Q_++UlWP-!A`$e`JhL^jdI3ADkXk^!K3$2tYSbn11otnCR
zdXESpgyOuLEk6kU%|GV3_!isPuZOO`6F;Bgu-OR<62xcgtb#8h7fHlF-7Y@Y>*;!O
z(>)ja%b7f%B!_xv;&R<I`MU33+bUmghWZfa#bx6Xw~G7Cvh3q}h!wjs?55CiUYxhC
ztjGOwMELK5Y(-ryvj4<0u^;;GDK4_MYvc7TpX_Gu9_yyLZjS4`e{Gl@53%j4YI5W3
z$jh3O;TYe6q<5a!vl%E!+JMnPtN?kff+L1e0vdw|qU5zPT6xqXOv<3qR)CR2-YIn^
zC#azS%G*?81?;-kKo-(~B?tEkz!_Fn5{wFHQH*4Zn$XOGMCUbu*CXmsI}A2OSDc2@
z<iJ4+m!PHdif?9SP9j>qA&e5rx<KS+mJQHowR6JD;1puO6f8+PFy0ZTmT>XkF_v|p
zIfStyLBmx{lnKh&XyyjCEzxB&6?sHTQn+Cy-`!hKbg_M6vcBSC*>J7}dL*(|yhV8u
zjMgMr;YPO7YC|d*Cf{_e45)nYK26~UmR1rw&hg9)ZZN+XsmZdFMvO(83>2lAktA=~
zHIAPg#zY^vqqO8wJEv&I<>sCu<DB(;7{P<#tVcAc;3-gA*A;iJwq6<g1}?{jMzUJD
wlnqXDixYr<(_Zg2e|8?eAKk!HdmX7ySO)j-knxy~lQWHJvE>im_(@v+1(@zVI{*Lx

literal 0
HcmV?d00001

diff --git a/src/components/direction-picker/icon--dial.svg b/src/components/direction-picker/icon--dial.svg
new file mode 100644
index 0000000000000000000000000000000000000000..d4aa8ed8205a7d51ee4c78086e409c2ef3248ff5
GIT binary patch
literal 2117
zcma)7U2mH(6#XlRz9z;7Y(h#Rb<f)$*vo_nN!Tbvfi;b){`cLls%|BxQ`6YF_#U5o
z?y;$+r$f~3Hu3lR+Ma)zINH^7y&UStR$>vvo*(;R+Qi3sKCP4Fa=EBWrpEIjNwwAq
zK*Z6d+s(&KY)l$Q$F@5hXZi8ewU=+>^Cs4jj^Hu=$J=Vx?x$@v&DXx&s-~Zo=4Cw9
zO*dcHTBY{>m4fNZzU%w-UEY+%elNjHg4wPpn?{1Umu}wDEw3ELp}n8x^Z2J--vMKp
zFZ*t2oBFgqkAH`qAN)ObgG67egh^HjQ=YpSDDS$uU+(Lstr8AYhp6f62~?tDpu(m*
zH+}0zL7E1Hunxv6k7pRHvT?GlPW62BS7*?RP5i^Ug?X@c0Uy0E^amkG(r@3I9^kr1
zy)-pGbrjM#vF_n5f(!A{Puwm!Y(H|&fio)NbFk2R!%VL*np~}b{BtTH7*nl^4Dr55
zEsaWxkEvy$OnxIZw<-eyw6RmEY@9o_0-!7IZB%&+z*%KLMFc1{=*8PRryRlpKxtKR
z3m~&97XYX;6aLwXro;dixEm!DnQ{WKa?0k+pn$2A0J6Y>uzaagB^*F3L8d5vW~dru
zz|%0mN~1;uAX5ZbfqhEWfTv*qDI6Jy90-t7;Upr<5eAUL5kTZX04|&WSdK7&;pQWN
zSb`W(^9g`05eAUvBY;?f7*O*GfGrUQ5Z@yJ9$wm>d{5aB3a5(i{Z#h-*Qs9J;@MAi
zb1^seX48{!d}=<`&2okTyxxmviNSmCxuIbIulI6lhyj_N1Yk>r0X+YUXA!{jKbtxX
z;Q3#&5Ww?4?+^y??f~&DF?hcNxW+JmcL&I+K>+U#V3orF-aW_{78%gH2if0Y0Ph~;
ZE0X}cdyt(50Qge)-}hFEuE&@E_8(c3CPn}N

literal 0
HcmV?d00001

diff --git a/src/components/direction-picker/icon--dont-rotate.svg b/src/components/direction-picker/icon--dont-rotate.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4796c03af2562424f84487a4d8522ea16f20537c
GIT binary patch
literal 2157
zcmZuz!EWO=5WVkLu<E4+Y??Ei;gCU`1?nQbWKY{(_U4VXwh_k)Y&FT}_fd9g1smHy
zq^HrF;hQ&Pu0Ov%?%Z>GIBvWBydfVN*Y5AT)poz0H~;*7JI&4Kn@`ur=e2v;uKLZq
z;qdg@xJ|oVZ~C{p=WYA)NB25!f(wr8uNjk4<B(>5oHv`kf4Z8@US3}OOZ44gJ>w9<
z4D{!atJmFj|L{>Dl~QKI)8^)r!{_>!>D2wz?%Tt%?+#b)>qFb$Z=8}R_nVpq`rS?4
zDC75T(>`vO=jHkP?s0ZJy)&JjDf?}|Yj0NFzMmete%ZIv<$b?>Zm(yjC#S@#_IQ7@
zIJ9`af-L>!oZDZ|hDUEvKaMvQ!JiYdcH7mw`I)_O$NtbgwDV@)?c2Az=~-~7_hY-;
zy*;eg)5E_zM7PiFzFV!%dJLL8El~}Ud?EIlb2g(%_92zPNyObz^f^T-ljJpVN^0U3
zBwi$&gfuZPAx1Ju4ru1c(G(042;WjFT0+UGq)G8&nG<COZ}O};M12P@0B1?c!N$>K
z9^Fhtc|yTc3`JC2jkTQcq@sx<MJ`|#T!&%_uy~RfMQErwMh;oH!YM?FtPaadDO(6b
z@Cc2mP{=x_xFa})nUc~t;R4BXAhHPNQFRDbfr#<lO30hANzpM{RfgV15Pu}n0zD~E
zTsc9}0*!?UHM7qoWXx6PQdlipML+1@H}|DBATpPjCefRcgiMeLEu<Vn%4q^2$t-X#
zZXrIF5|Rnl2)IDUBo1h1h#Ig&G!8vxZ~#qsFUbH()<W6c!7!Opi8R1?tr$Un<c3k0
z2o&aVX$F5q5L*U}&H_y#p!q;g^<<!E@`=tZsv3z|b%4)w4k${Fl4A`%(G3(IEDx(T
zLlO&Eo_HoJ4DC={GL&f<HbC;I%vKx-$)`vOE5l7e3xY_&07Q%f&;(tRQ@kvYSmQ)o
z?b8s1>*@@)SX2ae;F27W#wO4nViNjQ+a{PR;y6(x#_;5<gNfmGw5OnHZ(<B4W@u3B
zyogap3<LzYqGvHq<2Z<4Db8YmLOskH;BUZ;Cd4AqRTPUB1E``{LZM-Fe|8QmmEfIN
z$P{uZOpq)<p*w>nxRTnox@}<Ez_P%0f>}zMVTkBU(W3B4_Q;1F0F!|F;X}a|a#8Ps
zEie&TL2#Pfb0nG(Fz;H1aTaVEN!7haaR3Yr0>dR(kvFDLB7!fx>I>$gpdq6YE?`{)
zY=?~w)m~t1OBu`tqo_An5S?@%i&F=YJPT$o@vxa@IH7tECedP%AP)wPOE6YKahYO*
zsTsb=#-ixesJIchNZts(ha1PS*p1JxVUsrE98y%w?EmP<&m$C722J>87@M(iM}@Ir
z(;x}9DL}F1P*Fyg*&Ap{BOLqT>b^C@5w_?azi;>LYWj7ve8PcsI-h=(Ka1VoHaPU^
RF?2rkW^4TaP*3=q{{U_z=wSc=

literal 0
HcmV?d00001

diff --git a/src/components/direction-picker/icon--handle.svg b/src/components/direction-picker/icon--handle.svg
new file mode 100644
index 0000000000000000000000000000000000000000..8e5fee6e0ba3c7edd111b8a52e8fd58cb6239ddd
GIT binary patch
literal 508
zcmZuuU2B6d6#Xk9dyO|A8nvAY8yobY`vZF^A;ua=KhRJsW&eFqJJyGiOHMfVaPCP`
z^p`!db&0oXG~JG4R4cEdqdJ&7hXQv!xMN@9*8B6C(qR~2h~d=jsbGxJV1dzKYu}bQ
zVHmZ>?pwc{FV+n2r)!BBVknskS4C}_zAAb@I#YpmeZ<KO;?`K_)-Q=(Wli(?v-EDK
zv(na&YX)52$@s2pCRsrjtAZ}Z-uht7`0m3hhd!M-n4|9ty6}azopuIkr}}WGc|1ls
zL=+*r9vAFppw$z#c&g?p4(hY=?W1!X-m}DCf`N<)7my@Q68Q#M5{j^}I1!TY9MXIv
z1jN}^1SW+%RUBAO=55wR0(eZAi#WtdlmpK<T&hQ>2&4#IkvbVoGqO;BGyVg7PdTSQ
Gt9}7H#*Oy?

literal 0
HcmV?d00001

diff --git a/src/components/direction-picker/icon--left-right.svg b/src/components/direction-picker/icon--left-right.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4525bd6e917188b4f348b4215d4f00a15955f78d
GIT binary patch
literal 3772
zcmai1OLN;c5WeSEpv)zi5g|MXfXqnFOw~FU=hC#7+!TqnIFTif#M<t^?}sN>vD0!q
zW8x#Qk8c;dtHsCX`#pQ?Pv_xySXPpYiuH%haXTFDmepT>Ue|5)@#@Xu{CLNnhV8gp
zR!TfPS8Uf0ce^pK9*6$v*W>fD5==1V|0*g}Rt#zm=Vi4U$A=H|`P0)Af71MTx|=H@
z#2n(&mJiSS;qd*njk4>ydH7mgy<vD3ztlDR)F1lOdOV&!urJ^HakFDaa>?EqXM}vu
zYDP4EJMQ}XVV&ILo8$fb9M9Bs5;+dzzQ5Y{w_|<s4%X|<I6U@?dHfLdZu|4*YIW+@
zW4{F}<1VS@i+T8zcio<^Jc54_xnsk2S-lWmvGaI3e(#sn;dtots!nu+yW3&E&maAJ
zefoQk-Th;KIBvIzjlf9MaJ?TM>KpIFAKr(Gji>eDe0x0IFRSpnhvn}S2lIGltVwV0
ziKrm%VGZA4^5=$I+Zc^!rg$T5C#-eshQJ-S!m5U?2qpy=tpW%IZ5xjR(@NmHW12f7
z)vV)PtBr$fb|bhnHf+>0%XQmEMXMT;jBKob9KJ}`#S@nw3~w7R8^Wn1!>v}y<fIhN
z>o%G@nZ}(+r>PUX(@7aYCl;y@)N3D>5~{b&Z=Jio{weWVD>yl0mJ2C5)xBru&3fOz
ztL2O#X{Q*~is!xcw7EiB`Ow6)@{#h-qD0H_))`Nhn@I_htpmGk<=~%^A&3Na+%yx4
z$Vwj@#~bkCj~mkl1jn_Rq#Dr#sX&R)M(L)(VPz+gy73r}D{X^BW%E&$bjUUxOKU_S
z>&#mKB*UT@!8;lKqS^#+^<-`_OcWBR6M|cpMMQ6b1P5TO>9VhaH%TUVg<+9Eq_K+@
zwOkegu{ViawOm0~41(Y$84Hw%$lSJs+G0bSLNJ*Lr0_bq)N&&xMa6%>%13Y_3W$jj
z$Yc^EGd@8vC|Qg|Ok>!96n&DX=u*=pk7)o*O=*x5hWbjf3&f=K$SK%zVY6RT7Qdd`
zU)g7*fzr78NP5kcbFn`7w9ByKooI095nO2-6f%p;7m5Xhww<p)zPuIGhp>4G2ZsFd
zC@n3i&H`DT%pj69@0w^u(U~EicHXkQCbnyouq5jOEskC?fI|2h0|ucZ3>dCnVSu2N
z0Rkugvb3_CbSB7p0%ITu0~QtM2L>{f8OUqmE0IM|;^-v<NWLybm*xO2t1LVQvJrdr
zgEbkV?q@7{M&uM^QdpqYBpZnP9eP|bq2vHTGYJGD9=E&*y+Dihz{Au4&_JXJ^%p%e
z)uV*bhQPISE?ov+1Y_YswMvJlCJ`=0+S!yo!)1pI#SJR017%T1#Z%wl!WAv0Z=gbT
z0S$epw2n}?g2>RjLV57@oow~(A{4^34TMAp(+W8mG1Gc;u*KII6zbp^mbs(?lF3<O
zT4ay34Uzqyx+G?e<b1HSv<?g{K_YS|7yU4=7^^@p&Xd*=1{_ZpG9}aR$EVH*CGIVq
z1NuTtNh!TB0lA;!f;JW78nlJ_pIr}0>X{&96%0c|A`p=8Bb0eSxdU7Ti8KKkQ`!fn
z1R%y^V2`Ond=LF5m;jchMo=>MMS_2ggV^eL|7|$*+xp9H{eWRQ%*)4z^=26V!Os(}
zOrEFT;uiky%>NBj7=fhlQ>qmvUXYsvg&8oKxJr=p!y=-HrFC7L6#P^fcS%g0GDH*I
z16US^`BcvQkcyGQhk7MZ)?_^HM_L(Sf?@)0v(Rwb56dA{FqIQEVYc>JMGXoQxVOlg
z>3s`xx>w>dGI>kJTV0s@&+%4j3`~vjKP<w?syqAh`Ic;s18&&;GA4`pUHTE_Ul#rc
G<JG@6pFRHo

literal 0
HcmV?d00001

diff --git a/src/components/sprite-info/sprite-info.jsx b/src/components/sprite-info/sprite-info.jsx
index 37e18735a..b46b889a4 100644
--- a/src/components/sprite-info/sprite-info.jsx
+++ b/src/components/sprite-info/sprite-info.jsx
@@ -6,6 +6,8 @@ import Box from '../box/box.jsx';
 import Label from '../forms/label.jsx';
 import Input from '../forms/input.jsx';
 import BufferedInputHOC from '../forms/buffered-input-hoc.jsx';
+import DirectionPicker from '../../containers/direction-picker.jsx';
+
 import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
 
 import {STAGE_DISPLAY_SIZES} from '../../lib/layout-constants.js';
@@ -29,6 +31,7 @@ const messages = defineMessages({
 class SpriteInfo extends React.Component {
     shouldComponentUpdate (nextProps) {
         return (
+            this.props.rotationStyle !== nextProps.rotationStyle ||
             this.props.direction !== nextProps.direction ||
             this.props.disabled !== nextProps.disabled ||
             this.props.name !== nextProps.name ||
@@ -65,13 +68,6 @@ class SpriteInfo extends React.Component {
                 id="gui.SpriteInfo.size"
             />
         );
-        const directionLabel = (
-            <FormattedMessage
-                defaultMessage="Direction"
-                description="Sprite info direction label"
-                id="gui.SpriteInfo.direction"
-            />
-        );
 
         const spriteNameInput = (
             <BufferedInput
@@ -234,20 +230,13 @@ class SpriteInfo extends React.Component {
                         </Label>
                     </div>
                     <div className={classNames(styles.group, styles.largerInput)}>
-                        <Label
-                            secondary
-                            text={directionLabel}
-                        >
-                            <BufferedInput
-                                small
-                                disabled={this.props.disabled}
-                                label={directionLabel}
-                                tabIndex="0"
-                                type="text"
-                                value={this.props.disabled ? '' : this.props.direction}
-                                onSubmit={this.props.onChangeDirection}
-                            />
-                        </Label>
+                        <DirectionPicker
+                            direction={this.props.direction}
+                            disabled={this.props.disabled}
+                            rotationStyle={this.props.rotationStyle}
+                            onChangeDirection={this.props.onChangeDirection}
+                            onChangeRotationStyle={this.props.onChangeRotationStyle}
+                        />
                     </div>
                 </div>
             </Box>
@@ -265,6 +254,7 @@ SpriteInfo.propTypes = {
     name: PropTypes.string,
     onChangeDirection: PropTypes.func,
     onChangeName: PropTypes.func,
+    onChangeRotationStyle: PropTypes.func,
     onChangeSize: PropTypes.func,
     onChangeX: PropTypes.func,
     onChangeY: PropTypes.func,
@@ -272,6 +262,7 @@ SpriteInfo.propTypes = {
     onClickVisible: PropTypes.func,
     onPressNotVisible: PropTypes.func,
     onPressVisible: PropTypes.func,
+    rotationStyle: PropTypes.string,
     size: PropTypes.oneOfType([
         PropTypes.string,
         PropTypes.number
diff --git a/src/components/sprite-selector/sprite-selector.jsx b/src/components/sprite-selector/sprite-selector.jsx
index e6948c5b7..1ab064d38 100644
--- a/src/components/sprite-selector/sprite-selector.jsx
+++ b/src/components/sprite-selector/sprite-selector.jsx
@@ -47,6 +47,7 @@ const SpriteSelectorComponent = function (props) {
         intl,
         onChangeSpriteDirection,
         onChangeSpriteName,
+        onChangeSpriteRotationStyle,
         onChangeSpriteSize,
         onChangeSpriteVisibility,
         onChangeSpriteX,
@@ -84,6 +85,7 @@ const SpriteSelectorComponent = function (props) {
                 direction={selectedSprite.direction}
                 disabled={spriteInfoDisabled}
                 name={selectedSprite.name}
+                rotationStyle={selectedSprite.rotationStyle}
                 size={selectedSprite.size}
                 stageSize={stageSize}
                 visible={selectedSprite.visible}
@@ -91,6 +93,7 @@ const SpriteSelectorComponent = function (props) {
                 y={selectedSprite.y}
                 onChangeDirection={onChangeSpriteDirection}
                 onChangeName={onChangeSpriteName}
+                onChangeRotationStyle={onChangeSpriteRotationStyle}
                 onChangeSize={onChangeSpriteSize}
                 onChangeVisibility={onChangeSpriteVisibility}
                 onChangeX={onChangeSpriteX}
@@ -152,6 +155,7 @@ SpriteSelectorComponent.propTypes = {
     intl: intlShape.isRequired,
     onChangeSpriteDirection: PropTypes.func,
     onChangeSpriteName: PropTypes.func,
+    onChangeSpriteRotationStyle: PropTypes.func,
     onChangeSpriteSize: PropTypes.func,
     onChangeSpriteVisibility: PropTypes.func,
     onChangeSpriteX: PropTypes.func,
diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx
index c620c8504..565c9881c 100644
--- a/src/components/target-pane/target-pane.jsx
+++ b/src/components/target-pane/target-pane.jsx
@@ -23,6 +23,7 @@ const TargetPane = ({
     spriteLibraryVisible,
     onChangeSpriteDirection,
     onChangeSpriteName,
+    onChangeSpriteRotationStyle,
     onChangeSpriteSize,
     onChangeSpriteVisibility,
     onChangeSpriteX,
@@ -60,6 +61,7 @@ const TargetPane = ({
             stageSize={stageSize}
             onChangeSpriteDirection={onChangeSpriteDirection}
             onChangeSpriteName={onChangeSpriteName}
+            onChangeSpriteRotationStyle={onChangeSpriteRotationStyle}
             onChangeSpriteSize={onChangeSpriteSize}
             onChangeSpriteVisibility={onChangeSpriteVisibility}
             onChangeSpriteX={onChangeSpriteX}
@@ -128,6 +130,7 @@ TargetPane.propTypes = {
     }),
     onChangeSpriteDirection: PropTypes.func,
     onChangeSpriteName: PropTypes.func,
+    onChangeSpriteRotationStyle: PropTypes.func,
     onChangeSpriteSize: PropTypes.func,
     onChangeSpriteVisibility: PropTypes.func,
     onChangeSpriteX: PropTypes.func,
diff --git a/src/containers/direction-picker.jsx b/src/containers/direction-picker.jsx
new file mode 100644
index 000000000..76fc9bdb9
--- /dev/null
+++ b/src/containers/direction-picker.jsx
@@ -0,0 +1,62 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import DirectionComponent, {RotationStyle} from '../components/direction-picker/direction-picker.jsx';
+
+class DirectionPicker extends React.Component {
+    constructor (props) {
+        super(props);
+        bindAll(this, [
+            'handleOpenPopover',
+            'handleClosePopover',
+            'handleClickLeftRight',
+            'handleClickDontRotate',
+            'handleClickAllAround'
+        ]);
+        this.state = {
+            popoverOpen: false
+        };
+    }
+    handleOpenPopover () {
+        this.setState({popoverOpen: true});
+    }
+    handleClosePopover () {
+        this.setState({popoverOpen: false});
+    }
+    handleClickAllAround () {
+        this.props.onChangeRotationStyle(RotationStyle.ALL_AROUND);
+    }
+    handleClickLeftRight () {
+        this.props.onChangeRotationStyle(RotationStyle.LEFT_RIGHT);
+    }
+    handleClickDontRotate () {
+        this.props.onChangeRotationStyle(RotationStyle.DONT_ROTATE);
+    }
+    render () {
+        return (
+            <DirectionComponent
+                direction={this.props.direction}
+                disabled={this.props.disabled}
+                popoverOpen={this.state.popoverOpen && !this.props.disabled}
+                rotationStyle={this.props.rotationStyle}
+                onChangeDirection={this.props.onChangeDirection}
+                onClickAllAround={this.handleClickAllAround}
+                onClickDontRotate={this.handleClickDontRotate}
+                onClickLeftRight={this.handleClickLeftRight}
+                onClosePopover={this.handleClosePopover}
+                onOpenPopover={this.handleOpenPopover}
+            />
+        );
+    }
+}
+
+DirectionPicker.propTypes = {
+    direction: PropTypes.number,
+    disabled: PropTypes.bool,
+    onChangeDirection: PropTypes.func,
+    onChangeRotationStyle: PropTypes.func,
+    rotationStyle: PropTypes.string
+};
+
+export default DirectionPicker;
diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx
index a116b3c07..d4a763b0e 100644
--- a/src/containers/target-pane.jsx
+++ b/src/containers/target-pane.jsx
@@ -20,6 +20,7 @@ class TargetPane extends React.Component {
         super(props);
         bindAll(this, [
             'handleBlockDragEnd',
+            'handleChangeSpriteRotationStyle',
             'handleChangeSpriteDirection',
             'handleChangeSpriteName',
             'handleChangeSpriteSize',
@@ -48,6 +49,9 @@ class TargetPane extends React.Component {
     handleChangeSpriteDirection (direction) {
         this.props.vm.postSpriteInfo({direction});
     }
+    handleChangeSpriteRotationStyle (rotationStyle) {
+        this.props.vm.postSpriteInfo({rotationStyle});
+    }
     handleChangeSpriteName (name) {
         this.props.vm.renameSprite(this.props.editingTarget, name);
     }
@@ -165,6 +169,7 @@ class TargetPane extends React.Component {
             <TargetPaneComponent
                 {...componentProps}
                 fileInputRef={this.setFileInput}
+                onChangeSpriteRotationStyle={this.handleChangeSpriteRotationStyle}
                 onChangeSpriteDirection={this.handleChangeSpriteDirection}
                 onChangeSpriteName={this.handleChangeSpriteName}
                 onChangeSpriteSize={this.handleChangeSpriteSize}
-- 
GitLab