Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(doubleClick): Add Double click detection (#375) #382

Merged
merged 9 commits into from
Jan 26, 2023

Conversation

jbocce
Copy link
Contributor

@jbocce jbocce commented Jan 23, 2023

Added a timeout in mouseDownListener to determine if a double click occurs. If a double click occurs during the timeout,
then any mouse down and up events that occurred during the timeout are suppressed. If the timeout expires, then the first mouse down and up events during the timeout are fired and any browser double click event that might occur is suppressed/ignored. Double clicks that occur during tool interaction are suppressed Double click event capture phase listeners are used to more reliably suppress double clicks.

The related issue is: #375

Added a timeout in mouseDownListener to determine if a double click occurs.
If a double click occurs during the timeout,
then any mouse down and up events that occurred during the timeout are suppressed.
If the timeout expires, then the first mouse down and up events during the timeout
are fired and any browser double click event that might occur is suppressed/ignored.
Double clicks that occur during tool interaction are suppressed
Double click event capture phase listeners are used to more reliably suppress double clicks.
@netlify
Copy link

netlify bot commented Jan 23, 2023

Deploy Preview for cornerstone-3d-docs ready!

Name Link
🔨 Latest commit 2af6f67
🔍 Latest deploy log https://app.netlify.com/sites/cornerstone-3d-docs/deploys/63d28e2019d28f0009510232
😎 Deploy Preview https://deploy-preview-382--cornerstone-3d-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site settings.

@@ -0,0 +1,256 @@
import {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new example for double click.

@jbocce jbocce marked this pull request as ready for review January 23, 2023 18:52
@jbocce jbocce requested a review from sedghi January 23, 2023 18:52
@@ -9,6 +9,8 @@ import getMouseEventPoints from './getMouseEventPoints';
const { MOUSE_DOWN, MOUSE_DOWN_ACTIVATE, MOUSE_CLICK, MOUSE_UP, MOUSE_DRAG } =
Events;

const DOUBLE_CLICK_TOLERANCE_MS = 400;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the best way to make this configurable?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can worry about this in another PR, since we are not configuring any other suff

Joe Boccanfuso added 2 commits January 24, 2023 10:55
mouseDownListener - even during annotation creation. It is up to the any
listener of the dblclick event to decide how it should be handled.
Comment on lines +50 to +64
/**
* Asynchronously dispatches a mouse down followed by a mouse up on the given element.
* Since mouse down events are performed on a timeout to detect potential
* double clicks, the mouse up event is not triggered until the mouse down
* event has been processed. An optional callback is invoked after the mouse
* down is triggered but before the mouse up. An optional callback is invoked
* after the mouse up has fired.
*
* @param element the element to dispatch to
* @param mouseDownEvent the mouse down event to dispatch
* @param mouseUpEvent the mouse up event to dispatch
* @param betweenDownAndUpCallback optional callback between the down and up
* @param afterDownAndUpCallback optional callback after the up
* @returns a Promise for the eventual completion of the mouse down and up
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this doc, thanks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks

Comment on lines 51 to 65
* Asynchronously dispatches a mouse down followed by a mouse up on the given element.
* Since mouse down events are performed on a timeout to detect potential
* double clicks, the mouse up event is not triggered until the mouse down
* event has been processed. An optional callback is invoked after the mouse
* down is triggered but before the mouse up. An optional callback is invoked
* after the mouse up has fired.
*
* @param element the element to dispatch to
* @param mouseDownEvent the mouse down event to dispatch
* @param mouseUpEvent the mouse up event to dispatch
* @param betweenDownAndUpCallback optional callback between the down and up
* @param afterDownAndUpCallback optional callback after the up
* @returns a Promise for the eventual completion of the mouse down and up
*/
function doMouseDownAndUp(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we rename this function to performMouseDownAndUp?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as you wish

@@ -1,6 +1,8 @@
import * as cornerstone3D from '@cornerstonejs/core';
import * as csTools3d from '../src/index';
import * as testUtils from '../../../utils/test/testUtils';
import * as cornerstoneTools from '@cornerstonejs/tools';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a big deal, but just saying for future, let's order the library imports at top and the local ones ('../../etc') after

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I will try to be more careful next time. Thanks for the 'heads up'. I am not 100% certain, but I think it might have been VS Code that did this automatically. Funny enough, that import is no longer needed.


doMouseDownAndUp(
element,
evt,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know evt, is mouseDown, but renaming the variable to mouseDownEvt would match the mouseUpEvt nicelu

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I will go through the various unit test files and make updates.

@@ -446,6 +450,10 @@ class ArrowAnnotateTool extends AnnotationTool {

this.editData = null;
this.isDrawing = false;

// This double click was handled and the dialogue was displayed. No need for anyone else to handle it too.
evt.stopImmediatePropagation();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why stopImmediatePropagation as opposed to stopPropagation? not immediately clear to me so I appreciate an inline comment above it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do

Comment on lines 37 to 39
element.addEventListener('dblclick', mouseDoubleClickIgnoreListener, {
capture: true, // capture phase is the best way to ignore double clicks
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any reason we are adding again the dblclick listener?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes there are two reasons...

  • If the logic of mouseDoubleClickIgnoreListener is moved into mouseDoubleClickListener, then the ignoreDoubleClick flag in mouseDownListener has to be exposed. Furthermore its mutation from true to false also has to be exposed. I do not particularly like that.
  • Notice that a capture phase listener is used which mouseDoubleClickListener is not. To best prevent listeners of dblclick to NOT get an ignored double click, a capture phase listener is best.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then can we put them as inline comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most certainly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I will move this logic code out of this index.ts and put it in mouseDownListener because none of it needs to be exposed outside that module.

@@ -35,7 +35,21 @@ function mouseDoubleClickListener(evt: MouseEvent): void {
deltaPoints,
};

triggerEvent(element, Events.MOUSE_DOUBLE_CLICK, eventDetail);
const preventDefault = !triggerEvent(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we rename this to consumed or similar?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or eventDidNotPropagate

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or even better const eventDidPropagate = trigger ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that triggerEvent in turn calls dispatchEvent. I checked the mdn docs for dispatchEvent (https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent) and its return value has NOTHING to do with propagation. In fact it is related to preventDefault. So I still prefer the name I had. However of your choices I think consumed is best.

Comment on lines 144 to 147
doubleClickState.doubleClickTimeout = setTimeout(
_doubleClickTimeoutHandler,
DOUBLE_CLICK_TOLERANCE_MS
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be the first thing right after the following?

if (doubleClickState.doubleClickTimeout) {
    return;
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I don't understand why it should be. The listeners that follow have to be added. That said I can move the addListener calls before the setTimeout call and the method will return right away regardless. However, I am still wondering why the suggestion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the setting of the doubleClickTimeout to near the top of the method.

@@ -102,8 +134,30 @@ function mouseDownListener(evt: MouseEvent) {
// Prevent CornerstoneToolsMouseMove while mouse is down
state.element.removeEventListener('mousemove', mouseMoveListener);

const startPoints = getMouseEventPoints(evt, state.element);
const deltaPoints = _getDeltaPoints(startPoints, startPoints);
state.startPoints = getMouseEventPoints(evt, state.element);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we were doing copyPoints previously, so I'm wondering not copying will result some bug due to reference manipulation

Copy link
Contributor Author

@jbocce jbocce Jan 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At some point I did have it so that state.startPoints would get a copy. However, the CPU tests began to fail. The short answer to all this is that I can return to using _copyPoints here if StackViewport.canvasToWorldCPU returns [worldPos[0], worldPos[1], worldPos[2]] as Point3 instead of worldPos as Point3. Why? Because worldPos (for the CPU tests anyway) is a vec3/Float32Array which _copyPoints converts to a JSON object and NOT an array. Thus any operation expecting an array (in the case I saw it was the spread operator for copying the array) will fail.

There are other ways I could fix this. Let me know your thoughts. I suppose the safest thing might be to change _copyPoints to be...

  const copy = JSON.parse(JSON.stringify(points));
  copy.world = [copy.world[0], copy.world[1], copy.world[2]] as Point3;
  return copy;

*/
function _isDragPastDoubleClickTolerance(delta: Types.Point2): boolean {
return (
Math.sqrt(Math.pow(delta[0], 2) + Math.pow(delta[1], 2)) >
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too much to get calculated for each mouse move, a lot of computations. Can we make it simpler to not rely on sqrt and pow?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand and yes I can change it. However note that this is only for every move during the timeout. The timeout is relatively short. Also, once a drag/move goes past the tolerance the timeout is cleared. I would argue that yes it is expensive but for a very short time - likely less than the timeout if the mouse is moving any significant amount. Would you still like me to change it?

const mouseDownEvent = doubleClickState.mouseDownEvent;
const mouseUpEvent = doubleClickState.mouseUpEvent;

_clearDoubleClickTimeoutAndEvents();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can put ignore here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we can and as such I will be removing the doubleClickTimeoutHandler function entirely since all it does is call _doStateMouseDownAndUp. Great suggestion.

state = JSON.parse(JSON.stringify(defaultState));
} else {
// this is the first mouse up during the double click timeout; we'll need it later if the timeout expires
doubleClickState.mouseUpEvent = evt;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switch order

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do

- code comments added and modified for clarification
- the double click listener to ignore double click events is now entirely
encapsulated in the mouseDownListener module
- better named variables in the unit tests
- better conversion of world CPU coordinates to Point3
// value will cancel the timeout and suppress any double click that might occur.
// This tolerance is particularly important on touch devices where some movement
// might occur between the two clicks.
const DOUBLE_CLICK_DRAG_TOLERANCE = 3;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last commit had this as 2. I did some further testing on my phone and thought 3 was better. I will add a TODO to revisit this value for touch devices.

@jbocce jbocce mentioned this pull request Jan 26, 2023
// A separate double click listener for the element. Separate because...
// - it listens on the capture phase (and not the typical bubble phase)
// - the data used to ignore the double click is private to mouseDoubleClickIgnoreListener
element.removeEventListener('dblclick', mouseDoubleClickIgnoreListener, {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds the listener for every enabled viewport. It is a bit of a waste since only one (global) listener is required.

and destroy functions (respectively).
Double click drag tolerance is now a projected distance.
@sedghi sedghi changed the title fix(mouseDownListener): Double click detection (#375) feat(doubleClick): Add Double click detection (#375) Jan 26, 2023
@sedghi sedghi merged commit 8e4be96 into cornerstonejs:main Jan 26, 2023
@jbocce jbocce deleted the fix/#375-double-click-detection branch January 27, 2023 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants