Browse Source

add snapshot download UI to "screenshot" panel

David Sehnal 5 years ago
parent
commit
04d34b369a

+ 2 - 2
src/mol-plugin-ui/controls/common.tsx

@@ -328,7 +328,7 @@ export class ToggleButton extends React.PureComponent<ToggleButtonProps> {
     }
 }
 
-export class ExpandGroup extends React.PureComponent<{ header: string, headerStyle?: React.CSSProperties, initiallyExpanded?: boolean, noOffset?: boolean, marginTop?: 0 | string, headerLeftMargin?: string }, { isExpanded: boolean }> {
+export class ExpandGroup extends React.PureComponent<{ header: string, headerStyle?: React.CSSProperties, initiallyExpanded?: boolean, accent?: boolean, noOffset?: boolean, marginTop?: 0 | string, headerLeftMargin?: string }, { isExpanded: boolean }> {
     state = { isExpanded: !!this.props.initiallyExpanded };
 
     toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
@@ -344,7 +344,7 @@ export class ExpandGroup extends React.PureComponent<{ header: string, headerSty
             {this.state.isExpanded &&
                 (this.props.noOffset
                     ? this.props.children
-                    : <div className='msp-control-offset'>
+                    : <div className={this.props.accent ? 'msp-accent-offset' : 'msp-control-offset'}>
                         {this.props.children}
                     </div>)}
         </>;

+ 31 - 23
src/mol-plugin-ui/state/snapshots.tsx

@@ -18,7 +18,22 @@ import { PluginUIComponent, PurePluginUIComponent } from '../base';
 import { Button, IconButton, SectionHeader } from '../controls/common';
 import { ParameterControls } from '../controls/parameters';
 
-export class StateSnapshots extends PluginUIComponent<{ }> {
+export class StateSnapshots extends PluginUIComponent<{}> {
+    render() {
+        return <div>
+            <SectionHeader icon={SaveOutlined} title='Plugin State' />
+            <LocalStateSnapshots />
+            <LocalStateSnapshotList />
+            {this.plugin.spec.components?.remoteState !== 'none' && <RemoteStateSnapshots />}
+
+            <div style={{ marginTop: '10px' }}>
+                <StateExportImportControls />
+            </div>
+        </div>;
+    }
+}
+
+export class StateExportImportControls extends PluginUIComponent {
     downloadToFileJson = () => {
         PluginCommands.State.Snapshots.DownloadToFile(this.plugin, { type: 'json' });
     }
@@ -34,25 +49,18 @@ export class StateSnapshots extends PluginUIComponent<{ }> {
     }
 
     render() {
-        return <div>
-            <SectionHeader icon={SaveOutlined} title='Plugin State' />
-            <LocalStateSnapshots />
-            <LocalStateSnapshotList />
-            {this.plugin.spec.components?.remoteState !== 'none' && <RemoteStateSnapshots />}
-
-            <div className='msp-flex-row' style={{ marginTop: '10px' }}>
-                <Button icon={GetApp} onClick={this.downloadToFileJson}>JSON</Button>
-                <Button icon={GetApp} onClick={this.downloadToFileZip}>ZIP</Button>
-                <div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file'>
-                    {'Open'} <input onChange={this.open} type='file' multiple={false} accept='.json,.zip' />
-                </div>
+        return <div className='msp-flex-row'>
+            <Button icon={GetApp} onClick={this.downloadToFileJson} title='Save state description'>Base</Button>
+            <Button icon={GetApp} onClick={this.downloadToFileZip} title='Save state including the data as zip.'>All</Button>
+            <div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file'>
+                {'Open'} <input onChange={this.open} type='file' multiple={false} accept='.json,.zip' />
             </div>
         </div>;
     }
 }
 
 class LocalStateSnapshots extends PluginUIComponent<
-{ },
+{},
 { params: PD.Values<typeof LocalStateSnapshots.Params> }> {
 
     state = { params: PD.getDefaultValues(LocalStateSnapshots.Params) };
@@ -97,7 +105,7 @@ class LocalStateSnapshots extends PluginUIComponent<
                 const params = { ...this.state.params, [p.name]: p.value };
                 this.setState({ params } as any);
                 this.plugin.managers.snapshot.currentGetSnapshotParams = params.options;
-            }}/>
+            }} />
 
             <div className='msp-flex-row'>
                 <Button onClick={this.add}>Save</Button>
@@ -107,7 +115,7 @@ class LocalStateSnapshots extends PluginUIComponent<
     }
 }
 
-class LocalStateSnapshotList extends PluginUIComponent<{ }, { }> {
+class LocalStateSnapshotList extends PluginUIComponent<{}, {}> {
     componentDidMount() {
         this.subscribe(this.plugin.managers.snapshot.events.changed, () => this.forceUpdate());
     }
@@ -147,7 +155,7 @@ class LocalStateSnapshotList extends PluginUIComponent<{ }, { }> {
         return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
             {this.plugin.managers.snapshot.state.entries.map(e => <li key={e!.snapshot.id}>
                 <Button data-id={e!.snapshot.id} onClick={this.apply}>
-                    <span style={{ fontWeight: e!.snapshot.id === current ? 'bold' : void 0}}>
+                    <span style={{ fontWeight: e!.snapshot.id === current ? 'bold' : void 0 }}>
                         {e!.name || new Date(e!.timestamp).toLocaleString()}</span> <small>
                         {`${e!.snapshot.durationInMs ? formatTimespan(e!.snapshot.durationInMs, false) + `${e!.description ? ', ' : ''}` : ''}${e!.description ? e!.description : ''}`}
                     </small>
@@ -155,7 +163,7 @@ class LocalStateSnapshotList extends PluginUIComponent<{ }, { }> {
                 <div>
                     <IconButton svg={ArrowUpward} data-id={e!.snapshot.id} title='Move Up' onClick={this.moveUp} small={true} />
                     <IconButton svg={ArrowDownward} data-id={e!.snapshot.id} title='Move Down' onClick={this.moveDown} small={true} />
-                    <IconButton svg={SwapHoriz}  data-id={e!.snapshot.id} title='Replace' onClick={this.replace} small={true} />
+                    <IconButton svg={SwapHoriz} data-id={e!.snapshot.id} title='Replace' onClick={this.replace} small={true} />
                     <IconButton svg={DeleteOutlined} data-id={e!.snapshot.id} title='Remove' onClick={this.remove} small={true} />
                 </div>
             </li>)}
@@ -200,7 +208,7 @@ export class RemoteStateSnapshots extends PluginUIComponent<
             this.setState({ isBusy: true });
             this.plugin.config.set(PluginConfig.State.CurrentServer, this.state.params.options.serverUrl);
 
-            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;
@@ -279,7 +287,7 @@ export class RemoteStateSnapshots extends PluginUIComponent<
             {!this.props.listOnly && <>
                 <ParameterControls params={this.Params} values={this.state.params} onEnter={this.upload} onChange={p => {
                     this.setState({ params: { ...this.state.params, [p.name]: p.value } } as any);
-                }} isDisabled={this.state.isBusy}/>
+                }} isDisabled={this.state.isBusy} />
                 <div className='msp-flex-row'>
                     <Button icon={CloudUpload} onClick={this.upload} disabled={this.state.isBusy}>Upload</Button>
                     <Button onClick={this.refresh} disabled={this.state.isBusy}>Refresh</Button>
@@ -292,7 +300,7 @@ export class RemoteStateSnapshots extends PluginUIComponent<
             {this.props.listOnly && <>
                 <ParameterControls params={this.ListOnlyParams} values={this.state.params} onEnter={this.upload} onChange={p => {
                     this.setState({ params: { ...this.state.params, [p.name]: p.value } } as any);
-                }} isDisabled={this.state.isBusy}/>
+                }} isDisabled={this.state.isBusy} />
                 <div className='msp-flex-row'>
                     <Button onClick={this.refresh} disabled={this.state.isBusy}>Refresh</Button>
                 </div>
@@ -303,7 +311,7 @@ export class RemoteStateSnapshots extends PluginUIComponent<
 
 class RemoteStateSnapshotList extends PurePluginUIComponent<
 { entries: OrderedMap<string, RemoteEntry>, serverUrl: string, isBusy: boolean, fetch: (e: React.MouseEvent<HTMLElement>) => void, remove?: (e: React.MouseEvent<HTMLElement>) => void },
-{ }> {
+{}> {
 
     open = async (e: React.MouseEvent<HTMLElement>) => {
         const id = e.currentTarget.getAttribute('data-id');
@@ -320,7 +328,7 @@ class RemoteStateSnapshotList extends PurePluginUIComponent<
 
     render() {
         return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
-            {this.props.entries.valueSeq().map(e =><li key={e!.id} className='msp-flex-row'>
+            {this.props.entries.valueSeq().map(e => <li key={e!.id} className='msp-flex-row'>
                 <Button data-id={e!.id} onClick={this.props.fetch}
                     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>

+ 3 - 3
src/mol-plugin-ui/viewport.tsx

@@ -5,7 +5,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Autorenew, BuildOutlined, CameraAltOutlined, Close, Crop, Fullscreen, Tune } from '@material-ui/icons';
+import { Autorenew, BuildOutlined, CameraAltOutlined, Close, Crop, Fullscreen, Tune, CameraOutlined } from '@material-ui/icons';
 import * as React from 'react';
 import { resizeCanvas } from '../mol-canvas3d/util';
 import { PluginCommands } from '../mol-plugin/commands';
@@ -94,7 +94,7 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
                 </div>
                 <div>
                     <div className='msp-semi-transparent-background' />
-                    {this.icon(CameraAltOutlined, this.toggleScreenshotExpanded, 'Screenshot', this.state.isScreenshotExpanded)}
+                    {this.icon(CameraOutlined, this.toggleScreenshotExpanded, 'Screenshot / State Snapshot', this.state.isScreenshotExpanded)}
                 </div>
                 <div>
                     <div className='msp-semi-transparent-background' />
@@ -108,7 +108,7 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
                 </div>}
             </div>
             {this.state.isScreenshotExpanded && <div className='msp-viewport-controls-panel'>
-                <ControlGroup header='Screenshot' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleScreenshotExpanded}
+                <ControlGroup header='Screenshot / State Snapshot' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleScreenshotExpanded}
                     topRightIcon={Close} noTopMargin>
                     <DownloadScreenshotControls close={this.toggleScreenshotExpanded} />
                 </ControlGroup>

+ 24 - 2
src/mol-plugin-ui/viewport/screenshot.tsx

@@ -12,9 +12,12 @@ import { PluginUIComponent } from '../base';
 import { debounceTime } from 'rxjs/operators';
 import { Subject } from 'rxjs';
 import { ViewportScreenshotHelper } from '../../mol-plugin/util/viewport-screenshot';
-import { Button } from '../controls/common';
+import { Button, ExpandGroup } from '../controls/common';
 import { CameraHelperProps } from '../../mol-canvas3d/helper/camera-helper';
-import { GetApp, Launch } from '@material-ui/icons';
+import { GetApp, Launch, Warning } from '@material-ui/icons';
+import { PluginCommands } from '../../mol-plugin/commands';
+import { Icon } from '../controls/icons';
+import { StateExportImportControls } from '../state/snapshots';
 
 interface ImageControlsState {
     showPreview: boolean
@@ -118,6 +121,19 @@ export class DownloadScreenshotControls extends PluginUIComponent<{ close: () =>
         }
     }
 
+    downloadToFileJson = () => {
+        PluginCommands.State.Snapshots.DownloadToFile(this.plugin, { type: 'json' });
+    }
+
+    downloadToFileZip = () => {
+        PluginCommands.State.Snapshots.DownloadToFile(this.plugin, { type: 'zip' });
+    }
+
+    open = (e: React.ChangeEvent<HTMLInputElement>) => {
+        if (!e.target.files || !e.target.files![0]) return;
+        PluginCommands.State.Snapshots.OpenFile(this.plugin, { file: e.target.files![0] });
+    }
+
     render() {
         return <div>
             <div className='msp-image-preview'>
@@ -129,6 +145,12 @@ export class DownloadScreenshotControls extends PluginUIComponent<{ close: () =>
                 <Button icon={Launch} onClick={this.openTab} disabled={this.state.isDisabled}>Open in new Tab</Button>
             </div>
             <ParameterControls params={this.plugin.helpers.viewportScreenshot!.params} values={this.plugin.helpers.viewportScreenshot!.values} onChange={this.setProps} isDisabled={this.state.isDisabled} />
+            <ExpandGroup header='State Snapshot' accent>
+                <StateExportImportControls />
+                <div className='msp-help-description' style={{ padding: '10px'}}>
+                    <Icon svg={Warning} /> This is an experimental feature and states stored today might not be openable in an upcoming version.
+                </div>
+            </ExpandGroup>
         </div>;
     }
 }

+ 4 - 5
src/mol-plugin/state.ts

@@ -17,14 +17,15 @@ import { InteractivityManager } from '../mol-plugin-state/manager/interactivity'
 import { produce } from 'immer';
 import { StructureFocusSnapshot } from '../mol-plugin-state/manager/structure/focus';
 import { merge } from 'rxjs';
+import { PluginContext } from './context';
 
 export { PluginState };
 
 class PluginState {
     private get animation() { return this.plugin.managers.animation; }
 
-    readonly data: State;
-    readonly behaviors: State;
+    readonly data = State.create(new SO.Root({ }), { runTask: this.plugin.runTask, globalContext: this.plugin });
+    readonly behaviors = State.create(new PluginBehavior.Root({ }), { runTask: this.plugin.runTask, globalContext: this.plugin, rootState: { isLocked: true } });
 
     readonly events = {
         cell: {
@@ -111,9 +112,7 @@ class PluginState {
         this.animation.dispose();
     }
 
-    constructor(private plugin: import('./context').PluginContext) {
-        this.data = State.create(new SO.Root({ }), { runTask: plugin.runTask, globalContext: plugin });
-        this.behaviors = State.create(new PluginBehavior.Root({ }), { runTask: plugin.runTask, globalContext: plugin, rootState: { isLocked: true } });
+    constructor(private plugin: PluginContext) {
     }
 }