Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vision framework: add a screen curtain #10090

Merged
merged 10 commits into from
Sep 6, 2019
47 changes: 47 additions & 0 deletions source/globalCommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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')
Expand Down Expand Up @@ -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 by making the screen black 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:
screenCurtainName = "screenCurtain"
if not vision.getProviderClass(screenCurtainName).canStart():
# Translators: Reported when the screen curtain is not available.
message = _("Screen curtain not available")
return
scriptCount = scriptHandler.getLastScriptRepeatCount()
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 = _("Temporary Screen curtain, enabled until next restart")
else:
# Translators: Reported when the screen curtain is enabled.
message = _("Screen curtain enabled")
finally:
if message is not None:
ui.message(message, speechPriority=speech.priorities.SPRI_NOW)

__gestures = {
# Basic
"kb:NVDA+n": "showGui",
Expand Down
19 changes: 13 additions & 6 deletions source/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import gui
import speech
import braille
from typing import Optional


# From urlmon.h
URL_MK_UNIFORM = 1
Expand All @@ -32,6 +34,7 @@
HTMLDLG_PRINT_TEMPLATE = 0x0080
HTMLDLG_VERIFY = 0x0100


def browseableMessage(message,title=None,isHtml=False):
"""Present a message to the user that can be read in browse mode.
The message will be presented in an HTML document.
Expand Down Expand Up @@ -64,21 +67,25 @@ 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)
84 changes: 84 additions & 0 deletions source/visionEnhancementProviders/screenCurtain.py
Original file line number Diff line number Diff line change
@@ -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:
"""Static class 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
4 changes: 4 additions & 0 deletions source/winVersion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)