Skip to content

Commit

Permalink
Merge pull request #782 from Conifer-Point/contextmenu-updates
Browse files Browse the repository at this point in the history
Contextmenu updates.  Thanks!
  • Loading branch information
dkoes authored Apr 6, 2024
2 parents 83f7797 + f93dae7 commit 9ecd79c
Show file tree
Hide file tree
Showing 5 changed files with 363 additions and 18 deletions.
4 changes: 4 additions & 0 deletions src/GLShape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ export class GLShape {
shape.hoverable = stylespec.hoverable ? true : false;
shape.hover_callback = makeFunction(stylespec.hover_callback);
shape.unhover_callback = makeFunction(stylespec.unhover_callback);
shape.contextMenuEnabled = !!stylespec.contextMenuEnabled;

shape.hidden = stylespec.hidden;
shape.frame = stylespec.frame;
Expand All @@ -645,6 +646,7 @@ export class GLShape {
hoverable = false;
hover_callback: Func;
unhover_callback: Func;
contextMenuEnabled: boolean = false;
frame: any;
side = DoubleSide;
shapePosition: any;
Expand Down Expand Up @@ -1581,6 +1583,8 @@ export interface ShapeSpec {
hover_callback?: Func;
/** unhover callback */
unhover_callback?: Func;
/** if true, user can right-click or long press to trigger callback */
contextMenuEnabled?: boolean;
/** if set, only display in this frame of an animation */
frame?: number;
side?: number;
Expand Down
63 changes: 45 additions & 18 deletions src/GLViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ export class GLViewer {
private labels: Label[] = [];
private clickables = []; //things you can click on
private hoverables = []; //things you can hover over
private contextMenuEnabledAtoms = []; // atoms with context menu
private contextMenuEnabledObjects = []; // atoms and shapes with context menu
private current_hover: any = null;
private hoverDuration = 500;
private longTouchDuration = 1000;
private viewer_frame = 0;
private WIDTH: number;
private HEIGHT: number;
Expand Down Expand Up @@ -106,6 +107,7 @@ export class GLViewer {

private mouseButton: any;
private hoverTimeout: any;
private longTouchTimeout: any;

private divwatcher: any;
private spinInterval: any;
Expand Down Expand Up @@ -296,7 +298,7 @@ export class GLViewer {
private updateClickables() {
this.clickables.splice(0, this.clickables.length);
this.hoverables.splice(0, this.hoverables.length);
this.contextMenuEnabledAtoms.splice(0, this.contextMenuEnabledAtoms.length);
this.contextMenuEnabledObjects.splice(0, this.contextMenuEnabledObjects.length);

for (let i = 0, il = this.models.length; i < il; i++) {
let model = this.models[i];
Expand All @@ -320,9 +322,9 @@ export class GLViewer {
this.clickables.push(atoms[m]);
}

// add atoms into contextMenuEnabledAtoms
// add atoms into contextMenuEnabledObjects
for (let m = 0; m < contextMenuEnabled_atom.length; m++) {
this.contextMenuEnabledAtoms.push(contextMenuEnabled_atom[m]);
this.contextMenuEnabledObjects.push(contextMenuEnabled_atom[m]);
}

}
Expand All @@ -336,6 +338,9 @@ export class GLViewer {
if (shape && shape.hoverable) {
this.hoverables.push(shape);
}
if (shape && shape.contextMenuEnabled) {
this.contextMenuEnabledObjects.push(shape);
}
}
};

Expand All @@ -350,7 +355,16 @@ export class GLViewer {
selected.callback = makeFunction(selected.callback);
}
if (typeof (selected.callback) === "function") {
selected.callback(selected, this._viewer, event, this.container, intersects);
// Suppress click callbacks when context menu will be invoked.
// This only applies to clicks from "mouseup" events after right-click.
// Clicks from "touchend" after longtouch contextmenu are suppressed
// in _handleContextMenu.
const isContextMenu = this.mouseButton === 3
&& this.contextMenuEnabledObjects.includes(selected)
&& this.userContextMenuHandler;
if (!isContextMenu) {
selected.callback(selected, this._viewer, event, this.container, intersects);
}
}
}
}
Expand Down Expand Up @@ -896,19 +910,26 @@ export class GLViewer {
this.cslabFar = this.slabFar;

let self = this;
setTimeout(function () {
if (ev.targetTouches) {
if (ev.targetTouches && ev.targetTouches.length === 1) {
this.longTouchTimeout = setTimeout(function () {
if (self.touchHold == true) {
// console.log('Touch hold', x,y);
self.glDOM = self.renderer.domElement;
self.glDOM.dispatchEvent(new Event('contextmenu'));
const touch = ev.targetTouches[0];
const newEvent = new PointerEvent('contextmenu', {
...ev,
pageX: touch.pageX, pageY: touch.pageY,
screenX: touch.screenX, screenY: touch.screenY,
clientX: touch.clientX, clientY: touch.clientY,
});
self.glDOM.dispatchEvent(newEvent);
}
else {
// console.log('Touch hold ended earlier');

}
}
}, 1000);
}, this.longTouchDuration);
}

};

Expand Down Expand Up @@ -1100,6 +1121,13 @@ export class GLViewer {

if (!this.isDragging)
return;

// Cancel longtouch timer to avoid invoking context menu if dragged away from start
if (ev.targetTouches && (ev.targetTouches.length > 1 ||
(ev.targetTouches.length === 1 && !this.closeEnoughForClick(ev)))) {
clearTimeout(this.longTouchTimeout);
}

var dx = (x - this.mouseStartX) / this.WIDTH;
var dy = (y - this.mouseStartY) / this.HEIGHT;
// check for pinch
Expand Down Expand Up @@ -1154,20 +1182,15 @@ export class GLViewer {

public _handleContextMenu(ev) {
ev.preventDefault();
var newX = this.getX(ev);
var newY = this.getY(ev);

if (newX != this.mouseStartX || newY != this.mouseStartY) {
return;
} else {
if (this.closeEnoughForClick(ev)) {
var x = this.mouseStartX;
var y = this.mouseStartY;
var offset = this.canvasOffset();
let mouse = this.mouseXY(x, y);
let mouseX = mouse.x;
let mouseY = mouse.y;

let intersects = this.targetedObjects(mouseX, mouseY, this.contextMenuEnabledAtoms);
let intersects = this.targetedObjects(mouseX, mouseY, this.contextMenuEnabledObjects);
var selected = null;
if (intersects.length) {
selected = intersects[0].clickable;
Expand All @@ -1177,7 +1200,11 @@ export class GLViewer {
var x = this.mouseStartX - offset.left;
var y = this.mouseStartY - offset.top;
if (this.userContextMenuHandler) {
this.userContextMenuHandler(selected, x, y, intersects);
this.userContextMenuHandler(selected, x, y, intersects, ev);
// We've processed this as a context menu evt; ignore further mouseup / touchend.
// This is really for touchend after longtouch, since the mouseup for right-click
// occurs before the contextmenu event.
this.isDragging = false;
}
}
};
Expand Down
37 changes: 37 additions & 0 deletions tests/auto/RENDER_TEST_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Render test notes for 3Dmol.js contributors

## Overview
Automated "render tests" for the 3Dmol.js viewer live in tests/auto/tests.

GitHub Actions executes these tests in two ways after pushes:
1. glcheck runs each test, exporting the canvas image, and comparing it to a reference image (located in tests/glcheck/reference-images)
2. jest runs the tests with coverage checking enabled.

The test files are pre-processed by python code to produce the actual test cases that will get executed by the test harnesses:
1. glcheck: `glcheck/generate_glcheck_render_tests.py` creates tests/glcheck/render-tests/*
2. jest: `jest/generate_jest_render_tests.py` creates tests/jest/render.test.js

glcheck uses Puppeteer to render the canvas, whereas jest uses jsdom.
This can lead to slight differences in test behavior.

## Running test suites
1. glcheck: `npm run glcheck`
2. jest coverage: `npm run cover`

## Running individual tests:
To run test `MYTEST`:
1. glcheck
```
npm run generate:glcheck
npx glcheck --config ./tests/glcheck/glcheck.config.json --only tests/glcheck/render-tests/MYTEST.html
```

2. jest
```
npm run generate:jest
npx jest --coverage --testNamePattern "MYTEST"
```

3. browser
If you set up a way to serve tests/auto/generate_test.cgi, you can view the render test results in the browser:
eg: https://3dmol.org/tests/auto/generate_test.cgi?test=testclick
Loading

0 comments on commit 9ecd79c

Please sign in to comment.