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 {