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..cadb3faa8be 100644 --- a/source/_magnifier/commands.py +++ b/source/_magnifier/commands.py @@ -10,12 +10,17 @@ from typing import Literal import ui -from . import getMagnifier, initialize, terminate +from . import getMagnifier, initialize, changeMagnifierType, terminate from .config import ( - getDefaultZoomLevelString, - getDefaultFilter, - getDefaultFullscreenMode, ZoomLevel, + getZoomLevelString, + setZoomLevel, + getFilter, + setFilter, + getMagnifierType, + setMagnifierType, + getFullscreenMode, + setFullscreenMode, ) from .magnifier import Magnifier from .fullscreenMagnifier import FullScreenMagnifier @@ -100,20 +105,34 @@ def toggleMagnifier() -> None: else: initialize() - filter = getDefaultFilter() - fullscreenMode = getDefaultFullscreenMode() - - ui.message( - pgettext( - "magnifier", - # Translators: Message announced when starting the NVDA magnifier. - "Starting magnifier with {zoomLevel} zoom level, {filter} filter, and {fullscreenMode} full-screen mode", - ).format( - zoomLevel=getDefaultZoomLevelString(), - filter=filter.displayString, - fullscreenMode=fullscreenMode.displayString, - ), - ) + filter = getFilter() + magnifierType = getMagnifierType() + if magnifierType == MagnifierType.FULLSCREEN: + fullscreenMode = getFullscreenMode() + ui.message( + pgettext( + "magnifier", + # Translators: Message announced when starting the NVDA magnifier. + "Starting fullscreen magnifier with {zoomLevel} zoom level, {filter} filter, and {fullscreenMode} full-screen mode", + ).format( + magnifierType=magnifierType.displayString, + zoomLevel=getZoomLevelString(), + filter=filter.displayString, + fullscreenMode=fullscreenMode.displayString, + ), + ) + else: + ui.message( + 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=getZoomLevelString(), + filter=filter.displayString, + ), + ) def zoom(direction: Direction) -> None: @@ -126,6 +145,7 @@ def zoom(direction: Direction) -> None: magnifier: Magnifier = getMagnifier() if magnifierIsActiveVerify(magnifier, action): magnifier._zoom(direction) + setZoomLevel(magnifier.zoomLevel) ui.message( ZoomLevel.ZOOM_MESSAGE.format( zoomLevel=f"{magnifier.zoomLevel:.1f}", @@ -153,7 +173,31 @@ def pan(action: MagnifierAction) -> None: ) -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 cycleFilter() -> None: """Cycle through color filters""" magnifier: Magnifier = getMagnifier() log.debug(f"Toggling filter for magnifier: {magnifier}") @@ -166,6 +210,7 @@ def toggleFilter() -> None: magnifier.filterType = filters[(idx + 1) % len(filters)] if magnifier._magnifierType == MagnifierType.FULLSCREEN: magnifier._applyFilter() + setFilter(magnifier.filterType) ui.message( pgettext( "magnifier", @@ -175,7 +220,7 @@ def toggleFilter() -> None: ) -def toggleFullscreenMode() -> None: +def cycleFullscreenMode() -> None: """Cycle through full-screen focus modes (center, border, relative)""" magnifier: Magnifier = getMagnifier() if magnifierIsActiveVerify( @@ -192,6 +237,7 @@ def toggleFullscreenMode() -> None: newMode = modes[(idx + 1) % len(modes)] log.debug(f"Changing full-screen mode from {currentMode} to {newMode}") magnifier._fullscreenMode = newMode + setFullscreenMode(newMode) ui.message( pgettext( "magnifier", diff --git a/source/_magnifier/config.py b/source/_magnifier/config.py index 7fc38ba6ab5..ba16bac673f 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: @@ -49,96 +49,99 @@ 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] -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. """ if "magnifier" not in config.conf: config.conf["magnifier"] = {} - config.conf["magnifier"]["defaultZoomLevel"] = zoomLevel + config.conf["magnifier"]["zoomLevel"] = zoomLevel -def getDefaultPanStep() -> int: +def getMagnifierType() -> MagnifierType: """ - Get default pan value from config. + Get magnifier type from config. - :return: The default pan value. + :return: The magnifier type. """ - return config.conf["magnifier"]["defaultPanStep"] + return MagnifierType(config.conf["magnifier"]["magnifierType"]) -def setDefaultPanStep(panStep: int) -> None: +def setMagnifierType(magnifierType: MagnifierType) -> None: """ - Set default pan value from settings. + Set magnifier type in config. - :param panStep: The pan value to set. + :param magnifierType: The magnifier type to set. """ if "magnifier" not in config.conf: config.conf["magnifier"] = {} - config.conf["magnifier"]["defaultPanStep"] = panStep + config.conf["magnifier"]["magnifierType"] = magnifierType.value -def getDefaultFilter() -> Filter: +def getPanStep() -> int: """ - Get default filter from config. + Get pan value from config. - :return: The default filter. + :return: The pan value. """ - return Filter(config.conf["magnifier"]["defaultFilter"]) + return config.conf["magnifier"]["panStep"] -def setDefaultFilter(filter: Filter) -> None: +def setPanStep(panStep: int) -> None: """ - Set default filter from settings. + Set pan value from settings. - :param filter: The filter to set. + :param panStep: The pan value to set. """ - config.conf["magnifier"]["defaultFilter"] = filter.value + + if "magnifier" not in config.conf: + config.conf["magnifier"] = {} + config.conf["magnifier"]["panStep"] = panStep -def getDefaultFullscreenMode() -> FullScreenMode: +def getFilter() -> Filter: """ - Get default full-screen mode from config. + Get filter from config. - :return: The default full-screen mode. + :return: The filter. """ - return FullScreenMode(config.conf["magnifier"]["defaultFullscreenMode"]) + return Filter(config.conf["magnifier"]["filter"]) -def setDefaultFullscreenMode(mode: FullScreenMode) -> None: +def setFilter(filter: Filter) -> None: """ - Set default full-screen mode from settings. + Set filter from settings. - :param mode: The full-screen mode to set. + :param filter: The filter to set. """ - config.conf["magnifier"]["defaultFullscreenMode"] = mode.value + config.conf["magnifier"]["filter"] = filter.value def isTrueCentered() -> bool: @@ -157,3 +160,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 317f2bfd8f2..9ab8f122f4d 100644 --- a/source/_magnifier/fullscreenMagnifier.py +++ b/source/_magnifier/fullscreenMagnifier.py @@ -14,17 +14,26 @@ from .magnifier import Magnifier from .utils.filterHandler import FilterMatrix from .utils.spotlightManager import SpotlightManager -from .utils.types import Filter, Coordinates, FullScreenMode, FocusType -from .config import getDefaultFullscreenMode, shouldKeepMouseCentered +from .utils.types import ( + Filter, + Coordinates, + MagnifierType, + FullScreenMode, + FocusType, + Size, + MagnifierParameters, +) +from .config import getFullscreenMode, shouldKeepMouseCentered, isTrueCentered 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._startMagnifier() + self._displaySize = Size(self._displayOrientation.width, self._displayOrientation.height) @property def filterType(self) -> Filter: @@ -107,6 +116,9 @@ def _applyFilter(self) -> None: """ Apply the current color filter to the full-screen magnifier """ + if not self._isActive: + return + try: match self.filterType: case Filter.NORMAL: @@ -127,12 +139,15 @@ def _fullscreenMagnifier(self, coordinates: Coordinates) -> None: :coordinates: The (x, y) coordinates to center the magnifier on """ - left, top, visibleWidth, visibleHeight = self._getMagnifierPosition(coordinates) + if not self._isActive: + return + + params = self._getMagnifierParameters(coordinates) try: result = magnification.MagSetFullscreenTransform( self.zoomLevel, - left, - top, + params.coordinates.x, + params.coordinates.y, ) if not result: log.debug("Failed to set full-screen transform") @@ -172,11 +187,9 @@ def moveMouseToScreen(self) -> None: log.debug("Mouse button pressed, skipping cursor repositioning to avoid interfering with click") return - left, top, visibleWidth, visibleHeight = self._getMagnifierPosition( - self._currentCoordinates, - ) - centerX = int(left + (visibleWidth / 2)) - centerY = int(top + (visibleHeight / 2)) + params = self._getMagnifierParameters(self._currentCoordinates) + centerX = int(params.coordinates.x + (params.magnifierSize.width / 2)) + centerY = int(params.coordinates.y + (params.magnifierSize.height / 2)) winUser.setCursorPos(centerX, centerY) def _borderPos( @@ -192,14 +205,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 @@ -237,21 +252,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 @@ -271,3 +286,32 @@ 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 + :param displaySize: The size of the display area (width, height) - used to calculate capture size + + :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 4ee41440cab..59143b8d7de 100644 --- a/source/_magnifier/magnifier.py +++ b/source/_magnifier/magnifier.py @@ -18,7 +18,7 @@ from winAPI import _displayTracking from winAPI._displayTracking import OrientationState, getPrimaryDisplayOrientation from .utils.types import ( - MagnifierPosition, + MagnifierParameters, MagnifierAction, Coordinates, MagnifierType, @@ -26,9 +26,9 @@ Filter, ) from .config import ( - getDefaultZoomLevel, - getDefaultPanStep, - getDefaultFilter, + getZoomLevel, + getPanStep, + getFilter, ZoomLevel, isTrueCentered, ) @@ -41,16 +41,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 # Register for display changes _displayTracking.displayChanged.register(self._onDisplayChanged) @@ -277,29 +277,13 @@ 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 + :param displaySize: The size of the display area (width, height) - used to calculate capture size - :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/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..420f08aea2d 100644 --- a/source/_magnifier/utils/types.py +++ b/source/_magnifier/utils/types.py @@ -12,14 +12,6 @@ from utils.displayString import DisplayStringStrEnum, DisplayStringEnum -class MagnifierParams(NamedTuple): - """Named tuple representing magnifier parameters for initialization""" - - zoomLevel: float - filter: str - fullscreenMode: str - - class Direction(Enum): """Direction for zoom operations""" @@ -27,6 +19,20 @@ class Direction(Enum): OUT = False +class Coordinates(NamedTuple): + """Named tuple representing x and y coordinates""" + + x: int + y: int + + +class Size(NamedTuple): + """Named tuple representing width and height""" + + width: int + height: int + + class MagnifierAction(DisplayStringEnum): """Actions that can be performed with the magnifier""" @@ -41,6 +47,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 +76,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 +89,41 @@ 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 used before actual magnifier is implemented. + self.PLACEHOLDER: pgettext("magnifier", "Placeholder"), + } + + +class Filter(DisplayStringStrEnum): + NORMAL = "normal" + GRAYSCALE = "grayscale" + INVERTED = "inverted" + + @property + def _displayStringLabels(self) -> dict["Filter", str]: + return { + # Translators: Magnifier color filter - no filter applied. + self.NORMAL: pgettext("magnifier", "Normal"), + # Translators: Magnifier color filter - grayscale/black and white. + self.GRAYSCALE: pgettext("magnifier", "Grayscale"), + # Translators: Magnifier color filter - inverted colors. + self.INVERTED: pgettext("magnifier", "Inverted"), } @@ -103,13 +135,12 @@ class FocusType(Enum): NAVIGATOR = auto() -class MagnifierPosition(NamedTuple): - """Named tuple representing the position and size of the magnifier window""" +class MagnifierParameters(NamedTuple): + """Named tuple representing the size and position of the magnifier""" - left: int - top: int - visibleWidth: int - visibleHeight: int + magnifierSize: Size + coordinates: Coordinates + filter: Filter class Coordinates(NamedTuple): diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 205c9800d3a..765571cb915 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -114,13 +114,13 @@ # Magnifier settings [magnifier] - defaultZoomLevel = float(min=1.0, max=10.0, default=2.0) - defaultPanStep = integer(min=1, max=100, default=10) - defaultFullscreenMode = string(default="center") + zoomLevel = float(min=1.0, max=10.0, default=2.0) + panStep = integer(min=1, max=100, default=10) isTrueCentered = boolean(default=False) - defaultFilter = string(default="normal") + filter = string(default="normal") + magnifierType = string(default="fullscreen") + fullscreenMode = string(default="center") keepMouseCentered = boolean(default=false) - saveShortcutChanges = boolean(default=false) # Presentation settings [presentation] diff --git a/source/globalCommands.py b/source/globalCommands.py index a47b5c8a284..3c31bd9ff66 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -5153,29 +5153,43 @@ def script_panToBottomEdge( @script( description=_( # Translators: Describes a command. - "Toggle filter of the magnifier", + "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. + "Cycle filter of the magnifier", ), category=SCRCAT_VISION, gesture="kb:NVDA+shift+i", ) - def script_toggleFilter( + def script_cycleFilter( self, gesture: inputCore.InputGesture, ) -> None: - _magnifier.commands.toggleFilter() + _magnifier.commands.cycleFilter() @script( description=_( # Translators: Describes a command. - "Toggle focus mode for the full-screen magnifier", + "Cycle focus mode for the full-screen magnifier", ), category=SCRCAT_VISION, ) - def script_toggleFullscreenMode( + def script_cycleFullscreenMode( self, gesture: inputCore.InputGesture, ) -> None: - _magnifier.commands.toggleFullscreenMode() + _magnifier.commands.cycleFullscreenMode() @script( description=_( diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index d0b73bc1e99..3d52e16147b 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -41,7 +41,7 @@ import languageHandler import logHandler import _magnifier.config as magnifierConfig -from _magnifier.utils.types import Filter, FullScreenMode +from _magnifier.utils.types import Filter, FullScreenMode, MagnifierType import queueHandler import requests import speech @@ -5982,119 +5982,230 @@ def makeSettings( sizer=settingsSizer, ) + sHelper.addItem(wx.StaticText(self, label=self.panelDescription)) + + # GENERAL GROUP + # Translators: This is the label for a group of general magnifier options in the + # magnifier settings panel + generalGroupText = _("General") + generalGroupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=generalGroupText) + generalGroupBox = generalGroupSizer.GetStaticBox() + generalGroup = guiHelper.BoxSizerHelper(self, sizer=generalGroupSizer) + sHelper.addItem(generalGroup) + # 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 = generalGroup.addLabeledControl( + zoomLabelText, wx.Choice, choices=zoomChoices, ) self.bindHelpEvent( - "MagnifierDefaultZoom", - self.defaultZoomList, + "ZoomLevel", + 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) + + # FILTER SETTINGS + # Translators: The label for a setting in magnifier settings to select the filter + filterLabelText = _(" &filter:") + filterChoices = [f.displayString for f in Filter] + self.filterList = generalGroup.addLabeledControl( + filterLabelText, + wx.Choice, + choices=filterChoices, + ) + self.bindHelpEvent("MagnifierFilter", self.filterList) + + # Set value from config + filterValue = magnifierConfig.getFilter() + self.filterList.SetSelection(list(Filter).index(filterValue)) + + # TRUE CENTER + # Translators: The label for a setting in magnifier settings to select whether true center is used in full-screen mode + trueCenterText = _("Use &true center") + self.trueCenterCheckBox = generalGroup.addItem( + wx.CheckBox(generalGroupBox, label=trueCenterText), + ) + self.bindHelpEvent( + "MagnifierUseTrueCenter", + self.trueCenterCheckBox, + ) + self.trueCenterCheckBox.SetValue(magnifierConfig.isTrueCentered()) + + # MAGNIFIER TYPE SETTINGS + # Translators: The label for a setting in magnifier settings to select the magnifier type + magnifierTypeLabelText = _("&Magnifier type:") + magnifierTypeChoices = [mt.displayString for mt in MagnifierType] + self.magnifierTypeList = generalGroup.addLabeledControl( + magnifierTypeLabelText, + wx.Choice, + choices=magnifierTypeChoices, + ) + self.bindHelpEvent( + "MagnifierType", + self.magnifierTypeList, + ) + + # Set value from config + magnifierTypeValue = magnifierConfig.getMagnifierType() + self.magnifierTypeList.SetSelection(list(MagnifierType).index(magnifierTypeValue)) + + # Bind event to update visibility when magnifier type changes + self.magnifierTypeList.Bind(wx.EVT_CHOICE, self._onMagnifierTypeChange) # 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 = generalGroup.addLabeledControl( panStepSizeLabelText, wx.SpinCtrl, min=1, max=100, ) self.bindHelpEvent( - "magnifierPanStep", - self.defaultPanSpinCtrl, + "MagnifierPanStep", + self.panSpinCtrl, ) - # Set default value from config - defaultPan = magnifierConfig.getDefaultPanStep() - self.defaultPanSpinCtrl.SetValue(defaultPan) - - # FILTER SETTINGS - # Translators: The label for a setting in magnifier settings to select the default filter - defaultFilterLabelText = _("Default &filter:") - filterChoices = [f.displayString for f in Filter] - self.defaultFilterList = sHelper.addLabeledControl( - defaultFilterLabelText, - wx.Choice, - choices=filterChoices, - ) - self.bindHelpEvent("MagnifierDefaultFilter", self.defaultFilterList) + # Set value from config + panStep = magnifierConfig.getPanStep() + self.panSpinCtrl.SetValue(panStep) - # Set default value from config - defaultFilter = magnifierConfig.getDefaultFilter() - self.defaultFilterList.SetSelection(list(Filter).index(defaultFilter)) + # FULLSCREEN GROUP + # Translators: This is the label for a group of fullscreen magnifier options in the + # magnifier settings panel + fullscreenGroupText = _("Fullscreen") + self.fullscreenGroupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=fullscreenGroupText) + fullscreenGroupBox = self.fullscreenGroupSizer.GetStaticBox() + fullscreenGroup = guiHelper.BoxSizerHelper(self, sizer=self.fullscreenGroupSizer) + sHelper.addItem(fullscreenGroup) # 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 = fullscreenGroup.addLabeledControl( + fullscreenModeLabelText, wx.Choice, choices=fullscreenModeChoices, ) self.bindHelpEvent( - "MagnifierDefaultFullscreenFocusMode", - self.defaultFullscreenModeList, + "MagnifierFullscreenMode", + self.fullscreenModeList, ) - # TRUE CENTER - # Translators: The label for a setting in magnifier settings to select whether true center is used in full-screen mode - trueCenterText = _("Use &true center in fullscreen mode") - self.trueCenterCheckBox = sHelper.addItem(wx.CheckBox(self, label=trueCenterText)) - self.bindHelpEvent( - "MagnifierUseTrueCenter", - self.trueCenterCheckBox, - ) - self.trueCenterCheckBox.SetValue(magnifierConfig.isTrueCentered()) - - # Set default value from config - defaultFullscreenMode = magnifierConfig.getDefaultFullscreenMode() - self.defaultFullscreenModeList.SetSelection(list(FullScreenMode).index(defaultFullscreenMode)) + # Set value from config + fullscreenModeValue = magnifierConfig.getFullscreenMode() + self.fullscreenModeList.SetSelection(list(FullScreenMode).index(fullscreenModeValue)) # 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") - self.keepMouseCenteredCheckBox = sHelper.addItem(wx.CheckBox(self, label=keepMouseCenteredText)) + self.keepMouseCenteredCheckBox = fullscreenGroup.addItem( + wx.CheckBox(fullscreenGroupBox, label=keepMouseCenteredText), + ) self.bindHelpEvent( "MagnifierKeepMouseCentered", self.keepMouseCenteredCheckBox, ) self.keepMouseCenteredCheckBox.SetValue(magnifierConfig.shouldKeepMouseCentered()) + # FIXED MAGNIFIER GROUP + # Translators: This is the label for a group of fixed magnifier options in the + # magnifier settings panel + fixedGroupText = _("Fixed") + self.fixedGroupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=fixedGroupText) + fixedGroupBox = self.fixedGroupSizer.GetStaticBox() + fixedGroup = guiHelper.BoxSizerHelper(self, sizer=self.fixedGroupSizer) + sHelper.addItem(fixedGroup) + + # TODO: Add fixed magnifier specific options here + # Translators: Placeholder text for fixed magnifier options + fixedPlaceholderText = _("Options for fixed magnifier will be added here.") + fixedGroup.addItem(wx.StaticText(fixedGroupBox, label=fixedPlaceholderText)) + + # DOCKED MAGNIFIER GROUP + # Translators: This is the label for a group of docked magnifier options in the + # magnifier settings panel + dockedGroupText = _("Docked") + self.dockedGroupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=dockedGroupText) + dockedGroupBox = self.dockedGroupSizer.GetStaticBox() + dockedGroup = guiHelper.BoxSizerHelper(self, sizer=self.dockedGroupSizer) + sHelper.addItem(dockedGroup) + + # TODO: Add docked magnifier specific options here + # Translators: Placeholder text for docked magnifier options + dockedPlaceholderText = _("Options for docked magnifier will be added here.") + dockedGroup.addItem(wx.StaticText(dockedGroupBox, label=dockedPlaceholderText)) + + # LENS MAGNIFIER GROUP + # Translators: This is the label for a group of lens magnifier options in the + # magnifier settings panel + lensGroupText = _("Lens") + self.lensGroupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=lensGroupText) + lensGroupBox = self.lensGroupSizer.GetStaticBox() + lensGroup = guiHelper.BoxSizerHelper(self, sizer=self.lensGroupSizer) + sHelper.addItem(lensGroup) + + # TODO: Add lens magnifier specific options here + # Translators: Placeholder text for lens magnifier options + lensPlaceholderText = _("Options for lens magnifier will be added here.") + lensGroup.addItem(wx.StaticText(lensGroupBox, label=lensPlaceholderText)) + + # Initialize enabled state based on current selection + self._updateMagnifierGroupsState() + + def _onMagnifierTypeChange(self, evt): + """Update enabled state of magnifier type-specific groups when selection changes.""" + self._updateMagnifierGroupsState() + + def _updateMagnifierGroupsState(self): + """Enable/disable magnifier type-specific groups based on selected type.""" + selectedIdx = self.magnifierTypeList.GetSelection() + if selectedIdx == wx.NOT_FOUND: + return + + selectedType = list(MagnifierType)[selectedIdx] + + # Enable only the group corresponding to the selected magnifier type + self.fullscreenGroupSizer.GetStaticBox().Enable(selectedType == MagnifierType.FULLSCREEN) + self.fixedGroupSizer.GetStaticBox().Enable(selectedType == MagnifierType.FIXED) + self.dockedGroupSizer.GetStaticBox().Enable(selectedType == MagnifierType.DOCKED) + self.lensGroupSizer.GetStaticBox().Enable(selectedType == MagnifierType.LENS) + 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.setPanStep(self.panSpinCtrl.GetValue()) - magnifierConfig.setDefaultPanStep(self.defaultPanSpinCtrl.GetValue()) + selectedFilterIdx = self.filterList.GetSelection() + magnifierConfig.setFilter(list(Filter)[selectedFilterIdx]) - selectedFilterIdx = self.defaultFilterList.GetSelection() - magnifierConfig.setDefaultFilter(list(Filter)[selectedFilterIdx]) + selectedMagnifierTypeIdx = self.magnifierTypeList.GetSelection() + magnifierConfig.setMagnifierType(list(MagnifierType)[selectedMagnifierTypeIdx]) - 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() config.conf["magnifier"]["keepMouseCentered"] = self.keepMouseCenteredCheckBox.GetValue() diff --git a/tests/unit/test_magnifier/test_fullscreenMagnifier.py b/tests/unit/test_magnifier/test_fullscreenMagnifier.py index 56b23d64f9c..3f9ac2f06f0 100644 --- a/tests/unit/test_magnifier/test_fullscreenMagnifier.py +++ b/tests/unit/test_magnifier/test_fullscreenMagnifier.py @@ -10,12 +10,13 @@ from _magnifier.magnifier import Magnifier -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) @@ -28,6 +29,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 @@ -47,6 +49,7 @@ def testMagnifierZoom(self): def testMagnifierCoordinates(self): """Test coordinate handling.""" magnifier = FullScreenMagnifier() + magnifier._startMagnifier() # Test setting coordinates magnifier._currentCoordinates = (100, 200) @@ -62,6 +65,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)) @@ -84,6 +88,7 @@ def testMagnifierUpdate(self): def testMagnifierStop(self): """Test stopping the magnifier.""" magnifier = FullScreenMagnifier() + magnifier._startMagnifier() # Mock the timer magnifier._stopTimer = MagicMock() @@ -103,20 +108,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() @@ -189,6 +194,7 @@ def testMagnifierSimpleLifecycle(self): """Test simple magnifier lifecycle.""" # Create magnifier magnifier = FullScreenMagnifier() + magnifier._startMagnifier() self.assertTrue(magnifier._isActive) self.assertEqual(magnifier.zoomLevel, 2.0) diff --git a/tests/unit/test_magnifier/test_magnifier.py b/tests/unit/test_magnifier/test_magnifier.py index 1e864689255..925351f1e41 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) @@ -364,50 +363,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 91a4e97ea40..f51d0417d37 100644 --- a/user_docs/en/userGuide.md +++ b/user_docs/en/userGuide.md @@ -1563,7 +1563,7 @@ Once the magnifier is enabled, you can use the following keyboard commands to co -### Zoom Levels {#MagnifierZoomLevels} +### Zoom Levels {#MagnifierZoom} The magnifier supports zoom levels from 1.0 (no magnification) to 10.0 (maximum magnification). You can adjust the zoom level using the zoom in (`NVDA+shift+equals`) and zoom out (`NVDA+shift+minus`) commands. @@ -1571,7 +1571,7 @@ Each press increases or decreases the zoom by a fixed increment. The default zoom level when the magnifier is first enabled can be configured in the [Magnifier settings](#MagnifierSettings). -### Color Filters {#MagnifierColorFilters} +### Filters {#MagnifierFilter} Color filters can help users with certain visual impairments or light sensitivity by modifying the colors displayed on the screen. The magnifier provides three color filter options: @@ -1585,9 +1585,18 @@ 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 Magnifier {#MagnifierFullscreen} + +The fullscreen magnifier offers three different focus modes that determine which part of the screen is magnified: * 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. @@ -1597,14 +1606,14 @@ This mode provides a more stable view, only adjusting when necessary. * Relative: The magnified area maintains the relative position of the focus within the screen. This mode mimics the behavior of the Windows Magnifier. -To cycle through the focus tracking modes, please assign a custom gesture using the [Input Gestures dialog](#InputGestures). +To cycle through the modes, please assign a custom gesture using the [Input Gestures dialog](#InputGestures). 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 is a special feature designed for presentations or focused reading tasks. +Spotlight mode is a special fullscreen magnifier 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: @@ -1622,6 +1631,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`). @@ -2811,9 +2832,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 {#ZoomLevel} -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 the magnifier is first enabled. The zoom level can range from 1.0 (no magnification) to 10.0 (maximum magnification). The default value is 2.0 (200% zoom). @@ -2824,10 +2845,10 @@ 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. -You can cycle through the color filters by pressing `NVDA+shift+i`. +This combo box allows you to select the filter to apply when the magnifier is first enabled. +You can cycle through the filters by pressing `NVDA+shift+i`. The available options are: | . {.hideHeaderRow} |.| @@ -2842,25 +2863,7 @@ 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} - -This combo box allows you to select the default focus tracking mode when the magnifier is first enabled. -To cycle through the focus tracking modes, please assign a custom gesture using the [Input Gestures dialog](#InputGestures). -The available options are: - -| . {.hideHeaderRow} |.| -|---|---| -|Options |Center, Border, Relative| -|Default |Center| -|Toggle command |None | - -| Option | Description | -|---|---| -| Center | The magnified area is always centered on the current focus position. | -| Border | The magnified area only moves when the focus approaches the edge of the visible area. | -| Relative | The magnified area maintains the relative position of the focus within the screen. | - -##### Panning step size {#MagnifierPanningStepSize} +##### Panning step size {#MagnifierPanStep} This slider allows you to set the panning step size as a percentage of the visible magnified window. The panning step size can range from 1% to 100%, with a default value of 10%. @@ -2902,6 +2905,24 @@ This option is disabled by default. |Options |Disabled, Enabled| |Default |Disabled| +##### Fullscreen mode {#MagnifierFullscreenMode} + +This combo box allows you to select the mode when the fullscreen magnifier is first enabled. +To cycle through the focus tracking modes, please assign a custom gesture using the [Input Gestures dialog](#InputGestures). +The available options are: + +| . {.hideHeaderRow} |.| +|---|---| +|Options |Center, Border, Relative| +|Default |Center| +|Toggle command |None | + +| Option | Description | +|---|---| +| Center | The magnified area is always centered on the current focus position. | +| Border | The magnified area only moves when the focus approaches the edge of the visible area. | +| Relative | The magnified area maintains the relative position of the focus within the screen. | + #### Keyboard {#KeyboardSettings}