ソースを参照

mol-plugin: added TextInput wrapper and RGB color input

David Sehnal 5 年 前
コミット
0a16ec1bd2
2 ファイル変更163 行追加21 行削除
  1. 40 21
      src/mol-plugin/ui/controls/color.tsx
  2. 123 0
      src/mol-plugin/ui/controls/common.tsx

+ 40 - 21
src/mol-plugin/ui/controls/color.tsx

@@ -11,6 +11,7 @@ import { camelCaseToWords } from '../../../mol-util/string';
 import * as React from 'react';
 import { _Props, _State } from '../base';
 import { ParamProps } from './parameters';
+import { TextInput } from './common';
 
 export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Color>, { isExpanded: boolean }> {
     state = { isExpanded: false }
@@ -38,6 +39,12 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
         }
     }
 
+    onChangeText = (value: Color) => {
+        if (value !== this.props.value) {
+            this.update(value);
+        }
+    }
+
     swatch() {
         // const def = this.props.param.defaultValue;
         return <div className='msp-combined-color-swatch'>
@@ -46,25 +53,6 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
         </div>;
     }
 
-    // TODO: include text options as well?
-    // onChangeText = () => {};
-    // text() {
-    //     const [r, g, b] = Color.toRgb(this.props.value);
-    //     return <input type='text'
-    //         value={`${r} ${g} ${b}`}
-    //         placeholder={'Red Green Blue'}
-    //         onChange={this.onChangeText}
-    //         onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
-    //         disabled={this.props.isDisabled}
-    //     />;
-    // }
-    // onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
-    //     if (!this.props.onEnter) return;
-    //     if ((e.keyCode === 13 || e.charCode === 13)) {
-    //         this.props.onEnter();
-    //     }
-    // }
-
     stripStyle(): React.CSSProperties {
         return {
             background: Color.toStyle(this.props.value),
@@ -76,7 +64,6 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
         };
     }
 
-
     render() {
         const label = this.props.param.label || camelCaseToWords(this.props.name);
         return <>
@@ -89,7 +76,17 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
             {this.state.isExpanded && <div className='msp-control-offset'>
                 {this.swatch()}
                 <div className='msp-control-row'>
-                    <div style={{ position: 'relative' }}>
+                    <span>RGB</span>
+                    <div>
+                        <TextInput onChange={this.onChangeText} value={this.props.value}
+                            fromValue={formatColorRGB} toValue={getColorFromString} isValid={isValidColorString}
+                            className='msp-form-control' onEnter={this.props.onEnter} blurOnEnter={true} blurOnEscape={true}
+                            placeholder='e.g. 127 127 127' delayMs={250} />
+                    </div>
+                </div>
+                <div className='msp-control-row'>
+                    <span>Color List</span>
+                    <div>
                         <select value={this.props.value} onChange={this.onChangeSelect}>
                             {ColorValueOption(this.props.value)}
                             {ColorOptions()}
@@ -102,6 +99,28 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
     }
 }
 
+function formatColorRGB(c: Color) {
+    const [r, g, b] = Color.toRgb(c);
+    return `${r} ${g} ${b}`;
+}
+
+function getColorFromString(s: string) {
+    const cs = s.split(/\s+/g);
+    return Color.fromRgb(+cs[0], +cs[1], +cs[2]);
+}
+
+function isValidColorString(s: string) {
+    const cs = s.split(/\s+/g);
+    if (cs.length !== 3 && !(cs.length === 4 && cs[3] === '')) return false;
+    for (const c of cs) {
+        if (c === '') continue;
+        const n = +c;
+        if ('' + n !== c) return false;
+        if (n < 0 || n > 255) return false;
+    }
+    return true;
+}
+
 // the 1st color is the default value.
 const SwatchColors = [
     0x000000, 0x808080, 0xFFFFFF, 0xD33115, 0xE27300, 0xFCC400,

+ 123 - 0
src/mol-plugin/ui/controls/common.tsx

@@ -6,6 +6,7 @@
 
 import * as React from 'react';
 import { Color } from '../../../mol-util/color';
+import { PurePluginUIComponent } from '../base';
 
 export class ControlGroup extends React.Component<{ header: string, initialExpanded?: boolean }, { isExpanded: boolean }> {
     state = { isExpanded: !!this.props.initialExpanded }
@@ -28,6 +29,128 @@ export class ControlGroup extends React.Component<{ header: string, initialExpan
     }
 }
 
+export interface TextInputProps<T> {
+    className?: string,
+    style?: React.CSSProperties,
+    value: T,
+    fromValue?(v: T): string,
+    toValue?(s: string): T,
+    // TODO: add error/help messages here?
+    isValid?(s: string): boolean,
+    onChange(value: T): void,
+    onEnter?(): void,
+    onBlur?(): void,
+    delayMs?: number,
+    blurOnEnter?: boolean,
+    blurOnEscape?: boolean,
+    isDisabled?: boolean,
+    placeholder?: string
+}
+
+interface TextInputState {
+    originalValue: string,
+    value: string
+}
+
+function _id(x: any) { return x; }
+
+export class TextInput<T = string> extends PurePluginUIComponent<TextInputProps<T>, TextInputState> {
+    private input = React.createRef<HTMLInputElement>();
+    private delayHandle: any = void 0;
+    private pendingValue: T | undefined = void 0;
+
+    state = { originalValue: '', value: '' }
+
+    onBlur = () => {
+        this.setState({ value: '' + this.state.originalValue });
+        if (this.props.onBlur) this.props.onBlur();
+    }
+
+    get isPending() { return typeof this.delayHandle !== 'undefined'; }
+
+    clearTimeout() {
+        if (this.isPending) {
+            clearTimeout(this.delayHandle);
+            this.delayHandle = void 0;
+        }
+    }
+
+    raiseOnChange = () => {
+        this.props.onChange(this.pendingValue!);
+        this.pendingValue = void 0;
+    }
+
+    triggerChanged(formatted: string, converted: T) {
+        this.clearTimeout();
+
+        if (formatted === this.state.originalValue) return;
+
+        if (this.props.delayMs) {
+            this.pendingValue = converted;
+            this.delayHandle = setTimeout(this.raiseOnChange, this.props.delayMs);
+        } else {
+            this.props.onChange(converted);
+        }
+    }
+
+    onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+        const value = e.target.value;
+
+        if (this.props.isValid && !this.props.isValid(value)) {
+            this.clearTimeout();
+            this.setState({ value });
+            return;
+        }
+
+        const converted = (this.props.toValue || _id)(value);
+        const formatted = (this.props.fromValue || _id)(converted);
+        this.setState({ value: formatted }, () => this.triggerChanged(formatted, converted));
+    }
+
+    onKeyUp  = (e: React.KeyboardEvent<HTMLInputElement>) => {
+        if (e.charCode === 27 || e.keyCode === 27 /* esc */) {
+            if (this.props.blurOnEscape && this.input.current) {
+                this.input.current.blur();
+            }
+        }
+    }
+
+    onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
+        if (e.keyCode === 13 || e.charCode === 13 /* enter */) {
+            if (this.isPending) {
+                this.clearTimeout();
+                this.raiseOnChange();
+            }
+            if (this.props.blurOnEnter && this.input.current) {
+                this.input.current.blur();
+            }
+            if (this.props.onEnter) this.props.onEnter();
+        }
+    }
+
+    static getDerivedStateFromProps(props: TextInputProps<any>, state: TextInputState) {
+        const value = props.fromValue ? props.fromValue(props.value) : props.value;
+        if (value === state.originalValue) return null;
+        return { originalValue: value, value };
+    }
+
+    render() {
+        return <input type='text'
+            className={this.props.className}
+            style={this.props.style}
+            ref={this.input}
+            onBlur={this.onBlur}
+            value={this.state.value}
+            placeholder={this.props.placeholder}
+            onChange={this.onChange}
+            onKeyPress={this.props.onEnter || this.props.blurOnEnter || this.props.blurOnEscape ? this.onKeyPress : void 0}
+            onKeyDown={this.props.blurOnEscape ? this.onKeyUp : void 0}
+            disabled={!!this.props.isDisabled}
+        />;
+    }
+}
+
+// TODO: replace this with parametrized TextInput
 export class NumericInput extends React.PureComponent<{
     value: number,
     onChange: (v: number) => void,