From 2bf2ce19a2161a24cc7152290c1496fc4d8816ef Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Tue, 25 Feb 2020 23:24:23 +0100 Subject: [PATCH] * Added a script to move the mouse in the center of magnified view. * Added some other code rework. --- README.md | 1 + TODOList.txt | 60 ++++------- addon/globalPlugins/winMag/__init__.py | 62 +++++++++-- addon/globalPlugins/winMag/magnification.py | 108 ++++++++++++++++++++ 4 files changed, 187 insertions(+), 44 deletions(-) create mode 100644 addon/globalPlugins/winMag/magnification.py diff --git a/README.md b/README.md index 98382b8..7d8bbee 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ All the commands added to control Magnifier options are accessible through the M * NVDA+Windows+O then T: Toggles on or off tracking globally. * NVDA+Windows+O then S: Toggles on or off smoothing. * NVDA+Windows+O then R: Switches between mouse tracking modes (within the edge of the screen or centered on the screen); this feature is only available on Windows 10 build 17643 or higher. +* NVDA+Windows+O then V: Moves the mouse cursor in the center of the magnified view (command available in full screen view only). * NVDA+Windows+O then H: Displays help on Magnifier layer commands. There is no default gesture for each command, but you can attribute one normally in the input gesture dialog if you wish. The same way, You can also modify or delete the Magnifier layer access gesture (NVDA+Windows+O). Yet, you cannot modify the shortcut key of the Magnifier layer sub-commands. diff --git a/TODOList.txt b/TODOList.txt index 6dfc34d..7882a2f 100644 --- a/TODOList.txt +++ b/TODOList.txt @@ -1,37 +1,7 @@ TODO list: -Review: -DONE: -1. Magnifier shortcut keys are now translatable to match localized keyboard layout. -2. Note that ctrl+alt+arrow keys also turn the screen orientation if you have an Intel Grafic card. -> Why don't you turn off those shortkeys in the intel controlpanel? -> https://ccm.net/faq/33684-disable-hot-keys-on-intel-graphics -3. windows+alt+m is already used by the addon golden cursor. -> windows+nvda+v for “vision”)? --> Put NVDA+Windows+O. I find more handy to type this combination specially on some keyboard that do not have windows key on the right. Since this is the layer access key, it should be easy to type. -4. Browsible help message - -DONE with comments: -1. In buildVars.py: could you please add an addon URL? Also an entry for the update channel is very welcome if you want to update this addon regularly -- Github URL added. Is this OK -- buildVars.py has no channel, what corresponds to stable. But building the add-on with "scons dev=True" makes a dev version and add-on channel is updated accordingly in the manifest. AppVeyor config file takes advantage of this possibility. -2. control+alt+arrows vocal feedback -- What is expected as a feedback -- Implemented as a test, not sure to keep it. - -PARTIALLY DONE with comments: -3. alt+shift+arrow keys and ctrl+alt+r do also not report anything. If it is not possible to add reportings for these, please add a sentence to the documentation accordingly -alt+shift+arrow: -- docked (dim) + fullscreen (ok) -- lens: info but dim not available -ctrl+alt+R: Not done. How does it work exactly? What is the info to give and when. - -BLOCKED -1. Announce zoom when clicking on + or - in Magnifier UI (and when using control+alt+mouseWheel) -Trying to define event_nameChange to announce a change of the zoom factor label. The event handler is not called. -Trying to look at events with AccEvent, I get the following line: -OBJ_NAMECHANGE idChild=0 [Error: getting object: hr=0x80004005 - Erreur non spécifiée] -i.e. "Unspecified error" - +Before stable release: +Delete vocal feedback for move commands unless good reason to keep it. Known bugs: - ctrl+alt+M quickly: may say nothing or wrong value. @@ -44,9 +14,23 @@ Possible code enhancement (to investigate): - Try to move layered command framework in a separate dedicated class New features investigation: -- See if possible to implement moveToCaret/Focus/Mouse scripts. See: -* can we fire artificially caret/focus/mouse event -* see https://docs.microsoft.com/en-us/windows/win32/api/magnification/nf-magnification-magsetfullscreentransform -- See if possibl to get programmatically coords of current view to route mouse to view. See: -* https://docs.microsoft.com/fr-fr/windows/win32/api/magnification/nf-magnification-maggetfullscreentransform -* https://social.msdn.microsoft.com/Forums/en-US/8484d886-93e0-4bd7-ac6d-0e019d1bdace/how-do-i-get-the-bounds-of-the-current-windows-magnifier-view-displayed-on-the-screen-and-the-screen?forum=windowsgeneraldevelopmentissues) +* Announce zoom when clicking on + or - in Magnifier UI (and when using control+alt+mouseWheel) + -> Tried to define event_nameChange to announce a change of the zoom factor label. The event handler is not called. + - Tried to look at events with AccEvent, I get the following line: + OBJ_NAMECHANGE idChild=0 [Error: getting object: hr=0x80004005 - Erreur non spécifiée] + i.e. "Unspecified error" +* See if possible to implement moveToCaret/Focus/Mouse scripts. See: + - For mouse, tried to activate mouse tracking and mouseTrackingMode=inTheCenterOfTheScreen and to make slightly move the mouse cursor (via winUser.setCursorPos). + -> This works when the mouse cursor is in the magnified view or next to it, but not when it is too far from magnified view.. + -> See if more success for caret or focus by fireing associated windows events. + - Try to compute the number of left/right/up/down pane required to get mouse/caret/focus next to the center of the view and send programmatically control+alt+arrow to move the view. + -> To do +* mouseToView: + - full screen: already implemented + - docked: tried to get the source of magnified view via magnifier API, but unsuccessful. + - lens: not applicable: the zoomed view has always the mouse in center since this is the way the lens works. +* Beep when paning the view if we reached bhe screen edge. Or configurable (nothing, beep, message). +* Make paning commands feedback configurable (nothing, beep or direction); at least config without UI. + + + diff --git a/addon/globalPlugins/winMag/__init__.py b/addon/globalPlugins/winMag/__init__.py index c461783..8dcc7f1 100644 --- a/addon/globalPlugins/winMag/__init__.py +++ b/addon/globalPlugins/winMag/__init__.py @@ -8,6 +8,7 @@ from __future__ import unicode_literals from .msg import nvdaTranslation +from .magnification import Magnification import globalPluginHandler import ui @@ -16,7 +17,11 @@ from tones import beep from scriptHandler import script from logHandler import log +import mouseHandler import globalVars +import winUser + +import wx import sys try: @@ -97,9 +102,10 @@ def toggleMagnifierKeyValue(name, default=None): def isMagnifierRunning(): # We do not use the existing RunningState registry key because does not work in the following use case: # User logs off while Mag is active, then user logs on again. Even if Mag is not yet started by the user, the registry still holds RunningState value to 1. - return getDesktopChildObj('MagUIClass') is not None + # Instead we use the Magnifier UI window that is always present, even if hidden. + return getDesktopChildObject('MagUIClass') is not None -def getDesktopChildObj(windowClassName): +def getDesktopChildObject(windowClassName): o = api.getDesktopObject().firstChild while o: if o.windowClassName == windowClassName: @@ -107,6 +113,9 @@ def getDesktopChildObj(windowClassName): o = o.next return None +def getDockedWindowObject(): + return getDesktopChildObject(windowClassName="Screen Magnifier Window") + def isFullScreenView(): return getMagnifierKeyValue('MagnificationMode', default=MAG_DEFAULT_MAGNIFICATION_MODE) == MAG_VIEW_FULLSCREEN @@ -286,6 +295,8 @@ def toggle(self, eventType): DESC_TOGGLE_SMOOTHING = _("Toggles on or off smoothing") # Translators: The description for the toggleMouseCursorTrackingMode script. DESC_TOGGLE_MOUSE_CURSOR_TRACKING_MODE = _("Switches between mouse tracking modes (within the edge of the screen or centered on the screen)") +# Translators: The description for the moveMouseToView script. +DESC_MOVE_MOUSE_TO_VIEW = _("Moves the mouse cursor in the center of the zoomed view") # Translators: The description for the displayHelp script. DESC_DISPLAY_HELP = _("Displays help on Magnifier layer commands") @@ -301,6 +312,7 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin): ("t", "toggleTracking", DESC_TOGGLE_TRACKING), ("s", "toggleSmoothing", DESC_TOGGLE_SMOOTHING), ("r", "toggleMouseCursorTrackingMode", DESC_TOGGLE_MOUSE_CURSOR_TRACKING_MODE), + ("v", "moveMouseToView", DESC_MOVE_MOUSE_TO_VIEW), ("h", "displayHelp", DESC_DISPLAY_HELP), ] @@ -413,7 +425,7 @@ def script_changeMagnificationWindowSize(self, gesture): gesture.send() mode = getMagnifierKeyValue('MagnificationMode', default=MAG_DEFAULT_MAGNIFICATION_MODE) if mode == MAG_VIEW_DOCKED: - oMag = getDesktopChildObj("Screen Magnifier Window") + oMag = getDockedWindowObject() curResize = 'width' if gesture.mainKeyName in ['leftArrow', 'rightArrow'] else 'height' announceDim = curResize != self.lastResize or not scriptHandler.getLastScriptRepeatCount() msg = '{dimension}: {val}' if announceDim else '{val}' @@ -526,9 +538,7 @@ def script_toggleSmoothing(self, gesture): def script_toggleMouseCursorTrackingMode(self, gesture): if self.checkSecureScreen(): return - # Feature available on Windows 10 build 17643 or higher. - winVer = sys.getwindowsversion() - if winVer.major < 10 or winVer.build < 17643: + if not self.isFullScreenTrackingModeAvailable(): # Translators: The message reported when the user tries to toggle mouse tracking mode whereas his Windows version does not support it. ui.message(_('Feature unavailable in this version of Windows.')) return @@ -545,12 +555,52 @@ def script_toggleMouseCursorTrackingMode(self, gesture): # Translators: A message reporting mouse cursor tracking mode (cf. option in Magnifier settings). ui.message(_('Within the edge of the screen')) + @script( + description = DESC_MOVE_MOUSE_TO_VIEW + ) + @onlyIfMagRunning + def script_moveMouseToView(self, gesture): + mode = getMagnifierKeyValue('MagnificationMode', default=MAG_DEFAULT_MAGNIFICATION_MODE) + if mode == MAG_VIEW_LENS: + # Translators: A message reported when the user tries to execute script mouseToView + ui.message(_('Move mouse to view not applicable with lense view.')) + return + Magnification.MagInitialize() + try: + if mode == MAG_VIEW_FULLSCREEN: + zoomLevel, viewLeft, viewTop = Magnification.MagGetFullscreenTransform() + elif mode == MAG_VIEW_DOCKED: + # o = getDockedWindowObject() + # hwnd = o.windowHandle + # # Error on next line + # rect = Magnification.MagGetWindowSource(hwnd) + # Translators: A message reported when the user tries to execute script mouseToView + ui.message(_('Move mouse to view not implemented for docked view')) + finally: + Magnification.MagUninitialize() + if wx.Display.GetCount() != 1: + # Translators: A message reported when the user tries to execute script mouseToView + ui.message(_('This command is not yet available in multi-screen environment. Please contact the add-on author to have it implemented.')) + return + rect = wx.Display(0).GetGeometry() + viewHeight = rect.height / zoomLevel + viewWidth = rect.width / zoomLevel + x = viewLeft + int(viewWidth/2) + y = viewTop + int(viewHeight/2) + winUser.setCursorPos(x,y) + mouseHandler.executeMouseMoveEvent(x,y) + def checkSecureScreen(self): if globalVars.appArgs.secure: # Translators: A message reported in secure screen when the user attempts to modify magnifiers settings. ui.message(_('Command unavailable on this screen.')) return globalVars.appArgs.secure + def isFullScreenTrackingModeAvailable(self): + # Full screen tracking mode feature is available on Windows 10 build 17643 or higher. + winVer = sys.getwindowsversion() + return not (winVer.major < 10 or winVer.build < 17643) + def modifyRunningState(self, gesture): fetcher = lambda: getMagnifierKeyValue('RunningState', default=MAG_DEFAULT_RUNNING_STATE) val = _WaitForValueChangeForAction(gesture, fetcher, timeout=4) diff --git a/addon/globalPlugins/winMag/magnification.py b/addon/globalPlugins/winMag/magnification.py new file mode 100644 index 0000000..6732394 --- /dev/null +++ b/addon/globalPlugins/winMag/magnification.py @@ -0,0 +1,108 @@ +# 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) 2020 Cyrille Bougot, NV Access Limited + +import winVersion +from ctypes import Structure, windll, c_float, POINTER, WINFUNCTYPE, WinError +from ctypes.wintypes import BOOL +from ctypes.wintypes import HWND, PRECT, PINT, PFLOAT, INT, FLOAT + + +class MAGCOLOREFFECT(Structure): + _fields_ = (("transform", c_float * 5 * 5),) + + +# 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() +TRANSFORM_BLACK.transform[4][4] = 1.0 + + +def _errCheck(result, func, args): + if result == 0: + raise WinError() + return args + + +class Magnification: + """Static class that wraps necessary functions from the Windows magnification API.""" + + _magnification = windll.Magnification + + # Set full screen color effect + _MagSetFullscreenColorEffectFuncType = WINFUNCTYPE(BOOL, POINTER(MAGCOLOREFFECT)) + _MagSetFullscreenColorEffectArgTypes = ((1, "effect"),) + + # Get full screen color effect + _MagGetFullscreenColorEffectFuncType = WINFUNCTYPE(BOOL, POINTER(MAGCOLOREFFECT)) + _MagGetFullscreenColorEffectArgTypes = ((2, "effect"),) + + # SetFullscreenTransform + _MagSetFullscreenTransformFuncType = WINFUNCTYPE(BOOL, FLOAT, INT, INT) + _MagSetFullscreenTransformArgTypes = ((1, "magLevel"), (1, "xOffset"), (1, "yOffset")) + + # GetFullscreenTransform + _MagGetFullscreenTransformFuncType = WINFUNCTYPE(BOOL, PFLOAT, PINT, PINT) + _MagGetFullscreenTransformArgTypes = ((2, "MagLevel"), (2, "xOffset"), (2, "yOffset")) + + # show system cursor + _MagShowSystemCursorFuncType = WINFUNCTYPE(BOOL, BOOL) + _MagShowSystemCursorArgTypes = ((1, "showCursor"),) + + # GetWindowSource + _MagGetWindowSourceFuncType = WINFUNCTYPE(BOOL, HWND, PRECT) + _MagGetWindowSourceArgTypes = ((1, "hwnd"), (2, "pRect")) + + # initialize + _MagInitializeFuncType = WINFUNCTYPE(BOOL) + MagInitialize = _MagInitializeFuncType(("MagInitialize", _magnification)) + MagInitialize.errcheck = _errCheck + + # uninitialize + _MagUninitializeFuncType = WINFUNCTYPE(BOOL) + 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 + MagSetFullscreenTransform = _MagSetFullscreenTransformFuncType( + ("MagSetFullscreenTransform", _magnification), + _MagSetFullscreenTransformArgTypes + ) + MagSetFullscreenTransform.errcheck = _errCheck + MagGetFullscreenTransform = _MagGetFullscreenTransformFuncType( + ("MagGetFullscreenTransform", _magnification), + _MagGetFullscreenTransformArgTypes + ) + MagGetFullscreenTransform.errcheck = _errCheck + MagShowSystemCursor = _MagShowSystemCursorFuncType( + ("MagShowSystemCursor", _magnification), + _MagShowSystemCursorArgTypes + ) + MagShowSystemCursor.errcheck = _errCheck + + MagGetWindowSource = _MagGetWindowSourceFuncType( + ("MagGetWindowSource", _magnification), + _MagGetWindowSourceArgTypes + ) + MagGetWindowSource.errcheck = _errCheck + except AttributeError: + MagSetFullscreenColorEffect = None + MagGetFullscreenColorEffect = None + MagSetFullscreenTransform = None + MagGetFullscreenTransform = None + MagShowSystemCursor = None + +