Browse Source

mol-plugin: optimized sequence control

David Sehnal 5 years ago
parent
commit
e8de45789f

+ 3 - 3
src/mol-plugin/skin/base/components/sequence.scss

@@ -20,10 +20,10 @@
     overflow-y: auto;
     overflow-x: hidden;
     font-size: 90%;
+}
 
-    .msp-sequence-wrapper-non-empty {
-        font-family: monospace;
-    }
+.msp-sequence-wrapper-non-empty {
+    font-family: monospace;
 }
 
 .msp-sequence-wrapper {

+ 0 - 62
src/mol-plugin/ui/sequence/residue.tsx

@@ -1,62 +0,0 @@
-/**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import * as React from 'react'
-import { PurePluginUIComponent } from '../base';
-import { getButtons, getModifiers } from '../../../mol-util/input/input-observer';
-import { Sequence } from './sequence';
-import { Color } from '../../../mol-util/color';
-
-export class Residue extends PurePluginUIComponent<{ seqIdx: number, label: string, parent: Sequence<any>, marker: number, color: Color }> {
-
-    mouseEnter = (e: React.MouseEvent) => {
-        const buttons = getButtons(e.nativeEvent)
-        const modifiers = getModifiers(e.nativeEvent)
-        this.props.parent.hover(this.props.seqIdx, buttons, modifiers);
-    }
-
-    mouseLeave = (e: React.MouseEvent) => {
-        const buttons = getButtons(e.nativeEvent)
-        const modifiers = getModifiers(e.nativeEvent)
-        this.props.parent.hover(undefined, buttons, modifiers);
-    }
-
-    mouseDown = (e: React.MouseEvent) => {
-        const buttons = getButtons(e.nativeEvent)
-        const modifiers = getModifiers(e.nativeEvent)
-        this.props.parent.click(this.props.seqIdx, buttons, modifiers);
-        e.stopPropagation() // so that `parent.mouseDown` is not called
-    }
-
-    get backgroundColor() {
-        // TODO make marker color configurable
-        if (this.props.marker === 0) return ''
-        if (this.props.marker % 2 === 0) return 'rgb(51, 255, 25)' // selected
-        if (this.props.marker === undefined) console.error('unexpected marker value')
-        return 'rgb(255, 102, 153)' // highlighted
-    }
-
-    get margin() {
-        return this.props.label.length > 1
-            ? (this.props.seqIdx === 0 ? `0px 2px 0px 0px` : `0px 2px 0px 2px`)
-            : undefined
-    }
-
-    render() {
-        return <span
-            onMouseEnter={this.mouseEnter}
-            onMouseLeave={this.mouseLeave}
-            onMouseDown={this.mouseDown}
-            style={{
-                color: Color.toStyle(this.props.color),
-                backgroundColor: this.backgroundColor,
-                margin: this.margin
-            }}>
-            {this.props.label}
-        </span>;
-    }
-}

+ 86 - 36
src/mol-plugin/ui/sequence/sequence.tsx

@@ -9,49 +9,38 @@ 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 { ValueBox } from '../../../mol-util';
-import { Residue } from './residue';
+import { ButtonsType, ModifiersKeys, getButtons, getModifiers, MouseModifiers } from '../../../mol-util/input/input-observer';
 import { SequenceWrapper } from './wrapper';
 import { StructureElement } from '../../../mol-model/structure';
+import { Subject } from 'rxjs';
+import { debounceTime } from 'rxjs/operators';
+import { Color } from '../../../mol-util/color';
 
 type SequenceProps = { sequenceWrapper: SequenceWrapper.Any }
-type SequenceState = { markerData: ValueBox<Uint8Array> }
 
-function getState(markerData: ValueBox<Uint8Array>) {
-    return { markerData: ValueBox.withValue(markerData, markerData.value) }
-}
-
-// TODO: this is really inefficient and should be done using a canvas.
-export class Sequence<P extends SequenceProps> extends PluginUIComponent<P, SequenceState> {
-    state = {
-        markerData: ValueBox.create(this.props.sequenceWrapper.markerArray)
-    }
-
-    private setMarkerData(markerData: ValueBox<Uint8Array>) {
-        this.setState(getState(markerData))
-    }
+// TODO: this is somewhat inefficient and should be done using a canvas.
+export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
+    private parentDiv = React.createRef<HTMLDivElement>();
+    private lastMouseOverSeqIdx = -1;
+    private highlightQueue = new Subject<{ seqIdx: number, buttons: number, modifiers: MouseModifiers }>();
 
     private lociHighlightProvider = (loci: Interactivity.Loci, action: MarkerAction) => {
         const changed = this.props.sequenceWrapper.markResidue(loci.loci, action)
-        if (changed) this.setMarkerData(this.state.markerData)
+        if (changed) this.updateMarker();
     }
 
     private lociSelectionProvider = (loci: Interactivity.Loci, action: MarkerAction) => {
         const changed = this.props.sequenceWrapper.markResidue(loci.loci, action)
-        if (changed) this.setMarkerData(this.state.markerData)
-    }
-
-    static getDerivedStateFromProps(nextProps: SequenceProps, prevState: SequenceState): SequenceState | null {
-        if (prevState.markerData.value !== nextProps.sequenceWrapper.markerArray) {
-            return getState(ValueBox.create(nextProps.sequenceWrapper.markerArray))
-        }
-        return null
+        if (changed) this.updateMarker();
     }
 
     componentDidMount() {
         this.plugin.interactivity.lociHighlights.addProvider(this.lociHighlightProvider)
         this.plugin.interactivity.lociSelects.addProvider(this.lociSelectionProvider)
+
+        this.subscribe(debounceTime<{ seqIdx: number, buttons: number, modifiers: MouseModifiers }>(15)(this.highlightQueue), (e) => {
+            this.hover(e.seqIdx < 0 ? void 0 : e.seqIdx, e.buttons, e.modifiers);
+        });
     }
 
     componentWillUnmount() {
@@ -82,31 +71,92 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P, Sequ
     }
 
     mouseDown = (e: React.MouseEvent) => {
+        e.stopPropagation();
+
+        const buttons = getButtons(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(seqIdx, buttons, modifiers);
+    }
+
+    private getBackgroundColor(marker: number) {
+        // TODO: make marker color configurable
+        if (typeof marker === 'undefined') console.error('unexpected marker value')
+        return marker === 0 ? '' : marker % 2 === 0 ? 'rgb(51, 255, 25)' /* selected */ : 'rgb(255, 102, 153)' /* highlighted */;
+    }
+
+    private residue(seqIdx: number, label: string, marker: number, color: Color) {
+        const margin = label.length > 1 ? (seqIdx === 0 ? `0px 2px 0px 0px` : `0px 2px 0px 2px`) : void 0
+        return <span key={seqIdx} data-seqid={seqIdx} style={{ color: Color.toStyle(color), backgroundColor: this.getBackgroundColor(marker), margin }}>{label}</span>;
+    }
+
+    private updateMarker() {
+        if (!this.parentDiv.current) return;
+        const xs = this.parentDiv.current.children;
+        const markerData = this.props.sequenceWrapper.markerArray;
+
+        for (let i = 0, _i = markerData.length; i < _i; i++) {
+            const span = xs[i] as HTMLSpanElement;
+            if (!span) continue;
+
+            const backgroundColor = this.getBackgroundColor(markerData[i]);
+            if (span.style.backgroundColor !== backgroundColor) span.style.backgroundColor = backgroundColor;
+        }
+    }
+
+    mouseMove = (e: React.MouseEvent) => {
+        e.stopPropagation();
+
+        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 })
+            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 })
+        }
+    }
+
+    mouseLeave = (e: React.MouseEvent) => {
+        if (this.lastMouseOverSeqIdx === -1) return;
+        this.lastMouseOverSeqIdx = -1;
         const buttons = getButtons(e.nativeEvent)
         const modifiers = getModifiers(e.nativeEvent)
-        this.click(undefined, buttons, modifiers);
+        this.highlightQueue.next({ seqIdx: -1, buttons, modifiers })
     }
 
     render() {
-        const { markerData } = this.state;
+        const markerData = this.props.sequenceWrapper.markerArray;
         const sw = this.props.sequenceWrapper
 
         const elems: JSX.Element[] = [];
         for (let i = 0, il = sw.length; i < il; ++i) {
-            elems[elems.length] = <Residue
-                seqIdx={i}
-                label={sw.residueLabel(i)}
-                parent={this}
-                marker={markerData.value[i]}
-                color={sw.residueColor(i)}
-                key={i}
-            />;
+            elems[elems.length] = this.residue(i, sw.residueLabel(i), markerData[i], sw.residueColor(i));
+            // TODO: add seq idx markers every N residues? Would need to modify "updateMarker"
         }
 
         return <div
             className='msp-sequence-wrapper msp-sequence-wrapper-non-empty'
             onContextMenu={this.contextMenu}
             onMouseDown={this.mouseDown}
+            onMouseMove={this.mouseMove}
+            ref={this.parentDiv}
         >
             {elems}
         </div>;

+ 2 - 1
src/mol-util/input/input-observer.ts

@@ -37,7 +37,8 @@ export function getButtons(event: MouseEvent | Touch) {
     return 0
 }
 
-export function getModifiers(event: MouseEvent | Touch) {
+export type MouseModifiers = { alt: boolean, shift: boolean, control: boolean, meta: boolean }
+export function getModifiers(event: MouseEvent | Touch): MouseModifiers {
     return {
         alt: 'altKey' in event ? event.altKey : false,
         shift: 'shiftKey' in event ? event.shiftKey : false,