Skip to content
Draft
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
12 changes: 6 additions & 6 deletions source/_magnifier/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
import ui
from . import getMagnifier, initialize, terminate
from .config import (
getDefaultZoomLevelString,
getDefaultFilter,
getDefaultFullscreenMode,
getZoomLevelString,
getFilter,
getFullscreenMode,
ZoomLevel,
)
from .magnifier import Magnifier
Expand Down Expand Up @@ -100,16 +100,16 @@ def toggleMagnifier() -> None:
else:
initialize()

filter = getDefaultFilter()
fullscreenMode = getDefaultFullscreenMode()
filter = getFilter()
fullscreenMode = getFullscreenMode()

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(),
zoomLevel=getZoomLevelString(),
filter=filter.displayString,
fullscreenMode=fullscreenMode.displayString,
),
Expand Down
93 changes: 51 additions & 42 deletions source/_magnifier/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,111 +49,120 @@ 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]
if not zoomValues:
# Fallback: format the current zoom level directly if no predefined values are available.
return ZoomLevel.ZOOM_MESSAGE.format(
zoomLevel=f"{zoomLevel:.1f}",
)
# Find the index of the zoom value closest to the configured zoom level.
closestIndex = min(
range(len(zoomValues)),
key=lambda i: abs(zoomValues[i] - zoomLevel),
)
return zoomStrings[closestIndex]


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 getPanStep() -> int:
"""
Get default pan value from config.
Get pan value from config.

:return: The default pan value.
:return: The pan value.
"""
return config.conf["magnifier"]["defaultPanStep"]
return config.conf["magnifier"]["panStep"]


def setDefaultPanStep(panStep: int) -> None:
def setPanStep(panStep: int) -> None:
"""
Set default pan value from settings.
Set pan value from settings.

:param panStep: The pan value to set.
"""

if "magnifier" not in config.conf:
config.conf["magnifier"] = {}
config.conf["magnifier"]["defaultPanStep"] = panStep
config.conf["magnifier"]["panStep"] = panStep


def getDefaultFilter() -> Filter:
def getFilter() -> Filter:
"""
Get default filter from config.
Get filter from config.

:return: The default filter.
:return: The filter.
"""
return Filter(config.conf["magnifier"]["defaultFilter"])
return Filter(config.conf["magnifier"]["filter"])


def setDefaultFilter(filter: Filter) -> None:
def setFilter(filter: Filter) -> None:
"""
Set default filter from settings.
Set filter from settings.

:param filter: The filter to set.
"""
config.conf["magnifier"]["defaultFilter"] = filter.value
config.conf["magnifier"]["filter"] = filter.value


def getDefaultFullscreenMode() -> FullScreenMode:
def isTrueCentered() -> bool:
"""
Get default full-screen mode from config.
Check if true centered mode is enabled in config.

:return: The default full-screen mode.
:return: True if true centered mode is enabled, False otherwise.
"""
return FullScreenMode(config.conf["magnifier"]["defaultFullscreenMode"])
return config.conf["magnifier"]["isTrueCentered"]


def setDefaultFullscreenMode(mode: FullScreenMode) -> None:
def shouldKeepMouseCentered() -> bool:
"""
Set default full-screen mode from settings.
Check if mouse pointer should be kept centered in magnifier view.

:param mode: The full-screen mode to set.
:return: True if mouse should be kept centered, False otherwise.
"""
config.conf["magnifier"]["defaultFullscreenMode"] = mode.value
return config.conf["magnifier"]["keepMouseCentered"]


def isTrueCentered() -> bool:
def getFullscreenMode() -> FullScreenMode:
"""
Check if true centered mode is enabled in config.
Get full-screen mode from config.

:return: True if true centered mode is enabled, False otherwise.
:return: The full-screen mode.
"""
return config.conf["magnifier"]["isTrueCentered"]
return FullScreenMode(config.conf["magnifier"]["fullscreenMode"])


def shouldKeepMouseCentered() -> bool:
def setFullscreenMode(mode: FullScreenMode) -> None:
"""
Check if mouse pointer should be kept centered in magnifier view.
Set full-screen mode from settings.

:return: True if mouse should be kept centered, False otherwise.
:param mode: The full-screen mode to set.
"""
return config.conf["magnifier"]["keepMouseCentered"]
config.conf["magnifier"]["fullscreenMode"] = mode.value
96 changes: 71 additions & 25 deletions source/_magnifier/fullscreenMagnifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,25 @@
from .magnifier import Magnifier
from .utils.filterHandler import FilterMatrix
from .utils.spotlightManager import SpotlightManager
from .utils.types import Filter, Coordinates, FullScreenMode
from .config import getDefaultFullscreenMode
from .utils.types import (
Filter,
MagnifierType,
FullScreenMode,
Size,
MagnifierParameters,
Coordinates,
)
from .config import getFullscreenMode, 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._displaySize = Size(self._displayOrientation.width, self._displayOrientation.height)
Comment thread
Boumtchack marked this conversation as resolved.
self._startMagnifier()

@property
Expand Down Expand Up @@ -104,6 +113,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:
Expand All @@ -124,12 +136,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")
Expand Down Expand Up @@ -167,10 +182,11 @@ def _keepMouseCentered(self) -> None:
):
log.debug("Mouse button pressed, skipping cursor repositioning to avoid interfering with click")
return
coords = self._getCoordinatesForMode(self._currentCoordinates)
left, top, visibleWidth, visibleHeight = self._getMagnifierPosition(coords)
centerX = left + visibleWidth // 2
centerY = top + visibleHeight // 2

coordinates = self._getCoordinatesForMode(self._currentCoordinates)
params = self._getMagnifierParameters(coordinates)
centerX = params.coordinates.x + params.magnifierSize.width // 2
centerY = params.coordinates.y + params.magnifierSize.height // 2
winUser.setCursorPos(centerX, centerY)

def _borderPos(
Expand All @@ -186,14 +202,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
Expand All @@ -210,8 +228,8 @@ def _borderPos(

if dx != 0 or dy != 0:
return Coordinates(
self._lastScreenPosition[0] + dx,
self._lastScreenPosition[1] + dy,
self._lastScreenPosition.x + dx,
self._lastScreenPosition.y + dy,
)
else:
return self._lastScreenPosition
Expand All @@ -231,21 +249,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

Expand All @@ -265,3 +283,31 @@ 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

: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,
)
Loading
Loading