Browse Source

Merge branch 'master' into gl-lines

# Conflicts:
#	src/mol-plugin/ui/controls/parameters.tsx
#	src/mol-util/param-definition.ts
Alexander Rose 6 years ago
parent
commit
8dc339e450

+ 2 - 1
src/mol-plugin/behavior/behavior.ts

@@ -10,6 +10,7 @@ import { Task } from 'mol-task';
 import { PluginContext } from 'mol-plugin/context';
 import { PluginCommand } from '../command';
 import { Observable } from 'rxjs';
+import { ParamDefinition } from 'mol-util/param-definition';
 
 export { PluginBehavior }
 
@@ -36,7 +37,7 @@ namespace PluginBehavior {
             group: string,
             description?: string
         },
-        params?: Transformer.Definition<Root, Behavior, P>['params'],
+        params?(a: Root, globalCtx: PluginContext): { [K in keyof P]: ParamDefinition.Any }
     }
 
     export function create<P>(params: CreateParams<P>) {

+ 14 - 9
src/mol-plugin/behavior/static/representation.ts

@@ -6,19 +6,21 @@
 
 import { PluginStateObject as SO } from '../../state/objects';
 import { PluginContext } from 'mol-plugin/context';
+import { Representation } from 'mol-repr/representation';
+import { State } from 'mol-state';
 
 export function registerDefault(ctx: PluginContext) {
     SyncRepresentationToCanvas(ctx);
+    UpdateRepresentationVisibility(ctx);
 }
 
 export function SyncRepresentationToCanvas(ctx: PluginContext) {
     const events = ctx.state.dataState.events;
     events.object.created.subscribe(e => {
         if (!SO.isRepresentation3D(e.obj)) return;
+        updateVisibility(e, e.obj.data);
         ctx.canvas3d.add(e.obj.data);
         ctx.canvas3d.requestDraw(true);
-
-        // TODO: update visiblity, e.obj.data.setVisibility()
     });
     events.object.updated.subscribe(e => {
         if (e.oldObj && SO.isRepresentation3D(e.oldObj)) {
@@ -29,16 +31,15 @@ export function SyncRepresentationToCanvas(ctx: PluginContext) {
 
         if (!SO.isRepresentation3D(e.obj)) return;
 
-        // TODO: update visiblity, e.obj.data.setVisibility()
+        updateVisibility(e, e.obj.data);
         ctx.canvas3d.add(e.obj.data);
         ctx.canvas3d.requestDraw(true);
     });
     events.object.removed.subscribe(e => {
-        const oo = e.obj;
-        if (!SO.isRepresentation3D(oo)) return;
-        ctx.canvas3d.remove(oo.data);
+        if (!SO.isRepresentation3D(e.obj)) return;
+        ctx.canvas3d.remove(e.obj.data);
         ctx.canvas3d.requestDraw(true);
-        oo.data.destroy();
+        e.obj.data.destroy();
     });
 }
 
@@ -46,7 +47,11 @@ export function UpdateRepresentationVisibility(ctx: PluginContext) {
     ctx.state.dataState.events.cell.stateUpdated.subscribe(e => {
         const cell = e.state.cells.get(e.ref)!;
         if (!SO.isRepresentation3D(cell.obj)) return;
-
-        // TODO: update visiblity, e.obj.data.setVisibility()
+        updateVisibility(e, cell.obj.data);
+        ctx.canvas3d.requestDraw(true);
     })
+}
+
+function updateVisibility(e: State.ObjectEvent, r: Representation<any>) {
+    r.setVisibility(!e.state.cellStates.get(e.ref).isHidden);
 }

+ 23 - 2
src/mol-plugin/behavior/static/state.ts

@@ -8,8 +8,10 @@ import { PluginCommands } from '../../command';
 import { PluginContext } from '../../context';
 import { StateTree, Transform, State } from 'mol-state';
 import { PluginStateSnapshotManager } from 'mol-plugin/state/snapshots';
+import { PluginStateObject as SO } from '../../state/objects';
 
 export function registerDefault(ctx: PluginContext) {
+    SyncBehaviors(ctx);
     SetCurrentObject(ctx);
     Update(ctx);
     ApplyAction(ctx);
@@ -19,6 +21,25 @@ export function registerDefault(ctx: PluginContext) {
     Snapshots(ctx);
 }
 
+export function SyncBehaviors(ctx: PluginContext) {
+    ctx.events.state.object.created.subscribe(o => {
+        if (!SO.isBehavior(o.obj)) return;
+        o.obj.data.register();
+    });
+
+    ctx.events.state.object.removed.subscribe(o => {
+        if (!SO.isBehavior(o.obj)) return;
+        o.obj.data.unregister();
+    });
+
+    ctx.events.state.object.updated.subscribe(o => {
+        if (o.action === 'recreate') {
+            if (o.oldObj && SO.isBehavior(o.oldObj)) o.oldObj.data.unregister();
+            if (o.obj && SO.isBehavior(o.obj)) o.obj.data.register();
+        }
+    });
+}
+
 export function SetCurrentObject(ctx: PluginContext) {
     PluginCommands.State.SetCurrentObject.subscribe(ctx, ({ state, ref }) => state.setCurrent(ref));
 }
@@ -43,11 +64,11 @@ export function ToggleExpanded(ctx: PluginContext) {
 }
 
 export function ToggleVisibility(ctx: PluginContext) {
-    PluginCommands.State.ToggleVisibility.subscribe(ctx, ({ state, ref }) => setVisibility(state, ref, !state.tree.cellStates.get(ref).isHidden));
+    PluginCommands.State.ToggleVisibility.subscribe(ctx, ({ state, ref }) => setVisibility(state, ref, !state.cellStates.get(ref).isHidden));
 }
 
 function setVisibility(state: State, root: Transform.Ref, value: boolean) {
-    StateTree.doPreOrder(state.tree, state.tree.transforms.get(root), { state, value }, setVisibilityVisitor);
+    StateTree.doPreOrder(state.tree, state.transforms.get(root), { state, value }, setVisibilityVisitor);
 }
 
 function setVisibilityVisitor(t: Transform, tree: StateTree, ctx: { state: State, value: boolean }) {

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

@@ -20,7 +20,6 @@ import { BuiltInPluginBehaviors } from './behavior';
 import { PluginCommand, PluginCommands } from './command';
 import { PluginSpec } from './spec';
 import { PluginState } from './state';
-import { PluginStateObject as SO } from './state/objects';
 import { TaskManager } from './util/task-manager';
 
 export class PluginContext {
@@ -43,8 +42,6 @@ export class PluginContext {
                 removed: merge(this.state.dataState.events.object.removed, this.state.behaviorState.events.object.removed),
                 updated: merge(this.state.dataState.events.object.updated, this.state.behaviorState.events.object.updated)
             },
-            // data: this.state.dataState.events,
-            // behavior: this.state.behaviorState.events,
             cameraSnapshots: this.state.cameraSnapshots.events,
             snapshots: this.state.snapshots.events,
         },
@@ -58,10 +55,6 @@ export class PluginContext {
     }
 
     readonly behaviors = {
-        // state: {
-        //     data: this.state.dataState.behaviors,
-        //     behavior: this.state.behaviorState.behaviors
-        // },
         canvas: {
             highlightLoci: this.ev.behavior<{ loci: Loci, repr?: Representation.Any }>({ loci: EmptyLoci }),
             selectLoci: this.ev.behavior<{ loci: Loci, repr?: Representation.Any }>({ loci: EmptyLoci }),
@@ -145,33 +138,12 @@ export class PluginContext {
         return PluginCommands.State.Update.dispatch(this, { state, tree });
     }
 
-    private initEvents() {
-        this.events.state.object.created.subscribe(o => {
-            if (!SO.isBehavior(o.obj)) return;
-            o.obj.data.register();
-        });
-
-        this.events.state.object.removed.subscribe(o => {
-            if (!SO.isBehavior(o.obj)) return;
-            o.obj.data.unregister();
-        });
-
-        this.events.state.object.updated.subscribe(o => {
-            if (o.action === 'recreate') {
-                if (o.oldObj && SO.isBehavior(o.oldObj)) o.oldObj.data.unregister();
-                if (o.obj && SO.isBehavior(o.obj)) o.obj.data.register();
-            }
-        });
-    }
-
     constructor(public spec: PluginSpec) {
-        this.initEvents();
         this.initBuiltInBehavior();
 
         this.initBehaviors();
         this.initDataActions();
     }
 
-    // logger = ;
     // settings = ;
 }

+ 1 - 1
src/mol-plugin/spec.ts

@@ -6,7 +6,7 @@
 
 import { StateAction } from 'mol-state/action';
 import { Transformer } from 'mol-state';
-import { StateTransformParameters } from './ui/state/parameters';
+import { StateTransformParameters } from './ui/state/common';
 
 export { PluginSpec }
 

+ 5 - 10
src/mol-plugin/state/actions/basic.ts

@@ -17,13 +17,7 @@ export const CreateStructureFromPDBe = StateAction.create<PluginStateObject.Root
         name: 'Entry from PDBe',
         description: 'Download a structure from PDBe and create its default Assembly and visual'
     },
-    params: {
-        default: () => ({ id: '1grm' }),
-        definition: () => ({
-            id: PD.Text('1grm', { label: 'PDB id' }),
-        }),
-        // validate: p => !p.id || !p.id.trim() ? [['Enter id.', 'id']] : void 0
-    },
+    params: () => ({ id: PD.Text('1grm', { label: 'PDB id' }) }),
     apply({ params, state }) {
         const url = `http://www.ebi.ac.uk/pdbe/static/entry/${params.id.toLowerCase()}_updated.cif`;
         const b = state.build();
@@ -63,9 +57,10 @@ export const UpdateTrajectory = StateAction.create<PluginStateObject.Root, void,
     display: {
         name: 'Update Trajectory'
     },
-    params: {
-        default: () => ({ action: 'reset', by: 1 })
-    },
+    params: () => ({
+        action: PD.Select('advance', [['advance', 'Advance'], ['reset', 'Reset']]),
+        by: PD.Numeric(1, { min: -1, max: 1, step: 1 }, { isOptional: true })
+    }),
     apply({ params, state }) {
         const models = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Model).filter(c => c.transform.transformer === StateTransforms.Model.CreateModelFromTrajectory));
 

+ 5 - 11
src/mol-plugin/state/transforms/data.ts

@@ -22,17 +22,11 @@ const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.B
     },
     from: [SO.Root],
     to: [SO.Data.String, SO.Data.Binary],
-    params: {
-        default: () => ({
-            url: 'https://www.ebi.ac.uk/pdbe/static/entry/1cbs_updated.cif'
-        }),
-        definition: () => ({
-            url: PD.Text('', { description: 'Resource URL. Must be the same domain or support CORS.' }),
-            label: PD.Text(''),
-            isBinary: PD.Boolean(false, { description: 'If true, download data as binary (string otherwise)' })
-        }),
-        // validate: p => !p.url || !p.url.trim() ? [['Enter url.', 'url']] : void 0
-    },
+    params: () => ({
+        url: PD.Text('https://www.ebi.ac.uk/pdbe/static/entry/1cbs_updated.cif', { description: 'Resource URL. Must be the same domain or support CORS.' }),
+        label: PD.Text('', { isOptional: true }),
+        isBinary: PD.Boolean(false, { description: 'If true, download data as binary (string otherwise)', isOptional: true })
+    }),
     apply({ params: p }, globalCtx: PluginContext) {
         return Task.create('Download', async ctx => {
             // TODO: track progress

+ 18 - 20
src/mol-plugin/state/transforms/model.ts

@@ -12,6 +12,7 @@ import { ParamDefinition as PD } from 'mol-util/param-definition';
 import Expression from 'mol-script/language/expression';
 import { compile } from 'mol-script/runtime/query/compiler';
 import { Mat4 } from 'mol-math/linear-algebra';
+import { MolScriptBuilder } from 'mol-script/language/builder';
 
 export { ParseTrajectoryFromMmCif }
 namespace ParseTrajectoryFromMmCif { export interface Params { blockHeader?: string } }
@@ -23,16 +24,14 @@ const ParseTrajectoryFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Mol
     },
     from: [SO.Data.Cif],
     to: [SO.Molecule.Trajectory],
-    params: {
-        default: a => ({ blockHeader: a.data.blocks[0].header }),
-        definition(a) {
-            const { blocks } = a.data;
-            if (blocks.length === 0) return {};
-            return {
-                blockHeader: PD.Select(blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' })
-            };
-        }
+    params(a) {
+        const { blocks } = a.data;
+        if (blocks.length === 0) return { };
+        return {
+            blockHeader: PD.Select(blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' })
+        };
     },
+    isApplicable: a => a.data.blocks.length > 0,
     apply({ a, params }) {
         return Task.create('Parse mmCIF', async ctx => {
             const header = params.blockHeader || a.data.blocks[0].header;
@@ -47,6 +46,7 @@ const ParseTrajectoryFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Mol
 });
 
 export { CreateModelFromTrajectory }
+const plus1 = (v: number) => v + 1, minus1 = (v: number) => v - 1;
 namespace CreateModelFromTrajectory { export interface Params { modelIndex: number } }
 const CreateModelFromTrajectory = PluginStateTransform.Create<SO.Molecule.Trajectory, SO.Molecule.Model, CreateModelFromTrajectory.Params>({
     name: 'create-model-from-trajectory',
@@ -56,10 +56,7 @@ const CreateModelFromTrajectory = PluginStateTransform.Create<SO.Molecule.Trajec
     },
     from: [SO.Molecule.Trajectory],
     to: [SO.Molecule.Model],
-    params: {
-        default: () => ({ modelIndex: 0 }),
-        definition: a => ({ modelIndex: PD.Numeric(0, { min: 0, max: Math.max(0, a.data.length - 1), step: 1 }, { description: 'Model Index' }) })
-    },
+    params: a => ({ modelIndex: PD.Converted(plus1, minus1, PD.Numeric(1, { min: 1, max: a.data.length, step: 1 }, { description: 'Model Index' })) }),
     isApplicable: a => a.data.length > 0,
     apply({ a, params }) {
         if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`);
@@ -101,13 +98,10 @@ const CreateStructureAssembly = PluginStateTransform.Create<SO.Molecule.Model, S
     },
     from: [SO.Molecule.Model],
     to: [SO.Molecule.Structure],
-    params: {
-        default: () => ({ id: void 0 }),
-        definition(a) {
-            const model = a.data;
-            const ids = model.symmetry.assemblies.map(a => [a.id, a.id] as [string, string]);
-            return { id: PD.Select(ids.length ? ids[0][0] : '', ids, { label: 'Asm Id', description: 'Assembly Id' }) };
-        }
+    params(a) {
+        const model = a.data;
+        const ids = model.symmetry.assemblies.map(a => [a.id, a.id] as [string, string]);
+        return { id: PD.Select(ids.length ? ids[0][0] : '', ids, { label: 'Asm Id', description: 'Assembly Id' }) };
     },
     apply({ a, params }) {
         return Task.create('Build Assembly', async ctx => {
@@ -135,6 +129,10 @@ const CreateStructureSelection = PluginStateTransform.Create<SO.Molecule.Structu
     },
     from: [SO.Molecule.Structure],
     to: [SO.Molecule.Structure],
+    params: () => ({
+        query: PD.Value<Expression>(MolScriptBuilder.struct.generator.all),
+        label: PD.Text('', { isOptional: true })
+    }),
     apply({ a, params }) {
         // TODO: use cache, add "update"
         const compiled = compile<StructureSelection>(params.query);

+ 10 - 18
src/mol-plugin/state/transforms/visuals.ts

@@ -22,24 +22,16 @@ const CreateStructureRepresentation = PluginStateTransform.Create<SO.Molecule.St
     display: { name: 'Create 3D Representation' },
     from: [SO.Molecule.Structure],
     to: [SO.Molecule.Representation3D],
-    params: {
-        default: (a, ctx: PluginContext) => ({
-            type: {
-                name: ctx.structureReprensentation.registry.default.name,
-                params: ctx.structureReprensentation.registry.default.provider.defaultValues
-            }
-        }),
-        definition: (a, ctx: PluginContext) => ({
-            type: PD.Mapped(
-                ctx.structureReprensentation.registry.default.name,
-                ctx.structureReprensentation.registry.types,
-                name => PD.Group(
-                    ctx.structureReprensentation.registry.get(name).getParams(ctx.structureReprensentation.themeCtx, a.data),
-                    { label: 'Params' }
-                )
-            )
-        })
-    },
+    params: (a, ctx: PluginContext) => ({
+        type: PD.Mapped(
+            ctx.structureReprensentation.registry.default.name,
+            ctx.structureReprensentation.registry.types,
+            name => PD.Group<any>(
+                ctx.structureReprensentation.registry.get(name).getParams(ctx.structureReprensentation.themeCtx, a.data),
+                { label: 'Type Parameters' }
+            ),
+            { label: 'Type' })
+    }),
     apply({ a, params }, plugin: PluginContext) {
         return Task.create('Structure Representation', async ctx => {
             const provider = plugin.structureReprensentation.registry.get(params.type.name)

+ 114 - 103
src/mol-plugin/ui/controls/parameters.tsx

@@ -8,110 +8,97 @@
 import * as React from 'react'
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { camelCaseToWords } from 'mol-util/string';
+import { ColorNames } from 'mol-util/color/tables';
+import { Color } from 'mol-util/color';
 
 export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
     params: P,
     values: any,
     onChange: ParamOnChange,
-    isEnabled?: boolean,
+    isDisabled?: boolean,
     onEnter?: () => void
 }
 
 export class ParameterControls<P extends PD.Params> extends React.PureComponent<ParameterControlsProps<P>, {}> {
     render() {
-        const common = {
-            onChange: this.props.onChange,
-            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 => {
                 const param = params[key];
-                if (param.type === 'value') return null;
-                if (param.type === 'mapped') return <MappedControl param={param} key={key} {...common} name={key} value={values[key]} />
-                if (param.type === 'group') return <GroupControl param={param} key={key} {...common} name={key} value={values[key]} />
-                return <ParamWrapper control={controlFor(param)} param={param} key={key} {...common} name={key} value={values[key]} />
+                const Control = controlFor(param);
+                if (!Control) return null;
+                return <Control param={param} key={key} onChange={this.props.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} />
             })}
         </div>;
     }
 }
 
-function controlFor(param: PD.Any): ValueControl {
+function controlFor(param: PD.Any): ParamControl | undefined {
     switch (param.type) {
+        case 'value': return void 0;
         case 'boolean': return BoolControl;
         case 'number': return NumberControl;
+        case 'converted': return ConvertedControl;
         case 'multi-select': return MultiSelectControl;
         case 'color': return ColorControl;
         case 'select': return SelectControl;
         case 'text': return TextControl;
         case 'interval': return IntervalControl;
-        case 'converted': return ConvertedControl;
-        case 'group': throw Error('Must be handled separately');
-        case 'mapped': throw Error('Must be handled separately');
+        case 'group': return GroupControl;
+        case 'mapped': return MappedControl;
+        case 'line-graph': return void 0;
     }
     throw new Error('not supported');
 }
 
-type ParamWrapperProps = { name: string, value: any, param: PD.Base<any>, onChange: ParamOnChange, control: ValueControl, onEnter?: () => void, isEnabled?: boolean }
-export type ParamOnChange = (params: { param: PD.Base<any>, name: string, value: any }) => void
-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>>
+// type ParamWrapperProps = { name: string, value: any, param: PD.Base<any>, onChange: ParamOnChange, control: ValueControl, onEnter?: () => void, isEnabled?: boolean }
 
-function getLabel(name: string, param: PD.Base<any>) {
-    return param.label === undefined ? camelCaseToWords(name) : param.label
-}
+export type ParamOnChange = (params: { param: PD.Base<any>, name: string, value: any }) => void
+export interface ParamProps<P extends PD.Base<any> = PD.Base<any>> { name: string, value: P['defaultValue'], param: P, isDisabled?: boolean, onChange: ParamOnChange, onEnter?: () => void }
+export type ParamControl = React.ComponentClass<ParamProps<any>>
 
-export class ParamWrapper extends React.PureComponent<ParamWrapperProps> {
-    onChange = (value: any) => {
+export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent<ParamProps<P>> {
+    protected update(value: any) {
         this.props.onChange({ param: this.props.param, name: this.props.name, value });
     }
 
+    abstract renderControl(): JSX.Element;
+
     render() {
+        const label = this.props.param.label || camelCaseToWords(this.props.name);
         return <div style={{ padding: '0 3px', borderBottom: '1px solid #ccc' }}>
-            <div style={{ lineHeight: '20px', float: 'left' }} title={this.props.param.description}>{getLabel(this.props.name, this.props.param)}</div>
+            <div style={{ lineHeight: '20px', float: 'left' }} title={this.props.param.description}>{label}</div>
             <div style={{ float: 'left', marginLeft: '5px' }}>
-                <this.props.control value={this.props.value} param={this.props.param} onChange={this.onChange} onEnter={this.props.onEnter} isEnabled={this.props.isEnabled} />
+                {this.renderControl()}
             </div>
             <div style={{ clear: 'both' }} />
         </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 BoolControl extends SimpleParam<PD.Boolean> {
+    onClick = () => { this.update(!this.props.value); }
+    renderControl() {
+        return <button onClick={this.onClick} disabled={this.props.isDisabled}>{this.props.value ? '✓ On' : '✗ Off'}</button>;
     }
 }
 
-export class NumberControl extends React.PureComponent<ValueControlProps<PD.Numeric>, { value: string }> {
-    // state = { value: this.props.value }
-    onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-        this.props.onChange(+e.target.value);
-        // this.setState({ value: 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 NumberControl extends SimpleParam<PD.Numeric> {
+    onChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.update(+e.target.value); }
+    renderControl() {
+        return <span>
+            <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} disabled={this.props.isDisabled} />
+            <br />{this.props.value}
+        </span>
     }
 }
 
-export class TextControl extends React.PureComponent<ValueControlProps<PD.Text>> {
+export class TextControl extends SimpleParam<PD.Text> {
     onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
         const value = e.target.value;
         if (value !== this.props.value) {
-            this.props.onChange(value);
+            this.update(value);
         }
     }
 
@@ -122,79 +109,79 @@ export class TextControl extends React.PureComponent<ValueControlProps<PD.Text>>
         }
     }
 
-    render() {
+    renderControl() {
         return <input type='text'
             value={this.props.value || ''}
             onChange={this.onChange}
             onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
+            disabled={this.props.isDisabled}
         />;
     }
 }
 
-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}>
+export class SelectControl extends SimpleParam<PD.Select<any>> {
+    onChange = (e: React.ChangeEvent<HTMLSelectElement>) => { this.update(e.target.value); }
+    renderControl() {
+        return <select value={this.props.value || ''} onChange={this.onChange} disabled={this.props.isDisabled}>
             {this.props.param.options.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
         </select>;
     }
 }
 
-export class MultiSelectControl extends React.PureComponent<ValueControlProps<PD.MultiSelect<any>>> {
-    onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
-        const value = Array.from(e.target.options).filter(option => option.selected).map(option => option.value);
-        this.setState({ value });
-        this.props.onChange(value);
-    }
-
-    render() {
-        return <select multiple value={this.props.value || ''} onChange={this.onChange}>
-            {this.props.param.options.map(([value, label]) => <option key={label} value={value}>{label}</option>)}
-        </select>;
-    }
-}
-
-export class IntervalControl extends React.PureComponent<ValueControlProps<PD.Interval>> {
+export class IntervalControl extends SimpleParam<PD.Interval> {
     // onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
     //     this.setState({ value: e.target.value });
     //     this.props.onChange(e.target.value);
     // }
 
-    render() {
+    renderControl() {
         return <span>interval TODO</span>;
     }
 }
 
-export class ColorControl extends React.PureComponent<ValueControlProps<PD.Color>> {
-    // onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
-    //     this.setState({ value: e.target.value });
-    //     this.props.onChange(e.target.value);
-    // }
+export class ColorControl extends SimpleParam<PD.Color> {
+    onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
+        this.update(Color(parseInt(e.target.value)));
+    }
 
-    render() {
-        return <span>color TODO</span>;
+    renderControl() {
+        return <select value={this.props.value} onChange={this.onChange}>
+            {Object.keys(ColorNames).map(name => {
+                return <option key={name} value={(ColorNames as { [k: string]: Color})[name]}>{name}</option>
+            })}
+        </select>;
     }
 }
 
-export class ConvertedControl extends React.PureComponent<ValueControlProps<PD.Converted<any, any>>> {
-    onChange = (v: any) => {
-        console.log('onChange', v)
-        this.props.onChange(this.props.param.toValue(v));
+export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiSelect<any>>> {
+    change(value: PD.MultiSelect<any>['defaultValue'] ) {
+        console.log(this.props.name, value);
+        this.props.onChange({ name: this.props.name, param: this.props.param, value });
     }
 
-    render() {
-        const Control: ValueControl = controlFor(this.props.param.convertedControl as PD.Any);
+    toggle(key: string) {
+        return () => {
+            if (this.props.value.indexOf(key) < 0) this.change(this.props.value.concat(key));
+            else this.change(this.props.value.filter(v => v !== key))
+        }
+    }
 
-        return <Control value={this.props.param.fromValue(this.props.value)} param={this.props.param.convertedControl} onChange={this.onChange} onEnter={this.props.onEnter} isEnabled={this.props.isEnabled} />
+    render() {
+        const current = this.props.value;
+        const label = this.props.param.label || camelCaseToWords(this.props.name);
+        return <div>
+            <div>{label} <small>{`${current.length} of ${this.props.param.options.length}`}</small></div>
+            <div style={{ paddingLeft: '7px' }}>
+                {this.props.param.options.map(([value, label]) =>
+                    <button key={value} onClick={this.toggle(value)} disabled={this.props.isDisabled}>
+                        {current.indexOf(value) >= 0 ? `✓ ${label}` : `✗ ${label}`}
+                    </button>)}
+            </div>
+        </div>;
     }
 }
 
-type GroupWrapperProps = { name: string, value: PD.Group<any>['defaultValue'], param: PD.Group<any>, onChange: ParamOnChange, onEnter?: () => void, isEnabled?: boolean }
-export class GroupControl extends React.PureComponent<GroupWrapperProps> {
+export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>> {
     change(value: PD.Mapped<any>['defaultValue'] ) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
     }
@@ -207,20 +194,23 @@ export class GroupControl extends React.PureComponent<GroupWrapperProps> {
     render() {
         const value: PD.Mapped<any>['defaultValue'] = this.props.value;
         const params = this.props.param.params;
+        const label = this.props.param.label || camelCaseToWords(this.props.name);
 
+        // TODO toggle panel
         return <div>
-            <ParameterControls params={params} onChange={this.onChangeParam} values={value.params} onEnter={this.props.onEnter} isEnabled={this.props.isEnabled} />
+            <div>{label}</div>
+            <ParameterControls params={params} onChange={this.onChangeParam} values={value.params} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
         </div>
     }
 }
 
-type MappedWrapperProps = { name: string, value: PD.Mapped<any>['defaultValue'], param: PD.Mapped<any>, onChange: ParamOnChange, onEnter?: () => void, isEnabled?: boolean }
-export class MappedControl extends React.PureComponent<MappedWrapperProps> {
-    change(value: PD.Mapped<any>['defaultValue']) {
+export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>>> {
+    change(value: PD.Mapped<any>['defaultValue'] ) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
     }
 
     onChangeName: ParamOnChange = e => {
+        // TODO: Cache values when changing types?
         this.change({ name: e.value, params: this.props.param.map(e.value).defaultValue });
     }
 
@@ -232,18 +222,39 @@ export class MappedControl extends React.PureComponent<MappedWrapperProps> {
     render() {
         const value: PD.Mapped<any>['defaultValue'] = this.props.value;
         const param = this.props.param.map(value.name);
+        const Mapped = controlFor(param);
+
+        const select = <SelectControl param={this.props.param.select}
+            isDisabled={this.props.isDisabled} onChange={this.onChangeName} onEnter={this.props.onEnter}
+            name={'name'} value={value.name} />
+
+        if (!Mapped) {
+            return select;
+        }
 
         return <div>
-            <ParamWrapper control={SelectControl} param={this.props.param.select}
-                isEnabled={this.props.isEnabled} onChange={this.onChangeName} onEnter={this.props.onEnter}
-                name={getLabel(this.props.name, this.props.param)} value={value.name} />
+            {select}
             <div style={{ borderLeft: '5px solid #777', paddingLeft: '5px' }}>
-                {param.type === 'group'
-                    ? <GroupControl param={param} value={value} name='param' onChange={this.onChangeParam} onEnter={this.props.onEnter} isEnabled={this.props.isEnabled} />
-                    : param.type === 'mapped' || param.type === 'value'
-                        ? null
-                        : <ParamWrapper control={controlFor(param)} param={param} onChange={this.onChangeParam} onEnter={this.props.onEnter} isEnabled={this.props.isEnabled} name={'value'} value={value} />}
+                <Mapped param={param} value={value} name='param' onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
             </div>
         </div>
     }
-}
+}
+
+export class ConvertedControl extends React.PureComponent<ParamProps<PD.Converted<any, any>>> {
+    onChange: ParamOnChange = e => {
+        this.props.onChange({
+            name: this.props.name,
+            param: this.props.param,
+            value: this.props.param.toValue(e.value)
+        });
+    }
+
+    render() {
+        const value = this.props.param.fromValue(this.props.value);
+        const Converted = controlFor(this.props.param.converted);
+
+        if (!Converted) return null;
+        return <Converted param={this.props.param.converted} value={value} name={this.props.name} onChange={this.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
+    }
+}

+ 1 - 1
src/mol-plugin/ui/plugin.tsx

@@ -129,7 +129,7 @@ export class CurrentObject extends PluginComponent {
 
         const type = obj && obj.obj ? obj.obj.type : void 0;
 
-        const transform = current.state.tree.transforms.get(ref);
+        const transform = current.state.transforms.get(ref);
 
         const actions = type
             ? current.state.actions.fromType(type)

+ 4 - 4
src/mol-plugin/ui/state-tree.tsx

@@ -33,7 +33,7 @@ class StateTreeNode extends PluginComponent<{ nodeRef: string, state: State }, {
     }
 
     get cellState() {
-        return this.props.state.tree.cellStates.get(this.props.nodeRef);
+        return this.props.state.cellStates.get(this.props.nodeRef);
     }
 
     componentDidMount() {
@@ -104,7 +104,7 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
             } else if (isCurrent) {
                 isCurrent = false;
                 // have to check the node wasn't remove
-                if (e.state.tree.transforms.has(this.props.nodeRef)) this.forceUpdate();
+                if (e.state.transforms.has(this.props.nodeRef)) this.forceUpdate();
             }
         });
     }
@@ -125,7 +125,7 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
     }
 
     render() {
-        const n = this.props.state.tree.transforms.get(this.props.nodeRef)!;
+        const n = this.props.state.transforms.get(this.props.nodeRef)!;
         const cell = this.props.state.cells.get(this.props.nodeRef)!;
 
         const isCurrent = this.is(this.props.state.behaviors.currentObject.value);
@@ -141,7 +141,7 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
             label = <><a href='#' onClick={this.setCurrent}>{obj.label}</a> {obj.description ? <small>{obj.description}</small> : void 0}</>;
         }
 
-        const cellState = this.props.state.tree.cellStates.get(this.props.nodeRef);
+        const cellState = this.props.state.cellStates.get(this.props.nodeRef);
         const visibility = <>[<a href='#' onClick={this.toggleVisible}>{cellState.isHidden ? 'H' : 'V'}</a>]</>;
 
         return <>

+ 29 - 81
src/mol-plugin/ui/state/apply-action.tsx

@@ -4,15 +4,13 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import * as React from 'react';
 import { PluginCommands } from 'mol-plugin/command';
+import { PluginContext } from 'mol-plugin/context';
 import { State, Transform } from 'mol-state';
 import { StateAction } from 'mol-state/action';
-import { Subject } from 'rxjs';
-import { PurePluginComponent } from '../base';
-import { StateTransformParameters } from './parameters';
 import { memoizeOne } from 'mol-util/memoize';
-import { PluginContext } from 'mol-plugin/context';
+import { StateTransformParameters, TransformContolBase } from './common';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
 
 export { ApplyActionContol };
 
@@ -25,7 +23,8 @@ namespace ApplyActionContol {
     }
 
     export interface ComponentState {
-        nodeRef: Transform.Ref,
+        ref: Transform.Ref,
+        version: string,
         params: any,
         error?: string,
         busy: boolean,
@@ -33,92 +32,41 @@ namespace ApplyActionContol {
     }
 }
 
-class ApplyActionContol extends PurePluginComponent<ApplyActionContol.Props, ApplyActionContol.ComponentState> {
-    private busy: Subject<boolean>;
-
-    onEnter = () => {
-        if (this.state.error) return;
-        this.apply();
-    }
-
-    source = this.props.state.cells.get(this.props.nodeRef)!.obj!;
-
-    getInfo = memoizeOne((t: Transform.Ref) => StateTransformParameters.infoFromAction(this.plugin, this.props.state, this.props.action, this.props.nodeRef));
-
-    events: StateTransformParameters.Props['events'] = {
-        onEnter: this.onEnter,
-        onChange: (params, isInitial, errors) => {
-            this.setState({ params, isInitial, error: errors && errors[0] })
-        }
-    }
-
-    // getInitialParams() {
-    //     const p = this.props.action.definition.params;
-    //     if (!p || !p.default) return {};
-    //     return p.default(this.source, this.plugin);
-    // }
-
-    // initialErrors() {
-    //     const p = this.props.action.definition.params;
-    //     if (!p || !p.validate) return void 0;
-    //     const errors = p.validate(this.info.initialValues, this.source, this.plugin);
-    //     return errors && errors[0];
-    // }
-
-    state = { nodeRef: this.props.nodeRef, error: void 0, isInitial: true, params: this.getInfo(this.props.nodeRef).initialValues, busy: false };
-
-    apply = async () => {
-        this.setState({ busy: true });
-
-        try {
-            await PluginCommands.State.ApplyAction.dispatch(this.plugin, {
-                state: this.props.state,
-                action: this.props.action.create(this.state.params),
-                ref: this.props.nodeRef
-            });
-        } finally {
-            this.busy.next(false);
-        }
+class ApplyActionContol extends TransformContolBase<ApplyActionContol.Props, ApplyActionContol.ComponentState> {
+    applyAction() {
+        return PluginCommands.State.ApplyAction.dispatch(this.plugin, {
+            state: this.props.state,
+            action: this.props.action.create(this.state.params),
+            ref: this.props.nodeRef
+        });
     }
+    getInfo() { return this._getInfo(this.props.nodeRef, this.props.state.transforms.get(this.props.nodeRef).version); }
+    getHeader() { return this.props.action.definition.display; }
+    getHeaderFallback() { return this.props.action.id; }
+    isBusy() { return !!this.state.error || this.state.busy; }
+    applyText() { return 'Apply'; }
 
-    init() {
-        this.busy = new Subject();
-        this.subscribe(this.busy, busy => this.setState({ busy }));
-    }
+    private _getInfo = memoizeOne((t: Transform.Ref, v: string) => StateTransformParameters.infoFromAction(this.plugin, this.props.state, this.props.action, this.props.nodeRef));
 
-    refresh = () => {
-        this.setState({ params: this.getInfo(this.props.nodeRef).initialValues, isInitial: true, error: void 0 });
-    }
+    state = { ref: this.props.nodeRef, version: this.props.state.transforms.get(this.props.nodeRef).version, error: void 0, isInitial: true, params: this.getInfo().initialValues, busy: false };
 
     static getDerivedStateFromProps(props: ApplyActionContol.Props, state: ApplyActionContol.ComponentState) {
-        if (props.nodeRef === state.nodeRef) return null;
+        if (props.nodeRef === state.ref) return null;
+        const version = props.state.transforms.get(props.nodeRef).version;
+        if (version === state.version) return null;
+
         const source = props.state.cells.get(props.nodeRef)!.obj!;
-        const definition = props.action.definition.params || { };
-        const initialValues = definition.default ? definition.default(source, props.plugin) : {};
+        const params = props.action.definition.params
+            ? PD.getDefaultValues(props.action.definition.params(source, props.plugin))
+            : { };
 
         const newState: Partial<ApplyActionContol.ComponentState> = {
-            nodeRef: props.nodeRef,
-            params: initialValues,
+            ref: props.nodeRef,
+            version,
+            params,
             isInitial: true,
             error: void 0
         };
         return newState;
     }
-
-    render() {
-        const info = this.getInfo(this.props.nodeRef);
-        const action = this.props.action;
-
-        return <div>
-            <div style={{ borderBottom: '1px solid #999', marginBottom: '5px' }}><h3>{(action.definition.display && action.definition.display.name) || action.id}</h3></div>
-
-            <StateTransformParameters info={info} events={this.events} params={this.state.params} isEnabled={!this.state.busy} />
-
-            <div style={{ textAlign: 'right' }}>
-                <span style={{ color: 'red' }}>{this.state.error}</span>
-                {this.state.isInitial ? void 0 : <button title='Refresh Params' onClick={this.refresh} disabled={this.state.busy}>↻</button>}
-                <button onClick={this.apply} disabled={!!this.state.error || this.state.busy}>Create</button>
-            </div>
-        </div>
-    }
 }

+ 158 - 0
src/mol-plugin/ui/state/common.tsx

@@ -0,0 +1,158 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StateObject, State, Transform, StateObjectCell, Transformer } from 'mol-state';
+import * as React from 'react';
+import { PurePluginComponent } from '../base';
+import { ParameterControls, ParamOnChange } from '../controls/parameters';
+import { StateAction } from 'mol-state/action';
+import { PluginContext } from 'mol-plugin/context';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { Subject } from 'rxjs';
+
+export { StateTransformParameters, TransformContolBase };
+
+class StateTransformParameters extends PurePluginComponent<StateTransformParameters.Props> {
+    validate(params: any) {
+        // TODO
+        return void 0;
+    }
+
+    areInitial(params: any) {
+        return PD.areEqual(this.props.info.params, params, this.props.info.initialValues);
+    }
+
+    onChange: ParamOnChange = ({ name, value }) => {
+        const params = { ...this.props.params, [name]: value };
+        this.props.events.onChange(params, this.areInitial(params), this.validate(params));
+    };
+
+    render() {
+        return <ParameterControls params={this.props.info.params} values={this.props.params} onChange={this.onChange} onEnter={this.props.events.onEnter} isDisabled={this.props.isDisabled} />;
+    }
+}
+
+
+namespace StateTransformParameters {
+    export interface Props {
+        info: {
+            params: PD.Params,
+            initialValues: any,
+            source: StateObject,
+            isEmpty: boolean
+        },
+        events: {
+            onChange: (params: any, areInitial: boolean, errors?: string[]) => void,
+            onEnter: () => void,
+        }
+        params: any,
+        isDisabled?: boolean
+    }
+
+    export type Class = React.ComponentClass<Props>
+
+    export function infoFromAction(plugin: PluginContext, state: State, action: StateAction, nodeRef: Transform.Ref): Props['info'] {
+        const source = state.cells.get(nodeRef)!.obj!;
+        const params = action.definition.params ? action.definition.params(source, plugin) : { };
+        const initialValues = PD.getDefaultValues(params);
+        return {
+            source,
+            initialValues,
+            params,
+            isEmpty: Object.keys(params).length === 0
+        };
+    }
+
+    export function infoFromTransform(plugin: PluginContext, state: State, transform: Transform): Props['info'] {
+        const cell = state.cells.get(transform.ref)!;
+        const source: StateObjectCell | undefined = (cell.sourceRef && state.cells.get(cell.sourceRef)!) || void 0;
+        const create = transform.transformer.definition.params;
+        const params = create ? create((source && source.obj) as any, plugin) : { };
+        return {
+            source: (source && source.obj) as any,
+            initialValues: transform.params,
+            params,
+            isEmpty: Object.keys(params).length === 0
+        }
+    }
+}
+
+namespace TransformContolBase {
+    export interface State {
+        params: any,
+        error?: string,
+        busy: boolean,
+        isInitial: boolean
+    }
+}
+
+abstract class TransformContolBase<P, S extends TransformContolBase.State> extends PurePluginComponent<P, S> {
+    abstract applyAction(): Promise<void>;
+    abstract getInfo(): StateTransformParameters.Props['info'];
+    abstract getHeader(): Transformer.Definition['display'];
+    abstract getHeaderFallback(): string;
+    abstract isBusy(): boolean;
+    abstract applyText(): string;
+    abstract state: S;
+
+    private busy: Subject<boolean>;
+
+    private onEnter = () => {
+        if (this.state.error) return;
+        this.apply();
+    }
+
+    events: StateTransformParameters.Props['events'] = {
+        onEnter: this.onEnter,
+        onChange: (params, isInitial, errors) => this.setState({ params, isInitial, error: errors && errors[0] })
+    }
+
+    apply = async () => {
+        this.setState({ busy: true });
+        try {
+            await this.applyAction();
+        } finally {
+            this.busy.next(false);
+        }
+    }
+
+    init() {
+        this.busy = new Subject();
+        this.subscribe(this.busy, busy => this.setState({ busy }));
+    }
+
+    refresh = () => {
+        this.setState({ params: this.getInfo().initialValues, isInitial: true, error: void 0 });
+    }
+
+    setDefault = () => {
+        const info = this.getInfo();
+        const params = PD.getDefaultValues(info.params);
+        this.setState({ params, isInitial: PD.areEqual(info.params, params, info.initialValues), error: void 0 });
+    }
+
+    render() {
+        const info = this.getInfo();
+        if (info.isEmpty) return <div>Nothing to update</div>;
+
+        const display = this.getHeader();
+
+        return <div>
+            <div style={{ borderBottom: '1px solid #999', marginBottom: '5px' }}>
+                <button onClick={this.setDefault} disabled={this.state.busy} style={{ float: 'right'}} title='Set default params'>↻</button>
+                <h3>{(display && display.name) || this.getHeaderFallback()}</h3>
+            </div>
+
+            <StateTransformParameters info={info} events={this.events} params={this.state.params} isDisabled={this.state.busy} />
+
+            <div style={{ textAlign: 'right' }}>
+                <span style={{ color: 'red' }}>{this.state.error}</span>
+                {this.state.isInitial ? void 0 : <button title='Refresh params' onClick={this.refresh} disabled={this.state.busy}>↶</button>}
+                <button onClick={this.apply} disabled={this.isBusy()}>{this.applyText()}</button>
+            </div>
+        </div>
+    }
+}

+ 0 - 99
src/mol-plugin/ui/state/parameters.tsx

@@ -1,99 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { StateObject, State, Transform, StateObjectCell, Transformer } from 'mol-state';
-import { shallowEqual } from 'mol-util/object';
-import * as React from 'react';
-import { PurePluginComponent } from '../base';
-import { ParameterControls, ParamOnChange } from '../controls/parameters';
-import { StateAction } from 'mol-state/action';
-import { PluginContext } from 'mol-plugin/context';
-import { ParamDefinition as PD } from 'mol-util/param-definition';
-
-export { StateTransformParameters };
-
-class StateTransformParameters extends PurePluginComponent<StateTransformParameters.Props> {
-    getDefinition() {
-        const controls = this.props.info.definition.definition;
-        if (!controls) return { };
-        return controls!(this.props.info.source, this.plugin)
-    }
-
-    validate(params: any) {
-        // TODO
-        return void 0;
-
-        // const validate = this.props.info.definition.validate;
-        // if (!validate) return void 0;
-        // const result = validate(params, this.props.info.source, this.plugin);
-        // if (!result || result.length === 0) return void 0;
-        // return result.map(r => r[0]);
-    }
-
-    areInitial(params: any) {
-        const areEqual = this.props.info.definition.areEqual;
-        if (!areEqual) return shallowEqual(params, this.props.info.initialValues);
-        return areEqual(params, this.props.info.initialValues);
-    }
-
-    onChange: ParamOnChange = ({ name, value }) => {
-        const params = { ...this.props.params, [name]: value };
-        this.props.events.onChange(params, this.areInitial(params), this.validate(params));
-    };
-
-    render() {
-        return <ParameterControls params={this.props.info.params} values={this.props.params} onChange={this.onChange} onEnter={this.props.events.onEnter} isEnabled={this.props.isEnabled} />;
-    }
-}
-
-
-namespace StateTransformParameters {
-    export interface Props {
-        info: {
-            definition: Transformer.ParamsProvider,
-            params: PD.Params,
-            initialValues: any,
-            source: StateObject,
-            isEmpty: boolean
-        },
-        events: {
-            onChange: (params: any, areInitial: boolean, errors?: string[]) => void,
-            onEnter: () => void,
-        }
-        params: any,
-        isEnabled?: boolean
-    }
-
-    export type Class = React.ComponentClass<Props>
-
-    export function infoFromAction(plugin: PluginContext, state: State, action: StateAction, nodeRef: Transform.Ref): Props['info'] {
-        const source = state.cells.get(nodeRef)!.obj!;
-        const definition = action.definition.params || { };
-        const initialValues = definition.default ? definition.default(source, plugin) : {};
-        const params = definition.definition ? definition.definition(source, plugin) : {};
-        return {
-            source,
-            definition: action.definition.params || { },
-            initialValues,
-            params,
-            isEmpty: Object.keys(params).length === 0
-        };
-    }
-
-    export function infoFromTransform(plugin: PluginContext, state: State, transform: Transform): Props['info'] {
-        const cell = state.cells.get(transform.ref)!;
-        const source: StateObjectCell | undefined = (cell.sourceRef && state.cells.get(cell.sourceRef)!) || void 0;
-        const definition = transform.transformer.definition.params || { };
-        const params = definition.definition ? definition.definition((source && source.obj) as any, plugin) : {};
-        return {
-            source: (source && source.obj) as any,
-            definition,
-            initialValues: transform.params,
-            params,
-            isEmpty: Object.keys(params).length === 0
-        }
-    }
-}

+ 10 - 58
src/mol-plugin/ui/state/update-transform.tsx

@@ -5,11 +5,8 @@
  */
 
 import { State, Transform } from 'mol-state';
-import * as React from 'react';
-import { Subject } from 'rxjs';
-import { PurePluginComponent } from '../base';
-import { StateTransformParameters } from './parameters';
 import { memoizeOne } from 'mol-util/memoize';
+import { StateTransformParameters, TransformContolBase } from './common';
 
 export { UpdateTransformContol };
 
@@ -28,43 +25,17 @@ namespace UpdateTransformContol {
     }
 }
 
-class UpdateTransformContol extends PurePluginComponent<UpdateTransformContol.Props, UpdateTransformContol.ComponentState> {
-    private busy: Subject<boolean>;
+class UpdateTransformContol extends TransformContolBase<UpdateTransformContol.Props, UpdateTransformContol.ComponentState> {
+    applyAction() { return this.plugin.updateTransform(this.props.state, this.props.transform.ref, this.state.params); }
+    getInfo() { return this._getInfo(this.props.transform); }
+    getHeader() { return this.props.transform.transformer.definition.display; }
+    getHeaderFallback() { return this.props.transform.transformer.definition.name; }
+    isBusy() { return !!this.state.error || this.state.busy || this.state.isInitial; }
+    applyText() { return 'Update'; }
 
-    onEnter = () => {
-        if (this.state.error) return;
-        this.apply();
-    }
-
-    getInfo = memoizeOne((t: Transform) => StateTransformParameters.infoFromTransform(this.plugin, this.props.state, this.props.transform));
-
-    events: StateTransformParameters.Props['events'] = {
-        onEnter: this.onEnter,
-        onChange: (params, isInitial, errors) => {
-            this.setState({ params, isInitial, error: errors && errors[0] })
-        }
-    }
-
-    state: UpdateTransformContol.ComponentState = { transform: this.props.transform, error: void 0, isInitial: true, params: this.getInfo(this.props.transform).initialValues, busy: false };
+    private _getInfo = memoizeOne((t: Transform) => StateTransformParameters.infoFromTransform(this.plugin, this.props.state, this.props.transform));
 
-    apply = async () => {
-        this.setState({ busy: true });
-
-        try {
-            await this.plugin.updateTransform(this.props.state, this.props.transform.ref, this.state.params);
-        } finally {
-            this.busy.next(false);
-        }
-    }
-
-    init() {
-        this.busy = new Subject();
-        this.subscribe(this.busy, busy => this.setState({ busy }));
-    }
-
-    refresh = () => {
-        this.setState({ params: this.props.transform.params, isInitial: true, error: void 0 });
-    }
+    state: UpdateTransformContol.ComponentState = { transform: this.props.transform, error: void 0, isInitial: true, params: this.getInfo().initialValues, busy: false };
 
     static getDerivedStateFromProps(props: UpdateTransformContol.Props, state: UpdateTransformContol.ComponentState) {
         if (props.transform === state.transform) return null;
@@ -76,23 +47,4 @@ class UpdateTransformContol extends PurePluginComponent<UpdateTransformContol.Pr
         };
         return newState;
     }
-
-    render() {
-        const info = this.getInfo(this.props.transform);
-        if (info.isEmpty) return <div>Nothing to update</div>;
-
-        const tr = this.props.transform.transformer;
-
-        return <div>
-            <div style={{ borderBottom: '1px solid #999', marginBottom: '5px' }}><h3>{(tr.definition.display && tr.definition.display.name) || tr.id}</h3></div>
-
-            <StateTransformParameters info={info} events={this.events} params={this.state.params} isEnabled={!this.state.busy} />
-
-            <div style={{ textAlign: 'right' }}>
-                <span style={{ color: 'red' }}>{this.state.error}</span>
-                {this.state.isInitial ? void 0 : <button title='Refresh Params' onClick={this.refresh} disabled={this.state.busy}>↻</button>}
-                <button onClick={this.apply} disabled={!!this.state.error || this.state.busy || this.state.isInitial}>Update</button>
-            </div>
-        </div>
-    }
 }

+ 2 - 2
src/mol-state/action.ts

@@ -46,7 +46,7 @@ namespace StateAction {
          */
         apply(params: ApplyParams<A, P>, globalCtx: unknown): T | Task<T>,
 
-        readonly params?: Transformer.ParamsProvider<A, P>
+        params?(a: A, globalCtx: unknown): { [K in keyof P]: PD.Any },
 
         /** Test if the transform can be applied to a given node */
         isApplicable?(a: A, globalCtx: unknown): boolean
@@ -66,7 +66,7 @@ namespace StateAction {
         return create<Transformer.From<T>, void, Transformer.Params<T>>({
             from: def.from,
             display: def.display,
-            params: def.params as Transformer<Transformer.From<T>, any, Transformer.Params<T>>['definition']['params'],
+            params: def.params as Transformer.Definition<Transformer.From<T>, any, Transformer.Params<T>>['params'],
             apply({ cell, state, params }) {
                 const tree = state.build().to(cell.transform.ref).apply(transformer, params);
                 return state.update(tree);

+ 2 - 0
src/mol-state/state.ts

@@ -52,6 +52,8 @@ class State {
     readonly actions = new StateActionManager();
 
     get tree(): StateTree { return this._tree; }
+    get transforms() { return (this._tree as StateTree).transforms; }
+    get cellStates() { return (this._tree as StateTree).cellStates; }
     get current() { return this.behaviors.currentObject.value.ref; }
 
     build() { return this._tree.build(); }

+ 3 - 10
src/mol-state/transformer.ts

@@ -23,7 +23,6 @@ export namespace Transformer {
     export type Params<T extends Transformer<any, any, any>> = T extends Transformer<any, any, infer P> ? P : unknown;
     export type From<T extends Transformer<any, any, any>> = T extends Transformer<infer A, any, any> ? A : unknown;
     export type To<T extends Transformer<any, any, any>> = T extends Transformer<any, infer B, any> ? B : unknown;
-    export type ControlsFor<Props> = { [P in keyof Props]?: PD.Any }
 
     export function is(obj: any): obj is Transformer {
         return !!obj && typeof (obj as Transformer).toAction === 'function' && typeof (obj as Transformer).apply === 'function';
@@ -47,14 +46,8 @@ export namespace Transformer {
 
     export enum UpdateResult { Unchanged, Updated, Recreate }
 
-    export interface ParamsProvider<A extends StateObject = StateObject, P = any> {
-        /** Check the parameters and return a list of errors if the are not valid. */
-        default?(a: A, globalCtx: unknown): P,
-        /** Specify default control descriptors for the parameters */
-        definition?(a: A, globalCtx: unknown): { [K in keyof P]?: PD.Any },
-        /** Optional custom parameter equality. Use shallow structural equal by default. */
-        areEqual?(oldParams: P, newParams: P): boolean
-    }
+    /** Specify default control descriptors for the parameters */
+    // export type ParamsDefinition<A extends StateObject = StateObject, P = any> = (a: A, globalCtx: unknown) => { [K in keyof P]: PD.Any }
 
     export interface Definition<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> {
         readonly name: string,
@@ -75,7 +68,7 @@ export namespace Transformer {
          */
         update?(params: UpdateParams<A, B, P>, globalCtx: unknown): Task<UpdateResult> | UpdateResult,
 
-        readonly params?: ParamsProvider<A, P>,
+        params?(a: A, globalCtx: unknown): { [K in keyof P]: PD.Any },
 
         /** Test if the transform can be applied to a given node */
         isApplicable?(a: A, globalCtx: unknown): boolean,

+ 3 - 7
src/mol-state/tree/transient.ts

@@ -138,13 +138,9 @@ class TransientTree implements StateTree {
         ensurePresent(this.transforms, ref);
 
         const transform = this.transforms.get(ref)!;
-        const def = transform.transformer.definition;
-        if (def.params && def.params.areEqual) {
-            if (def.params.areEqual(transform.params, params)) return false;
-        } else {
-            if (shallowEqual(transform.params, params)) {
-                return false;
-            }
+        // TODO: should this be here?
+        if (shallowEqual(transform.params, params)) {
+            return false;
         }
 
         if (!this.changedNodes) {

+ 25 - 17
src/mol-util/param-definition.ts

@@ -12,14 +12,16 @@ import { deepClone } from './object';
 
 export namespace ParamDefinition {
     export interface Info {
-        label?: string
-        description?: string
+        label?: string,
+        description?: string,
+        isOptional?: boolean
     }
 
     function setInfo<T extends Info>(param: T, info?: Info): T {
         if (!info) return param;
         if (info.description) param.description = info.description;
         if (info.label) param.label = info.label;
+        if (info.isOptional) param.isOptional = info.isOptional;
         return param;
     }
 
@@ -73,8 +75,7 @@ export namespace ParamDefinition {
         return setInfo<Color>({ type: 'color', defaultValue }, info)
     }
 
-    export interface Numeric extends Base<number> {
-        type: 'number'
+    export interface Range {
         /** If given treat as a range. */
         min?: number
         /** If given treat as a range. */
@@ -85,10 +86,7 @@ export namespace ParamDefinition {
          */
         step?: number
     }
-    export function Numeric(defaultValue: number, range?: { min?: number, max?: number, step?: number }, info?: Info): Numeric {
-        return setInfo<Numeric>(setRange({ type: 'number', defaultValue }, range), info)
-    }
-    function setRange(p: Numeric, range?: { min?: number, max?: number, step?: number }) {
+    function setRange<T extends Numeric | Interval>(p: T, range?: { min?: number, max?: number, step?: number }) {
         if (!range) return p;
         if (typeof range.min !== 'undefined') p.min = range.min;
         if (typeof range.max !== 'undefined') p.max = range.max;
@@ -96,11 +94,18 @@ export namespace ParamDefinition {
         return p;
     }
 
-    export interface Interval extends Base<[number, number]> {
+    export interface Numeric extends Base<number>, Range {
+        type: 'number'
+    }
+    export function Numeric(defaultValue: number, range?: { min?: number, max?: number, step?: number }, info?: Info): Numeric {
+        return setInfo<Numeric>(setRange({ type: 'number', defaultValue }, range), info)
+    }
+
+    export interface Interval extends Base<[number, number]>, Range {
         type: 'interval'
     }
-    export function Interval(defaultValue: [number, number], info?: Info): Interval {
-        return setInfo<Interval>({ type: 'interval', defaultValue }, info)
+    export function Interval(defaultValue: [number, number], range?: { min?: number, max?: number, step?: number }, info?: Info): Interval {
+        return setInfo<Interval>(setRange({ type: 'interval', defaultValue }, range), info)
     }
 
     export interface LineGraph extends Base<Vec2[]> {
@@ -134,14 +139,14 @@ export namespace ParamDefinition {
 
     export interface Converted<T, C> extends Base<T> {
         type: 'converted',
-        convertedControl: Any,
-        /** converts from prop value to display value */
+        converted: Any,
+        /** converts from props value to display value */
         fromValue(v: T): C,
         /** converts from display value to prop value */
         toValue(v: C): T
     }
-    export function Converted<T, C extends Any>(defaultValue: T, convertedControl: C, fromValue: (v: T) => C, toValue: (v: C) => T, info?: Info): Converted<T, C> {
-        return setInfo<Converted<T, C>>({ type: 'converted', defaultValue, convertedControl, fromValue, toValue }, info);
+    export function Converted<T, C extends Any>(fromValue: (v: T) => C['defaultValue'], toValue: (v: C['defaultValue']) => T, converted: C): Converted<T, C['defaultValue']> {
+        return { type: 'converted', defaultValue: toValue(converted.defaultValue), converted, fromValue, toValue };
     }
 
     export type Any = Value<any> | Select<any> | MultiSelect<any> | Boolean | Text | Color | Numeric | Interval | LineGraph | Group<any> | Mapped<any> | Converted<any, any>
@@ -151,8 +156,11 @@ export namespace ParamDefinition {
 
     export function getDefaultValues<T extends Params>(params: T) {
         const d: { [k: string]: any } = {}
-        Object.keys(params).forEach(k => d[k] = params[k].defaultValue)
-        return d as Values<T>
+        for (const k of Object.keys(params)) {
+            if (params[k].isOptional) continue;
+            d[k] = params[k].defaultValue;
+        }
+        return d as Values<T>;
     }
 
     export function clone<P extends Params>(params: P): P {