Skip to content
Draft
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
77b7e19
placeholder and option for magnifier types
Boumtchack Jan 19, 2026
9c1a665
pre-commit
Boumtchack Jan 19, 2026
8523c02
Merge branch 'master' of https://github.com/France-Travail/nvda into …
Boumtchack Jan 28, 2026
b4ccd7c
pre-commit
Boumtchack Jan 28, 2026
a1d9f4b
changing MagnifierPosition to MagnifierParameters prepare for window …
Boumtchack Feb 3, 2026
04c53e7
Merge branch 'master' of https://github.com/France-Travail/nvda into …
Boumtchack Feb 3, 2026
e47e926
Merge branch 'nvaccess:master' into fixedMagWindow
Boumtchack Feb 4, 2026
704a4a8
fixed window creation, first draft
Boumtchack Feb 4, 2026
85f4e39
Merge branch 'fixedMagWindow' of https://github.com/France-Travail/nv…
Boumtchack Feb 4, 2026
f695dc6
updated setting dialog for clearer options
Boumtchack Feb 4, 2026
bb4dff1
changed typing, wip
Boumtchack Feb 4, 2026
090a247
Pre-commit auto-fix
pre-commit-ci[bot] Feb 4, 2026
50e45b7
fix tests
Boumtchack Feb 4, 2026
d9a737e
Merge pre-commit autofix changes
Boumtchack Feb 4, 2026
80124fe
Merge branch 'nvaccess:master' into fixedMagWindow
Boumtchack Feb 10, 2026
53d90f1
added filter and better window creation handling
Boumtchack Feb 10, 2026
8e87f5d
adding settings
Boumtchack Feb 11, 2026
a08a6db
added userguide for fixedWindow and updated fullscreen
Boumtchack Feb 11, 2026
d9a54c8
simplifying WindowCreator
Boumtchack Feb 11, 2026
516be80
tests for magnifierPanel & magnifierFrame
Boumtchack Feb 11, 2026
34ccff9
added tests fpr WindowedMagnifier
Boumtchack Feb 11, 2026
aa4e7d5
unit test for fixed Magnifier
Boumtchack Feb 11, 2026
1bd5868
Merge branch 'nvaccess:master' into fixedMagWindow
Boumtchack Feb 18, 2026
c2fba2c
merge conflict resolve
Boumtchack Feb 25, 2026
011d4c3
fixed true center, update gui
Boumtchack Feb 25, 2026
939c3ff
Merge branch 'master' of https://github.com/France-Travail/nvda into …
Boumtchack Mar 2, 2026
b3bdfc8
changed to native win32 for window handling
Boumtchack Mar 3, 2026
5bcad40
continuing changes
Boumtchack Mar 10, 2026
1785883
Merge branch 'nvaccess:master' into fixedMagWindow
Boumtchack Mar 10, 2026
e70fc26
Merge branch 'nvaccess:master' into FixedMagWindow
Boumtchack Mar 10, 2026
5280b10
Merge pull request #6 from France-Travail/FixedMagWindow
Boumtchack Mar 10, 2026
f046a8c
copilot review, without doc
Boumtchack Mar 10, 2026
9a192e4
master merge + doc
Boumtchack Mar 11, 2026
ab30830
removed 'default' mentions
Boumtchack Mar 17, 2026
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
93 changes: 75 additions & 18 deletions source/_magnifier/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,55 +9,112 @@
"""

from typing import TYPE_CHECKING
from .fullscreenMagnifier import FullScreenMagnifier

from .config import getDefaultMagnifierType
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 = getDefaultMagnifierType()
_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 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
66 changes: 52 additions & 14 deletions source/_magnifier/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@

from typing import Literal
import ui
from . import getMagnifier, initialize, terminate
from . import getMagnifier, initialize, changeMagnifierType, terminate
from .config import (
getDefaultZoomLevelString,
getDefaultFilter,
getDefaultMagnifierType,
getDefaultFullscreenMode,
ZoomLevel,
)
Expand Down Expand Up @@ -101,19 +102,33 @@ def toggleMagnifier() -> None:
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,
),
)
magnifierType = getDefaultMagnifierType()
if magnifierType == MagnifierType.FULLSCREEN:
fullscreenMode = getDefaultFullscreenMode()
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=getDefaultZoomLevelString(),
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=getDefaultZoomLevelString(),
filter=filter.displayString,
),
)


def zoom(direction: Direction) -> None:
Expand Down Expand Up @@ -153,6 +168,29 @@ def pan(action: MagnifierAction) -> None:
)


def toggleMagnifierType() -> None:
"""Cycle through magnifier types (full-screen, docked, lens)"""
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}")
Comment thread
Boumtchack marked this conversation as resolved.
Outdated
changeMagnifierType(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 toggleFilter() -> None:
"""Cycle through color filters"""
magnifier: Magnifier = getMagnifier()
Expand Down
93 changes: 81 additions & 12 deletions source/_magnifier/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"""

import config
from .utils.types import Filter, FullScreenMode
from .utils.types import Filter, FullScreenMode, MagnifierType, FixedWindowPosition


class ZoomLevel:
Expand Down Expand Up @@ -78,12 +78,27 @@ def setDefaultZoomLevel(zoomLevel: float) -> None:

:param zoomLevel: The zoom level to set.
"""

if "magnifier" not in config.conf:
config.conf["magnifier"] = {}
config.conf["magnifier"]["defaultZoomLevel"] = zoomLevel


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

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


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

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


def getDefaultPanStep() -> int:
"""
Get default pan value from config.
Expand Down Expand Up @@ -123,6 +138,24 @@ def setDefaultFilter(filter: Filter) -> None:
config.conf["magnifier"]["defaultFilter"] = filter.value


def isTrueCentered() -> bool:
"""
Check if true centered mode is enabled in config.

:return: True if true centered mode is enabled, False otherwise.
"""
return config.conf["magnifier"]["isTrueCentered"]


def shouldKeepMouseCentered() -> bool:
"""
Check if mouse pointer should be kept centered in magnifier view.

:return: True if mouse should be kept centered, False otherwise.
"""
return config.conf["magnifier"]["keepMouseCentered"]


def getDefaultFullscreenMode() -> FullScreenMode:
"""
Get default full-screen mode from config.
Expand All @@ -141,19 +174,55 @@ def setDefaultFullscreenMode(mode: FullScreenMode) -> None:
config.conf["magnifier"]["defaultFullscreenMode"] = mode.value


def isTrueCentered() -> bool:
def getDefaultFixedWindowWidth() -> int:
"""
Check if true centered mode is enabled in config.
Get default fixed magnifier window width from config.

:return: True if true centered mode is enabled, False otherwise.
:return: The default fixed magnifier window width.
"""
return config.conf["magnifier"]["isTrueCentered"]
return config.conf["magnifier"]["defaultFixedWindowWidth"]


def shouldKeepMouseCentered() -> bool:
def setDefaultFixedWindowWidth(width: int) -> None:
"""
Check if mouse pointer should be kept centered in magnifier view.
Set default fixed magnifier window width from settings.

:return: True if mouse should be kept centered, False otherwise.
:param width: The fixed magnifier window width to set.
"""
return config.conf["magnifier"]["keepMouseCentered"]
config.conf["magnifier"]["defaultFixedWindowWidth"] = width


def getDefaultFixedWindowHeight() -> int:
"""
Get default fixed magnifier window height from config.

:return: The default fixed magnifier window height.
"""
return config.conf["magnifier"]["defaultFixedWindowHeight"]


def setDefaultFixedWindowHeight(height: int) -> None:
"""
Set default fixed magnifier window height from settings.

:param height: The fixed magnifier window height to set.
"""
config.conf["magnifier"]["defaultFixedWindowHeight"] = height


def getDefaultFixedWindowPosition() -> FixedWindowPosition:
"""
Get default magnifier window position from config.

:return: The default magnifier window position.
"""
return FixedWindowPosition(config.conf["magnifier"]["defaultFixedWindowPosition"])


def setDefaultFixedWindowPosition(position: FixedWindowPosition) -> None:
"""
Set default magnifier window position from settings.

:param position: The magnifier window position to set.
"""
config.conf["magnifier"]["defaultFixedWindowPosition"] = position.value
27 changes: 27 additions & 0 deletions source/_magnifier/dockedMagnifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 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

"""
Docked magnifier module.
"""

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


class DockedMagnifier(Magnifier):
def __init__(self):
super().__init__()
self._magnifierType = MagnifierType.DOCKED
self._currentCoordinates = Coordinates(0, 0)

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

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

def _doUpdate(self):
super()._doUpdate()
Loading
Loading