Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 79 additions & 18 deletions source/_magnifier/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,55 +9,116 @@
"""

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

_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 .fixedMagnifier import FixedMagnifier

return FixedMagnifier()

case MagnifierType.DOCKED:
from .dockedMagnifier import DockedMagnifier

return DockedMagnifier()

case MagnifierType.LENS:
from .lensMagnifier import LensMagnifier

return LensMagnifier()

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
65 changes: 55 additions & 10 deletions source/_magnifier/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@

from typing import Literal
import ui
from . import getMagnifier, initialize, terminate
from . import getMagnifier, initialize, terminate, changeMagnifierType
from .config import (
getMagnifierType,
setMagnifierType,
getZoomLevelString,
getFilter,
getFullscreenMode,
Expand Down Expand Up @@ -103,21 +105,32 @@ def toggleMagnifier() -> None:
return
else:
initialize()

filter = getFilter()
fullscreenMode = getFullscreenMode()

ui.message(
pgettext(
magnifierType = getMagnifierType()
zoomLevel = getZoomLevelString()
if magnifierType == MagnifierType.FULLSCREEN:
fullscreenMode = getFullscreenMode()
msg = pgettext(
"magnifier",
# Translators: Message announced when starting the NVDA magnifier.
"Starting magnifier with {zoomLevel} zoom level, {filter} filter, and {fullscreenMode} full-screen mode",
"Starting {magnifierType} magnifier with {zoomLevel} zoom level, {filter} filter, and {fullscreenMode} full-screen mode",
).format(
zoomLevel=getZoomLevelString(),
magnifierType=magnifierType.displayString,
zoomLevel=zoomLevel,
filter=filter.displayString,
fullscreenMode=fullscreenMode.displayString,
),
)
)
else:
msg = 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=zoomLevel,
filter=filter.displayString,
)
ui.message(msg)


def zoom(direction: Direction) -> None:
Expand Down Expand Up @@ -185,6 +198,38 @@ def toggleFilter() -> None:
)


_CYCLING_MAGNIFIER_TYPES = [
Comment thread
Boumtchack marked this conversation as resolved.
Outdated
MagnifierType.FULLSCREEN,
MagnifierType.FIXED,
MagnifierType.DOCKED,
MagnifierType.LENS,
]


def cycleMagnifierType() -> None:
"""Cycle through magnifier types (full-screen, fixed, docked, lens)"""
magnifier: Magnifier = getMagnifier()
if magnifierIsActiveVerify(
magnifier,
MagnifierAction.CHANGE_MAGNIFIER_TYPE,
):
types = _CYCLING_MAGNIFIER_TYPES
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}",
Comment thread
Boumtchack marked this conversation as resolved.
Outdated
).format(type=magnifier._magnifierType.displayString),
)


def toggleFollow(focusType: MagnifierFollowFocusType) -> None:
"""
Toggle the specified follow mode setting.
Expand Down
20 changes: 19 additions & 1 deletion source/_magnifier/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import config
from dataclasses import dataclass, field
from .utils.types import Filter, FullScreenMode, MagnifierFollowFocusType
from .utils.types import Filter, FullScreenMode, MagnifierFollowFocusType, MagnifierType


class ZoomLevel:
Expand Down Expand Up @@ -121,6 +121,24 @@ def setFilter(filter: Filter) -> None:
config.conf["magnifier"]["filter"] = filter.value


def getMagnifierType() -> MagnifierType:
"""
Get magnifier type from config.

:return: The magnifier type.
"""
return MagnifierType(config.conf["magnifier"]["magnifierType"])


def setMagnifierType(magnifierType: MagnifierType) -> None:
"""
Set magnifier type from settings.

:param magnifierType: The magnifier type to set.
"""
config.conf["magnifier"]["magnifierType"] = magnifierType.value


_FOLLOW_CONFIG_KEYS: dict[MagnifierFollowFocusType, str] = {
MagnifierFollowFocusType.MOUSE: "followMouse",
MagnifierFollowFocusType.SYSTEM_FOCUS: "followSystemFocus",
Expand Down
26 changes: 26 additions & 0 deletions source/_magnifier/dockedMagnifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2025-2026 NV Access Limited, Antoine Haffreingue
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt

"""
Docked magnifier module.
Comment thread
Boumtchack marked this conversation as resolved.
"""

from .magnifier import Magnifier
from .utils.types import MagnifierType


class DockedMagnifier(Magnifier):
def __init__(self):
super().__init__()
self._magnifierType = MagnifierType.DOCKED

def _startMagnifier(self) -> None:
super()._startMagnifier()

def _stopMagnifier(self) -> None:
super()._stopMagnifier()

def _doUpdate(self):
pass
26 changes: 26 additions & 0 deletions source/_magnifier/fixedMagnifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2025-2026 NV Access Limited, Antoine Haffreingue
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt

"""
Fixed magnifier module.
Comment thread
Boumtchack marked this conversation as resolved.
"""

from .magnifier import Magnifier
from .utils.types import MagnifierType


class FixedMagnifier(Magnifier):
def __init__(self):
super().__init__()
self._magnifierType = MagnifierType.FIXED

def _startMagnifier(self) -> None:
super()._startMagnifier()

def _stopMagnifier(self) -> None:
super()._stopMagnifier()

def _doUpdate(self):
pass
6 changes: 1 addition & 5 deletions source/_magnifier/fullscreenMagnifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,7 @@ def __init__(self):
self._displaySize = Size(self._displayOrientation.width, self._displayOrientation.height)
self._startMagnifier()

@property
def filterType(self) -> Filter:
return self._filterType

@filterType.setter
@Magnifier.filterType.setter
def filterType(self, value: Filter) -> None:
self._filterType = value
if self._isActive:
Expand Down
26 changes: 26 additions & 0 deletions source/_magnifier/lensMagnifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2025-2026 NV Access Limited, Antoine Haffreingue
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt

"""
Lens magnifier module.
Comment thread
Boumtchack marked this conversation as resolved.
"""

from .magnifier import Magnifier
from .utils.types import MagnifierType


class LensMagnifier(Magnifier):
def __init__(self):
super().__init__()
self._magnifierType = MagnifierType.LENS

def _startMagnifier(self) -> None:
super()._startMagnifier()

def _stopMagnifier(self) -> None:
super()._stopMagnifier()

def _doUpdate(self):
pass
10 changes: 9 additions & 1 deletion source/_magnifier/magnifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class Magnifier:

def __init__(self):
self._displayOrientation = getPrimaryDisplayOrientation()
self._magnifierType: MagnifierType
self._magnifierType: MagnifierType | None = None
Comment thread
seanbudd marked this conversation as resolved.
Outdated
self._isActive: bool = False
self._zoomLevel: float = getZoomLevel()
self._panStep: int = getPanStep()
Expand All @@ -59,6 +59,14 @@ def __init__(self):
_displayTracking.displayChanged.register(self._onDisplayChanged)
self._screenCurtainIsActive: bool = False

@property
def filterType(self) -> Filter:
return self._filterType

@filterType.setter
def filterType(self, value: Filter) -> None:
self._filterType = value

@property
def zoomLevel(self) -> float:
return self._zoomLevel
Expand Down
3 changes: 3 additions & 0 deletions source/_magnifier/utils/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class MagnifierAction(DisplayStringEnum):
PAN_TOP_EDGE = auto()
PAN_BOTTOM_EDGE = auto()
TOGGLE_FILTER = auto()
CHANGE_MAGNIFIER_TYPE = auto()
TOGGLE_FOLLOW_SETTINGS = auto()
CHANGE_FULLSCREEN_MODE = auto()
START_SPOTLIGHT = auto()
Expand Down Expand Up @@ -79,6 +80,8 @@ def _displayStringLabels(self) -> dict["MagnifierAction", str]:
self.TOGGLE_FOLLOW_SETTINGS: pgettext("magnifier action", "toggle follow settings"),
# 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.
Expand Down
Loading
Loading