From 3d993b7581a8edc1b8cb3ad989e6304567cf9675 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Mon, 4 May 2026 16:37:04 +1000 Subject: [PATCH 1/8] Persist magnifier starting --- source/_magnifier/commands.py | 9 +++++---- source/_magnifier/config.py | 20 +++++++++++++++++++- source/config/configSpec.py | 1 + source/gui/settingsDialogs.py | 25 +++++++++++++++++++++---- user_docs/en/userGuide.md | 5 +++++ 5 files changed, 51 insertions(+), 9 deletions(-) diff --git a/source/_magnifier/commands.py b/source/_magnifier/commands.py index 1a50df3a719..127f5afc071 100644 --- a/source/_magnifier/commands.py +++ b/source/_magnifier/commands.py @@ -10,15 +10,16 @@ from typing import Literal import ui -from . import getMagnifier, initialize, terminate +from . import getMagnifier, initialize, isActive, terminate from .config import ( getZoomLevelString, getFilter, - getFullscreenMode, - ZoomLevel, getFollowState, + getFullscreenMode, + setEnabled, setFollowState, toggleAllFollowStates, + ZoomLevel, ) from .magnifier import Magnifier from .fullscreenMagnifier import FullScreenMagnifier @@ -100,7 +101,6 @@ def toggleMagnifier() -> None: "Cannot start magnifier: Screen Curtain is active. Please disable Screen Curtain first.", ), ) - return else: initialize() @@ -118,6 +118,7 @@ def toggleMagnifier() -> None: fullscreenMode=fullscreenMode.displayString, ), ) + setEnabled(isActive()) def zoom(direction: Direction) -> None: diff --git a/source/_magnifier/config.py b/source/_magnifier/config.py index e5abe6b8d96..1fc964f058d 100644 --- a/source/_magnifier/config.py +++ b/source/_magnifier/config.py @@ -1,5 +1,5 @@ # 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 @@ -13,6 +13,24 @@ from .utils.types import Filter, FullScreenMode, MagnifierFollowFocusType +def setEnabled(enable: bool) -> None: + """ + Set the config for the magnifier state (enable or disabled). + + :param enabled: True if the magnifier is enabled, False if it is disabled. + """ + config.conf["magnifier"]["enabled"] = enable + + +def getEnabled() -> bool: + """ + Check if the magnifier is enabled in config. + + :return: True if the magnifier is enabled, False otherwise. + """ + return config.conf["magnifier"]["enabled"] + + class ZoomLevel: """ Constants and utilities for zoom level management. diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 982532d4d5f..61fe5be4b4f 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -116,6 +116,7 @@ # Magnifier settings [magnifier] + enabled = boolean(default=false) zoomLevel = float(min=1.0, max=10.0, default=2.0) isTrueCentered = boolean(default=False) filter = string(default="normal") diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index c549a6ad38c..8aa3c5acd62 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -40,6 +40,7 @@ import keyboardHandler import languageHandler import logHandler +from _magnifier.commands import toggleMagnifier import _magnifier.config as magnifierConfig from _magnifier.utils.types import Filter, FullScreenMode, MagnifierFollowFocusType import queueHandler @@ -6035,6 +6036,17 @@ def makeSettings( sizer=settingsSizer, ) + # Enable the magnifier + # Translators: The label for a setting in magnifier settings to enable or disable the magnifier. + enableMagnifierText = _("&Enable magnifier (immediate effect)") + self.enableMagnifierCheckBox = sHelper.addItem(wx.CheckBox(self, label=enableMagnifierText)) + self.bindHelpEvent( + "MagnifierEnable", + self.enableMagnifierCheckBox, + ) + self.enableMagnifierCheckBox.Bind(wx.EVT_CHECKBOX, self.onEnableMagnifierChange) + self.enableMagnifierCheckBox.SetValue(magnifierConfig.getEnabled()) + # ZOOM SETTINGS # Translators: The label for a setting in magnifier settings to select the zoom level. zoomLabelText = _("&Zoom level:") @@ -6052,7 +6064,7 @@ def makeSettings( self.zoomList, ) - # Set value from config + # Set value from config zoomLevel = magnifierConfig.getZoomLevel() zoomIndex = bisect.bisect_left(zoomValues, zoomLevel) # Find the closest value @@ -6085,7 +6097,7 @@ def makeSettings( # FILTER SETTINGS # Translators: The label for a setting in magnifier settings to select the default filter - filterLabelText = _("&filter:") + filterLabelText = _("&Filter:") filterChoices = [f.displayString for f in Filter] self.filterList = sHelper.addLabeledControl( filterLabelText, @@ -6100,7 +6112,7 @@ def makeSettings( # FULLSCREEN MODE SETTINGS # Translators: The label for a setting in magnifier settings to select the full-screen mode - fullscreenModeLabelText = _("&fullscreen mode:") + fullscreenModeLabelText = _("F&ullscreen mode:") fullscreenModeChoices = [mode.displayString for mode in FullScreenMode] if FullScreenMode else [] self.fullscreenModeList = sHelper.addLabeledControl( fullscreenModeLabelText, @@ -6156,7 +6168,7 @@ def makeSettings( # KEEP MOUSE CENTERED # Translators: The label for a checkbox to keep the mouse pointer centered in the magnifier view - keepMouseCenteredText = _("Keep &mouse pointer centered in magnifier view") + keepMouseCenteredText = _("Keep mouse pointer ¢ered in magnifier view") self.keepMouseCenteredCheckBox = sHelper.addItem(wx.CheckBox(self, label=keepMouseCenteredText)) self.bindHelpEvent( "MagnifierKeepMouseCentered", @@ -6166,6 +6178,8 @@ def makeSettings( def onSave(self): """Save the current selections to config.""" + magnifierConfig.setEnabled(self.enableMagnifierCheckBox.GetValue()) + selectedZoom = self.zoomList.GetSelection() magnifierConfig.setZoomLevel(magnifierConfig.ZoomLevel.zoom_range()[selectedZoom]) @@ -6182,6 +6196,9 @@ def onSave(self): magnifierConfig.setFollowState(focusType, checkBox.GetValue()) config.conf["magnifier"]["keepMouseCentered"] = self.keepMouseCenteredCheckBox.GetValue() + def onEnableMagnifierChange(self, evt: wx.CommandEvent): + toggleMagnifier() + class PrivacyAndSecuritySettingsPanel(SettingsPanel): # Translators: The title of the privacy and security category in NVDA's settings. diff --git a/user_docs/en/userGuide.md b/user_docs/en/userGuide.md index 26151130228..3287026140a 100644 --- a/user_docs/en/userGuide.md +++ b/user_docs/en/userGuide.md @@ -2871,6 +2871,11 @@ Key: `NVDA+control+w` The Magnifier category in the NVDA Settings dialog allows you to configure the default behavior of NVDA's built-in [Magnifier](#Magnifier) feature. This settings category contains the following options: +##### Enable Magnfier {#MagnifierEnable} + +Enables the magnifier. +When toggled, the magnifier will start and stop immediately, rather than on-save. + ##### Zoom level {#MagnifierZoom} This slider allows you to set the zoom level when using the magnifier. From e1145a30b22cf89d3c97c9c3ca05b65046ef3732 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Mon, 11 May 2026 15:24:20 +1000 Subject: [PATCH 2/8] typo fixes --- source/_magnifier/config.py | 2 +- user_docs/en/userGuide.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/_magnifier/config.py b/source/_magnifier/config.py index 842c29a1f98..e4cbecbc314 100644 --- a/source/_magnifier/config.py +++ b/source/_magnifier/config.py @@ -17,7 +17,7 @@ def setEnabled(enable: bool) -> None: """ Set the config for the magnifier state (enable or disabled). - :param enabled: True if the magnifier is enabled, False if it is disabled. + :param enable: True if the magnifier is enabled, False if it is disabled. """ config.conf["magnifier"]["enabled"] = enable diff --git a/user_docs/en/userGuide.md b/user_docs/en/userGuide.md index 0d85c63dce5..ff4e834798e 100644 --- a/user_docs/en/userGuide.md +++ b/user_docs/en/userGuide.md @@ -2877,7 +2877,7 @@ Key: `NVDA+control+w` The Magnifier category in the NVDA Settings dialog allows you to configure the default behavior of NVDA's built-in [Magnifier](#Magnifier) feature. This settings category contains the following options: -##### Enable Magnfier {#MagnifierEnable} +##### Enable Magnifier {#MagnifierEnable} Enables the magnifier. When toggled, the magnifier will start and stop immediately, rather than on-save. From 888d740c257da8ab0e17f7380f3dbea126acd6f2 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Mon, 11 May 2026 15:34:43 +1000 Subject: [PATCH 3/8] add table --- user_docs/en/userGuide.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/user_docs/en/userGuide.md b/user_docs/en/userGuide.md index ff4e834798e..a09fedc3b0c 100644 --- a/user_docs/en/userGuide.md +++ b/user_docs/en/userGuide.md @@ -2882,6 +2882,11 @@ This settings category contains the following options: Enables the magnifier. When toggled, the magnifier will start and stop immediately, rather than on-save. +| . {.hideHeaderRow} |.| +|---|---| +|Options |Disabled, Enabled| +|Default |Disabled| + ##### Zoom level {#MagnifierZoom} This slider allows you to set the zoom level when using the magnifier. From 9ec92b67a11e8bcaee2219d5e3b9845cb6ab0e52 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Mon, 11 May 2026 17:22:03 +1000 Subject: [PATCH 4/8] improve initialization/termination --- source/_magnifier/__init__.py | 46 ++++++++++++++++++++++++++--------- source/_magnifier/commands.py | 8 +++--- source/core.py | 13 +++++++--- 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/source/_magnifier/__init__.py b/source/_magnifier/__init__.py index f742dd6f1ea..25a19447c53 100644 --- a/source/_magnifier/__init__.py +++ b/source/_magnifier/__init__.py @@ -9,7 +9,10 @@ """ from typing import TYPE_CHECKING -from .config import getMagnifiedView + +from logHandler import log + +from .config import getMagnifiedView, getEnabled, setEnabled from .utils.types import MagnifiedView if TYPE_CHECKING: @@ -72,9 +75,39 @@ def initialize() -> None: """ Initialize the magnifier module with the default magnifier view from config. """ + log.debug("Initializing magnifier") magnifiedView = getMagnifiedView() _setMagnifiedView(magnifiedView) + if getEnabled(): + start() + + +def terminate() -> None: + """ + Terminate the magnifier module. + Called when NVDA shuts down. + """ + global _magnifier + + log.debug("Terminating magnifier") + stop() + _magnifier = None + + +def start() -> None: + if _magnifier is None: + log.error("Attempted to start magnifier, but it is not initialized.") + return _magnifier._startMagnifier() + setEnabled(True) + + +def stop() -> None: + if isActive(): + _magnifier._stopMagnifier() + setEnabled(False) + else: + log.debug("Attempted to stop magnifier, but it is not active.") def isActive() -> bool: @@ -111,14 +144,3 @@ def getMagnifier() -> "Magnifier | None": """ global _magnifier return _magnifier - - -def terminate() -> None: - """ - Terminate the magnifier module. - Called when NVDA shuts down. - """ - global _magnifier - if _magnifier and _magnifier._isActive: - _magnifier._stopMagnifier() - _magnifier = None diff --git a/source/_magnifier/commands.py b/source/_magnifier/commands.py index 6c2f4fdd5ca..4cfa2c7bd43 100644 --- a/source/_magnifier/commands.py +++ b/source/_magnifier/commands.py @@ -10,7 +10,7 @@ from typing import Literal import ui -from . import changeMagnifiedView, getMagnifier, initialize, isActive, terminate +from . import changeMagnifiedView, getMagnifier, start, stop from .config import ( getMagnifiedView, setMagnifiedView, @@ -18,7 +18,6 @@ getFilter, getFollowState, getFullscreenMode, - setEnabled, setFollowState, toggleAllFollowStates, ZoomLevel, @@ -86,7 +85,7 @@ def toggleMagnifier() -> None: magnifier: Magnifier | None = getMagnifier() if magnifier and magnifier._isActive: # Stop magnifier - terminate() + stop() ui.message( pgettext( "magnifier", @@ -104,7 +103,7 @@ def toggleMagnifier() -> None: ), ) else: - initialize() + start() currentFilter = getFilter() magnifiedView = getMagnifiedView() zoomLevel = getZoomLevelString() @@ -131,7 +130,6 @@ def toggleMagnifier() -> None: filter=currentFilter.displayString, ) ui.message(msg) - setEnabled(isActive()) def zoom(direction: Direction) -> None: diff --git a/source/core.py b/source/core.py index 0deada7f022..48ec6ad4c33 100644 --- a/source/core.py +++ b/source/core.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2025 NV Access Limited, Aleksey Sadovoy, Christopher Toth, Joseph Lee, Peter Vágner, +# Copyright (C) 2006-2026 NV Access Limited, Aleksey Sadovoy, Christopher Toth, Joseph Lee, Peter Vágner, # Derek Riemer, Babbage B.V., Zahari Yurukov, Łukasz Golonka, Cyrille Bougot, Julien Cochuyt # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -10,7 +10,6 @@ from typing import ( TYPE_CHECKING, Any, - List, Optional, ) import comtypes @@ -135,7 +134,7 @@ def handleReplaceCLIArg(cliArgument: str) -> bool: return cliArgument in ("-r", "--replace") addonHandler.isCLIParamKnown.register(handleReplaceCLIArg) - unknownCLIParams: List[str] = list() + unknownCLIParams: list[str] = list() for param in globalVars.unknownAppArgs: isParamKnown = addonHandler.isCLIParamKnown.decide(cliArgument=param) if not isParamKnown: @@ -324,7 +323,9 @@ def resetConfiguration(factoryDefaults=False): import audio import screenCurtain import mathPres + import _magnifier as magnifier + magnifier.terminate() log.debug("Terminating vision") vision.terminate() log.debug("Terminating Screen Curtain") @@ -399,6 +400,7 @@ def resetConfiguration(factoryDefaults=False): vision.initialize() log.debug("initializing Screen Curtain") screenCurtain.initialize() + magnifier.initialize() log.debug("Reloading user and locale input gesture maps") inputCore.manager.loadUserGestureMap() inputCore.manager.loadLocaleGestureMap() @@ -1053,6 +1055,10 @@ def Notify(self): sessionTracking.initialize() + import _magnifier as magnifier + + magnifier.initialize() + NVDAState._TrackNVDAInitialization.markInitializationComplete() log.info("NVDA initialized") @@ -1079,6 +1085,7 @@ def _doPostNvdaStartupAction(): ) queueHandler.pumpAll() _terminate(gui) + _terminate(magnifier) config.saveOnExit() _doLoseFocus() From 5dc16ed4cb2147f2e3c79f911b93cae801742d76 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Mon, 11 May 2026 17:25:38 +1000 Subject: [PATCH 5/8] safer toggle --- source/gui/settingsDialogs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 208c2c45acc..bda31800ecb 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -6211,7 +6211,10 @@ def onSave(self): config.conf["magnifier"]["keepMouseCentered"] = self.keepMouseCenteredCheckBox.GetValue() def onEnableMagnifierChange(self, evt: wx.CommandEvent): - toggleMagnifier() + requestedEnabled = evt.IsChecked() + currentEnabled = magnifierConfig.getEnabled() + if requestedEnabled != currentEnabled: + toggleMagnifier() class PrivacyAndSecuritySettingsPanel(SettingsPanel): From e36fc10ec21d8033cfcf57bc5d842b12e9856845 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Mon, 11 May 2026 17:34:10 +1000 Subject: [PATCH 6/8] fix persistence --- source/_magnifier/__init__.py | 11 ++++++++--- source/_magnifier/fullscreenMagnifier.py | 1 - 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/source/_magnifier/__init__.py b/source/_magnifier/__init__.py index 25a19447c53..2929c2ed8a1 100644 --- a/source/_magnifier/__init__.py +++ b/source/_magnifier/__init__.py @@ -90,7 +90,7 @@ def terminate() -> None: global _magnifier log.debug("Terminating magnifier") - stop() + stop(persist=False) _magnifier = None @@ -102,10 +102,15 @@ def start() -> None: setEnabled(True) -def stop() -> None: +def stop(persist: bool = True) -> None: + """Stop the magnifier if it is active. + + :param persist: Whether to persist the magnifier state + """ if isActive(): _magnifier._stopMagnifier() - setEnabled(False) + if persist: + setEnabled(False) else: log.debug("Attempted to stop magnifier, but it is not active.") diff --git a/source/_magnifier/fullscreenMagnifier.py b/source/_magnifier/fullscreenMagnifier.py index 6dff46beaf1..7b77fa61808 100644 --- a/source/_magnifier/fullscreenMagnifier.py +++ b/source/_magnifier/fullscreenMagnifier.py @@ -41,7 +41,6 @@ def __init__(self): self.currentCoordinates = Coordinates(0, 0) self._spotlightManager = SpotlightManager(self) self._displaySize = Size(self._displayOrientation.width, self._displayOrientation.height) - self._startMagnifier() @Magnifier.filterType.setter def filterType(self, value: Filter) -> None: From 47225b39d0ec892d27c3a7415042f27faaf373e9 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Tue, 12 May 2026 12:48:30 +1000 Subject: [PATCH 7/8] fix unit tests --- tests/unit/test_magnifier/test_fullscreenMagnifier.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/test_magnifier/test_fullscreenMagnifier.py b/tests/unit/test_magnifier/test_fullscreenMagnifier.py index 3e9359eec6c..3c918ee26a7 100644 --- a/tests/unit/test_magnifier/test_fullscreenMagnifier.py +++ b/tests/unit/test_magnifier/test_fullscreenMagnifier.py @@ -298,6 +298,7 @@ def testCannotStartWhenWindowsMagnifierRunning(self): with patch("_magnifier.fullscreenMagnifier.ui.message") as mock_message: magnifier = FullScreenMagnifier() + magnifier._startMagnifier() self.assertFalse(magnifier._isActive) mock_message.assert_called_once() @@ -311,6 +312,7 @@ def testCannotStartWhenMagInitializeFails(self): with patch("_magnifier.fullscreenMagnifier.ui.message") as mock_message: magnifier = FullScreenMagnifier() + magnifier._startMagnifier() self.assertFalse(magnifier._isActive) mock_message.assert_called_once() From 198242ee36a0295f456272f6b0c5f7640aa08b60 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Tue, 12 May 2026 12:52:04 +1000 Subject: [PATCH 8/8] fix sync --- source/gui/settingsDialogs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index bda31800ecb..c1f177e64e2 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -6215,6 +6215,7 @@ def onEnableMagnifierChange(self, evt: wx.CommandEvent): currentEnabled = magnifierConfig.getEnabled() if requestedEnabled != currentEnabled: toggleMagnifier() + self.enableMagnifierCheckBox.SetValue(magnifierConfig.getEnabled()) class PrivacyAndSecuritySettingsPanel(SettingsPanel):