Browse Source

wip, selection viewport controls

- refactored selection controls as components
- initial css, buggy
Alexander Rose 5 years ago
parent
commit
900f2e1f76

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

@@ -16,7 +16,7 @@ import { StateTransforms } from '../mol-plugin-state/transforms';
 import { StateTransformer } from '../mol-state';
 import { ModelFromTrajectory } from '../mol-plugin-state/transforms/model';
 import { AnimationControls } from './state/animation';
-import { StructureSelectionControls } from './structure/selection';
+import { StructureSelectionControls, StructureSelectionActionsControls } from './structure/selection';
 import { Icon } from './controls/icons';
 import { StructureComponentControls } from './structure/components';
 import { StructureSourceControls } from './structure/source';
@@ -246,6 +246,20 @@ export class AnimationViewportControls extends PluginUIComponent<{}, { isEmpty:
     }
 }
 
+export class SelectionViewportControls extends PluginUIComponent<{}, { isEmpty: boolean, isExpanded: boolean, isBusy: boolean, isAnimating: boolean, isPlaying: boolean }> {
+    state = { isEmpty: true, isExpanded: false, isBusy: false, isAnimating: false, isPlaying: false };
+
+    componentDidMount() {
+
+    }
+
+    render() {
+        return <div className='msp-selection-viewport-controls'>
+            <StructureSelectionActionsControls />
+        </div>;
+    }
+}
+
 export class LociLabels extends PluginUIComponent<{}, { labels: ReadonlyArray<LociLabel> }> {
     state = { labels: [] }
 

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

@@ -11,7 +11,7 @@ import { PluginContext } from '../mol-plugin/context';
 import { formatTime } from '../mol-util';
 import { LogEntry } from '../mol-util/log-entry';
 import { PluginReactContext, PluginUIComponent } from './base';
-import { AnimationViewportControls, DefaultStructureTools, LociLabels, StateSnapshotViewportControls, TrajectoryViewportControls } from './controls';
+import { AnimationViewportControls, DefaultStructureTools, LociLabels, StateSnapshotViewportControls, TrajectoryViewportControls, SelectionViewportControls } from './controls';
 import { LeftPanelControls } from './left-panel';
 import { SequenceView } from './sequence';
 import { BackgroundTaskProgress } from './task';
@@ -138,6 +138,7 @@ export class DefaultViewport extends PluginUIComponent {
                 <TrajectoryViewportControls />
                 <StateSnapshotViewportControls />
             </div>
+            <SelectionViewportControls />
             <VPControls />
             <BackgroundTaskProgress />
             <div className='msp-highlight-toast-wrapper'>

+ 14 - 4
src/mol-plugin-ui/skin/base/components/misc.scss

@@ -423,6 +423,16 @@
     }
 }
 
+.msp-selection-viewport-controls {
+    position: relative;
+    justify-content: center;
+    display: flex;
+    margin-top: $control-spacing;
+
+    line-height: $row-height;
+    margin-right: $control-spacing;
+}
+
 .msp-param-object-list-item {
     margin-top: 1px;
     position: relative;
@@ -498,10 +508,10 @@
     }
 }
 
-@mixin type-class-border($name, $color) {    
+@mixin type-class-border($name, $color) {
     .msp-type-class-#{$name} {
         border-left-color: $color;
-    } 
+    }
 }
 
 // TypeClass = 'Root' | 'Group' | 'Data' | 'Object' | 'Representation3D' | 'Behavior'
@@ -512,10 +522,10 @@
 @include type-class-border('Representation3D', $type-class-Representation3D);
 @include type-class-border('Behavior', $type-class-Behavior);
 
-@mixin accent($name, $color) {    
+@mixin accent($name, $color) {
     .msp-accent-color-#{$name} {
         color: $color;
-    } 
+    }
     .msp-accent-bg-#{$name} {
         background: $color;
     }

+ 118 - 57
src/mol-plugin-ui/structure/selection.tsx

@@ -14,7 +14,7 @@ import { StructureSelectionModifier } from '../../mol-plugin-state/manager/struc
 import { memoizeLatest } from '../../mol-util/memoize';
 import { ParamDefinition } from '../../mol-util/param-definition';
 import { stripTags } from '../../mol-util/string';
-import { CollapsableControls, CollapsableState, PurePluginUIComponent } from '../base';
+import { CollapsableControls, CollapsableState, PurePluginUIComponent, PluginUIComponent } from '../base';
 import { ActionMenu } from '../controls/action-menu';
 import { ControlGroup, ToggleButton, IconButton, Button } from '../controls/common';
 import { ParameterControls } from '../controls/parameters';
@@ -26,17 +26,8 @@ const StructureSelectionParams = {
 interface StructureSelectionControlsState extends CollapsableState {
     isEmpty: boolean,
     isBusy: boolean,
-
-    action?: StructureSelectionModifier | 'color'
 }
 
-const ActionHeader = new Map<StructureSelectionModifier, string>([
-    ['add', 'Add/Union'],
-    ['remove', 'Remove/Subtract'],
-    ['intersect', 'Intersect'],
-    ['set', 'Set']
-] as const);
-
 export class StructureSelectionControls<P, S extends StructureSelectionControlsState> extends CollapsableControls<P, S> {
     componentDidMount() {
         this.subscribe(this.plugin.managers.structure.selection.events.changed, () => {
@@ -53,33 +44,12 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
                 this.setState({ isEmpty });
             }
         });
-
-        this.subscribe(this.plugin.behaviors.state.isBusy, v => {
-            this.setState({ isBusy: v, action: void 0 })
-        })
     }
 
     get isDisabled() {
         return this.state.isBusy || this.state.isEmpty
     }
 
-    get stats() {
-        const stats = this.plugin.managers.structure.selection.stats
-        if (stats.structureCount === 0 || stats.elementCount === 0) {
-            return 'Nothing Selected'
-        } else {
-            return `${stripTags(stats.label)} Selected`
-        }
-    }
-
-    clear = () => this.plugin.managers.interactivity.lociSelects.deselectAll();
-
-    focus = () => {
-        if (this.plugin.managers.structure.selection.stats.elementCount === 0) return;
-        const { sphere } = this.plugin.managers.structure.selection.getBoundary();
-        this.plugin.managers.camera.focusSphere(sphere);
-    }
-
     setProps = (props: any) => {
         this.plugin.managers.interactivity.setProps(props);
     }
@@ -90,6 +60,68 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
         }
     }
 
+    defaultState() {
+        return {
+            isCollapsed: false,
+            header: 'Selection',
+
+            isEmpty: true,
+            isBusy: false,
+
+            brand: { name: 'Sel', accent: 'red' }
+        } as S
+    }
+
+    renderControls() {
+        return <>
+            <ParameterControls params={StructureSelectionParams} values={this.values} onChangeValues={this.setProps} />
+            <StructureSelectionActionsControls />
+            <div style={{ margin: '6px 0' }}>
+                <StructureSelectionStatsControls />
+            </div>
+        </>
+    }
+}
+
+interface StructureSelectionActionsControlsState {
+    isEmpty: boolean,
+    isBusy: boolean,
+
+    action?: StructureSelectionModifier | 'color'
+}
+
+const ActionHeader = new Map<StructureSelectionModifier, string>([
+    ['add', 'Add/Union'],
+    ['remove', 'Remove/Subtract'],
+    ['intersect', 'Intersect'],
+    ['set', 'Set']
+] as const);
+
+export class StructureSelectionActionsControls extends PluginUIComponent<{}, StructureSelectionActionsControlsState> {
+    state = {
+        action: void 0 as StructureSelectionActionsControlsState['action'],
+
+        isEmpty: true,
+        isBusy: false,
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, c => {
+            const isEmpty = c.structures.length === 0;
+            if (this.state.isEmpty !== isEmpty) {
+                this.setState({ isEmpty });
+            }
+        });
+
+        this.subscribe(this.plugin.behaviors.state.isBusy, v => {
+            this.setState({ isBusy: v, action: void 0 })
+        })
+    }
+
+    get isDisabled() {
+        return this.state.isBusy || this.state.isEmpty
+    }
+
     set = (modifier: StructureSelectionModifier, selectionQuery: StructureSelectionQuery) => {
         this.plugin.managers.structure.selection.fromSelectionQuery(modifier, selectionQuery, false)
     }
@@ -121,7 +153,7 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
         return this.queriesItems
     }
 
-    private showAction(q: StructureSelectionControlsState['action']) {
+    private showAction(q: StructureSelectionActionsControlsState['action']) {
         return () => this.setState({ action: this.state.action === q ? void 0 : q });
     }
 
@@ -131,18 +163,7 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
     toggleSet = this.showAction('set')
     toggleColor = this.showAction('color')
 
-    highlight = (e: React.MouseEvent<HTMLElement>) => {
-        this.plugin.managers.interactivity.lociHighlights.clearHighlights();
-        this.plugin.managers.structure.selection.entries.forEach(e => {
-            this.plugin.managers.interactivity.lociHighlights.highlight({ loci: e.selection }, false);
-        })
-    }
-
-    clearHighlight = () => {
-        this.plugin.managers.interactivity.lociHighlights.clearHighlights();
-    }
-
-    get controls() {
+    render() {
         return <>
             <div className='msp-flex-row'>
                 <ToggleButton icon='union' title={ActionHeader.get('add')} toggle={this.toggleAdd} isSelected={this.state.action === 'add'} disabled={this.isDisabled} />
@@ -157,29 +178,69 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
             </ControlGroup>}
         </>
     }
+}
 
-    defaultState() {
-        return {
-            isCollapsed: false,
-            header: 'Selection',
+export class StructureSelectionStatsControls extends PluginUIComponent<{}, { isEmpty: boolean, isBusy: boolean }> {
+    state = {
+        isEmpty: true,
+        isBusy: false
+    }
 
-            action: void 0,
+    componentDidMount() {
+        this.subscribe(this.plugin.managers.structure.selection.events.changed, () => {
+            this.forceUpdate()
+        });
 
-            isEmpty: true,
-            isBusy: false,
+        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, c => {
+            const isEmpty = c.structures.length === 0;
+            if (this.state.isEmpty !== isEmpty) {
+                this.setState({ isEmpty });
+            }
+        });
 
-            brand: { name: 'Sel', accent: 'red' }
-        } as S
+        this.subscribe(this.plugin.behaviors.state.isBusy, v => {
+            this.setState({ isBusy: v })
+        })
     }
 
-    renderControls() {
+    get isDisabled() {
+        return this.state.isBusy || this.state.isEmpty
+    }
+
+    get stats() {
+        const stats = this.plugin.managers.structure.selection.stats
+        if (stats.structureCount === 0 || stats.elementCount === 0) {
+            return 'Nothing Selected'
+        } else {
+            return `${stripTags(stats.label)} Selected`
+        }
+    }
+
+    clear = () => this.plugin.managers.interactivity.lociSelects.deselectAll();
+
+    focus = () => {
+        if (this.plugin.managers.structure.selection.stats.elementCount === 0) return;
+        const { sphere } = this.plugin.managers.structure.selection.getBoundary();
+        this.plugin.managers.camera.focusSphere(sphere);
+    }
+
+    highlight = (e: React.MouseEvent<HTMLElement>) => {
+        this.plugin.managers.interactivity.lociHighlights.clearHighlights();
+        this.plugin.managers.structure.selection.entries.forEach(e => {
+            this.plugin.managers.interactivity.lociHighlights.highlight({ loci: e.selection }, false);
+        })
+    }
+
+    clearHighlight = () => {
+        this.plugin.managers.interactivity.lociHighlights.clearHighlights();
+    }
+
+    render() {
         const stats = this.plugin.managers.structure.selection.stats
         const empty = stats.structureCount === 0 || stats.elementCount === 0;
 
         return <>
-            <ParameterControls params={StructureSelectionParams} values={this.values} onChangeValues={this.setProps} />
-            {this.controls}
-            <div className='msp-flex-row' style={{ margin: '6px 0' }}>
+            <div className='msp-flex-row'>
                 <Button noOverflow onClick={this.focus} title='Click to Focus Selection' disabled={empty} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight}
                     style={{ textAlignLast: !empty ? 'left' : void 0 }}>
                     {this.stats}