Browse Source

mol-plugin: global state save options

David Sehnal 5 years ago
parent
commit
094a018b5b

+ 2 - 6
src/mol-plugin-state/manager/snapshots.ts

@@ -33,8 +33,6 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{
         changed: this.ev()
     };
 
-    currentGetSnapshotParams: PluginState.GetSnapshotParams = PluginState.DefaultGetSnapshotParams as any;
-
     getIndex(e: PluginStateSnapshotManager.Entry) {
         return this.state.entries.indexOf(e);
     }
@@ -164,7 +162,7 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{
         return next;
     }
 
-    private syncCurrent(options?: { name?: string, description?: string, params?: PluginState.GetSnapshotParams }) {
+    private syncCurrent(options?: { name?: string, description?: string, params?: PluginState.SnapshotParams }) {
         const snapshot = this.plugin.state.getSnapshot(options?.params);
         if (this.state.entries.size === 0 || !this.state.current) {
             this.add(PluginStateSnapshotManager.Entry(snapshot, { name: options?.name, description: options?.description }));
@@ -173,10 +171,8 @@ class PluginStateSnapshotManager extends StatefulPluginComponent<{
         }
     }
 
-    getStateSnapshot(options?: { name?: string, description?: string, playOnLoad?: boolean, params?: PluginState.GetSnapshotParams }): PluginStateSnapshotManager.StateSnapshot {
+    getStateSnapshot(options?: { name?: string, description?: string, playOnLoad?: boolean, params?: PluginState.SnapshotParams }): PluginStateSnapshotManager.StateSnapshot {
         // TODO: diffing and all that fancy stuff
-
-        // TODO: the options need to be handled better, particularky options.params
         this.syncCurrent(options);
 
         return {

+ 2 - 2
src/mol-plugin-ui/base.tsx

@@ -8,7 +8,7 @@
 import * as React from 'react';
 import { Observable, Subscription } from 'rxjs';
 import { PluginContext } from '../mol-plugin/context';
-import { Button } from './controls/common';
+import { Button, ColorAccent } from './controls/common';
 import { ArrowRight, ArrowDropDown } from '@material-ui/icons';
 import { Icon } from './controls/icons';
 
@@ -77,7 +77,7 @@ export type CollapsableState = {
     header: string,
     description?: string,
     isHidden?: boolean,
-    brand?: { svg?: React.FC, accent: 'cyan' | 'red' | 'gray' | 'green' | 'purple' | 'blue' | 'orange' }
+    brand?: { svg?: React.FC, accent: ColorAccent }
 }
 
 export abstract class CollapsableControls<P = {}, S = {}, SS = {}> extends PluginUIComponent<P & CollapsableProps, S & CollapsableState, SS> {

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

@@ -9,6 +9,8 @@ import { Color } from '../../mol-util/color';
 import { Icon } from './icons';
 import { ArrowRight, ArrowDropDown, Remove, Add } from '@material-ui/icons';
 
+export type ColorAccent = 'cyan' | 'red' | 'gray' | 'green' | 'purple' | 'blue' | 'orange'
+
 export class ControlGroup extends React.Component<{
     header: string,
     initialExpanded?: boolean,
@@ -211,8 +213,8 @@ export class ExpandableControlRow extends React.Component<{
     }
 }
 
-export function SectionHeader(props: { icon?: React.FC, title: string | JSX.Element, desc?: string }) {
-    return <div className='msp-section-header'>
+export function SectionHeader(props: { icon?: React.FC, title: string | JSX.Element, desc?: string, accent?: ColorAccent }) {
+    return <div className={`msp-section-header${props.accent ? ' msp-transform-header-brand-' + props.accent : ''}` }>
         {props.icon && <Icon svg={props.icon} />}
         {props.title} <small>{props.desc}</small>
     </div>;
@@ -224,6 +226,7 @@ export type ButtonProps = {
     disabled?: boolean,
     title?: string,
     icon?: React.FC,
+    commit?: boolean | 'on' | 'off'
     children?: React.ReactNode,
     onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void,
     onContextMenu?: (e: React.MouseEvent<HTMLButtonElement>) => void,
@@ -241,6 +244,8 @@ export function Button(props: ButtonProps) {
     if (!props.inline) className += ' msp-btn-block';
     if (props.noOverflow) className += ' msp-no-overflow';
     if (props.flex) className += ' msp-flex-item';
+    if (props.commit === 'on' || props.commit) className += ' msp-btn-commit msp-btn-commit-on';
+    if (props.commit === 'off') className += ' msp-btn-commit msp-btn-commit-off';
     if (!props.children) className += ' msp-btn-childless';
     if (props.className) className += ' ' + props.className;
 

+ 40 - 41
src/mol-plugin-ui/state/snapshots.tsx

@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { ArrowDownward, ArrowUpward, CloudUpload, DeleteOutlined, GetApp, OpenInBrowser, SaveOutlined, SwapHoriz } from '@material-ui/icons';
+import { Add, ArrowDownward, ArrowUpward, CloudUpload, DeleteOutlined, GetApp, OpenInBrowser, SaveOutlined, SwapHoriz, Refresh } from '@material-ui/icons';
 import { OrderedMap } from 'immutable';
 import * as React from 'react';
 import { PluginCommands } from '../../mol-plugin/commands';
@@ -15,7 +15,7 @@ import { formatTimespan } from '../../mol-util/now';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { urlCombine } from '../../mol-util/url';
 import { PluginUIComponent, PurePluginUIComponent } from '../base';
-import { Button, IconButton, SectionHeader } from '../controls/common';
+import { Button, ExpandGroup, IconButton, SectionHeader } from '../controls/common';
 import { Icon } from '../controls/icons';
 import { ParameterControls } from '../controls/parameters';
 
@@ -23,13 +23,21 @@ export class StateSnapshots extends PluginUIComponent<{}> {
     render() {
         return <div>
             <SectionHeader icon={SaveOutlined} title='Plugin State' />
+
+            <div style={{ marginBottom: '10px' }}>
+                <ExpandGroup header='Save Options' initiallyExpanded={false}>
+                    <LocalStateSnapshotParams />
+                </ExpandGroup>
+            </div>
+
             <LocalStateSnapshots />
             <LocalStateSnapshotList />
+
+            <SectionHeader title='Save as File' accent='blue' />
+            <StateExportImportControls />
+
             {this.plugin.spec.components?.remoteState !== 'none' && <RemoteStateSnapshots />}
 
-            <div style={{ marginTop: '10px' }}>
-                <StateExportImportControls />
-            </div>
         </div>;
     }
 }
@@ -67,37 +75,35 @@ export class StateExportImportControls extends PluginUIComponent {
     }
 }
 
+export class LocalStateSnapshotParams extends PluginUIComponent {
+    componentDidMount() {
+        this.subscribe(this.plugin.state.snapshotParams, () => this.forceUpdate());
+    }
+
+    render() {
+        return <ParameterControls params={PluginState.SnapshotParams} values={this.plugin.state.snapshotParams.value} onChangeValues={this.plugin.state.setSnapshotParams} />;
+    }
+}
+
 class LocalStateSnapshots extends PluginUIComponent<
 {},
 { params: PD.Values<typeof LocalStateSnapshots.Params> }> {
-
     state = { params: PD.getDefaultValues(LocalStateSnapshots.Params) };
 
     static Params = {
         name: PD.Text(),
-        options: PD.Group({
-            description: PD.Text(),
-            ...PluginState.GetSnapshotParams
-        })
+        description: PD.Text()
     };
 
     add = () => {
         PluginCommands.State.Snapshots.Add(this.plugin, {
             name: this.state.params.name,
-            description: this.state.params.options.description,
-            params: this.state.params.options
-        });
-        this.setState({
-            params: {
-                name: '',
-                options: {
-                    ...this.state.params.options,
-                    description: ''
-                }
-            }
+            description: this.state.params.description
         });
     }
 
+    updateParams = (params: PD.Values<typeof LocalStateSnapshots.Params>) => this.setState({ params });
+
     clear = () => {
         PluginCommands.State.Snapshots.Clear(this.plugin, {});
     }
@@ -107,17 +113,11 @@ class LocalStateSnapshots extends PluginUIComponent<
     }
 
     render() {
-        // TODO: proper styling
         return <div>
-            <ParameterControls params={LocalStateSnapshots.Params} values={this.state.params} onEnter={this.add} onChange={p => {
-                const params = { ...this.state.params, [p.name]: p.value };
-                this.setState({ params } as any);
-                this.plugin.managers.snapshot.currentGetSnapshotParams = params.options;
-            }} />
-
+            <ParameterControls params={LocalStateSnapshots.Params} values={this.state.params} onEnter={this.add} onChangeValues={this.updateParams} />
             <div className='msp-flex-row'>
-                <Button onClick={this.add}>Save</Button>
-                <Button onClick={this.clear}>Clear</Button>
+                <IconButton onClick={this.clear} svg={DeleteOutlined} title='Remove All' />
+                <Button onClick={this.add} icon={Add} style={{ textAlign: 'right' }} commit>Add</Button>
             </div>
         </div>;
     }
@@ -155,12 +155,12 @@ class LocalStateSnapshotList extends PluginUIComponent<{}, {}> {
     replace = (e: React.MouseEvent<HTMLElement>) => {
         const id = e.currentTarget.getAttribute('data-id');
         if (!id) return;
-        PluginCommands.State.Snapshots.Replace(this.plugin, { id, params: this.plugin.managers.snapshot.currentGetSnapshotParams });
+        PluginCommands.State.Snapshots.Replace(this.plugin, { id });
     }
 
     render() {
         const current = this.plugin.managers.snapshot.state.current;
-        return <ul style={{ listStyle: 'none', marginTop: '1px' }} className='msp-state-list'>
+        return <ul style={{ listStyle: 'none', marginTop: '10px' }} className='msp-state-list'>
             {this.plugin.managers.snapshot.state.entries.map(e => <li key={e!.snapshot.id} className='msp-flex-row'>
                 <Button data-id={e!.snapshot.id} onClick={this.apply} className='msp-no-overflow'>
                     {(console.log(e!.snapshot.durationInMs), false)}
@@ -246,8 +246,7 @@ export class RemoteStateSnapshots extends PluginUIComponent<
             name: this.state.params.name,
             description: this.state.params.options.description,
             playOnLoad: this.state.params.options.playOnLoad,
-            serverUrl: this.state.params.options.serverUrl,
-            params: this.plugin.managers.snapshot.currentGetSnapshotParams
+            serverUrl: this.state.params.options.serverUrl
         });
 
         this.setState({ isBusy: false });
@@ -283,29 +282,29 @@ export class RemoteStateSnapshots extends PluginUIComponent<
 
     render() {
         return <>
-            <SectionHeader title='Remote States' />
+            <SectionHeader title='Remote States' accent='blue' />
 
             {!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} />
                 <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>
+                    <IconButton onClick={this.refresh} disabled={this.state.isBusy} svg={Refresh} />
+                    <Button icon={CloudUpload} onClick={this.upload} disabled={this.state.isBusy} commit>Upload</Button>
                 </div>
             </>}
 
             <RemoteStateSnapshotList entries={this.state.entries} isBusy={this.state.isBusy} serverUrl={this.state.params.options.serverUrl}
                 fetch={this.fetch} remove={this.props.listOnly ? void 0 : this.remove} />
 
-            {this.props.listOnly && <>
+            {this.props.listOnly && <div style={{ marginTop: '10px' }}>
                 <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} />
                 <div className='msp-flex-row'>
-                    <Button onClick={this.refresh} disabled={this.state.isBusy}>Refresh</Button>
+                    <Button onClick={this.refresh} disabled={this.state.isBusy} icon={Refresh}>Refresh</Button>
                 </div>
-            </>}
+            </div>}
         </>;
     }
 }
@@ -328,7 +327,7 @@ class RemoteStateSnapshotList extends PurePluginUIComponent<
     }
 
     render() {
-        return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
+        return <ul style={{ listStyle: 'none', marginTop: '10px' }} className='msp-state-list'>
             {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.'>

+ 4 - 1
src/mol-plugin-ui/viewport/screenshot.tsx

@@ -17,7 +17,7 @@ import { CameraHelperProps } from '../../mol-canvas3d/helper/camera-helper';
 import { GetApp, Launch, Warning } from '@material-ui/icons';
 import { PluginCommands } from '../../mol-plugin/commands';
 import { Icon } from '../controls/icons';
-import { StateExportImportControls } from '../state/snapshots';
+import { StateExportImportControls, LocalStateSnapshotParams } from '../state/snapshots';
 
 interface ImageControlsState {
     showPreview: boolean
@@ -147,6 +147,9 @@ export class DownloadScreenshotControls extends PluginUIComponent<{ close: () =>
             <ParameterControls params={this.plugin.helpers.viewportScreenshot!.params} values={this.plugin.helpers.viewportScreenshot!.values} onChange={this.setProps} isDisabled={this.state.isDisabled} />
             <ExpandGroup header='State Snapshot'>
                 <StateExportImportControls />
+                <ExpandGroup header='Save Options' initiallyExpanded={false} noOffset>
+                    <LocalStateSnapshotParams />
+                </ExpandGroup>
                 <div className='msp-help-text' style={{ padding: '10px'}}>
                     <Icon svg={Warning} /> This is an experimental feature and stored states might not be openable in a future version.
                 </div>

+ 1 - 1
src/mol-plugin/behavior/static/state.ts

@@ -169,7 +169,7 @@ export function Snapshots(ctx: PluginContext) {
             mode: 'cors',
             referrer: 'no-referrer',
             headers: { 'Content-Type': 'application/json; charset=utf-8' },
-            body: JSON.stringify(ctx.managers.snapshot.getStateSnapshot({ name, description, playOnLoad, params }))
+            body: JSON.stringify(ctx.managers.snapshot.getStateSnapshot({ name, description, playOnLoad }))
         }) as any as Promise<void>;
     });
 

+ 3 - 3
src/mol-plugin/commands.ts

@@ -27,14 +27,14 @@ export const PluginCommands = {
         ToggleVisibility: PluginCommand<{ state: State, ref: StateTransform.Ref }>(),
 
         Snapshots: {
-            Add: PluginCommand<{ name?: string, description?: string, params?: PluginState.GetSnapshotParams }>(),
-            Replace: PluginCommand<{ id: string, params?: PluginState.GetSnapshotParams }>(),
+            Add: PluginCommand<{ name?: string, description?: string, params?: PluginState.SnapshotParams }>(),
+            Replace: PluginCommand<{ id: string, params?: PluginState.SnapshotParams }>(),
             Move: PluginCommand<{ id: string, dir: -1 | 1 }>(),
             Remove: PluginCommand<{ id: string }>(),
             Apply: PluginCommand<{ id: string }>(),
             Clear: PluginCommand<{}>(),
 
-            Upload: PluginCommand<{ name?: string, description?: string, playOnLoad?: boolean, serverUrl: string, params?: PluginState.GetSnapshotParams }>(),
+            Upload: PluginCommand<{ name?: string, description?: string, playOnLoad?: boolean, serverUrl: string, params?: PluginState.SnapshotParams }>(),
             Fetch: PluginCommand<{ url: string }>(),
 
             DownloadToFile: PluginCommand<{ name?: string, type: 'json' | 'zip' }>(),

+ 18 - 9
src/mol-plugin/state.ts

@@ -18,10 +18,11 @@ import { produce } from 'immer';
 import { StructureFocusSnapshot } from '../mol-plugin-state/manager/structure/focus';
 import { merge } from 'rxjs';
 import { PluginContext } from './context';
+import { PluginComponent } from '../mol-plugin-state/component';
 
 export { PluginState };
 
-class PluginState {
+class PluginState extends PluginComponent {
     private get animation() { return this.plugin.managers.animation; }
 
     readonly data = State.create(new SO.Root({ }), { runTask: this.plugin.runTask, globalContext: this.plugin });
@@ -38,10 +39,16 @@ class PluginState {
             removed: merge(this.data.events.object.removed, this.behaviors.events.object.removed),
             updated: merge(this.data.events.object.updated, this.behaviors.events.object.updated)
         }
-    } as const
+    } as const;
 
-    getSnapshot(params?: PluginState.GetSnapshotParams): PluginState.Snapshot {
-        const p = { ...PluginState.DefaultGetSnapshotParams, ...params };
+    readonly snapshotParams = this.ev.behavior<PluginState.SnapshotParams>(PluginState.DefaultSnapshotParams);
+
+    setSnapshotParams = (params?: PluginState.SnapshotParams) => {
+        this.snapshotParams.next({ ...PluginState.DefaultSnapshotParams, ...params });
+    }
+
+    getSnapshot(params?: PluginState.SnapshotParams): PluginState.Snapshot {
+        const p = { ...this.snapshotParams.value, ...params };
         return {
             id: UUID.create22(),
             data: p.data ? this.data.getSnapshot() : void 0,
@@ -50,8 +57,8 @@ class PluginState {
             startAnimation: p.startAnimation ? !!p.startAnimation : void 0,
             camera: p.camera ? {
                 current: this.plugin.canvas3d!.camera.getSnapshot(),
-                transitionStyle: p.cameraTranstion.name,
-                transitionDurationInMs: (params && params.cameraTranstion && params.cameraTranstion.name === 'animate') ? params.cameraTranstion.params.durationInMs : undefined
+                transitionStyle: p.cameraTranstion!.name,
+                transitionDurationInMs: p?.cameraTranstion?.name === 'animate' ? p.cameraTranstion.params.durationInMs : void 0
             } : void 0,
             canvas3d: p.canvas3d ? { props: this.plugin.canvas3d?.props } : void 0,
             interactivity: p.interactivity ? { props: this.plugin.managers.interactivity.props } : void 0,
@@ -107,18 +114,20 @@ class PluginState {
     }
 
     dispose() {
+        super.dispose();
         this.data.dispose();
         this.behaviors.dispose();
         this.animation.dispose();
     }
 
     constructor(private plugin: PluginContext) {
+        super();
     }
 }
 
 namespace PluginState {
     export type CameraTransitionStyle = 'instant' | 'animate'
-    export const GetSnapshotParams = {
+    export const SnapshotParams = {
         durationInMs: PD.Numeric(1500, { min: 100, max: 15000, step: 100 }, { label: 'Duration in ms' }),
         data: PD.Boolean(true),
         behavior: PD.Boolean(false),
@@ -134,8 +143,8 @@ namespace PluginState {
             instant: PD.Group({ })
         }, { options: [['animate', 'Animate'], ['instant', 'Instant']] })
     };
-    export type GetSnapshotParams = Partial<PD.Values<typeof GetSnapshotParams>>
-    export const DefaultGetSnapshotParams = PD.getDefaultValues(GetSnapshotParams);
+    export type SnapshotParams = Partial<PD.Values<typeof SnapshotParams>>
+    export const DefaultSnapshotParams = PD.getDefaultValues(SnapshotParams);
 
     export interface Snapshot {
         id: UUID,