From 5eef3fc42ae15df48237dc3b51d2d7f56dfd4f90 Mon Sep 17 00:00:00 2001 From: David Sehnal <david.sehnal@gmail.com> Date: Tue, 10 Mar 2020 13:10:47 +0100 Subject: [PATCH] mol-plugin: Structure components UI --- src/mol-plugin-state/actions/structure.ts | 2 +- src/mol-plugin-ui/controls.tsx | 2 +- src/mol-plugin-ui/controls/common.tsx | 9 ++- src/mol-plugin-ui/controls/parameters.tsx | 6 +- src/mol-plugin-ui/left-panel.tsx | 3 + .../skin/base/components/controls.scss | 7 +- src/mol-plugin-ui/state/snapshots.tsx | 8 +- src/mol-plugin-ui/structure/components.tsx | 74 +++++++++++++------ src/mol-plugin-ui/structure/measurements.tsx | 12 +-- src/mol-plugin/behavior/static/camera.ts | 7 ++ src/mol-plugin/commands.ts | 2 + 11 files changed, 86 insertions(+), 46 deletions(-) diff --git a/src/mol-plugin-state/actions/structure.ts b/src/mol-plugin-state/actions/structure.ts index 715bc094b..893922caf 100644 --- a/src/mol-plugin-state/actions/structure.ts +++ b/src/mol-plugin-state/actions/structure.ts @@ -175,7 +175,7 @@ const DownloadStructure = StateAction.build({ }) } })(({ params, state }, plugin: PluginContext) => Task.create('Download Structure', async ctx => { - plugin.behaviors.layout.leftPanelTabName.next('data'); + // plugin.behaviors.layout.leftPanelTabName.next('data'); const src = params.source; let downloadParams: StateTransformer.Params<Download>[]; diff --git a/src/mol-plugin-ui/controls.tsx b/src/mol-plugin-ui/controls.tsx index 3efa7e99b..6a3a7abb0 100644 --- a/src/mol-plugin-ui/controls.tsx +++ b/src/mol-plugin-ui/controls.tsx @@ -269,8 +269,8 @@ export class StructureToolsWrapper extends PluginUIComponent { <StructureSelectionControls /> <StructureRepresentationControls /> - <StructureMeasurementsControls /> <StructureComponentControls /> + <StructureMeasurementsControls /> </div>; } } diff --git a/src/mol-plugin-ui/controls/common.tsx b/src/mol-plugin-ui/controls/common.tsx index 7af1c19b4..f0e4be2dd 100644 --- a/src/mol-plugin-ui/controls/common.tsx +++ b/src/mol-plugin-ui/controls/common.tsx @@ -29,7 +29,7 @@ export class ControlGroup extends React.Component<{ render() { // TODO: customize header style (bg color, togle button etc) - return <div className='msp-control-group-wrapper'> + return <div className='msp-control-group-wrapper' style={{ position: 'relative' }}> <div className='msp-control-group-header'> <button className='msp-btn msp-btn-block' onClick={this.headerClicked}> {!this.props.hideExpander && <Icon name={this.state.isExpanded ? 'collapse' : 'expand'} />} @@ -257,7 +257,7 @@ export class ExpandableGroup extends React.Component<{ export function IconButton(props: { icon: IconName, - isSmall?: boolean, + small?: boolean, onClick: (e: React.MouseEvent<HTMLButtonElement>) => void, title?: string, toggleState?: boolean, @@ -267,12 +267,13 @@ export function IconButton(props: { 'data-id'?: string, extraContent?: JSX.Element }) { - let className = `msp-btn-link msp-btn-icon${props.isSmall ? '-small' : ''}${props.customClass ? ' ' + props.customClass : ''}`; + let className = `msp-btn-link msp-btn-icon${props.small ? '-small' : ''}${props.customClass ? ' ' + props.customClass : ''}`; if (typeof props.toggleState !== 'undefined') { className += ` msp-btn-link-toggle-${props.toggleState ? 'on' : 'off'}` } + const iconStyle = props.small ? { fontSize: '80%' } : void 0; return <button className={className} onClick={props.onClick} title={props.title} disabled={props.disabled} data-id={props['data-id']} style={props.style}> - <Icon name={props.icon} /> + <Icon name={props.icon} style={iconStyle} /> {props.extraContent} </button>; } diff --git a/src/mol-plugin-ui/controls/parameters.tsx b/src/mol-plugin-ui/controls/parameters.tsx index 04e6f8688..333bb6340 100644 --- a/src/mol-plugin-ui/controls/parameters.tsx +++ b/src/mol-plugin-ui/controls/parameters.tsx @@ -939,9 +939,9 @@ class ObjectListItem extends React.PureComponent<{ param: PD.ObjectList, value: {this.props.param.getLabel(this.props.value)} </button> <div> - <IconButton icon='up-thin' title='Move Up' onClick={this.moveUp} isSmall={true} /> - <IconButton icon='down-thin' title='Move Down' onClick={this.moveDown} isSmall={true} /> - <IconButton icon='remove' title='Remove' onClick={this.remove} isSmall={true} /> + <IconButton icon='up-thin' title='Move Up' onClick={this.moveUp} small={true} /> + <IconButton icon='down-thin' title='Move Down' onClick={this.moveDown} small={true} /> + <IconButton icon='remove' title='Remove' onClick={this.remove} small={true} /> </div> </div> {this.state.isExpanded && <div className='msp-control-offset'> diff --git a/src/mol-plugin-ui/left-panel.tsx b/src/mol-plugin-ui/left-panel.tsx index a97e0fcc7..5dcd7e9ff 100644 --- a/src/mol-plugin-ui/left-panel.tsx +++ b/src/mol-plugin-ui/left-panel.tsx @@ -25,6 +25,9 @@ export class LeftPanelControls extends PluginUIComponent<{}, { tab: LeftPanelTab componentDidMount() { this.subscribe(this.plugin.behaviors.layout.leftPanelTabName, tab => { if (this.state.tab !== tab) this.setState({ tab }); + if (tab === 'none' && this.plugin.layout.state.regionState.left !== 'collapsed') { + PluginCommands.Layout.Update(this.plugin, { state: { regionState: { ...this.plugin.layout.state.regionState, left: 'collapsed' } } }); + } }); this.subscribe(this.plugin.state.dataState.events.changed, ({ state }) => { diff --git a/src/mol-plugin-ui/skin/base/components/controls.scss b/src/mol-plugin-ui/skin/base/components/controls.scss index 725039cf8..9ccf04dfb 100644 --- a/src/mol-plugin-ui/skin/base/components/controls.scss +++ b/src/mol-plugin-ui/skin/base/components/controls.scss @@ -21,7 +21,7 @@ background: $default-background; margin-top: 1px; - > span { + > span:first-child, > button.msp-control-button-label { line-height: $row-height; display: block; width: $control-label-width + $control-spacing; @@ -36,6 +36,11 @@ @include non-selectable; } + > button.msp-control-button-label { + background: $default-background; + cursor: pointer; + } + select, button, input[type=text] { @extend .msp-form-control; } diff --git a/src/mol-plugin-ui/state/snapshots.tsx b/src/mol-plugin-ui/state/snapshots.tsx index 006556f8f..e54c86c98 100644 --- a/src/mol-plugin-ui/state/snapshots.tsx +++ b/src/mol-plugin-ui/state/snapshots.tsx @@ -150,10 +150,10 @@ class LocalStateSnapshotList extends PluginUIComponent<{ }, { }> { </small> </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} /> + <IconButton data-id={e!.snapshot.id} icon='up-thin' title='Move Up' onClick={this.moveUp} small={true} /> + <IconButton data-id={e!.snapshot.id} icon='down-thin' title='Move Down' onClick={this.moveDown} small={true} /> + <IconButton data-id={e!.snapshot.id} icon='switch' title='Replace' onClick={this.replace} small={true} /> + <IconButton data-id={e!.snapshot.id} icon='remove' title='Remove' onClick={this.remove} small={true} /> </div> </li>)} </ul>; diff --git a/src/mol-plugin-ui/structure/components.tsx b/src/mol-plugin-ui/structure/components.tsx index c83796dfb..fed5301cc 100644 --- a/src/mol-plugin-ui/structure/components.tsx +++ b/src/mol-plugin-ui/structure/components.tsx @@ -8,10 +8,9 @@ import * as React from 'react'; import { CollapsableControls, CollapsableState, PurePluginUIComponent } from '../base'; import { StructureHierarchyManager } from '../../mol-plugin-state/manager/structure'; import { StructureComponentRef, StructureRepresentationRef } from '../../mol-plugin-state/manager/structure/hierarchy'; -import { Icon } from '../controls/icons'; import { State, StateAction } from '../../mol-state'; import { PluginCommands } from '../../mol-plugin/commands'; -import { ExpandGroup, IconButton } from '../controls/common'; +import { ExpandGroup, IconButton, ControlGroup } from '../controls/common'; import { UpdateTransformControl } from '../state/update-transform'; import { ActionMenu } from '../controls/action-menu'; import { ApplyActionControl } from '../state/apply-action'; @@ -21,9 +20,15 @@ interface StructureComponentControlState extends CollapsableState { isDisabled: boolean } +const MeasurementFocusOptions = { + minRadius: 6, + extraRadius: 6, + durationMs: 250, +} + export class StructureComponentControls extends CollapsableControls<{}, StructureComponentControlState> { protected defaultState(): StructureComponentControlState { - return { header: 'Structure Components', isCollapsed: false, isDisabled: false }; + return { header: 'Components', isCollapsed: false, isDisabled: false }; } get currentModels() { @@ -42,13 +47,11 @@ export class StructureComponentControls extends CollapsableControls<{}, Structur } } -const createRepr = StateAction.fromTransformer(StateTransforms.Representation.StructureRepresentation3D); -class StructureComponentEntry extends PurePluginUIComponent<{ component: StructureComponentRef }, { showActions: boolean, showAddRepr: boolean }> { - state = { showActions: false, showAddRepr: false } +type StructureComponentEntryActions = 'add-repr' | 'remove' | 'none' - is(e: State.ObjectEvent) { - return e.ref === this.ref && e.state === this.props.component.cell.parent; - } +const createRepr = StateAction.fromTransformer(StateTransforms.Representation.StructureRepresentation3D); +class StructureComponentEntry extends PurePluginUIComponent<{ component: StructureComponentRef }, { action: StructureComponentEntryActions }> { + state = { action: 'none' as StructureComponentEntryActions } get ref() { return this.props.component.cell.transform.ref; @@ -56,7 +59,7 @@ class StructureComponentEntry extends PurePluginUIComponent<{ component: Structu componentDidMount() { this.subscribe(this.plugin.events.state.cell.stateUpdated, e => { - if (this.is(e)) this.forceUpdate(); + if (State.ObjectEvent.isCell(e, this.props.component.cell)) this.forceUpdate(); }); } @@ -68,16 +71,15 @@ class StructureComponentEntry extends PurePluginUIComponent<{ component: Structu remove(ref: string) { return () => { - this.setState({ showActions: false }); + this.setState({ action: 'none' }); PluginCommands.State.RemoveObject(this.plugin, { state: this.props.component.cell.parent, ref, removeParentGhosts: true }); } } - get actions(): ActionMenu.Items { + get removeActions(): ActionMenu.Items { const ret = [ - ActionMenu.Item(`${this.state.showAddRepr ? 'Hide ' : ''}Add Representation`, 'plus', this.toggleAddRepr), - ActionMenu.Item('Remove', 'remove', this.remove(this.ref)) + ActionMenu.Item('Remove Component', 'remove', this.remove(this.ref)) ]; for (const repr of this.props.component.representations) { ret.push(ActionMenu.Item(`Remove ${repr.cell.obj?.label}`, 'remove', this.remove(repr.cell.transform.ref))) @@ -85,13 +87,32 @@ class StructureComponentEntry extends PurePluginUIComponent<{ component: Structu return ret; } - selectAction: ActionMenu.OnSelect = item => { + selectRemoveAction: ActionMenu.OnSelect = item => { if (!item) return; (item?.value as any)(); } - toggleAddRepr = () => this.setState({ showActions: false, showAddRepr: !this.state.showAddRepr }); - toggleActions = () => this.setState({ showActions: !this.state.showActions }); + toggleAddRepr = () => this.setState({ action: this.state.action === 'none' ? 'add-repr' : 'none' }); + toggleRemoveActions = () => this.setState({ action: this.state.action === 'none' ? 'remove' : 'none' }); + + highlight = (e: React.MouseEvent<HTMLElement>) => { + e.preventDefault(); + PluginCommands.State.Highlight(this.plugin, { state: this.props.component.cell.parent, ref: this.ref }); + } + + clearHighlight = (e: React.MouseEvent<HTMLElement>) => { + e.preventDefault(); + PluginCommands.State.ClearHighlight(this.plugin, { state: this.props.component.cell.parent, ref: this.ref }); + } + + focus = () => { + const sphere = this.props.component.cell.obj?.data.boundary.sphere; + if (sphere) { + const { extraRadius, minRadius, durationMs } = MeasurementFocusOptions; + const radius = Math.max(sphere.radius + extraRadius, minRadius); + PluginCommands.Camera.Focus(this.plugin, { center: sphere.center, radius, durationMs }); + } + } render() { const component = this.props.component; @@ -99,16 +120,21 @@ class StructureComponentEntry extends PurePluginUIComponent<{ component: Structu const label = cell.obj?.label; return <> <div className='msp-control-row'> - <span title={label}>{label}</span> + <button className='msp-control-button-label' title={`${label}. Click to focus.`} onClick={this.focus} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight} style={{ textAlign: 'left' }}> + {label} + </button> <div className='msp-select-row'> - <button onClick={this.toggleVisible}><Icon name='visual-visibility' style={{ fontSize: '80%' }} /> {cell.state.isHidden ? 'Show' : 'Hide'}</button> - <IconButton onClick={this.toggleActions} icon='menu' style={{ width: '64px' }} toggleState={this.state.showActions} title='Actions' /> + <IconButton onClick={this.toggleVisible} icon='visual-visibility' toggleState={!cell.state.isHidden} title={`${cell.state.isHidden ? 'Show' : 'Hide'} component`} small /> + <IconButton onClick={this.toggleRemoveActions} icon='remove' title='Remove' small toggleState={this.state.action === 'remove'} /> + <IconButton onClick={this.toggleAddRepr} icon='plus' title='Add Representation' toggleState={this.state.action === 'add-repr'} /> </div> </div> - {this.state.showActions && <ActionMenu items={this.actions} onSelect={this.selectAction} />} + {this.state.action === 'remove' && <ActionMenu items={this.removeActions} onSelect={this.selectRemoveAction} />} <div className='msp-control-offset'> - {this.state.showAddRepr && - <ApplyActionControl plugin={this.plugin} state={cell.parent} action={createRepr} nodeRef={this.ref} hideHeader noMargin onApply={this.toggleAddRepr} applyLabel='Add' />} + {this.state.action === 'add-repr' && + <ControlGroup header='Add Representation' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleAddRepr} topRightIcon='off'> + <ApplyActionControl plugin={this.plugin} state={cell.parent} action={createRepr} nodeRef={this.ref} hideHeader noMargin onApply={this.toggleAddRepr} applyLabel='Add' /> + </ControlGroup>} {component.representations.map(r => <StructureRepresentationEntry key={r.cell.transform.ref} representation={r} />)} </div> </>; @@ -118,7 +144,7 @@ class StructureComponentEntry extends PurePluginUIComponent<{ component: Structu class StructureRepresentationEntry extends PurePluginUIComponent<{ representation: StructureRepresentationRef }> { render() { const repr = this.props.representation.cell; - return <ExpandGroup header={repr.obj?.label || ''} noOffset> + return <ExpandGroup header={`${repr.obj?.label || ''} Representation`} noOffset> <UpdateTransformControl state={repr.parent} transform={repr.transform} customHeader='none' noMargin /> </ExpandGroup>; } diff --git a/src/mol-plugin-ui/structure/measurements.tsx b/src/mol-plugin-ui/structure/measurements.tsx index a859b0a2e..a0b162041 100644 --- a/src/mol-plugin-ui/structure/measurements.tsx +++ b/src/mol-plugin-ui/structure/measurements.tsx @@ -23,13 +23,10 @@ const MeasurementFocusOptions = { minRadius: 8, extraRadius: 4, durationMs: 250, - unitLabel: '\u212B', } interface StructureMeasurementsControlsState extends CollapsableState { - unitLabel: string, - - isDisabled: boolean, + isDisabled: boolean } export class StructureMeasurementsControls extends CollapsableControls<{}, StructureMeasurementsControlsState> { @@ -47,7 +44,6 @@ export class StructureMeasurementsControls extends CollapsableControls<{}, Struc return { isCollapsed: false, header: 'Measurements', - unitLabel: '\u212B', isDisabled: false } as StructureMeasurementsControlsState } @@ -145,7 +141,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen if (sphere) { const { extraRadius, minRadius, durationMs } = MeasurementFocusOptions; const radius = Math.max(sphere.radius + extraRadius, minRadius); - this.plugin.canvas3d?.camera.focus(sphere.center, radius, this.plugin.canvas3d.boundingSphere.radius, durationMs); + PluginCommands.Camera.Focus(this.plugin, { center: sphere.center, radius, durationMs }); } } @@ -169,8 +165,8 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen <button className='msp-btn msp-btn-block msp-form-control' title='Click to focus. Hover to highlight.' onClick={this.focus}> <span dangerouslySetInnerHTML={{ __html: this.label }} /> </button> - <IconButton isSmall={true} customClass='msp-form-control' onClick={this.delete} icon='remove' style={{ width: '52px' }} title='Delete' /> - <IconButton isSmall={true} customClass='msp-form-control' onClick={this.toggleVisibility} icon='eye' style={{ width: '52px' }} title={cell.state.isHidden ? 'Show' : 'Hide'} toggleState={!cell.state.isHidden} /> + <IconButton small={true} customClass='msp-form-control' onClick={this.delete} icon='remove' style={{ width: '52px' }} title='Delete' /> + <IconButton small={true} customClass='msp-form-control' onClick={this.toggleVisibility} icon='eye' style={{ width: '52px' }} title={cell.state.isHidden ? 'Show' : 'Hide'} toggleState={!cell.state.isHidden} /> </div> } diff --git a/src/mol-plugin/behavior/static/camera.ts b/src/mol-plugin/behavior/static/camera.ts index 755df696f..9c42ede0a 100644 --- a/src/mol-plugin/behavior/static/camera.ts +++ b/src/mol-plugin/behavior/static/camera.ts @@ -10,6 +10,7 @@ import { CameraSnapshotManager } from '../../../mol-plugin-state/camera'; export function registerDefault(ctx: PluginContext) { Reset(ctx); + Focus(ctx); SetSnapshot(ctx); Snapshots(ctx); } @@ -26,6 +27,12 @@ export function SetSnapshot(ctx: PluginContext) { }) } +export function Focus(ctx: PluginContext) { + PluginCommands.Camera.Focus.subscribe(ctx, ({ center, radius, durationMs }) => { + ctx.canvas3d?.camera.focus(center, radius, ctx.canvas3d?.boundingSphere.radius, durationMs); + }) +} + export function Snapshots(ctx: PluginContext) { PluginCommands.Camera.Snapshots.Clear.subscribe(ctx, () => { ctx.state.cameraSnapshots.clear(); diff --git a/src/mol-plugin/commands.ts b/src/mol-plugin/commands.ts index 46808e15f..95ba9d430 100644 --- a/src/mol-plugin/commands.ts +++ b/src/mol-plugin/commands.ts @@ -13,6 +13,7 @@ import { StructureElement } from '../mol-model/structure'; import { PluginState } from './state'; import { Interactivity } from './util/interactivity'; import { PluginToast } from './util/toast'; +import { Vec3 } from '../mol-math/linear-algebra'; export const PluginCommands = { State: { @@ -59,6 +60,7 @@ export const PluginCommands = { Camera: { Reset: PluginCommand<{ durationMs?: number, snapshot?: Partial<Camera.Snapshot> }>(), SetSnapshot: PluginCommand<{ snapshot: Partial<Camera.Snapshot>, durationMs?: number }>(), + Focus: PluginCommand<{ center: Vec3, radius: number, durationMs?: number }>(), Snapshots: { Add: PluginCommand<{ name?: string, description?: string }>(), Remove: PluginCommand<{ id: string }>(), -- GitLab