From 8e7ae67ae6e6ade63cfeb7050fd8dc3797060019 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 9 Mar 2018 14:40:08 -0500 Subject: [PATCH 01/35] Make the text tool a real tool --- src/components/paint-editor/paint-editor.jsx | 12 +- src/components/text-mode/text-mode.jsx | 36 +++-- src/containers/text-mode.jsx | 130 ++++++++++++++++++ src/helper/tools/text-tool.js | 134 +++++++++++++++++++ src/lib/modes.js | 3 +- 5 files changed, 289 insertions(+), 26 deletions(-) create mode 100644 src/containers/text-mode.jsx create mode 100644 src/helper/tools/text-tool.js diff --git a/src/components/paint-editor/paint-editor.jsx b/src/components/paint-editor/paint-editor.jsx index 413a2449..f69de1ed 100644 --- a/src/components/paint-editor/paint-editor.jsx +++ b/src/components/paint-editor/paint-editor.jsx @@ -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 => { - {/* Text mode will go here */} - - + { - {/* text tool, coming soon */} - ) : null} diff --git a/src/components/text-mode/text-mode.jsx b/src/components/text-mode/text-mode.jsx index 1ab29d09..747f68b9 100644 --- a/src/components/text-mode/text-mode.jsx +++ b/src/components/text-mode/text-mode.jsx @@ -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 = () => ( - - - +const TextModeComponent = props => ( + ); +TextModeComponent.propTypes = { + isSelected: PropTypes.bool.isRequired, + onMouseDown: PropTypes.func.isRequired +}; + export default TextModeComponent; diff --git a/src/containers/text-mode.jsx b/src/containers/text-mode.jsx new file mode 100644 index 00000000..257c465f --- /dev/null +++ b/src/containers/text-mode.jsx @@ -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 ( + + ); + } +} + +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); diff --git a/src/helper/tools/text-tool.js b/src/helper/tools/text-tool.js new file mode 100644 index 00000000..73a1224e --- /dev/null +++ b/src/helper/tools/text-tool.js @@ -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} 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; diff --git a/src/lib/modes.js b/src/lib/modes.js index 6736f742..7fcbbe57 100644 --- a/src/lib/modes.js +++ b/src/lib/modes.js @@ -9,7 +9,8 @@ const Modes = keyMirror({ RESHAPE: null, OVAL: null, RECT: null, - ROUNDED_RECT: null + ROUNDED_RECT: null, + TEXT: null }); export default Modes; From 8e8622209715f14eb41ee1f63f8f677d2d8e94bb Mon Sep 17 00:00:00 2001 From: DD Date: Mon, 12 Mar 2018 17:51:23 -0400 Subject: [PATCH 02/35] Basic text edit --- src/containers/paint-editor.jsx | 18 ++++----- src/helper/tools/text-tool.js | 71 +++++++++++++++++---------------- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 86d3f263..a572c52d 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -263,15 +263,15 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ onKeyPress: event => { - if (event.key === 'e') { - dispatch(changeMode(Modes.ERASER)); - } else if (event.key === 'b') { - dispatch(changeMode(Modes.BRUSH)); - } else if (event.key === 'l') { - dispatch(changeMode(Modes.LINE)); - } else if (event.key === 's') { - dispatch(changeMode(Modes.SELECT)); - } + // if (event.key === 'e') { + // dispatch(changeMode(Modes.ERASER)); + // } else if (event.key === 'b') { + // dispatch(changeMode(Modes.BRUSH)); + // } else if (event.key === 'l') { + // dispatch(changeMode(Modes.LINE)); + // } else if (event.key === 's') { + // dispatch(changeMode(Modes.SELECT)); + // } }, clearSelectedItems: () => { dispatch(clearSelectedItems()); diff --git a/src/helper/tools/text-tool.js b/src/helper/tools/text-tool.js index 73a1224e..2e1e1af7 100644 --- a/src/helper/tools/text-tool.js +++ b/src/helper/tools/text-tool.js @@ -24,17 +24,17 @@ class TextTool extends paper.Tool { this.clearSelectedItems = clearSelectedItems; this.onUpdateSvg = onUpdateSvg; this.boundingBoxTool = new BoundingBoxTool(Modes.TEXT, setSelectedItems, clearSelectedItems, onUpdateSvg); - const nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateSvg); + this.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.onKeyUp = this.handleKeyUp; + this.onKeyDown = this.handleKeyDown; - this.oval = null; + this.textBox = null; this.colorState = null; this.isBoundingBoxMode = null; this.active = false; @@ -66,16 +66,23 @@ class TextTool extends paper.Tool { if (event.event.button > 0) return; // only first mouse button this.active = true; + if (this.textBox && this.textBox.content.trim() === '') { + this.textBox.remove(); + this.textBox = null; + } + 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 + this.textBox = new paper.PointText({ + point: event.point, + content: 'لوحة المفاتKeyboardيح العربية', + font: 'Times', + fontSize: 30 }); - styleShape(this.oval, this.colorState); + styleShape(this.textBox, this.colorState); } } handleMouseDrag (event) { @@ -86,18 +93,7 @@ class TextTool extends paper.Tool { 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)); - } + // TODO selection } handleMouseUp (event) { @@ -109,25 +105,32 @@ class TextTool extends paper.Tool { 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(); + // TODO + this.active = false; + } + handleKeyUp (event) { + if (this.isBoundingBoxMode) { + this.nudgeTool.onKeyUp(event); + } + } + handleKeyDown (event) { + if (this.isBoundingBoxMode) { + this.nudgeTool.onKeyUp(event); + } else { + if ((event.key === 'delete' || event.key === 'backspace') && this.textBox.content.length) { + this.textBox.content = this.textBox.content.slice(0, this.textBox.content.length - 1); + } else if (!(event.modifiers.alt || event.modifiers.comand || event.modifiers.control || + event.modifiers.meta || event.modifiers.option)) { + this.textBox.content = this.textBox.content + event.character; } } - this.active = false; } deactivateTool () { this.boundingBoxTool.removeBoundsPath(); + if (this.textBox && this.textBox.content.trim() === '') { + this.textBox.remove(); + this.textBox = null; + } } } From 4fd9f991f168a1051c00fbe7f8758211f617daed Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 14 Mar 2018 14:43:39 -0400 Subject: [PATCH 03/35] Mode switching --- src/helper/tools/text-tool.js | 109 ++++++++++++++++++++++++++++------ 1 file changed, 90 insertions(+), 19 deletions(-) diff --git a/src/helper/tools/text-tool.js b/src/helper/tools/text-tool.js index 2e1e1af7..3636d74a 100644 --- a/src/helper/tools/text-tool.js +++ b/src/helper/tools/text-tool.js @@ -13,6 +13,16 @@ class TextTool extends paper.Tool { static get TOLERANCE () { return 6; } + static get TEXT_EDIT_MODE () { + return 'TEXT_EDIT_MODE'; + } + static get SELECT_MODE () { + return 'SELECT_MODE'; + } + /** Clicks registered within this amount of time are registered as double clicks */ + static get DOUBLE_CLICK_MILLIS () { + return 250; + } /** * @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 @@ -25,21 +35,27 @@ class TextTool extends paper.Tool { this.onUpdateSvg = onUpdateSvg; this.boundingBoxTool = new BoundingBoxTool(Modes.TEXT, setSelectedItems, clearSelectedItems, onUpdateSvg); this.nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateSvg); + this.lastEvent = null; // 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.onMouseMove = this.handleMouseMove; this.onKeyUp = this.handleKeyUp; this.onKeyDown = this.handleKeyDown; this.textBox = null; + this.guide = null; this.colorState = null; - this.isBoundingBoxMode = null; + this.mode = null; this.active = false; + + // If text selected and then activate this tool, switch to text edit mode for that text + // If double click on text while in select mode, does mode change to text mode? Text fully selected by default } - getHitOptions () { + getBoundingBoxHitOptions () { return { segments: true, stroke: true, @@ -52,6 +68,18 @@ class TextTool extends paper.Tool { tolerance: TextTool.TOLERANCE / paper.view.zoom }; } + getTextEditHitOptions () { + return { + class: paper.PointText, + segments: true, + stroke: true, + curves: true, + fill: true, + guide: false, + match: hitResult => hitResult.item && !hitResult.item.selected, // Unselected only + tolerance: TextTool.TOLERANCE / paper.view.zoom + }; + } /** * Should be called if the selection changes to update the bounds of the bounding box. * @param {Array} selectedItems Array of selected items. @@ -62,6 +90,14 @@ class TextTool extends paper.Tool { setColorState (colorState) { this.colorState = colorState; } + handleMouseMove (event) { + const hitResults = paper.project.hitTestAll(event.point, this.getTextEditHitOptions()); + if (hitResults.length) { + document.body.style.cursor = 'text'; + } else { + document.body.style.cursor = 'auto'; + } + } handleMouseDown (event) { if (event.event.button > 0) return; // only first mouse button this.active = true; @@ -70,25 +106,60 @@ class TextTool extends paper.Tool { this.textBox.remove(); this.textBox = null; } - - if (this.boundingBoxTool.onMouseDown(event, false /* clone */, false /* multiselect */, this.getHitOptions())) { - this.isBoundingBoxMode = true; - } else { - this.isBoundingBoxMode = false; + + // Check if double clicked + let doubleClicked = false; + if (this.lastEvent) { + if ((event.event.timeStamp - this.lastEvent.event.timeStamp) < TextTool.DOUBLE_CLICK_MILLIS) { + doubleClicked = true; + } else { + doubleClicked = false; + } + } + this.lastEvent = event; + + const doubleClickHitTest = paper.project.hitTest(event.point, this.getBoundingBoxHitOptions()); + if (doubleClicked && + this.mode === TextTool.SELECT_MODE && + doubleClickHitTest) { clearSelection(this.clearSelectedItems); - this.textBox = new paper.PointText({ - point: event.point, - content: 'لوحة المفاتKeyboardيح العربية', - font: 'Times', - fontSize: 30 - }); - styleShape(this.textBox, this.colorState); + this.textBox = doubleClickHitTest.item; + this.mode = TextTool.TEXT_EDIT_MODE; + } else if (this.boundingBoxTool.onMouseDown( + event, false /* clone */, false /* multiselect */, this.getBoundingBoxHitOptions())) { + // In select mode staying in select mode + this.mode = TextTool.SELECT_MODE; + } else { + clearSelection(this.clearSelectedItems); + const hitResults = paper.project.hitTestAll(event.point, this.getTextEditHitOptions()); + if (hitResults.length) { + // Clicking a text item to begin text edit mode on that item + this.textBox = hitResults[0].item; + this.mode = TextTool.TEXT_EDIT_MODE; + } else if (this.mode === TextTool.TEXT_EDIT_MODE) { + // In text mode clicking away to begin select mode + this.mode = TextTool.SELECT_MODE; + // this.guide.reomve(); + this.textBox.selected = true; + this.setSelectedItems(); + } else { + // In no mode or select mode clicking away to begin text edit mode + this.mode = TextTool.TEXT_EDIT_MODE; + clearSelection(this.clearSelectedItems); + this.textBox = new paper.PointText({ + point: event.point, + content: 'لوحة المفاتKeyboardيح العربية', + font: 'Times', + fontSize: 30 + }); + styleShape(this.textBox, this.colorState); + } } } handleMouseDrag (event) { if (event.event.button > 0 || !this.active) return; // only first mouse button - if (this.isBoundingBoxMode) { + if (this.mode === TextTool.SELECT_MODE) { this.boundingBoxTool.onMouseDrag(event); return; } @@ -99,7 +170,7 @@ class TextTool extends paper.Tool { handleMouseUp (event) { if (event.event.button > 0 || !this.active) return; // only first mouse button - if (this.isBoundingBoxMode) { + if (this.mode === TextTool.SELECT_MODE) { this.boundingBoxTool.onMouseUp(event); this.isBoundingBoxMode = null; return; @@ -109,14 +180,14 @@ class TextTool extends paper.Tool { this.active = false; } handleKeyUp (event) { - if (this.isBoundingBoxMode) { + if (this.mode === TextTool.SELECT_MODEe) { this.nudgeTool.onKeyUp(event); } } handleKeyDown (event) { - if (this.isBoundingBoxMode) { + if (this.mode === TextTool.SELECT_MODE) { this.nudgeTool.onKeyUp(event); - } else { + } else if (this.mode === TextTool.TEXT_EDIT_MODE) { if ((event.key === 'delete' || event.key === 'backspace') && this.textBox.content.length) { this.textBox.content = this.textBox.content.slice(0, this.textBox.content.length - 1); } else if (!(event.modifiers.alt || event.modifiers.comand || event.modifiers.control || From f0b570dc5168a135648b95c208fb28926cf8bebb Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 14 Mar 2018 15:28:22 -0400 Subject: [PATCH 04/35] Add dotted lines in text edit mode --- src/helper/tools/text-tool.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/helper/tools/text-tool.js b/src/helper/tools/text-tool.js index 3636d74a..744b4f98 100644 --- a/src/helper/tools/text-tool.js +++ b/src/helper/tools/text-tool.js @@ -4,6 +4,8 @@ import {styleShape} from '../style-path'; import {clearSelection} from '../selection'; import BoundingBoxTool from '../selection-tools/bounding-box-tool'; import NudgeTool from '../selection-tools/nudge-tool'; +import {getGuideLayer} from '../layer'; +import {getGuideColor} from '../guides'; /** * Tool for adding text. Text elements have limited editability; they can't be reshaped, @@ -23,6 +25,9 @@ class TextTool extends paper.Tool { static get DOUBLE_CLICK_MILLIS () { return 250; } + static get TEXT_PADDING () { + return 8; + } /** * @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 @@ -134,6 +139,10 @@ class TextTool extends paper.Tool { const hitResults = paper.project.hitTestAll(event.point, this.getTextEditHitOptions()); if (hitResults.length) { // Clicking a text item to begin text edit mode on that item + if (this.guide) { + this.guide.remove(); + this.guide = null; + } this.textBox = hitResults[0].item; this.mode = TextTool.TEXT_EDIT_MODE; } else if (this.mode === TextTool.TEXT_EDIT_MODE) { @@ -155,6 +164,17 @@ class TextTool extends paper.Tool { styleShape(this.textBox, this.colorState); } } + + if (this.mode === TextTool.TEXT_EDIT_MODE) { + this.guide = new paper.Shape.Rectangle(this.textBox.bounds.expand(TextTool.TEXT_PADDING)); + this.guide.strokeColor = getGuideColor(); + this.guide.dashArray = [4, 4]; + this.guide.strokeWidth = 2; + this.guide.parent = getGuideLayer(); + } else if (this.guide) { + this.guide.remove(); + this.guide = null; + } } handleMouseDrag (event) { if (event.event.button > 0 || !this.active) return; // only first mouse button @@ -193,6 +213,8 @@ class TextTool extends paper.Tool { } else if (!(event.modifiers.alt || event.modifiers.comand || event.modifiers.control || event.modifiers.meta || event.modifiers.option)) { this.textBox.content = this.textBox.content + event.character; + this.guide.size = this.textBox.bounds.expand(TextTool.TEXT_PADDING); + this.guide.position = this.textBox.position; } } } @@ -202,6 +224,10 @@ class TextTool extends paper.Tool { this.textBox.remove(); this.textBox = null; } + if (this.guide) { + this.guide.remove(); + this.guide = null; + } } } From 240282f9d23577dbe9678f162d49e2ad2402a212 Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 14 Mar 2018 16:01:20 -0400 Subject: [PATCH 05/35] Make the guide stay in the shape of the text when text is rotated --- src/helper/math.js | 14 +++++++------- src/helper/tools/fill-tool.js | 4 ++-- src/helper/tools/text-tool.js | 17 ++++++++--------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/helper/math.js b/src/helper/math.js index 11eab4a4..c15ef3fa 100644 --- a/src/helper/math.js +++ b/src/helper/math.js @@ -82,17 +82,17 @@ const sortItemsByZIndex = function (a, b) { return null; }; -// Expand the size of the path by approx one pixel all around -const expandByOne = function (path) { +// Expand the size of the path by amount all around +const expandBy = function (path, amount) { const center = path.position; let pathArea = path.area; for (const seg of path.segments) { - const halfNorm = seg.point.subtract(center) + const halfDelta = seg.point.subtract(center) .normalize() - .divide(2); - seg.point = seg.point.add(halfNorm); + .multiply(amount); + seg.point = seg.point.add(halfDelta); // If that made the path area smaller, go the other way. - if (path.area < pathArea) seg.point = seg.point.subtract(halfNorm.multiply(2)); + if (path.area < pathArea) seg.point = seg.point.subtract(halfDelta.multiply(2)); pathArea = path.area; } }; @@ -112,7 +112,7 @@ export { HANDLE_RATIO, checkPointsClose, ensureClockwise, - expandByOne, + expandBy, getRandomInt, getRandomBoolean, snapDeltaToAngle, diff --git a/src/helper/tools/fill-tool.js b/src/helper/tools/fill-tool.js index 55a88308..30208b74 100644 --- a/src/helper/tools/fill-tool.js +++ b/src/helper/tools/fill-tool.js @@ -1,6 +1,6 @@ import paper from '@scratch/paper'; import {getHoveredItem} from '../hover'; -import {expandByOne} from '../math'; +import {expandBy} from '../math'; class FillTool extends paper.Tool { static get TOLERANCE () { @@ -104,7 +104,7 @@ class FillTool extends paper.Tool { this.addedFillItem.data.origItem = hitItem; // This usually fixes it so there isn't a teeny tiny gap in between the fill and the outline // when filling in a hole - expandByOne(this.addedFillItem); + expandBy(this.addedFillItem, .5); this.addedFillItem.insertAbove(hitItem.parent); } else if (this.fillItem.parent instanceof paper.CompoundPath) { this.fillItemOrigColor = hitItem.parent.fillColor; diff --git a/src/helper/tools/text-tool.js b/src/helper/tools/text-tool.js index 744b4f98..7746fbc5 100644 --- a/src/helper/tools/text-tool.js +++ b/src/helper/tools/text-tool.js @@ -4,8 +4,8 @@ import {styleShape} from '../style-path'; import {clearSelection} from '../selection'; import BoundingBoxTool from '../selection-tools/bounding-box-tool'; import NudgeTool from '../selection-tools/nudge-tool'; -import {getGuideLayer} from '../layer'; -import {getGuideColor} from '../guides'; +import {hoverBounds} from '../guides'; +import {expandBy} from '../math'; /** * Tool for adding text. Text elements have limited editability; they can't be reshaped, @@ -148,7 +148,6 @@ class TextTool extends paper.Tool { } else if (this.mode === TextTool.TEXT_EDIT_MODE) { // In text mode clicking away to begin select mode this.mode = TextTool.SELECT_MODE; - // this.guide.reomve(); this.textBox.selected = true; this.setSelectedItems(); } else { @@ -166,11 +165,9 @@ class TextTool extends paper.Tool { } if (this.mode === TextTool.TEXT_EDIT_MODE) { - this.guide = new paper.Shape.Rectangle(this.textBox.bounds.expand(TextTool.TEXT_PADDING)); - this.guide.strokeColor = getGuideColor(); + this.guide = hoverBounds(this.textBox); + expandBy(this.guide, TextTool.TEXT_PADDING); this.guide.dashArray = [4, 4]; - this.guide.strokeWidth = 2; - this.guide.parent = getGuideLayer(); } else if (this.guide) { this.guide.remove(); this.guide = null; @@ -213,8 +210,10 @@ class TextTool extends paper.Tool { } else if (!(event.modifiers.alt || event.modifiers.comand || event.modifiers.control || event.modifiers.meta || event.modifiers.option)) { this.textBox.content = this.textBox.content + event.character; - this.guide.size = this.textBox.bounds.expand(TextTool.TEXT_PADDING); - this.guide.position = this.textBox.position; + if (this.guide) this.guide.remove(); + this.guide = hoverBounds(this.textBox); + expandBy(this.guide, TextTool.TEXT_PADDING); + this.guide.dashArray = [4, 4]; } } } From f9aabe1bebc777a49e49b9407ca1442c1409d27a Mon Sep 17 00:00:00 2001 From: DD Date: Wed, 14 Mar 2018 16:47:07 -0400 Subject: [PATCH 06/35] Fix some bugs --- src/containers/paint-editor.jsx | 20 +++++++++++--------- src/helper/guides.js | 8 ++++++-- src/helper/tools/text-tool.js | 30 ++++++++++++++---------------- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index a572c52d..8da676ce 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -263,15 +263,17 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ onKeyPress: event => { - // if (event.key === 'e') { - // dispatch(changeMode(Modes.ERASER)); - // } else if (event.key === 'b') { - // dispatch(changeMode(Modes.BRUSH)); - // } else if (event.key === 'l') { - // dispatch(changeMode(Modes.LINE)); - // } else if (event.key === 's') { - // dispatch(changeMode(Modes.SELECT)); - // } + // TODO if text tool not in text edit mode + // TODO unfocus other text input fields + if (event.key === 'e') { + dispatch(changeMode(Modes.ERASER)); + } else if (event.key === 'b') { + dispatch(changeMode(Modes.BRUSH)); + } else if (event.key === 'l') { + dispatch(changeMode(Modes.LINE)); + } else if (event.key === 's') { + dispatch(changeMode(Modes.SELECT)); + } }, clearSelectedItems: () => { dispatch(clearSelectedItems()); diff --git a/src/helper/guides.js b/src/helper/guides.js index 2dfd266c..36e1294c 100644 --- a/src/helper/guides.js +++ b/src/helper/guides.js @@ -30,8 +30,12 @@ const hoverItem = function (item) { return clone; }; -const hoverBounds = function (item) { - const rect = new paper.Path.Rectangle(item.internalBounds); +const hoverBounds = function (item, expandBy) { + let bounds = item.internalBounds; + if (expandBy) { + bounds = bounds.expand(expandBy); + } + const rect = new paper.Path.Rectangle(bounds); rect.matrix = item.matrix; setDefaultGuideStyle(rect); rect.parent = getGuideLayer(); diff --git a/src/helper/tools/text-tool.js b/src/helper/tools/text-tool.js index 7746fbc5..aa67de9d 100644 --- a/src/helper/tools/text-tool.js +++ b/src/helper/tools/text-tool.js @@ -5,7 +5,6 @@ import {clearSelection} from '../selection'; import BoundingBoxTool from '../selection-tools/bounding-box-tool'; import NudgeTool from '../selection-tools/nudge-tool'; import {hoverBounds} from '../guides'; -import {expandBy} from '../math'; /** * Tool for adding text. Text elements have limited editability; they can't be reshaped, @@ -130,7 +129,8 @@ class TextTool extends paper.Tool { clearSelection(this.clearSelectedItems); this.textBox = doubleClickHitTest.item; this.mode = TextTool.TEXT_EDIT_MODE; - } else if (this.boundingBoxTool.onMouseDown( + } else if ( + this.boundingBoxTool.onMouseDown( event, false /* clone */, false /* multiselect */, this.getBoundingBoxHitOptions())) { // In select mode staying in select mode this.mode = TextTool.SELECT_MODE; @@ -147,16 +147,20 @@ class TextTool extends paper.Tool { this.mode = TextTool.TEXT_EDIT_MODE; } else if (this.mode === TextTool.TEXT_EDIT_MODE) { // In text mode clicking away to begin select mode - this.mode = TextTool.SELECT_MODE; - this.textBox.selected = true; - this.setSelectedItems(); + if (this.textBox) { + this.mode = TextTool.SELECT_MODE; + this.textBox.selected = true; + this.setSelectedItems(); + } else { + this.mode = null; + } } else { // In no mode or select mode clicking away to begin text edit mode this.mode = TextTool.TEXT_EDIT_MODE; clearSelection(this.clearSelectedItems); this.textBox = new paper.PointText({ point: event.point, - content: 'لوحة المفاتKeyboardيح العربية', + content: '', font: 'Times', fontSize: 30 }); @@ -165,8 +169,7 @@ class TextTool extends paper.Tool { } if (this.mode === TextTool.TEXT_EDIT_MODE) { - this.guide = hoverBounds(this.textBox); - expandBy(this.guide, TextTool.TEXT_PADDING); + this.guide = hoverBounds(this.textBox, TextTool.TEXT_PADDING); this.guide.dashArray = [4, 4]; } else if (this.guide) { this.guide.remove(); @@ -180,9 +183,6 @@ class TextTool extends paper.Tool { this.boundingBoxTool.onMouseDrag(event); return; } - - // TODO selection - } handleMouseUp (event) { if (event.event.button > 0 || !this.active) return; // only first mouse button @@ -193,7 +193,6 @@ class TextTool extends paper.Tool { return; } - // TODO this.active = false; } handleKeyUp (event) { @@ -210,11 +209,10 @@ class TextTool extends paper.Tool { } else if (!(event.modifiers.alt || event.modifiers.comand || event.modifiers.control || event.modifiers.meta || event.modifiers.option)) { this.textBox.content = this.textBox.content + event.character; - if (this.guide) this.guide.remove(); - this.guide = hoverBounds(this.textBox); - expandBy(this.guide, TextTool.TEXT_PADDING); - this.guide.dashArray = [4, 4]; } + if (this.guide) this.guide.remove(); + this.guide = hoverBounds(this.textBox, TextTool.TEXT_PADDING); + this.guide.dashArray = [4, 4]; } } deactivateTool () { From 254bbe1285616e461f515c2104535e04239bbeca Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 15 Mar 2018 13:19:05 -0400 Subject: [PATCH 07/35] Ignore nudge when text field focused --- src/helper/tools/text-tool.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/helper/tools/text-tool.js b/src/helper/tools/text-tool.js index aa67de9d..33549839 100644 --- a/src/helper/tools/text-tool.js +++ b/src/helper/tools/text-tool.js @@ -201,6 +201,11 @@ class TextTool extends paper.Tool { } } handleKeyDown (event) { + if (event.event.target instanceof HTMLInputElement) { + // Ignore nudge if a text input field is focused + return; + } + if (this.mode === TextTool.SELECT_MODE) { this.nudgeTool.onKeyUp(event); } else if (this.mode === TextTool.TEXT_EDIT_MODE) { From 7a0a0784e1af5982d0e8a495713640eba7c73b7b Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 15 Mar 2018 13:20:07 -0400 Subject: [PATCH 08/35] Unfocus text fields when canvas clicked --- src/containers/paint-editor.jsx | 17 ++++++++++------- src/helper/selection-tools/reshape-tool.js | 5 +++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 8da676ce..99a06ed9 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -50,6 +50,10 @@ class PaintEditor extends React.Component { } componentDidMount () { 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); } componentDidUpdate (prevProps) { if (this.props.isEyeDropping && !prevProps.isEyeDropping) { @@ -61,6 +65,8 @@ class PaintEditor extends React.Component { componentWillUnmount () { document.removeEventListener('keydown', this.props.onKeyPress); this.stopEyeDroppingLoop(); + document.removeEventListener('mousedown', this.onMouseDown); + document.removeEventListener('touchstart', this.onMouseDown); } handleUpdateSvg (skipSnapshot) { // Store the zoom/pan and restore it after snapshotting @@ -135,6 +141,10 @@ class PaintEditor extends React.Component { this.canvas = canvas; } onMouseDown () { + if (document.activeElement instanceof HTMLInputElement) { + document.activeElement.blur(); + } + if (this.props.isEyeDropping) { const colorString = this.eyeDropper.colorString; const callback = this.props.changeColorToEyeDropper; @@ -164,11 +174,6 @@ class PaintEditor extends React.Component { this.eyeDropper.pickX = -1; this.eyeDropper.pickY = -1; this.eyeDropper.activate(); - - // 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); this.intervalId = setInterval(() => { const colorInfo = this.eyeDropper.getColorInfo( @@ -190,8 +195,6 @@ class PaintEditor extends React.Component { } stopEyeDroppingLoop () { clearInterval(this.intervalId); - document.removeEventListener('mousedown', this.onMouseDown); - document.removeEventListener('touchstart', this.onMouseDown); } render () { return ( diff --git a/src/helper/selection-tools/reshape-tool.js b/src/helper/selection-tools/reshape-tool.js index 0ebfb433..4e42ac75 100644 --- a/src/helper/selection-tools/reshape-tool.js +++ b/src/helper/selection-tools/reshape-tool.js @@ -232,6 +232,11 @@ class ReshapeTool extends paper.Tool { this.active = false; } handleKeyDown (event) { + if (event.event.target instanceof HTMLInputElement) { + // Ignore nudge if a text input field is focused + return; + } + const nudgeAmount = 1 / paper.view.zoom; const selected = getSelectedLeafItems(); if (selected.length === 0) return; From 8d61a7b0602d76b26edb46f9b89fde72716609b1 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 16 Mar 2018 11:36:06 -0400 Subject: [PATCH 09/35] Move the text edit target to the state and make fill work --- src/containers/fill-color-indicator.jsx | 8 ++- src/containers/paint-editor.jsx | 21 ++++++- src/containers/stroke-color-indicator.jsx | 8 ++- src/containers/stroke-width-indicator.jsx | 10 +++- src/containers/text-mode.jsx | 10 +++- src/helper/guides.js | 1 + src/helper/hover.js | 1 - src/helper/item.js | 9 +-- src/helper/style-path.js | 69 +++++++++++------------ src/helper/tools/fill-tool.js | 4 +- src/helper/tools/text-tool.js | 7 ++- src/reducers/scratch-paint-reducer.js | 2 + src/reducers/text-edit-target.js | 40 +++++++++++++ 13 files changed, 131 insertions(+), 59 deletions(-) create mode 100644 src/reducers/text-edit-target.js diff --git a/src/containers/fill-color-indicator.jsx b/src/containers/fill-color-indicator.jsx index 30a0690b..317dec16 100644 --- a/src/containers/fill-color-indicator.jsx +++ b/src/containers/fill-color-indicator.jsx @@ -30,7 +30,7 @@ class FillColorIndicator extends React.Component { } handleChangeFillColor (newColor) { // Apply color and update redux, but do not update svg until picker closes. - const isDifferent = applyFillColorToSelection(newColor); + const isDifferent = applyFillColorToSelection(newColor, this.props.textEditTarget); this._hasChanged = this._hasChanged || isDifferent; this.props.onChangeFillColor(newColor); } @@ -54,7 +54,8 @@ const mapStateToProps = state => ({ disabled: state.scratchPaint.mode === Modes.LINE, fillColor: state.scratchPaint.color.fillColor, fillColorModalVisible: state.scratchPaint.modals.fillColor, - isEyeDropping: state.scratchPaint.color.eyeDropper.active + isEyeDropping: state.scratchPaint.color.eyeDropper.active, + textEditTarget: state.scratchPaint.textEditTarget }); const mapDispatchToProps = dispatch => ({ @@ -76,7 +77,8 @@ FillColorIndicator.propTypes = { isEyeDropping: PropTypes.bool.isRequired, onChangeFillColor: PropTypes.func.isRequired, onCloseFillColor: PropTypes.func.isRequired, - onUpdateSvg: PropTypes.func.isRequired + onUpdateSvg: PropTypes.func.isRequired, + textEditTarget: PropTypes.string }; export default connect( diff --git a/src/containers/paint-editor.jsx b/src/containers/paint-editor.jsx index 99a06ed9..e081d8c3 100644 --- a/src/containers/paint-editor.jsx +++ b/src/containers/paint-editor.jsx @@ -49,7 +49,12 @@ class PaintEditor extends React.Component { }; } componentDidMount () { - document.addEventListener('keydown', this.props.onKeyPress); + document.addEventListener('keydown', event => { + // Don't activate keyboard shortcuts during text editing + if (!this.props.textEditing) { + this.props.onKeyPress(event); + } + }); // 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); @@ -248,6 +253,7 @@ PaintEditor.propTypes = { setSelectedItems: PropTypes.func.isRequired, svg: PropTypes.string, svgId: PropTypes.string, + textEditing: PropTypes.bool.isRequired, undoSnapshot: PropTypes.func.isRequired, undoState: PropTypes.shape({ stack: PropTypes.arrayOf(PropTypes.object).isRequired, @@ -262,12 +268,11 @@ const mapStateToProps = state => ({ pasteOffset: state.scratchPaint.clipboard.pasteOffset, previousTool: state.scratchPaint.color.eyeDropper.previousTool, selectedItems: state.scratchPaint.selectedItems, + textEditing: state.scratchPaint.textEditTarget !== null, undoState: state.scratchPaint.undo }); const mapDispatchToProps = dispatch => ({ onKeyPress: event => { - // TODO if text tool not in text edit mode - // TODO unfocus other text input fields if (event.key === 'e') { dispatch(changeMode(Modes.ERASER)); } else if (event.key === 'b') { @@ -276,6 +281,16 @@ const mapDispatchToProps = dispatch => ({ dispatch(changeMode(Modes.LINE)); } else if (event.key === 's') { dispatch(changeMode(Modes.SELECT)); + } else if (event.key === 'w') { + dispatch(changeMode(Modes.RESHAPE)); + } else if (event.key === 'f') { + dispatch(changeMode(Modes.FILL)); + } else if (event.key === 't') { + dispatch(changeMode(Modes.TEXT)); + } else if (event.key === 'c') { + dispatch(changeMode(Modes.OVAL)); + } else if (event.key === 'r') { + dispatch(changeMode(Modes.RECT)); } }, clearSelectedItems: () => { diff --git a/src/containers/stroke-color-indicator.jsx b/src/containers/stroke-color-indicator.jsx index ba675086..fc341ca4 100644 --- a/src/containers/stroke-color-indicator.jsx +++ b/src/containers/stroke-color-indicator.jsx @@ -30,7 +30,7 @@ class StrokeColorIndicator extends React.Component { } handleChangeStrokeColor (newColor) { // Apply color and update redux, but do not update svg until picker closes. - const isDifferent = applyStrokeColorToSelection(newColor); + const isDifferent = applyStrokeColorToSelection(newColor, this.props.textEditTarget); this._hasChanged = this._hasChanged || isDifferent; this.props.onChangeStrokeColor(newColor); } @@ -54,7 +54,8 @@ const mapStateToProps = state => ({ disabled: state.scratchPaint.mode === Modes.BRUSH, isEyeDropping: state.scratchPaint.color.eyeDropper.active, strokeColor: state.scratchPaint.color.strokeColor, - strokeColorModalVisible: state.scratchPaint.modals.strokeColor + strokeColorModalVisible: state.scratchPaint.modals.strokeColor, + textEditTarget: state.scratchPaint.textEditTarget }); const mapDispatchToProps = dispatch => ({ @@ -76,7 +77,8 @@ StrokeColorIndicator.propTypes = { onCloseStrokeColor: PropTypes.func.isRequired, onUpdateSvg: PropTypes.func.isRequired, strokeColor: PropTypes.string, - strokeColorModalVisible: PropTypes.bool.isRequired + strokeColorModalVisible: PropTypes.bool.isRequired, + textEditTarget: PropTypes.string }; export default connect( diff --git a/src/containers/stroke-width-indicator.jsx b/src/containers/stroke-width-indicator.jsx index e462a02d..9eb6391e 100644 --- a/src/containers/stroke-width-indicator.jsx +++ b/src/containers/stroke-width-indicator.jsx @@ -15,7 +15,9 @@ class StrokeWidthIndicator extends React.Component { ]); } handleChangeStrokeWidth (newWidth) { - applyStrokeWidthToSelection(newWidth, this.props.onUpdateSvg); + if (applyStrokeWidthToSelection(newWidth, this.props.textEditTarget)) { + this.props.onUpdateSvg(); + } this.props.onChangeStrokeWidth(newWidth); } render () { @@ -31,7 +33,8 @@ class StrokeWidthIndicator extends React.Component { const mapStateToProps = state => ({ disabled: state.scratchPaint.mode === Modes.BRUSH, - strokeWidth: state.scratchPaint.color.strokeWidth + strokeWidth: state.scratchPaint.color.strokeWidth, + textEditTarget: state.scratchPaint.textEditTarget }); const mapDispatchToProps = dispatch => ({ onChangeStrokeWidth: strokeWidth => { @@ -43,7 +46,8 @@ StrokeWidthIndicator.propTypes = { disabled: PropTypes.bool.isRequired, onChangeStrokeWidth: PropTypes.func.isRequired, onUpdateSvg: PropTypes.func.isRequired, - strokeWidth: PropTypes.number + strokeWidth: PropTypes.number, + textEditTarget: PropTypes.string }; export default connect( diff --git a/src/containers/text-mode.jsx b/src/containers/text-mode.jsx index 257c465f..3a09766d 100644 --- a/src/containers/text-mode.jsx +++ b/src/containers/text-mode.jsx @@ -9,6 +9,7 @@ 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 {setTextEditTarget} from '../reducers/text-edit-target'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; import {clearSelection, getSelectedLeafItems} from '../helper/selection'; @@ -65,7 +66,8 @@ class TextMode extends React.Component { this.tool = new TextTool( this.props.setSelectedItems, this.props.clearSelectedItems, - this.props.onUpdateSvg + this.props.onUpdateSvg, + this.props.setTextEditTarget, ); this.tool.setColorState(this.props.colorState); this.tool.activate(); @@ -98,7 +100,8 @@ TextMode.propTypes = { onChangeStrokeColor: PropTypes.func.isRequired, onUpdateSvg: PropTypes.func.isRequired, selectedItems: PropTypes.arrayOf(PropTypes.instanceOf(paper.Item)), - setSelectedItems: PropTypes.func.isRequired + setSelectedItems: PropTypes.func.isRequired, + setTextEditTarget: PropTypes.func.isRequired }; const mapStateToProps = state => ({ @@ -113,6 +116,9 @@ const mapDispatchToProps = dispatch => ({ setSelectedItems: () => { dispatch(setSelectedItems(getSelectedLeafItems())); }, + setTextEditTarget: targetId => { + dispatch(setTextEditTarget(targetId)); + }, handleMouseDown: () => { dispatch(changeMode(Modes.TEXT)); }, diff --git a/src/helper/guides.js b/src/helper/guides.js index 36e1294c..9fc3ccd7 100644 --- a/src/helper/guides.js +++ b/src/helper/guides.js @@ -42,6 +42,7 @@ const hoverBounds = function (item, expandBy) { rect.strokeColor = GUIDE_BLUE; rect.fillColor = null; rect.data.isHelperItem = true; + rect.data.origItem = item; rect.bringToFront(); return rect; diff --git a/src/helper/hover.js b/src/helper/hover.js index ed4caf97..d98ba9d7 100644 --- a/src/helper/hover.js +++ b/src/helper/hover.js @@ -30,7 +30,6 @@ const getHoveredItem = function (event, hitOptions, subselect) { if (!item) { return null; } - if (isBoundsItem(item)) { return hoverBounds(item); } else if (!subselect && isGroupChild(item)) { diff --git a/src/helper/item.js b/src/helper/item.js index 51e8e5bf..736eb64e 100644 --- a/src/helper/item.js +++ b/src/helper/item.js @@ -33,15 +33,16 @@ const isGroupItem = function (item) { }; +const isPGTextItem = function (item) { + return getRootItem(item).data.isPGTextItem; +}; + + const isPointTextItem = function (item) { return item.className === 'PointText'; }; -const isPGTextItem = function (item) { - return getRootItem(item).data.isPGTextItem; -}; - const setPivot = function (item, point) { if (isBoundsItem(item)) { item.pivot = item.globalToLocal(point); diff --git a/src/helper/style-path.js b/src/helper/style-path.js index f7b50916..eda8085a 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -2,6 +2,7 @@ import paper from '@scratch/paper'; import {getSelectedLeafItems} from './selection'; import {isPGTextItem, isPointTextItem} from './item'; import {isGroup} from './group'; +import {getItems} from './selection'; const MIXED = 'scratch-paint/style-path/mixed'; @@ -15,44 +16,38 @@ const _colorMatch = function (itemColor, incomingColor) { (itemColor && incomingColor && itemColor.toCSS() === new paper.Color(incomingColor).toCSS()); }; +// Selected items and currently active text edit items respond to color changes. +const _getColorStateListeners = function (textEditTargetId) { + const items = getSelectedLeafItems(); + if (textEditTargetId) { + const matches = getItems({ + match: item => item.id === textEditTargetId + }); + if (matches.length) { + items.push(matches[0]); + } + } + return items; +}; + /** * Called when setting fill color * @param {string} colorString New color, css format + * @param {?string} textEditTargetId paper.Item.id of text editing target, if any * @return {boolean} Whether the color application actually changed visibly. */ -const applyFillColorToSelection = function (colorString) { - const items = getSelectedLeafItems(); +const applyFillColorToSelection = function (colorString, textEditTargetId) { + const items = _getColorStateListeners(textEditTargetId); let changed = false; for (let item of items) { - if (item.parent instanceof paper.CompoundPath) { + if (isPointTextItem(item) && !colorString) { + colorString = 'rgba(0,0,0,0)'; + } else if (item.parent instanceof paper.CompoundPath) { item = item.parent; } - if (isPGTextItem(item)) { - for (const child of item.children) { - if (child.children) { - for (const path of child.children) { - if (!path.data.isPGGlyphRect) { - if (!_colorMatch(path.fillColor, colorString)) { - changed = true; - path.fillColor = colorString; - } - } - } - } else if (!child.data.isPGGlyphRect) { - if (!_colorMatch(child.fillColor, colorString)) { - changed = true; - child.fillColor = colorString; - } - } - } - } else { - if (isPointTextItem(item) && !colorString) { - colorString = 'rgba(0,0,0,0)'; - } - if (!_colorMatch(item.fillColor, colorString)) { - changed = true; - item.fillColor = colorString; - } + if (!_colorMatch(item.fillColor, colorString)) { + changed = true; + item.fillColor = colorString; } } return changed; @@ -61,10 +56,11 @@ const applyFillColorToSelection = function (colorString) { /** * Called when setting stroke color * @param {string} colorString New color, css format + * @param {?string} textEditTargetId paper.Item.id of text editing target, if any * @return {boolean} Whether the color application actually changed visibly. */ -const applyStrokeColorToSelection = function (colorString) { - const items = getSelectedLeafItems(); +const applyStrokeColorToSelection = function (colorString, textEditTargetId) { + const items = _getColorStateListeners(textEditTargetId); let changed = false; for (let item of items) { if (item.parent instanceof paper.CompoundPath) { @@ -106,11 +102,12 @@ const applyStrokeColorToSelection = function (colorString) { /** * Called when setting stroke width * @param {number} value New stroke width - * @param {!function} onUpdateSvg A callback to call when the image visibly changes + * @param {?string} textEditTargetId paper.Item.id of text editing target, if any + * @return {boolean} Whether the color application actually changed visibly. */ -const applyStrokeWidthToSelection = function (value, onUpdateSvg) { +const applyStrokeWidthToSelection = function (value, textEditTargetId) { let changed = false; - const items = getSelectedLeafItems(); + const items = _getColorStateListeners(textEditTargetId); for (let item of items) { if (item.parent instanceof paper.CompoundPath) { item = item.parent; @@ -122,9 +119,7 @@ const applyStrokeWidthToSelection = function (value, onUpdateSvg) { changed = true; } } - if (changed) { - onUpdateSvg(); - } + return changed; }; /** diff --git a/src/helper/tools/fill-tool.js b/src/helper/tools/fill-tool.js index 30208b74..7e797ac2 100644 --- a/src/helper/tools/fill-tool.js +++ b/src/helper/tools/fill-tool.js @@ -38,14 +38,14 @@ class FillTool extends paper.Tool { item.lastSegment.point.getDistance(item.firstSegment.point) < 8; }; return { - class: paper.Path, segments: true, stroke: true, curves: true, fill: true, guide: false, match: function (hitResult) { - return (hitResult.item.hasFill() || hitResult.item.closed || isAlmostClosedPath(hitResult.item)); + return (hitResult.item instanceof paper.Path || hitResult.item instanceof paper.PointText) && + (hitResult.item.hasFill() || hitResult.item.closed || isAlmostClosedPath(hitResult.item)); }, hitUnfilledPaths: true, tolerance: FillTool.TOLERANCE / paper.view.zoom diff --git a/src/helper/tools/text-tool.js b/src/helper/tools/text-tool.js index 33549839..a89416c0 100644 --- a/src/helper/tools/text-tool.js +++ b/src/helper/tools/text-tool.js @@ -31,12 +31,14 @@ class TextTool extends paper.Tool { * @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 + * @param {!function} setTextEditTarget Call to set text editing target whenever text editing is active */ - constructor (setSelectedItems, clearSelectedItems, onUpdateSvg) { + constructor (setSelectedItems, clearSelectedItems, onUpdateSvg, setTextEditTarget) { super(); this.setSelectedItems = setSelectedItems; this.clearSelectedItems = clearSelectedItems; this.onUpdateSvg = onUpdateSvg; + this.setTextEditTarget = setTextEditTarget; this.boundingBoxTool = new BoundingBoxTool(Modes.TEXT, setSelectedItems, clearSelectedItems, onUpdateSvg); this.nudgeTool = new NudgeTool(this.boundingBoxTool, onUpdateSvg); this.lastEvent = null; @@ -171,9 +173,11 @@ class TextTool extends paper.Tool { if (this.mode === TextTool.TEXT_EDIT_MODE) { this.guide = hoverBounds(this.textBox, TextTool.TEXT_PADDING); this.guide.dashArray = [4, 4]; + this.setTextEditTarget(this.textBox.id); } else if (this.guide) { this.guide.remove(); this.guide = null; + this.setTextEditTarget(); } } handleMouseDrag (event) { @@ -229,6 +233,7 @@ class TextTool extends paper.Tool { if (this.guide) { this.guide.remove(); this.guide = null; + this.setTextEditTarget(); } } } diff --git a/src/reducers/scratch-paint-reducer.js b/src/reducers/scratch-paint-reducer.js index 7bbe8c6f..51e689f7 100644 --- a/src/reducers/scratch-paint-reducer.js +++ b/src/reducers/scratch-paint-reducer.js @@ -7,6 +7,7 @@ import clipboardReducer from './clipboard'; import hoverReducer from './hover'; import modalsReducer from './modals'; import selectedItemReducer from './selected-items'; +import textEditTargetReducer from './text-edit-target'; import undoReducer from './undo'; export default combineReducers({ @@ -18,5 +19,6 @@ export default combineReducers({ hoveredItemId: hoverReducer, modals: modalsReducer, selectedItems: selectedItemReducer, + textEditTarget: textEditTargetReducer, undo: undoReducer }); diff --git a/src/reducers/text-edit-target.js b/src/reducers/text-edit-target.js new file mode 100644 index 00000000..45380bd8 --- /dev/null +++ b/src/reducers/text-edit-target.js @@ -0,0 +1,40 @@ +import log from '../log/log'; + +const CHANGE_TEXT_EDIT_TARGET = 'scratch-paint/text-tool/CHANGE_TEXT_EDIT_TARGET'; +const initialState = null; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case CHANGE_TEXT_EDIT_TARGET: + if (typeof action.textEditTargetId === 'undefined') { + log.warn(`Text edit target should not be set to undefined. Use null.`); + return state; + } else if (typeof action.textEditTargetId === 'undefined' || isNaN(action.textEditTargetId)) { + log.warn(`Text edit target should be an item ID number. Got: ${action.textEditTargetId}`); + return state; + } + return action.textEditTargetId; + default: + return state; + } +}; + +// Action creators ================================== +/** + * Set the currently-being-edited text field to the given item ID + * @param {?number} textEditTargetId The paper.Item ID of the active text field. + * Leave empty if there is no text editing target. + * @return {object} Redux action to change the text edit target. + */ +const setTextEditTarget = function (textEditTargetId) { + return { + type: CHANGE_TEXT_EDIT_TARGET, + textEditTargetId: textEditTargetId ? textEditTargetId : null + }; +}; + +export { + reducer as default, + setTextEditTarget +}; From 79e7d210234df28c9de6c4965001dfe4a8800635 Mon Sep 17 00:00:00 2001 From: DD Date: Fri, 16 Mar 2018 14:16:27 -0400 Subject: [PATCH 10/35] Change proptype --- src/containers/fill-color-indicator.jsx | 2 +- src/containers/stroke-color-indicator.jsx | 2 +- src/containers/stroke-width-indicator.jsx | 2 +- src/helper/tools/text-tool.js | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/containers/fill-color-indicator.jsx b/src/containers/fill-color-indicator.jsx index 317dec16..4af2befa 100644 --- a/src/containers/fill-color-indicator.jsx +++ b/src/containers/fill-color-indicator.jsx @@ -78,7 +78,7 @@ FillColorIndicator.propTypes = { onChangeFillColor: PropTypes.func.isRequired, onCloseFillColor: PropTypes.func.isRequired, onUpdateSvg: PropTypes.func.isRequired, - textEditTarget: PropTypes.string + textEditTarget: PropTypes.number }; export default connect( diff --git a/src/containers/stroke-color-indicator.jsx b/src/containers/stroke-color-indicator.jsx index fc341ca4..fd9263d3 100644 --- a/src/containers/stroke-color-indicator.jsx +++ b/src/containers/stroke-color-indicator.jsx @@ -78,7 +78,7 @@ StrokeColorIndicator.propTypes = { onUpdateSvg: PropTypes.func.isRequired, strokeColor: PropTypes.string, strokeColorModalVisible: PropTypes.bool.isRequired, - textEditTarget: PropTypes.string + textEditTarget: PropTypes.number }; export default connect( diff --git a/src/containers/stroke-width-indicator.jsx b/src/containers/stroke-width-indicator.jsx index 9eb6391e..4a3a98e3 100644 --- a/src/containers/stroke-width-indicator.jsx +++ b/src/containers/stroke-width-indicator.jsx @@ -47,7 +47,7 @@ StrokeWidthIndicator.propTypes = { onChangeStrokeWidth: PropTypes.func.isRequired, onUpdateSvg: PropTypes.func.isRequired, strokeWidth: PropTypes.number, - textEditTarget: PropTypes.string + textEditTarget: PropTypes.number }; export default connect( diff --git a/src/helper/tools/text-tool.js b/src/helper/tools/text-tool.js index a89416c0..deb04984 100644 --- a/src/helper/tools/text-tool.js +++ b/src/helper/tools/text-tool.js @@ -200,11 +200,12 @@ class TextTool extends paper.Tool { this.active = false; } handleKeyUp (event) { - if (this.mode === TextTool.SELECT_MODEe) { + if (this.mode === TextTool.SELECT_MODE) { this.nudgeTool.onKeyUp(event); } } handleKeyDown (event) { + debugger; if (event.event.target instanceof HTMLInputElement) { // Ignore nudge if a text input field is focused return; From 242ee0d6c7c667115ea43928420ca6c6fa297cd2 Mon Sep 17 00:00:00 2001 From: DD Date: Thu, 15 Mar 2018 13:20:07 -0400 Subject: [PATCH 11/35] Unfocus text fields when canvas clicked From 44eedfe2254add418356c46258616cb111416664 Mon Sep 17 00:00:00 2001 From: DD Date: Mon, 19 Mar 2018 12:47:59 -0400 Subject: [PATCH 12/35] Remove unnecessary changes --- src/helper/hover.js | 1 + src/helper/item.js | 9 ++++----- src/helper/tools/text-tool.js | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/helper/hover.js b/src/helper/hover.js index d98ba9d7..ed4caf97 100644 --- a/src/helper/hover.js +++ b/src/helper/hover.js @@ -30,6 +30,7 @@ const getHoveredItem = function (event, hitOptions, subselect) { if (!item) { return null; } + if (isBoundsItem(item)) { return hoverBounds(item); } else if (!subselect && isGroupChild(item)) { diff --git a/src/helper/item.js b/src/helper/item.js index 736eb64e..51e8e5bf 100644 --- a/src/helper/item.js +++ b/src/helper/item.js @@ -33,16 +33,15 @@ const isGroupItem = function (item) { }; -const isPGTextItem = function (item) { - return getRootItem(item).data.isPGTextItem; -}; - - const isPointTextItem = function (item) { return item.className === 'PointText'; }; +const isPGTextItem = function (item) { + return getRootItem(item).data.isPGTextItem; +}; + const setPivot = function (item, point) { if (isBoundsItem(item)) { item.pivot = item.globalToLocal(point); diff --git a/src/helper/tools/text-tool.js b/src/helper/tools/text-tool.js index deb04984..811255c0 100644 --- a/src/helper/tools/text-tool.js +++ b/src/helper/tools/text-tool.js @@ -205,7 +205,6 @@ class TextTool extends paper.Tool { } } handleKeyDown (event) { - debugger; if (event.event.target instanceof HTMLInputElement) { // Ignore nudge if a text input field is focused return; From da0864b81bf36436bdaa6f7e9bb01e19eb29c192 Mon Sep 17 00:00:00 2001 From: DD Date: Tue, 20 Mar 2018 10:48:35 -0400 Subject: [PATCH 13/35] Add a text edit area --- src/components/text-mode/text-mode.css | 15 +++++++ src/components/text-mode/text-mode.jsx | 31 +++++++++------ src/containers/text-mode.jsx | 8 +++- src/helper/tools/text-tool.js | 55 ++++++++++++++++++-------- 4 files changed, 80 insertions(+), 29 deletions(-) create mode 100644 src/components/text-mode/text-mode.css diff --git a/src/components/text-mode/text-mode.css b/src/components/text-mode/text-mode.css new file mode 100644 index 00000000..a2e4a958 --- /dev/null +++ b/src/components/text-mode/text-mode.css @@ -0,0 +1,15 @@ +.text-area { + background: transparent; + border: none; + display: none; + font-family: Times; + font-size: 30px; + left: 0; + outline: none; + overflow: hidden; + position: absolute; + resize: none; + top: 0; + white-space: nowrap; + z-index: 1; +} \ No newline at end of file diff --git a/src/components/text-mode/text-mode.jsx b/src/components/text-mode/text-mode.jsx index 747f68b9..f4234873 100644 --- a/src/components/text-mode/text-mode.jsx +++ b/src/components/text-mode/text-mode.jsx @@ -3,23 +3,32 @@ import PropTypes from 'prop-types'; import ToolSelectComponent from '../tool-select-base/tool-select-base.jsx'; import textIcon from './text.svg'; +import styles from './text-mode.css'; const TextModeComponent = props => ( - +
+ + +