diff --git a/src/mol-plugin/behavior/dynamic/camera.ts b/src/mol-plugin/behavior/dynamic/camera.ts index f2c194dc3ce7dc4353e015fb593b9ce17372463e..5c4e50a2b775511fc4b80edb314d23dca8710d1f 100644 --- a/src/mol-plugin/behavior/dynamic/camera.ts +++ b/src/mol-plugin/behavior/dynamic/camera.ts @@ -14,7 +14,7 @@ export const FocusLociOnSelect = PluginBehavior.create<{ minRadius: number, extr category: 'interaction', ctor: class extends PluginBehavior.Handler<{ minRadius: number, extraRadius: number }> { register(): void { - this.subscribeObservable(this.ctx.behaviors.canvas3d.click, ({ current, buttons, modifiers }) => { + this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, buttons, modifiers }) => { if (!this.ctx.canvas3d || buttons !== ButtonsType.Flag.Primary || !ModifiersKeys.areEqual(modifiers, ModifiersKeys.None)) return; const sphere = Loci.getBoundingSphere(current.loci); @@ -25,7 +25,7 @@ export const FocusLociOnSelect = PluginBehavior.create<{ minRadius: number, extr }, params: () => ({ minRadius: ParamDefinition.Numeric(8, { min: 1, max: 50, step: 1 }), - extraRadius: ParamDefinition.Numeric(4, { min: 1, max: 50, step: 1 }, { description: 'Value added to the boundning sphere radius of the Loci.' }) + extraRadius: ParamDefinition.Numeric(4, { min: 1, max: 50, step: 1 }, { description: 'Value added to the bounding-sphere radius of the Loci.' }) }), display: { name: 'Focus Loci on Select' } }); \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/representation.ts b/src/mol-plugin/behavior/dynamic/representation.ts index eb10a2b15262ace4111c6c0a207ddf6893a62e34..5760897bdb39ff0d34d820daae79756c45c81e61 100644 --- a/src/mol-plugin/behavior/dynamic/representation.ts +++ b/src/mol-plugin/behavior/dynamic/representation.ts @@ -6,44 +6,24 @@ */ import { MarkerAction } from '../../../mol-geo/geometry/marker-data'; -import { EmptyLoci } from '../../../mol-model/loci'; -import { StructureElement } from '../../../mol-model/structure'; import { PluginContext } from '../../../mol-plugin/context'; -import { Representation } from '../../../mol-repr/representation'; import { labelFirst } from '../../../mol-theme/label'; -import { ButtonsType } from '../../../mol-util/input/input-observer'; import { PluginBehavior } from '../behavior'; +import { Interaction } from '../../util/interaction'; export const HighlightLoci = PluginBehavior.create({ name: 'representation-highlight-loci', category: 'interaction', ctor: class extends PluginBehavior.Handler { - register(): void { - let prev: Representation.Loci = { loci: EmptyLoci, repr: void 0 }; - const sel = this.ctx.helpers.structureSelection; - - this.subscribeObservable(this.ctx.behaviors.canvas3d.highlight, ({ current, modifiers }) => { - if (!this.ctx.canvas3d) return; - - if (StructureElement.isLoci(current.loci)) { - let loci: StructureElement.Loci = current.loci; - if (modifiers && modifiers.shift) { - loci = sel.tryGetRange(loci) || loci; - } - - this.ctx.canvas3d.mark(prev, MarkerAction.RemoveHighlight); - const toHighlight = { loci, repr: current.repr }; - this.ctx.canvas3d.mark(toHighlight, MarkerAction.Highlight); - prev = toHighlight; - } else { - if (!Representation.Loci.areEqual(prev, current)) { - this.ctx.canvas3d.mark(prev, MarkerAction.RemoveHighlight); - this.ctx.canvas3d.mark(current, MarkerAction.Highlight); - prev = current; - } - } - - }); + private lociMarkProvider = (loci: Interaction.Loci, action: MarkerAction) => { + if (!this.ctx.canvas3d) return; + this.ctx.canvas3d.mark(loci, action) + } + register() { + this.ctx.lociHighlights.addProvider(this.lociMarkProvider) + } + unregister() { + this.ctx.lociHighlights.removeProvider(this.lociMarkProvider) } }, display: { name: 'Highlight Loci on Canvas' } @@ -53,51 +33,15 @@ export const SelectLoci = PluginBehavior.create({ name: 'representation-select-loci', category: 'interaction', ctor: class extends PluginBehavior.Handler { - register(): void { - const sel = this.ctx.helpers.structureSelection; - - const toggleSel = (current: Representation.Loci<StructureElement.Loci>) => { - if (sel.has(current.loci)) { - sel.remove(current.loci); - this.ctx.canvas3d.mark(current, MarkerAction.Deselect); - } else { - sel.add(current.loci); - this.ctx.canvas3d.mark(current, MarkerAction.Select); - } - } - - this.subscribeObservable(this.ctx.behaviors.canvas3d.click, ({ current, buttons, modifiers }) => { - if (!this.ctx.canvas3d) return; - - if (current.loci.kind === 'empty-loci') { - if (modifiers.control && buttons === ButtonsType.Flag.Secondary) { - // clear the selection on Ctrl + Right-Click on empty - const sels = sel.clear(); - for (const s of sels) this.ctx.canvas3d.mark({ loci: s }, MarkerAction.Deselect); - } - } else if (StructureElement.isLoci(current.loci)) { - if (modifiers.control && buttons === ButtonsType.Flag.Secondary) { - // select only the current element on Ctrl + Right-Click - const old = sel.get(current.loci.structure); - this.ctx.canvas3d.mark({ loci: old }, MarkerAction.Deselect); - sel.set(current.loci); - this.ctx.canvas3d.mark(current, MarkerAction.Select); - } else if (modifiers.control && buttons === ButtonsType.Flag.Primary) { - // toggle current element on Ctrl + Left-Click - toggleSel(current as Representation.Loci<StructureElement.Loci>); - } else if (modifiers.shift && buttons === ButtonsType.Flag.Primary) { - // try to extend sequence on Shift + Left-Click - let loci: StructureElement.Loci = current.loci; - if (modifiers && modifiers.shift) { - loci = sel.tryGetRange(loci) || loci; - } - toggleSel({ loci, repr: current.repr }); - } - } else { - if (!ButtonsType.has(buttons, ButtonsType.Flag.Secondary)) return; - this.ctx.canvas3d.mark(current, MarkerAction.Toggle); - } - }); + private lociMarkProvider = (loci: Interaction.Loci, action: MarkerAction) => { + if (!this.ctx.canvas3d) return; + this.ctx.canvas3d.mark(loci, action) + } + register() { + this.ctx.lociSelections.addProvider(this.lociMarkProvider) + } + unregister() { + this.ctx.lociSelections.removeProvider(this.lociMarkProvider) } }, display: { name: 'Select Loci on Canvas' } @@ -108,7 +52,7 @@ export const DefaultLociLabelProvider = PluginBehavior.create({ category: 'interaction', ctor: class implements PluginBehavior<undefined> { private f = labelFirst; - register(): void { this.ctx.lociLabels.addProvider(this.f); } + register() { this.ctx.lociLabels.addProvider(this.f); } unregister() { this.ctx.lociLabels.removeProvider(this.f); } constructor(protected ctx: PluginContext) { } }, diff --git a/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts b/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts index f883837006f41bc0afe6cbc42575f9f15288cac7..7221b05a7079b114198ccc9e7c408e3aad6dbfd0 100644 --- a/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts +++ b/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts @@ -121,7 +121,7 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W } }); - this.subscribeObservable(this.plugin.behaviors.canvas3d.click, ({ current, buttons, modifiers }) => { + this.subscribeObservable(this.plugin.behaviors.interaction.click, ({ current, buttons, modifiers }) => { if (buttons !== ButtonsType.Flag.Secondary) return; if (current.loci.kind === 'empty-loci') { diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts index 5388a1c473912fb8a279a8a5f125e9a35545087e..5eb0071828a1283ed66247e3cf00a9dff099252b 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts +++ b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts @@ -195,7 +195,7 @@ export namespace VolumeStreaming { } }); - this.subscribeObservable(this.plugin.behaviors.canvas3d.click, ({ current, buttons, modifiers }) => { + this.subscribeObservable(this.plugin.behaviors.interaction.click, ({ current, buttons, modifiers }) => { if (buttons !== ButtonsType.Flag.Secondary || this.params.view.name !== 'selection-box') return; if (current.loci.kind === 'empty-loci') { diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 5d696e47f10672bcf1457facff03b01aaf53ef14..4a4bd7f0a9716dc2a57e8d8c077386ca69349924 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -1,7 +1,8 @@ /** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import { List } from 'immutable'; @@ -32,10 +33,10 @@ import { TaskManager } from './util/task-manager'; import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version'; import { StructureElementSelectionManager } from './util/structure-element-selection'; import { SubstructureParentHelper } from './util/substructure-parent-helper'; -import { Representation } from '../mol-repr/representation'; import { ModifiersKeys } from '../mol-util/input/input-observer'; import { isProductionMode, isDebugMode } from '../mol-util/debug'; import { Model, Structure } from '../mol-model/structure'; +import { Interaction } from './util/interaction'; export class PluginContext { private disposed = false; @@ -72,9 +73,9 @@ export class PluginContext { isAnimating: this.ev.behavior<boolean>(false), isUpdating: this.ev.behavior<boolean>(false) }, - canvas3d: { - highlight: this.ev.behavior<Canvas3D.HighlightEvent>({ current: Representation.Loci.Empty }), - click: this.ev.behavior<Canvas3D.ClickEvent>({ current: Representation.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0 }) + interaction: { + highlight: this.ev.behavior<Interaction.HighlightEvent>({ current: Interaction.Loci.Empty }), + click: this.ev.behavior<Interaction.ClickEvent>({ current: Interaction.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0 }) }, labels: { highlight: this.ev.behavior<{ entries: ReadonlyArray<LociLabelEntry> }>({ entries: [] }) @@ -85,6 +86,8 @@ export class PluginContext { readonly layout: PluginLayout = new PluginLayout(this); readonly lociLabels: LociLabelManager; + readonly lociSelections: Interaction.LociSelectionManager; + readonly lociHighlights: Interaction.LociHighlightManager; readonly structureRepresentation = { registry: new StructureRepresentationRegistry(), @@ -227,6 +230,8 @@ export class PluginContext { this.initCustomParamEditors(); this.lociLabels = new LociLabelManager(this); + this.lociSelections = new Interaction.LociSelectionManager(this); + this.lociHighlights = new Interaction.LociHighlightManager(this); this.log.message(`Mol* Plugin ${PLUGIN_VERSION} [${PLUGIN_VERSION_DATE.toLocaleString()}]`); if (!isProductionMode) this.log.message(`Development mode enabled`); diff --git a/src/mol-plugin/ui/viewport.tsx b/src/mol-plugin/ui/viewport.tsx index c42dfe9e17abf851df5960eb9fb1be5cf9a17124..12c97fdfbb8ea8abd201715333b23d32c9b53e74 100644 --- a/src/mol-plugin/ui/viewport.tsx +++ b/src/mol-plugin/ui/viewport.tsx @@ -109,8 +109,8 @@ export class Viewport extends PluginUIComponent<{ }, ViewportState> { const canvas3d = this.plugin.canvas3d; this.subscribe(canvas3d.input.resize, this.handleResize); - this.subscribe(canvas3d.interaction.click, e => this.plugin.behaviors.canvas3d.click.next(e)); - this.subscribe(canvas3d.interaction.highlight, e => this.plugin.behaviors.canvas3d.highlight.next(e)); + this.subscribe(canvas3d.interaction.click, e => this.plugin.behaviors.interaction.click.next(e)); + this.subscribe(canvas3d.interaction.highlight, e => this.plugin.behaviors.interaction.highlight.next(e)); this.subscribe(this.plugin.layout.events.updated, () => { setTimeout(this.handleResize, 50); }); diff --git a/src/mol-plugin/util/interaction.ts b/src/mol-plugin/util/interaction.ts new file mode 100644 index 0000000000000000000000000000000000000000..226421501fb1bf5cf13f45a9f7b975b78c75d5f1 --- /dev/null +++ b/src/mol-plugin/util/interaction.ts @@ -0,0 +1,143 @@ +/** + * Copyright (c) 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 { Loci as ModelLoci, EmptyLoci } from '../../mol-model/loci'; +import { ModifiersKeys, ButtonsType } from '../../mol-util/input/input-observer'; +import { Representation } from '../../mol-repr/representation'; +import { StructureElement } from '../../mol-model/structure'; +import { MarkerAction } from '../../mol-geo/geometry/marker-data'; +import { StructureElementSelectionManager } from './structure-element-selection'; +import { PluginContext } from '../context'; + +export namespace Interaction { + export interface Loci<T extends ModelLoci = ModelLoci> { loci: T, repr?: Representation.Any } + + export namespace Loci { + export function areEqual(a: Loci, b: Loci) { + return a.repr === b.repr && ModelLoci.areEqual(a.loci, b.loci); + } + export const Empty: Loci = { loci: EmptyLoci }; + } + + export interface HighlightEvent { current: Loci, modifiers?: ModifiersKeys } + export interface ClickEvent { current: Loci, buttons: ButtonsType, modifiers: ModifiersKeys } + + export type LociMarkProvider = (loci: Loci, action: MarkerAction) => void + + export abstract class LociMarkManager<MarkEvent extends any> { + protected providers: LociMarkProvider[] = []; + protected sel: StructureElementSelectionManager + + addProvider(provider: LociMarkProvider) { + this.providers.push(provider); + } + + removeProvider(provider: LociMarkProvider) { + this.providers = this.providers.filter(p => p !== provider); + // TODO clear, then re-apply remaining providers + } + + toggleSel(current: Loci<ModelLoci>) { + if (this.sel.has(current.loci)) { + this.sel.remove(current.loci); + this.mark(current, MarkerAction.Deselect); + } else { + this.sel.add(current.loci); + this.mark(current, MarkerAction.Select); + } + } + + protected mark(current: Loci<ModelLoci>, action: MarkerAction) { + for (let p of this.providers) p(current, action); + } + + abstract apply(e: MarkEvent): void + + constructor(public ctx: PluginContext) { + this.sel = ctx.helpers.structureSelection + } + } + + export class LociHighlightManager extends LociMarkManager<HighlightEvent> { + private prev: Loci = { loci: EmptyLoci, repr: void 0 }; + + apply(e: HighlightEvent) { + const { current, modifiers } = e + if (StructureElement.isLoci(current.loci)) { + let loci: StructureElement.Loci = current.loci; + if (modifiers && modifiers.shift) { + loci = this.sel.tryGetRange(loci) || loci; + } + + this.mark(this.prev, MarkerAction.RemoveHighlight); + const toHighlight = { loci, repr: current.repr }; + this.mark(toHighlight, MarkerAction.Highlight); + this.prev = toHighlight; + } else { + if (!Loci.areEqual(this.prev, current)) { + this.mark(this.prev, MarkerAction.RemoveHighlight); + this.mark(current, MarkerAction.Highlight); + this.prev = current; + } + } + } + + constructor(public ctx: PluginContext) { + super(ctx) + ctx.behaviors.interaction.highlight.subscribe(e => this.apply(e)); + } + } + + export class LociSelectionManager extends LociMarkManager<ClickEvent> { + toggleSel(current: Loci<ModelLoci>) { + if (this.sel.has(current.loci)) { + this.sel.remove(current.loci); + this.mark(current, MarkerAction.Deselect); + } else { + this.sel.add(current.loci); + this.mark(current, MarkerAction.Select); + } + } + + apply(e: ClickEvent) { + const { current, buttons, modifiers } = e + if (current.loci.kind === 'empty-loci') { + if (modifiers.control && buttons === ButtonsType.Flag.Secondary) { + // clear the selection on Ctrl + Right-Click on empty + const sels = this.sel.clear(); + for (const s of sels) this.mark({ loci: s }, MarkerAction.Deselect); + } + } else if (StructureElement.isLoci(current.loci)) { + if (modifiers.control && buttons === ButtonsType.Flag.Secondary) { + // select only the current element on Ctrl + Right-Click + const old = this.sel.get(current.loci.structure); + this.mark({ loci: old }, MarkerAction.Deselect); + this.sel.set(current.loci); + this.mark(current, MarkerAction.Select); + } else if (modifiers.control && buttons === ButtonsType.Flag.Primary) { + // toggle current element on Ctrl + Left-Click + this.toggleSel(current as Representation.Loci<StructureElement.Loci>); + } else if (modifiers.shift && buttons === ButtonsType.Flag.Primary) { + // try to extend sequence on Shift + Left-Click + let loci: StructureElement.Loci = current.loci; + if (modifiers && modifiers.shift) { + loci = this.sel.tryGetRange(loci) || loci; + } + this.toggleSel({ loci, repr: current.repr }); + } + } else { + if (!ButtonsType.has(buttons, ButtonsType.Flag.Secondary)) return; + for (let p of this.providers) p(current, MarkerAction.Toggle); + } + } + + constructor(public ctx: PluginContext) { + super(ctx) + ctx.behaviors.interaction.click.subscribe(e => this.apply(e)); + } + } +} \ No newline at end of file diff --git a/src/mol-plugin/util/loci-label-manager.ts b/src/mol-plugin/util/loci-label-manager.ts index 869f519585ff6251a75e27ac581e34a578e31d41..d6d6f53993749676adafc8e1a715800eb30c6b9b 100644 --- a/src/mol-plugin/util/loci-label-manager.ts +++ b/src/mol-plugin/util/loci-label-manager.ts @@ -35,6 +35,6 @@ export class LociLabelManager { } constructor(public ctx: PluginContext) { - ctx.behaviors.canvas3d.highlight.subscribe(ev => ctx.behaviors.labels.highlight.next({ entries: this.getInfo(ev.current) })); + ctx.behaviors.interaction.highlight.subscribe(ev => ctx.behaviors.labels.highlight.next({ entries: this.getInfo(ev.current) })); } } \ No newline at end of file