diff --git a/src/mol-plugin-state/actions/structure.ts b/src/mol-plugin-state/actions/structure.ts index e46b2728095d9a645e4d0f1b8dd8eec63d3ad984..491bb1d843ee12590bda83f8e6911bedfc83c571 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 7299930892096cbf62c77b1dd85037a003472bd8..9eb66bdbb4a47a34369b05443203c74708d23879 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 2b2158feca07b8b87011578616a54dd3a93446e6..5d26e1997372d35ec3fffea73ed4acd8096f2a3e 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 575c3e98d4ceb8850d01839b067634a0dbccd08d..96960b539f71a3b1a663638fbe4e386ae2f7a434 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 66fecd1f3dcac4f6e0c1a135aae353d14a000946..666838216e25196d323ba8efcbe2b0f42e14076c 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 6458d1cf78dc17ee6a16c32a5012ccf8f9584aae..9ce69a933ef1b4111ed76bbde2acf3ed35472809 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 9c7a88d2539bf93e085dcf11c68184a5c0f5fe18..9ab15d24ebfc0fd02b55abe53738bafcbba71b41 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 b35f02d89e0a4075520e1dd12b6f9a5652fb541d..d00baa243cb32810407ca77e6c1c390b242a7ae1 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 864147950dcf2751e84d78d39ea8b2932ea77181..bb51b0ed548b4a89d4b941d14bb556d5dac66ece 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 3faa3f7b3c66a88116c1152a9566a95dd97bde8f..c450cba0098f9a751613bfa4c52b5d25caba3c6c 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 {