diff --git a/src/mol-plugin/skin/base/components/misc.scss b/src/mol-plugin/skin/base/components/misc.scss index dd4eafd4f36f7a21844f04d0929426874009bc69..7079a75f6d0e1caa4cb5be02a7abdc762e05f625 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 492afe1d9f313da6ef49beb5c31a55ed37030491..0c537cae2955be0d3cf227e5f0193014da9fc161 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 16dfe6dbd73178fb4f12cf2e4c894b0ee4920dd6..28f271945142a228a1d99078dcb48777de99999a 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 7266aa18db9135765905b91b35f889c4334af097..b4f06ebee7895e393a30d56c26528e53bc210b95 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 c65637efbc4f5db7a1e45c6cba63ee0c3900d0e1..55ed12952686f7e0289bcf2eeef6cc377537c087 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 0000000000000000000000000000000000000000..627d7b6dee140f8063167b0792f8adcaafd6d0ff --- /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 72d0aeea3c78810cd8cf8dcb6d6de6a110276eb4..beaa6e66aecd00a4e7f29a3fa94e79dcff3fd31d 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 d16a2845a4ef8b661c90e43f2cbffbcb6232b4f2..5a340965fd012b85e79d1ee431ca9b6c45585498 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 3fbff269114574cb21933f4dc5c9ca318ea2f338..8a0e082061b984b7fa1dda12e56e9e2fb0e8b49a 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;