diff --git a/src/mol-plugin/skin/base/components/temp.scss b/src/mol-plugin/skin/base/components/temp.scss index c476c298a21c6929206bc016509068266f8b8554..b94dd4262f4e875bddb170d27d32f7ab6807fa9b 100644 --- a/src/mol-plugin/skin/base/components/temp.scss +++ b/src/mol-plugin/skin/base/components/temp.scss @@ -151,16 +151,32 @@ } } -.msp-traj-controls { +.msp-viewport-top-left-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; + .msp-traj-controls { + line-height: $row-height; + float: left; + margin-right: $control-spacing; + + > span { + color: $font-color; + padding-top: 1px; + font-size: 85%; + display: inline-block; + } + } + + .msp-state-snapshot-viewport-controls { + line-height: $row-height; + float: left; + margin-right: $control-spacing; + + > select { + display: inline-block; + width: 200px; + } } } \ No newline at end of file diff --git a/src/mol-plugin/state/snapshots.ts b/src/mol-plugin/state/snapshots.ts index 3aed583fc613acfa782b28d43f98f7ff3cbba8b6..1648eb7850e965b80410e0fb7c07c43eb8ec61b5 100644 --- a/src/mol-plugin/state/snapshots.ts +++ b/src/mol-plugin/state/snapshots.ts @@ -49,6 +49,23 @@ class PluginStateSnapshotManager extends PluginComponent<{ current?: UUID | unde return e && e.snapshot; } + getNextId(id: string | undefined, dir: -1 | 1) { + const xs = this.state.entries; + const keys = xs.keys(); + let k = keys.next(); + let prev = k.value; + const fst = prev; + while (!k.done) { + k = keys.next(); + if (k.value === id && dir === -1) return prev; + if (!k.done && prev === id && dir === 1) return k.value; + if (!k.done) prev = k.value; + else break; + } + if (dir === -1) return prev; + return fst; + } + setRemoteSnapshot(snapshot: PluginStateSnapshotManager.RemoteSnapshot): PluginState.Snapshot | undefined { this.clear(); const entries = this.state.entries.withMutations(m => { diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx index 4b54f9d4a9305827841c62c6510c2e6801ea56d0..847b7b215264217b8cef2dd802fd5d5dce376e7a 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/src/mol-plugin/ui/controls.tsx @@ -25,7 +25,8 @@ export class TrajectoryControls extends PluginUIComponent<{}, { show: boolean, l .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory)); if (models.length === 0) { - this.setState({ show: false }) + this.setState({ show: false }); + return; } let label = '', count = 0, parents = new Set<string>(); @@ -57,27 +58,85 @@ export class TrajectoryControls extends PluginUIComponent<{}, { show: boolean, l this.subscribe(this.plugin.state.dataState.events.changed, this.update); } + reset = () => PluginCommands.State.ApplyAction.dispatch(this.plugin, { + state: this.plugin.state.dataState, + action: UpdateTrajectory.create({ action: 'reset' }) + }); + + prev = () => PluginCommands.State.ApplyAction.dispatch(this.plugin, { + state: this.plugin.state.dataState, + action: UpdateTrajectory.create({ action: 'advance', by: -1 }) + }); + + next = () => PluginCommands.State.ApplyAction.dispatch(this.plugin, { + state: this.plugin.state.dataState, + action: UpdateTrajectory.create({ action: 'advance', by: 1 }) + }); + render() { 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' }) - })} /> - <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 }) - })} /> + <IconButton icon='model-first' title='First Model' onClick={this.reset} /> + <IconButton icon='model-prev' title='Previous Model' onClick={this.prev} /> + <IconButton icon='model-next' title='Next Model' onClick={this.next} /> { !!this.state.label && <span>{this.state.label}</span> } </div>; } } +export class StateSnapshotViewportControls extends PluginUIComponent<{}, { isBusy: boolean }> { + state = { isBusy: false } + + componentDidMount() { + // TODO: this needs to be diabled when the state is updating! + this.subscribe(this.plugin.state.snapshots.events.changed, () => this.forceUpdate()); + } + + async update(id: string) { + this.setState({ isBusy: true }); + await PluginCommands.State.Snapshots.Apply.dispatch(this.plugin, { id }); + this.setState({ isBusy: false }); + } + + change = (e: React.ChangeEvent<HTMLSelectElement>) => { + if (e.target.value === 'none') return; + this.update(e.target.value); + } + + prev = () => { + const s = this.plugin.state.snapshots; + const id = s.getNextId(s.state.current, -1); + if (id) this.update(id); + } + + next = () => { + const s = this.plugin.state.snapshots; + const id = s.getNextId(s.state.current, 1); + if (id) this.update(id); + } + + render() { + const snapshots = this.plugin.state.snapshots; + const count = snapshots.state.entries.size; + + if (count < 2) { + return null; + } + + const current = snapshots.state.current; + + return <div className='msp-state-snapshot-viewport-controls'> + <select className='msp-form-control' value={current || 'none'} onChange={this.change} disabled={this.state.isBusy}> + {!current && <option key='none' value='none'></option>} + {snapshots.state.entries.valueSeq().map((e, i) => <option key={e!.snapshot.id} value={e!.snapshot.id}>{`[${i! + 1}/${count}]`} {e!.name || new Date(e!.timestamp).toLocaleString()}</option>)} + </select> + <IconButton icon='model-prev' title='Previous State' onClick={this.prev} disabled={this.state.isBusy} /> + <IconButton icon='model-next' title='Next State' onClick={this.next} disabled={this.state.isBusy} /> + </div>; + } +} + export class LociLabelControl extends PluginUIComponent<{}, { entries: ReadonlyArray<LociLabelEntry> }> { state = { entries: [] } diff --git a/src/mol-plugin/ui/controls/common.tsx b/src/mol-plugin/ui/controls/common.tsx index 83fad49cb09fa2e75336d2f7decee862cd211bbd..423b0784c0abee51a9d521161e2ea4759d81dca4 100644 --- a/src/mol-plugin/ui/controls/common.tsx +++ b/src/mol-plugin/ui/controls/common.tsx @@ -81,10 +81,10 @@ export class NumericInput extends React.PureComponent<{ } } -export function IconButton(props: { icon: string, onClick: (e: React.MouseEvent<HTMLButtonElement>) => void, title?: string, toggleState?: boolean }) { +export function IconButton(props: { icon: string, onClick: (e: React.MouseEvent<HTMLButtonElement>) => void, title?: string, toggleState?: boolean, disabled?: 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}> + return <button className={className} onClick={props.onClick} title={props.title} disabled={props.disabled}> <span className={`msp-icon msp-icon-${props.icon}`}/> </button>; } diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index 6410c7cd4b3172751aed089f202ea684a9c98fd7..4641dc353e15196123d5196471ce435db35e3ef5 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -11,7 +11,7 @@ import { LogEntry } from 'mol-util/log-entry'; import * as React from 'react'; import { PluginContext } from '../context'; import { PluginReactContext, PluginUIComponent } from './base'; -import { LociLabelControl, TrajectoryControls } from './controls'; +import { LociLabelControl, TrajectoryControls, StateSnapshotViewportControls } from './controls'; import { StateSnapshots } from './state'; import { StateObjectActions } from './state/actions'; import { AnimationControls } from './state/animation'; @@ -84,7 +84,10 @@ export class ViewportWrapper extends PluginUIComponent { render() { return <> <Viewport /> - <TrajectoryControls /> + <div className='msp-viewport-top-left-controls'> + <TrajectoryControls /> + <StateSnapshotViewportControls /> + </div> <ViewportControls /> <div style={{ position: 'absolute', left: '10px', bottom: '10px' }}> <BackgroundTaskProgress /> @@ -186,7 +189,7 @@ export class CurrentObject extends PluginUIComponent { if (!showActions) return null; return <> - {(cell.status === 'ok' || cell.status == 'error') && <UpdateTransformContol state={current.state} transform={transform} /> } + {(cell.status === 'ok' || cell.status === 'error') && <UpdateTransformContol state={current.state} transform={transform} /> } {cell.status === 'ok' && <StateObjectActions state={current.state} nodeRef={ref} />} </>; }