Make the text tool a real tool

This commit is contained in:
DD 2018-03-09 14:40:08 -05:00
parent 0d3b276e5e
commit 8e7ae67ae6
5 changed files with 289 additions and 26 deletions

View file

@ -33,7 +33,7 @@ import ReshapeMode from '../../containers/reshape-mode.jsx';
import SelectMode from '../../containers/select-mode.jsx'; import SelectMode from '../../containers/select-mode.jsx';
import StrokeColorIndicatorComponent from '../../containers/stroke-color-indicator.jsx'; import StrokeColorIndicatorComponent from '../../containers/stroke-color-indicator.jsx';
import StrokeWidthIndicatorComponent from '../../containers/stroke-width-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 layout from '../../lib/layout-constants';
import styles from './paint-editor.css'; import styles from './paint-editor.css';
@ -350,11 +350,13 @@ const PaintEditorComponent = props => {
<EraserMode <EraserMode
onUpdateSvg={props.onUpdateSvg} onUpdateSvg={props.onUpdateSvg}
/> />
{/* Text mode will go here */} <FillMode
<LineMode
onUpdateSvg={props.onUpdateSvg} onUpdateSvg={props.onUpdateSvg}
/> />
<FillMode <TextMode
onUpdateSvg={props.onUpdateSvg}
/>
<LineMode
onUpdateSvg={props.onUpdateSvg} onUpdateSvg={props.onUpdateSvg}
/> />
<OvalMode <OvalMode
@ -363,8 +365,6 @@ const PaintEditorComponent = props => {
<RectMode <RectMode
onUpdateSvg={props.onUpdateSvg} onUpdateSvg={props.onUpdateSvg}
/> />
{/* text tool, coming soon */}
<TextModeComponent />
</div> </div>
) : null} ) : null}

View file

@ -1,27 +1,25 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
import ToolSelectComponent from '../tool-select-base/tool-select-base.jsx'; import ToolSelectComponent from '../tool-select-base/tool-select-base.jsx';
import textIcon from './text.svg'; import textIcon from './text.svg';
const TextModeComponent = () => ( const TextModeComponent = props => (
<ComingSoonTooltip
place="right"
tooltipId="text-mode-select"
>
<ToolSelectComponent <ToolSelectComponent
disabled
imgDescriptor={{ imgDescriptor={{
defaultMessage: 'Text', defaultMessage: 'Text',
description: 'Label for the text tool', description: 'Label for the text tool',
id: 'paint.textMode.text' id: 'paint.textMode.text'
}} }}
imgSrc={textIcon} imgSrc={textIcon}
isSelected={false} isSelected={props.isSelected}
onMouseDown={function () {}} // eslint-disable-line react/jsx-no-bind onMouseDown={props.onMouseDown}
/> />
</ComingSoonTooltip>
); );
TextModeComponent.propTypes = {
isSelected: PropTypes.bool.isRequired,
onMouseDown: PropTypes.func.isRequired
};
export default TextModeComponent; export default TextModeComponent;

View 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);

View 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;

View file

@ -9,7 +9,8 @@ const Modes = keyMirror({
RESHAPE: null, RESHAPE: null,
OVAL: null, OVAL: null,
RECT: null, RECT: null,
ROUNDED_RECT: null ROUNDED_RECT: null,
TEXT: null
}); });
export default Modes; export default Modes;