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