Browse Source

mol-plugin: state controls in viewport

David Sehnal 6 years ago
parent
commit
0bc93904a3

+ 23 - 7
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;
+        }
     }
 }

+ 17 - 0
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 => {

+ 72 - 13
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: [] }
 

+ 2 - 2
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>;
 }

+ 6 - 3
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} />}
         </>;
     }