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..d67c95d8580
--- /dev/null
+++ b/src/components/action-menu/action-menu.jsx
@@ -0,0 +1,146 @@
+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 () {
+ // Mouse enter back in after timeout was started prevents it from closing.
+ if (this.closeTimeoutId) {
+ clearTimeout(this.closeTimeoutId);
+ this.closeTimeoutId = null;
+ } else if (!this.state.isOpen) {
+ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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 @@
+
+
\ 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..f2ab58cae0d 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 => {
@@ -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(1);
+ newButtonSection = (
+
+
+
+ );
+ }
+
return (
@@ -35,16 +52,7 @@ const Selector = props => {
/>
))}
-
- {buttons.map(({message, img, onClick}, index) => (
-
- ))}
-
+ {newButtonSection}
);
};
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..7590f286e5e 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: 'Sprite 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) {
}
-
@@ -106,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 eea81140946..073ca257c15 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: 'Backdrop 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}
-
@@ -68,8 +113,11 @@ 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
};
+
export default injectIntl(StageSelector);
diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx
index 30267688fe9..7369f07fc1e 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}
+ onPaintSpriteClick={onPaintSpriteClick}
onSelectSprite={onSelectSprite}
+ onSurpriseSpriteClick={onSurpriseSpriteClick}
/>
{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 {
@@ -120,9 +161,7 @@ class CostumeTab extends React.Component {
onNewLibraryBackdropClick,
onNewLibraryCostumeClick,
costumeLibraryVisible,
- backdropLibraryVisible,
onRequestCloseCostumeLibrary,
- onRequestCloseBackdropLibrary,
...props
} = this.props;
@@ -140,7 +179,7 @@ class CostumeTab extends React.Component {
}
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 {
) : null}
- {backdropLibraryVisible ? (
-
- ) : null}
);
}
@@ -233,9 +278,6 @@ const mapDispatchToProps = dispatch => ({
e.preventDefault();
dispatch(openCostumeLibrary());
},
- onRequestCloseBackdropLibrary: () => {
- dispatch(closeBackdropLibrary());
- },
onRequestCloseCostumeLibrary: () => {
dispatch(closeCostumeLibrary());
}
diff --git a/src/containers/extension-library.jsx b/src/containers/extension-library.jsx
index 9998fbb3e91..d036caa236d 100644
--- a/src/containers/extension-library.jsx
+++ b/src/containers/extension-library.jsx
@@ -7,7 +7,7 @@ import extensionLibraryContent from '../lib/libraries/extensions/index';
import analytics from '../lib/analytics';
import LibraryComponent from '../components/library/library.jsx';
-import extensionIcon from '../components/sprite-selector/icon--sprite.svg';
+import extensionIcon from '../components/action-menu/icon--sprite.svg';
class ExtensionLibrary extends React.PureComponent {
constructor (props) {
diff --git a/src/containers/sound-tab.jsx b/src/containers/sound-tab.jsx
index 8686ae6f905..471ebe99e65 100644
--- a/src/containers/sound-tab.jsx
+++ b/src/containers/sound-tab.jsx
@@ -8,10 +8,15 @@ import AssetPanel from '../components/asset-panel/asset-panel.jsx';
import soundIcon from '../components/asset-panel/icon--sound.svg';
import addSoundFromLibraryIcon from '../components/asset-panel/icon--add-sound-lib.svg';
import addSoundFromRecordingIcon from '../components/asset-panel/icon--add-sound-record.svg';
+import fileUploadIcon from '../components/action-menu/icon--file-upload.svg';
+import surpriseIcon from '../components/action-menu/icon--surprise.svg';
+
import RecordModal from './record-modal.jsx';
import SoundEditor from './sound-editor.jsx';
import SoundLibrary from './sound-library.jsx';
+import soundLibraryContent from '../lib/libraries/sounds.json';
+
import {connect} from 'react-redux';
import {
@@ -27,7 +32,8 @@ class SoundTab extends React.Component {
'handleSelectSound',
'handleDeleteSound',
'handleDuplicateSound',
- 'handleNewSound'
+ 'handleNewSound',
+ 'handleSurpriseSound'
]);
this.state = {selectedSoundIndex: 0};
}
@@ -72,6 +78,20 @@ class SoundTab extends React.Component {
this.setState({selectedSoundIndex: Math.max(sounds.length - 1, 0)});
}
+ handleSurpriseSound () {
+ const soundItem = soundLibraryContent[Math.floor(Math.random() * soundLibraryContent.length)];
+ const vmSound = {
+ format: soundItem.format,
+ md5: soundItem.md5,
+ rate: soundItem.rate,
+ sampleCount: soundItem.sampleCount,
+ name: soundItem.name
+ };
+ this.props.vm.addSound(vmSound).then(() => {
+ this.handleNewSound();
+ });
+ }
+
render () {
const {
intl,
@@ -94,28 +114,45 @@ 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: 'Sound Library',
description: 'Button to add a sound in the editor tab',
- id: 'gui.soundTab.addSound'
+ id: 'gui.soundTab.addSoundFromLibrary'
}
});
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;
diff --git a/test/integration/costumes.test.js b/test/integration/costumes.test.js
index f07c3d0f309..00700cba2b4 100644
--- a/test/integration/costumes.test.js
+++ b/test/integration/costumes.test.js
@@ -29,7 +29,7 @@ describe('Working with costumes', () => {
await loadUri(uri);
await clickXpath('//button[@title="tryit"]');
await clickText('Costumes');
- await clickXpath('//button[@title="Add Costume"]');
+ await clickXpath('//button[@aria-label="Costume Library"]');
const el = await findByXpath("//input[@placeholder='what are you looking for?']");
await el.sendKeys('abb');
await clickText('Abby-a'); // Should close the modal, then click the costumes in the selector
@@ -57,7 +57,7 @@ describe('Working with costumes', () => {
test('Adding a backdrop', async () => {
await loadUri(uri);
await clickXpath('//button[@title="tryit"]');
- await clickXpath('//button[@title="Add Backdrop"]');
+ await clickXpath('//button[@aria-label="Backdrop Library"]');
const el = await findByXpath("//input[@placeholder='what are you looking for?']");
await el.sendKeys('blue');
await clickText('Blue Sky'); // Should close the modal
diff --git a/test/integration/sounds.test.js b/test/integration/sounds.test.js
index e3154868417..fc36c55a553 100644
--- a/test/integration/sounds.test.js
+++ b/test/integration/sounds.test.js
@@ -37,13 +37,13 @@ describe('Working with sounds', () => {
.accept();
// Add it back
- await clickXpath('//button[@title="Add Sound"]');
+ await clickXpath('//button[@aria-label="Sound Library"]');
let el = await findByXpath("//input[@placeholder='what are you looking for?']");
await el.sendKeys('meow');
await clickText('Meow', scope.modal); // Should close the modal
// Add a new sound
- await clickXpath('//button[@title="Add Sound"]');
+ await clickXpath('//button[@aria-label="Sound Library"]');
el = await findByXpath("//input[@placeholder='what are you looking for?']");
await el.sendKeys('chom');
await clickText('Chomp'); // Should close the modal, then click the sounds in the selector
@@ -86,11 +86,11 @@ describe('Working with sounds', () => {
// Add a sound so this sprite has 2 sounds.
await clickText('Sounds');
- await clickXpath('//button[@title="Add Sound"]');
+ await clickXpath('//button[@aria-label="Sound Library"]');
await clickText('A Bass'); // Closes the modal
// Now add a sprite with only one sound.
- await clickXpath('//button[@title="Add Sprite"]');
+ await clickXpath('//button[@aria-label="Sprite Library"]');
await clickText('Abby'); // Doing this used to crash the editor.
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for error