Browse Source

mol-plugin: replace & move state snapshots

David Sehnal 6 years ago
parent
commit
d15b902ee6

+ 8 - 0
src/mol-plugin/behavior/static/state.ts

@@ -132,6 +132,14 @@ export function Snapshots(ctx: PluginContext) {
         ctx.state.snapshots.add(entry);
     });
 
+    PluginCommands.State.Snapshots.Replace.subscribe(ctx, ({ id, params }) => {
+        ctx.state.snapshots.replace(id, ctx.state.getSnapshot(params));
+    });
+
+    PluginCommands.State.Snapshots.Move.subscribe(ctx, ({ id, dir }) => {
+        ctx.state.snapshots.move(id, dir);
+    });
+
     PluginCommands.State.Snapshots.Apply.subscribe(ctx, ({ id }) => {
         const snapshot = ctx.state.snapshots.setCurrent(id);
         if (!snapshot) return;

+ 2 - 0
src/mol-plugin/command.ts

@@ -29,6 +29,8 @@ export const PluginCommands = {
 
         Snapshots: {
             Add: PluginCommand<{ name?: string, description?: string, params?: PluginState.GetSnapshotParams }>({ isImmediate: true }),
+            Replace: PluginCommand<{ id: string, params?: PluginState.GetSnapshotParams }>({ isImmediate: true }),
+            Move: PluginCommand<{ id: string, dir: -1 | 1 }>({ isImmediate: true }),
             Remove: PluginCommand<{ id: string }>({ isImmediate: true }),
             Apply: PluginCommand<{ id: string }>({ isImmediate: true }),
             Clear: PluginCommand<{}>({ isImmediate: true }),

+ 9 - 0
src/mol-plugin/skin/base/components/controls-base.scss

@@ -18,6 +18,15 @@
     text-align: center;
 }
 
+.msp-btn-icon-small {
+    height: $row-height;
+    width: 20px;
+    font-size: 85%;
+    line-height: $row-height;
+    padding: 0;
+    text-align: center;
+}
+
 .msp-btn-link {
     .msp-icon {
         font-size: 100%;

+ 6 - 7
src/mol-plugin/skin/base/components/temp.scss

@@ -52,6 +52,12 @@
         > button:first-child {
             border-left: $control-spacing solid color-increase-contrast($default-background, 12%) !important;
         }
+
+        > div {
+            position: absolute;
+            right: 0;
+            top: 0;        
+        }
     }
 
     button {
@@ -60,13 +66,6 @@
     }
 }
 
-.msp-state-list-remove-button {
-    position: absolute;
-    right: 0;
-    top: 0;
-    width: $row-height;
-}
-
 .msp-tree-row {
     position: relative;
     height: $row-height;

+ 20 - 0
src/mol-plugin/skin/base/icons.scss

@@ -144,4 +144,24 @@
 
 .msp-icon-model-first:before {
     content: "\e89c";
+}
+
+.msp-icon-down-thin:before {
+	content: "\e88b";
+}
+
+.msp-icon-up-thin:before {
+	content: "\e88e";
+}
+
+.msp-icon-left-thin:before {
+	content: "\e88c";
+}
+
+.msp-icon-right-thin:before {
+	content: "\e88d";
+}
+
+.msp-icon-switch:before {
+	content: "\e896";
 }

+ 77 - 28
src/mol-plugin/state/snapshots.ts

@@ -4,39 +4,86 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { OrderedMap } from 'immutable';
+import { List } from 'immutable';
 import { UUID } from 'mol-util';
 import { PluginState } from '../state';
 import { PluginComponent } from 'mol-plugin/component';
 
 export { PluginStateSnapshotManager }
 
-class PluginStateSnapshotManager extends PluginComponent<{ current?: UUID | undefined, entries: OrderedMap<string, PluginStateSnapshotManager.Entry> }> {
+class PluginStateSnapshotManager extends PluginComponent<{
+    current?: UUID | undefined,
+    entries: List<PluginStateSnapshotManager.Entry>,
+    entryMap: Map<string, PluginStateSnapshotManager.Entry>
+}> {
     readonly events = {
         changed: this.ev()
     };
 
-    getEntry(id: string) {
-        return this.state.entries.get(id);
+    currentGetSnapshotParams: PluginState.GetSnapshotParams = PluginState.DefaultGetSnapshotParams as any;
+
+    getIndex(e: PluginStateSnapshotManager.Entry) {
+        return this.state.entries.indexOf(e);
+    }
+
+    getEntry(id: string | undefined) {
+        if (!id) return;
+        return this.state.entryMap.get(id);
     }
 
     remove(id: string) {
-        if (!this.state.entries.has(id)) return;
+        const e = this.state.entryMap.get(id);
+        if (!e) return;
+
+        this.state.entryMap.delete(id);
         this.updateState({
             current: this.state.current === id ? void 0 : this.state.current,
-            entries: this.state.entries.delete(id)
+            entries: this.state.entries.delete(this.getIndex(e))
         });
         this.events.changed.next();
     }
 
     add(e: PluginStateSnapshotManager.Entry) {
-        this.updateState({ current: e.snapshot.id, entries: this.state.entries.set(e.snapshot.id, e) });
+        this.state.entryMap.set(e.snapshot.id, e);
+        this.updateState({ current: e.snapshot.id, entries: this.state.entries.push(e) });
+        this.events.changed.next();
+    }
+
+    replace(id: string, snapshot: PluginState.Snapshot) {
+        const old = this.getEntry(id);
+        if (!old) return;
+
+        const idx = this.getIndex(old);
+        // The id changes here!
+        const e = PluginStateSnapshotManager.Entry(snapshot, old.name, old.description);
+        this.state.entryMap.set(snapshot.id, e);
+        this.updateState({ current: e.snapshot.id, entries: this.state.entries.set(idx, e) });
+        this.events.changed.next();
+    }
+
+    move(id: string, dir: -1 | 1) {
+        const len = this.state.entries.size;
+        if (len < 2) return;
+
+        const e = this.getEntry(id);
+        if (!e) return;
+        const from = this.getIndex(e);
+        let to = (from + dir) % len;
+        if (to < 0) to += len;
+        const f = this.state.entries.get(to);
+
+        const entries = this.state.entries.asMutable();
+        entries.set(to, e);
+        entries.set(from, f);
+
+        this.updateState({ current: e.snapshot.id, entries: entries.asImmutable() });
         this.events.changed.next();
     }
 
     clear() {
         if (this.state.entries.size === 0) return;
-        this.updateState({ current: void 0, entries: OrderedMap<string, PluginStateSnapshotManager.Entry>() });
+        this.state.entryMap.clear();
+        this.updateState({ current: void 0, entries: List<PluginStateSnapshotManager.Entry>() });
         this.events.changed.next();
     }
 
@@ -50,35 +97,37 @@ class PluginStateSnapshotManager extends PluginComponent<{ current?: UUID | unde
     }
 
     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;
+        const len = this.state.entries.size;
+        if (!id) {
+            if (len === 0) return void 0;
+            const idx = dir === -1 ? len - 1 : 0;
+            return this.state.entries.get(idx).snapshot.id;
         }
-        if (dir === -1) return prev;
-        return fst;
+
+        const e = this.getEntry(id);
+        if (!e) return;
+        let idx = this.getIndex(e);
+        if (idx < 0) return;
+
+        idx = (idx + dir) % len;
+        if (idx < 0) idx += len;
+
+        return this.state.entries.get(idx).snapshot.id;
     }
 
     setRemoteSnapshot(snapshot: PluginStateSnapshotManager.RemoteSnapshot): PluginState.Snapshot | undefined {
         this.clear();
-        const entries = this.state.entries.withMutations(m => {
-            for (const e of snapshot.entries) {
-                m.set(e.snapshot.id, e);
-            }
-        });
+        const entries = List<PluginStateSnapshotManager.Entry>().asMutable()
+        for (const e of snapshot.entries) {
+            this.state.entryMap.set(e.snapshot.id, e);
+            entries.push(e);
+        }
         const current = snapshot.current
             ? snapshot.current
             : snapshot.entries.length > 0
             ? snapshot.entries[0].snapshot.id
             : void 0;
-        this.updateState({ current, entries });
+        this.updateState({ current, entries: entries.asImmutable() });
         this.events.changed.next();
         if (!current) return;
         const ret = this.getEntry(current);
@@ -97,7 +146,7 @@ class PluginStateSnapshotManager extends PluginComponent<{ current?: UUID | unde
     }
 
     constructor() {
-        super({ current: void 0, entries: OrderedMap<string, PluginStateSnapshotManager.Entry>() });
+        super({ current: void 0, entries: List(), entryMap: new Map() });
     }
 }
 

+ 11 - 3
src/mol-plugin/ui/controls/common.tsx

@@ -81,10 +81,18 @@ export class NumericInput extends React.PureComponent<{
     }
 }
 
-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`;
+export function IconButton(props: {
+    icon: string,
+    isSmall?: boolean,
+    onClick: (e: React.MouseEvent<HTMLButtonElement>) => void,
+    title?: string,
+    toggleState?: boolean,
+    disabled?: boolean,
+    'data-id'?: string
+}) {
+    let className = `msp-btn msp-btn-link msp-btn-icon${props.isSmall ? '-small' : ''}`;
     if (typeof props.toggleState !== 'undefined') className += ` msp-btn-link-toggle-${props.toggleState ? 'on' : 'off'}`
-    return <button className={className} onClick={props.onClick} title={props.title} disabled={props.disabled}>
+    return <button className={className} onClick={props.onClick} title={props.title} disabled={props.disabled} data-id={props['data-id']}>
         <span className={`msp-icon msp-icon-${props.icon}`}/>
     </button>;
 }

+ 50 - 20
src/mol-plugin/ui/state.tsx

@@ -13,24 +13,25 @@ import { ParameterControls } from './controls/parameters';
 import { ParamDefinition as PD} from 'mol-util/param-definition';
 import { PluginState } from 'mol-plugin/state';
 import { urlCombine } from 'mol-util/url';
+import { IconButton } from './controls/common';
 
 export class StateSnapshots extends PluginUIComponent<{ }> {
 
     render() {
         return <div>
             <div className='msp-section-header'>State</div>
-            <StateSnapshotControls />
+            <LocalStateSnapshots />
             <LocalStateSnapshotList />
             <RemoteStateSnapshots />
         </div>;
     }
 }
 
-class StateSnapshotControls extends PluginUIComponent<
+class LocalStateSnapshots extends PluginUIComponent<
     { },
-    { params: PD.Values<typeof StateSnapshotControls.Params> }> {
+    { params: PD.Values<typeof LocalStateSnapshots.Params> }> {
 
-    state = { params: PD.getDefaultValues(StateSnapshotControls.Params) };
+    state = { params: PD.getDefaultValues(LocalStateSnapshots.Params) };
 
     static Params = {
         name: PD.Text(),
@@ -41,7 +42,11 @@ class StateSnapshotControls extends PluginUIComponent<
     };
 
     add = () => {
-        PluginCommands.State.Snapshots.Add.dispatch(this.plugin, { name: this.state.params.name, description: this.state.params.options.description });
+        PluginCommands.State.Snapshots.Add.dispatch(this.plugin, {
+            name: this.state.params.name,
+            description: this.state.params.options.description,
+            params: this.state.params.options
+        });
         this.setState({
             params: {
                 name: '',
@@ -81,8 +86,10 @@ class StateSnapshotControls extends PluginUIComponent<
                 </div>
             </div>
 
-            <ParameterControls params={StateSnapshotControls.Params} values={this.state.params} onEnter={this.add} onChange={p => {
-                this.setState({ params: { ...this.state.params, [p.name]: p.value } } as any);
+            <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.state.snapshots.currentGetSnapshotParams = params.options;
             }}/>
 
             <div className='msp-btn-row-group'>
@@ -99,26 +106,49 @@ class LocalStateSnapshotList extends PluginUIComponent<{ }, { }> {
         this.subscribe(this.plugin.events.state.snapshots.changed, () => this.forceUpdate());
     }
 
-    apply(id: string) {
-        return () => PluginCommands.State.Snapshots.Apply.dispatch(this.plugin, { id });
+    apply = (e: React.MouseEvent<HTMLElement>) => {
+        const id = e.currentTarget.getAttribute('data-id');
+        if (!id) return;
+        PluginCommands.State.Snapshots.Apply.dispatch(this.plugin, { id });
     }
 
-    remove(id: string) {
-        return () => {
-            PluginCommands.State.Snapshots.Remove.dispatch(this.plugin, { id });
-        }
+    remove = (e: React.MouseEvent<HTMLElement>) => {
+        const id = e.currentTarget.getAttribute('data-id');
+        if (!id) return;
+        PluginCommands.State.Snapshots.Remove.dispatch(this.plugin, { id });
+    }
+
+    moveUp = (e: React.MouseEvent<HTMLElement>) => {
+        const id = e.currentTarget.getAttribute('data-id');
+        if (!id) return;
+        PluginCommands.State.Snapshots.Move.dispatch(this.plugin, { id, dir: -1 });
+    }
+
+    moveDown = (e: React.MouseEvent<HTMLElement>) => {
+        const id = e.currentTarget.getAttribute('data-id');
+        if (!id) return;
+        PluginCommands.State.Snapshots.Move.dispatch(this.plugin, { id, dir: 1 });
+    }
+
+    replace = (e: React.MouseEvent<HTMLElement>) => {
+        const id = e.currentTarget.getAttribute('data-id');
+        if (!id) return;
+        PluginCommands.State.Snapshots.Replace.dispatch(this.plugin, { id, params: this.plugin.state.snapshots.currentGetSnapshotParams });
     }
 
     render() {
         const current = this.plugin.state.snapshots.state.current;
         return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
             {this.plugin.state.snapshots.state.entries.valueSeq().map(e =><li key={e!.snapshot.id}>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.apply(e!.snapshot.id)}>
+                <button data-id={e!.snapshot.id} className='msp-btn msp-btn-block msp-form-control' onClick={this.apply}>
                     <span style={{ fontWeight: e!.snapshot.id === current ? 'bold' : void 0}}>{e!.name || new Date(e!.timestamp).toLocaleString()}</span> <small>{e!.description}</small>
                 </button>
-                <button onClick={this.remove(e!.snapshot.id)} className='msp-btn msp-btn-link msp-state-list-remove-button'>
-                    <span className='msp-icon msp-icon-remove' />
-                </button>
+                <div>
+                    <IconButton data-id={e!.snapshot.id} icon='up-thin' title='Move Up' onClick={this.moveUp} isSmall={true} />
+                    <IconButton data-id={e!.snapshot.id} icon='down-thin' title='Move Down' onClick={this.moveDown} isSmall={true} />
+                    <IconButton data-id={e!.snapshot.id} icon='switch' title='Replace' onClick={this.replace} isSmall={true} />
+                    <IconButton data-id={e!.snapshot.id} icon='remove' title='Remove' onClick={this.remove} isSmall={true} />
+                </div>
             </li>)}
         </ul>;
     }
@@ -254,9 +284,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>
-                <button data-id={e!.id} onClick={this.props.remove} className='msp-btn msp-btn-link msp-state-list-remove-button' disabled={this.props.isBusy}>
-                    <span className='msp-icon msp-icon-remove' />
-                </button>
+                <div>
+                    <IconButton data-id={e!.id} icon='remove' title='Remove' onClick={this.props.remove} disabled={this.props.isBusy} />
+                </div>
             </li>)}
         </ul>;
     }