diff --git a/src/apps/viewer/index.ts b/src/apps/viewer/index.ts index 4f2b72751c4f43fc6d178625d01c3e3379a8c5d9..d67a0e9396a68933718ec84730cd55f7f22bfaab 100644 --- a/src/apps/viewer/index.ts +++ b/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); diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts index 1e2f8331c1bd92eba141fe66224c696b0775b854..2ad87f7ff3b7a676dcd14d4d2298f06a267a7ecb 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts +++ b/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({ diff --git a/src/mol-plugin/state/snapshots.ts b/src/mol-plugin/state/snapshots.ts index a8c68a38e74b1c356aa3506252cda80469df6e80..318925085f7891c62039ec79bdaa114fbc66face 100644 --- a/src/mol-plugin/state/snapshots.ts +++ b/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); diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx index e11cd58d6e159514aa5cbdaa4d0b4dd95e166d70..0b251fdc277c998271ca9a68334475f03ee59eb5 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/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>} diff --git a/src/mol-plugin/ui/state.tsx b/src/mol-plugin/ui/state.tsx index 740627056f7b2725b604a64afd30653622a163a5..a2a2f400dd2c103ccea6a4bfa504e4ca117bcaed 100644 --- a/src/mol-plugin/ui/state.tsx +++ b/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>; }