Browse Source

improved selection of residue ranges

- support brushing in sequence widget
- introduce reference-loci in selection manager
Alexander Rose 5 years ago
parent
commit
73ae95ed06

+ 81 - 26
src/mol-plugin/ui/sequence/sequence.tsx

@@ -9,12 +9,13 @@ import * as React from 'react'
 import { PluginUIComponent } from '../base';
 import { Interactivity } from '../../util/interactivity';
 import { MarkerAction } from '../../../mol-util/marker-action';
-import { ButtonsType, ModifiersKeys, getButtons, getModifiers } from '../../../mol-util/input/input-observer';
+import { ButtonsType, ModifiersKeys, getButtons, getModifiers, getButton } from '../../../mol-util/input/input-observer';
 import { SequenceWrapper } from './wrapper';
 import { StructureElement, StructureProperties, Unit } from '../../../mol-model/structure';
 import { Subject } from 'rxjs';
 import { debounceTime } from 'rxjs/operators';
 import { Color } from '../../../mol-util/color';
+import { OrderedSet } from '../../../mol-data/int';
 
 type SequenceProps = {
     sequenceWrapper: SequenceWrapper.Any,
@@ -55,8 +56,9 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
         this.plugin.interactivity.lociHighlights.addProvider(this.lociHighlightProvider)
         this.plugin.interactivity.lociSelects.addProvider(this.lociSelectionProvider)
 
-        this.subscribe(debounceTime<{ seqIdx: number, buttons: number, modifiers: ModifiersKeys }>(15)(this.highlightQueue), (e) => {
-            this.hover(e.seqIdx < 0 ? void 0 : e.seqIdx, e.buttons, e.modifiers);
+        this.subscribe(debounceTime<{ seqIdx: number, buttons: number, button: number, modifiers: ModifiersKeys }>(15)(this.highlightQueue), (e) => {
+            const loci = this.getLoci(e.seqIdx < 0 ? void 0 : e.seqIdx)
+            this.hover(loci, e.buttons, e.button, e.modifiers);
         });
 
         // this.updateMarker()
@@ -67,20 +69,34 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
         this.plugin.interactivity.lociSelects.removeProvider(this.lociSelectionProvider)
     }
 
-    hover(seqId: number | undefined, buttons: ButtonsType, modifiers: ModifiersKeys) {
-        const ev = { current: Interactivity.Loci.Empty, buttons, modifiers }
-        if (seqId !== undefined) {
-            const loci = this.props.sequenceWrapper.getLoci(seqId);
-            if (!StructureElement.Loci.isEmpty(loci)) ev.current = { loci };
+    getLoci(seqIdx: number | undefined) {
+        if (seqIdx !== undefined) {
+            const loci = this.props.sequenceWrapper.getLoci(seqIdx);
+            if (!StructureElement.Loci.isEmpty(loci)) return loci
+        }
+    }
+
+    getSeqIdx(e: React.MouseEvent) {
+        let seqIdx: number | undefined = undefined;
+        const el = e.target as HTMLElement;
+        if (el && el.getAttribute) {
+            seqIdx = el.hasAttribute('data-seqid') ? +el.getAttribute('data-seqid')! : undefined;
+        }
+        return seqIdx
+    }
+
+    hover(loci: StructureElement.Loci | undefined, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
+        const ev = { current: Interactivity.Loci.Empty, buttons, button, modifiers }
+        if (loci !== undefined && !StructureElement.Loci.isEmpty(loci)) {
+            ev.current = { loci };
         }
         this.plugin.behaviors.interaction.hover.next(ev)
     }
 
-    click(seqId: number | undefined, buttons: ButtonsType, modifiers: ModifiersKeys) {
-        const ev = { current: Interactivity.Loci.Empty, buttons, modifiers }
-        if (seqId !== undefined) {
-            const loci = this.props.sequenceWrapper.getLoci(seqId);
-            if (!StructureElement.Loci.isEmpty(loci)) ev.current = { loci };
+    click(loci: StructureElement.Loci | undefined, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
+        const ev = { current: Interactivity.Loci.Empty, buttons, button, modifiers }
+        if (loci !== undefined && !StructureElement.Loci.isEmpty(loci)) {
+            ev.current = { loci };
         }
         this.plugin.behaviors.interaction.click.next(ev)
     }
@@ -89,18 +105,48 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
         e.preventDefault()
     }
 
+    private mouseDownLoci: StructureElement.Loci | undefined = undefined
+
     mouseDown = (e: React.MouseEvent) => {
         e.stopPropagation();
 
+        const seqIdx = this.getSeqIdx(e)
+        const loci = this.getLoci(seqIdx)
         const buttons = getButtons(e.nativeEvent)
+        const button = getButton(e.nativeEvent)
         const modifiers = getModifiers(e.nativeEvent)
 
-        let seqIdx: number | undefined = undefined;
-        const el = e.target as HTMLElement;
-        if (el && el.getAttribute) {
-            seqIdx = el.hasAttribute('data-seqid') ? +el.getAttribute('data-seqid')! : undefined;
+        this.click(loci, buttons, button, modifiers);
+        this.mouseDownLoci = loci;
+    }
+
+    mouseUp = (e: React.MouseEvent) => {
+        e.stopPropagation();
+
+        // ignore mouse-up events without a bound loci
+        if (this.mouseDownLoci === undefined) return
+
+        const seqIdx = this.getSeqIdx(e)
+        const loci = this.getLoci(seqIdx)
+
+        if (loci && !StructureElement.Loci.areEqual(this.mouseDownLoci, loci)) {
+            const buttons = getButtons(e.nativeEvent)
+            const button = getButton(e.nativeEvent)
+            const modifiers = getModifiers(e.nativeEvent)
+
+            const ref = this.mouseDownLoci.elements[0]
+            const ext = loci.elements[0]
+            const min = Math.min(OrderedSet.min(ref.indices), OrderedSet.min(ext.indices))
+            const max = Math.max(OrderedSet.max(ref.indices), OrderedSet.max(ext.indices))
+
+            const range = StructureElement.Loci(loci.structure, [{
+                unit: ref.unit,
+                indices: OrderedSet.ofRange(min as StructureElement.UnitIndex, max as StructureElement.UnitIndex)
+            }]);
+
+            this.click(StructureElement.Loci.subtract(range, this.mouseDownLoci), buttons, button, modifiers);
         }
-        this.click(seqIdx, buttons, modifiers);
+        this.mouseDownLoci = undefined;
     }
 
     private getBackgroundColor(marker: number) {
@@ -188,33 +234,41 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
     mouseMove = (e: React.MouseEvent) => {
         e.stopPropagation();
 
+        const buttons = getButtons(e.nativeEvent)
+        const button = getButton(e.nativeEvent)
+        const modifiers = getModifiers(e.nativeEvent)
+
         const el = e.target as HTMLElement;
         if (!el || !el.getAttribute) {
             if (this.lastMouseOverSeqIdx === -1) return;
-
             this.lastMouseOverSeqIdx = -1;
-            const buttons = getButtons(e.nativeEvent)
-            const modifiers = getModifiers(e.nativeEvent)
-            this.highlightQueue.next({ seqIdx: -1, buttons, modifiers })
+            this.highlightQueue.next({ seqIdx: -1, buttons, button, modifiers })
             return;
         }
         const seqIdx = el.hasAttribute('data-seqid') ? +el.getAttribute('data-seqid')! : -1;
         if (this.lastMouseOverSeqIdx === seqIdx) {
             return;
         } else {
-            const buttons = getButtons(e.nativeEvent)
-            const modifiers = getModifiers(e.nativeEvent)
             this.lastMouseOverSeqIdx = seqIdx;
-            this.highlightQueue.next({ seqIdx, buttons, modifiers })
+            if (this.mouseDownLoci !== undefined) {
+                const loci = this.getLoci(seqIdx)
+                this.hover(loci, ButtonsType.Flag.None, ButtonsType.Flag.None, { ...modifiers, shift: true })
+            } else {
+                this.highlightQueue.next({ seqIdx, buttons, button, modifiers })
+            }
         }
     }
 
     mouseLeave = (e: React.MouseEvent) => {
+        e.stopPropagation();
+        this.mouseDownLoci = undefined;
+
         if (this.lastMouseOverSeqIdx === -1) return;
         this.lastMouseOverSeqIdx = -1;
         const buttons = getButtons(e.nativeEvent)
+        const button = getButton(e.nativeEvent)
         const modifiers = getModifiers(e.nativeEvent)
-        this.highlightQueue.next({ seqIdx: -1, buttons, modifiers })
+        this.highlightQueue.next({ seqIdx: -1, buttons, button, modifiers })
     }
 
     render() {
@@ -240,6 +294,7 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
             className='msp-sequence-wrapper msp-sequence-wrapper-non-empty'
             onContextMenu={this.contextMenu}
             onMouseDown={this.mouseDown}
+            onMouseUp={this.mouseUp}
             onMouseMove={this.mouseMove}
             onMouseLeave={this.mouseLeave}
             ref={this.parentDiv}

+ 20 - 24
src/mol-plugin/util/structure-element-selection.ts

@@ -25,6 +25,7 @@ export { StructureElementSelectionManager };
 class StructureElementSelectionManager {
     private entries = new Map<string, SelectionEntry>();
     private _latestLoci: LatestEntry[] = [];
+    private referenceLoci: Loci | undefined
 
     private getEntry(s: Structure) {
         const cell = this.plugin.helpers.substructureParent.get(s);
@@ -121,6 +122,7 @@ class StructureElementSelectionManager {
             if (entry) {
                 entry.selection = StructureElement.Loci.union(entry.selection, loci);
                 this.addLatest(loci);
+                this.referenceLoci = loci
                 this.plugin.events.interactivity.selectionUpdated.next();
                 return entry.selection;
             }
@@ -134,6 +136,7 @@ class StructureElementSelectionManager {
             if (entry) {
                 entry.selection = StructureElement.Loci.subtract(entry.selection, loci);
                 this.removeLatest(loci);
+                this.referenceLoci = loci
                 this.plugin.events.interactivity.selectionUpdated.next();
                 return StructureElement.Loci.isEmpty(entry.selection) ? EmptyLoci : entry.selection;
             }
@@ -146,6 +149,7 @@ class StructureElementSelectionManager {
             const entry = this.getEntry(loci.structure);
             if (entry) {
                 entry.selection = loci;
+                this.referenceLoci = undefined
                 this.plugin.events.interactivity.selectionUpdated.next()
                 return StructureElement.Loci.isEmpty(entry.selection) ? EmptyLoci : entry.selection;
             }
@@ -164,6 +168,7 @@ class StructureElementSelectionManager {
             if (!StructureElement.Loci.isEmpty(s.selection)) selections.push(s.selection);
             s.selection = StructureElement.Loci(s.selection.structure, []);
         }
+        this.referenceLoci = undefined
         this.plugin.events.interactivity.selectionUpdated.next()
         return selections;
     }
@@ -190,10 +195,14 @@ class StructureElementSelectionManager {
         const entry = this.getEntry(loci.structure);
         if (!entry) return;
 
-        let xs = loci.elements[0];
+        const xs = loci.elements[0];
         if (!xs) return;
+
+        const ref = this.referenceLoci
+        if (!ref || !StructureElement.Loci.is(ref) || ref.structure.root !== loci.structure.root) return;
+
         let e: StructureElement.Loci['elements'][0] | undefined;
-        for (const _e of entry.selection.elements) {
+        for (const _e of ref.elements) {
             if (xs.unit === _e.unit) {
                 e = _e;
                 break;
@@ -201,7 +210,9 @@ class StructureElementSelectionManager {
         }
         if (!e) return;
 
-        return tryGetElementRange(entry.selection.structure, e, xs)
+        if (xs.unit !== e.unit) return;
+
+        return getElementRange(loci.structure.root, e, xs)
     }
 
     private prevHighlight: StructureElement.Loci | undefined = void 0;
@@ -228,6 +239,7 @@ class StructureElementSelectionManager {
             this.entries.delete(ref);
             // TODO: property update the latest loci
             this._latestLoci = [];
+            this.referenceLoci = undefined
         }
     }
 
@@ -237,8 +249,9 @@ class StructureElementSelectionManager {
         if (this.entries.has(ref)) {
             if (!PluginStateObject.Molecule.Structure.is(oldObj) || oldObj === obj || oldObj.data === obj.data) return;
 
-            // TODO: property update the latest loci
+            // TODO: property update the latest loci & reference loci
             this._latestLoci = [];
+            this.referenceLoci = undefined
 
             // remap the old selection to be related to the new object if possible.
             if (Structure.areUnitAndIndicesEqual(oldObj.data, obj.data)) {
@@ -326,26 +339,9 @@ function remapSelectionEntry(e: SelectionEntry, s: Structure): SelectionEntry {
 /**
  * Assumes `ref` and `ext` belong to the same unit in the same structure
  */
-function tryGetElementRange(structure: Structure, ref: StructureElement.Loci['elements'][0], ext: StructureElement.Loci['elements'][0]) {
-
-    const refMin = OrderedSet.min(ref.indices)
-    const refMax = OrderedSet.max(ref.indices)
-    const extMin = OrderedSet.min(ext.indices)
-    const extMax = OrderedSet.max(ext.indices)
-
-    let min: number
-    let max: number
-
-    if (refMax < extMin) {
-        min = refMax + 1
-        max = extMax
-    } else if (extMax < refMin) {
-        min = extMin
-        max = refMin - 1
-    } else {
-        // TODO handle range overlap cases
-        return
-    }
+function getElementRange(structure: Structure, ref: StructureElement.Loci['elements'][0], ext: StructureElement.Loci['elements'][0]) {
+    const min = Math.min(OrderedSet.min(ref.indices), OrderedSet.min(ext.indices))
+    const max = Math.max(OrderedSet.max(ref.indices), OrderedSet.max(ext.indices))
 
     return StructureElement.Loci(structure, [{
         unit: ref.unit,