Skip to content
Snippets Groups Projects
Commit e8de4578 authored by David Sehnal's avatar David Sehnal
Browse files

mol-plugin: optimized sequence control

parent 3d2bd167
No related branches found
No related tags found
No related merge requests found
......@@ -20,11 +20,11 @@
overflow-y: auto;
overflow-x: hidden;
font-size: 90%;
}
.msp-sequence-wrapper-non-empty {
font-family: monospace;
}
}
.msp-sequence-wrapper {
span {
......
/**
* 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
......@@ -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>;
......
......@@ -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,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment