Browse Source

mol-plugin: ui

David Sehnal 6 years ago
parent
commit
fca68b683f

+ 51 - 0
src/mol-plugin/skin/base/components/temp.scss

@@ -53,4 +53,55 @@
     right: 0;
     top: 0;
     width: $row-height;
+}
+
+.msp-tree-row {
+    position: relative;
+    height: $row-height;
+    line-height: $row-height;
+    background: color-lower-contrast($control-background, 4%);
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    margin-bottom: 1px;
+    padding-left: $row-height;
+    padding-right: 2 * $row-height + $control-spacing;
+    border-bottom-left-radius: $control-spacing;
+
+    &-current {
+        background: $control-background
+    }
+}
+
+.msp-tree-remove-button {
+    position: absolute;
+    right: $row-height;
+    top: 0;
+    width: $row-height;
+    font-size: 80%;
+    color: color-lower-contrast($font-color, 24%);
+}
+
+.msp-tree-toggle-exp-button {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: $row-height;
+    color: color-lower-contrast($font-color, 24%);
+}
+
+.msp-tree-visibility {
+    position: absolute;
+    right: 0;
+    top: 0;
+    width: $row-height;
+    font-size: 80%;
+
+    &-hidden {
+        color: color-lower-contrast($font-color, 36%);
+    }
+}
+
+.msp-tree-children {
+    margin-left: $control-spacing;
 }

+ 4 - 0
src/mol-plugin/skin/base/components/transformer.scss

@@ -84,4 +84,8 @@
     left: $control-label-width + $control-spacing;
     right: 0;
     top: 0;
+}
+
+.msp-data-beh {
+    margin: $control-spacing 0 !important;
 }

+ 1 - 1
src/mol-plugin/ui/camera.tsx

@@ -67,7 +67,7 @@ class CameraSnapshotList extends PluginComponent<{ }, { }> {
         return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
             {this.plugin.state.cameraSnapshots.entries.valueSeq().map(e =><li key={e!.id}>
                 <button className='msp-btn msp-btn-block msp-form-control' onClick={this.apply(e!.id)}>{e!.name || e!.timestamp} <small>{e!.description}</small></button>
-                <button onClick={this.remove(e!.id)} style={{ float: 'right' }} className='msp-btn msp-btn-link msp-state-list-remove-button'>
+                <button onClick={this.remove(e!.id)} className='msp-btn msp-btn-link msp-state-list-remove-button'>
                     <span className='msp-icon msp-icon-remove' />
                 </button>
             </li>)}

+ 6 - 7
src/mol-plugin/ui/controls.tsx

@@ -20,19 +20,18 @@ export class Controls extends PluginComponent<{ }, { }> {
 export class TrajectoryControls extends PluginComponent {
     render() {
         return <div>
-            <b>Trajectory: </b>
-            <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, {
+            <button className='msp-btn msp-btn-link' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, {
                 state: this.plugin.state.dataState,
                 action: UpdateTrajectory.create({ action: 'advance', by: -1 })
-            })}>&lt;&lt;</button>
-            <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, {
+            })}></button>
+            <button className='msp-btn msp-btn-link' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, {
                 state: this.plugin.state.dataState,
                 action: UpdateTrajectory.create({ action: 'reset' })
-            })}>Reset</button>
-            <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, {
+            })}></button>
+            <button className='msp-btn msp-btn-link' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, {
                 state: this.plugin.state.dataState,
                 action: UpdateTrajectory.create({ action: 'advance', by: +1 })
-            })}>&gt;&gt;</button><br />
+            })}></button><br />
         </div>
     }
 }

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

@@ -59,7 +59,7 @@ export class ViewportWrapper extends PluginComponent {
                 <TrajectoryControls />
             </div>
             <ViewportControls />
-            <div style={{ position: 'absolute', left: '10px', bottom: '10px', color: 'white' }}>
+            <div style={{ position: 'absolute', left: '10px', bottom: '10px' }}>
                 <BackgroundTaskProgress />
             </div>
         </>;
@@ -79,8 +79,10 @@ export class State extends PluginComponent {
     render() {
         const kind = this.plugin.state.behavior.kind.value;
         return <>
-            <button onClick={() => this.set('data')} style={{ fontWeight: kind === 'data' ? 'bold' : 'normal'}}>Data</button>
-            <button onClick={() => this.set('behavior')} style={{ fontWeight: kind === 'behavior' ? 'bold' : 'normal'}}>Behavior</button>
+            <div className='msp-btn-row-group msp-data-beh'>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('data')} style={{ fontWeight: kind === 'data' ? 'bold' : 'normal'}}>Data</button>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('behavior')} style={{ fontWeight: kind === 'behavior' ? 'bold' : 'normal'}}>Behavior</button>
+            </div>
             <StateTree state={kind === 'data' ? this.plugin.state.dataState : this.plugin.state.behaviorState} />
         </>
     }

+ 30 - 19
src/mol-plugin/ui/state-tree.tsx

@@ -58,24 +58,15 @@ class StateTreeNode extends PluginComponent<{ nodeRef: string, state: State }, {
         });
     }
 
-    toggleExpanded = (e: React.MouseEvent<HTMLElement>) => {
-        e.preventDefault();
-        PluginCommands.State.ToggleExpanded.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
-    }
-
     render() {
         const cellState = this.cellState;
 
-        const expander = <>
-            [<a href='#' onClick={this.toggleExpanded}>{cellState.isCollapsed ? '+' : '-'}</a>]
-        </>;
-
         const children = this.props.state.tree.children.get(this.props.nodeRef);
         return <div>
-            {children.size === 0 ? void 0 : expander} <StateTreeNodeLabel nodeRef={this.props.nodeRef} state={this.props.state} />
+            <StateTreeNodeLabel nodeRef={this.props.nodeRef} state={this.props.state} />
             {children.size === 0
                 ? void 0
-                : <div style={{ marginLeft: '7px', paddingLeft: '3px', borderLeft: '1px solid #999', display: cellState.isCollapsed ? 'none' : 'block' }}>
+                : <div className='msp-tree-children' style={{ display: cellState.isCollapsed ? 'none' : 'block' }}>
                     {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} />)}
                 </div>
             }
@@ -103,7 +94,7 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
                 }
             } else if (isCurrent) {
                 isCurrent = false;
-                // have to check the node wasn't remove
+                // have to check the node wasn't removed
                 if (e.state.transforms.has(this.props.nodeRef)) this.forceUpdate();
             }
         });
@@ -122,6 +113,13 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
     toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
         PluginCommands.State.ToggleVisibility.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        e.currentTarget.blur();
+    }
+
+    toggleExpanded = (e: React.MouseEvent<HTMLElement>) => {
+        e.preventDefault();
+        PluginCommands.State.ToggleExpanded.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        e.currentTarget.blur();
     }
 
     render() {
@@ -130,22 +128,35 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
 
         const isCurrent = this.is(this.props.state.behaviors.currentObject.value);
 
-        const remove = <>[<a href='#' onClick={this.remove}>X</a>]</>
 
         let label: any;
         if (cell.status !== 'ok' || !cell.obj) {
             const name = (n.transformer.definition.display && n.transformer.definition.display.name) || n.transformer.definition.name;
-            label = <><b>{cell.status}</b> <a href='#' onClick={this.setCurrent}>{name}</a>: <i>{cell.errorText}</i></>;
+            const title = `${cell.errorText}`
+            label = <><b>{cell.status}</b> <a title={title} href='#' onClick={this.setCurrent}>{name}</a>: <i>{cell.errorText}</i></>;
         } else {
             const obj = cell.obj as PluginStateObject.Any;
-            label = <><a href='#' onClick={this.setCurrent}>{obj.label}</a> {obj.description ? <small>{obj.description}</small> : void 0}</>;
+            const title = `${obj.label} ${obj.description ? obj.description : ''}`
+            label = <><a title={title} href='#' onClick={this.setCurrent}>{obj.label}</a> {obj.description ? <small>{obj.description}</small> : void 0}</>;
         }
 
+        const children = this.props.state.tree.children.get(this.props.nodeRef);
         const cellState = this.props.state.cellStates.get(this.props.nodeRef);
-        const visibility = <>[<a href='#' onClick={this.toggleVisible}>{cellState.isHidden ? 'H' : 'V'}</a>]</>;
 
-        return <>
-            {remove}{visibility} {isCurrent ? <b>{label}</b> : label}
-        </>;
+        const remove = <button onClick={this.remove} className='msp-btn msp-btn-link msp-tree-remove-button'>
+            <span className='msp-icon msp-icon-remove' />
+        </button>;
+
+        const visibility = <button onClick={this.toggleVisible} className={`msp-btn msp-btn-link msp-tree-visibility${cellState.isHidden ? ' msp-tree-visibility-hidden' : ''}`}>
+            <span className='msp-icon msp-icon-visual-visibility' />
+        </button>;
+
+        return <div className={`msp-tree-row${isCurrent ? ' msp-tree-row-current' : ''}`}>
+            {isCurrent ? <b>{label}</b> : label}
+            {children.size > 0 &&  <button onClick={this.toggleExpanded} className='msp-btn msp-btn-link msp-tree-toggle-exp-button'>
+                <span className={`msp-icon msp-icon-${cellState.isCollapsed ? 'expand' : 'collapse'}`} />
+            </button>}
+            {remove}{visibility}
+        </div>
     }
 }

+ 31 - 5
src/mol-plugin/ui/state.tsx

@@ -12,9 +12,10 @@ import { List } from 'immutable';
 import { LogEntry } from 'mol-util/log-entry';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ParameterControls } from './controls/parameters';
+import { Subject } from 'rxjs';
 
 export class StateSnapshots extends PluginComponent<{ }, { serverUrl: string }> {
-    state = { serverUrl: 'http://webchem.ncbr.muni.cz/molstar-state' }
+    state = { serverUrl: 'https://webchem.ncbr.muni.cz/molstar-state' }
 
     updateServerUrl = (serverUrl: string) => { this.setState({ serverUrl }) };
 
@@ -28,13 +29,16 @@ export class StateSnapshots extends PluginComponent<{ }, { serverUrl: string }>
     }
 }
 
+// TODO: this is not nice: device some custom event system.
+const UploadedEvent = new Subject();
+
 class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverChanged: (url: string) => void }, { name: string, description: string, serverUrl: string, isUploading: boolean }> {
     state = { name: '', description: '', serverUrl: this.props.serverUrl, isUploading: false };
 
     static Params = {
         name: PD.Text(),
         description: PD.Text(),
-        serverUrl: PD.Text('http://webchem.ncbr.muni.cz/molstar-state')
+        serverUrl: PD.Text()
     }
 
     add = () => {
@@ -54,6 +58,8 @@ class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverC
         this.setState({ isUploading: true });
         await PluginCommands.State.Snapshots.Upload.dispatch(this.plugin, { name: this.state.name, description: this.state.description, serverUrl: this.state.serverUrl });
         this.setState({ isUploading: false });
+        this.plugin.log(LogEntry.message('Snapshot uploaded.'));
+        UploadedEvent.next();
     }
 
     render() {
@@ -91,7 +97,7 @@ class LocalStateSnapshotList extends PluginComponent<{ }, { }> {
         return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
             {this.plugin.state.snapshots.entries.valueSeq().map(e =><li key={e!.id}>
                 <button className='msp-btn msp-btn-block msp-form-control' onClick={this.apply(e!.id)}>{e!.name || e!.timestamp} <small>{e!.description}</small></button>
-                <button onClick={this.remove(e!.id)} style={{ float: 'right' }} className='msp-btn msp-btn-link msp-state-list-remove-button'>
+                <button onClick={this.remove(e!.id)} className='msp-btn msp-btn-link msp-state-list-remove-button'>
                     <span className='msp-icon msp-icon-remove' />
                 </button>
             </li>)}
@@ -99,13 +105,14 @@ class LocalStateSnapshotList extends PluginComponent<{ }, { }> {
     }
 }
 
-type RemoteEntry = { url: string, timestamp: number, id: string, name: string, description: string }
+type RemoteEntry = { url: string, removeUrl: string, timestamp: number, id: string, name: string, description: string }
 class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { entries: List<RemoteEntry>, isFetching: boolean }> {
     state = { entries: List<RemoteEntry>(), isFetching: false };
 
     componentDidMount() {
         this.subscribe(this.plugin.events.state.snapshots.changed, () => this.forceUpdate());
         this.refresh();
+        this.subscribe(UploadedEvent, this.refresh);
     }
 
     refresh = async () => {
@@ -113,7 +120,13 @@ class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { e
             this.setState({ isFetching: true });
             const req = await fetch(`${this.props.serverUrl}/list`);
             const json: RemoteEntry[] = await req.json();
-            this.setState({ entries: List<RemoteEntry>(json.map((e: RemoteEntry) => ({ ...e, url: `${this.props.serverUrl}/get/${e.id}` }))), isFetching: false })
+            this.setState({
+                entries: List<RemoteEntry>(json.map((e: RemoteEntry) => ({
+                    ...e,
+                    url: `${this.props.serverUrl}/get/${e.id}`,
+                    removeUrl: `${this.props.serverUrl}/remove/${e.id}`
+                }))),
+                isFetching: false })
         } catch (e) {
             this.plugin.log(LogEntry.error('Fetching Remote Snapshots: ' + e));
             this.setState({ entries: List<RemoteEntry>(), isFetching: false })
@@ -124,6 +137,16 @@ class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { e
         return () => PluginCommands.State.Snapshots.Fetch.dispatch(this.plugin, { url });
     }
 
+    remove(url: string) {
+        return async () => {
+            this.setState({ entries: List() });
+            try {
+                await fetch(url);
+            } catch { }
+            this.refresh();
+        }
+    }
+
     render() {
         return <div>
             <button title='Click to Refresh' style={{fontWeight: 'bold'}} className='msp-btn msp-btn-block msp-form-control' onClick={this.refresh} disabled={this.state.isFetching}>↻ Remote Snapshots</button>
@@ -131,6 +154,9 @@ class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { e
             <ul style={{ listStyle: 'none' }} className='msp-state-list'>
                 {this.state.entries.valueSeq().map(e =><li key={e!.id}>
                     <button className='msp-btn msp-btn-block msp-form-control' onClick={this.fetch(e!.url)}>{e!.name || e!.timestamp} <small>{e!.description}</small></button>
+                    <button onClick={this.remove(e!.removeUrl)} className='msp-btn msp-btn-link msp-state-list-remove-button'>
+                        <span className='msp-icon msp-icon-remove' />
+                    </button>
                 </li>)}
             </ul>
         </div>;

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

@@ -22,7 +22,7 @@ export class ViewportControls extends PluginComponent {
 
     render() {
         return <div style={{ position: 'absolute', right: '10px', top: '10px', height: '100%', color: 'white' }}>
-            <button onClick={this.resetCamera}>Reset Camera</button>
+            <button className='msp-btn msp-btn-link' onClick={this.resetCamera}>↻ Camera</button>
         </div>
     }
 }