Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b6fd89a
Added Web navigation to core
kefaslungu Jan 3, 2026
b4e9bd4
Pre-commit auto-fix
pre-commit-ci[bot] Jan 3, 2026
62eb77f
Added relevant user documentation and changes
kefaslungu Jan 3, 2026
daf47b4
Added name
kefaslungu Jan 3, 2026
8fe2f73
Merge branch 'webmode' of https://github.com/kefaslungu/nvda into web…
kefaslungu Jan 3, 2026
82043fa
Move web touch navigation into BrowseModeTreeInterceptor with dynamic…
kefaslungu Feb 19, 2026
6030aba
Merge branch 'master' into webmode
kefaslungu Feb 19, 2026
de05928
Pre-commit auto-fix
pre-commit-ci[bot] Feb 19, 2026
266c625
Addressed reviewer's comments
kefaslungu Feb 23, 2026
6a93eed
Merge branch 'webmode' of https://github.com/kefaslungu/nvda into web…
kefaslungu Feb 23, 2026
a68cdf3
Pre-commit auto-fix
pre-commit-ci[bot] Feb 23, 2026
d670765
Added documentation and minor fixes
kefaslungu Feb 27, 2026
3707430
Merge branch 'webmode' of https://github.com/kefaslungu/nvda into web…
kefaslungu Feb 27, 2026
4052b06
Fix type hint where necessary
kefaslungu Feb 27, 2026
152190a
Changed from webmode to browsemode
kefaslungu Mar 2, 2026
1fffa9e
Made edits to user docs and changes.md
kefaslungu Mar 2, 2026
c5ce667
rename web→browse, type hints, deprecation
kefaslungu Mar 2, 2026
5703f06
sentence split onto two lines in user docs and remove unnecessary imp…
kefaslungu Mar 4, 2026
7649ef3
Added documentation explaining browse mode in touch is only active wh…
kefaslungu Mar 5, 2026
3028100
updated docs and changes
kefaslungu Mar 9, 2026
53830c4
- Type annotate _enabledBrowseElements; Sphinx docstring
kefaslungu Mar 9, 2026
8af69b1
Merge upstream/master into webmode
kefaslungu Mar 9, 2026
8a9b3f2
changing the enum values to shorter strings, making _curTouchMode sto…
kefaslungu Mar 10, 2026
1c2a459
Fixed reviewers comments
kefaslungu Mar 10, 2026
3745f52
renamed AVAILABLE_TOUCH_MODES to availableTouchModes
kefaslungu Mar 11, 2026
f11f808
docs: Explicitly states the element type is remembered per document
kefaslungu Mar 24, 2026
f612318
translator comment now says browse mode touch navigation element types
kefaslungu Mar 24, 2026
6a7ba8e
clarifying comment added above the setting
kefaslungu Mar 24, 2026
9e237ef
addQuickNav docstring converted fully to Sphinx style
kefaslungu Mar 24, 2026
74614a2
Apply suggestions from code review
SaschaCowley Mar 30, 2026
ac048ae
Fixed reviewers suggestions
kefaslungu Mar 31, 2026
5690479
Merge branch 'webmode' of https://github.com/kefaslungu/nvda into web…
kefaslungu Mar 31, 2026
f0067e6
Update user_docs/en/userGuide.md
SaschaCowley Mar 31, 2026
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
205 changes: 194 additions & 11 deletions source/browseMode.py

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion source/config/configSpec.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2006-2026 NV Access Limited, Babbage B.V., Davy Kager, Bill Dengler, Julien Cochuyt,
# Joseph Lee, Dawid Pieper, mltony, Bram Duvigneau, Cyrille Bougot, Rob Meredith,
# Burman's Computer and Education Ltd., Leonard de Ruijter, Łukasz Golonka, Cary-rowen
# Burman's Computer and Education Ltd., Leonard de Ruijter, Łukasz Golonka, Cary-rowen, Kefas Lungu
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

Expand Down Expand Up @@ -215,6 +215,8 @@
enableOnPageLoad = boolean(default=true)
loadChromiumVBufOnBusyState = featureFlag(optionsEnum="BoolFlag", behaviorOfDefault="enabled")
textParagraphRegex = string(default="{configDefaults.DEFAULT_TEXT_PARAGRAPH_REGEX}")
# Element types available for cycling in browse touch mode.
browseModeTouchNavigationElements = string_list(default=list("heading", "link", "formField", "list", "table"))

[touch]
enabled = boolean(default=true)
Expand Down
7 changes: 1 addition & 6 deletions source/globalCommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4582,12 +4582,7 @@ def script_touch_changeMode(self, gesture):
index = (index + 1) % len(touchHandler.availableTouchModes)
newMode = touchHandler.availableTouchModes[index]
touchHandler.handler._curTouchMode = newMode
try:
newModeLabel = touchHandler.touchModeLabels[newMode]
except KeyError:
# Translators: Cycles through available touch modes (a group of related touch gestures; example output: "object mode"; see the user guide for more information on touch modes).
newModeLabel = _("%s mode") % newMode
ui.message(newModeLabel)
ui.message(newMode.displayString)

@script(
# Translators: Input help mode message for a touchscreen gesture.
Expand Down
24 changes: 23 additions & 1 deletion source/gui/settingsDialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# Łukasz Golonka, Aaron Cannon, Adriani90, André-Abush Clause, Dawid Pieper,
# Takuya Nishimoto, jakubl7545, Tony Malykh, Rob Meredith,
# Burman's Computer and Education Ltd, hwf1324, Cary-rowen, Christopher Proß, Tianze
# Neil Soiffer, Ryan McCleary.
# Neil Soiffer, Ryan McCleary, Kefas Lungu.
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

Expand Down Expand Up @@ -2664,6 +2664,23 @@ def makeSettings(self, settingsSizer):
)
self.trapNonCommandGesturesCheckBox.SetValue(config.conf["virtualBuffers"]["trapNonCommandGestures"])

# browseMode imports gui, which imports from settingsDialogs, so a top-level import
# would create a circular dependency. Keep this import lazy.
import browseMode

# Store element types for use in onSave (excludes the always-active "default" entry).
self._browseModeElements = list(browseMode.BrowseModeTreeInterceptor._browseTouchNavRegistry)
self._browseModeCheckListBox: nvdaControls.CustomCheckListBox = sHelper.addLabeledControl(
# Translators: Label for the list of browse mode touch navigation element types in browse mode settings.
_("T&ouch navigation elements:"),
nvdaControls.CustomCheckListBox,
choices=[label for _itemType, label in self._browseModeElements],
)
self._browseModeCheckListBox.Enable(touchHandler.touchSupported())
enabledTypes = set(config.conf["virtualBuffers"]["browseModeTouchNavigationElements"])
for i, (itemType, _label) in enumerate(self._browseModeElements):
self._browseModeCheckListBox.Check(i, itemType in enabledTypes)

def onSave(self):
config.conf["virtualBuffers"]["maxLineLength"] = self.maxLengthEdit.GetValue()
config.conf["virtualBuffers"]["linesPerPage"] = self.pageLinesEdit.GetValue()
Expand All @@ -2683,6 +2700,11 @@ def onSave(self):
config.conf["virtualBuffers"]["trapNonCommandGestures"] = (
self.trapNonCommandGesturesCheckBox.IsChecked()
)
config.conf["virtualBuffers"]["browseModeTouchNavigationElements"] = [
itemType
for i, (itemType, _label) in enumerate(self._browseModeElements)
if self._browseModeCheckListBox.IsChecked(i)
]


class MathSettingsPanel(SettingsPanel):
Expand Down
78 changes: 69 additions & 9 deletions source/touchHandler.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2012-2025 NV Access Limited, Joseph Lee, Babbage B.V.
# Copyright (C) 2012-2026 NV Access Limited, Joseph Lee, Babbage B.V., Kefas Lungu

"""handles touchscreen interaction.
Used to provide input gestures for touchscreens, touch modes and other support facilities.
In order to use touch features, NVDA must be installed on a touchscreen computer.
"""

import threading
from functools import cached_property
from typing import (
TYPE_CHECKING,
Self,
)

if TYPE_CHECKING:
import browseMode

from ctypes import (
byref,
Structure,
Expand Down Expand Up @@ -43,6 +52,8 @@
import core
import systemUtils
from utils import _deprecate
from utils.displayString import DisplayStringStrEnum
from treeInterceptorHandler import post_browseModeStateChange

__getattr__ = _deprecate.handleDeprecations(
_deprecate.MovedSymbol(
Expand All @@ -51,15 +62,38 @@
"SystemMetrics",
"MAXIMUM_TOUCHES",
),
_deprecate.RemovedSymbol(
"touchModeLabels",
{
"text": _("text mode"),
"object": _("object mode"),
"browse": _("browse mode"),
},
message="Use touchHandler.TouchMode enum instead.",
),
)


availableTouchModes = ["text", "object"]
class TouchMode(DisplayStringStrEnum):
"""Available touch screen navigation modes."""

TEXT = "text"
OBJECT = "object"
BROWSE = "browse"

@cached_property
def _displayStringLabels(self) -> dict[Self, str]:
return {
# Translators: The name of a touch mode.
TouchMode.TEXT: _("text mode"),
# Translators: The name of a touch mode.
TouchMode.OBJECT: _("object mode"),
# Translators: The name of a touch mode used when in browse mode.
TouchMode.BROWSE: _("browse mode"),
}


touchModeLabels = {
"text": _("text mode"),
"object": _("object mode"),
}
availableTouchModes: list[TouchMode] = [TouchMode.TEXT, TouchMode.OBJECT]

HWND_MESSAGE = -3

Expand Down Expand Up @@ -95,6 +129,30 @@
POINTER_MESSAGE_FLAG_CANCELED = 0x400


def _browseModeStateChange(
browseMode: bool = False,
interceptor: "browseMode.BrowseModeTreeInterceptor | None" = None,
**kwargs,
) -> None:
if not handler:
return

if browseMode:
# Entering browse mode
if TouchMode.BROWSE not in availableTouchModes:
availableTouchModes.append(TouchMode.BROWSE)

handler._curTouchMode = TouchMode.BROWSE

else:
# Leaving browse mode
if TouchMode.BROWSE in availableTouchModes:
availableTouchModes.remove(TouchMode.BROWSE)

if handler._curTouchMode == TouchMode.BROWSE:
handler._curTouchMode = TouchMode.OBJECT


class POINTER_INFO(Structure):
_fields_ = [
("pointerType", DWORD),
Expand Down Expand Up @@ -232,7 +290,7 @@ def getDisplayTextForIdentifier(cls, identifier):
# Translators: a touch screen gesture
source = _("Touch screen")
if mode:
source = "{source}, {mode}".format(source=source, mode=touchModeLabels[mode])
source = "{source}, {mode}".format(source=source, mode=TouchMode(mode).displayString)
return source, " + ".join(actions)

def _get__immediate(self):
Expand All @@ -249,7 +307,7 @@ class TouchHandler(threading.Thread):
def __init__(self):
self.pendingEmitsTimer = gui.NonReEntrantTimer(core.requestPump)
super().__init__(name=f"{self.__class__.__module__}.{self.__class__.__qualname__}")
self._curTouchMode = "object"
self._curTouchMode = TouchMode.OBJECT
self.initializedEvent = threading.Event()
self.threadExc = None
self.start()
Expand Down Expand Up @@ -333,7 +391,7 @@ def setMode(self, mode):

def pump(self):
for preheldTracker, tracker in self.trackerManager.emitTrackers():
gesture = TouchInputGesture(preheldTracker, tracker, self._curTouchMode)
gesture = TouchInputGesture(preheldTracker, tracker, self._curTouchMode.value)
try:
inputCore.manager.executeGesture(gesture)
except inputCore.NoInputGestureAction:
Expand Down Expand Up @@ -408,12 +466,14 @@ def initialize():
% user32.GetSystemMetrics(SystemMetrics.MAXIMUM_TOUCHES),
)
config.post_configProfileSwitch.register(handlePostConfigProfileSwitch)
post_browseModeStateChange.register(_browseModeStateChange)
setTouchSupport(config.conf["touch"]["enabled"])


def terminate():
global handler
config.post_configProfileSwitch.unregister(handlePostConfigProfileSwitch)
post_browseModeStateChange.unregister(_browseModeStateChange)
if handler:
handler.terminate()
handler = None
2 changes: 0 additions & 2 deletions tests/checkPot.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@
"column break",
"background pattern {pattern}",
"NVDA Speech Viewer",
"text mode",
"object mode",
"NonVisual Desktop Access",
"A free and open source screen reader for Microsoft Windows",
"Copyright (C) {years} NVDA Contributors",
Expand Down
3 changes: 3 additions & 0 deletions user_docs/en/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
* A new command, assigned to `NVDA+x`, has been introduced to repeat the last information spoken by NVDA; pressing it twice shows it in a browseable message. (#625, @CyrilleB79)
* Added an unassigned command to toggle keyboard layout. (#19211, @CyrilleB79)
* Added an unassigned Quick Navigation Command for jumping to next/previous slider in browse mode. (#17005, @hdzrvcc0X74)
* Added touch based navigation of browse mode elements, allowing touch screen users to move between links, headings, form fields, lists, tables and other quick navigation elements. (#3424, @kefaslungu)
* Flick down or up to cycle through element types; flick right or left to navigate between elements of the selected type.
* The element types shown when cycling can be configured in the Browse Mode settings panel.
* Added support for custom speech dictionaries. (#19558, #17468, @LeonarddeR)
* Dictionaries can be provided in the `speechDicts` folder in an add-on package.
* Dictionary metadata can be added to an optional `speechDictionaries` section in the add-on manifest.
Expand Down
45 changes: 43 additions & 2 deletions user_docs/en/userGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -599,13 +599,13 @@ Therefore, gestures such as 2-finger flick up and 4-finger flick left are all po
#### Touch Modes {#TouchModes}

As there are many more NVDA commands than possible touch gestures, NVDA has several touch modes you can switch between which make certain subsets of commands available.
The two modes are text mode and object mode.
The three modes are text mode, object mode and browse mode.
Certain NVDA commands listed in this document may have a touch mode listed in brackets after the touch gesture.
For example, flick up (text mode) means that the command will be performed if you flick up, but only while in text mode.
If the command does not have a mode listed, it will work in any mode.

<!-- KC:beginInclude -->
To toggle touch modes, perform a 3-finger tap.
To switch between touch modes, perform a 3-finger tap.
<!-- KC:endInclude -->

#### Touch keyboard {#TouchKeyboard}
Expand Down Expand Up @@ -1050,6 +1050,38 @@ If you want to use these while still being able to use your cursor keys to read
To toggle single letter navigation on and off for the current document, press NVDA+shift+space.
<!-- KC:endInclude -->

#### Touch Navigation in Browse Mode {#BrowseModeTouch}

When using a touch enabled device, NVDA provides an additional touch navigation mode for browsing content in browse mode.

When browse mode is active in supported documents such as web pages or Word documents, NVDA can expose a browse touch mode.
This mode allows users to navigate structural elements of a document using touch gestures, similar to browse mode navigation with the keyboard.

In browse touch mode, flick gestures are used to move between common document elements such as links, buttons, headings, form fields, landmarks, and other document structures.

This feature is intended to provide touch users with efficient, structured navigation that mirrors existing browse mode functionality.

##### Touch gestures in browse mode

<!-- KC:beginInclude -->

| Name | Touch | Description |
|---|---|---|
| Select next element type | flick down | Switches to the next browse mode navigation element type |
| Select previous element type | flick up | Switches to the previous browse mode navigation element type |
| Move to next element | flick right | Moves to the next browse mode element of the selected type |
| Move to previous element | flick left | Moves to the previous browse mode element of the selected type |

<!-- KC:endInclude -->

When the "default" element type is selected, flicking left or right moves through all elements in the document.
When any other element type is selected, flicking left or right moves to the previous or next element of that type.
Flicking up or down cycles through the available element types.

The selected element type is remembered separately for each document while it remains open.
Note that browse touch mode gestures only take effect when browse mode is active in the document.
If focus moves outside the document (for example, to the browser address bar or the taskbar), browse touch mode gestures will not navigate the document until focus returns to it in browse mode.

#### Text paragraph navigation command {#TextNavigationCommand}

You can jump to the next or previous text paragraph by pressing `p` or `shift+p`.
Expand Down Expand Up @@ -3292,6 +3324,15 @@ Enabled by default, this option allows you to choose if gestures (such as key pr
As an example, if enabled and the letter j was pressed, it would be trapped from reaching the document, even though it is not a quick navigation command nor is it likely to be a command in the application itself.
In this case NVDA will tell Windows to play a default sound whenever a key which gets trapped is pressed.

##### Browse mode touch navigation elements {#BrowseModeSettingsBrowseModeNavigationElements}

This list allows you to choose which element types are available when cycling through elements in browse touch mode.
Use the checkboxes to enable or disable individual element types.
Only the checked element types will appear when flicking up or down to cycle through browse mode navigation elements.
This setting only affects touch navigation and has no effect on keyboard browse mode navigation.

Available element types are those available from [single letter navigation](#SingleLetterNavigation).

#### Document Formatting {#DocumentFormattingSettings}

<!-- KC:setting -->
Expand Down
Loading