Browse Source

mol-plugin: Structure components UI

David Sehnal 5 years ago
parent
commit
5eef3fc42a

+ 1 - 1
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>[];

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

@@ -269,8 +269,8 @@ export class StructureToolsWrapper extends PluginUIComponent {
 
             <StructureSelectionControls />
             <StructureRepresentationControls />
-            <StructureMeasurementsControls />
             <StructureComponentControls />
+            <StructureMeasurementsControls />
         </div>;
     }
 }

+ 5 - 4
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>;
 }

+ 3 - 3
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'>

+ 3 - 0
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 }) => {

+ 6 - 1
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;
     }

+ 4 - 4
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>;

+ 50 - 24
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>;
     }

+ 4 - 8
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>
     }
 

+ 7 - 0
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();

+ 2 - 0
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 }>(),