diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index 3be5884d..72d78f14 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -113,7 +113,7 @@ class PaintEditorComponent extends React.Component { imgAlt="Ungroup Icon" imgSrc={ungroupIcon} title="Ungroup" - onClick={function () {}} + onClick={this.props.onUngroup} /> diff --git a/src/containers/oval-mode.jsx b/src/containers/oval-mode.jsx index 5e727a92..cf7a79bb 100644 --- a/src/containers/oval-mode.jsx +++ b/src/containers/oval-mode.jsx @@ -1,3 +1,4 @@ +import paper from '@scratch/paper'; import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; @@ -28,6 +29,9 @@ class OvalMode extends React.Component { if (this.tool && nextProps.colorState !== this.props.colorState) { this.tool.setColorState(nextProps.colorState); } + if (this.tool && nextProps.selectedItems !== this.props.selectedItems) { + this.tool.onSelectionChanged(nextProps.selectedItems); + } if (nextProps.isOvalModeActive && !this.props.isOvalModeActive) { this.activateTool(); @@ -73,12 +77,14 @@ OvalMode.propTypes = { handleMouseDown: PropTypes.func.isRequired, isOvalModeActive: PropTypes.bool.isRequired, onUpdateSvg: PropTypes.func.isRequired, + selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)), setSelectedItems: PropTypes.func.isRequired }; const mapStateToProps = state => ({ colorState: state.scratchPaint.color, - isOvalModeActive: state.scratchPaint.mode === Modes.OVAL + isOvalModeActive: state.scratchPaint.mode === Modes.OVAL, + selectedItems: state.scratchPaint.selectedItems }); const mapDispatchToProps = dispatch => ({ clearSelectedItems: () => { @@ -89,8 +95,6 @@ const mapDispatchToProps = dispatch => ({ }, handleMouseDown: () => { dispatch(changeMode(Modes.OVAL)); - }, - deactivateTool () { } }); diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index acd00433..172b78f0 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -4,10 +4,13 @@ import PaintEditorComponent from '../components/paint-editor/paint-editor.jsx'; import {changeMode} from '../reducers/modes'; import {undo, redo, undoSnapshot} from '../reducers/undo'; +import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; import {getGuideLayer} from '../helper/layer'; import {performUndo, performRedo, performSnapshot} from '../helper/undo'; import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order'; +import {groupSelection, ungroupSelection} from '../helper/group'; +import {getSelectedLeafItems} from '../helper/selection'; import Modes from '../modes/modes'; import {connect} from 'react-redux'; @@ -24,7 +27,9 @@ class PaintEditor extends React.Component { 'handleSendBackward', 'handleSendForward', 'handleSendToBack', - 'handleSendToFront' + 'handleSendToFront', + 'handleGroup', + 'handleUngroup' ]); } componentDidMount () { @@ -51,10 +56,16 @@ class PaintEditor extends React.Component { paper.project.addLayer(guideLayer); } handleUndo () { - performUndo(this.props.undoState, this.props.onUndo, this.handleUpdateSvg); + performUndo(this.props.undoState, this.props.onUndo, this.props.setSelectedItems, this.handleUpdateSvg); } handleRedo () { - performRedo(this.props.undoState, this.props.onRedo, this.handleUpdateSvg); + performRedo(this.props.undoState, this.props.onRedo, this.props.setSelectedItems, this.handleUpdateSvg); + } + handleGroup () { + groupSelection(this.props.clearSelectedItems, this.props.setSelectedItems, this.handleUpdateSvg); + } + handleUngroup () { + ungroupSelection(this.props.clearSelectedItems, this.props.setSelectedItems, this.handleUpdateSvg); } handleSendBackward () { sendBackward(this.handleUpdateSvg); @@ -76,12 +87,14 @@ class PaintEditor extends React.Component { rotationCenterY={this.props.rotationCenterY} svg={this.props.svg} svgId={this.props.svgId} + onGroup={this.handleGroup} onRedo={this.handleRedo} onSendBackward={this.handleSendBackward} onSendForward={this.handleSendForward} onSendToBack={this.handleSendToBack} onSendToFront={this.handleSendToFront} onUndo={this.handleUndo} + onUngroup={this.handleUngroup} onUpdateName={this.props.onUpdateName} onUpdateSvg={this.handleUpdateSvg} /> @@ -90,6 +103,7 @@ class PaintEditor extends React.Component { } PaintEditor.propTypes = { + clearSelectedItems: PropTypes.func.isRequired, name: PropTypes.string, onKeyPress: PropTypes.func.isRequired, onRedo: PropTypes.func.isRequired, @@ -98,6 +112,7 @@ PaintEditor.propTypes = { onUpdateSvg: PropTypes.func.isRequired, rotationCenterX: PropTypes.number, rotationCenterY: PropTypes.number, + setSelectedItems: PropTypes.func.isRequired, svg: PropTypes.string, svgId: PropTypes.string, undoSnapshot: PropTypes.func.isRequired, @@ -122,6 +137,12 @@ const mapDispatchToProps = dispatch => ({ dispatch(changeMode(Modes.SELECT)); } }, + clearSelectedItems: () => { + dispatch(clearSelectedItems()); + }, + setSelectedItems: () => { + dispatch(setSelectedItems(getSelectedLeafItems())); + }, onUndo: () => { dispatch(undo()); }, diff --git a/src/containers/rect-mode.jsx b/src/containers/rect-mode.jsx index 59a2faee..d4dba28f 100644 --- a/src/containers/rect-mode.jsx +++ b/src/containers/rect-mode.jsx @@ -1,3 +1,4 @@ +import paper from '@scratch/paper'; import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; @@ -28,6 +29,9 @@ class RectMode extends React.Component { if (this.tool && nextProps.colorState !== this.props.colorState) { this.tool.setColorState(nextProps.colorState); } + if (this.tool && nextProps.selectedItems !== this.props.selectedItems) { + this.tool.onSelectionChanged(nextProps.selectedItems); + } if (nextProps.isRectModeActive && !this.props.isRectModeActive) { this.activateTool(); @@ -73,12 +77,14 @@ RectMode.propTypes = { handleMouseDown: PropTypes.func.isRequired, isRectModeActive: PropTypes.bool.isRequired, onUpdateSvg: PropTypes.func.isRequired, + selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)), setSelectedItems: PropTypes.func.isRequired }; const mapStateToProps = state => ({ colorState: state.scratchPaint.color, - isRectModeActive: state.scratchPaint.mode === Modes.RECT + isRectModeActive: state.scratchPaint.mode === Modes.RECT, + selectedItems: state.scratchPaint.selectedItems }); const mapDispatchToProps = dispatch => ({ clearSelectedItems: () => { diff --git a/src/containers/select-mode.jsx b/src/containers/select-mode.jsx index a9f09d84..596805b9 100644 --- a/src/containers/select-mode.jsx +++ b/src/containers/select-mode.jsx @@ -1,3 +1,4 @@ +import paper from '@scratch/paper'; import PropTypes from 'prop-types'; import React from 'react'; import {connect} from 'react-redux'; @@ -29,6 +30,9 @@ class SelectMode extends React.Component { if (this.tool && nextProps.hoveredItemId !== this.props.hoveredItemId) { this.tool.setPrevHoveredItemId(nextProps.hoveredItemId); } + if (this.tool && nextProps.selectedItems !== this.props.selectedItems) { + this.tool.onSelectionChanged(nextProps.selectedItems); + } if (nextProps.isSelectModeActive && !this.props.isSelectModeActive) { this.activateTool(); @@ -71,13 +75,15 @@ SelectMode.propTypes = { hoveredItemId: PropTypes.number, isSelectModeActive: PropTypes.bool.isRequired, onUpdateSvg: PropTypes.func.isRequired, + selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)), setHoveredItem: PropTypes.func.isRequired, setSelectedItems: PropTypes.func.isRequired }; const mapStateToProps = state => ({ isSelectModeActive: state.scratchPaint.mode === Modes.SELECT, - hoveredItemId: state.scratchPaint.hoveredItemId + hoveredItemId: state.scratchPaint.hoveredItemId, + selectedItems: state.scratchPaint.selectedItems }); const mapDispatchToProps = dispatch => ({ setHoveredItem: hoveredItemId => { diff --git a/src/helper/group.js b/src/helper/group.js index 56e5a2a0..4d005ce2 100644 --- a/src/helper/group.js +++ b/src/helper/group.js @@ -6,8 +6,7 @@ const isGroup = function (item) { return isGroupItem(item); }; -const groupSelection = function (clearSelectedItems) { - const items = getSelectedRootItems(); +const groupItems = function (items, clearSelectedItems, setSelectedItems, onUpdateSvg) { if (items.length > 0) { const group = new paper.Group(items); clearSelection(clearSelectedItems); @@ -15,15 +14,20 @@ const groupSelection = function (clearSelectedItems) { for (let i = 0; i < group.children.length; i++) { group.children[i].selected = true; } + setSelectedItems(); // @todo: Set selection bounds; enable/disable grouping icons - // @todo add back undo - // pg.undo.snapshot('groupSelection'); + onUpdateSvg(); return group; } return false; }; -const ungroupLoop = function (group, recursive, selectUngroupedItems) { +const groupSelection = function (clearSelectedItems, setSelectedItems, onUpdateSvg) { + const items = getSelectedRootItems(); + return groupItems(items, clearSelectedItems, setSelectedItems, onUpdateSvg); +}; + +const ungroupLoop = function (group, recursive, setSelectedItems) { // Can't ungroup items that are not groups if (!group || !group.children || !isGroup(group)) return; @@ -34,7 +38,7 @@ const ungroupLoop = function (group, recursive, selectUngroupedItems) { if (groupChild.hasChildren()) { // recursion (groups can contain groups, ie. from SVG import) if (recursive) { - ungroupLoop(groupChild, recursive, selectUngroupedItems); + ungroupLoop(groupChild, recursive, setSelectedItems); continue; } if (groupChild.children.length === 1) { @@ -44,7 +48,7 @@ const ungroupLoop = function (group, recursive, selectUngroupedItems) { groupChild.applyMatrix = true; // move items from the group to the activeLayer (ungrouping) groupChild.insertBelow(group); - if (selectUngroupedItems) { + if (setSelectedItems) { groupChild.selected = true; } i--; @@ -52,44 +56,38 @@ const ungroupLoop = function (group, recursive, selectUngroupedItems) { }; // ungroup items (only top hierarchy) -const ungroupItems = function (items, selectUngroupedItems) { +const ungroupItems = function (items, setSelectedItems, onUpdateSvg) { + if (items.length === 0) { + return; + } const emptyGroups = []; for (let i = 0; i < items.length; i++) { const item = items[i]; if (isGroup(item) && !item.data.isPGTextItem) { - ungroupLoop(item, false /* recursive */, selectUngroupedItems /* selectUngroupedItems */); + ungroupLoop(item, false /* recursive */, setSelectedItems); if (!item.hasChildren()) { emptyGroups.push(item); } } } - + if (setSelectedItems) { + setSelectedItems(); + } // remove all empty groups after ungrouping for (let j = 0; j < emptyGroups.length; j++) { emptyGroups[j].remove(); } // @todo: Set selection bounds; enable/disable grouping icons - // @todo add back undo - // pg.undo.snapshot('ungroupItems'); + if (onUpdateSvg) { + onUpdateSvg(); + } }; -const ungroupSelection = function (clearSelectedItems) { +const ungroupSelection = function (clearSelectedItems, setSelectedItems, onUpdateSvg) { const items = getSelectedRootItems(); clearSelection(clearSelectedItems); - ungroupItems(items, true /* selectUngroupedItems */); -}; - - -const groupItems = function (items) { - if (items.length > 0) { - const group = new paper.Group(items); - // @todo: Set selection bounds; enable/disable grouping icons - // @todo add back undo - // pg.undo.snapshot('groupItems'); - return group; - } - return false; + ungroupItems(items, setSelectedItems, onUpdateSvg); }; const getItemsGroup = function (item) { diff --git a/src/helper/selection-tools/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js index 1bbdd25b..58dc3db0 100644 --- a/src/helper/selection-tools/bounding-box-tool.js +++ b/src/helper/selection-tools/bounding-box-tool.js @@ -49,6 +49,18 @@ class BoundingBoxTool { this._modeMap[BoundingBoxModes.MOVE] = new MoveTool(mode, setSelectedItems, clearSelectedItems, onUpdateSvg); } + /** + * Should be called if the selection changes to update the bounds of the bounding box. + * @param {Array} selectedItems Array of selected items. + */ + onSelectionChanged (selectedItems) { + if (selectedItems) { + this.setSelectionBounds(); + } else { + this.removeBoundsPath(); + } + } + /** * @param {!MouseEvent} event The mouse event * @param {boolean} clone Whether to clone on mouse down (e.g. alt key held) diff --git a/src/helper/selection-tools/select-tool.js b/src/helper/selection-tools/select-tool.js index a66ee78b..1f3e513d 100644 --- a/src/helper/selection-tools/select-tool.js +++ b/src/helper/selection-tools/select-tool.js @@ -57,6 +57,13 @@ class SelectTool extends paper.Tool { setPrevHoveredItemId (prevHoveredItemId) { this.prevHoveredItemId = prevHoveredItemId; } + /** + * Should be called if the selection changes to update the bounds of the bounding box. + * @param {Array} selectedItems Array of selected items. + */ + onSelectionChanged (selectedItems) { + this.boundingBoxTool.onSelectionChanged(selectedItems); + } /** * Returns the hit options to use when conducting hit tests. * @param {boolean} preselectedOnly True if we should only return results that are already diff --git a/src/helper/tools/oval-tool.js b/src/helper/tools/oval-tool.js index eb6cd368..282f3fe3 100644 --- a/src/helper/tools/oval-tool.js +++ b/src/helper/tools/oval-tool.js @@ -47,6 +47,13 @@ class OvalTool extends paper.Tool { tolerance: OvalTool.TOLERANCE / paper.view.zoom }; } + /** + * Should be called if the selection changes to update the bounds of the bounding box. + * @param {Array} selectedItems Array of selected items. + */ + onSelectionChanged (selectedItems) { + this.boundingBoxTool.onSelectionChanged(selectedItems); + } setColorState (colorState) { this.colorState = colorState; } diff --git a/src/helper/tools/rect-tool.js b/src/helper/tools/rect-tool.js index f4caa171..ba5496d2 100644 --- a/src/helper/tools/rect-tool.js +++ b/src/helper/tools/rect-tool.js @@ -47,6 +47,13 @@ class RectTool extends paper.Tool { tolerance: RectTool.TOLERANCE / paper.view.zoom }; } + /** + * Should be called if the selection changes to update the bounds of the bounding box. + * @param {Array} selectedItems Array of selected items. + */ + onSelectionChanged (selectedItems) { + this.boundingBoxTool.onSelectionChanged(selectedItems); + } setColorState (colorState) { this.colorState = colorState; } diff --git a/src/helper/undo.js b/src/helper/undo.js index d7440e50..79bb5f6a 100644 --- a/src/helper/undo.js +++ b/src/helper/undo.js @@ -11,19 +11,21 @@ const performSnapshot = function (dispatchPerformSnapshot) { // updateButtonVisibility(); }; -const _restore = function (entry, onUpdateSvg) { +const _restore = function (entry, setSelectedItems, onUpdateSvg) { for (const layer of paper.project.layers) { layer.removeChildren(); } paper.project.clear(); paper.project.importJSON(entry.json); paper.view.update(); + + setSelectedItems(); onUpdateSvg(true /* skipSnapshot */); }; -const performUndo = function (undoState, dispatchPerformUndo, onUpdateSvg) { +const performUndo = function (undoState, dispatchPerformUndo, setSelectedItems, onUpdateSvg) { if (undoState.pointer > 0) { - _restore(undoState.stack[undoState.pointer - 1], onUpdateSvg); + _restore(undoState.stack[undoState.pointer - 1], setSelectedItems, onUpdateSvg); dispatchPerformUndo(); // @todo enable/disable buttons @@ -32,9 +34,9 @@ const performUndo = function (undoState, dispatchPerformUndo, onUpdateSvg) { }; -const performRedo = function (undoState, dispatchPerformRedo, onUpdateSvg) { +const performRedo = function (undoState, dispatchPerformRedo, setSelectedItems, onUpdateSvg) { if (undoState.pointer >= 0 && undoState.pointer < undoState.stack.length - 1) { - _restore(undoState.stack[undoState.pointer + 1], onUpdateSvg); + _restore(undoState.stack[undoState.pointer + 1], setSelectedItems, onUpdateSvg); dispatchPerformRedo(); // @todo enable/disable buttons diff --git a/src/index.js b/src/index.js index bbcbba68..9ed86c0e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,8 @@ import PaintEditor from './containers/paint-editor.jsx'; -import SelectionHOV from './containers/selection-hoc.jsx'; +import SelectionHOC from './containers/selection-hoc.jsx'; import ScratchPaintReducer from './reducers/scratch-paint-reducer'; -const Wrapped = SelectionHOV(PaintEditor); +const Wrapped = SelectionHOC(PaintEditor); export { Wrapped as default,