From d431c3f8ba7329509a3d5ef7e6de63f23a433e4f Mon Sep 17 00:00:00 2001 From: apple502j <apple502j@yahoo.co.jp> Date: Sun, 17 Feb 2019 08:58:20 +0900 Subject: [PATCH] Slider range change with decimal value support --- src/components/context-menu/context-menu.css | 2 +- src/components/context-menu/context-menu.jsx | 8 ++ src/components/monitor/monitor.jsx | 13 +- .../slider-prompt/slider-prompt.css | 61 ++++++++ .../slider-prompt/slider-prompt.jsx | 130 ++++++++++++++++++ .../sprite-selector-item.jsx | 6 +- src/containers/monitor.jsx | 72 +++++++--- src/containers/slider-prompt.jsx | 84 +++++++++++ 8 files changed, 351 insertions(+), 25 deletions(-) create mode 100644 src/components/slider-prompt/slider-prompt.css create mode 100644 src/components/slider-prompt/slider-prompt.jsx create mode 100644 src/containers/slider-prompt.jsx diff --git a/src/components/context-menu/context-menu.css b/src/components/context-menu/context-menu.css index 84e2699f0..7f21c92f7 100644 --- a/src/components/context-menu/context-menu.css +++ b/src/components/context-menu/context-menu.css @@ -33,6 +33,6 @@ border-top: 1px solid $ui-black-transparent; } -.menu-item-bordered:hover { +.menu-item-danger:hover { background: $error-primary; } diff --git a/src/components/context-menu/context-menu.jsx b/src/components/context-menu/context-menu.jsx index 677f89441..b89d97af3 100644 --- a/src/components/context-menu/context-menu.jsx +++ b/src/components/context-menu/context-menu.jsx @@ -25,9 +25,17 @@ const BorderedMenuItem = props => ( /> ); +const DangerousMenuItem = props => ( + <MenuItem + {...props} + attributes={{className: classNames(styles.menuItem, styles.menuItemBordered, styles.menuItemDanger)}} + /> +); + export { BorderedMenuItem, + DangerousMenuItem, StyledContextMenu as ContextMenu, StyledMenuItem as MenuItem }; diff --git a/src/components/monitor/monitor.jsx b/src/components/monitor/monitor.jsx index 221343275..5d52655c0 100644 --- a/src/components/monitor/monitor.jsx +++ b/src/components/monitor/monitor.jsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import Draggable from 'react-draggable'; import {FormattedMessage} from 'react-intl'; import {ContextMenuTrigger} from 'react-contextmenu'; -import {ContextMenu, MenuItem} from '../context-menu/context-menu.jsx'; +import {BorderedMenuItem, ContextMenu, MenuItem} from '../context-menu/context-menu.jsx'; import Box from '../box/box.jsx'; import DefaultMonitor from './default-monitor.jsx'; import LargeMonitor from './large-monitor.jsx'; @@ -84,6 +84,14 @@ const MonitorComponent = props => ( id="gui.monitor.contextMenu.slider" /> </MenuItem>} + {props.onSliderPromptOpen && props.mode === 'slider' && + <BorderedMenuItem onClick={props.onSliderPromptOpen}> + <FormattedMessage + defaultMessage="change slider range" + description="Menu item to change the slider range" + id="gui.monitor.contextMenu.sliderRange" + /> + </BorderedMenuItem>} {props.onImport && <MenuItem onClick={props.onImport}> <FormattedMessage @@ -122,7 +130,8 @@ MonitorComponent.propTypes = { onNextMode: PropTypes.func.isRequired, onSetModeToDefault: PropTypes.func, onSetModeToLarge: PropTypes.func, - onSetModeToSlider: PropTypes.func + onSetModeToSlider: PropTypes.func, + onSliderPromptOpen: PropTypes.func }; MonitorComponent.defaultProps = { diff --git a/src/components/slider-prompt/slider-prompt.css b/src/components/slider-prompt/slider-prompt.css new file mode 100644 index 000000000..741bc505e --- /dev/null +++ b/src/components/slider-prompt/slider-prompt.css @@ -0,0 +1,61 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; + +.modal-content { + width: 360px; +} + +.body { + background: $ui-white; + padding: 1.5rem 2.25rem; +} + +.label { + font-weight: 500; + margin: 0 0 0.75rem; +} + +.min-input, .max-input { + margin-bottom: 1.5rem; + width: 100%; + border: 1px solid $ui-black-transparent; + border-radius: 5px; + padding: 0 1rem; + height: 3rem; + color: $text-primary-transparent; + font-size: .875rem; +} + +.decimal-option { + font-weight: 500; + margin: 0 0 0.75rem; + padding-bottom: 1rem; +} + +.button-row { + font-weight: bolder; + text-align: right; +} + +.button-row button { + padding: 0.75rem 1rem; + border-radius: 0.25rem; + background: white; + border: 1px solid $ui-black-transparent; + font-weight: 600; + font-size: 0.85rem; +} + +.button-row button.ok-button { + background: $motion-primary; + border: $motion-primary; + color: white; +} + +[dir="ltr"] .button-row button + button { + margin-left: 0.5rem; +} + +[dir="rtl"] .button-row button + button { + margin-right: 0.5rem; +} diff --git a/src/components/slider-prompt/slider-prompt.jsx b/src/components/slider-prompt/slider-prompt.jsx new file mode 100644 index 000000000..515bc3280 --- /dev/null +++ b/src/components/slider-prompt/slider-prompt.jsx @@ -0,0 +1,130 @@ +import classNames from 'classnames'; +import {defineMessages, FormattedMessage, intlShape, injectIntl} from 'react-intl'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import Box from '../box/box.jsx'; +import Modal from '../../containers/modal.jsx'; + +import styles from './slider-prompt.css'; + + +const messages = defineMessages({ + minValue: { + defaultMessage: 'Minimum value', + description: 'Label of slider modal', + id: 'gui.sliderModal.min' + }, + maxValue: { + defaultMessage: 'Maximum value', + description: 'Label of slider modal', + id: 'gui.sliderModal.max' + }, + title: { + defaultMessage: 'Change slider range', + description: 'Title of slider modal', + id: 'gui.sliderModal.title' + }, + decimal: { + defaultMessage: 'Handle as decimal value', + description: 'Label of a checkbox which is checked when it should handle decimal values.', + id: 'gui.sliderModal.decimal' + } +}); + +const SliderPromptComponent = props => ( + <Modal + className={styles.modalContent} + contentLabel={props.intl.formatMessage(messages.title)} + id="sliderPrompt" + onRequestClose={props.onCancel} + > + <Box className={styles.body}> + <Box className={styles.label}> + {props.intl.formatMessage(messages.minValue)} + </Box> + <Box> + <input + autoFocus + className={styles.minInput} + defaultValue={props.defaultMinValue} + name={props.intl.formatMessage(messages.minValue)} + step="0.01" + type="number" + onChange={props.onChangeMin} + onFocus={props.onFocus} + onKeyPress={props.onKeyPress} + /> + </Box> + <Box className={styles.label}> + {props.intl.formatMessage(messages.maxValue)} + </Box> + <Box> + <input + className={styles.maxInput} + defaultValue={props.defaultMaxValue} + name={props.intl.formatMessage(messages.maxValue)} + step="0.01" + type="number" + onChange={props.onChangeMax} + onFocus={props.onFocus} + onKeyPress={props.onKeyPress} + /> + </Box> + <Box className={classNames(styles.decimalOption)}> + <label + className={classNames({[styles.disabledLabel]: props.mustDecimal})} + > + <input + checked={props.decimalSelected || props.mustDecimal} + defaultValue={props.defaultDecimal} + disabled={props.mustDecimal} + type="checkbox" + onChange={props.onDecimalOptionChange} + /> + {props.intl.formatMessage(messages.decimal)} + </label> + </Box> + <Box className={styles.buttonRow}> + <button + className={styles.cancelButton} + onClick={props.onCancel} + > + <FormattedMessage + defaultMessage="Cancel" + description="Button in prompt for cancelling the dialog" + id="gui.sliderPrompt.cancel" + /> + </button> + <button + className={styles.okButton} + onClick={props.onOk} + > + <FormattedMessage + defaultMessage="OK" + description="Button in prompt for confirming the dialog" + id="gui.sliderPrompt.ok" + /> + </button> + </Box> + </Box> + </Modal> +); + +SliderPromptComponent.propTypes = { + decimalSelected: PropTypes.bool.isRequired, + defaultDecimal: PropTypes.bool, + defaultMaxValue: PropTypes.number, + defaultMinValue: PropTypes.number, + intl: intlShape, + mustDecimal: PropTypes.bool.isRequired, + onCancel: PropTypes.func.isRequired, + onChangeMax: PropTypes.func.isRequired, + onChangeMin: PropTypes.func.isRequired, + onDecimalOptionChange: PropTypes.func.isRequired, + onFocus: PropTypes.func.isRequired, + onKeyPress: PropTypes.func.isRequired, + onOk: PropTypes.func.isRequired +}; + +export default injectIntl(SliderPromptComponent); diff --git a/src/components/sprite-selector-item/sprite-selector-item.jsx b/src/components/sprite-selector-item/sprite-selector-item.jsx index 26fd3f258..e2e56170b 100644 --- a/src/components/sprite-selector-item/sprite-selector-item.jsx +++ b/src/components/sprite-selector-item/sprite-selector-item.jsx @@ -5,7 +5,7 @@ import React from 'react'; import CloseButton from '../close-button/close-button.jsx'; import styles from './sprite-selector-item.css'; import {ContextMenuTrigger} from 'react-contextmenu'; -import {BorderedMenuItem, ContextMenu, MenuItem} from '../context-menu/context-menu.jsx'; +import {DangerousMenuItem, ContextMenu, MenuItem} from '../context-menu/context-menu.jsx'; import {FormattedMessage} from 'react-intl'; // react-contextmenu requires unique id to match trigger and context menu @@ -74,13 +74,13 @@ const SpriteSelectorItem = props => ( </MenuItem> ) : null } {props.onDeleteButtonClick ? ( - <BorderedMenuItem onClick={props.onDeleteButtonClick}> + <DangerousMenuItem onClick={props.onDeleteButtonClick}> <FormattedMessage defaultMessage="delete" description="Menu item to delete in the right click menu" id="gui.spriteSelectorItem.contextMenuDelete" /> - </BorderedMenuItem> + </DangerousMenuItem> ) : null } </ContextMenu> ) : null} diff --git a/src/containers/monitor.jsx b/src/containers/monitor.jsx index dfcdd3aa4..b024d0fb9 100644 --- a/src/containers/monitor.jsx +++ b/src/containers/monitor.jsx @@ -9,6 +9,7 @@ import {addMonitorRect, getInitialPosition, resizeMonitorRect, removeMonitorRect import {getVariable, setVariableValue} from '../lib/variable-utils'; import importCSV from '../lib/import-csv'; import downloadBlob from '../lib/download-blob'; +import SliderPrompt from './slider-prompt.jsx'; import {connect} from 'react-redux'; import {Map} from 'immutable'; @@ -42,10 +43,16 @@ class Monitor extends React.Component { 'handleSetModeToDefault', 'handleSetModeToLarge', 'handleSetModeToSlider', + 'handleSliderPromptClose', + 'handleSliderPromptOk', + 'handleSliderPromptOpen', 'handleImport', 'handleExport', 'setElement' ]); + this.state = { + sliderPrompt: false + }; } componentDidMount () { let rect; @@ -135,6 +142,23 @@ class Monitor extends React.Component { mode: 'slider' })); } + handleSliderPromptClose () { + this.setState({sliderPrompt: false}); + } + handleSliderPromptOpen () { + this.setState({sliderPrompt: true}); + } + handleSliderPromptOk (min, max, decimal) { + if (min < max) { + this.props.vm.runtime.requestUpdateMonitor(Map({ + id: this.props.id, + sliderMin: min, + sliderMax: max, + isDiscrete: !decimal + })); + } + this.handleSliderPromptClose(); + } setElement (monitorElt) { this.element = monitorElt; } @@ -164,25 +188,35 @@ class Monitor extends React.Component { const showSliderOption = availableModes(this.props.opcode).indexOf('slider') !== -1; const isList = this.props.mode === 'list'; return ( - <MonitorComponent - componentRef={this.setElement} - {...monitorProps} - draggable={this.props.draggable} - height={this.props.height} - isDiscrete={this.props.isDiscrete} - max={this.props.max} - min={this.props.min} - mode={this.props.mode} - targetId={this.props.targetId} - width={this.props.width} - onDragEnd={this.handleDragEnd} - onExport={isList ? this.handleExport : null} - onImport={isList ? this.handleImport : null} - onNextMode={this.handleNextMode} - onSetModeToDefault={isList ? null : this.handleSetModeToDefault} - onSetModeToLarge={isList ? null : this.handleSetModeToLarge} - onSetModeToSlider={showSliderOption ? this.handleSetModeToSlider : null} - /> + <React.Fragment> + {this.state.sliderPrompt && <SliderPrompt + defaultDecimal={!this.props.isDiscrete} + defaultMaxValue={parseFloat(this.props.max)} + defaultMinValue={parseFloat(this.props.min)} + onCancel={this.handleSliderPromptClose} + onOk={this.handleSliderPromptOk} + />} + <MonitorComponent + componentRef={this.setElement} + {...monitorProps} + draggable={this.props.draggable} + height={this.props.height} + isDiscrete={this.props.isDiscrete} + max={this.props.max} + min={this.props.min} + mode={this.props.mode} + targetId={this.props.targetId} + width={this.props.width} + onDragEnd={this.handleDragEnd} + onExport={isList ? this.handleExport : null} + onImport={isList ? this.handleImport : null} + onNextMode={this.handleNextMode} + onSetModeToDefault={isList ? null : this.handleSetModeToDefault} + onSetModeToLarge={isList ? null : this.handleSetModeToLarge} + onSetModeToSlider={showSliderOption ? this.handleSetModeToSlider : null} + onSliderPromptOpen={this.handleSliderPromptOpen} + /> + </React.Fragment> ); } } diff --git a/src/containers/slider-prompt.jsx b/src/containers/slider-prompt.jsx new file mode 100644 index 000000000..468fa0242 --- /dev/null +++ b/src/containers/slider-prompt.jsx @@ -0,0 +1,84 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import bindAll from 'lodash.bindall'; +import SliderPromptComponent from '../components/slider-prompt/slider-prompt.jsx'; + +class SliderPrompt extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleOk', + 'handleCancel', + 'handleChangeMin', + 'handleChangeMax', + 'handleChangeDecimalOption', + 'handleKeyPress' + ]); + this.state = { + decimalSelected: this.props.defaultDecimal, + minValue: 0, + maxValue: 100, + mustDecimal: this.checkMustDecimal(this.props.defaultMinValue, this.props.defaultMaxValue) + }; + } + handleKeyPress (event) { + if (event.key === 'Enter') this.handleOk(); + } + handleFocus (event) { + event.target.select(); + } + handleOk () { + this.props.onOk(this.state.minValue, this.state.maxValue, + (this.state.decimalSelected || this.state.mustDecimal)); + } + handleCancel () { + this.props.onCancel(); + } + checkMustDecimal (min, max) { + if (min === '' || max === '') return false; + return !Number.isInteger(parseFloat(min)) || !Number.isInteger(parseFloat(max)); + } + handleChangeMin (e) { + this.setState({ + minValue: parseFloat(e.target.value), + mustDecimal: this.checkMustDecimal(e.target.value, this.state.maxValue) + }); + } + handleChangeMax (e) { + this.setState({ + maxValue: parseFloat(e.target.value), + mustDecimal: this.checkMustDecimal(this.state.minValue, e.target.value) + }); + } + handleChangeDecimalOption (e) { + this.setState({decimalSelected: e.target.checked}); + } + render () { + return ( + <SliderPromptComponent + decimalSelected={this.state.decimalSelected} + defaultDecimal={this.props.defaultDecimal} + defaultMaxValue={this.props.defaultMaxValue} + defaultMinValue={this.props.defaultMinValue} + mustDecimal={this.state.mustDecimal} + onCancel={this.handleCancel} + onChangeMax={this.handleChangeMax} + onChangeMin={this.handleChangeMin} + onDecimalOptionChange={this.handleChangeDecimalOption} + onFocus={this.handleFocus} + onKeyPress={this.handleKeyPress} + onOk={this.handleOk} + /> + ); + } +} + +SliderPrompt.propTypes = { + defaultDecimal: PropTypes.bool, + defaultMaxValue: PropTypes.number, + defaultMinValue: PropTypes.number, + onCancel: PropTypes.func.isRequired, + onOk: PropTypes.func.isRequired +}; + +export default SliderPrompt; -- GitLab