From ae484443d8298b57783d4744b6b2e03d3039d554 Mon Sep 17 00:00:00 2001 From: David Sehnal <david.sehnal@gmail.com> Date: Sun, 8 Mar 2020 14:40:23 +0100 Subject: [PATCH] decorator transforms --- src/mol-plugin-state/actions/structure.ts | 12 +++--- src/mol-plugin-state/builder/structure.ts | 48 +++++++++++++++-------- src/mol-plugin-state/transforms/model.ts | 6 +-- src/mol-plugin-ui/controls/common.tsx | 23 +++++++++++ src/mol-plugin-ui/controls/parameters.tsx | 25 +----------- src/mol-plugin-ui/plugin.tsx | 35 +++++++++++++---- src/mol-plugin-ui/state/tree.tsx | 18 ++++++--- src/mol-state/state/builder.ts | 5 +++ src/mol-state/transform.ts | 8 +++- src/mol-state/tree/spine.ts | 10 +++++ 10 files changed, 126 insertions(+), 64 deletions(-) diff --git a/src/mol-plugin-state/actions/structure.ts b/src/mol-plugin-state/actions/structure.ts index e46b27280..491bb1d84 100644 --- a/src/mol-plugin-state/actions/structure.ts +++ b/src/mol-plugin-state/actions/structure.ts @@ -234,19 +234,19 @@ const DownloadStructure = StateAction.build({ const traj = await plugin.builders.structure.parseTrajectory(data, { formats: downloadParams.map((_, i) => ({ id: '' + i, format: 'cif' as 'cif' })) }); - const model = await plugin.builders.structure.createModel(traj, void 0, supportProps); - const struct = await plugin.builders.structure.createStructure(model, src.params.structure.type); + const { model } = await plugin.builders.structure.createModel(traj, { properties: supportProps }); + const { structure } = await plugin.builders.structure.createStructure(model, { structure: src.params.structure.type, properties: supportProps }); if (createRepr) { - await plugin.builders.representation.structurePreset(struct.ref, 'auto'); + await plugin.builders.representation.structurePreset(structure, 'auto'); } } else { for (const download of downloadParams) { const data = await plugin.builders.data.download(download, { state: { isGhost: true } }); const traj = await plugin.builders.structure.parseTrajectory(data, format); - const model = await plugin.builders.structure.createModel(traj, void 0, supportProps); - const struct = await plugin.builders.structure.createStructure(model, src.params.structure.type); + const { model } = await plugin.builders.structure.createModel(traj, { properties: supportProps }); + const { structure } = await plugin.builders.structure.createStructure(model, { structure: src.params.structure.type, properties: supportProps }); if (createRepr) { - await plugin.builders.representation.structurePreset(struct.ref, 'auto'); + await plugin.builders.representation.structurePreset(structure, 'auto'); } } } diff --git a/src/mol-plugin-state/builder/structure.ts b/src/mol-plugin-state/builder/structure.ts index 729993089..9eb66bdbb 100644 --- a/src/mol-plugin-state/builder/structure.ts +++ b/src/mol-plugin-state/builder/structure.ts @@ -18,6 +18,7 @@ export enum StructureBuilderTags { Model = 'model', ModelProperties = 'model-properties', Structure = 'structure', + StructureProperties = 'structure-properties', Component = 'structure-component' } @@ -78,28 +79,43 @@ export class StructureBuilder { } } - async createModel(trajectory: StateObjectRef<SO.Molecule.Trajectory>, params?: StateTransformer.Params<StateTransforms['Model']['ModelFromTrajectory']>, supportProps?: boolean) { + async createModel(trajectory: StateObjectRef<SO.Molecule.Trajectory>, params?: { + model?: StateTransformer.Params<StateTransforms['Model']['ModelFromTrajectory']>, + properties?: boolean | StateTransformer.Params<StateTransforms['Model']['CustomModelProperties']> + }) { const state = this.dataState; - if (supportProps) { - const model = state.build().to(trajectory) - .apply(StateTransforms.Model.ModelFromTrajectory, params || { modelIndex: 0 }) - .apply(StateTransforms.Model.CustomModelProperties, void 0, { tags: [StructureBuilderTags.Model, StructureBuilderTags.ModelProperties] }); - await this.plugin.runTask(this.dataState.updateTree(model, { revertOnError: true })); - return model.selector; - } else { - const model = state.build().to(trajectory) - .apply(StateTransforms.Model.ModelFromTrajectory, params || { modelIndex: 0 }, { tags: StructureBuilderTags.Model }); - await this.plugin.runTask(this.dataState.updateTree(model, { revertOnError: true })); - return model.selector; - } + + const model = state.build().to(trajectory) + .apply(StateTransforms.Model.ModelFromTrajectory, params?.model || void 0, { tags: StructureBuilderTags.Model }); + + const props = !!params?.properties + ? model.apply(StateTransforms.Model.CustomModelProperties, typeof params?.properties !== 'boolean' ? params?.properties : void 0, { tags: StructureBuilderTags.ModelProperties, isDecorator: true }) + : void 0; + + await this.plugin.runTask(this.dataState.updateTree(model, { revertOnError: true })); + + const modelSelector = model.selector, propertiesSelector = props?.selector; + + return { model: propertiesSelector || modelSelector, index: modelSelector, properties: propertiesSelector }; } - async createStructure(model: StateObjectRef<SO.Molecule.Model>, params?: RootStructureDefinition.Params) { + async createStructure(model: StateObjectRef<SO.Molecule.Model>, params?: { + structure?: RootStructureDefinition.Params, + properties?: boolean | StateTransformer.Params<StateTransforms['Model']['CustomStructureProperties']> + }) { const state = this.dataState; const structure = state.build().to(model) - .apply(StateTransforms.Model.StructureFromModel, { type: params || { name: 'assembly', params: { } } }, { tags: StructureBuilderTags.Structure }); + .apply(StateTransforms.Model.StructureFromModel, { type: params?.structure || { name: 'assembly', params: { } } }, { tags: StructureBuilderTags.Structure }); + + const props = !!params?.properties + ? structure.apply(StateTransforms.Model.CustomStructureProperties, typeof params?.properties !== 'boolean' ? params?.properties : void 0, { tags: StructureBuilderTags.StructureProperties, isDecorator: true }) + : void 0; + await this.plugin.runTask(this.dataState.updateTree(structure, { revertOnError: true })); - return structure.selector; + + const structureSelector = structure.selector, propertiesSelector = props?.selector; + + return { structure: propertiesSelector || structureSelector, definition: structureSelector, properties: propertiesSelector }; } /** returns undefined if the component is empty/null */ diff --git a/src/mol-plugin-state/transforms/model.ts b/src/mol-plugin-state/transforms/model.ts index 2b2158fec..5d26e1997 100644 --- a/src/mol-plugin-state/transforms/model.ts +++ b/src/mol-plugin-state/transforms/model.ts @@ -689,7 +689,7 @@ const StructureComponent = PluginStateTransform.BuiltIn({ type CustomModelProperties = typeof CustomModelProperties const CustomModelProperties = PluginStateTransform.BuiltIn({ name: 'custom-model-properties', - display: { name: 'Custom Properties' }, + display: { name: 'Custom Model Properties' }, from: SO.Molecule.Model, to: SO.Molecule.Model, params: (a, ctx: PluginContext) => { @@ -699,7 +699,7 @@ const CustomModelProperties = PluginStateTransform.BuiltIn({ apply({ a, params }, ctx: PluginContext) { return Task.create('Custom Props', async taskCtx => { await attachModelProps(a.data, ctx, taskCtx, params); - return new SO.Molecule.Model(a.data, { label: 'Model Props' }); + return a; }); }, update({ a, oldParams, newParams }, ctx: PluginContext) { @@ -745,7 +745,7 @@ const CustomStructureProperties = PluginStateTransform.BuiltIn({ apply({ a, params }, ctx: PluginContext) { return Task.create('Custom Props', async taskCtx => { await attachStructureProps(a.data, ctx, taskCtx, params); - return new SO.Molecule.Structure(a.data, { label: 'Structure Props' }); + return a; }); }, update({ a, oldParams, newParams }, ctx: PluginContext) { diff --git a/src/mol-plugin-ui/controls/common.tsx b/src/mol-plugin-ui/controls/common.tsx index 575c3e98d..96960b539 100644 --- a/src/mol-plugin-ui/controls/common.tsx +++ b/src/mol-plugin-ui/controls/common.tsx @@ -328,4 +328,27 @@ export class ToggleButton extends React.PureComponent<ToggleButtonProps> { {this.props.isSelected ? <b>{label}</b> : label} </button>; } +} + +export class ExpandGroup extends React.PureComponent<{ header: string, initiallyExpanded?: boolean, noOffset?: boolean }, { isExpanded: boolean }> { + state = { isExpanded: !!this.props.initiallyExpanded }; + + toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded }); + + render() { + return <> + <div className='msp-control-group-header' style={{ marginTop: '1px' }}> + <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}> + <Icon name={this.state.isExpanded ? 'collapse' : 'expand'} /> + {this.props.header} + </button> + </div> + {this.state.isExpanded && + (this.props.noOffset + ? this.props.children + : <div className='msp-control-offset'> + {this.props.children} + </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 index 66fecd1f3..666838216 100644 --- a/src/mol-plugin-ui/controls/parameters.tsx +++ b/src/mol-plugin-ui/controls/parameters.tsx @@ -14,7 +14,7 @@ import { camelCaseToWords } from '../../mol-util/string'; import * as React from 'react'; import LineGraphComponent from './line-graph/line-graph-component'; import { Slider, Slider2 } from './slider'; -import { NumericInput, IconButton, ControlGroup, ToggleButton } from './common'; +import { NumericInput, IconButton, ControlGroup, ToggleButton, ExpandGroup } from './common'; import { _Props, _State, PluginUIComponent } from '../base'; import { legendFor } from './legend'; import { Legend as LegendData } from '../../mol-util/legend'; @@ -114,29 +114,6 @@ export class ParameterMappingControl<S, T> extends PluginUIComponent<{ mapping: } } -class ExpandGroup extends React.PureComponent<{ header: string, initiallyExpanded?: boolean, noOffset?: boolean }, { isExpanded: boolean }> { - state = { isExpanded: !!this.props.initiallyExpanded }; - - toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded }); - - render() { - return <> - <div className='msp-control-group-header' style={{ marginTop: '1px' }}> - <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}> - <Icon name={this.state.isExpanded ? 'collapse' : 'expand'} /> - {this.props.header} - </button> - </div> - {this.state.isExpanded && - (this.props.noOffset - ? this.props.children - : <div className='msp-control-offset'> - {this.props.children} - </div>)} - </>; - } -} - type ParamInfo = [string, PD.Any, ParamControl]; function classifyParams(params: PD.Params) { function addParam(k: string, p: PD.Any, group: typeof essentials) { diff --git a/src/mol-plugin-ui/plugin.tsx b/src/mol-plugin-ui/plugin.tsx index 6458d1cf7..9ce69a933 100644 --- a/src/mol-plugin-ui/plugin.tsx +++ b/src/mol-plugin-ui/plugin.tsx @@ -19,8 +19,9 @@ import { StateTransform } from '../mol-state'; import { UpdateTransformControl } from './state/update-transform'; import { SequenceView } from './sequence'; import { Toasts } from './toast'; -import { SectionHeader } from './controls/common'; +import { SectionHeader, ExpandGroup } from './controls/common'; import { LeftPanelControls } from './left-panel'; +import { StateTreeSpine } from '../mol-state/tree/spine'; export class Plugin extends React.Component<{ plugin: PluginContext }, {}> { region(kind: 'left' | 'right' | 'bottom' | 'main', element: JSX.Element) { @@ -228,16 +229,34 @@ export class CurrentObject extends PluginUIComponent { if (!showActions) return null; - return <> - {(cell.status === 'ok' || cell.status === 'error') && <> + const actions = cell.status === 'ok' && <StateObjectActionSelect state={current.state} nodeRef={ref} plugin={this.plugin} /> + + if (cell.status === 'error') { + return <> <SectionHeader icon='flow-cascade' title={`${cell.obj?.label || transform.transformer.definition.display.name}`} desc={transform.transformer.definition.display.name} /> <UpdateTransformControl state={current.state} transform={transform} customHeader='none' /> - </> } - {cell.status === 'ok' && - <StateObjectActionSelect state={current.state} nodeRef={ref} plugin={this.plugin} /> - } + {actions} + </>; + } + + if (cell.status !== 'ok') return null; + + const decoratorChain = StateTreeSpine.getDecoratorChain(this.current.state, this.current.ref); + const parent = decoratorChain[decoratorChain.length - 1]; + + let decorators: JSX.Element[] | undefined = decoratorChain.length > 1 ? [] : void 0; + for (let i = decoratorChain.length - 2; i >= 0; i--) { + const d = decoratorChain[i]; + decorators!.push(<ExpandGroup header={d.transform.transformer.definition.display.name}> + <UpdateTransformControl state={current.state} transform={d.transform} customHeader='none' /> + </ExpandGroup>); + } - {/* <StateObjectActions state={current.state} nodeRef={ref} initiallyCollapsed />} */} + return <> + <SectionHeader icon='flow-cascade' title={`${parent.obj?.label || parent.transform.transformer.definition.display.name}`} desc={parent.transform.transformer.definition.display.name} /> + <UpdateTransformControl state={current.state} transform={parent.transform} customHeader='none' /> + {decorators && <div className='msp-controls-section'>{decorators}</div>} + {actions} </>; } } \ No newline at end of file diff --git a/src/mol-plugin-ui/state/tree.tsx b/src/mol-plugin-ui/state/tree.tsx index 9c7a88d25..9ab15d24e 100644 --- a/src/mol-plugin-ui/state/tree.tsx +++ b/src/mol-plugin-ui/state/tree.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { PluginStateObject } from '../../mol-plugin-state/objects'; -import { State, StateObject, StateTransform, StateObjectCell } from '../../mol-state' +import { State, StateTree as _StateTree, StateObject, StateTransform, StateObjectCell } from '../../mol-state' import { PluginCommands } from '../../mol-plugin/commands'; import { PluginUIComponent, _Props, _State } from '../base'; import { Icon } from '../controls/icons'; @@ -85,6 +85,12 @@ class StateTreeNode extends PluginUIComponent<{ cell: StateObjectCell, depth: nu return { isCollapsed: !!props.cell.state.isCollapsed }; } + hasDecorator(children: _StateTree.ChildSet) { + if (children.size !== 1) return false; + const ref = children.values().next().value; + return !!this.props.cell.parent.tree.transforms.get(ref).isDecorator; + } + render() { const cell = this.props.cell; if (!cell || cell.obj === StateObject.Null || !cell.parent.tree.transforms.has(cell.transform.ref)) { @@ -92,17 +98,17 @@ class StateTreeNode extends PluginUIComponent<{ cell: StateObjectCell, depth: nu } const cellState = cell.state; - const showLabel = (cell.transform.ref !== StateTransform.RootRef) && (cell.status !== 'ok' || !cell.state.isGhost); const children = cell.parent.tree.children.get(this.ref); - const newDepth = showLabel ? this.props.depth + 1 : this.props.depth; - + const showLabel = (cell.transform.ref !== StateTransform.RootRef) && (cell.status !== 'ok' || (!cell.state.isGhost && !this.hasDecorator(children))); + if (!showLabel) { if (children.size === 0) return null; return <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}> - {children.map(c => <StateTreeNode cell={cell.parent.cells.get(c!)!} key={c} depth={newDepth} />)} + {children.map(c => <StateTreeNode cell={cell.parent.cells.get(c!)!} key={c} depth={this.props.depth} />)} </div>; } - + + const newDepth = this.props.depth + 1; return <> <StateTreeNodeLabel cell={cell} depth={this.props.depth} /> {children.size === 0 diff --git a/src/mol-state/state/builder.ts b/src/mol-state/state/builder.ts index b35f02d89..d00baa243 100644 --- a/src/mol-state/state/builder.ts +++ b/src/mol-state/state/builder.ts @@ -115,6 +115,11 @@ namespace StateBuilder { * If no params are specified (params <- undefined), default params are lazily resolved. */ apply<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>, T> { + if (options?.isDecorator) { + const children = this.state.tree.children.get(this.ref); + if (children.size > 0) throw new Error('Decorators can only be applied to childless nodes.'); + } + const t = tr.apply(this.ref, params, options); this.state.tree.add(t); this.editInfo.count++; diff --git a/src/mol-state/transform.ts b/src/mol-state/transform.ts index 864147950..bb51b0ed5 100644 --- a/src/mol-state/transform.ts +++ b/src/mol-state/transform.ts @@ -14,6 +14,7 @@ interface Transform<T extends StateTransformer = StateTransformer> { readonly transformer: T, readonly state: Transform.State, readonly tags?: string[], + readonly isDecorator?: boolean, readonly ref: Transform.Ref, /** * Sibling-like dependency @@ -90,6 +91,7 @@ namespace Transform { export interface Options { ref?: string, tags?: string | string[], + isDecorator?: boolean, state?: State, dependsOn?: Ref[] } @@ -105,8 +107,9 @@ namespace Transform { return { parent, transformer, - state: (options && options.state) || { }, + state: options?.state || { }, tags, + isDecorator: options?.isDecorator, ref, dependsOn: options && options.dependsOn, params, @@ -161,6 +164,7 @@ namespace Transform { params: any, state?: State, tags?: string[], + isDecorator?: boolean, ref: string, dependsOn?: string[] version: string @@ -184,6 +188,7 @@ namespace Transform { params: t.params ? pToJson(t.params) : void 0, state, tags: t.tags, + isDecorator: t.isDecorator || void 0, ref: t.ref, dependsOn: t.dependsOn, version: t.version @@ -201,6 +206,7 @@ namespace Transform { params: t.params ? pFromJson(t.params) : void 0, state: t.state || { }, tags: t.tags, + isDecorator: t.isDecorator, ref: t.ref as Ref, dependsOn: t.dependsOn, version: t.version diff --git a/src/mol-state/tree/spine.ts b/src/mol-state/tree/spine.ts index 3faa3f7b3..c450cba00 100644 --- a/src/mol-state/tree/spine.ts +++ b/src/mol-state/tree/spine.ts @@ -49,8 +49,18 @@ namespace StateTreeSpine { } constructor(private cells: State.Cells) { + } + } + export function getDecoratorChain(state: State, currentRef: StateTransform.Ref): StateObjectCell[] { + const cells = state.cells; + let current = cells.get(currentRef)!; + const ret: StateObjectCell[] = [current]; + while (current?.transform.isDecorator) { + current = cells.get(current.transform.parent)!; + ret.push(current); } + return ret; } export function getRootOfType<T extends StateObject.Ctor>(state: State, t: T, ref: string): StateObject.From<T> | undefined { -- GitLab