Sfoglia il codice sorgente

mol-plugin: selection mode

David Sehnal 5 anni fa
parent
commit
e56063b065

+ 4 - 6
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, StructureSelectionActionsControls } from './structure/selection';
+import { StructureSelectionActionsControls } from './structure/selection';
 import { Icon } from './controls/icons';
 import { StructureComponentControls } from './structure/components';
 import { StructureSourceControls } from './structure/source';
@@ -246,14 +246,13 @@ 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 };
-
+export class SelectionViewportControls extends PluginUIComponent {
     componentDidMount() {
-
+        this.subscribe(this.plugin.behaviors.interaction.selectionMode, () => this.forceUpdate());
     }
 
     render() {
+        if (!this.plugin.selectionMode) return null;
         return <div className='msp-selection-viewport-controls'>
             <StructureSelectionActionsControls />
         </div>;
@@ -298,7 +297,6 @@ export class DefaultStructureTools extends PluginUIComponent {
             <div className='msp-section-header'><Icon name='tools' />Structure Tools</div>
 
             <StructureSourceControls />
-            <StructureSelectionControls />
             <StructureMeasurementsControls />
             <StructureComponentControls />
             <VolumeStreamingControls />

+ 40 - 6
src/mol-plugin-ui/controls/action-menu.tsx

@@ -5,19 +5,22 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import * as React from 'react'
-import { IconName } from './icons';
+import * as React from 'react';
 import { ParamDefinition } from '../../mol-util/param-definition';
-import { ControlGroup, Button } from './common';
+import { Button, ControlGroup } from './common';
+import { IconName } from './icons';
 
 export class ActionMenu extends React.PureComponent<ActionMenu.Props> {
     hide = () => this.props.onSelect(void 0)
 
     render() {
         const cmd = this.props;
-        return <div className='msp-action-menu-options' style={{ /* marginTop: cmd.header ? void 0 : '1px', */ maxHeight: '300px', overflow: 'hidden', overflowY: 'auto' }}>
-            {cmd.header && <ControlGroup header={cmd.header} initialExpanded={true} hideExpander={true} hideOffset={false} onHeaderClick={this.hide} topRightIcon='off'></ControlGroup>}
-            <Section items={cmd.items} onSelect={cmd.onSelect} current={cmd.current} multiselect={this.props.multiselect} noOffset={this.props.noOffset} noAccent={this.props.noAccent} />
+        const section = <Section items={cmd.items} onSelect={cmd.onSelect} current={cmd.current} multiselect={this.props.multiselect} noOffset={this.props.noOffset} noAccent={this.props.noAccent} />;
+        return <div className={`msp-action-menu-options${cmd.header ? '' : ' msp-action-menu-options-no-header'}`}>
+            {cmd.header && <ControlGroup header={cmd.header} initialExpanded={true} hideExpander={true} hideOffset onHeaderClick={this.hide} topRightIcon='off'>
+                {section}
+            </ControlGroup>}
+            {!cmd.header && section}
         </div>
     }
 }
@@ -129,6 +132,37 @@ export namespace ActionMenu {
             if (found) return found;
         }
     }
+
+    // export type SelectProps<T> = {
+    //     items: Items,
+    //     onSelect: (item: Item) => void,
+    //     disabled?: boolean,
+    //     label?: string,
+    //     current?: Item,
+    //     style?: React.CSSProperties
+    // }
+
+    // export class Select<T> extends React.PureComponent<SelectProps<T>, { isExpanded: boolean }> {
+    //     state = { isExpanded: false };
+
+    //     toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded })
+    //     onSelect: OnSelect = (item) => {
+    //         this.setState({ isExpanded: false });
+    //         if (!item) return;
+    //         this.onSelect(item);
+    //     }
+
+    //     render() {
+    //         const current = this.props.current;
+    //         const label = this.props.label || current?.label || '';
+
+    //         return <div className='msp-action-menu-select' style={this.props.style}>
+    //             <ToggleButton disabled={this.props.disabled} style={{ textAlign: 'left' }} className='msp-no-overflow'
+    //                 label={label} title={label as string} toggle={this.toggleExpanded} isSelected={this.state.isExpanded} />
+    //             {this.state.isExpanded && <ActionMenu items={this.props.items} current={this.props.current} onSelect={this.onSelect} />}
+    //         </div>
+    //     }
+    // }
 }
 
 type SectionProps = {

+ 6 - 2
src/mol-plugin-ui/controls/common.tsx

@@ -16,7 +16,8 @@ export class ControlGroup extends React.Component<{
     topRightIcon?: IconName,
     headerLeftMargin?: string,
     onHeaderClick?: () => void,
-    noTopMargin?: boolean
+    noTopMargin?: boolean,
+    childrenClassName?: string
 }, { isExpanded: boolean }> {
     state = { isExpanded: !!this.props.initialExpanded }
 
@@ -29,6 +30,9 @@ export class ControlGroup extends React.Component<{
     }
 
     render() {
+        let groupClassName = this.props.hideOffset ? 'msp-control-group-children' : 'msp-control-group-children msp-control-offset';
+        if (this.props.childrenClassName) groupClassName += ' ' + this.props.childrenClassName;
+
         // TODO: customize header style (bg color, togle button etc)
         return <div className='msp-control-group-wrapper' style={{ position: 'relative', marginTop: this.props.noTopMargin ? 0 : void 0 }}>
             <div className='msp-control-group-header' style={{ marginLeft: this.props.headerLeftMargin }}>
@@ -38,7 +42,7 @@ export class ControlGroup extends React.Component<{
                     <b>{this.props.header}</b>
                 </Button>
             </div>
-            {this.state.isExpanded && <div className={this.props.hideOffset ? '' : 'msp-control-offset'} style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
+            {this.state.isExpanded && <div className={groupClassName} style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
                 {this.props.children}
             </div>}
         </div>

+ 6 - 0
src/mol-plugin-ui/skin/base/components/controls.scss

@@ -221,6 +221,12 @@
     border-left: 2px solid $color-accent-orange;
 }
 
+// .msp-accent-offset-right {
+//     margin-left: $control-spacing;
+//     padding-right: 1px;
+//     border-right: 2px solid $color-accent-orange;
+// }
+
 .msp-control-group-wrapper {
     //border-left-width: $control-spacing / 2;
     //border-left-style: solid;

+ 30 - 7
src/mol-plugin-ui/skin/base/components/misc.scss

@@ -425,12 +425,29 @@
 
 .msp-selection-viewport-controls {
     position: relative;
-    justify-content: center;
-    display: flex;
-    margin-top: $control-spacing;
-
+    // display: inline-block;
+    margin: $control-spacing auto 0 auto;
+    width: 332px;
     line-height: $row-height;
-    margin-right: $control-spacing;
+
+    &-actions {
+        position: absolute;
+        width: 100%;
+        top: $row-height;
+        background: $control-background;
+    }
+
+    > .msp-flex-row .msp-btn {
+        padding: 0 5px;
+    }
+
+    select.msp-form-control {
+        padding: 0 5px;
+        text-align: center;
+        background: none;
+        flex: 0 0 80px;
+        text-overflow: ellipsis;
+    }
 }
 
 .msp-param-object-list-item {
@@ -474,8 +491,14 @@
     }
 }
 
-.msp-action-menu-options {
-    .msp-control-row, button, .msp-icon {
+.msp-action-menu-options {   
+    &-no-header, .msp-control-group-children {
+        max-height: 300px;
+        overflow: hidden;
+        overflow-y: auto;
+    }
+
+    .msp-control-row, button, .msp-icon, .msp-flex-row {
         height: 24px;
         line-height: 24px;
     }

+ 5 - 2
src/mol-plugin-ui/skin/base/components/viewport.scss

@@ -82,8 +82,6 @@
 }
 
 .msp-viewport-controls-panel {
-    overflow-y: auto;
-    max-height: 400px;
     width: 290px;
     top: 0;
     right: $row-height + 4px;
@@ -93,6 +91,11 @@
     .msp-control-group-wrapper:first-child {
         padding-top: 0;
     }
+
+    .msp-viewport-controls-panel-controls {
+        overflow-y: auto;
+        max-height: 400px;    
+    }
 }
 
 /* highlight & toasts */

+ 89 - 70
src/mol-plugin-ui/structure/selection.tsx

@@ -7,81 +7,81 @@
 
 import * as React from 'react';
 import { StructureSelectionQueries, StructureSelectionQuery } from '../../mol-plugin-state/helpers/structure-selection-query';
-import { InteractivityManager } from '../../mol-plugin-state/manager/interactivity';
 import { StructureComponentManager } from '../../mol-plugin-state/manager/structure/component';
 import { StructureRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
 import { StructureSelectionModifier } from '../../mol-plugin-state/manager/structure/selection';
 import { memoizeLatest } from '../../mol-util/memoize';
 import { ParamDefinition } from '../../mol-util/param-definition';
 import { stripTags } from '../../mol-util/string';
-import { CollapsableControls, CollapsableState, PurePluginUIComponent, PluginUIComponent } from '../base';
+import { PluginUIComponent, PurePluginUIComponent } from '../base';
 import { ActionMenu } from '../controls/action-menu';
-import { ControlGroup, ToggleButton, IconButton, Button } from '../controls/common';
-import { ParameterControls } from '../controls/parameters';
+import { Button, ControlGroup, IconButton, ToggleButton } from '../controls/common';
+import { ParameterControls, ParamOnChange, PureSelectControl } from '../controls/parameters';
+import { InteractivityManager } from '../../mol-plugin-state/manager/interactivity';
 
 const StructureSelectionParams = {
     granularity: InteractivityManager.Params.granularity,
 }
 
-interface StructureSelectionControlsState extends CollapsableState {
-    isEmpty: boolean,
-    isBusy: boolean,
-}
-
-export class StructureSelectionControls<P, S extends StructureSelectionControlsState> extends CollapsableControls<P, S> {
-    componentDidMount() {
-        this.subscribe(this.plugin.managers.structure.selection.events.changed, () => {
-            this.forceUpdate()
-        });
-
-        this.subscribe(this.plugin.managers.interactivity.events.propsUpdated, () => {
-            this.forceUpdate()
-        });
-
-        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, c => {
-            const isEmpty = c.structures.length === 0;
-            if (this.state.isEmpty !== isEmpty) {
-                this.setState({ isEmpty });
-            }
-        });
-    }
-
-    get isDisabled() {
-        return this.state.isBusy || this.state.isEmpty
-    }
-
-    setProps = (props: any) => {
-        this.plugin.managers.interactivity.setProps(props);
-    }
-
-    get values () {
-        return {
-            granularity: this.plugin.managers.interactivity.props.granularity,
-        }
-    }
-
-    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 StructureSelectionControlsState extends CollapsableState {
+//     isEmpty: boolean,
+//     isBusy: boolean,
+// }
+
+// export class StructureSelectionControls<P, S extends StructureSelectionControlsState> extends CollapsableControls<P, S> {
+//     componentDidMount() {
+//         this.subscribe(this.plugin.managers.structure.selection.events.changed, () => {
+//             this.forceUpdate()
+//         });
+
+//         this.subscribe(this.plugin.managers.interactivity.events.propsUpdated, () => {
+//             this.forceUpdate()
+//         });
+
+//         this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, c => {
+//             const isEmpty = c.structures.length === 0;
+//             if (this.state.isEmpty !== isEmpty) {
+//                 this.setState({ isEmpty });
+//             }
+//         });
+//     }
+
+//     get isDisabled() {
+//         return this.state.isBusy || this.state.isEmpty
+//     }
+
+//     setProps = (props: any) => {
+//         this.plugin.managers.interactivity.setProps(props);
+//     }
+
+//     get values () {
+//         return {
+//             granularity: this.plugin.managers.interactivity.props.granularity,
+//         }
+//     }
+
+//     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 /> */}
+//             <StructureSelectionStatsControls />
+//             {/* <div style={{ margin: '6px 0' }}>
+//             </div> */}
+//         </>
+//     }
+// }
 
 interface StructureSelectionActionsControlsState {
     isEmpty: boolean,
@@ -115,11 +115,15 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
 
         this.subscribe(this.plugin.behaviors.state.isBusy, v => {
             this.setState({ isBusy: v, action: void 0 })
-        })
+        });
+
+        this.subscribe(this.plugin.managers.interactivity.events.propsUpdated, () => {
+            this.forceUpdate()
+        });
     }
 
     get isDisabled() {
-        return this.state.isBusy || this.state.isEmpty
+        return this.state.isBusy || this.state.isEmpty;
     }
 
     set = (modifier: StructureSelectionModifier, selectionQuery: StructureSelectionQuery) => {
@@ -163,7 +167,14 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
     toggleSet = this.showAction('set')
     toggleColor = this.showAction('color')
 
+    setGranuality: ParamOnChange = ({ value }) => {
+        this.plugin.managers.interactivity.setProps({ granularity: value });
+    }
+
+    turnOff = () => this.plugin.selectionMode = false;
+
     render() {
+        const granularity = this.plugin.managers.interactivity.props.granularity;
         return <>
             <div className='msp-flex-row'>
                 <ToggleButton icon='union' title={ActionHeader.get('add')} toggle={this.toggleAdd} isSelected={this.state.action === 'add'} disabled={this.isDisabled} />
@@ -171,16 +182,22 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
                 <ToggleButton icon='intersect' title={ActionHeader.get('intersect')} toggle={this.toggleIntersect} isSelected={this.state.action === 'intersect'} disabled={this.isDisabled} />
                 <ToggleButton icon='set' title={ActionHeader.get('set')} toggle={this.toggleSet} isSelected={this.state.action === 'set'} disabled={this.isDisabled} />
                 <ToggleButton icon='brush' title='Color' toggle={this.toggleColor} isSelected={this.state.action === 'color'} disabled={this.isDisabled} />
+                <PureSelectControl title={`Picking Level`} param={StructureSelectionParams.granularity} name='granularity' value={granularity} onChange={this.setGranuality} isDisabled={this.isDisabled} />
+                <IconButton icon='cancel' title='Turn selection mode off' onClick={this.turnOff} />
             </div>
-            {(this.state.action && this.state.action !== 'color') && <ActionMenu header={ActionHeader.get(this.state.action as StructureSelectionModifier)} items={this.queries} onSelect={this.selectQuery} />}
-            {this.state.action === 'color' && <ControlGroup header='Color' initialExpanded={true} hideExpander={true} hideOffset={false} onHeaderClick={this.toggleColor} topRightIcon='off'>
-                <ApplyColorControls />
-            </ControlGroup>}
+            {(this.state.action && this.state.action !== 'color') && <div className='msp-selection-viewport-controls-actions'>
+                <ActionMenu header={ActionHeader.get(this.state.action as StructureSelectionModifier)} items={this.queries} onSelect={this.selectQuery} noOffset />
+            </div>}
+            {this.state.action === 'color' && <div className='msp-selection-viewport-controls-actions'>
+                <ControlGroup header='Color' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleColor} topRightIcon='off'>
+                    <ApplyColorControls />
+                </ControlGroup>
+            </div>}
         </>
     }
 }
 
-export class StructureSelectionStatsControls extends PluginUIComponent<{}, { isEmpty: boolean, isBusy: boolean }> {
+export class StructureSelectionStatsControls extends PluginUIComponent<{ hideOnEmpty?: boolean }, { isEmpty: boolean, isBusy: boolean }> {
     state = {
         isEmpty: true,
         isBusy: false
@@ -239,6 +256,8 @@ export class StructureSelectionStatsControls extends PluginUIComponent<{}, { isE
         const stats = this.plugin.managers.structure.selection.stats
         const empty = stats.structureCount === 0 || stats.elementCount === 0;
 
+        if (empty && this.props.hideOnEmpty) return null;
+
         return <>
             <div className='msp-flex-row'>
                 <Button noOverflow onClick={this.focus} title='Click to Focus Selection' disabled={empty} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight}

+ 2 - 0
src/mol-plugin-ui/structure/source.tsx

@@ -14,6 +14,7 @@ import { Button, IconButton } from '../controls/common';
 import { ParameterControls } from '../controls/parameters';
 import { StructureFocusControls } from './focus';
 import { UpdateTransformControl } from '../state/update-transform';
+import { StructureSelectionStatsControls } from './selection';
 
 interface StructureSourceControlState extends CollapsableState {
     isBusy: boolean,
@@ -262,6 +263,7 @@ export class StructureSourceControls extends CollapsableControls<{}, StructureSo
 
             <div style={{ marginTop: '6px' }}>
                 <StructureFocusControls />
+                <StructureSelectionStatsControls hideOnEmpty />
             </div>
         </>;
     }

+ 13 - 2
src/mol-plugin-ui/viewport.tsx

@@ -54,6 +54,10 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
         PluginCommands.Layout.Update(this.plugin, { state: { isExpanded: !this.plugin.layout.state.isExpanded } });
     }
 
+    toggleSelectionMode = () => {
+        this.plugin.selectionMode = !this.plugin.selectionMode;
+    }
+
     setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => {
         PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { [p.name]: p.value } });
     }
@@ -69,6 +73,7 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
     componentDidMount() {
         this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
         this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
+        this.subscribe(this.plugin.behaviors.interaction.selectionMode, () => this.forceUpdate());
     }
 
     icon(name: IconName, onClick: (e: React.MouseEvent<HTMLButtonElement>) => void, title: string, isOn = true) {
@@ -97,14 +102,20 @@ export class ViewportControls extends PluginUIComponent<ViewportControlsProps, V
                     {this.plugin.config.get(PluginConfig.Viewport.ShowExpand) && this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.state.isExpanded)}
                     {this.icon('settings', this.toggleSettingsExpanded, 'Settings / Controls Info', this.state.isSettingsExpanded)}
                 </div>
+                {this.plugin.config.get(PluginConfig.Viewport.ShowSelectionMode) && <div>
+                    <div className='msp-semi-transparent-background' />
+                    {this.icon('check', this.toggleSelectionMode, 'Toggle Selection Mode', this.plugin.behaviors.interaction.selectionMode.value)}
+                </div>}
             </div>
             {this.state.isScreenshotExpanded && <div className='msp-viewport-controls-panel'>
-                <ControlGroup header='Screenshot' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleScreenshotExpanded} topRightIcon='off' noTopMargin>
+                <ControlGroup header='Screenshot' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleScreenshotExpanded}
+                    topRightIcon='off' noTopMargin>
                     <DownloadScreenshotControls close={this.toggleScreenshotExpanded} />
                 </ControlGroup>
             </div>}
             {this.state.isSettingsExpanded && <div className='msp-viewport-controls-panel'>
-                <ControlGroup header='Settings / Controls Info' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleSettingsExpanded} topRightIcon='off' noTopMargin>
+                <ControlGroup header='Settings / Controls Info' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleSettingsExpanded}
+                    topRightIcon='off' noTopMargin childrenClassName='msp-viewport-controls-panel-controls'>
                     <SimpleSettingsControl />
                 </ControlGroup>
             </div>}

+ 9 - 1
src/mol-plugin/behavior/dynamic/camera.ts

@@ -18,6 +18,9 @@ const Trigger = Binding.Trigger
 
 const DefaultFocusLociBindings = {
     clickCenterFocus: Binding([
+        Trigger(B.Flag.Primary, M.create())
+    ], 'Camera center and focus', 'Click element using ${triggers}'),
+    clickCenterFocusSelectMode: Binding([
         Trigger(B.Flag.Auxilary, M.create()),
         Trigger(B.Flag.Primary, M.create({ alt: true }))
     ], 'Camera center and focus', 'Click element using ${triggers}'),
@@ -38,7 +41,12 @@ export const FocusLoci = PluginBehavior.create<FocusLociProps>({
         register(): void {
             this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
                 if (!this.ctx.canvas3d) return;
-                if (Binding.match(this.params.bindings.clickCenterFocus, button, modifiers)) {
+
+                const binding = this.ctx.selectionMode
+                    ? this.params.bindings.clickCenterFocusSelectMode
+                    : this.params.bindings.clickCenterFocus;
+
+                if (Binding.match(binding, button, modifiers)) {
                     const loci = Loci.normalize(current.loci, this.ctx.managers.interactivity.props.granularity)
                     if (Loci.isEmpty(loci)) {
                         PluginCommands.Camera.Reset(this.ctx, { })

+ 10 - 3
src/mol-plugin/behavior/dynamic/representation.ts

@@ -133,7 +133,7 @@ export const SelectLoci = PluginBehavior.create({
             })
 
             this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
-                if (!this.ctx.canvas3d || this.ctx.isBusy) return;
+                if (!this.ctx.canvas3d || this.ctx.isBusy || !this.ctx.selectionMode) return;
 
                 // only trigger the 1st action that matches
                 for (const [binding, action, condition] of actions) {
@@ -199,6 +199,9 @@ export const DefaultLociLabelProvider = PluginBehavior.create({
 
 const DefaultFocusLociBindings = {
     clickFocus: Binding([
+        Trigger(B.Flag.Primary, M.create()),
+    ], 'Representation Focus', 'Click element using ${triggers}'),
+    clickFocusSelectMode: Binding([
         Trigger(B.Flag.Secondary, M.create()),
         Trigger(B.Flag.Primary, M.create({ control: true }))
     ], 'Representation Focus', 'Click element using ${triggers}'),
@@ -214,9 +217,13 @@ export const FocusLoci = PluginBehavior.create<FocusLociProps>({
     ctor: class extends PluginBehavior.Handler<FocusLociProps> {
         register(): void {
             this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
-                const { clickFocus } = this.params.bindings
+                const { clickFocus, clickFocusSelectMode } = this.params.bindings;
+
+                const binding = this.ctx.selectionMode
+                    ? clickFocusSelectMode
+                    : clickFocus;
 
-                if (Binding.match(clickFocus, button, modifiers)) {
+                if (Binding.match(binding, button, modifiers)) {
                     const loci = Loci.normalize(current.loci, 'residue')
                     const entry = this.ctx.managers.structure.focus.current
                     if (entry && Loci.areEqual(entry.loci, loci)) {

+ 2 - 1
src/mol-plugin/config.ts

@@ -35,7 +35,8 @@ export const PluginConfig = {
         EmdbHeaderServer: item('volume-streaming.emdb-header-server', 'https://ftp.wwpdb.org/pub/emdb/structures'),
     },
     Viewport: {
-        ShowExpand: item('viewer.show-expand-button', true)
+        ShowExpand: item('viewer.show-expand-button', true),
+        ShowSelectionMode: item('viewer.show-selection-model-button', true)
     }
 }
 

+ 16 - 1
src/mol-plugin/context.ts

@@ -96,7 +96,8 @@ export class PluginContext {
         },
         interaction: {
             hover: this.ev.behavior<InteractivityManager.HoverEvent>({ current: Representation.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0, button: 0 }),
-            click: this.ev.behavior<InteractivityManager.ClickEvent>({ current: Representation.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0, button: 0 })
+            click: this.ev.behavior<InteractivityManager.ClickEvent>({ current: Representation.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0, button: 0 }),
+            selectionMode: this.ev.behavior<boolean>(false)
         },
         labels: {
             highlight: this.ev.behavior<{ labels: ReadonlyArray<LociLabel> }>({ labels: [] })
@@ -209,6 +210,14 @@ export class PluginContext {
         return this.behaviors.state.isAnimating.value || this.behaviors.state.isUpdating.value;
     }
 
+    get selectionMode() {
+        return this.behaviors.interaction.selectionMode.value;
+    }
+
+    set selectionMode(mode: boolean) {
+        this.behaviors.interaction.selectionMode.next(mode);
+    }
+
     runTask<T>(task: Task<T>) {
         return this.tasks.run(task);
     }
@@ -265,6 +274,12 @@ export class PluginContext {
                 }
             }
         });
+
+        this.behaviors.interaction.selectionMode.subscribe(v => {
+            if (!v) {
+                this.managers.interactivity?.lociSelects.deselectAll();
+            }
+        })
     }
 
     private initBuiltInBehavior() {