From 688ee5c73c723d9499eebcdf8997b2eda9d65eea Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Thu, 22 Feb 2018 14:26:22 -0500 Subject: [PATCH 1/7] Add thermometer menus with new surprise options --- src/components/action-menu/action-menu.css | 174 ++++++++++++++++++ src/components/action-menu/action-menu.jsx | 142 ++++++++++++++ .../icon--backdrop.svg | 0 src/components/action-menu/icon--camera.svg | 12 ++ .../action-menu/icon--file-upload.svg | 12 ++ src/components/action-menu/icon--paint.svg | 12 ++ .../icon--sprite.svg | 0 src/components/action-menu/icon--surprise.svg | 12 ++ src/components/asset-button/asset-button.css | 21 --- src/components/asset-button/asset-button.jsx | 32 ---- src/components/asset-panel/selector.css | 19 +- src/components/asset-panel/selector.jsx | 16 +- src/components/green-flag/green-flag.css | 1 + src/components/gui/gui.css | 16 +- .../sprite-selector/sprite-selector.css | 25 +-- .../sprite-selector/sprite-selector.jsx | 60 +++++- .../stage-selector/stage-selector.jsx | 62 ++++++- src/components/target-pane/target-pane.jsx | 4 + src/containers/costume-tab.jsx | 94 +++++++--- src/containers/extension-library.jsx | 2 +- src/containers/sound-tab.jsx | 53 +++++- src/containers/stage-selector.jsx | 42 ++++- src/containers/target-pane.jsx | 25 ++- src/css/colors.css | 2 + 24 files changed, 713 insertions(+), 125 deletions(-) create mode 100644 src/components/action-menu/action-menu.css create mode 100644 src/components/action-menu/action-menu.jsx rename src/components/{stage-selector => action-menu}/icon--backdrop.svg (100%) create mode 100644 src/components/action-menu/icon--camera.svg create mode 100644 src/components/action-menu/icon--file-upload.svg create mode 100644 src/components/action-menu/icon--paint.svg rename src/components/{sprite-selector => action-menu}/icon--sprite.svg (100%) create mode 100644 src/components/action-menu/icon--surprise.svg delete mode 100644 src/components/asset-button/asset-button.css delete mode 100644 src/components/asset-button/asset-button.jsx diff --git a/src/components/action-menu/action-menu.css b/src/components/action-menu/action-menu.css new file mode 100644 index 00000000000..c222216b1a2 --- /dev/null +++ b/src/components/action-menu/action-menu.css @@ -0,0 +1,174 @@ +@import "../../css/colors.css"; + +$main-button-size: 2.75rem; +$more-button-size: 2.25rem; + +.menu-container { + display: flex; + flex-direction: column-reverse; + transition: 0.2s; + position: relative; +} + +.button { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + background: $motion-primary; + outline: none; + border: none; + transition: background-color 0.2s; +} + +.button:hover { + background: $pen-primary; +} + +.button.coming-soon:hover { + background: $data-primary; +} + +.main-button { + border-radius: 100%; + width: $main-button-size; + height: $main-button-size; + z-index: 20; /* TODO reorder layout to prevent z-index need */ +} + +.main-icon { + width: calc($main-button-size - 1rem); + height: calc($main-button-size - 1rem); +} + +.more-buttons-outer { + /* + Need to use two divs to set different overflow x/y + which is needed to get animation to look right while + allowing the tooltips to be visible. + */ + overflow-y: hidden; + + background: $motion-tertiary; + border-top-left-radius: $more-button-size; + border-top-right-radius: $more-button-size; + width: $more-button-size; + margin-left: calc(($main-button-size - $more-button-size) / 2); + + position: absolute; + bottom: calc($main-button-size); + + margin-bottom: calc($main-button-size / -2); + padding-bottom: calc($main-button-size / 2); +} + +.more-buttons { + max-height: 0; + transition: max-height 1s; + overflow-x: visible; + display: flex; + flex-direction: column; + z-index: 10; /* @todo justify */ +} + +.expanded .more-buttons { + max-height: 1000px; /* Arbitrary, needs to be a value in order for animation to run */ +} + +.force-hidden .more-buttons { + display: none; /* This property does not animate */ +} + +.more-buttons:first-child { /* Round off top button */ + border-top-right-radius: $more-button-size; + border-top-left-radius: $more-button-size; +} + +.more-button { + width: $more-button-size; + height: $more-button-size; + background: $motion-tertiary; +} + +.more-icon { + width: calc($more-button-size - 1rem); + height: calc($more-button-size - 1rem); +} + +.coming-soon .more-icon { + opacity: 0.5; +} + +/* + @todo needs to be refactored with coming soon tooltip overrides. + The "!important"s are for the same reason as with coming soon, the library + is not very easy to style. +*/ +.tooltip { + background-color: $pen-primary !important; + opacity: 1 !important; + border: 1px solid hsla(0, 0%, 0%, .1) !important; + box-shadow: 0 0 .5rem hsla(0, 0%, 0%, .25) !important; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; +} + +.tooltip:after { + background-color: $pen-primary; +} + +.coming-soon-tooltip { + background-color: $data-primary !important; +} + +.coming-soon-tooltip:after { + background-color: $data-primary !important; +} + +.tooltip { + border: 1px solid hsla(0, 0%, 0%, .1) !important; + border-radius: .25rem !important; + box-shadow: 0 0 .5rem hsla(0, 0%, 0%, .25) !important; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; + z-index: 100 !important; +} + +$arrow-size: 0.5rem; +$arrow-inset: -0.25rem; +$arrow-rounding: 0.125rem; + +.tooltip:after { + content: ""; + border-top: 1px solid hsla(0, 0%, 0%, .1) !important; + border-left: 0 !important; + border-bottom: 0 !important; + border-right: 1px solid hsla(0, 0%, 0%, .1) !important; + border-radius: $arrow-rounding; + height: $arrow-size !important; + width: $arrow-size !important; +} + +.tooltip:global(.place-left):after { + margin-top: $arrow-inset !important; + right: $arrow-inset !important; + transform: rotate(45deg) !important; +} + +.tooltip:global(.place-right):after { + margin-top: $arrow-inset !important; + left: $arrow-inset !important; + transform: rotate(-135deg) !important; +} + +.tooltip:global(.place-top):after { + margin-right: $arrow-inset !important; + bottom: $arrow-inset !important; + transform: rotate(135deg) !important; +} + +.tooltip:global(.place-bottom):after { + margin-left: $arrow-inset !important; + top: $arrow-inset !important; + transform: rotate(-45deg) !important; +} diff --git a/src/components/action-menu/action-menu.jsx b/src/components/action-menu/action-menu.jsx new file mode 100644 index 00000000000..750a0ea3e9a --- /dev/null +++ b/src/components/action-menu/action-menu.jsx @@ -0,0 +1,142 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import bindAll from 'lodash.bindall'; +import ReactTooltip from 'react-tooltip'; + +import styles from './action-menu.css'; + +const CLOSE_DELAY = 300; // ms + +class ActionMenu extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleClosePopover', + 'handleToggleOpenState', + 'clickDelayer' + ]); + this.state = { + isOpen: false, + forceHide: false + }; + } + handleClosePopover () { + this.closeTimeoutId = setTimeout(() => { + this.setState({isOpen: false}); + this.closeTimeoutId = null; + }, CLOSE_DELAY); + } + handleToggleOpenState () { + if (this.closeTimeoutId) { + clearTimeout(this.closeTimeoutId); + this.closeTimeoutId = null; + } else { + this.setState({ + isOpen: true, + forceHide: false + }); + } + } + clickDelayer (fn) { + // Return a wrapped action that manages the menu closing. + // @todo we may be able to use react-transition for this in the future + // for now all this work is to ensure the menu closes BEFORE the + // (possibly slow) action is started. + return event => { + this.setState({forceHide: true, isOpen: false}, () => { + if (fn) fn(event); + setTimeout(() => this.setState({forceHide: false})); + }); + }; + } + render () { + const { + className, + img: mainImg, + title: mainTitle, + moreButtons, + onClick + } = this.props; + + const mainTooltipId = `tooltip-${Math.random()}`; + + return ( +
+ + +
+
+ {(moreButtons || []).map(({img, title, onClick: handleClick}) => { + const isComingSoon = !handleClick; + const tooltipId = `tooltip-${Math.random()}`; + return ( +
+ + +
+ ); + })} +
+
+
+ ); + } +} + +ActionMenu.propTypes = { + className: PropTypes.string, + img: PropTypes.string, + moreButtons: PropTypes.arrayOf(PropTypes.shape({ + img: PropTypes.string, + title: PropTypes.node.isRequired, + onClick: PropTypes.func // Optional, "coming soon" if no callback provided + })), + onClick: PropTypes.func.isRequired, + title: PropTypes.node.isRequired +}; + +export default ActionMenu; diff --git a/src/components/stage-selector/icon--backdrop.svg b/src/components/action-menu/icon--backdrop.svg similarity index 100% rename from src/components/stage-selector/icon--backdrop.svg rename to src/components/action-menu/icon--backdrop.svg diff --git a/src/components/action-menu/icon--camera.svg b/src/components/action-menu/icon--camera.svg new file mode 100644 index 00000000000..e8c442d8f5d --- /dev/null +++ b/src/components/action-menu/icon--camera.svg @@ -0,0 +1,12 @@ + + + + camera + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/src/components/action-menu/icon--file-upload.svg b/src/components/action-menu/icon--file-upload.svg new file mode 100644 index 00000000000..57337d959c1 --- /dev/null +++ b/src/components/action-menu/icon--file-upload.svg @@ -0,0 +1,12 @@ + + + + file-upload + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/src/components/action-menu/icon--paint.svg b/src/components/action-menu/icon--paint.svg new file mode 100644 index 00000000000..f79d562c350 --- /dev/null +++ b/src/components/action-menu/icon--paint.svg @@ -0,0 +1,12 @@ + + + + paint + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/src/components/sprite-selector/icon--sprite.svg b/src/components/action-menu/icon--sprite.svg similarity index 100% rename from src/components/sprite-selector/icon--sprite.svg rename to src/components/action-menu/icon--sprite.svg diff --git a/src/components/action-menu/icon--surprise.svg b/src/components/action-menu/icon--surprise.svg new file mode 100644 index 00000000000..41655999f67 --- /dev/null +++ b/src/components/action-menu/icon--surprise.svg @@ -0,0 +1,12 @@ + + + + surprise + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/src/components/asset-button/asset-button.css b/src/components/asset-button/asset-button.css deleted file mode 100644 index b36fa76b8ec..00000000000 --- a/src/components/asset-button/asset-button.css +++ /dev/null @@ -1,21 +0,0 @@ -@import "../../css/colors.css"; - -.container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - cursor: pointer; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - background: $motion-primary; - border-radius: 100%; - width: 2.75rem; - height: 2.75rem; - outline: none; - border: none; -} - -.icon { - width: 1.75rem; - height: 1.75rem; -} diff --git a/src/components/asset-button/asset-button.jsx b/src/components/asset-button/asset-button.jsx deleted file mode 100644 index 08883899094..00000000000 --- a/src/components/asset-button/asset-button.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import classNames from 'classnames'; -import styles from './asset-button.css'; - -const AssetButton = ({ - img, - className, - title, - onClick -}) => ( - -); - -AssetButton.propTypes = { - className: PropTypes.string, - img: PropTypes.string, - onClick: PropTypes.func.isRequired, - title: PropTypes.node.isRequired -}; - -export default AssetButton; diff --git a/src/components/asset-panel/selector.css b/src/components/asset-panel/selector.css index 2a19296d66a..24a5c2ec609 100644 --- a/src/components/asset-panel/selector.css +++ b/src/components/asset-panel/selector.css @@ -11,13 +11,30 @@ } .new-buttons { + position: absolute; + bottom: 0; + width: 100%; + display: flex; flex-direction: column; align-items: center; justify-content: space-around; - margin: 0.75rem 0; + padding: 0.75rem 0; color: $motion-primary; text-align: center; + background: none; +} + +$fade-out-distance: 100px; + +.new-buttons:before { + content: ""; + position: absolute; + bottom: 0; + left: 0; + background: linear-gradient(rgba(232,237,241, 0),rgba(232,237,241, 1)); + height: $fade-out-distance; + width: 100%; } .new-buttons > button + button { diff --git a/src/components/asset-panel/selector.jsx b/src/components/asset-panel/selector.jsx index b6970778155..51094059f07 100644 --- a/src/components/asset-panel/selector.jsx +++ b/src/components/asset-panel/selector.jsx @@ -4,7 +4,7 @@ import React from 'react'; import SpriteSelectorItem from '../../containers/sprite-selector-item.jsx'; import Box from '../box/box.jsx'; -import AssetButton from '../asset-button/asset-button.jsx'; +import ActionMenu from '../action-menu/action-menu.jsx'; import styles from './selector.css'; const Selector = props => { @@ -36,14 +36,12 @@ const Selector = props => { ))} - {buttons.map(({message, img, onClick}, index) => ( - - ))} + ); diff --git a/src/components/green-flag/green-flag.css b/src/components/green-flag/green-flag.css index 2c145283869..35ba2972638 100644 --- a/src/components/green-flag/green-flag.css +++ b/src/components/green-flag/green-flag.css @@ -5,6 +5,7 @@ padding: 0.375rem; border-radius: 0.25rem; user-select: none; + user-drag: none; cursor: pointer; transition: 0.2s ease-out; } diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index 92b83acc2db..5132af685bf 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -140,7 +140,8 @@ For making the sprite-selector a scrollable pane @todo: Not working in Safari */ - overflow: hidden; + /* TODO this also breaks the thermometer menu */ + /* overflow: hidden; */ } .extension-button-container { @@ -156,6 +157,19 @@ box-sizing: content-box; /* To match scratch-block vertical toolbox borders */ } +$fade-out-distance: 15px; + +.extension-button-container:before { + content: ""; + position: absolute; + top: calc(calc(-1 * $fade-out-distance) - 1px); + left: -1px; + background: linear-gradient(rgba(0, 0, 0, 0),rgba(0, 0, 0, 0.15)); + height: $fade-out-distance; + width: calc(100% + 0.5px); +} + + .extension-button { background: none; border: none; diff --git a/src/components/sprite-selector/sprite-selector.css b/src/components/sprite-selector/sprite-selector.css index 67b0532d394..4cb03ecd581 100644 --- a/src/components/sprite-selector/sprite-selector.css +++ b/src/components/sprite-selector/sprite-selector.css @@ -2,7 +2,7 @@ .sprite-selector { flex-grow: 1; - position: relative; + position: relative; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; margin-right: calc($space / 2); background-color: #f9f9f9; @@ -16,14 +16,14 @@ /* In prep for renaming sprite-selector-item to sprite */ .sprite { - /* + /* Our goal is to fit sprites evenly in a row without leftover space. - Flexbox's `space between` property gets us close, but doesn't flow + Flexbox's `space between` property gets us close, but doesn't flow well when the # of items per row > 1 and less than the max per row. - Solving by explicitly calc'ing the width of each sprite. Setting - `border-box` simplifies things, because content, padding and - border-width all are included in the width, leaving us only to subtract + Solving by explicitly calc'ing the width of each sprite. Setting + `border-box` simplifies things, because content, padding and + border-width all are included in the width, leaving us only to subtract the left + right margins. @todo: make room for the scrollbar @@ -32,7 +32,7 @@ width: calc((100% / $sprites-per-row ) - $space); min-width: 4rem; min-height: 4rem; /* @todo: calc height same as width */ - margin: calc($space / 2); + margin: calc($space / 2); } @@ -41,11 +41,11 @@ Sets the sprite-selector items as a scrollable pane @todo: Safari: pane doesn't stretch to fill height; - @todo: Adding `position: relative` still doesn't fix Safari scrolling pane, and - also introduces a new bug in Chrome when vertically resizing window down, - then back up, introduces white space in the outside the page container. + @todo: Adding `position: relative` still doesn't fix Safari scrolling pane, and + also introduces a new bug in Chrome when vertically resizing window down, + then back up, introduces white space in the outside the page container. */ - height: calc(100% - $sprite-info-height); + height: calc(100% - $sprite-info-height); overflow-y: scroll; } @@ -57,11 +57,12 @@ padding-top: calc($space / 2); padding-left: calc($space / 2); padding-right: calc($space / 2); - padding-bottom: $space; + padding-bottom: $space; } .add-button { position: absolute; bottom: 0.75rem; right: 1rem; + z-index: 1; /* TODO overlaps the stage, this doesn't work, fix! */ } diff --git a/src/components/sprite-selector/sprite-selector.jsx b/src/components/sprite-selector/sprite-selector.jsx index 41e51a31626..9817386f76c 100644 --- a/src/components/sprite-selector/sprite-selector.jsx +++ b/src/components/sprite-selector/sprite-selector.jsx @@ -5,16 +5,41 @@ import {defineMessages, injectIntl, intlShape} from 'react-intl'; import Box from '../box/box.jsx'; import SpriteInfo from '../../containers/sprite-info.jsx'; import SpriteSelectorItem from '../../containers/sprite-selector-item.jsx'; -import AssetButton from '../asset-button/asset-button.jsx'; +import ActionMenu from '../action-menu/action-menu.jsx'; import styles from './sprite-selector.css'; -import spriteIcon from './icon--sprite.svg'; + +import cameraIcon from '../action-menu/icon--camera.svg'; +import fileUploadIcon from '../action-menu/icon--file-upload.svg'; +import paintIcon from '../action-menu/icon--paint.svg'; +import spriteIcon from '../action-menu/icon--sprite.svg'; +import surpriseIcon from '../action-menu/icon--surprise.svg'; const messages = defineMessages({ - addSprite: { - id: 'gui.spriteSelector.addSprite', - description: 'Button to add a sprite in the target pane', - defaultMessage: 'Add Sprite' + addSpriteFromLibrary: { + id: 'gui.spriteSelector.addSpriteFromLibrary', + description: 'Button to add a sprite in the target pane from library', + defaultMessage: 'Library' + }, + addSpriteFromPaint: { + id: 'gui.spriteSelector.addSpriteFromPaint', + description: 'Button to add a sprite in the target pane from paint', + defaultMessage: 'Paint' + }, + addSpriteFromSurprise: { + id: 'gui.spriteSelector.addSpriteFromSurprise', + description: 'Button to add a random sprite in the target pane', + defaultMessage: 'Surprise' + }, + addSpriteFromFile: { + id: 'gui.spriteSelector.addSpriteFromFile', + description: 'Button to add a sprite in the target pane from file', + defaultMessage: 'Coming Soon' + }, + addSpriteFromCamera: { + id: 'gui.spriteSelector.addSpriteFromCamera', + description: 'Button to add a sprite in the target pane from camera', + defaultMessage: 'Coming Soon' } }); @@ -30,6 +55,8 @@ const SpriteSelectorComponent = function (props) { onDeleteSprite, onDuplicateSprite, onNewSpriteClick, + onSurpriseSpriteClick, + onPaintSpriteClick, onSelectSprite, selectedId, sprites, @@ -85,10 +112,27 @@ const SpriteSelectorComponent = function (props) { } - diff --git a/src/components/stage-selector/stage-selector.jsx b/src/components/stage-selector/stage-selector.jsx index eea81140946..bb27710722d 100644 --- a/src/components/stage-selector/stage-selector.jsx +++ b/src/components/stage-selector/stage-selector.jsx @@ -4,16 +4,41 @@ import React from 'react'; import {defineMessages, intlShape, injectIntl, FormattedMessage} from 'react-intl'; import Box from '../box/box.jsx'; -import AssetButton from '../asset-button/asset-button.jsx'; +import ActionMenu from '../action-menu/action-menu.jsx'; import CostumeCanvas from '../costume-canvas/costume-canvas.jsx'; import styles from './stage-selector.css'; -import backdropIcon from './icon--backdrop.svg'; + +import backdropIcon from '../action-menu/icon--backdrop.svg'; +import cameraIcon from '../action-menu/icon--camera.svg'; +import fileUploadIcon from '../action-menu/icon--file-upload.svg'; +import paintIcon from '../action-menu/icon--paint.svg'; +import surpriseIcon from '../action-menu/icon--surprise.svg'; const messages = defineMessages({ - addBackdrop: { - id: 'gui.stageSelector.targetPaneAddBackdrop', - description: 'Button to add a backdrop in the target pane', - defaultMessage: 'Add Backdrop' + addBackdropFromLibrary: { + id: 'gui.spriteSelector.addBackdropFromLibrary', + description: 'Button to add a stage in the target pane from library', + defaultMessage: 'Library' + }, + addBackdropFromPaint: { + id: 'gui.stageSelector.addBackdropFromPaint', + description: 'Button to add a stage in the target pane from paint', + defaultMessage: 'Paint' + }, + addBackdropFromSurprise: { + id: 'gui.stageSelector.addBackdropFromSurprise', + description: 'Button to add a random stage in the target pane', + defaultMessage: 'Surprise' + }, + addBackdropFromFile: { + id: 'gui.stageSelector.addBackdropFromFile', + description: 'Button to add a stage in the target pane from file', + defaultMessage: 'Coming Soon' + }, + addBackdropFromCamera: { + id: 'gui.stageSelector.addBackdropFromCamera', + description: 'Button to add a stage in the target pane from camera', + defaultMessage: 'Coming Soon' } }); @@ -25,6 +50,8 @@ const StageSelector = props => { url, onClick, onNewBackdropClick, + onSurpriseBackdropClick, + onEmptyBackdropClick, ...componentProps } = props; return ( @@ -54,10 +81,28 @@ const StageSelector = props => { />
{backdropCount}
- @@ -72,4 +117,5 @@ StageSelector.propTypes = { selected: PropTypes.bool.isRequired, url: PropTypes.string }; + export default injectIntl(StageSelector); diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx index 30267688fe9..da19c36fc0a 100644 --- a/src/components/target-pane/target-pane.jsx +++ b/src/components/target-pane/target-pane.jsx @@ -29,6 +29,8 @@ const TargetPane = ({ onDeleteSprite, onDuplicateSprite, onNewSpriteClick, + onSurpriseSpriteClick, + onPaintSpriteClick, onRequestCloseSpriteLibrary, onRequestCloseBackdropLibrary, onSelectSprite, @@ -54,7 +56,9 @@ const TargetPane = ({ onDeleteSprite={onDeleteSprite} onDuplicateSprite={onDuplicateSprite} onNewSpriteClick={onNewSpriteClick} + onSurpriseSpriteClick={onSurpriseSpriteClick} onSelectSprite={onSelectSprite} + onPaintSpriteClick={onPaintSpriteClick} />
{stage.id && 2 ? item.info[2] : 1, + skinId: null + }; + this.props.vm.addCostume(item.md5, vmCostume).then(() => { + this.handleNewCostume(); + }); + } + handleSurpriseBackdrop () { + const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)]; + const vmCostume = { + name: item.name, + rotationCenterX: item.info[0] && item.info[0] / 2, + rotationCenterY: item.info[1] && item.info[1] / 2, + bitmapResolution: item.info.length > 2 ? item.info[2] : 1, + skinId: null + }; + this.props.vm.addCostume(item.md5, vmCostume).then(() => { + this.handleNewCostume(); + }); + } render () { // For paint wrapper const { @@ -139,8 +179,7 @@ class CostumeTab extends React.Component { return null; } - const addLibraryMessage = target.isStage ? messages.addLibraryBackdropMsg : messages.addLibraryCostumeMsg; - const addBlankMessage = target.isStage ? messages.addBlankBackdropMsg : messages.addBlankCostumeMsg; + const addSurpriseFunc = target.isStage ? this.handleSurpriseBackdrop : this.handleSurpriseCostume; const addLibraryFunc = target.isStage ? onNewLibraryBackdropClick : onNewLibraryCostumeClick; const addLibraryIcon = target.isStage ? addLibraryBackdropIcon : addLibraryCostumeIcon; @@ -148,14 +187,27 @@ class CostumeTab extends React.Component { { + this.handleNewSound(); + }); + } + render () { const { intl, @@ -94,13 +114,23 @@ class SoundTab extends React.Component { )) : []; const messages = defineMessages({ + fileUploadSound: { + defaultMessage: 'Coming Soon', + description: 'Button to upload sound from file in the editor tab', + id: 'gui.soundTab.fileUploadSound' + }, + surpriseSound: { + defaultMessage: 'Surprise', + description: 'Button to get a random sound in the editor tab', + id: 'gui.soundTab.surpriseSound' + }, recordSound: { - defaultMessage: 'Record Sound', + defaultMessage: 'Record', description: 'Button to record a sound in the editor tab', id: 'gui.soundTab.recordSound' }, addSound: { - defaultMessage: 'Add Sound', + defaultMessage: 'Library', description: 'Button to add a sound in the editor tab', id: 'gui.soundTab.addSound' } @@ -109,13 +139,20 @@ class SoundTab extends React.Component { return ( ({ url: soundIcon, diff --git a/src/containers/stage-selector.jsx b/src/containers/stage-selector.jsx index 83a5a1bb06f..3d280bd1603 100644 --- a/src/containers/stage-selector.jsx +++ b/src/containers/stage-selector.jsx @@ -4,19 +4,51 @@ import React from 'react'; import {connect} from 'react-redux'; import {openBackdropLibrary} from '../reducers/modals'; +import {activateTab, COSTUMES_TAB_INDEX} from '../reducers/editor-tab'; + import StageSelectorComponent from '../components/stage-selector/stage-selector.jsx'; +import backdropLibraryContent from '../lib/libraries/backdrops.json'; +import costumeLibraryContent from '../lib/libraries/costumes.json'; + class StageSelector extends React.Component { constructor (props) { super(props); bindAll(this, [ - 'handleClick' + 'handleClick', + 'handleSurpriseBackdrop', + 'handleEmptyBackdrop', + 'addBackdropFromLibraryItem' ]); } + addBackdropFromLibraryItem (item) { + const vmBackdrop = { + name: item.name, + rotationCenterX: item.info[0] && item.info[0] / 2, + rotationCenterY: item.info[1] && item.info[1] / 2, + bitmapResolution: item.info.length > 2 ? item.info[2] : 1, + skinId: null + }; + return this.props.vm.addBackdrop(item.md5, vmBackdrop); + } handleClick (e) { e.preventDefault(); this.props.onSelect(this.props.id); } + handleSurpriseBackdrop () { + // @todo should this not add a backdrop you already have? + const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)]; + this.addBackdropFromLibraryItem(item); + } + handleEmptyBackdrop () { + // @todo this is brittle, will need to be refactored for localized libraries + const emptyItem = costumeLibraryContent.find(item => item.name === 'Empty'); + if (emptyItem) { + this.addBackdropFromLibraryItem(emptyItem).then(() => { + this.props.onActivateTab(COSTUMES_TAB_INDEX); + }); + } + } render () { const { /* eslint-disable no-unused-vars */ @@ -29,6 +61,8 @@ class StageSelector extends React.Component { return ( ); @@ -41,13 +75,17 @@ StageSelector.propTypes = { }; const mapStateToProps = (state, {assetId}) => ({ - url: assetId && state.vm.runtime.storage.get(assetId).encodeDataURI() + url: assetId && state.vm.runtime.storage.get(assetId).encodeDataURI(), + vm: state.vm }); const mapDispatchToProps = dispatch => ({ onNewBackdropClick: e => { e.preventDefault(); dispatch(openBackdropLibrary()); + }, + onActivateTab: tabIndex => { + dispatch(activateTab(tabIndex)); } }); diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx index 32ad448bc7d..a1665f80866 100644 --- a/src/containers/target-pane.jsx +++ b/src/containers/target-pane.jsx @@ -9,7 +9,10 @@ import { closeSpriteLibrary } from '../reducers/modals'; +import {activateTab, COSTUMES_TAB_INDEX} from '../reducers/editor-tab'; + import TargetPaneComponent from '../components/target-pane/target-pane.jsx'; +import spriteLibraryContent from '../lib/libraries/sprites.json'; class TargetPane extends React.Component { constructor (props) { @@ -23,7 +26,9 @@ class TargetPane extends React.Component { 'handleChangeSpriteY', 'handleDeleteSprite', 'handleDuplicateSprite', - 'handleSelectSprite' + 'handleSelectSprite', + 'handleSurpriseSpriteClick', + 'handlePaintSpriteClick' ]); } handleChangeSpriteDirection (direction) { @@ -53,6 +58,19 @@ class TargetPane extends React.Component { handleSelectSprite (id) { this.props.vm.setEditingTarget(id); } + handleSurpriseSpriteClick () { + const item = spriteLibraryContent[Math.floor(Math.random() * spriteLibraryContent.length)]; + this.props.vm.addSprite2(JSON.stringify(item.json)); + } + handlePaintSpriteClick () { + // @todo this is brittle, will need to be refactored for localized libraries + const emptyItem = spriteLibraryContent.find(item => item.name === 'Empty'); + if (emptyItem) { + this.props.vm.addSprite2(JSON.stringify(emptyItem.json)).then(() => { + this.props.onActivateTab(COSTUMES_TAB_INDEX); + }); + } + } render () { return ( ); } @@ -105,6 +125,9 @@ const mapDispatchToProps = dispatch => ({ }, onRequestCloseBackdropLibrary: () => { dispatch(closeBackdropLibrary()); + }, + onActivateTab: tabIndex => { + dispatch(activateTab(tabIndex)); } }); diff --git a/src/css/colors.css b/src/css/colors.css index e270217f9a5..f4611c06e25 100644 --- a/src/css/colors.css +++ b/src/css/colors.css @@ -18,4 +18,6 @@ $control-primary: #FFAB19; $data-primary: #FF8C1A; +$pen-primary: #11B581; + $form-border: #E9EEF2; From e1efbc5bc369d2a34dd10fbb6eb031325e3260d0 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Mon, 26 Feb 2018 09:43:20 -0500 Subject: [PATCH 2/7] Fix linting --- src/components/asset-panel/selector.jsx | 26 +++++++++++++------ .../sprite-selector/sprite-selector.jsx | 2 ++ .../stage-selector/stage-selector.jsx | 2 ++ src/components/target-pane/target-pane.jsx | 6 +++-- src/containers/costume-tab.jsx | 2 -- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/components/asset-panel/selector.jsx b/src/components/asset-panel/selector.jsx index 51094059f07..471e9d94227 100644 --- a/src/components/asset-panel/selector.jsx +++ b/src/components/asset-panel/selector.jsx @@ -17,6 +17,23 @@ const Selector = props => { onItemClick } = props; + let newButtonSection = null; + + if (buttons.length > 0) { + const {img, title, onClick} = buttons[0]; + const moreButtons = buttons.slice(0); + newButtonSection = ( + + + + ); + } + return ( @@ -35,14 +52,7 @@ const Selector = props => { /> ))} - - - + {newButtonSection} ); }; diff --git a/src/components/sprite-selector/sprite-selector.jsx b/src/components/sprite-selector/sprite-selector.jsx index 9817386f76c..c1ae710759c 100644 --- a/src/components/sprite-selector/sprite-selector.jsx +++ b/src/components/sprite-selector/sprite-selector.jsx @@ -150,7 +150,9 @@ SpriteSelectorComponent.propTypes = { onDeleteSprite: PropTypes.func, onDuplicateSprite: PropTypes.func, onNewSpriteClick: PropTypes.func, + onPaintSpriteClick: PropTypes.func, onSelectSprite: PropTypes.func, + onSurpriseSpriteClick: PropTypes.func, selectedId: PropTypes.string, sprites: PropTypes.shape({ id: PropTypes.shape({ diff --git a/src/components/stage-selector/stage-selector.jsx b/src/components/stage-selector/stage-selector.jsx index bb27710722d..8c7a00180c2 100644 --- a/src/components/stage-selector/stage-selector.jsx +++ b/src/components/stage-selector/stage-selector.jsx @@ -113,7 +113,9 @@ StageSelector.propTypes = { backdropCount: PropTypes.number.isRequired, intl: intlShape.isRequired, onClick: PropTypes.func, + onEmptyBackdropClick: PropTypes.func, onNewBackdropClick: PropTypes.func, + onSurpriseBackdropClick: PropTypes.func, selected: PropTypes.bool.isRequired, url: PropTypes.string }; diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx index da19c36fc0a..7369f07fc1e 100644 --- a/src/components/target-pane/target-pane.jsx +++ b/src/components/target-pane/target-pane.jsx @@ -56,9 +56,9 @@ const TargetPane = ({ onDeleteSprite={onDeleteSprite} onDuplicateSprite={onDuplicateSprite} onNewSpriteClick={onNewSpriteClick} - onSurpriseSpriteClick={onSurpriseSpriteClick} - onSelectSprite={onSelectSprite} onPaintSpriteClick={onPaintSpriteClick} + onSelectSprite={onSelectSprite} + onSurpriseSpriteClick={onSurpriseSpriteClick} />
{stage.id && Date: Mon, 26 Feb 2018 10:24:04 -0500 Subject: [PATCH 3/7] Fix issues with asset panel buttons --- src/components/action-menu/action-menu.jsx | 10 +++++++++- src/components/asset-panel/selector.jsx | 2 +- src/containers/costume-tab.jsx | 8 +++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/components/action-menu/action-menu.jsx b/src/components/action-menu/action-menu.jsx index 750a0ea3e9a..dfefb5c5136 100644 --- a/src/components/action-menu/action-menu.jsx +++ b/src/components/action-menu/action-menu.jsx @@ -27,7 +27,12 @@ class ActionMenu extends React.Component { this.closeTimeoutId = null; }, CLOSE_DELAY); } - handleToggleOpenState () { + handleToggleOpenState (e) { + if (!this.state.isOpen) { + e.stopPropagation(); // For touch start, to prevent clicking primary button + } + + // Mouse enter back in after timeout was started prevents it from closing. if (this.closeTimeoutId) { clearTimeout(this.closeTimeoutId); this.closeTimeoutId = null; @@ -69,8 +74,10 @@ class ActionMenu extends React.Component { })} onMouseEnter={this.handleToggleOpenState} onMouseLeave={this.handleClosePopover} + onTouchStart={this.handleToggleOpenState} >