From b839c3cec1c9e1a307a832f5cdb9a5bd4f16f06b Mon Sep 17 00:00:00 2001 From: Antoine HAFFREINGUE Date: Tue, 7 Apr 2026 16:10:50 +0200 Subject: [PATCH 01/11] preparing the new magnifier types --- source/_magnifier/__init__.py | 97 +++++++++++++++---- source/_magnifier/commands.py | 56 +++++++++-- source/_magnifier/config.py | 20 +++- source/_magnifier/fullscreenMagnifier.py | 4 +- source/_magnifier/magnifier.py | 2 +- source/_magnifier/placeholderMagnifier.py | 26 +++++ source/_magnifier/utils/types.py | 9 ++ source/config/configSpec.py | 1 + source/globalCommands.py | 14 +++ .../test_fullscreenMagnifier.py | 16 ++- tests/unit/test_magnifier/test_magnifier.py | 3 +- user_docs/en/userGuide.md | 28 +++++- 12 files changed, 238 insertions(+), 38 deletions(-) create mode 100644 source/_magnifier/placeholderMagnifier.py diff --git a/source/_magnifier/__init__.py b/source/_magnifier/__init__.py index dedad330819..46da76ab129 100644 --- a/source/_magnifier/__init__.py +++ b/source/_magnifier/__init__.py @@ -9,7 +9,8 @@ """ from typing import TYPE_CHECKING -from .fullscreenMagnifier import FullScreenMagnifier +from .config import getMagnifierType +from .utils.types import MagnifierType if TYPE_CHECKING: from .magnifier import Magnifier @@ -17,47 +18,107 @@ _magnifier: "Magnifier | None" = None -def initialize(): +def createMagnifier(magnifierType: MagnifierType) -> "Magnifier": """ - Initialize the magnifier module - For now, only the full-screen magnifier is supported + Create a magnifier instance based on the specified type. + + :param magnifierType: The type of magnifier to create + :return: The created magnifier instance + :raises ValueError: If the magnifier type is not supported + """ + + match magnifierType: + case MagnifierType.FULLSCREEN: + from .fullscreenMagnifier import FullScreenMagnifier + + return FullScreenMagnifier() + + case MagnifierType.FIXED: + from .placeholderMagnifier import PlaceholderMagnifier + + return PlaceholderMagnifier() + + case MagnifierType.DOCKED: + from .placeholderMagnifier import PlaceholderMagnifier + + return PlaceholderMagnifier() + + case MagnifierType.LENS: + from .placeholderMagnifier import PlaceholderMagnifier + + return PlaceholderMagnifier() + + case _: + raise ValueError(f"Unsupported magnifier type: {magnifierType}") + + +def _setMagnifierType(magnifierType: MagnifierType) -> None: + """ + Set the magnifier type, stopping the current one if active and creating a new instance. + + :param magnifierType: The type of magnifier to set """ + global _magnifier + + # Stop current magnifier if active + if _magnifier and _magnifier._isActive: + _magnifier._stopMagnifier() - magnifier = FullScreenMagnifier() - setMagnifier(magnifier) + # Create and set new magnifier instance + _magnifier = createMagnifier(magnifierType) + + +def initialize() -> None: + """ + Initialize the magnifier module with the default magnifier type from config. + """ + magnifierType = getMagnifierType() + _setMagnifierType(magnifierType) + _magnifier._startMagnifier() def isActive() -> bool: """ - Check if magnifier is currently active for settings + Check if magnifier is currently active. + + :return: True if magnifier is active, False otherwise """ global _magnifier - return _magnifier and _magnifier._isActive + return _magnifier is not None and _magnifier._isActive -def getMagnifier() -> "Magnifier | None": +def changeMagnifierType(magnifierType: MagnifierType) -> None: """ - Get current magnifier + Change the magnifier type at runtime. + Stops the current magnifier and starts a new one of the specified type. + + :param magnifierType: The new magnifier type to use + :raises RuntimeError: If no magnifier is currently active """ global _magnifier - return _magnifier + if not _magnifier or not _magnifier._isActive: + raise RuntimeError("Cannot change magnifier type: magnifier is not active") + _setMagnifierType(magnifierType) + _magnifier._startMagnifier() -def setMagnifier(magnifier: "Magnifier") -> None: + +def getMagnifier() -> "Magnifier | None": """ - Set magnifier instance + Get the current magnifier instance. - :param magnifier: The magnifier instance to set + :return: The current magnifier instance, or None if not initialized """ global _magnifier - _magnifier = magnifier + return _magnifier -def terminate(): +def terminate() -> None: """ - Called when NVDA shuts down + Terminate the magnifier module. + Called when NVDA shuts down. """ global _magnifier if _magnifier and _magnifier._isActive: _magnifier._stopMagnifier() - _magnifier = None + _magnifier = None diff --git a/source/_magnifier/commands.py b/source/_magnifier/commands.py index 7c75e2ec42c..603ecd0bd0a 100644 --- a/source/_magnifier/commands.py +++ b/source/_magnifier/commands.py @@ -10,11 +10,13 @@ from typing import Literal import ui -from . import getMagnifier, initialize, terminate +from . import getMagnifier, initialize, terminate, changeMagnifierType from .config import ( getDefaultZoomLevelString, getDefaultFilter, getDefaultFullscreenMode, + getMagnifierType, + setMagnifierType, ZoomLevel, ) from .magnifier import Magnifier @@ -101,19 +103,31 @@ def toggleMagnifier() -> None: initialize() filter = getDefaultFilter() - fullscreenMode = getDefaultFullscreenMode() - - ui.message( - pgettext( + magnifierType = getMagnifierType() + zoomLevel = getDefaultZoomLevelString() + if magnifierType == MagnifierType.FULLSCREEN: + fullscreenMode = getDefaultFullscreenMode() + msg = pgettext( "magnifier", # Translators: Message announced when starting the NVDA magnifier. - "Starting magnifier with {zoomLevel} zoom level, {filter} filter, and {fullscreenMode} full-screen mode", + "Starting {magnifierType} magnifier with {zoomLevel} zoom level, {filter} filter, and {fullscreenMode} full-screen mode", ).format( - zoomLevel=getDefaultZoomLevelString(), + magnifierType=magnifierType.displayString, + zoomLevel=zoomLevel, filter=filter.displayString, fullscreenMode=fullscreenMode.displayString, - ), - ) + ) + else: + msg = pgettext( + "magnifier", + # Translators: Message announced when starting the NVDA magnifier. + "Starting {magnifierType} magnifier with {zoomLevel} zoom level and {filter} filter", + ).format( + magnifierType=magnifierType.displayString, + zoomLevel=zoomLevel, + filter=filter.displayString, + ) + ui.message(msg) def zoom(direction: Direction) -> None: @@ -175,6 +189,30 @@ def toggleFilter() -> None: ) +def cycleMagnifierType() -> None: + """Cycle through magnifier types (full-screen, fixed, docked (to do), lens (to do))""" + magnifier: Magnifier = getMagnifier() + if magnifierIsActiveVerify( + magnifier, + MagnifierAction.CHANGE_MAGNIFIER_TYPE, + ): + types = list(MagnifierType) + currentType = magnifier._magnifierType + idx = types.index(currentType) + newType = types[(idx + 1) % len(types)] + log.debug(f"Changing magnifier type from {currentType} to {newType}") + changeMagnifierType(newType) + setMagnifierType(newType) + magnifier = getMagnifier() + ui.message( + pgettext( + "magnifier", + # Translators: Message announced when changing the magnifier type with {type} being the new magnifier type. + "Magnifier type changed to {type}", + ).format(type=magnifier._magnifierType.displayString), + ) + + def toggleFullscreenMode() -> None: """Cycle through full-screen focus modes (center, border, relative)""" magnifier: Magnifier = getMagnifier() diff --git a/source/_magnifier/config.py b/source/_magnifier/config.py index 7fc38ba6ab5..36e6881eb01 100644 --- a/source/_magnifier/config.py +++ b/source/_magnifier/config.py @@ -9,7 +9,7 @@ """ import config -from .utils.types import Filter, FullScreenMode +from .utils.types import Filter, FullScreenMode, MagnifierType class ZoomLevel: @@ -123,6 +123,24 @@ def setDefaultFilter(filter: Filter) -> None: config.conf["magnifier"]["defaultFilter"] = filter.value +def getMagnifierType() -> MagnifierType: + """ + Get magnifier type from config. + + :return: The magnifier type. + """ + return MagnifierType(config.conf["magnifier"]["magnifierType"]) + + +def setMagnifierType(magnifierType: MagnifierType) -> None: + """ + Set magnifier type from settings. + + :param magnifierType: The magnifier type to set. + """ + config.conf["magnifier"]["magnifierType"] = magnifierType.value + + def getDefaultFullscreenMode() -> FullScreenMode: """ Get default full-screen mode from config. diff --git a/source/_magnifier/fullscreenMagnifier.py b/source/_magnifier/fullscreenMagnifier.py index 35c9fb076c5..30638b8624f 100644 --- a/source/_magnifier/fullscreenMagnifier.py +++ b/source/_magnifier/fullscreenMagnifier.py @@ -14,17 +14,17 @@ from .magnifier import Magnifier from .utils.filterHandler import FilterMatrix from .utils.spotlightManager import SpotlightManager -from .utils.types import Filter, Coordinates, FullScreenMode +from .utils.types import Filter, Coordinates, FullScreenMode, MagnifierType from .config import getDefaultFullscreenMode class FullScreenMagnifier(Magnifier): def __init__(self): super().__init__() + self._magnifierType = MagnifierType.FULLSCREEN self._fullscreenMode = getDefaultFullscreenMode() self._currentCoordinates = Coordinates(0, 0) self._spotlightManager = SpotlightManager(self) - self._startMagnifier() @property def filterType(self) -> Filter: diff --git a/source/_magnifier/magnifier.py b/source/_magnifier/magnifier.py index e3ebf34dee0..56f53889de6 100644 --- a/source/_magnifier/magnifier.py +++ b/source/_magnifier/magnifier.py @@ -42,7 +42,7 @@ class Magnifier: def __init__(self): self._displayOrientation = getPrimaryDisplayOrientation() - self._magnifierType: MagnifierType = MagnifierType.FULLSCREEN + self._magnifierType: MagnifierType self._isActive: bool = False self._zoomLevel: float = getDefaultZoomLevel() self._panStep: int = getDefaultPanStep() diff --git a/source/_magnifier/placeholderMagnifier.py b/source/_magnifier/placeholderMagnifier.py new file mode 100644 index 00000000000..acd63426377 --- /dev/null +++ b/source/_magnifier/placeholderMagnifier.py @@ -0,0 +1,26 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2025 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 +""" +Placeholder magnifier module. +""" + +from .magnifier import Magnifier +from .utils.types import Coordinates, MagnifierType + + +class PlaceholderMagnifier(Magnifier): + def __init__(self): + super().__init__() + self._magnifierType = MagnifierType.PLACEHOLDER + self._currentCoordinates = Coordinates(0, 0) + + def _startMagnifier(self) -> None: + super()._startMagnifier() + + def _stopMagnifier(self) -> None: + super()._stopMagnifier() + + def _doUpdate(self): + super()._doUpdate() diff --git a/source/_magnifier/utils/types.py b/source/_magnifier/utils/types.py index cc18b9f70b6..847117a9ca2 100644 --- a/source/_magnifier/utils/types.py +++ b/source/_magnifier/utils/types.py @@ -41,6 +41,7 @@ class MagnifierAction(DisplayStringEnum): PAN_TOP_EDGE = auto() PAN_BOTTOM_EDGE = auto() TOGGLE_FILTER = auto() + CHANGE_MAGNIFIER_TYPE = auto() CHANGE_FULLSCREEN_MODE = auto() START_SPOTLIGHT = auto() @@ -69,6 +70,8 @@ def _displayStringLabels(self) -> dict["MagnifierAction", str]: self.PAN_BOTTOM_EDGE: pgettext("magnifier action", "pan to bottom edge"), # Translators: Action description for toggling color filters. self.TOGGLE_FILTER: pgettext("magnifier action", "toggle filters"), + # Translators: Action description for changing magnifier type. + self.CHANGE_MAGNIFIER_TYPE: pgettext("magnifier action", "change magnifier type"), # Translators: Action description for changing full-screen mode. self.CHANGE_FULLSCREEN_MODE: pgettext("magnifier action", "change full-screen mode"), # Translators: Action description for starting spotlight mode. @@ -80,18 +83,24 @@ class MagnifierType(DisplayStringStrEnum): """Type of magnifier""" FULLSCREEN = "fullscreen" + FIXED = "fixed" DOCKED = "docked" LENS = "lens" + PLACEHOLDER = "placeholder" @property def _displayStringLabels(self) -> dict["MagnifierType", str]: return { # Translators: Magnifier type - full-screen mode. self.FULLSCREEN: pgettext("magnifier", "Fullscreen"), + # Translators: Magnifier type - fixed mode. + self.FIXED: pgettext("magnifier", "Fixed"), # Translators: Magnifier type - docked mode. self.DOCKED: pgettext("magnifier", "Docked"), # Translators: Magnifier type - lens mode. self.LENS: pgettext("magnifier", "Lens"), + # Translators: Magnifier type - placeholder for unsupported types. + self.PLACEHOLDER: pgettext("magnifier", "Placeholder"), } diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 1cc61e0c6a1..997d54a82a8 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -116,6 +116,7 @@ [magnifier] defaultZoomLevel = float(min=1.0, max=10.0, default=2.0) defaultPanStep = integer(min=1, max=100, default=10) + magnifierType = string(default="fullscreen") defaultFullscreenMode = string(default="center") isTrueCentered = boolean(default=False) defaultFilter = string(default="normal") diff --git a/source/globalCommands.py b/source/globalCommands.py index de4a04cfe6d..bd61131c34b 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -5159,6 +5159,20 @@ def script_toggleFilter( ) -> None: _magnifier.commands.toggleFilter() + @script( + description=_( + # Translators: Describes a command. + "Cycle through Magnifier type", + ), + category=SCRCAT_VISION, + gesture="kb:nvda+shift+t", + ) + def script_cycleMagnifierType( + self, + gesture: inputCore.InputGesture, + ) -> None: + _magnifier.commands.cycleMagnifierType() + @script( description=_( # Translators: Describes a command. diff --git a/tests/unit/test_magnifier/test_fullscreenMagnifier.py b/tests/unit/test_magnifier/test_fullscreenMagnifier.py index f300db49d3e..ecc55d8364e 100644 --- a/tests/unit/test_magnifier/test_fullscreenMagnifier.py +++ b/tests/unit/test_magnifier/test_fullscreenMagnifier.py @@ -11,12 +11,13 @@ from winAPI._displayTracking import getPrimaryDisplayOrientation -class TestMagnifierEndToEnd(_TestMagnifier): - """End-to-end test suite for Magnifier functionality.""" +class TestFullscreenMagnifierEndToEnd(_TestMagnifier): + """End-to-end test suite for fullscreen magnifier functionality.""" def testMagnifierCreation(self): """Test creating a magnifier.""" magnifier = FullScreenMagnifier() + magnifier._startMagnifier() self.assertEqual(magnifier.zoomLevel, 2.0) self.assertEqual(magnifier.filterType, Filter.NORMAL) @@ -29,6 +30,7 @@ def testMagnifierCreation(self): def testMagnifierZoom(self): """Test zoom functionality.""" magnifier = FullScreenMagnifier() + magnifier._startMagnifier() # Set initial zoom to 1.0 for predictable testing magnifier.zoomLevel = 1.0 @@ -48,6 +50,7 @@ def testMagnifierZoom(self): def testMagnifierCoordinates(self): """Test coordinate handling.""" magnifier = FullScreenMagnifier() + magnifier._startMagnifier() # Test setting coordinates magnifier._currentCoordinates = (100, 200) @@ -63,6 +66,7 @@ def testMagnifierCoordinates(self): def testMagnifierUpdate(self): """Test magnifier update cycle.""" magnifier = FullScreenMagnifier() + magnifier._startMagnifier() # Mock the update methods magnifier._getCoordinatesForMode = MagicMock(return_value=(150, 250)) @@ -85,6 +89,7 @@ def testMagnifierUpdate(self): def testMagnifierStop(self): """Test stopping the magnifier.""" magnifier = FullScreenMagnifier() + magnifier._startMagnifier() # Mock the timer magnifier._stopTimer = MagicMock() @@ -102,6 +107,7 @@ def testMagnifierStop(self): def testMagnifierPositionCalculation(self): """Test position calculation.""" magnifier = FullScreenMagnifier() + magnifier._startMagnifier() # Test position calculation left, top, width, height = magnifier._getMagnifierPosition((500, 400)) @@ -125,6 +131,7 @@ def testMagnifierPositionCalculation(self): def testMagnifierZoomBoundaries(self): """Test zoom boundaries.""" magnifier = FullScreenMagnifier() + magnifier._startMagnifier() magnifier.zoomLevel = 1.0 # Test minimum boundary @@ -142,6 +149,7 @@ def testMagnifierZoomBoundaries(self): def testMagnifierTypeProperty(self): """Test magnifierType property for FullScreenMagnifier.""" magnifier = FullScreenMagnifier() + magnifier._startMagnifier() # Should default to FULLSCREEN self.assertEqual(magnifier._magnifierType, MagnifierType.FULLSCREEN) @@ -155,6 +163,7 @@ def testMagnifierTypeProperty(self): def testMagnifierInheritance(self): """Test inheritance structure.""" magnifier = FullScreenMagnifier() + magnifier._startMagnifier() self.assertIsInstance(magnifier, Magnifier) @@ -172,6 +181,7 @@ def testMagnifierInheritance(self): def testMagnifierApiHandling(self): """Test API error handling.""" magnifier = FullScreenMagnifier() + magnifier._startMagnifier() # Mock magnification API to fail magnifier._stopTimer = MagicMock() @@ -190,6 +200,7 @@ def testMagnifierSimpleLifecycle(self): """Test simple magnifier lifecycle.""" # Create magnifier magnifier = FullScreenMagnifier() + magnifier._startMagnifier() self.assertTrue(magnifier._isActive) self.assertEqual(magnifier.zoomLevel, 2.0) @@ -220,6 +231,7 @@ class TestFullScreenMagnifierKeepMouseCentered(_TestMagnifier): def setUp(self): super().setUp() self.magnifier = FullScreenMagnifier() + self.magnifier._startMagnifier() self.screen = getPrimaryDisplayOrientation() def tearDown(self): diff --git a/tests/unit/test_magnifier/test_magnifier.py b/tests/unit/test_magnifier/test_magnifier.py index b901507ea15..bb0b961bf5b 100644 --- a/tests/unit/test_magnifier/test_magnifier.py +++ b/tests/unit/test_magnifier/test_magnifier.py @@ -3,7 +3,7 @@ # 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, MagnifierType +from _magnifier.magnifier import Magnifier from _magnifier.utils.types import Filter, Direction, Coordinates, MagnifierAction import unittest from winAPI._displayTracking import getPrimaryDisplayOrientation @@ -63,7 +63,6 @@ def testMagnifierCreation(self): """Can we create a magnifier with valid parameters?""" self.assertEqual(self.magnifier.zoomLevel, 2.0) self.assertEqual(self.magnifier._filterType, Filter.NORMAL) - self.assertEqual(self.magnifier._magnifierType, MagnifierType.FULLSCREEN) self.assertFalse(self.magnifier._isActive) self.assertIsNotNone(self.magnifier._focusManager) diff --git a/user_docs/en/userGuide.md b/user_docs/en/userGuide.md index b4d15196fd8..01489be91c5 100644 --- a/user_docs/en/userGuide.md +++ b/user_docs/en/userGuide.md @@ -1617,9 +1617,19 @@ NVDA will announce the name of the currently selected filter. The default color filter when the magnifier is first enabled can be configured in the [Magnifier settings](#MagnifierSettings). -### Focus Tracking Modes {#MagnifierFullscreenFocusModes} +### Magnifier Types {#MagnifierType} The magnifier offers three different modes for tracking focus and determining which part of the screen to magnify: +The magnifier can be used in multiple modes, each designed to suit different user needs and preferences: + +* Full-screen: The entire screen is magnified, and the magnified view follows the system focus or mouse pointer. +* Fixed window: A separate window displays the magnified content, and the rest of the screen remains at normal size. This allows you to see both the magnified content and the surrounding context simultaneously. +* Docked: The magnified view is docked to one edge of the screen, providing a larger view of the area around the system focus or mouse pointer while still showing most of the screen at normal size. +* Lens: A rectangular area around the system focus or mouse pointer is magnified, while the rest of the screen remains at normal size. This allows you to focus on a specific area without losing sight of the overall screen layout. + +### Fullscreen Focus Modes {#MagnifierFullscreenFocusModes} + +The fullscreen magnifier offers three different modes for tracking focus and determining which part of the screen to magnify: * Center: The magnified area is centered on the current focus position. This mode keeps the focused element at the center of the screen and clamps to the screen edge. @@ -1634,9 +1644,9 @@ NVDA will announce the name of the currently selected mode. The default focus mode when the magnifier is first enabled can be configured in the [Magnifier settings](#MagnifierSettings). -### Spotlight Mode {#MagnifierSpotlight} +#### Spotlight Mode {#MagnifierSpotlight} -Spotlight mode is a special feature designed for presentations or focused reading tasks. +Spotlight mode is a special fullscreen feature designed for presentations or focused reading tasks. When activated, it temporarily zooms out the magnified view to show the full screen, then zooms back in to the current focus position after a brief period of mouse inactivity. This is useful when you want to: @@ -1654,6 +1664,18 @@ Once activated, the magnifier will: Spotlight mode automatically deactivates after zooming back in. If you move the mouse before the zoom-back occurs, the timer resets, giving you more time to view the full screen. +### Fixed Magnifier {#MagnifierFixed} + +Placeholder + +### Docked Magnifier {#MagnifierDocked} + +Placeholder + +### Lens Magnifier {#MagnifierLens} + +Placeholder + ### Magnifier Settings {#MagnifierSettings} The magnifier can be configured in the "Magnifier" category of the NVDA Settings dialog (`NVDA+control+w`). From 489604c474d29be1606a7c834d8f441ecf6faee4 Mon Sep 17 00:00:00 2001 From: Antoine HAFFREINGUE Date: Mon, 27 Apr 2026 17:18:36 +0200 Subject: [PATCH 02/11] removed placeholder --- source/_magnifier/__init__.py | 12 ++++----- source/_magnifier/commands.py | 12 +++++++-- ...eholderMagnifier.py => dockedMagnifier.py} | 14 +++++----- source/_magnifier/fixedMagnifier.py | 26 +++++++++++++++++++ source/_magnifier/fullscreenMagnifier.py | 6 +---- source/_magnifier/lensMagnifier.py | 26 +++++++++++++++++++ source/_magnifier/magnifier.py | 8 ++++++ source/_magnifier/utils/types.py | 3 --- 8 files changed, 84 insertions(+), 23 deletions(-) rename source/_magnifier/{placeholderMagnifier.py => dockedMagnifier.py} (64%) create mode 100644 source/_magnifier/fixedMagnifier.py create mode 100644 source/_magnifier/lensMagnifier.py diff --git a/source/_magnifier/__init__.py b/source/_magnifier/__init__.py index 46da76ab129..b8dee2d9feb 100644 --- a/source/_magnifier/__init__.py +++ b/source/_magnifier/__init__.py @@ -34,19 +34,19 @@ def createMagnifier(magnifierType: MagnifierType) -> "Magnifier": return FullScreenMagnifier() case MagnifierType.FIXED: - from .placeholderMagnifier import PlaceholderMagnifier + from .fixedMagnifier import FixedMagnifier - return PlaceholderMagnifier() + return FixedMagnifier() case MagnifierType.DOCKED: - from .placeholderMagnifier import PlaceholderMagnifier + from .dockedMagnifier import DockedMagnifier - return PlaceholderMagnifier() + return DockedMagnifier() case MagnifierType.LENS: - from .placeholderMagnifier import PlaceholderMagnifier + from .lensMagnifier import LensMagnifier - return PlaceholderMagnifier() + return LensMagnifier() case _: raise ValueError(f"Unsupported magnifier type: {magnifierType}") diff --git a/source/_magnifier/commands.py b/source/_magnifier/commands.py index 8cea94e6781..fef52ae1fc9 100644 --- a/source/_magnifier/commands.py +++ b/source/_magnifier/commands.py @@ -199,14 +199,22 @@ def toggleFilter() -> None: ) +_CYCLING_MAGNIFIER_TYPES = [ + MagnifierType.FULLSCREEN, + MagnifierType.FIXED, + MagnifierType.DOCKED, + MagnifierType.LENS, +] + + def cycleMagnifierType() -> None: - """Cycle through magnifier types (full-screen, fixed, docked (to do), lens (to do))""" + """Cycle through magnifier types (full-screen, fixed, docked, lens)""" magnifier: Magnifier = getMagnifier() if magnifierIsActiveVerify( magnifier, MagnifierAction.CHANGE_MAGNIFIER_TYPE, ): - types = list(MagnifierType) + types = _CYCLING_MAGNIFIER_TYPES currentType = magnifier._magnifierType idx = types.index(currentType) newType = types[(idx + 1) % len(types)] diff --git a/source/_magnifier/placeholderMagnifier.py b/source/_magnifier/dockedMagnifier.py similarity index 64% rename from source/_magnifier/placeholderMagnifier.py rename to source/_magnifier/dockedMagnifier.py index acd63426377..882c7aa8223 100644 --- a/source/_magnifier/placeholderMagnifier.py +++ b/source/_magnifier/dockedMagnifier.py @@ -1,20 +1,20 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2025 NV Access Limited, Antoine Haffreingue +# 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 + """ -Placeholder magnifier module. +Docked magnifier module. """ from .magnifier import Magnifier -from .utils.types import Coordinates, MagnifierType +from .utils.types import MagnifierType -class PlaceholderMagnifier(Magnifier): +class DockedMagnifier(Magnifier): def __init__(self): super().__init__() - self._magnifierType = MagnifierType.PLACEHOLDER - self._currentCoordinates = Coordinates(0, 0) + self._magnifierType = MagnifierType.DOCKED def _startMagnifier(self) -> None: super()._startMagnifier() @@ -23,4 +23,4 @@ def _stopMagnifier(self) -> None: super()._stopMagnifier() def _doUpdate(self): - super()._doUpdate() + pass diff --git a/source/_magnifier/fixedMagnifier.py b/source/_magnifier/fixedMagnifier.py new file mode 100644 index 00000000000..dc6e66a63fa --- /dev/null +++ b/source/_magnifier/fixedMagnifier.py @@ -0,0 +1,26 @@ +# 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 + +""" +Fixed magnifier module. +""" + +from .magnifier import Magnifier +from .utils.types import MagnifierType + + +class FixedMagnifier(Magnifier): + def __init__(self): + super().__init__() + self._magnifierType = MagnifierType.FIXED + + def _startMagnifier(self) -> None: + super()._startMagnifier() + + def _stopMagnifier(self) -> None: + super()._stopMagnifier() + + def _doUpdate(self): + pass diff --git a/source/_magnifier/fullscreenMagnifier.py b/source/_magnifier/fullscreenMagnifier.py index 25d8654e2c1..2532e405d4c 100644 --- a/source/_magnifier/fullscreenMagnifier.py +++ b/source/_magnifier/fullscreenMagnifier.py @@ -28,11 +28,7 @@ def __init__(self): self._currentCoordinates = Coordinates(0, 0) self._spotlightManager = SpotlightManager(self) - @property - def filterType(self) -> Filter: - return self._filterType - - @filterType.setter + @Magnifier.filterType.setter def filterType(self, value: Filter) -> None: self._filterType = value if self._isActive: diff --git a/source/_magnifier/lensMagnifier.py b/source/_magnifier/lensMagnifier.py new file mode 100644 index 00000000000..608164c0536 --- /dev/null +++ b/source/_magnifier/lensMagnifier.py @@ -0,0 +1,26 @@ +# 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 + +""" +Lens magnifier module. +""" + +from .magnifier import Magnifier +from .utils.types import MagnifierType + + +class LensMagnifier(Magnifier): + def __init__(self): + super().__init__() + self._magnifierType = MagnifierType.LENS + + def _startMagnifier(self) -> None: + super()._startMagnifier() + + def _stopMagnifier(self) -> None: + super()._stopMagnifier() + + def _doUpdate(self): + pass diff --git a/source/_magnifier/magnifier.py b/source/_magnifier/magnifier.py index 84ca00f12bb..a30cd988d07 100644 --- a/source/_magnifier/magnifier.py +++ b/source/_magnifier/magnifier.py @@ -59,6 +59,14 @@ def __init__(self): _displayTracking.displayChanged.register(self._onDisplayChanged) self._screenCurtainIsActive: bool = False + @property + def filterType(self) -> Filter: + return self._filterType + + @filterType.setter + def filterType(self, value: Filter) -> None: + self._filterType = value + @property def zoomLevel(self) -> float: return self._zoomLevel diff --git a/source/_magnifier/utils/types.py b/source/_magnifier/utils/types.py index 3da8f4f3c84..5eb9dfdf8b2 100644 --- a/source/_magnifier/utils/types.py +++ b/source/_magnifier/utils/types.py @@ -111,7 +111,6 @@ class MagnifierType(DisplayStringStrEnum): FIXED = "fixed" DOCKED = "docked" LENS = "lens" - PLACEHOLDER = "placeholder" @property def _displayStringLabels(self) -> dict["MagnifierType", str]: @@ -124,8 +123,6 @@ def _displayStringLabels(self) -> dict["MagnifierType", str]: self.DOCKED: pgettext("magnifier", "Docked"), # Translators: Magnifier type - lens mode. self.LENS: pgettext("magnifier", "Lens"), - # Translators: Magnifier type - placeholder for unsupported types. - self.PLACEHOLDER: pgettext("magnifier", "Placeholder"), } From 600e5c084b7ac63e4a60cadee885c84336842cd5 Mon Sep 17 00:00:00 2001 From: Antoine HAFFREINGUE Date: Tue, 28 Apr 2026 08:59:48 +0200 Subject: [PATCH 03/11] update userguide --- user_docs/en/userGuide.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/user_docs/en/userGuide.md b/user_docs/en/userGuide.md index 60e8f3016fb..73195059eab 100644 --- a/user_docs/en/userGuide.md +++ b/user_docs/en/userGuide.md @@ -1620,7 +1620,6 @@ The default color filter when the magnifier is first enabled can be configured i ### Magnifier Types {#MagnifierType} -The magnifier offers three different modes for tracking focus and determining which part of the screen to magnify: The magnifier can be used in multiple modes, each designed to suit different user needs and preferences: * Full-screen: The entire screen is magnified, and the magnified view follows the system focus or mouse pointer. @@ -1667,15 +1666,17 @@ If you move the mouse before the zoom-back occurs, the timer resets, giving you ### Fixed Magnifier {#MagnifierFixed} -Placeholder +Fixed Magnifier provides a separate window that displays a magnified view of the area around the system focus or mouse pointer. This allows you to see both the magnified content and the surrounding context simultaneously. +The fixed magnifier window can be resized and moved in corners, giving you flexibility in how you view magnified content. ### Docked Magnifier {#MagnifierDocked} -Placeholder +Docked Magnifier is a mode where the magnified view is docked to one edge of the screen, providing a larger view of the area around the system focus or mouse pointer while still showing most of the screen at normal size. +You can choose to dock the magnifier to the top, bottom, left, or right edge of the screen, depending on your preference and the layout of your applications. ### Lens Magnifier {#MagnifierLens} -Placeholder +Lens Magnifier is a mode where a rectangular area around the system focus or mouse pointer is magnified, while the rest of the screen remains at normal size. This allows you to focus on a specific area without losing sight of the overall screen layout. The lens follows the system focus or mouse pointer, providing a dynamic magnification experience. ### Magnifier Settings {#MagnifierSettings} From 8024c3feb137a7547b93770bcf61be45daa93034 Mon Sep 17 00:00:00 2001 From: Antoine HAFFREINGUE Date: Tue, 28 Apr 2026 10:24:53 +0200 Subject: [PATCH 04/11] tests --- source/_magnifier/magnifier.py | 2 +- source/globalCommands.py | 2 +- .../test_magnifier/test_magnifierCommands.py | 41 ++++++++++++++++++- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/source/_magnifier/magnifier.py b/source/_magnifier/magnifier.py index a6dad70365e..cb14c1730af 100644 --- a/source/_magnifier/magnifier.py +++ b/source/_magnifier/magnifier.py @@ -43,7 +43,7 @@ class Magnifier: def __init__(self): self._displayOrientation = getPrimaryDisplayOrientation() - self._magnifierType: MagnifierType + self._magnifierType: MagnifierType | None = None self._isActive: bool = False self._zoomLevel: float = getZoomLevel() self._panStep: int = getPanStep() diff --git a/source/globalCommands.py b/source/globalCommands.py index 1d9b39b022f..0fe941a394a 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -5208,7 +5208,7 @@ def script_toggleFilter( @script( description=_( # Translators: Describes a command. - "Cycle through Magnifier type", + "Cycle through magnifier types", ), category=SCRCAT_VISION, gesture="kb:nvda+shift+t", diff --git a/tests/unit/test_magnifier/test_magnifierCommands.py b/tests/unit/test_magnifier/test_magnifierCommands.py index 9c597404ba3..e25feafa633 100644 --- a/tests/unit/test_magnifier/test_magnifierCommands.py +++ b/tests/unit/test_magnifier/test_magnifierCommands.py @@ -5,8 +5,8 @@ import unittest from unittest.mock import MagicMock, patch -from _magnifier.commands import zoom -from _magnifier.utils.types import Direction +from _magnifier.commands import zoom, cycleMagnifierType +from _magnifier.utils.types import Direction, MagnifierType class TestZoomCommand(unittest.TestCase): @@ -53,3 +53,40 @@ def testActiveZoomOut(self): zoom(Direction.OUT) self.mockToggle.assert_not_called() mag._zoom.assert_called_once_with(Direction.OUT) + + +class TestCycleMagnifierType(unittest.TestCase): + """Tests for cycleMagnifierType command.""" + + def setUp(self): + self.mockMessage = patch("_magnifier.commands.ui.message").start() + self.mockGetMagnifier = patch("_magnifier.commands.getMagnifier").start() + self.mockChangeMagnifierType = patch("_magnifier.commands.changeMagnifierType").start() + self.mockSetMagnifierType = patch("_magnifier.commands.setMagnifierType").start() + + def tearDown(self): + patch.stopall() + + def _makeMockMagnifier(self, magnifierType: MagnifierType): + magnifier = MagicMock() + magnifier._isActive = True + magnifier._magnifierType = magnifierType + return magnifier + + def testFullCycle(self): + """All four types cycle in order and wrap back to FULLSCREEN.""" + expectedCycle = [ + (MagnifierType.FULLSCREEN, MagnifierType.FIXED), + (MagnifierType.FIXED, MagnifierType.DOCKED), + (MagnifierType.DOCKED, MagnifierType.LENS), + (MagnifierType.LENS, MagnifierType.FULLSCREEN), + ] + for currentType, expectedNext in expectedCycle: + with self.subTest(currentType=currentType): + self.mockGetMagnifier.side_effect = [ + self._makeMockMagnifier(currentType), + self._makeMockMagnifier(expectedNext), + ] + cycleMagnifierType() + self.mockChangeMagnifierType.assert_called_once_with(expectedNext) + self.mockChangeMagnifierType.reset_mock() From c90478f4e300f142ee1d5175a2663765157b42b8 Mon Sep 17 00:00:00 2001 From: Antoine HAFFREINGUE Date: Wed, 29 Apr 2026 10:41:15 +0200 Subject: [PATCH 05/11] changed magnifier type to magnified view --- source/_magnifier/__init__.py | 36 ++++++++-------- source/_magnifier/commands.py | 42 +++++++++---------- source/_magnifier/config.py | 12 +++--- source/_magnifier/fullscreenMagnifier.py | 4 +- source/_magnifier/magnifier.py | 4 +- source/_magnifier/utils/types.py | 4 +- source/config/configSpec.py | 2 +- .../test_fullscreenMagnifier.py | 14 +++---- .../test_magnifier/test_magnifierCommands.py | 30 ++++++------- user_docs/en/userGuide.md | 24 ++++++----- 10 files changed, 88 insertions(+), 84 deletions(-) diff --git a/source/_magnifier/__init__.py b/source/_magnifier/__init__.py index b8dee2d9feb..b9112cb818d 100644 --- a/source/_magnifier/__init__.py +++ b/source/_magnifier/__init__.py @@ -9,8 +9,8 @@ """ from typing import TYPE_CHECKING -from .config import getMagnifierType -from .utils.types import MagnifierType +from .config import getMagnifiedView +from .utils.types import MagnifiedView if TYPE_CHECKING: from .magnifier import Magnifier @@ -18,45 +18,45 @@ _magnifier: "Magnifier | None" = None -def createMagnifier(magnifierType: MagnifierType) -> "Magnifier": +def createMagnifier(magnifiedView: MagnifiedView) -> "Magnifier": """ Create a magnifier instance based on the specified type. - :param magnifierType: The type of magnifier to create + :param magnifiedView: The type of magnifier to create :return: The created magnifier instance :raises ValueError: If the magnifier type is not supported """ - match magnifierType: - case MagnifierType.FULLSCREEN: + match magnifiedView: + case MagnifiedView.FULLSCREEN: from .fullscreenMagnifier import FullScreenMagnifier return FullScreenMagnifier() - case MagnifierType.FIXED: + case MagnifiedView.FIXED: from .fixedMagnifier import FixedMagnifier return FixedMagnifier() - case MagnifierType.DOCKED: + case MagnifiedView.DOCKED: from .dockedMagnifier import DockedMagnifier return DockedMagnifier() - case MagnifierType.LENS: + case MagnifiedView.LENS: from .lensMagnifier import LensMagnifier return LensMagnifier() case _: - raise ValueError(f"Unsupported magnifier type: {magnifierType}") + raise ValueError(f"Unsupported magnifier type: {MagnifiedView}") -def _setMagnifierType(magnifierType: MagnifierType) -> None: +def _setMagnifiedView(magnifiedView: MagnifiedView) -> None: """ Set the magnifier type, stopping the current one if active and creating a new instance. - :param magnifierType: The type of magnifier to set + :param magnifiedView: The type of magnifier to set """ global _magnifier @@ -65,15 +65,15 @@ def _setMagnifierType(magnifierType: MagnifierType) -> None: _magnifier._stopMagnifier() # Create and set new magnifier instance - _magnifier = createMagnifier(magnifierType) + _magnifier = createMagnifier(magnifiedView) def initialize() -> None: """ Initialize the magnifier module with the default magnifier type from config. """ - magnifierType = getMagnifierType() - _setMagnifierType(magnifierType) + magnifiedView = getMagnifiedView() + _setMagnifiedView(magnifiedView) _magnifier._startMagnifier() @@ -87,19 +87,19 @@ def isActive() -> bool: return _magnifier is not None and _magnifier._isActive -def changeMagnifierType(magnifierType: MagnifierType) -> None: +def changeMagnifiedView(magnifiedView: MagnifiedView) -> None: """ Change the magnifier type at runtime. Stops the current magnifier and starts a new one of the specified type. - :param magnifierType: The new magnifier type to use + :param magnifiedView: The new magnifier type to use :raises RuntimeError: If no magnifier is currently active """ global _magnifier if not _magnifier or not _magnifier._isActive: raise RuntimeError("Cannot change magnifier type: magnifier is not active") - _setMagnifierType(magnifierType) + _setMagnifiedView(magnifiedView) _magnifier._startMagnifier() diff --git a/source/_magnifier/commands.py b/source/_magnifier/commands.py index 77046b1b887..387525c7eba 100644 --- a/source/_magnifier/commands.py +++ b/source/_magnifier/commands.py @@ -10,10 +10,10 @@ from typing import Literal import ui -from . import getMagnifier, initialize, terminate, changeMagnifierType +from . import getMagnifier, initialize, terminate, changeMagnifiedView from .config import ( - getMagnifierType, - setMagnifierType, + getMagnifiedView, + setMagnifiedView, getZoomLevelString, getFilter, getFullscreenMode, @@ -27,7 +27,7 @@ from .utils.types import ( Filter, Direction, - MagnifierType, + MagnifiedView, FullScreenMode, MagnifierAction, MagnifierFollowFocusType, @@ -106,16 +106,16 @@ def toggleMagnifier() -> None: else: initialize() filter = getFilter() - magnifierType = getMagnifierType() + magnifiedView = getMagnifiedView() zoomLevel = getZoomLevelString() - if magnifierType == MagnifierType.FULLSCREEN: + if magnifiedView == MagnifiedView.FULLSCREEN: fullscreenMode = getFullscreenMode() msg = pgettext( "magnifier", # Translators: Message announced when starting the NVDA magnifier. - "Starting {magnifierType} magnifier with {zoomLevel} zoom level, {filter} filter, and {fullscreenMode} full-screen mode", + "Starting {MagnifiedView} magnifier with {zoomLevel} zoom level, {filter} filter, and {fullscreenMode} full-screen mode", ).format( - magnifierType=magnifierType.displayString, + MagnifiedView=magnifiedView.displayString, zoomLevel=zoomLevel, filter=filter.displayString, fullscreenMode=fullscreenMode.displayString, @@ -124,9 +124,9 @@ def toggleMagnifier() -> None: msg = pgettext( "magnifier", # Translators: Message announced when starting the NVDA magnifier. - "Starting {magnifierType} magnifier with {zoomLevel} zoom level and {filter} filter", + "Starting {MagnifiedView} magnifier with {zoomLevel} zoom level and {filter} filter", ).format( - magnifierType=magnifierType.displayString, + MagnifiedView=magnifiedView.displayString, zoomLevel=zoomLevel, filter=filter.displayString, ) @@ -187,7 +187,7 @@ def toggleFilter() -> None: filters = list(Filter) idx = filters.index(magnifier.filterType) magnifier.filterType = filters[(idx + 1) % len(filters)] - if magnifier._magnifierType == MagnifierType.FULLSCREEN: + if magnifier._magnifiedView == MagnifiedView.FULLSCREEN: magnifier._applyFilter() ui.message( pgettext( @@ -199,14 +199,14 @@ def toggleFilter() -> None: _CYCLING_MAGNIFIER_TYPES = [ - MagnifierType.FULLSCREEN, - MagnifierType.FIXED, - MagnifierType.DOCKED, - MagnifierType.LENS, + MagnifiedView.FULLSCREEN, + MagnifiedView.FIXED, + MagnifiedView.DOCKED, + MagnifiedView.LENS, ] -def cycleMagnifierType() -> None: +def cycleMagnifiedView() -> None: """Cycle through magnifier types (full-screen, fixed, docked, lens)""" magnifier: Magnifier = getMagnifier() if magnifierIsActiveVerify( @@ -214,19 +214,19 @@ def cycleMagnifierType() -> None: MagnifierAction.CHANGE_MAGNIFIER_TYPE, ): types = _CYCLING_MAGNIFIER_TYPES - currentType = magnifier._magnifierType + currentType = magnifier._magnifiedView idx = types.index(currentType) newType = types[(idx + 1) % len(types)] log.debug(f"Changing magnifier type from {currentType} to {newType}") - changeMagnifierType(newType) - setMagnifierType(newType) + changeMagnifiedView(newType) + setMagnifiedView(newType) magnifier = getMagnifier() ui.message( pgettext( "magnifier", # Translators: Message announced when changing the magnifier type with {type} being the new magnifier type. "Magnifier type changed to {type}", - ).format(type=magnifier._magnifierType.displayString), + ).format(type=magnifier._magnifiedView.displayString), ) @@ -385,7 +385,7 @@ def magnifierIsFullscreenVerify( :return: True if the magnifier is full-screen, False otherwise """ - if magnifier._magnifierType == MagnifierType.FULLSCREEN: + if magnifier._magnifiedView == MagnifiedView.FULLSCREEN: return True else: ui.message( diff --git a/source/_magnifier/config.py b/source/_magnifier/config.py index 3913b3ae571..22998cd457d 100644 --- a/source/_magnifier/config.py +++ b/source/_magnifier/config.py @@ -10,7 +10,7 @@ import config from dataclasses import dataclass, field -from .utils.types import Filter, FullScreenMode, MagnifierFollowFocusType, MagnifierType +from .utils.types import Filter, FullScreenMode, MagnifierFollowFocusType, MagnifiedView class ZoomLevel: @@ -121,22 +121,22 @@ def setFilter(filter: Filter) -> None: config.conf["magnifier"]["filter"] = filter.value -def getMagnifierType() -> MagnifierType: +def getMagnifiedView() -> MagnifiedView: """ Get magnifier type from config. :return: The magnifier type. """ - return MagnifierType(config.conf["magnifier"]["magnifierType"]) + return MagnifiedView(config.conf["magnifier"]["magnifiedView"]) -def setMagnifierType(magnifierType: MagnifierType) -> None: +def setMagnifiedView(magnifiedView: MagnifiedView) -> None: """ Set magnifier type from settings. - :param magnifierType: The magnifier type to set. + :param magnifiedView: The magnifier type to set. """ - config.conf["magnifier"]["magnifierType"] = magnifierType.value + config.conf["magnifier"]["magnifiedView"] = magnifiedView.value _FOLLOW_CONFIG_KEYS: dict[MagnifierFollowFocusType, str] = { diff --git a/source/_magnifier/fullscreenMagnifier.py b/source/_magnifier/fullscreenMagnifier.py index 243c81affcd..32e986ebc5e 100644 --- a/source/_magnifier/fullscreenMagnifier.py +++ b/source/_magnifier/fullscreenMagnifier.py @@ -17,7 +17,7 @@ from .utils.spotlightManager import SpotlightManager from .utils.types import ( Filter, - MagnifierType, + MagnifiedView, FullScreenMode, Size, MagnifierParameters, @@ -30,7 +30,7 @@ class FullScreenMagnifier(Magnifier): def __init__(self): super().__init__() - self._magnifierType = MagnifierType.FULLSCREEN + self._magnifiedView = MagnifiedView.FULLSCREEN self._fullscreenMode = getFullscreenMode() self._currentCoordinates = Coordinates(0, 0) self._spotlightManager = SpotlightManager(self) diff --git a/source/_magnifier/magnifier.py b/source/_magnifier/magnifier.py index cb14c1730af..f52ad7feca3 100644 --- a/source/_magnifier/magnifier.py +++ b/source/_magnifier/magnifier.py @@ -21,7 +21,7 @@ from .utils.types import ( MagnifierParameters, MagnifierAction, - MagnifierType, + MagnifiedView, Direction, Filter, Coordinates, @@ -43,7 +43,7 @@ class Magnifier: def __init__(self): self._displayOrientation = getPrimaryDisplayOrientation() - self._magnifierType: MagnifierType | None = None + self._magnifiedView: MagnifiedView self._isActive: bool = False self._zoomLevel: float = getZoomLevel() self._panStep: int = getPanStep() diff --git a/source/_magnifier/utils/types.py b/source/_magnifier/utils/types.py index dfb5e2643e0..dd9d35bd923 100644 --- a/source/_magnifier/utils/types.py +++ b/source/_magnifier/utils/types.py @@ -111,7 +111,7 @@ def _displayStringLabels(self) -> dict["MagnifierFollowFocusType", str]: } -class MagnifierType(DisplayStringStrEnum): +class MagnifiedView(DisplayStringStrEnum): """Type of magnifier""" FULLSCREEN = "fullscreen" @@ -120,7 +120,7 @@ class MagnifierType(DisplayStringStrEnum): LENS = "lens" @property - def _displayStringLabels(self) -> dict["MagnifierType", str]: + def _displayStringLabels(self) -> dict["MagnifiedView", str]: return { # Translators: Magnifier type - full-screen mode. self.FULLSCREEN: pgettext("magnifier", "Fullscreen"), diff --git a/source/config/configSpec.py b/source/config/configSpec.py index b01cedb3f0f..dd29b10dd8d 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -116,7 +116,7 @@ # Magnifier settings [magnifier] - magnifierType = string(default="fullscreen") + magnifiedView = string(default="fullscreen") zoomLevel = float(min=1.0, max=10.0, default=2.0) isTrueCentered = boolean(default=False) filter = string(default="normal") diff --git a/tests/unit/test_magnifier/test_fullscreenMagnifier.py b/tests/unit/test_magnifier/test_fullscreenMagnifier.py index d7e7e0f67fe..cb9cfdcb41b 100644 --- a/tests/unit/test_magnifier/test_fullscreenMagnifier.py +++ b/tests/unit/test_magnifier/test_fullscreenMagnifier.py @@ -4,7 +4,7 @@ # For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt from unittest.mock import MagicMock, patch -from _magnifier.utils.types import Filter, FullScreenMode, MagnifierType, Direction, Coordinates +from _magnifier.utils.types import Filter, FullScreenMode, MagnifiedView, Direction, Coordinates from _magnifier.fullscreenMagnifier import FullScreenMagnifier from tests.unit.test_magnifier.test_magnifier import _TestMagnifier from _magnifier.magnifier import Magnifier @@ -22,7 +22,7 @@ def testMagnifierCreation(self): self.assertEqual(magnifier.zoomLevel, 2.0) self.assertEqual(magnifier.filterType, Filter.NORMAL) self.assertEqual(magnifier._fullscreenMode, FullScreenMode.CENTER) - self.assertEqual(magnifier._magnifierType, MagnifierType.FULLSCREEN) + self.assertEqual(magnifier._magnifiedView, MagnifiedView.FULLSCREEN) self.assertTrue(magnifier._isActive) magnifier._stopMagnifier() @@ -146,16 +146,16 @@ def testMagnifierZoomBoundaries(self): # Cleanup magnifier._stopMagnifier() - def testMagnifierTypeProperty(self): - """Test magnifierType property for FullScreenMagnifier.""" + def testMagnifiedViewProperty(self): + """Test magnifiedView property for FullScreenMagnifier.""" magnifier = FullScreenMagnifier() magnifier._startMagnifier() # Should default to FULLSCREEN - self.assertEqual(magnifier._magnifierType, MagnifierType.FULLSCREEN) + self.assertEqual(magnifier._magnifiedView, MagnifiedView.FULLSCREEN) # Test that we can read it (inherited property from Magnifier) - self.assertIsNotNone(magnifier._magnifierType) + self.assertIsNotNone(magnifier._magnifiedView) # Cleanup magnifier._stopMagnifier() @@ -170,7 +170,7 @@ def testMagnifierInheritance(self): # Test basic properties exist self.assertTrue(hasattr(magnifier, "zoomLevel")) self.assertTrue(hasattr(magnifier, "filterType")) - self.assertTrue(hasattr(magnifier, "_magnifierType")) + self.assertTrue(hasattr(magnifier, "_magnifiedView")) self.assertTrue(hasattr(magnifier, "_fullscreenMode")) self.assertTrue(hasattr(magnifier, "_isActive")) self.assertTrue(hasattr(magnifier, "_currentCoordinates")) diff --git a/tests/unit/test_magnifier/test_magnifierCommands.py b/tests/unit/test_magnifier/test_magnifierCommands.py index e25feafa633..8e442d339ed 100644 --- a/tests/unit/test_magnifier/test_magnifierCommands.py +++ b/tests/unit/test_magnifier/test_magnifierCommands.py @@ -5,8 +5,8 @@ import unittest from unittest.mock import MagicMock, patch -from _magnifier.commands import zoom, cycleMagnifierType -from _magnifier.utils.types import Direction, MagnifierType +from _magnifier.commands import zoom, cycleMagnifiedView +from _magnifier.utils.types import Direction, MagnifiedView class TestZoomCommand(unittest.TestCase): @@ -55,31 +55,31 @@ def testActiveZoomOut(self): mag._zoom.assert_called_once_with(Direction.OUT) -class TestCycleMagnifierType(unittest.TestCase): - """Tests for cycleMagnifierType command.""" +class TestCycleMagnifiedView(unittest.TestCase): + """Tests for cycleMagnifiedView command.""" def setUp(self): self.mockMessage = patch("_magnifier.commands.ui.message").start() self.mockGetMagnifier = patch("_magnifier.commands.getMagnifier").start() - self.mockChangeMagnifierType = patch("_magnifier.commands.changeMagnifierType").start() - self.mockSetMagnifierType = patch("_magnifier.commands.setMagnifierType").start() + self.mockChangeMagnifiedView = patch("_magnifier.commands.changeMagnifiedView").start() + self.mockSetMagnifiedView = patch("_magnifier.commands.setMagnifiedView").start() def tearDown(self): patch.stopall() - def _makeMockMagnifier(self, magnifierType: MagnifierType): + def _makeMockMagnifier(self, magnifiedView: MagnifiedView): magnifier = MagicMock() magnifier._isActive = True - magnifier._magnifierType = magnifierType + magnifier._magnifiedView = magnifiedView return magnifier def testFullCycle(self): """All four types cycle in order and wrap back to FULLSCREEN.""" expectedCycle = [ - (MagnifierType.FULLSCREEN, MagnifierType.FIXED), - (MagnifierType.FIXED, MagnifierType.DOCKED), - (MagnifierType.DOCKED, MagnifierType.LENS), - (MagnifierType.LENS, MagnifierType.FULLSCREEN), + (MagnifiedView.FULLSCREEN, MagnifiedView.FIXED), + (MagnifiedView.FIXED, MagnifiedView.DOCKED), + (MagnifiedView.DOCKED, MagnifiedView.LENS), + (MagnifiedView.LENS, MagnifiedView.FULLSCREEN), ] for currentType, expectedNext in expectedCycle: with self.subTest(currentType=currentType): @@ -87,6 +87,6 @@ def testFullCycle(self): self._makeMockMagnifier(currentType), self._makeMockMagnifier(expectedNext), ] - cycleMagnifierType() - self.mockChangeMagnifierType.assert_called_once_with(expectedNext) - self.mockChangeMagnifierType.reset_mock() + cycleMagnifiedView() + self.mockChangeMagnifiedView.assert_called_once_with(expectedNext) + self.mockChangeMagnifiedView.reset_mock() diff --git a/user_docs/en/userGuide.md b/user_docs/en/userGuide.md index 73195059eab..2eb9fe0c812 100644 --- a/user_docs/en/userGuide.md +++ b/user_docs/en/userGuide.md @@ -1618,14 +1618,16 @@ NVDA will announce the name of the currently selected filter. The default color filter when the magnifier is first enabled can be configured in the [Magnifier settings](#MagnifierSettings). -### Magnifier Types {#MagnifierType} +### Magnified Views {#MagnifiedViews} -The magnifier can be used in multiple modes, each designed to suit different user needs and preferences: +The magnifier can be used in multiple types of view, each designed to suit different user needs and preferences: * Full-screen: The entire screen is magnified, and the magnified view follows the system focus or mouse pointer. -* Fixed window: A separate window displays the magnified content, and the rest of the screen remains at normal size. This allows you to see both the magnified content and the surrounding context simultaneously. +* Fixed window: A separate window displays the magnified content, and the rest of the screen remains at normal size. +This allows you to see both the magnified content and the surrounding context simultaneously. * Docked: The magnified view is docked to one edge of the screen, providing a larger view of the area around the system focus or mouse pointer while still showing most of the screen at normal size. -* Lens: A rectangular area around the system focus or mouse pointer is magnified, while the rest of the screen remains at normal size. This allows you to focus on a specific area without losing sight of the overall screen layout. +* Lens: A rectangular area around the system focus or mouse pointer is magnified, while the rest of the screen remains at normal size. +This allows you to focus on a specific area without losing sight of the overall screen layout. ### Fullscreen Focus Modes {#MagnifierFullscreenFocusModes} @@ -1644,7 +1646,7 @@ NVDA will announce the name of the currently selected mode. The default focus mode when the magnifier is first enabled can be configured in the [Magnifier settings](#MagnifierSettings). -#### Spotlight Mode {#MagnifierSpotlight} +### Spotlight {#MagnifierSpotlight} Spotlight mode is a special fullscreen feature designed for presentations or focused reading tasks. When activated, it temporarily zooms out the magnified view to show the full screen, then zooms back in to the current focus position after a brief period of mouse inactivity. @@ -1666,17 +1668,19 @@ If you move the mouse before the zoom-back occurs, the timer resets, giving you ### Fixed Magnifier {#MagnifierFixed} -Fixed Magnifier provides a separate window that displays a magnified view of the area around the system focus or mouse pointer. This allows you to see both the magnified content and the surrounding context simultaneously. -The fixed magnifier window can be resized and moved in corners, giving you flexibility in how you view magnified content. +Fixed Magnifier provides a separate window that displays a magnified view of the area around the system focus or mouse pointer. +It is a floating magnifier window that can be moved and resized, and it is typically positioned in a screen corner. +This lets you see the magnified content and the surrounding context at the same time, while keeping the rest of the screen visible. ### Docked Magnifier {#MagnifierDocked} -Docked Magnifier is a mode where the magnified view is docked to one edge of the screen, providing a larger view of the area around the system focus or mouse pointer while still showing most of the screen at normal size. -You can choose to dock the magnifier to the top, bottom, left, or right edge of the screen, depending on your preference and the layout of your applications. +Docked Magnifier is a mode where the magnified view is anchored to one edge of the screen, providing a larger view of the area around the system focus or mouse pointer while still showing most of the screen at normal size. +You can choose to dock the magnifier to the top, bottom, left, or right edge of the screen, so the magnified area forms a strip along that edge rather than a movable corner window. ### Lens Magnifier {#MagnifierLens} -Lens Magnifier is a mode where a rectangular area around the system focus or mouse pointer is magnified, while the rest of the screen remains at normal size. This allows you to focus on a specific area without losing sight of the overall screen layout. The lens follows the system focus or mouse pointer, providing a dynamic magnification experience. +Lens Magnifier is a mode where a rectangular area around the system focus or mouse pointer is magnified, while the rest of the screen remains at normal size. +This allows you to focus on a specific area without losing sight of the overall screen layout. The lens follows the system focus or mouse pointer, providing a dynamic magnification experience. ### Magnifier Settings {#MagnifierSettings} From e273c6a90a27adfe782d76fc71388f96a7ee14be Mon Sep 17 00:00:00 2001 From: Antoine HAFFREINGUE Date: Wed, 29 Apr 2026 11:35:28 +0200 Subject: [PATCH 06/11] stronger typing for fullscreen --- source/_magnifier/commands.py | 13 ++++++++----- source/_magnifier/dockedMagnifier.py | 4 ++-- source/_magnifier/fixedMagnifier.py | 4 ++-- source/_magnifier/lensMagnifier.py | 4 ++-- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/source/_magnifier/commands.py b/source/_magnifier/commands.py index 387525c7eba..9b7fc38d0e7 100644 --- a/source/_magnifier/commands.py +++ b/source/_magnifier/commands.py @@ -188,7 +188,8 @@ def toggleFilter() -> None: idx = filters.index(magnifier.filterType) magnifier.filterType = filters[(idx + 1) % len(filters)] if magnifier._magnifiedView == MagnifiedView.FULLSCREEN: - magnifier._applyFilter() + fullscreenMagnifier: FullScreenMagnifier = magnifier + fullscreenMagnifier._applyFilter() ui.message( pgettext( "magnifier", @@ -300,12 +301,13 @@ def toggleFullscreenMode() -> None: magnifier, MagnifierAction.CHANGE_FULLSCREEN_MODE, ): + fullscreenMagnifier: FullScreenMagnifier = magnifier modes = list(FullScreenMode) - currentMode = magnifier._fullscreenMode + currentMode = fullscreenMagnifier._fullscreenMode idx = modes.index(currentMode) newMode = modes[(idx + 1) % len(modes)] log.debug(f"Changing full-screen mode from {currentMode} to {newMode}") - magnifier._fullscreenMode = newMode + fullscreenMagnifier._fullscreenMode = newMode ui.message( pgettext( "magnifier", @@ -326,8 +328,9 @@ def startSpotlight() -> None: magnifier, MagnifierAction.START_SPOTLIGHT, ): + fullscreenMagnifier: FullScreenMagnifier = magnifier log.debug("trying to launch spotlight mode") - if magnifier._spotlightManager._spotlightIsActive: + if fullscreenMagnifier._spotlightManager._spotlightIsActive: log.debug("found spotlight manager and it is active") ui.message( pgettext( @@ -338,7 +341,7 @@ def startSpotlight() -> None: ) else: log.debug("no active spotlight manager found, starting new one") - magnifier._startSpotlight() + fullscreenMagnifier._startSpotlight() ui.message( pgettext( "magnifier", diff --git a/source/_magnifier/dockedMagnifier.py b/source/_magnifier/dockedMagnifier.py index 882c7aa8223..aee79739e51 100644 --- a/source/_magnifier/dockedMagnifier.py +++ b/source/_magnifier/dockedMagnifier.py @@ -8,13 +8,13 @@ """ from .magnifier import Magnifier -from .utils.types import MagnifierType +from .utils.types import MagnifiedView class DockedMagnifier(Magnifier): def __init__(self): super().__init__() - self._magnifierType = MagnifierType.DOCKED + self._magnifiedView = MagnifiedView.DOCKED def _startMagnifier(self) -> None: super()._startMagnifier() diff --git a/source/_magnifier/fixedMagnifier.py b/source/_magnifier/fixedMagnifier.py index dc6e66a63fa..7e1c96afda1 100644 --- a/source/_magnifier/fixedMagnifier.py +++ b/source/_magnifier/fixedMagnifier.py @@ -8,13 +8,13 @@ """ from .magnifier import Magnifier -from .utils.types import MagnifierType +from .utils.types import MagnifiedView class FixedMagnifier(Magnifier): def __init__(self): super().__init__() - self._magnifierType = MagnifierType.FIXED + self._magnifiedView = MagnifiedView.FIXED def _startMagnifier(self) -> None: super()._startMagnifier() diff --git a/source/_magnifier/lensMagnifier.py b/source/_magnifier/lensMagnifier.py index 608164c0536..68f35ad27fd 100644 --- a/source/_magnifier/lensMagnifier.py +++ b/source/_magnifier/lensMagnifier.py @@ -8,13 +8,13 @@ """ from .magnifier import Magnifier -from .utils.types import MagnifierType +from .utils.types import MagnifiedView class LensMagnifier(Magnifier): def __init__(self): super().__init__() - self._magnifierType = MagnifierType.LENS + self._magnifiedView = MagnifiedView.LENS def _startMagnifier(self) -> None: super()._startMagnifier() From 23a2b5b4a664f2b0682a70511dc9959518d9ed4e Mon Sep 17 00:00:00 2001 From: Antoine HAFFREINGUE Date: Mon, 4 May 2026 11:07:08 +0200 Subject: [PATCH 07/11] restore userGuide and globalCommands --- source/globalCommands.py | 14 -------------- user_docs/en/userGuide.md | 35 ++++------------------------------- 2 files changed, 4 insertions(+), 45 deletions(-) diff --git a/source/globalCommands.py b/source/globalCommands.py index 0fe941a394a..57490c715cb 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -5205,20 +5205,6 @@ def script_toggleFilter( ) -> None: _magnifier.commands.toggleFilter() - @script( - description=_( - # Translators: Describes a command. - "Cycle through magnifier types", - ), - category=SCRCAT_VISION, - gesture="kb:nvda+shift+t", - ) - def script_cycleMagnifierType( - self, - gesture: inputCore.InputGesture, - ) -> None: - _magnifier.commands.cycleMagnifierType() - @script( description=_( # Translators: Describes a command. diff --git a/user_docs/en/userGuide.md b/user_docs/en/userGuide.md index 2eb9fe0c812..26151130228 100644 --- a/user_docs/en/userGuide.md +++ b/user_docs/en/userGuide.md @@ -1618,20 +1618,9 @@ NVDA will announce the name of the currently selected filter. The default color filter when the magnifier is first enabled can be configured in the [Magnifier settings](#MagnifierSettings). -### Magnified Views {#MagnifiedViews} +### Focus Tracking Modes {#MagnifierFullscreenFocusModes} -The magnifier can be used in multiple types of view, each designed to suit different user needs and preferences: - -* Full-screen: The entire screen is magnified, and the magnified view follows the system focus or mouse pointer. -* Fixed window: A separate window displays the magnified content, and the rest of the screen remains at normal size. -This allows you to see both the magnified content and the surrounding context simultaneously. -* Docked: The magnified view is docked to one edge of the screen, providing a larger view of the area around the system focus or mouse pointer while still showing most of the screen at normal size. -* Lens: A rectangular area around the system focus or mouse pointer is magnified, while the rest of the screen remains at normal size. -This allows you to focus on a specific area without losing sight of the overall screen layout. - -### Fullscreen Focus Modes {#MagnifierFullscreenFocusModes} - -The fullscreen magnifier offers three different modes for tracking focus and determining which part of the screen to magnify: +The magnifier offers three different modes for tracking focus and determining which part of the screen to magnify: * Center: The magnified area is centered on the current focus position. This mode keeps the focused element at the center of the screen and clamps to the screen edge. @@ -1646,9 +1635,9 @@ NVDA will announce the name of the currently selected mode. The default focus mode when the magnifier is first enabled can be configured in the [Magnifier settings](#MagnifierSettings). -### Spotlight {#MagnifierSpotlight} +### Spotlight Mode {#MagnifierSpotlight} -Spotlight mode is a special fullscreen feature designed for presentations or focused reading tasks. +Spotlight mode is a special feature designed for presentations or focused reading tasks. When activated, it temporarily zooms out the magnified view to show the full screen, then zooms back in to the current focus position after a brief period of mouse inactivity. This is useful when you want to: @@ -1666,22 +1655,6 @@ Once activated, the magnifier will: Spotlight mode automatically deactivates after zooming back in. If you move the mouse before the zoom-back occurs, the timer resets, giving you more time to view the full screen. -### Fixed Magnifier {#MagnifierFixed} - -Fixed Magnifier provides a separate window that displays a magnified view of the area around the system focus or mouse pointer. -It is a floating magnifier window that can be moved and resized, and it is typically positioned in a screen corner. -This lets you see the magnified content and the surrounding context at the same time, while keeping the rest of the screen visible. - -### Docked Magnifier {#MagnifierDocked} - -Docked Magnifier is a mode where the magnified view is anchored to one edge of the screen, providing a larger view of the area around the system focus or mouse pointer while still showing most of the screen at normal size. -You can choose to dock the magnifier to the top, bottom, left, or right edge of the screen, so the magnified area forms a strip along that edge rather than a movable corner window. - -### Lens Magnifier {#MagnifierLens} - -Lens Magnifier is a mode where a rectangular area around the system focus or mouse pointer is magnified, while the rest of the screen remains at normal size. -This allows you to focus on a specific area without losing sight of the overall screen layout. The lens follows the system focus or mouse pointer, providing a dynamic magnification experience. - ### Magnifier Settings {#MagnifierSettings} The magnifier can be configured in the "Magnifier" category of the NVDA Settings dialog (`NVDA+control+w`). From 13d408db4b376b4e9940ec1e5d9d21f112ef6744 Mon Sep 17 00:00:00 2001 From: Antoine HAFFREINGUE Date: Mon, 4 May 2026 11:22:21 +0200 Subject: [PATCH 08/11] review modifications --- source/_magnifier/__init__.py | 22 ++++++------- source/_magnifier/commands.py | 40 ++++++++++-------------- source/_magnifier/config.py | 8 ++--- source/_magnifier/dockedMagnifier.py | 2 ++ source/_magnifier/fixedMagnifier.py | 2 ++ source/_magnifier/fullscreenMagnifier.py | 2 ++ source/_magnifier/lensMagnifier.py | 2 ++ source/_magnifier/utils/types.py | 14 ++++----- 8 files changed, 46 insertions(+), 46 deletions(-) diff --git a/source/_magnifier/__init__.py b/source/_magnifier/__init__.py index b9112cb818d..f742dd6f1ea 100644 --- a/source/_magnifier/__init__.py +++ b/source/_magnifier/__init__.py @@ -20,11 +20,11 @@ def createMagnifier(magnifiedView: MagnifiedView) -> "Magnifier": """ - Create a magnifier instance based on the specified type. + Create a magnifier instance based on the specified view. - :param magnifiedView: The type of magnifier to create + :param magnifiedView: The magnifier view to create :return: The created magnifier instance - :raises ValueError: If the magnifier type is not supported + :raises ValueError: If the magnifier view is not supported """ match magnifiedView: @@ -49,14 +49,14 @@ def createMagnifier(magnifiedView: MagnifiedView) -> "Magnifier": return LensMagnifier() case _: - raise ValueError(f"Unsupported magnifier type: {MagnifiedView}") + raise ValueError(f"Unsupported magnifier view: {MagnifiedView}") def _setMagnifiedView(magnifiedView: MagnifiedView) -> None: """ - Set the magnifier type, stopping the current one if active and creating a new instance. + Set the magnifier view, stopping the current one if active and creating a new instance. - :param magnifiedView: The type of magnifier to set + :param magnifiedView: The magnifier view to set """ global _magnifier @@ -70,7 +70,7 @@ def _setMagnifiedView(magnifiedView: MagnifiedView) -> None: def initialize() -> None: """ - Initialize the magnifier module with the default magnifier type from config. + Initialize the magnifier module with the default magnifier view from config. """ magnifiedView = getMagnifiedView() _setMagnifiedView(magnifiedView) @@ -89,15 +89,15 @@ def isActive() -> bool: def changeMagnifiedView(magnifiedView: MagnifiedView) -> None: """ - Change the magnifier type at runtime. - Stops the current magnifier and starts a new one of the specified type. + Change the magnifier view at runtime. + Stops the current magnifier and starts a new one of the specified view. - :param magnifiedView: The new magnifier type to use + :param magnifiedView: The new magnifier view to use :raises RuntimeError: If no magnifier is currently active """ global _magnifier if not _magnifier or not _magnifier._isActive: - raise RuntimeError("Cannot change magnifier type: magnifier is not active") + raise RuntimeError("Cannot change magnifier view: magnifier is not active") _setMagnifiedView(magnifiedView) _magnifier._startMagnifier() diff --git a/source/_magnifier/commands.py b/source/_magnifier/commands.py index 9b7fc38d0e7..6032e5b56cd 100644 --- a/source/_magnifier/commands.py +++ b/source/_magnifier/commands.py @@ -113,9 +113,9 @@ def toggleMagnifier() -> None: msg = pgettext( "magnifier", # Translators: Message announced when starting the NVDA magnifier. - "Starting {MagnifiedView} magnifier with {zoomLevel} zoom level, {filter} filter, and {fullscreenMode} full-screen mode", + "Starting {magnifiedView} magnifier with {zoomLevel} zoom level, {filter} filter, and {fullscreenMode} full-screen mode", ).format( - MagnifiedView=magnifiedView.displayString, + magnifiedView=magnifiedView.displayString, zoomLevel=zoomLevel, filter=filter.displayString, fullscreenMode=fullscreenMode.displayString, @@ -124,9 +124,9 @@ def toggleMagnifier() -> None: msg = pgettext( "magnifier", # Translators: Message announced when starting the NVDA magnifier. - "Starting {MagnifiedView} magnifier with {zoomLevel} zoom level and {filter} filter", + "Starting {magnifiedView} magnifier with {zoomLevel} zoom level and {filter} filter", ).format( - MagnifiedView=magnifiedView.displayString, + magnifiedView=magnifiedView.displayString, zoomLevel=zoomLevel, filter=filter.displayString, ) @@ -199,35 +199,27 @@ def toggleFilter() -> None: ) -_CYCLING_MAGNIFIER_TYPES = [ - MagnifiedView.FULLSCREEN, - MagnifiedView.FIXED, - MagnifiedView.DOCKED, - MagnifiedView.LENS, -] - - def cycleMagnifiedView() -> None: - """Cycle through magnifier types (full-screen, fixed, docked, lens)""" + """Cycle through magnifier views (full-screen, fixed, docked, lens)""" magnifier: Magnifier = getMagnifier() if magnifierIsActiveVerify( magnifier, - MagnifierAction.CHANGE_MAGNIFIER_TYPE, + MagnifierAction.CHANGE_MAGNIFIER_VIEW, ): - types = _CYCLING_MAGNIFIER_TYPES - currentType = magnifier._magnifiedView - idx = types.index(currentType) - newType = types[(idx + 1) % len(types)] - log.debug(f"Changing magnifier type from {currentType} to {newType}") - changeMagnifiedView(newType) - setMagnifiedView(newType) + views = list(MagnifiedView) + currentView = magnifier._magnifiedView + idx = views.index(currentView) + newView = views[(idx + 1) % len(views)] + log.debug(f"Changing magnifier view from {currentView} to {newView}") + changeMagnifiedView(newView) + setMagnifiedView(newView) magnifier = getMagnifier() ui.message( pgettext( "magnifier", - # Translators: Message announced when changing the magnifier type with {type} being the new magnifier type. - "Magnifier type changed to {type}", - ).format(type=magnifier._magnifiedView.displayString), + # Translators: Message announced when changing the magnifier view with {view} being the new magnifier view. + "Magnifier view changed to {view}", + ).format(view=magnifier._magnifiedView.displayString), ) diff --git a/source/_magnifier/config.py b/source/_magnifier/config.py index 22998cd457d..822b86c9b2e 100644 --- a/source/_magnifier/config.py +++ b/source/_magnifier/config.py @@ -123,18 +123,18 @@ def setFilter(filter: Filter) -> None: def getMagnifiedView() -> MagnifiedView: """ - Get magnifier type from config. + Get magnifier view from config. - :return: The magnifier type. + :return: The magnifier view. """ return MagnifiedView(config.conf["magnifier"]["magnifiedView"]) def setMagnifiedView(magnifiedView: MagnifiedView) -> None: """ - Set magnifier type from settings. + Set magnifier view in settings. - :param magnifiedView: The magnifier type to set. + :param magnifiedView: The magnifier view to set. """ config.conf["magnifier"]["magnifiedView"] = magnifiedView.value diff --git a/source/_magnifier/dockedMagnifier.py b/source/_magnifier/dockedMagnifier.py index aee79739e51..037a7cdc343 100644 --- a/source/_magnifier/dockedMagnifier.py +++ b/source/_magnifier/dockedMagnifier.py @@ -12,6 +12,8 @@ class DockedMagnifier(Magnifier): + """Displays a magnified panel anchored to one edge of the screen.""" + def __init__(self): super().__init__() self._magnifiedView = MagnifiedView.DOCKED diff --git a/source/_magnifier/fixedMagnifier.py b/source/_magnifier/fixedMagnifier.py index 7e1c96afda1..d40d3b3fa33 100644 --- a/source/_magnifier/fixedMagnifier.py +++ b/source/_magnifier/fixedMagnifier.py @@ -12,6 +12,8 @@ class FixedMagnifier(Magnifier): + """Displays a magnified panel anchored to one corner of the screen.""" + def __init__(self): super().__init__() self._magnifiedView = MagnifiedView.FIXED diff --git a/source/_magnifier/fullscreenMagnifier.py b/source/_magnifier/fullscreenMagnifier.py index 0f2d124c202..34091991739 100644 --- a/source/_magnifier/fullscreenMagnifier.py +++ b/source/_magnifier/fullscreenMagnifier.py @@ -29,6 +29,8 @@ class FullScreenMagnifier(Magnifier): + """Magnifier that uses the Windows Magnification API to magnify the entire screen.""" + _MAX_RECOVERY_ATTEMPTS: int = 3 def __init__(self): diff --git a/source/_magnifier/lensMagnifier.py b/source/_magnifier/lensMagnifier.py index 68f35ad27fd..34ec764a2b9 100644 --- a/source/_magnifier/lensMagnifier.py +++ b/source/_magnifier/lensMagnifier.py @@ -12,6 +12,8 @@ class LensMagnifier(Magnifier): + """Displays a magnified panel above the focused object and magnifies it.""" + def __init__(self): super().__init__() self._magnifiedView = MagnifiedView.LENS diff --git a/source/_magnifier/utils/types.py b/source/_magnifier/utils/types.py index dd9d35bd923..c1a47364603 100644 --- a/source/_magnifier/utils/types.py +++ b/source/_magnifier/utils/types.py @@ -48,7 +48,7 @@ class MagnifierAction(DisplayStringEnum): PAN_TOP_EDGE = auto() PAN_BOTTOM_EDGE = auto() TOGGLE_FILTER = auto() - CHANGE_MAGNIFIER_TYPE = auto() + CHANGE_MAGNIFIER_VIEW = auto() TOGGLE_FOLLOW_SETTINGS = auto() CHANGE_FULLSCREEN_MODE = auto() START_SPOTLIGHT = auto() @@ -80,8 +80,8 @@ def _displayStringLabels(self) -> dict["MagnifierAction", str]: self.TOGGLE_FOLLOW_SETTINGS: pgettext("magnifier action", "toggle follow settings"), # Translators: Action description for toggling color filters. self.TOGGLE_FILTER: pgettext("magnifier action", "toggle filters"), - # Translators: Action description for changing magnifier type. - self.CHANGE_MAGNIFIER_TYPE: pgettext("magnifier action", "change magnifier type"), + # Translators: Action description for changing magnifier view. + self.CHANGE_MAGNIFIER_VIEW: pgettext("magnifier action", "change magnifier view"), # Translators: Action description for changing full-screen mode. self.CHANGE_FULLSCREEN_MODE: pgettext("magnifier action", "change full-screen mode"), # Translators: Action description for starting spotlight mode. @@ -122,13 +122,13 @@ class MagnifiedView(DisplayStringStrEnum): @property def _displayStringLabels(self) -> dict["MagnifiedView", str]: return { - # Translators: Magnifier type - full-screen mode. + # Translators: Magnifier view - full-screen mode. self.FULLSCREEN: pgettext("magnifier", "Fullscreen"), - # Translators: Magnifier type - fixed mode. + # Translators: Magnifier view - fixed mode. self.FIXED: pgettext("magnifier", "Fixed"), - # Translators: Magnifier type - docked mode. + # Translators: Magnifier view - docked mode. self.DOCKED: pgettext("magnifier", "Docked"), - # Translators: Magnifier type - lens mode. + # Translators: Magnifier view - lens mode. self.LENS: pgettext("magnifier", "Lens"), } From b00619681d7b9a38613e64ea719dd4b2f7b16abd Mon Sep 17 00:00:00 2001 From: Antoine HAFFREINGUE Date: Tue, 5 May 2026 11:37:42 +0200 Subject: [PATCH 09/11] changed view type to constant --- source/_magnifier/commands.py | 8 ++++---- source/_magnifier/dockedMagnifier.py | 3 ++- source/_magnifier/fixedMagnifier.py | 3 ++- source/_magnifier/fullscreenMagnifier.py | 2 +- source/_magnifier/lensMagnifier.py | 3 ++- source/_magnifier/magnifier.py | 2 +- tests/unit/test_magnifier/test_fullscreenMagnifier.py | 8 ++++---- tests/unit/test_magnifier/test_magnifierCommands.py | 2 +- 8 files changed, 17 insertions(+), 14 deletions(-) diff --git a/source/_magnifier/commands.py b/source/_magnifier/commands.py index 6032e5b56cd..4ab40da2594 100644 --- a/source/_magnifier/commands.py +++ b/source/_magnifier/commands.py @@ -187,7 +187,7 @@ def toggleFilter() -> None: filters = list(Filter) idx = filters.index(magnifier.filterType) magnifier.filterType = filters[(idx + 1) % len(filters)] - if magnifier._magnifiedView == MagnifiedView.FULLSCREEN: + if magnifier._MAGNIFIED_VIEW == MagnifiedView.FULLSCREEN: fullscreenMagnifier: FullScreenMagnifier = magnifier fullscreenMagnifier._applyFilter() ui.message( @@ -207,7 +207,7 @@ def cycleMagnifiedView() -> None: MagnifierAction.CHANGE_MAGNIFIER_VIEW, ): views = list(MagnifiedView) - currentView = magnifier._magnifiedView + currentView = magnifier._MAGNIFIED_VIEW idx = views.index(currentView) newView = views[(idx + 1) % len(views)] log.debug(f"Changing magnifier view from {currentView} to {newView}") @@ -219,7 +219,7 @@ def cycleMagnifiedView() -> None: "magnifier", # Translators: Message announced when changing the magnifier view with {view} being the new magnifier view. "Magnifier view changed to {view}", - ).format(view=magnifier._magnifiedView.displayString), + ).format(view=magnifier._MAGNIFIED_VIEW.displayString), ) @@ -380,7 +380,7 @@ def magnifierIsFullscreenVerify( :return: True if the magnifier is full-screen, False otherwise """ - if magnifier._magnifiedView == MagnifiedView.FULLSCREEN: + if magnifier._MAGNIFIED_VIEW == MagnifiedView.FULLSCREEN: return True else: ui.message( diff --git a/source/_magnifier/dockedMagnifier.py b/source/_magnifier/dockedMagnifier.py index 037a7cdc343..befc45e4522 100644 --- a/source/_magnifier/dockedMagnifier.py +++ b/source/_magnifier/dockedMagnifier.py @@ -14,9 +14,10 @@ class DockedMagnifier(Magnifier): """Displays a magnified panel anchored to one edge of the screen.""" + _MAGNIFIED_VIEW = MagnifiedView.DOCKED + def __init__(self): super().__init__() - self._magnifiedView = MagnifiedView.DOCKED def _startMagnifier(self) -> None: super()._startMagnifier() diff --git a/source/_magnifier/fixedMagnifier.py b/source/_magnifier/fixedMagnifier.py index d40d3b3fa33..e7bdef43e3f 100644 --- a/source/_magnifier/fixedMagnifier.py +++ b/source/_magnifier/fixedMagnifier.py @@ -14,9 +14,10 @@ class FixedMagnifier(Magnifier): """Displays a magnified panel anchored to one corner of the screen.""" + _MAGNIFIED_VIEW = MagnifiedView.FIXED + def __init__(self): super().__init__() - self._magnifiedView = MagnifiedView.FIXED def _startMagnifier(self) -> None: super()._startMagnifier() diff --git a/source/_magnifier/fullscreenMagnifier.py b/source/_magnifier/fullscreenMagnifier.py index 34091991739..6557ea56c19 100644 --- a/source/_magnifier/fullscreenMagnifier.py +++ b/source/_magnifier/fullscreenMagnifier.py @@ -32,10 +32,10 @@ class FullScreenMagnifier(Magnifier): """Magnifier that uses the Windows Magnification API to magnify the entire screen.""" _MAX_RECOVERY_ATTEMPTS: int = 3 + _MAGNIFIED_VIEW = MagnifiedView.FULLSCREEN def __init__(self): super().__init__() - self._magnifiedView = MagnifiedView.FULLSCREEN self._fullscreenMode = getFullscreenMode() self._currentCoordinates = Coordinates(0, 0) self._spotlightManager = SpotlightManager(self) diff --git a/source/_magnifier/lensMagnifier.py b/source/_magnifier/lensMagnifier.py index 34ec764a2b9..2eef34e71eb 100644 --- a/source/_magnifier/lensMagnifier.py +++ b/source/_magnifier/lensMagnifier.py @@ -14,9 +14,10 @@ class LensMagnifier(Magnifier): """Displays a magnified panel above the focused object and magnifies it.""" + _MAGNIFIED_VIEW = MagnifiedView.LENS + def __init__(self): super().__init__() - self._magnifiedView = MagnifiedView.LENS def _startMagnifier(self) -> None: super()._startMagnifier() diff --git a/source/_magnifier/magnifier.py b/source/_magnifier/magnifier.py index 06949983d17..4a757c88a70 100644 --- a/source/_magnifier/magnifier.py +++ b/source/_magnifier/magnifier.py @@ -41,10 +41,10 @@ class Magnifier: _TIMER_INTERVAL_MS: int = 12 _MARGIN_BORDER: int = 50 _MAX_CONSECUTIVE_ERRORS: int = 3 + _MAGNIFIED_VIEW: MagnifiedView def __init__(self): self._displayOrientation = getPrimaryDisplayOrientation() - self._magnifiedView: MagnifiedView self._isActive: bool = False self._zoomLevel: float = getZoomLevel() self._panStep: int = getPanStep() diff --git a/tests/unit/test_magnifier/test_fullscreenMagnifier.py b/tests/unit/test_magnifier/test_fullscreenMagnifier.py index 5ae1c77553c..e16bab78f23 100644 --- a/tests/unit/test_magnifier/test_fullscreenMagnifier.py +++ b/tests/unit/test_magnifier/test_fullscreenMagnifier.py @@ -22,7 +22,7 @@ def testMagnifierCreation(self): self.assertEqual(magnifier.zoomLevel, 2.0) self.assertEqual(magnifier.filterType, Filter.NORMAL) self.assertEqual(magnifier._fullscreenMode, FullScreenMode.CENTER) - self.assertEqual(magnifier._magnifiedView, MagnifiedView.FULLSCREEN) + self.assertEqual(magnifier._MAGNIFIED_VIEW, MagnifiedView.FULLSCREEN) self.assertTrue(magnifier._isActive) magnifier._stopMagnifier() @@ -152,10 +152,10 @@ def testMagnifiedViewProperty(self): magnifier._startMagnifier() # Should default to FULLSCREEN - self.assertEqual(magnifier._magnifiedView, MagnifiedView.FULLSCREEN) + self.assertEqual(magnifier._MAGNIFIED_VIEW, MagnifiedView.FULLSCREEN) # Test that we can read it (inherited property from Magnifier) - self.assertIsNotNone(magnifier._magnifiedView) + self.assertIsNotNone(magnifier._MAGNIFIED_VIEW) # Cleanup magnifier._stopMagnifier() @@ -170,7 +170,7 @@ def testMagnifierInheritance(self): # Test basic properties exist self.assertTrue(hasattr(magnifier, "zoomLevel")) self.assertTrue(hasattr(magnifier, "filterType")) - self.assertTrue(hasattr(magnifier, "_magnifiedView")) + self.assertTrue(hasattr(magnifier, "_MAGNIFIED_VIEW")) self.assertTrue(hasattr(magnifier, "_fullscreenMode")) self.assertTrue(hasattr(magnifier, "_isActive")) self.assertTrue(hasattr(magnifier, "_currentCoordinates")) diff --git a/tests/unit/test_magnifier/test_magnifierCommands.py b/tests/unit/test_magnifier/test_magnifierCommands.py index 8e442d339ed..ddf41f318c4 100644 --- a/tests/unit/test_magnifier/test_magnifierCommands.py +++ b/tests/unit/test_magnifier/test_magnifierCommands.py @@ -70,7 +70,7 @@ def tearDown(self): def _makeMockMagnifier(self, magnifiedView: MagnifiedView): magnifier = MagicMock() magnifier._isActive = True - magnifier._magnifiedView = magnifiedView + magnifier._MAGNIFIED_VIEW = magnifiedView return magnifier def testFullCycle(self): From 5b28ae7f7689c2dad0c3463e8cfd7a86123df9b1 Mon Sep 17 00:00:00 2001 From: Boumtchack <147637328+Boumtchack@users.noreply.github.com> Date: Tue, 5 May 2026 11:42:50 +0200 Subject: [PATCH 10/11] Update source/_magnifier/lensMagnifier.py Co-authored-by: Sean Budd --- source/_magnifier/lensMagnifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/_magnifier/lensMagnifier.py b/source/_magnifier/lensMagnifier.py index 2eef34e71eb..83675b64f9c 100644 --- a/source/_magnifier/lensMagnifier.py +++ b/source/_magnifier/lensMagnifier.py @@ -12,7 +12,7 @@ class LensMagnifier(Magnifier): - """Displays a magnified panel above the focused object and magnifies it.""" + """Displays a magnified panel beside the focused object and magnifies it.""" _MAGNIFIED_VIEW = MagnifiedView.LENS From 1b8c0f6a4bf5a479d883554e27f32d4be46e9751 Mon Sep 17 00:00:00 2001 From: Antoine HAFFREINGUE Date: Tue, 5 May 2026 11:46:20 +0200 Subject: [PATCH 11/11] update fixed description --- source/_magnifier/fixedMagnifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/_magnifier/fixedMagnifier.py b/source/_magnifier/fixedMagnifier.py index e7bdef43e3f..4c29bd2f235 100644 --- a/source/_magnifier/fixedMagnifier.py +++ b/source/_magnifier/fixedMagnifier.py @@ -12,7 +12,7 @@ class FixedMagnifier(Magnifier): - """Displays a magnified panel anchored to one corner of the screen.""" + """Displays a floating magnified panel that can be pinned anywhere on the screen.""" _MAGNIFIED_VIEW = MagnifiedView.FIXED