Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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
59 changes: 18 additions & 41 deletions source/_magnifier/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,9 @@
import ui
from . import changeMagnifiedView, getMagnifier, start, stop
from .config import (
getMagnifiedView,
setMagnifiedView,
getZoomLevelString,
getFilter,
getFollowState,
setFilter,
getFullscreenMode,
setFollowState,
setFullscreenMode,
toggleAllFollowStates,
Expand All @@ -32,7 +28,7 @@
MagnifiedView,
FullScreenMode,
MagnifierAction,
MagnifierFollowFocusType,
MagnifierTrackingType,
)
from logHandler import log

Expand Down Expand Up @@ -96,7 +92,7 @@ def toggleMagnifier() -> None:
pgettext(
"magnifier",
# Translators: Message announced when stopping the NVDA magnifier.
"Exiting magnifier",
"Magnifier disabled",
),
)
# Check if Screen Curtain is active
Expand All @@ -105,37 +101,18 @@ def toggleMagnifier() -> None:
pgettext(
"magnifier",
# Translators: Message announced when trying to start magnifier while Screen Curtain is active.
"Cannot start magnifier: Screen Curtain is active. Please disable Screen Curtain first.",
"Cannot start magnifier. Please disable Screen Curtain first.",
),
)
else:
start()
currentFilter = getFilter()
magnifiedView = getMagnifiedView()
zoomLevel = getZoomLevelString()
if magnifiedView == MagnifiedView.FULLSCREEN:
fullscreenMode = getFullscreenMode()
msg = pgettext(
"magnifier",
# Translators: Message announced when starting the NVDA magnifier.
"Starting {magnifiedView} magnifier with {zoomLevel} zoom level, {filter} filter, and {fullscreenMode} full-screen mode",
).format(
magnifiedView=magnifiedView.displayString,
zoomLevel=zoomLevel,
filter=currentFilter.displayString,
fullscreenMode=fullscreenMode.displayString,
)
else:
msg = pgettext(
ui.message(
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=currentFilter.displayString,
)
ui.message(msg)
"Magnifier enabled",
),
)


def zoom(direction: Direction) -> None:
Expand Down Expand Up @@ -193,7 +170,7 @@ def toggleFilter() -> None:
pgettext(
"magnifier",
# Translators: Message announced when changing the color filter with {filter} being the new color filter.
"Color filter changed to {filter}",
"Color filter {filter}",
).format(filter=magnifier.filterType.displayString),
)

Expand All @@ -217,12 +194,12 @@ def cycleMagnifiedView() -> None:
pgettext(
"magnifier",
# Translators: Message announced when changing the magnifier view with {view} being the new magnifier view.
"Magnifier view changed to {view}",
"{view} view",
).format(view=magnifier._MAGNIFIED_VIEW.displayString),
)


def toggleFollow(focusType: MagnifierFollowFocusType) -> None:
def toggleFollow(focusType: MagnifierTrackingType) -> None:
"""
Toggle the specified follow mode setting.

Expand Down Expand Up @@ -270,13 +247,13 @@ def toggleAllFollow() -> None:
stateMessage = pgettext(
"magnifier",
# Translators: State of all follow settings being toggled disabled.
"All follow settings disabled",
"All tracking settings disabled",
)
else:
stateMessage = pgettext(
"magnifier",
# Translators: State of all follow settings being restored.
"All follow settings restored",
"Tracking settings restored",
)
ui.message(stateMessage)

Expand Down Expand Up @@ -304,7 +281,7 @@ def toggleFullscreenMode() -> None:
pgettext(
"magnifier",
# Translators: Message announced when changing the full-screen mode with {mode} being the new full-screen mode.
"Full-screen mode changed to {mode}",
"Full-screen mode {mode}",
).format(mode=newMode.displayString),
)

Expand All @@ -327,8 +304,8 @@ def startSpotlight() -> None:
ui.message(
pgettext(
"magnifier",
# Translators: Message announced when trying to start spotlight mode while it's already active.
"Spotlight mode is already active",
# Translators: Message announced when trying to show temporary overview of the screen while it's already active.
"The screen overview is already active",
),
)
else:
Expand All @@ -337,8 +314,8 @@ def startSpotlight() -> None:
ui.message(
pgettext(
"magnifier",
# Translators: Message announced when spotlight mode is started.
"Spotlight mode started",
# Translators: Message announced when overview of the entire screen is being showed.
"Showing entire screen",
),
)

Expand Down
20 changes: 10 additions & 10 deletions source/_magnifier/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2025-2026 NV Access Limited, Antoine Haffreingue
# Copyright (C) 2025-2026 NV Access Limited, Antoine Haffreingue, Cyrille Bougot
# 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

Expand All @@ -10,7 +10,7 @@

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


def setEnabled(enable: bool) -> None:
Expand Down Expand Up @@ -153,17 +153,17 @@ def setMagnifiedView(magnifiedView: MagnifiedView) -> None:
config.conf["magnifier"]["magnifiedView"] = magnifiedView.value


_FOLLOW_CONFIG_KEYS: dict[MagnifierFollowFocusType, str] = {
MagnifierFollowFocusType.MOUSE: "followMouse",
MagnifierFollowFocusType.SYSTEM_FOCUS: "followSystemFocus",
MagnifierFollowFocusType.REVIEW: "followReviewCursor",
MagnifierFollowFocusType.NAVIGATOR_OBJECT: "followNavigatorObject",
_FOLLOW_CONFIG_KEYS: dict[MagnifierTrackingType, str] = {
MagnifierTrackingType.MOUSE: "followMouse",
MagnifierTrackingType.SYSTEM_FOCUS: "followSystemFocus",
MagnifierTrackingType.REVIEW: "followReviewCursor",
MagnifierTrackingType.NAVIGATOR_OBJECT: "followNavigatorObject",
}


@dataclass
class _FollowStateOverride:
savedStates: dict[MagnifierFollowFocusType, bool] = field(default_factory=dict)
savedStates: dict[MagnifierTrackingType, bool] = field(default_factory=dict)
isActive: bool = False


Expand All @@ -179,7 +179,7 @@ def _ensureSavedStatesInitialized() -> None:
saveFollowStates()


def getFollowState(focusType: MagnifierFollowFocusType) -> bool:
def getFollowState(focusType: MagnifierTrackingType) -> bool:
"""
Get the current follow state for a given focus type.

Expand All @@ -189,7 +189,7 @@ def getFollowState(focusType: MagnifierFollowFocusType) -> bool:
return config.conf["magnifier"][_FOLLOW_CONFIG_KEYS[focusType]]


def setFollowState(focusType: MagnifierFollowFocusType, state: bool) -> None:
def setFollowState(focusType: MagnifierTrackingType, state: bool) -> None:
"""
Set the follow state for a given focus type.

Expand Down
6 changes: 3 additions & 3 deletions source/_magnifier/magnifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def _startMagnifier(self) -> None:
message = pgettext(
"magnifier",
# Translators: Message when trying to enable magnifier while screen curtain is active
"Cannot enable magnifier: screen curtain is active. Please disable screen curtain first.",
"Cannot enable magnifier. Please disable screen curtain first.",
)
ui.message(message, speechPriority=speech.priorities.Spri.NOW)
return
Expand Down Expand Up @@ -265,7 +265,7 @@ def onScreenCurtainEnabled(self) -> None:
pgettext(
"magnifier",
# Translators: Spoken message when magnifier is disabled due to screen curtain being enabled.
"Magnifier is active, disabling it before enabling screen curtain",
"Disabling magnifier",
),
)
self._stopMagnifier()
Expand All @@ -283,7 +283,7 @@ def onScreenCurtainDisabled(self) -> None:
pgettext(
"magnifier",
# Translators: Spoken message when magnifier is re-enabled after screen curtain is disabled.
"Magnifier was active before screen curtain, re-enabling it",
"Re-enabling magnifier",
),
)
self._startMagnifier()
Expand Down
32 changes: 16 additions & 16 deletions source/_magnifier/utils/focusManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import locationHelper
import textInfos
from textInfos.offsets import OffsetsTextInfo
from .types import Coordinates, MagnifierFollowFocusType
from .types import Coordinates, MagnifierTrackingType
from ..config import getFollowState


Expand All @@ -30,7 +30,7 @@ class FocusManager:

def __init__(self):
"""Initialize the focus manager."""
self._lastFocusedObject: MagnifierFollowFocusType | None = None
self._lastFocusedObject: MagnifierTrackingType | None = None
self._lastReportedCoordinates = Coordinates(0, 0)
self._lastMousePosition = Coordinates(0, 0)
self._lastSystemFocusPosition = Coordinates(0, 0)
Expand Down Expand Up @@ -62,10 +62,10 @@ def getCurrentFocusCoordinates(self) -> Coordinates:
isClickPressed = winUser.getAsyncKeyState(winUser.VK_LBUTTON) < 0

# Cache settings once — each call reads from config.conf
isFollowMouse = getFollowState(MagnifierFollowFocusType.MOUSE)
isFollowSystemFocus = getFollowState(MagnifierFollowFocusType.SYSTEM_FOCUS)
isFollowReviewCursor = getFollowState(MagnifierFollowFocusType.REVIEW)
isFollowNavigatorObject = getFollowState(MagnifierFollowFocusType.NAVIGATOR_OBJECT)
isFollowMouse = getFollowState(MagnifierTrackingType.MOUSE)
isFollowSystemFocus = getFollowState(MagnifierTrackingType.SYSTEM_FOCUS)
isFollowReviewCursor = getFollowState(MagnifierTrackingType.REVIEW)
isFollowNavigatorObject = getFollowState(MagnifierTrackingType.NAVIGATOR_OBJECT)

mouseChanged = self._lastMousePosition != mousePosition
systemFocusChanged = self._lastSystemFocusPosition != systemFocusPosition
Expand All @@ -85,30 +85,30 @@ def getCurrentFocusCoordinates(self) -> Coordinates:

# Priority 1: Mouse — drag (fires even when stationary) or movement
if (isClickPressed or mouseChanged) and isFollowMouse:
self._lastFocusedObject = MagnifierFollowFocusType.MOUSE
self._lastFocusedObject = MagnifierTrackingType.MOUSE
return self._rememberAndReturnCoordinates(mousePosition)

# Special case: table cell navigation (numpad).
# When both the system focus and the navigator object change simultaneously but the
# review cursor does not, the navigator object reflects the user's explicit navigation
# intent and therefore takes priority over the system focus.
if navigatorChanged and systemFocusChanged and not reviewChanged and isFollowNavigatorObject:
self._lastFocusedObject = MagnifierFollowFocusType.NAVIGATOR_OBJECT
self._lastFocusedObject = MagnifierTrackingType.NAVIGATOR_OBJECT
return self._rememberAndReturnCoordinates(navigatorPosition)

# Priority 2: System focus (focus object + browse mode cursor)
if systemFocusChanged and isFollowSystemFocus:
self._lastFocusedObject = MagnifierFollowFocusType.SYSTEM_FOCUS
self._lastFocusedObject = MagnifierTrackingType.SYSTEM_FOCUS
return self._rememberAndReturnCoordinates(systemFocusPosition)

# Priority 3: Review cursor
if reviewChanged and isFollowReviewCursor and reviewPosition is not None:
self._lastFocusedObject = MagnifierFollowFocusType.REVIEW
self._lastFocusedObject = MagnifierTrackingType.REVIEW
return self._rememberAndReturnCoordinates(reviewPosition)

# Priority 4: Navigator object (NumPad navigation)
if navigatorChanged and isFollowNavigatorObject:
self._lastFocusedObject = MagnifierFollowFocusType.NAVIGATOR_OBJECT
self._lastFocusedObject = MagnifierTrackingType.NAVIGATOR_OBJECT
return self._rememberAndReturnCoordinates(navigatorPosition)

# Resolve the effective review position once (fallback to last valid when None)
Expand All @@ -118,10 +118,10 @@ def getCurrentFocusCoordinates(self) -> Coordinates:

# All sources in priority order
_sources = (
(MagnifierFollowFocusType.MOUSE, isFollowMouse, mousePosition),
(MagnifierFollowFocusType.SYSTEM_FOCUS, isFollowSystemFocus, systemFocusPosition),
(MagnifierFollowFocusType.REVIEW, isFollowReviewCursor, reviewEffectivePosition),
(MagnifierFollowFocusType.NAVIGATOR_OBJECT, isFollowNavigatorObject, navigatorPosition),
(MagnifierTrackingType.MOUSE, isFollowMouse, mousePosition),
(MagnifierTrackingType.SYSTEM_FOCUS, isFollowSystemFocus, systemFocusPosition),
(MagnifierTrackingType.REVIEW, isFollowReviewCursor, reviewEffectivePosition),
(MagnifierTrackingType.NAVIGATOR_OBJECT, isFollowNavigatorObject, navigatorPosition),
)

# Keep current source if still enabled; otherwise clear it and freeze at _lastReportedCoordinates
Expand Down Expand Up @@ -271,7 +271,7 @@ def _getNavigatorObjectPosition(self) -> Coordinates:
return position
return self._lastValidNavigatorObjectPosition

def getLastFocusType(self) -> MagnifierFollowFocusType | None:
def getLastFocusType(self) -> MagnifierTrackingType | None:
"""
Get the type of the last focused object.

Expand Down
6 changes: 3 additions & 3 deletions source/_magnifier/utils/spotlightManager.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2025-2026 NV Access Limited, Antoine Haffreingue
# Copyright (C) 2025-2026 NV Access Limited, Antoine Haffreingue, Cyrille Bougot
# 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

Expand Down Expand Up @@ -65,8 +65,8 @@ def _stopSpotlight(self) -> None:
ui.message(
pgettext(
"magnifier",
# Translators: Message announced when stopping the magnifier spotlight.
"Magnifier spotlight stopped",
# Translators: Message announced when turning back to normal view after entire screen overview.
"Back to normal view",
),
)
if self._timer:
Expand Down
12 changes: 6 additions & 6 deletions source/_magnifier/utils/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,19 @@ def _displayStringLabels(self) -> dict["MagnifierAction", str]:
# Translators: Action description for panning to bottom edge.
self.PAN_BOTTOM_EDGE: pgettext("magnifier action", "pan to bottom edge"),
# Translators: Action description for toggling settings.
self.TOGGLE_FOLLOW_SETTINGS: pgettext("magnifier action", "toggle follow settings"),
self.TOGGLE_FOLLOW_SETTINGS: pgettext("magnifier action", "toggle tracking settings"),
# Translators: Action description for toggling color filters.
self.TOGGLE_FILTER: pgettext("magnifier action", "toggle filters"),
self.TOGGLE_FILTER: pgettext("magnifier action", "cycle color filters"),
Comment thread
seanbudd marked this conversation as resolved.
# Translators: Action description for changing magnifier view.
self.CHANGE_MAGNIFIER_VIEW: pgettext("magnifier action", "change magnifier view"),
# 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.
self.START_SPOTLIGHT: pgettext("magnifier action", "start spotlight mode"),
# Translators: Action description for showing entire screen overview.
self.START_SPOTLIGHT: pgettext("magnifier action", "show screen overview"),
}


class MagnifierFollowFocusType(DisplayStringEnum):
class MagnifierTrackingType(DisplayStringEnum):
"""Type of focus the magnifier should follow based on user settings"""

MOUSE = auto()
Expand All @@ -98,7 +98,7 @@ class MagnifierFollowFocusType(DisplayStringEnum):
NAVIGATOR_OBJECT = auto()

@property
def _displayStringLabels(self) -> dict["MagnifierFollowFocusType", str]:
def _displayStringLabels(self) -> dict["MagnifierTrackingType", str]:
return {
# Translators: Focus type for magnifier to follow - mouse cursor.
self.MOUSE: pgettext("magnifier follow focus type", "Mouse"),
Expand Down
Loading
Loading