diff --git a/source/_magnifier/commands.py b/source/_magnifier/commands.py index 95acc9073bb..1a50df3a719 100644 --- a/source/_magnifier/commands.py +++ b/source/_magnifier/commands.py @@ -12,9 +12,9 @@ import ui from . import getMagnifier, initialize, terminate from .config import ( - getDefaultZoomLevelString, - getDefaultFilter, - getDefaultFullscreenMode, + getZoomLevelString, + getFilter, + getFullscreenMode, ZoomLevel, getFollowState, setFollowState, @@ -104,8 +104,8 @@ def toggleMagnifier() -> None: else: initialize() - filter = getDefaultFilter() - fullscreenMode = getDefaultFullscreenMode() + filter = getFilter() + fullscreenMode = getFullscreenMode() ui.message( pgettext( @@ -113,7 +113,7 @@ def toggleMagnifier() -> None: # Translators: Message announced when starting the NVDA magnifier. "Starting magnifier with {zoomLevel} zoom level, {filter} filter, and {fullscreenMode} full-screen mode", ).format( - zoomLevel=getDefaultZoomLevelString(), + zoomLevel=getZoomLevelString(), filter=filter.displayString, fullscreenMode=fullscreenMode.displayString, ), diff --git a/source/_magnifier/config.py b/source/_magnifier/config.py index e8ed684fb25..e5abe6b8d96 100644 --- a/source/_magnifier/config.py +++ b/source/_magnifier/config.py @@ -50,72 +50,75 @@ def zoom_strings(cls) -> list[str]: ] -def getDefaultZoomLevel() -> float: +def getZoomLevel() -> float: """ - Get default zoom level from config. + Get zoom level from config. - :return: The default zoom level. + :return: The zoom level. """ - zoomLevel = config.conf["magnifier"]["defaultZoomLevel"] + zoomLevel = config.conf["magnifier"]["zoomLevel"] return zoomLevel -def getDefaultZoomLevelString() -> str: +def getZoomLevelString() -> str: """ - Get default zoom level as a formatted string. + Get zoom level as a formatted string. :return: Formatted zoom level string. """ - zoomLevel = getDefaultZoomLevel() + zoomLevel = getZoomLevel() zoomValues = ZoomLevel.zoom_range() zoomStrings = ZoomLevel.zoom_strings() - zoomIndex = zoomValues.index(zoomLevel) - return zoomStrings[zoomIndex] + closestIndex = min( + range(len(zoomValues)), + key=lambda i: abs(zoomValues[i] - zoomLevel), + ) + return zoomStrings[closestIndex] -def setDefaultZoomLevel(zoomLevel: float) -> None: +def setZoomLevel(zoomLevel: float) -> None: """ - Set default zoom level from settings. + Set zoom level from settings. :param zoomLevel: The zoom level to set. """ - config.conf["magnifier"]["defaultZoomLevel"] = zoomLevel + config.conf["magnifier"]["zoomLevel"] = zoomLevel -def getDefaultPanStep() -> int: +def getPanStep() -> int: """ - Get default pan value from config. + Get pan value from config. - :return: The default pan value. + :return: The pan value. """ - return config.conf["magnifier"]["defaultPanStep"] + return config.conf["magnifier"]["panStep"] -def setDefaultPanStep(panStep: int) -> None: +def setPanStep(panStep: int) -> None: """ - Set default pan value from settings. + Set pan value from settings. :param panStep: The pan value to set. """ - config.conf["magnifier"]["defaultPanStep"] = panStep + config.conf["magnifier"]["panStep"] = panStep -def getDefaultFilter() -> Filter: +def getFilter() -> Filter: """ - Get default filter from config. + Get filter from config. - :return: The default filter. + :return: The filter. """ - return Filter(config.conf["magnifier"]["defaultFilter"]) + return Filter(config.conf["magnifier"]["filter"]) -def setDefaultFilter(filter: Filter) -> None: +def setFilter(filter: Filter) -> None: """ - Set default filter from settings. + Set filter from settings. :param filter: The filter to set. """ - config.conf["magnifier"]["defaultFilter"] = filter.value + config.conf["magnifier"]["filter"] = filter.value _FOLLOW_CONFIG_KEYS: dict[MagnifierFollowFocusType, str] = { @@ -223,3 +226,21 @@ def shouldKeepMouseCentered() -> bool: :return: True if mouse should be kept centered, False otherwise. """ return config.conf["magnifier"]["keepMouseCentered"] + + +def getFullscreenMode() -> FullScreenMode: + """ + Get full-screen mode from config. + + :return: The full-screen mode. + """ + return FullScreenMode(config.conf["magnifier"]["fullscreenMode"]) + + +def setFullscreenMode(mode: FullScreenMode) -> None: + """ + Set full-screen mode from settings. + + :param mode: The full-screen mode to set. + """ + config.conf["magnifier"]["fullscreenMode"] = mode.value diff --git a/source/_magnifier/fullscreenMagnifier.py b/source/_magnifier/fullscreenMagnifier.py index d8df85dfeaa..3be03c6212c 100644 --- a/source/_magnifier/fullscreenMagnifier.py +++ b/source/_magnifier/fullscreenMagnifier.py @@ -15,17 +15,26 @@ from .magnifier import Magnifier from .utils.filterHandler import FilterMatrix from .utils.spotlightManager import SpotlightManager -from .utils.types import Filter, Coordinates, FullScreenMode -from .config import getDefaultFullscreenMode +from .utils.types import ( + Filter, + MagnifierType, + FullScreenMode, + Size, + MagnifierParameters, + Coordinates, +) +from .config import getFullscreenMode, isTrueCentered from .utils.errorHandling import trackNativeMagnifierErrors class FullScreenMagnifier(Magnifier): def __init__(self): super().__init__() - self._fullscreenMode = getDefaultFullscreenMode() + self._magnifierType = MagnifierType.FULLSCREEN + self._fullscreenMode = getFullscreenMode() self._currentCoordinates = Coordinates(0, 0) self._spotlightManager = SpotlightManager(self) + self._displaySize = Size(self._displayOrientation.width, self._displayOrientation.height) self._startMagnifier() @property @@ -193,11 +202,11 @@ def _fullscreenMagnifier(self, coordinates: Coordinates) -> None: :coordinates: The (x, y) coordinates to center the magnifier on """ - left, top, visibleWidth, visibleHeight = self._getMagnifierPosition(coordinates) + params = self._getMagnifierParameters(coordinates) magnification.MagSetFullscreenTransform( self.zoomLevel, - left, - top, + params.coordinates.x, + params.coordinates.y, ) def _getCoordinatesForMode( @@ -231,11 +240,11 @@ def _keepMouseCentered(self) -> None: ): log.debug("Mouse button pressed, skipping cursor repositioning to avoid interfering with click") return - coords = self._getCoordinatesForMode(self._currentCoordinates) - left, top, visibleWidth, visibleHeight = self._getMagnifierPosition(coords) - centerX = left + visibleWidth // 2 - centerY = top + visibleHeight // 2 - self._setCursorToCenter(centerX, centerY) + coordinates = self._getCoordinatesForMode(self._currentCoordinates) + params = self._getMagnifierParameters(coordinates) + centerX = params.coordinates.x + params.magnifierSize.width // 2 + centerY = params.coordinates.y + params.magnifierSize.height // 2 + winUser.setCursorPos(centerX, centerY) @trackNativeMagnifierErrors def _setCursorToCenter(self, x: int, y: int) -> None: @@ -259,14 +268,16 @@ def _borderPos( :return: The adjusted position (x, y) of the focus point """ focusX, focusY = coordinates - lastLeft, lastTop, visibleWidth, visibleHeight = self._getMagnifierPosition( - self._lastScreenPosition, - ) + params = self._getMagnifierParameters(self._lastScreenPosition) + magnifierWidth = params.magnifierSize.width + magnifierHeight = params.magnifierSize.height + lastLeft = params.coordinates.x + lastTop = params.coordinates.y minX = lastLeft + self._MARGIN_BORDER - maxX = lastLeft + visibleWidth - self._MARGIN_BORDER + maxX = lastLeft + magnifierWidth - self._MARGIN_BORDER minY = lastTop + self._MARGIN_BORDER - maxY = lastTop + visibleHeight - self._MARGIN_BORDER + maxY = lastTop + magnifierHeight - self._MARGIN_BORDER dx = 0 dy = 0 @@ -283,8 +294,8 @@ def _borderPos( if dx != 0 or dy != 0: return Coordinates( - self._lastScreenPosition[0] + dx, - self._lastScreenPosition[1] + dy, + self._lastScreenPosition.x + dx, + self._lastScreenPosition.y + dy, ) else: return self._lastScreenPosition @@ -304,21 +315,21 @@ def _relativePos( zoom = self.zoomLevel mouseX, mouseY = coordinates - visibleWidth = self._displayOrientation.width / zoom - visibleHeight = self._displayOrientation.height / zoom + magnifierWidth = self._displayOrientation.width / zoom + magnifierHeight = self._displayOrientation.height / zoom margin = int(zoom * 10) # Calculate left/top maintaining mouse relative position - left = mouseX - (mouseX / self._displayOrientation.width) * (visibleWidth - margin) - top = mouseY - (mouseY / self._displayOrientation.height) * (visibleHeight - margin) + left = mouseX - (mouseX / self._displayOrientation.width) * (magnifierWidth - margin) + top = mouseY - (mouseY / self._displayOrientation.height) * (magnifierHeight - margin) # Clamp to screen boundaries - left = max(0, min(left, self._displayOrientation.width - visibleWidth)) - top = max(0, min(top, self._displayOrientation.height - visibleHeight)) + left = max(0, min(left, self._displayOrientation.width - magnifierWidth)) + top = max(0, min(top, self._displayOrientation.height - magnifierHeight)) # Return center of zoom window - centerX = int(left + visibleWidth / 2) - centerY = int(top + visibleHeight / 2) + centerX = int(left + magnifierWidth / 2) + centerY = int(top + magnifierHeight / 2) self._lastScreenPosition = Coordinates(centerX, centerY) return self._lastScreenPosition @@ -338,3 +349,31 @@ def _stopSpotlight(self) -> None: """ self._spotlightManager._spotlightIsActive = False self._startTimer(self._updateMagnifier) + + def _getMagnifierParameters(self, coordinates: Coordinates) -> MagnifierParameters: + """ + Compute the top-left corner of the magnifier window centered on (x, y) + + :param coordinates: The (x, y) coordinates to center the magnifier on + + :return: The size, position and filter of the magnifier window + """ + 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 + + # Compute the top-left corner so that (x, y) is at the center + left = int(x - (magnifierWidth / 2)) + top = int(y - (magnifierHeight / 2)) + + # Clamp to screen boundaries only if not in true center mode + if not isTrueCentered(): + left = max(0, min(left, int(self._displayOrientation.width - magnifierWidth))) + top = max(0, min(top, int(self._displayOrientation.height - magnifierHeight))) + + return MagnifierParameters( + Size(int(magnifierWidth), int(magnifierHeight)), + Coordinates(left, top), + self._filterType, + ) diff --git a/source/_magnifier/magnifier.py b/source/_magnifier/magnifier.py index a050c6d002d..5e5184642c6 100644 --- a/source/_magnifier/magnifier.py +++ b/source/_magnifier/magnifier.py @@ -19,17 +19,17 @@ from winAPI import _displayTracking from winAPI._displayTracking import OrientationState, getPrimaryDisplayOrientation from .utils.types import ( - MagnifierPosition, + MagnifierParameters, MagnifierAction, - Coordinates, MagnifierType, Direction, Filter, + Coordinates, ) from .config import ( - getDefaultZoomLevel, - getDefaultPanStep, - getDefaultFilter, + getZoomLevel, + getPanStep, + getFilter, ZoomLevel, isTrueCentered, shouldKeepMouseCentered, @@ -43,16 +43,16 @@ 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() + self._zoomLevel: float = getZoomLevel() + self._panStep: int = getPanStep() self._timer: None | wx.Timer = None self._focusManager = FocusManager() self._lastScreenPosition = Coordinates(0, 0) self._currentCoordinates = Coordinates(0, 0) self._lastFocusCoordinates = Coordinates(0, 0) - self._filterType: Filter = getDefaultFilter() + self._filterType: Filter = getFilter() self._isManualPanning: bool = False self._consecutiveErrors: int = 0 # Register for display changes @@ -343,29 +343,12 @@ def _stopTimer(self) -> None: else: log.debug("no timer to stop") - def _getMagnifierPosition( - self, - coordinates: Coordinates, - ) -> MagnifierPosition: + def _getMagnifierParameters(self, coordinates: Coordinates) -> MagnifierParameters: """ Compute the top-left corner of the magnifier window centered on (x, y) :param coordinates: The (x, y) coordinates to center the magnifier on - :return: The position and size of the magnifier window + :return: The size, position and filter of the magnifier window """ - x, y = coordinates - # Calculate the size of the capture area at the current zoom level - visibleWidth = self._displayOrientation.width / self.zoomLevel - visibleHeight = self._displayOrientation.height / self.zoomLevel - - # Compute the top-left corner so that (x, y) is at the center - left = int(x - (visibleWidth / 2)) - top = int(y - (visibleHeight / 2)) - - # Clamp to screen boundaries only if not in true center mode - if not isTrueCentered(): - left = max(0, min(left, int(self._displayOrientation.width - visibleWidth))) - top = max(0, min(top, int(self._displayOrientation.height - visibleHeight))) - - return MagnifierPosition(left, top, int(visibleWidth), int(visibleHeight)) + raise NotImplementedError("Subclasses must implement this method") diff --git a/source/_magnifier/utils/types.py b/source/_magnifier/utils/types.py index 2d5fcf7c7b2..1afc75941bf 100644 --- a/source/_magnifier/utils/types.py +++ b/source/_magnifier/utils/types.py @@ -12,12 +12,12 @@ from utils.displayString import DisplayStringStrEnum, DisplayStringEnum -class MagnifierParams(NamedTuple): - """Named tuple representing magnifier parameters for initialization""" +class MagnifierParameters(NamedTuple): + """Named tuple representing the size, position and filter of the magnifier""" - zoomLevel: float - filter: str - fullscreenMode: str + magnifierSize: "Size" + coordinates: "Coordinates" + filter: "Filter" class Direction(Enum): @@ -27,6 +27,13 @@ class Direction(Enum): OUT = False +class Size(NamedTuple): + """Named tuple representing width and height""" + + width: int + height: int + + class MagnifierAction(DisplayStringEnum): """Actions that can be performed with the magnifier""" @@ -105,6 +112,7 @@ class MagnifierType(DisplayStringStrEnum): """Type of magnifier""" FULLSCREEN = "fullscreen" + FIXED = "fixed" DOCKED = "docked" LENS = "lens" @@ -113,6 +121,8 @@ 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. @@ -120,15 +130,6 @@ def _displayStringLabels(self) -> dict["MagnifierType", str]: } -class MagnifierPosition(NamedTuple): - """Named tuple representing the position and size of the magnifier window""" - - left: int - top: int - visibleWidth: int - visibleHeight: int - - class Coordinates(NamedTuple): """Named tuple representing x and y coordinates""" diff --git a/source/config/configSpec.py b/source/config/configSpec.py index d31d666e897..982532d4d5f 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -116,17 +116,16 @@ # Magnifier settings [magnifier] - defaultZoomLevel = float(min=1.0, max=10.0, default=2.0) + zoomLevel = float(min=1.0, max=10.0, default=2.0) isTrueCentered = boolean(default=False) - defaultFilter = string(default="normal") + filter = string(default="normal") followMouse = boolean(default=True) followSystemFocus = boolean(default=True) followReviewCursor = boolean(default=True) followNavigatorObject = boolean(default=True) - defaultPanStep = integer(min=1, max=100, default=10) - defaultFullscreenMode = string(default="center") + panStep = integer(min=1, max=100, default=10) + fullscreenMode = string(default="center") keepMouseCentered = boolean(default=false) - saveShortcutChanges = boolean(default=false) # Presentation settings [presentation] diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index da624705d3e..c549a6ad38c 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -6036,39 +6036,39 @@ def makeSettings( ) # ZOOM SETTINGS - # Translators: The label for a setting in magnifier settings to select the default zoom level. - defaultZoomLabelText = _("Default &zoom level:") + # Translators: The label for a setting in magnifier settings to select the zoom level. + zoomLabelText = _("&Zoom level:") zoomValues = magnifierConfig.ZoomLevel.zoom_range() zoomChoices = magnifierConfig.ZoomLevel.zoom_strings() - self.defaultZoomList = sHelper.addLabeledControl( - defaultZoomLabelText, + self.zoomList = sHelper.addLabeledControl( + zoomLabelText, wx.Choice, choices=zoomChoices, ) self.bindHelpEvent( - "MagnifierDefaultZoom", - self.defaultZoomList, + "MagnifierZoom", + self.zoomList, ) - # Set default value from config - defaultZoom = magnifierConfig.getDefaultZoomLevel() - zoomIndex = bisect.bisect_left(zoomValues, defaultZoom) + # 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] - defaultZoom)) - self.defaultZoomList.SetSelection(closestIndex) + closestIndex = min(zoomIndex - 1, zoomIndex, key=lambda i: abs(zoomValues[i] - zoomLevel)) + self.zoomList.SetSelection(closestIndex) # PAN SETTINGS # Translators: The label for a setting in magnifier settings to select the pan step size (in percentage). panStepSizeLabelText = _("&Panning step size (%):") - self.defaultPanSpinCtrl = sHelper.addLabeledControl( + self.panSpinCtrl = sHelper.addLabeledControl( panStepSizeLabelText, wx.SpinCtrl, min=1, @@ -6076,40 +6076,40 @@ def makeSettings( ) self.bindHelpEvent( "magnifierPanStep", - self.defaultPanSpinCtrl, + self.panSpinCtrl, ) - # Set default value from config - defaultPan = magnifierConfig.getDefaultPanStep() - self.defaultPanSpinCtrl.SetValue(defaultPan) + # Set value from config + panStep = magnifierConfig.getPanStep() + self.panSpinCtrl.SetValue(panStep) # FILTER SETTINGS # Translators: The label for a setting in magnifier settings to select the default filter - defaultFilterLabelText = _("Default &filter:") + filterLabelText = _("&filter:") filterChoices = [f.displayString for f in Filter] - self.defaultFilterList = sHelper.addLabeledControl( - defaultFilterLabelText, + self.filterList = sHelper.addLabeledControl( + filterLabelText, wx.Choice, choices=filterChoices, ) - self.bindHelpEvent("MagnifierDefaultFilter", self.defaultFilterList) + self.bindHelpEvent("MagnifierFilter", self.filterList) - # Set default value from config - defaultFilter = magnifierConfig.getDefaultFilter() - self.defaultFilterList.SetSelection(list(Filter).index(defaultFilter)) + # Set value from config + filterValue = magnifierConfig.getFilter() + self.filterList.SetSelection(list(Filter).index(filterValue)) # FULLSCREEN MODE SETTINGS - # Translators: The label for a setting in magnifier settings to select the default full-screen mode - defaultFullscreenModeLabelText = _("Default &fullscreen mode:") + # Translators: The label for a setting in magnifier settings to select the full-screen mode + fullscreenModeLabelText = _("&fullscreen mode:") fullscreenModeChoices = [mode.displayString for mode in FullScreenMode] if FullScreenMode else [] - self.defaultFullscreenModeList = sHelper.addLabeledControl( - defaultFullscreenModeLabelText, + self.fullscreenModeList = sHelper.addLabeledControl( + fullscreenModeLabelText, wx.Choice, choices=fullscreenModeChoices, ) self.bindHelpEvent( - "MagnifierDefaultFullscreenFocusMode", - self.defaultFullscreenModeList, + "MagnifierFullscreenFocusMode", + self.fullscreenModeList, ) # TRUE CENTER @@ -6123,8 +6123,8 @@ def makeSettings( self.trueCenterCheckBox.SetValue(magnifierConfig.isTrueCentered()) # Set default value from config - defaultFullscreenMode = magnifierConfig.getDefaultFullscreenMode() - self.defaultFullscreenModeList.SetSelection(list(FullScreenMode).index(defaultFullscreenMode)) + defaultFullscreenMode = magnifierConfig.getFullscreenMode() + self.fullscreenModeList.SetSelection(list(FullScreenMode).index(defaultFullscreenMode)) # FOCUS GROUP # Translators: This is the label for a group of focus options in the magnifier settings panel @@ -6166,16 +6166,16 @@ def makeSettings( def onSave(self): """Save the current selections to config.""" - selectedZoom = self.defaultZoomList.GetSelection() - magnifierConfig.setDefaultZoomLevel(magnifierConfig.ZoomLevel.zoom_range()[selectedZoom]) + selectedZoom = self.zoomList.GetSelection() + magnifierConfig.setZoomLevel(magnifierConfig.ZoomLevel.zoom_range()[selectedZoom]) - magnifierConfig.setDefaultPanStep(self.defaultPanSpinCtrl.GetValue()) + magnifierConfig.setPanStep(self.panSpinCtrl.GetValue()) - selectedFilterIdx = self.defaultFilterList.GetSelection() - magnifierConfig.setDefaultFilter(list(Filter)[selectedFilterIdx]) + selectedFilterIdx = self.filterList.GetSelection() + magnifierConfig.setFilter(list(Filter)[selectedFilterIdx]) - selectedModeIdx = self.defaultFullscreenModeList.GetSelection() - magnifierConfig.setDefaultFullscreenMode(list(FullScreenMode)[selectedModeIdx]) + selectedModeIdx = self.fullscreenModeList.GetSelection() + magnifierConfig.setFullscreenMode(list(FullScreenMode)[selectedModeIdx]) config.conf["magnifier"]["isTrueCentered"] = self.trueCenterCheckBox.GetValue() for focusType, checkBox in self._followFocusCheckBoxes.items(): diff --git a/tests/unit/test_magnifier/test_focusManager.py b/tests/unit/test_magnifier/test_focusManager.py index 3e1e4b64051..a96e3f14781 100644 --- a/tests/unit/test_magnifier/test_focusManager.py +++ b/tests/unit/test_magnifier/test_focusManager.py @@ -183,7 +183,7 @@ def testGetCurrentFocusCoordinates(self): FocusTestParam( navigatorObjectPos=Coordinates(0, 0), systemFocusPos=Coordinates(0, 0), - mousePos=(0, 0), + mousePos=Coordinates(0, 0), leftPressed=True, expectedCoords=Coordinates(0, 0), expectedFocus=MagnifierFollowFocusType.MOUSE, @@ -192,7 +192,7 @@ def testGetCurrentFocusCoordinates(self): FocusTestParam( navigatorObjectPos=Coordinates(0, 0), systemFocusPos=Coordinates(0, 0), - mousePos=(10, 10), + mousePos=Coordinates(10, 10), leftPressed=False, expectedCoords=Coordinates(10, 10), expectedFocus=MagnifierFollowFocusType.MOUSE, @@ -201,7 +201,7 @@ def testGetCurrentFocusCoordinates(self): FocusTestParam( navigatorObjectPos=Coordinates(0, 0), systemFocusPos=Coordinates(15, 15), - mousePos=(0, 0), + mousePos=Coordinates(0, 0), leftPressed=False, expectedCoords=Coordinates(15, 15), expectedFocus=MagnifierFollowFocusType.SYSTEM_FOCUS, @@ -210,7 +210,7 @@ def testGetCurrentFocusCoordinates(self): FocusTestParam( navigatorObjectPos=Coordinates(20, 20), systemFocusPos=Coordinates(0, 0), - mousePos=(0, 0), + mousePos=Coordinates(0, 0), leftPressed=False, expectedCoords=Coordinates(20, 20), expectedFocus=MagnifierFollowFocusType.NAVIGATOR_OBJECT, @@ -219,7 +219,7 @@ def testGetCurrentFocusCoordinates(self): FocusTestParam( navigatorObjectPos=Coordinates(30, 30), systemFocusPos=Coordinates(15, 15), - mousePos=(0, 0), + mousePos=Coordinates(0, 0), leftPressed=False, expectedCoords=Coordinates(30, 30), expectedFocus=MagnifierFollowFocusType.NAVIGATOR_OBJECT, @@ -228,7 +228,7 @@ def testGetCurrentFocusCoordinates(self): FocusTestParam( navigatorObjectPos=Coordinates(0, 0), systemFocusPos=Coordinates(0, 0), - mousePos=(0, 0), + mousePos=Coordinates(0, 0), leftPressed=False, reviewPos=Coordinates(30, 30), expectedCoords=Coordinates(30, 30), @@ -259,7 +259,7 @@ def testGetCurrentFocusCoordinates(self): FocusTestParam( navigatorObjectPos=Coordinates(0, 0), systemFocusPos=Coordinates(0, 0), - mousePos=(0, 0), + mousePos=Coordinates(0, 0), leftPressed=False, expectedCoords=Coordinates(0, 0), expectedFocus=MagnifierFollowFocusType.MOUSE, @@ -290,7 +290,7 @@ def testGetCurrentFocusCoordinates(self): FocusTestParam( navigatorObjectPos=Coordinates(10, 10), systemFocusPos=Coordinates(0, 0), - mousePos=(20, 20), + mousePos=Coordinates(20, 20), leftPressed=False, expectedCoords=Coordinates(20, 20), expectedFocus=MagnifierFollowFocusType.MOUSE, @@ -299,7 +299,7 @@ def testGetCurrentFocusCoordinates(self): FocusTestParam( navigatorObjectPos=Coordinates(10, 10), systemFocusPos=Coordinates(15, 15), - mousePos=(20, 20), + mousePos=Coordinates(20, 20), leftPressed=True, expectedCoords=Coordinates(20, 20), expectedFocus=MagnifierFollowFocusType.MOUSE, diff --git a/tests/unit/test_magnifier/test_fullscreenMagnifier.py b/tests/unit/test_magnifier/test_fullscreenMagnifier.py index 23546c00e73..36111b9f232 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() @@ -104,20 +109,20 @@ def testMagnifierPositionCalculation(self): magnifier = FullScreenMagnifier() # Test position calculation - left, top, width, height = magnifier._getMagnifierPosition((500, 400)) + params = magnifier._getMagnifierParameters((500, 400)) # Basic checks - self.assertIsInstance(left, int) - self.assertIsInstance(top, int) - self.assertIsInstance(width, int) - self.assertIsInstance(height, int) + self.assertIsInstance(params.coordinates.x, int) + self.assertIsInstance(params.coordinates.y, int) + self.assertIsInstance(params.magnifierSize.width, int) + self.assertIsInstance(params.magnifierSize.height, int) # Width and height should be screen size divided by zoom expectedWidth = int(magnifier._displayOrientation.width / 2.0) expectedHeight = int(magnifier._displayOrientation.height / 2.0) - self.assertEqual(width, expectedWidth) - self.assertEqual(height, expectedHeight) + self.assertEqual(params.magnifierSize.width, expectedWidth) + self.assertEqual(params.magnifierSize.height, expectedHeight) # Cleanup magnifier._stopMagnifier() @@ -190,6 +195,7 @@ def testMagnifierSimpleLifecycle(self): """Test simple magnifier lifecycle.""" # Create magnifier magnifier = FullScreenMagnifier() + magnifier._startMagnifier() self.assertTrue(magnifier._isActive) self.assertEqual(magnifier.zoomLevel, 2.0) @@ -284,8 +290,11 @@ def tearDown(self): def _expectedCenter(self, rawCoords: Coordinates) -> tuple[int, int]: """Compute the expected cursor position using the same pipeline as _keepMouseCentered.""" coords = self.magnifier._getCoordinatesForMode(rawCoords) - left, top, w, h = self.magnifier._getMagnifierPosition(coords) - return left + w // 2, top + h // 2 + params = self.magnifier._getMagnifierParameters(coords) + return ( + params.coordinates.x + params.magnifierSize.width // 2, + params.coordinates.y + params.magnifierSize.height // 2, + ) def testSkipsWhenLeftButtonPressed(self): """Cursor is not moved when the left mouse button is held.""" diff --git a/tests/unit/test_magnifier/test_magnifier.py b/tests/unit/test_magnifier/test_magnifier.py index 73d978ca085..270fa8f43c6 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 from comtypes import COMError import unittest @@ -65,7 +65,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) self.assertEqual(self.magnifier._consecutiveErrors, 0) @@ -498,50 +497,3 @@ def testStopTimer(self): # Test stopping when no timer exists (should not raise error) self.magnifier._stopTimer() self.assertIsNone(self.magnifier._timer) - - def testMagnifierPosition(self): - """Computing magnifier position and size.""" - x, y = int(self.screenWidth / 2), int(self.screenHeight / 2) - left, top, width, height = self.magnifier._getMagnifierPosition((x, y)) - - expected_width = int(self.screenWidth / self.magnifier.zoomLevel) - expected_height = int(self.screenHeight / self.magnifier.zoomLevel) - expected_left = int(x - (expected_width / 2)) - expected_top = int(y - (expected_height / 2)) - - self.assertEqual(left, expected_left) - self.assertEqual(top, expected_top) - self.assertEqual(width, expected_width) - self.assertEqual(height, expected_height) - - # Test left clamping - left, top, width, height = self.magnifier._getMagnifierPosition((100, 540)) - self.assertGreaterEqual(left, 0) - - # Test right clamping - left, top, width, height = self.magnifier._getMagnifierPosition((1800, 540)) - self.assertLessEqual(left + width, self.screenWidth) - - # Test different zoom level - self.magnifier.zoomLevel = 4.0 - left, top, width, height = self.magnifier._getMagnifierPosition((960, 540)) - expected_width = int(self.screenWidth / self.magnifier.zoomLevel) - expected_height = int(self.screenHeight / self.magnifier.zoomLevel) - self.assertEqual(width, expected_width) - self.assertEqual(height, expected_height) - - def testMagnifierPositionTrueCentered(self): - """Test magnifier position calculation with true centered mode.""" - x, y = int(self.screenWidth / 2), int(self.screenHeight / 2) - with patch("source._magnifier.magnifier.isTrueCentered", return_value=True): - left, top, width, height = self.magnifier._getMagnifierPosition((x, y)) - - expected_width = int(self.screenWidth / self.magnifier.zoomLevel) - expected_height = int(self.screenHeight / self.magnifier.zoomLevel) - expected_left = int(x - (expected_width / 2)) - expected_top = int(y - (expected_height / 2)) - - self.assertEqual(left, expected_left) - self.assertEqual(top, expected_top) - self.assertEqual(width, expected_width) - self.assertEqual(height, expected_height) diff --git a/user_docs/en/userGuide.md b/user_docs/en/userGuide.md index fd0e5a4f80d..26151130228 100644 --- a/user_docs/en/userGuide.md +++ b/user_docs/en/userGuide.md @@ -2871,9 +2871,9 @@ 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: -##### Default zoom level {#MagnifierDefaultZoom} +##### Zoom level {#MagnifierZoom} -This slider allows you to set the default zoom level when the magnifier is first enabled. +This slider allows you to set the zoom level when using the magnifier. The zoom level can range from 1.0 (no magnification) to 10.0 (maximum magnification). The default value is 2.0 (200% zoom). @@ -2884,9 +2884,9 @@ You can always adjust the zoom level on the fly using the zoom in (`NVDA+shift+e |Options |1.0 to 10.0| |Default |2.0| -##### Default color filter {#MagnifierDefaultFilter} +##### Filter {#MagnifierFilter} -This combo box allows you to select the default color filter to apply when the magnifier is first enabled. +This combo box allows you to select the filter to apply when using the magnifier. You can cycle through the color filters by pressing `NVDA+shift+i`. The available options are: @@ -2902,9 +2902,9 @@ The available options are: | Grayscale | Converts all colors to shades of gray, which can help reduce eye strain and improve contrast. | | Inverted | Inverts all colors on the screen, which can be helpful for users who prefer light text on dark backgrounds or have photophobia. | -##### Default focus mode {#MagnifierDefaultFullscreenFocusMode} +##### Focus mode {#MagnifierFullscreenFocusMode} -This combo box allows you to select the default focus tracking mode when the magnifier is first enabled. +This combo box allows you to select the focus tracking mode when using the magnifier. To cycle through the focus tracking modes, please assign a custom gesture using the [Input Gestures dialog](#InputGestures). The available options are: