diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index b30d5a7e40733d800702971a1f88a464ec3e45cb..b112f78f83c62ee36d731b941ed80e8b6a2fccf4 100644 --- a/src/mol-plugin/context.ts +++ b/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) diff --git a/src/mol-plugin/state/actions/basic.ts b/src/mol-plugin/state/actions/basic.ts index ca75fa2cbacd8fb64be0f2ce1558ef70e0c4b020..5fdfdbc2ca4eea7ad4c06db994742426cbc10ad1 100644 --- a/src/mol-plugin/state/actions/basic.ts +++ b/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`; diff --git a/src/mol-plugin/state/transforms/data.ts b/src/mol-plugin/state/transforms/data.ts index 454529c201535a98a69c188b9bdd70a1f0dfab51..a3379657811969c19913feae848307dc058f74b5 100644 --- a/src/mol-plugin/state/transforms/data.ts +++ b/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; } }); diff --git a/src/mol-plugin/ui/action.tsx b/src/mol-plugin/ui/action.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b6f6eafed6b9293723331daa29e019cea1abd451 --- /dev/null +++ b/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> + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx index 02c439164246225127525a1e08318b32a34ef9f2..095db8a58dd69434c28041e3a0bc30db4b5772bd 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/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 { })}>>></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> - } } \ No newline at end of file diff --git a/src/mol-plugin/ui/controls/parameters.tsx b/src/mol-plugin/ui/controls/parameters.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b50caeb6e460ff002a8177415e716961e9a11e4e --- /dev/null +++ b/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>; + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index 959829a78b73272e5b4aff7e4a54e484ad47a0f6..6fe4d445b723fa0e48fd73b0f49a9dc46b540aba 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/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>; diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts index b8aa03809bfce3ae0d0dc48647ffb3577c359256..99742d0010a5db60ad20b144cb820bdaeb206ad1 100644 --- a/src/mol-state/transformer.ts +++ b/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 },