Skip to content

Commit 7bf0d9a

Browse files
authored
Persist magnifier starting (#20051)
Fixes #19494 Summary of the issue: Whether or not the magnifier has already started was not persisted in config Description of user facing changes: magnifier being enabled is now persisted in config fixed some collisions with accelerator keys in the magnifier settings dialog Description of developer facing changes: none Description of development approach: added code to toggle magnifier and persist in settings
1 parent 967631e commit 7bf0d9a

9 files changed

Lines changed: 120 additions & 26 deletions

File tree

source/_magnifier/__init__.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
"""
1010

1111
from typing import TYPE_CHECKING
12-
from .config import getMagnifiedView
12+
13+
from logHandler import log
14+
15+
from .config import getMagnifiedView, getEnabled, setEnabled
1316
from .utils.types import MagnifiedView
1417

1518
if TYPE_CHECKING:
@@ -72,9 +75,44 @@ def initialize() -> None:
7275
"""
7376
Initialize the magnifier module with the default magnifier view from config.
7477
"""
78+
log.debug("Initializing magnifier")
7579
magnifiedView = getMagnifiedView()
7680
_setMagnifiedView(magnifiedView)
81+
if getEnabled():
82+
start()
83+
84+
85+
def terminate() -> None:
86+
"""
87+
Terminate the magnifier module.
88+
Called when NVDA shuts down.
89+
"""
90+
global _magnifier
91+
92+
log.debug("Terminating magnifier")
93+
stop(persist=False)
94+
_magnifier = None
95+
96+
97+
def start() -> None:
98+
if _magnifier is None:
99+
log.error("Attempted to start magnifier, but it is not initialized.")
100+
return
77101
_magnifier._startMagnifier()
102+
setEnabled(True)
103+
104+
105+
def stop(persist: bool = True) -> None:
106+
"""Stop the magnifier if it is active.
107+
108+
:param persist: Whether to persist the magnifier state
109+
"""
110+
if isActive():
111+
_magnifier._stopMagnifier()
112+
if persist:
113+
setEnabled(False)
114+
else:
115+
log.debug("Attempted to stop magnifier, but it is not active.")
78116

79117

80118
def isActive() -> bool:
@@ -111,14 +149,3 @@ def getMagnifier() -> "Magnifier | None":
111149
"""
112150
global _magnifier
113151
return _magnifier
114-
115-
116-
def terminate() -> None:
117-
"""
118-
Terminate the magnifier module.
119-
Called when NVDA shuts down.
120-
"""
121-
global _magnifier
122-
if _magnifier and _magnifier._isActive:
123-
_magnifier._stopMagnifier()
124-
_magnifier = None

source/_magnifier/commands.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@
1010

1111
from typing import Literal
1212
import ui
13-
from . import getMagnifier, initialize, terminate, changeMagnifiedView
13+
from . import changeMagnifiedView, getMagnifier, start, stop
1414
from .config import (
1515
getMagnifiedView,
1616
setMagnifiedView,
1717
getZoomLevelString,
1818
getFilter,
19-
getFullscreenMode,
20-
ZoomLevel,
2119
getFollowState,
20+
getFullscreenMode,
2221
setFollowState,
2322
toggleAllFollowStates,
23+
ZoomLevel,
2424
)
2525
from .magnifier import Magnifier
2626
from .fullscreenMagnifier import FullScreenMagnifier
@@ -89,7 +89,7 @@ def toggleMagnifier() -> None:
8989
magnifier: Magnifier | None = getMagnifier()
9090
if magnifier and magnifier._isActive:
9191
# Stop magnifier
92-
terminate()
92+
stop()
9393
ui.message(
9494
pgettext(
9595
"magnifier",
@@ -107,7 +107,7 @@ def toggleMagnifier() -> None:
107107
),
108108
)
109109
else:
110-
initialize()
110+
start()
111111
currentFilter = getFilter()
112112
magnifiedView = getMagnifiedView()
113113
zoomLevel = getZoomLevelString()

source/_magnifier/config.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# A part of NonVisual Desktop Access (NVDA)
2-
# Copyright (C) 2025 NV Access Limited, Antoine Haffreingue
2+
# Copyright (C) 2025-2026 NV Access Limited, Antoine Haffreingue
33
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
44
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt
55

@@ -13,6 +13,24 @@
1313
from .utils.types import Filter, FullScreenMode, MagnifierFollowFocusType, MagnifiedView
1414

1515

16+
def setEnabled(enable: bool) -> None:
17+
"""
18+
Set the config for the magnifier state (enable or disabled).
19+
20+
:param enable: True if the magnifier is enabled, False if it is disabled.
21+
"""
22+
config.conf["magnifier"]["enabled"] = enable
23+
24+
25+
def getEnabled() -> bool:
26+
"""
27+
Check if the magnifier is enabled in config.
28+
29+
:return: True if the magnifier is enabled, False otherwise.
30+
"""
31+
return config.conf["magnifier"]["enabled"]
32+
33+
1634
class ZoomLevel:
1735
"""
1836
Constants and utilities for zoom level management.

source/_magnifier/fullscreenMagnifier.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ def __init__(self):
4141
self.currentCoordinates = Coordinates(0, 0)
4242
self._spotlightManager = SpotlightManager(self)
4343
self._displaySize = Size(self._displayOrientation.width, self._displayOrientation.height)
44-
self._startMagnifier()
4544

4645
@Magnifier.filterType.setter
4746
def filterType(self, value: Filter) -> None:

source/config/configSpec.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
117117
# Magnifier settings
118118
[magnifier]
119+
enabled = boolean(default=false)
119120
magnifiedView = string(default="fullscreen")
120121
zoomLevel = float(min=1.0, max=10.0, default=2.0)
121122
isTrueCentered = boolean(default=False)

source/core.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# A part of NonVisual Desktop Access (NVDA)
2-
# Copyright (C) 2006-2025 NV Access Limited, Aleksey Sadovoy, Christopher Toth, Joseph Lee, Peter Vágner,
2+
# Copyright (C) 2006-2026 NV Access Limited, Aleksey Sadovoy, Christopher Toth, Joseph Lee, Peter Vágner,
33
# Derek Riemer, Babbage B.V., Zahari Yurukov, Łukasz Golonka, Cyrille Bougot, Julien Cochuyt
44
# This file is covered by the GNU General Public License.
55
# See the file COPYING for more details.
@@ -10,7 +10,6 @@
1010
from typing import (
1111
TYPE_CHECKING,
1212
Any,
13-
List,
1413
Optional,
1514
)
1615
import comtypes
@@ -135,7 +134,7 @@ def handleReplaceCLIArg(cliArgument: str) -> bool:
135134
return cliArgument in ("-r", "--replace")
136135

137136
addonHandler.isCLIParamKnown.register(handleReplaceCLIArg)
138-
unknownCLIParams: List[str] = list()
137+
unknownCLIParams: list[str] = list()
139138
for param in globalVars.unknownAppArgs:
140139
isParamKnown = addonHandler.isCLIParamKnown.decide(cliArgument=param)
141140
if not isParamKnown:
@@ -324,7 +323,9 @@ def resetConfiguration(factoryDefaults=False):
324323
import audio
325324
import screenCurtain
326325
import mathPres
326+
import _magnifier as magnifier
327327

328+
magnifier.terminate()
328329
log.debug("Terminating vision")
329330
vision.terminate()
330331
log.debug("Terminating Screen Curtain")
@@ -399,6 +400,7 @@ def resetConfiguration(factoryDefaults=False):
399400
vision.initialize()
400401
log.debug("initializing Screen Curtain")
401402
screenCurtain.initialize()
403+
magnifier.initialize()
402404
log.debug("Reloading user and locale input gesture maps")
403405
inputCore.manager.loadUserGestureMap()
404406
inputCore.manager.loadLocaleGestureMap()
@@ -1058,6 +1060,10 @@ def Notify(self):
10581060

10591061
sessionTracking.initialize()
10601062

1063+
import _magnifier as magnifier
1064+
1065+
magnifier.initialize()
1066+
10611067
NVDAState._TrackNVDAInitialization.markInitializationComplete()
10621068

10631069
log.info("NVDA initialized")
@@ -1084,6 +1090,7 @@ def _doPostNvdaStartupAction():
10841090
)
10851091
queueHandler.pumpAll()
10861092
_terminate(gui)
1093+
_terminate(magnifier)
10871094
config.saveOnExit()
10881095

10891096
_doLoseFocus()

source/gui/settingsDialogs.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import keyboardHandler
4141
import languageHandler
4242
import logHandler
43+
from _magnifier.commands import toggleMagnifier
4344
import _magnifier.config as magnifierConfig
4445
from _magnifier.utils.types import Filter, FullScreenMode, MagnifierFollowFocusType
4546
import queueHandler
@@ -6049,6 +6050,18 @@ def makeSettings(
60496050
sizer=settingsSizer,
60506051
)
60516052

6053+
# Enable the magnifier
6054+
# Translators: The label for a setting in magnifier settings to enable or disable the magnifier.
6055+
enableMagnifierText = _("&Enable magnifier (immediate effect)")
6056+
self._magnifierEnabledInitially = magnifierConfig.getEnabled()
6057+
self.enableMagnifierCheckBox = sHelper.addItem(wx.CheckBox(self, label=enableMagnifierText))
6058+
self.bindHelpEvent(
6059+
"MagnifierEnable",
6060+
self.enableMagnifierCheckBox,
6061+
)
6062+
self.enableMagnifierCheckBox.Bind(wx.EVT_CHECKBOX, self.onEnableMagnifierChange)
6063+
self.enableMagnifierCheckBox.SetValue(self._magnifierEnabledInitially)
6064+
60526065
# ZOOM SETTINGS
60536066
# Translators: The label for a setting in magnifier settings to select the zoom level.
60546067
zoomLabelText = _("&Zoom level:")
@@ -6066,7 +6079,7 @@ def makeSettings(
60666079
self.zoomList,
60676080
)
60686081

6069-
# Set value from config
6082+
# Set value from config
60706083
zoomLevel = magnifierConfig.getZoomLevel()
60716084
zoomIndex = bisect.bisect_left(zoomValues, zoomLevel)
60726085
# Find the closest value
@@ -6099,7 +6112,7 @@ def makeSettings(
60996112

61006113
# FILTER SETTINGS
61016114
# Translators: The label for a setting in magnifier settings to select the default filter
6102-
filterLabelText = _("&filter:")
6115+
filterLabelText = _("F&ilter:")
61036116
filterChoices = [f.displayString for f in Filter]
61046117
self.filterList = sHelper.addLabeledControl(
61056118
filterLabelText,
@@ -6114,7 +6127,7 @@ def makeSettings(
61146127

61156128
# FULLSCREEN MODE SETTINGS
61166129
# Translators: The label for a setting in magnifier settings to select the full-screen mode
6117-
fullscreenModeLabelText = _("&fullscreen mode:")
6130+
fullscreenModeLabelText = _("&Fullscreen mode:")
61186131
fullscreenModeChoices = [mode.displayString for mode in FullScreenMode] if FullScreenMode else []
61196132
self.fullscreenModeList = sHelper.addLabeledControl(
61206133
fullscreenModeLabelText,
@@ -6170,7 +6183,7 @@ def makeSettings(
61706183

61716184
# KEEP MOUSE CENTERED
61726185
# Translators: The label for a checkbox to keep the mouse pointer centered in the magnifier view
6173-
keepMouseCenteredText = _("Keep &mouse pointer centered in magnifier view")
6186+
keepMouseCenteredText = _("Keep mouse pointer &centered in magnifier view")
61746187
self.keepMouseCenteredCheckBox = sHelper.addItem(wx.CheckBox(self, label=keepMouseCenteredText))
61756188
self.bindHelpEvent(
61766189
"MagnifierKeepMouseCentered",
@@ -6180,6 +6193,8 @@ def makeSettings(
61806193

61816194
def onSave(self):
61826195
"""Save the current selections to config."""
6196+
magnifierConfig.setEnabled(self.enableMagnifierCheckBox.GetValue())
6197+
61836198
selectedZoom = self.zoomList.GetSelection()
61846199
magnifierConfig.setZoomLevel(magnifierConfig.ZoomLevel.zoom_range()[selectedZoom])
61856200

@@ -6196,6 +6211,20 @@ def onSave(self):
61966211
magnifierConfig.setFollowState(focusType, checkBox.GetValue())
61976212
config.conf["magnifier"]["keepMouseCentered"] = self.keepMouseCenteredCheckBox.GetValue()
61986213

6214+
def onDiscard(self):
6215+
"""Restore magnifier state from original settings from config."""
6216+
if self._magnifierEnabledInitially != magnifierConfig.getEnabled():
6217+
toggleMagnifier()
6218+
self.enableMagnifierCheckBox.SetValue(self._magnifierEnabledInitially)
6219+
6220+
def onEnableMagnifierChange(self, evt: wx.CommandEvent):
6221+
"""Enable magnifier immediately when the checkbox is toggled, and update the checkbox state if there is an error enabling the magnifier."""
6222+
requestedEnabled = evt.IsChecked()
6223+
currentEnabled = magnifierConfig.getEnabled()
6224+
if requestedEnabled != currentEnabled:
6225+
toggleMagnifier()
6226+
self.enableMagnifierCheckBox.SetValue(magnifierConfig.getEnabled())
6227+
61996228

62006229
class PrivacyAndSecuritySettingsPanel(SettingsPanel):
62016230
# Translators: The title of the privacy and security category in NVDA's settings.

tests/unit/test_magnifier/test_fullscreenMagnifier.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ def testCannotStartWhenWindowsMagnifierRunning(self):
298298

299299
with patch("_magnifier.fullscreenMagnifier.ui.message") as mock_message:
300300
magnifier = FullScreenMagnifier()
301+
magnifier._startMagnifier()
301302

302303
self.assertFalse(magnifier._isActive)
303304
mock_message.assert_called_once()
@@ -311,6 +312,7 @@ def testCannotStartWhenMagInitializeFails(self):
311312

312313
with patch("_magnifier.fullscreenMagnifier.ui.message") as mock_message:
313314
magnifier = FullScreenMagnifier()
315+
magnifier._startMagnifier()
314316

315317
self.assertFalse(magnifier._isActive)
316318
mock_message.assert_called_once()

user_docs/en/userGuide.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1573,6 +1573,7 @@ To enable or disable the magnifier, press `NVDA+shift+w`.
15731573
The "Increase magnification level" keystroke, `NVDA+shift+equals`, will start the magnifier if it is not running.
15741574
When the magnifier is enabled, NVDA will announce the current zoom level, color filter, and focus tracking mode.
15751575
When disabled, the screen returns to its normal size.
1576+
The Magnifier state is saved for future NVDA sessions, so after enabling Magnifier, it will start automatically after restarting NVDA
15761577

15771578
Important: The NVDA Magnifier cannot be used simultaneously with Screen Curtain for security reasons.
15781579
If you attempt to enable the magnifier while Screen Curtain is active, NVDA will prompt you to disable Screen Curtain first, and vice versa.
@@ -2877,6 +2878,16 @@ Key: `NVDA+control+w`
28772878
The Magnifier category in the NVDA Settings dialog allows you to configure the default behavior of NVDA's built-in [Magnifier](#Magnifier) feature.
28782879
This settings category contains the following options:
28792880

2881+
##### Enable Magnifier {#MagnifierEnable}
2882+
2883+
When toggled, Magnifier will start and stop immediately.
2884+
The selected state is also saved for future NVDA sessions, so if you enable Magnifier here, it will start automatically after restarting NVDA.
2885+
2886+
| . {.hideHeaderRow} |.|
2887+
|---|---|
2888+
|Options |Disabled, Enabled|
2889+
|Default |Disabled|
2890+
28802891
##### Zoom level {#MagnifierZoom}
28812892

28822893
This slider allows you to set the zoom level when using the magnifier.

0 commit comments

Comments
 (0)