diff --git a/src/mol-plugin/behavior/behavior.ts b/src/mol-plugin/behavior/behavior.ts index f15d4fcfd5b719c0f9a1aa307e3aff1910105107..f97fedad1137c055836ffd61ad48c26a4ef760ad 100644 --- a/src/mol-plugin/behavior/behavior.ts +++ b/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>) { diff --git a/src/mol-plugin/behavior/static/representation.ts b/src/mol-plugin/behavior/static/representation.ts index 0bd4ec276ddbf7f2be67231bdcfc86e6c4f51ac7..90b7e473802ea99cc02e3c9f76c5a23033008790 100644 --- a/src/mol-plugin/behavior/static/representation.ts +++ b/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); } \ No newline at end of file diff --git a/src/mol-plugin/behavior/static/state.ts b/src/mol-plugin/behavior/static/state.ts index 4c4124f913246b77ee1a7e0356baa2585dfe07ca..f83a7ca8de9f4e087862ed2a02f3e52a4c24b073 100644 --- a/src/mol-plugin/behavior/static/state.ts +++ b/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 }) { diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 8eda5baa8fa611bb50dde28f9cec1a9d333b97f4..19dfe141da26b466c0619f7ab7d75b6a9c0fa382 100644 --- a/src/mol-plugin/context.ts +++ b/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 = ; } \ No newline at end of file diff --git a/src/mol-plugin/spec.ts b/src/mol-plugin/spec.ts index 2d297190919b18a67f4e76b2fb16880b7f0feefb..40413fd87827a946700446b29e671924b0c3d7a0 100644 --- a/src/mol-plugin/spec.ts +++ b/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 } diff --git a/src/mol-plugin/state/actions/basic.ts b/src/mol-plugin/state/actions/basic.ts index 7ccbf3c40d79cf96835e6fb746f4cc42f0491e5a..65914ce4c61fe9b3ce28b580288e686e4ae45dc4 100644 --- a/src/mol-plugin/state/actions/basic.ts +++ b/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)); diff --git a/src/mol-plugin/state/transforms/data.ts b/src/mol-plugin/state/transforms/data.ts index 78499dcd95365026dddd0b3bc40fd649eebb799a..d2f76a919cdf03315877531c7bc7ed44df5f9204 100644 --- a/src/mol-plugin/state/transforms/data.ts +++ b/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 diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts index 220220e34ba9e50d403f0d30616d2ad07953182e..3a4a7963993809f53642b98cfab6f88a1de84fb8 100644 --- a/src/mol-plugin/state/transforms/model.ts +++ b/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); diff --git a/src/mol-plugin/state/transforms/visuals.ts b/src/mol-plugin/state/transforms/visuals.ts index 7c42b0490bc422b9152a42dd74bb17fdf4cc65eb..49d63cf041ec2cdcbc9061801c53c34e30244929 100644 --- a/src/mol-plugin/state/transforms/visuals.ts +++ b/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) diff --git a/src/mol-plugin/ui/controls/parameters.tsx b/src/mol-plugin/ui/controls/parameters.tsx index e118025d9d09b3216294fc83f0fb2a484709bec2..9a65e23bca41031435050954fc14585a12332c31 100644 --- a/src/mol-plugin/ui/controls/parameters.tsx +++ b/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> } -} \ No newline at end of file +} + +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} /> + } +} diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index bd0435deb4ac2c9adee7715d999c607500e79dc3..bb3ff2eeba2fe479de73488b56a8cebd9a1dfae2 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/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) diff --git a/src/mol-plugin/ui/state-tree.tsx b/src/mol-plugin/ui/state-tree.tsx index 621024d58918565b20a3c12e9080b0032661b0de..9634c8dcf48433cf512bde18b2efb03cf213a4f6 100644 --- a/src/mol-plugin/ui/state-tree.tsx +++ b/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 <> diff --git a/src/mol-plugin/ui/state/apply-action.tsx b/src/mol-plugin/ui/state/apply-action.tsx index 4e34aced24449a36c41371f2cd34f17ce6c761ba..0c6b7a8db8715f4b644325bafeb18a7b3bdf3d52 100644 --- a/src/mol-plugin/ui/state/apply-action.tsx +++ b/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> - } } \ No newline at end of file diff --git a/src/mol-plugin/ui/state/common.tsx b/src/mol-plugin/ui/state/common.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7ccc7bc3103555648c34537d823acc5726419c4c --- /dev/null +++ b/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> + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/state/parameters.tsx b/src/mol-plugin/ui/state/parameters.tsx deleted file mode 100644 index 86d6727bdb10fee6e0eef30e4dba2bccd6dbfa92..0000000000000000000000000000000000000000 --- a/src/mol-plugin/ui/state/parameters.tsx +++ /dev/null @@ -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 - } - } -} \ No newline at end of file diff --git a/src/mol-plugin/ui/state/update-transform.tsx b/src/mol-plugin/ui/state/update-transform.tsx index 591e1f7556c60d52f93f81e3b7a5c06c67f2a9ab..1452b9dfe16e7911b9cf385149db8a31d9ea92c7 100644 --- a/src/mol-plugin/ui/state/update-transform.tsx +++ b/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> - } } \ No newline at end of file diff --git a/src/mol-state/action.ts b/src/mol-state/action.ts index 024fe3e0d3c14db89d0c145be1c67787ad200b74..64826d3b64472b546082a6c8ea9e12fdff842d0a 100644 --- a/src/mol-state/action.ts +++ b/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); diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index 5c2f27e15f4465f27b1814fe5de794571ec08fed..8385b27f947ed19f1a8004c4bb5e1d0816357c1a 100644 --- a/src/mol-state/state.ts +++ b/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(); } diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts index 673e2786960828599411424d1099503e9fd2052b..950e1b3764613e5ab795bd5ccea307352945b2aa 100644 --- a/src/mol-state/transformer.ts +++ b/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, diff --git a/src/mol-state/tree/transient.ts b/src/mol-state/tree/transient.ts index 279ed6fcf771db3ec9a201718a82863a3712b5a3..9646dab8f430b2322fc5e2c83138c976019b835c 100644 --- a/src/mol-state/tree/transient.ts +++ b/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) { diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts index fb96eee073b6de2fdac2b40dfd7c6a5844f2fa39..2c1922a953cad32e68a35fea67d6719389d5826b 100644 --- a/src/mol-util/param-definition.ts +++ b/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 {