Move the text edit target to the state and make fill work

This commit is contained in:
DD 2018-03-16 11:36:06 -04:00
parent 7a0a0784e1
commit 8d61a7b060
13 changed files with 131 additions and 59 deletions

View file

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

View file

@ -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: () => {

View file

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

View file

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

View file

@ -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));
},

View file

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

View file

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

View file

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

View file

@ -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,56 +16,51 @@ 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) {
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)';
} else if (item.parent instanceof paper.CompoundPath) {
item = item.parent;
}
if (!_colorMatch(item.fillColor, colorString)) {
changed = true;
item.fillColor = colorString;
}
}
}
return changed;
};
/**
* 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;
};
/**

View file

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

View file

@ -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();
}
}
}

View file

@ -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
});

View file

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