mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2024-12-22 21:42:30 -05:00
Make the text tool a real tool
This commit is contained in:
parent
0d3b276e5e
commit
8e7ae67ae6
5 changed files with 289 additions and 26 deletions
|
@ -33,7 +33,7 @@ import ReshapeMode from '../../containers/reshape-mode.jsx';
|
|||
import SelectMode from '../../containers/select-mode.jsx';
|
||||
import StrokeColorIndicatorComponent from '../../containers/stroke-color-indicator.jsx';
|
||||
import StrokeWidthIndicatorComponent from '../../containers/stroke-width-indicator.jsx';
|
||||
import TextModeComponent from '../text-mode/text-mode.jsx';
|
||||
import TextMode from '../../containers/text-mode.jsx';
|
||||
|
||||
import layout from '../../lib/layout-constants';
|
||||
import styles from './paint-editor.css';
|
||||
|
@ -350,11 +350,13 @@ const PaintEditorComponent = props => {
|
|||
<EraserMode
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
{/* Text mode will go here */}
|
||||
<LineMode
|
||||
<FillMode
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
<FillMode
|
||||
<TextMode
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
<LineMode
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
<OvalMode
|
||||
|
@ -363,8 +365,6 @@ const PaintEditorComponent = props => {
|
|||
<RectMode
|
||||
onUpdateSvg={props.onUpdateSvg}
|
||||
/>
|
||||
{/* text tool, coming soon */}
|
||||
<TextModeComponent />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
|
|
@ -1,27 +1,25 @@
|
|||
import React from 'react';
|
||||
|
||||
import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import ToolSelectComponent from '../tool-select-base/tool-select-base.jsx';
|
||||
|
||||
import textIcon from './text.svg';
|
||||
|
||||
const TextModeComponent = () => (
|
||||
<ComingSoonTooltip
|
||||
place="right"
|
||||
tooltipId="text-mode-select"
|
||||
>
|
||||
<ToolSelectComponent
|
||||
disabled
|
||||
imgDescriptor={{
|
||||
defaultMessage: 'Text',
|
||||
description: 'Label for the text tool',
|
||||
id: 'paint.textMode.text'
|
||||
}}
|
||||
imgSrc={textIcon}
|
||||
isSelected={false}
|
||||
onMouseDown={function () {}} // eslint-disable-line react/jsx-no-bind
|
||||
/>
|
||||
</ComingSoonTooltip>
|
||||
const TextModeComponent = props => (
|
||||
<ToolSelectComponent
|
||||
imgDescriptor={{
|
||||
defaultMessage: 'Text',
|
||||
description: 'Label for the text tool',
|
||||
id: 'paint.textMode.text'
|
||||
}}
|
||||
imgSrc={textIcon}
|
||||
isSelected={props.isSelected}
|
||||
onMouseDown={props.onMouseDown}
|
||||
/>
|
||||
);
|
||||
|
||||
TextModeComponent.propTypes = {
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
onMouseDown: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default TextModeComponent;
|
||||
|
|
130
src/containers/text-mode.jsx
Normal file
130
src/containers/text-mode.jsx
Normal file
|
@ -0,0 +1,130 @@
|
|||
import paper from '@scratch/paper';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import bindAll from 'lodash.bindall';
|
||||
import Modes from '../lib/modes';
|
||||
import {MIXED} from '../helper/style-path';
|
||||
|
||||
import {changeFillColor, DEFAULT_COLOR} from '../reducers/fill-color';
|
||||
import {changeStrokeColor} from '../reducers/stroke-color';
|
||||
import {changeMode} from '../reducers/modes';
|
||||
import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items';
|
||||
|
||||
import {clearSelection, getSelectedLeafItems} from '../helper/selection';
|
||||
import TextTool from '../helper/tools/text-tool';
|
||||
import TextModeComponent from '../components/text-mode/text-mode.jsx';
|
||||
|
||||
class TextMode extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'activateTool',
|
||||
'deactivateTool'
|
||||
]);
|
||||
}
|
||||
componentDidMount () {
|
||||
if (this.props.isTextModeActive) {
|
||||
this.activateTool(this.props);
|
||||
}
|
||||
}
|
||||
componentWillReceiveProps (nextProps) {
|
||||
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.isTextModeActive && !this.props.isTextModeActive) {
|
||||
this.activateTool();
|
||||
} else if (!nextProps.isTextModeActive && this.props.isTextModeActive) {
|
||||
this.deactivateTool();
|
||||
}
|
||||
}
|
||||
shouldComponentUpdate (nextProps) {
|
||||
return nextProps.isTextModeActive !== this.props.isTextModeActive;
|
||||
}
|
||||
activateTool () {
|
||||
clearSelection(this.props.clearSelectedItems);
|
||||
// If fill and stroke color are both mixed/transparent/absent, set fill to default and stroke to transparent.
|
||||
// If exactly one of fill or stroke color is set, set the other one to transparent.
|
||||
// This way the tool won't draw an invisible state, or be unclear about what will be drawn.
|
||||
const {fillColor, strokeColor, strokeWidth} = this.props.colorState;
|
||||
const fillColorPresent = fillColor !== MIXED && fillColor !== null;
|
||||
const strokeColorPresent =
|
||||
strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0;
|
||||
if (!fillColorPresent && !strokeColorPresent) {
|
||||
this.props.onChangeFillColor(DEFAULT_COLOR);
|
||||
this.props.onChangeStrokeColor(null);
|
||||
} else if (!fillColorPresent && strokeColorPresent) {
|
||||
this.props.onChangeFillColor(null);
|
||||
} else if (fillColorPresent && !strokeColorPresent) {
|
||||
this.props.onChangeStrokeColor(null);
|
||||
}
|
||||
this.tool = new TextTool(
|
||||
this.props.setSelectedItems,
|
||||
this.props.clearSelectedItems,
|
||||
this.props.onUpdateSvg
|
||||
);
|
||||
this.tool.setColorState(this.props.colorState);
|
||||
this.tool.activate();
|
||||
}
|
||||
deactivateTool () {
|
||||
this.tool.deactivateTool();
|
||||
this.tool.remove();
|
||||
this.tool = null;
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<TextModeComponent
|
||||
isSelected={this.props.isTextModeActive}
|
||||
onMouseDown={this.props.handleMouseDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TextMode.propTypes = {
|
||||
clearSelectedItems: PropTypes.func.isRequired,
|
||||
colorState: PropTypes.shape({
|
||||
fillColor: PropTypes.string,
|
||||
strokeColor: PropTypes.string,
|
||||
strokeWidth: PropTypes.number
|
||||
}).isRequired,
|
||||
handleMouseDown: PropTypes.func.isRequired,
|
||||
isTextModeActive: PropTypes.bool.isRequired,
|
||||
onChangeFillColor: PropTypes.func.isRequired,
|
||||
onChangeStrokeColor: PropTypes.func.isRequired,
|
||||
onUpdateSvg: PropTypes.func.isRequired,
|
||||
selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)),
|
||||
setSelectedItems: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
colorState: state.scratchPaint.color,
|
||||
isTextModeActive: state.scratchPaint.mode === Modes.TEXT,
|
||||
selectedItems: state.scratchPaint.selectedItems
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
clearSelectedItems: () => {
|
||||
dispatch(clearSelectedItems());
|
||||
},
|
||||
setSelectedItems: () => {
|
||||
dispatch(setSelectedItems(getSelectedLeafItems()));
|
||||
},
|
||||
handleMouseDown: () => {
|
||||
dispatch(changeMode(Modes.TEXT));
|
||||
},
|
||||
onChangeFillColor: fillColor => {
|
||||
dispatch(changeFillColor(fillColor));
|
||||
},
|
||||
onChangeStrokeColor: strokeColor => {
|
||||
dispatch(changeStrokeColor(strokeColor));
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TextMode);
|
134
src/helper/tools/text-tool.js
Normal file
134
src/helper/tools/text-tool.js
Normal file
|
@ -0,0 +1,134 @@
|
|||
import paper from '@scratch/paper';
|
||||
import Modes from '../../lib/modes';
|
||||
import {styleShape} from '../style-path';
|
||||
import {clearSelection} from '../selection';
|
||||
import BoundingBoxTool from '../selection-tools/bounding-box-tool';
|
||||
import NudgeTool from '../selection-tools/nudge-tool';
|
||||
|
||||
/**
|
||||
* Tool for adding text. Text elements have limited editability; they can't be reshaped,
|
||||
* drawn on or erased. This way they can preserve their ability to have the text edited.
|
||||
*/
|
||||
class TextTool extends paper.Tool {
|
||||
static get TOLERANCE () {
|
||||
return 6;
|
||||
}
|
||||
/**
|
||||
* @param {function} setSelectedItems Callback to set the set of selected items in the Redux state
|
||||
* @param {function} clearSelectedItems Callback to clear the set of selected items in the Redux state
|
||||
* @param {!function} onUpdateSvg A callback to call when the image visibly changes
|
||||
*/
|
||||
constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) {
|
||||
super();
|
||||
this.setSelectedItems = setSelectedItems;
|
||||
this.clearSelectedItems = clearSelectedItems;
|
||||
this.onUpdateSvg = onUpdateSvg;
|
||||
this.boundingBoxTool = new BoundingBoxTool(Modes.TEXT, setSelectedItems, clearSelectedItems, onUpdateSvg);
|
||||
const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateSvg);
|
||||
|
||||
// We have to set these functions instead of just declaring them because
|
||||
// paper.js tools hook up the listeners in the setter functions.
|
||||
this.onMouseDown = this.handleMouseDown;
|
||||
this.onMouseDrag = this.handleMouseDrag;
|
||||
this.onMouseUp = this.handleMouseUp;
|
||||
this.onKeyUp = nudgeTool.onKeyUp;
|
||||
this.onKeyDown = nudgeTool.onKeyDown;
|
||||
|
||||
this.oval = null;
|
||||
this.colorState = null;
|
||||
this.isBoundingBoxMode = null;
|
||||
this.active = false;
|
||||
}
|
||||
getHitOptions () {
|
||||
return {
|
||||
segments: true,
|
||||
stroke: true,
|
||||
curves: true,
|
||||
fill: true,
|
||||
guide: false,
|
||||
match: hitResult =>
|
||||
(hitResult.item.data && hitResult.item.data.isHelperItem) ||
|
||||
hitResult.item.selected, // Allow hits on bounding box and selected only
|
||||
tolerance: TextTool.TOLERANCE / paper.view.zoom
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Should be called if the selection changes to update the bounds of the bounding box.
|
||||
* @param {Array<paper.Item>} selectedItems Array of selected items.
|
||||
*/
|
||||
onSelectionChanged (selectedItems) {
|
||||
this.boundingBoxTool.onSelectionChanged(selectedItems);
|
||||
}
|
||||
setColorState (colorState) {
|
||||
this.colorState = colorState;
|
||||
}
|
||||
handleMouseDown (event) {
|
||||
if (event.event.button > 0) return; // only first mouse button
|
||||
this.active = true;
|
||||
|
||||
if (this.boundingBoxTool.onMouseDown(event, false /* clone */, false /* multiselect */, this.getHitOptions())) {
|
||||
this.isBoundingBoxMode = true;
|
||||
} else {
|
||||
this.isBoundingBoxMode = false;
|
||||
clearSelection(this.clearSelectedItems);
|
||||
this.oval = new paper.Shape.Ellipse({
|
||||
point: event.downPoint,
|
||||
size: 0
|
||||
});
|
||||
styleShape(this.oval, this.colorState);
|
||||
}
|
||||
}
|
||||
handleMouseDrag (event) {
|
||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||
|
||||
if (this.isBoundingBoxMode) {
|
||||
this.boundingBoxTool.onMouseDrag(event);
|
||||
return;
|
||||
}
|
||||
|
||||
const downPoint = new paper.Point(event.downPoint.x, event.downPoint.y);
|
||||
const point = new paper.Point(event.point.x, event.point.y);
|
||||
if (event.modifiers.shift) {
|
||||
this.oval.size = new paper.Point(event.downPoint.x - event.point.x, event.downPoint.x - event.point.x);
|
||||
} else {
|
||||
this.oval.size = downPoint.subtract(point);
|
||||
}
|
||||
if (event.modifiers.alt) {
|
||||
this.oval.position = downPoint;
|
||||
} else {
|
||||
this.oval.position = downPoint.subtract(this.oval.size.multiply(0.5));
|
||||
}
|
||||
|
||||
}
|
||||
handleMouseUp (event) {
|
||||
if (event.event.button > 0 || !this.active) return; // only first mouse button
|
||||
|
||||
if (this.isBoundingBoxMode) {
|
||||
this.boundingBoxTool.onMouseUp(event);
|
||||
this.isBoundingBoxMode = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.oval) {
|
||||
if (Math.abs(this.oval.size.width * this.oval.size.height) < TextTool.TOLERANCE / paper.view.zoom) {
|
||||
// Tiny oval created unintentionally?
|
||||
this.oval.remove();
|
||||
this.oval = null;
|
||||
} else {
|
||||
const ovalPath = this.oval.toPath(true /* insert */);
|
||||
this.oval.remove();
|
||||
this.oval = null;
|
||||
|
||||
ovalPath.selected = true;
|
||||
this.setSelectedItems();
|
||||
this.onUpdateSvg();
|
||||
}
|
||||
}
|
||||
this.active = false;
|
||||
}
|
||||
deactivateTool () {
|
||||
this.boundingBoxTool.removeBoundsPath();
|
||||
}
|
||||
}
|
||||
|
||||
export default TextTool;
|
|
@ -9,7 +9,8 @@ const Modes = keyMirror({
|
|||
RESHAPE: null,
|
||||
OVAL: null,
|
||||
RECT: null,
|
||||
ROUNDED_RECT: null
|
||||
ROUNDED_RECT: null,
|
||||
TEXT: null
|
||||
});
|
||||
|
||||
export default Modes;
|
||||
|
|
Loading…
Reference in a new issue