Skip to content

Commit

Permalink
Merge a0b563e into 2faead2
Browse files Browse the repository at this point in the history
  • Loading branch information
seanbudd authored Nov 19, 2024
2 parents 2faead2 + a0b563e commit 1ba7192
Show file tree
Hide file tree
Showing 5 changed files with 333 additions and 25 deletions.
119 changes: 119 additions & 0 deletions source/visionEnhancementProviders/magnifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from ctypes import WinError
from ctypes.wintypes import RECT

from locationHelper import RectLTRB, RectLTWH
from logHandler import log
from vision import (
_isDebug,
)
from .screenCurtain import MAGTRANSFORM, Magnification
from winAPI import _displayTracking
from windowUtils import CustomWindow
import winUser

WindowClassName = "MagnifierWindow"
WindowTitle = "Screen Magnifier Sample"
WC_MAGNIFIER = "Magnifier"
RESTOREDWINDOWSTYLES = (
winUser.WS_SIZEBOX
| winUser.WS_SYSMENU
| winUser.WS_CLIPCHILDREN
| winUser.WS_CAPTION
| winUser.WS_MAXIMIZEBOX
)


class HostWindow(CustomWindow):
className = WindowClassName
windowName = WindowTitle
windowsStyle = RESTOREDWINDOWSTYLES
extendedWindowStyle = (
# Ensure that the window is on top of all other windows
winUser.WS_EX_TOPMOST
# A layered window ensures that L{transparentColor} will be considered transparent, when painted
| winUser.WS_EX_LAYERED
)

def __init__(self, magnificationFactor: int = 2):
super().__init__(
windowName=self.windowName,
windowStyle=self.windowsStyle,
extendedWindowStyle=self.extendedWindowStyle,
parent=None,
)
winUser.SetLayeredWindowAttributes(
self.handle,
0x00,
0xFF,
winUser.LWA_ALPHA,
)
if not winUser.user32.SetWindowPos(
self.handle,
winUser.HWND_TOPMOST,
self.targetRect.left,
self.targetRect.top,
self.targetRect.width,
int(self.targetRect.height),
winUser.SWP_NOACTIVATE | winUser.SWP_NOMOVE | winUser.SWP_NOSIZE,
):
raise WinError()
if not winUser.user32.UpdateWindow(self.handle):
raise WinError()
self.magnifierWindow = MagnifierWindow(self, magnificationFactor)

@property
def targetRect(self) -> RectLTRB:
# Top quarter of screen
return RectLTRB(
0, 0, _displayTracking._orientationState.width, _displayTracking._orientationState.height / 4
)

def windowProc(self, hwnd: int, msg: int, wParam: int, lParam: int):
log.debug(f"received window proc message: {msg}")


class MagnifierWindow(CustomWindow):
className = WC_MAGNIFIER
windowName = "MagnifierWindow"
windowStyle = winUser.WS_CHILD | winUser.MS_SHOWMAGNIFIEDCURSOR | winUser.WS_VISIBLE

def __init__(self, hostWindow: HostWindow, magnificationFactor: int = 2):
self.hostWindow = hostWindow
self.magnificationFactor = magnificationFactor
if _isDebug():
log.debug("initializing NVDA Magnifier window")
super().__init__(
windowName=self.windowName,
windowStyle=self.windowStyle,
parent=hostWindow.handle,
)

magWindowRect = self.magWindowRect
if not winUser.user32.SetWindowPos(
self.handle,
winUser.HWND_TOPMOST,
magWindowRect.left,
magWindowRect.top,
magWindowRect.width,
magWindowRect.height,
winUser.SWP_NOACTIVATE | winUser.SWP_NOMOVE | winUser.SWP_NOSIZE,
):
raise WinError()
if not winUser.user32.UpdateWindow(self.handle):
raise WinError()

Magnification.MagSetWindowSource(self.handle, RECT(200, 200, 700, 700))
Magnification.MagSetWindowTransform(self.handle, MAGTRANSFORM(self.magnificationFactor))

@property
def magWindowRect(self) -> RectLTWH:
r = winUser.getClientRect(self.hostWindow.handle)
return RectLTRB(
r.left,
r.top,
r.right,
r.bottom,
).toLTWH()

def windowProc(self, hwnd: int, msg: int, wParam: int, lParam: int):
log.debug(f"received window proc message: {msg}")
125 changes: 101 additions & 24 deletions source/visionEnhancementProviders/screenCurtain.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2018-2023 NV Access Limited, Babbage B.V., Leonard de Ruijter
# Copyright (C) 2018-2024 NV Access Limited, Babbage B.V., Leonard de Ruijter

"""Screen curtain implementation based on the windows magnification API.
The Magnification API has been marked by MS as unsupported for WOW64 applications such as NVDA. (#12491)
Expand All @@ -10,7 +10,7 @@
import os
from vision import providerBase
from ctypes import Structure, windll, c_float, POINTER, WINFUNCTYPE, WinError
from ctypes.wintypes import BOOL
from ctypes.wintypes import BOOL, FLOAT, HWND, RECT, INT
from autoSettingsUtils.driverSetting import BooleanDriverSetting
from autoSettingsUtils.autoSettings import SupportedSettingType
import wx
Expand All @@ -30,6 +30,24 @@ class MAGCOLOREFFECT(Structure):
_fields_ = (("transform", c_float * 5 * 5),)


class MAGTRANSFORM(Structure):
_fields_ = (("v", c_float * 3 * 3),)

def __init__(self, magnificationFactor: float = 1.0):
"""
https://learn.microsoft.com/en-us/windows/win32/api/magnification/ns-magnification-magtransform
:param magnificationFactor: defaults to 1.0.
The minimum value of this parameter is 1.0, and the maximum value is 4096.0.
If this value is 1.0, the screen content is not magnified and no offsets are applied.
"""
super().__init__()
assert 1.0 <= magnificationFactor <= 4096.0
self.v[0][0] = magnificationFactor
self.v[1][1] = magnificationFactor
self.v[2][2] = 1.0


# homogeneous matrix for a 4-space transformation (red, green, blue, opacity).
# https://docs.microsoft.com/en-gb/windows/win32/gdiplus/-gdiplus-using-a-color-matrix-to-transform-a-single-color-use
TRANSFORM_BLACK = MAGCOLOREFFECT() # empty transformation
Expand All @@ -51,14 +69,29 @@ class Magnification:
# Set full screen color effect
_MagSetFullscreenColorEffectFuncType = WINFUNCTYPE(BOOL, POINTER(MAGCOLOREFFECT))
_MagSetFullscreenColorEffectArgTypes = ((1, "effect"),)
MagSetFullscreenColorEffect = _MagSetFullscreenColorEffectFuncType(
("MagSetFullscreenColorEffect", _magnification),
_MagSetFullscreenColorEffectArgTypes,
)
MagSetFullscreenColorEffect.errcheck = _errCheck

# Get full screen color effect
_MagGetFullscreenColorEffectFuncType = WINFUNCTYPE(BOOL, POINTER(MAGCOLOREFFECT))
_MagGetFullscreenColorEffectArgTypes = ((2, "effect"),)
MagGetFullscreenColorEffect = _MagGetFullscreenColorEffectFuncType(
("MagGetFullscreenColorEffect", _magnification),
_MagGetFullscreenColorEffectArgTypes,
)
MagGetFullscreenColorEffect.errcheck = _errCheck

# show system cursor
_MagShowSystemCursorFuncType = WINFUNCTYPE(BOOL, BOOL)
_MagShowSystemCursorArgTypes = ((1, "showCursor"),)
MagShowSystemCursor = _MagShowSystemCursorFuncType(
("MagShowSystemCursor", _magnification),
_MagShowSystemCursorArgTypes,
)
MagShowSystemCursor.errcheck = _errCheck

# initialize
_MagInitializeFuncType = WINFUNCTYPE(BOOL)
Expand All @@ -70,28 +103,72 @@ class Magnification:
MagUninitialize = _MagUninitializeFuncType(("MagUninitialize", _magnification))
MagUninitialize.errcheck = _errCheck

# These magnification functions are not available on versions of Windows prior to Windows 8,
# and therefore looking them up from the magnification library will raise an AttributeError.
try:
MagSetFullscreenColorEffect = _MagSetFullscreenColorEffectFuncType(
("MagSetFullscreenColorEffect", _magnification),
_MagSetFullscreenColorEffectArgTypes,
)
MagSetFullscreenColorEffect.errcheck = _errCheck
MagGetFullscreenColorEffect = _MagGetFullscreenColorEffectFuncType(
("MagGetFullscreenColorEffect", _magnification),
_MagGetFullscreenColorEffectArgTypes,
)
MagGetFullscreenColorEffect.errcheck = _errCheck
MagShowSystemCursor = _MagShowSystemCursorFuncType(
("MagShowSystemCursor", _magnification),
_MagShowSystemCursorArgTypes,
)
MagShowSystemCursor.errcheck = _errCheck
except AttributeError:
MagSetFullscreenColorEffect = None
MagGetFullscreenColorEffect = None
MagShowSystemCursor = None
_MagSetWindowSourceFuncType = WINFUNCTYPE(BOOL, HWND, POINTER(RECT))
_MagSetWindowSourceArgTypes = ((1, "hwnd"), (1, "rect"))
MagSetWindowSource = _MagSetWindowSourceFuncType(
("MagSetWindowSource", _magnification),
_MagSetWindowSourceArgTypes,
)
MagSetWindowSource.errcheck = _errCheck

_MagGetWindowSourceFuncType = WINFUNCTYPE(BOOL, HWND, POINTER(RECT))
_MagGetWindowSourceArgTypes = ((1, "hwnd"), (2, "rect"))
MagGetWindowSource = _MagGetWindowSourceFuncType(
("MagGetWindowSource", _magnification),
_MagGetWindowSourceArgTypes,
)
MagGetWindowSource.errcheck = _errCheck

_MagSetWindowTransformFuncType = WINFUNCTYPE(BOOL, HWND, POINTER(MAGTRANSFORM))
_MagSetWindowTransformArgTypes = ((1, "hwnd"), (1, "transform"))
MagSetWindowTransform = _MagSetWindowTransformFuncType(
("MagSetWindowTransform", _magnification),
_MagSetWindowTransformArgTypes,
)
MagSetWindowTransform.errcheck = _errCheck

# Create transformation window
_MagGetWindowTransformFuncType = WINFUNCTYPE(BOOL, HWND, POINTER(MAGTRANSFORM))
_MagGetWindowTransformArgTypes = ((1, "hwnd"), (2, "transform"))
MagGetWindowTransform = _MagGetWindowTransformFuncType(
("MagGetWindowTransform", _magnification),
_MagGetWindowTransformArgTypes,
)
MagGetWindowTransform.errcheck = _errCheck

_MagSetFullscreenTransformFuncType = WINFUNCTYPE(BOOL, POINTER(FLOAT), POINTER(INT), POINTER(INT))
_MagSetFullscreenTransformArgTypes = ((1, "magLevel"), (1, "offsetX"), (1, "offsetY"))
MagSetFullscreenTransform = _MagSetFullscreenTransformFuncType(
("MagSetFullscreenTransform", _magnification),
_MagSetFullscreenTransformArgTypes,
)
MagSetFullscreenTransform.errcheck = _errCheck

_MagGetFullscreenTransformFuncType = WINFUNCTYPE(BOOL, POINTER(FLOAT), POINTER(INT), POINTER(INT))
_MagGetFullscreenTransformArgTypes = ((2, "magLevel"), (2, "offsetX"), (2, "offsetY"))
MagGetFullscreenTransform = _MagGetFullscreenTransformFuncType(
("MagGetFullscreenTransform", _magnification),
_MagGetFullscreenTransformArgTypes,
)
MagGetFullscreenTransform.errcheck = _errCheck

# # Create transformation window
# _MagGetInputTransformFuncType = WINFUNCTYPE(BOOL, POINTER(BOOL), POINTER(RECT), POINTER(RECT))
# _MagGetInputTransformArgTypes = ((2, "enabled"), (2, "src"), (2, "dest"))
# MagGetInputTransform = _MagGetInputTransformFuncType(
# ("MagGetInputTransform", _magnification),
# _MagGetInputTransformArgTypes,
# )
# MagGetInputTransform.errcheck = _errCheck

# # Create transformation window
# _MagSetInputTransformFuncType = WINFUNCTYPE(BOOL, POINTER(BOOL), POINTER(RECT), POINTER(RECT))
# _MagSetInputTransformArgTypes = ((1, "enabled"), (1, "src"), (1, "dest"))
# MagSetInputTransform = _MagGetInputTransformFuncType(
# ("MagSetInputTransform", _magnification),
# _MagSetInputTransformArgTypes,
# )
# MagSetInputTransform.errcheck = _errCheck


# Translators: Name for a vision enhancement provider that disables output to the screen,
Expand Down
5 changes: 4 additions & 1 deletion source/winUser.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ class GUITHREADINFO(Structure):
WS_VSCROLL = 0x200000
WS_CAPTION = 0xC00000
WS_CLIPCHILDREN = 0x02000000
WS_MAXIMIZEBOX = 0x00010000
WS_CHILD = 0x40000000
MS_SHOWMAGNIFIEDCURSOR = 0x0001
WS_EX_TOPMOST = 0x00000008
WS_EX_LAYERED = 0x80000
WS_EX_TOOLWINDOW = 0x00000080
Expand Down Expand Up @@ -533,7 +536,7 @@ def getControlID(hwnd):
return user32.GetWindowLongW(hwnd, GWL_ID)


def getClientRect(hwnd):
def getClientRect(hwnd: HWND) -> RECT:
r = RECT()
if not user32.GetClientRect(hwnd, byref(r)):
raise WinError()
Expand Down
6 changes: 6 additions & 0 deletions tests/unit/test_visionEnhancementProviders/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2024 NV Access Limited.

"""Unit tests for the vision enhancement providers submodule."""
Loading

0 comments on commit 1ba7192

Please sign in to comment.