Browse Source

mol-plugin: transform controls

David Sehnal 6 years ago
parent
commit
458eae3b05

+ 1 - 0
src/mol-plugin/context.ts

@@ -114,6 +114,7 @@ export class PluginContext {
         this.state.data.actions
             .add(CreateStructureFromPDBe)
             .add(StateTransforms.Data.Download)
+            .add(StateTransforms.Data.ParseCif)
             .add(StateTransforms.Model.CreateStructureAssembly)
             .add(StateTransforms.Model.CreateStructure)
             .add(StateTransforms.Model.CreateModelFromTrajectory)

+ 2 - 1
src/mol-plugin/state/actions/basic.ts

@@ -20,7 +20,8 @@ export const CreateStructureFromPDBe = StateAction.create<PluginStateObject.Root
         default: () => ({ id: '1grm' }),
         controls: () => ({
             id: PD.Text('PDB id', '', '1grm'),
-        })
+        }),
+        validate: p => !p.id || !p.id.trim() ? ['Enter id.'] : void 0
     },
     apply({ params, state }) {
         const url = `http://www.ebi.ac.uk/pdbe/static/entry/${params.id.toLowerCase()}_updated.cif`;

+ 12 - 1
src/mol-plugin/state/transforms/data.ts

@@ -10,6 +10,7 @@ import { Task } from 'mol-task';
 import CIF from 'mol-io/reader/cif'
 import { PluginContext } from 'mol-plugin/context';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { Transformer } from 'mol-state';
 
 export { Download }
 namespace Download { export interface Params { url: string, isBinary?: boolean, label?: string } }
@@ -27,8 +28,10 @@ const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.B
         }),
         controls: () => ({
             url: PD.Text('URL', 'Resource URL. Must be the same domain or support CORS.', ''),
+            label: PD.Text('Label', '', ''),
             isBinary: PD.Boolean('Binary', 'If true, download data as binary (string otherwise)', false)
-        })
+        }),
+        validate: p => !p.url || !p.url.trim() ? ['Enter url.'] : void 0
     },
     apply({ params: p }, globalCtx: PluginContext) {
         return Task.create('Download', async ctx => {
@@ -38,6 +41,14 @@ const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.B
                 ? new SO.Data.Binary(data as Uint8Array, { label: p.label ? p.label : p.url })
                 : new SO.Data.String(data as string, { label: p.label ? p.label : p.url });
         });
+    },
+    update({ oldParams, newParams, b }) {
+        if (oldParams.url !== newParams.url || oldParams.isBinary !== newParams.isBinary) return Transformer.UpdateResult.Recreate;
+        if (oldParams.label !== newParams.label) {
+            (b.label as string) = newParams.label || newParams.url;
+            return Transformer.UpdateResult.Updated;
+        }
+        return Transformer.UpdateResult.Unchanged;
     }
 });
 

+ 140 - 0
src/mol-plugin/ui/action.tsx

@@ -0,0 +1,140 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as React from 'react';
+import { Transform, State, Transformer } from 'mol-state';
+import { StateAction } from 'mol-state/action';
+import { PluginCommands } from 'mol-plugin/command';
+import { PluginComponent } from './base';
+import { ParameterControls, createChangeSubject, ParamChanges } from './controls/parameters';
+import { Subject } from 'rxjs';
+import { shallowEqual } from 'mol-util/object';
+
+export { ActionContol }
+
+namespace ActionContol {
+    export interface Props {
+        nodeRef: Transform.Ref,
+        state: State,
+        action?: StateAction
+    }
+}
+
+class ActionContol extends PluginComponent<ActionContol.Props, { params: any, initialParams: any, error?: string, busy: boolean, canApply: boolean }> {
+    private changes: ParamChanges;
+    private busy: Subject<boolean>;
+
+    cell = this.props.state.cells.get(this.props.nodeRef)!;
+    parentCell = (this.cell.sourceRef && this.props.state.cells.get(this.cell.sourceRef)) || void 0;
+
+    action: StateAction | Transformer = !this.props.action ? this.cell.transform.transformer : this.props.action
+    isUpdate = !this.props.action
+
+    getDefaultParams() {
+        if (this.isUpdate) {
+            return this.cell.transform.params;
+        } else {
+            const p = this.action.definition.params;
+            if (!p || !p.default) return {};
+            const obj = this.cell;
+            if (!obj.obj) return {};
+            return p.default(obj.obj, this.plugin);
+        }
+    }
+
+    getParamDefinitions() {
+        if (this.isUpdate) {
+            const cell = this.cell;
+            const def = cell.transform.transformer.definition;
+
+            if (!cell.sourceRef || !def.params || !def.params.controls) return { };
+            const src = this.parentCell;
+            if (!src || !src.obj) return { };
+
+            return def.params.controls(src.obj, this.plugin);
+        } else {
+            const p = this.action.definition.params;
+            if (!p || !p.controls) return {};
+            const cell = this.cell;
+            if (!cell.obj) return {};
+            return p.controls(cell.obj, this.plugin);
+        }
+    }
+
+    defaultState() {
+        const params = this.getDefaultParams();
+        return { error: void 0, params, initialParams: params, busy: false, canApply: !this.isUpdate };
+    }
+
+    apply = async () => {
+        this.setState({ busy: true, initialParams: this.state.params, canApply: !this.isUpdate });
+
+        try {
+            if (Transformer.is(this.action)) {
+                await this.plugin.updateTransform(this.props.state, this.props.nodeRef, this.state.params);
+            } else {
+                await PluginCommands.State.ApplyAction.dispatch(this.plugin, {
+                    state: this.props.state,
+                    action: this.action.create(this.state.params),
+                    ref: this.props.nodeRef
+                });
+            }
+        } finally {
+            this.busy.next(false);
+        }
+    }
+
+    validate(params: any) {
+        const def = this.isUpdate ? this.cell.transform.transformer.definition.params : this.action.definition.params;
+        if (!def || !def.validate) return;
+        const cell = this.cell;
+        const error = def.validate(params, this.isUpdate ? this.parentCell!.obj! : cell.obj!, this.plugin);
+        return error && error[0];
+    }
+
+    init() {
+        this.changes = createChangeSubject();
+        this.subscribe(this.changes, ({ name, value }) => {
+            const params = { ...this.state.params, [name]: value };
+            const canApply = this.isUpdate ? !shallowEqual(params, this.state.initialParams) : true;
+            this.setState({ params, error: this.validate(params), canApply });
+        });
+
+        this.busy = new Subject();
+        this.subscribe(this.busy, busy => this.setState({ busy }));
+    }
+
+    onEnter = () => {
+        if (this.state.error) return;
+        this.apply();
+    }
+
+    refresh = () => {
+        this.setState({ params: this.state.initialParams, canApply: !this.isUpdate });
+    }
+
+    state = this.defaultState()
+
+    render() {
+        console.log('render', this.props.nodeRef, this.action.id);
+        const cell = this.cell;
+        if (cell.status !== 'ok' || (this.isUpdate && cell.transform.ref === Transform.RootRef)) return null;
+
+        const action = this.action;
+
+        return <div>
+            <div style={{ borderBottom: '1px solid #999', marginBottom: '5px' }}><h3>{(action.definition.display && action.definition.display.name) || action.id}</h3></div>
+
+            <ParameterControls params={this.getParamDefinitions()} values={this.state.params} changes={this.changes} onEnter={this.onEnter} isEnabled={!this.state.busy} />
+
+            <div style={{ textAlign: 'right' }}>
+                <span style={{ color: 'red' }}>{this.state.error}</span>
+                <button onClick={this.apply} disabled={!this.state.canApply || !!this.state.error || this.state.busy}>{this.isUpdate ? 'Update' : 'Create'}</button>
+                <button title='Refresh Params' onClick={this.refresh} disabled={this.state.busy}>↻</button>
+            </div>
+        </div>
+    }
+}

+ 0 - 115
src/mol-plugin/ui/controls.tsx

@@ -5,9 +5,6 @@
  */
 
 import * as React from 'react';
-import { Transform, State } from 'mol-state';
-import { ParametersComponent } from 'mol-app/component/parameters';
-import { StateAction } from 'mol-state/action';
 import { PluginCommands } from 'mol-plugin/command';
 import { UpdateTrajectory } from 'mol-plugin/state/actions/basic';
 import { PluginComponent } from './base';
@@ -20,7 +17,6 @@ export class Controls extends PluginComponent<{ }, { }> {
     }
 }
 
-
 export class TrajectoryControls extends PluginComponent {
     render() {
         return <div>
@@ -39,115 +35,4 @@ export class TrajectoryControls extends PluginComponent {
             })}>&gt;&gt;</button><br />
         </div>
     }
-}
-
-export class _test_ApplyAction extends PluginComponent<{ nodeRef: Transform.Ref, state: State, action: StateAction }, { params: any }> {
-    private getObj() {
-        const obj = this.props.state.cells.get(this.props.nodeRef)!;
-        return obj;
-    }
-
-    private getDefaultParams() {
-        const p = this.props.action.definition.params;
-        if (!p || !p.default) return {};
-        const obj = this.getObj();
-        if (!obj.obj) return {};
-        return p.default(obj.obj, this.plugin);
-    }
-
-    private getParamDef() {
-        const p = this.props.action.definition.params;
-        if (!p || !p.controls) return {};
-        const obj = this.getObj();
-        if (!obj.obj) return {};
-        return p.controls(obj.obj, this.plugin);
-    }
-
-    private create() {
-        console.log('Apply Action', this.state.params);
-        PluginCommands.State.ApplyAction.dispatch(this.plugin, {
-            state: this.props.state,
-            action: this.props.action.create(this.state.params),
-            ref: this.props.nodeRef
-        });
-        // this.context.applyTransform(this.props.state, this.props.nodeRef, this.props.transformer, this.state.params);
-    }
-
-    state = { params: this.getDefaultParams() }
-
-    render() {
-        const obj = this.getObj();
-        if (obj.status !== 'ok') {
-            // TODO filter this elsewhere
-            return <div />;
-        }
-
-        const action = this.props.action;
-
-        return <div key={`${this.props.nodeRef} ${this.props.action.id}`}>
-            <div style={{ borderBottom: '1px solid #999', marginBottom: '5px' }}><h3>{(action.definition.display && action.definition.display.name) || action.id}</h3></div>
-            <ParametersComponent params={this.getParamDef()} values={this.state.params as any} onChange={(k, v) => {
-                this.setState({ params: { ...this.state.params, [k]: v } });
-            }} />
-            <div style={{ textAlign: 'right' }}>
-                <button onClick={() => this.create()}>Create</button>
-            </div>
-        </div>
-    }
-}
-
-export class _test_UpdateTransform extends PluginComponent<{ state: State, nodeRef: Transform.Ref }, { params: any }> {
-    private getCell(ref?: string) {
-        return this.props.state.cells.get(ref || this.props.nodeRef)!;
-    }
-
-    private getDefParams() {
-        const cell = this.getCell();
-        if (!cell) return {};
-        return cell.transform.params;
-    }
-
-    private getParamDef() {
-        const cell = this.getCell();
-        const def = cell.transform.transformer.definition;
-
-        if (!cell.sourceRef || !def.params || !def.params.controls) return void 0;
-        const src = this.getCell(cell.sourceRef);
-        if (!src || !src.obj) return void 0;
-
-        return def.params.controls(src.obj, this.plugin);
-    }
-
-    private update() {
-        console.log(this.props.nodeRef, this.state.params);
-        this.plugin.updateTransform(this.props.state, this.props.nodeRef, this.state.params);
-    }
-
-    // componentDidMount() {
-    //     const t = this.context.state.data.tree.nodes.get(this.props.nodeRef)!;
-    //     if (t) this.setState({ params: t.value.params });
-    // }
-
-    state = { params: this.getDefParams() };
-
-    render() {
-        const cell = this.getCell();
-        const transform = cell.transform;
-        if (!transform || transform.ref === Transform.RootRef) {
-            return <div />;
-        }
-
-        const params = this.getParamDef();
-        if (!params) return <div />;
-
-        const tr = transform.transformer;
-
-        return <div key={`${this.props.nodeRef} ${tr.id}`} style={{ marginBottom: '10ox' }}>
-            <div style={{ borderBottom: '1px solid #999' }}><h3>{(tr.definition.display && tr.definition.display.name) || tr.definition.name}</h3></div>
-            <ParametersComponent params={params} values={this.state.params as any} onChange={(k, v) => {
-                this.setState({ params: { ...this.state.params, [k]: v } });
-            }} />
-            <button onClick={() => this.update()} style={{ width: '100%' }}>Update</button>
-        </div>
-    }
 }

+ 132 - 0
src/mol-plugin/ui/controls/parameters.tsx

@@ -0,0 +1,132 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { Subject } from 'rxjs';
+
+export function createChangeSubject(): ParamChanges {
+    return new Subject<{ param: PD.Base<any>, name: string, value: any }>();
+}
+
+export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
+    params: P,
+    values: any,
+    changes: ParamChanges,
+    isEnabled?: boolean,
+    onEnter?: () => void
+}
+
+export class ParameterControls<P extends PD.Params> extends React.PureComponent<ParameterControlsProps<P>, {}> {
+    render() {
+        const common = {
+            changes: this.props.changes,
+            isEnabled: this.props.isEnabled,
+            onEnter: this.props.onEnter,
+        }
+        const params = this.props.params;
+        const values = this.props.values;
+        return <div style={{ width: '100%' }}>
+            {Object.keys(params).map(key => <ParamWrapper control={controlFor(params[key])} param={params[key]} key={key} {...common} name={key} value={values[key]} />)}
+        </div>;
+    }
+}
+
+function controlFor(param: PD.Any): ValueControl {
+    switch (param.type) {
+        case 'boolean': return BoolControl;
+        case 'number': return NumberControl;
+        case 'range': return NumberControl;
+        case 'multi-select': throw new Error('nyi');
+        case 'color': throw new Error('nyi');
+        case 'select': return SelectControl;
+        case 'text': return TextControl;
+    }
+    throw new Error('not supporter');
+}
+type ParamWrapperProps = { name: string, value: any, param: PD.Base<any>, changes: ParamChanges, control: ValueControl, onEnter?: () => void, isEnabled?: boolean }
+export type ParamChanges = Subject<{ param: PD.Base<any>, name: string, value: any }>
+type ValueControlProps<P extends PD.Base<any> = PD.Base<any>> = { value: any, param: P, isEnabled?: boolean, onChange: (v: any) => void, onEnter?: () => void }
+type ValueControl = React.ComponentClass<ValueControlProps<any>>
+
+export class ParamWrapper extends React.PureComponent<ParamWrapperProps> {
+    onChange = (value: any) => {
+        this.props.changes.next({ param: this.props.param, name: this.props.name, value });
+    }
+
+    render() {
+        return <div>
+            <span title={this.props.param.description}>{this.props.param.label}</span>
+            <div>
+                <this.props.control value={this.props.value} param={this.props.param} onChange={this.onChange} onEnter={this.props.onEnter} isEnabled={this.props.isEnabled} />
+            </div>
+        </div>;
+    }
+}
+
+export class BoolControl extends React.PureComponent<ValueControlProps> {
+    onClick = () => {
+        this.props.onChange(!this.props.value);
+    }
+
+    render() {
+        return <button onClick={this.onClick} disabled={!this.props.isEnabled}>{this.props.value ? '✓ On' : '✗ Off'}</button>;
+    }
+}
+
+export class NumberControl extends React.PureComponent<ValueControlProps<PD.Numeric>> {
+    onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+        this.props.onChange(+e.target.value);
+    }
+
+    render() {
+        return <input type='range'
+            value={this.props.value}
+            min={this.props.param.min}
+            max={this.props.param.max}
+            step={this.props.param.step}
+            onChange={this.onChange}
+        />;
+    }
+}
+
+export class TextControl extends React.PureComponent<ValueControlProps<PD.Text>> {
+    onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+        const value = e.target.value;
+        if (value !== this.props.value) {
+            this.props.onChange(value);
+        }
+    }
+
+    onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
+        if (!this.props.onEnter) return;
+        if ((e.keyCode === 13 || e.charCode === 13)) {
+            this.props.onEnter();
+        }
+    }
+
+    render() {
+        return <input type='text'
+            value={this.props.value || ''}
+            onChange={this.onChange}
+            onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
+        />;
+    }
+}
+
+export class SelectControl extends React.PureComponent<ValueControlProps<PD.Select<any>>> {
+    onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
+        this.setState({ value: e.target.value });
+        this.props.onChange(e.target.value);
+    }
+
+    render() {
+        return <select value={this.props.value} onChange={this.onChange}>
+            {this.props.param.options.map(([value, label]) => <option key={label} value={value}>{label}</option>)}
+        </select>;
+    }
+}

+ 12 - 7
src/mol-plugin/ui/plugin.tsx

@@ -8,16 +8,16 @@ import * as React from 'react';
 import { PluginContext } from '../context';
 import { StateTree } from './state-tree';
 import { Viewport, ViewportControls } from './viewport';
-import { Controls, _test_UpdateTransform, _test_ApplyAction, TrajectoryControls } from './controls';
+import { Controls, TrajectoryControls } from './controls';
 import { PluginComponent, PluginReactContext } from './base';
 import { merge } from 'rxjs';
-import { State } from 'mol-state';
 import { CameraSnapshots } from './camera';
 import { StateSnapshots } from './state';
 import { List } from 'immutable';
 import { LogEntry } from 'mol-util/log-entry';
 import { formatTime } from 'mol-util';
 import { BackgroundTaskProgress } from './task';
+import { ActionContol } from './action';
 
 export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
     render() {
@@ -86,19 +86,24 @@ export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> {
 
 export class CurrentObject extends PluginComponent {
     componentDidMount() {
-        let current: State.ObjectEvent | undefined = void 0;
+        // let current: State.ObjectEvent | undefined = void 0;
         this.subscribe(merge(this.plugin.behaviors.state.data.currentObject, this.plugin.behaviors.state.behavior.currentObject), o => {
-            current = o;
+            // current = o;
             this.forceUpdate()
         });
 
         this.subscribe(this.plugin.events.state.data.object.updated, ({ ref, state }) => {
-            if (!current || current.ref !== ref && current.state !== state) return;
+            console.log('curr event', +new Date);
+            const current = this.plugin.behaviors.state.data.currentObject.value;
+            if (current.ref !== ref || current.state !== state) return;
+            console.log('curr event pass', +new Date);
             this.forceUpdate();
         });
     }
 
     render() {
+        console.log('curr', +new Date);
+
         const current = this.plugin.behaviors.state.data.currentObject.value;
 
         const ref = current.ref;
@@ -113,11 +118,11 @@ export class CurrentObject extends PluginComponent {
         return <div>
             <hr />
             <h3>Update {obj.obj ? obj.obj.label : ref}</h3>
-            <_test_UpdateTransform key={`${ref} update`} state={current.state} nodeRef={ref} />
+            <ActionContol key={`${ref} update`} state={current.state} nodeRef={ref} />
             <hr />
             <h3>Create</h3>
             {
-                actions.map((act, i) => <_test_ApplyAction key={`${act.id} ${ref} ${i}`}
+                actions.map((act, i) => <ActionContol key={`${act.id}`}
                     state={current.state} action={act} nodeRef={ref} />)
             }
         </div>;

+ 1 - 1
src/mol-state/transformer.ts

@@ -72,7 +72,7 @@ export namespace Transformer {
             /** Specify default control descriptors for the parameters */
             controls?(a: A, globalCtx: unknown): ControlsFor<P>,
             /** Check the parameters and return a list of errors if the are not valid. */
-            validate?(a: A, params: P, globalCtx: unknown): string[] | undefined,
+            validate?(params: P, a: A, globalCtx: unknown): string[] | undefined,
             /** Optional custom parameter equality. Use deep structural equal by default. */
             areEqual?(oldParams: P, newParams: P): boolean
         },