Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
39 changes: 39 additions & 0 deletions source/_magnifier/magnifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@
from comtypes import COMError
from logHandler import log
import wx
import gui
import ui
import speech
import screenCurtain
import touchHandler
from winAPI import _displayTracking
from winAPI.winUser.constants import SystemMetrics
from winBindings import user32
from winAPI._displayTracking import OrientationState, getPrimaryDisplayOrientation
from .utils.types import (
MagnifierParameters,
Expand Down Expand Up @@ -184,6 +188,39 @@ def _startMagnifier(self) -> None:

self._isActive = True
self.currentCoordinates = self._focusManager.getCurrentFocusCoordinates()
if touchHandler.handler is not None:
# Touch events are already intercepted by NVDA; block gesture execution
# to prevent incorrect coordinates from the magnified view reaching the system.
touchHandler.blockTouchInput = True
elif user32.GetSystemMetrics(SystemMetrics.MAXIMUM_TOUCHES) > 0:
# The user has a touchscreen but NVDA cannot intercept inputs; warn them.
if touchHandler.touchSupported():
reason = pgettext(
"magnifier",
# Translators: Warning when magnifier is started on a device with a touch screen but NVDA's touch support is disabled.
"Touch screen input cannot be used because NVDA touch support is disabled. ",
)
else:
reason = pgettext(
"magnifier",
# Translators: Warning when the magnifier starts on a portable/source copy with a touchscreen.
"Touch screen input cannot be used because NVDA is not installed. ",
)
wx.CallAfter(
gui.messageBox,
reason
+ pgettext(
"magnifier",
# Translators: Suffix appended to touch warning when touch inputs cannot be blocked by the magnifier.
"Touch inputs may not behave as expected while the magnifier is running.",
),
pgettext(
"magnifier",
# Translators: Title of the warning dialog shown when touch cannot be intercepted while the magnifier is running.
"Touch Screen Warning",
),
wx.OK | wx.ICON_WARNING,
)

def _updateMagnifier(self) -> None:
"""
Expand Down Expand Up @@ -254,6 +291,8 @@ def _stopMagnifier(self) -> None:
return
self._stopTimer()
self._isActive = False
if touchHandler.handler is not None:
touchHandler.blockTouchInput = False
# Unregister from display changes
_displayTracking.displayChanged.unregister(self._onDisplayChanged)

Expand Down
5 changes: 5 additions & 0 deletions source/touchHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"""

import threading
import winsound
from functools import cached_property
from typing import (
TYPE_CHECKING,
Expand Down Expand Up @@ -189,6 +190,7 @@ class POINTER_TOUCH_INFO(Structure):

touchWindow = None
touchThread = None
blockTouchInput: bool = False


class TouchInputGesture(inputCore.InputGesture):
Expand Down Expand Up @@ -391,6 +393,9 @@ def setMode(self, mode):

def pump(self):
for preheldTracker, tracker in self.trackerManager.emitTrackers():
if blockTouchInput:
winsound.MessageBeep(winsound.MB_ICONASTERISK)
continue
gesture = TouchInputGesture(preheldTracker, tracker, self._curTouchMode.value)
try:
inputCore.manager.executeGesture(gesture)
Expand Down
86 changes: 86 additions & 0 deletions tests/unit/test_magnifier/test_magnifierTouchscreen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2025-2026 NV Access Limited, Antoine Haffreingue
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt

from _magnifier.magnifier import Magnifier
from _magnifier.utils.types import Coordinates
from tests.unit.test_magnifier.test_magnifier import _TestMagnifier
import touchHandler
from winAPI._displayTracking import getPrimaryDisplayOrientation
from unittest.mock import MagicMock, patch


class TestMagnifierTouchscreen(_TestMagnifier):
"""Tests for touchscreen blocking/warning behaviour when the magnifier starts and stops."""

def setUp(self):
super().setUp()
self.magnifier = Magnifier()
screenDimensions = getPrimaryDisplayOrientation()
self.focusCoords = Coordinates(screenDimensions.width // 2, screenDimensions.height // 2)
self.magnifier._focusManager.getCurrentFocusCoordinates = MagicMock(return_value=self.focusCoords)

def tearDown(self):
if hasattr(self.magnifier, "_timer") and self.magnifier._timer:
self.magnifier._timer.Stop()
self.magnifier._timer = None
if hasattr(self.magnifier, "_isActive") and self.magnifier._isActive:
self.magnifier._isActive = False
super().tearDown()
Comment on lines +17 to +30

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.

this still seems to be unnecessarily duplicating setup/tearDown from TestMagnifier

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think tests could use a good clean up regarding all the changes that have been made since the start, I would say we let this like that and I'll do a big clean up in the future

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 we make base class and use it across these two test suites? I'm not asking for a full refactor, just for the changed/added code. We try to fix things incrementally when we touch them rather than do large scale refactors.


def testStartMagnifierBlocksTouchWhenHandlerActive(self):
"""blockTouchInput is set when the magnifier starts with the touch handler running."""
with (
patch("touchHandler.handler", new=MagicMock()),
patch("touchHandler.blockTouchInput", False),
):
self.magnifier._startMagnifier()
self.assertTrue(touchHandler.blockTouchInput)

def testStartMagnifierWarnsWhenTouchSupportDisabled(self):
"""Dialog shown when device has a touchscreen but NVDA touch support is disabled."""
with (
patch("touchHandler.handler", new=None),
patch("winBindings.user32.GetSystemMetrics", return_value=5),
patch("touchHandler.touchSupported", return_value=True),
patch("_magnifier.magnifier.wx.CallAfter") as mock_call_after,
):
self.magnifier._startMagnifier()

mock_call_after.assert_called_once()

def testStartMagnifierWarnsOnPortableOrNoUIAccess(self):
"""Dialog shown when device has a touchscreen but NVDA cannot intercept (portable/no UI access)."""
with (
patch("touchHandler.handler", new=None),
patch("winBindings.user32.GetSystemMetrics", return_value=5),
patch("touchHandler.touchSupported", return_value=False),
patch("_magnifier.magnifier.wx.CallAfter") as mock_call_after,
):
self.magnifier._startMagnifier()

mock_call_after.assert_called_once()

def testStartMagnifierNoActionWithoutTouchscreen(self):
"""No dialog and no blocking when the device has no touchscreen."""
with (
patch("touchHandler.handler", new=None),
patch("winBindings.user32.GetSystemMetrics", return_value=0),
patch("_magnifier.magnifier.wx.CallAfter") as mock_call_after,
):
self.magnifier._startMagnifier()

mock_call_after.assert_not_called()

def testStopMagnifierUnblocksTouchInput(self):
"""blockTouchInput is reset to False when the magnifier stops with the touch handler active."""
self.magnifier._stopTimer = MagicMock()
self.magnifier._isActive = True
touchHandler.blockTouchInput = True
self.addCleanup(setattr, touchHandler, "blockTouchInput", False)

with patch("touchHandler.handler", new=MagicMock()):
self.magnifier._stopMagnifier()

self.assertFalse(touchHandler.blockTouchInput)
Loading