mirror of
https://github.com/scratchfoundation/scratch-paint.git
synced 2025-01-10 14:42:13 -05:00
Move the text edit target to the state and make fill work
This commit is contained in:
parent
7a0a0784e1
commit
8d61a7b060
13 changed files with 131 additions and 59 deletions
|
@ -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(
|
||||
|
|
|
@ -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: () => {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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));
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
40
src/reducers/text-edit-target.js
Normal file
40
src/reducers/text-edit-target.js
Normal 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
|
||||
};
|
Loading…
Reference in a new issue