Ver código fonte

mol-plugin: UI improvements (wip)

David Sehnal 5 anos atrás
pai
commit
0c6b80d370

+ 26 - 0
src/mol-plugin/skin/base/components/misc.scss

@@ -131,4 +131,30 @@
     font-weight: 500;
     background: $default-background; 
     color: $font-color;
+}
+
+.msp-left-panel-controls-buttons {    
+    position: absolute;
+    width: $row-height;
+    top: 0;
+    bottom: 0;
+    padding-top: $control-spacing;
+
+    background: $default-background;
+
+    // .msp-btn-link-toggle-on {
+    //     border-left: 1px solid $font-color;
+    // }
+}
+
+.msp-left-panel-controls-buttons-bottom {
+    position: absolute;
+    bottom: 0;
+
+}
+
+.msp-left-panel-controls {
+    .msp-scrollable-container {
+        left: $row-height + 1px;
+    }
 }

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

@@ -1,7 +1,3 @@
-.msp-right-controls {
-    padding-top: $control-spacing;
-}
-
 .msp-section-header {
     height: $row-height;
     line-height: $row-height;

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

@@ -227,4 +227,12 @@
 
 .msp-icon-flow-tree:before {
 	content: "\e8da";
+}
+
+.msp-icon-home:before {
+	content: "\e821";
+}
+
+.msp-icon-address:before {
+	content: "\e841";
 }

+ 1 - 1
src/mol-plugin/skin/base/variables.scss

@@ -13,7 +13,7 @@ $slider-border-radius-base: 6px;
 $expanded-top-height:    100px;
 $expanded-bottom-height: 3 * $row-height + 2;
 $expanded-right-width:   300px;
-$expanded-left-width:    300px;
+$expanded-left-width:    330px;
 
 $expanded-portrait-bottom-height: 10 * ($row-height + 1) + 3 * $control-spacing + 1;
 $expanded-portrait-top-height:    3 * $row-height + 1;

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

@@ -298,6 +298,13 @@ export function Options(options: [string, string][]) {
     return options.map(([value, label]) => <option key={value} value={value}>{label}</option>)
 }
 
+export function SectionHeader(props: { icon?: string, title: string, desc?: string}) {
+    return <div className='msp-section-header'>
+        {props.icon && <Icon name={props.icon} />}
+        {props.title} <small>{props.desc}</small>
+    </div>
+}
+
 // export const ToggleButton = (props: {
 //     onChange: (v: boolean) => void,
 //     value: boolean,

+ 116 - 0
src/mol-plugin/ui/left-panel.tsx

@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as React from 'react';
+import { PluginUIComponent } from './base';
+import { StateTree } from './state/tree';
+import { IconButton, SectionHeader, ControlGroup } from './controls/common';
+import { StateObjectActions } from './state/actions';
+import { StateTransform } from '../../mol-state';
+import { PluginCommands } from '../command';
+import { ParameterControls } from './controls/parameters';
+import { Canvas3DParams } from '../../mol-canvas3d/canvas3d';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { StateSnapshots } from './state/snapshots';
+
+type TabName = 'root' | 'data' | 'behavior' | 'states' | 'viewport-settings'
+
+export class LeftPanelControls extends PluginUIComponent<{}, { currentTab: TabName }> {
+    state = { currentTab: 'data' as TabName };
+
+    componentDidMount() {
+        // this.subscribe(this.plugin.state.behavior.kind, () => this.forceUpdate());
+    }
+
+    set(kind: TabName) {
+        switch (kind) {
+            case 'data': this.plugin.state.setKind('data'); break;
+            case 'behavior': this.plugin.state.setKind('behavior'); break;
+        }
+        this.setState({ currentTab: kind });
+    }
+
+    tabs: { [K in TabName]: JSX.Element } = {
+        'root': <>
+            <SectionHeader icon='home' title='Home' />
+            <StateObjectActions state={this.plugin.state.dataState} nodeRef={StateTransform.RootRef} hideHeader={true} initiallyCollapsed={true} alwaysExpandFirst={true} />
+        </>,
+        'data': <>
+        <SectionHeader icon='flow-tree' title='State Tree' />
+            <StateTree state={this.plugin.state.dataState} />
+        </>,
+        'states': <StateSnapshots />,
+        'behavior': <>
+            <SectionHeader icon='address' title='Plugin Behavior' />
+            <StateTree state={this.plugin.state.behaviorState} />
+        </>,
+        'viewport-settings': <>
+            <SectionHeader icon='settings' title='Plugin Settings' />
+            <FullSettings />
+        </>
+    }
+
+    render() {
+        const tab = this.state.currentTab;
+
+        return <div className='msp-left-panel-controls'>
+            <div className='msp-left-panel-controls-buttons'>
+                <IconButton icon='home' toggleState={tab === 'root'} onClick={() => this.set('root')} title='Home' />
+                <IconButton icon='flow-tree' toggleState={tab === 'data'} onClick={() => this.set('data')} title='State Tree' />
+                <IconButton icon='floppy' toggleState={tab === 'states'} onClick={() => this.set('states')} title='Plugin State' />
+                <div className='msp-left-panel-controls-buttons-bottom'>
+                    <IconButton icon='address' toggleState={tab === 'behavior'} onClick={() => this.set('behavior')} title='Plugin Behavior' />
+                    <IconButton icon='settings' toggleState={tab === 'viewport-settings'} onClick={() => this.set('viewport-settings')} title='Viewport Settings' />
+                </div>
+            </div>
+            <div className='msp-scrollable-container'>
+                {this.tabs[tab]}
+            </div>
+        </div>;
+    }
+}
+
+class FullSettings extends PluginUIComponent {
+    private setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => {
+        PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { [p.name]: p.value } });
+    }
+
+    setLayout = (p: { param: PD.Base<any>, name: string, value: any }) => {
+        PluginCommands.Layout.Update.dispatch(this.plugin, { state: { [p.name]: p.value } });
+    }
+
+    setInteractivityProps = (p: { param: PD.Base<any>, name: string, value: any }) => {
+        PluginCommands.Interactivity.SetProps.dispatch(this.plugin, { props: { [p.name]: p.value } });
+    }
+
+    screenshot = () => {
+        this.plugin.helpers.viewportScreenshot?.download();
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
+        this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
+        this.subscribe(this.plugin.events.interactivity.propsUpdated, () => this.forceUpdate());
+    }
+
+    icon(name: string, onClick: (e: React.MouseEvent<HTMLButtonElement>) => void, title: string, isOn = true) {
+        return <IconButton icon={name} toggleState={isOn} onClick={onClick} title={title} />;
+    }
+
+    render() {
+        return <>
+            {/* <ControlGroup header='Layout' initialExpanded={true}>
+                <ParameterControls params={PluginLayoutStateParams} values={this.plugin.layout.state} onChange={this.setLayout} />
+            </ControlGroup>
+            <ControlGroup header='Interactivity' initialExpanded={true}>
+                <ParameterControls params={Interactivity.Params} values={this.plugin.interactivity.props} onChange={this.setInteractivityProps} />
+            </ControlGroup> */}
+            {this.plugin.canvas3d && <ControlGroup header='Viewport' initialExpanded={true}>
+                <ParameterControls params={Canvas3DParams} values={this.plugin.canvas3d.props} onChange={this.setSettings} />
+            </ControlGroup>}
+        </>
+    }
+}

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

@@ -6,16 +6,13 @@
  */
 
 import { List } from 'immutable';
-import { PluginState } from '../../mol-plugin/state';
 import { formatTime } from '../../mol-util';
 import { LogEntry } from '../../mol-util/log-entry';
 import * as React from 'react';
 import { PluginContext } from '../context';
 import { PluginReactContext, PluginUIComponent } from './base';
 import { LociLabels, TrajectoryViewportControls, StateSnapshotViewportControls, AnimationViewportControls, StructureToolsWrapper } from './controls';
-import { StateSnapshots } from './state';
 import { StateObjectActionSelect } from './state/actions';
-import { StateTree } from './state/tree';
 import { BackgroundTaskProgress } from './task';
 import { Viewport, ViewportControls } from './viewport';
 import { StateTransform } from '../../mol-state';
@@ -23,7 +20,8 @@ import { UpdateTransformControl } from './state/update-transform';
 import { SequenceView } from './sequence';
 import { Toasts } from './toast';
 import { ImageControls } from './image';
-import { Icon } from './controls/common';
+import { SectionHeader } from './controls/common';
+import { LeftPanelControls } from './left-panel';
 
 export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
     region(kind: 'left' | 'right' | 'bottom' | 'main', element: JSX.Element) {
@@ -110,7 +108,7 @@ class Layout extends PluginUIComponent {
                 <div className={this.layoutVisibilityClassName}>
                     {this.region('main', viewport)}
                     {layout.showControls && controls.top !== 'none' && this.region('top', controls.top || SequenceView)}
-                    {layout.showControls && controls.left !== 'none' && this.region('left', controls.left || State)}
+                    {layout.showControls && controls.left !== 'none' && this.region('left', controls.left || LeftPanelControls)}
                     {layout.showControls && controls.right !== 'none' && this.region('right', controls.right || ControlsWrapper)}
                     {layout.showControls && controls.bottom !== 'none' && this.region('bottom', controls.bottom || Log)}
                 </div>
@@ -121,13 +119,12 @@ class Layout extends PluginUIComponent {
 
 export class ControlsWrapper extends PluginUIComponent {
     render() {
-        return <div className='msp-scrollable-container msp-right-controls'>
+        return <div className='msp-scrollable-container'>
             <CurrentObject />
             {/* <AnimationControlsWrapper /> */}
             {/* <CameraSnapshots /> */}
             <StructureToolsWrapper />
             <ImageControls />
-            <StateSnapshots />
         </div>;
     }
 }
@@ -151,32 +148,6 @@ export class ViewportWrapper extends PluginUIComponent {
     }
 }
 
-export class State extends PluginUIComponent {
-    componentDidMount() {
-        this.subscribe(this.plugin.state.behavior.kind, () => this.forceUpdate());
-    }
-
-    set(kind: PluginState.Kind) {
-        // TODO: do command for this?
-        this.plugin.state.setKind(kind);
-    }
-
-    render() {
-        const kind = this.plugin.state.behavior.kind.value;
-        return <div className='msp-scrollable-container'>
-            <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' }}>
-                    <span className='msp-icon msp-icon-database' /> Data
-                </button>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('behavior')} style={{ fontWeight: kind === 'behavior' ? 'bold' : 'normal' }}>
-                    <span className='msp-icon msp-icon-tools' /> Behavior
-                </button>
-            </div>
-            <StateTree state={kind === 'data' ? this.plugin.state.dataState : this.plugin.state.behaviorState} />
-        </div>
-    }
-}
-
 export class Log extends PluginUIComponent<{}, { entries: List<LogEntry> }> {
     private wrapper = React.createRef<HTMLDivElement>();
 
@@ -248,10 +219,7 @@ export class CurrentObject extends PluginUIComponent {
 
         return <>
             {(cell.status === 'ok' || cell.status === 'error') && <>
-                <div className='msp-section-header' style={{ margin: '0 0 -1px 0' }}>
-                    <Icon name='flow-cascade' />
-                    {`${cell.obj?.label || transform.transformer.definition.display.name}`} <small>{transform.transformer.definition.display.name}</small>
-                </div>
+                <SectionHeader icon='flow-cascade' title={`${cell.obj?.label || transform.transformer.definition.display.name}`} desc={transform.transformer.definition.display.name} />
                 <UpdateTransformControl state={current.state} transform={transform} customHeader='none' />
             </> }
             {cell.status === 'ok' &&

+ 13 - 12
src/mol-plugin/ui/state.tsx → src/mol-plugin/ui/state/snapshots.tsx

@@ -4,17 +4,17 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { PluginCommands } from '../../mol-plugin/command';
+import { PluginCommands } from '../../command';
 import * as React from 'react';
-import { PluginUIComponent, PurePluginUIComponent } from './base';
-import { shallowEqual } from '../../mol-util';
+import { PluginUIComponent, PurePluginUIComponent } from '../base';
+import { shallowEqual } from '../../../mol-util';
 import { OrderedMap } from 'immutable';
-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, Icon } from './controls/common';
-import { formatTimespan } from '../../mol-util/now';
+import { ParameterControls } from '../controls/parameters';
+import { ParamDefinition as PD} from '../../../mol-util/param-definition';
+import { PluginState } from '../../state';
+import { urlCombine } from '../../../mol-util/url';
+import { IconButton, Icon, SectionHeader } from '../controls/common';
+import { formatTimespan } from '../../../mol-util/now';
 
 export class StateSnapshots extends PluginUIComponent<{ }> {
     downloadToFile = () => {
@@ -29,7 +29,7 @@ export class StateSnapshots extends PluginUIComponent<{ }> {
 
     render() {
         return <div>
-            <div className='msp-section-header'><Icon name='code' /> State</div>
+            <SectionHeader icon='floppy' title='Plugin State' />
             <LocalStateSnapshots />
             <LocalStateSnapshotList />
             <RemoteStateSnapshots />
@@ -93,7 +93,8 @@ class LocalStateSnapshots extends PluginUIComponent<
             }}/>
 
             <div className='msp-btn-row-group'>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.add}><Icon name='floppy' /> Save</button>
+                {/* <button className='msp-btn msp-btn-block msp-form-control' onClick={this.add}><Icon name='floppy' /> Save</button> */}
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.add}>Save</button>
                 {/* <button className='msp-btn msp-btn-block msp-form-control' onClick={this.upload} disabled={this.state.isUploading}>Upload</button> */}
                 <button className='msp-btn msp-btn-block msp-form-control' onClick={this.clear}>Clear</button>
             </div>
@@ -258,7 +259,7 @@ class RemoteStateSnapshots extends PluginUIComponent<
 
     render() {
         return <div>
-            <div className='msp-section-header'><Icon name='code' /> Remote State</div>
+            <SectionHeader icon='floppy' title='Remote States' />
 
             <ParameterControls params={RemoteStateSnapshots.Params} values={this.state.params} onEnter={this.upload} onChange={p => {
                 this.setState({ params: { ...this.state.params, [p.name]: p.value } } as any);

+ 1 - 1
src/mol-plugin/ui/state/tree.tsx

@@ -89,7 +89,7 @@ class StateTreeNode extends PluginUIComponent<{ cell: StateObjectCell, depth: nu
         }
 
         const cellState = cell.state;
-        const showLabel = cell.status !== 'ok' || !cell.state.isGhost;
+        const showLabel = (cell.transform.ref !== StateTransform.RootRef) && (cell.status !== 'ok' || !cell.state.isGhost);
         const children = cell.parent.tree.children.get(this.ref);
         const newDepth = showLabel ? this.props.depth + 1 : this.props.depth;