-
Notifications
You must be signed in to change notification settings - Fork 298
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
feat(doubleClick): Add Double click detection (#375) #382
Conversation
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.
✅ Deploy Preview for cornerstone-3d-docs ready!
To edit notification comments on pull requests, go to your Netlify site settings. |
@@ -0,0 +1,256 @@ | |||
import { |
There was a problem hiding this comment.
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.
@@ -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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
mouseDownListener - even during annotation creation. It is up to the any listener of the dblclick event to decide how it should be handled.
…ick will not occur
/** | ||
* 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 | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love this doc, thanks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks
utils/test/testUtilsMouseEvents.ts
Outdated
* 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( |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will do
element.addEventListener('dblclick', mouseDoubleClickIgnoreListener, { | ||
capture: true, // capture phase is the best way to ignore double clicks | ||
}); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 intomouseDoubleClickListener
, then theignoreDoubleClick
flag inmouseDownListener
has to be exposed. Furthermore its mutation fromtrue
tofalse
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 ofdblclick
to NOT get an ignored double click, a capture phase listener is best.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most certainly.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or eventDidNotPropagate
There was a problem hiding this comment.
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 ...
There was a problem hiding this comment.
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.
doubleClickState.doubleClickTimeout = setTimeout( | ||
_doubleClickTimeoutHandler, | ||
DOUBLE_CLICK_TOLERANCE_MS | ||
); |
There was a problem hiding this comment.
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;
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)) > |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
switch order
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
// 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, { |
There was a problem hiding this comment.
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.
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