Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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 getMagnifiedView
from .utils.types import MagnifiedView

if TYPE_CHECKING:
from .magnifier import Magnifier

_magnifier: "Magnifier | None" = None


def initialize():
def createMagnifier(magnifiedView: MagnifiedView) -> "Magnifier":
"""
Initialize the magnifier module
For now, only the full-screen magnifier is supported
Create a magnifier instance based on the specified view.

:param magnifiedView: The magnifier view to create
:return: The created magnifier instance
:raises ValueError: If the magnifier view is not supported
"""

match magnifiedView:
case MagnifiedView.FULLSCREEN:
from .fullscreenMagnifier import FullScreenMagnifier

return FullScreenMagnifier()

case MagnifiedView.FIXED:
from .fixedMagnifier import FixedMagnifier

return FixedMagnifier()

case MagnifiedView.DOCKED:
from .dockedMagnifier import DockedMagnifier

return DockedMagnifier()

case MagnifiedView.LENS:
from .lensMagnifier import LensMagnifier

return LensMagnifier()

case _:
raise ValueError(f"Unsupported magnifier view: {MagnifiedView}")


def _setMagnifiedView(magnifiedView: MagnifiedView) -> None:
"""
Set the magnifier view, stopping the current one if active and creating a new instance.

:param magnifiedView: The magnifier view 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(magnifiedView)


def initialize() -> None:
"""
Initialize the magnifier module with the default magnifier view from config.
"""
magnifiedView = getMagnifiedView()
_setMagnifiedView(magnifiedView)
_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 changeMagnifiedView(magnifiedView: MagnifiedView) -> None:
"""
Get current magnifier
Change the magnifier view at runtime.
Stops the current magnifier and starts a new one of the specified view.

:param magnifiedView: The new magnifier view 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 view: magnifier is not active")

_setMagnifiedView(magnifiedView)
_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
76 changes: 58 additions & 18 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, changeMagnifiedView
from .config import (
getMagnifiedView,
setMagnifiedView,
getZoomLevelString,
getFilter,
getFullscreenMode,
Expand All @@ -25,7 +27,7 @@
from .utils.types import (
Filter,
Direction,
MagnifierType,
MagnifiedView,
FullScreenMode,
MagnifierAction,
MagnifierFollowFocusType,
Expand Down Expand Up @@ -103,21 +105,32 @@ def toggleMagnifier() -> None:
return
else:
initialize()

filter = getFilter()
fullscreenMode = getFullscreenMode()

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


def zoom(direction: Direction) -> None:
Expand Down Expand Up @@ -174,8 +187,9 @@ def toggleFilter() -> None:
filters = list(Filter)
idx = filters.index(magnifier.filterType)
magnifier.filterType = filters[(idx + 1) % len(filters)]
if magnifier._magnifierType == MagnifierType.FULLSCREEN:
magnifier._applyFilter()
if magnifier._magnifiedView == MagnifiedView.FULLSCREEN:
fullscreenMagnifier: FullScreenMagnifier = magnifier
fullscreenMagnifier._applyFilter()
ui.message(
pgettext(
"magnifier",
Expand All @@ -185,6 +199,30 @@ def toggleFilter() -> None:
)


def cycleMagnifiedView() -> None:
"""Cycle through magnifier views (full-screen, fixed, docked, lens)"""
magnifier: Magnifier = getMagnifier()
if magnifierIsActiveVerify(
magnifier,
MagnifierAction.CHANGE_MAGNIFIER_VIEW,
):
views = list(MagnifiedView)
currentView = magnifier._magnifiedView
idx = views.index(currentView)
newView = views[(idx + 1) % len(views)]
log.debug(f"Changing magnifier view from {currentView} to {newView}")
changeMagnifiedView(newView)
setMagnifiedView(newView)
magnifier = getMagnifier()
ui.message(
pgettext(
"magnifier",
# Translators: Message announced when changing the magnifier view with {view} being the new magnifier view.
"Magnifier view changed to {view}",
).format(view=magnifier._magnifiedView.displayString),
)


def toggleFollow(focusType: MagnifierFollowFocusType) -> None:
"""
Toggle the specified follow mode setting.
Expand Down Expand Up @@ -255,12 +293,13 @@ def toggleFullscreenMode() -> None:
magnifier,
MagnifierAction.CHANGE_FULLSCREEN_MODE,
):
fullscreenMagnifier: FullScreenMagnifier = magnifier
modes = list(FullScreenMode)
currentMode = magnifier._fullscreenMode
currentMode = fullscreenMagnifier._fullscreenMode
idx = modes.index(currentMode)
newMode = modes[(idx + 1) % len(modes)]
log.debug(f"Changing full-screen mode from {currentMode} to {newMode}")
magnifier._fullscreenMode = newMode
fullscreenMagnifier._fullscreenMode = newMode
ui.message(
pgettext(
"magnifier",
Expand All @@ -281,8 +320,9 @@ def startSpotlight() -> None:
magnifier,
MagnifierAction.START_SPOTLIGHT,
):
fullscreenMagnifier: FullScreenMagnifier = magnifier
log.debug("trying to launch spotlight mode")
if magnifier._spotlightManager._spotlightIsActive:
if fullscreenMagnifier._spotlightManager._spotlightIsActive:
log.debug("found spotlight manager and it is active")
ui.message(
pgettext(
Expand All @@ -293,7 +333,7 @@ def startSpotlight() -> None:
)
else:
log.debug("no active spotlight manager found, starting new one")
magnifier._startSpotlight()
fullscreenMagnifier._startSpotlight()
ui.message(
pgettext(
"magnifier",
Expand Down Expand Up @@ -340,7 +380,7 @@ def magnifierIsFullscreenVerify(

:return: True if the magnifier is full-screen, False otherwise
"""
if magnifier._magnifierType == MagnifierType.FULLSCREEN:
if magnifier._magnifiedView == MagnifiedView.FULLSCREEN:
return True
else:
ui.message(
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, MagnifiedView


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


def getMagnifiedView() -> MagnifiedView:
"""
Get magnifier view from config.

:return: The magnifier view.
"""
return MagnifiedView(config.conf["magnifier"]["magnifiedView"])


def setMagnifiedView(magnifiedView: MagnifiedView) -> None:
"""
Set magnifier view in settings.

:param magnifiedView: The magnifier view to set.
"""
config.conf["magnifier"]["magnifiedView"] = magnifiedView.value


_FOLLOW_CONFIG_KEYS: dict[MagnifierFollowFocusType, str] = {
MagnifierFollowFocusType.MOUSE: "followMouse",
MagnifierFollowFocusType.SYSTEM_FOCUS: "followSystemFocus",
Expand Down
28 changes: 28 additions & 0 deletions source/_magnifier/dockedMagnifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 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 MagnifiedView


class DockedMagnifier(Magnifier):
"""Displays a magnified panel anchored to one edge of the screen."""

def __init__(self):
super().__init__()
self._magnifiedView = MagnifiedView.DOCKED

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

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

def _doUpdate(self):
pass
28 changes: 28 additions & 0 deletions source/_magnifier/fixedMagnifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 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 MagnifiedView


class FixedMagnifier(Magnifier):
"""Displays a magnified panel anchored to one corner of the screen."""
Comment thread
Boumtchack marked this conversation as resolved.
Outdated

def __init__(self):
super().__init__()
self._magnifiedView = MagnifiedView.FIXED

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

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

def _doUpdate(self):
pass
Loading
Loading