diff --git a/src/mol-plugin/skin/base/components/temp.scss b/src/mol-plugin/skin/base/components/temp.scss index 8c6521668790495cbad78c722baf0f41e1c96ae4..c476c298a21c6929206bc016509068266f8b8554 100644 --- a/src/mol-plugin/skin/base/components/temp.scss +++ b/src/mol-plugin/skin/base/components/temp.scss @@ -149,4 +149,18 @@ margin-bottom: 1px; padding: 3px 6px; } +} + +.msp-traj-controls { + position: absolute; + left: $control-spacing; + top: $control-spacing; + line-height: $row-height; + + > span { + color: $font-color; + padding-top: 1px; + font-size: 85%; + display: inline-block; + } } \ No newline at end of file diff --git a/src/mol-plugin/skin/base/components/viewport.scss b/src/mol-plugin/skin/base/components/viewport.scss index c8123e2f1870928fa462806de1a4d7f2bcdcd25b..f87599bbb3ef8ff14bb0fd5251a30b010616d356 100644 --- a/src/mol-plugin/skin/base/components/viewport.scss +++ b/src/mol-plugin/skin/base/components/viewport.scss @@ -75,9 +75,9 @@ background: $default-background; //$highlight-info-background; position: absolute; - top: $control-spacing; - left: $control-spacing; - text-align: left; + right: $control-spacing; + bottom: $control-spacing; + text-align: right; min-height: $row-height; max-width: 95%; diff --git a/src/mol-plugin/skin/base/icons.scss b/src/mol-plugin/skin/base/icons.scss index d79242cfc706b4831107786af0152cb6c16107cf..64502c7276d2d22746ea32c5390f6e880b15eeb8 100644 --- a/src/mol-plugin/skin/base/icons.scss +++ b/src/mol-plugin/skin/base/icons.scss @@ -132,4 +132,16 @@ .msp-icon-help-circle:before { content: "\e81d"; +} + +.msp-icon-model-prev:before { + content: "\e884"; +} + +.msp-icon-model-next:before { + content: "\e885"; +} + +.msp-icon-model-first:before { + content: "\e89c"; } \ No newline at end of file diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx index c45e9fa015170f3646ea1590ea11db59976df29f..813fa0b5829462eb778563f5d63d1f0c9ef84c64 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/src/mol-plugin/ui/controls.tsx @@ -9,31 +9,72 @@ import { PluginCommands } from 'mol-plugin/command'; import { UpdateTrajectory } from 'mol-plugin/state/actions/structure'; import { PluginUIComponent } from './base'; import { LociLabelEntry } from 'mol-plugin/util/loci-label-manager'; +import { IconButton } from './controls/common'; +import { PluginStateObject } from 'mol-plugin/state/objects'; +import { StateTransforms } from 'mol-plugin/state/transforms'; +import { StateTransformer } from 'mol-state'; +import { ModelFromTrajectory } from 'mol-plugin/state/transforms/model'; -export class Controls extends PluginUIComponent<{ }, { }> { - render() { - return <> +export class TrajectoryControls extends PluginUIComponent<{}, { show: boolean, label: string }> { + state = { show: false, label: '' } + + private update = () => { + const state = this.plugin.state.dataState; + + const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model) + .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory)); + + if (models.length === 0) { + this.setState({ show: false }) + } + + let label = '', count = 0, parents = new Set<string>(); + for (const m of models) { + if (!m.sourceRef) continue; + const parent = state.cells.get(m.sourceRef)!.obj as PluginStateObject.Molecule.Trajectory; - </>; + if (!parent) continue; + if (parent.data.length > 1) { + if (parents.has(m.sourceRef)) { + // do not show the controls if there are 2 models of the same trajectory present + this.setState({ show: false }); + } + + parents.add(m.sourceRef); + count++; + if (!label) { + const idx = (m.transform.params! as StateTransformer.Params<ModelFromTrajectory>).modelIndex; + label = `Model ${idx + 1} / ${parent.data.length}`; + } + } + } + + if (count > 1) label = ''; + this.setState({ show: count > 0, label }); + } + + componentDidMount() { + this.subscribe(this.plugin.state.dataState.events.changed, this.update); } -} -export class TrajectoryControls extends PluginUIComponent { render() { - return <div> - <button className='msp-btn msp-btn-link' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, { - state: this.plugin.state.dataState, - action: UpdateTrajectory.create({ action: 'advance', by: -1 }) - })} title='Previou Model'>◀</button> - <button className='msp-btn msp-btn-link' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, { + if (!this.state.show) return null; + + return <div className='msp-traj-controls'> + <IconButton icon='model-first' title='First Model' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, { state: this.plugin.state.dataState, action: UpdateTrajectory.create({ action: 'reset' }) - })} title='First Model'>↻</button> - <button className='msp-btn msp-btn-link' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, { + })} /> + <IconButton icon='model-prev' title='Previous Model' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, { + state: this.plugin.state.dataState, + action: UpdateTrajectory.create({ action: 'advance', by: -1 }) + })} /> + <IconButton icon='model-next' title='Next Model' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, { state: this.plugin.state.dataState, - action: UpdateTrajectory.create({ action: 'advance', by: +1 }) - })} title='Next Model'>►</button><br /> - </div> + action: UpdateTrajectory.create({ action: 'advance', by: 1 }) + })} /> + { !!this.state.label && <span>{this.state.label}</span> } + </div>; } } @@ -45,8 +86,10 @@ export class LociLabelControl extends PluginUIComponent<{}, { entries: ReadonlyA } render() { - return <div style={{ textAlign: 'right' }}> + if (this.state.entries.length === 0) return null; + + return <div className='msp-highlight-info'> {this.state.entries.map((e, i) => <div key={'' + i}>{e}</div>)} - </div> + </div>; } } \ No newline at end of file diff --git a/src/mol-plugin/ui/controls/common.tsx b/src/mol-plugin/ui/controls/common.tsx index 3412a9df0350e98e5355314b8b3aac8a66c25171..83fad49cb09fa2e75336d2f7decee862cd211bbd 100644 --- a/src/mol-plugin/ui/controls/common.tsx +++ b/src/mol-plugin/ui/controls/common.tsx @@ -81,6 +81,14 @@ export class NumericInput extends React.PureComponent<{ } } +export function IconButton(props: { icon: string, onClick: (e: React.MouseEvent<HTMLButtonElement>) => void, title?: string, toggleState?: boolean }) { + let className = `msp-btn msp-btn-link msp-btn-icon`; + if (typeof props.toggleState !== 'undefined') className += ` msp-btn-link-toggle-${props.toggleState ? 'on' : 'off'}` + return <button className={className} onClick={props.onClick} title={props.title}> + <span className={`msp-icon msp-icon-${props.icon}`}/> + </button>; +} + // export const ToggleButton = (props: { // onChange: (v: boolean) => void, diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index 12b4855985287c87f1a55501fe9c15370234b296..27c4304a2f9fd5b9246c5e80da8a89cad0a01c3b 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -12,7 +12,7 @@ import * as React from 'react'; import { PluginContext } from '../context'; import { PluginReactContext, PluginUIComponent } from './base'; import { CameraSnapshots } from './camera'; -import { Controls, LociLabelControl, TrajectoryControls } from './controls'; +import { LociLabelControl, TrajectoryControls } from './controls'; import { StateSnapshots } from './state'; import { StateObjectActions } from './state/actions'; import { AnimationControls } from './state/animation'; @@ -60,7 +60,6 @@ class Layout extends PluginUIComponent { {layout.showControls && this.region('left', <State />)} {layout.showControls && this.region('right', <div className='msp-scrollable-container msp-right-controls'> <CurrentObject /> - <Controls /> <AnimationControls /> <CameraSnapshots /> <StateSnapshots /> @@ -72,20 +71,16 @@ class Layout extends PluginUIComponent { } } -export class ViewportWrapper extends PluginUIComponent { +export class ViewportWrapper extends PluginUIComponent { render() { return <> <Viewport /> - <div style={{ position: 'absolute', left: '10px', top: '10px', color: 'white' }}> - <TrajectoryControls /> - </div> + <TrajectoryControls /> <ViewportControls /> <div style={{ position: 'absolute', left: '10px', bottom: '10px' }}> <BackgroundTaskProgress /> </div> - <div style={{ position: 'absolute', right: '10px', bottom: '10px' }}> - <LociLabelControl /> - </div> + <LociLabelControl /> </>; } } diff --git a/src/mol-state/state/selection.ts b/src/mol-state/state/selection.ts index 986dc4467fc670796d1928b4ee4a1731cd0b02e8..97807c000147850d4ff33a34a5bcdaa08a8219d8 100644 --- a/src/mol-state/state/selection.ts +++ b/src/mol-state/state/selection.ts @@ -212,7 +212,7 @@ namespace StateSelection { registerModifier('parent', parent); export function parent(b: Selector) { return unique(mapEntity(b, (n, s) => s.cells.get(s.tree.transforms.get(n.transform.ref)!.parent))); } - export function findAncestorOfType(tree: StateTree, cells: State.Cells, root: StateTransform.Ref, types: StateObject.Ctor[]): StateObjectCell | undefined { + export function findAncestorOfType<T extends StateObject.Ctor>(tree: StateTree, cells: State.Cells, root: StateTransform.Ref, types: T[]): StateObjectCell<StateObject.From<T>> | undefined { let current = tree.transforms.get(root)!, len = types.length; while (true) { current = tree.transforms.get(current.parent)!; @@ -220,7 +220,7 @@ namespace StateSelection { if (!cell.obj) return void 0; const obj = cell.obj; for (let i = 0; i < len; i++) { - if (obj.type === types[i].type) return cells.get(current.ref); + if (obj.type === types[i].type) return cell as StateObjectCell<StateObject.From<T>>; } if (current.ref === StateTransform.RootRef) { return void 0;