Skip to content

Add sequential two-flick touch gestures#20086

Draft
kefaslungu wants to merge 1 commit into
nvaccess:masterfrom
kefaslungu:sequentialgestures
Draft

Add sequential two-flick touch gestures#20086
kefaslungu wants to merge 1 commit into
nvaccess:masterfrom
kefaslungu:sequentialgestures

Conversation

@kefaslungu
Copy link
Copy Markdown
Contributor

Link to issue number:

#19938

Summary of the issue:

This is the second of 3 PRs to address the above issue.
Adds twelve sequential two-flick gestures (opposite-direction and perpendicular L-shaped pairs) that can be bound to scripts. The two flicks may be performed either by lifting the finger between strokes or as a single continuous swipe with a sharp change in direction.

Description of user facing changes:

Touch screen users can now perform two flicks in quick succession to trigger a sequential flick gesture. This expands the number of touch gestures that can be assigned to NVDA commands.
Twelve combinations are recognised:
• Four opposite-direction pairs: flick right then left, flick left then right, flick up then down, flick down then up.
• Eight perpendicular L-shaped pairs: flick right then up, flick right then down, flick left then up, flick left then down, flick up then right, flick up then left, flick down then right, flick down then left.
The two flicks can be performed either by lifting the finger between strokes (within the multi-touch timeout) or as a single continuous swipe that sharply changes direction.

Description of developer facing changes:

• Twelve new action_flickThen constants in touchTracker, with translatable labels in actionLabels.
• SingleTouchTracker now records peakX/peakY (farthest point reached on each axis) and a rolling window of recent samples for velocity calculation. getVelocity() returns velocity in pixels per second using least-squares linear regression.
• SingleTouchTracker.update() now uses velocity direction rather than total displacement direction to classify flicks, so late-stroke direction changes are reflected correctly.
• TrackerManager._checkContinuousFlick() detects a direction change without a finger lift; when the finger has moved far enough away from its peak, the first flick is queued and the tracker is reset to track the return stroke as the second gesture.
• TouchHandler.pump() now combines two matching flicks that arrive in the same pump cycle into a single sequential gesture using _flickSequenceMap. Single flicks that aren't followed by a second flick still fire immediately at end of cycle, so normal flick latency is unchanged.
• New tunables in touchTracker: minFlickVelocity (100 px/s), continuousFlickTimeout (0.6 s), _VELOCITY_SAMPLE_WINDOW (0.1 s).

Description of development approach:

The detection is split between the two layers that already existed:
• The low-level layer (touchTracker) classifies a single finger as a flick using velocity direction and detects mid-swipe direction changes. The first flick of a continuous gesture is emitted as soon as the second stroke begins; the tracker is then reset so the return stroke is classified independently.
• The high-level layer (touchHandler.pump) combines two flicks emitted in the same pump cycle into a compound action. This is a local pump-cycle decision — no timer, no cross-pump buffering — so a single flick fires immediately at end of cycle without waiting for a possible second flick.
This keeps single-flick latency unchanged while still allowing sequential flicks to be detected reliably.

Testing strategy:

Build from the PR branch and test on a touch screen enabled computer. Must self sign the build.

Known issues with pull request:

None.

Code Review Checklist:

  • Documentation:
    • Change log entry
    • User Documentation
    • Developer / Technical Documentation
    • Context sensitive help for GUI changes
  • Testing:
    • Unit tests
    • System (end to end) tests
    • Manual testing
  • UX of all users considered:
    • Speech
    • Braille
    • Low Vision
    • Different web browsers
    • Localization in other languages / culture than English
  • API is compatible with existing add-ons.
  • Security precautions taken.

Adds twelve sequential two-flick gestures (opposite-direction and perpendicular L-shaped pairs)
that can be bound to scripts. The two flicks may be performed either by lifting the finger
between strokes or as a single continuous swipe with a sharp change in direction.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@kefaslungu kefaslungu requested review from a team as code owners May 8, 2026 06:57
@seanbudd seanbudd added this to the 2026.3 milestone May 11, 2026
@seanbudd seanbudd added the conceptApproved Similar 'triaged' for issues, PR accepted in theory, implementation needs review. label May 18, 2026
Copy link
Copy Markdown
Member

@seanbudd seanbudd left a comment

Choose a reason for hiding this comment

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

Thanks @kefaslungu - just a few minor things

Comment thread source/touchHandler.py
Comment on lines +194 to +202
#: The set of single-direction flick actions that can begin a sequential flick gesture.
_flickActions: frozenset[str] = frozenset(
{
touchTracker.action_flickRight,
touchTracker.action_flickLeft,
touchTracker.action_flickUp,
touchTracker.action_flickDown,
},
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

instead of comments like this, can you use triple quoted strings underneath the variable

Suggested change
#: The set of single-direction flick actions that can begin a sequential flick gesture.
_flickActions: frozenset[str] = frozenset(
{
touchTracker.action_flickRight,
touchTracker.action_flickLeft,
touchTracker.action_flickUp,
touchTracker.action_flickDown,
},
)
_flickActions: frozenset[str] = frozenset(
{
touchTracker.action_flickRight,
touchTracker.action_flickLeft,
touchTracker.action_flickUp,
touchTracker.action_flickDown,
},
)
"""The set of single-direction flick actions that can begin a sequential flick gesture."""

Comment thread source/touchHandler.py
},
)

#: Maps (firstFlickAction, secondFlickAction) to the corresponding sequential flick action.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same here

Comment thread source/touchHandler.py
Comment on lines +435 to +466
if tracker.action in _flickActions:
if pendingFlick is not None and pendingFlick.tracker.numFingers == tracker.numFingers:
compoundAction = _flickSequenceMap.get((pendingFlick.tracker.action, tracker.action))
if compoundAction is not None:
# Two matching flicks arrived in the same pump cycle: combine into a sequential gesture.
compoundTracker = touchTracker.MultiTouchTracker(
compoundAction,
pendingFlick.tracker.x,
pendingFlick.tracker.y,
pendingFlick.tracker.startTime,
tracker.endTime,
numFingers=tracker.numFingers,
)
sequentialGesture = TouchInputGesture(
pendingFlick.preheldTracker,
compoundTracker,
pendingFlick.mode,
)
pendingFlick = None
self._executeGesture(sequentialGesture)
continue
# No match yet: flush any earlier flick, then hold this one for the rest of this cycle.
if pendingFlick is not None:
self._executeGesture(pendingFlick)
pendingFlick = gesture
else:
if pendingFlick is not None:
self._executeGesture(pendingFlick)
pendingFlick = None
self._executeGesture(gesture)
if pendingFlick is not None:
self._executeGesture(pendingFlick)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can this be moved to a helper function (or a few helper functions?) this is a lot of complex nested code added under what should be a simple function to execute the pump

Comment thread source/touchTracker.py
#: Two fingers moving away from each other.
action_pinchOut: str = "pinchout"
# Sequential two-flick gesture actions (opposite directions)
action_flickRightThenLeft = "flickrightflickleft"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can this be turned into a StrEnum easily? ideally a displayStringStrEnum with the action labels

Comment thread source/touchTracker.py
self.complete = False
self._samples: list[tuple[int, int, float]] = []

def update(self, x, y, complete=False):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

please add type hints

Comment thread source/touchTracker.py
"_samples",
]

def __init__(self, ID, x, y):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

please add type hints

Comment thread user_docs/en/changes.md
### New Features

* Added pinch in and pinch out touch gestures, allowing two-finger pinch gestures to be bound to scripts. (#19938, @kefaslungu)
* Added sequential two-flick touch gestures that combine two flicks performed in quick succession into a single gesture, increasing the number of touch gestures that can be bound to scripts. (#19938, @kefaslungu)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

please migrate this to 2026.3 changelog

Comment thread user_docs/en/userGuide.md
##### Sequential Flicks {#SequentialFlicks}

You can perform two flicks in quick succession to trigger a sequential flick gesture.
This greatly expands the number of touch gestures that can be assigned to NVDA commands.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure if this sentence is necessary, thoughts @Qchristensen ?

@seanbudd seanbudd marked this pull request as draft May 19, 2026 02:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conceptApproved Similar 'triaged' for issues, PR accepted in theory, implementation needs review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants