Skip to content

Commit

Permalink
* Added a script to move the mouse in the center of magnified view.
Browse files Browse the repository at this point in the history
* Added some other code rework.
  • Loading branch information
CyrilleB79 committed Feb 25, 2020
1 parent 73c560e commit 2bf2ce1
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 44 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
60 changes: 22 additions & 38 deletions TODOList.txt
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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.



62 changes: 56 additions & 6 deletions addon/globalPlugins/winMag/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from __future__ import unicode_literals

from .msg import nvdaTranslation
from .magnification import Magnification

import globalPluginHandler
import ui
Expand All @@ -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:
Expand Down Expand Up @@ -97,16 +102,20 @@ 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:
return o
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

Expand Down Expand Up @@ -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")

Expand All @@ -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),
]

Expand Down Expand Up @@ -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}'
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
108 changes: 108 additions & 0 deletions addon/globalPlugins/winMag/magnification.py
Original file line number Diff line number Diff line change
@@ -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


0 comments on commit 2bf2ce1

Please sign in to comment.