diff --git a/source/globalCommands.py b/source/globalCommands.py index 8312dd3003d..088fd92f7f4 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -43,6 +43,7 @@ import core import winVersion from base64 import b16encode +import vision #: Script category for text review commands. # Translators: The name of a category of NVDA commands. @@ -68,6 +69,9 @@ #: Script category for Braille commands. # Translators: The name of a category of NVDA commands. SCRCAT_BRAILLE = _("Braille") +#: Script category for Vision commands. +# Translators: The name of a category of NVDA commands. +SCRCAT_VISION = _("Vision") #: Script category for tools commands. # Translators: The name of a category of NVDA commands. SCRCAT_TOOLS = pgettext('script category', 'Tools') @@ -2278,6 +2282,49 @@ def script_recognizeWithUwpOcr(self, gesture): # Translators: Describes a command. script_recognizeWithUwpOcr.__doc__ = _("Recognizes the content of the current navigator object with Windows 10 OCR") + @script( + # Translators: Describes a command. + description=_( + "Toggles the state of the screen curtain, " + "either hiding or SHOWING the contents of the screen. " + "If pressed to enable once, the screen curtain is enabled until you restart NVDA. " + "If pressed tree times, it is enabled until you disable it" + ), + category=SCRCAT_VISION + ) + def script_toggleScreenCurtain(self, gesture): + message = None + try: + if not winVersion.isFullScreenMagnificationAvailable(): + return + scriptCount = scriptHandler.getLastScriptRepeatCount() + screenCurtainName = "screenCurtain" + if scriptCount == 0 and screenCurtainName in vision.handler.providers: + vision.handler.terminateProvider(screenCurtainName) + # Translators: Reported when the screen curtain is disabled. + message = _("Screen curtain disabled") + elif scriptCount in (0, 2): + temporary = scriptCount == 0 + if not vision.handler.initializeProvider( + screenCurtainName, + temporary=temporary, + ): + # Translators: Reported when the screen curtain could not be enabled. + message = _("Could not enable screen curtain") + return + else: + if temporary: + # Translators: Reported when the screen curtain is temporarily enabled. + message = _("Screen curtain enabled until next restart") + else: + # Translators: Reported when the screen curtain is enabled. + message = _("Screen curtain enabled") + finally: + if message is None: + # Translators: Reported when the screen curtain is not available. + message = _("Screen curtain not available") + ui.message(message, speechPriority=speech.priorities.SPRI_NOW) + __gestures = { # Basic "kb:NVDA+n": "showGui", diff --git a/source/ui.py b/source/ui.py index ca19d3c9bf6..829054b09d6 100644 --- a/source/ui.py +++ b/source/ui.py @@ -18,6 +18,8 @@ import gui import speech import braille +from typing import Optional + # From urlmon.h URL_MK_UNIFORM = 1 @@ -64,21 +66,23 @@ def browseableMessage(message,title=None,isHtml=False): ) gui.mainFrame.postPopup() -def message(text): +def message(text: str, speechPriority: Optional[int] = None): """Present a message to the user. The message will be presented in both speech and braille. @param text: The text of the message. - @type text: str + @param speechPriority: The speech priority. + One of the C{speech.priorities.SPRI_*} constants. """ - speech.speakMessage(text) + speech.speakMessage(text, priority=speechPriority) braille.handler.message(text) -def reviewMessage(text): +def reviewMessage(text: str, speechPriority: Optional[int] = None): """Present a message from review or object navigation to the user. The message will always be presented in speech, and also in braille if it is tethered to review or when auto tethering is on. @param text: The text of the message. - @type text: str + @param speechPriority: The speech priority. + One of the C{speech.priorities.SPRI_*} constants. """ - speech.speakMessage(text) + speech.speakMessage(text, priority=speechPriority) if braille.handler.shouldAutoTether or braille.handler.getTether() == braille.handler.TETHER_REVIEW: braille.handler.message(text) diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py new file mode 100644 index 00000000000..0de2f42455c --- /dev/null +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -0,0 +1,84 @@ +# 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-2019 NV Access Limited, Babbage B.V., Leonard de Ruijter + +"""Screen curtain implementation based on the windows magnification API. +This implementation only works on Windows 8 and above. +""" + +import vision +import winVersion +from ctypes import Structure, windll, c_float, POINTER, WINFUNCTYPE, WinError +from ctypes.wintypes import BOOL + + +class MAGCOLOREFFECT(Structure): + _fields_ = (("transform", c_float * 5 * 5),) + + +TRANSFORM_BLACK = MAGCOLOREFFECT() +TRANSFORM_BLACK.transform[4][4] = 1.0 + + +def _errCheck(result, func, args): + if result == 0: + raise WinError() + return args + + +class Magnification: + """Singleton that wraps necessary functions from the Windows magnification API.""" + + _magnification = windll.Magnification + + _MagInitializeFuncType = WINFUNCTYPE(BOOL) + _MagUninitializeFuncType = WINFUNCTYPE(BOOL) + _MagSetFullscreenColorEffectFuncType = WINFUNCTYPE(BOOL, POINTER(MAGCOLOREFFECT)) + _MagSetFullscreenColorEffectArgTypes = ((1, "effect"),) + _MagGetFullscreenColorEffectFuncType = WINFUNCTYPE(BOOL, POINTER(MAGCOLOREFFECT)) + _MagGetFullscreenColorEffectArgTypes = ((2, "effect"),) + + MagInitialize = _MagInitializeFuncType(("MagInitialize", _magnification)) + MagInitialize.errcheck = _errCheck + MagUninitialize = _MagUninitializeFuncType(("MagUninitialize", _magnification)) + MagUninitialize.errcheck = _errCheck + try: + MagSetFullscreenColorEffect = _MagSetFullscreenColorEffectFuncType( + ("MagSetFullscreenColorEffect", _magnification), + _MagSetFullscreenColorEffectArgTypes + ) + MagSetFullscreenColorEffect.errcheck = _errCheck + MagGetFullscreenColorEffect = _MagGetFullscreenColorEffectFuncType( + ("MagGetFullscreenColorEffect", _magnification), + _MagGetFullscreenColorEffectArgTypes + ) + MagGetFullscreenColorEffect.errcheck = _errCheck + except AttributeError: + MagSetFullscreenColorEffect = None + MagGetFullscreenColorEffect = None + + +class VisionEnhancementProvider(vision.providerBase.VisionEnhancementProvider): + name = "screenCurtain" + # Translators: Description of a vision enhancement provider that disables output to the screen, + # making it black. + description = _("Screen Curtain") + supportedRoles = frozenset([vision.constants.Role.COLORENHANCER]) + + @classmethod + def canStart(cls): + return winVersion.isFullScreenMagnificationAvailable() + + def __init__(self): + super(VisionEnhancementProvider, self).__init__() + Magnification.MagInitialize() + Magnification.MagSetFullscreenColorEffect(TRANSFORM_BLACK) + + def terminate(self): + super(VisionEnhancementProvider, self).terminate() + Magnification.MagUninitialize() + + def registerEventExtensionPoints(self, extensionPoints): + # The screen curtain isn't interested in any events + pass diff --git a/source/winVersion.py b/source/winVersion.py index 153796c1c7c..14b08e4fd42 100644 --- a/source/winVersion.py +++ b/source/winVersion.py @@ -56,3 +56,7 @@ def isWin10(version=1507, atLeast=True): except KeyError: log.error("Unknown Windows 10 version {}".format(version)) return False + + +def isFullScreenMagnificationAvailable(): + return (winVersion.major, winVersion.minor) >= (6, 2)