Browse Source

VolumeStreaming manager & UI

David Sehnal 5 years ago
parent
commit
3bcf1cb6b5

+ 1 - 1
src/mol-plugin-state/helpers/root-structure.ts

@@ -47,7 +47,7 @@ export namespace RootStructureDefinition {
                     : PD.Text('', { label: 'Asm Id', description: 'Assembly Id (use empty for the 1st assembly)' }))
             }, { isFlat: true }),
             'symmetry-mates': PD.Group({
-                radius: PD.Numeric(5)
+                radius: PD.Numeric(5, { min: 0, max: 50, step: 1 })
             }, { isFlat: true }),
             'symmetry': PD.Group({
                 ijkMin: PD.Vec3(Vec3.create(-1, -1, -1), { step: 1 }, { label: 'Min IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } }),

+ 25 - 10
src/mol-plugin-state/manager/structure/hierarchy-state.ts

@@ -10,6 +10,8 @@ import { StructureBuilderTags } from '../../builder/structure';
 import { RepresentationProviderTags } from '../../builder/structure/provider';
 import { StructureRepresentationInteractionTags } from '../../../mol-plugin/behavior/dynamic/selection/structure-representation-interaction';
 import { StateTransforms } from '../../transforms';
+import { VolumeStreaming } from '../../../mol-plugin/behavior/dynamic/volume-streaming/behavior';
+import { CreateVolumeStreamingBehavior } from '../../../mol-plugin/behavior/dynamic/volume-streaming/transformers';
 
 export function buildStructureHierarchy(state: State, previous?: StructureHierarchy) {
     const build = BuildState(state, previous || StructureHierarchy());
@@ -39,7 +41,8 @@ interface RefBase<K extends string = string, O extends StateObject = StateObject
 export type HierarchyRef = 
     | TrajectoryRef
     | ModelRef | ModelPropertiesRef
-    | StructureRef | StructurePropertiesRef | StructureComponentRef | StructureRepresentationRef
+    | StructureRef | StructurePropertiesRef | StructureVolumeStreamingRef | StructureComponentRef | StructureRepresentationRef
+    | GenericRepresentationRef
 
 export interface TrajectoryRef extends RefBase<'trajectory', SO.Molecule.Trajectory> {
     models: ModelRef[]
@@ -77,7 +80,7 @@ export interface StructureRef extends RefBase<'structure', SO.Molecule.Structure
         surroundings?: StructureComponentRef,
     },
     genericRepresentations?: GenericRepresentationRef[],
-    // volumeStreaming?: ....
+    volumeStreaming?: StructureVolumeStreamingRef
 }
 
 function StructureRef(cell: StateObjectCell<SO.Molecule.Structure>, model?: ModelRef): StructureRef {
@@ -92,6 +95,14 @@ function StructurePropertiesRef(cell: StateObjectCell<SO.Molecule.Structure>, st
     return { kind: 'structure-properties', cell, version: cell.transform.version, structure };
 }
 
+export interface StructureVolumeStreamingRef extends RefBase<'structure-volume-streaming', VolumeStreaming, CreateVolumeStreamingBehavior> {
+    structure: StructureRef
+}
+
+function StructureVolumeStreamingRef(cell: StateObjectCell<VolumeStreaming>, structure: StructureRef): StructureVolumeStreamingRef {
+    return { kind: 'structure-volume-streaming', cell, version: cell.transform.version, structure };
+}
+
 export interface StructureComponentRef extends RefBase<'structure-component', SO.Molecule.Structure, StateTransforms['Model']['StructureComponent']> {
     structure: StructureRef,
     key?: string,
@@ -157,9 +168,10 @@ function createOrUpdateRefList<R extends HierarchyRef, C extends any[]>(state: B
     return ref;
 }
 
-function createOrUpdateRef<R extends HierarchyRef, C extends any[]>(state: BuildState, cell: StateObjectCell, old: R | undefined, ctor: (...args: C) => R, ...args: C) {
+function createOrUpdateRef<R extends HierarchyRef, C extends any[]>(state: BuildState, cell: StateObjectCell, ctor: (...args: C) => R, ...args: C) {
     const ref: R = ctor(...args);
     state.hierarchy.refs.set(cell.transform.ref, ref);
+    const old = state.oldHierarchy.refs.get(cell.transform.ref);
     if (old) {
         if (old.version !== cell.transform.version) state.updated.push(ref);
     } else {
@@ -176,25 +188,25 @@ const tagMap: [string, (state: BuildState, cell: StateObjectCell) => boolean | v
         if (state.currentTrajectory) {
             state.currentModel = createOrUpdateRefList(state, cell, state.currentTrajectory.models, ModelRef, cell, state.currentTrajectory);
         } else {
-            state.currentModel = ModelRef(cell)
+            state.currentModel = createOrUpdateRef(state, cell, ModelRef, cell);
         }
         state.hierarchy.models.push(state.currentModel);
     }, state => state.currentModel = void 0],
     [StructureBuilderTags.ModelProperties, (state, cell) => {
         if (!state.currentModel) return false;
-        state.currentModel.properties = createOrUpdateRef(state, cell, state.currentModel.properties, ModelPropertiesRef, cell, state.currentModel);
+        state.currentModel.properties = createOrUpdateRef(state, cell, ModelPropertiesRef, cell, state.currentModel);
     }, state => { }],
     [StructureBuilderTags.Structure, (state, cell) => {
         if (state.currentModel) {
             state.currentStructure = createOrUpdateRefList(state, cell, state.currentModel.structures, StructureRef, cell, state.currentModel);
         } else {
-            state.currentStructure = StructureRef(cell);
+            state.currentStructure = createOrUpdateRef(state, cell, StructureRef, cell);
         }
         state.hierarchy.structures.push(state.currentStructure);
     }, state => state.currentStructure = void 0],
     [StructureBuilderTags.StructureProperties, (state, cell) => {
         if (!state.currentStructure) return false;
-        state.currentStructure.properties = createOrUpdateRef(state, cell, state.currentStructure.properties, StructurePropertiesRef, cell, state.currentStructure);
+        state.currentStructure.properties = createOrUpdateRef(state, cell, StructurePropertiesRef, cell, state.currentStructure);
     }, state => { }],
     [StructureBuilderTags.Component, (state, cell) => {
         if (!state.currentStructure) return false;
@@ -207,13 +219,13 @@ const tagMap: [string, (state: BuildState, cell: StateObjectCell) => boolean | v
     [StructureRepresentationInteractionTags.ResidueSel, (state, cell) => {
         if (!state.currentStructure) return false;
         if (!state.currentStructure.currentFocus) state.currentStructure.currentFocus = { };
-        state.currentStructure.currentFocus.focus = StructureComponentRef(cell, state.currentStructure);
+        state.currentStructure.currentFocus.focus = createOrUpdateRef(state, cell, StructureComponentRef, cell, state.currentStructure);
         state.currentComponent = state.currentStructure.currentFocus.focus;
     }, state => state.currentComponent = void 0],
     [StructureRepresentationInteractionTags.SurrSel, (state, cell) => {
         if (!state.currentStructure) return false;
         if (!state.currentStructure.currentFocus) state.currentStructure.currentFocus = { };
-        state.currentStructure.currentFocus.surroundings = StructureComponentRef(cell, state.currentStructure);
+        state.currentStructure.currentFocus.surroundings = createOrUpdateRef(state, cell, StructureComponentRef, cell, state.currentStructure);
         state.currentComponent = state.currentStructure.currentFocus.surroundings;
     }, state => state.currentComponent = void 0]
 ]
@@ -257,8 +269,11 @@ function _doPreOrder(ctx: VisitorCtx, root: StateTransform) {
         const genericTarget = state.currentComponent || state.currentModel || state.currentStructure;
         if (genericTarget) {
             if (!genericTarget.genericRepresentations) genericTarget.genericRepresentations = [];
-            genericTarget.genericRepresentations.push(GenericRepresentationRef(cell, genericTarget));
+            genericTarget.genericRepresentations.push(createOrUpdateRef(state, cell, GenericRepresentationRef, cell, genericTarget));
         }
+    } else if (state.currentStructure && VolumeStreaming.is(cell.obj)) {
+        state.currentStructure.volumeStreaming = createOrUpdateRef(state, cell, StructureVolumeStreamingRef, cell, state.currentStructure);
+        return;
     }
 
     const children = ctx.tree.children.get(root.ref);

+ 3 - 3
src/mol-plugin-state/manager/structure/hierarchy.ts

@@ -21,7 +21,7 @@ interface StructureHierarchyManagerState {
 
 export class StructureHierarchyManager extends PluginComponent<StructureHierarchyManagerState> {
     readonly behaviors = {
-        changed: this.ev.behavior({
+        selection: this.ev.behavior({
             hierarchy: this.state.hierarchy,
             trajectories: this.state.selection.trajectories,
             models: this.state.selection.models,
@@ -101,7 +101,7 @@ export class StructureHierarchyManager extends PluginComponent<StructureHierarch
         this.nextSelection.clear();
 
         this.updateState({ hierarchy, selection: { trajectories, models, structures } });
-        this.behaviors.changed.next({ hierarchy, trajectories, models, structures });
+        this.behaviors.selection.next({ hierarchy, trajectories, models, structures });
     }
 
     updateCurrent(refs: HierarchyRef[], action: 'add' | 'remove') {
@@ -132,7 +132,7 @@ export class StructureHierarchyManager extends PluginComponent<StructureHierarch
         // if (structures.length === 0 && hierarchy.structures.length > 0) structures.push(hierarchy.structures[0]);
 
         this.updateState({ selection: { trajectories, models, structures } });
-        this.behaviors.changed.next({ hierarchy, trajectories, models, structures });
+        this.behaviors.selection.next({ hierarchy, trajectories, models, structures });
     }
 
     remove(refs: HierarchyRef[], canUndo?: boolean) {

+ 3 - 1
src/mol-plugin-ui/base.tsx

@@ -70,7 +70,7 @@ export type _State<C extends React.Component> = C extends React.Component<any, i
 //
 
 export type CollapsableProps = { initiallyCollapsed?: boolean, header?: string }
-export type CollapsableState = { isCollapsed: boolean, header: string }
+export type CollapsableState = { isCollapsed: boolean, header: string, isHidden?: boolean }
 
 export abstract class CollapsableControls<P = {}, S = {}, SS = {}> extends PluginUIComponent<P & CollapsableProps, S & CollapsableState, SS> {
     toggleCollapsed = () => {
@@ -81,6 +81,8 @@ export abstract class CollapsableControls<P = {}, S = {}, SS = {}> extends Plugi
     protected abstract renderControls(): JSX.Element | null
 
     render() {
+        if (this.state.isHidden) return null;
+
         const wrapClass = this.state.isCollapsed
             ? 'msp-transform-wrapper msp-transform-wrapper-collapsed'
             : 'msp-transform-wrapper';

+ 2 - 0
src/mol-plugin-ui/controls.tsx

@@ -21,6 +21,7 @@ import { StructureMeasurementsControls } from './structure/measurements';
 import { Icon } from './controls/icons';
 import { StructureComponentControls } from './structure/components';
 import { StructureSourceControls } from './structure/source';
+import { VolumeStreamingControls } from './structure/volume';
 
 export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: boolean, label: string }> {
     state = { show: false, label: '' }
@@ -272,6 +273,7 @@ export class DefaultStructureTools extends PluginUIComponent {
             <StructureSelectionControls />
             <StructureComponentControls />
             <StructureMeasurementsControls />
+            <VolumeStreamingControls />
         </>;
     }
 }

+ 10 - 3
src/mol-plugin-ui/controls/parameters.tsx

@@ -857,6 +857,13 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
 
     toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
 
+    areParamsEmpty(params: PD.Params) {
+        for (const k of Object.keys(params)) {
+            if (!params[k].isHidden) return false;
+        }
+        return true;
+    }
+
     render() {
         const value: PD.Mapped<any>['defaultValue'] = this.props.value;
         const param = this.props.param.map(value.name);
@@ -880,7 +887,7 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
         }
 
         if (param.type === 'group' && !param.isFlat) {
-            if (Object.keys(param.params).length > 0) {
+            if (!this.areParamsEmpty(param.params)) {
                 return <div className='msp-mapped-parameter-group'>
                     {Select}
                     <IconButton icon='dot-3' onClick={this.toggleExpanded} toggleState={this.state.isExpanded} title={`${label} Properties`} />
@@ -1036,12 +1043,12 @@ export class ObjectListControl extends React.PureComponent<ParamProps<PD.ObjectL
             <div className='msp-control-row'>
                 <span>{label}</span>
                 <div>
-                    <button onClick={this.toggleExpanded}>{value}</button>
+                    <button onClick={this.toggleExpanded} disabled={this.props.isDisabled}>{value}</button>
                 </div>
             </div>
 
             {this.state.isExpanded && <div className='msp-control-offset'>
-                {this.props.value.map((v, i) => <ObjectListItem key={i} param={this.props.param} value={v} index={i} actions={this.actions} />)}
+                {this.props.value.map((v, i) => <ObjectListItem key={i} param={this.props.param} value={v} index={i} actions={this.actions} isDisabled={this.props.isDisabled} />)}
                 <ControlGroup header='New Item'>
                     <ObjectListEditor params={this.props.param.element} apply={this.add} value={this.props.param.ctor()} isDisabled={this.props.isDisabled} />
                 </ControlGroup>

+ 16 - 7
src/mol-plugin-ui/custom/volume.tsx

@@ -38,7 +38,7 @@ function Channel(props: {
     const channel = props.channels[props.name]!;
 
     const { min, max, mean, sigma } = stats;
-    const value = channel.isoValue.kind === 'relative' ? channel.isoValue.relativeValue : channel.isoValue.absoluteValue;
+    const value =  Math.round(100 * (channel.isoValue.kind === 'relative' ? channel.isoValue.relativeValue : channel.isoValue.absoluteValue)) / 100;
     const relMin = (min - mean) / sigma;
     const relMax = (max - mean) / sigma;
     const step = toPrecision(isRelative ? Math.round(((max - min) / sigma)) / 100 : sigma / 100, 2)
@@ -180,25 +180,30 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
 
         const sampling = b.info.header.sampling[0];
 
-        // TODO: factor common things out
+        const isRelativeParam = PD.Boolean(isRelative, { description: 'Use normalized or absolute isocontour scale.', label: 'Normalized' });
+
+        const isOff = params.entry.params.view.name === 'off';
+        // TODO: factor common things out, cache
         const OptionsParams = {
-            entry: PD.Select(params.entry.name, b.data.entries.map(info => [info.dataId, info.dataId] as [string, string]), { description: 'Which entry with volume data to display.' }),
+            entry: PD.Select(params.entry.name, b.data.entries.map(info => [info.dataId, info.dataId] as [string, string]), { isHidden: isOff, description: 'Which entry with volume data to display.' }),
             view: PD.MappedStatic(params.entry.params.view.name, {
-                'off': PD.Group({}, { description: 'Display off.' }),
+                'off': PD.Group({ 
+                    isRelative: PD.Boolean(isRelative, { isHidden: true })
+                }, { description: 'Display off.' }),
                 'box': PD.Group({
                     bottomLeft: PD.Vec3(Vec3.zero()),
                     topRight: PD.Vec3(Vec3.zero()),
                     detailLevel,
-                    isRelative: PD.Boolean(isRelative, { description: 'Use normalized or absolute isocontour scale.', label: 'Normalized' })
+                    isRelative: isRelativeParam
                 }, { description: 'Static box defined by cartesian coords.' }),
                 'selection-box': PD.Group({
                     radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
                     detailLevel,
-                    isRelative: PD.Boolean(isRelative, { description: 'Use normalized or absolute isocontour scale.', label: 'Normalized' })
+                    isRelative: isRelativeParam
                 }, { description: 'Box around last-interacted element.' }),
                 'cell': PD.Group({
                     detailLevel,
-                    isRelative: PD.Boolean(isRelative, { description: 'Use normalized or absolute isocontour scale.', label: 'Normalized' })
+                    isRelative: isRelativeParam
                 }, { description: 'Box around the structure\'s bounding box.' }),
                 // 'auto': PD.Group({  }), // TODO based on camera distance/active selection/whatever, show whole structure or slice.
             }, { options: VolumeStreaming.ViewTypeOptions, description: 'Controls what of the volume is displayed. "Off" hides the volume alltogether. "Bounded box" shows the volume inside the given box. "Around Interaction" shows the volume around the element/atom last interacted with. "Whole Structure" shows the volume for the whole structure.' })
@@ -217,6 +222,10 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
             }
         };
 
+        if (isOff) {
+            return <ParameterControls onChange={this.changeOption} params={OptionsParams} values={options} onEnter={this.props.events.onEnter} />;
+        }
+
         return <>
             {!isEM && <Channel label='2Fo-Fc' name='2fo-fc' channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[0]} />}
             {!isEM && <Channel label='Fo-Fc(+ve)' name='fo-fc(+ve)' channels={params.entry.params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[1]} />}

+ 80 - 81
src/mol-plugin-ui/state/actions.tsx

@@ -5,11 +5,10 @@
  */
 
 import * as React from 'react';
+import { State } from '../../mol-state';
 import { PluginUIComponent } from '../base';
-import { ApplyActionControl } from './apply-action';
-import { State, StateAction } from '../../mol-state';
 import { Icon } from '../controls/icons';
-import { PluginContext } from '../../mol-plugin/context';
+import { ApplyActionControl } from './apply-action';
 
 export class StateObjectActions extends PluginUIComponent<{ state: State, nodeRef: string, hideHeader?: boolean, initiallyCollapsed?: boolean, alwaysExpandFirst?: boolean }> {
     get current() {
@@ -41,86 +40,86 @@ export class StateObjectActions extends PluginUIComponent<{ state: State, nodeRe
         return <div className='msp-state-actions'>
             {!this.props.hideHeader && <div className='msp-section-header'><Icon name='code' /> {`Actions (${display})`}</div> }
             {actions.map((act, i) => <ApplyActionControl
-                plugin={this.plugin} key={`${act.id}`} state={state} action={act} nodeRef={ref}
+                key={`${act.id}`} state={state} action={act} nodeRef={ref}
                 initiallyCollapsed={i === 0 ? !this.props.alwaysExpandFirst && this.props.initiallyCollapsed : this.props.initiallyCollapsed} />)}
         </div>;
     }
 }
 
-interface StateObjectActionSelectProps {
-    plugin: PluginContext,
-    state: State,
-    nodeRef: string
-}
-
-interface StateObjectActionSelectState {
-    state: State,
-    nodeRef: string,
-    version: string,
-    actions: readonly StateAction[],
-    currentActionIndex: number
-}
-
-function createStateObjectActionSelectState(props: StateObjectActionSelectProps): StateObjectActionSelectState {
-    const cell = props.state.cells.get(props.nodeRef)!;
-    const actions = [...props.state.actions.fromCell(cell, props.plugin)];
-    actions.sort((a, b) => a.definition.display.name < b.definition.display.name ? -1 : a.definition.display.name === b.definition.display.name ? 0 : 1);
-    return {
-        state: props.state,
-        nodeRef: props.nodeRef,
-        version: cell.transform.version,
-        actions,
-        currentActionIndex: -1
-    }
-}
-
-export class StateObjectActionSelect extends PluginUIComponent<StateObjectActionSelectProps, StateObjectActionSelectState> {
-    state = createStateObjectActionSelectState(this.props);
-
-    get current() {
-        return this.plugin.state.behavior.currentObject.value;
-    }
-
-    static getDerivedStateFromProps(props: StateObjectActionSelectProps, state: StateObjectActionSelectState) {
-        if (state.state !== props.state || state.nodeRef !== props.nodeRef) return createStateObjectActionSelectState(props);
-        const cell = props.state.cells.get(props.nodeRef)!;
-        if (cell.transform.version !== state.version) return createStateObjectActionSelectState(props);
-        return null;
-    }
-
-    componentDidMount() {
-        // TODO: handle tree change: some state actions might become invalid
-        // this.subscribe(this.props.state.events.changed, o => {
-        //     this.setState(createStateObjectActionSelectState(this.props));
-        // });
-
-        this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => {
-            const current = this.current;
-            if (current.ref !== ref || current.state !== state) return;
-            this.setState(createStateObjectActionSelectState(this.props));
-        });
-    }
-
-    onChange =  (e: React.ChangeEvent<HTMLSelectElement>) => {
-        this.setState({ currentActionIndex: parseInt(e.target.value, 10) });
-    }
-
-    render() {
-        const actions = this.state.actions;
-        if (actions.length === 0) return null;
-
-        const current = this.state.currentActionIndex >= 0 && actions[this.state.currentActionIndex];
-        const title = current ? current.definition.display.description : 'Select Action';
-
-        return <>
-            <div className='msp-contol-row msp-action-select'>
-                <select className='msp-form-control' title={title} value={this.state.currentActionIndex} onChange={this.onChange} style={{ fontWeight: 'bold' }}>
-                    <option key={-1} value={-1} style={{ color: '#999' }}>[ Select Action ]</option>
-                    {actions.map((a, i) => <option key={i} value={i}>{a.definition.display.name}</option>)}
-                </select>
-                <Icon name='flow-tree' />
-            </div>
-            {current && <ApplyActionControl key={current.id} plugin={this.plugin} state={this.props.state} action={current} nodeRef={this.props.nodeRef} hideHeader />}
-        </>;
-    }
-}
+// interface StateObjectActionSelectProps {
+//     plugin: PluginContext,
+//     state: State,
+//     nodeRef: string
+// }
+
+// interface StateObjectActionSelectState {
+//     state: State,
+//     nodeRef: string,
+//     version: string,
+//     actions: readonly StateAction[],
+//     currentActionIndex: number
+// }
+
+// function createStateObjectActionSelectState(props: StateObjectActionSelectProps): StateObjectActionSelectState {
+//     const cell = props.state.cells.get(props.nodeRef)!;
+//     const actions = [...props.state.actions.fromCell(cell, props.plugin)];
+//     actions.sort((a, b) => a.definition.display.name < b.definition.display.name ? -1 : a.definition.display.name === b.definition.display.name ? 0 : 1);
+//     return {
+//         state: props.state,
+//         nodeRef: props.nodeRef,
+//         version: cell.transform.version,
+//         actions,
+//         currentActionIndex: -1
+//     }
+// }
+
+// export class StateObjectActionSelect extends PluginUIComponent<StateObjectActionSelectProps, StateObjectActionSelectState> {
+//     state = createStateObjectActionSelectState(this.props);
+
+//     get current() {
+//         return this.plugin.state.behavior.currentObject.value;
+//     }
+
+//     static getDerivedStateFromProps(props: StateObjectActionSelectProps, state: StateObjectActionSelectState) {
+//         if (state.state !== props.state || state.nodeRef !== props.nodeRef) return createStateObjectActionSelectState(props);
+//         const cell = props.state.cells.get(props.nodeRef)!;
+//         if (cell.transform.version !== state.version) return createStateObjectActionSelectState(props);
+//         return null;
+//     }
+
+//     componentDidMount() {
+//         // TODO: handle tree change: some state actions might become invalid
+//         // this.subscribe(this.props.state.events.changed, o => {
+//         //     this.setState(createStateObjectActionSelectState(this.props));
+//         // });
+
+//         this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => {
+//             const current = this.current;
+//             if (current.ref !== ref || current.state !== state) return;
+//             this.setState(createStateObjectActionSelectState(this.props));
+//         });
+//     }
+
+//     onChange =  (e: React.ChangeEvent<HTMLSelectElement>) => {
+//         this.setState({ currentActionIndex: parseInt(e.target.value, 10) });
+//     }
+
+//     render() {
+//         const actions = this.state.actions;
+//         if (actions.length === 0) return null;
+
+//         const current = this.state.currentActionIndex >= 0 && actions[this.state.currentActionIndex];
+//         const title = current ? current.definition.display.description : 'Select Action';
+
+//         return <>
+//             <div className='msp-contol-row msp-action-select'>
+//                 <select className='msp-form-control' title={title} value={this.state.currentActionIndex} onChange={this.onChange} style={{ fontWeight: 'bold' }}>
+//                     <option key={-1} value={-1} style={{ color: '#999' }}>[ Select Action ]</option>
+//                     {actions.map((a, i) => <option key={i} value={i}>{a.definition.display.name}</option>)}
+//                 </select>
+//                 <Icon name='flow-tree' />
+//             </div>
+//             {current && <ApplyActionControl key={current.id} plugin={this.plugin} state={this.props.state} action={current} nodeRef={this.props.nodeRef} hideHeader />}
+//         </>;
+//     }
+// }

+ 5 - 5
src/mol-plugin-ui/state/apply-action.tsx

@@ -10,20 +10,19 @@ import { State, StateTransform, StateAction } from '../../mol-state';
 import { memoizeLatest } from '../../mol-util/memoize';
 import { StateTransformParameters, TransformControlBase } from './common';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
-
 export { ApplyActionControl };
 
 namespace ApplyActionControl {
     export interface Props {
-        plugin: PluginContext,
         nodeRef: StateTransform.Ref,
         state: State,
         action: StateAction,
         hideHeader?: boolean,
         initiallyCollapsed?: boolean
     }
-
+    
     export interface ComponentState {
+        plugin: PluginContext,
         ref: StateTransform.Ref,
         version: string,
         params: any,
@@ -52,7 +51,7 @@ class ApplyActionControl extends TransformControlBase<ApplyActionControl.Props,
 
     private _getInfo = memoizeLatest((t: StateTransform.Ref, v: string) => StateTransformParameters.infoFromAction(this.plugin, this.props.state, this.props.action, this.props.nodeRef));
 
-    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, isCollapsed: this.props.initiallyCollapsed };
+    state = { plugin: this.plugin, 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, isCollapsed: this.props.initiallyCollapsed };
 
     static getDerivedStateFromProps(props: ApplyActionControl.Props, state: ApplyActionControl.ComponentState) {
         if (props.nodeRef === state.ref) return null;
@@ -61,10 +60,11 @@ class ApplyActionControl extends TransformControlBase<ApplyActionControl.Props,
 
         const source = props.state.cells.get(props.nodeRef)!.obj!;
         const params = props.action.definition.params
-            ? PD.getDefaultValues(props.action.definition.params(source, props.plugin))
+            ? PD.getDefaultValues(props.action.definition.params(source, state.plugin))
             : { };
 
         const newState: Partial<ApplyActionControl.ComponentState> = {
+            plugin: state.plugin,
             ref: props.nodeRef,
             version,
             params,

+ 68 - 19
src/mol-plugin-ui/state/common.tsx

@@ -11,8 +11,8 @@ import { ParameterControls, ParamOnChange } from '../controls/parameters';
 import { PluginContext } from '../../mol-plugin/context';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { Subject } from 'rxjs';
-import { Icon } from '../controls/icons';
-import { ExpandGroup } from '../controls/common';
+import { Icon, IconName } from '../controls/icons';
+import { ExpandGroup, ToggleButton } from '../controls/common';
 
 export { StateTransformParameters, TransformControlBase };
 
@@ -99,9 +99,18 @@ namespace TransformControlBase {
         simpleOnly?: boolean,
         isCollapsed?: boolean
     }
+
+    export interface CommonProps {
+        simpleApply?: { header: string, icon: IconName },
+        noMargin?: boolean,
+        applyLabel?: string,
+        onApply?: () => void,
+        autoHideApply?: boolean,
+        wrapInExpander?: boolean
+    }
 }
 
-abstract class TransformControlBase<P, S extends TransformControlBase.ComponentState> extends PurePluginUIComponent<P & { noMargin?: boolean, applyLabel?: string, onApply?: () => void, wrapInExpander?: boolean }, S> {
+abstract class TransformControlBase<P, S extends TransformControlBase.ComponentState> extends PurePluginUIComponent<P & TransformControlBase.CommonProps, S> {
     abstract applyAction(): Promise<void>;
     abstract getInfo(): StateTransformParameters.Props['info'];
     abstract getHeader(): StateTransformer.Definition['display'] | 'none';
@@ -154,6 +163,10 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS
         }
     }
 
+    componentDidMount() {
+        this.subscribe(this.plugin.behaviors.state.isBusy, b => this.busy.next(b));
+    }
+
     init() {
         this.busy = new Subject();
         this.subscribe(this.busy, busy => this.setState({ busy }));
@@ -173,7 +186,27 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS
         this.setState({ isCollapsed: !this.state.isCollapsed });
     }
 
-    render() {
+    renderApply() {
+        const showBack = this.isUpdate() && !(this.state.busy || this.state.isInitial);
+        const canApply = this.canApply();
+
+        return this.props.autoHideApply && !canApply
+            ? null
+            : <div className='msp-transform-apply-wrap'>
+                <button className='msp-btn msp-btn-block msp-transform-default-params' onClick={this.setDefault} disabled={this.state.busy} title='Set default params'><Icon name='cw' /></button>
+                {showBack && <button className='msp-btn msp-btn-block msp-transform-refresh msp-form-control' title='Refresh params' onClick={this.refresh} disabled={this.state.busy || this.state.isInitial}>
+                    <Icon name='back' /> Back
+                </button>}
+                <div className={`msp-transform-apply${!showBack ? ' msp-transform-apply-wider' : ''}`}>
+                    <button className={`msp-btn msp-btn-block msp-btn-commit msp-btn-commit-${canApply ? 'on' : 'off'}`} onClick={this.apply} disabled={!canApply}>
+                        {canApply && <Icon name='ok' />}
+                        {this.props.applyLabel || this.applyText()}
+                    </button>
+                </div>
+            </div>;
+    }
+
+    renderDefault() {
         const info = this.getInfo();
         const isEmpty = info.isEmpty && this.isUpdate();
 
@@ -189,8 +222,7 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS
             : 'msp-transform-wrapper';
 
         const { a, b } = this.getSourceAndTarget();
-
-        const showBack = this.isUpdate() && !(this.state.busy || this.state.isInitial);
+        const applyControl = this.renderApply();
 
         const ctrl = <div className={wrapClass} style={{ marginBottom: this.props.noMargin ? 0 : void 0 }}>
             {display !== 'none' && !this.props.wrapInExpander && <div className='msp-transform-header'>
@@ -201,19 +233,7 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS
             </div>}
             {!isEmpty && !this.state.isCollapsed && <>
                 <ParamEditor info={info} a={a} b={b} events={this.events} params={this.state.params} isDisabled={this.state.busy} />
-
-                <div className='msp-transform-apply-wrap'>
-                    <button className='msp-btn msp-btn-block msp-transform-default-params' onClick={this.setDefault} disabled={this.state.busy} title='Set default params'><Icon name='cw' /></button>
-                    {showBack && <button className='msp-btn msp-btn-block msp-transform-refresh msp-form-control' title='Refresh params' onClick={this.refresh} disabled={this.state.busy || this.state.isInitial}>
-                        <Icon name='back' /> Back
-                    </button>}
-                    <div className={`msp-transform-apply${!showBack ? ' msp-transform-apply-wider' : ''}`}>
-                        <button className={`msp-btn msp-btn-block msp-btn-commit msp-btn-commit-${this.canApply() ? 'on' : 'off'}`} onClick={this.apply} disabled={!this.canApply()}>
-                            {this.canApply() && <Icon name='ok' />}
-                            {this.props.applyLabel || this.applyText()}
-                        </button>
-                    </div>
-                </div>
+                {applyControl}
             </>}
         </div>;
 
@@ -223,4 +243,33 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS
             {ctrl}
         </ExpandGroup>;
     }
+
+    renderSimple() {        
+        const info = this.getInfo();
+        const canApply = this.canApply();
+        const apply = <div className='msp-control-row msp-select-row'>
+            <button disabled={this.state.busy || !canApply} onClick={this.apply}>
+                <Icon name={this.props.simpleApply?.icon} />
+                {this.props.simpleApply?.header}
+            </button>
+            {!info.isEmpty && <ToggleButton icon='cog' label='' title='Options' toggle={this.toggleExpanded} isSelected={!this.state.isCollapsed} disabled={this.state.busy} style={{ flex: '0 0 40px' }} />}
+        </div>
+
+        if (this.state.isCollapsed) return apply;
+
+        const tId = this.getTransformerId();
+        const ParamEditor: StateTransformParameters.Class = this.plugin.customParamEditors.has(tId)
+            ? this.plugin.customParamEditors.get(tId)!
+            : StateTransformParameters;
+        const { a, b } = this.getSourceAndTarget();
+
+        return <>
+            {apply}
+            <ParamEditor info={info} a={a} b={b} events={this.events} params={this.state.params} isDisabled={this.state.busy} />
+        </>
+    }
+
+    render() {
+        return this.props.simpleApply ? this.renderSimple() : this.renderDefault();
+    }
 }

+ 1 - 1
src/mol-plugin-ui/state/tree.tsx

@@ -306,7 +306,7 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
             return <div style={{ marginBottom: '1px' }}>
                 {row}
                 <ControlGroup header={`Apply ${this.state.currentAction.definition.display.name}`} initialExpanded={true} hideExpander={true} hideOffset={false} onHeaderClick={this.hideAction} topRightIcon='off'>
-                    <ApplyActionControl onApply={this.hideAction} plugin={this.plugin} state={this.props.cell.parent} action={this.state.currentAction} nodeRef={this.props.cell.transform.ref} hideHeader noMargin />
+                    <ApplyActionControl onApply={this.hideAction} state={this.props.cell.parent} action={this.state.currentAction} nodeRef={this.props.cell.transform.ref} hideHeader noMargin />
                 </ControlGroup>
             </div>
         }

+ 2 - 2
src/mol-plugin-ui/structure/components.tsx

@@ -55,7 +55,7 @@ class ComponentEditorControls extends PurePluginUIComponent<{}, ComponentEditorC
     }
 
     componentDidMount() {
-        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.changed, c => this.setState({
+        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, c => this.setState({
             action: this.state.action !== 'options' || c.structures.length === 0 ? void 0 : 'options',
             isEmpty: c.structures.length === 0
         }));
@@ -184,7 +184,7 @@ class ComponentOptionsControls extends PurePluginUIComponent<{ isDisabled: boole
 
 class ComponentListControls extends PurePluginUIComponent {
     get current() {
-        return this.plugin.managers.structure.hierarchy.behaviors.changed;
+        return this.plugin.managers.structure.hierarchy.behaviors.selection;
     }
 
     componentDidMount() {

+ 1 - 1
src/mol-plugin-ui/structure/selection.tsx

@@ -54,7 +54,7 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
             this.forceUpdate()
         });
 
-        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.changed, c => {
+        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, c => {
             const isEmpty = c.structures.length === 0;
             if (this.state.isEmpty !== isEmpty) {
                 this.setState({ isEmpty });

+ 1 - 1
src/mol-plugin-ui/structure/source.tsx

@@ -30,7 +30,7 @@ export class StructureSourceControls extends CollapsableControls<{}, StructureSo
     }
 
     componentDidMount() {
-        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.changed, () => this.forceUpdate());
+        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, () => this.forceUpdate());
         this.subscribe(this.plugin.behaviors.state.isBusy, v => {
             this.setState({ isBusy: v })
         });

+ 61 - 0
src/mol-plugin-ui/structure/volume.tsx

@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as React from 'react';
+import { InitVolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/transformers';
+import { CollapsableControls, CollapsableState } from '../base';
+import { ApplyActionControl } from '../state/apply-action';
+import { UpdateTransformControl } from '../state/update-transform';
+
+interface VolumeStreamingControlState extends CollapsableState {
+    isBusy: boolean
+}
+
+export class VolumeStreamingControls extends CollapsableControls<{}, VolumeStreamingControlState> {
+    protected defaultState(): VolumeStreamingControlState {
+        return {
+            header: 'Volume Streaming',
+            isCollapsed: false,
+            isBusy: false,
+            isHidden: true
+        };
+    }
+
+    componentDidMount() {
+        // TODO: do not hide this but instead show some help text??
+        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, () => this.setState({ isHidden: !this.canEnable() }));
+        this.subscribe(this.plugin.behaviors.state.isBusy, v => this.setState({ isBusy: v }));
+    }
+
+    get pivot() {
+        return this.plugin.managers.structure.hierarchy.selection.structures[0];
+    }
+
+    canEnable() {
+        const { selection } = this.plugin.managers.structure.hierarchy;
+        if (selection.structures.length !== 1) return false;
+        const pivot = this.pivot.cell;
+        if (!pivot.obj) return false;
+        return !!InitVolumeStreaming.definition.isApplicable?.(pivot.obj, pivot.transform, this.plugin);
+    }
+
+    renderEnable() {
+        const pivot = this.pivot.cell;
+        return <ApplyActionControl state={pivot.parent} action={InitVolumeStreaming} initiallyCollapsed={true} nodeRef={pivot.transform.ref} simpleApply={{ header: 'Enable', icon: 'check' }} />;
+    }
+
+    renderParams() {
+        const pivot = this.pivot;
+        return <UpdateTransformControl state={pivot.cell.parent} transform={pivot.volumeStreaming!.cell.transform} customHeader='none' noMargin autoHideApply />;
+    }
+
+    renderControls() {
+        const pivot = this.pivot;
+        if (!pivot) return null;
+        if (!pivot.volumeStreaming) return this.renderEnable();
+        return this.renderParams();
+    }
+}

+ 8 - 3
src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts

@@ -21,6 +21,7 @@ import { VolumeRepresentationRegistry } from '../../../../mol-repr/volume/regist
 import { Theme } from '../../../../mol-theme/theme';
 import { Box3D } from '../../../../mol-math/geometry';
 import { Vec3 } from '../../../../mol-math/linear-algebra';
+import { PluginConfig } from '../../../config';
 
 function addEntry(entries: InfoEntryProps[], method: VolumeServerInfo.Kind, dataId: string, emDefaultContourLevel: number) {
     entries.push({
@@ -34,7 +35,7 @@ function addEntry(entries: InfoEntryProps[], method: VolumeServerInfo.Kind, data
 export const InitVolumeStreaming = StateAction.build({
     display: { name: 'Volume Streaming' },
     from: SO.Molecule.Structure,
-    params(a) {
+    params(a, plugin: PluginContext) {
         const method = getStreamingMethod(a && a.data);
         const ids = getIds(method, a && a.data);
         return {
@@ -42,7 +43,7 @@ export const InitVolumeStreaming = StateAction.build({
             entries: PD.ObjectList({ id: PD.Text(ids[0] || '') }, ({ id }) => id, { defaultValue: ids.map(id => ({ id })) }),
             defaultView: PD.Select<VolumeStreaming.ViewTypes>(method === 'em' ? 'cell' : 'selection-box', VolumeStreaming.ViewTypeOptions as any),
             options: PD.Group({
-                serverUrl: PD.Text('https://ds.litemol.org'),
+                serverUrl: PD.Text(plugin.config.get(PluginConfig.VolumeStreaming.DefaultServer) || 'https://ds.litemol.org'),
                 behaviorRef: PD.Text('', { isHidden: true }),
                 emContourProvider: PD.Select<'wwpdb' | 'pdbe'>('wwpdb', [['wwpdb', 'wwPDB'], ['pdbe', 'PDBe']], { isHidden: true }),
                 bindings: PD.Value(VolumeStreaming.DefaultBindings, { isHidden: true }),
@@ -50,7 +51,11 @@ export const InitVolumeStreaming = StateAction.build({
             })
         };
     },
-    isApplicable: (a) => a.data.models.length === 1
+    isApplicable: (a, _, plugin: PluginContext) => {
+        const canStreamTest = plugin.config.get(PluginConfig.VolumeStreaming.CanStream);
+        if (canStreamTest) return canStreamTest(a.data, plugin);
+        return a.data.models.length === 1;
+    }
 })(({ ref, state, params }, plugin: PluginContext) => Task.create('Volume Streaming', async taskCtx => {
     const entries: InfoEntryProps[] = []
 

+ 7 - 0
src/mol-plugin/config.ts

@@ -4,6 +4,9 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
+import { Structure } from '../mol-model/structure';
+import { PluginContext } from './context';
+
 export class PluginConfigItem<T = any> {
     toString() { return this.key; }
     valueOf() { return this.key; }
@@ -17,6 +20,10 @@ export const PluginConfig = {
     State: {
         DefaultServer: item('plugin-state.server', 'https://webchem.ncbr.muni.cz/molstar-state'),
         CurrentServer: item('plugin-state.server', 'https://webchem.ncbr.muni.cz/molstar-state')
+    },
+    VolumeStreaming: {
+        DefaultServer: item('volume-streaming.server', 'https://ds.litemol.org'),
+        CanStream: item('volume-streaming.can-stream', (s: Structure, plugin: PluginContext) => s.models.length === 1)
     }
 }