diff --git a/src/components/fill-color-indicator.jsx b/src/components/color-indicator.jsx similarity index 53% rename from src/components/fill-color-indicator.jsx rename to src/components/color-indicator.jsx index 0a0fa0cf..d04ea2bb 100644 --- a/src/components/fill-color-indicator.jsx +++ b/src/components/color-indicator.jsx @@ -1,7 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import Popover from 'react-popover'; -import {defineMessages, injectIntl, intlShape} from 'react-intl'; import ColorButton from './color-button/color-button.jsx'; import ColorPicker from '../containers/color-picker.jsx'; @@ -10,15 +9,7 @@ import Label from './forms/label.jsx'; import GradientTypes from '../lib/gradient-types'; -const messages = defineMessages({ - fill: { - id: 'paint.paintEditor.fill', - description: 'Label for the color picker for the fill color', - defaultMessage: 'Fill' - } -}); - -const FillColorIndicatorComponent = props => ( +const ColorIndicatorComponent = props => ( ( } - isOpen={props.fillColorModalVisible} + isOpen={props.colorModalVisible} preferPlace="below" - onOuterAction={props.onCloseFillColor} + onOuterAction={props.onCloseColor} > - ); -FillColorIndicatorComponent.propTypes = { +ColorIndicatorComponent.propTypes = { className: PropTypes.string, disabled: PropTypes.bool.isRequired, - fillColor: PropTypes.string, - fillColor2: PropTypes.string, - fillColorModalVisible: PropTypes.bool.isRequired, + color: PropTypes.string, + color2: PropTypes.string, + colorModalVisible: PropTypes.bool.isRequired, gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired, - intl: intlShape, - onChangeFillColor: PropTypes.func.isRequired, + label: PropTypes.string.isRequired, + onChangeColor: PropTypes.func.isRequired, onChangeGradientType: PropTypes.func.isRequired, - onCloseFillColor: PropTypes.func.isRequired, - onOpenFillColor: PropTypes.func.isRequired, + onCloseColor: PropTypes.func.isRequired, + onOpenColor: PropTypes.func.isRequired, onSwap: PropTypes.func.isRequired, + outline: PropTypes.bool.isRequired, shouldShowGradientTools: PropTypes.bool.isRequired }; -export default injectIntl(FillColorIndicatorComponent); +export default ColorIndicatorComponent; diff --git a/src/components/stroke-color-indicator.jsx b/src/components/stroke-color-indicator.jsx deleted file mode 100644 index 15c40d9b..00000000 --- a/src/components/stroke-color-indicator.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Popover from 'react-popover'; -import {defineMessages, injectIntl, intlShape} from 'react-intl'; - -import ColorButton from './color-button/color-button.jsx'; -import ColorPicker from '../containers/color-picker.jsx'; -import InputGroup from './input-group/input-group.jsx'; -import Label from './forms/label.jsx'; -import GradientTypes from '../lib/gradient-types'; - -const messages = defineMessages({ - stroke: { - id: 'paint.paintEditor.stroke', - description: 'Label for the color picker for the outline color', - defaultMessage: 'Outline' - } -}); - -const StrokeColorIndicatorComponent = props => ( - - - } - isOpen={props.strokeColorModalVisible} - preferPlace="below" - onOuterAction={props.onCloseStrokeColor} - > - - - -); - -StrokeColorIndicatorComponent.propTypes = { - className: PropTypes.string, - disabled: PropTypes.bool.isRequired, - intl: intlShape, - onChangeStrokeColor: PropTypes.func.isRequired, - onCloseStrokeColor: PropTypes.func.isRequired, - onOpenStrokeColor: PropTypes.func.isRequired, - strokeColor: PropTypes.string, - strokeColorModalVisible: PropTypes.bool.isRequired -}; - -export default injectIntl(StrokeColorIndicatorComponent); diff --git a/src/containers/brush-mode.jsx b/src/containers/brush-mode.jsx index 32a80640..f1936ad6 100644 --- a/src/containers/brush-mode.jsx +++ b/src/containers/brush-mode.jsx @@ -3,6 +3,7 @@ import React from 'react'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Modes from '../lib/modes'; +import ColorStyleProptype from '../lib/color-style-proptype'; import Blobbiness from '../helper/blob-tools/blob'; import {MIXED} from '../helper/style-path'; @@ -38,7 +39,7 @@ class BrushMode extends React.Component { this.blob.setOptions({ isEraser: false, fillColor: fillColor.primary, - strokeColor, + strokeColor: strokeColor.primary, strokeWidth, ...nextProps.brushModeState }); @@ -88,11 +89,8 @@ BrushMode.propTypes = { clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ - fillColor: PropTypes.shape({ - primary: PropTypes.string, - secondary: PropTypes.string - }), - strokeColor: PropTypes.string, + fillColor: ColorStyleProptype, + strokeColor: ColorStyleProptype, strokeWidth: PropTypes.number }).isRequired, handleMouseDown: PropTypes.func.isRequired, diff --git a/src/containers/color-indicator.jsx b/src/containers/color-indicator.jsx new file mode 100644 index 00000000..37e2b40a --- /dev/null +++ b/src/containers/color-indicator.jsx @@ -0,0 +1,156 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import bindAll from 'lodash.bindall'; +import parseColor from 'parse-color'; +import {injectIntl, intlShape} from 'react-intl'; + +import {getSelectedLeafItems} from '../helper/selection'; +import Formats from '../lib/format'; +import {isBitmap} from '../lib/format'; +import GradientTypes from '../lib/gradient-types'; + +import ColorIndicatorComponent from '../components/color-indicator.jsx'; +import {applyColorToSelection, + applyGradientTypeToSelection, + applyStrokeWidthToSelection, + getRotatedColor, + swapColorsInSelection, + MIXED} from '../helper/style-path'; + +const makeColorIndicator = (label, isStroke) => { + class ColorIndicator extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleChangeColor', + 'handleChangeGradientType', + 'handleCloseColor', + 'handleSwap' + ]); + + // Flag to track whether an svg-update-worthy change has been made + this._hasChanged = false; + } + componentWillReceiveProps (newProps) { + const {colorModalVisible, onUpdateImage} = this.props; + if (colorModalVisible && !newProps.colorModalVisible) { + // Submit the new SVG, which also stores a single undo/redo action. + if (this._hasChanged) onUpdateImage(); + this._hasChanged = false; + } + } + handleChangeColor (newColor) { + // Stroke-selector-specific logic: if we change the stroke color from "none" to something visible, ensure + // there's a nonzero stroke width. If we change the stroke color to "none", set the stroke width to zero. + if (isStroke) { + if (this.props.color === null && newColor !== null) { + this._hasChanged = applyStrokeWidthToSelection(1, this.props.textEditTarget) || this._hasChanged; + this.props.onChangeStrokeWidth(1); + } else if (this.props.color !== null && newColor === null) { + this._hasChanged = applyStrokeWidthToSelection(0, this.props.textEditTarget) || this._hasChanged; + this.props.onChangeStrokeWidth(0); + } + } + + // Apply color and update redux, but do not update svg until picker closes. + const isDifferent = applyColorToSelection( + newColor, + this.props.colorIndex, + this.props.gradientType === GradientTypes.SOLID, + isBitmap(this.props.format), + isStroke, + this.props.textEditTarget); + this._hasChanged = this._hasChanged || isDifferent; + this.props.onChangeColor(newColor, this.props.colorIndex); + } + handleChangeGradientType (gradientType) { + // Apply color and update redux, but do not update svg until picker closes. + const isDifferent = applyGradientTypeToSelection( + gradientType, + isBitmap(this.props.format), + isStroke, + this.props.textEditTarget); + this._hasChanged = this._hasChanged || isDifferent; + const hasSelectedItems = getSelectedLeafItems().length > 0; + if (hasSelectedItems) { + if (isDifferent) { + // Recalculates the swatch colors + this.props.setSelectedItems(); + } + } + if (this.props.gradientType === GradientTypes.SOLID && gradientType !== GradientTypes.SOLID) { + // Generate color 2 and change to the 2nd swatch when switching from solid to gradient + if (!hasSelectedItems) { + this.props.onChangeColor(getRotatedColor(this.props.color), 1); + } + this.props.onChangeColorIndex(1); + } + if (this.props.onChangeGradientType) this.props.onChangeGradientType(gradientType); + } + handleCloseColor () { + // If the eyedropper is currently being used, don't + // close the color menu. + if (this.props.isEyeDropping) return; + + // Otherwise, close the color menu and + // also reset the color index to indicate + // that `color1` is selected. + this.props.onCloseColor(); + this.props.onChangeColorIndex(0); + } + handleSwap () { + if (getSelectedLeafItems().length) { + const isDifferent = swapColorsInSelection( + isBitmap(this.props.format), + isStroke, + this.props.textEditTarget); + this.props.setSelectedItems(); + this._hasChanged = this._hasChanged || isDifferent; + } else { + let color1 = this.props.color; + let color2 = this.props.color2; + color1 = color1 === null || color1 === MIXED ? color1 : parseColor(color1).hex; + color2 = color2 === null || color2 === MIXED ? color2 : parseColor(color2).hex; + this.props.onChangeColor(color1, 1); + this.props.onChangeColor(color2, 0); + } + } + render () { + return ( + + ); + } + } + + ColorIndicator.propTypes = { + colorIndex: PropTypes.number.isRequired, + disabled: PropTypes.bool.isRequired, + color: PropTypes.string, + color2: PropTypes.string, + colorModalVisible: PropTypes.bool.isRequired, + format: PropTypes.oneOf(Object.keys(Formats)), + gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired, + intl: intlShape, + isEyeDropping: PropTypes.bool.isRequired, + onChangeColorIndex: PropTypes.func.isRequired, + onChangeColor: PropTypes.func.isRequired, + onChangeGradientType: PropTypes.func, + onChangeStrokeWidth: PropTypes.func, + onCloseColor: PropTypes.func.isRequired, + onUpdateImage: PropTypes.func.isRequired, + setSelectedItems: PropTypes.func.isRequired, + textEditTarget: PropTypes.number + }; + + return injectIntl(ColorIndicator); +}; + +export default makeColorIndicator; diff --git a/src/containers/fill-color-indicator.jsx b/src/containers/fill-color-indicator.jsx index 43291462..9d88070d 100644 --- a/src/containers/fill-color-indicator.jsx +++ b/src/containers/fill-color-indicator.jsx @@ -1,8 +1,5 @@ import {connect} from 'react-redux'; -import PropTypes from 'prop-types'; -import React from 'react'; -import bindAll from 'lodash.bindall'; -import parseColor from 'parse-color'; +import {defineMessages} from 'react-intl'; import {changeColorIndex} from '../reducers/color-index'; import {changeFillColor, changeFillColor2} from '../reducers/fill-style'; @@ -11,121 +8,26 @@ import {openFillColor, closeFillColor} from '../reducers/modals'; import {getSelectedLeafItems} from '../helper/selection'; import {setSelectedItems} from '../reducers/selected-items'; import Modes from '../lib/modes'; -import Formats from '../lib/format'; import {isBitmap} from '../lib/format'; -import GradientTypes from '../lib/gradient-types'; -import FillColorIndicatorComponent from '../components/fill-color-indicator.jsx'; -import {applyColorToSelection, - applyGradientTypeToSelection, - getRotatedColor, - swapColorsInSelection, - MIXED} from '../helper/style-path'; +import makeColorIndicator from './color-indicator.jsx'; -class FillColorIndicator extends React.Component { - constructor (props) { - super(props); - bindAll(this, [ - 'handleChangeFillColor', - 'handleChangeGradientType', - 'handleCloseFillColor', - 'handleSwap' - ]); +const messages = defineMessages({ + label: { + id: 'paint.paintEditor.fill', + description: 'Label for the color picker for the fill color', + defaultMessage: 'Fill' + } +}); - // Flag to track whether an svg-update-worthy change has been made - this._hasChanged = false; - } - componentWillReceiveProps (newProps) { - const {fillColorModalVisible, onUpdateImage} = this.props; - if (fillColorModalVisible && !newProps.fillColorModalVisible) { - // Submit the new SVG, which also stores a single undo/redo action. - if (this._hasChanged) onUpdateImage(); - this._hasChanged = false; - } - } - handleChangeFillColor (newColor) { - // Apply color and update redux, but do not update svg until picker closes. - const isDifferent = applyColorToSelection( - newColor, - this.props.colorIndex, - this.props.gradientType === GradientTypes.SOLID, - isBitmap(this.props.format), - false, // applyToStroke - this.props.textEditTarget); - this._hasChanged = this._hasChanged || isDifferent; - this.props.onChangeFillColor(newColor, this.props.colorIndex); - } - handleChangeGradientType (gradientType) { - // Apply color and update redux, but do not update svg until picker closes. - const isDifferent = applyGradientTypeToSelection( - gradientType, - isBitmap(this.props.format), - false, // applyToStroke - this.props.textEditTarget); - this._hasChanged = this._hasChanged || isDifferent; - const hasSelectedItems = getSelectedLeafItems().length > 0; - if (hasSelectedItems) { - if (isDifferent) { - // Recalculates the swatch colors - this.props.setSelectedItems(); - } - } - if (this.props.gradientType === GradientTypes.SOLID && gradientType !== GradientTypes.SOLID) { - // Generate color 2 and change to the 2nd swatch when switching from solid to gradient - if (!hasSelectedItems) { - this.props.onChangeFillColor(getRotatedColor(this.props.fillColor), 1); - } - this.props.onChangeColorIndex(1); - } - this.props.onChangeGradientType(gradientType); - } - handleCloseFillColor () { - // If the eyedropper is currently being used, don't - // close the fill color menu. - if (this.props.isEyeDropping) return; - - // Otherwise, close the fill color menu and - // also reset the color index to indicate - // that `color1` is selected. - this.props.onCloseFillColor(); - this.props.onChangeColorIndex(0); - } - handleSwap () { - if (getSelectedLeafItems().length) { - const isDifferent = swapColorsInSelection( - isBitmap(this.props.format), - false, // applyToStroke - this.props.textEditTarget); - this.props.setSelectedItems(); - this._hasChanged = this._hasChanged || isDifferent; - } else { - let color1 = this.props.fillColor; - let color2 = this.props.fillColor2; - color1 = color1 === null || color1 === MIXED ? color1 : parseColor(color1).hex; - color2 = color2 === null || color2 === MIXED ? color2 : parseColor(color2).hex; - this.props.onChangeFillColor(color1, 1); - this.props.onChangeFillColor(color2, 0); - } - } - render () { - return ( - - ); - } -} +const FillColorIndicator = makeColorIndicator(messages.label, false); const mapStateToProps = state => ({ colorIndex: state.scratchPaint.fillMode.colorIndex, disabled: state.scratchPaint.mode === Modes.LINE, - fillColor: state.scratchPaint.color.fillColor.primary, - fillColor2: state.scratchPaint.color.fillColor.secondary, - fillColorModalVisible: state.scratchPaint.modals.fillColor, + color: state.scratchPaint.color.fillColor.primary, + color2: state.scratchPaint.color.fillColor.secondary, + colorModalVisible: state.scratchPaint.modals.fillColor, format: state.scratchPaint.format, gradientType: state.scratchPaint.color.fillColor.gradientType, isEyeDropping: state.scratchPaint.color.eyeDropper.active, @@ -142,17 +44,17 @@ const mapDispatchToProps = dispatch => ({ onChangeColorIndex: index => { dispatch(changeColorIndex(index)); }, - onChangeFillColor: (fillColor, index) => { + onChangeColor: (fillColor, index) => { if (index === 0) { dispatch(changeFillColor(fillColor)); } else if (index === 1) { dispatch(changeFillColor2(fillColor)); } }, - onOpenFillColor: () => { + onOpenColor: () => { dispatch(openFillColor()); }, - onCloseFillColor: () => { + onCloseColor: () => { dispatch(closeFillColor()); }, onChangeGradientType: gradientType => { @@ -163,24 +65,6 @@ const mapDispatchToProps = dispatch => ({ } }); -FillColorIndicator.propTypes = { - colorIndex: PropTypes.number.isRequired, - disabled: PropTypes.bool.isRequired, - fillColor: PropTypes.string, - fillColor2: PropTypes.string, - fillColorModalVisible: PropTypes.bool.isRequired, - format: PropTypes.oneOf(Object.keys(Formats)), - gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired, - isEyeDropping: PropTypes.bool.isRequired, - onChangeColorIndex: PropTypes.func.isRequired, - onChangeFillColor: PropTypes.func.isRequired, - onChangeGradientType: PropTypes.func.isRequired, - onCloseFillColor: PropTypes.func.isRequired, - onUpdateImage: PropTypes.func.isRequired, - setSelectedItems: PropTypes.func.isRequired, - textEditTarget: PropTypes.number -}; - export default connect( mapStateToProps, mapDispatchToProps diff --git a/src/containers/line-mode.jsx b/src/containers/line-mode.jsx index 9f2d675e..2368c43b 100644 --- a/src/containers/line-mode.jsx +++ b/src/containers/line-mode.jsx @@ -4,11 +4,12 @@ import React from 'react'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Modes from '../lib/modes'; +import ColorStyleProptype from '../lib/color-style-proptype'; import {clearSelection} from '../helper/selection'; import {endPointHit, touching} from '../helper/snapping'; import {drawHitPoint, removeHitPoint} from '../helper/guides'; import {stylePath} from '../helper/style-path'; -import {changeStrokeColor} from '../reducers/stroke-color'; +import {changeStrokeColor, clearStrokeGradient} from '../reducers/stroke-style'; import {changeStrokeWidth} from '../reducers/stroke-width'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems} from '../reducers/selected-items'; @@ -58,9 +59,10 @@ class LineMode extends React.Component { } activateTool () { clearSelection(this.props.clearSelectedItems); + this.props.clearGradient(); // Force the default line color if stroke is MIXED or transparent - const {strokeColor} = this.props.colorState; + const strokeColor = this.props.colorState.strokeColor.primary; if (strokeColor === MIXED || strokeColor === null) { this.props.onChangeStrokeColor(LineMode.DEFAULT_COLOR); } @@ -101,7 +103,7 @@ class LineMode extends React.Component { this.hitResult = endPointHit(event.point, LineMode.SNAP_TOLERANCE); if (this.hitResult) { this.path = this.hitResult.path; - stylePath(this.path, this.props.colorState.strokeColor, this.props.colorState.strokeWidth); + stylePath(this.path, this.props.colorState.strokeColor.primary, this.props.colorState.strokeWidth); if (this.hitResult.isFirst) { this.path.reverse(); } @@ -114,7 +116,7 @@ class LineMode extends React.Component { if (!this.path) { this.path = new paper.Path(); this.path.strokeCap = 'round'; - stylePath(this.path, this.props.colorState.strokeColor, this.props.colorState.strokeWidth); + stylePath(this.path, this.props.colorState.strokeColor.primary, this.props.colorState.strokeWidth); this.path.add(event.point); this.path.add(event.point); // Add second point, which is what will move when dragged @@ -253,13 +255,11 @@ class LineMode extends React.Component { } LineMode.propTypes = { + clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ - fillColor: PropTypes.shape({ - primary: PropTypes.string, - secondary: PropTypes.string - }), - strokeColor: PropTypes.string, + fillColor: ColorStyleProptype, + strokeColor: ColorStyleProptype, strokeWidth: PropTypes.number }).isRequired, handleMouseDown: PropTypes.func.isRequired, @@ -274,6 +274,9 @@ const mapStateToProps = state => ({ isLineModeActive: state.scratchPaint.mode === Modes.LINE }); const mapDispatchToProps = dispatch => ({ + clearGradient: () => { + dispatch(clearStrokeGradient()); + }, clearSelectedItems: () => { dispatch(clearSelectedItems()); }, diff --git a/src/containers/oval-mode.jsx b/src/containers/oval-mode.jsx index c5b99d99..8333b3f2 100644 --- a/src/containers/oval-mode.jsx +++ b/src/containers/oval-mode.jsx @@ -4,10 +4,11 @@ import React from 'react'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Modes from '../lib/modes'; +import ColorStyleProptype from '../lib/color-style-proptype'; import {MIXED} from '../helper/style-path'; import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; -import {changeStrokeColor} from '../reducers/stroke-color'; +import {changeStrokeColor} from '../reducers/stroke-style'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; import {setCursor} from '../reducers/cursor'; @@ -57,8 +58,9 @@ class OvalMode extends React.Component { // 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 {strokeColor, strokeWidth} = this.props.colorState; + const {strokeWidth} = this.props.colorState; const fillColor = this.props.colorState.fillColor.primary; + const strokeColor = this.props.colorState.strokeColor.primary; const fillColorPresent = fillColor !== MIXED && fillColor !== null; const strokeColorPresent = strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0; @@ -98,11 +100,8 @@ OvalMode.propTypes = { clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ - fillColor: PropTypes.shape({ - primary: PropTypes.string, - secondary: PropTypes.string - }), - strokeColor: PropTypes.string, + fillColor: ColorStyleProptype, + strokeColor: ColorStyleProptype, strokeWidth: PropTypes.number }).isRequired, handleMouseDown: PropTypes.func.isRequired, diff --git a/src/containers/rect-mode.jsx b/src/containers/rect-mode.jsx index f4929f93..5730b0a6 100644 --- a/src/containers/rect-mode.jsx +++ b/src/containers/rect-mode.jsx @@ -4,10 +4,11 @@ import React from 'react'; import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Modes from '../lib/modes'; +import ColorStyleProptype from '../lib/color-style-proptype'; import {MIXED} from '../helper/style-path'; import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; -import {changeStrokeColor} from '../reducers/stroke-color'; +import {changeStrokeColor} from '../reducers/stroke-style'; import {changeMode} from '../reducers/modes'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; import {setCursor} from '../reducers/cursor'; @@ -57,8 +58,9 @@ class RectMode extends React.Component { // 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 {strokeColor, strokeWidth} = this.props.colorState; + const {strokeWidth} = this.props.colorState; const fillColor = this.props.colorState.fillColor.primary; + const strokeColor = this.props.colorState.strokeColor.primary; const fillColorPresent = fillColor !== MIXED && fillColor !== null; const strokeColorPresent = strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0; @@ -98,11 +100,8 @@ RectMode.propTypes = { clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ - fillColor: PropTypes.shape({ - primary: PropTypes.string, - secondary: PropTypes.string - }), - strokeColor: PropTypes.string, + fillColor: ColorStyleProptype, + strokeColor: ColorStyleProptype, strokeWidth: PropTypes.number }).isRequired, handleMouseDown: PropTypes.func.isRequired, diff --git a/src/containers/stroke-color-indicator.jsx b/src/containers/stroke-color-indicator.jsx index 57f68e3e..409064c1 100644 --- a/src/containers/stroke-color-indicator.jsx +++ b/src/containers/stroke-color-indicator.jsx @@ -1,109 +1,73 @@ import {connect} from 'react-redux'; -import PropTypes from 'prop-types'; -import React from 'react'; -import bindAll from 'lodash.bindall'; -import {changeStrokeColor} from '../reducers/stroke-color'; +import {defineMessages} from 'react-intl'; + +import {changeColorIndex} from '../reducers/color-index'; +import {changeStrokeColor, changeStrokeColor2} from '../reducers/stroke-style'; import {changeStrokeWidth} from '../reducers/stroke-width'; +import {changeStrokeGradientType} from '../reducers/stroke-style'; import {openStrokeColor, closeStrokeColor} from '../reducers/modals'; +import {getSelectedLeafItems} from '../helper/selection'; +import {setSelectedItems} from '../reducers/selected-items'; import Modes from '../lib/modes'; -import Formats from '../lib/format'; import {isBitmap} from '../lib/format'; -import StrokeColorIndicatorComponent from '../components/stroke-color-indicator.jsx'; -import {applyColorToSelection, applyStrokeWidthToSelection} from '../helper/style-path'; +import makeColorIndicator from './color-indicator.jsx'; -class StrokeColorIndicator extends React.Component { - constructor (props) { - super(props); - bindAll(this, [ - 'handleChangeStrokeColor', - 'handleCloseStrokeColor' - ]); +const messages = defineMessages({ + label: { + id: 'paint.paintEditor.stroke', + description: 'Label for the color picker for the outline color', + defaultMessage: 'Outline' + } +}); - // Flag to track whether an svg-update-worthy change has been made - this._hasChanged = false; - } - componentWillReceiveProps (newProps) { - const {strokeColorModalVisible, onUpdateImage} = this.props; - if (strokeColorModalVisible && !newProps.strokeColorModalVisible) { - // Submit the new SVG, which also stores a single undo/redo action. - if (this._hasChanged) onUpdateImage(); - this._hasChanged = false; - } - } - handleChangeStrokeColor (newColor) { - if (this.props.strokeColor === null && newColor !== null) { - this._hasChanged = applyStrokeWidthToSelection(1, this.props.textEditTarget) || this._hasChanged; - this.props.onChangeStrokeWidth(1); - } else if (this.props.strokeColor !== null && newColor === null) { - this._hasChanged = applyStrokeWidthToSelection(0, this.props.textEditTarget) || this._hasChanged; - this.props.onChangeStrokeWidth(0); - } - // Apply color and update redux, but do not update svg until picker closes. - this._hasChanged = applyColorToSelection( - newColor, - 0, // colorIndex, - true, // isSolidGradient - isBitmap(this.props.format), - true, // applyToStroke - this.props.textEditTarget) || - this._hasChanged; - this.props.onChangeStrokeColor(newColor); - } - handleCloseStrokeColor () { - if (!this.props.isEyeDropping) { - this.props.onCloseStrokeColor(); - } - } - render () { - return ( - - ); - } -} +const StrokeColorIndicator = makeColorIndicator(messages.label, true); const mapStateToProps = state => ({ + colorIndex: state.scratchPaint.fillMode.colorIndex, disabled: state.scratchPaint.mode === Modes.BRUSH || state.scratchPaint.mode === Modes.TEXT || state.scratchPaint.mode === Modes.FILL, + color: state.scratchPaint.color.strokeColor.primary, + color2: state.scratchPaint.color.strokeColor.secondary, + colorModalVisible: state.scratchPaint.modals.strokeColor, format: state.scratchPaint.format, + gradientType: state.scratchPaint.color.strokeColor.gradientType, isEyeDropping: state.scratchPaint.color.eyeDropper.active, - strokeColor: state.scratchPaint.color.strokeColor, - strokeColorModalVisible: state.scratchPaint.modals.strokeColor, + mode: state.scratchPaint.mode, + shouldShowGradientTools: state.scratchPaint.mode === Modes.SELECT || + state.scratchPaint.mode === Modes.RESHAPE, textEditTarget: state.scratchPaint.textEditTarget }); const mapDispatchToProps = dispatch => ({ - onChangeStrokeColor: strokeColor => { - dispatch(changeStrokeColor(strokeColor)); + onChangeColorIndex: index => { + dispatch(changeColorIndex(index)); + }, + onChangeColor: (strokeColor, index) => { + if (index === 0) { + dispatch(changeStrokeColor(strokeColor)); + } else if (index === 1) { + dispatch(changeStrokeColor2(strokeColor)); + } }, onChangeStrokeWidth: strokeWidth => { dispatch(changeStrokeWidth(strokeWidth)); }, - onOpenStrokeColor: () => { + onOpenColor: () => { dispatch(openStrokeColor()); }, - onCloseStrokeColor: () => { + onCloseColor: () => { dispatch(closeStrokeColor()); + }, + onChangeGradientType: gradientType => { + dispatch(changeStrokeGradientType(gradientType)); + }, + setSelectedItems: format => { + dispatch(setSelectedItems(getSelectedLeafItems(), isBitmap(format))); } }); -StrokeColorIndicator.propTypes = { - format: PropTypes.oneOf(Object.keys(Formats)), - isEyeDropping: PropTypes.bool.isRequired, - onChangeStrokeColor: PropTypes.func.isRequired, - onChangeStrokeWidth: PropTypes.func.isRequired, - onCloseStrokeColor: PropTypes.func.isRequired, - onUpdateImage: PropTypes.func.isRequired, - strokeColor: PropTypes.string, - strokeColorModalVisible: PropTypes.bool.isRequired, - textEditTarget: PropTypes.number -}; - export default connect( mapStateToProps, mapDispatchToProps diff --git a/src/containers/stroke-width-indicator.jsx b/src/containers/stroke-width-indicator.jsx index 16412677..31e4d88f 100644 --- a/src/containers/stroke-width-indicator.jsx +++ b/src/containers/stroke-width-indicator.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import bindAll from 'lodash.bindall'; import parseColor from 'parse-color'; -import {changeStrokeColor} from '../reducers/stroke-color'; +import {changeStrokeColor} from '../reducers/stroke-style'; import {changeStrokeWidth} from '../reducers/stroke-width'; import StrokeWidthIndicatorComponent from '../components/stroke-width-indicator.jsx'; import {getSelectedLeafItems} from '../helper/selection'; diff --git a/src/containers/text-mode.jsx b/src/containers/text-mode.jsx index eda064d7..ce270cd5 100644 --- a/src/containers/text-mode.jsx +++ b/src/containers/text-mode.jsx @@ -5,11 +5,12 @@ import {connect} from 'react-redux'; import bindAll from 'lodash.bindall'; import Fonts from '../lib/fonts'; import Modes from '../lib/modes'; +import ColorStyleProptype from '../lib/color-style-proptype'; import {MIXED} from '../helper/style-path'; import {changeFont} from '../reducers/font'; import {changeFillColor, clearFillGradient, DEFAULT_COLOR} from '../reducers/fill-style'; -import {changeStrokeColor} from '../reducers/stroke-color'; +import {changeStrokeColor} from '../reducers/stroke-style'; import {changeMode} from '../reducers/modes'; import {setTextEditTarget} from '../reducers/text-edit-target'; import {clearSelectedItems, setSelectedItems} from '../reducers/selected-items'; @@ -81,8 +82,9 @@ class TextMode extends React.Component { // 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 {strokeColor, strokeWidth} = nextProps.colorState; - const fillColor = this.props.colorState.fillColor.primary; + const {strokeWidth} = nextProps.colorState; + const fillColor = nextProps.colorState.fillColor.primary; + const strokeColor = nextProps.colorState.strokeColor.primary; const fillColorPresent = fillColor !== MIXED && fillColor !== null; const strokeColorPresent = nextProps.isBitmap ? false : strokeColor !== MIXED && strokeColor !== null && strokeWidth !== null && strokeWidth !== 0; @@ -143,11 +145,8 @@ TextMode.propTypes = { clearGradient: PropTypes.func.isRequired, clearSelectedItems: PropTypes.func.isRequired, colorState: PropTypes.shape({ - fillColor: PropTypes.shape({ - primary: PropTypes.string, - secondary: PropTypes.string - }), - strokeColor: PropTypes.string, + fillColor: ColorStyleProptype, + strokeColor: ColorStyleProptype, strokeWidth: PropTypes.number }).isRequired, font: PropTypes.string, diff --git a/src/helper/style-path.js b/src/helper/style-path.js index b6af421a..53ad35e3 100644 --- a/src/helper/style-path.js +++ b/src/helper/style-path.js @@ -516,10 +516,6 @@ const getColorsFromSelection = function (selectedItems, bitmapMode) { }; } - // Treat stroke gradients as MIXED - // TODO: remove this once stroke gradients are supported - if (selectionStrokeGradientType !== GradientTypes.SOLID) selectionStrokeColorString = MIXED; - return { fillColor: selectionFillColorString ? selectionFillColorString : null, fillColor2: selectionFillColor2String ? selectionFillColor2String : null, @@ -566,7 +562,7 @@ const styleCursorPreview = function (path, options) { // TODO: style using gradient? const styleShape = function (path, options) { path.fillColor = options.fillColor.primary; - path.strokeColor = options.strokeColor; + path.strokeColor = options.strokeColor.primary; path.strokeWidth = options.strokeWidth; }; diff --git a/src/lib/color-style-proptype.js b/src/lib/color-style-proptype.js new file mode 100644 index 00000000..6e59603f --- /dev/null +++ b/src/lib/color-style-proptype.js @@ -0,0 +1,9 @@ +import {PropTypes} from 'prop-types'; + +import GradientTypes from './gradient-types'; + +export default PropTypes.shape({ + primary: PropTypes.string, + secondary: PropTypes.string, + gradientType: PropTypes.oneOf(Object.keys(GradientTypes)).isRequired +}); diff --git a/src/reducers/color.js b/src/reducers/color.js index 1ceb3aa4..4d50d49b 100644 --- a/src/reducers/color.js +++ b/src/reducers/color.js @@ -1,7 +1,7 @@ import {combineReducers} from 'redux'; import eyeDropperReducer from './eye-dropper'; import fillColorReducer from './fill-style'; -import strokeColorReducer from './stroke-color'; +import strokeColorReducer from './stroke-style'; import strokeWidthReducer from './stroke-width'; export default combineReducers({ diff --git a/src/reducers/fill-style.js b/src/reducers/fill-style.js index 98046c49..b97c4bef 100644 --- a/src/reducers/fill-style.js +++ b/src/reducers/fill-style.js @@ -35,7 +35,7 @@ const changeFillColor2 = function (fillColor) { const changeFillGradientType = function (gradientType) { return { type: CHANGE_FILL_GRADIENT_TYPE, - gradientType: gradientType + gradientType }; }; diff --git a/src/reducers/stroke-color.js b/src/reducers/stroke-color.js deleted file mode 100644 index 50cad337..00000000 --- a/src/reducers/stroke-color.js +++ /dev/null @@ -1,51 +0,0 @@ -import log from '../log/log'; -import {CHANGE_SELECTED_ITEMS} from './selected-items'; -import {CHANGE_STROKE_WIDTH} from './stroke-width'; -import {getColorsFromSelection, MIXED} from '../helper/style-path'; - -const CHANGE_STROKE_COLOR = 'scratch-paint/stroke-color/CHANGE_STROKE_COLOR'; -const initialState = '#000'; -// Matches hex colors -const regExp = /^#([0-9a-f]{3}){1,2}$/i; - -const reducer = function (state, action) { - if (typeof state === 'undefined') state = initialState; - switch (action.type) { - case CHANGE_STROKE_WIDTH: - if (Math.max(0, action.strokeWidth) === 0) { - return null; - } - return state; - case CHANGE_STROKE_COLOR: - if (!regExp.test(action.strokeColor) && action.strokeColor !== null && action.strokeColor !== MIXED) { - log.warn(`Invalid hex color code: ${action.fillColor}`); - return state; - } - return action.strokeColor; - case CHANGE_SELECTED_ITEMS: - // Don't change state if no selection - if (!action.selectedItems || !action.selectedItems.length) { - return state; - } - // Bitmap mode doesn't have stroke color - if (action.bitmapMode) { - return state; - } - return getColorsFromSelection(action.selectedItems, action.bitmapMode).strokeColor; - default: - return state; - } -}; - -// Action creators ================================== -const changeStrokeColor = function (strokeColor) { - return { - type: CHANGE_STROKE_COLOR, - strokeColor: strokeColor - }; -}; - -export { - reducer as default, - changeStrokeColor -}; diff --git a/src/reducers/stroke-style.js b/src/reducers/stroke-style.js new file mode 100644 index 00000000..72a15168 --- /dev/null +++ b/src/reducers/stroke-style.js @@ -0,0 +1,56 @@ +import makeColorStyleReducer from '../lib/make-color-style-reducer'; + +const CHANGE_STROKE_COLOR = 'scratch-paint/stroke-style/CHANGE_STROKE_COLOR'; +const CHANGE_STROKE_COLOR_2 = 'scratch-paint/stroke-style/CHANGE_STROKE_COLOR_2'; +const CHANGE_STROKE_GRADIENT_TYPE = 'scratch-paint/stroke-style/CHANGE_STROKE_GRADIENT_TYPE'; +const CLEAR_STROKE_GRADIENT = 'scratch-paint/stroke-style/CLEAR_STROKE_GRADIENT'; +const DEFAULT_COLOR = '#000000'; + +const reducer = makeColorStyleReducer({ + changePrimaryColorAction: CHANGE_STROKE_COLOR, + changeSecondaryColorAction: CHANGE_STROKE_COLOR_2, + changeGradientTypeAction: CHANGE_STROKE_GRADIENT_TYPE, + clearGradientAction: CLEAR_STROKE_GRADIENT, + defaultColor: DEFAULT_COLOR, + selectionPrimaryColorKey: 'strokeColor', + selectionSecondaryColorKey: 'strokeColor2', + selectionGradientTypeKey: 'strokeGradientType' +}); + +// Action creators ================================== +const changeStrokeColor = function (strokeColor) { + return { + type: CHANGE_STROKE_COLOR, + color: strokeColor + }; +}; + +const changeStrokeColor2 = function (strokeColor) { + return { + type: CHANGE_STROKE_COLOR_2, + color: strokeColor + }; +}; + +const changeStrokeGradientType = function (gradientType) { + return { + type: CHANGE_STROKE_GRADIENT_TYPE, + gradientType + }; +}; + +const clearStrokeGradient = function () { + return { + type: CLEAR_STROKE_GRADIENT + }; +}; + +export { + reducer as default, + changeStrokeColor, + changeStrokeColor2, + changeStrokeGradientType, + clearStrokeGradient, + DEFAULT_COLOR, + CHANGE_STROKE_GRADIENT_TYPE +};