Skip to content

Commit 7db453d

Browse files
authored
Merge 4a08c36 into 96fe482
2 parents 96fe482 + 4a08c36 commit 7db453d

4 files changed

Lines changed: 262 additions & 4 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from ctypes import WinError
2+
from ctypes.wintypes import RECT
3+
4+
from locationHelper import RectLTRB, RectLTWH
5+
from logHandler import log
6+
from vision import (
7+
_isDebug,
8+
)
9+
from .screenCurtain import MAGTRANSFORM, Magnification
10+
from winAPI import _displayTracking
11+
from windowUtils import CustomWindow
12+
import winUser
13+
14+
WindowClassName = "MagnifierWindow"
15+
WindowTitle = "Screen Magnifier Sample"
16+
WC_MAGNIFIER = "Magnifier"
17+
RESTOREDWINDOWSTYLES = (
18+
winUser.WS_SIZEBOX
19+
| winUser.WS_SYSMENU
20+
| winUser.WS_CLIPCHILDREN
21+
| winUser.WS_CAPTION
22+
| winUser.WS_MAXIMIZEBOX
23+
)
24+
25+
26+
class HostWindow(CustomWindow):
27+
className = WindowClassName
28+
windowName = WindowTitle
29+
windowsStyle = RESTOREDWINDOWSTYLES
30+
extendedWindowStyle = (
31+
# Ensure that the window is on top of all other windows
32+
winUser.WS_EX_TOPMOST
33+
# A layered window ensures that L{transparentColor} will be considered transparent, when painted
34+
| winUser.WS_EX_LAYERED
35+
)
36+
37+
def __init__(self, magnificationFactor: int = 2):
38+
super().__init__(
39+
windowName=self.windowName,
40+
windowStyle=self.windowsStyle,
41+
extendedWindowStyle=self.extendedWindowStyle,
42+
parent=None,
43+
)
44+
winUser.SetLayeredWindowAttributes(
45+
self.handle,
46+
0x00,
47+
0xFF,
48+
winUser.LWA_ALPHA,
49+
)
50+
if not winUser.user32.SetWindowPos(
51+
self.handle,
52+
winUser.HWND_TOPMOST,
53+
self.targetRect.left,
54+
self.targetRect.top,
55+
self.targetRect.width,
56+
int(self.targetRect.height),
57+
winUser.SWP_NOACTIVATE | winUser.SWP_NOMOVE | winUser.SWP_NOSIZE,
58+
):
59+
raise WinError()
60+
if not winUser.user32.UpdateWindow(self.handle):
61+
raise WinError()
62+
self.magnifierWindow = MagnifierWindow(self, magnificationFactor)
63+
64+
@property
65+
def targetRect(self) -> RectLTRB:
66+
# Top quarter of screen
67+
return RectLTRB(
68+
0,
69+
0,
70+
_displayTracking._orientationState.width,
71+
_displayTracking._orientationState.height / 4,
72+
)
73+
74+
def windowProc(self, hwnd: int, msg: int, wParam: int, lParam: int):
75+
log.debug(f"received window proc message: {msg}")
76+
77+
78+
class MagnifierWindow(CustomWindow):
79+
className = WC_MAGNIFIER
80+
windowName = "MagnifierWindow"
81+
windowStyle = winUser.WS_CHILD | winUser.MS_SHOWMAGNIFIEDCURSOR | winUser.WS_VISIBLE
82+
83+
def __init__(self, hostWindow: HostWindow, magnificationFactor: int = 2):
84+
self.hostWindow = hostWindow
85+
self.magnificationFactor = magnificationFactor
86+
if _isDebug():
87+
log.debug("initializing NVDA Magnifier window")
88+
super().__init__(
89+
windowName=self.windowName,
90+
windowStyle=self.windowStyle,
91+
parent=hostWindow.handle,
92+
)
93+
94+
magWindowRect = self.magWindowRect
95+
if not winUser.user32.SetWindowPos(
96+
self.handle,
97+
winUser.HWND_TOPMOST,
98+
magWindowRect.left,
99+
magWindowRect.top,
100+
magWindowRect.width,
101+
magWindowRect.height,
102+
winUser.SWP_NOACTIVATE | winUser.SWP_NOMOVE | winUser.SWP_NOSIZE,
103+
):
104+
raise WinError()
105+
if not winUser.user32.UpdateWindow(self.handle):
106+
raise WinError()
107+
108+
Magnification.MagSetWindowSource(self.handle, RECT(200, 200, 700, 700))
109+
Magnification.MagSetWindowTransform(self.handle, MAGTRANSFORM(self.magnificationFactor))
110+
111+
@property
112+
def magWindowRect(self) -> RectLTWH:
113+
r = winUser.getClientRect(self.hostWindow.handle)
114+
return RectLTRB(
115+
r.left,
116+
r.top,
117+
r.right,
118+
r.bottom,
119+
).toLTWH()
120+
121+
def windowProc(self, hwnd: int, msg: int, wParam: int, lParam: int):
122+
log.debug(f"received window proc message: {msg}")

source/visionEnhancementProviders/screenCurtain.py

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# A part of NonVisual Desktop Access (NVDA)
22
# This file is covered by the GNU General Public License.
33
# See the file COPYING for more details.
4-
# Copyright (C) 2018-2023 NV Access Limited, Babbage B.V., Leonard de Ruijter
4+
# Copyright (C) 2018-2024 NV Access Limited, Babbage B.V., Leonard de Ruijter
55

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

3232

33+
class MAGTRANSFORM(Structure):
34+
_fields_ = (("v", c_float * 3 * 3),)
35+
36+
def __init__(self, magnificationFactor: float = 1.0):
37+
"""
38+
https://learn.microsoft.com/en-us/windows/win32/api/magnification/ns-magnification-magtransform
39+
40+
:param magnificationFactor: defaults to 1.0.
41+
The minimum value of this parameter is 1.0, and the maximum value is 4096.0.
42+
If this value is 1.0, the screen content is not magnified and no offsets are applied.
43+
"""
44+
super().__init__()
45+
assert 1.0 <= magnificationFactor <= 4096.0
46+
self.v[0][0] = magnificationFactor
47+
self.v[1][1] = magnificationFactor
48+
self.v[2][2] = 1.0
49+
50+
3351
# homogeneous matrix for a 4-space transformation (red, green, blue, opacity).
3452
# https://docs.microsoft.com/en-gb/windows/win32/gdiplus/-gdiplus-using-a-color-matrix-to-transform-a-single-color-use
3553
TRANSFORM_BLACK = MAGCOLOREFFECT() # empty transformation
@@ -85,6 +103,73 @@ class Magnification:
85103
MagUninitialize = _MagUninitializeFuncType(("MagUninitialize", _magnification))
86104
MagUninitialize.errcheck = _errCheck
87105

106+
_MagSetWindowSourceFuncType = WINFUNCTYPE(BOOL, HWND, POINTER(RECT))
107+
_MagSetWindowSourceArgTypes = ((1, "hwnd"), (1, "rect"))
108+
MagSetWindowSource = _MagSetWindowSourceFuncType(
109+
("MagSetWindowSource", _magnification),
110+
_MagSetWindowSourceArgTypes,
111+
)
112+
MagSetWindowSource.errcheck = _errCheck
113+
114+
_MagGetWindowSourceFuncType = WINFUNCTYPE(BOOL, HWND, POINTER(RECT))
115+
_MagGetWindowSourceArgTypes = ((1, "hwnd"), (2, "rect"))
116+
MagGetWindowSource = _MagGetWindowSourceFuncType(
117+
("MagGetWindowSource", _magnification),
118+
_MagGetWindowSourceArgTypes,
119+
)
120+
MagGetWindowSource.errcheck = _errCheck
121+
122+
_MagSetWindowTransformFuncType = WINFUNCTYPE(BOOL, HWND, POINTER(MAGTRANSFORM))
123+
_MagSetWindowTransformArgTypes = ((1, "hwnd"), (1, "transform"))
124+
MagSetWindowTransform = _MagSetWindowTransformFuncType(
125+
("MagSetWindowTransform", _magnification),
126+
_MagSetWindowTransformArgTypes,
127+
)
128+
MagSetWindowTransform.errcheck = _errCheck
129+
130+
# Create transformation window
131+
_MagGetWindowTransformFuncType = WINFUNCTYPE(BOOL, HWND, POINTER(MAGTRANSFORM))
132+
_MagGetWindowTransformArgTypes = ((1, "hwnd"), (2, "transform"))
133+
MagGetWindowTransform = _MagGetWindowTransformFuncType(
134+
("MagGetWindowTransform", _magnification),
135+
_MagGetWindowTransformArgTypes,
136+
)
137+
MagGetWindowTransform.errcheck = _errCheck
138+
139+
_MagSetFullscreenTransformFuncType = WINFUNCTYPE(BOOL, POINTER(FLOAT), POINTER(INT), POINTER(INT))
140+
_MagSetFullscreenTransformArgTypes = ((1, "magLevel"), (1, "offsetX"), (1, "offsetY"))
141+
MagSetFullscreenTransform = _MagSetFullscreenTransformFuncType(
142+
("MagSetFullscreenTransform", _magnification),
143+
_MagSetFullscreenTransformArgTypes,
144+
)
145+
MagSetFullscreenTransform.errcheck = _errCheck
146+
147+
_MagGetFullscreenTransformFuncType = WINFUNCTYPE(BOOL, POINTER(FLOAT), POINTER(INT), POINTER(INT))
148+
_MagGetFullscreenTransformArgTypes = ((2, "magLevel"), (2, "offsetX"), (2, "offsetY"))
149+
MagGetFullscreenTransform = _MagGetFullscreenTransformFuncType(
150+
("MagGetFullscreenTransform", _magnification),
151+
_MagGetFullscreenTransformArgTypes,
152+
)
153+
MagGetFullscreenTransform.errcheck = _errCheck
154+
155+
# # Create transformation window
156+
# _MagGetInputTransformFuncType = WINFUNCTYPE(BOOL, POINTER(BOOL), POINTER(RECT), POINTER(RECT))
157+
# _MagGetInputTransformArgTypes = ((2, "enabled"), (2, "src"), (2, "dest"))
158+
# MagGetInputTransform = _MagGetInputTransformFuncType(
159+
# ("MagGetInputTransform", _magnification),
160+
# _MagGetInputTransformArgTypes,
161+
# )
162+
# MagGetInputTransform.errcheck = _errCheck
163+
164+
# # Create transformation window
165+
# _MagSetInputTransformFuncType = WINFUNCTYPE(BOOL, POINTER(BOOL), POINTER(RECT), POINTER(RECT))
166+
# _MagSetInputTransformArgTypes = ((1, "enabled"), (1, "src"), (1, "dest"))
167+
# MagSetInputTransform = _MagGetInputTransformFuncType(
168+
# ("MagSetInputTransform", _magnification),
169+
# _MagSetInputTransformArgTypes,
170+
# )
171+
# MagSetInputTransform.errcheck = _errCheck
172+
88173

89174
# Translators: Name for a vision enhancement provider that disables output to the screen,
90175
# making it black.

source/winUser.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ class GUITHREADINFO(Structure):
142142
WS_VSCROLL = 0x200000
143143
WS_CAPTION = 0xC00000
144144
WS_CLIPCHILDREN = 0x02000000
145+
WS_MAXIMIZEBOX = 0x00010000
146+
WS_CHILD = 0x40000000
147+
MS_SHOWMAGNIFIEDCURSOR = 0x0001
145148
WS_EX_TOPMOST = 0x00000008
146149
WS_EX_LAYERED = 0x80000
147150
WS_EX_TOOLWINDOW = 0x00000080
@@ -533,7 +536,7 @@ def getControlID(hwnd):
533536
return user32.GetWindowLongW(hwnd, GWL_ID)
534537

535538

536-
def getClientRect(hwnd):
539+
def getClientRect(hwnd: HWND) -> RECT:
537540
r = RECT()
538541
if not user32.GetClientRect(hwnd, byref(r)):
539542
raise WinError()

tests/unit/test_visionEnhancementProviders/test_magnificationAPI.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
import unittest
99

10-
from visionEnhancementProviders.screenCurtain import Magnification, TRANSFORM_BLACK
10+
from winAPI import _displayTracking
11+
from visionEnhancementProviders.magnifier import HostWindow
12+
from visionEnhancementProviders.screenCurtain import MAGTRANSFORM, Magnification, TRANSFORM_BLACK
1113

1214

1315
class _Test_MagnificationAPI(unittest.TestCase):
@@ -53,3 +55,49 @@ def test_MagShowSystemCursor(self):
5355
def test_MagHideSystemCursor(self):
5456
result = Magnification.MagShowSystemCursor(False)
5557
self.assertTrue(result)
58+
59+
60+
class Test_Magnification(unittest.TestCase):
61+
def setUp(self):
62+
self.hostWindow: HostWindow | None = None
63+
self._prevOrientationState = _displayTracking._orientationState
64+
self.assertIsNone(self._prevOrientationState)
65+
_displayTracking.initialize()
66+
self.assertTrue(Magnification.MagInitialize())
67+
68+
def tearDown(self):
69+
self.assertTrue(Magnification.MagUninitialize())
70+
_displayTracking._orientationState = self._prevOrientationState
71+
if self.hostWindow:
72+
self.hostWindow.destroy()
73+
74+
def _initializeMagWindow(self, magnificationFactor: int = 1):
75+
self.hostWindow = HostWindow(magnificationFactor)
76+
self.assertTrue(self.hostWindow.handle)
77+
self.assertTrue(self.hostWindow.magnifierWindow.handle)
78+
79+
def test_setAndConfirmMagLevel(self):
80+
expectedTransform = MAGTRANSFORM(2)
81+
self._initializeMagWindow(2)
82+
resultTransform = Magnification.MagGetWindowTransform(self.hostWindow.magnifierWindow.handle)
83+
for i in range(3):
84+
for j in range(3):
85+
with self.subTest(i=i, j=j):
86+
self.assertEqual(
87+
expectedTransform.v[i][j],
88+
resultTransform.v[i][j],
89+
msg=f"i={i}, j={j}, resultTransform={resultTransform}",
90+
)
91+
92+
def test_getDefaultIdentityMagLevel(self):
93+
self._initializeMagWindow()
94+
resultTransform = Magnification.MagGetWindowTransform(self.hostWindow.magnifierWindow.handle)
95+
for i in range(3):
96+
for j in range(3):
97+
with self.subTest(i=i, j=j):
98+
self.assertEqual(
99+
# The transform matrix should be the identity matrix
100+
int(i == j),
101+
resultTransform.v[i][j],
102+
msg=f"i={i}, j={j}, resultTransform={resultTransform}",
103+
)

0 commit comments

Comments
 (0)