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