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..96338b87d9fd12efdb6614f2707613ab958d2603 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'; @@ -279,15 +280,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" - /> + <DeletionRestorer>{(handleRestore, {restorable, deletedItem}) => ( + <MenuItem + className={classNames({[styles.disabled]:!restorable})} + onClick={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." + id="gui.menuBar.restore" + /> + } </MenuItem> - </MenuItemTooltip> + )}</DeletionRestorer> <MenuItemTooltip id="redo"> <MenuItem> <FormattedMessage diff --git a/src/containers/deletion-restorer.jsx b/src/containers/deletion-restorer.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6c95e29207bb6bf3088b096e33a99268c4d56ca8 --- /dev/null +++ b/src/containers/deletion-restorer.jsx @@ -0,0 +1,67 @@ +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, + /* eslint-enable no-unused-vars */ + ...props + } = this.props; + props.restorable = typeof this.props.restore === 'function'; + props.deletedItem = this.props.deletedItem; + return this.props.children(this.restoreDeletion, props); + } +} + +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..2bfc7725372f421e0a15e94cfab95088ad75d049 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); @@ -240,6 +246,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 +};