Skip to content
4 changes: 1 addition & 3 deletions source/_magnifier/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,7 @@ def zoom(direction: Direction) -> None:
return
magnifier._zoom(direction)
ui.message(
ZoomLevel.ZOOM_MESSAGE.format(
zoomLevel=f"{magnifier.zoomLevel:.1f}",
),
ZoomLevel.zoomMessage(magnifier.zoomLevel),
)


Expand Down
70 changes: 26 additions & 44 deletions source/_magnifier/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,45 +36,27 @@ class ZoomLevel:
Constants and utilities for zoom level management.
"""

MAX_ZOOM: float = 10.0
MIN_ZOOM: float = 1.0
STEP_FACTOR: float = 0.5
ZOOM_MESSAGE = pgettext(
"magnifier",
# Translators: Message announced when zooming in with {zoomLevel} being the target zoom level.
"{zoomLevel}x",
)

@classmethod
def zoom_range(cls) -> list[float]:
"""
Return the list of available zoom levels.
"""
start = round(cls.MIN_ZOOM / cls.STEP_FACTOR)
end = round(cls.MAX_ZOOM / cls.STEP_FACTOR)

return [i * cls.STEP_FACTOR for i in range(start, end + 1)]

@classmethod
def zoom_strings(cls) -> list[str]:
"""
Return localized zoom level strings.
"""
return [
cls.ZOOM_MESSAGE.format(
zoomLevel=f"{value:.1f}",
)
for value in cls.zoom_range()
]


def getZoomLevel() -> float:
MAX_ZOOM: int = 5000
MIN_ZOOM: int = 100
STEP_FACTOR: int = 50

@staticmethod
def zoomMessage(zoomLevel: int) -> str:
zoomLevel = zoomLevel / 100.0
return pgettext(
"magnifier",
# Translators: Message announced when zooming in with {zoomLevel} being the target zoom level.
"{zoomLevel}x",
).format(zoomLevel=f"{zoomLevel:.1f}")


def getZoomLevel() -> int:
"""
Get zoom level from config.

:return: The zoom level.
:return: The zoom level (percentage).
"""
zoomLevel = config.conf["magnifier"]["zoomLevel"]
zoomLevel = config.conf["magnifier"]["zoom"]
return zoomLevel


Expand All @@ -85,22 +67,22 @@ def getZoomLevelString() -> str:
:return: Formatted zoom level string.
"""
zoomLevel = getZoomLevel()
zoomValues = ZoomLevel.zoom_range()
zoomStrings = ZoomLevel.zoom_strings()
closestIndex = min(
range(len(zoomValues)),
key=lambda i: abs(zoomValues[i] - zoomLevel),
)
return zoomStrings[closestIndex]
return ZoomLevel.zoomMessage(zoomLevel)


def setZoomLevel(zoomLevel: float) -> None:
def setZoomLevel(zoomLevel: int) -> None:
"""
Set zoom level from settings.

:param zoomLevel: The zoom level to set.
"""
config.conf["magnifier"]["zoomLevel"] = zoomLevel
if not isinstance(zoomLevel, int):
raise ValueError("Zoom level must be an integer percentage")
if not (ZoomLevel.MIN_ZOOM <= zoomLevel <= ZoomLevel.MAX_ZOOM):
raise ValueError(f"Zoom level must be between {ZoomLevel.MIN_ZOOM} and {ZoomLevel.MAX_ZOOM}")
if zoomLevel % ZoomLevel.STEP_FACTOR != 0:
raise ValueError(f"Zoom level must be a multiple of {ZoomLevel.STEP_FACTOR}")
config.conf["magnifier"]["zoom"] = zoomLevel


def getPanStep() -> int:
Expand Down
8 changes: 4 additions & 4 deletions source/_magnifier/fullscreenMagnifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def _fullscreenMagnifier(self, coordinates: Coordinates) -> None:
"""
params = self._getMagnifierParameters(coordinates)
magnification.MagSetFullscreenTransform(
self.zoomLevel,
self.zoomLevelRatio,
params.coordinates.x,
params.coordinates.y,
)
Expand Down Expand Up @@ -340,7 +340,7 @@ def _relativePos(
:return: The (x, y) coordinates of the magnifier center
"""

zoom = self.zoomLevel
zoom = self.zoomLevelRatio
mouseX, mouseY = coordinates
magnifierWidth = self._displayOrientation.width / zoom
magnifierHeight = self._displayOrientation.height / zoom
Expand Down Expand Up @@ -388,8 +388,8 @@ def _getMagnifierParameters(self, coordinates: Coordinates) -> MagnifierParamete
"""
x, y = coordinates
# Calculate the size of the capture area at the current zoom level
magnifierWidth = self._displayOrientation.width / self.zoomLevel
magnifierHeight = self._displayOrientation.height / self.zoomLevel
magnifierWidth = self._displayOrientation.width / self.zoomLevelRatio
magnifierHeight = self._displayOrientation.height / self.zoomLevelRatio

# Compute the top-left corner so that (x, y) is at the center
left = int(x - (magnifierWidth / 2))
Expand Down
33 changes: 20 additions & 13 deletions source/_magnifier/magnifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,31 @@ def filterType(self) -> Filter:
def filterType(self, value: Filter) -> None:
self._filterType = value

@property
def zoomLevelRatio(self) -> float:
"""Get the zoom level as a float (e.g., 2.0 for 200% zoom)"""
return self._zoomLevel / 100.0
Comment thread
seanbudd marked this conversation as resolved.

@property
def zoomLevel(self) -> float:
"""Get the zoom level as a percentage (e.g., 200 for 200% zoom)"""
return self._zoomLevel

@zoomLevel.setter
def zoomLevel(self, value: float) -> None:
def zoomLevel(self, value: int) -> None:
"""
Set zoom level, ensuring it's a valid value in the zoom range.
Comment thread
seanbudd marked this conversation as resolved.

:param value: The zoom level to set
:raises ValueError: If the value is not in the valid zoom range
"""
validZoomValues = ZoomLevel.zoom_range()
if value not in validZoomValues:
# Find the closest valid zoom value
closestZoom = min(validZoomValues, key=lambda x: abs(x - value))
log.warning(f"Invalid zoom level {value}, using closest valid value {closestZoom}")
value = closestZoom
self._zoomLevel = value
if not isinstance(value, int):
raise ValueError("Zoom level must be an integer percentage")
if not (ZoomLevel.MIN_ZOOM <= value <= ZoomLevel.MAX_ZOOM):
raise ValueError(f"Zoom level must be between {ZoomLevel.MIN_ZOOM} and {ZoomLevel.MAX_ZOOM}")
if value % ZoomLevel.STEP_FACTOR != 0:
raise ValueError(f"Zoom level must be a multiple of {ZoomLevel.STEP_FACTOR}")
Comment thread
seanbudd marked this conversation as resolved.
self._zoomLevel = float(value)

@property
def currentCoordinates(self) -> Coordinates:
Expand Down Expand Up @@ -119,8 +125,8 @@ def _getScreenLimits(self) -> tuple[int, int, int, int]:
return (0, 0, self._displayOrientation.width, self._displayOrientation.height)
else:
# In normal mode: calculate limits to keep view within screen
visibleWidth = self._displayOrientation.width / self.zoomLevel
visibleHeight = self._displayOrientation.height / self.zoomLevel
visibleWidth = self._displayOrientation.width / self.zoomLevelRatio
visibleHeight = self._displayOrientation.height / self.zoomLevelRatio
minX = int(visibleWidth / 2)
minY = int(visibleHeight / 2)
maxX = int(self._displayOrientation.width - (visibleWidth / 2))
Expand Down Expand Up @@ -148,6 +154,7 @@ def _setZoomRawValue(self, value: float) -> None:

:param value: The zoom level to set (can be any intermediate value)
"""
value = max(ZoomLevel.MIN_ZOOM, min(value, ZoomLevel.MAX_ZOOM))
self._zoomLevel = value

def _onDisplayChanged(self, orientationState: OrientationState) -> None:
Expand Down Expand Up @@ -293,11 +300,11 @@ def _zoom(self, direction: Direction) -> None:
:param direction: Direction.IN to zoom in, Direction.OUT to zoom out
"""
if direction == Direction.IN:
newZoom = self.zoomLevel + ZoomLevel.STEP_FACTOR
newZoom = int(self.zoomLevel + ZoomLevel.STEP_FACTOR)
if newZoom <= ZoomLevel.MAX_ZOOM:
self.zoomLevel = newZoom
elif direction == Direction.OUT:
newZoom = self.zoomLevel - ZoomLevel.STEP_FACTOR
newZoom = int(self.zoomLevel - ZoomLevel.STEP_FACTOR)
if newZoom >= ZoomLevel.MIN_ZOOM:
self.zoomLevel = newZoom

Expand All @@ -313,7 +320,7 @@ def _pan(self, action: MagnifierAction) -> bool:

minX, minY, maxX, maxY = self._getScreenLimits()

panPixels = int((self._displayOrientation.width / self.zoomLevel) * self._panStep / 100)
panPixels = int((self._displayOrientation.width / self.zoomLevelRatio) * self._panStep / 100)

match action:
case MagnifierAction.PAN_LEFT:
Expand Down
12 changes: 6 additions & 6 deletions source/_magnifier/utils/spotlightManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def __init__(
self._animationSteps: int = 40
self._animationStepDelay: int = 12
self._currentCoordinates: Coordinates = fullscreenMagnifier._focusManager.getCurrentFocusCoordinates()
self._originalZoomLevel: float = 0.0
self._originalZoomLevel: int = 0
self._currentZoomLevel: float = 0.0
self._originalMode: FullScreenMode | None = None

Expand Down Expand Up @@ -92,8 +92,8 @@ def _animateZoom(
)

self._animationStepsList = self._computeAnimationSteps(
self._currentZoomLevel,
target.zoomLevel,
round(self._currentZoomLevel),
round(target.zoomLevel),
self._currentCoordinates,
target.coordinates,
)
Expand Down Expand Up @@ -175,8 +175,8 @@ def zoomBack(self) -> None:

def _computeAnimationSteps(
self,
zoomStart: float,
zoomEnd: float,
zoomStart: int,
zoomEnd: int,
coordinateStart: Coordinates,
coordinateEnd: Coordinates,
) -> list[ZoomHistory]:
Expand All @@ -196,7 +196,7 @@ def _computeAnimationSteps(

startX, startY = coordinateStart
endX, endY = coordinateEnd
animationSteps = []
animationSteps: list[ZoomHistory] = []

zoomDelta = (zoomEnd - zoomStart) / self._animationSteps
coordDeltaX = (endX - startX) / self._animationSteps
Expand Down
2 changes: 1 addition & 1 deletion source/config/configSpec.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
[magnifier]
enabled = boolean(default=false)
magnifiedView = string(default="fullscreen")
zoomLevel = float(min=1.0, max=10.0, default=2.0)
zoom = integer(min=100, max=5000, default=200)
isTrueCentered = boolean(default=False)
filter = string(default="normal")
followMouse = boolean(default=True)
Expand Down
2 changes: 1 addition & 1 deletion source/config/profileUpgradeSteps.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2016-2025 NV Access Limited, Bill Dengler, Cyrille Bougot, Łukasz Golonka, Leonard de Ruijter, Cary-rowen
# Copyright (C) 2016-2026 NV Access Limited, Bill Dengler, Cyrille Bougot, Łukasz Golonka, Leonard de Ruijter, Cary-rowen
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

Expand Down
32 changes: 11 additions & 21 deletions source/gui/settingsDialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
# 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

import bisect
import copy
import logging
import math
Expand Down Expand Up @@ -6064,32 +6063,23 @@ def makeSettings(

# ZOOM SETTINGS
# Translators: The label for a setting in magnifier settings to select the zoom level.
zoomLabelText = _("&Zoom level:")
zoomLabelText = _("&Zoom (%):")

zoomValues = magnifierConfig.ZoomLevel.zoom_range()
zoomChoices = magnifierConfig.ZoomLevel.zoom_strings()

self.zoomList = sHelper.addLabeledControl(
self.zoomCtrl = sHelper.addLabeledControl(
zoomLabelText,
wx.Choice,
choices=zoomChoices,
wx.SpinCtrl,
min=magnifierConfig.ZoomLevel.MIN_ZOOM,
max=magnifierConfig.ZoomLevel.MAX_ZOOM,
)
self.zoomCtrl.SetIncrement(magnifierConfig.ZoomLevel.STEP_FACTOR)
self.bindHelpEvent(
"MagnifierZoom",
self.zoomList,
self.zoomCtrl,
)

# Set value from config
zoomLevel = magnifierConfig.getZoomLevel()
zoomIndex = bisect.bisect_left(zoomValues, zoomLevel)
# Find the closest value
if zoomIndex == 0:
closestIndex = 0
elif zoomIndex >= len(zoomValues):
closestIndex = len(zoomValues) - 1
else:
closestIndex = min(zoomIndex - 1, zoomIndex, key=lambda i: abs(zoomValues[i] - zoomLevel))
self.zoomList.SetSelection(closestIndex)
self.zoomCtrl.SetValue(zoomLevel)

# PAN SETTINGS
# Translators: The label for a setting in magnifier settings to select the pan step size (in percentage).
Expand All @@ -6102,7 +6092,7 @@ def makeSettings(
max=100,
)
self.bindHelpEvent(
"magnifierPanStep",
"MagnifierPanningStepSize",
self.panSpinCtrl,
)

Expand Down Expand Up @@ -6195,8 +6185,8 @@ 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])
selectedZoom = self.zoomCtrl.GetValue()
magnifierConfig.setZoomLevel(selectedZoom)

magnifierConfig.setPanStep(self.panSpinCtrl.GetValue())

Expand Down
Loading
Loading