Browse Source

mol-plugin: control snapshots by ctrl+arrow keys. sticky snapshot support, tweaks

David Sehnal 6 years ago
parent
commit
d4bfd50168

+ 7 - 2
src/apps/viewer/index.ts

@@ -38,8 +38,13 @@ function init() {
 async function trySetSnapshot(ctx: PluginContext) {
     try {
         const snapshotUrl = getParam('snapshot-url', `[^&]+`);
-        if (!snapshotUrl) return;
-        await PluginCommands.State.Snapshots.Fetch.dispatch(ctx, { url: snapshotUrl })
+        const snapshotId = getParam('snapshot-id', `[^&]+`);
+        if (!snapshotUrl && !snapshotId) return;
+        // TODO parametrize the server
+        const url = snapshotId
+            ? `https://webchem.ncbr.muni.cz/molstar-state/get/${snapshotId}`
+            : snapshotUrl;
+        await PluginCommands.State.Snapshots.Fetch.dispatch(ctx, { url })
     } catch (e) {
         ctx.log.error('Failed to load snapshot.');
         console.warn('Failed to load snapshot', e);

+ 2 - 2
src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts

@@ -47,7 +47,7 @@ export namespace VolumeStreaming {
         const box = (data && data.structure.boundary.box) || Box3D.empty();
 
         return {
-            view: PD.MappedStatic('selection-box', {
+            view: PD.MappedStatic(info.kind === 'em' ? 'cell' : 'selection-box', {
                 'box': PD.Group({
                     bottomLeft: PD.Vec3(box.min),
                     topRight: PD.Vec3(box.max),
@@ -60,7 +60,7 @@ export namespace VolumeStreaming {
                 'cell': PD.Group({}),
                 // 'auto': PD.Group({  }), // based on camera distance/active selection/whatever, show whole structure or slice.
             }, { options: [['box', 'Bounded Box'], ['selection-box', 'Selection'], ['cell', 'Whole Structure']] }),
-            detailLevel: PD.Select<number>(Math.min(1, info.header.availablePrecisions.length - 1),
+            detailLevel: PD.Select<number>(Math.min(3, info.header.availablePrecisions.length - 1),
                 info.header.availablePrecisions.map((p, i) => [i, `${i + 1} [ ${Math.pow(p.maxVoxels, 1 / 3) | 0}^3 cells ]`] as [number, string])),
             channels: info.kind === 'em'
                 ? PD.Group({

+ 1 - 0
src/mol-plugin/state/snapshots.ts

@@ -190,6 +190,7 @@ class PluginStateSnapshotManager extends PluginComponent<{
                 this.next();
                 return;
             }
+            this.events.changed.next();
             const snapshot = e.snapshot;
             const delay = typeof snapshot.durationInMs !== 'undefined' ? snapshot.durationInMs : this.state.nextSnapshotDelayInMs;
             this.timeoutHandle = setTimeout(this.next, delay);

+ 37 - 5
src/mol-plugin/ui/controls.tsx

@@ -109,9 +109,38 @@ export class StateSnapshotViewportControls extends PluginUIComponent<{}, { isBus
         // TODO: this needs to be diabled when the state is updating!
         this.subscribe(this.plugin.state.snapshots.events.changed, () => this.forceUpdate());
         this.subscribe(this.plugin.behaviors.state.isUpdating, isBusy => this.setState({ isBusy }));
-        this.subscribe(this.plugin.behaviors.state.isAnimating, isBusy => this.setState({ isBusy }));
+        this.subscribe(this.plugin.behaviors.state.isAnimating, isBusy => this.setState({ isBusy }))
+
+        window.addEventListener('keyup', this.keyUp, false);
+    }
+
+    componentWillUnmount() {
+        super.componentWillUnmount();
+        window.removeEventListener('keyup', this.keyUp, false);
     }
 
+    keyUp = (e: KeyboardEvent) => {
+        if (!e.ctrlKey || this.state.isBusy || e.target !== document.body) return;
+        const snapshots = this.plugin.state.snapshots;
+        if (e.keyCode === 37) { // left
+            if (snapshots.state.isPlaying) snapshots.stop();
+            this.prev();
+        } else if (e.keyCode === 38) { // up
+            if (snapshots.state.isPlaying) snapshots.stop();
+            if (snapshots.state.entries.size === 0) return;
+            const e = snapshots.state.entries.get(0);
+            this.update(e.snapshot.id);
+        } else if (e.keyCode === 39) { // right
+            if (snapshots.state.isPlaying) snapshots.stop();
+            this.next();
+        } else if (e.keyCode === 40) { // down
+            if (snapshots.state.isPlaying) snapshots.stop();
+            if (snapshots.state.entries.size === 0) return;
+            const e = snapshots.state.entries.get(snapshots.state.entries.size - 1);
+            this.update(e.snapshot.id);
+        }
+    };
+
     async update(id: string) {
         this.setState({ isBusy: true });
         await PluginCommands.State.Snapshots.Apply.dispatch(this.plugin, { id });
@@ -155,7 +184,7 @@ export class StateSnapshotViewportControls extends PluginUIComponent<{}, { isBus
                 {!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={isPlaying ? 'pause' : 'play'} title={isPlaying ? 'Pause' : 'Cycle States'} onClick={this.togglePlay}
+            <IconButton icon={isPlaying ? 'stop' : 'play'} title={isPlaying ? 'Pause' : 'Cycle States'} onClick={this.togglePlay}
                 disabled={isPlaying ? false : this.state.isBusy} />
             {!isPlaying && <>
                 <IconButton icon='left-open' title='Previous State' onClick={this.prev} disabled={this.state.isBusy || isPlaying} />
@@ -190,12 +219,15 @@ export class AnimationViewportControls extends PluginUIComponent<{}, { isEmpty:
 
     render() {
         // if (!this.state.show) return null;
+        const isPlaying = this.plugin.state.snapshots.state.isPlaying;
+        if (isPlaying) return null;
+
         const isAnimating = this.state.isAnimating;
 
         return <div className='msp-animation-viewport-controls'>
-            <IconButton icon={isAnimating ? 'stop' : 'play'} title={isAnimating ? 'Stop' : 'Select Animation'}
-                onClick={isAnimating ? this.stop : this.toggleExpanded}
-                disabled={isAnimating ? false : this.state.isUpdating || this.state.isPlaying || this.state.isEmpty} />
+            <IconButton icon={isAnimating || isPlaying ? 'stop' : 'play'} title={isAnimating ? 'Stop' : 'Select Animation'}
+                onClick={isAnimating || isPlaying ? this.stop : this.toggleExpanded}
+                disabled={isAnimating|| isPlaying ? false : this.state.isUpdating || this.state.isPlaying || this.state.isEmpty} />
             {(this.state.isExpanded && !this.state.isUpdating) && <div className='msp-animation-viewport-controls-select'>
                 <AnimationControls onStart={this.toggleExpanded} />
             </div>}

+ 10 - 4
src/mol-plugin/ui/state.tsx

@@ -157,7 +157,7 @@ class LocalStateSnapshotList extends PluginUIComponent<{ }, { }> {
     }
 }
 
-type RemoteEntry = { url: string, removeUrl: string, timestamp: number, id: string, name: string, description: string }
+type RemoteEntry = { url: string, removeUrl: string, timestamp: number, id: string, name: string, description: string, isSticky?: boolean }
 class RemoteStateSnapshots extends PluginUIComponent<
     { },
     { params: PD.Values<typeof RemoteStateSnapshots.Params>, entries: OrderedMap<string, RemoteEntry>, isBusy: boolean }> {
@@ -186,7 +186,13 @@ class RemoteStateSnapshots extends PluginUIComponent<
     refresh = async () => {
         try {
             this.setState({ isBusy: true });
-            const json = await this.plugin.runTask<RemoteEntry[]>(this.plugin.fetch({ url: this.serverUrl('list'), type: 'json'  }));
+            const json = (await this.plugin.runTask<RemoteEntry[]>(this.plugin.fetch({ url: this.serverUrl('list'), type: 'json'  }))) || [];
+
+            json.sort((a, b) => {
+                if (a.isSticky === b.isSticky) return a.timestamp - b.timestamp;
+                return a.isSticky ? -1 : 1;
+            });
+
             const entries = OrderedMap<string, RemoteEntry>().asMutable();
             for (const e of json) {
                 entries.set(e.id, {
@@ -293,9 +299,9 @@ class RemoteStateSnapshotList extends PurePluginUIComponent<
                     disabled={this.props.isBusy} onContextMenu={this.open} title='Click to download, right-click to open in a new tab.'>
                     {e!.name || new Date(e!.timestamp).toLocaleString()} <small>{e!.description}</small>
                 </button>
-                <div>
+                {!e!.isSticky && <div>
                     <IconButton data-id={e!.id} icon='remove' title='Remove' onClick={this.props.remove} disabled={this.props.isBusy} />
-                </div>
+                </div>}
             </li>)}
         </ul>;
     }