mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 21:42:30 -05:00
add copy/paste
This commit is contained in:
parent
1a654a22ed
commit
7e1e8d96ac
8 changed files with 127 additions and 22 deletions
|
@ -1,3 +1,4 @@
|
||||||
|
import paper from '@scratch/paper';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {connect} from 'react-redux';
|
import {connect} from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
@ -115,16 +116,16 @@ const ModeToolsComponent = props => {
|
||||||
<div className={classNames(props.className, styles.modeTools)}>
|
<div className={classNames(props.className, styles.modeTools)}>
|
||||||
<InputGroup className={classNames(styles.modDashedBorder, styles.modLabeledIconHeight)}>
|
<InputGroup className={classNames(styles.modDashedBorder, styles.modLabeledIconHeight)}>
|
||||||
<LabeledIconButton
|
<LabeledIconButton
|
||||||
disabled
|
disabled={!props.selectedItems.length}
|
||||||
imgSrc={copyIcon}
|
imgSrc={copyIcon}
|
||||||
title={props.intl.formatMessage(messages.copy)}
|
title={props.intl.formatMessage(messages.copy)}
|
||||||
onClick={function () {}}
|
onClick={props.onCopyToClipboard}
|
||||||
/>
|
/>
|
||||||
<LabeledIconButton
|
<LabeledIconButton
|
||||||
disabled
|
disabled={!(props.clipboard.length > 0)}
|
||||||
imgSrc={pasteIcon}
|
imgSrc={pasteIcon}
|
||||||
title={props.intl.formatMessage(messages.paste)}
|
title={props.intl.formatMessage(messages.paste)}
|
||||||
onClick={function () {}}
|
onClick={props.onPasteFromClipboard}
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
{/* <LabeledIconButton
|
{/* <LabeledIconButton
|
||||||
|
@ -152,17 +153,23 @@ const ModeToolsComponent = props => {
|
||||||
ModeToolsComponent.propTypes = {
|
ModeToolsComponent.propTypes = {
|
||||||
brushValue: PropTypes.number,
|
brushValue: PropTypes.number,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
clipboard: PropTypes.arrayOf(PropTypes.array),
|
||||||
eraserValue: PropTypes.number,
|
eraserValue: PropTypes.number,
|
||||||
intl: intlShape.isRequired,
|
intl: intlShape.isRequired,
|
||||||
mode: PropTypes.string.isRequired,
|
mode: PropTypes.string.isRequired,
|
||||||
onBrushSliderChange: PropTypes.func,
|
onBrushSliderChange: PropTypes.func,
|
||||||
onEraserSliderChange: PropTypes.func
|
onCopyToClipboard: PropTypes.func.isRequired,
|
||||||
|
onEraserSliderChange: PropTypes.func,
|
||||||
|
onPasteFromClipboard: PropTypes.func.isRequired,
|
||||||
|
selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item))
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
mode: state.scratchPaint.mode,
|
mode: state.scratchPaint.mode,
|
||||||
brushValue: state.scratchPaint.brushMode.brushSize,
|
brushValue: state.scratchPaint.brushMode.brushSize,
|
||||||
eraserValue: state.scratchPaint.eraserMode.brushSize
|
clipboard: state.scratchPaint.clipboard,
|
||||||
|
eraserValue: state.scratchPaint.eraserMode.brushSize,
|
||||||
|
selectedItems: state.scratchPaint.selectedItems
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onBrushSliderChange: brushSize => {
|
onBrushSliderChange: brushSize => {
|
||||||
|
|
|
@ -247,7 +247,10 @@ class PaintEditorComponent extends React.Component {
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<InputGroup className={styles.modModeTools}>
|
<InputGroup className={styles.modModeTools}>
|
||||||
<ModeToolsComponent />
|
<ModeToolsComponent
|
||||||
|
onCopyToClipboard={this.props.onCopyToClipboard}
|
||||||
|
onPasteFromClipboard={this.props.onPasteFromClipboard}
|
||||||
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -339,7 +342,9 @@ PaintEditorComponent.propTypes = {
|
||||||
canUndo: PropTypes.func.isRequired,
|
canUndo: PropTypes.func.isRequired,
|
||||||
intl: intlShape,
|
intl: intlShape,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
|
onCopyToClipboard: PropTypes.func.isRequired,
|
||||||
onGroup: PropTypes.func.isRequired,
|
onGroup: PropTypes.func.isRequired,
|
||||||
|
onPasteFromClipboard: PropTypes.func.isRequired,
|
||||||
onRedo: PropTypes.func.isRequired,
|
onRedo: PropTypes.func.isRequired,
|
||||||
onSendBackward: PropTypes.func.isRequired,
|
onSendBackward: PropTypes.func.isRequired,
|
||||||
onSendForward: PropTypes.func.isRequired,
|
onSendForward: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -5,12 +5,13 @@ import PaintEditorComponent from '../components/paint-editor/paint-editor.jsx';
|
||||||
import {changeMode} from '../reducers/modes';
|
import {changeMode} from '../reducers/modes';
|
||||||
import {undo, redo, undoSnapshot} from '../reducers/undo';
|
import {undo, redo, undoSnapshot} from '../reducers/undo';
|
||||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||||
|
import {setClipboardItems} from '../reducers/clipboard';
|
||||||
|
|
||||||
import {getGuideLayer, getBackgroundGuideLayer} from '../helper/layer';
|
import {getGuideLayer, getBackgroundGuideLayer} from '../helper/layer';
|
||||||
import {performUndo, performRedo, performSnapshot, shouldShowUndo, shouldShowRedo} from '../helper/undo';
|
import {performUndo, performRedo, performSnapshot, shouldShowUndo, shouldShowRedo} from '../helper/undo';
|
||||||
import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order';
|
import {bringToFront, sendBackward, sendToBack, bringForward} from '../helper/order';
|
||||||
import {groupSelection, ungroupSelection} from '../helper/group';
|
import {groupSelection, ungroupSelection} from '../helper/group';
|
||||||
import {getSelectedLeafItems} from '../helper/selection';
|
import {clearSelection, getSelectedLeafItems, getSelectedRootItems} from '../helper/selection';
|
||||||
import {resetZoom, zoomOnSelection} from '../helper/view';
|
import {resetZoom, zoomOnSelection} from '../helper/view';
|
||||||
|
|
||||||
import Modes from '../modes/modes';
|
import Modes from '../modes/modes';
|
||||||
|
@ -35,7 +36,9 @@ class PaintEditor extends React.Component {
|
||||||
'handleGroup',
|
'handleGroup',
|
||||||
'handleUngroup',
|
'handleUngroup',
|
||||||
'canRedo',
|
'canRedo',
|
||||||
'canUndo'
|
'canUndo',
|
||||||
|
'handleCopyToClipboard',
|
||||||
|
'handlePasteFromClipboard'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
@ -98,6 +101,33 @@ class PaintEditor extends React.Component {
|
||||||
handleSendToFront () {
|
handleSendToFront () {
|
||||||
bringToFront(this.handleUpdateSvg);
|
bringToFront(this.handleUpdateSvg);
|
||||||
}
|
}
|
||||||
|
handleCopyToClipboard () {
|
||||||
|
const selectedItems = getSelectedRootItems();
|
||||||
|
if (selectedItems.length > 0) {
|
||||||
|
const clipboardItems = [];
|
||||||
|
for (let i = 0; i < selectedItems.length; i++) {
|
||||||
|
const jsonItem = selectedItems[i].exportJSON({asString: false});
|
||||||
|
clipboardItems.push(jsonItem);
|
||||||
|
}
|
||||||
|
this.props.setClipboardItems(clipboardItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handlePasteFromClipboard () {
|
||||||
|
clearSelection(this.props.clearSelectedItems);
|
||||||
|
|
||||||
|
if (this.props.clipboard.length > 0) {
|
||||||
|
for (let i = 0; i < this.props.clipboard.length; i++) {
|
||||||
|
const item = paper.Base.importJSON(this.props.clipboard[i]);
|
||||||
|
if (item) {
|
||||||
|
item.selected = true;
|
||||||
|
}
|
||||||
|
paper.project.getActiveLayer().addChild(item);
|
||||||
|
}
|
||||||
|
this.props.setSelectedItems();
|
||||||
|
paper.project.view.update();
|
||||||
|
this.handleUpdateSvg();
|
||||||
|
}
|
||||||
|
}
|
||||||
canUndo () {
|
canUndo () {
|
||||||
return shouldShowUndo(this.props.undoState);
|
return shouldShowUndo(this.props.undoState);
|
||||||
}
|
}
|
||||||
|
@ -123,7 +153,9 @@ class PaintEditor extends React.Component {
|
||||||
rotationCenterY={this.props.rotationCenterY}
|
rotationCenterY={this.props.rotationCenterY}
|
||||||
svg={this.props.svg}
|
svg={this.props.svg}
|
||||||
svgId={this.props.svgId}
|
svgId={this.props.svgId}
|
||||||
|
onCopyToClipboard={this.handleCopyToClipboard}
|
||||||
onGroup={this.handleGroup}
|
onGroup={this.handleGroup}
|
||||||
|
onPasteFromClipboard={this.handlePasteFromClipboard}
|
||||||
onRedo={this.handleRedo}
|
onRedo={this.handleRedo}
|
||||||
onSendBackward={this.handleSendBackward}
|
onSendBackward={this.handleSendBackward}
|
||||||
onSendForward={this.handleSendForward}
|
onSendForward={this.handleSendForward}
|
||||||
|
@ -143,6 +175,7 @@ class PaintEditor extends React.Component {
|
||||||
|
|
||||||
PaintEditor.propTypes = {
|
PaintEditor.propTypes = {
|
||||||
clearSelectedItems: PropTypes.func.isRequired,
|
clearSelectedItems: PropTypes.func.isRequired,
|
||||||
|
clipboard: PropTypes.arrayOf(PropTypes.array),
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
onKeyPress: PropTypes.func.isRequired,
|
onKeyPress: PropTypes.func.isRequired,
|
||||||
onRedo: PropTypes.func.isRequired,
|
onRedo: PropTypes.func.isRequired,
|
||||||
|
@ -151,6 +184,7 @@ PaintEditor.propTypes = {
|
||||||
onUpdateSvg: PropTypes.func.isRequired,
|
onUpdateSvg: PropTypes.func.isRequired,
|
||||||
rotationCenterX: PropTypes.number,
|
rotationCenterX: PropTypes.number,
|
||||||
rotationCenterY: PropTypes.number,
|
rotationCenterY: PropTypes.number,
|
||||||
|
setClipboardItems: PropTypes.func.isRequired,
|
||||||
setSelectedItems: PropTypes.func.isRequired,
|
setSelectedItems: PropTypes.func.isRequired,
|
||||||
svg: PropTypes.string,
|
svg: PropTypes.string,
|
||||||
svgId: PropTypes.string,
|
svgId: PropTypes.string,
|
||||||
|
@ -163,7 +197,8 @@ PaintEditor.propTypes = {
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
selectedItems: state.scratchPaint.selectedItems,
|
selectedItems: state.scratchPaint.selectedItems,
|
||||||
undoState: state.scratchPaint.undo
|
undoState: state.scratchPaint.undo,
|
||||||
|
clipboard: state.scratchPaint.clipboard
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onKeyPress: event => {
|
onKeyPress: event => {
|
||||||
|
@ -191,6 +226,9 @@ const mapDispatchToProps = dispatch => ({
|
||||||
},
|
},
|
||||||
undoSnapshot: snapshot => {
|
undoSnapshot: snapshot => {
|
||||||
dispatch(undoSnapshot(snapshot));
|
dispatch(undoSnapshot(snapshot));
|
||||||
|
},
|
||||||
|
setClipboardItems: items => {
|
||||||
|
dispatch(setClipboardItems(items));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -157,7 +157,7 @@ class PaperCanvas extends React.Component {
|
||||||
PaperCanvas.propTypes = {
|
PaperCanvas.propTypes = {
|
||||||
canvasRef: PropTypes.func,
|
canvasRef: PropTypes.func,
|
||||||
clearUndo: PropTypes.func.isRequired,
|
clearUndo: PropTypes.func.isRequired,
|
||||||
mode: PropTypes.instanceOf(Modes),
|
mode: PropTypes.oneOf(Object.values(Modes)),
|
||||||
onUpdateSvg: PropTypes.func.isRequired,
|
onUpdateSvg: PropTypes.func.isRequired,
|
||||||
rotationCenterX: PropTypes.number,
|
rotationCenterX: PropTypes.number,
|
||||||
rotationCenterY: PropTypes.number,
|
rotationCenterY: PropTypes.number,
|
||||||
|
|
|
@ -394,14 +394,6 @@ const selectRootItem = function () {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldShowIfSelection = function () {
|
|
||||||
return getSelectedRootItems().length > 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const shouldShowIfSelectionRecursive = function () {
|
|
||||||
return getSelectedRootItems().length > 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const shouldShowSelectAll = function () {
|
const shouldShowSelectAll = function () {
|
||||||
return paper.project.getItems({class: paper.PathItem}).length > 0;
|
return paper.project.getItems({class: paper.PathItem}).length > 0;
|
||||||
};
|
};
|
||||||
|
@ -419,7 +411,5 @@ export {
|
||||||
getSelectedRootItems,
|
getSelectedRootItems,
|
||||||
processRectangularSelection,
|
processRectangularSelection,
|
||||||
selectRootItem,
|
selectRootItem,
|
||||||
shouldShowIfSelection,
|
|
||||||
shouldShowIfSelectionRecursive,
|
|
||||||
shouldShowSelectAll
|
shouldShowSelectAll
|
||||||
};
|
};
|
||||||
|
|
31
src/reducers/clipboard.js
Normal file
31
src/reducers/clipboard.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import log from '../log/log';
|
||||||
|
|
||||||
|
const SET = 'scratch-paint/clipboard/SET';
|
||||||
|
const initialState = [];
|
||||||
|
|
||||||
|
const reducer = function (state, action) {
|
||||||
|
if (typeof state === 'undefined') state = initialState;
|
||||||
|
switch (action.type) {
|
||||||
|
case SET:
|
||||||
|
if (!action.clipboardItems || !(action.clipboardItems instanceof Array) || action.clipboardItems.length === 0) {
|
||||||
|
log.warn(`Invalid clipboard item format`);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return action.clipboardItems;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Action creators ==================================
|
||||||
|
const setClipboardItems = function (clipboardItems) {
|
||||||
|
return {
|
||||||
|
type: SET,
|
||||||
|
clipboardItems: clipboardItems
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
reducer as default,
|
||||||
|
setClipboardItems
|
||||||
|
};
|
|
@ -3,6 +3,7 @@ import modeReducer from './modes';
|
||||||
import brushModeReducer from './brush-mode';
|
import brushModeReducer from './brush-mode';
|
||||||
import eraserModeReducer from './eraser-mode';
|
import eraserModeReducer from './eraser-mode';
|
||||||
import colorReducer from './color';
|
import colorReducer from './color';
|
||||||
|
import clipboardReducer from './clipboard';
|
||||||
import hoverReducer from './hover';
|
import hoverReducer from './hover';
|
||||||
import modalsReducer from './modals';
|
import modalsReducer from './modals';
|
||||||
import selectedItemReducer from './selected-items';
|
import selectedItemReducer from './selected-items';
|
||||||
|
@ -11,8 +12,9 @@ import undoReducer from './undo';
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
mode: modeReducer,
|
mode: modeReducer,
|
||||||
brushMode: brushModeReducer,
|
brushMode: brushModeReducer,
|
||||||
eraserMode: eraserModeReducer,
|
|
||||||
color: colorReducer,
|
color: colorReducer,
|
||||||
|
clipboard: clipboardReducer,
|
||||||
|
eraserMode: eraserModeReducer,
|
||||||
hoveredItemId: hoverReducer,
|
hoveredItemId: hoverReducer,
|
||||||
modals: modalsReducer,
|
modals: modalsReducer,
|
||||||
selectedItems: selectedItemReducer,
|
selectedItems: selectedItemReducer,
|
||||||
|
|
32
test/unit/clipboard-reducer.test.js
Normal file
32
test/unit/clipboard-reducer.test.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/* eslint-env jest */
|
||||||
|
import clipboardReducer from '../../src/reducers/clipboard';
|
||||||
|
import {setClipboardItems} from '../../src/reducers/clipboard';
|
||||||
|
|
||||||
|
test('initialState', () => {
|
||||||
|
let defaultState;
|
||||||
|
|
||||||
|
expect(clipboardReducer(defaultState /* state */, {type: 'anything'} /* action */)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setClipboardItems', () => {
|
||||||
|
let defaultState;
|
||||||
|
|
||||||
|
const newSelected1 = ['selected1', 'selected2'];
|
||||||
|
const newSelected2 = ['selected1', 'selected3'];
|
||||||
|
expect(clipboardReducer(defaultState /* state */, setClipboardItems(newSelected1) /* action */))
|
||||||
|
.toEqual(newSelected1);
|
||||||
|
expect(clipboardReducer(newSelected1, setClipboardItems(newSelected2) /* action */))
|
||||||
|
.toEqual(newSelected2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalidSetClipboardItems', () => {
|
||||||
|
const origState = ['selected1', 'selected2'];
|
||||||
|
const nothingSelected = [];
|
||||||
|
|
||||||
|
expect(clipboardReducer(origState /* state */, setClipboardItems() /* action */))
|
||||||
|
.toBe(origState);
|
||||||
|
expect(clipboardReducer(origState /* state */, setClipboardItems('notAnArray') /* action */))
|
||||||
|
.toBe(origState);
|
||||||
|
expect(clipboardReducer(origState /* state */, setClipboardItems(nothingSelected) /* action */))
|
||||||
|
.toBe(origState);
|
||||||
|
});
|
Loading…
Reference in a new issue