From e8de45789f7c761c5fcfb5ae3c72d7f2ca74ec1f Mon Sep 17 00:00:00 2001 From: David Sehnal <david.sehnal@gmail.com> Date: Fri, 4 Oct 2019 16:52:36 +0200 Subject: [PATCH] mol-plugin: optimized sequence control --- .../skin/base/components/sequence.scss | 6 +- src/mol-plugin/ui/sequence/residue.tsx | 62 --------- src/mol-plugin/ui/sequence/sequence.tsx | 122 ++++++++++++------ src/mol-util/input/input-observer.ts | 3 +- 4 files changed, 91 insertions(+), 102 deletions(-) delete mode 100644 src/mol-plugin/ui/sequence/residue.tsx diff --git a/src/mol-plugin/skin/base/components/sequence.scss b/src/mol-plugin/skin/base/components/sequence.scss index 27b50d739..7c9f0441e 100644 --- a/src/mol-plugin/skin/base/components/sequence.scss +++ b/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 { diff --git a/src/mol-plugin/ui/sequence/residue.tsx b/src/mol-plugin/ui/sequence/residue.tsx deleted file mode 100644 index e2ffd4fc3..000000000 --- a/src/mol-plugin/ui/sequence/residue.tsx +++ /dev/null @@ -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>; - } -} \ No newline at end of file diff --git a/src/mol-plugin/ui/sequence/sequence.tsx b/src/mol-plugin/ui/sequence/sequence.tsx index 7034e510d..a8c9c3a48 100644 --- a/src/mol-plugin/ui/sequence/sequence.tsx +++ b/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>; diff --git a/src/mol-util/input/input-observer.ts b/src/mol-util/input/input-observer.ts index b2cc707b3..e75930916 100644 --- a/src/mol-util/input/input-observer.ts +++ b/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, -- GitLab