diff --git a/src/mol-plugin-state/helpers/root-structure.ts b/src/mol-plugin-state/helpers/root-structure.ts index 73e8573f6c4dfe21481556c68244dc6158d59d40..a95104bce9bc8a3e651e5c6a89b3aa0f2676f0b3 100644 --- a/src/mol-plugin-state/helpers/root-structure.ts +++ b/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' } }), diff --git a/src/mol-plugin-state/manager/structure/hierarchy-state.ts b/src/mol-plugin-state/manager/structure/hierarchy-state.ts index b543b03718631788e13f0de80bee70b0f5fb4626..3ab715cbfa1e29c29a7fe62ba06d01ebb75a7dc0 100644 --- a/src/mol-plugin-state/manager/structure/hierarchy-state.ts +++ b/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); diff --git a/src/mol-plugin-state/manager/structure/hierarchy.ts b/src/mol-plugin-state/manager/structure/hierarchy.ts index 424dcaf04c65f89ab547ddd14db7fc4e1ccc0979..1eaf661d7af3af7e8ef3506e7e15bed8a462e024 100644 --- a/src/mol-plugin-state/manager/structure/hierarchy.ts +++ b/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) { diff --git a/src/mol-plugin-ui/base.tsx b/src/mol-plugin-ui/base.tsx index 8fc03d1ea70047b0b1237792f1dbdd423922e7b4..200231b0f6ecc4b2f495cee9a6fb5893fa6375ec 100644 --- a/src/mol-plugin-ui/base.tsx +++ b/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'; diff --git a/src/mol-plugin-ui/controls.tsx b/src/mol-plugin-ui/controls.tsx index 9c1f88fe40d9090f8591d8a37f12ac87a16deff6..71fb91e96116eaa70bc8e3bf0162ead105f023a0 100644 --- a/src/mol-plugin-ui/controls.tsx +++ b/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 /> </>; } } diff --git a/src/mol-plugin-ui/controls/parameters.tsx b/src/mol-plugin-ui/controls/parameters.tsx index dad863d979764c35da0302461e5416ace10cdb15..7b8d954b7a90968be73a3d09bb6dd3587a9b1a6c 100644 --- a/src/mol-plugin-ui/controls/parameters.tsx +++ b/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> diff --git a/src/mol-plugin-ui/custom/volume.tsx b/src/mol-plugin-ui/custom/volume.tsx index 95fd9cec46b34556ea605d1d545a68686c8e8324..939fb0fd7cce89c6fdaf2f7f2878aee8c55d87b1 100644 --- a/src/mol-plugin-ui/custom/volume.tsx +++ b/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]} />} diff --git a/src/mol-plugin-ui/state/actions.tsx b/src/mol-plugin-ui/state/actions.tsx index b0d554766bd931172a4e91100dfc57f260ebd852..246c0d7fbd79664b3744759a6e94455c01ac0944 100644 --- a/src/mol-plugin-ui/state/actions.tsx +++ b/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 />} - </>; - } -} \ No newline at end of file +// 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 />} +// </>; +// } +// } \ No newline at end of file diff --git a/src/mol-plugin-ui/state/apply-action.tsx b/src/mol-plugin-ui/state/apply-action.tsx index f1149642587ed58e346e133197f1edab3c0a2eae..a1140e10872f1f91bb295bb0fb9b4d16362f8567 100644 --- a/src/mol-plugin-ui/state/apply-action.tsx +++ b/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, diff --git a/src/mol-plugin-ui/state/common.tsx b/src/mol-plugin-ui/state/common.tsx index b799ce30ff0c145ed05ca98ac1407584d827ce4c..b97943573203cf26fe717ce180028eb11abafc6f 100644 --- a/src/mol-plugin-ui/state/common.tsx +++ b/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(); + } } \ 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 b80f33007735e114627fef83688019a8897ec5b9..e4dda875a6fabd5802057e50a76ccee1a263b9b9 100644 --- a/src/mol-plugin-ui/state/tree.tsx +++ b/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> } diff --git a/src/mol-plugin-ui/structure/components.tsx b/src/mol-plugin-ui/structure/components.tsx index 1cb52fec1d7815e34207e5276c18e7979c137099..ff6ccc6a76180c180451bec6d26ea364051234ef 100644 --- a/src/mol-plugin-ui/structure/components.tsx +++ b/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() { diff --git a/src/mol-plugin-ui/structure/selection.tsx b/src/mol-plugin-ui/structure/selection.tsx index e8e6edd7b71be9db982ec5db741c7633375dbd8a..c08494fcd1e5412858bd6103be040b28d71889b7 100644 --- a/src/mol-plugin-ui/structure/selection.tsx +++ b/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 }); diff --git a/src/mol-plugin-ui/structure/source.tsx b/src/mol-plugin-ui/structure/source.tsx index c07b439b615d422cb9050f2f561ab2cdac47c544..c36bcb64ea69dc478e46e3318a9bba52f4973ba4 100644 --- a/src/mol-plugin-ui/structure/source.tsx +++ b/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 }) }); diff --git a/src/mol-plugin-ui/structure/volume.tsx b/src/mol-plugin-ui/structure/volume.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a46d6243efd7fa67ee7c1dfd345981f154d0c8d2 --- /dev/null +++ b/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(); + } +} \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts index 051df9fc70f974317dbbc9a49df43453a8a65cdd..a981754dc8cc7320e361d04aae9c0dd661dd4c5f 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts +++ b/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[] = [] diff --git a/src/mol-plugin/config.ts b/src/mol-plugin/config.ts index 7f386b79b502ea0221e93d76a5e1a5c160e1e245..68c4cd82f9f45f3d39f121bd5f08c5fe82a92fd6 100644 --- a/src/mol-plugin/config.ts +++ b/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) } }