Break paint-editor.jsx code out into HOCs (#633)

This commit is contained in:
DD Liu 2018-08-30 17:51:11 -04:00 committed by GitHub
parent c6458ddebc
commit 20a98db397
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 555 additions and 255 deletions

View file

@ -24,7 +24,7 @@ import FillMode from '../../containers/fill-mode.jsx';
import InputGroup from '../input-group/input-group.jsx';
import LineMode from '../../containers/line-mode.jsx';
import Loupe from '../loupe/loupe.jsx';
import FixedToolsComponent from '../fixed-tools/fixed-tools.jsx';
import FixedToolsContainer from '../../containers/fixed-tools.jsx';
import ModeToolsContainer from '../../containers/mode-tools.jsx';
import OvalMode from '../../containers/oval-mode.jsx';
import RectMode from '../../containers/rect-mode.jsx';
@ -65,18 +65,12 @@ const PaintEditorComponent = props => (
<div className={styles.editorContainerTop}>
{/* First row */}
<div className={styles.row}>
<FixedToolsComponent
<FixedToolsContainer
canRedo={props.canRedo}
canUndo={props.canUndo}
name={props.name}
onGroup={props.onGroup}
onRedo={props.onRedo}
onSendBackward={props.onSendBackward}
onSendForward={props.onSendForward}
onSendToBack={props.onSendToBack}
onSendToFront={props.onSendToFront}
onUndo={props.onUndo}
onUngroup={props.onUngroup}
onUpdateImage={props.onUpdateImage}
onUpdateName={props.onUpdateName}
/>
@ -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,

View file

@ -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 (
<FixedToolsComponent
canRedo={this.props.canRedo}
canUndo={this.props.canUndo}
name={this.props.name}
onGroup={this.handleGroup}
onRedo={this.props.onRedo}
onSendBackward={this.handleSendBackward}
onSendForward={this.handleSendForward}
onSendToBack={this.handleSendToBack}
onSendToFront={this.handleSendToFront}
onUndo={this.props.onUndo}
onUngroup={this.handleUngroup}
onUpdateImage={this.props.onUpdateImage}
onUpdateName={this.props.onUpdateName}
/>
);
}
}
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);

View file

@ -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';

View file

@ -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 (
<PaintEditorComponent
canRedo={this.canRedo}
canUndo={this.canUndo}
canRedo={this.props.shouldShowRedo}
canUndo={this.props.shouldShowUndo}
canvas={this.state.canvas}
colorInfo={this.state.colorInfo}
format={this.props.format}
@ -443,17 +265,11 @@ class PaintEditor extends React.Component {
setCanvas={this.setCanvas}
setTextArea={this.setTextArea}
textArea={this.state.textArea}
onGroup={this.handleGroup}
onRedo={this.handleRedo}
onSendBackward={this.handleSendBackward}
onSendForward={this.handleSendForward}
onSendToBack={this.handleSendToBack}
onSendToFront={this.handleSendToFront}
onRedo={this.props.onRedo}
onSwitchToBitmap={this.props.handleSwitchToBitmap}
onSwitchToVector={this.props.handleSwitchToVector}
onUndo={this.handleUndo}
onUngroup={this.handleUngroup}
onUpdateImage={this.handleUpdateImage}
onUndo={this.props.onUndo}
onUpdateImage={this.props.onUpdateImage}
onUpdateName={this.props.onUpdateName}
onZoomIn={this.handleZoomIn}
onZoomOut={this.handleZoomOut}
@ -479,9 +295,8 @@ PaintEditor.propTypes = {
isEyeDropping: PropTypes.bool,
mode: PropTypes.oneOf(Object.keys(Modes)).isRequired,
name: PropTypes.string,
onCopyToClipboard: PropTypes.func.isRequired,
onDeactivateEyeDropper: PropTypes.func.isRequired,
onPasteFromClipboard: PropTypes.func.isRequired,
onKeyPress: PropTypes.func.isRequired,
onRedo: PropTypes.func.isRequired,
onUndo: PropTypes.func.isRequired,
onUpdateImage: PropTypes.func.isRequired,
@ -496,27 +311,18 @@ PaintEditor.propTypes = {
rtl: PropTypes.bool,
setLayout: PropTypes.func.isRequired,
setSelectedItems: PropTypes.func.isRequired,
textEditing: PropTypes.bool.isRequired,
undoSnapshot: PropTypes.func.isRequired,
undoState: PropTypes.shape({
stack: PropTypes.arrayOf(PropTypes.object).isRequired,
pointer: PropTypes.number.isRequired
}),
shouldShowRedo: PropTypes.func.isRequired,
shouldShowUndo: PropTypes.func.isRequired,
updateViewBounds: PropTypes.func.isRequired,
viewBounds: PropTypes.instanceOf(paper.Matrix).isRequired
};
const mapStateToProps = state => ({
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)))));

View file

@ -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) {

View file

@ -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

View file

@ -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 (
<WrappedComponent
onKeyPress={this.handleKeyPress}
{...componentProps}
/>
);
}
}
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;

95
src/hocs/undo-hoc.jsx Normal file
View file

@ -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 (
<WrappedComponent
shouldShowRedo={this.shouldShowRedo}
shouldShowUndo={this.shouldShowUndo}
onRedo={this.handleRedo}
onUndo={this.handleUndo}
{...componentProps}
/>
);
}
}
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;

View file

@ -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 (
<WrappedComponent
onUpdateImage={this.handleUpdateImage}
{...componentProps}
/>
);
}
}
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;