diff --git a/package.json b/package.json index f06fed8d8e21a86ac474daba78742373118f9049..0d94daebd918e1e6b68716331bc66db55318627b 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "scratch-render": "0.1.0-prerelease.20180808184135", "scratch-storage": "0.5.1", "scratch-svg-renderer": "0.2.0-prerelease.20180712223402", - "scratch-vm": "^0.2.0-prerelease.20180808205317", + "scratch-vm": "0.2.0-prerelease.20180809193416", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", "style-loader": "^0.22.1", diff --git a/src/components/menu-bar/menu-bar.css b/src/components/menu-bar/menu-bar.css index 45669f8d6ed979207c8d2774b55504df885118c0..635b18012b2a5e88bc5e7f56372da3d102a2eec0 100644 --- a/src/components/menu-bar/menu-bar.css +++ b/src/components/menu-bar/menu-bar.css @@ -172,3 +172,7 @@ width: 0.5rem; height: 0.5rem; } + +.disabled { + opacity: 0.5; +} diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index 24117372b46f1524a3bcf46c4189b34e3a020d33..8355f8dfd4a5f6fe86b29ae15cc03b5e788e9316 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -14,6 +14,7 @@ import ProjectLoader from '../../containers/project-loader.jsx'; import Menu from '../../containers/menu.jsx'; import {MenuItem, MenuSection} from '../menu/menu.jsx'; import ProjectSaver from '../../containers/project-saver.jsx'; +import DeletionRestorer from '../../containers/deletion-restorer.jsx'; import TurboMode from '../../containers/turbo-mode.jsx'; import {openTipsLibrary} from '../../reducers/modals'; @@ -133,7 +134,8 @@ class MenuBar extends React.Component { constructor (props) { super(props); bindAll(this, [ - 'handleLanguageMouseUp' + 'handleLanguageMouseUp', + 'handleRestoreOption' ]); } handleLanguageMouseUp (e) { @@ -141,6 +143,12 @@ class MenuBar extends React.Component { this.props.onClickLanguage(e); } } + handleRestoreOption (restoreFun) { + return () => { + restoreFun(); + this.props.onRequestCloseEdit(); + }; + } render () { return ( <Box className={styles.menuBar}> @@ -279,24 +287,25 @@ class MenuBar extends React.Component { open={this.props.editMenuOpen} onRequestClose={this.props.onRequestCloseEdit} > - <MenuItemTooltip id="undo"> - <MenuItem> - <FormattedMessage - defaultMessage="Undo" - description="Menu bar item for undoing" - id="gui.menuBar.undo" - /> - </MenuItem> - </MenuItemTooltip> - <MenuItemTooltip id="redo"> - <MenuItem> - <FormattedMessage - defaultMessage="Redo" - description="Menu bar item for redoing" - id="gui.menuBar.redo" - /> + <DeletionRestorer>{(handleRestore, {restorable, deletedItem}) => ( + <MenuItem + className={classNames({[styles.disabled]: !restorable})} + onClick={this.handleRestoreOption(handleRestore)} + > + {deletedItem === 'Sprite' ? + <FormattedMessage + defaultMessage="Restore Sprite" + description="Menu bar item for restoring the last deleted sprite." + id="gui.menuBar.restoreSprite" + /> : + <FormattedMessage + defaultMessage="Restore" + description="Menu bar item for restoring the last deleted item in its disabled state." /* eslint-disable-line max-len */ + id="gui.menuBar.restore" + /> + } </MenuItem> - </MenuItemTooltip> + )}</DeletionRestorer> <MenuSection> <TurboMode>{(toggleTurboMode, {turboMode}) => ( <MenuItem onClick={toggleTurboMode}> diff --git a/src/containers/deletion-restorer.jsx b/src/containers/deletion-restorer.jsx new file mode 100644 index 0000000000000000000000000000000000000000..854836eebe47967b2a7e805655be2c1c9020bed8 --- /dev/null +++ b/src/containers/deletion-restorer.jsx @@ -0,0 +1,70 @@ +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {connect} from 'react-redux'; +import {setRestore} from '../reducers/restore-deletion'; + +/** + * DeletionRestorer component passes a restoreDeletion function to its child. + * It expects this child to be a function with the signature + * function (restoreDeletion, props) {} + * The component can then be used to attach deletion restoring functionality + * to any other component: + * + * <DeletionRestorer>{(restoreDeletion, props) => ( + * <MyCoolComponent + * onClick={restoreDeletion} + * {...props} + * /> + * )}</DeletionRestorer> + */ +class DeletionRestorer extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'restoreDeletion' + ]); + } + restoreDeletion () { + if (typeof this.props.restore === 'function') { + this.props.restore(); + this.props.dispatchUpdateRestore({restoreFun: null, deletedItem: ''}); + } + } + render () { + const { + /* eslint-disable no-unused-vars */ + children, + dispatchUpdateRestore, + /* eslint-enable no-unused-vars */ + ...props + } = this.props; + const restorable = typeof this.props.restore === 'function'; + return this.props.children(this.restoreDeletion, { + ...props, + restorable + }); + } +} + +DeletionRestorer.propTypes = { + children: PropTypes.func, + deletedItem: PropTypes.string, + dispatchUpdateRestore: PropTypes.func, + restore: PropTypes.func +}; + +const mapStateToProps = state => ({ + deletedItem: state.scratchGui.restoreDeletion.deletedItem, + restore: state.scratchGui.restoreDeletion.restoreFun +}); +const mapDispatchToProps = dispatch => ({ + dispatchUpdateRestore: updatedState => { + dispatch(setRestore(updatedState)); + } +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(DeletionRestorer); diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx index 870b9716440211dab3249f4393a25095dbdb58fa..4444c31432c50174ff1eba9a799fd7a8d710c9c7 100644 --- a/src/containers/target-pane.jsx +++ b/src/containers/target-pane.jsx @@ -10,6 +10,7 @@ import { import {activateTab, COSTUMES_TAB_INDEX} from '../reducers/editor-tab'; import {setReceivedBlocks} from '../reducers/hovered-target'; +import {setRestore} from '../reducers/restore-deletion'; import DragConstants from '../lib/drag-constants'; import TargetPaneComponent from '../components/target-pane/target-pane.jsx'; import spriteLibraryContent from '../lib/libraries/sprites.json'; @@ -68,7 +69,12 @@ class TargetPane extends React.Component { this.props.vm.postSpriteInfo({y}); } handleDeleteSprite (id) { - this.props.vm.deleteSprite(id); + const restoreFun = this.props.vm.deleteSprite(id); + this.props.dispatchUpdateRestore({ + restoreFun: restoreFun, + deletedItem: 'Sprite' + }); + } handleDuplicateSprite (id) { this.props.vm.duplicateSprite(id); @@ -175,6 +181,7 @@ class TargetPane extends React.Component { const { onActivateTab, // eslint-disable-line no-unused-vars onReceivedBlocks, // eslint-disable-line no-unused-vars + dispatchUpdateRestore, // eslint-disable-line no-unused-vars ...componentProps } = this.props; return ( @@ -240,6 +247,9 @@ const mapDispatchToProps = dispatch => ({ }, onReceivedBlocks: receivedBlocks => { dispatch(setReceivedBlocks(receivedBlocks)); + }, + dispatchUpdateRestore: restoreState => { + dispatch(setRestore(restoreState)); } }); diff --git a/src/reducers/gui.js b/src/reducers/gui.js index 2c5642416a52b1e65a4324fee3ad0019ffb52e8b..04b2644686165b0038c4824c0092701d36a0f697 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -11,6 +11,7 @@ import modalReducer, {modalsInitialState} from './modals'; import modeReducer, {modeInitialState} from './mode'; import monitorReducer, {monitorsInitialState} from './monitors'; import monitorLayoutReducer, {monitorLayoutInitialState} from './monitor-layout'; +import restoreDeletionReducer, {restoreDeletionInitialState} from './restore-deletion'; import stageSizeReducer, {stageSizeInitialState} from './stage-size'; import targetReducer, {targetsInitialState} from './targets'; import toolboxReducer, {toolboxInitialState} from './toolbox'; @@ -34,6 +35,7 @@ const guiInitialState = { modals: modalsInitialState, monitors: monitorsInitialState, monitorLayout: monitorLayoutInitialState, + restoreDeletion: restoreDeletionInitialState, targets: targetsInitialState, toolbox: toolboxInitialState, vm: vmInitialState, @@ -75,6 +77,7 @@ const guiReducer = combineReducers({ modals: modalReducer, monitors: monitorReducer, monitorLayout: monitorLayoutReducer, + restoreDeletion: restoreDeletionReducer, targets: targetReducer, toolbox: toolboxReducer, vm: vmReducer, diff --git a/src/reducers/restore-deletion.js b/src/reducers/restore-deletion.js new file mode 100644 index 0000000000000000000000000000000000000000..0c723fc0323224df5dd53969281d055160f16589 --- /dev/null +++ b/src/reducers/restore-deletion.js @@ -0,0 +1,33 @@ +const RESTORE_UPDATE = 'scratch-gui/restore-deletion/RESTORE_UPDATE'; + +const initialState = { + restoreFun: null, + deletedItem: '' +}; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + + switch (action.type) { + case RESTORE_UPDATE: + return Object.assign({}, state, action.state); + default: + return state; + } +}; + +const setRestore = function (state) { + return { + type: RESTORE_UPDATE, + state: { + restoreFun: state.restoreFun, + deletedItem: state.deletedItem + } + }; +}; + +export { + reducer as default, + initialState as restoreDeletionInitialState, + setRestore +};