Skip to content

Commit

Permalink
Emulate right click via dual-tap on touch devices (#1789)
Browse files Browse the repository at this point in the history
Resolves #1782.

This PR adds support for issuing context clicks from touch devices by
tapping with two fingers at once.

- I had to slightly refactor the existing code in order to accommodate
the right-click logic. Mainly:
  - `touchInfo` has now become a `touchInfos` list
- The `isDoubleClick` and `isRightClick` methods now also take that list
as first argument. Both function also check the arity of that list, so
that they are more self-contained.
- Contrary to [the initial idea in the proof-of-concept
branch](#1779 (review)),
I ended up implementing “dual tap” (two-finger tap) rather than “tap and
hold” (with one finger). The logic of the latter would have become quite
complex, because we then would have to make the decision when the tap is
released (stopped), not when it’s started. This also would bring further
complexity with it, because more things might happen between touch-start
and touch-end, such as a drag-and-drop attempt, or some multi-finger
gesture. So I thought it was most simple to implement “dual tap”,
because then the logic appeared much simpler, and we can make the
decision right away at touch-start time.
- I’ve also added a note to the top-level comment to make it clear that
this implementation does not cover all cases 100% correctly.

I have tested this on my iPad, so a code check would suffice from my
side.
<a data-ca-tag
href="https://codeapprove.com/pr/tiny-pilot/tinypilot/1789"><img
src="https://codeapprove.com/external/github-tag-allbg.png" alt="Review
on CodeApprove" /></a>

---------

Co-authored-by: Jan Heuermann <[email protected]>
  • Loading branch information
jotaen4tinypilot and jotaen authored Apr 24, 2024
1 parent 5b2fe52 commit 3323b23
Showing 1 changed file with 44 additions and 19 deletions.
63 changes: 44 additions & 19 deletions app/static/js/touch.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
*
* We currently only provide basic support for touch devices. So for now, this
* adapter can emulate the following mouse actions:
* - Single left click
* - Double left click
* - Single left click (i.e., touch with single finger)
* - Double left click (i.e., two consecutive taps with single finger)
* - Right click (i.e., touch with two fingers simultaneously)
* For all other gestures, this adapter may not behave in a well-defined way.
*/
export class TouchToMouseAdapter {
constructor() {
Expand All @@ -32,22 +34,32 @@ export class TouchToMouseAdapter {
* @returns {SyntheticMouseEvent}
*/
fromTouchStart(evt) {
const touchInfo = {
timestamp: new Date(),
clientX: evt.touches[0].clientX,
clientY: evt.touches[0].clientY,
};
const timestamp = new Date();
const touchInfos = Array.from(evt.touches).map((t) => ({
timestamp,
clientX: t.clientX,
clientY: t.clientY,
}));

const button = (() => {
if (isRightClick(touchInfos)) {
return 2;
}

// If this touch was a double click, use the mouse coordinates from the
// previous touch, so that the position is exactly the same. (See comment
// of `isDoubleClick` for why this is important.)
if (isDoubleClick(touchInfo, this._lastTouchInfo)) {
touchInfo.clientX = this._lastTouchInfo.clientX;
touchInfo.clientY = this._lastTouchInfo.clientY;
}
// If this touch was a double click, use the mouse coordinates from the
// previous touch, so that the position is exactly the same. (See comment
// of `isDoubleClick` for why this is important.)
if (isDoubleClick(touchInfos, this._lastTouchInfo)) {
touchInfos[0].clientX = this._lastTouchInfo.clientX;
touchInfos[0].clientY = this._lastTouchInfo.clientY;
}

this._lastTouchInfo = touchInfo;
return mouseClickEvent(evt.target, this._lastTouchInfo, 1);
return 1;
})();

// Interpret the first touch point as primary one.
this._lastTouchInfo = touchInfos[0];
return mouseClickEvent(evt.target, this._lastTouchInfo, button);
}

/**
Expand Down Expand Up @@ -85,10 +97,23 @@ function mouseClickEvent(target, touchPosition, buttons) {
* click in the wrong place, or the target operating system might not recognize
* the two clicks as proper double click.
*/
function isDoubleClick(touchInfo1, touchInfo2) {
function isDoubleClick(touchInfos, lastTouchInfo) {
return (
touchInfos.length === 1 &&
distancePx(touchInfos[0], lastTouchInfo) < 50 &&
delayMs(touchInfos[0].timestamp, lastTouchInfo.timestamp) < 500
);
}

/**
* Checks whether two simultaneous touches are intended to be a right click
* (context click). This is true if both touches appear close to each other,
* due to the user tapping with both fingers – either at the same time, or one
* after the other.
*/
function isRightClick(touchInfos) {
return (
distancePx(touchInfo1, touchInfo2) < 50 &&
delayMs(touchInfo1.timestamp, touchInfo2.timestamp) < 500
touchInfos.length === 2 && distancePx(touchInfos[0], touchInfos[1]) < 200
);
}

Expand Down

0 comments on commit 3323b23

Please sign in to comment.