-
@@ -324,16 +318,10 @@ PaintEditorComponent.propTypes = {
intl: intlShape,
isEyeDropping: PropTypes.bool,
name: PropTypes.string,
- onGroup: PropTypes.func.isRequired,
onRedo: PropTypes.func.isRequired,
- onSendBackward: PropTypes.func.isRequired,
- onSendForward: PropTypes.func.isRequired,
- onSendToBack: PropTypes.func.isRequired,
- onSendToFront: PropTypes.func.isRequired,
onSwitchToBitmap: PropTypes.func.isRequired,
onSwitchToVector: PropTypes.func.isRequired,
onUndo: PropTypes.func.isRequired,
- onUngroup: PropTypes.func.isRequired,
onUpdateImage: PropTypes.func.isRequired,
onUpdateName: PropTypes.func.isRequired,
onZoomIn: PropTypes.func.isRequired,
diff --git a/src/containers/fixed-tools.jsx b/src/containers/fixed-tools.jsx
new file mode 100644
index 00000000..fb5aeb40
--- /dev/null
+++ b/src/containers/fixed-tools.jsx
@@ -0,0 +1,131 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import {connect} from 'react-redux';
+
+import FixedToolsComponent from '../components/fixed-tools/fixed-tools.jsx';
+
+import {changeMode} from '../reducers/modes';
+import {changeFormat} from '../reducers/format';
+import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
+import {deactivateEyeDropper} from '../reducers/eye-dropper';
+import {setTextEditTarget} from '../reducers/text-edit-target';
+import {setLayout} from '../reducers/layout';
+
+import {getSelectedLeafItems} from '../helper/selection';
+import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order';
+import {groupSelection, ungroupSelection} from '../helper/group';
+
+import Formats from '../lib/format';
+import {isBitmap} from '../lib/format';
+import bindAll from 'lodash.bindall';
+
+class FixedTools extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleSendBackward',
+ 'handleSendForward',
+ 'handleSendToBack',
+ 'handleSendToFront',
+ 'handleSetSelectedItems',
+ 'handleGroup',
+ 'handleUngroup'
+ ]);
+ }
+ handleGroup () {
+ groupSelection(this.props.clearSelectedItems, this.handleSetSelectedItems, this.props.onUpdateImage);
+ }
+ handleUngroup () {
+ ungroupSelection(this.props.clearSelectedItems, this.handleSetSelectedItems, this.props.onUpdateImage);
+ }
+ handleSendBackward () {
+ sendBackward(this.props.onUpdateImage);
+ }
+ handleSendForward () {
+ bringForward(this.props.onUpdateImage);
+ }
+ handleSendToBack () {
+ sendToBack(this.props.onUpdateImage);
+ }
+ handleSendToFront () {
+ bringToFront(this.props.onUpdateImage);
+ }
+ handleSetSelectedItems () {
+ this.props.setSelectedItems(this.props.format);
+ }
+ render () {
+ return (
+
+ );
+ }
+}
+
+FixedTools.propTypes = {
+ canRedo: PropTypes.func.isRequired,
+ canUndo: PropTypes.func.isRequired,
+ clearSelectedItems: PropTypes.func.isRequired,
+ format: PropTypes.oneOf(Object.keys(Formats)),
+ name: PropTypes.string,
+ onRedo: PropTypes.func.isRequired,
+ onUndo: PropTypes.func.isRequired,
+ onUpdateImage: PropTypes.func.isRequired,
+ onUpdateName: PropTypes.func.isRequired,
+ setSelectedItems: PropTypes.func.isRequired
+};
+
+const mapStateToProps = state => ({
+ changeColorToEyeDropper: state.scratchPaint.color.eyeDropper.callback,
+ format: state.scratchPaint.format,
+ isEyeDropping: state.scratchPaint.color.eyeDropper.active,
+ mode: state.scratchPaint.mode,
+ pasteOffset: state.scratchPaint.clipboard.pasteOffset,
+ previousTool: state.scratchPaint.color.eyeDropper.previousTool,
+ selectedItems: state.scratchPaint.selectedItems,
+ viewBounds: state.scratchPaint.viewBounds
+});
+const mapDispatchToProps = dispatch => ({
+ changeMode: mode => {
+ dispatch(changeMode(mode));
+ },
+ clearSelectedItems: () => {
+ dispatch(clearSelectedItems());
+ },
+ handleSwitchToBitmap: () => {
+ dispatch(changeFormat(Formats.BITMAP));
+ },
+ handleSwitchToVector: () => {
+ dispatch(changeFormat(Formats.VECTOR));
+ },
+ removeTextEditTarget: () => {
+ dispatch(setTextEditTarget());
+ },
+ setLayout: layout => {
+ dispatch(setLayout(layout));
+ },
+ setSelectedItems: format => {
+ dispatch(setSelectedItems(getSelectedLeafItems(), isBitmap(format)));
+ },
+ onDeactivateEyeDropper: () => {
+ // set redux values to default for eye dropper reducer
+ dispatch(deactivateEyeDropper());
+ }
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(FixedTools);
diff --git a/src/containers/mode-tools.jsx b/src/containers/mode-tools.jsx
index 02265c06..f9edbbb5 100644
--- a/src/containers/mode-tools.jsx
+++ b/src/containers/mode-tools.jsx
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
-import CopyPasteHOC from './copy-paste-hoc.jsx';
+import CopyPasteHOC from '../hocs/copy-paste-hoc.jsx';
import ModeToolsComponent from '../components/mode-tools/mode-tools.jsx';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
import {incrementPasteOffset, setClipboardItems} from '../reducers/clipboard';
diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx
index 008601a3..45bf9f6b 100644
--- a/src/containers/paint-editor.jsx
+++ b/src/containers/paint-editor.jsx
@@ -1,37 +1,29 @@
import paper from '@scratch/paper';
import PropTypes from 'prop-types';
import log from '../log/log';
-
import React from 'react';
import {connect} from 'react-redux';
+
import PaintEditorComponent from '../components/paint-editor/paint-editor.jsx';
-import CopyPasteHOC from './copy-paste-hoc.jsx';
-import SelectionHOC from './selection-hoc.jsx';
+import KeyboardShortcutsHOC from '../hocs/keyboard-shortcuts-hoc.jsx';
+import SelectionHOC from '../hocs/selection-hoc.jsx';
+import UndoHOC from '../hocs/undo-hoc.jsx';
+import UpdateImageHOC from '../hocs/update-image-hoc.jsx';
import {changeMode} from '../reducers/modes';
import {changeFormat} from '../reducers/format';
-import {undo, redo, undoSnapshot} from '../reducers/undo';
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
import {deactivateEyeDropper} from '../reducers/eye-dropper';
import {setTextEditTarget} from '../reducers/text-edit-target';
import {updateViewBounds} from '../reducers/view-bounds';
import {setLayout} from '../reducers/layout';
-import {getRaster, hideGuideLayers, showGuideLayers} from '../helper/layer';
-import {commitSelectionToBitmap, convertToBitmap, convertToVector, getHitBounds,
- selectAllBitmap} from '../helper/bitmap';
-import {performUndo, performRedo, performSnapshot, shouldShowUndo, shouldShowRedo} from '../helper/undo';
-import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order';
-import {groupSelection, ungroupSelection} from '../helper/group';
-import {scaleWithStrokes} from '../helper/math';
-import {clearSelection, deleteSelection, getSelectedLeafItems,
- selectAllItems, selectAllSegments} from '../helper/selection';
-import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, SVG_ART_BOARD_WIDTH, SVG_ART_BOARD_HEIGHT} from '../helper/view';
+import {getSelectedLeafItems} from '../helper/selection';
+import {convertToBitmap, convertToVector} from '../helper/bitmap';
import {resetZoom, zoomOnSelection} from '../helper/view';
import EyeDropperTool from '../helper/tools/eye-dropper';
import Modes from '../lib/modes';
-import {BitmapModes} from '../lib/modes';
import Formats from '../lib/format';
import {isBitmap, isVector} from '../lib/format';
import bindAll from 'lodash.bindall';
@@ -43,52 +35,32 @@ class PaintEditor extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
- 'handleUpdateImage',
- 'handleUpdateBitmap',
- 'handleUpdateVector',
- 'handleUndo',
- 'handleRedo',
- 'handleSendBackward',
- 'handleSendForward',
- 'handleSendToBack',
- 'handleSendToFront',
- 'handleSetSelectedItems',
- 'handleGroup',
- 'handleUngroup',
- 'handleZoomIn',
- 'handleZoomOut',
- 'handleZoomReset',
- 'canRedo',
- 'canUndo',
'switchMode',
- 'onKeyPress',
'onMouseDown',
'setCanvas',
'setTextArea',
'startEyeDroppingLoop',
- 'stopEyeDroppingLoop'
+ 'stopEyeDroppingLoop',
+ 'handleSetSelectedItems',
+ 'handleZoomIn',
+ 'handleZoomOut',
+ 'handleZoomReset'
]);
this.state = {
canvas: null,
colorInfo: null
};
- // When isSwitchingFormats is true, the format is about to switch, but isn't done switching.
- // This gives currently active tools a chance to finish what they were doing.
- this.isSwitchingFormats = false;
this.props.setLayout(this.props.rtl ? 'rtl' : 'ltr');
}
componentDidMount () {
- document.addEventListener('keydown', this.onKeyPress);
+ document.addEventListener('keydown', this.props.onKeyPress);
+
// document listeners used to detect if a mouse is down outside of the
// canvas, and should therefore stop the eye dropper
document.addEventListener('mousedown', this.onMouseDown);
document.addEventListener('touchstart', this.onMouseDown);
}
componentWillReceiveProps (newProps) {
- if ((isVector(this.props.format) && newProps.format === Formats.BITMAP) ||
- (isBitmap(this.props.format) && newProps.format === Formats.VECTOR)) {
- this.isSwitchingFormats = true;
- }
if (isVector(this.props.format) && isBitmap(newProps.format)) {
this.switchMode(Formats.BITMAP);
} else if (isVector(newProps.format) && isBitmap(this.props.format)) {
@@ -108,12 +80,11 @@ class PaintEditor extends React.Component {
this.props.onDeactivateEyeDropper();
this.stopEyeDroppingLoop();
}
+
if (this.props.format === Formats.VECTOR && isBitmap(prevProps.format)) {
- this.isSwitchingFormats = false;
- convertToVector(this.props.clearSelectedItems, this.handleUpdateImage);
+ convertToVector(this.props.clearSelectedItems, this.props.onUpdateImage);
} else if (isVector(prevProps.format) && this.props.format === Formats.BITMAP) {
- this.isSwitchingFormats = false;
- convertToBitmap(this.props.clearSelectedItems, this.handleUpdateImage);
+ convertToBitmap(this.props.clearSelectedItems, this.props.onUpdateImage);
}
}
componentWillUnmount () {
@@ -187,108 +158,6 @@ class PaintEditor extends React.Component {
}
}
}
- handleUpdateImage (skipSnapshot) {
- // If in the middle of switching formats, rely on the current mode instead of format.
- let actualFormat = this.props.format;
- if (this.isSwitchingFormats) {
- actualFormat = BitmapModes[this.props.mode] ? Formats.BITMAP : Formats.VECTOR;
- }
- if (isBitmap(actualFormat)) {
- this.handleUpdateBitmap(skipSnapshot);
- } else if (isVector(actualFormat)) {
- this.handleUpdateVector(skipSnapshot);
- }
- }
- handleUpdateBitmap (skipSnapshot) {
- if (!getRaster().loaded) {
- // In general, callers of updateImage should wait for getRaster().loaded = true before
- // calling updateImage.
- // However, this may happen if the user is rapidly undoing/redoing. In this case it's safe
- // to skip the update.
- log.warn('Bitmap layer should be loaded before calling updateImage.');
- return;
- }
- // Plaster the selection onto the raster layer before exporting, if there is a selection.
- const plasteredRaster = getRaster().getSubRaster(getRaster().bounds);
- plasteredRaster.remove(); // Don't insert
- const selectedItems = getSelectedLeafItems();
- if (selectedItems.length === 1 && selectedItems[0] instanceof paper.Raster) {
- if (!selectedItems[0].loaded ||
- (selectedItems[0].data && selectedItems[0].data.expanded && !selectedItems[0].data.expanded.loaded)) {
- log.warn('Bitmap layer should be loaded before calling updateImage.');
- return;
- }
- commitSelectionToBitmap(selectedItems[0], plasteredRaster);
- }
- const rect = getHitBounds(plasteredRaster);
- this.props.onUpdateImage(
- false /* isVector */,
- plasteredRaster.getImageData(rect),
- (ART_BOARD_WIDTH / 2) - rect.x,
- (ART_BOARD_HEIGHT / 2) - rect.y);
-
- if (!skipSnapshot) {
- performSnapshot(this.props.undoSnapshot, Formats.BITMAP);
- }
- }
- handleUpdateVector (skipSnapshot) {
- const guideLayers = hideGuideLayers(true /* includeRaster */);
-
- // Export at 0.5x
- scaleWithStrokes(paper.project.activeLayer, .5, new paper.Point());
- const bounds = paper.project.activeLayer.bounds;
- // @todo generate view box
- this.props.onUpdateImage(
- true /* isVector */,
- paper.project.exportSVG({
- asString: true,
- bounds: 'content',
- matrix: new paper.Matrix().translate(-bounds.x, -bounds.y)
- }),
- (SVG_ART_BOARD_WIDTH / 2) - bounds.x,
- (SVG_ART_BOARD_HEIGHT / 2) - bounds.y);
- scaleWithStrokes(paper.project.activeLayer, 2, new paper.Point());
- paper.project.activeLayer.applyMatrix = true;
-
- showGuideLayers(guideLayers);
-
- if (!skipSnapshot) {
- performSnapshot(this.props.undoSnapshot, Formats.VECTOR);
- }
- }
- handleUndo () {
- performUndo(this.props.undoState, this.props.onUndo, this.handleSetSelectedItems, this.handleUpdateImage);
- }
- handleRedo () {
- performRedo(this.props.undoState, this.props.onRedo, this.handleSetSelectedItems, this.handleUpdateImage);
- }
- handleGroup () {
- groupSelection(this.props.clearSelectedItems, this.handleSetSelectedItems, this.handleUpdateImage);
- }
- handleUngroup () {
- ungroupSelection(this.props.clearSelectedItems, this.handleSetSelectedItems, this.handleUpdateImage);
- }
- handleSendBackward () {
- sendBackward(this.handleUpdateImage);
- }
- handleSendForward () {
- bringForward(this.handleUpdateImage);
- }
- handleSendToBack () {
- sendToBack(this.handleUpdateImage);
- }
- handleSendToFront () {
- bringToFront(this.handleUpdateImage);
- }
- handleSetSelectedItems () {
- this.props.setSelectedItems(this.props.format);
- }
- canUndo () {
- return shouldShowUndo(this.props.undoState);
- }
- canRedo () {
- return shouldShowRedo(this.props.undoState);
- }
handleZoomIn () {
zoomOnSelection(PaintEditor.ZOOM_INCREMENT);
this.props.updateViewBounds(paper.view.matrix);
@@ -304,6 +173,9 @@ class PaintEditor extends React.Component {
this.props.updateViewBounds(paper.view.matrix);
this.handleSetSelectedItems();
}
+ handleSetSelectedItems () {
+ this.props.setSelectedItems(this.props.format);
+ }
setCanvas (canvas) {
this.setState({canvas: canvas});
this.canvas = canvas;
@@ -311,56 +183,6 @@ class PaintEditor extends React.Component {
setTextArea (element) {
this.setState({textArea: element});
}
- onKeyPress (event) {
- // Don't activate keyboard shortcuts during text editing
- if (this.props.textEditing) return;
-
- if (event.key === 'Escape') {
- event.preventDefault();
- clearSelection(this.props.clearSelectedItems);
- } else if (event.key === 'Delete' || event.key === 'Backspace') {
- if (deleteSelection(this.props.mode, this.handleUpdateImage)) {
- this.handleSetSelectedItems();
- }
- } else if (event.metaKey || event.ctrlKey) {
- if (event.shiftKey && event.key === 'z') {
- this.handleRedo();
- } else if (event.key === 'z') {
- this.handleUndo();
- } else if (event.key === 'c') {
- this.props.onCopyToClipboard();
- } else if (event.key === 'v') {
- this.changeToASelectMode();
- if (this.props.onPasteFromClipboard()) {
- this.handleUpdateImage();
- }
- } else if (event.key === 'a') {
- this.changeToASelectMode();
- event.preventDefault();
- this.selectAll();
- }
- }
- }
- changeToASelectMode () {
- if (isBitmap(this.props.format)) {
- if (this.props.mode !== Modes.BIT_SELECT) {
- this.props.changeMode(Modes.BIT_SELECT);
- }
- } else if (this.props.mode !== Modes.SELECT && this.props.mode !== Modes.RESHAPE) {
- this.props.changeMode(Modes.SELECT);
- }
- }
- selectAll () {
- if (isBitmap(this.props.format)) {
- selectAllBitmap(this.props.clearSelectedItems);
- this.handleSetSelectedItems();
- } else if (this.props.mode === Modes.RESHAPE) {
- if (selectAllSegments()) this.handleSetSelectedItems();
- } else {
- // Disable lint for easier to read logic
- if (selectAllItems()) this.handleSetSelectedItems(); // eslint-disable-line no-lonely-if
- }
- }
onMouseDown (event) {
if (event.target === paper.view.element &&
document.activeElement instanceof HTMLInputElement) {
@@ -427,8 +249,8 @@ class PaintEditor extends React.Component {
render () {
return (
({
changeColorToEyeDropper: state.scratchPaint.color.eyeDropper.callback,
- clipboardItems: state.scratchPaint.clipboard.items,
format: state.scratchPaint.format,
isEyeDropping: state.scratchPaint.color.eyeDropper.active,
mode: state.scratchPaint.mode,
- pasteOffset: state.scratchPaint.clipboard.pasteOffset,
previousTool: state.scratchPaint.color.eyeDropper.previousTool,
- selectedItems: state.scratchPaint.selectedItems,
- textEditing: state.scratchPaint.textEditTarget !== null,
- undoState: state.scratchPaint.undo,
viewBounds: state.scratchPaint.viewBounds
});
const mapDispatchToProps = dispatch => ({
@@ -545,21 +351,12 @@ const mapDispatchToProps = dispatch => ({
// set redux values to default for eye dropper reducer
dispatch(deactivateEyeDropper());
},
- onUndo: format => {
- dispatch(undo(format));
- },
- onRedo: format => {
- dispatch(redo(format));
- },
- undoSnapshot: snapshot => {
- dispatch(undoSnapshot(snapshot));
- },
updateViewBounds: matrix => {
dispatch(updateViewBounds(matrix));
}
});
-export default SelectionHOC(CopyPasteHOC(connect(
+export default UpdateImageHOC(SelectionHOC(UndoHOC(KeyboardShortcutsHOC(connect(
mapStateToProps,
mapDispatchToProps
-)(PaintEditor)));
+)(PaintEditor)))));
diff --git a/src/helper/bitmap.js b/src/helper/bitmap.js
index 8eb670cc..15ce0d45 100644
--- a/src/helper/bitmap.js
+++ b/src/helper/bitmap.js
@@ -3,6 +3,7 @@ import {createCanvas, clearRaster, getRaster, hideGuideLayers, showGuideLayers}
import {getGuideColor} from './guides';
import {clearSelection} from './selection';
import {inlineSvgFonts} from 'scratch-svg-renderer';
+import Formats from '../lib/format';
const forEachLinePoint = function (point1, point2, callback) {
// Bresenham line algorithm
@@ -374,7 +375,7 @@ const convertToBitmap = function (clearSelectedItems, onUpdateImage) {
new paper.Point(Math.floor(bounds.topLeft.x), Math.floor(bounds.topLeft.y)));
}
paper.project.activeLayer.removeChildren();
- onUpdateImage();
+ onUpdateImage(false /* skipSnapshot */, Formats.BITMAP /* formatOverride */);
};
img.onerror = () => {
// Fallback if browser does not support SVG data URIs in images.
@@ -385,7 +386,7 @@ const convertToBitmap = function (clearSelectedItems, onUpdateImage) {
getRaster().drawImage(raster.canvas, raster.bounds.topLeft);
}
paper.project.activeLayer.removeChildren();
- onUpdateImage();
+ onUpdateImage(false /* skipSnapshot */, Formats.BITMAP /* formatOverride */);
};
};
// Hash tags will break image loading without being encoded first
@@ -399,7 +400,7 @@ const convertToVector = function (clearSelectedItems, onUpdateImage) {
paper.project.activeLayer.addChild(trimmedRaster);
}
clearRaster();
- onUpdateImage();
+ onUpdateImage(false /* skipSnapshot */, Formats.VECTOR /* formatOverride */);
};
const getColor_ = function (x, y, context) {
diff --git a/src/containers/copy-paste-hoc.jsx b/src/hocs/copy-paste-hoc.jsx
similarity index 95%
rename from src/containers/copy-paste-hoc.jsx
rename to src/hocs/copy-paste-hoc.jsx
index 542f4d01..e71b75b2 100644
--- a/src/containers/copy-paste-hoc.jsx
+++ b/src/hocs/copy-paste-hoc.jsx
@@ -50,11 +50,10 @@ const CopyPasteHOC = function (WrappedComponent) {
this.props.setClipboardItems(clipboardItems);
}
}
- // Returns true if anything was pasted, false if nothing changed
handlePaste () {
clearSelection(this.props.clearSelectedItems);
- if (this.props.clipboardItems.length === 0) return false;
+ if (this.props.clipboardItems.length === 0) return;
let items = [];
for (let i = 0; i < this.props.clipboardItems.length; i++) {
@@ -63,7 +62,7 @@ const CopyPasteHOC = function (WrappedComponent) {
items.push(item);
}
}
- if (!items.length) return false;
+ if (!items.length) return;
// If pasting a group or non-raster to bitmap, rasterize first
if (isBitmap(this.props.format) && !(items.length === 1 && items[0] instanceof paper.Raster)) {
const group = new paper.Group(items);
@@ -78,13 +77,15 @@ const CopyPasteHOC = function (WrappedComponent) {
}
this.props.incrementPasteOffset();
this.props.setSelectedItems(this.props.format);
- return true;
+ this.props.onUpdateImage();
}
render () {
const componentProps = omit(this.props, [
'clearSelectedItems',
'clipboardItems',
+ 'format',
'incrementPasteOffset',
+ 'mode',
'pasteOffset',
'setClipboardItems',
'setSelectedItems']);
@@ -104,6 +105,7 @@ const CopyPasteHOC = function (WrappedComponent) {
format: PropTypes.oneOf(Object.keys(Formats)),
incrementPasteOffset: PropTypes.func.isRequired,
mode: PropTypes.oneOf(Object.keys(Modes)),
+ onUpdateImage: PropTypes.func.isRequired,
pasteOffset: PropTypes.number,
setClipboardItems: PropTypes.func.isRequired,
setSelectedItems: PropTypes.func.isRequired
diff --git a/src/hocs/keyboard-shortcuts-hoc.jsx b/src/hocs/keyboard-shortcuts-hoc.jsx
new file mode 100644
index 00000000..32c1bb7e
--- /dev/null
+++ b/src/hocs/keyboard-shortcuts-hoc.jsx
@@ -0,0 +1,132 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+import omit from 'lodash.omit';
+import {connect} from 'react-redux';
+
+import CopyPasteHOC from './copy-paste-hoc.jsx';
+
+import {selectAllBitmap} from '../helper/bitmap';
+import {clearSelection, deleteSelection, getSelectedLeafItems,
+ selectAllItems, selectAllSegments} from '../helper/selection';
+import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
+import {changeMode} from '../reducers/modes';
+
+import {isBitmap} from '../lib/format';
+import Formats from '../lib/format';
+import Modes from '../lib/modes';
+
+const KeyboardShortcutsHOC = function (WrappedComponent) {
+ class KeyboardShortcutsWrapper extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleKeyPress',
+ 'changeToASelectMode',
+ 'selectAll'
+ ]);
+ }
+ handleKeyPress (event) {
+ // Don't activate keyboard shortcuts during text editing
+ if (this.props.textEditing) return;
+
+ if (event.key === 'Escape') {
+ event.preventDefault();
+ clearSelection(this.props.clearSelectedItems);
+ } else if (event.key === 'Delete' || event.key === 'Backspace') {
+ if (deleteSelection(this.props.mode, this.props.onUpdateImage)) {
+ this.props.setSelectedItems(this.props.format);
+ }
+ } else if (event.metaKey || event.ctrlKey) {
+ if (event.shiftKey && event.key === 'z') {
+ this.props.onRedo();
+ } else if (event.key === 'z') {
+ this.props.onUndo();
+ } else if (event.key === 'c') {
+ this.props.onCopyToClipboard();
+ } else if (event.key === 'v') {
+ this.changeToASelectMode();
+ this.props.onPasteFromClipboard();
+ } else if (event.key === 'a') {
+ this.changeToASelectMode();
+ event.preventDefault();
+ this.selectAll();
+ }
+ }
+ }
+ changeToASelectMode () {
+ if (isBitmap(this.props.format)) {
+ if (this.props.mode !== Modes.BIT_SELECT) {
+ this.props.changeMode(Modes.BIT_SELECT);
+ }
+ } else if (this.props.mode !== Modes.SELECT && this.props.mode !== Modes.RESHAPE) {
+ this.props.changeMode(Modes.SELECT);
+ }
+ }
+ selectAll () {
+ if (isBitmap(this.props.format)) {
+ selectAllBitmap(this.props.clearSelectedItems);
+ this.props.setSelectedItems(this.props.format);
+ } else if (this.props.mode === Modes.RESHAPE) {
+ if (selectAllSegments()) this.props.setSelectedItems(this.props.format);
+ } else if (selectAllItems()) {
+ this.props.setSelectedItems(this.props.format);
+ }
+ }
+ render () {
+ const componentProps = omit(this.props, [
+ 'changeMode',
+ 'clearSelectedItems',
+ 'format',
+ 'mode',
+ 'onCopyToClipboard',
+ 'onPasteFromClipboard',
+ 'setSelectedItems',
+ 'textEditing']);
+ return (
+
+ );
+ }
+ }
+
+ KeyboardShortcutsWrapper.propTypes = {
+ changeMode: PropTypes.func.isRequired,
+ clearSelectedItems: PropTypes.func.isRequired,
+ format: PropTypes.oneOf(Object.keys(Formats)),
+ mode: PropTypes.oneOf(Object.keys(Modes)).isRequired,
+ onCopyToClipboard: PropTypes.func.isRequired,
+ onPasteFromClipboard: PropTypes.func.isRequired,
+ onRedo: PropTypes.func.isRequired,
+ onUndo: PropTypes.func.isRequired,
+ onUpdateImage: PropTypes.func.isRequired,
+ setSelectedItems: PropTypes.func.isRequired,
+ textEditing: PropTypes.bool.isRequired
+ };
+
+ const mapStateToProps = state => ({
+ mode: state.scratchPaint.mode,
+ format: state.scratchPaint.format,
+ textEditing: state.scratchPaint.textEditTarget !== null
+ });
+ const mapDispatchToProps = dispatch => ({
+ changeMode: mode => {
+ dispatch(changeMode(mode));
+ },
+ clearSelectedItems: () => {
+ dispatch(clearSelectedItems());
+ },
+ setSelectedItems: format => {
+ dispatch(setSelectedItems(getSelectedLeafItems(), isBitmap(format)));
+ }
+ });
+
+ return CopyPasteHOC(connect(
+ mapStateToProps,
+ mapDispatchToProps
+ )(KeyboardShortcutsWrapper));
+};
+
+export default KeyboardShortcutsHOC;
diff --git a/src/containers/selection-hoc.jsx b/src/hocs/selection-hoc.jsx
similarity index 100%
rename from src/containers/selection-hoc.jsx
rename to src/hocs/selection-hoc.jsx
diff --git a/src/hocs/undo-hoc.jsx b/src/hocs/undo-hoc.jsx
new file mode 100644
index 00000000..294d7ea9
--- /dev/null
+++ b/src/hocs/undo-hoc.jsx
@@ -0,0 +1,95 @@
+import bindAll from 'lodash.bindall';
+import PropTypes from 'prop-types';
+import React from 'react';
+import omit from 'lodash.omit';
+import {connect} from 'react-redux';
+
+import {getSelectedLeafItems} from '../helper/selection';
+import {setSelectedItems} from '../reducers/selected-items';
+import {performUndo, performRedo, shouldShowUndo, shouldShowRedo} from '../helper/undo';
+import {undo, redo} from '../reducers/undo';
+
+import {isBitmap} from '../lib/format';
+import Formats from '../lib/format';
+
+const UndoHOC = function (WrappedComponent) {
+ class UndoWrapper extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleUndo',
+ 'handleRedo',
+ 'handleSetSelectedItems',
+ 'shouldShowUndo',
+ 'shouldShowRedo'
+ ]);
+ }
+ handleUndo () {
+ performUndo(this.props.undoState, this.props.onUndo, this.handleSetSelectedItems, this.props.onUpdateImage);
+ }
+ handleRedo () {
+ performRedo(this.props.undoState, this.props.onRedo, this.handleSetSelectedItems, this.props.onUpdateImage);
+ }
+ handleSetSelectedItems () {
+ this.props.setSelectedItems(this.props.format);
+ }
+ shouldShowUndo () {
+ return shouldShowUndo(this.props.undoState);
+ }
+ shouldShowRedo () {
+ return shouldShowRedo(this.props.undoState);
+ }
+ render () {
+ const componentProps = omit(this.props, [
+ 'format',
+ 'onUndo',
+ 'onRedo',
+ 'setSelectedItems',
+ 'undoState']);
+ return (
+
+ );
+ }
+ }
+
+ UndoWrapper.propTypes = {
+ format: PropTypes.oneOf(Object.keys(Formats)),
+ onRedo: PropTypes.func.isRequired,
+ onUndo: PropTypes.func.isRequired,
+ onUpdateImage: PropTypes.func.isRequired,
+ setSelectedItems: PropTypes.func.isRequired,
+ undoState: PropTypes.shape({
+ stack: PropTypes.arrayOf(PropTypes.object).isRequired,
+ pointer: PropTypes.number.isRequired
+ })
+ };
+
+ const mapStateToProps = state => ({
+ format: state.scratchPaint.format,
+ undoState: state.scratchPaint.undo
+ });
+ const mapDispatchToProps = dispatch => ({
+ setSelectedItems: format => {
+ dispatch(setSelectedItems(getSelectedLeafItems(), isBitmap(format)));
+ },
+ onUndo: format => {
+ dispatch(undo(format));
+ },
+ onRedo: format => {
+ dispatch(redo(format));
+ }
+ });
+
+ return connect(
+ mapStateToProps,
+ mapDispatchToProps
+ )(UndoWrapper);
+};
+
+export default UndoHOC;
diff --git a/src/hocs/update-image-hoc.jsx b/src/hocs/update-image-hoc.jsx
new file mode 100644
index 00000000..d2c14c73
--- /dev/null
+++ b/src/hocs/update-image-hoc.jsx
@@ -0,0 +1,154 @@
+import paper from '@scratch/paper';
+import PropTypes from 'prop-types';
+import log from '../log/log';
+import bindAll from 'lodash.bindall';
+import React from 'react';
+import omit from 'lodash.omit';
+import {connect} from 'react-redux';
+
+import {undoSnapshot} from '../reducers/undo';
+import {setSelectedItems} from '../reducers/selected-items';
+
+import {getSelectedLeafItems} from '../helper/selection';
+import {getRaster, hideGuideLayers, showGuideLayers} from '../helper/layer';
+import {commitSelectionToBitmap, getHitBounds} from '../helper/bitmap';
+import {performSnapshot} from '../helper/undo';
+import {scaleWithStrokes} from '../helper/math';
+import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT, SVG_ART_BOARD_WIDTH, SVG_ART_BOARD_HEIGHT} from '../helper/view';
+
+import Modes from '../lib/modes';
+import {BitmapModes} from '../lib/modes';
+import Formats from '../lib/format';
+import {isBitmap, isVector} from '../lib/format';
+
+const UpdateImageHOC = function (WrappedComponent) {
+ class UpdateImageWrapper extends React.Component {
+ constructor (props) {
+ super(props);
+ bindAll(this, [
+ 'handleUpdateImage',
+ 'handleUpdateBitmap',
+ 'handleUpdateVector'
+ ]);
+ }
+ /**
+ * @param {?boolean} skipSnapshot True if the call to update image should not trigger saving
+ * an undo state. For instance after calling undo.
+ * @param {?Formats} formatOverride Normally the mode is used to determine the format of the image,
+ * but the format used can be overridden here. In particular when converting between formats,
+ * the does not accurately represent the format.
+ */
+ handleUpdateImage (skipSnapshot, formatOverride) {
+ // If in the middle of switching formats, rely on the current mode instead of format.
+ const actualFormat = formatOverride ? formatOverride :
+ BitmapModes[this.props.mode] ? Formats.BITMAP : Formats.VECTOR;
+ if (isBitmap(actualFormat)) {
+ this.handleUpdateBitmap(skipSnapshot);
+ } else if (isVector(actualFormat)) {
+ this.handleUpdateVector(skipSnapshot);
+ }
+ }
+ handleUpdateBitmap (skipSnapshot) {
+ if (!getRaster().loaded) {
+ // In general, callers of updateImage should wait for getRaster().loaded = true before
+ // calling updateImage.
+ // However, this may happen if the user is rapidly undoing/redoing. In this case it's safe
+ // to skip the update.
+ log.warn('Bitmap layer should be loaded before calling updateImage.');
+ return;
+ }
+ // Plaster the selection onto the raster layer before exporting, if there is a selection.
+ const plasteredRaster = getRaster().getSubRaster(getRaster().bounds);
+ plasteredRaster.remove(); // Don't insert
+ const selectedItems = getSelectedLeafItems();
+ if (selectedItems.length === 1 && selectedItems[0] instanceof paper.Raster) {
+ if (!selectedItems[0].loaded ||
+ (selectedItems[0].data &&
+ selectedItems[0].data.expanded &&
+ !selectedItems[0].data.expanded.loaded)) {
+ // This may get logged when rapidly undoing/redoing or changing costumes,
+ // in which case the warning is not relevant.
+ log.warn('Bitmap layer should be loaded before calling updateImage.');
+ return;
+ }
+ commitSelectionToBitmap(selectedItems[0], plasteredRaster);
+ }
+ const rect = getHitBounds(plasteredRaster);
+ this.props.onUpdateImage(
+ false /* isVector */,
+ plasteredRaster.getImageData(rect),
+ (ART_BOARD_WIDTH / 2) - rect.x,
+ (ART_BOARD_HEIGHT / 2) - rect.y);
+
+ if (!skipSnapshot) {
+ performSnapshot(this.props.undoSnapshot, Formats.BITMAP);
+ }
+ }
+ handleUpdateVector (skipSnapshot) {
+ const guideLayers = hideGuideLayers(true /* includeRaster */);
+
+ // Export at 0.5x
+ scaleWithStrokes(paper.project.activeLayer, .5, new paper.Point());
+ const bounds = paper.project.activeLayer.bounds;
+ // @todo (https://github.com/LLK/scratch-paint/issues/445) generate view box
+ this.props.onUpdateImage(
+ true /* isVector */,
+ paper.project.exportSVG({
+ asString: true,
+ bounds: 'content',
+ matrix: new paper.Matrix().translate(-bounds.x, -bounds.y)
+ }),
+ (SVG_ART_BOARD_WIDTH / 2) - bounds.x,
+ (SVG_ART_BOARD_HEIGHT / 2) - bounds.y);
+ scaleWithStrokes(paper.project.activeLayer, 2, new paper.Point());
+ paper.project.activeLayer.applyMatrix = true;
+
+ showGuideLayers(guideLayers);
+
+ if (!skipSnapshot) {
+ performSnapshot(this.props.undoSnapshot, Formats.VECTOR);
+ }
+ }
+ render () {
+ const componentProps = omit(this.props, [
+ 'format',
+ 'onUpdateImage',
+ 'undoSnapshot'
+ ]);
+ return (
+
+ );
+ }
+ }
+
+ UpdateImageWrapper.propTypes = {
+ format: PropTypes.oneOf(Object.keys(Formats)),
+ mode: PropTypes.oneOf(Object.keys(Modes)).isRequired,
+ onUpdateImage: PropTypes.func.isRequired,
+ undoSnapshot: PropTypes.func.isRequired
+ };
+
+ const mapStateToProps = state => ({
+ format: state.scratchPaint.format,
+ mode: state.scratchPaint.mode,
+ undoState: state.scratchPaint.undo
+ });
+ const mapDispatchToProps = dispatch => ({
+ setSelectedItems: format => {
+ dispatch(setSelectedItems(getSelectedLeafItems(), isBitmap(format)));
+ },
+ undoSnapshot: snapshot => {
+ dispatch(undoSnapshot(snapshot));
+ }
+ });
+
+ return connect(
+ mapStateToProps,
+ mapDispatchToProps
+ )(UpdateImageWrapper);
+};
+
+export default UpdateImageHOC;