Skip to content

Commit

Permalink
Support horizontal zooming selection with right mouse button
Browse files Browse the repository at this point in the history
Allow zooming selection with the mouse with right button drag. Change
the cursor to 'col-resize' while zooming selection is ongoing.

Restore the cursor to 'default' when zooming selection is ended by
releasing the right mouse button.

Add a zooming selection transparent overlay while zoom selection is
ongoing. Update the overlay when the view range is changed externally.

Prevent the context menu popup on right mouse click on time graph.

Ensure right mouse button mouse up outside of time graph canvas is
properly detected and handled.

Prevent selection of row element on mouse click during mouse panning and
zooming selection.

Signed-off-by: Patrick Tasse <patrick.tasse@ericsson.com>
  • Loading branch information
PatrickTasse committed Apr 20, 2021
1 parent 7e59702 commit 885dec8
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 1 deletion.
1 change: 1 addition & 0 deletions doc/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ A reusable time axis component, that can be used independently of the other comp
* WASD or IJKL keystrokes are available for zooming and navigation. The zooming is centered on the mouse cursor position.
* Horizontal zooming can be performed using Ctrl+mouse wheel. The zooming is centered on the mouse cursor position.
* Horizontal panning can be performed using the middle mouse button or Ctrl+left mouse button.
* Horizontal zooming selection can be performed using the right mouse button.
* The view is connected to a time controller instance and synchronizes its viewport, zoom level cursors bi-directionally.

## Data Model
Expand Down
26 changes: 26 additions & 0 deletions timeline-chart/src/layer/time-graph-chart-cursors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,17 @@ export class TimeGraphChartCursors extends TimeGraphChartLayer {
}
});
this.stage.on('mousemove', (event: PIXI.InteractionEvent) => {
this.mouseButtons = event.data.buttons;
if (this.mouseSelecting && this.unitController.selectionRange) {
if ((this.mouseButtons & 1) === 0) {
// handle missed button mouseup event
this.mouseSelecting = false;
const orig = event.data.originalEvent;
if (!orig.shiftKey || orig.ctrlKey || orig.altKey) {
this.stage.cursor = 'default';
}
return;
}
const mouseX = event.data.global.x;
const xStartPos = this.unitController.selectionRange.start;
const xEndPos = this.unitController.viewRange.start + (mouseX / this.stateController.zoomFactor);
Expand All @@ -106,6 +116,22 @@ export class TimeGraphChartCursors extends TimeGraphChartLayer {
};
this.stage.on('mouseup', mouseUpHandler);
this.stage.on('mouseupoutside', mouseUpHandler);
// right mouse button is not detected on stage
this.onCanvasEvent('mousedown', (e: MouseEvent) => {
this.mouseButtons = e.buttons;
// if right button is pressed
if (e.button === 2) {
// this is the only way to detect mouseup outside of right button
const mouseUpListener = (e: MouseEvent) => {
this.mouseButtons = e.buttons;
if (e.button === 2) {
document.removeEventListener('mouseup', mouseUpListener);
}
}
document.addEventListener('mouseup', mouseUpListener);
}
});

this.unitController.onViewRangeChanged(() => this.update());
this.unitController.onSelectionRangeChange(() => this.update());
this.update();
Expand Down
89 changes: 88 additions & 1 deletion timeline-chart/src/layer/time-graph-chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { TimeGraphComponent, TimeGraphRect, TimeGraphStyledRect } from "../compo
import { TimeGraphChartLayer } from "./time-graph-chart-layer";
import { TimeGraphRowController } from "../time-graph-row-controller";
import { TimeGraphAnnotationComponent, TimeGraphAnnotationComponentOptions, TimeGraphAnnotationStyle } from "../components/time-graph-annotation";
import { TimeGraphRectangle } from "../components/time-graph-rectangle";

export interface TimeGraphRowElementMouseInteractions {
click?: (el: TimeGraphRowElement, ev: PIXI.InteractionEvent) => void
Expand Down Expand Up @@ -49,9 +50,13 @@ export class TimeGraphChart extends TimeGraphChartLayer {
protected isNavigating: boolean;

protected mousePanning: boolean = false;
protected mouseZooming: boolean = false;
protected mouseButtons: number = 0;
protected mouseDownButton: number;
protected mouseStartX: number;
protected mouseEndX: number;
protected mouseZoomingStart: number;
protected zoomingSelection?: TimeGraphRectangle;

constructor(id: string,
protected providers: TimeGraphChartProviders,
Expand All @@ -63,6 +68,7 @@ export class TimeGraphChart extends TimeGraphChartLayer {
}

protected afterAddToContainer() {
this.stage.cursor = 'default';
let mousePositionX = 1;
const horizontalDelta = 3;
let triggerKeyEvent = false;
Expand Down Expand Up @@ -174,11 +180,26 @@ export class TimeGraphChart extends TimeGraphChartLayer {
this.stage.cursor = 'grabbing';
});
this.stage.on('mousemove', (event: PIXI.InteractionEvent) => {
this.mouseButtons = event.data.buttons;
if (this.mousePanning) {
if ((this.mouseDownButton == 1 && (this.mouseButtons & 4) === 0) ||
(this.mouseDownButton == 0 && (this.mouseButtons & 1) === 0)) {
// handle missed button mouseup event
this.mousePanning = false;
const orig = event.data.originalEvent;
if (!orig.ctrlKey || orig.shiftKey || orig.altKey) {
this.stage.cursor = 'default';
}
return;
}
const horizontalDelta = this.mouseStartX - event.data.global.x;
moveHorizontally(horizontalDelta);
this.mouseStartX = event.data.global.x;
}
if (this.mouseZooming) {
this.mouseEndX = event.data.global.x;
this.updateZoomingSelection();
}
});
const mouseUpHandler = (event: PIXI.InteractionEvent) => {
this.mouseButtons = event.data.buttons;
Expand Down Expand Up @@ -216,6 +237,43 @@ export class TimeGraphChart extends TimeGraphChartLayer {
this.onCanvasEvent('keyup', keyUpHandler);
this.onCanvasEvent('mousewheel', mouseWheelHandler);
this.onCanvasEvent('wheel', mouseWheelHandler);
this.onCanvasEvent('contextmenu', (e: MouseEvent) => {
e.preventDefault();
});
const mouseDownListener = (e: MouseEvent) => {
this.mouseButtons = e.buttons;
// if only right button is pressed
if (e.button === 2 && e.buttons === 2 && this.stage.cursor === 'default') {
this.mouseZooming = true;
this.mouseDownButton = e.button;
this.mouseStartX = e.offsetX;
this.mouseEndX = e.offsetX;
this.mouseZoomingStart = this.unitController.viewRange.start + (this.mouseStartX / this.stateController.zoomFactor);
this.stage.cursor = 'col-resize';
// this is the only way to detect mouseup outside of right button
document.addEventListener('mouseup', mouseUpListener);
this.updateZoomingSelection();
}
};
const mouseUpListener = (e: MouseEvent) => {
this.mouseButtons = e.buttons;
if (e.button === this.mouseDownButton && this.mouseZooming) {
this.mouseZooming = false;
this.mouseEndX = e.offsetX;
const start = this.mouseZoomingStart;
const end = this.unitController.viewRange.start + (this.mouseEndX / this.stateController.zoomFactor);
if (start !== end) {
this.unitController.viewRange = {
start: Math.max(Math.min(start, end), this.unitController.viewRange.start),
end: Math.min(Math.max(start, end), this.unitController.viewRange.end)
}
}
this.stage.cursor = 'default';
document.removeEventListener('mouseup', mouseUpListener);
this.updateZoomingSelection();
}
};
this.onCanvasEvent('mousedown', mouseDownListener);

this.rowController.onVerticalOffsetChangedHandler(verticalOffset => {
this.layer.position.y = -verticalOffset;
Expand All @@ -226,6 +284,9 @@ export class TimeGraphChart extends TimeGraphChartLayer {
if (!this.fetching && this.unitController.viewRangeLength !== 0) {
this.maybeFetchNewData();
}
if (this.mouseZooming) {
this.updateZoomingSelection();
}
});
if (this.unitController.viewRangeLength && this.stateController.canvasDisplayWidth) {
this.maybeFetchNewData();
Expand All @@ -243,6 +304,27 @@ export class TimeGraphChart extends TimeGraphChartLayer {
this.updateScaleAndPosition();
}

updateZoomingSelection() {
if (this.zoomingSelection) {
this.removeChild(this.zoomingSelection);
delete this.zoomingSelection;
}
if (this.mouseZooming) {
const mouseStartX = (this.mouseZoomingStart - this.unitController.viewRange.start) * this.stateController.zoomFactor;
this.zoomingSelection = new TimeGraphRectangle({
color: 0xbbbbbb,
opacity: 0.2,
position: {
x: mouseStartX,
y: 0
},
height: this.layer.height,
width: this.mouseEndX - mouseStartX
});
this.addChild(this.zoomingSelection);
}
}

protected async maybeFetchNewData(update?: boolean) {
const resolution = this.unitController.viewRangeLength / this.stateController.canvasDisplayWidth;
const viewRange = this.unitController.viewRange;
Expand All @@ -264,6 +346,9 @@ export class TimeGraphChart extends TimeGraphChartLayer {
if (this.isNavigating) {
this.selectStateInNavigation();
}
if (this.mouseZooming) {
this.updateZoomingSelection();
}
}
} finally {
this.fetching = false;
Expand Down Expand Up @@ -397,7 +482,9 @@ export class TimeGraphChart extends TimeGraphChartLayer {
protected addElementInteractions(el: TimeGraphRowElement) {
el.displayObject.interactive = true;
el.displayObject.on('click', ((e: PIXI.InteractionEvent) => {
this.selectRowElement(el.model);
if (!this.mousePanning && !this.mouseZooming) {
this.selectRowElement(el.model);
}
if (this.rowElementMouseInteractions && this.rowElementMouseInteractions.click) {
this.rowElementMouseInteractions.click(el, e);
}
Expand Down

0 comments on commit 885dec8

Please sign in to comment.