From 0c6b80d3703424bf1dcc609825c71e1d8ae0a20f Mon Sep 17 00:00:00 2001 From: David Sehnal <david.sehnal@gmail.com> Date: Thu, 28 Nov 2019 16:58:43 +0100 Subject: [PATCH] mol-plugin: UI improvements (wip) --- src/mol-plugin/skin/base/components/misc.scss | 26 ++++ src/mol-plugin/skin/base/components/temp.scss | 4 - src/mol-plugin/skin/base/icons.scss | 8 ++ src/mol-plugin/skin/base/variables.scss | 2 +- src/mol-plugin/ui/controls/common.tsx | 7 ++ src/mol-plugin/ui/left-panel.tsx | 116 ++++++++++++++++++ src/mol-plugin/ui/plugin.tsx | 42 +------ .../ui/{state.tsx => state/snapshots.tsx} | 25 ++-- src/mol-plugin/ui/state/tree.tsx | 2 +- 9 files changed, 177 insertions(+), 55 deletions(-) create mode 100644 src/mol-plugin/ui/left-panel.tsx rename src/mol-plugin/ui/{state.tsx => state/snapshots.tsx} (93%) diff --git a/src/mol-plugin/skin/base/components/misc.scss b/src/mol-plugin/skin/base/components/misc.scss index dd4eafd4f..7079a75f6 100644 --- a/src/mol-plugin/skin/base/components/misc.scss +++ b/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; + } } \ No newline at end of file diff --git a/src/mol-plugin/skin/base/components/temp.scss b/src/mol-plugin/skin/base/components/temp.scss index 492afe1d9..0c537cae2 100644 --- a/src/mol-plugin/skin/base/components/temp.scss +++ b/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; diff --git a/src/mol-plugin/skin/base/icons.scss b/src/mol-plugin/skin/base/icons.scss index 16dfe6dbd..28f271945 100644 --- a/src/mol-plugin/skin/base/icons.scss +++ b/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"; } \ No newline at end of file diff --git a/src/mol-plugin/skin/base/variables.scss b/src/mol-plugin/skin/base/variables.scss index 7266aa18d..b4f06ebee 100644 --- a/src/mol-plugin/skin/base/variables.scss +++ b/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; diff --git a/src/mol-plugin/ui/controls/common.tsx b/src/mol-plugin/ui/controls/common.tsx index c65637efb..55ed12952 100644 --- a/src/mol-plugin/ui/controls/common.tsx +++ b/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, diff --git a/src/mol-plugin/ui/left-panel.tsx b/src/mol-plugin/ui/left-panel.tsx new file mode 100644 index 000000000..627d7b6de --- /dev/null +++ b/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>} + </> + } +} diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index 72d0aeea3..beaa6e66a 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/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' && diff --git a/src/mol-plugin/ui/state.tsx b/src/mol-plugin/ui/state/snapshots.tsx similarity index 93% rename from src/mol-plugin/ui/state.tsx rename to src/mol-plugin/ui/state/snapshots.tsx index d16a2845a..5a340965f 100644 --- a/src/mol-plugin/ui/state.tsx +++ b/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); diff --git a/src/mol-plugin/ui/state/tree.tsx b/src/mol-plugin/ui/state/tree.tsx index 3fbff2691..8a0e08206 100644 --- a/src/mol-plugin/ui/state/tree.tsx +++ b/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; -- GitLab