Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Address review comments on sequential two-flick gestures
- Convert action constants to TouchAction(DisplayStringStrEnum), replacing the actionLabels dict
- Update sequential gesture identifier values to use "then" separator (e.g. flickdownthenup)
- Replace #: comments with triple-quoted docstrings on _flickActions and _flickSequenceMap
- Extract _tryBuildSequentialGesture() helper from pump()
- Add type hints to SingleTouchTracker.__init__ and update()
- Migrate changelog entry to 2026.3 section
  • Loading branch information
kefaslungu committed May 25, 2026
commit 3b01a2015bdfc6820e57f166934e17ef878ea725
52 changes: 32 additions & 20 deletions source/touchHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ class POINTER_TOUCH_INFO(Structure):
touchThread = None


#: The set of single-direction flick actions that can begin a sequential flick gesture.
_flickActions: frozenset[str] = frozenset(
{
touchTracker.action_flickRight,
Expand All @@ -200,8 +199,8 @@ class POINTER_TOUCH_INFO(Structure):
touchTracker.action_flickDown,
},
)
"""The set of single-direction flick actions that can begin a sequential flick gesture."""

#: Maps (firstFlickAction, secondFlickAction) to the corresponding sequential flick action.
_flickSequenceMap: dict[tuple[str, str], str] = {
(touchTracker.action_flickRight, touchTracker.action_flickLeft): touchTracker.action_flickRightThenLeft,
(touchTracker.action_flickLeft, touchTracker.action_flickRight): touchTracker.action_flickLeftThenRight,
Expand All @@ -216,6 +215,7 @@ class POINTER_TOUCH_INFO(Structure):
(touchTracker.action_flickDown, touchTracker.action_flickRight): touchTracker.action_flickDownThenRight,
(touchTracker.action_flickDown, touchTracker.action_flickLeft): touchTracker.action_flickDownThenLeft,
}
"""Maps (firstFlickAction, secondFlickAction) to the corresponding sequential flick action."""


class TouchInputGesture(inputCore.InputGesture):
Expand Down Expand Up @@ -294,7 +294,7 @@ def getDisplayTextForIdentifier(cls, identifier):
foundAction = foundPlural = False
for subID in reversed(ID.split("_")):
if not foundAction:
action = touchTracker.actionLabels[subID]
action = touchTracker.TouchAction(subID).displayString
foundAction = True
continue
if not foundPlural:
Expand Down Expand Up @@ -426,30 +426,42 @@ def _executeGesture(self, gesture: "TouchInputGesture") -> None:
except inputCore.NoInputGestureAction:
pass

def _tryBuildSequentialGesture(
self,
first: "TouchInputGesture",
second: "TouchInputGesture",
) -> "TouchInputGesture | None":
"""Attempt to combine two consecutive flick gestures into a single sequential flick gesture.

:param first: The first flick gesture.
:param second: The second flick gesture.
:return: A combined sequential gesture, or ``None`` if the pair is not a recognised combination.
"""
if first.tracker.numFingers != second.tracker.numFingers:
return None
compoundAction = _flickSequenceMap.get((first.tracker.action, second.tracker.action))
if compoundAction is None:
return None
compoundTracker = touchTracker.MultiTouchTracker(
compoundAction,
first.tracker.x,
first.tracker.y,
first.tracker.startTime,
second.tracker.endTime,
numFingers=second.tracker.numFingers,
)
return TouchInputGesture(first.preheldTracker, compoundTracker, first.mode)

def pump(self):
# pendingFlick holds the first flick within this pump cycle, waiting to see if a second follows.
# This is a local variable — no timer, no cross-pump buffering, so normal flicks fire immediately.
pendingFlick: TouchInputGesture | None = None
for preheldTracker, tracker in self.trackerManager.emitTrackers():
gesture = TouchInputGesture(preheldTracker, tracker, self._curTouchMode.value)
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,
)
if pendingFlick is not None:
sequentialGesture = self._tryBuildSequentialGesture(pendingFlick, gesture)
if sequentialGesture is not None:
pendingFlick = None
self._executeGesture(sequentialGesture)
continue
Expand Down
205 changes: 120 additions & 85 deletions source/touchTracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,125 @@
import threading
import time
from collections import OrderedDict

# Possible actions (single trackers)
action_tap = "tap"
action_hold = "hold"
action_tapAndHold = "tapandhold"
action_flickUp = "flickup"
action_flickDown = "flickdown"
action_flickLeft = "flickleft"
action_flickRight = "flickright"
action_hoverDown = "hoverdown"
action_hover = "hover"
action_hoverUp = "hoverup"
action_unknown = "unknown"
from functools import cached_property
from typing import Self

from utils.displayString import DisplayStringStrEnum


class TouchAction(DisplayStringStrEnum):
"""All recognised touch screen actions, with translated display strings."""

TAP = "tap"
HOLD = "hold"
TAP_AND_HOLD = "tapandhold"
FLICK_UP = "flickup"
FLICK_DOWN = "flickdown"
FLICK_LEFT = "flickleft"
FLICK_RIGHT = "flickright"
HOVER_DOWN = "hoverdown"
HOVER = "hover"
HOVER_UP = "hoverup"
UNKNOWN = "unknown"
# Pinch gesture actions
PINCH_IN = "pinchin"
PINCH_OUT = "pinchout"
# Sequential two-flick gesture actions (opposite directions)
FLICK_RIGHT_THEN_LEFT = "flickrightthenleft"
FLICK_LEFT_THEN_RIGHT = "flickleftthenright"
FLICK_UP_THEN_DOWN = "flickupthendown"
FLICK_DOWN_THEN_UP = "flickdownthenup"
# Sequential two-flick gesture actions (perpendicular / L-shaped)
FLICK_RIGHT_THEN_UP = "flickrightthenup"
FLICK_RIGHT_THEN_DOWN = "flickrightthendown"
FLICK_LEFT_THEN_UP = "flickleftthenup"
FLICK_LEFT_THEN_DOWN = "flickleftthendown"
FLICK_UP_THEN_RIGHT = "flickupthenright"
FLICK_UP_THEN_LEFT = "flickupthenleft"
FLICK_DOWN_THEN_RIGHT = "flickdownthenright"
FLICK_DOWN_THEN_LEFT = "flickdownthenleft"

@cached_property
def _displayStringLabels(self) -> dict[Self, str]:
return {
# Translators: a very quick touch and release of a finger on a touch screen
TouchAction.TAP: pgettext("touch action", "tap"),
# Translators: a touch with no release, on a touch screen.
TouchAction.HOLD: pgettext("touch action", "hold"),
# Translators: a very quick touch and release, then another touch with no release, on a touch screen
TouchAction.TAP_AND_HOLD: pgettext("touch action", "tap and hold"),
# Translators: a quick swipe of a finger in an up direction, on a touch screen.
TouchAction.FLICK_UP: pgettext("touch action", "flick up"),
# Translators: a quick swipe of a finger in a down direction, on a touch screen.
TouchAction.FLICK_DOWN: pgettext("touch action", "flick down"),
# Translators: a quick swipe of a finger in a left direction, on a touch screen.
TouchAction.FLICK_LEFT: pgettext("touch action", "flick left"),
# Translators: a quick swipe of a finger in a right direction, on a touch screen.
TouchAction.FLICK_RIGHT: pgettext("touch action", "flick right"),
# Translators: a finger has been held on the touch screen long enough to be considered as hovering
TouchAction.HOVER_DOWN: pgettext("touch action", "hover down"),
# Translators: a finger is still touching the touch screen and is moving around without breaking contact.
TouchAction.HOVER: pgettext("touch action", "hover"),
# Translators: a finger that was hovering (touching the touch screen for a long time) has been released
TouchAction.HOVER_UP: pgettext("touch action", "hover up"),
# Translators: a touch screen gesture where two fingers move toward each other (zoom out)
TouchAction.PINCH_IN: pgettext("touch action", "pinch in"),
# Translators: a touch screen gesture where two fingers move away from each other (zoom in)
TouchAction.PINCH_OUT: pgettext("touch action", "pinch out"),
# Translators: a quick swipe right followed by a quick swipe left, on a touch screen.
TouchAction.FLICK_RIGHT_THEN_LEFT: pgettext("touch action", "flick right then left"),
# Translators: a quick swipe left followed by a quick swipe right, on a touch screen.
TouchAction.FLICK_LEFT_THEN_RIGHT: pgettext("touch action", "flick left then right"),
# Translators: a quick swipe up followed by a quick swipe down, on a touch screen.
TouchAction.FLICK_UP_THEN_DOWN: pgettext("touch action", "flick up then down"),
# Translators: a quick swipe down followed by a quick swipe up, on a touch screen.
TouchAction.FLICK_DOWN_THEN_UP: pgettext("touch action", "flick down then up"),
# Translators: a quick swipe right followed by a quick swipe up, on a touch screen.
TouchAction.FLICK_RIGHT_THEN_UP: pgettext("touch action", "flick right then up"),
# Translators: a quick swipe right followed by a quick swipe down, on a touch screen.
TouchAction.FLICK_RIGHT_THEN_DOWN: pgettext("touch action", "flick right then down"),
# Translators: a quick swipe left followed by a quick swipe up, on a touch screen.
TouchAction.FLICK_LEFT_THEN_UP: pgettext("touch action", "flick left then up"),
# Translators: a quick swipe left followed by a quick swipe down, on a touch screen.
TouchAction.FLICK_LEFT_THEN_DOWN: pgettext("touch action", "flick left then down"),
# Translators: a quick swipe up followed by a quick swipe right, on a touch screen.
TouchAction.FLICK_UP_THEN_RIGHT: pgettext("touch action", "flick up then right"),
# Translators: a quick swipe up followed by a quick swipe left, on a touch screen.
TouchAction.FLICK_UP_THEN_LEFT: pgettext("touch action", "flick up then left"),
# Translators: a quick swipe down followed by a quick swipe right, on a touch screen.
TouchAction.FLICK_DOWN_THEN_RIGHT: pgettext("touch action", "flick down then right"),
# Translators: a quick swipe down followed by a quick swipe left, on a touch screen.
TouchAction.FLICK_DOWN_THEN_LEFT: pgettext("touch action", "flick down then left"),
}


# Module-level aliases for backwards compatibility
action_tap = TouchAction.TAP
Comment thread
kefaslungu marked this conversation as resolved.
Outdated
action_hold = TouchAction.HOLD
action_tapAndHold = TouchAction.TAP_AND_HOLD
action_flickUp = TouchAction.FLICK_UP
action_flickDown = TouchAction.FLICK_DOWN
action_flickLeft = TouchAction.FLICK_LEFT
action_flickRight = TouchAction.FLICK_RIGHT
action_hoverDown = TouchAction.HOVER_DOWN
action_hover = TouchAction.HOVER
action_hoverUp = TouchAction.HOVER_UP
action_unknown = TouchAction.UNKNOWN
hoverActions = (action_hoverDown, action_hover, action_hoverUp)
# Pinch gesture actions
#: Two fingers moving toward each other.
action_pinchIn: str = "pinchin"
#: Two fingers moving away from each other.
action_pinchOut: str = "pinchout"
# Sequential two-flick gesture actions (opposite directions)
action_flickRightThenLeft = "flickrightflickleft"
action_flickLeftThenRight = "flickleftflickright"
action_flickUpThenDown = "flickupflickdown"
action_flickDownThenUp = "flickdownflickup"
# Sequential two-flick gesture actions (perpendicular / L-shaped)
action_flickRightThenUp = "flickrightflickup"
action_flickRightThenDown = "flickrightflickdown"
action_flickLeftThenUp = "flickleftflickup"
action_flickLeftThenDown = "flickleftflickdown"
action_flickUpThenRight = "flickupflickright"
action_flickUpThenLeft = "flickupflickleft"
action_flickDownThenRight = "flickdownflickright"
action_flickDownThenLeft = "flickdownflickleft"
action_pinchIn = TouchAction.PINCH_IN
action_pinchOut = TouchAction.PINCH_OUT
action_flickRightThenLeft = TouchAction.FLICK_RIGHT_THEN_LEFT
action_flickLeftThenRight = TouchAction.FLICK_LEFT_THEN_RIGHT
action_flickUpThenDown = TouchAction.FLICK_UP_THEN_DOWN
action_flickDownThenUp = TouchAction.FLICK_DOWN_THEN_UP
action_flickRightThenUp = TouchAction.FLICK_RIGHT_THEN_UP
action_flickRightThenDown = TouchAction.FLICK_RIGHT_THEN_DOWN
action_flickLeftThenUp = TouchAction.FLICK_LEFT_THEN_UP
action_flickLeftThenDown = TouchAction.FLICK_LEFT_THEN_DOWN
action_flickUpThenRight = TouchAction.FLICK_UP_THEN_RIGHT
action_flickUpThenLeft = TouchAction.FLICK_UP_THEN_LEFT
action_flickDownThenRight = TouchAction.FLICK_DOWN_THEN_RIGHT
action_flickDownThenLeft = TouchAction.FLICK_DOWN_THEN_LEFT
# timeout for detection of flicks and plural trackers
multitouchTimeout = 0.25
# The distance a finger must travel to be treeted as a flick
Expand All @@ -57,57 +143,6 @@
#: Longer than multitouchTimeout so users aren't forced to rush the gesture.
continuousFlickTimeout: float = 0.6

actionLabels = {
# Translators: a touch screen gesture where two fingers move toward each other (zoom out)
action_pinchIn: pgettext("touch action", "pinch in"),
# Translators: a touch screen gesture where two fingers move away from each other (zoom in)
action_pinchOut: pgettext("touch action", "pinch out"),
# Translators: a very quick touch and release of a finger on a touch screen
action_tap: pgettext("touch action", "tap"),
# Translators: a very quick touch and release, then another touch with no release, on a touch screen
action_tapAndHold: pgettext("touch action", "tap and hold"),
# Translators: a touch with no release, on a touch screen.
action_hold: pgettext("touch action", "hold"),
# Translators: a quick swipe of a finger in an up direction, on a touch screen.
action_flickUp: pgettext("touch action", "flick up"),
# Translators: a quick swipe of a finger in an down direction, on a touch screen.
action_flickDown: pgettext("touch action", "flick down"),
# Translators: a quick swipe of a finger in a left direction, on a touch screen.
action_flickLeft: pgettext("touch action", "flick left"),
# Translators: a quick swipe of a finger in a right direction, on a touch screen.
action_flickRight: pgettext("touch action", "flick right"),
# Translators: a finger has been held on the touch screen long enough to be considered as hovering
action_hoverDown: pgettext("touch action", "hover down"),
# Translators: A finger is still touching the touch screen and is moving around with out breaking contact.
action_hover: pgettext("touch action", "hover"),
# Translators: a finger that was hovering (touching the touch screen for a long time) has been released
action_hoverUp: pgettext("touch action", "hover up"),
# Translators: a quick swipe right followed by a quick swipe left, on a touch screen.
action_flickRightThenLeft: pgettext("touch action", "flick right then left"),
# Translators: a quick swipe left followed by a quick swipe right, on a touch screen.
action_flickLeftThenRight: pgettext("touch action", "flick left then right"),
# Translators: a quick swipe up followed by a quick swipe down, on a touch screen.
action_flickUpThenDown: pgettext("touch action", "flick up then down"),
# Translators: a quick swipe down followed by a quick swipe up, on a touch screen.
action_flickDownThenUp: pgettext("touch action", "flick down then up"),
# Translators: a quick swipe right followed by a quick swipe up, on a touch screen.
action_flickRightThenUp: pgettext("touch action", "flick right then up"),
# Translators: a quick swipe right followed by a quick swipe down, on a touch screen.
action_flickRightThenDown: pgettext("touch action", "flick right then down"),
# Translators: a quick swipe left followed by a quick swipe up, on a touch screen.
action_flickLeftThenUp: pgettext("touch action", "flick left then up"),
# Translators: a quick swipe left followed by a quick swipe down, on a touch screen.
action_flickLeftThenDown: pgettext("touch action", "flick left then down"),
# Translators: a quick swipe up followed by a quick swipe right, on a touch screen.
action_flickUpThenRight: pgettext("touch action", "flick up then right"),
# Translators: a quick swipe up followed by a quick swipe left, on a touch screen.
action_flickUpThenLeft: pgettext("touch action", "flick up then left"),
# Translators: a quick swipe down followed by a quick swipe right, on a touch screen.
action_flickDownThenRight: pgettext("touch action", "flick down then right"),
# Translators: a quick swipe down followed by a quick swipe left, on a touch screen.
action_flickDownThenLeft: pgettext("touch action", "flick down then left"),
}


class SingleTouchTracker(object):
"""
Expand Down Expand Up @@ -154,7 +189,7 @@ class SingleTouchTracker(object):
"_samples",
]

def __init__(self, ID, x, y):
def __init__(self, ID: int, x: int, y: int) -> None:
self.ID = ID
self.x = self.startX = self.peakX = x
self.y = self.startY = self.peakY = y
Expand All @@ -166,7 +201,7 @@ def __init__(self, ID, x, y):
self.complete = False
self._samples: list[tuple[int, int, float]] = []

def update(self, x, y, complete=False):
def update(self, x: int, y: int, complete: bool = False) -> None:
"""Called to alert this single tracker that the finger has moved or broken contact."""
self.x = x
self.y = y
Expand Down
9 changes: 8 additions & 1 deletion user_docs/en/changes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# What's New in NVDA

## 2026.2
## 2026.3

### Important notes

Expand All @@ -9,6 +9,13 @@
* 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)
Comment thread
kefaslungu marked this conversation as resolved.
* Twelve combinations are recognised: opposite-direction pairs (e.g. flick right then flick left) and perpendicular L-shaped pairs (e.g. flick right then flick up).
* The two flicks can be performed either by lifting the finger between strokes or as a single continuous swipe with a sharp change in direction.

## 2026.2

### Important notes

### New Features

* Added the ability to automatically scroll the braille display. (#18573, @nvdaes)
* After installing or updating NVDA, a dialog now offers options to restart Windows, start the installed copy, or exit the installer. (#19268, #19718, @kefaslungu)
* NVDA now includes a built-in Magnifier feature that allows you to zoom and magnify parts of the screen. (#19228, @Boumtchack)
Expand Down