From 694912e95ce827953b1c4c33c48b55bfd2c047b6 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Tue, 8 Dec 2020 10:47:15 +0800 Subject: [PATCH 001/174] Start dev cycle for 2021.1 (PR #11913) Branch for release 2020.4 PR #11910 --- source/buildVersion.py | 12 ++++++------ user_docs/en/changes.t2t | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/source/buildVersion.py b/source/buildVersion.py index 1333239a4cc..dd10aafa766 100644 --- a/source/buildVersion.py +++ b/source/buildVersion.py @@ -1,7 +1,7 @@ -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2006-2019 NV Access Limited -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2006-2020 NV Access Limited +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. import os @@ -65,8 +65,8 @@ def formatVersionForGUI(year, major, minor): # Version information for NVDA name = "NVDA" -version_year = 2020 -version_major = 4 +version_year = 2021 +version_major = 1 version_minor = 0 version_build = 0 # Should not be set manually. Set in 'sconscript' provided by 'appVeyor.yml' version=_formatDevVersionString() diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index cc65846e12b..0bff88cace0 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -3,6 +3,21 @@ What's New in NVDA %!includeconf: ../changes.t2tconf += 2021.1 = + +== New Features == + + +== Changes == + + +== Bug Fixes == + + +== Changes for Developers == +- Note: this is a Add-on API compatibility breaking realease. Add-ons will need to be re-tested and have their manifest updated. + + = 2020.4 = == New Features == From 2030dd6ac88d792c478144bf0ee116ffdc9756fd Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Wed, 9 Dec 2020 03:25:45 +0100 Subject: [PATCH 002/174] Developer Guide: inline docstring and `__doc__` attribute explanation (PR #9949) Fixes #9943 Explain not to provide inline docstring when no `__doc__` attribute and using the legacy script construct (#9943) --- devDocs/developerGuide.t2t | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/devDocs/developerGuide.t2t b/devDocs/developerGuide.t2t index 3da21bcea28..ff7d37a112d 100644 --- a/devDocs/developerGuide.t2t +++ b/devDocs/developerGuide.t2t @@ -433,9 +433,9 @@ The order for gesture binding lookup is: +++ Defining script properties +++[DefiningScriptProperties] For NVDA 2018.3 and above, the recommended way to set script properties is by means of the so called script decorator. -In short, a decorator is a function that modifies the behavior of a particular function. +In short, a decorator is a function that modifies the behavior of a particular function or method. The script decorator modifies the script in such a way that it will be properly bound to the desired gestures. -Furthermore, it ensures that the script is listed with the description you specify, and that it is categorised under the desired category in the input gestures dialog. +Furthermore, it ensures that the script is listed with the description you specify, and that it is categorised under the desired category in the input gestures dialog. In order for you to use the script decorator, you will have to import it from the scriptHandler module. ``` @@ -486,8 +486,10 @@ The following keyword arguments can be used when applying the script decorator: Though the script decorator makes the script definition process a lot easier, there are more ways of binding gestures and setting script properties. For example, a special "__gestures" Python dictionary can be defined as a class variable on an App Module, Global Plugin or NVDA Object. This dictionary should contain gesture identifier strings pointing to the name of the requested script, without the "script_" prefix. -You can also specify a description of the script in the function's docstring. -Furthermore, an alternative way of specifying the script's category is by means of setting a "category" attribute on the script function to a string containing the name of the category. +You can also specify a description of the script in the method's "__doc__" attribute. +However, beware not to include an inline docstring at the start of the method if you do not set the "__doc__" attribute, as it would render the description not translatable. +The script decorator does not suffer from this limitation, so you are encouraged to provide inline docstrings as needed when using it. +Furthermore, an alternative way of specifying the script's category is by means of setting a "category" attribute on the script method to a string containing the name of the category. ++ Example 4: A Global Plugin to Find out Window Class and Control ID ++ The following Global Plugin allows you to press NVDA+leftArrow to have the window class of the current focus announced, and NVDA+rightArrow to have the window control ID of the current focus announced. From d3ba21a3a8840c1b07fe9ece40ab6c940f0403e4 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 9 Dec 2020 10:28:33 +0800 Subject: [PATCH 003/174] Fix typo in changes file --- user_docs/en/changes.t2t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 0bff88cace0..1d41e49bc0f 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -15,7 +15,7 @@ What's New in NVDA == Changes for Developers == -- Note: this is a Add-on API compatibility breaking realease. Add-ons will need to be re-tested and have their manifest updated. +- Note: this is a Add-on API compatibility breaking release. Add-ons will need to be re-tested and have their manifest updated. = 2020.4 = From 7b92e4425a7f0ae29252d42e46e6614d5dfb106e Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Tue, 15 Dec 2020 17:09:36 +0800 Subject: [PATCH 004/174] Update espeak to commit 82d5b7b04 (PR #11928) * Update Espeak to commit 82d5b7b04 Commit: "Ported voices from NVSpeechPlayer to espeak variants." Full sha: 82d5b7b04488412845101851f36da6953cac4378F * Update build process for Espeak - Split build environment settings for internal and 3rd party code. - Specifically don't enable warning level 3 for third party code builds. - Use 3rd party build config for Espeak and Libluis - centralize preprocessor macros --- include/espeak | 2 +- nvdaHelper/archBuild_sconscript | 32 +++++++++++-------- nvdaHelper/espeak/config.h | 13 +++----- nvdaHelper/espeak/sconscript | 56 ++++++++++++++++++++++++--------- nvdaHelper/liblouis/sconscript | 10 ++++-- readme.md | 2 +- 6 files changed, 74 insertions(+), 41 deletions(-) diff --git a/include/espeak b/include/espeak index aafd2e72058..82d5b7b0448 160000 --- a/include/espeak +++ b/include/espeak @@ -1 +1 @@ -Subproject commit aafd2e720582d7ccdadef675ce656903bc775c1d +Subproject commit 82d5b7b04488412845101851f36da6953cac4378 diff --git a/nvdaHelper/archBuild_sconscript b/nvdaHelper/archBuild_sconscript index ef73eb04b18..be1192483f8 100644 --- a/nvdaHelper/archBuild_sconscript +++ b/nvdaHelper/archBuild_sconscript @@ -93,18 +93,8 @@ env.Append( ] ) env.Append(CCFLAGS=[ - '/W3', - '/WX', - '/std:c++17' + '/std:c++17', ]) -if 'analyze' in debug: - env.Append(CCFLAGS=['/analyze']) - # Disable: Inconsistent annotation for 'x': this instance has no annotations. - # Seems all MIDL-generated code from idl files don't add annotations - env.Append(CCFLAGS='/wd28251') - # Disable: 'x': unreferenced formal parameter - # We use a great deal of hook functions where we have no need for various parameters - env.Append(CCFLAGS='/wd4100') env.Append(CXXFLAGS=['/EHsc']) @@ -155,6 +145,22 @@ if 'debugCRT' in debug: else: env.Append(CCFLAGS=['/MT']) +# Don't enable warnings and warnings as errors or analysis to 3rd party code. +thirdPartyEnv = env.Clone() +env.Append(CCFLAGS=[ + '/W3', + '/WX', +]) +if 'analyze' in debug: + env.Append(CCFLAGS=['/analyze']) + # Disable: Inconsistent annotation for 'x': this instance has no annotations. + # Seems all MIDL-generated code from idl files don't add annotations + env.Append(CCFLAGS='/wd28251') + # Disable: 'x': unreferenced formal parameter + # We use a great deal of hook functions where we have no need for various parameters + env.Append(CCFLAGS='/wd4100') + +Export('thirdPartyEnv') Export('env') acrobatAccessRPCStubs=env.SConscript('acrobatAccess_sconscript') @@ -218,5 +224,5 @@ if TARGET_ARCH in ('x86_64', 'arm64'): env.Install(libInstallDir,remoteLoaderProgram) if TARGET_ARCH=='x86': - env.SConscript('espeak/sconscript') - env.SConscript('liblouis/sconscript') + thirdPartyEnv.SConscript('espeak/sconscript') + thirdPartyEnv.SConscript('liblouis/sconscript') diff --git a/nvdaHelper/espeak/config.h b/nvdaHelper/espeak/config.h index 0c96e87da10..9032f70f805 100644 --- a/nvdaHelper/espeak/config.h +++ b/nvdaHelper/espeak/config.h @@ -1,10 +1,5 @@ -// general headers and types -#define HAVE_STDINT_H 1 -#define __WIN32__ 1 -#define LIBESPEAK_NG_EXPORT +// Supplies the "config.h" include for espeak files +// Replaces the include/espeak/src/windows/config.h file -// Espeak features -#define INCLUDE_KLATT 1 -#define HAVE_SONIC_H 1 - -#define PACKAGE_VERSION "1.49.3 dev" +// Preprocessor definitions have been moved to the build system +// See: nvdaHelper/espeak/sconscript diff --git a/nvdaHelper/espeak/sconscript b/nvdaHelper/espeak/sconscript index 2f8e8147bd2..a38b590b1b2 100644 --- a/nvdaHelper/espeak/sconscript +++ b/nvdaHelper/espeak/sconscript @@ -1,5 +1,7 @@ +from include.scons.SCons.Util import CLVar + Import([ - 'env', + 'thirdPartyEnv', 'sourceDir', ]) @@ -30,19 +32,37 @@ class espeak_VOICE(ctypes.Structure): ('spare',ctypes.c_void_p), ] -env=env.Clone() -#Whole-program optimization causes eSpeak to distort and worble with its Klatt4 voice -#Therefore specifically force it off -env.Append(CCFLAGS='/GL-') - -# Don't analyze the code as not our project -if 'analyze' in env['nvdaHelperDebugFlags']: - env.Append(CCFLAGS='/analyze-') - -# Ignore all warnings as the code is not ours -env.Replace(CCFLAGS='/W0') - -env.Append(CPPPATH=['#nvdaHelper/espeak',espeakIncludeDir,espeakIncludeDir.Dir('compat'),sonicSrcDir,espeakSrcDir.Dir('ucd-tools/src/include')]) +env = thirdPartyEnv.Clone() +env.Append( + CCFLAGS=[ + # Whole-program optimization causes eSpeak to distort and warble with its Klatt4 voice + # Therefore specifically force it off + '/GL-', + # Ignore all warnings as the code is not ours. + '/W0', + # Preprocessor definitions. Migrated from 'nvdaHelper/espeak/config.h' + '/DPACKAGE_VERSION=\\"1.51-dev\\"', # See 'include/espeak/src/windows/config.h' + '/DHAVE_STDINT_H=1', + '/D__WIN32__#1', + '/DLIBESPEAK_NG_EXPORT', + # Define WIN32_LEAN_AND_MEAN for preprocessor to prevent windows.h including winsock causing redefinition + # errors when winsock2 is included by espeak\src\include\compat\endian.h + '/DWIN32_LEAN_AND_MEAN', + # Preprocessor definitions. Espeak Features + '/DINCLUDE_SPEECHPLAYER=1', + '/DINCLUDE_KLATT=1', + '/DHAVE_SONIC_H=1', + ]) + +env.Append( + CPPPATH=[ + '#nvdaHelper/espeak', # ensure that nvdaHelper/espeak/config.h is found first. + espeakIncludeDir, + espeakIncludeDir.Dir('compat'), + espeakSrcDir.Dir('speechPlayer/include'), + sonicSrcDir, + espeakSrcDir.Dir('ucd-tools/src/include') + ]) def espeak_compilePhonemeData_buildEmitter(target,source,env): phSourceIgnores=['error_log','error_intonation','compile_prog_log','compile_report','envelopes.png'] @@ -131,6 +151,14 @@ espeakLib=env.SharedLibrary( "voices.c", "wavegen.c", sonicLib, + # espeak OPT_SPEECHPLAYER block + "sPlayer.c", + "../speechPlayer/src/frame.cpp", + "../speechPlayer/src/speechPlayer.cpp", + "../speechPlayer/src/speechWaveGenerator.cpp", + #"../speak-ng.cpp", + # if not OPT_SPEECHPLAYER + # "../speak-ng.c", # espeak does not need to handle its own audio output so dont include: # pcaudiolib\src\audio.c # pcaudiolib\src\windows.c diff --git a/nvdaHelper/liblouis/sconscript b/nvdaHelper/liblouis/sconscript index 6bb37a5a85c..793cc1b611a 100644 --- a/nvdaHelper/liblouis/sconscript +++ b/nvdaHelper/liblouis/sconscript @@ -16,11 +16,17 @@ import os import re import glob from SCons.Tool.MSCommon.vc import find_vc_pdir +import typing +from include.scons.SCons.Environment import Environment +from include.scons.SCons.Environment import Base Import([ - "env", + "thirdPartyEnv", "sourceDir", ]) +sourceDir:Base = sourceDir +thirdPartyEnv: Environment = thirdPartyEnv +env: Environment = typing.cast(Environment, thirdPartyEnv.Clone()) louisRootDir = env.Dir("#include/liblouis") louisSourceDir = louisRootDir.Dir("liblouis") @@ -37,8 +43,6 @@ def getLouisVersion(): return m.group("version") return "unknown" -env = env.Clone() - # Liblouis is build with Clang, as Microsoft Visual C++ is unable to build C99 code. clangDirs = glob.glob(os.path.join( find_vc_pdir(env, env.get("MSVC_VERSION")), diff --git a/readme.md b/readme.md index 9bfb44ce9c8..4ee3e02e918 100644 --- a/readme.md +++ b/readme.md @@ -84,7 +84,7 @@ For reference, the following run time dependencies are included in Git submodule * [comtypes](https://github.com/enthought/comtypes), version 1.1.7 * [wxPython](https://www.wxpython.org/), version 4.0.3 -* [eSpeak NG](https://github.com/espeak-ng/espeak-ng), version 1.51-dev commit 1fb68ffffea4 +* [eSpeak NG](https://github.com/espeak-ng/espeak-ng), version 1.51-dev commit 82d5b7b04 * [Sonic](https://github.com/waywardgeek/sonic), commit 4f8c1d11 * [IAccessible2](https://wiki.linuxfoundation.org/accessibility/iaccessible2/start), commit cbc1f29631780 * [ConfigObj](https://github.com/DiffSK/configobj), commit f9a265c From 9889285680dba4198330691f77f395f6f120abf8 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Thu, 31 Dec 2020 04:24:36 -0500 Subject: [PATCH 005/174] diff-match-patch for LiveText (PR #11639) Refactor LiveText to use diff-match-patch via IPC with another process Changes diffing functions to operate at the string rather than line level. Adds diff-match-patch (DMP) as an optional diffing algorithm for LiveText objects. It is anticipated that DMP will become the default in a future NVDA release pending positive user testing. Unlike #11500, this PR does not import or dynamically link to DMP due to licensing issues. Instead, a Python application is run in another process that calls the DMP extension, and communicates over standard IO. Co-authored-by: Michael Curran Co-authored-by: Reef Turner --- .gitmodules | 3 + include/nvda_dmp | 1 + source/NVDAObjects/IAccessible/winConsole.py | 45 +++-- source/NVDAObjects/UIA/winConsoleUIA.py | 15 -- source/NVDAObjects/behaviors.py | 96 ++++------ source/NVDAObjects/window/winConsole.py | 21 +- source/config/configSpec.py | 1 + source/core.py | 8 + source/diffHandler.py | 192 +++++++++++++++++++ source/gui/settingsDialogs.py | 41 ++++ source/setup.py | 26 ++- user_docs/en/changes.t2t | 6 + user_docs/en/userGuide.t2t | 14 ++ 13 files changed, 364 insertions(+), 105 deletions(-) create mode 160000 include/nvda_dmp create mode 100644 source/diffHandler.py diff --git a/.gitmodules b/.gitmodules index 41835aa2324..ad949f1e294 100644 --- a/.gitmodules +++ b/.gitmodules @@ -40,6 +40,9 @@ [submodule "include/javaAccessBridge32"] path = include/javaAccessBridge32 url = https://github.com/nvaccess/javaAccessBridge32-bin.git +[submodule "include/nvda_dmp"] + path = include/nvda_dmp + url = https://github.com/codeofdusk/nvda_dmp [submodule "include/w3c-aria-practices"] path = include/w3c-aria-practices url = https://github.com/w3c/aria-practices diff --git a/include/nvda_dmp b/include/nvda_dmp new file mode 160000 index 00000000000..b2ccf200866 --- /dev/null +++ b/include/nvda_dmp @@ -0,0 +1 @@ +Subproject commit b2ccf2008669e1acb0a7181c268ce6a1311d4d7e diff --git a/source/NVDAObjects/IAccessible/winConsole.py b/source/NVDAObjects/IAccessible/winConsole.py index 9347169bc5d..d2c9343ce3e 100644 --- a/source/NVDAObjects/IAccessible/winConsole.py +++ b/source/NVDAObjects/IAccessible/winConsole.py @@ -1,22 +1,45 @@ -#NVDAObjects/IAccessible/WinConsole.py -#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) 2007-2019 NV Access Limited, Bill Dengler +# 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) 2007-2020 NV Access Limited, Bill Dengler import config +from NVDAObjects.behaviors import KeyboardHandlerBasedTypedCharSupport from winVersion import isWin10 from . import IAccessible from ..window import winConsole -class WinConsole(winConsole.WinConsole, IAccessible): - "The legacy console implementation for situations where UIA isn't supported." - pass + +class EnhancedLegacyWinConsole(KeyboardHandlerBasedTypedCharSupport, winConsole.WinConsole, IAccessible): + """ + A hybrid approach to console access, using legacy APIs to read output + and KeyboardHandlerBasedTypedCharSupport for input. + """ + #: Legacy consoles take quite a while to send textChange events. + #: This significantly impacts typing performance, so don't queue chars. + _supportsTextChange = False + + +class LegacyWinConsole(winConsole.WinConsole, IAccessible): + """ + NVDA's original console support, used by default on Windows versions + before 1607. + """ + + def _get_diffAlgo(self): + # Non-enhanced legacy consoles use caret proximity to detect + # typed/deleted text. + # Single-character changes are not reported as + # they are confused for typed characters. + # Force difflib to keep meaningful edit reporting in these consoles. + from diffHandler import get_difflib_algo + return get_difflib_algo() + def findExtraOverlayClasses(obj, clsList): if isWin10(1607) and config.conf['terminals']['keyboardSupportInLegacy']: - from NVDAObjects.behaviors import KeyboardHandlerBasedTypedCharSupport - clsList.append(KeyboardHandlerBasedTypedCharSupport) - clsList.append(WinConsole) + clsList.append(EnhancedLegacyWinConsole) + else: + clsList.append(LegacyWinConsole) diff --git a/source/NVDAObjects/UIA/winConsoleUIA.py b/source/NVDAObjects/UIA/winConsoleUIA.py index 03513bf48f1..c0d847b0bd3 100644 --- a/source/NVDAObjects/UIA/winConsoleUIA.py +++ b/source/NVDAObjects/UIA/winConsoleUIA.py @@ -1,4 +1,3 @@ -# NVDAObjects/UIA/winConsoleUIA.py # A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -363,20 +362,6 @@ def _get_TextInfo(self): movement.""" return consoleUIATextInfo if self.is21H1Plus else consoleUIATextInfoPre21H1 - def _getTextLines(self): - if self.is21H1Plus: - # #11760: the 21H1 UIA console wraps across lines. - # When text wraps, NVDA starts reading from the beginning of the visible text for every new line of output. - # Use the superclass _getTextLines instead. - return super()._getTextLines() - # This override of _getTextLines takes advantage of the fact that - # the console text contains linefeeds for every line - # Thus a simple string splitlines is much faster than splitting by unit line. - ti = self.makeTextInfo(textInfos.POSITION_ALL) - text = ti.text or "" - return text.splitlines() - - def detectPossibleSelectionChange(self): try: return super().detectPossibleSelectionChange() diff --git a/source/NVDAObjects/behaviors.py b/source/NVDAObjects/behaviors.py index e09d93ec28e..6713ef9c296 100755 --- a/source/NVDAObjects/behaviors.py +++ b/source/NVDAObjects/behaviors.py @@ -1,5 +1,4 @@ # -*- coding: UTF-8 -*- -# NVDAObjects/behaviors.py # A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -12,7 +11,6 @@ import os import time import threading -import difflib import tones import queueHandler import eventHandler @@ -30,6 +28,8 @@ import braille import nvwave import globalVars +from typing import List +import diffHandler class ProgressBar(NVDAObject): @@ -265,15 +265,31 @@ def event_textChange(self): """ self._event.set() - def _getTextLines(self): - """Retrieve the text of this object in lines. + def _get_diffAlgo(self): + """ + This property controls which diffing algorithm should be used by + this object. Most subclasses should simply use the base + implementation, which returns DMP (character-based diffing). + + @Note: DMP is experimental, and can be disallowed via user + preference. In this case, the prior stable implementation, Difflib + (line-based diffing), will be used. + """ + return diffHandler.get_dmp_algo() + + def _get_devInfo(self): + info = super().devInfo + info.append(f"diffing algorithm: {self.diffAlgo}") + return info + + def _getText(self) -> str: + """Retrieve the text of this object. This will be used to determine the new text to speak. The base implementation uses the L{TextInfo}. However, subclasses should override this if there is a better way to retrieve the text. - @return: The current lines of text. - @rtype: list of str """ - return list(self.makeTextInfo(textInfos.POSITION_ALL).getTextInChunks(textInfos.UNIT_LINE)) + ti = self.makeTextInfo(textInfos.POSITION_ALL) + return self.diffAlgo._getText(ti) def _reportNewLines(self, lines): """ @@ -291,10 +307,10 @@ def _reportNewText(self, line): def _monitor(self): try: - oldLines = self._getTextLines() + oldText = self._getText() except: - log.exception("Error getting initial lines") - oldLines = [] + log.exception("Error getting initial text") + oldText = "" while self._keepMonitoring: self._event.wait() @@ -309,9 +325,9 @@ def _monitor(self): self._event.clear() try: - newLines = self._getTextLines() + newText = self._getText() if config.conf["presentation"]["reportDynamicContentChanges"]: - outLines = self._calculateNewText(newLines, oldLines) + outLines = self._calculateNewText(newText, oldText) if len(outLines) == 1 and len(outLines[0].strip()) == 1: # This is only a single character, # which probably means it is just a typed character, @@ -319,61 +335,13 @@ def _monitor(self): del outLines[0] if outLines: queueHandler.queueFunction(queueHandler.eventQueue, self._reportNewLines, outLines) - oldLines = newLines + oldText = newText except: - log.exception("Error getting lines or calculating new text") - - def _calculateNewText(self, newLines, oldLines): - outLines = [] - - prevLine = None - for line in difflib.ndiff(oldLines, newLines): - if line[0] == "?": - # We're never interested in these. - continue - if line[0] != "+": - # We're only interested in new lines. - prevLine = line - continue - text = line[2:] - if not text or text.isspace(): - prevLine = line - continue - - if prevLine and prevLine[0] == "-" and len(prevLine) > 2: - # It's possible that only a few characters have changed in this line. - # If so, we want to speak just the changed section, rather than the entire line. - prevText = prevLine[2:] - textLen = len(text) - prevTextLen = len(prevText) - # Find the first character that differs between the two lines. - for pos in range(min(textLen, prevTextLen)): - if text[pos] != prevText[pos]: - start = pos - break - else: - # We haven't found a differing character so far and we've hit the end of one of the lines. - # This means that the differing text starts here. - start = pos + 1 - # Find the end of the differing text. - if textLen != prevTextLen: - # The lines are different lengths, so assume the rest of the line changed. - end = textLen - else: - for pos in range(textLen - 1, start - 1, -1): - if text[pos] != prevText[pos]: - end = pos + 1 - break - - if end - start < 15: - # Less than 15 characters have changed, so only speak the changed chunk. - text = text[start:end] + log.exception("Error getting or calculating new text") - if text and not text.isspace(): - outLines.append(text) - prevLine = line + def _calculateNewText(self, newText: str, oldText: str) -> List[str]: + return self.diffAlgo.diff(newText, oldText) - return outLines class Terminal(LiveText, EditableText): """An object which both accepts text input and outputs text which should be reported automatically. diff --git a/source/NVDAObjects/window/winConsole.py b/source/NVDAObjects/window/winConsole.py index 96d3d93155b..01ecec4e75e 100644 --- a/source/NVDAObjects/window/winConsole.py +++ b/source/NVDAObjects/window/winConsole.py @@ -1,8 +1,7 @@ -#NVDAObjects/WinConsole.py -#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) 2007-2019 NV Access Limited, Bill Dengler +# 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) 2007-2020 NV Access Limited, Bill Dengler import winConsoleHandler from . import Window @@ -14,18 +13,12 @@ class WinConsole(Terminal, EditableTextWithoutAutoSelectDetection, Window): """ - NVDA's legacy Windows Console support. + Base class for NVDA's legacy Windows Console support. This is used in situations where UIA isn't available. Please consider using NVDAObjects.UIA.winConsoleUIA instead. """ STABILIZE_DELAY = 0.03 - def initOverlayClass(self): - # Legacy consoles take quite a while to send textChange events. - # This significantly impacts typing performance, so don't queue chars. - if isinstance(self, KeyboardHandlerBasedTypedCharSupport): - self._supportsTextChange = False - def _get_windowThreadID(self): # #10113: Windows forces the thread of console windows to match the thread of the first attached process. # However, To correctly handle speaking of typed characters, @@ -69,8 +62,8 @@ def event_loseFocus(self): def event_nameChange(self): pass - def _getTextLines(self): - return winConsoleHandler.getConsoleVisibleLines() + def _getText(self): + return '\n'.join(winConsoleHandler.getConsoleVisibleLines()) def script_caret_backspaceCharacter(self, gesture): super(WinConsole, self).script_caret_backspaceCharacter(gesture) diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 9e23fbe4fe7..97f5f284582 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -225,6 +225,7 @@ [terminals] speakPasswords = boolean(default=false) keyboardSupportInLegacy = boolean(default=True) + diffAlgo = option("auto", "dmp", "difflib", default="auto") [update] autoCheck = boolean(default=true) diff --git a/source/core.py b/source/core.py index 00d34fdf9aa..e2af1a42ab5 100644 --- a/source/core.py +++ b/source/core.py @@ -605,6 +605,14 @@ def run(self): _terminate(speech) _terminate(addonHandler) _terminate(garbageHandler) + # DMP is only started if needed. + # Terminate manually (and let it write to the log if necessary) + # as core._terminate always writes an entry. + try: + import diffHandler + diffHandler._dmp._terminate() + except Exception: + log.exception("Exception while terminating DMP") if not globalVars.appArgs.minimal and config.conf["general"]["playStartAndExitSounds"]: try: diff --git a/source/diffHandler.py b/source/diffHandler.py new file mode 100644 index 00000000000..5d37065c054 --- /dev/null +++ b/source/diffHandler.py @@ -0,0 +1,192 @@ +# 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 Bill Dengler + +import config +import globalVars +import os +import struct +import subprocess +import sys +from abc import abstractmethod +from baseObject import AutoPropertyObject +from difflib import ndiff +from logHandler import log +from textInfos import TextInfo, UNIT_LINE +from threading import Lock +from typing import List + + +class DiffAlgo(AutoPropertyObject): + @abstractmethod + def diff(self, newText: str, oldText: str) -> List[str]: + raise NotImplementedError + + @abstractmethod + def _getText(self, ti: TextInfo) -> str: + raise NotImplementedError + + +class DiffMatchPatch(DiffAlgo): + """A character-based diffing approach, using the Google Diff Match Patch + library in a proxy process (to work around a licence conflict). + """ + #: A subprocess.Popen object for the nvda_dmp process. + _proc = None + #: A lock to control access to the nvda_dmp process. + #: Control access to avoid synchronization problems if multiple threads + #: attempt to use nvda_dmp at the same time. + _lock = Lock() + + def _initialize(self): + """Start the nvda_dmp process if it is not already running. + @note: This should be run from within the context of an acquired lock.""" + if not DiffMatchPatch._proc: + log.debug("Starting diff-match-patch proxy") + if hasattr(sys, "frozen"): + dmp_path = (os.path.join(globalVars.appDir, "nvda_dmp.exe"),) + else: + dmp_path = (sys.executable, os.path.join( + globalVars.appDir, "..", "include", "nvda_dmp", "nvda_dmp.py" + )) + DiffMatchPatch._proc = subprocess.Popen( + dmp_path, + creationflags=subprocess.CREATE_NO_WINDOW, + bufsize=0, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE + ) + + def _getText(self, ti: TextInfo) -> str: + return ti.text + + def diff(self, newText: str, oldText: str) -> List[str]: + try: + if not newText and not oldText: + # Return an empty list here to avoid exiting + # nvda_dmp uses two zero-length texts as a sentinal value + return [] + with DiffMatchPatch._lock: + self._initialize() + old = oldText.encode("utf-8") + new = newText.encode("utf-8") + # Sizes are packed as 32-bit ints in native byte order. + # Since nvda and nvda_dmp are running on the same Python + # platform/version, this is okay. + tl = struct.pack("=II", len(old), len(new)) + DiffMatchPatch._proc.stdin.write(tl) + DiffMatchPatch._proc.stdin.write(old) + DiffMatchPatch._proc.stdin.write(new) + buf = b"" + sizeb = b"" + SIZELEN = 4 + while len(sizeb) < SIZELEN: + try: + sizeb += DiffMatchPatch._proc.stdout.read(SIZELEN - len(sizeb)) + except TypeError: + pass + (size,) = struct.unpack("=I", sizeb) + while len(buf) < size: + buf += DiffMatchPatch._proc.stdout.read(size - len(buf)) + return [ + line + for line in buf.decode("utf-8").splitlines() + if line and not line.isspace() + ] + except Exception: + log.exception("Exception in DMP, falling back to difflib") + return Difflib().diff(newText, oldText) + + def _terminate(self): + with DiffMatchPatch._lock: + if DiffMatchPatch._proc: + log.debug("Terminating diff-match-patch proxy") + # nvda_dmp exits when it receives two zero-length texts. + DiffMatchPatch._proc.stdin.write(struct.pack("=II", 0, 0)) + DiffMatchPatch._proc.wait(timeout=5) + + +class Difflib(DiffAlgo): + "A line-based diffing approach in pure Python, using the Python standard library." + + def diff(self, newText: str, oldText: str) -> List[str]: + newLines = newText.splitlines() + oldLines = oldText.splitlines() + outLines = [] + + prevLine = None + + for line in ndiff(oldLines, newLines): + if line[0] == "?": + # We're never interested in these. + continue + if line[0] != "+": + # We're only interested in new lines. + prevLine = line + continue + text = line[2:] + if not text or text.isspace(): + prevLine = line + continue + + if prevLine and prevLine[0] == "-" and len(prevLine) > 2: + # It's possible that only a few characters have changed in this line. + # If so, we want to speak just the changed section, rather than the entire line. + prevText = prevLine[2:] + textLen = len(text) + prevTextLen = len(prevText) + # Find the first character that differs between the two lines. + for pos in range(min(textLen, prevTextLen)): + if text[pos] != prevText[pos]: + start = pos + break + else: + # We haven't found a differing character so far and we've hit the end of one of the lines. + # This means that the differing text starts here. + start = pos + 1 + # Find the end of the differing text. + if textLen != prevTextLen: + # The lines are different lengths, so assume the rest of the line changed. + end = textLen + else: + for pos in range(textLen - 1, start - 1, -1): + if text[pos] != prevText[pos]: + end = pos + 1 + break + + if end - start < 15: + # Less than 15 characters have changed, so only speak the changed chunk. + text = text[start:end] + + if text and not text.isspace(): + outLines.append(text) + prevLine = line + + return outLines + + def _getText(self, ti: TextInfo) -> str: + return "\n".join(ti.getTextInChunks(UNIT_LINE)) + + +def get_dmp_algo(): + """ + This function returns a Diff Match Patch object if allowed by the user. + DMP is experimental and can be explicitly enabled/disabled by a user + setting to opt in or out of the experiment. If config does not allow + DMP, this function returns a Difflib instance instead. + """ + return ( + _dmp + if config.conf["terminals"]["diffAlgo"] == "dmp" + else _difflib + ) + + +def get_difflib_algo(): + "Returns an instance of the difflib diffAlgo." + return _difflib + + +_difflib = Difflib() +_dmp = DiffMatchPatch() diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index b2647c7dfbb..4267534c0fc 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2580,6 +2580,41 @@ def __init__(self, parent): self.keyboardSupportInLegacyCheckBox.defaultValue = self._getDefaultValue(["terminals", "keyboardSupportInLegacy"]) self.keyboardSupportInLegacyCheckBox.Enable(winVersion.isWin10(1607)) + # Translators: This is the label for a combo box for selecting a + # method of detecting changed content in terminals in the advanced + # settings panel. + # Choices are automatic, allow Diff Match Patch, and force Difflib. + diffAlgoComboText = _("&Diff algorithm:") + diffAlgoChoices = [ + # Translators: A choice in a combo box in the advanced settings + # panel to have NVDA determine the method of detecting changed + # content in terminals automatically. + _("Automatic (Difflib)"), + # Translators: A choice in a combo box in the advanced settings + # panel to have NVDA detect changes in terminals + # by character when supported, using the diff match patch algorithm. + _("allow Diff Match Patch"), + # Translators: A choice in a combo box in the advanced settings + # panel to have NVDA detect changes in terminals + # by line, using the difflib algorithm. + _("force Difflib") + ] + #: The possible diffAlgo config values, in the order they appear + #: in the combo box. + self.diffAlgoVals = ( + "auto", + "dmp", + "difflib" + ) + self.diffAlgoCombo = terminalsGroup.addLabeledControl(diffAlgoComboText, wx.Choice, choices=diffAlgoChoices) + curChoice = self.diffAlgoVals.index( + config.conf['terminals']['diffAlgo'] + ) + self.diffAlgoCombo.SetSelection(curChoice) + self.diffAlgoCombo.defaultValue = self.diffAlgoVals.index( + self._getDefaultValue(["terminals", "diffAlgo"]) + ) + # Translators: This is the label for a group of advanced options in the # Advanced settings panel label = _("Speech") @@ -2701,6 +2736,7 @@ def haveConfigDefaultsBeenRestored(self): and self.winConsoleSpeakPasswordsCheckBox.IsChecked() == self.winConsoleSpeakPasswordsCheckBox.defaultValue and self.cancelExpiredFocusSpeechCombo.GetSelection() == self.cancelExpiredFocusSpeechCombo.defaultValue and self.keyboardSupportInLegacyCheckBox.IsChecked() == self.keyboardSupportInLegacyCheckBox.defaultValue + and self.diffAlgoCombo.GetSelection() == self.diffAlgoCombo.defaultValue and self.caretMoveTimeoutSpinControl.GetValue() == self.caretMoveTimeoutSpinControl.defaultValue and set(self.logCategoriesList.CheckedItems) == set(self.logCategoriesList.defaultCheckedItems) and True # reduce noise in diff when the list is extended. @@ -2714,6 +2750,7 @@ def restoreToDefaults(self): self.winConsoleSpeakPasswordsCheckBox.SetValue(self.winConsoleSpeakPasswordsCheckBox.defaultValue) self.cancelExpiredFocusSpeechCombo.SetSelection(self.cancelExpiredFocusSpeechCombo.defaultValue) self.keyboardSupportInLegacyCheckBox.SetValue(self.keyboardSupportInLegacyCheckBox.defaultValue) + self.diffAlgoCombo.SetSelection(self.diffAlgoCombo.defaultValue == 'auto') self.caretMoveTimeoutSpinControl.SetValue(self.caretMoveTimeoutSpinControl.defaultValue) self.logCategoriesList.CheckedItems = self.logCategoriesList.defaultCheckedItems self._defaultsRestored = True @@ -2730,6 +2767,10 @@ def onSave(self): config.conf["terminals"]["speakPasswords"] = self.winConsoleSpeakPasswordsCheckBox.IsChecked() config.conf["featureFlag"]["cancelExpiredFocusSpeech"] = self.cancelExpiredFocusSpeechCombo.GetSelection() config.conf["terminals"]["keyboardSupportInLegacy"]=self.keyboardSupportInLegacyCheckBox.IsChecked() + diffAlgoChoice = self.diffAlgoCombo.GetSelection() + config.conf['terminals']['diffAlgo'] = ( + self.diffAlgoVals[diffAlgoChoice] + ) config.conf["editableText"]["caretMoveTimeoutMs"]=self.caretMoveTimeoutSpinControl.GetValue() for index,key in enumerate(self.logCategories): config.conf['debugLog'][key]=self.logCategoriesList.IsChecked(index) diff --git a/source/setup.py b/source/setup.py index ba6b0639e8c..9db1366581a 100755 --- a/source/setup.py +++ b/source/setup.py @@ -6,6 +6,7 @@ #See the file COPYING for more details. import os +import sys import copy import gettext gettext.install("nvda") @@ -13,12 +14,21 @@ import py2exe as py2exeModule from glob import glob import fnmatch +# versionInfo names must be imported after Gettext +# Suppress E402 (module level import not at top of file) +from versionInfo import ( + formatBuildVersionString, + name, + version, + publisher +) # noqa: E402 from versionInfo import * from py2exe import distutils_buildexe from py2exe.dllfinder import DllFinder import wx import importlib.machinery - +# Explicitly put the nvda_dmp dir on the build path so the DMP library is included +sys.path.append(os.path.join("..", "include", "nvda_dmp")) RT_MANIFEST = 24 manifest_template = """\ @@ -187,6 +197,20 @@ def getRecursiveDataFiles(dest,source,excludes=()): "company_name": publisher, }, ], + console=[ + { + "script": os.path.join("..", "include", "nvda_dmp", "nvda_dmp.py"), + "uiAccess": False, + "icon_resources": [(1, "images/nvda.ico")], + "other_resources": [], # Populated at runtime + "version":formatBuildVersionString(), + "description": "NVDA Diff-match-patch proxy", + "product_name": name, + "product_version": version, + "copyright": f"{copyright}, Bill Dengler", + "company_name": f"Bill Dengler, {publisher}", + }, + ], options = {"py2exe": { "bundle_files": 3, "excludes": ["tkinter", diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 25f92ff365b..1ce7ab07062 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -12,10 +12,16 @@ What's New in NVDA == Bug Fixes == +- In terminal programs on Windows 10 version 1607 and later, when inserting or deleting characters in the middle of a line, the characters to the right of the caret are no longer read out. (#3200) + - This experimental fix must be manually enabled in NVDA's advanced settings panel by changing the diff algorithm to Diff Match Patch. == Changes for Developers == - Note: this is a Add-on API compatibility breaking release. Add-ons will need to be re-tested and have their manifest updated. +- `LiveText._getTextLines` has been removed. (#11639) + - Instead, override `_getText` which returns a string of all text in the object. +- `LiveText` objects can now calculate diffs by character. (#11639) + - To alter the diff behaviour for some object, override the `diffAlgo` property (see the docstring for details). = 2020.4 = diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 93fd0f6b898..8ec6f64f7e7 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -1843,6 +1843,20 @@ This feature is available and enabled by default on Windows 10 versions 1607 and Warning: with this option enabled, typed characters that do not appear onscreen, such as passwords, will not be suppressed. In untrusted environments, you may temporarily disable [speak typed characters #KeyboardSettingsSpeakTypedCharacters] and [speak typed words #KeyboardSettingsSpeakTypedWords] when entering passwords. +==== Diff algorithm ====[AdvancedSettingsDiffAlgo] +This setting controls how NVDA determines the new text to speak in terminals. +The diff algorithm combo box has three options: +- Automatic: as of NVDA 2021.1, this option is equivalent to Difflib. +In a future release, it may be changed to Diff Match Patch pending positive user testing. +- allow Diff Match Patch: This option causes NVDA to calculate changes to terminal text by character. +It may improve performance when large volumes of text are written to the console and allow more accurate reporting of changes made in the middle of lines. +However, it may be incompatible with some applications. +This feature is supported in Windows Console on Windows 10 versions 1607 and later. +Additionally, it may be available in other terminals on earlier Windows releases. +- force Difflib: this option causes NVDA to calculate changes to terminal text by line. +It is identical to NVDA's behaviour in versions 2020.3 and earlier. +- + ==== Attempt to cancel speech for expired focus events ====[CancelExpiredFocusSpeech] This option enables behaviour which attempts to cancel speech for expired focus events. In particular moving quickly through messages in Gmail with Chrome can cause NVDA to speak outdated information. From 1f43f98fec5d18ccd9500003c3f793105f0772b7 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Sun, 10 Jan 2021 22:18:35 -0500 Subject: [PATCH 006/174] Fix user guide for DMP. (PR #11981) --- user_docs/en/userGuide.t2t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 8ec6f64f7e7..7623e1ffc9a 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -1854,7 +1854,7 @@ However, it may be incompatible with some applications. This feature is supported in Windows Console on Windows 10 versions 1607 and later. Additionally, it may be available in other terminals on earlier Windows releases. - force Difflib: this option causes NVDA to calculate changes to terminal text by line. -It is identical to NVDA's behaviour in versions 2020.3 and earlier. +It is identical to NVDA's behaviour in versions 2020.4 and earlier. - ==== Attempt to cancel speech for expired focus events ====[CancelExpiredFocusSpeech] From 4c49001d22c346b0cd071b6c5723f374ed6d7258 Mon Sep 17 00:00:00 2001 From: Joseph Lee Date: Sun, 10 Jan 2021 21:56:51 -0800 Subject: [PATCH 007/174] Synth drivers/eSpeak NG internal: pass in a NULL pointer (path string) when obtaining eSpeak NG version string (#11975) * Synth drivers/eSpeak NG internal: update copyright header * Synth drivers/eSpeak NG internal: passin NULL (None) when obtaining version info. Python 3.8: without passing in a path string, access violation is thrown, which can cause NVDA executable to hang when trying to obtain eSpeak NG version string. Therefore pass in NULL (None) because what NVDA is interested in is synthesizer version. * Synth drivers/eSpeak NG internal: address review comments. Comment from Lukasz Golonka: remove file name from copyright header, use bytes.decode to transform eSpeak NG version string from bytes to Unicode. --- source/synthDrivers/_espeak.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/source/synthDrivers/_espeak.py b/source/synthDrivers/_espeak.py index c2a5ee197df..a0ee6dc0175 100755 --- a/source/synthDrivers/_espeak.py +++ b/source/synthDrivers/_espeak.py @@ -1,9 +1,8 @@ # -*- coding: UTF-8 -*- -#synthDrivers/_espeak.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2007-2017 NV Access Limited, Peter Vágner -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2007-2020 NV Access Limited, Peter Vágner +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. import time import nvwave @@ -369,7 +368,8 @@ def terminate(): onIndexReached = None def info(): - return espeakDLL.espeak_Info() + # Python 3.8: a path string must be specified, a NULL is fine when what we need is version string. + return espeakDLL.espeak_Info(None).decode() def getVariantDict(): dir = os.path.join(globalVars.appDir, "synthDrivers", "espeak-ng-data", "voices", "!v") From 8ec984e264635b60f9ee38f9538baa6c17025e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Mon, 11 Jan 2021 07:27:00 +0100 Subject: [PATCH 008/174] Script decorator: add ability to specify that given script should be active in sleep mode. (#11979) * Script decorator: Allow to set `allowInSleepMode` for a decorated script. * Also take this oportunity to use type hints for script decorator parameters * Unit test for allowInSleepMode * Mention `allowInSleepMode` in script decorator's docstring * Lint fixes * Update developer guide * Update what's new Co-authored-by: Michael Curran --- devDocs/developerGuide.t2t | 2 ++ source/scriptHandler.py | 28 ++++++++++++---------------- tests/unit/test_scriptHandler.py | 13 +++++++------ user_docs/en/changes.t2t | 1 + 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/devDocs/developerGuide.t2t b/devDocs/developerGuide.t2t index ff7d37a112d..31b87a2bddb 100644 --- a/devDocs/developerGuide.t2t +++ b/devDocs/developerGuide.t2t @@ -478,6 +478,8 @@ The following keyword arguments can be used when applying the script decorator: This option defaults to False. - bypassInputHelp: A boolean indicating whether this script should run when input help is active. This option defaults to False. +- allowInSleepMode: A boolean indicating whether this script should run when sleep mode is active. + This option defaults to False. - resumeSayAllMode: The say all mode that should be resumed when active before executing this script. The constants for say all mode are prefixed with CURSOR_ and specified in the sayAllHandler modules. If resumeSayAllMode is not specified, say all does not resume after this script. diff --git a/source/scriptHandler.py b/source/scriptHandler.py index 6c3422f6fe1..cebbf17464b 100644 --- a/source/scriptHandler.py +++ b/source/scriptHandler.py @@ -1,9 +1,9 @@ -# scriptHandler.py # A part of NonVisual Desktop Access (NVDA) # Copyright (C) 2007-2020 NV Access Limited, Babbage B.V., Julien Cochuyt # This file is covered by the GNU General Public License. # See the file COPYING for more details. +from typing import List, Optional import time import weakref import inspect @@ -241,32 +241,27 @@ def isScriptWaiting(): return bool(_numScriptsQueued) def script( - description="", - category=None, - gesture=None, - gestures=None, - canPropagate=False, - bypassInputHelp=False, - resumeSayAllMode=None + description: str = "", + category: Optional[str] = None, + gesture: Optional[str] = None, + gestures: Optional[List[str]] = None, + canPropagate: bool = False, + bypassInputHelp: bool = False, + allowInSleepMode: bool = False, + resumeSayAllMode: Optional[int] = None ): """Define metadata for a script. This function is to be used as a decorator to set metadata used by the scripting system and gesture editor. It can only decorate methods which name start swith "script_" @param description: A short translatable description of the script to be used in the gesture editor, etc. - @type description: string @param category: The category of the script displayed in the gesture editor. - @type category: string @param gesture: A gesture associated with this script. - @type gesture: string @param gestures: A list of gestures associated with this script - @type gestures: list(string) @param canPropagate: Whether this script should also apply when it belongs to a focus ancestor object. - @type canPropagate: bool @param bypassInputHelp: Whether this script should run when input help is active. - @type bypassInputHelp: bool + @param allowInSleepMode: Whether this script should run when NVDA is in sleep mode. @param resumeSayAllMode: The say all mode that should be resumed when active before executing this script. - One of the C{sayAllHandler.CURSOR_*} constants. - @type resumeSayAllMode: int + One of the C{sayAllHandler.CURSOR_*} constants. """ if gestures is None: gestures = [] @@ -295,5 +290,6 @@ def script_decorator(decoratedScript): decoratedScript.bypassInputHelp = bypassInputHelp if resumeSayAllMode is not None: decoratedScript.resumeSayAllMode = resumeSayAllMode + decoratedScript.allowInSleepMode = allowInSleepMode return decoratedScript return script_decorator diff --git a/tests/unit/test_scriptHandler.py b/tests/unit/test_scriptHandler.py index 35ffa861823..70127da27a0 100644 --- a/tests/unit/test_scriptHandler.py +++ b/tests/unit/test_scriptHandler.py @@ -1,13 +1,12 @@ -#tests/unit/test_scriptHandler.py -#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. +# 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-2021 NV Access Limited, Babbage B.V., Łukasz Golonka """Unit tests for the scriptHandler module.""" import unittest -from scriptHandler import * +from scriptHandler import script from inputCore import SCRCAT_MISC from sayAllHandler import CURSOR_CARET @@ -22,6 +21,7 @@ def test_scriptdecoration(self): gestures=["kb:b", "kb:c"], canPropagate=True, bypassInputHelp=True, + allowInSleepMode=True, resumeSayAllMode=CURSOR_CARET ) def script_test(self, gesture): @@ -32,4 +32,5 @@ def script_test(self, gesture): self.assertCountEqual(script_test.gestures, ["kb:a", "kb:b", "kb:c"]) self.assertTrue(script_test.canPropagate) self.assertTrue(script_test.bypassInputHelp) + self.assertTrue(script_test.allowInSleepMode) self.assertEqual(script_test.resumeSayAllMode, CURSOR_CARET) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 1ce7ab07062..9a5124af8e6 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -22,6 +22,7 @@ What's New in NVDA - Instead, override `_getText` which returns a string of all text in the object. - `LiveText` objects can now calculate diffs by character. (#11639) - To alter the diff behaviour for some object, override the `diffAlgo` property (see the docstring for details). +- When defining a script with the script decorator, the 'allowInSleepMode' boolean argument can be specified to control if a script is available in sleep mode or not. (#11979) = 2020.4 = From e34ec59e9b39cf790881553c956b75254bb4b0fd Mon Sep 17 00:00:00 2001 From: Joseph Lee Date: Sun, 10 Jan 2021 23:07:34 -0800 Subject: [PATCH 009/174] Dev docs: tell NVDA to treat source directory as app directory, allowing dev docs build with Sphinx to succeed (#11972) * Dev docs/config: point globalVars.appDir to source directory when building source code dev docs. re #11971. Before building dev docs with Sphinx, config module is imported without NVDA knowing where the app dir is i.e. globalVars.appDir is undefined. Therefore tell Sphinx that globalVars.appDir is source directory so Sphinx can build source code documentation. * Dev docs/Sphinx config: update copyright header * Dev docs/Sphinx config: address review comments. Reviewed by Lukasz Golonka: move globalvars.appDir definition, along with removing duplicate globalVars import statement. --- devDocs/conf.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/devDocs/conf.py b/devDocs/conf.py index 06f073db94b..b01ee4fd6f2 100644 --- a/devDocs/conf.py +++ b/devDocs/conf.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2019 NV Access Limited, Leonard de Ruijter +# Copyright (C) 2019-2020 NV Access Limited, Leonard de Ruijter, Joseph Lee # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -16,7 +16,7 @@ import languageHandler # noqa: E402 languageHandler.setLanguage("en") -# Initialize globalvars.appArgs to something sensible. +# Initialize globalVars.appArgs to something sensible. import globalVars # noqa: E402 @@ -30,6 +30,11 @@ class AppArgs: globalVars.appArgs = AppArgs() +# #11971: NVDA is not running, therefore app dir is undefined. +# Therefore tell NVDA that apt source directory is app dir. +appDir = os.path.join("..", "source") +globalVars.appDir = os.path.abspath(appDir) + # Import NVDA's versionInfo module. import versionInfo # noqa: E402 From b58e6cc0786e44655a531fc5881c97edbaa531d4 Mon Sep 17 00:00:00 2001 From: Joseph Lee Date: Sun, 10 Jan 2021 23:13:18 -0800 Subject: [PATCH 010/174] Sphinx: remove 2.2.2 restriction and update readme regarding building source code docs locally (#11973) * Sphinx: remove version requirement (2.2.2). NVDA docs can be built using more recent Sphinx releases such as 3.4.1. Therefore remove checking for Sphinx 2.2.2 in dev docs requirements. * Readme: replace Epydoc with Sphinx, and remove dev docs limitation statement. Replace Epydoc with Sphinx. As a result, remove the Python 3 source dev docs limitation statement, as Sphinx will now build NVDA dev docs. * Readme: document how to build source code docs with 'scons devDocs'. * Readme: output/DevDocs -> output/NVDA for source code documentation. * Dev docs/Sphinx: specify Sphinx 3.4.1 (December 2020 release). Comment from Lukasz Golonka: specify Sphinx==3.4.1 to align with readme. --- devDocs/devDocsInstall/requirements.txt | 2 +- readme.md | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/devDocs/devDocsInstall/requirements.txt b/devDocs/devDocsInstall/requirements.txt index 0111644238c..44ee79ace41 100644 --- a/devDocs/devDocsInstall/requirements.txt +++ b/devDocs/devDocsInstall/requirements.txt @@ -1,2 +1,2 @@ -sphinx==2.2.2 +sphinx==3.4.1 sphinx_rtd_theme diff --git a/readme.md b/readme.md index 4ee3e02e918..0d0154ac074 100644 --- a/readme.md +++ b/readme.md @@ -110,7 +110,6 @@ Additionally, the following build time dependencies are included in Git submodul * [Nulsoft Install System](https://nsis.sourceforge.io/Main_Page/), version 2.51 * [NSIS UAC plug-in](https://nsis.sourceforge.io/UAC_plug-in), version 0.2.4, ansi * xgettext and msgfmt from [GNU gettext](https://sourceforge.net/projects/cppcms/files/boost_locale/gettext_for_windows/) -* [epydoc](http://epydoc.sourceforge.net/), version 3.0.1 with patch for bug #303 * [Boost Optional (stand-alone header)](https://github.com/akrzemi1/Optional), from commit [3922965](https://github.com/akrzemi1/Optional/commit/3922965396fc455c6b1770374b9b4111799588a9) ### Other Dependencies @@ -122,6 +121,7 @@ Although this [must be run manually](#linting-your-changes), developers may wish The following dependencies aren't needed by most people, and are not included in Git submodules: +* To generate developer documentation: [Sphinx](http://sphinx-doc.org/), version 3.4.1 * To generate developer documentation for nvdaHelper: [Doxygen Windows installer](http://www.doxygen.nl/download.html), version 1.8.15: * When you are using Visual Studio Code as your integrated development environment of preference, you can make use of our [prepopulated workspace configuration](https://github.com/nvaccess/vscode-nvda/) for [Visual Studio Code](https://code.visualstudio.com/). While this VSCode project is not included as a submodule in the NVDA repository, you can easily check out the workspace configuration in your repository by executing the following from the root of the repository. @@ -216,7 +216,14 @@ scons developerGuide ``` The developer guide will be placed in the `devDocs` folder in the output directory. -Note that the Python 3 sources of NVDA currently do not support building NVDA developer documentation using the `scons devDocs` command. + +To generate the HTML-based source code documentation, type: + +``` +scons devDocs +``` + +The documentation will be placed in the `NVDA` folder in the output directory. To generate developer documentation for nvdaHelper (not included in the devDocs target): From 714fa6f3c8a018bb184146744b70e0d55aa2bf22 Mon Sep 17 00:00:00 2001 From: Samuel Thibault Date: Tue, 12 Jan 2021 00:17:51 +0100 Subject: [PATCH 011/174] Symbols: test regex group references through the symbols engine (#11961) * Symbols: test regex group references through the engine This adds a test for regex group reference replacement that goes through the complete speech symbol processor, using the French locale. * Fix test content --- tests/unit/test_characterProcessing.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/unit/test_characterProcessing.py b/tests/unit/test_characterProcessing.py index d3d77f49caf..ae780627c21 100644 --- a/tests/unit/test_characterProcessing.py +++ b/tests/unit/test_characterProcessing.py @@ -9,6 +9,8 @@ import unittest import re from characterProcessing import SpeechSymbolProcessor +from characterProcessing import SYMLVL_ALL +from characterProcessing import processSpeechSymbols as process class TestComplex(unittest.TestCase): @@ -118,3 +120,11 @@ def test_multiple_group_replacement(self): name="foo" ) self.assertEqual(replaced, "BAT>bar") + + def test_engine(self): + """Test inclusion of group replacement in engine + """ + replaced = process("fr_FR", "Le 03.04.05.", SYMLVL_ALL) + self.assertEqual(replaced, "Le 03 point 04 point 05 point.") + replaced = process("fr_FR", "Le 03/04/05.", SYMLVL_ALL) + self.assertEqual(replaced, "Le 03 barre oblique 04 barre oblique 05 point.") From 04858db337dd13605bab0f73ab1d55feceec7e81 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Mon, 11 Jan 2021 18:30:46 -0500 Subject: [PATCH 012/174] Update nvda_dmp to possibly address an autoread hang (#11998) * Update nvda_dmp. * Add flush calls for completeness. --- include/nvda_dmp | 2 +- source/diffHandler.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/include/nvda_dmp b/include/nvda_dmp index b2ccf200866..b0f8d87c79d 160000 --- a/include/nvda_dmp +++ b/include/nvda_dmp @@ -1 +1 @@ -Subproject commit b2ccf2008669e1acb0a7181c268ce6a1311d4d7e +Subproject commit b0f8d87c79dfa27fc9b33371a3f4a4a5e193c384 diff --git a/source/diffHandler.py b/source/diffHandler.py index 5d37065c054..4bacff6ae38 100644 --- a/source/diffHandler.py +++ b/source/diffHandler.py @@ -89,6 +89,8 @@ def diff(self, newText: str, oldText: str) -> List[str]: (size,) = struct.unpack("=I", sizeb) while len(buf) < size: buf += DiffMatchPatch._proc.stdout.read(size - len(buf)) + DiffMatchPatch._proc.stdin.flush() + DiffMatchPatch._proc.stdout.flush() return [ line for line in buf.decode("utf-8").splitlines() From 4288882b4f83d3f2c7c3cdb3de0ac0067533a026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Tue, 12 Jan 2021 00:51:21 +0100 Subject: [PATCH 013/174] Don't use UnidentifiedEdit for windows with empty windowText (#11966) * Don't use UnidentifiedEdit for windows with empty windowText Fix-up of https://github.com/nvaccess/nvda/pull/8165 * Update what's new Co-authored-by: Michael Curran --- source/NVDAObjects/window/__init__.py | 11 +++++------ user_docs/en/changes.t2t | 1 + 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/source/NVDAObjects/window/__init__.py b/source/NVDAObjects/window/__init__.py index 9e73257a1f7..0149372ae22 100644 --- a/source/NVDAObjects/window/__init__.py +++ b/source/NVDAObjects/window/__init__.py @@ -1,8 +1,7 @@ -#NVDAObjects/window.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2006-2019 NV Access Limited, Babbage B.V., Bill Dengler -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2006-2020 NV Access Limited, Babbage B.V., Bill Dengler +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. import re import ctypes @@ -136,7 +135,7 @@ def findOverlayClasses(self,clsList): if not any(issubclass(cls,EditableText) for cls in clsList): gi=winUser.getGUIThreadInfo(self.windowThreadID) if gi.hwndCaret==self.windowHandle and gi.flags&winUser.GUI_CARETBLINKING: - if self.windowTextLineCount: + if self.windowTextLineCount and self.windowText: from .edit import UnidentifiedEdit clsList.append(UnidentifiedEdit) else: diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 9a5124af8e6..818c9701cf5 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -14,6 +14,7 @@ What's New in NVDA == Bug Fixes == - In terminal programs on Windows 10 version 1607 and later, when inserting or deleting characters in the middle of a line, the characters to the right of the caret are no longer read out. (#3200) - This experimental fix must be manually enabled in NVDA's advanced settings panel by changing the diff algorithm to Diff Match Patch. +- Fixed access to edit fields in MCS Electronics IDE's. (#11966) == Changes for Developers == From c4d6fb5d87e887950f84a767e73362f7cc60f862 Mon Sep 17 00:00:00 2001 From: Joseph Lee Date: Mon, 11 Jan 2021 16:30:21 -0800 Subject: [PATCH 014/174] Global commands: replace gestures map with script decorator (#11974) * Global commands: convert basic commands to use script decorator. Re #11964. The following commands were edited to use scriptHandler.script decorator: NVDA+N (show NVDA menu), NVDA+1 (toggle input help), NVDA+Q (quit NVDA), NVDA+F2 (pass next key through), NVDA+Shift+S/Z (toggle sleep mode), as well as unassigned restart NVDA command. For sleep mode toggle command, allow sleep mode flag is kept, and for NVDA menu and sleep mode toggle commands, gestures tuple is used (gestures order: keyboard (desktop and laptop), braille (including braille input and emulated keys), touch). * Global commands: convert system status scripts to use script decorator. Re #11964. Convert the following scripts: NVDA+F12 (time and date), NVDA+C (clipboard data announcement), NVDA+Shift+B (battery status). * Global commands: convert system focus and caret scripts to use script decorator. Re #11964. Convert the following commands: NVDA+up arrow/L (read current line), NVDA+Tab (current focus), NVDA+End/Shift+End (read status line), NVDA+down arrow/A (say all), NVDA+Shift+up arrow/Shift+S (say selection), NVDA+T (say title), NVDA+B (read foreground window). * Global commands: convert object navigation scripts to use script decorator. Re #11964. Converted object navigation scripts: NVDA+Numpad 5/4/6/8/2, NVDA+Shift+O/right/left/up/down arrows, object touch mode flicks (announce current object/move to next/previous/parent/first child) and friends. * Global commands: convert review cursor commands to script decorator. Re #11964. Convert review cursor commands: Shift+Numpad 7/9, Control+NvDA+Home/End on laptop layout (top/bottom), Numpad 7/8/9 (previous/current/next line)m Numpad 4/5/6 (previous/current/next word), Numpad 1/2/3 (previous/current/next character), Shift+Numpad 1/3 (start/end of line) and touch equivalents in text mode, along with review mark/copy commands. * Global commands: convert mouse and browse mode scripts to use script decorator. Re #11964. Convert the following commands: left/right mouse click/lock, move navigator object to mouse and mouse to navigator object, focus/browse mode toggle, parent tree interceptor. * Global commands: convert config dialogs/panels commands to use script decorator. Re #11964. Convert settings dialogs/panels opener commands to use script decorator, including ones iwthout a set gesture such as dictionary dialogs and review cursor panel. * Global commands: convert config management scripts to use script decorator. Re #11964. Convert the following configuration management scripts: Control+NVDA+C (save configuration), Control+NVDA+R (revert/reset configuration), Control+NVDA+P (open config profiles dialog), and an unassigned command to toggle profile triggers. * Global commands: convert settings scripts to use script decorator. Re #11964. Convert various settings scripts to use script decorator, including NVDA+2 (toggle speak typed characters), NVDA+U (probress bar output) and others. * Global commands: convert synth settings ring scripts to use script decorator. Re #11964. Convert synth settings ring scripts: Control+NVDA+arrows/Control+NVDA+Shift+arrows (next/previous setting, increase/decrease current setting). * Global commands: convert document formatting settings scripts to use script decorator. Re #11964. Convert document formatting scripts to use script decorator, all of them unassigned. * Global commands: convert unassigned settings scripts to script decorator. Re #11964. Convert unassigned settings scripts such as braille focus presentation, change braille cursor/shape, mouse text resolution, all unassigned. * Global commands: convert tools scripts to use script decorator. Re #11964. Convert tools scripts to script decorator, including app module info, UWP OCR, speech viewer, and others, some of them with gestures unassigned. * Global commands: convert braille display scripts to use script decorator. Re 311964. Converted braille input and outpu scripts (except keyboard emulation) to use script decorator (tested with a HumanWare BrailleNote Touch Plus). * Global commands: convert touch gestures to use script decorator. Re #11964. Convert touch-specific scripts to use script decorator, including touch hover, right click, and touch mode toggle. * Global commands: convert keyboard emulation scripts to use script decorator. Re #11964. Convert keyboard emulation scripts for Control, Alt, Windows, Shift, and NVDA keys to use script decorator. * Global commands: rearrange bypassInputHelp and remove commas from ends of function argument definitions. Re #11964. * Global commands: lint (Flake8 E203, E251) * Global commands: lint (Flake8 E501) * Global commands: fix spelling - 'wil' -> 'will' in speech mode command description. * Global commands: scriptHandler.script -> script * Global commands: remove gestures map, replaced by script decorator. Re #11964. Replace gestures map found in global commands with script decorator. * Global commands: update copyright year * Global commands: use allowInSleepMode flag for sleep mode toggle script. Re #11964. --- source/globalCommands.py | 1655 +++++++++++++++++++++++--------------- 1 file changed, 991 insertions(+), 664 deletions(-) diff --git a/source/globalCommands.py b/source/globalCommands.py index e9ecfdb5d77..7ce329cf161 100644 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -2,7 +2,7 @@ # 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) 2006-2020 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Rui Batista, Joseph Lee, +# Copyright (C) 2006-2021 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Rui Batista, Joseph Lee, # Leonard de Ruijter, Derek Riemer, Babbage B.V., Davy Kager, Ethan Holliger, Łukasz Golonka, Accessolutions, # Julien Cochuyt @@ -98,6 +98,13 @@ class GlobalCommands(ScriptableObject): """Commands that are available at all times, regardless of the current focus. """ + @script( + description=_( + # Translators: Describes the Cycle audio ducking mode command. + "Cycles through audio ducking modes which determine when NVDA lowers the volume of other sounds" + ), + gesture="kb:NVDA+shift+d" + ) def script_cycleAudioDuckingMode(self,gesture): if not audioDucking.isAudioDuckingSupported(): # Translators: a message when audio ducking is not supported on this machine @@ -110,9 +117,17 @@ def script_cycleAudioDuckingMode(self,gesture): config.conf['audio']['audioDuckingMode']=nextMode nextLabel=audioDucking.audioDuckingModes[nextMode] ui.message(nextLabel) - # Translators: Describes the Cycle audio ducking mode command. - script_cycleAudioDuckingMode.__doc__=_("Cycles through audio ducking modes which determine when NVDA lowers the volume of other sounds") + @script( + description=_( + # Translators: Input help mode message for toggle input help command. + "Turns input help on or off. " + "When on, any input such as pressing a key on the keyboard " + "will tell you what script is associated with that input, if any." + ), + category=SCRCAT_INPUT, + gesture="kb:NVDA+1" + ) def script_toggleInputHelp(self,gesture): inputCore.manager.isInputHelpActive = not inputCore.manager.isInputHelpActive # Translators: This will be presented when the input help is toggled. @@ -121,10 +136,13 @@ def script_toggleInputHelp(self,gesture): stateOff = _("input help off") state = stateOn if inputCore.manager.isInputHelpActive else stateOff ui.message(state) - # Translators: Input help mode message for toggle input help command. - script_toggleInputHelp.__doc__=_("Turns input help on or off. When on, any input such as pressing a key on the keyboard will tell you what script is associated with that input, if any.") - script_toggleInputHelp.category=SCRCAT_INPUT + @script( + # Translators: Input help mode message for toggle sleep mode command. + description=_("Toggles sleep mode on and off for the active application."), + gestures=("kb(desktop):NVDA+shift+s", "kb(laptop):NVDA+shift+z"), + allowInSleepMode=True + ) def script_toggleCurrentAppSleepMode(self,gesture): curFocus=api.getFocusObject() curApp=curFocus.appModule @@ -138,10 +156,17 @@ def script_toggleCurrentAppSleepMode(self,gesture): curApp.sleepMode=True # Translators: This is presented when sleep mode is activated, the focused application is self voicing, such as klango or openbook. ui.message(_("Sleep mode on")) - # Translators: Input help mode message for toggle sleep mode command. - script_toggleCurrentAppSleepMode.__doc__=_("Toggles sleep mode on and off for the active application.") - script_toggleCurrentAppSleepMode.allowInSleepMode=True + @script( + description=_( + # Translators: Input help mode message for report current line command. + "Reports the current line under the application cursor. " + "Pressing this key twice will spell the current line. " + "Pressing three times will spell the line using character descriptions." + ), + category=SCRCAT_SYSTEMCARET, + gestures=("kb(desktop):NVDA+upArrow", "kb(laptop):NVDA+l") + ) def script_reportCurrentLine(self,gesture): obj=api.getFocusObject() treeInterceptor=obj.treeInterceptor @@ -157,28 +182,37 @@ def script_reportCurrentLine(self,gesture): speech.speakTextInfo(info,unit=textInfos.UNIT_LINE,reason=controlTypes.REASON_CARET) else: speech.spellTextInfo(info,useCharacterDescriptions=scriptCount>1) - # Translators: Input help mode message for report current line command. - script_reportCurrentLine.__doc__=_("Reports the current line under the application cursor. Pressing this key twice will spell the current line. Pressing three times will spell the line using character descriptions.") - script_reportCurrentLine.category=SCRCAT_SYSTEMCARET + @script( + # Translators: Input help mode message for left mouse click command. + description=_("Clicks the left mouse button once at the current mouse position"), + category=SCRCAT_MOUSE, + gestures=("kb:numpadDivide", "kb(laptop):NVDA+[") + ) def script_leftMouseClick(self,gesture): # Translators: Reported when left mouse button is clicked. ui.message(_("Left click")) mouseHandler.executeMouseEvent(winUser.MOUSEEVENTF_LEFTDOWN,0,0) mouseHandler.executeMouseEvent(winUser.MOUSEEVENTF_LEFTUP,0,0) - # Translators: Input help mode message for left mouse click command. - script_leftMouseClick.__doc__=_("Clicks the left mouse button once at the current mouse position") - script_leftMouseClick.category=SCRCAT_MOUSE + @script( + # Translators: Input help mode message for right mouse click command. + description=_("Clicks the right mouse button once at the current mouse position"), + category=SCRCAT_MOUSE, + gestures=("kb:numpadMultiply", "kb(laptop):NVDA+]") + ) def script_rightMouseClick(self,gesture): # Translators: Reported when right mouse button is clicked. ui.message(_("Right click")) mouseHandler.executeMouseEvent(winUser.MOUSEEVENTF_RIGHTDOWN,0,0) mouseHandler.executeMouseEvent(winUser.MOUSEEVENTF_RIGHTUP,0,0) - # Translators: Input help mode message for right mouse click command. - script_rightMouseClick.__doc__=_("Clicks the right mouse button once at the current mouse position") - script_rightMouseClick.category=SCRCAT_MOUSE + @script( + # Translators: Input help mode message for left mouse lock/unlock toggle command. + description=_("Locks or unlocks the left mouse button"), + category=SCRCAT_MOUSE, + gestures=("kb:shift+numpadDivide", "kb(laptop):NVDA+control+[") + ) def script_toggleLeftMouseButton(self,gesture): if winUser.getKeyState(winUser.VK_LBUTTON)&32768: # Translators: This is presented when the left mouse button lock is released (used for drag and drop). @@ -188,10 +222,13 @@ def script_toggleLeftMouseButton(self,gesture): # Translators: This is presented when the left mouse button is locked down (used for drag and drop). ui.message(_("Left mouse button lock")) mouseHandler.executeMouseEvent(winUser.MOUSEEVENTF_LEFTDOWN,0,0) - # Translators: Input help mode message for left mouse lock/unlock toggle command. - script_toggleLeftMouseButton.__doc__=_("Locks or unlocks the left mouse button") - script_toggleLeftMouseButton.category=SCRCAT_MOUSE + @script( + # Translators: Input help mode message for right mouse lock/unlock command. + description=_("Locks or unlocks the right mouse button"), + category=SCRCAT_MOUSE, + gestures=("kb:shift+numpadMultiply", "kb(laptop):NVDA+control+]") + ) def script_toggleRightMouseButton(self,gesture): if winUser.getKeyState(winUser.VK_RBUTTON)&32768: # Translators: This is presented when the right mouse button lock is released (used for drag and drop). @@ -201,10 +238,16 @@ def script_toggleRightMouseButton(self,gesture): # Translators: This is presented when the right mouse button is locked down (used for drag and drop). ui.message(_("Right mouse button lock")) mouseHandler.executeMouseEvent(winUser.MOUSEEVENTF_RIGHTDOWN,0,0) - # Translators: Input help mode message for right mouse lock/unlock command. - script_toggleRightMouseButton.__doc__=_("Locks or unlocks the right mouse button") - script_toggleRightMouseButton.category=SCRCAT_MOUSE + @script( + description=_( + # Translators: Input help mode message for report current selection command. + "Announces the current selection in edit controls and documents. " + "If there is no selection it says so." + ), + category=SCRCAT_SYSTEMCARET, + gestures=("kb(desktop):NVDA+shift+upArrow", "kb(laptop):NVDA+shift+s") + ) def script_reportCurrentSelection(self,gesture): obj=api.getFocusObject() treeInterceptor=obj.treeInterceptor @@ -218,20 +261,26 @@ def script_reportCurrentSelection(self,gesture): speech.speakMessage(_("No selection")) else: speech.speakTextSelected(info.text) - # Translators: Input help mode message for report current selection command. - script_reportCurrentSelection.__doc__=_("Announces the current selection in edit controls and documents. If there is no selection it says so.") - script_reportCurrentSelection.category=SCRCAT_SYSTEMCARET + @script( + # Translators: Input help mode message for report date and time command. + description=_("If pressed once, reports the current time. If pressed twice, reports the current date"), + category=SCRCAT_SYSTEM, + gesture="kb:NVDA+f12" + ) def script_dateTime(self,gesture): if scriptHandler.getLastScriptRepeatCount()==0: text=winKernel.GetTimeFormatEx(winKernel.LOCALE_NAME_USER_DEFAULT, winKernel.TIME_NOSECONDS, None, None) else: text=winKernel.GetDateFormatEx(winKernel.LOCALE_NAME_USER_DEFAULT, winKernel.DATE_LONGDATE, None, None) ui.message(text) - # Translators: Input help mode message for report date and time command. - script_dateTime.__doc__=_("If pressed once, reports the current time. If pressed twice, reports the current date") - script_dateTime.category=SCRCAT_SYSTEM + @script( + # Translators: Input help mode message for increase synth setting value command. + description=_("Increases the currently active setting in the synth settings ring"), + category=SCRCAT_SPEECH, + gestures=("kb(desktop):NVDA+control+upArrow", "kb(laptop):NVDA+shift+control+upArrow") + ) def script_increaseSynthSetting(self,gesture): settingName=globalVars.settingsRing.currentSettingName if not settingName: @@ -240,10 +289,13 @@ def script_increaseSynthSetting(self,gesture): return settingValue=globalVars.settingsRing.increase() ui.message("%s %s" % (settingName,settingValue)) - # Translators: Input help mode message for increase synth setting value command. - script_increaseSynthSetting.__doc__=_("Increases the currently active setting in the synth settings ring") - script_increaseSynthSetting.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for decrease synth setting value command. + description=_("Decreases the currently active setting in the synth settings ring"), + category=SCRCAT_SPEECH, + gestures=("kb(desktop):NVDA+control+downArrow", "kb(laptop):NVDA+control+shift+downArrow") + ) def script_decreaseSynthSetting(self,gesture): settingName=globalVars.settingsRing.currentSettingName if not settingName: @@ -251,10 +303,13 @@ def script_decreaseSynthSetting(self,gesture): return settingValue=globalVars.settingsRing.decrease() ui.message("%s %s" % (settingName,settingValue)) - # Translators: Input help mode message for decrease synth setting value command. - script_decreaseSynthSetting.__doc__=_("Decreases the currently active setting in the synth settings ring") - script_decreaseSynthSetting.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for next synth setting command. + description=_("Moves to the next available setting in the synth settings ring"), + category=SCRCAT_SPEECH, + gestures=("kb(desktop):NVDA+control+rightArrow", "kb(laptop):NVDA+shift+control+rightArrow") + ) def script_nextSynthSetting(self,gesture): nextSettingName=globalVars.settingsRing.next() if not nextSettingName: @@ -262,10 +317,13 @@ def script_nextSynthSetting(self,gesture): return nextSettingValue=globalVars.settingsRing.currentSettingValue ui.message("%s %s"%(nextSettingName,nextSettingValue)) - # Translators: Input help mode message for next synth setting command. - script_nextSynthSetting.__doc__=_("Moves to the next available setting in the synth settings ring") - script_nextSynthSetting.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for previous synth setting command. + description=_("Moves to the previous available setting in the synth settings ring"), + category=SCRCAT_SPEECH, + gestures=("kb(desktop):NVDA+control+leftArrow", "kb(laptop):NVDA+shift+control+leftArrow") + ) def script_previousSynthSetting(self,gesture): previousSettingName=globalVars.settingsRing.previous() if not previousSettingName: @@ -273,10 +331,13 @@ def script_previousSynthSetting(self,gesture): return previousSettingValue=globalVars.settingsRing.currentSettingValue ui.message("%s %s"%(previousSettingName,previousSettingValue)) - # Translators: Input help mode message for previous synth setting command. - script_previousSynthSetting.__doc__=_("Moves to the previous available setting in the synth settings ring") - script_previousSynthSetting.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for toggle speaked typed characters command. + description=_("Toggles on and off the speaking of typed characters"), + category=SCRCAT_SPEECH, + gesture="kb:NVDA+2" + ) def script_toggleSpeakTypedCharacters(self,gesture): if config.conf["keyboard"]["speakTypedCharacters"]: # Translators: The message announced when toggling the speak typed characters keyboard setting. @@ -287,10 +348,13 @@ def script_toggleSpeakTypedCharacters(self,gesture): state = _("speak typed characters on") config.conf["keyboard"]["speakTypedCharacters"]=True ui.message(state) - # Translators: Input help mode message for toggle speaked typed characters command. - script_toggleSpeakTypedCharacters.__doc__=_("Toggles on and off the speaking of typed characters") - script_toggleSpeakTypedCharacters.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for toggle speak typed words command. + description=_("Toggles on and off the speaking of typed words"), + category=SCRCAT_SPEECH, + gesture="kb:NVDA+3" + ) def script_toggleSpeakTypedWords(self,gesture): if config.conf["keyboard"]["speakTypedWords"]: # Translators: The message announced when toggling the speak typed words keyboard setting. @@ -301,10 +365,13 @@ def script_toggleSpeakTypedWords(self,gesture): state = _("speak typed words on") config.conf["keyboard"]["speakTypedWords"]=True ui.message(state) - # Translators: Input help mode message for toggle speak typed words command. - script_toggleSpeakTypedWords.__doc__=_("Toggles on and off the speaking of typed words") - script_toggleSpeakTypedWords.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for toggle speak command keys command. + description=_("Toggles on and off the speaking of typed keys, that are not specifically characters"), + category=SCRCAT_SPEECH, + gesture="kb:NVDA+4" + ) def script_toggleSpeakCommandKeys(self,gesture): if config.conf["keyboard"]["speakCommandKeys"]: # Translators: The message announced when toggling the speak typed command keyboard setting. @@ -315,10 +382,12 @@ def script_toggleSpeakCommandKeys(self,gesture): state = _("speak command keys on") config.conf["keyboard"]["speakCommandKeys"]=True ui.message(state) - # Translators: Input help mode message for toggle speak command keys command. - script_toggleSpeakCommandKeys.__doc__=_("Toggles on and off the speaking of typed keys, that are not specifically characters") - script_toggleSpeakCommandKeys.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for toggle report font name command. + description=_("Toggles on and off the reporting of font changes"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportFontName(self,gesture): if config.conf["documentFormatting"]["reportFontName"]: # Translators: The message announced when toggling the report font name document formatting setting. @@ -329,10 +398,12 @@ def script_toggleReportFontName(self,gesture): state = _("report font name on") config.conf["documentFormatting"]["reportFontName"]=True ui.message(state) - # Translators: Input help mode message for toggle report font name command. - script_toggleReportFontName.__doc__=_("Toggles on and off the reporting of font changes") - script_toggleReportFontName.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report font size command. + description=_("Toggles on and off the reporting of font size changes"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportFontSize(self,gesture): if config.conf["documentFormatting"]["reportFontSize"]: # Translators: The message announced when toggling the report font size document formatting setting. @@ -343,10 +414,12 @@ def script_toggleReportFontSize(self,gesture): state = _("report font size on") config.conf["documentFormatting"]["reportFontSize"]=True ui.message(state) - # Translators: Input help mode message for toggle report font size command. - script_toggleReportFontSize.__doc__=_("Toggles on and off the reporting of font size changes") - script_toggleReportFontSize.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report font attributes command. + description=_("Toggles on and off the reporting of font attributes"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportFontAttributes(self,gesture): if config.conf["documentFormatting"]["reportFontAttributes"]: # Translators: The message announced when toggling the report font attributes document formatting setting. @@ -357,14 +430,11 @@ def script_toggleReportFontAttributes(self,gesture): state = _("report font attributes on") config.conf["documentFormatting"]["reportFontAttributes"]=True ui.message(state) - # Translators: Input help mode message for toggle report font attributes command. - script_toggleReportFontAttributes.__doc__=_("Toggles on and off the reporting of font attributes") - script_toggleReportFontAttributes.category=SCRCAT_DOCUMENTFORMATTING @script( # Translators: Input help mode message for toggle superscripts and subscripts command. description=_("Toggles on and off the reporting of superscripts and subscripts"), - category=SCRCAT_DOCUMENTFORMATTING, + category=SCRCAT_DOCUMENTFORMATTING ) def script_toggleReportSuperscriptsAndSubscripts(self, gesture): shouldReport: bool = not config.conf["documentFormatting"]["reportSuperscriptsAndSubscripts"] @@ -378,7 +448,12 @@ def script_toggleReportSuperscriptsAndSubscripts(self, gesture): # document formatting setting. state = _("report superscripts and subscripts off") ui.message(state) - + + @script( + # Translators: Input help mode message for toggle report revisions command. + description=_("Toggles on and off the reporting of revisions"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportRevisions(self,gesture): if config.conf["documentFormatting"]["reportRevisions"]: # Translators: The message announced when toggling the report revisions document formatting setting. @@ -389,10 +464,12 @@ def script_toggleReportRevisions(self,gesture): state = _("report revisions on") config.conf["documentFormatting"]["reportRevisions"]=True ui.message(state) - # Translators: Input help mode message for toggle report revisions command. - script_toggleReportRevisions.__doc__=_("Toggles on and off the reporting of revisions") - script_toggleReportRevisions.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report emphasis command. + description=_("Toggles on and off the reporting of emphasis"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportEmphasis(self,gesture): if config.conf["documentFormatting"]["reportEmphasis"]: # Translators: The message announced when toggling the report emphasis document formatting setting. @@ -403,14 +480,11 @@ def script_toggleReportEmphasis(self,gesture): state = _("report emphasis on") config.conf["documentFormatting"]["reportEmphasis"]=True ui.message(state) - # Translators: Input help mode message for toggle report emphasis command. - script_toggleReportEmphasis.__doc__=_("Toggles on and off the reporting of emphasis") - script_toggleReportEmphasis.category=SCRCAT_DOCUMENTFORMATTING - + @script( # Translators: Input help mode message for toggle report marked (highlighted) content command. description=_("Toggles on and off the reporting of marked text"), - category=SCRCAT_DOCUMENTFORMATTING, + category=SCRCAT_DOCUMENTFORMATTING ) def script_toggleReportHighlightedText(self, gesture): shouldReport: bool = not config.conf["documentFormatting"]["reportHighlight"] @@ -423,6 +497,11 @@ def script_toggleReportHighlightedText(self, gesture): state = _("report marked off") ui.message(state) + @script( + # Translators: Input help mode message for toggle report colors command. + description=_("Toggles on and off the reporting of colors"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportColor(self,gesture): if config.conf["documentFormatting"]["reportColor"]: # Translators: The message announced when toggling the report colors document formatting setting. @@ -433,10 +512,12 @@ def script_toggleReportColor(self,gesture): state = _("report colors on") config.conf["documentFormatting"]["reportColor"]=True ui.message(state) - # Translators: Input help mode message for toggle report colors command. - script_toggleReportColor.__doc__=_("Toggles on and off the reporting of colors") - script_toggleReportColor.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report alignment command. + description=_("Toggles on and off the reporting of text alignment"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportAlignment(self,gesture): if config.conf["documentFormatting"]["reportAlignment"]: # Translators: The message announced when toggling the report alignment document formatting setting. @@ -447,10 +528,12 @@ def script_toggleReportAlignment(self,gesture): state = _("report alignment on") config.conf["documentFormatting"]["reportAlignment"]=True ui.message(state) - # Translators: Input help mode message for toggle report alignment command. - script_toggleReportAlignment.__doc__=_("Toggles on and off the reporting of text alignment") - script_toggleReportAlignment.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report style command. + description=_("Toggles on and off the reporting of style changes"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportStyle(self,gesture): if config.conf["documentFormatting"]["reportStyle"]: # Translators: The message announced when toggling the report style document formatting setting. @@ -461,10 +544,12 @@ def script_toggleReportStyle(self,gesture): state = _("report style on") config.conf["documentFormatting"]["reportStyle"]=True ui.message(state) - # Translators: Input help mode message for toggle report style command. - script_toggleReportStyle.__doc__=_("Toggles on and off the reporting of style changes") - script_toggleReportStyle.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report spelling errors command. + description=_("Toggles on and off the reporting of spelling errors"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportSpellingErrors(self,gesture): if config.conf["documentFormatting"]["reportSpellingErrors"]: # Translators: The message announced when toggling the report spelling errors document formatting setting. @@ -475,10 +560,12 @@ def script_toggleReportSpellingErrors(self,gesture): state = _("report spelling errors on") config.conf["documentFormatting"]["reportSpellingErrors"]=True ui.message(state) - # Translators: Input help mode message for toggle report spelling errors command. - script_toggleReportSpellingErrors.__doc__=_("Toggles on and off the reporting of spelling errors") - script_toggleReportSpellingErrors.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report pages command. + description=_("Toggles on and off the reporting of pages"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportPage(self,gesture): if config.conf["documentFormatting"]["reportPage"]: # Translators: The message announced when toggling the report pages document formatting setting. @@ -489,10 +576,12 @@ def script_toggleReportPage(self,gesture): state = _("report pages on") config.conf["documentFormatting"]["reportPage"]=True ui.message(state) - # Translators: Input help mode message for toggle report pages command. - script_toggleReportPage.__doc__=_("Toggles on and off the reporting of pages") - script_toggleReportPage.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report line numbers command. + description=_("Toggles on and off the reporting of line numbers"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportLineNumber(self,gesture): if config.conf["documentFormatting"]["reportLineNumber"]: # Translators: The message announced when toggling the report line numbers document formatting setting. @@ -503,10 +592,12 @@ def script_toggleReportLineNumber(self,gesture): state = _("report line numbers on") config.conf["documentFormatting"]["reportLineNumber"]=True ui.message(state) - # Translators: Input help mode message for toggle report line numbers command. - script_toggleReportLineNumber.__doc__=_("Toggles on and off the reporting of line numbers") - script_toggleReportLineNumber.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report line indentation command. + description=_("Cycles through line indentation settings"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportLineIndentation(self,gesture): lineIndentationSpeech = config.conf["documentFormatting"]["reportLineIndentation"] lineIndentationTones = config.conf["documentFormatting"]["reportLineIndentationWithTones"] @@ -530,10 +621,12 @@ def script_toggleReportLineIndentation(self,gesture): lineIndentationTones = False config.conf["documentFormatting"]["reportLineIndentation"] = lineIndentationSpeech config.conf["documentFormatting"]["reportLineIndentationWithTones"] = lineIndentationTones - # Translators: Input help mode message for toggle report line indentation command. - script_toggleReportLineIndentation.__doc__=_("Cycles through line indentation settings") - script_toggleReportLineIndentation.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report paragraph indentation command. + description=_("Toggles on and off the reporting of paragraph indentation"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportParagraphIndentation(self,gesture): if config.conf["documentFormatting"]["reportParagraphIndentation"]: # Translators: The message announced when toggling the report paragraph indentation document formatting setting. @@ -544,10 +637,12 @@ def script_toggleReportParagraphIndentation(self,gesture): state = _("report paragraph indentation on") config.conf["documentFormatting"]["reportParagraphIndentation"]=True ui.message(state) - # Translators: Input help mode message for toggle report paragraph indentation command. - script_toggleReportParagraphIndentation.__doc__=_("Toggles on and off the reporting of paragraph indentation") - script_toggleReportParagraphIndentation.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report line spacing command. + description=_("Toggles on and off the reporting of line spacing"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportLineSpacing(self,gesture): if config.conf["documentFormatting"]["reportLineSpacing"]: # Translators: The message announced when toggling the report line spacing document formatting setting. @@ -558,10 +653,12 @@ def script_toggleReportLineSpacing(self,gesture): state = _("report line spacing on") config.conf["documentFormatting"]["reportLineSpacing"]=True ui.message(state) - # Translators: Input help mode message for toggle report line spacing command. - script_toggleReportLineSpacing.__doc__=_("Toggles on and off the reporting of line spacing") - script_toggleReportLineSpacing.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report tables command. + description=_("Toggles on and off the reporting of tables"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportTables(self,gesture): if config.conf["documentFormatting"]["reportTables"]: # Translators: The message announced when toggling the report tables document formatting setting. @@ -572,10 +669,12 @@ def script_toggleReportTables(self,gesture): state = _("report tables on") config.conf["documentFormatting"]["reportTables"]=True ui.message(state) - # Translators: Input help mode message for toggle report tables command. - script_toggleReportTables.__doc__=_("Toggles on and off the reporting of tables") - script_toggleReportTables.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report table row/column headers command. + description=_("Toggles on and off the reporting of table row and column headers"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportTableHeaders(self,gesture): if config.conf["documentFormatting"]["reportTableHeaders"]: # Translators: The message announced when toggling the report table row/column headers document formatting setting. @@ -586,10 +685,12 @@ def script_toggleReportTableHeaders(self,gesture): state = _("report table row and column headers on") config.conf["documentFormatting"]["reportTableHeaders"]=True ui.message(state) - # Translators: Input help mode message for toggle report table row/column headers command. - script_toggleReportTableHeaders.__doc__=_("Toggles on and off the reporting of table row and column headers") - script_toggleReportTableHeaders.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report table cell coordinates command. + description=_("Toggles on and off the reporting of table cell coordinates"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportTableCellCoords(self,gesture): if config.conf["documentFormatting"]["reportTableCellCoords"]: # Translators: The message announced when toggling the report table cell coordinates document formatting setting. @@ -600,10 +701,12 @@ def script_toggleReportTableCellCoords(self,gesture): state = _("report table cell coordinates on") config.conf["documentFormatting"]["reportTableCellCoords"]=True ui.message(state) - # Translators: Input help mode message for toggle report table cell coordinates command. - script_toggleReportTableCellCoords.__doc__=_("Toggles on and off the reporting of table cell coordinates") - script_toggleReportTableCellCoords.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report links command. + description=_("Toggles on and off the reporting of links"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportLinks(self,gesture): if config.conf["documentFormatting"]["reportLinks"]: # Translators: The message announced when toggling the report links document formatting setting. @@ -614,11 +717,8 @@ def script_toggleReportLinks(self,gesture): state = _("report links on") config.conf["documentFormatting"]["reportLinks"]=True ui.message(state) - # Translators: Input help mode message for toggle report links command. - script_toggleReportLinks.__doc__=_("Toggles on and off the reporting of links") - script_toggleReportLinks.category=SCRCAT_DOCUMENTFORMATTING - @scriptHandler.script( + @script( # Translators: Input help mode message for toggle report graphics command. description=_("Toggles on and off the reporting of graphics"), category=SCRCAT_DOCUMENTFORMATTING @@ -634,6 +734,11 @@ def script_toggleReportGraphics(self, gesture): config.conf["documentFormatting"]["reportGraphics"] = True ui.message(state) + @script( + # Translators: Input help mode message for toggle report comments command. + description=_("Toggles on and off the reporting of comments"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportComments(self,gesture): if config.conf["documentFormatting"]["reportComments"]: # Translators: The message announced when toggling the report comments document formatting setting. @@ -644,10 +749,12 @@ def script_toggleReportComments(self,gesture): state = _("report comments on") config.conf["documentFormatting"]["reportComments"]=True ui.message(state) - # Translators: Input help mode message for toggle report comments command. - script_toggleReportComments.__doc__=_("Toggles on and off the reporting of comments") - script_toggleReportComments.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report lists command. + description=_("Toggles on and off the reporting of lists"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportLists(self,gesture): if config.conf["documentFormatting"]["reportLists"]: # Translators: The message announced when toggling the report lists document formatting setting. @@ -658,10 +765,12 @@ def script_toggleReportLists(self,gesture): state = _("report lists on") config.conf["documentFormatting"]["reportLists"]=True ui.message(state) - # Translators: Input help mode message for toggle report lists command. - script_toggleReportLists.__doc__=_("Toggles on and off the reporting of lists") - script_toggleReportLists.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report headings command. + description=_("Toggles on and off the reporting of headings"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportHeadings(self,gesture): if config.conf["documentFormatting"]["reportHeadings"]: # Translators: The message announced when toggling the report headings document formatting setting. @@ -672,9 +781,6 @@ def script_toggleReportHeadings(self,gesture): state = _("report headings on") config.conf["documentFormatting"]["reportHeadings"]=True ui.message(state) - # Translators: Input help mode message for toggle report headings command. - script_toggleReportHeadings.__doc__=_("Toggles on and off the reporting of headings") - script_toggleReportHeadings.category=SCRCAT_DOCUMENTFORMATTING @script( # Translators: Input help mode message for toggle report groupings command. @@ -692,6 +798,11 @@ def script_toggleReportGroupings(self, gesture): config.conf["documentFormatting"]["reportGroupings"] = True ui.message(state) + @script( + # Translators: Input help mode message for toggle report block quotes command. + description=_("Toggles on and off the reporting of block quotes"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportBlockQuotes(self,gesture): if config.conf["documentFormatting"]["reportBlockQuotes"]: # Translators: The message announced when toggling the report block quotes document formatting setting. @@ -702,10 +813,12 @@ def script_toggleReportBlockQuotes(self,gesture): state = _("report block quotes on") config.conf["documentFormatting"]["reportBlockQuotes"]=True ui.message(state) - # Translators: Input help mode message for toggle report block quotes command. - script_toggleReportBlockQuotes.__doc__=_("Toggles on and off the reporting of block quotes") - script_toggleReportBlockQuotes.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report landmarks command. + description=_("Toggles on and off the reporting of landmarks"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportLandmarks(self,gesture): if config.conf["documentFormatting"]["reportLandmarks"]: # Translators: The message announced when toggling the report landmarks document formatting setting. @@ -716,9 +829,6 @@ def script_toggleReportLandmarks(self,gesture): state = _("report landmarks and regions on") config.conf["documentFormatting"]["reportLandmarks"]=True ui.message(state) - # Translators: Input help mode message for toggle report landmarks command. - script_toggleReportLandmarks.__doc__=_("Toggles on and off the reporting of landmarks") - script_toggleReportLandmarks.category=SCRCAT_DOCUMENTFORMATTING @script( # Translators: Input help mode message for toggle report articles command. @@ -736,6 +846,11 @@ def script_toggleReportArticles(self, gesture): config.conf["documentFormatting"]["reportArticles"] = True ui.message(state) + @script( + # Translators: Input help mode message for toggle report frames command. + description=_("Toggles on and off the reporting of frames"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportFrames(self,gesture): if config.conf["documentFormatting"]["reportFrames"]: # Translators: The message announced when toggling the report frames document formatting setting. @@ -746,10 +861,12 @@ def script_toggleReportFrames(self,gesture): state = _("report frames on") config.conf["documentFormatting"]["reportFrames"]=True ui.message(state) - # Translators: Input help mode message for toggle report frames command. - script_toggleReportFrames.__doc__=_("Toggles on and off the reporting of frames") - script_toggleReportFrames.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for toggle report if clickable command. + description=_("Toggles on and off reporting if clickable"), + category=SCRCAT_DOCUMENTFORMATTING + ) def script_toggleReportClickable(self,gesture): if config.conf["documentFormatting"]["reportClickable"]: # Translators: The message announced when toggling the report if clickable document formatting setting. @@ -760,10 +877,13 @@ def script_toggleReportClickable(self,gesture): state = _("report if clickable on") config.conf["documentFormatting"]["reportClickable"]=True ui.message(state) - # Translators: Input help mode message for toggle report if clickable command. - script_toggleReportClickable.__doc__=_("Toggles on and off reporting if clickable") - script_toggleReportClickable.category=SCRCAT_DOCUMENTFORMATTING + @script( + # Translators: Input help mode message for cycle speech symbol level command. + description=_("Cycles through speech symbol levels which determine what symbols are spoken"), + category=SCRCAT_SPEECH, + gesture="kb:NVDA+p" + ) def script_cycleSpeechSymbolLevel(self,gesture): curLevel = config.conf["speech"]["symbolLevel"] for level in characterProcessing.CONFIGURABLE_SPEECH_SYMBOL_LEVELS: @@ -777,10 +897,13 @@ def script_cycleSpeechSymbolLevel(self,gesture): # which determine what symbols are spoken. # %s will be replaced with the symbol level; e.g. none, some, most and all. ui.message(_("Symbol level %s") % name) - # Translators: Input help mode message for cycle speech symbol level command. - script_cycleSpeechSymbolLevel.__doc__=_("Cycles through speech symbol levels which determine what symbols are spoken") - script_cycleSpeechSymbolLevel.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for move mouse to navigator object command. + description=_("Moves the mouse pointer to the current navigator object"), + category=SCRCAT_MOUSE, + gestures=("kb:NVDA+numpadDivide", "kb(laptop):NVDA+shift+m") + ) def script_moveMouseToNavigatorObject(self,gesture): try: p=api.getReviewPosition().pointAtStart @@ -800,20 +923,29 @@ def script_moveMouseToNavigatorObject(self,gesture): y=top+(height//2) winUser.setCursorPos(x,y) mouseHandler.executeMouseMoveEvent(x,y) - # Translators: Input help mode message for move mouse to navigator object command. - script_moveMouseToNavigatorObject.__doc__=_("Moves the mouse pointer to the current navigator object") - script_moveMouseToNavigatorObject.category=SCRCAT_MOUSE + @script( + # Translators: Input help mode message for move navigator object to mouse command. + description=_("Sets the navigator object to the current object under the mouse pointer and speaks it"), + category=SCRCAT_MOUSE, + gestures=("kb:NVDA+numpadMultiply", "kb(laptop):NVDA+shift+n") + ) def script_moveNavigatorObjectToMouse(self,gesture): # Translators: Reported when attempting to move the navigator object to the object under mouse pointer. ui.message(_("Move navigator object to mouse")) obj=api.getMouseObject() api.setNavigatorObject(obj) speech.speakObject(obj) - # Translators: Input help mode message for move navigator object to mouse command. - script_moveNavigatorObjectToMouse.__doc__=_("Sets the navigator object to the current object under the mouse pointer and speaks it") - script_moveNavigatorObjectToMouse.category=SCRCAT_MOUSE + @script( + description=_( + # Translators: Script help message for next review mode command. + "Switches to the next review mode (e.g. object, document or screen) " + "and positions the review position at the point of the navigator object" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:NVDA+numpad7", "kb(laptop):NVDA+pageUp", "ts(object):2finger_flickUp") + ) def script_reviewMode_next(self,gesture): label=review.nextMode() if label: @@ -825,10 +957,16 @@ def script_reviewMode_next(self,gesture): else: # Translators: reported when there are no other available review modes for this object ui.reviewMessage(_("No next review mode")) - # Translators: Script help message for next review mode command. - script_reviewMode_next.__doc__=_("Switches to the next review mode (e.g. object, document or screen) and positions the review position at the point of the navigator object") - script_reviewMode_next.category=SCRCAT_TEXTREVIEW + @script( + description=_( + # Translators: Script help message for previous review mode command. + "Switches to the previous review mode (e.g. object, document or screen) " + "and positions the review position at the point of the navigator object" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:NVDA+numpad1", "kb(laptop):NVDA+pageDown", "ts(object):2finger_flickDown") + ) def script_reviewMode_previous(self,gesture): label=review.nextMode(prev=True) if label: @@ -840,10 +978,12 @@ def script_reviewMode_previous(self,gesture): else: # Translators: reported when there are no other available review modes for this object ui.reviewMessage(_("No previous review mode")) - # Translators: Script help message for previous review mode command. - script_reviewMode_previous.__doc__=_("Switches to the previous review mode (e.g. object, document or screen) and positions the review position at the point of the navigator object") - script_reviewMode_previous.category=SCRCAT_TEXTREVIEW + @script( + # Translators: Input help mode message for toggle simple review mode command. + description=_("Toggles simple review mode on and off"), + category=SCRCAT_OBJECTNAVIGATION + ) def script_toggleSimpleReviewMode(self,gesture): if config.conf["reviewCursor"]["simpleReviewMode"]: # Translators: The message announced when toggling simple review mode. @@ -854,10 +994,17 @@ def script_toggleSimpleReviewMode(self,gesture): state = _("Simple review mode on") config.conf["reviewCursor"]["simpleReviewMode"]=True ui.message(state) - # Translators: Input help mode message for toggle simple review mode command. - script_toggleSimpleReviewMode.__doc__=_("Toggles simple review mode on and off") - script_toggleSimpleReviewMode.category=SCRCAT_OBJECTNAVIGATION + @script( + description=_( + # Translators: Input help mode message for report current navigator object command. + "Reports the current navigator object. " + "Pressing twice spells this information, " + "and pressing three times Copies name and value of this object to the clipboard" + ), + category=SCRCAT_OBJECTNAVIGATION, + gestures=("kb:NVDA+numpad5", "kb(laptop):NVDA+shift+o") + ) def script_navigatorObject_current(self,gesture): curObject=api.getNavigatorObject() if not isinstance(curObject,NVDAObject): @@ -895,10 +1042,16 @@ def script_navigatorObject_current(self,gesture): api.copyToClip(text, notify=True) else: speech.speakObject(curObject,reason=controlTypes.REASON_QUERY) - # Translators: Input help mode message for report current navigator object command. - script_navigatorObject_current.__doc__=_("Reports the current navigator object. Pressing twice spells this information, and pressing three times Copies name and value of this object to the clipboard") - script_navigatorObject_current.category=SCRCAT_OBJECTNAVIGATION + @script( + description=_( + # Translators: Description for report review cursor location command. + "Reports information about the location of the text or object at the review cursor. " + "Pressing twice may provide further detail." + ), + category=SCRCAT_OBJECTNAVIGATION, + gestures=("kb:NVDA+numpadDelete", "kb(laptop):NVDA+delete") + ) def script_navigatorObject_currentDimensions(self,gesture): count=scriptHandler.getLastScriptRepeatCount() locationText=api.getReviewPosition().locationText if count==0 else None @@ -909,9 +1062,6 @@ def script_navigatorObject_currentDimensions(self,gesture): ui.message(_("No location information")) return ui.message(locationText) - # Translators: Description for report review cursor location command. - script_navigatorObject_currentDimensions.__doc__=_("Reports information about the location of the text or object at the review cursor. Pressing twice may provide further detail.") - script_navigatorObject_currentDimensions.category=SCRCAT_OBJECTNAVIGATION @script( description=_( @@ -920,7 +1070,7 @@ def script_navigatorObject_currentDimensions(self,gesture): "and the review cursor to the position of the caret inside it, if possible." ), category=SCRCAT_OBJECTNAVIGATION, - gestures=("kb:NVDA+numpadMinus", "kb(laptop):NVDA+backspace"), + gestures=("kb:NVDA+numpadMinus", "kb(laptop):NVDA+backspace") ) def script_navigatorObject_toFocus(self,gesture): tIAtCaret = self._getTIAtCaret(True) @@ -931,6 +1081,15 @@ def script_navigatorObject_toFocus(self,gesture): speech.speakMessage(_("Move to focus")) speech.speakObject(api.getNavigatorObject(), reason=controlTypes.OutputReason.FOCUS) + @script( + description=_( + # Translators: Input help mode message for move focus to current navigator object command. + "Pressed once sets the keyboard focus to the navigator object, " + "pressed twice sets the system caret to the position of the review cursor" + ), + category=SCRCAT_OBJECTNAVIGATION, + gestures=("kb:NVDA+shift+numpadMinus", "kb(laptop):NVDA+shift+backspace") + ) def script_navigatorObject_moveFocus(self,gesture): obj=api.getNavigatorObject() if not isinstance(obj,NVDAObject): @@ -953,10 +1112,13 @@ def script_navigatorObject_moveFocus(self,gesture): info=review.copy() info.expand(textInfos.UNIT_LINE) speech.speakTextInfo(info,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move focus to current navigator object command. - script_navigatorObject_moveFocus.__doc__=_("Pressed once sets the keyboard focus to the navigator object, pressed twice sets the system caret to the position of the review cursor") - script_navigatorObject_moveFocus.category=SCRCAT_OBJECTNAVIGATION + @script( + # Translators: Input help mode message for move to parent object command. + description=_("Moves the navigator object to the object containing it"), + category=SCRCAT_OBJECTNAVIGATION, + gestures=("kb:NVDA+numpad8", "kb(laptop):NVDA+shift+upArrow", "ts(object):flickup") + ) def script_navigatorObject_parent(self,gesture): curObject=api.getNavigatorObject() if not isinstance(curObject,NVDAObject): @@ -972,10 +1134,13 @@ def script_navigatorObject_parent(self,gesture): else: # Translators: Reported when there is no containing (parent) object such as when focused on desktop. ui.reviewMessage(_("No containing object")) - # Translators: Input help mode message for move to parent object command. - script_navigatorObject_parent.__doc__=_("Moves the navigator object to the object containing it") - script_navigatorObject_parent.category=SCRCAT_OBJECTNAVIGATION + @script( + # Translators: Input help mode message for move to next object command. + description=_("Moves the navigator object to the next object"), + category=SCRCAT_OBJECTNAVIGATION, + gestures=("kb:NVDA+numpad6", "kb(laptop):NVDA+shift+rightArrow", "ts(object):2finger_flickright") + ) def script_navigatorObject_next(self,gesture): curObject=api.getNavigatorObject() if not isinstance(curObject,NVDAObject): @@ -991,10 +1156,13 @@ def script_navigatorObject_next(self,gesture): else: # Translators: Reported when there is no next object (current object is the last object). ui.reviewMessage(_("No next")) - # Translators: Input help mode message for move to next object command. - script_navigatorObject_next.__doc__=_("Moves the navigator object to the next object") - script_navigatorObject_next.category=SCRCAT_OBJECTNAVIGATION + @script( + # Translators: Input help mode message for move to previous object command. + description=_("Moves the navigator object to the previous object"), + category=SCRCAT_OBJECTNAVIGATION, + gestures=("kb:NVDA+numpad4", "kb(laptop):NVDA+shift+leftArrow", "ts(object):2finger_flickleft") + ) def script_navigatorObject_previous(self,gesture): curObject=api.getNavigatorObject() if not isinstance(curObject,NVDAObject): @@ -1010,10 +1178,13 @@ def script_navigatorObject_previous(self,gesture): else: # Translators: Reported when there is no previous object (current object is the first object). ui.reviewMessage(_("No previous")) - # Translators: Input help mode message for move to previous object command. - script_navigatorObject_previous.__doc__=_("Moves the navigator object to the previous object") - script_navigatorObject_previous.category=SCRCAT_OBJECTNAVIGATION + @script( + # Translators: Input help mode message for move to first child object command. + description=_("Moves the navigator object to the first object inside it"), + category=SCRCAT_OBJECTNAVIGATION, + gestures=("kb:NVDA+numpad2", "kb(laptop):NVDA+shift+downArrow", "ts(object):flickdown") + ) def script_navigatorObject_firstChild(self,gesture): curObject=api.getNavigatorObject() if not isinstance(curObject,NVDAObject): @@ -1029,10 +1200,16 @@ def script_navigatorObject_firstChild(self,gesture): else: # Translators: Reported when there is no contained (first child) object such as inside a document. ui.reviewMessage(_("No objects inside")) - # Translators: Input help mode message for move to first child object command. - script_navigatorObject_firstChild.__doc__=_("Moves the navigator object to the first object inside it") - script_navigatorObject_firstChild.category=SCRCAT_OBJECTNAVIGATION + @script( + description=_( + # Translators: Input help mode message for activate current object command. + "Performs the default action on the current navigator object " + "(example: presses it if it is a button)." + ), + category=SCRCAT_OBJECTNAVIGATION, + gestures=("kb:NVDA+numpadEnter", "kb(laptop):NVDA+enter", "ts:double_tap") + ) def script_review_activate(self,gesture): # Translators: a message reported when the action at the position of the review cursor or navigator object is performed. actionName=_("Activate") @@ -1063,19 +1240,26 @@ def script_review_activate(self,gesture): obj=obj.parent # Translators: the message reported when there is no action to perform on the review position or navigator object. ui.message(_("No action")) - # Translators: Input help mode message for activate current object command. - script_review_activate.__doc__=_("Performs the default action on the current navigator object (example: presses it if it is a button).") - script_review_activate.category=SCRCAT_OBJECTNAVIGATION + @script( + # Translators: Input help mode message for move review cursor to top line command. + description=_("Moves the review cursor to the top line of the current navigator object and speaks it"), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:shift+numpad7", "kb(laptop):NVDA+control+home") + ) def script_review_top(self,gesture): info=api.getReviewPosition().obj.makeTextInfo(textInfos.POSITION_FIRST) api.setReviewPosition(info) info.expand(textInfos.UNIT_LINE) speech.speakTextInfo(info,unit=textInfos.UNIT_LINE,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move review cursor to top line command. - script_review_top.__doc__=_("Moves the review cursor to the top line of the current navigator object and speaks it") - script_review_top.category=SCRCAT_TEXTREVIEW + @script( + # Translators: Input help mode message for move review cursor to previous line command. + description=_("Moves the review cursor to the previous line of the current navigator object and speaks it"), + resumeSayAllMode=sayAllHandler.CURSOR_REVIEW, + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad7", "kb(laptop):NVDA+upArrow", "ts(text):flickUp") + ) def script_review_previousLine(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_LINE) @@ -1088,11 +1272,17 @@ def script_review_previousLine(self,gesture): api.setReviewPosition(info) info.expand(textInfos.UNIT_LINE) speech.speakTextInfo(info,unit=textInfos.UNIT_LINE,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move review cursor to previous line command. - script_review_previousLine.__doc__=_("Moves the review cursor to the previous line of the current navigator object and speaks it") - script_review_previousLine.resumeSayAllMode=sayAllHandler.CURSOR_REVIEW - script_review_previousLine.category=SCRCAT_TEXTREVIEW + @script( + description=_( + # Translators: Input help mode message for read current line under review cursor command. + "Reports the line of the current navigator object where the review cursor is situated. " + "If this key is pressed twice, the current line will be spelled. " + "Pressing three times will spell the line using character descriptions." + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad8", "kb(laptop):NVDA+shift+.") + ) def script_review_currentLine(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_LINE) @@ -1103,10 +1293,14 @@ def script_review_currentLine(self,gesture): speech.speakTextInfo(info,unit=textInfos.UNIT_LINE,reason=controlTypes.REASON_CARET) else: speech.spellTextInfo(info,useCharacterDescriptions=scriptCount>1) - # Translators: Input help mode message for read current line under review cursor command. - script_review_currentLine.__doc__=_("Reports the line of the current navigator object where the review cursor is situated. If this key is pressed twice, the current line will be spelled. Pressing three times will spell the line using character descriptions.") - script_review_currentLine.category=SCRCAT_TEXTREVIEW + @script( + # Translators: Input help mode message for move review cursor to next line command. + description=_("Moves the review cursor to the next line of the current navigator object and speaks it"), + resumeSayAllMode=sayAllHandler.CURSOR_REVIEW, + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad9", "kb(laptop):NVDA+downArrow", "ts(text):flickDown") + ) def script_review_nextLine(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_LINE) @@ -1119,20 +1313,25 @@ def script_review_nextLine(self,gesture): api.setReviewPosition(info) info.expand(textInfos.UNIT_LINE) speech.speakTextInfo(info,unit=textInfos.UNIT_LINE,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move review cursor to next line command. - script_review_nextLine.__doc__=_("Moves the review cursor to the next line of the current navigator object and speaks it") - script_review_nextLine.resumeSayAllMode=sayAllHandler.CURSOR_REVIEW - script_review_nextLine.category=SCRCAT_TEXTREVIEW + @script( + # Translators: Input help mode message for move review cursor to bottom line command. + description=_("Moves the review cursor to the bottom line of the current navigator object and speaks it"), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:shift+numpad9", "kb(laptop):NVDA+control+end") + ) def script_review_bottom(self,gesture): info=api.getReviewPosition().obj.makeTextInfo(textInfos.POSITION_LAST) api.setReviewPosition(info) info.expand(textInfos.UNIT_LINE) speech.speakTextInfo(info,unit=textInfos.UNIT_LINE,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move review cursor to bottom line command. - script_review_bottom.__doc__=_("Moves the review cursor to the bottom line of the current navigator object and speaks it") - script_review_bottom.category=SCRCAT_TEXTREVIEW + @script( + # Translators: Input help mode message for move review cursor to previous word command. + description=_("Moves the review cursor to the previous word of the current navigator object and speaks it"), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad4", "kb(laptop):NVDA+control+leftArrow", "ts(text):2finger_flickLeft") + ) def script_review_previousWord(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_WORD) @@ -1145,10 +1344,17 @@ def script_review_previousWord(self,gesture): api.setReviewPosition(info) info.expand(textInfos.UNIT_WORD) speech.speakTextInfo(info,reason=controlTypes.REASON_CARET,unit=textInfos.UNIT_WORD) - # Translators: Input help mode message for move review cursor to previous word command. - script_review_previousWord.__doc__=_("Moves the review cursor to the previous word of the current navigator object and speaks it") - script_review_previousWord.category=SCRCAT_TEXTREVIEW + @script( + description=_( + # Translators: Input help mode message for report current word under review cursor command. + "Speaks the word of the current navigator object where the review cursor is situated. " + "Pressing twice spells the word. " + "Pressing three times spells the word using character descriptions" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad5", "kb(laptop):NVDA+control+.", "ts(text):hoverUp") + ) def script_review_currentWord(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_WORD) @@ -1159,10 +1365,13 @@ def script_review_currentWord(self,gesture): speech.speakTextInfo(info,reason=controlTypes.REASON_CARET,unit=textInfos.UNIT_WORD) else: speech.spellTextInfo(info,useCharacterDescriptions=scriptCount>1) - # Translators: Input help mode message for report current word under review cursor command. - script_review_currentWord.__doc__=_("Speaks the word of the current navigator object where the review cursor is situated. Pressing twice spells the word. Pressing three times spells the word using character descriptions") - script_review_currentWord.category=SCRCAT_TEXTREVIEW + @script( + # Translators: Input help mode message for move review cursor to next word command. + description=_("Moves the review cursor to the next word of the current navigator object and speaks it"), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad6", "kb(laptop):NVDA+control+rightArrow", "ts(text):2finger_flickRight") + ) def script_review_nextWord(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_WORD) @@ -1175,10 +1384,16 @@ def script_review_nextWord(self,gesture): api.setReviewPosition(info) info.expand(textInfos.UNIT_WORD) speech.speakTextInfo(info,reason=controlTypes.REASON_CARET,unit=textInfos.UNIT_WORD) - # Translators: Input help mode message for move review cursor to next word command. - script_review_nextWord.__doc__=_("Moves the review cursor to the next word of the current navigator object and speaks it") - script_review_nextWord.category=SCRCAT_TEXTREVIEW + @script( + description=_( + # Translators: Input help mode message for move review cursor to start of current line command. + "Moves the review cursor to the first character of the line " + "where it is situated in the current navigator object and speaks it" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:shift+numpad1", "kb(laptop):NVDA+home") + ) def script_review_startOfLine(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_LINE) @@ -1186,10 +1401,15 @@ def script_review_startOfLine(self,gesture): api.setReviewPosition(info) info.expand(textInfos.UNIT_CHARACTER) speech.speakTextInfo(info,unit=textInfos.UNIT_CHARACTER,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move review cursor to start of current line command. - script_review_startOfLine.__doc__=_("Moves the review cursor to the first character of the line where it is situated in the current navigator object and speaks it") - script_review_startOfLine.category=SCRCAT_TEXTREVIEW + @script( + description=_( + # Translators: Input help mode message for move review cursor to previous character command. + "Moves the review cursor to the previous character of the current navigator object and speaks it" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad1", "kb(laptop):NVDA+leftArrow", "ts(text):flickLeft") + ) def script_review_previousCharacter(self,gesture): lineInfo=api.getReviewPosition().copy() lineInfo.expand(textInfos.UNIT_LINE) @@ -1207,10 +1427,17 @@ def script_review_previousCharacter(self,gesture): api.setReviewPosition(charInfo) charInfo.expand(textInfos.UNIT_CHARACTER) speech.speakTextInfo(charInfo,unit=textInfos.UNIT_CHARACTER,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move review cursor to previous character command. - script_review_previousCharacter.__doc__=_("Moves the review cursor to the previous character of the current navigator object and speaks it") - script_review_previousCharacter.category=SCRCAT_TEXTREVIEW + @script( + description=_( + # Translators: Input help mode message for report current character under review cursor command. + "Reports the character of the current navigator object where the review cursor is situated. " + "Pressing twice reports a description or example of that character. " + "Pressing three times reports the numeric value of the character in decimal and hexadecimal" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad2", "kb(laptop):NVDA+.") + ) def script_review_currentCharacter(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_CHARACTER) @@ -1232,10 +1459,15 @@ def script_review_currentCharacter(self,gesture): else: log.debugWarning("Couldn't calculate ordinal for character %r" % info.text) speech.speakTextInfo(info,unit=textInfos.UNIT_CHARACTER,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for report current character under review cursor command. - script_review_currentCharacter.__doc__=_("Reports the character of the current navigator object where the review cursor is situated. Pressing twice reports a description or example of that character. Pressing three times reports the numeric value of the character in decimal and hexadecimal") - script_review_currentCharacter.category=SCRCAT_TEXTREVIEW + @script( + description=_( + # Translators: Input help mode message for move review cursor to next character command. + "Moves the review cursor to the next character of the current navigator object and speaks it" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpad3", "kb(laptop):NVDA+rightArrow", "ts(text):flickRight") + ) def script_review_nextCharacter(self,gesture): lineInfo=api.getReviewPosition().copy() lineInfo.expand(textInfos.UNIT_LINE) @@ -1253,10 +1485,16 @@ def script_review_nextCharacter(self,gesture): api.setReviewPosition(charInfo) charInfo.expand(textInfos.UNIT_CHARACTER) speech.speakTextInfo(charInfo,unit=textInfos.UNIT_CHARACTER,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move review cursor to next character command. - script_review_nextCharacter.__doc__=_("Moves the review cursor to the next character of the current navigator object and speaks it") - script_review_nextCharacter.category=SCRCAT_TEXTREVIEW + @script( + description=_( + # Translators: Input help mode message for move review cursor to end of current line command. + "Moves the review cursor to the last character of the line " + "where it is situated in the current navigator object and speaks it" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:shift+numpad3", "kb(laptop):NVDA+end") + ) def script_review_endOfLine(self,gesture): info=api.getReviewPosition().copy() info.expand(textInfos.UNIT_LINE) @@ -1265,9 +1503,6 @@ def script_review_endOfLine(self,gesture): api.setReviewPosition(info) info.expand(textInfos.UNIT_CHARACTER) speech.speakTextInfo(info,unit=textInfos.UNIT_CHARACTER,reason=controlTypes.REASON_CARET) - # Translators: Input help mode message for move review cursor to end of current line command. - script_review_endOfLine.__doc__=_("Moves the review cursor to the last character of the line where it is situated in the current navigator object and speaks it") - script_review_endOfLine.category=SCRCAT_TEXTREVIEW def _getCurrentLanguageForTextInfo(self, info): curLanguage = None @@ -1282,7 +1517,7 @@ def _getCurrentLanguageForTextInfo(self, info): @script( # Translators: Input help mode message for Review Current Symbol command. description=_("Reports the symbol where the review cursor is positioned. Pressed twice, shows the symbol and the text used to speak it in browse mode"), - category=SCRCAT_TEXTREVIEW, + category=SCRCAT_TEXTREVIEW ) def script_review_currentSymbol(self,gesture): info=api.getReviewPosition().copy() @@ -1305,6 +1540,17 @@ def script_review_currentSymbol(self,gesture): title = _("Expanded symbol ({})").format(languageDescription) ui.browseableMessage(message, title) + @script( + description=_( + # Translators: Input help mode message for toggle speech mode command. + "Toggles between the speech modes of off, beep and talk. " + "When set to off NVDA will not speak anything. " + "If beeps then NVDA will simply beep each time it its supposed to speak something. " + "If talk then NVDA will just speak normally." + ), + category=SCRCAT_SPEECH, + gesture="kb:NVDA+s" + ) def script_speechMode(self,gesture): curMode=speech.speechMode speech.speechMode=speech.speechMode_talk @@ -1321,10 +1567,16 @@ def script_speechMode(self,gesture): speech.cancelSpeech() ui.message(name) speech.speechMode=newMode - # Translators: Input help mode message for toggle speech mode command. - script_speechMode.__doc__=_("Toggles between the speech modes of off, beep and talk. When set to off NVDA will not speak anything. If beeps then NVDA will simply beep each time it its supposed to speak something. If talk then NVDA wil just speak normally.") - script_speechMode.category=SCRCAT_SPEECH + @script( + description=_( + # Translators: Input help mode message for move to next document with focus command, + # mostly used in web browsing to move from embedded object to the webpage document. + "Moves the focus to the next closest document that contains the focus" + ), + category=SCRCAT_FOCUS, + gesture="kb:NVDA+control+space" + ) def script_moveToParentTreeInterceptor(self,gesture): obj=api.getFocusObject() parent=obj.parent @@ -1340,10 +1592,19 @@ def script_moveToParentTreeInterceptor(self,gesture): # We must use core.callLater rather than wx.CallLater to ensure that the callback runs within NVDA's core pump. # If it didn't, and it directly or indirectly called wx.Yield, it could start executing NVDA's core pump from within the yield, causing recursion. core.callLater(50,eventHandler.executeEvent,"gainFocus",parent.treeInterceptor.rootNVDAObject) - # Translators: Input help mode message for move to next document with focus command, mostly used in web browsing to move from embedded object to the webpage document. - script_moveToParentTreeInterceptor.__doc__=_("Moves the focus to the next closest document that contains the focus") - script_moveToParentTreeInterceptor.category=SCRCAT_FOCUS + @script( + description=_( + # Translators: Input help mode message for toggle focus and browse mode command + # in web browsing and other situations. + "Toggles between browse mode and focus mode. " + "When in focus mode, keys will pass straight through to the application, " + "allowing you to interact directly with a control. " + "When in browse mode, you can navigate the document with the cursor, quick navigation keys, etc." + ), + category=inputCore.SCRCAT_BROWSEMODE, + gesture="kb:NVDA+space" + ) def script_toggleVirtualBufferPassThrough(self,gesture): focus = api.getFocusObject() vbuf = focus.treeInterceptor @@ -1383,39 +1644,50 @@ def script_toggleVirtualBufferPassThrough(self,gesture): # If we're disabling pass-through, re-enable auto-pass-through. vbuf.disableAutoPassThrough = vbuf.passThrough browseMode.reportPassThrough(vbuf) - # Translators: Input help mode message for toggle focus and browse mode command in web browsing and other situations. - script_toggleVirtualBufferPassThrough.__doc__=_("Toggles between browse mode and focus mode. When in focus mode, keys will pass straight through to the application, allowing you to interact directly with a control. When in browse mode, you can navigate the document with the cursor, quick navigation keys, etc.") - script_toggleVirtualBufferPassThrough.category=inputCore.SCRCAT_BROWSEMODE + @script( + # Translators: Input help mode message for quit NVDA command. + description=_("Quits NVDA!"), + gesture="kb:NVDA+q" + ) def script_quit(self,gesture): gui.quit() - # Translators: Input help mode message for quit NVDA command. - script_quit.__doc__=_("Quits NVDA!") + @script( + # Translators: Input help mode message for restart NVDA command. + description=_("Restarts NVDA!") + ) def script_restart(self,gesture): core.restart() - # Translators: Input help mode message for restart NVDA command. - script_restart.__doc__=_("Restarts NVDA!") + @script( + # Translators: Input help mode message for show NVDA menu command. + description=_("Shows the NVDA menu"), + gestures=("kb:NVDA+n", "ts:2finger_double_tap") + ) def script_showGui(self,gesture): gui.showGui() - # Translators: Input help mode message for show NVDA menu command. - script_showGui.__doc__=_("Shows the NVDA menu") + @script( + description=_( + # Translators: Input help mode message for say all in review cursor command. + "Reads from the review cursor up to the end of the current text," + " moving the review cursor as it goes" + ), + category=SCRCAT_TEXTREVIEW, + gestures=("kb:numpadPlus", "kb(laptop):NVDA+shift+a", "ts(text):3finger_flickDown") + ) def script_review_sayAll(self,gesture): sayAllHandler.readText(sayAllHandler.CURSOR_REVIEW) - script_review_sayAll.__doc__ = _( - # Translators: Input help mode message for say all in review cursor command. - "Reads from the review cursor up to the end of the current text," - " moving the review cursor as it goes" - ) - script_review_sayAll.category=SCRCAT_TEXTREVIEW + @script( + # Translators: Input help mode message for say all with system caret command. + description=_("Reads from the system caret up to the end of the text, moving the caret as it goes"), + category=SCRCAT_SYSTEMCARET, + gestures=("kb(desktop):NVDA+downArrow", "kb(laptop):NVDA+a") + ) def script_sayAll(self,gesture): sayAllHandler.readText(sayAllHandler.CURSOR_CARET) - # Translators: Input help mode message for say all with system caret command. - script_sayAll.__doc__ = _("Reads from the system caret up to the end of the text, moving the caret as it goes") - script_sayAll.category=SCRCAT_SYSTEMCARET def _reportFormattingHelper(self, info, browseable=False): # Report all formatting-related changes regardless of user settings @@ -1517,7 +1789,7 @@ def _getTIAtCaret(fallbackToPOSITION_FIRST=False): @script( # Translators: Input help mode message for report formatting command. description=_("Reports formatting info for the current review cursor position."), - category=SCRCAT_TEXTREVIEW, + category=SCRCAT_TEXTREVIEW ) def script_reportFormattingAtReview(self, gesture): self._reportFormattingHelper(api.getReviewPosition(), False) @@ -1525,7 +1797,7 @@ def script_reportFormattingAtReview(self, gesture): @script( # Translators: Input help mode message for show formatting at review cursor command. description=_("Presents, in browse mode, formatting info for the current review cursor position."), - category=SCRCAT_TEXTREVIEW, + category=SCRCAT_TEXTREVIEW ) def script_showFormattingAtReview(self, gesture): self._reportFormattingHelper(api.getReviewPosition(), True) @@ -1537,7 +1809,7 @@ def script_showFormattingAtReview(self, gesture): " If pressed twice, presents the information in browse mode" ), category=SCRCAT_TEXTREVIEW, - gesture="kb:NVDA+shift+f", + gesture="kb:NVDA+shift+f" ) def script_reportFormatting(self, gesture): repeats = scriptHandler.getLastScriptRepeatCount() @@ -1549,7 +1821,7 @@ def script_reportFormatting(self, gesture): @script( # Translators: Input help mode message for report formatting at caret command. description=_("Reports formatting info for the text under the caret."), - category=SCRCAT_SYSTEMCARET, + category=SCRCAT_SYSTEMCARET ) def script_reportFormattingAtCaret(self, gesture): self._reportFormattingHelper(self._getTIAtCaret(True), False) @@ -1557,7 +1829,7 @@ def script_reportFormattingAtCaret(self, gesture): @script( # Translators: Input help mode message for show formatting at caret position command. description=_("Presents, in browse mode, formatting info for the text under the caret."), - category=SCRCAT_SYSTEMCARET, + category=SCRCAT_SYSTEMCARET ) def script_showFormattingAtCaret(self, gesture): self._reportFormattingHelper(self._getTIAtCaret(True), True) @@ -1569,7 +1841,7 @@ def script_showFormattingAtCaret(self, gesture): " If pressed twice, presents the information in browse mode" ), category=SCRCAT_SYSTEMCARET, - gesture="kb:NVDA+f", + gesture="kb:NVDA+f" ) def script_reportOrShowFormattingAtCaret(self, gesture): repeats = scriptHandler.getLastScriptRepeatCount() @@ -1578,6 +1850,12 @@ def script_reportOrShowFormattingAtCaret(self, gesture): elif repeats == 1: self.script_showFormattingAtCaret(gesture) + @script( + # Translators: Input help mode message for report current focus command. + description=_("Reports the object with focus. If pressed twice, spells the information"), + category=SCRCAT_FOCUS, + gesture="kb:NVDA+tab" + ) def script_reportCurrentFocus(self,gesture): focusObject=api.getFocusObject() if isinstance(focusObject,NVDAObject): @@ -1587,10 +1865,17 @@ def script_reportCurrentFocus(self,gesture): speech.speakSpelling(focusObject.name) else: ui.message(_("No focus")) - # Translators: Input help mode message for report current focus command. - script_reportCurrentFocus.__doc__ = _("Reports the object with focus. If pressed twice, spells the information") - script_reportCurrentFocus.category=SCRCAT_FOCUS + @script( + description=_( + # Translators: Input help mode message for report status line text command. + "Reads the current application status bar and moves the navigator to it. " + "If pressed twice, spells the information. " + "If pressed three times, copies the status bar to the clipboard" + ), + category=SCRCAT_FOCUS, + gestures=("kb(desktop):NVDA+end", "kb(laptop):NVDA+shift+end") + ) def script_reportStatusLine(self,gesture): obj = api.getStatusBar() found=False @@ -1635,10 +1920,13 @@ def script_reportStatusLine(self,gesture): ui.message(_("unable to copy status bar content to clipboard")) else: api.copyToClip(text, notify=True) - # Translators: Input help mode message for report status line text command. - script_reportStatusLine.__doc__ = _("Reads the current application status bar and moves the navigator to it. If pressed twice, spells the information. If pressed three times, copies the status bar to the clipboard") - script_reportStatusLine.category=SCRCAT_FOCUS + @script( + # Translators: Input help mode message for toggle mouse tracking command. + description=_("Toggles the reporting of information as the mouse moves"), + category=SCRCAT_MOUSE, + gesture="kb:NVDA+m" + ) def script_toggleMouseTracking(self,gesture): if config.conf["mouse"]["enableMouseTracking"]: # Translators: presented when the mouse tracking is toggled. @@ -1649,10 +1937,12 @@ def script_toggleMouseTracking(self,gesture): state = _("Mouse tracking on") config.conf["mouse"]["enableMouseTracking"]=True ui.message(state) - # Translators: Input help mode message for toggle mouse tracking command. - script_toggleMouseTracking.__doc__=_("Toggles the reporting of information as the mouse moves") - script_toggleMouseTracking.category=SCRCAT_MOUSE + @script( + # Translators: Input help mode message for toggle mouse text unit resolution command. + description=_("Toggles how much text will be spoken when the mouse moves"), + category=SCRCAT_MOUSE + ) def script_toggleMouseTextResolution(self,gesture): values = textInfos.MOUSE_TEXT_RESOLUTION_UNITS labels = [textInfos.unitLabels[x] for x in values] @@ -1668,10 +1958,17 @@ def script_toggleMouseTextResolution(self,gesture): # %s will be replaced with the new label. # For example, the full message might be "Mouse text unit resolution character" ui.message(_("Mouse text unit resolution %s")%labels[newIndex]) - # Translators: Input help mode message for toggle mouse text unit resolution command. - script_toggleMouseTextResolution.__doc__=_("Toggles how much text will be spoken when the mouse moves") - script_toggleMouseTextResolution.category=SCRCAT_MOUSE + @script( + description=_( + # Translators: Input help mode message for report title bar command. + "Reports the title of the current application or foreground window. " + "If pressed twice, spells the title. " + "If pressed three times, copies the title to the clipboard" + ), + category=SCRCAT_FOCUS, + gesture="kb:NVDA+t" + ) def script_title(self,gesture): obj=api.getForegroundObject() title=obj.name @@ -1687,42 +1984,55 @@ def script_title(self,gesture): speech.speakSpelling(title) else: api.copyToClip(title, notify=True) - # Translators: Input help mode message for report title bar command. - script_title.__doc__=_("Reports the title of the current application or foreground window. If pressed twice, spells the title. If pressed three times, copies the title to the clipboard") - script_title.category=SCRCAT_FOCUS + @script( + # Translators: Input help mode message for read foreground object command (usually the foreground window). + description=_("Reads all controls in the active window"), + category=SCRCAT_FOCUS, + gesture="kb:NVDA+b" + ) def script_speakForeground(self,gesture): obj=api.getForegroundObject() if obj: sayAllHandler.readObjects(obj) - # Translators: Input help mode message for read foreground object command (usually the foreground window). - script_speakForeground.__doc__ = _("Reads all controls in the active window") - script_speakForeground.category=SCRCAT_FOCUS + @script( + gesture="kb(desktop):NVDA+control+f2" + ) def script_test_navigatorDisplayModelText(self,gesture): obj=api.getNavigatorObject() text=obj.displayText speech.speakMessage(text) log.info(text) + @script( + description=_( + # Translators: GUI development tool, to get information about the components used in the NVDA GUI + "Opens the WX GUI inspection tool. Used to get more information about the state of GUI components." + ), + category=SCRCAT_TOOLS + ) def script_startWxInspectionTool(self, gesture): import wx.lib.inspection wx.lib.inspection.InspectionTool().Show() - script_startWxInspectionTool.__doc__ = _( - # Translators: GUI development tool, to get information about the components used in the NVDA GUI - "Opens the WX GUI inspection tool. Used to get more information about the state of GUI components." - ) - script_startWxInspectionTool.category = SCRCAT_TOOLS + @script( + description=_( + # Translators: Input help mode message for developer info for current navigator object command, + # used by developers to examine technical info on navigator object. + # This command also serves as a shortcut to open NVDA log viewer. + "Logs information about the current navigator object which is useful to developers " + "and activates the log viewer so the information can be examined." + ), + category=SCRCAT_TOOLS, + gesture="kb:NVDA+f1" + ) def script_navigatorObject_devInfo(self,gesture): obj=api.getNavigatorObject() if hasattr(obj, "devInfo"): log.info("Developer info for navigator object:\n%s" % "\n".join(obj.devInfo), activateLogViewer=True) else: log.info("No developer info for navigator object", activateLogViewer=True) - # Translators: Input help mode message for developer info for current navigator object command, used by developers to examine technical info on navigator object. This command also serves as a shortcut to open NVDA log viewer. - script_navigatorObject_devInfo.__doc__ = _("Logs information about the current navigator object which is useful to developers and activates the log viewer so the information can be examined.") - script_navigatorObject_devInfo.category=SCRCAT_TOOLS @script( description=_( @@ -1771,6 +2081,14 @@ def script_openUserConfigurationDirectory(self, gesture): import systemUtils systemUtils.openUserConfigurationDirectory() + @script( + description=_( + # Translators: Input help mode message for toggle progress bar output command. + "Toggles between beeps, speech, beeps and speech, and off, for reporting progress bar updates" + ), + category=SCRCAT_SPEECH, + gesture="kb:NVDA+u" + ) def script_toggleProgressBarOutput(self,gesture): outputMode=config.conf["presentation"]["progressBarUpdates"]["progressBarOutputMode"] if outputMode=="both": @@ -1790,10 +2108,16 @@ def script_toggleProgressBarOutput(self,gesture): # Translators: A mode where both speech and beeps will indicate progress bar updates. ui.message(_("Beep and speak progress bar updates")) config.conf["presentation"]["progressBarUpdates"]["progressBarOutputMode"]=outputMode - # Translators: Input help mode message for toggle progress bar output command. - script_toggleProgressBarOutput.__doc__=_("Toggles between beeps, speech, beeps and speech, and off, for reporting progress bar updates") - script_toggleProgressBarOutput.category=SCRCAT_SPEECH + @script( + description=_( + # Translators: Input help mode message for toggle dynamic content changes command. + "Toggles on and off the reporting of dynamic content changes, " + "such as new text in dos console windows" + ), + category=SCRCAT_SPEECH, + gesture="kb:NVDA+5" + ) def script_toggleReportDynamicContentChanges(self,gesture): if config.conf["presentation"]["reportDynamicContentChanges"]: # Translators: presented when the present dynamic changes is toggled. @@ -1804,10 +2128,13 @@ def script_toggleReportDynamicContentChanges(self,gesture): state = _("report dynamic content changes on") config.conf["presentation"]["reportDynamicContentChanges"]=True ui.message(state) - # Translators: Input help mode message for toggle dynamic content changes command. - script_toggleReportDynamicContentChanges.__doc__=_("Toggles on and off the reporting of dynamic content changes, such as new text in dos console windows") - script_toggleReportDynamicContentChanges.category=SCRCAT_SPEECH + @script( + # Translators: Input help mode message for toggle caret moves review cursor command. + description=_("Toggles on and off the movement of the review cursor due to the caret moving."), + category=SCRCAT_TEXTREVIEW, + gesture="kb:NVDA+6" + ) def script_toggleCaretMovesReviewCursor(self,gesture): if config.conf["reviewCursor"]["followCaret"]: # Translators: presented when toggled. @@ -1818,10 +2145,13 @@ def script_toggleCaretMovesReviewCursor(self,gesture): state = _("caret moves review cursor on") config.conf["reviewCursor"]["followCaret"]=True ui.message(state) - # Translators: Input help mode message for toggle caret moves review cursor command. - script_toggleCaretMovesReviewCursor.__doc__=_("Toggles on and off the movement of the review cursor due to the caret moving.") - script_toggleCaretMovesReviewCursor.category=SCRCAT_TEXTREVIEW + @script( + # Translators: Input help mode message for toggle focus moves navigator object command. + description=_("Toggles on and off the movement of the navigator object due to focus changes"), + category=SCRCAT_OBJECTNAVIGATION, + gesture="kb:NVDA+7" + ) def script_toggleFocusMovesNavigatorObject(self,gesture): if config.conf["reviewCursor"]["followFocus"]: # Translators: presented when toggled. @@ -1832,10 +2162,13 @@ def script_toggleFocusMovesNavigatorObject(self,gesture): state = _("focus moves navigator object on") config.conf["reviewCursor"]["followFocus"]=True ui.message(state) - # Translators: Input help mode message for toggle focus moves navigator object command. - script_toggleFocusMovesNavigatorObject.__doc__=_("Toggles on and off the movement of the navigator object due to focus changes") - script_toggleFocusMovesNavigatorObject.category=SCRCAT_OBJECTNAVIGATION + @script( + # Translators: Input help mode message for toggle auto focus focusable elements command. + description=_("Toggles on and off automatic movement of the system focus due to browse mode commands"), + category=inputCore.SCRCAT_BROWSEMODE, + gesture="kb:NVDA+8" + ) def script_toggleAutoFocusFocusableElements(self,gesture): if config.conf["virtualBuffers"]["autoFocusFocusableElements"]: # Translators: presented when toggled. @@ -1846,11 +2179,14 @@ def script_toggleAutoFocusFocusableElements(self,gesture): state = _("Automatically set system focus to focusable elements on") config.conf["virtualBuffers"]["autoFocusFocusableElements"]=True ui.message(state) - # Translators: Input help mode message for toggle auto focus focusable elements command. - script_toggleAutoFocusFocusableElements.__doc__=_("Toggles on and off automatic movement of the system focus due to browse mode commands") - script_toggleAutoFocusFocusableElements.category=inputCore.SCRCAT_BROWSEMODE - #added by Rui Batista to implement a battery status script + # added by Rui Batista to implement a battery status script + @script( + # Translators: Input help mode message for report battery status command. + description=_("Reports battery status and time remaining if AC is not plugged in"), + category=SCRCAT_SYSTEM, + gesture="kb:NVDA+shift+b" + ) def script_say_battery_status(self,gesture): UNKNOWN_BATTERY_STATUS = 0xFF AC_ONLINE = 0X1 @@ -1871,18 +2207,29 @@ def script_say_battery_status(self,gesture): # Translators: This is the estimated remaining runtime of the laptop battery. text += _("{hours:d} hours and {minutes:d} minutes remaining") .format(hours=sps.BatteryLifeTime // 3600, minutes=(sps.BatteryLifeTime % 3600) // 60) ui.message(text) - # Translators: Input help mode message for report battery status command. - script_say_battery_status.__doc__ = _("Reports battery status and time remaining if AC is not plugged in") - script_say_battery_status.category=SCRCAT_SYSTEM + @script( + description=_( + # Translators: Input help mode message for pass next key through command. + "The next key that is pressed will not be handled at all by NVDA, " + "it will be passed directly through to Windows." + ), + category=SCRCAT_INPUT, + gesture="kb:NVDA+f2" + ) def script_passNextKeyThrough(self,gesture): keyboardHandler.passNextKeyThrough() # Translators: Spoken to indicate that the next key press will be sent straight to the current program as though NVDA is not running. ui.message(_("Pass next key through")) - # Translators: Input help mode message for pass next key through command. - script_passNextKeyThrough.__doc__=_("The next key that is pressed will not be handled at all by NVDA, it will be passed directly through to Windows.") - script_passNextKeyThrough.category=SCRCAT_INPUT + @script( + description=_( + # Translators: Input help mode message for report current program name and app module name command. + "Speaks the filename of the active application along with the name of the currently loaded appModule" + ), + category=SCRCAT_TOOLS, + gesture="kb:NVDA+control+f1" + ) def script_reportAppModuleInfo(self,gesture): focus=api.getFocusObject() message = '' @@ -1897,116 +2244,156 @@ def script_reportAppModuleInfo(self,gesture): # For example, the complete message for Windows explorer is: "explorer module is loaded. Explorer.exe is currenty running." message +=_(" %s is currently running.") % appName ui.message(message) - # Translators: Input help mode message for report current program name and app module name command. - script_reportAppModuleInfo.__doc__ = _("Speaks the filename of the active application along with the name of the currently loaded appModule") - script_reportAppModuleInfo.category=SCRCAT_TOOLS + @script( + # Translators: Input help mode message for go to general settings command. + description=_("Shows NVDA's general settings"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+g" + ) def script_activateGeneralSettingsDialog(self, gesture): wx.CallAfter(gui.mainFrame.onGeneralSettingsCommand, None) - # Translators: Input help mode message for go to general settings command. - script_activateGeneralSettingsDialog.__doc__ = _("Shows NVDA's general settings") - script_activateGeneralSettingsDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to select synthesizer command. + description=_("Shows the NVDA synthesizer selection dialog"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+s" + ) def script_activateSynthesizerDialog(self, gesture): wx.CallAfter(gui.mainFrame.onSelectSynthesizerCommand, None) - # Translators: Input help mode message for go to select synthesizer command. - script_activateSynthesizerDialog.__doc__ = _("Shows the NVDA synthesizer selection dialog") - script_activateSynthesizerDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to speech settings command. + description=_("Shows NVDA's speech settings"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+v" + ) def script_activateVoiceDialog(self, gesture): wx.CallAfter(gui.mainFrame.onSpeechSettingsCommand, None) - # Translators: Input help mode message for go to speech settings command. - script_activateVoiceDialog.__doc__ = _("Shows NVDA's speech settings") - script_activateVoiceDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to select braille display command. + description=_("Shows the NVDA braille display selection dialog"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+a" + ) def script_activateBrailleDisplayDialog(self, gesture): wx.CallAfter(gui.mainFrame.onSelectBrailleDisplayCommand, None) - # Translators: Input help mode message for go to select braille display command. - script_activateBrailleDisplayDialog.__doc__ = _("Shows the NVDA braille display selection dialog") - script_activateBrailleDisplayDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to braille settings command. + description=_("Shows NVDA's braille settings"), + category=SCRCAT_CONFIG + ) def script_activateBrailleSettingsDialog(self, gesture): wx.CallAfter(gui.mainFrame.onBrailleSettingsCommand, None) - # Translators: Input help mode message for go to braille settings command. - script_activateBrailleSettingsDialog.__doc__ = _("Shows NVDA's braille settings") - script_activateBrailleSettingsDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to keyboard settings command. + description=_("Shows NVDA's keyboard settings"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+k" + ) def script_activateKeyboardSettingsDialog(self, gesture): wx.CallAfter(gui.mainFrame.onKeyboardSettingsCommand, None) - # Translators: Input help mode message for go to keyboard settings command. - script_activateKeyboardSettingsDialog.__doc__ = _("Shows NVDA's keyboard settings") - script_activateKeyboardSettingsDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to mouse settings command. + description=_("Shows NVDA's mouse settings"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+m" + ) def script_activateMouseSettingsDialog(self, gesture): wx.CallAfter(gui.mainFrame.onMouseSettingsCommand, None) - # Translators: Input help mode message for go to mouse settings command. - script_activateMouseSettingsDialog.__doc__ = _("Shows NVDA's mouse settings") - script_activateMouseSettingsDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to review cursor settings command. + description=_("Shows NVDA's review cursor settings"), + category=SCRCAT_CONFIG + ) def script_activateReviewCursorDialog(self, gesture): wx.CallAfter(gui.mainFrame.onReviewCursorCommand, None) - # Translators: Input help mode message for go to review cursor settings command. - script_activateReviewCursorDialog.__doc__ = _("Shows NVDA's review cursor settings") - script_activateReviewCursorDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to input composition settings command. + description=_("Shows NVDA's input composition settings"), + category=SCRCAT_CONFIG + ) def script_activateInputCompositionDialog(self, gesture): wx.CallAfter(gui.mainFrame.onInputCompositionCommand, None) - # Translators: Input help mode message for go to input composition settings command. - script_activateInputCompositionDialog.__doc__ = _("Shows NVDA's input composition settings") - script_activateInputCompositionDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to object presentation settings command. + description=_("Shows NVDA's object presentation settings"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+o" + ) def script_activateObjectPresentationDialog(self, gesture): wx.CallAfter(gui.mainFrame. onObjectPresentationCommand, None) - # Translators: Input help mode message for go to object presentation settings command. - script_activateObjectPresentationDialog.__doc__ = _("Shows NVDA's object presentation settings") - script_activateObjectPresentationDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to browse mode settings command. + description=_("Shows NVDA's browse mode settings"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+b" + ) def script_activateBrowseModeDialog(self, gesture): wx.CallAfter(gui.mainFrame.onBrowseModeCommand, None) - # Translators: Input help mode message for go to browse mode settings command. - script_activateBrowseModeDialog.__doc__ = _("Shows NVDA's browse mode settings") - script_activateBrowseModeDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to document formatting settings command. + description=_("Shows NVDA's document formatting settings"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+d" + ) def script_activateDocumentFormattingDialog(self, gesture): wx.CallAfter(gui.mainFrame.onDocumentFormattingCommand, None) - # Translators: Input help mode message for go to document formatting settings command. - script_activateDocumentFormattingDialog.__doc__ = _("Shows NVDA's document formatting settings") - script_activateDocumentFormattingDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for opening default dictionary dialog. + description=_("Shows the NVDA default dictionary dialog"), + category=SCRCAT_CONFIG + ) def script_activateDefaultDictionaryDialog(self, gesture): wx.CallAfter(gui.mainFrame.onDefaultDictionaryCommand, None) - # Translators: Input help mode message for opening default dictionary dialog. - script_activateDefaultDictionaryDialog.__doc__ = _("Shows the NVDA default dictionary dialog") - script_activateDefaultDictionaryDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for opening voice-specific dictionary dialog. + description=_("Shows the NVDA voice-specific dictionary dialog"), + category=SCRCAT_CONFIG + ) def script_activateVoiceDictionaryDialog(self, gesture): wx.CallAfter(gui.mainFrame.onVoiceDictionaryCommand, None) - # Translators: Input help mode message for opening voice-specific dictionary dialog. - script_activateVoiceDictionaryDialog.__doc__ = _("Shows the NVDA voice-specific dictionary dialog") - script_activateVoiceDictionaryDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for opening temporary dictionary. + description=_("Shows the NVDA temporary dictionary dialog"), + category=SCRCAT_CONFIG + ) def script_activateTemporaryDictionaryDialog(self, gesture): wx.CallAfter(gui.mainFrame.onTemporaryDictionaryCommand, None) - # Translators: Input help mode message for opening temporary dictionary. - script_activateTemporaryDictionaryDialog.__doc__ = _("Shows the NVDA temporary dictionary dialog") - script_activateTemporaryDictionaryDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to punctuation/symbol pronunciation dialog. + description=_("Shows the NVDA symbol pronunciation dialog"), + category=SCRCAT_CONFIG + ) def script_activateSpeechSymbolsDialog(self, gesture): wx.CallAfter(gui.mainFrame.onSpeechSymbolsCommand, None) - # Translators: Input help mode message for go to punctuation/symbol pronunciation dialog. - script_activateSpeechSymbolsDialog.__doc__ = _("Shows the NVDA symbol pronunciation dialog") - script_activateSpeechSymbolsDialog.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for go to input gestures dialog command. + description=_("Shows the NVDA input gestures dialog"), + category=SCRCAT_CONFIG + ) def script_activateInputGesturesDialog(self, gesture): wx.CallAfter(gui.mainFrame.onInputGesturesCommand, None) - # Translators: Input help mode message for go to input gestures dialog command. - script_activateInputGesturesDialog.__doc__ = _("Shows the NVDA input gestures dialog") - script_activateInputGesturesDialog.category=SCRCAT_CONFIG @script( # Translators: Input help mode message for the report current configuration profile command. description=_("Reports the name of the current NVDA configuration profile"), - category=SCRCAT_CONFIG, + category=SCRCAT_CONFIG ) def script_reportActiveConfigurationProfile(self, gesture): activeProfileName = config.conf.profiles[-1].name @@ -2023,25 +2410,37 @@ def script_reportActiveConfigurationProfile(self, gesture): ) ui.message(activeProfileMessage) + @script( + # Translators: Input help mode message for save current configuration command. + description=_("Saves the current NVDA configuration"), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+c" + ) def script_saveConfiguration(self,gesture): wx.CallAfter(gui.mainFrame.onSaveConfigurationCommand, None) - # Translators: Input help mode message for save current configuration command. - script_saveConfiguration.__doc__ = _("Saves the current NVDA configuration") - script_saveConfiguration.category=SCRCAT_CONFIG + @script( + description=_( + # Translators: Input help mode message for apply last saved or default settings command. + "Pressing once reverts the current configuration to the most recently saved state." + " Pressing three times resets to factory defaults." + ), + category=SCRCAT_CONFIG, + gesture="kb:NVDA+control+r" + ) def script_revertConfiguration(self,gesture): scriptCount=scriptHandler.getLastScriptRepeatCount() if scriptCount==0: gui.mainFrame.onRevertToSavedConfigurationCommand(None) elif scriptCount==2: gui.mainFrame.onRevertToDefaultConfigurationCommand(None) - script_revertConfiguration.__doc__ = _( - # Translators: Input help mode message for apply last saved or default settings command. - "Pressing once reverts the current configuration to the most recently saved state." - " Pressing three times resets to factory defaults." - ) - script_revertConfiguration.category=SCRCAT_CONFIG + @script( + # Translators: Input help mode message for activate python console command. + description=_("Activates the NVDA Python Console, primarily useful for development"), + category=SCRCAT_TOOLS, + gesture="kb:NVDA+control+z" + ) def script_activatePythonConsole(self,gesture): if globalVars.appArgs.secure or config.isAppX: return @@ -2050,16 +2449,23 @@ def script_activatePythonConsole(self,gesture): pythonConsole.initialize() pythonConsole.consoleUI.console.updateNamespaceSnapshotVars() pythonConsole.activate() - # Translators: Input help mode message for activate python console command. - script_activatePythonConsole.__doc__ = _("Activates the NVDA Python Console, primarily useful for development") - script_activatePythonConsole.category=SCRCAT_TOOLS + @script( + # Translators: Input help mode message for activate manage add-ons command. + description=_("Activates the NVDA Add-ons Manager to install and uninstall add-on packages for NVDA"), + category=SCRCAT_TOOLS + ) def script_activateAddonsManager(self,gesture): wx.CallAfter(gui.mainFrame.onAddonsManagerCommand, None) - # Translators: Input help mode message for activate manage add-ons command. - script_activateAddonsManager.__doc__ = _("Activates the NVDA Add-ons Manager to install and uninstall add-on packages for NVDA") - script_activateAddonsManager.category=SCRCAT_TOOLS + @script( + description=_( + # Translators: Input help mode message for toggle speech viewer command. + "Toggles the NVDA Speech viewer, " + "a floating window that allows you to view all the text that NVDA is currently speaking" + ), + category=SCRCAT_TOOLS + ) def script_toggleSpeechViewer(self,gesture): if gui.speechViewer.isActive: # Translators: The message announced when disabling speech viewer. @@ -2072,10 +2478,14 @@ def script_toggleSpeechViewer(self,gesture): gui.speechViewer.activate() gui.mainFrame.sysTrayIcon.menu_tools_toggleSpeechViewer.Check(True) ui.message(state) - # Translators: Input help mode message for toggle speech viewer command. - script_toggleSpeechViewer.__doc__ = _("Toggles the NVDA Speech viewer, a floating window that allows you to view all the text that NVDA is currently speaking") - script_toggleSpeechViewer.category=SCRCAT_TOOLS + @script( + # Translators: Input help mode message for toggle braille tether to command + # (tethered means connected to or follows). + description=_("Toggle tethering of braille between the focus and the review position"), + category=SCRCAT_BRAILLE, + gesture="kb:NVDA+control+t" + ) def script_braille_toggleTether(self, gesture): values = [x[0] for x in braille.handler.tetherValues] labels = [x[1] for x in braille.handler.tetherValues] @@ -2100,10 +2510,12 @@ def script_braille_toggleTether(self, gesture): # Translators: Reports which position braille is tethered to # (braille can be tethered automatically or to either focus or review position). ui.message(_("Braille tethered %s") % labels[newIndex]) - # Translators: Input help mode message for toggle braille tether to command (tethered means connected to or follows). - script_braille_toggleTether.__doc__ = _("Toggle tethering of braille between the focus and the review position") - script_braille_toggleTether.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for toggle braille focus context presentation command. + description=_("Toggle the way context information is presented in braille"), + category=SCRCAT_BRAILLE + ) def script_braille_toggleFocusContextPresentation(self, gesture): values = [x[0] for x in braille.focusContextPresentations] labels = [x[1] for x in braille.focusContextPresentations] @@ -2119,10 +2531,12 @@ def script_braille_toggleFocusContextPresentation(self, gesture): # %s will be replaced with the context presentation setting. # For example, the full message might be "Braille focus context presentation: fill display for context changes" ui.message(_("Braille focus context presentation: %s")%labels[newIndex].lower()) - # Translators: Input help mode message for toggle braille focus context presentation command. - script_braille_toggleFocusContextPresentation.__doc__ = _("Toggle the way context information is presented in braille") - script_braille_toggleFocusContextPresentation.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for toggle braille cursor command. + description=_("Toggle the braille cursor on and off"), + category=SCRCAT_BRAILLE + ) def script_braille_toggleShowCursor(self, gesture): if config.conf["braille"]["showCursor"]: # Translators: The message announced when toggling the braille cursor. @@ -2133,10 +2547,12 @@ def script_braille_toggleShowCursor(self, gesture): state = _("Braille cursor on") config.conf["braille"]["showCursor"]=True ui.message(state) - # Translators: Input help mode message for toggle braille cursor command. - script_braille_toggleShowCursor.__doc__ = _("Toggle the braille cursor on and off") - script_braille_toggleShowCursor.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for cycle braille cursor shape command. + description=_("Cycle through the braille cursor shapes"), + category=SCRCAT_BRAILLE + ) def script_braille_cycleCursorShape(self, gesture): if not config.conf["braille"]["showCursor"]: # Translators: A message reported when changing the braille cursor shape when the braille cursor is turned off. @@ -2157,10 +2573,13 @@ def script_braille_cycleCursorShape(self, gesture): shapeMsg = braille.CURSOR_SHAPES[index][1] # Translators: Reports which braille cursor shape is activated. ui.message(_("Braille cursor %s") % shapeMsg) - # Translators: Input help mode message for cycle braille cursor shape command. - script_braille_cycleCursorShape.__doc__ = _("Cycle through the braille cursor shapes") - script_braille_cycleCursorShape.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for report clipboard text command. + description=_("Reports the text on the Windows clipboard"), + category=SCRCAT_SYSTEM, + gesture="kb:NVDA+c" + ) def script_reportClipboardText(self,gesture): try: text = api.getClipData() @@ -2176,10 +2595,16 @@ def script_reportClipboardText(self,gesture): # Translators: If the number of characters on the clipboard is greater than about 1000, it reports this message and gives number of characters on the clipboard. # Example output: The clipboard contains a large portion of text. It is 2300 characters long. ui.message(_("The clipboard contains a large portion of text. It is %s characters long") % len(text)) - # Translators: Input help mode message for report clipboard text command. - script_reportClipboardText.__doc__ = _("Reports the text on the Windows clipboard") - script_reportClipboardText.category=SCRCAT_SYSTEM + @script( + description=_( + # Translators: Input help mode message for mark review cursor position for a select or copy command + # (that is, marks the current review cursor position as the starting point for text to be selected). + "Marks the current position of the review cursor as the start of text to be selected or copied" + ), + category=SCRCAT_TEXTREVIEW, + gesture="kb:NVDA+f9" + ) def script_review_markStartForCopy(self, gesture): reviewPos = api.getReviewPosition() # attach the marker to obj so that the marker is cleaned up when obj is cleaned up. @@ -2187,9 +2612,6 @@ def script_review_markStartForCopy(self, gesture): reviewPos.obj._selectThenCopyRange = None # we may be part way through a select, reset the copy range. # Translators: Indicates start of review cursor text to be copied to clipboard. ui.message(_("Start marked")) - # Translators: Input help mode message for mark review cursor position for a select or copy command (that is, marks the current review cursor position as the starting point for text to be selected). - script_review_markStartForCopy.__doc__ = _("Marks the current position of the review cursor as the start of text to be selected or copied") - script_review_markStartForCopy.category=SCRCAT_TEXTREVIEW @script( description=_( @@ -2198,7 +2620,7 @@ def script_review_markStartForCopy(self, gesture): "Move the review cursor to the position marked as the start of text to be selected or copied" ), category=SCRCAT_TEXTREVIEW, - gesture="kb:NVDA+shift+F9", + gesture="kb:NVDA+shift+F9" ) def script_review_moveToStartMarkedForCopy(self, gesture): pos = api.getReviewPosition() @@ -2212,6 +2634,16 @@ def script_review_moveToStartMarkedForCopy(self, gesture): startMarker.expand(textInfos.UNIT_CHARACTER) speech.speakTextInfo(startMarker, unit=textInfos.UNIT_CHARACTER, reason=controlTypes.REASON_CARET) + @script( + description=_( + # Translators: Input help mode message for the select then copy command. + # The select then copy command first selects the review cursor text, then copies it to the clipboard. + "If pressed once, the text from the previously set start marker up to and including the current " + "position of the review cursor is selected. If pressed twice, the text is copied to the clipboard" + ), + category=SCRCAT_TEXTREVIEW, + gesture="kb:NVDA+f10" + ) def script_review_copy(self, gesture): pos = api.getReviewPosition().copy() if not getattr(pos.obj, "_copyStartMarker", None): @@ -2275,30 +2707,38 @@ def script_review_copy(self, gesture): api.getReviewPosition().obj._selectThenCopyRange = None api.getReviewPosition().obj._copyStartMarker = None return - # Translators: Input help mode message for the select then copy command. The select then copy command first selects the review cursor text, then copies it to the clipboard. - script_review_copy.__doc__ = _("If pressed once, the text from the previously set start marker up to and including the current position of the review cursor is selected. If pressed twice, the text is copied to the clipboard") - script_review_copy.category=SCRCAT_TEXTREVIEW + @script( + # Translators: Input help mode message for a braille command. + description=_("Scrolls the braille display back"), + category=SCRCAT_BRAILLE, + bypassInputHelp=True + ) def script_braille_scrollBack(self, gesture): braille.handler.scrollBack() - # Translators: Input help mode message for a braille command. - script_braille_scrollBack.__doc__ = _("Scrolls the braille display back") - script_braille_scrollBack.bypassInputHelp = True - script_braille_scrollBack.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Scrolls the braille display forward"), + category=SCRCAT_BRAILLE, + bypassInputHelp=True + ) def script_braille_scrollForward(self, gesture): braille.handler.scrollForward() - # Translators: Input help mode message for a braille command. - script_braille_scrollForward.__doc__ = _("Scrolls the braille display forward") - script_braille_scrollForward.bypassInputHelp = True - script_braille_scrollForward.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Routes the cursor to or activates the object under this braille cell"), + category=SCRCAT_BRAILLE + ) def script_braille_routeTo(self, gesture): braille.handler.routeTo(gesture.routingIndex) - # Translators: Input help mode message for a braille command. - script_braille_routeTo.__doc__ = _("Routes the cursor to or activates the object under this braille cell") - script_braille_routeTo.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for Braille report formatting command. + description=_("Reports formatting info for the text under this braille cell"), + category=SCRCAT_BRAILLE + ) def script_braille_reportFormatting(self, gesture): info = braille.handler.getTextInfoForWindowPos(gesture.routingIndex) if info is None: @@ -2306,30 +2746,39 @@ def script_braille_reportFormatting(self, gesture): ui.message(_("No formatting information")) return self._reportFormattingHelper(info, False) - # Translators: Input help mode message for Braille report formatting command. - script_braille_reportFormatting.__doc__ = _("Reports formatting info for the text under this braille cell") - script_braille_reportFormatting.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Moves the braille display to the previous line"), + category=SCRCAT_BRAILLE + ) def script_braille_previousLine(self, gesture): if braille.handler.buffer.regions: braille.handler.buffer.regions[-1].previousLine(start=True) - # Translators: Input help mode message for a braille command. - script_braille_previousLine.__doc__ = _("Moves the braille display to the previous line") - script_braille_previousLine.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Moves the braille display to the next line"), + category=SCRCAT_BRAILLE + ) def script_braille_nextLine(self, gesture): if braille.handler.buffer.regions: braille.handler.buffer.regions[-1].nextLine() - # Translators: Input help mode message for a braille command. - script_braille_nextLine.__doc__ = _("Moves the braille display to the next line") - script_braille_nextLine.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Inputs braille dots via the braille keyboard"), + category=SCRCAT_BRAILLE, + gesture="bk:dots" + ) def script_braille_dots(self, gesture): brailleInput.handler.input(gesture.dots) - # Translators: Input help mode message for a braille command. - script_braille_dots.__doc__= _("Inputs braille dots via the braille keyboard") - script_braille_dots.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Moves the braille display to the current focus"), + category=SCRCAT_BRAILLE + ) def script_braille_toFocus(self, gesture): braille.handler.setTether(braille.handler.TETHER_FOCUS, auto=True) if braille.handler.getTether() == braille.handler.TETHER_REVIEW: @@ -2346,63 +2795,87 @@ def script_braille_toFocus(self, gesture): braille.handler.mainBuffer.updateDisplay() else: braille.handler.handleGainFocus(obj,shouldAutoTether=False) - # Translators: Input help mode message for a braille command. - script_braille_toFocus.__doc__= _("Moves the braille display to the current focus") - script_braille_toFocus.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Erases the last entered braille cell or character"), + category=SCRCAT_BRAILLE, + gesture="bk:dot7" + ) def script_braille_eraseLastCell(self, gesture): brailleInput.handler.eraseLastCell() - # Translators: Input help mode message for a braille command. - script_braille_eraseLastCell.__doc__= _("Erases the last entered braille cell or character") - script_braille_eraseLastCell.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Translates any braille input and presses the enter key"), + category=SCRCAT_BRAILLE, + gesture="bk:dot8" + ) def script_braille_enter(self, gesture): brailleInput.handler.enter() - # Translators: Input help mode message for a braille command. - script_braille_enter.__doc__= _("Translates any braille input and presses the enter key") - script_braille_enter.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Translates any braille input"), + category=SCRCAT_BRAILLE, + gesture="bk:dot7+dot8" + ) def script_braille_translate(self, gesture): brailleInput.handler.translate() - # Translators: Input help mode message for a braille command. - script_braille_translate.__doc__= _("Translates any braille input") - script_braille_translate.category=SCRCAT_BRAILLE + @script( + # Translators: Input help mode message for a braille command. + description=_("Virtually toggles the shift key to emulate a keyboard shortcut with braille input"), + category=inputCore.SCRCAT_KBEMU, + bypassInputHelp=True + ) def script_braille_toggleShift(self, gesture): brailleInput.handler.toggleModifier("shift") - # Translators: Input help mode message for a braille command. - script_braille_toggleShift.__doc__= _("Virtually toggles the shift key to emulate a keyboard shortcut with braille input") - script_braille_toggleShift.category=inputCore.SCRCAT_KBEMU - script_braille_toggleShift.bypassInputHelp = True + @script( + # Translators: Input help mode message for a braille command. + description=_("Virtually toggles the control key to emulate a keyboard shortcut with braille input"), + category=inputCore.SCRCAT_KBEMU, + bypassInputHelp=True + ) def script_braille_toggleControl(self, gesture): brailleInput.handler.toggleModifier("control") - # Translators: Input help mode message for a braille command. - script_braille_toggleControl.__doc__= _("Virtually toggles the control key to emulate a keyboard shortcut with braille input") - script_braille_toggleControl.category=inputCore.SCRCAT_KBEMU - script_braille_toggleControl.bypassInputHelp = True + @script( + # Translators: Input help mode message for a braille command. + description=_("Virtually toggles the alt key to emulate a keyboard shortcut with braille input"), + category=inputCore.SCRCAT_KBEMU, + bypassInputHelp=True + ) def script_braille_toggleAlt(self, gesture): brailleInput.handler.toggleModifier("alt") - # Translators: Input help mode message for a braille command. - script_braille_toggleAlt.__doc__= _("Virtually toggles the alt key to emulate a keyboard shortcut with braille input") - script_braille_toggleAlt.category=inputCore.SCRCAT_KBEMU - script_braille_toggleAlt.bypassInputHelp = True + @script( + # Translators: Input help mode message for a braille command. + description=_("Virtually toggles the left windows key to emulate a keyboard shortcut with braille input"), + category=inputCore.SCRCAT_KBEMU, + bypassInputHelp=True + ) def script_braille_toggleWindows(self, gesture): brailleInput.handler.toggleModifier("leftWindows") - # Translators: Input help mode message for a braille command. - script_braille_toggleWindows.__doc__= _("Virtually toggles the left windows key to emulate a keyboard shortcut with braille input") - script_braille_toggleWindows.category=inputCore.SCRCAT_KBEMU - script_braille_toggleAlt.bypassInputHelp = True + @script( + # Translators: Input help mode message for a braille command. + description=_("Virtually toggles the NVDA key to emulate a keyboard shortcut with braille input"), + category=inputCore.SCRCAT_KBEMU, + bypassInputHelp=True + ) def script_braille_toggleNVDAKey(self, gesture): brailleInput.handler.toggleModifier("NVDA") - # Translators: Input help mode message for a braille command. - script_braille_toggleNVDAKey.__doc__= _("Virtually toggles the NVDA key to emulate a keyboard shortcut with braille input") - script_braille_toggleNVDAKey.category=inputCore.SCRCAT_KBEMU - script_braille_toggleNVDAKey.bypassInputHelp = True + @script( + description=_( + # Translators: Input help mode message for reload plugins command. + "Reloads app modules and global plugins without restarting NVDA, which can be Useful for developers" + ), + category=SCRCAT_TOOLS, + gesture="kb:NVDA+control+f3" + ) def script_reloadPlugins(self, gesture): import globalPluginHandler appModuleHandler.reloadAppModules() @@ -2410,10 +2883,13 @@ def script_reloadPlugins(self, gesture): NVDAObject.clearDynamicClassCache() # Translators: Presented when plugins (app modules and global plugins) are reloaded. ui.message(_("Plugins reloaded")) - # Translators: Input help mode message for reload plugins command. - script_reloadPlugins.__doc__=_("Reloads app modules and global plugins without restarting NVDA, which can be Useful for developers") - script_reloadPlugins.category=SCRCAT_TOOLS + @script( + # Translators: Input help mode message for a touchscreen gesture. + description=_("Moves to the next object in a flattened view of the object navigation hierarchy"), + category=SCRCAT_OBJECTNAVIGATION, + gesture="ts(object):flickright" + ) def script_navigatorObject_nextInFlow(self,gesture): curObject=api.getNavigatorObject() newObject=None @@ -2433,10 +2909,13 @@ def script_navigatorObject_nextInFlow(self,gesture): else: # Translators: a message when there is no next object when navigating ui.reviewMessage(_("No next")) - # Translators: Input help mode message for a touchscreen gesture. - script_navigatorObject_nextInFlow.__doc__=_("Moves to the next object in a flattened view of the object navigation hierarchy") - script_navigatorObject_nextInFlow.category=SCRCAT_OBJECTNAVIGATION + @script( + # Translators: Input help mode message for a touchscreen gesture. + description=_("Moves to the previous object in a flattened view of the object navigation hierarchy"), + category=SCRCAT_OBJECTNAVIGATION, + gesture="ts(object):flickleft" + ) def script_navigatorObject_previousInFlow(self,gesture): curObject=api.getNavigatorObject() newObject=curObject.simplePrevious @@ -2451,15 +2930,12 @@ def script_navigatorObject_previousInFlow(self,gesture): else: # Translators: a message when there is no previous object when navigating ui.reviewMessage(_("No previous")) - # Translators: Input help mode message for a touchscreen gesture. - script_navigatorObject_previousInFlow.__doc__=_("Moves to the previous object in a flattened view of the object navigation hierarchy") - script_navigatorObject_previousInFlow.category=SCRCAT_OBJECTNAVIGATION @script( # Translators: Describes a command. description=_("Toggles the support of touch interaction"), category=SCRCAT_TOUCH, - gesture="kb:NVDA+control+alt+t", + gesture="kb:NVDA+control+alt+t" ) def script_toggleTouchSupport(self, gesture): enabled = not bool(config.conf["touch"]["enabled"]) @@ -2478,6 +2954,12 @@ def script_toggleTouchSupport(self, gesture): # Translators: Presented when support of touch interaction has been disabled ui.message(_("Touch interaction disabled")) + @script( + # Translators: Input help mode message for a touchscreen gesture. + description=_("Cycles between available touch modes"), + category=SCRCAT_TOUCH, + gesture="ts:3finger_tap" + ) def script_touch_changeMode(self,gesture): mode=touchHandler.handler._curTouchMode index=touchHandler.availableTouchModes.index(mode) @@ -2490,33 +2972,51 @@ def script_touch_changeMode(self,gesture): # Translators: Cycles through available touch modes (a group of related touch gestures; example output: "object mode"; see the user guide for more information on touch modes). newModeLabel=_("%s mode")%newMode ui.message(newModeLabel) - # Translators: Input help mode message for a touchscreen gesture. - script_touch_changeMode.__doc__=_("Cycles between available touch modes") - script_touch_changeMode.category=SCRCAT_TOUCH - + @script( + # Translators: Input help mode message for a touchscreen gesture. + description=_("Reports the object and content directly under your finger"), + category=SCRCAT_TOUCH, + gestures=("ts:tap", "ts:hoverDown") + ) def script_touch_newExplore(self,gesture): touchHandler.handler.screenExplorer.moveTo(gesture.x,gesture.y,new=True) - # Translators: Input help mode message for a touchscreen gesture. - script_touch_newExplore.__doc__=_("Reports the object and content directly under your finger") - script_touch_newExplore.category=SCRCAT_TOUCH + @script( + description=_( + # Translators: Input help mode message for a touchscreen gesture. + "Reports the new object or content under your finger " + "if different to where your finger was last" + ), + category=SCRCAT_TOUCH, + gesture="ts:hover" + ) def script_touch_explore(self,gesture): touchHandler.handler.screenExplorer.moveTo(gesture.x,gesture.y) - # Translators: Input help mode message for a touchscreen gesture. - script_touch_explore.__doc__=_("Reports the new object or content under your finger if different to where your finger was last") - script_touch_explore.category=SCRCAT_TOUCH + @script( + category=SCRCAT_TOUCH, + gesture="ts:hoverUp" + ) def script_touch_hoverUp(self,gesture): #Specifically for touch typing with onscreen keyboard keys - # #7309: by default, one mustdouble tap the touch key. To restore old behavior, go to Touch Interaction dialog and change touch typing option. + # #7309: by default, one must double-tap the touch key. + # To restore old behavior, go to Touch Interaction dialog and change touch typing option. if config.conf["touch"]["touchTyping"]: obj=api.getNavigatorObject() import NVDAObjects.UIA if isinstance(obj,NVDAObjects.UIA.UIA) and obj.UIAElement.cachedClassName=="CRootKey": obj.doAction() - script_touch_hoverUp.category=SCRCAT_TOUCH + @script( + description=_( + # Translators: Input help mode message for touch right click command. + "Clicks the right mouse button at the current touch position. " + "This is generally used to activate a context menu." + ), + category=SCRCAT_TOUCH, + gesture="ts:tapAndHold" + ) def script_touch_rightClick(self, gesture): obj = api.getNavigatorObject() # Ignore invisible or offscreen objects as they cannot even be navigated with touch gestures. @@ -2544,16 +3044,24 @@ def script_touch_rightClick(self, gesture): y = top + (height // 2) winUser.setCursorPos(x, y) self.script_rightMouseClick(gesture) - # Translators: Input help mode message for touch right click command. - script_touch_rightClick.__doc__ = _("Clicks the right mouse button at the current touch position. This is generally used to activate a context menu.") # noqa Flake8/E501 - script_touch_rightClick.category = SCRCAT_TOUCH + @script( + # Translators: Describes the command to open the Configuration Profiles dialog. + description=_("Shows the NVDA Configuration Profiles dialog"), + category=SCRCAT_CONFIG_PROFILES, + gesture="kb:NVDA+control+p" + ) def script_activateConfigProfilesDialog(self, gesture): wx.CallAfter(gui.mainFrame.onConfigProfilesCommand, None) - # Translators: Describes the command to open the Configuration Profiles dialog. - script_activateConfigProfilesDialog.__doc__ = _("Shows the NVDA Configuration Profiles dialog") - script_activateConfigProfilesDialog.category=SCRCAT_CONFIG_PROFILES + @script( + description=_( + # Translators: Input help mode message for toggle configuration profile triggers command. + "Toggles disabling of all configuration profile triggers. " + "Disabling remains in effect until NVDA is restarted" + ), + category=SCRCAT_CONFIG + ) def script_toggleConfigProfileTriggers(self,gesture): if config.conf.profileTriggersEnabled: config.conf.disableProfileTriggers() @@ -2568,10 +3076,12 @@ def script_toggleConfigProfileTriggers(self,gesture): # Translators: The message announced when re-enabling all configuration profile triggers. state = _("Configuration profile triggers enabled") ui.message(state) - # Translators: Input help mode message for toggle configuration profile triggers command. - script_toggleConfigProfileTriggers.__doc__=_("Toggles disabling of all configuration profile triggers. Disabling remains in effect until NVDA is restarted") - script_toggleConfigProfileTriggers.category=SCRCAT_CONFIG + @script( + # Translators: Describes a command. + description=_("Begins interaction with math content"), + gesture="kb:NVDA+alt+m" + ) def script_interactWithMath(self, gesture): import mathPres mathMl = mathPres.getMathMlFromTextInfo(api.getReviewPosition()) @@ -2588,9 +3098,12 @@ def script_interactWithMath(self, gesture): ui.message(_("Not math")) return mathPres.interactWithMathMl(mathMl) - # Translators: Describes a command. - script_interactWithMath.__doc__ = _("Begins interaction with math content") + @script( + # Translators: Describes a command. + description=_("Recognizes the content of the current navigator object with Windows 10 OCR"), + gesture="kb:NVDA+r" + ) def script_recognizeWithUwpOcr(self, gesture): if not winVersion.isUwpOcrAvailable(): # Translators: Reported when Windows 10 OCR is not available. @@ -2599,8 +3112,6 @@ def script_recognizeWithUwpOcr(self, gesture): from contentRecog import uwpOcr, recogUi recog = uwpOcr.UwpOcr() recogUi.recognizeNavigatorObject(recog) - # Translators: Describes a command. - script_recognizeWithUwpOcr.__doc__ = _("Recognizes the content of the current navigator object with Windows 10 OCR") _tempEnableScreenCurtain = True _waitingOnScreenCurtainWarningDialog: Optional[wx.Dialog] = None @@ -2608,7 +3119,7 @@ def script_recognizeWithUwpOcr(self, gesture): @script( # Translators: Input help mode message for toggle report CLDR command. description=_("Toggles on and off the reporting of CLDR characters, such as emojis"), - category=SCRCAT_SPEECH, + category=SCRCAT_SPEECH ) def script_toggleReportCLDR(self, gesture): if config.conf["speech"]["includeCLDR"]: @@ -2755,190 +3266,6 @@ def _enableScreenCurtain(doEnable: bool = True): else: _enableScreenCurtain() - __gestures = { - # Basic - "kb:NVDA+n": "showGui", - "kb:NVDA+1": "toggleInputHelp", - "kb:NVDA+q": "quit", - "kb:NVDA+f2": "passNextKeyThrough", - "kb(desktop):NVDA+shift+s":"toggleCurrentAppSleepMode", - "kb(laptop):NVDA+shift+z":"toggleCurrentAppSleepMode", - - # System status - "kb:NVDA+f12": "dateTime", - "kb:NVDA+shift+b": "say_battery_status", - "kb:NVDA+c": "reportClipboardText", - - # System focus - "kb:NVDA+tab": "reportCurrentFocus", - "kb:NVDA+t": "title", - "kb:NVDA+b": "speakForeground", - "kb(desktop):NVDA+end": "reportStatusLine", - "kb(laptop):NVDA+shift+end": "reportStatusLine", - - # System caret - "kb(desktop):NVDA+downArrow": "sayAll", - "kb(laptop):NVDA+a": "sayAll", - "kb(desktop):NVDA+upArrow": "reportCurrentLine", - "kb(laptop):NVDA+l": "reportCurrentLine", - "kb(desktop):NVDA+shift+upArrow": "reportCurrentSelection", - "kb(laptop):NVDA+shift+s": "reportCurrentSelection", - # Object navigation - "kb:NVDA+numpad5": "navigatorObject_current", - "kb(laptop):NVDA+shift+o": "navigatorObject_current", - "kb:NVDA+numpad8": "navigatorObject_parent", - "kb(laptop):NVDA+shift+upArrow": "navigatorObject_parent", - "ts(object):flickup":"navigatorObject_parent", - "kb:NVDA+numpad4": "navigatorObject_previous", - "kb(laptop):NVDA+shift+leftArrow": "navigatorObject_previous", - "ts(object):flickleft":"navigatorObject_previousInFlow", - "ts(object):2finger_flickleft":"navigatorObject_previous", - "kb:NVDA+numpad6": "navigatorObject_next", - "kb(laptop):NVDA+shift+rightArrow": "navigatorObject_next", - "ts(object):flickright":"navigatorObject_nextInFlow", - "ts(object):2finger_flickright":"navigatorObject_next", - "kb:NVDA+numpad2": "navigatorObject_firstChild", - "kb(laptop):NVDA+shift+downArrow": "navigatorObject_firstChild", - "ts(object):flickdown":"navigatorObject_firstChild", - "kb:NVDA+numpadEnter": "review_activate", - "kb(laptop):NVDA+enter": "review_activate", - "ts:double_tap": "review_activate", - "kb:NVDA+shift+numpadMinus": "navigatorObject_moveFocus", - "kb(laptop):NVDA+shift+backspace": "navigatorObject_moveFocus", - "kb:NVDA+numpadDelete": "navigatorObject_currentDimensions", - "kb(laptop):NVDA+delete": "navigatorObject_currentDimensions", - - #Touch-specific commands - "ts:tap":"touch_newExplore", - "ts:hoverDown":"touch_newExplore", - "ts:hover":"touch_explore", - "ts:3finger_tap":"touch_changeMode", - "ts:2finger_double_tap":"showGui", - "ts:hoverUp":"touch_hoverUp", - "ts:tapAndHold": "touch_rightClick", # noqa (Flake8/ET121) - - # Review cursor - "kb:shift+numpad7": "review_top", - "kb(laptop):NVDA+control+home": "review_top", - "kb:numpad7": "review_previousLine", - "ts(text):flickUp":"review_previousLine", - "kb(laptop):NVDA+upArrow": "review_previousLine", - "kb:numpad8": "review_currentLine", - "kb(laptop):NVDA+shift+.": "review_currentLine", - "kb:numpad9": "review_nextLine", - "kb(laptop):NVDA+downArrow": "review_nextLine", - "ts(text):flickDown":"review_nextLine", - "kb:shift+numpad9": "review_bottom", - "kb(laptop):NVDA+control+end": "review_bottom", - "kb:numpad4": "review_previousWord", - "kb(laptop):NVDA+control+leftArrow": "review_previousWord", - "ts(text):2finger_flickLeft":"review_previousWord", - "kb:numpad5": "review_currentWord", - "kb(laptop):NVDA+control+.": "review_currentWord", - "ts(text):hoverUp":"review_currentWord", - "kb:numpad6": "review_nextWord", - "kb(laptop):NVDA+control+rightArrow": "review_nextWord", - "ts(text):2finger_flickRight":"review_nextWord", - "kb:shift+numpad1": "review_startOfLine", - "kb(laptop):NVDA+home": "review_startOfLine", - "kb:numpad1": "review_previousCharacter", - "kb(laptop):NVDA+leftArrow": "review_previousCharacter", - "ts(text):flickLeft":"review_previousCharacter", - "kb:numpad2": "review_currentCharacter", - "kb(laptop):NVDA+.": "review_currentCharacter", - "kb:numpad3": "review_nextCharacter", - "kb(laptop):NVDA+rightArrow": "review_nextCharacter", - "ts(text):flickRight":"review_nextCharacter", - "kb:shift+numpad3": "review_endOfLine", - "kb(laptop):NVDA+end": "review_endOfLine", - "kb:numpadPlus": "review_sayAll", - "kb(laptop):NVDA+shift+a": "review_sayAll", - "ts(text):3finger_flickDown":"review_sayAll", - "kb:NVDA+f9": "review_markStartForCopy", - "kb:NVDA+f10": "review_copy", - - # Flat review - "kb:NVDA+numpad7": "reviewMode_next", - "kb(laptop):NVDA+pageUp": "reviewMode_next", - "ts(object):2finger_flickUp": "reviewMode_next", - "kb:NVDA+numpad1": "reviewMode_previous", - "kb(laptop):NVDA+pageDown": "reviewMode_previous", - "ts(object):2finger_flickDown": "reviewMode_previous", - - # Mouse - "kb:numpadDivide": "leftMouseClick", - "kb(laptop):NVDA+[": "leftMouseClick", - "kb:shift+numpadDivide": "toggleLeftMouseButton", - "kb(laptop):NVDA+control+[": "toggleLeftMouseButton", - "kb:numpadMultiply": "rightMouseClick", - "kb(laptop):NVDA+]": "rightMouseClick", - "kb:shift+numpadMultiply": "toggleRightMouseButton", - "kb(laptop):NVDA+control+]": "toggleRightMouseButton", - "kb:NVDA+numpadDivide": "moveMouseToNavigatorObject", - "kb(laptop):NVDA+shift+m": "moveMouseToNavigatorObject", - "kb:NVDA+numpadMultiply": "moveNavigatorObjectToMouse", - "kb(laptop):NVDA+shift+n": "moveNavigatorObjectToMouse", - - # Tree interceptors - "kb:NVDA+space": "toggleVirtualBufferPassThrough", - "kb:NVDA+control+space": "moveToParentTreeInterceptor", - - # Preferences dialogs and panels - "kb:NVDA+control+g": "activateGeneralSettingsDialog", - "kb:NVDA+control+s": "activateSynthesizerDialog", - "kb:NVDA+control+v": "activateVoiceDialog", - "kb:NVDA+control+a": "activateBrailleDisplayDialog", - "kb:NVDA+control+k": "activateKeyboardSettingsDialog", - "kb:NVDA+control+m": "activateMouseSettingsDialog", - "kb:NVDA+control+o": "activateObjectPresentationDialog", - "kb:NVDA+control+b": "activateBrowseModeDialog", - "kb:NVDA+control+d": "activateDocumentFormattingDialog", - - # Configuration management - "kb:NVDA+control+c": "saveConfiguration", - "kb:NVDA+control+r": "revertConfiguration", - "kb:NVDA+control+p": "activateConfigProfilesDialog", - - # Settings - "kb:NVDA+shift+d":"cycleAudioDuckingMode", - "kb:NVDA+2": "toggleSpeakTypedCharacters", - "kb:NVDA+3": "toggleSpeakTypedWords", - "kb:NVDA+4": "toggleSpeakCommandKeys", - "kb:NVDA+p": "cycleSpeechSymbolLevel", - "kb:NVDA+s": "speechMode", - "kb:NVDA+m": "toggleMouseTracking", - "kb:NVDA+u": "toggleProgressBarOutput", - "kb:NVDA+5": "toggleReportDynamicContentChanges", - "kb:NVDA+6": "toggleCaretMovesReviewCursor", - "kb:NVDA+7": "toggleFocusMovesNavigatorObject", - "kb:NVDA+8": "toggleAutoFocusFocusableElements", - "kb:NVDA+control+t": "braille_toggleTether", - - # Synth settings ring - "kb(desktop):NVDA+control+leftArrow": "previousSynthSetting", - "kb(laptop):NVDA+shift+control+leftArrow": "previousSynthSetting", - "kb(desktop):NVDA+control+rightArrow": "nextSynthSetting", - "kb(laptop):NVDA+shift+control+rightArrow": "nextSynthSetting", - "kb(desktop):NVDA+control+upArrow": "increaseSynthSetting", - "kb(laptop):NVDA+shift+control+upArrow": "increaseSynthSetting", - "kb(desktop):NVDA+control+downArrow": "decreaseSynthSetting", - "kb(laptop):NVDA+control+shift+downArrow": "decreaseSynthSetting", - - # Braille keyboard - "bk:dots" : "braille_dots", - "bk:dot7" : "braille_eraseLastCell", - "bk:dot8" : "braille_enter", - "bk:dot7+dot8" : "braille_translate", - - # Tools - "kb:NVDA+f1": "navigatorObject_devInfo", - "kb:NVDA+control+f1": "reportAppModuleInfo", - "kb:NVDA+control+z": "activatePythonConsole", - "kb:NVDA+control+f3": "reloadPlugins", - "kb(desktop):NVDA+control+f2": "test_navigatorDisplayModelText", - "kb:NVDA+alt+m": "interactWithMath", - "kb:NVDA+r": "recognizeWithUwpOcr", - } #: The single global commands instance. #: @type: L{GlobalCommands} From bf3671540481fc4a6ccd40043398e9d83432e94b Mon Sep 17 00:00:00 2001 From: eric <26911141+dingpengyu@users.noreply.github.com> Date: Thu, 21 Jan 2021 06:14:54 +0800 Subject: [PATCH 015/174] Modify the copyrightYears = "2006-2020" copyrightYears = "2006-2021" (#11927) --- source/versionInfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/versionInfo.py b/source/versionInfo.py index 67a4612b383..95ab654ce84 100644 --- a/source/versionInfo.py +++ b/source/versionInfo.py @@ -16,7 +16,7 @@ longName = _("NonVisual Desktop Access") description = _("A free and open source screen reader for Microsoft Windows") url = "https://www.nvaccess.org/" -copyrightYears = "2006-2020" +copyrightYears = "2006-2021" copyright = _("Copyright (C) {years} NVDA Contributors").format( years=copyrightYears) aboutMessage = _( From 5daf693f3aae504c414b3b62a7f651af929952aa Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Wed, 20 Jan 2021 23:48:46 +0100 Subject: [PATCH 016/174] Fix for inappropriate distance announcement when pressing shift+tab from Outlook message (#11925) * When writing an e-mail in MS Outlook, pressing shift+tab keystroke to go back to the e-mail's headers or attachments should not announce a distance anymore. * Fixed linting. * Update what's new Co-authored-by: Michael Curran --- source/NVDAObjects/window/winword.py | 3 +++ source/appModules/outlook.py | 20 ++++++++++++++++++-- user_docs/en/changes.t2t | 1 + 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/source/NVDAObjects/window/winword.py b/source/NVDAObjects/window/winword.py index 98f05c76c74..7b44739f4cf 100755 --- a/source/NVDAObjects/window/winword.py +++ b/source/NVDAObjects/window/winword.py @@ -1402,6 +1402,9 @@ def script_tab(self,gesture): * If not in a table, announces the distance of the caret from the left edge of the document, and any remaining text on that line. """ gesture.send() + self.reportTab() + + def reportTab(self): selectionObj=self.WinwordSelectionObject inTable=selectionObj.tables.count>0 if selectionObj else False info=self.makeTextInfo(textInfos.POSITION_SELECTION) diff --git a/source/appModules/outlook.py b/source/appModules/outlook.py index f24131cd74f..5fc629819c8 100644 --- a/source/appModules/outlook.py +++ b/source/appModules/outlook.py @@ -11,6 +11,7 @@ import ctypes from hwPortUtils import SYSTEMTIME import scriptHandler +from scriptHandler import script import winKernel import comHelper import NVDAHelper @@ -29,6 +30,7 @@ import ui from NVDAObjects.IAccessible import IAccessible from NVDAObjects.window import Window +from NVDAObjects.window.winword import WordDocument as BaseWordDocument from NVDAObjects.IAccessible.winword import WordDocument, WordDocumentTreeInterceptor, BrowseModeWordDocumentTextInfo, WordDocumentTextInfo from NVDAObjects.IAccessible.MSHTML import MSHTML from NVDAObjects.behaviors import RowWithFakeNavigation, Dialog @@ -552,7 +554,20 @@ def script_tab(self,gesture): "kb:shift+tab":"tab", } -class OutlookWordDocument(WordDocument): + +class BaseOutlookWordDocument(BaseWordDocument): + + @script(gestures=["kb:tab", "kb:shift+tab"]) + def script_tab(self, gesture): + bookmark = self.makeTextInfo(textInfos.POSITION_SELECTION).bookmark + gesture.send() + info, caretMoved = self._hasCaretMoved(bookmark) + if not caretMoved: + return + self.reportTab() + + +class OutlookWordDocument(WordDocument, BaseOutlookWordDocument): def _get_isReadonlyViewer(self): # #2975: The only way we know an email is read-only is if the underlying email has been sent. @@ -575,7 +590,8 @@ def _get_role(self): ignoreEditorRevisions=True ignorePageNumbers=True # This includes page sections, and page columns. None of which are appropriate for outlook. -class OutlookUIAWordDocument(UIAWordDocument): + +class OutlookUIAWordDocument(UIAWordDocument, BaseOutlookWordDocument): """ Forces browse mode to be used on the UI Automation Outlook message viewer if the message is being read).""" def _get_isReadonlyViewer(self): diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 818c9701cf5..77aa16fe426 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -15,6 +15,7 @@ What's New in NVDA - In terminal programs on Windows 10 version 1607 and later, when inserting or deleting characters in the middle of a line, the characters to the right of the caret are no longer read out. (#3200) - This experimental fix must be manually enabled in NVDA's advanced settings panel by changing the diff algorithm to Diff Match Patch. - Fixed access to edit fields in MCS Electronics IDE's. (#11966) +- In MS Outlook, inappropriate distance reporting when shift+tabbing from the message body to the subject field should not occur anymore. (#10254) == Changes for Developers == From 33cd507817fbefbc0d1c46db13ea8768cde00c25 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Wed, 20 Jan 2021 23:53:54 +0100 Subject: [PATCH 017/174] Enable activating a control with cursor routing on any of its braille cells (#7447) (#11922) --- source/braille.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/braille.py b/source/braille.py index 7b2e0da7718..ec3b5772523 100644 --- a/source/braille.py +++ b/source/braille.py @@ -1087,7 +1087,9 @@ def routeTo(self, braillePos): brailleInput.handler.updateDisplay() return - if braillePos == self.brailleCursorPos: + dest = self.getTextInfoForBraillePos(braillePos) + cursor = self.getTextInfoForBraillePos(self.brailleCursorPos) + if dest.compareEndPoints(cursor, "startToStart") == 0: # The cursor is already at this position, # so activate the position. try: @@ -1095,7 +1097,6 @@ def routeTo(self, braillePos): except NotImplementedError: pass return - dest = self.getTextInfoForBraillePos(braillePos) self._setCursor(dest) def nextLine(self): From c5697d6e9e45a5043bfbebb3047addc2b5db7211 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 21 Jan 2021 08:54:53 +1000 Subject: [PATCH 018/174] Update what's new --- user_docs/en/changes.t2t | 1 + 1 file changed, 1 insertion(+) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 77aa16fe426..e6fb263c544 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -9,6 +9,7 @@ What's New in NVDA == Changes == +- In browse mode, controls can now be activated with braille cursor routing on their descriptor (ie. "lnk" for a link). This is especially useful for activating eg. check-boxes with no labels. (#7447) == Bug Fixes == From b3b25d50c9d04c4f3b72a1c73771afb7848c45fe Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Thu, 21 Jan 2021 00:21:17 +0100 Subject: [PATCH 019/174] Prevent running Windows 10 OCR if screen curtain is enabled. (#11921) * Prevent running Windows 10 OCR if screen curtain is enabled. * Update what's new Co-authored-by: Michael Curran --- source/globalCommands.py | 8 ++++++++ user_docs/en/changes.t2t | 1 + user_docs/en/userGuide.t2t | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/source/globalCommands.py b/source/globalCommands.py index 7ce329cf161..10aaf7d11e5 100644 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -3109,6 +3109,14 @@ def script_recognizeWithUwpOcr(self, gesture): # Translators: Reported when Windows 10 OCR is not available. ui.message(_("Windows 10 OCR not available")) return + from visionEnhancementProviders.screenCurtain import ScreenCurtainProvider + screenCurtainId = ScreenCurtainProvider.getSettings().getId() + screenCurtainProviderInfo = vision.handler.getProviderInfo(screenCurtainId) + isScreenCurtainRunning = bool(vision.handler.getProviderInstance(screenCurtainProviderInfo)) + if isScreenCurtainRunning: + # Translators: Reported when screen curtain is enabled. + ui.message(_("Please disable screen curtain before using Windows 10 OCR.")) + return from contentRecog import uwpOcr, recogUi recog = uwpOcr.UwpOcr() recogUi.recognizeNavigatorObject(recog) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index e6fb263c544..b17f71c3213 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -10,6 +10,7 @@ What's New in NVDA == Changes == - In browse mode, controls can now be activated with braille cursor routing on their descriptor (ie. "lnk" for a link). This is especially useful for activating eg. check-boxes with no labels. (#7447) +- NVDA now prevents the user from performing Windows 10 OCR if screen curtain is enabled. (#11911) == Bug Fixes == diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 7623e1ffc9a..95c25fd8b99 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -794,6 +794,8 @@ For this situation, NVDA contains a feature called "screen curtain" which can be You can enable the Screen Curtain in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog. +When the screen curtain is active some tasks directly based on what appears on the screen such as performing [OCR #Win10Ocr] or taking a screenshot cannot be achieved. + + Content Recognition +[ContentRecognition] When authors don't provide sufficient information for a screen reader user to determine the content of something, various tools can be used to attempt to recognize the content from an image. NVDA supports the optical character recognition (OCR) functionality built into Windows 10 to recognize text from images. @@ -815,6 +817,8 @@ NVDA can use this to recognize text from images or inaccessible applications. You can set the language to use for text recognition in the [Windows 10 OCR category #Win10OcrSettings] of the [NVDA Settings #NVDASettings] dialog. Additional languages can be installed by opening the Start menu, choosing Settings, selecting Time & Language -> Region & Language and then choosing Add a language. +Windows 10 OCR may be partially or fully incompatible with [NVDA vision enhancements #Vision] or other external visual aids. You will need to disable these aids before proceeding to a recognition. + %kc:beginInclude To recognize the text in the current navigator object using Windows 10 OCR, press NVDA+r. %kc:endInclude From 561beeb369f754623596b1eabcc8fa66664df9f9 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Thu, 21 Jan 2021 01:09:07 +0100 Subject: [PATCH 020/174] Python Console: Fix handling of the tab key in the input pane (#11532) (#11936) * Python Console: Fix handling of the tab key in the input pane (#11532) * Support indenting with tabs when editing a non-empty input line * Support tab-completion in the middle of an input line * Python Console: Tab-completion: Handle selection * Consider selection start rather than cursor position (different if selection is anchored at start) * Replace selection upon successful completion --- source/pythonConsole.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/source/pythonConsole.py b/source/pythonConsole.py index 3612dfdd8b3..33cc9384d93 100755 --- a/source/pythonConsole.py +++ b/source/pythonConsole.py @@ -306,9 +306,11 @@ def historyMove(self, movement): return True RE_COMPLETE_UNIT = re.compile(r"[\w.]*$") + def complete(self): + textBeforeCursor = self.inputCtrl.GetRange(0, self.inputCtrl.GetSelection()[0]) try: - original = self.RE_COMPLETE_UNIT.search(self.inputCtrl.GetValue()).group(0) + original = self.RE_COMPLETE_UNIT.search(textBeforeCursor).group(0) except AttributeError: return False @@ -373,16 +375,20 @@ def _insertCompletion(self, original, completed): insert = completed[len(original):] if not insert: return - self.inputCtrl.SetValue(self.inputCtrl.GetValue() + insert) + inputCtrl = self.inputCtrl + selStartPos, selEndPos = inputCtrl.GetSelection() + prefix = inputCtrl.GetRange(0, selStartPos) + suffix = inputCtrl.GetRange(selEndPos, inputCtrl.GetLastPosition()) + inputCtrl.SetValue(prefix + insert + suffix) queueHandler.queueFunction(queueHandler.eventQueue, speech.speakText, insert) - self.inputCtrl.SetInsertionPointEnd() + inputCtrl.SetInsertionPoint(selStartPos + len(insert)) def onInputChar(self, evt): key = evt.GetKeyCode() if key == wx.WXK_TAB: - line = self.inputCtrl.GetValue() - if line and not line.isspace(): + textBeforeCursor = self.inputCtrl.GetRange(0, self.inputCtrl.GetSelection()[0]) + if textBeforeCursor and not textBeforeCursor.isspace(): if not self.complete(): wx.Bell() return From 90c37874578e98d4eca465adccb8f32d9ee8cb3f Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 21 Jan 2021 10:11:02 +1000 Subject: [PATCH 021/174] Update what's new --- user_docs/en/changes.t2t | 1 + 1 file changed, 1 insertion(+) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index b17f71c3213..ed3b5486652 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -18,6 +18,7 @@ What's New in NVDA - This experimental fix must be manually enabled in NVDA's advanced settings panel by changing the diff algorithm to Diff Match Patch. - Fixed access to edit fields in MCS Electronics IDE's. (#11966) - In MS Outlook, inappropriate distance reporting when shift+tabbing from the message body to the subject field should not occur anymore. (#10254) +- In the Python Console, inserting a tab for indentation at the beginning of a non-empty input line and performing tab-completion in the middle of an input line are now supported. (#11532) == Changes for Developers == From d353d1bc4f56f158ddf8a3fd2163605826da4594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Thu, 21 Jan 2021 01:53:32 +0100 Subject: [PATCH 022/174] Remove deprecated functions from the config module (#11935) * Remove compatibility wrappers around `hasUiAccess` and `execElevated` introduced in #10493 from the config module * Remove deprecated `getConfigDirs` from the config module * Add missing imports from `typing` to the config module * Remove deprecated `canStartOnSecureScreens` from the config module * Update what's new Co-authored-by: Michael Curran --- source/config/__init__.py | 32 +------------------------------- source/gui/settingsDialogs.py | 4 ++-- user_docs/en/changes.t2t | 4 ++++ 3 files changed, 7 insertions(+), 33 deletions(-) diff --git a/source/config/__init__.py b/source/config/__init__.py index 55c689897e5..61de93ace03 100644 --- a/source/config/__init__.py +++ b/source/config/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: UTF-8 -*- # A part of NonVisual Desktop Access (NVDA) # Copyright (C) 2006-2020 NV Access Limited, Aleksey Sadovoy, Peter Vágner, Rui Batista, Zahari Yurukov, # Joseph Lee, Babbage B.V., Łukasz Golonka, Julien Cochuyt @@ -34,7 +33,7 @@ import extensionPoints from . import profileUpgrader from .configSpec import confspec -from typing import Optional, List +from typing import Any, Dict, List, Optional, Set #: True if NVDA is running as a Windows Store Desktop Bridge application isAppX=False @@ -222,10 +221,6 @@ def setStartAfterLogon(enable): except WindowsError: pass -def canStartOnSecureScreens(): - # No more need to check for the NVDA service nor presence of Ease of Access, as only Windows 7 SP1 and higher is supported. - # This function will be transformed into a flag in a future release. - return isInstalledCopy() SLAVE_FILENAME = os.path.join(globalVars.appDir, "nvda_slave.exe") @@ -310,18 +305,6 @@ def setStartOnLogonScreen(enable): ) != 0: raise RuntimeError("Slave failed to set startOnLogonScreen") -def getConfigDirs(subpath=None): - """Retrieve all directories that should be used when searching for configuration. - IF C{subpath} is provided, it will be added to each directory returned. - @param subpath: The path to be added to each directory, C{None} for none. - @type subpath: str - @return: The configuration directories in the order in which they should be searched. - @rtype: list of str - """ - log.warning("getConfigDirs is deprecated. Use globalVars.appArgs.configPath instead") - return [os.path.join(dir, subpath) if subpath else dir - for dir in (globalVars.appArgs.configPath,) - ] def addConfigDirsToPythonPackagePath(module, subdir=None): """Add the configuration directories to the module search path (__path__) of a Python package. @@ -1179,16 +1162,3 @@ def exit(self): def __exit__(self, excType, excVal, traceback): self.exit() -# The below functions are moved to systemUtils module. -# They are kept here for backwards compatibility. -# They would be removed from the config module in NVDA 2021.1. - - -def execElevated(*args, **kwargs): - import systemUtils - systemUtils.execElevated(*args, **kwargs) - - -def hasUiAccess(*args, **kwargs): - import systemUtils - systemUtils.hasUiAccess(*args, **kwargs) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 4267534c0fc..c967f74cd52 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -737,7 +737,7 @@ def makeSettings(self, settingsSizer): ) self.bindHelpEvent("GeneralSettingsStartOnLogOnScreen", self.startOnLogonScreenCheckBox) self.startOnLogonScreenCheckBox.SetValue(config.getStartOnLogonScreen()) - if globalVars.appArgs.secure or not config.canStartOnSecureScreens(): + if globalVars.appArgs.secure or not config.isInstalledCopy(): self.startOnLogonScreenCheckBox.Disable() settingsSizerHelper.addItem(self.startOnLogonScreenCheckBox) @@ -754,7 +754,7 @@ def makeSettings(self, settingsSizer): ) self.bindHelpEvent("GeneralSettingsCopySettings", self.copySettingsButton) self.copySettingsButton.Bind(wx.EVT_BUTTON,self.onCopySettings) - if globalVars.appArgs.secure or not config.canStartOnSecureScreens(): + if globalVars.appArgs.secure or not config.isInstalledCopy(): self.copySettingsButton.Disable() settingsSizerHelper.addItem(self.copySettingsButton) if updateCheck: diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index ed3b5486652..1122e47662b 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -28,6 +28,10 @@ What's New in NVDA - `LiveText` objects can now calculate diffs by character. (#11639) - To alter the diff behaviour for some object, override the `diffAlgo` property (see the docstring for details). - When defining a script with the script decorator, the 'allowInSleepMode' boolean argument can be specified to control if a script is available in sleep mode or not. (#11979) +- The following functions are removed from the config module: + - canStartOnSecureScreens - use config.isInstalledCopy instead. + - hasUiAccess and execElevated - use them from the systemUtils module. + - getConfigDirs - use globalVars.appArgs.configPath instead = 2020.4 = From ed24425e84286bcc7bbb4bbbeddc076983f3c0da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ozancan=20Karata=C5=9F?= Date: Thu, 21 Jan 2021 04:04:35 +0300 Subject: [PATCH 023/174] Update CLDR to version 38.1 (#11943) --- include/cldr | 2 +- readme.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/cldr b/include/cldr index d1c59aeea6a..367b79b4a86 160000 --- a/include/cldr +++ b/include/cldr @@ -1 +1 @@ -Subproject commit d1c59aeea6a26cd1018e164e8c470e4873925cac +Subproject commit 367b79b4a86a9cb12314158897d950268dc77b02 diff --git a/readme.md b/readme.md index 0d0154ac074..61910f12878 100644 --- a/readme.md +++ b/readme.md @@ -90,7 +90,7 @@ For reference, the following run time dependencies are included in Git submodule * [ConfigObj](https://github.com/DiffSK/configobj), commit f9a265c * [Six](https://pypi.python.org/pypi/six), version 1.12.0, required by wxPython and ConfigObj * [liblouis](http://www.liblouis.org/), version 3.16.1 -* [Unicode Common Locale Data Repository (CLDR)](http://cldr.unicode.org/) Emoji Annotations, version 38.0 +* [Unicode Common Locale Data Repository (CLDR)](http://cldr.unicode.org/), version 38.1 * NVDA images and sounds * [Adobe Acrobat accessibility interface, version XI](https://download.macromedia.com/pub/developer/acrobat/AcrobatAccess.zip) * Adobe FlashAccessibility interface typelib From 874947300921217edd94c444d44050deece2f650 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 21 Jan 2021 11:06:52 +1000 Subject: [PATCH 024/174] Update what's new --- user_docs/en/changes.t2t | 1 + 1 file changed, 1 insertion(+) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 1122e47662b..443f67f20cc 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -11,6 +11,7 @@ What's New in NVDA == Changes == - In browse mode, controls can now be activated with braille cursor routing on their descriptor (ie. "lnk" for a link). This is especially useful for activating eg. check-boxes with no labels. (#7447) - NVDA now prevents the user from performing Windows 10 OCR if screen curtain is enabled. (#11911) +- Updated Unicode Common Locale Data Repository (CLDR) to 38.1. (#11943) == Bug Fixes == From 2e0ebb5691b4eabff6bdceaab4f45a5485fca026 Mon Sep 17 00:00:00 2001 From: Adriani90 Date: Thu, 21 Jan 2021 02:49:30 +0100 Subject: [PATCH 025/174] Added some mathematical symbols (#11467) * Added some mathematical symbols * Addressed missing tab character and a missing level * Added some more mathematical symbols and restructured for better overview * Fixed some symols to match different languages (i.e. see issue #11502 * Addressed review actions and removed the ordinal symbols because they cause issues in many lating languages. Those ordinal symbols should be controlled by the synthesizers. * Addressed review actions (removed the mathematical constants and the incremental symbol to avoid confusions and improved consistency in ortographics) --- source/locale/en/symbols.dic | 614 ++++++++++++++++++++--------------- 1 file changed, 361 insertions(+), 253 deletions(-) diff --git a/source/locale/en/symbols.dic b/source/locale/en/symbols.dic index 5bffe0d72c9..09356a19169 100644 --- a/source/locale/en/symbols.dic +++ b/source/locale/en/symbols.dic @@ -1,253 +1,361 @@ -#locale/en/symbols.dic -#A part of NonVisual Desktop Access (NVDA) -#Copyright (c) 2011-2017 NVDA Contributors -#This file is covered by the GNU General Public License. - -complexSymbols: -# identifier regexp -# Sentence endings. -. sentence ending (?<=[^\s.])\.(?=[\"'”’)\s]|$) -! sentence ending (?<=[^\s!])\!(?=[\"'”’)\s]|$) -? sentence ending (?<=[^\s?])\?(?=[\"'”’)\s]|$) -# Phrase endings. -; phrase ending (?<=[^\s;]);(?=\s|$) -: phrase ending (?<=[^\s:]):(?=\s|$) -# Others -decimal point (? greater some -= equals some -? question all -@ at some -[ left bracket most -] right bracket most -\\ backslash most -^ caret most -_ line most -` graav most -{ left brace most -} right brace most -| bar most -¦ broken bar most -~ tilda most -¡ inverted exclamation point some -¿ inverted question mark some -· middle dot most -‚ single low quote most -„ double low quote most -′ prime some -″ double prime some -‴ triple prime some - -# Other characters -• bullet some -… dot dot dot all always -... dot dot dot all always -“ left quote most -” right quote most -‘ left tick most -’ right tick most -– en dash most always -— em dash most -­ soft hyphen most -⁃ hyphen bullet none -● circle most -○ white circle most -¨ diaeresis most -¯ macron most -´ acute most -¸ cedilla most -‎ left to right mark char -‏ right to left mark char -¶ paragraph marker most -■ black square some -▪ black square some -◾ black square some -□ white square some -◦ white bullet some -⇒ right double arrow some -⇨ right white arrow some -➔ right-pointing arrow some -➢ right arrowhead some -⮚ right arrowhead some -❖ black diamond minus white X some -♣ black club some -♦ black diamond some -◆ black diamond some -§ section all -° degrees some -« double left pointing angle bracket -» double right pointing angle bracket -µ micro some -º ordinal some -ª superscript a some -⁰ superscript 0 some -¹ superscript 1 some -² superscript 2 some -³ superscript 3 some -⁴ superscript 4 some -⁵ superscript 5 some -⁶ superscript 6 some -⁷ superscript 7 some -⁸ superscript 8 some -⁹ superscript 9 some -⁺ superscript plus some -⁻ superscript minus some -⁼ superscript equals some -⁽ superscript left paren some -⁾ superscript right paren some -ⁿ superscript n some -₀ subscript 0 some -₁ subscript 1 some -₂ subscript 2 some -₃ subscript 3 some -₄ subscript 4 some -₅ subscript 5 some -₆ subscript 6 some -₇ subscript 7 some -₈ subscript 8 some -₉ subscript 9 some -₊ subscript plus some -₋ subscript minus some -₌ subscript equals some -₍ subscript left paren some -₎ subscript right paren some -® registered some -™ Trademark some -© Copyright some -℠ ServiceMark some -± Plus or Minus some -× times some -÷ divide by some -← left arrow some -↑ up arrow some -→ right arrow some -↓ down arrow some -✓ check some -✔ check some -🡺 right arrow some -† dagger some -‡ double dagger some -‣ triangular bullet none -✗ x-shaped bullet none - -#Mathematical Operators U+2200 to U+220F -∀ for all none -∁ complement none -∂ partial derivative none -∃ there exists none -∄ there does not exist none -∅ empty set none -∆ increment none -∇ nabla none -∈ element of none -∉ not an element of none -∊ small element of none -∋ contains as member none -∌ does not contain as member none -∍ small contains as member none -∎ end of proof none -∏ n-ary product none - -# Miscellaneous Mathematical Operators -∑ n-ary summation none -√ square root none -∛ cube root none -∜ fourth root none -∝ proportional to none -∞ infinity none -∟ right angle none -∠ angle none -∥ parallel to none -∦ not parallel to none -∧ logical and none -∨ logical or none -¬ logical not none -∩ intersection none -∪ union none -∫ integral none -∴ therefore none -∵ because none -∶ ratio none -∷ proportion none -≤ less- than or equal to none -≥ greater-than or equal to none -⊂ subset of none -⊃ superset of none -⊆ subset of or equal to none -⊇ superset of or equal to none - -# Vulgur Fractions U+2150 to U+215E -¼ one quarter none -½ one half none -¾ three quarters none -⅐ one seventh none -⅑ one ninth none -⅒ one tenth none -⅓ one third none -⅔ two thirds none -⅕ one fifth none -⅖ two fifths none -⅗ three fifths none -⅘ four fifths none -⅙ one sixth none -⅚ five sixths none -⅛ one eighth none -⅜ three eights none -⅝ five eighths none -⅞ seven eighths none - -# Miscellaneous Technical -⌘ Mac Command key none +#locale/en/symbols.dic +#A part of NonVisual Desktop Access (NVDA) +#Copyright (c) 2011-2017 NVDA Contributors +#This file is covered by the GNU General Public License. + +complexSymbols: +# identifier regexp +# Sentence endings. +. sentence ending (?<=[^\s.])\.(?=[\"'”’)\s]|$) +! sentence ending (?<=[^\s!])\!(?=[\"'”’)\s]|$) +? sentence ending (?<=[^\s?])\?(?=[\"'”’)\s]|$) +# Phrase endings. +; phrase ending (?<=[^\s;]);(?=\s|$) +: phrase ending (?<=[^\s:]):(?=\s|$) +# Others +decimal point (? greater some +≤ less- than or equal to none +≦ less- than or equal to none +≪ much smaller than none +≥ greater-than or equal to none +≧ greater-than or equal to none +≫ much bigger than none +≶ less than or greater than none +≷ greater than or less than none +≮ not less than none +≯ not greater than none + +#Functions +⁻ inverse some +∘ of some +∂ partial derivative none +∇ gradient of none + +#Geometry and linear Algebra +⃗ vector between none +△ triangle none +▭ rectangle none +∟ right angle none +∠ angle none +∥ parallel to none +∦ not parallel to none +⊥ perpendicular to none +⟂ ortogonal to none +‖ norm of vector none +̂ normalizes none +∿ sine wave none +∡ measured Angle none +∢ spherical Angle none + +#Logical operators +∀ for all none +∃ there exists none +∄ there does not exist none +⇏ does not imply none +⇐ is implied by none + +#Other mathematical Operators +∈ element of none +∉ not an element of none +∊ small element of none +∋ contains as member none +∌ does not contain as member none +∍ small contains as member none +∎ end of proof none +∏ n-ary product none +∐ n-ary coproduct none +∑ n-ary summation none +√ square root none +∛ cube root none +∜ fourth root none +∝ proportional to none +∞ infinity none +∧ and none +∨ or none +¬ not none +∩ intersection none +∪ union none +∫ integral none +∬ double Integral none +∭ tripple Integral none +∮ contour Integral none +∯ surface Integral none +∰ volume Integral none +∱ clockwise Integral none +∲ clockwise contour Integral none +∳ anticlockwise Contour Integral none +∴ therefore none +∵ because none +∶ ratio none +∷ proportion none +∹ excess none +∺ geometric proportion none +≀ wreath product none +≏ difference between none +≐ approaches the limit none +∘ ring Operator none +∙ bullet Operator none +∣ divides none +∤ does not divide none +≔ colon equals none +≕ equals colon none +≙ estimates none +≺ precedes none +≻ succeeds none +⊀ does not precede none +⊁ does not succeed none + +# Vulgur Fractions U+2150 to U+215E +¼ one quarter none +½ one half none +¾ three quarters none +⅐ one seventh none +⅑ one ninth none +⅒ one tenth none +⅓ one third none +⅔ two thirds none +⅕ one fifth none +⅖ two fifths none +⅗ three fifths none +⅘ four fifths none +⅙ one sixth none +⅚ five sixths none +⅛ one eighth none +⅜ three eights none +⅝ five eighths none +⅞ seven eighths none + +#Number sets +𝔸 algebraic numbers none +ℂ complex numbers none +ℑ imaginary part of complex number none +ℍ quaternions none +ℕ natural numbers none +𝕁 nonnegative (whole) numbers none +ℚ rational numbers none +ℝ real numbers none +ℜ real part of complex number none +ℤ integers none +ℵ aleph number none +ℶ beth number none + +# Miscellaneous Technical +⌘ mac Command key none From 84e2c6e662fb86f411dda2d128c022f171004ae9 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 21 Jan 2021 11:50:25 +1000 Subject: [PATCH 026/174] Update what's new --- user_docs/en/changes.t2t | 1 + 1 file changed, 1 insertion(+) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 443f67f20cc..d6ffaabd966 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -12,6 +12,7 @@ What's New in NVDA - In browse mode, controls can now be activated with braille cursor routing on their descriptor (ie. "lnk" for a link). This is especially useful for activating eg. check-boxes with no labels. (#7447) - NVDA now prevents the user from performing Windows 10 OCR if screen curtain is enabled. (#11911) - Updated Unicode Common Locale Data Repository (CLDR) to 38.1. (#11943) +- Added more mathematical symbols to the symbols dictionary. (#11467) == Bug Fixes == From e8f14f9ae4d239fff28f78578c2a84128f214fa2 Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Fri, 22 Jan 2021 03:09:34 +0100 Subject: [PATCH 027/174] Remove blank lines in browseable message (#12005) * Removed the blank lines in browseable message. * Code clean-up. * Addressed review comments: use html.escape to fully escape the message string. * Fixing commit issue (a modification was not correctly added in previous commit). * Update what's new Co-authored-by: Michael Curran --- source/message.html | 14 ++++---------- source/ui.py | 6 ++++-- user_docs/en/changes.t2t | 1 + 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/source/message.html b/source/message.html index 6af6aea2890..130b7af0f00 100644 --- a/source/message.html +++ b/source/message.html @@ -15,16 +15,11 @@ function windowOnLoad() { // #5875: string.prototype.split strips the tail when a limit is supplied, // so use a regexp instead. - var args = window.dialogArguments.match(/^(.*?);(.*?);([\s\S]*)$/); + var args = window.dialogArguments.match(/^(.*?);([\s\S]*)$/); // args[0] is the whole string. - if (args && args.length == 4){ - document.title= args[2]; - if ( args[1] == "true") { - messageID.innerHTML= args[3]; - } - else if ( args[1] == "false") { - textID.innerText= args[3]; - } + if (args && args.length == 3){ + document.title= args[1]; + messageID.innerHTML= args[2]; } } //--> @@ -32,6 +27,5 @@
-

 
 
diff --git a/source/ui.py b/source/ui.py
index 74720c9bbeb..1f7e6fd6558 100644
--- a/source/ui.py
+++ b/source/ui.py
@@ -15,6 +15,7 @@
 from ctypes import windll, byref, POINTER, addressof
 from comtypes import IUnknown
 from comtypes import automation 
+from html import escape
 from logHandler import log
 import gui
 import speech
@@ -55,8 +56,9 @@ def browseableMessage(message,title=None,isHtml=False):
 	if not title:
 		# Translators: The title for the dialog used to present general NVDA messages in browse mode.
 		title = _("NVDA Message")
-	isHtmlArgument = "true" if isHtml else "false"
-	dialogString = u"{isHtml};{title};{message}".format( isHtml = isHtmlArgument , title=title , message=message ) 
+	if not isHtml:
+		message = f"
{escape(message)}
" + dialogString = f"{title};{message}" dialogArguements = automation.VARIANT( dialogString ) gui.mainFrame.prePopup() windll.mshtml.ShowHTMLDialogEx( diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index d6ffaabd966..a3495278a02 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -21,6 +21,7 @@ What's New in NVDA - Fixed access to edit fields in MCS Electronics IDE's. (#11966) - In MS Outlook, inappropriate distance reporting when shift+tabbing from the message body to the subject field should not occur anymore. (#10254) - In the Python Console, inserting a tab for indentation at the beginning of a non-empty input line and performing tab-completion in the middle of an input line are now supported. (#11532) +- Formatting information and other browseable messages no longer present unexpected blank lines when screen layout is turned off. (#12004) == Changes for Developers == From 856adec68f83a5ca6e48d6366e5c58d64957972d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Wed, 27 Jan 2021 00:10:15 +0100 Subject: [PATCH 028/174] Convert NVDA's code base to use output reasons from enum in controlTypes and remove deprecated REASON_* module level constants (#11969) * Convert NVDA's code base to use output reasons from enum in controlTypes rather than module level constants * Remove deprecated REASON_* constants from controlTypes * Update what's new Co-authored-by: Michael Curran --- source/NVDAObjects/IAccessible/__init__.py | 8 +- source/NVDAObjects/IAccessible/ia2Web.py | 5 +- source/NVDAObjects/IAccessible/winword.py | 4 +- source/NVDAObjects/UIA/__init__.py | 4 +- source/NVDAObjects/UIA/edge.py | 6 +- source/NVDAObjects/UIA/wordDocument.py | 2 +- source/NVDAObjects/__init__.py | 12 +-- source/NVDAObjects/behaviors.py | 6 +- source/NVDAObjects/window/winword.py | 6 +- source/appModules/kindle.py | 5 +- source/appModules/miranda32.py | 2 +- source/appModules/outlook.py | 2 +- source/appModules/powerpnt.py | 2 +- source/appModules/winamp.py | 2 +- source/braille.py | 10 ++- source/browseMode.py | 66 ++++++++-------- source/compoundDocuments.py | 4 +- source/controlTypes.py | 43 ++++------- source/cursorManager.py | 5 +- source/documentBase.py | 2 +- source/editableText.py | 10 ++- source/eventHandler.py | 2 +- source/globalCommands.py | 58 +++++++------- source/inputCore.py | 6 +- source/sayAllHandler.py | 4 +- source/screenExplorer.py | 2 +- source/speech/__init__.py | 90 ++++++++++++---------- source/textInfos/__init__.py | 4 +- tests/unit/test_controlTypes.py | 8 +- user_docs/en/changes.t2t | 2 + 30 files changed, 207 insertions(+), 175 deletions(-) diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index ebe78723975..295a982d15b 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -1471,10 +1471,10 @@ def event_alert(self): api.processPendingEvents() if self in api.getFocusAncestors(): return - speech.speakObject(self, reason=controlTypes.REASON_FOCUS, priority=speech.Spri.NOW) + speech.speakObject(self, reason=controlTypes.OutputReason.FOCUS, priority=speech.Spri.NOW) for child in self.recursiveDescendants: if controlTypes.STATE_FOCUSABLE in child.states: - speech.speakObject(child, reason=controlTypes.REASON_FOCUS, priority=speech.Spri.NOW) + speech.speakObject(child, reason=controlTypes.OutputReason.FOCUS, priority=speech.Spri.NOW) def event_caret(self): focus = api.getFocusObject() @@ -1956,11 +1956,11 @@ class IENotificationBar(Dialog,IAccessible): def event_alert(self): speech.cancelSpeech() - speech.speakObject(self,reason=controlTypes.REASON_FOCUS) + speech.speakObject(self, reason=controlTypes.OutputReason.FOCUS) child=self.simpleFirstChild while child: if child.role!=controlTypes.ROLE_STATICTEXT: - speech.speakObject(child,reason=controlTypes.REASON_FOCUS) + speech.speakObject(child, reason=controlTypes.OutputReason.FOCUS) child=child.simpleNext diff --git a/source/NVDAObjects/IAccessible/ia2Web.py b/source/NVDAObjects/IAccessible/ia2Web.py index 2f69390c967..d475130c6d0 100644 --- a/source/NVDAObjects/IAccessible/ia2Web.py +++ b/source/NVDAObjects/IAccessible/ia2Web.py @@ -87,7 +87,10 @@ def event_IA2AttributeChange(self): if self is api.getFocusObject(): # Report aria-current if it changed. speech.speakObjectProperties( - self, current=True, reason=controlTypes.REASON_CHANGE) + self, + current=True, + reason=controlTypes.OutputReason.CHANGE + ) # super calls event_stateChange which updates braille, so no need to # update braille here. diff --git a/source/NVDAObjects/IAccessible/winword.py b/source/NVDAObjects/IAccessible/winword.py index 57bcce07eb1..acc273a6377 100644 --- a/source/NVDAObjects/IAccessible/winword.py +++ b/source/NVDAObjects/IAccessible/winword.py @@ -269,7 +269,7 @@ def script_caret_moveByCell(self,gesture): isCollapsed=info.isCollapsed if inTable: info.expand(textInfos.UNIT_CELL) - speech.speakTextInfo(info,reason=controlTypes.REASON_FOCUS) + speech.speakTextInfo(info, reason=controlTypes.OutputReason.FOCUS) braille.handler.handleCaretMove(self) def script_reportCurrentComment(self,gesture): @@ -339,7 +339,7 @@ def _moveInTable(self,row=True,forward=True): ui.message(_("Edge of table")) return False newInfo=WordDocumentTextInfo(self,textInfos.POSITION_CARET,_rangeObj=foundCell) - speech.speakTextInfo(newInfo,reason=controlTypes.REASON_CARET, unit=textInfos.UNIT_CELL) + speech.speakTextInfo(newInfo, reason=controlTypes.OutputReason.CARET, unit=textInfos.UNIT_CELL) newInfo.collapse() newInfo.updateCaret() return True diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index 37cd11a9599..639d65e80f8 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -1668,7 +1668,7 @@ def event_UIA_systemAlert(self): This just reports the element that received the alert in speech and braille, similar to how focus is presented. Skype for business toast notifications being one example. """ - speech.speakObject(self, reason=controlTypes.REASON_FOCUS) + speech.speakObject(self, reason=controlTypes.OutputReason.FOCUS) # Ideally, we wouldn't use getPropertiesBraille directly. braille.handler.message(braille.getPropertiesBraille(name=self.name, role=self.role)) @@ -1768,7 +1768,7 @@ def event_focusEntered(self): def event_valueChange(self): focusParent=api.getFocusObject().parent if self==focusParent: - speech.speakObjectProperties(self,value=True,reason=controlTypes.REASON_CHANGE) + speech.speakObjectProperties(self, value=True, reason=controlTypes.OutputReason.CHANGE) else: super(SensitiveSlider,self).event_valueChange() diff --git a/source/NVDAObjects/UIA/edge.py b/source/NVDAObjects/UIA/edge.py index cbd84185868..beb661d9808 100644 --- a/source/NVDAObjects/UIA/edge.py +++ b/source/NVDAObjects/UIA/edge.py @@ -588,7 +588,11 @@ def _iterNodesByType(self,nodeType,direction="next",pos=None): def shouldPassThrough(self,obj,reason=None): # Enter focus mode for selectable list items ( and role=listbox) - if( - reason == controlTypes.OutputReason.FOCUS - and obj.role == controlTypes.ROLE_LISTITEM - and controlTypes.STATE_SELECTABLE in obj.states - ): - return True - return super(EdgeHTMLTreeInterceptor,self).shouldPassThrough(obj,reason=reason) - - def makeTextInfo(self,position): - try: - return super(EdgeHTMLTreeInterceptor,self).makeTextInfo(position) - except RuntimeError as e: - # sometimes the stored TextRange we have for the caret/selection can die if the page mutates too much. - # Therefore, if we detect this, just give back the first position in the document, updating our stored version as we go. - if position in (textInfos.POSITION_SELECTION,textInfos.POSITION_CARET): - log.debugWarning("%s died. Using first position instead."%position) - info=self.makeTextInfo(textInfos.POSITION_FIRST) - self._selection=info - return info - raise e class EdgeHTMLRoot(EdgeNode): diff --git a/source/NVDAObjects/UIA/web.py b/source/NVDAObjects/UIA/web.py new file mode 100644 index 00000000000..298fd26a6d0 --- /dev/null +++ b/source/NVDAObjects/UIA/web.py @@ -0,0 +1,391 @@ +# 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) 2015-2020 NV Access Limited, Babbage B.V., Leonard de Ruijter + +from comtypes import COMError +from comtypes.automation import VARIANT +from ctypes import byref +import winVersion +from logHandler import log +import eventHandler +import config +import controlTypes +import cursorManager +import re +import aria +import textInfos +import UIAHandler +from UIABrowseMode import UIABrowseModeDocument, UIABrowseModeDocumentTextInfo, UIATextRangeQuickNavItem,UIAControlQuicknavIterator +from UIAUtils import * +from . import UIA, UIATextInfo + + +def splitUIAElementAttribs(attribsString): + """Split an UIA Element attributes string into a dict of attribute keys and values. + An invalid attributes string does not cause an error, but strange results may be returned. + @param attribsString: The UIA Element attributes string to convert. + @type attribsString: str + @return: A dict of the attribute keys and values, where values are strings + @rtype: {str: str} + """ + attribsDict = {} + tmp = "" + key = "" + inEscape = False + for char in attribsString: + if inEscape: + tmp += char + inEscape = False + elif char == "\\": + inEscape = True + elif char == "=": + # We're about to move on to the value, so save the key and clear tmp. + key = tmp + tmp = "" + elif char == ";": + # We're about to move on to a new attribute. + if key: + # Add this key/value pair to the dict. + attribsDict[key] = tmp + key = "" + tmp = "" + else: + tmp += char + # If there was no trailing semi-colon, we need to handle the last attribute. + if key: + # Add this key/value pair to the dict. + attribsDict[key] = tmp + return attribsDict + + +class EdgeTextInfo(UIATextInfo): + + def _get_UIAElementAtStartWithReplacedContent(self): + """Fetches the deepest UIAElement at the start of the text range whos name has been overridden by the author (such as aria-label).""" + element=self.UIAElementAtStart + condition=createUIAMultiPropertyCondition({UIAHandler.UIA_ControlTypePropertyId:self.UIAControlTypesWhereNameIsContent}) + # A part from the condition given, we must always match on the root of the document so we know when to stop walking + runtimeID=VARIANT() + self.obj.UIAElement._IUIAutomationElement__com_GetCurrentPropertyValue(UIAHandler.UIA_RuntimeIdPropertyId,byref(runtimeID)) + condition=UIAHandler.handler.clientObject.createOrCondition(UIAHandler.handler.clientObject.createPropertyCondition(UIAHandler.UIA_RuntimeIdPropertyId,runtimeID),condition) + walker=UIAHandler.handler.clientObject.createTreeWalker(condition) + cacheRequest=UIAHandler.handler.clientObject.createCacheRequest() + cacheRequest.addProperty(UIAHandler.UIA_NamePropertyId) + cacheRequest.addProperty(UIAHandler.UIA_AriaPropertiesPropertyId) + element=walker.normalizeElementBuildCache(element,cacheRequest) + while element and not UIAHandler.handler.clientObject.compareElements(element,self.obj.UIAElement): + name=element.getCachedPropertyValue(UIAHandler.UIA_NamePropertyId) + if name: + ariaProperties=element.getCachedPropertyValue(UIAHandler.UIA_AriaPropertiesPropertyId) + if ('label=' in ariaProperties) or ('labelledby=' in ariaProperties): + return element + try: + textRange=self.obj.UIATextPattern.rangeFromChild(element) + except COMError: + return + text = textRange.getText(-1) + if not text or text.isspace(): + return element + element=walker.getParentElementBuildCache(element,cacheRequest) + + def _moveToEdgeOfReplacedContent(self,back=False): + """If within replaced content (E.g. aria-label is used), moves to the first or last character covered, so that a following call to move in the same direction will move out of the replaced content, in order to ensure that the content only takes up one character stop.""" + element=self.UIAElementAtStartWithReplacedContent + if not element: + return + try: + textRange=self.obj.UIATextPattern.rangeFromChild(element) + except COMError: + return + if not back: + textRange.MoveEndpointByRange(UIAHandler.TextPatternRangeEndpoint_Start, textRange, UIAHandler.TextPatternRangeEndpoint_End) + textRange.move(UIAHandler.TextUnit_Character, -1) + else: + textRange.MoveEndpointByRange(UIAHandler.TextPatternRangeEndpoint_End, textRange, UIAHandler.TextPatternRangeEndpoint_Start) + self._rangeObj=textRange + + def _collapsedMove(self,unit,direction,skipReplacedContent): + """A simple collapsed move (i.e. both ends move together), but whether it classes replaced content as one character stop can be configured via the skipReplacedContent argument.""" + if not skipReplacedContent: + return super(EdgeTextInfo,self).move(unit,direction) + if direction==0: + return + chunk=1 if direction>0 else -1 + finalRes=0 + while finalRes!=direction: + self._moveToEdgeOfReplacedContent(back=direction<0) + res=super(EdgeTextInfo,self).move(unit,chunk) + if res==0: + break + finalRes+=res + return finalRes + + def move(self,unit,direction,endPoint=None,skipReplacedContent=True): + if not endPoint: + return self._collapsedMove(unit,direction,skipReplacedContent) + else: + tempInfo=self.copy() + res=tempInfo.move(unit,direction,skipReplacedContent=skipReplacedContent) + if res!=0: + self.setEndPoint(tempInfo,"endToEnd" if endPoint=="end" else "startToStart") + return res + + def _getControlFieldForObject(self,obj,isEmbedded=False,startOfNode=False,endOfNode=False): + field=super(EdgeTextInfo,self)._getControlFieldForObject(obj,isEmbedded=isEmbedded,startOfNode=startOfNode,endOfNode=endOfNode) + field['embedded']=isEmbedded + role=field.get('role') + # Fields should be treated as block for certain roles. + # This can affect whether the field is presented as a container (e.g. announcing entering and exiting) + if role in ( + controlTypes.ROLE_GROUPING, + controlTypes.ROLE_SECTION, + controlTypes.ROLE_PARAGRAPH, + controlTypes.ROLE_ARTICLE, + controlTypes.ROLE_LANDMARK, + controlTypes.ROLE_REGION, + ): + field['isBlock']=True + ariaProperties = splitUIAElementAttribs( + obj._getUIACacheablePropertyValue(UIAHandler.UIA_AriaPropertiesPropertyId) + ) + # ARIA roledescription and landmarks + field['roleText'] = ariaProperties.get('roledescription') + # provide landmarks + field['landmark']=obj.landmark + # Combo boxes with a text pattern are editable + if obj.role==controlTypes.ROLE_COMBOBOX and obj.UIATextPattern: + field['states'].add(controlTypes.STATE_EDITABLE) + # report if the field is 'current' + field['current']=obj.isCurrent + if obj.placeholder and obj._isTextEmpty: + field['placeholder']=obj.placeholder + # For certain controls, if ARIA overrides the label, then force the field's content (value) to the label + # Later processing in Edge's getTextWithFields will remove descendant content from fields with a content attribute. + hasAriaLabel = 'label' in ariaProperties + hasAriaLabelledby = 'labelledby' in ariaProperties + if field.get('nameIsContent'): + content="" + field.pop('name',None) + if hasAriaLabel or hasAriaLabelledby: + content=obj.name + if not content: + text=self.obj.makeTextInfo(obj).text + if not text or text.isspace(): + content=obj.name or field.pop('description',None) + if content: + field['content']=content + elif isEmbedded: + field['content']=obj.value + if field['role']==controlTypes.ROLE_GROUPING: + field['role']=controlTypes.ROLE_EMBEDDEDOBJECT + if not obj.value: + field['content']=obj.name + elif hasAriaLabel or hasAriaLabelledby: + field['alwaysReportName'] = True + # Give lists an item count + if obj.role==controlTypes.ROLE_LIST: + child=UIAHandler.handler.clientObject.ControlViewWalker.GetFirstChildElement(obj.UIAElement) + if child: + field['_childcontrolcount']=child.getCurrentPropertyValue(UIAHandler.UIA_SizeOfSetPropertyId) + return field + + def getTextWithFields(self,formatConfig=None): + # We don't want fields for collapsed ranges. + # This would normally be a general rule, but MS Word currently needs fields for collapsed ranges, thus this code is not in the base. + if self.isCollapsed: + return [] + fields=super(EdgeTextInfo,self).getTextWithFields(formatConfig) + seenText=False + curStarts=[] + # remove clickable state on descendants of controls with clickable state + clickableField=None + for field in fields: + if isinstance(field,textInfos.FieldCommand) and field.command=="controlStart": + states=field.field['states'] + if clickableField: + states.discard(controlTypes.STATE_CLICKABLE) + elif controlTypes.STATE_CLICKABLE in states: + clickableField=field.field + elif clickableField and isinstance(field,textInfos.FieldCommand) and field.command=="controlEnd" and field.field is clickableField: + clickableField=None + # Chop extra whitespace off the end incorrectly put there by Edge + numFields=len(fields) + index=0 + while index1 and isinstance(field,str) and field.isspace(): + prevField=fields[index-2] + if isinstance(prevField,textInfos.FieldCommand) and prevField.command=="controlEnd": + del fields[index-1:index+1] + index+=1 + # chop fields off the end incorrectly placed there by Edge + # This can happen if expanding to line covers element start chars at its end + startCount=0 + lastStartIndex=None + numFields=len(fields) + for index in range(numFields-1,-1,-1): + field=fields[index] + if isinstance(field,str): + break + elif isinstance(field,textInfos.FieldCommand) and field.command=="controlStart" and not field.field.get('embedded'): + startCount+=1 + lastStartIndex=index + if lastStartIndex: + del fields[lastStartIndex:lastStartIndex+(startCount*2)] + # Remove any content from fields with a content attribute + numFields=len(fields) + curField=None + for index in range(numFields-1,-1,-1): + field=fields[index] + if not curField and isinstance(field,textInfos.FieldCommand) and field.command=="controlEnd" and field.field.get('content'): + curField=field.field + endIndex=index + elif curField and isinstance(field,textInfos.FieldCommand) and field.command=="controlStart" and field.field is curField: + fields[index+1:endIndex]=" " + curField=None + return fields + + +class EdgeNode(UIA): + def _get_role(self): + role=super(EdgeNode,self).role + if not isinstance(self,EdgeHTMLRoot) and role==controlTypes.ROLE_PANE and self.UIATextPattern: + return controlTypes.ROLE_INTERNALFRAME + ariaRole=self._getUIACacheablePropertyValue(UIAHandler.UIA_AriaRolePropertyId).lower() + # #7333: It is valid to provide multiple, space separated aria roles in HTML + # The role used is the first role in the list that has an associated NVDA role in aria.ariaRolesToNVDARoles + for ariaRole in ariaRole.split(): + newRole=aria.ariaRolesToNVDARoles.get(ariaRole) + if newRole: + role=newRole + break + return role + + def _get_states(self): + states=super(EdgeNode,self).states + if self.role in (controlTypes.ROLE_STATICTEXT,controlTypes.ROLE_GROUPING,controlTypes.ROLE_SECTION,controlTypes.ROLE_GRAPHIC) and self.UIAInvokePattern: + states.add(controlTypes.STATE_CLICKABLE) + return states + + def _get_ariaProperties(self): + return splitUIAElementAttribs(self.UIAElement.currentAriaProperties) + + # RegEx to get the value for the aria-current property. This will be looking for a the value of 'current' + # in a list of strings like "something=true;current=date;". We want to capture one group, after the '=' + # character and before the ';' character. + # This could be one of: "false", "true", "page", "step", "location", "date", "time" + # "false" is ignored by the regEx and will not produce a match + RE_ARIA_CURRENT_PROP_VALUE = re.compile("current=(?!false)(\w+);") + + def _get_isCurrent(self) -> controlTypes.IsCurrent: + ariaProperties=self._getUIACacheablePropertyValue(UIAHandler.UIA_AriaPropertiesPropertyId) + match = self.RE_ARIA_CURRENT_PROP_VALUE.search(ariaProperties) + if match: + valueOfAriaCurrent = match.group(1) + try: + return controlTypes.IsCurrent(valueOfAriaCurrent) + except ValueError: + log.debugWarning( + f"Unknown aria-current value: {valueOfAriaCurrent}, ariaProperties: {ariaProperties}" + ) + return controlTypes.IsCurrent.NO + + def _get_roleText(self): + roleText = self.ariaProperties.get('roledescription', None) + if roleText: + return roleText + return super().roleText + + def _get_placeholder(self): + ariaPlaceholder = self.ariaProperties.get('placeholder', None) + return ariaPlaceholder + + def _get_landmark(self): + landmarkId=self._getUIACacheablePropertyValue(UIAHandler.UIA_LandmarkTypePropertyId) + if not landmarkId: # will be 0 for non-landmarks + return None + landmarkRole = UIAHandler.UIALandmarkTypeIdsToLandmarkNames.get(landmarkId) + if landmarkRole: + return landmarkRole + ariaRoles=self._getUIACacheablePropertyValue(UIAHandler.UIA_AriaRolePropertyId).lower() + # #7333: It is valid to provide multiple, space separated aria roles in HTML + # If multiple roles or even multiple landmark roles are provided, the first one is used + ariaRole = ariaRoles.split(" ")[0] + if ariaRole in aria.landmarkRoles and (ariaRole != 'region' or self.name): + return ariaRole + return None + + +class EdgeList(EdgeNode): + + # non-focusable lists are readonly lists (ensures correct NVDA presentation category) + def _get_states(self): + states=super(EdgeList,self).states + if controlTypes.STATE_FOCUSABLE not in states: + states.add(controlTypes.STATE_READONLY) + return states + + +class EdgeHeadingQuickNavItem(UIATextRangeQuickNavItem): + + @property + def level(self): + if not hasattr(self,'_level'): + styleVal=getUIATextAttributeValueFromRange(self.textInfo._rangeObj,UIAHandler.UIA_StyleIdAttributeId) + self._level=styleVal-(UIAHandler.StyleId_Heading1-1) if UIAHandler.StyleId_Heading1<=styleVal<=UIAHandler.StyleId_Heading6 else None + return self._level + + def isChild(self,parent): + return self.level>parent.level + + +def EdgeHeadingQuicknavIterator(itemType,document,position,direction="next"): + """ + A helper for L{EdgeHTMLTreeInterceptor._iterNodesByType} that specifically yields L{EdgeHeadingQuickNavItem} objects found in the given document, starting the search from the given position, searching in the given direction. + See L{browseMode._iterNodesByType} for details on these specific arguments. + """ + # Edge exposes all headings as UIA elements with a controlType of text, and a level. Thus we can quickly search for these. + # However, sometimes when ARIA is used, the level on the element may not match the level in the text attributes. + # Therefore we need to search for all levels 1 through 6, even if a specific level is specified. + # Though this is still much faster than searching text attributes alone + # #9078: this must be wrapped inside a list, as Python 3 will treat this as iteration. + levels=list(range(1,7)) + condition=createUIAMultiPropertyCondition({UIAHandler.UIA_ControlTypePropertyId:UIAHandler.UIA_TextControlTypeId,UIAHandler.UIA_LevelPropertyId:levels}) + levelString=itemType[7:] + for item in UIAControlQuicknavIterator(itemType,document,position,condition,direction=direction,itemClass=EdgeHeadingQuickNavItem): + # Verify this is the correct heading level via text attributes + if item.level and (not levelString or levelString==str(item.level)): + yield item + + +class EdgeHTMLTreeInterceptor(cursorManager.ReviewCursorManager,UIABrowseModeDocument): + def makeTextInfo(self,position): + try: + return super().makeTextInfo(position) + except RuntimeError as e: + # sometimes the stored TextRange we have for the caret/selection can die if the page mutates too much. + # Therefore, if we detect this, just give back the first position in the document, updating our stored version as we go. + if position in (textInfos.POSITION_SELECTION,textInfos.POSITION_CARET): + log.debugWarning("%s died. Using first position instead."%position) + info=self.makeTextInfo(textInfos.POSITION_FIRST) + self._selection=info + return info + raise e + + def shouldPassThrough(self,obj,reason=None): + # Enter focus mode for selectable list items ( and role=listbox) if ( - reason == controlTypes.OutputReason.FOCUS - and obj.role == controlTypes.ROLE_LISTITEM - and controlTypes.STATE_SELECTABLE in obj.states + reason == controlTypes.OutputReason.FOCUS + and obj.role == controlTypes.ROLE_LISTITEM + and controlTypes.STATE_SELECTABLE in obj.states ): return True return super().shouldPassThrough(obj, reason=reason) - def _iterNodesByType(self,nodeType,direction="next",pos=None): + def _iterNodesByType(self, nodeType, direction="next", pos=None): if nodeType.startswith("heading"): - return HeadingControlQuicknavIterator(nodeType,self,pos,direction=direction) + return HeadingControlQuicknavIterator(nodeType, self, pos, direction=direction) else: - return super()._iterNodesByType(nodeType,direction=direction,pos=pos) - + return super()._iterNodesByType(nodeType, direction=direction, pos=pos) From 0032d5ff3c9284714c79ab505b776623a9dd1f09 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Fri, 1 May 2020 15:59:37 +0200 Subject: [PATCH 040/174] Add chromium module Use chromium treeinterceptor Use Web module for chromium module Use chromium.ChromiumUIADocument in findOverlayClasses --- source/NVDAObjects/UIA/__init__.py | 11 +++++++++++ source/NVDAObjects/UIA/chromium.py | 31 ++++++++++++++++++++++++++++++ source/_UIAHandler.py | 10 +++++++--- 3 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 source/NVDAObjects/UIA/chromium.py diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index 5cc3d876cd6..541b45d1f38 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -903,6 +903,17 @@ def findOverlayClasses(self,clsList): clsList.append(spartanEdge.EdgeList) else: clsList.append(spartanEdge.EdgeNode) + elif self.windowClassName == "Chrome_RenderWidgetHostHWND": + from . import chromium + if ( + self.UIATextPattern + and self.role == controlTypes.ROLE_DOCUMENT + and self.parent + and self.parent.role == controlTypes.ROLE_PANE + ): + clsList.append(chromium.ChromiumUIADocument) + else: + clsList.append(chromium.ChromiumUIA) elif self.role == controlTypes.ROLE_DOCUMENT and UIAAutomationId == "Microsoft.Windows.PDF.DocumentView": # PDFs from . import spartanEdge diff --git a/source/NVDAObjects/UIA/chromium.py b/source/NVDAObjects/UIA/chromium.py new file mode 100644 index 00000000000..38ea65396b8 --- /dev/null +++ b/source/NVDAObjects/UIA/chromium.py @@ -0,0 +1,31 @@ +# 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 NV Access limited, Leonard de Ruijter + +import UIAHandler +from . import web +import controlTypes + +""" +This module provides UIA behaviour specific to the chromium family of browsers. +Note this is more specialised than UIA.web but less so than browser specific modules such as UIA.spartan_edge +or UIA.anaheim_edge. +""" + + +class ChromiumUIA(web.UIAWeb): + ... + + +class ChromiumUIATreeInterceptor(web.UIAWebTreeInterceptor): + + def _get_documentConstantIdentifier(self): + return self.rootNVDAObject.parent._getUIACacheablePropertyValue(UIAHandler.UIA_AutomationIdPropertyId) + + +class ChromiumUIADocument(ChromiumUIA): + treeInterceptorClass = ChromiumUIATreeInterceptor + + def _get_shouldCreateTreeInterceptor(self): + return self.role == controlTypes.ROLE_DOCUMENT diff --git a/source/_UIAHandler.py b/source/_UIAHandler.py index cbc465c5926..68ba4ca4ca6 100644 --- a/source/_UIAHandler.py +++ b/source/_UIAHandler.py @@ -69,9 +69,6 @@ "Button", # #8944: The Foxit UIA implementation is incomplete and should not be used for now. "FoxitDocWnd", - # All Chromium implementations (including Edge) should not be UIA, - # As their IA2 implementation is still better at the moment. - "Chrome_RenderWidgetHostHWND", ] # #8405: used to detect UIA dialogs prior to Windows 10 RS5. @@ -644,6 +641,9 @@ def IUIAutomationNotificationEventHandler_HandleNotificationEvent( return eventHandler.queueEvent("UIA_notification",obj, notificationKind=NotificationKind, notificationProcessing=NotificationProcessing, displayString=displayString, activityId=activityId) + def _allowUiaInChromium(self): + return True # todo: config.conf['UIA']['allowInChromium'] + def _isBadUIAWindowClassName(self, windowClass): "Given a windowClassName, returns True if this is a known problematic UIA implementation." # #7497: Windows 10 Fall Creators Update has an incomplete UIA @@ -653,6 +653,10 @@ def _isBadUIAWindowClassName(self, windowClass): # events. if windowClass == "ConsoleWindowClass" and config.conf['UIA']['winConsoleImplementation'] != "UIA": return True + # Unless explicitly allowed, all Chromium implementations (including Edge) should not be UIA, + # As their IA2 implementation is still better at the moment. + elif windowClass == "Chrome_RenderWidgetHostHWND" and not self._allowUiaInChromium(): + return True return windowClass in badUIAWindowClassNames def _isUIAWindowHelper(self,hwnd): From 833ab785b26c1b72d48c9ad0ea6e6ee73e738529 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Tue, 3 Nov 2020 17:18:28 +1000 Subject: [PATCH 041/174] Fix freeze in Chromium with UIA Trade off: slight degradation of performance. UIATextInfo._getTextWithFieldsForUIARange: Test each child to see if it is clipped by or lives completely outside the parent text range, and if so appropriately clip the child range and break out of the for loop. Previously these checks only happened on the final child to try and avoid extra calls, but Edge sometimes returns many more children then it should when on the end of a list, and a trivial slow down is much preferred over a 20 second freeze. --- source/NVDAObjects/UIA/__init__.py | 41 ++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index 541b45d1f38..e9ae0db292c 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -647,21 +647,33 @@ def _getTextWithFieldsForUIARange(self,rootElement,textRange,formatConfig,includ if debug: log.debug("NULL childRange. Skipping") continue - clippedStart=clippedEnd=False - if index==lastChildIndex and childRange.CompareEndpoints(UIAHandler.TextPatternRangeEndpoint_Start,textRange,UIAHandler.TextPatternRangeEndpoint_End)>=0: + clippedStart = False + clippedEnd = False + if childRange.CompareEndpoints( + UIAHandler.TextPatternRangeEndpoint_Start, + textRange, + UIAHandler.TextPatternRangeEndpoint_End + ) >= 0: if debug: log.debug("Child at or past end of textRange. Breaking") break - if index==lastChildIndex: - lastChildEndDelta=childRange.CompareEndpoints(UIAHandler.TextPatternRangeEndpoint_End,textRange,UIAHandler.TextPatternRangeEndpoint_End) - if lastChildEndDelta>0: - if debug: - log.debug( - "textRange ended part way through the child. " - "Crop end of childRange to fit" - ) - childRange.MoveEndpointByRange(UIAHandler.TextPatternRangeEndpoint_End,textRange,UIAHandler.TextPatternRangeEndpoint_End) - clippedEnd=True + lastChildEndDelta = childRange.CompareEndpoints( + UIAHandler.TextPatternRangeEndpoint_End, + textRange, + UIAHandler.TextPatternRangeEndpoint_End + ) + if lastChildEndDelta > 0: + if debug: + log.debug( + "textRange ended part way through the child. " + "Crop end of childRange to fit" + ) + childRange.MoveEndpointByRange( + UIAHandler.TextPatternRangeEndpoint_End, + textRange, + UIAHandler.TextPatternRangeEndpoint_End + ) + clippedEnd = True childStartDelta=childRange.CompareEndpoints(UIAHandler.TextPatternRangeEndpoint_Start,tempRange,UIAHandler.TextPatternRangeEndpoint_End) if childStartDelta>0: # plain text before this child @@ -914,7 +926,10 @@ def findOverlayClasses(self,clsList): clsList.append(chromium.ChromiumUIADocument) else: clsList.append(chromium.ChromiumUIA) - elif self.role == controlTypes.ROLE_DOCUMENT and UIAAutomationId == "Microsoft.Windows.PDF.DocumentView": + elif ( + self.role == controlTypes.ROLE_DOCUMENT + and self.UIAElement.cachedAutomationId == "Microsoft.Windows.PDF.DocumentView" + ): # PDFs from . import spartanEdge clsList.append(spartanEdge.EdgeHTMLRoot) From 65c5b5914d921e20129d4906bf8b7f8218cda1cb Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Tue, 3 Nov 2020 17:23:22 +1000 Subject: [PATCH 042/174] Fix freeze in Chromium with UIA Trade-off: only the first line is fetched. speech.getObjectSpeech: If the given object should have its text presented (E.g. a document or edit field) and the line at the selection cannot be fetched, then only present the first line. Previously the text for the entire object would be fetched, which in the case of Edgium, was extremely costly and could cause a freeze of 10 seconds or more. --- source/speech/__init__.py | 40 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/source/speech/__init__.py b/source/speech/__init__.py index 41d92821c6b..f89bcb11d91 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -526,31 +526,23 @@ def getObjectSpeech( # noqa: C901 if shouldReportTextContent: try: info = obj.makeTextInfo(textInfos.POSITION_SELECTION) - if not info.isCollapsed: - # if there is selected text, then there is a value and we do not report placeholder - sequence.extend(getPreselectedTextSpeech(info.text)) - else: - info.expand(textInfos.UNIT_LINE) - textEmpty, placeholderSeq = _getPlaceholderSpeechIfTextEmpty(obj, reason) - sequence.extend(placeholderSeq) - speechGen = getTextInfoSpeech( - info, - unit=textInfos.UNIT_LINE, - reason=OutputReason.CARET - ) - sequence.extend(_flattenNestedSequences(speechGen)) - except: # noqa E722 legacy bare except. Unknown what exceptions may be raised. - newInfo = obj.makeTextInfo(textInfos.POSITION_ALL) + except NotImplementedError: + info = None + if info and not info.isCollapsed: + # if there is selected text, then there is a value and we do not report placeholder + sequence.extend(getPreselectedTextSpeech(info.text)) + else: + if not info: + info = obj.makeTextInfo(textInfos.POSITION_FIRST) + info.expand(textInfos.UNIT_LINE) textEmpty, placeholderSeq = _getPlaceholderSpeechIfTextEmpty(obj, reason) - if textEmpty: - sequence.extend(placeholderSeq) - else: - speechGen = getTextInfoSpeech( - newInfo, - unit=textInfos.UNIT_PARAGRAPH, - reason=OutputReason.CARET, - ) - sequence.extend(_flattenNestedSequences(speechGen)) + sequence.extend(placeholderSeq) + speechGen = getTextInfoSpeech( + info, + unit=textInfos.UNIT_LINE, + reason=OutputReason.CARET, + ) + sequence.extend(_flattenNestedSequences(speechGen)) elif role == controlTypes.ROLE_MATH: import mathPres mathPres.ensureInit() From 9c825b6bc45f62c67fc316245f9fc3214cd99089 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 5 Nov 2020 09:55:33 +1000 Subject: [PATCH 043/174] Fix duplicate heading announcement Chromium with UIA As headings are exposed in the element tree, don't expose them in format fields. --- source/NVDAObjects/UIA/chromium.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/source/NVDAObjects/UIA/chromium.py b/source/NVDAObjects/UIA/chromium.py index 38ea65396b8..7b7706514e8 100644 --- a/source/NVDAObjects/UIA/chromium.py +++ b/source/NVDAObjects/UIA/chromium.py @@ -14,8 +14,22 @@ """ +class ChromiumUIATextInfo(web.UIAWebTextInfo): + + def _getFormatFieldAtRange(self, textRange, formatConfig, ignoreMixedValues=False): + formatField = super()._getFormatFieldAtRange(textRange, formatConfig, ignoreMixedValues=ignoreMixedValues) + # Headings are also exposed in the element tree, + # And therefore exposing in a formatField is redundant and causes duplicate reporting. + # So remove heading-level from the formatField if it exists. + try: + del formatField.field['heading-level'] + except KeyError: + pass + return formatField + + class ChromiumUIA(web.UIAWeb): - ... + _TextInfo = ChromiumUIATextInfo class ChromiumUIATreeInterceptor(web.UIAWebTreeInterceptor): From 6809fc65639eeb14427b59af5f05ce01b27802fc Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 5 Nov 2020 09:59:50 +1000 Subject: [PATCH 044/174] Fix freeze on checkboxes for chromium with UIA UIATextInfo._getTextwithFieldsForUIARange: skip children that appear completely before the start of the parent textRange. Stops a freeze on some checkboxes in Edgium and is generally safer. --- source/NVDAObjects/UIA/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index e9ae0db292c..5a0af2d7be3 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -649,6 +649,14 @@ def _getTextWithFieldsForUIARange(self,rootElement,textRange,formatConfig,includ continue clippedStart = False clippedEnd = False + if childRange.CompareEndpoints( + UIAHandler.TextPatternRangeEndpoint_End, + textRange, + UIAHandler.TextPatternRangeEndpoint_Start + ) <= 0: + if debug: + log.debug("Child completely before textRange. Skipping") + continue if childRange.CompareEndpoints( UIAHandler.TextPatternRangeEndpoint_Start, textRange, From 50a3097798e18b3b6bc7eb288478446735813580 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 5 Nov 2020 10:01:01 +1000 Subject: [PATCH 045/174] Fix list presentation while in browse mode for Chromium with UIA Make sure interactive / presentational lists are presented correctly in browseMode in Edgium. --- source/NVDAObjects/UIA/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index 5a0af2d7be3..b6bd3ba9e8d 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -925,6 +925,7 @@ def findOverlayClasses(self,clsList): clsList.append(spartanEdge.EdgeNode) elif self.windowClassName == "Chrome_RenderWidgetHostHWND": from . import chromium + from . import web if ( self.UIATextPattern and self.role == controlTypes.ROLE_DOCUMENT @@ -933,6 +934,8 @@ def findOverlayClasses(self,clsList): ): clsList.append(chromium.ChromiumUIADocument) else: + if self.role == controlTypes.ROLE_LIST: + clsList.append(web.List) clsList.append(chromium.ChromiumUIA) elif ( self.role == controlTypes.ROLE_DOCUMENT From 8d90a619f3894d848d076b55310d1490397f32bf Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 5 Nov 2020 15:08:49 +1000 Subject: [PATCH 046/174] Fix overriding labels when embedded object chars present for Chromium with UIA Embedded object characters in Edgium should not stop overriding labels from being used. --- source/NVDAObjects/UIA/web.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/source/NVDAObjects/UIA/web.py b/source/NVDAObjects/UIA/web.py index f0c2c6acf8d..70fc013d675 100644 --- a/source/NVDAObjects/UIA/web.py +++ b/source/NVDAObjects/UIA/web.py @@ -82,9 +82,15 @@ def _get_UIAElementAtStartWithReplacedContent(self): whose name has been overridden by the author (such as aria-label). """ element = self.UIAElementAtStart - condition = createUIAMultiPropertyCondition({ - UIAHandler.UIA_ControlTypePropertyId: self.UIAControlTypesWhereNameIsContent - }) + condition = createUIAMultiPropertyCondition( + { + UIAHandler.UIA_ControlTypePropertyId: self.UIAControlTypesWhereNameIsContent + }, + { + UIAHandler.UIA_ControlTypePropertyId: UIAHandler.UIA_ListControlTypeId, + UIAHandler.UIA_IsKeyboardFocusablePropertyId: True, + } + ) # A part from the condition given, we must always match on the root of the document # so we know when to stop walking runtimeID = VARIANT() @@ -101,10 +107,19 @@ def _get_UIAElementAtStartWithReplacedContent(self): ) walker = UIAHandler.handler.clientObject.createTreeWalker(condition) cacheRequest = UIAHandler.handler.clientObject.createCacheRequest() + cacheRequest.addProperty(UIAHandler.UIA_ControlTypePropertyId) + cacheRequest.addProperty(UIAHandler.UIA_IsKeyboardFocusablePropertyId) cacheRequest.addProperty(UIAHandler.UIA_NamePropertyId) cacheRequest.addProperty(UIAHandler.UIA_AriaPropertiesPropertyId) element = walker.normalizeElementBuildCache(element, cacheRequest) while element and not UIAHandler.handler.clientObject.compareElements(element, self.obj.UIAElement): + # Interactive lists + controlType = element.getCachedPropertyValue(UIAHandler.UIA_ControlTypePropertyId) + if controlType == UIAHandler.UIA_ListControlTypeId: + isFocusable = element.getCachedPropertyValue(UIAHandler.UIA_IsKeyboardFocusablePropertyId) + if isFocusable: + return element + # Nodes with an aria label or labelledby attribute name = element.getCachedPropertyValue(UIAHandler.UIA_NamePropertyId) if name: ariaProperties = element.getCachedPropertyValue(UIAHandler.UIA_AriaPropertiesPropertyId) @@ -221,6 +236,10 @@ def _getControlFieldForObject(self, obj, isEmbedded=False, startOfNode=False, en content = obj.name if not content: text = self.obj.makeTextInfo(obj).text + # embedded object characters (which can appear in Edgium) + # should also be treated as whitespace + # allowing to be replaced by an overridden label + text = text.replace('\ufffc', '') if not text or text.isspace(): content = obj.name or field.pop('description', None) if content: From 56eb0680be5e2560950ec330ca740d837f72ef4d Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Tue, 15 Dec 2020 16:54:01 +1000 Subject: [PATCH 047/174] Fix identification of layout tables for Chromium with UIA Allows ignoring of layout tables when configured to do so. --- source/NVDAObjects/UIA/chromium.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/source/NVDAObjects/UIA/chromium.py b/source/NVDAObjects/UIA/chromium.py index 7b7706514e8..9fdbe8b7ad2 100644 --- a/source/NVDAObjects/UIA/chromium.py +++ b/source/NVDAObjects/UIA/chromium.py @@ -27,6 +27,19 @@ def _getFormatFieldAtRange(self, textRange, formatConfig, ignoreMixedValues=Fals pass return formatField + def _getControlFieldForObject(self, obj, isEmbedded=False, startOfNode=False, endOfNode=False): + field = super()._getControlFieldForObject( + obj, + isEmbedded=isEmbedded, + startOfNode=startOfNode, + endOfNode=endOfNode + ) + # Layout tables do not have the UIA table pattern + if field['role'] == controlTypes.ROLE_TABLE: + if not obj._getUIACacheablePropertyValue(UIAHandler.UIA_IsTablePatternAvailablePropertyId): + field['table-layout'] = True + return field + class ChromiumUIA(web.UIAWeb): _TextInfo = ChromiumUIATextInfo From 6c676a8d4876bac196c243e213d1186df7185550 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Fri, 18 Dec 2020 12:19:28 +1000 Subject: [PATCH 048/174] Fix missing value for comboboxes for Chromium with UIA Ensure the value of comboboxes are used as content. --- source/NVDAObjects/UIA/chromium.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source/NVDAObjects/UIA/chromium.py b/source/NVDAObjects/UIA/chromium.py index 9fdbe8b7ad2..248a698d0ed 100644 --- a/source/NVDAObjects/UIA/chromium.py +++ b/source/NVDAObjects/UIA/chromium.py @@ -34,6 +34,9 @@ def _getControlFieldForObject(self, obj, isEmbedded=False, startOfNode=False, en startOfNode=startOfNode, endOfNode=endOfNode ) + # use the value of comboboxes as content. + if obj.role == controlTypes.ROLE_COMBOBOX: + field['content'] = obj.value # Layout tables do not have the UIA table pattern if field['role'] == controlTypes.ROLE_TABLE: if not obj._getUIACacheablePropertyValue(UIAHandler.UIA_IsTablePatternAvailablePropertyId): From 0defba76186b02550783c782a4f7b84b2370fc17 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Tue, 2 Feb 2021 11:04:49 +0800 Subject: [PATCH 049/174] Fix missing control names for Chromium with UIA Names of controls (that do not use their name as content) are always reported: Currently no way to tell if author has explicitly set name. Therefore always report the name if the control is not of a type that by definition uses its name for content. This may cause some duplicate speaking, but that is currently better than nothing at all. --- source/NVDAObjects/UIA/chromium.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/source/NVDAObjects/UIA/chromium.py b/source/NVDAObjects/UIA/chromium.py index 248a698d0ed..6d8ea2ee5d1 100644 --- a/source/NVDAObjects/UIA/chromium.py +++ b/source/NVDAObjects/UIA/chromium.py @@ -41,6 +41,13 @@ def _getControlFieldForObject(self, obj, isEmbedded=False, startOfNode=False, en if field['role'] == controlTypes.ROLE_TABLE: if not obj._getUIACacheablePropertyValue(UIAHandler.UIA_IsTablePatternAvailablePropertyId): field['table-layout'] = True + # Currently no way to tell if author has explicitly set name. + # Therefore always report the name if the control is not of a type that + # by definition uses its name for content. + # this may cause some duplicate speaking, + # But that is currently better than nothing at all. + if not field.get('nameIsContent') and field.get('name'): + field['alwaysReportName'] = True return field From 6a00a716d0d1f2f1d76b76c14662b0274669960c Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Fri, 1 May 2020 15:55:24 +0200 Subject: [PATCH 050/174] Add user config for UIA with Chromium --- source/_UIAHandler.py | 41 ++++++++++++++++++++++++++++------- source/config/configSpec.py | 2 ++ source/gui/settingsDialogs.py | 24 ++++++++++++++++++++ user_docs/en/userGuide.t2t | 10 +++++++++ 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/source/_UIAHandler.py b/source/_UIAHandler.py index 68ba4ca4ca6..30deaa41514 100644 --- a/source/_UIAHandler.py +++ b/source/_UIAHandler.py @@ -6,6 +6,8 @@ from ctypes import * from ctypes.wintypes import * +from enum import Enum + import comtypes.client from comtypes.automation import VT_EMPTY from comtypes import COMError @@ -204,6 +206,21 @@ for id in UIAEventIdsToNVDAEventNames.keys(): ignoreWinEventsMap[id] = [0] + +class AllowUiaInChromium(Enum): + _DEFAULT = 0 # maps to 'when necessary' + WHEN_NECESSARY = 1 # the current default + YES = 2 + NO = 3 + + @staticmethod + def getConfig() -> 'AllowUiaInChromium': + allow = AllowUiaInChromium(config.conf['UIA']['allowInChromium']) + if allow == AllowUiaInChromium._DEFAULT: + return AllowUiaInChromium.WHEN_NECESSARY + return allow + + class UIAHandler(COMObject): _com_interfaces_ = [ UIA.IUIAutomationEventHandler, @@ -641,9 +658,6 @@ def IUIAutomationNotificationEventHandler_HandleNotificationEvent( return eventHandler.queueEvent("UIA_notification",obj, notificationKind=NotificationKind, notificationProcessing=NotificationProcessing, displayString=displayString, activityId=activityId) - def _allowUiaInChromium(self): - return True # todo: config.conf['UIA']['allowInChromium'] - def _isBadUIAWindowClassName(self, windowClass): "Given a windowClassName, returns True if this is a known problematic UIA implementation." # #7497: Windows 10 Fall Creators Update has an incomplete UIA @@ -653,10 +667,6 @@ def _isBadUIAWindowClassName(self, windowClass): # events. if windowClass == "ConsoleWindowClass" and config.conf['UIA']['winConsoleImplementation'] != "UIA": return True - # Unless explicitly allowed, all Chromium implementations (including Edge) should not be UIA, - # As their IA2 implementation is still better at the moment. - elif windowClass == "Chrome_RenderWidgetHostHWND" and not self._allowUiaInChromium(): - return True return windowClass in badUIAWindowClassNames def _isUIAWindowHelper(self,hwnd): @@ -706,15 +716,30 @@ def _isUIAWindowHelper(self,hwnd): # Builds of MS Office 2016 before build 9000 or so had bugs which we cannot work around. # And even current builds of Office 2016 are still missing enough info from UIA that it is still impossible to switch to UIA completely. # Therefore, if we can inject in-process, refuse to use UIA and instead fall back to the MS Word object model. + canUseOlderInProcessApproach = bool(appModule.helperLocalBindingHandle) if ( # An MS Word document window windowClass=="_WwG" # Disabling is only useful if we can inject in-process (and use our older code) - and appModule.helperLocalBindingHandle + and canUseOlderInProcessApproach # Allow the user to explisitly force UIA support for MS Word documents no matter the Office version and not config.conf['UIA']['useInMSWordWhenAvailable'] ): return False + # Unless explicitly allowed, all Chromium implementations (including Edge) should not be UIA, + # As their IA2 implementation is still better at the moment. + elif ( + windowClass == "Chrome_RenderWidgetHostHWND" + and ( + AllowUiaInChromium.getConfig() == AllowUiaInChromium.NO + # Disabling is only useful if we can inject in-process (and use our older code) + or ( + canUseOlderInProcessApproach + and AllowUiaInChromium.getConfig() != AllowUiaInChromium.YES # Users can prefer to use UIA + ) + ) + ): + return False return bool(res) def isUIAWindow(self,hwnd): diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 97f5f284582..54dc5a49c6f 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -221,6 +221,8 @@ useInMSWordWhenAvailable = boolean(default=false) winConsoleImplementation= option("auto", "legacy", "UIA", default="auto") selectiveEventRegistration = boolean(default=false) + # 0:default, 1:Only when necessary, 2:yes, 3:no + allowInChromium = integer(0, 3, default=0) [terminals] speakPasswords = boolean(default=false) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index c967f74cd52..d28bad112c4 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2563,6 +2563,27 @@ def __init__(self, parent): self.winConsoleSpeakPasswordsCheckBox.SetValue(config.conf["terminals"]["speakPasswords"]) self.winConsoleSpeakPasswordsCheckBox.defaultValue = self._getDefaultValue(["terminals", "speakPasswords"]) + label = pgettext( + "advanced.uiaWithChromium", + # Translators: Label for the Use UIA with Chromium combobox, in the Advanced settings panel. + # Note the '\n' is used to split this long label approximately in half. + "Use UIA with Microsoft Edge and other \n&Chromium based browsers when available:" + ) + chromiumChoices = ( + # Translators: Label for the default value of the Use UIA with Chromium combobox, + # in the Advanced settings panel. + pgettext("advanced.uiaWithChromium", "Default (Only when necessary)"), + # Translators: Label for a value in the Use UIA with Chromium combobox, in the Advanced settings panel. + pgettext("advanced.uiaWithChromium", "Only when necessary"), + # Translators: Label for a value in the Use UIA with Chromium combobox, in the Advanced settings panel. + pgettext("advanced.uiaWithChromium", "Yes"), + # Translators: Label for a value in the Use UIA with Chromium combobox, in the Advanced settings panel. + pgettext("advanced.uiaWithChromium", "No"), + ) + self.UIAInChromiumCombo = UIAGroup.addLabeledControl(label, wx.Choice, choices=chromiumChoices) + self.UIAInChromiumCombo.SetSelection(config.conf["UIA"]["allowInChromium"]) + self.UIAInChromiumCombo.defaultValue = self._getDefaultValue(["UIA", "allowInChromium"]) + # Translators: This is the label for a group of advanced options in the # Advanced settings panel label = _("Terminal programs") @@ -2735,6 +2756,7 @@ def haveConfigDefaultsBeenRestored(self): and self.ConsoleUIACheckBox.IsChecked() == (self.ConsoleUIACheckBox.defaultValue == 'UIA') and self.winConsoleSpeakPasswordsCheckBox.IsChecked() == self.winConsoleSpeakPasswordsCheckBox.defaultValue and self.cancelExpiredFocusSpeechCombo.GetSelection() == self.cancelExpiredFocusSpeechCombo.defaultValue + and self.UIAInChromiumCombo.selection == self.UIAInChromiumCombo.defaultValue and self.keyboardSupportInLegacyCheckBox.IsChecked() == self.keyboardSupportInLegacyCheckBox.defaultValue and self.diffAlgoCombo.GetSelection() == self.diffAlgoCombo.defaultValue and self.caretMoveTimeoutSpinControl.GetValue() == self.caretMoveTimeoutSpinControl.defaultValue @@ -2747,6 +2769,7 @@ def restoreToDefaults(self): self.selectiveUIAEventRegistrationCheckBox.SetValue(self.selectiveUIAEventRegistrationCheckBox.defaultValue) self.UIAInMSWordCheckBox.SetValue(self.UIAInMSWordCheckBox.defaultValue) self.ConsoleUIACheckBox.SetValue(self.ConsoleUIACheckBox.defaultValue == 'UIA') + self.UIAInChromiumCombo.SetSelection(self.UIAInChromiumCombo.defaultValue) self.winConsoleSpeakPasswordsCheckBox.SetValue(self.winConsoleSpeakPasswordsCheckBox.defaultValue) self.cancelExpiredFocusSpeechCombo.SetSelection(self.cancelExpiredFocusSpeechCombo.defaultValue) self.keyboardSupportInLegacyCheckBox.SetValue(self.keyboardSupportInLegacyCheckBox.defaultValue) @@ -2766,6 +2789,7 @@ def onSave(self): config.conf['UIA']['winConsoleImplementation'] = "auto" config.conf["terminals"]["speakPasswords"] = self.winConsoleSpeakPasswordsCheckBox.IsChecked() config.conf["featureFlag"]["cancelExpiredFocusSpeech"] = self.cancelExpiredFocusSpeechCombo.GetSelection() + config.conf["UIA"]["allowInChromium"] = self.UIAInChromiumCombo.GetSelection() config.conf["terminals"]["keyboardSupportInLegacy"]=self.keyboardSupportInLegacyCheckBox.IsChecked() diffAlgoChoice = self.diffAlgoCombo.GetSelection() config.conf['terminals']['diffAlgo'] = ( diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 95c25fd8b99..7dab14755e4 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -1840,6 +1840,16 @@ When this option is enabled, NVDA will use a new, work in progress version of it ==== Speak passwords in UIA consoles ====[AdvancedSettingsWinConsoleSpeakPasswords] This setting controls whether characters are spoken by [speak typed characters #KeyboardSettingsSpeakTypedCharacters] or [speak typed words #KeyboardSettingsSpeakTypedWords] in situations where the screen does not update (such as password entry) in the Windows Console with UI automation support enabled. For security purposes, this setting should be left disabled. However, you may wish to enable it if you experience performance issues or instability with typed character and/or word reporting while using NVDA's new experimental console support. +==== Use UIA with Microsoft Edge and other Chromium based browsers when available ====[ChromiumUIA] +Allows specifying when UIA will be used when it is available in Chromium based browsers such as Microsoft Edge. +UIA support for Chromium based browsers is early in development and may not provide the same level of access as IA2. +The combo box has the following options: +- Default (Only when necessary): The NVDA default, currently this is "Only when necessary". This default may change in the future as the technology matures. +- Only when necessary: When NVDA is unable to inject into the browser process in order to use IA2 and UIA is available, then NVDA will fall back to using UIA. +- Yes: If the browser makes UIA available, NVDA will use it. +- No: Don't use UIA, even if NVDA is unable to inject in process. This may be useful for developers debugging issues with IA2 and want to ensure that NVDA does not fall back to UIA. +- + ==== Use the new typed character support in Windows Console when available ====[AdvancedSettingsKeyboardSupportInLegacy] This option enables an alternative method for detecting typed characters in Windows command consoles. While it improves performance and prevents some console output from being spelled out, it may be incompatible with some terminal programs. From 11ba984fd2071d75fc7a9386c3661645cdbb9a82 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 3 Feb 2021 17:02:40 +0800 Subject: [PATCH 051/174] Update changes file for PR #12025 PR #12025 was not squash merged (which is the usual practice for this project). Instead, to preserve the history of moved then modified code, the commits have been rebased directly onto master. --- user_docs/en/changes.t2t | 1 + 1 file changed, 1 insertion(+) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 32d4a2cd172..9867dab33ba 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -6,6 +6,7 @@ What's New in NVDA = 2021.1 = == New Features == +- Early support for UIA with Chromium based browsers (such as Edge). (#12025) == Changes == From 8f31bdc884ec9cf4e9ed0d2ae464401d0aedd1fb Mon Sep 17 00:00:00 2001 From: jakubl7545 <48619364+jakubl7545@users.noreply.github.com> Date: Wed, 3 Feb 2021 22:00:39 +0100 Subject: [PATCH 052/174] Report comments in MS Word with UIA enabled (#9631) * Change condition for filtering comments * Deal with unretrievable date * Split filtering condition to two separate lines * Replace gesture dictionary with script decorator * Adress review requests. * Create helper function * Address linting errors * Address more linting errors * Modify copyright header * Fixes * Correct syntax * Linting * Ensure comments are read in different Office versions. * Linting * Update what's new Co-authored-by: Michael Curran --- source/NVDAObjects/UIA/wordDocument.py | 90 +++++++++++++++----------- user_docs/en/changes.t2t | 1 + 2 files changed, 52 insertions(+), 39 deletions(-) diff --git a/source/NVDAObjects/UIA/wordDocument.py b/source/NVDAObjects/UIA/wordDocument.py index b5df742331c..bccd3d03783 100644 --- a/source/NVDAObjects/UIA/wordDocument.py +++ b/source/NVDAObjects/UIA/wordDocument.py @@ -1,7 +1,7 @@ -# A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. +# A part of NonVisual Desktop Access (NVDA) # See the file COPYING for more details. -# Copyright (C) 2016-2020 NV Access Limited, Joseph Lee +# Copyright (C) 2016-2021 NV Access Limited, Joseph Lee, Jakub Lukowicz from comtypes import COMError from collections import defaultdict @@ -18,6 +18,8 @@ from UIAUtils import * from . import UIA, UIATextInfo from NVDAObjects.window.winword import WordDocument as WordDocumentBase +from scriptHandler import script + """Support for Microsoft Word via UI Automation.""" @@ -67,15 +69,40 @@ def getCommentInfoFromPosition(position): for index in range(UIAElementArray.length): UIAElement=UIAElementArray.getElement(index) UIAElement=UIAElement.buildUpdatedCache(UIAHandler.handler.baseCacheRequest) - obj=UIA(UIAElement=UIAElement) - if not obj.parent or obj.parent.name!='Comment': - continue - comment=obj.makeTextInfo(textInfos.POSITION_ALL).text - dateObj=obj.previous - date=dateObj.name - authorObj=dateObj.previous - author=authorObj.name - return dict(comment=comment,author=author,date=date) + typeID = UIAElement.GetCurrentPropertyValue(UIAHandler.UIA_AnnotationAnnotationTypeIdPropertyId) + # Use Annotation Type Comment if available + if typeID == UIAHandler.AnnotationType_Comment: + comment = UIAElement.GetCurrentPropertyValue(UIAHandler.UIA_NamePropertyId) + author = UIAElement.GetCurrentPropertyValue(UIAHandler.UIA_AnnotationAuthorPropertyId) + date = UIAElement.GetCurrentPropertyValue(UIAHandler.UIA_AnnotationDateTimePropertyId) + return dict(comment=comment, author=author, date=date) + else: + obj = UIA(UIAElement=UIAElement) + if ( + not obj.parent + # Because the name of this object is language sensetive check if it has UIA Annotation Pattern + or not obj.parent.UIAElement.getCurrentPropertyValue( + UIAHandler.UIA_IsAnnotationPatternAvailablePropertyId + ) + ): + continue + comment = obj.makeTextInfo(textInfos.POSITION_ALL).text + tempObj = obj.previous.previous + authorObj = tempObj or obj.previous + author = authorObj.name + if not tempObj: + return dict(comment=comment, author=author) + dateObj = obj.previous + date = dateObj.name + return dict(comment=comment, author=author, date=date) + + +def getPresentableCommentInfoFromPosition(commentInfo): + if "date" not in commentInfo: + # Translators: The message reported for a comment in Microsoft Word + return _("Comment: {comment} by {author}").format(**commentInfo) + # Translators: The message reported for a comment in Microsoft Word + return _("Comment: {comment} by {author} on {date}").format(**commentInfo) class CommentUIATextInfoQuickNavItem(TextAttribUIATextInfoQuickNavItem): attribID=UIAHandler.UIA_AnnotationTypesAttributeId @@ -84,8 +111,7 @@ class CommentUIATextInfoQuickNavItem(TextAttribUIATextInfoQuickNavItem): @property def label(self): commentInfo=getCommentInfoFromPosition(self.textInfo) - # Translators: The message reported for a comment in Microsoft Word - return _("Comment: {comment} by {author} on {date}").format(**commentInfo) + return getPresentableCommentInfoFromPosition(commentInfo) class WordDocumentTextInfo(UIATextInfo): @@ -329,31 +355,17 @@ def event_UIA_notification(self, activityId=None, **kwargs): return super(WordDocument, self).event_UIA_notification(**kwargs) + @script( + gesture="kb:NVDA+alt+c", + # Translators: a description for a script that reports the comment at the caret. + description=_("Reports the text of the comment where the System caret is located.") + ) def script_reportCurrentComment(self,gesture): caretInfo=self.makeTextInfo(textInfos.POSITION_CARET) - caretInfo.expand(textInfos.UNIT_CHARACTER) - val=caretInfo._rangeObj.getAttributeValue(UIAHandler.UIA_AnnotationObjectsAttributeId) - if not val: - return - try: - UIAElementArray=val.QueryInterface(UIAHandler.IUIAutomationElementArray) - except COMError: - return - for index in range(UIAElementArray.length): - UIAElement=UIAElementArray.getElement(index) - UIAElement=UIAElement.buildUpdatedCache(UIAHandler.handler.baseCacheRequest) - obj=UIA(UIAElement=UIAElement) - if not obj.parent or obj.parent.name!='Comment': - continue - comment=obj.makeTextInfo(textInfos.POSITION_ALL).text - dateObj=obj.previous - date=dateObj.name - authorObj=dateObj.previous - author=authorObj.name - # Translators: The message reported for a comment in Microsoft Word - ui.message(_("{comment} by {author} on {date}").format(comment=comment,date=date,author=author)) - return - - __gestures={ - "kb:NVDA+alt+c":"reportCurrentComment", - } + commentInfo = getCommentInfoFromPosition(caretInfo) + if commentInfo is not None: + ui.message(getPresentableCommentInfoFromPosition(commentInfo)) + else: + # Translators: a message when there is no comment to report in Microsoft Word + ui.message(_("No comments")) + return diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 9867dab33ba..4cff155a559 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -24,6 +24,7 @@ What's New in NVDA - In MS Outlook, inappropriate distance reporting when shift+tabbing from the message body to the subject field should not occur anymore. (#10254) - In the Python Console, inserting a tab for indentation at the beginning of a non-empty input line and performing tab-completion in the middle of an input line are now supported. (#11532) - Formatting information and other browseable messages no longer present unexpected blank lines when screen layout is turned off. (#12004) +- It is now possible to read comments in MS Word with UIA enabled. (#9285) == Changes for Developers == From 1392aa3acb3d6b9e042077caad7a3edba3065e08 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Fri, 5 Feb 2021 15:50:56 +0800 Subject: [PATCH 053/174] Add code review checklist for PRs (PR #12037) Co-authored-by: Leonard de Ruijter --- .github/PULL_REQUEST_TEMPLATE.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c1a874b202e..21c99dab42d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -9,7 +9,7 @@ Please also note that the NVDA project has a Citizen and Contributor Code of Con ### Description of how this pull request fixes the issue: -### Testing performed: +### Testing strategy: ### Known issues with pull request: @@ -17,3 +17,16 @@ Please also note that the NVDA project has a Citizen and Contributor Code of Con Section: New features, Changes, Bug fixes +### Code Review Checklist: + +This checklist is a reminder of things commonly forgotten in a new PR. +Please do a self-review to check these items. +Reviewers will not approve the PR until these are met. + +- [ ] Pull Request description is up to date. +- [ ] Unit tests. +- [ ] System (end to end) tests. +- [ ] Manual tests. +- [ ] User Documentation. +- [ ] Change log entry. +- [ ] Context sensitive help for GUI changes. From a73b481769ad324a89c5678740e51d6597e24fb0 Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Mon, 8 Feb 2021 11:17:47 +0100 Subject: [PATCH 054/174] Add missing context help. (PR #12034) * Fixes some missing context help in advanced settings panel. --- source/gui/settingsDialogs.py | 5 +++++ user_docs/en/userGuide.t2t | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index d28bad112c4..f3216b55b24 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2478,6 +2478,9 @@ class AdvancedPanelControls( """Holds the actual controls for the Advanced Settings panel, this allows the state of the controls to be more easily managed. """ + + helpId = "AdvancedSettings" + def __init__(self, parent): super().__init__(parent) self._defaultsRestored = False @@ -2581,6 +2584,7 @@ def __init__(self, parent): pgettext("advanced.uiaWithChromium", "No"), ) self.UIAInChromiumCombo = UIAGroup.addLabeledControl(label, wx.Choice, choices=chromiumChoices) + self.bindHelpEvent("ChromiumUIA", self.UIAInChromiumCombo) self.UIAInChromiumCombo.SetSelection(config.conf["UIA"]["allowInChromium"]) self.UIAInChromiumCombo.defaultValue = self._getDefaultValue(["UIA", "allowInChromium"]) @@ -2628,6 +2632,7 @@ def __init__(self, parent): "difflib" ) self.diffAlgoCombo = terminalsGroup.addLabeledControl(diffAlgoComboText, wx.Choice, choices=diffAlgoChoices) + self.bindHelpEvent("DiffAlgo", self.diffAlgoCombo) curChoice = self.diffAlgoVals.index( config.conf['terminals']['diffAlgo'] ) diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 629292e43d7..ba1579e2b7d 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -1857,7 +1857,7 @@ This feature is available and enabled by default on Windows 10 versions 1607 and Warning: with this option enabled, typed characters that do not appear onscreen, such as passwords, will not be suppressed. In untrusted environments, you may temporarily disable [speak typed characters #KeyboardSettingsSpeakTypedCharacters] and [speak typed words #KeyboardSettingsSpeakTypedWords] when entering passwords. -==== Diff algorithm ====[AdvancedSettingsDiffAlgo] +==== Diff algorithm ====[DiffAlgo] This setting controls how NVDA determines the new text to speak in terminals. The diff algorithm combo box has three options: - Automatic: as of NVDA 2021.1, this option is equivalent to Difflib. From dcf3a32525acdf026f71c0c0fa96de9b5093cd7a Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Tue, 9 Feb 2021 15:15:58 +0800 Subject: [PATCH 055/174] Fix typo in changes doc (PR #12053) --- user_docs/en/changes.t2t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 4cff155a559..0f12bff9bd1 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -114,7 +114,7 @@ Plus many other important bug fixes and improvements. - ARIA treegrids are now exposed as normal tables in browse mode in both Firefox and Chrome. (#9715) - A reverse search can now be initiated with 'find previous' via NVDA+shift+F3 (#11770) - An NVDA script is no longer treated as being repeated if an unrelated key press happens in between the two executions of the script. (#11388) -- Strong and emphasis tags in Internet Explorer can again be suppressed from being reported by returning off Report Emphasis in NVDA's Document Formatting settings. (#11808) +- Strong and emphasis tags in Internet Explorer can again be suppressed from being reported by turning off Report Emphasis in NVDA's Document Formatting settings. (#11808) - A freeze of several seconds experienced by a small amount of users when arrowing between cells in Excel should no longer occur. (#11818) - In Microsoft Teams builds with version numbers like 1.3.00.28xxx, NVDA no longer fails reading messages in chats or Teams channels due to an incorrectly focused menu. (#11821) - Text marked both as being a spelling and grammar error at the same time in Google Chrome will be appropriately announced as both a spelling and grammar error by NVDA. (#11787) From e5d62e3296b6af1b9bc92d9cd6c5d3777b03cb06 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Fri, 12 Feb 2021 14:48:11 +0800 Subject: [PATCH 056/174] Clarify code review checklist instructions (PR #12056) The intent of the checklist is to serve purely as a reminder, checking items communicates that you have considered the item, and have not accidentally forgotten about it. Further, the checklist acts as a reminder for reviewers, unfortunately GitHub does not yet provide a way to create a dedicated code review template. Additionally the wiki page has been adjusted: https://github.com/nvaccess/nvda/wiki/Github-pull-request-template-explanation-and-examples Co-authored-by: Cyrille Bougot --- .github/PULL_REQUEST_TEMPLATE.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 21c99dab42d..d6bd5031b62 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -20,8 +20,10 @@ Section: New features, Changes, Bug fixes ### Code Review Checklist: This checklist is a reminder of things commonly forgotten in a new PR. -Please do a self-review to check these items. -Reviewers will not approve the PR until these are met. +Authors, please do a self-review and confirm you have considered the following items. +Mark items you have considered by checking them. +You can do this when editing the Pull request description with an x: `[ ]` becomes `[x]`. +You can also check the checkboxes after the PR is created. - [ ] Pull Request description is up to date. - [ ] Unit tests. From 8e51a7f825845b69b0a74cdaf341d786a579b9c2 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 18 Feb 2021 13:26:29 +1000 Subject: [PATCH 057/174] Bump for alpha snapshots From 567627fdadbf8aa56826e2f9583370dc2a103375 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 18 Feb 2021 14:23:41 +1000 Subject: [PATCH 058/174] Deploy to deploy.nvaccess.org --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 364c5d87591..fe417bb51c4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -252,7 +252,7 @@ deploy_script: exe=$exe; hash=$hash; artifacts=$artifacts } - ConvertTo-Json -InputObject $data -Compress | ssh nvaccess@exbi.nvaccess.org nvdaAppveyorHook + ConvertTo-Json -InputObject $data -Compress | ssh nvaccess@deploy.nvaccess.org nvdaAppveyorHook # Upload symbols to Mozilla. py -m pip install --no-warn-script-location requests From ec1a5c5ca27ce13ede069cdfe7f68356c27040a3 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 18 Feb 2021 16:52:20 +1000 Subject: [PATCH 059/174] appveyor: save off the deploy json before sending it to NV Access. --- appveyor.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index fe417bb51c4..b638ca5289b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -252,7 +252,9 @@ deploy_script: exe=$exe; hash=$hash; artifacts=$artifacts } - ConvertTo-Json -InputObject $data -Compress | ssh nvaccess@deploy.nvaccess.org nvdaAppveyorHook + ConvertTo-Json -InputObject $data -Compress >deploy.json + Push-AppveyorArtifact deploy.json + ssh nvaccess@deploy.nvaccess.org nvdaAppveyorHook Date: Thu, 18 Feb 2021 17:12:15 +1000 Subject: [PATCH 060/174] appveyor: fix syntax error. --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index b638ca5289b..2188b103e39 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -254,7 +254,7 @@ deploy_script: } ConvertTo-Json -InputObject $data -Compress >deploy.json Push-AppveyorArtifact deploy.json - ssh nvaccess@deploy.nvaccess.org nvdaAppveyorHook Date: Thu, 18 Feb 2021 18:15:32 +1000 Subject: [PATCH 061/174] appveyor: ensure deploy.json is utf8 encoded, not unicode. --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 2188b103e39..779d4a02a98 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -252,7 +252,7 @@ deploy_script: exe=$exe; hash=$hash; artifacts=$artifacts } - ConvertTo-Json -InputObject $data -Compress >deploy.json + ConvertTo-Json -InputObject $data -Compress | Out-File -FilePath deploy.json Push-AppveyorArtifact deploy.json cat deploy.json | ssh nvaccess@deploy.nvaccess.org nvdaAppveyorHook From f2a509a2e4f0b7bd346dedae88a6170964312562 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 18 Feb 2021 18:58:04 +1000 Subject: [PATCH 062/174] appveyor: make ssh verbose to aide in debugging. --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 779d4a02a98..0712adbd6c0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -254,7 +254,7 @@ deploy_script: } ConvertTo-Json -InputObject $data -Compress | Out-File -FilePath deploy.json Push-AppveyorArtifact deploy.json - cat deploy.json | ssh nvaccess@deploy.nvaccess.org nvdaAppveyorHook + cat deploy.json | ssh -v nvaccess@deploy.nvaccess.org nvdaAppveyorHook # Upload symbols to Mozilla. py -m pip install --no-warn-script-location requests From 71c385eae1d7f77c47a0871a690c1142c4c724e2 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Fri, 19 Feb 2021 01:11:32 +1000 Subject: [PATCH 063/174] Deploy via ssh to deploy.nvaccess.org this time with a correct entry in the ssh known_hosts file. --- appveyor.yml | 2 +- appveyor/ssh_known_hosts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 0712adbd6c0..779d4a02a98 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -254,7 +254,7 @@ deploy_script: } ConvertTo-Json -InputObject $data -Compress | Out-File -FilePath deploy.json Push-AppveyorArtifact deploy.json - cat deploy.json | ssh -v nvaccess@deploy.nvaccess.org nvdaAppveyorHook + cat deploy.json | ssh nvaccess@deploy.nvaccess.org nvdaAppveyorHook # Upload symbols to Mozilla. py -m pip install --no-warn-script-location requests diff --git a/appveyor/ssh_known_hosts b/appveyor/ssh_known_hosts index 5d596d692c3..9970fffd9f6 100644 --- a/appveyor/ssh_known_hosts +++ b/appveyor/ssh_known_hosts @@ -1,2 +1 @@ -exbi.nvaccess.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJZYcI3sI+IbIaiUcd5/LIL5+wfEoO+NQ8Gw1Ww9bxwpOyP/HYu/1yzuRNTDSckGM3hqaGGliTW2/3DYOvPDUwI= -45.33.21.113 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJZYcI3sI+IbIaiUcd5/LIL5+wfEoO+NQ8Gw1Ww9bxwpOyP/HYu/1yzuRNTDSckGM3hqaGGliTW2/3DYOvPDUwI= +deploy.nvaccess.org,45.33.23.174 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBZdYWLmrUzPukq3wa15XFFgi2THBc92S9gpvZwrDUDkxNUD5VzRISf2uuFD+cRwn+gwj2jvH77fCFtwqfo4N3w= From eea040691e3db08bc1b74a326861c951b4c67de6 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Mon, 22 Feb 2021 07:47:56 +1000 Subject: [PATCH 064/174] appveyor.yml: add a comment above the call to ssh warning of a freeze if the server address is changed yet known_hosts is not updated. --- appveyor.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 779d4a02a98..52c46ae42c2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -254,6 +254,10 @@ deploy_script: } ConvertTo-Json -InputObject $data -Compress | Out-File -FilePath deploy.json Push-AppveyorArtifact deploy.json + # Execute the deploy script on the NV Access server via ssh. + # Warning: if the server address is changed, + # The new address must be also included in appveyor\ssh_known_hosts in this repo + # Otherwise ssh will freeze on input! cat deploy.json | ssh nvaccess@deploy.nvaccess.org nvdaAppveyorHook # Upload symbols to Mozilla. From e34085257074fc060cbbd965690f2bfdbac9039d Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Tue, 23 Feb 2021 10:49:16 +0800 Subject: [PATCH 065/174] Fix isCurrent errors for Internet Explorer (PR #12066) --- source/NVDAObjects/IAccessible/MSHTML.py | 15 +++++++++++++-- source/virtualBuffers/MSHTML.py | 20 ++++++++++++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/source/NVDAObjects/IAccessible/MSHTML.py b/source/NVDAObjects/IAccessible/MSHTML.py index 16ed0988041..bf7286854a8 100644 --- a/source/NVDAObjects/IAccessible/MSHTML.py +++ b/source/NVDAObjects/IAccessible/MSHTML.py @@ -543,14 +543,25 @@ def _get_treeInterceptorClass(self): return virtualBuffers.MSHTML.MSHTML return super(MSHTML,self).treeInterceptorClass - def _get_isCurrent(self): - isCurrent = self.HTMLAttributes["aria-current"] + def _get_isCurrent(self) -> controlTypes.IsCurrent: + try: + isCurrent = self.HTMLAttributes["aria-current"] + except LookupError: + return controlTypes.IsCurrent.NO + + # key may be in HTMLAttributes with a value of None + if isCurrent is None: + return controlTypes.IsCurrent.NO + try: return controlTypes.IsCurrent(isCurrent) except ValueError: log.debugWarning(f"Unknown aria-current value: {isCurrent}") return controlTypes.IsCurrent.NO + #: Typing for autoproperty _get_HTMLAttributes + HTMLAttributes: HTMLAttribCache + def _get_HTMLAttributes(self): return HTMLAttribCache(self.HTMLNode) diff --git a/source/virtualBuffers/MSHTML.py b/source/virtualBuffers/MSHTML.py index 797c7477de2..74367175bf7 100644 --- a/source/virtualBuffers/MSHTML.py +++ b/source/virtualBuffers/MSHTML.py @@ -46,18 +46,26 @@ def _normalizeFormatField(self, attrs): attrs['language']=languageHandler.normalizeLanguage(language) return attrs - def _normalizeControlField(self,attrs): - level=None - - ariaCurrentValue = attrs.get('HTMLAttrib::aria-current', 'false') + def _getIsCurrentAttribute(self, attrs: dict) -> controlTypes.IsCurrent: + defaultAriaCurrentStringVal = "false" + ariaCurrentValue = attrs.get('HTMLAttrib::aria-current', defaultAriaCurrentStringVal) + # key 'HTMLAttrib::aria-current' may be in attrs with a value of None + ariaCurrentValue = defaultAriaCurrentStringVal if ariaCurrentValue is None else ariaCurrentValue try: ariaCurrent = controlTypes.IsCurrent(ariaCurrentValue) except ValueError: log.debugWarning(f"Unknown aria-current value: {ariaCurrentValue}") ariaCurrent = controlTypes.IsCurrent.NO + return ariaCurrent + + # C901 'MSHTMLTextInfo._normalizeControlField' is too complex (42) + # Look for opportunities to simplify this function. + def _normalizeControlField(self, attrs: dict): # noqa: C901 + level = None - if ariaCurrent != controlTypes.IsCurrent.NO: - attrs['current'] = ariaCurrent + isCurrent = self._getIsCurrentAttribute(attrs) + if isCurrent != controlTypes.IsCurrent.NO: + attrs['current'] = isCurrent placeholder = self._getPlaceholderAttribute(attrs, 'HTMLAttrib::aria-placeholder') if placeholder: From bdc71bacfbe1468324d1b4502d8bac3318a8ecd3 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Tue, 23 Feb 2021 10:51:14 +0800 Subject: [PATCH 066/174] Fix command line length limit reached (PR #12083) The number of files passed to xgettext had grown too great, use a file list instead --- sconstruct | 47 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/sconstruct b/sconstruct index 2824c19d335..41101eba2d6 100755 --- a/sconstruct +++ b/sconstruct @@ -426,9 +426,22 @@ userGuideFile=env.Command(outputDir.File("userGuide.html"),userDocsDir.File('en/ env.Depends(userGuideFile, outputStylesFile) env.Alias('userGuide',userGuideFile) + +def makePotSourceFileList(target, sourceFiles, env): + potSourceFiles = [ + os.path.relpath(str(f), str(sourceDir)) for f in sourceFiles + ] + with open(target.abspath, "w") as fileList: + fileList.writelines([f + '\n' for f in potSourceFiles]) + + def makePot(target, source, env): + potSourceFileList = outputDir.File("potSourceFileList.txt") + makePotSourceFileList(potSourceFileList, source, env) # Generate the pot. - if env.Execute([["cd", sourceDir, "&&", + if env.Execute([ + [ + "cd", sourceDir, "&&", XGETTEXT, "-o", target[0].abspath, "--package-name", versionInfo.name, "--package-version", version, @@ -438,7 +451,9 @@ def makePot(target, source, env): "--from-code", "utf-8", # Needed because xgettext doesn't recognise the .pyw extension. "--language=python", - ] + [os.path.relpath(str(f), str(sourceDir)) for f in source] + # Too many files to list on commandline, use a file list instead. + f"--files-from={potSourceFileList.abspath}", + ] ]) != 0: raise RuntimeError("xgettext failed") @@ -458,17 +473,35 @@ def makePot(target, source, env): os.remove(potFn) os.rename(tmpFn, potFn) + env.SConscript("devDocs/sconscript", exports=["env", "outputDir", "sourceDir", "t2tBuildConf"]) -pot = env.Command(outputDir.File("%s.pot" % outFilePrefix), + +def getSubDirs(path): + for root, dirNames, fileNames in os.walk(path): + yield root + + +# The Glob() SCons function doesnt have the ability to go recursive. Instead +# walk the dirs and match patterns. +potSourceFiles = [ # Don't use sourceDir as the source, as this depends on comInterfaces and nvdaHelper. # We only depend on the Python files. - [f for pattern in ("*.py", "*.pyw", r"*\*.py", r"*\*\*.py") - for f in env.Glob(os.path.join(sourceDir.path, pattern)) + f for recurseDirs in getSubDirs(sourceDir.path) + for pattern in ("*.py", "*.pyw") + for f in env.Glob( + os.path.join(recurseDirs, pattern), # Exclude comInterfaces, since these don't contain translatable strings # and they cause unknown encoding warnings. - if not f.path.startswith("source\\comInterfaces\\")], - makePot) + exclude="source/comInterfaces/*" + ) +] + +pot = env.Command( + outputDir.File("%s.pot" % outFilePrefix), + potSourceFiles, + makePot +) env.Alias("pot", pot) From b4087a2c447a272fc871477df5f7249a9ba58a64 Mon Sep 17 00:00:00 2001 From: jakubl7545 <48619364+jakubl7545@users.noreply.github.com> Date: Tue, 23 Feb 2021 10:09:26 +0100 Subject: [PATCH 067/174] Report 'unsupported' when toggling screen layout in Word (#10795) Report 'unsupported' when toggling screen layout in Word (PR #10795) Fixes #7297 Report "unsupported" when trying to toggle screen layout (NVDA+v) in MS Word and other applications that don't have support for screen layout. Previously 'v' was passed through to the application which was confusing for users who expected screen layout to be available. Co-authored-by: Reef Turner --- source/browseMode.py | 13 ++++++++++++- source/virtualBuffers/__init__.py | 12 ++++++++---- user_docs/en/changes.t2t | 1 + 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/source/browseMode.py b/source/browseMode.py index 60ae2644338..e98c2b49252 100644 --- a/source/browseMode.py +++ b/source/browseMode.py @@ -23,7 +23,7 @@ import gui import ui import cursorManager -from scriptHandler import isScriptWaiting, willSayAllResume +from scriptHandler import script, isScriptWaiting, willSayAllResume import aria import controlTypes from controlTypes import OutputReason @@ -1860,3 +1860,14 @@ def _iterNotLinkBlock(self, direction="next", pos=None): "kb:shift+,": "moveToStartOfContainer", "kb:,": "movePastEndOfContainer", } + + @script( + description=_( + # Translators: the description for the toggleScreenLayout script. + "Toggles on and off if the screen layout is preserved while rendering the document content" + ), + gesture="kb:NVDA+v", + ) + def script_toggleScreenLayout(self, gesture): + # Translators: The message reported for not supported toggling of screen layout + ui.message(_("Not supported in this document.")) diff --git a/source/virtualBuffers/__init__.py b/source/virtualBuffers/__init__.py index f718f6c4067..649678f506a 100644 --- a/source/virtualBuffers/__init__.py +++ b/source/virtualBuffers/__init__.py @@ -16,7 +16,7 @@ import NVDAHelper import XMLFormatting import scriptHandler -from scriptHandler import isScriptWaiting, willSayAllResume +from scriptHandler import script import speech import NVDAObjects import api @@ -539,6 +539,13 @@ def script_refreshBuffer(self,gesture): # Translators: the description for the refreshBuffer script on virtualBuffers. script_refreshBuffer.__doc__ = _("Refreshes the document content") + @script( + description=_( + # Translators: the description for the toggleScreenLayout script on virtualBuffers. + "Toggles on and off if the screen layout is preserved while rendering the document content" + ), + gesture="kb:NVDA+v", + ) def script_toggleScreenLayout(self,gesture): config.conf["virtualBuffers"]["useScreenLayout"]=not config.conf["virtualBuffers"]["useScreenLayout"] if config.conf["virtualBuffers"]["useScreenLayout"]: @@ -547,8 +554,6 @@ def script_toggleScreenLayout(self,gesture): else: # Translators: Presented when use screen layout option is toggled. ui.message(_("Use screen layout off")) - # Translators: the description for the toggleScreenLayout script on virtualBuffers. - script_toggleScreenLayout.__doc__ = _("Toggles on and off if the screen layout is preserved while rendering the document content") def _searchableAttributesForNodeType(self,nodeType): pass @@ -724,5 +729,4 @@ def _isNVDAObjectInApplication_noWalk(self, obj): __gestures = { "kb:NVDA+f5": "refreshBuffer", - "kb:NVDA+v": "toggleScreenLayout", } diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 0f12bff9bd1..6718130619f 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -15,6 +15,7 @@ What's New in NVDA - Updated Unicode Common Locale Data Repository (CLDR) to 38.1. (#11943) - Added more mathematical symbols to the symbols dictionary. (#11467) - The user guide, changes file, and key commands listing now have a refreshed appearance. (#12027) +- "Unsupported" now reported when attempting to toggle screen layout in applications that do not support it, such as Microsoft Word. (#7297) == Bug Fixes == From 5678c8beb2ccd4ce48ba81732940dcdf99c656b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Mon, 1 Mar 2021 02:02:16 +0100 Subject: [PATCH 068/174] remove unused imports from the nvwave module (#12048) * Nvwave: Update copyright header * Nvwave: remove unused imports * virtualBuffers: Remove unused import of nvwave --- source/nvwave.py | 13 ++++--------- source/virtualBuffers/__init__.py | 1 - 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/source/nvwave.py b/source/nvwave.py index b84bce674a7..cf6dc706d8d 100644 --- a/source/nvwave.py +++ b/source/nvwave.py @@ -1,8 +1,7 @@ -#nvwave.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2007-2017 NV Access Limited, Aleksey Sadovoy -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2007-2021 NV Access Limited, Aleksey Sadovoy, Cyrille Bougot, Peter Vágner +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. """Provides a simple Python interface to playing audio using the Windows multimedia waveOut functions, as well as other useful utilities. """ @@ -31,11 +30,7 @@ UINT, LPUINT ) -from ctypes import * -from ctypes.wintypes import * -import time import atexit -import wx import garbageHandler import winKernel import wave diff --git a/source/virtualBuffers/__init__.py b/source/virtualBuffers/__init__.py index 649678f506a..39330350ad9 100644 --- a/source/virtualBuffers/__init__.py +++ b/source/virtualBuffers/__init__.py @@ -33,7 +33,6 @@ from logHandler import log import ui import aria -import nvwave import treeInterceptorHandler import watchdog from abc import abstractmethod From 8e3657db8995ea8bb2e69da5891582d97154784b Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Tue, 2 Mar 2021 12:01:48 +0800 Subject: [PATCH 069/174] Fix typos in issue template (PR #12111) --- .github/ISSUE_TEMPLATE.md | 2 +- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 3ee7a74a6f1..7c030e839bb 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -34,6 +34,6 @@ Please also note that the NVDA project has a Citizen and Contributor Code of Con #### Have you tried any other versions of NVDA? If so, please report their behaviors. -#### If addons are disabled, is your problem still occuring? +#### If add-ons are disabled, is your problem still occurring? #### Did you try to run the COM registry fixing tool in NVDA menu / tools? diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5cb5fec6274..cf48177814b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -33,6 +33,6 @@ Please also note that the NVDA project has a Citizen and Contributor Code of Con #### Have you tried any other versions of NVDA? If so, please report their behaviors. -#### If addons are disabled, is your problem still occuring? +#### If add-ons are disabled, is your problem still occurring? #### Did you try to run the COM registry fixing tool in NVDA menu / tools? From 2c4316dff06cea2aabb946752ae4b1716978333d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Wed, 3 Mar 2021 06:58:45 +0100 Subject: [PATCH 070/174] Fix usages of some remaining Python 2 names (#12036) * NVDA controller client Python example: xrange -> range * Add unit test to make sure that locationHelper classes raises expected exceptions when attempting to create their isntances from wrong types. * locationHelper: Long no longer exists in Pythonn 3 --- extras/controllerClient/x86/example_python.py | 2 +- source/locationHelper.py | 13 ++++--- tests/unit/test_locationHelper.py | 35 +++++++++++++++---- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/extras/controllerClient/x86/example_python.py b/extras/controllerClient/x86/example_python.py index 73e6d457af8..f6abf79992c 100644 --- a/extras/controllerClient/x86/example_python.py +++ b/extras/controllerClient/x86/example_python.py @@ -11,7 +11,7 @@ ctypes.windll.user32.MessageBoxW(0,u"Error: %s"%errorMessage,u"Error communicating with NVDA",0) #Speak and braille some messages -for count in xrange(4): +for count in range(4): clientLib.nvdaController_speakText(u"This is a test client for NVDA") clientLib.nvdaController_brailleMessage(u"Time: %g seconds"%(0.75*count)) time.sleep(0.625) diff --git a/source/locationHelper.py b/source/locationHelper.py index 4eb2028ea97..dc5d4ee154b 100644 --- a/source/locationHelper.py +++ b/source/locationHelper.py @@ -1,8 +1,7 @@ -#locationHelper.py -#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) 2017-2018 NV Access Limited, Babbage B.V. +# 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) 2017-2021 NV Access Limited, Babbage B.V. """Classes and helper functions for working with rectangles and coordinates.""" @@ -39,8 +38,8 @@ def fromCompatibleType(cls, point): def fromDWORD(cls, dwPoint): if isinstance(dwPoint,DWORD): dwPoint = dwPoint.value - if not isinstance(dwPoint,(int,long)): - raise TypeError("dwPoint should be one of int, long or ctypes.wintypes.DWORD (ctypes.ulong)") + if not isinstance(dwPoint, int): + raise TypeError("dwPoint should be either int or ctypes.wintypes.DWORD (ctypes.ulong)") return Point(winUser.GET_X_LPARAM(dwPoint),winUser.GET_Y_LPARAM(dwPoint)) def __add__(self,other): diff --git a/tests/unit/test_locationHelper.py b/tests/unit/test_locationHelper.py index c14c8e11fd2..96c17fc4fb2 100644 --- a/tests/unit/test_locationHelper.py +++ b/tests/unit/test_locationHelper.py @@ -1,14 +1,13 @@ -#tests/unit/test_locationHelper.py -#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) 2017 NV Access Limited, Babbage B.V. +# 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) 2017-2021 NV Access Limited, Babbage B.V., Łukasz Golonka """Unit tests for the locationHelper module. """ import unittest -from locationHelper import * +from locationHelper import Point, RectLTRB, RectLTWH from ctypes.wintypes import RECT, POINT class TestRectOperators(unittest.TestCase): @@ -192,4 +191,26 @@ def test_ctypesPOINT(self): self.assertFalse(Point(x=3, y=4).yWiseLessOrEq(POINT(x=4, y=3))) # Equality self.assertEqual(POINT(x=4, y=3), Point(x=4, y=3)) - self.assertNotEqual(POINT(x=3, y=4), Point(x=4, y=3)) \ No newline at end of file + self.assertNotEqual(POINT(x=3, y=4), Point(x=4, y=3)) + + +class TestFailuresFromUnexpectedTypes(unittest.TestCase): + + def test_PointFailures(self): + self.assertRaises(TypeError, Point.fromFloatCollection, 22.22, 22, 33) + self.assertRaises(TypeError, Point.fromCompatibleType, 22.22) + self.assertRaises(TypeError, Point.fromDWORD, 22.22) + + def test_RectLTRBFailures(self): + self.assertRaises(TypeError, RectLTRB.fromFloatCollection, 22, 33.33, 44.44) + self.assertRaises(TypeError, RectLTRB.fromCompatibleType, 33.33) + self.assertRaises(TypeError, RectLTRB.fromPoint, 33.33) + self.assertRaises(TypeError, RectLTRB.fromCollection) + self.assertRaises(ValueError, RectLTRB.fromCollection, 22.22) + + def test_RectLTWHFailures(self): + self.assertRaises(TypeError, RectLTWH.fromFloatCollection, 22, 33.33, 44.44) + self.assertRaises(TypeError, RectLTWH.fromCompatibleType, 33.33) + self.assertRaises(TypeError, RectLTWH.fromPoint, 33.33) + self.assertRaises(TypeError, RectLTWH.fromCollection) + self.assertRaises(ValueError, RectLTWH.fromCollection, 22.22) From db664bef7881d35c6af156287b9b518130aa2a69 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Wed, 3 Mar 2021 17:10:34 +1000 Subject: [PATCH 071/174] Microsoft Word documents (both with UIA enabled and disabled) now get a treeInterceptor created straight way, but with passThrough (focus mode) enabled. Thus, NVDA+f7 (elements list) is now available with out having to switch to browse mode in Microsoft Word first. (#12051) --- source/NVDAObjects/IAccessible/winword.py | 7 +++++-- source/NVDAObjects/UIA/wordDocument.py | 12 ++++++++++-- source/NVDAObjects/window/winword.py | 8 ++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/source/NVDAObjects/IAccessible/winword.py b/source/NVDAObjects/IAccessible/winword.py index acc273a6377..72da470d693 100644 --- a/source/NVDAObjects/IAccessible/winword.py +++ b/source/NVDAObjects/IAccessible/winword.py @@ -21,11 +21,14 @@ from NVDAObjects.window import DisplayModelEditableText from ..behaviors import EditableTextWithoutAutoSelectDetection from NVDAObjects.window.winword import * +from NVDAObjects.window.winword import WordDocumentTreeInterceptor + class WordDocument(IAccessible,EditableTextWithoutAutoSelectDetection,WordDocument): - treeInterceptorClass=WordDocumentTreeInterceptor - shouldCreateTreeInterceptor=False + treeInterceptorClass = WordDocumentTreeInterceptor + shouldCreateTreeInterceptor = True + TextInfo=WordDocumentTextInfo def _get_ignoreEditorRevisions(self): diff --git a/source/NVDAObjects/UIA/wordDocument.py b/source/NVDAObjects/UIA/wordDocument.py index bccd3d03783..105843d999c 100644 --- a/source/NVDAObjects/UIA/wordDocument.py +++ b/source/NVDAObjects/UIA/wordDocument.py @@ -298,6 +298,14 @@ def getTextWithFields(self,formatConfig=None): class WordBrowseModeDocument(UIABrowseModeDocument): + # This treeInterceptor starts in focus mode, thus escape should not switch back to browse mode + disableAutoPassThrough = True + + def __init__(self, rootNVDAObject): + super(WordBrowseModeDocument, self).__init__(rootNVDAObject) + self.passThrough = True + browseMode.reportPassThrough.last = True + def shouldSetFocusToObj(self,obj): # Ignore strange editable text fields surrounding most inner fields (links, table cells etc) if obj.role==controlTypes.ROLE_EDITABLETEXT and obj.UIAElement.cachedAutomationID.startswith('UIA_AutomationId_Word_Content'): @@ -341,8 +349,8 @@ def _get_role(self): return role class WordDocument(UIADocumentWithTableNavigation,WordDocumentNode,WordDocumentBase): - treeInterceptorClass=WordBrowseModeDocument - shouldCreateTreeInterceptor=False + treeInterceptorClass = WordBrowseModeDocument + shouldCreateTreeInterceptor = True announceEntireNewLine=True # Microsoft Word duplicates the full title of the document on this control, which is redundant as it appears in the title of the app itself. diff --git a/source/NVDAObjects/window/winword.py b/source/NVDAObjects/window/winword.py index 2d83596adba..cd2449b0195 100755 --- a/source/NVDAObjects/window/winword.py +++ b/source/NVDAObjects/window/winword.py @@ -1069,6 +1069,14 @@ def _get_focusableNVDAObjectAtStart(self): class WordDocumentTreeInterceptor(browseMode.BrowseModeDocumentTreeInterceptor): + # This treeInterceptor starts in focus mode, thus escape should not switch back to browse mode + disableAutoPassThrough = True + + def __init__(self, rootNVDAObject): + super(WordDocumentTreeInterceptor, self).__init__(rootNVDAObject) + self.passThrough = True + browseMode.reportPassThrough.last = True + TextInfo=BrowseModeWordDocumentTextInfo def _activateLongDesc(self,controlField): From 0d22a00fe44358402d7e56f248d1558b25e2dfca Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Thu, 11 Mar 2021 11:56:18 +1100 Subject: [PATCH 072/174] remove winKernel.GetTimeFormat, winKernel.GetDateFormat and usages (#12139) * remove winKernel.GetTimeFormat, winKernel.GetDateFormat and usages * updated changes.t2t --- source/appModules/outlook.py | 28 +++++++++++++++++++++++++--- source/winKernel.py | 24 ------------------------ user_docs/en/changes.t2t | 3 ++- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/source/appModules/outlook.py b/source/appModules/outlook.py index 6c410b4aaef..a40eefae45a 100644 --- a/source/appModules/outlook.py +++ b/source/appModules/outlook.py @@ -365,9 +365,31 @@ def reportFocus(self): except COMError: return super(CalendarView,self).reportFocus() timeSlotText=self._generateTimeRangeText(selectedStartTime,selectedEndTime) - startLimit=u"%s %s"%(winKernel.GetDateFormatEx(winKernel.LOCALE_NAME_USER_DEFAULT, winKernel.DATE_LONGDATE, selectedStartTime, None),winKernel.GetTimeFormat(winKernel.LOCALE_USER_DEFAULT, winKernel.TIME_NOSECONDS, selectedStartTime, None)) - endLimit=u"%s %s"%(winKernel.GetDateFormatEx(winKernel.LOCALE_NAME_USER_DEFAULT, winKernel.DATE_LONGDATE, selectedEndTime, None),winKernel.GetTimeFormat(winKernel.LOCALE_USER_DEFAULT, winKernel.TIME_NOSECONDS, selectedEndTime, None)) - query=u'[Start] < "{endLimit}" And [End] > "{startLimit}"'.format(startLimit=startLimit,endLimit=endLimit) + startDate = winKernel.GetDateFormatEx( + winKernel.LOCALE_NAME_USER_DEFAULT, + winKernel.DATE_LONGDATE, + selectedStartTime, + None + ) + startTime = winKernel.GetTimeFormatEx( + winKernel.LOCALE_NAME_USER_DEFAULT, + winKernel.TIME_NOSECONDS, + selectedStartTime, + None + ) + endDate = winKernel.GetDateFormatEx( + winKernel.LOCALE_NAME_USER_DEFAULT, + winKernel.DATE_LONGDATE, + selectedEndTime, + None + ) + endTime = winKernel.GetTimeFormatEx( + winKernel.LOCALE_NAME_USER_DEFAULT, + winKernel.TIME_NOSECONDS, + selectedEndTime, + None + ) + query = f'[Start] < "{endDate} {endTime}" And [End] > "{startDate} {startTime}"' i=e.currentFolder.items i.sort('[Start]') i.IncludeRecurrences =True diff --git a/source/winKernel.py b/source/winKernel.py index 8645d295103..75575f8bf8a 100644 --- a/source/winKernel.py +++ b/source/winKernel.py @@ -160,18 +160,6 @@ class SYSTEMTIME(ctypes.Structure): ("wMilliseconds", WORD) ) -def GetDateFormat(Locale,dwFlags,date,lpFormat): - """@Deprecated: use GetDateFormatEx instead.""" - if date is not None: - date=SYSTEMTIME(date.year,date.month,0,date.day,date.hour,date.minute,date.second,0) - lpDate=byref(date) - else: - lpDate=None - bufferLength=kernel32.GetDateFormatW(Locale, dwFlags, lpDate, lpFormat, None, 0) - buf=ctypes.create_unicode_buffer("", bufferLength) - kernel32.GetDateFormatW(Locale, dwFlags, lpDate, lpFormat, buf, bufferLength) - return buf.value - def GetDateFormatEx(Locale,dwFlags,date,lpFormat): if date is not None: date=SYSTEMTIME(date.year,date.month,0,date.day,date.hour,date.minute,date.second,0) @@ -183,18 +171,6 @@ def GetDateFormatEx(Locale,dwFlags,date,lpFormat): kernel32.GetDateFormatEx(Locale, dwFlags, lpDate, lpFormat, buf, bufferLength, None) return buf.value -def GetTimeFormat(Locale,dwFlags,date,lpFormat): - """@Deprecated: use GetTimeFormatEx instead.""" - if date is not None: - date=SYSTEMTIME(date.year,date.month,0,date.day,date.hour,date.minute,date.second,0) - lpTime=byref(date) - else: - lpTime=None - bufferLength=kernel32.GetTimeFormatW(Locale,dwFlags,lpTime,lpFormat, None, 0) - buf=ctypes.create_unicode_buffer("", bufferLength) - kernel32.GetTimeFormatW(Locale,dwFlags,lpTime,lpFormat, buf, bufferLength) - return buf.value - def GetTimeFormatEx(Locale,dwFlags,date,lpFormat): if date is not None: date=SYSTEMTIME(date.year,date.month,0,date.day,date.hour,date.minute,date.second,0) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 6718130619f..5e00fd4919d 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -50,6 +50,8 @@ What's New in NVDA - 'isCurrent' is no longer Optional, and thus will not return None, when an object is not current 'controlTypes.IsCurrent.NO' is returned. - The 'controlTypes.isCurrentLabels' has been removed, instead use the 'displayString' property on a 'controlTypes.IsCurrent' enum value. (#11782) - EG 'controlTypes.IsCurrent.YES.displayString' +- `winKernel.GetTimeFormat` has been removed - use `winKernel.GetTimeFormatEx` instead (#12139) +- `winKernel.GetDateFormat` has been removed - use `winKernel.GetDateFormatEx` instead (#12139) = 2020.4 = @@ -3124,4 +3126,3 @@ Major highlights of this release include support for 64 bit editions of Windows; - NVDA now asks if it should save configuration and restart if the user has just changed the language in the User Interface Settings Dialog. NVDA must be restarted for the language change to fully take effect. - If a synthesizer can not be loaded, when choosing it from the synthesizer dialog, a message box alerts the user to the fact. - When loading a synthesizer for the first time, NVDA lets the synthesizer choose the most suitable voice, rate and pitch parameters, rather than forcing it to defaults it thinks are ok. This fixes a problem where Eloquence and Viavoice sapi4 synths start speaking way too fast for the first time. - From 16b83338360cd110509dc5e59b0ea7393ea734cd Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Thu, 11 Mar 2021 12:24:22 +1100 Subject: [PATCH 073/174] remove DriverSettingsMixin alias for AutoSettingsMixin (#12144) * remove DriverSettingsMixin alias for AutoSettingsMixin * update changes.t2t --- source/gui/settingsDialogs.py | 5 ----- user_docs/en/changes.t2t | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index f3216b55b24..869eed402d0 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -1389,11 +1389,6 @@ def onPanelActivated(self): super().onPanelActivated() -#: DriverSettingsMixin name is provided or backwards compatibility. -# The name DriverSettingsMixin should be considered deprecated, use AutoSettingsMixin instead. -DriverSettingsMixin = AutoSettingsMixin - - class VoiceSettingsPanel(AutoSettingsMixin, SettingsPanel): # Translators: This is the label for the voice settings panel. title = _("Voice") diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 5e00fd4919d..50fcf18c420 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -52,6 +52,7 @@ What's New in NVDA - EG 'controlTypes.IsCurrent.YES.displayString' - `winKernel.GetTimeFormat` has been removed - use `winKernel.GetTimeFormatEx` instead (#12139) - `winKernel.GetDateFormat` has been removed - use `winKernel.GetDateFormatEx` instead (#12139) +- `gui.DriverSettingsMixin` has been removed - use `gui.AutoSettingsMixin` (#12144) = 2020.4 = From 8e4b189bfe51a4b31b3954348a6a57251d8e8fed Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Thu, 11 Mar 2021 13:33:35 +1100 Subject: [PATCH 074/174] remove getSpeechForSpelling alias for getSpellingSpeech (#12145) * remove getSpeechForSpelling alias for getSpellingSpeech * fix flake8 error * updated changes.t2t --- source/speech/__init__.py | 7 ------- user_docs/en/changes.t2t | 1 + 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/source/speech/__init__.py b/source/speech/__init__.py index f89bcb11d91..f0c74c34e9e 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -293,13 +293,6 @@ def getSpellingSpeech( # noqa: C901 yield PitchCommand() yield EndUtteranceCommand() - -# 'getSpeechForSpelling' should be considered deprecated, use getSpellingSpeech instead. -# The name 'getSpeechForSpelling' was introduced during the 2019.3 release. -# The decision was made to rename it to be consistent with the other functions (get*Speech) which create -# speech sequences. -getSpeechForSpelling = getSpellingSpeech - def getCharDescListFromText(text,locale): """This method prepares a list, which contains character and its description for all characters the text is made up of, by checking the presence of character descriptions in characterDescriptions.dic of that locale for all possible combination of consecutive characters in the text. This is done to take care of conjunct characters present in several languages such as Hindi, Urdu, etc. diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 50fcf18c420..4f24482c071 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -53,6 +53,7 @@ What's New in NVDA - `winKernel.GetTimeFormat` has been removed - use `winKernel.GetTimeFormatEx` instead (#12139) - `winKernel.GetDateFormat` has been removed - use `winKernel.GetDateFormatEx` instead (#12139) - `gui.DriverSettingsMixin` has been removed - use `gui.AutoSettingsMixin` (#12144) +- `speech.getSpeechForSpelling` has been removed - use `speech.getSpellingSpeech` (#12145) = 2020.4 = From 528d5708aa13a0a384e1fcd5ef00a933ba077695 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 11 Mar 2021 18:06:57 +1000 Subject: [PATCH 075/174] Py3.8: Transparently use a Python virtual environment under the hood (#12075) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Python virtual environment is now used transparently by the NVDA build system, and all Python dependencies are installed into this environment using pip. From a developer's perspective using the NVDA build system, there is no need to worry about the Python virtual environment behind the scenes, nor should you create and or activate one manually. All NVDA build system commands will handle this transparently. * To build NVDA, SCons should continue to be used in the usual way. E.g. executing scons.bat in the root of the repository. Running py -m SCons is no longer supported, and scons.py has also been removed. * To run NVDA from source, rather than executing source/nvda.pyw directly, the developer should now use runnvda.bat in the root of the repository. If you do try to execute source/nva.pyw, a message box will alert you this is no longer supported. * To perform unit tests, execute rununittests.bat [] * To perform system tests: execute runsystemtests.bat [] * To perform linting, execute runlint.bat Behind the scenes, the above batch files (scons, runnvda, rununittests, runsystemtests and runlint) ensure that the Python virtual environment is created and up to date, activates the environment, runs the command and then deactivates. All transparently. A developer should not have to know about the Python virtual environment at all. The first time one of these commands are run, the virtual environment will be created, and all required Python dependencies will be installed with pip. You can see the entire list of packages and their exact versions that pip will use, in requirements.txt in the root of the repository. venvUtils/ensureVenv.py contains the logic to check for, create and update the virtual environment. If a previous virtual environment exists but has a miss-matching Python version or pip package requirements have changed, The virtual environment is recreated with the updated version of Python and packages. If a virtual environment is found but does not seem to be ours, the user is asked if it should be overwritten or not. This script also detects if it is being run from an existing 3rd party Python virtual environment and aborts if so. thus, it is impossible to execute SCons or NVDA from source within another Python virtual environment. venvUtils/ensureAndActivate.bat can be used to ensure the virtual environment exists and is up to date, and then activates it in the current shell, ready for other commands to be executed in the context of NVDA's build system Python virtual environment. this would never normally be executed by itself, though appVeyor uses it at the beginning of its execution and leaves the environment active for the remainder of its execution. venvUtils/venvCmd.bat is a script that runs a single command within the context of the NVDA build system Python virtual environment. It ensures and activates the environment, executes the command, and then deactivates the environment. this script is what all the high-level batch files use internally. SConstruct, and source/nvda.pyw both contain logic that detects the NVDA build system Python virtual environment, and abort if it is not active. As this PR is specific for Python 3.8, it also upgrades the following Python dependencies: • wxPython to 4.1.1 • pySerial to 3.5 • py2exe to 0.10.1.0 --- .gitignore | 1 + .gitmodules | 19 --- appveyor.yml | 47 +++--- devDocs/buildSystemNotes.md | 74 ++++++++++ devDocs/devDocsInstall/.gitignore | 1 - devDocs/devDocsInstall/requirements.txt | 2 - devDocs/devDocsInstall/sconscript | 35 ----- devDocs/sconscript | 5 - include/comtypes | 1 - include/configobj | 1 - include/nvda_dmp | 2 +- include/py2exe | 1 - include/pyserial | 1 - include/scons | 1 - include/wxPython | 1 - miscDeps | 2 +- nvdaHelper/espeak/sconscript | 2 - nvdaHelper/liblouis/sconscript | 4 +- readme.md | 55 +++---- requirements.txt | 30 ++++ runlint.bat | 9 ++ runnvda.bat | 2 + runsystemtests.bat | 3 + rununittests.bat | 2 + scons.bat | 14 +- scons.py | 16 -- sconstruct | 63 ++++---- source/addonHandler/__init__.py | 14 +- source/gui/__init__.py | 15 ++ source/languageHandler.py | 37 +---- source/louisHelper.py | 16 +- source/nvda.pyw | 21 ++- source/setup.py | 17 ++- source/sourceEnv.py | 6 +- tests/lint/lintInstall/.gitignore | 1 - tests/lint/lintInstall/requirements.txt | 5 - tests/lint/lintInstall/sconscript | 35 ----- tests/lint/sconscript | 62 -------- tests/sconscript | 34 ----- tests/system/libraries/NvdaLib.py | 4 +- .../libraries/SystemTestSpy/configManager.py | 2 - tests/system/readme.md | 44 ++---- tests/system/requirements.txt | 4 - tests/system/sconscript | 43 ------ tests/unit/sconscript | 29 ---- user_docs/en/changes.t2t | 8 + user_docs/en/userGuide.t2t | 4 +- venvUtils/ensureAndActivate.bat | 9 ++ venvUtils/ensureVenv.py | 137 ++++++++++++++++++ venvUtils/exportPackageList.bat | 8 + venvUtils/venvCmd.bat | 27 ++++ 51 files changed, 473 insertions(+), 503 deletions(-) create mode 100644 devDocs/buildSystemNotes.md delete mode 100644 devDocs/devDocsInstall/.gitignore delete mode 100644 devDocs/devDocsInstall/requirements.txt delete mode 100644 devDocs/devDocsInstall/sconscript delete mode 160000 include/comtypes delete mode 160000 include/configobj delete mode 160000 include/py2exe delete mode 160000 include/pyserial delete mode 160000 include/scons delete mode 160000 include/wxPython create mode 100644 requirements.txt create mode 100644 runlint.bat create mode 100644 runnvda.bat create mode 100644 runsystemtests.bat create mode 100644 rununittests.bat delete mode 100644 scons.py delete mode 100644 tests/lint/lintInstall/.gitignore delete mode 100644 tests/lint/lintInstall/requirements.txt delete mode 100644 tests/lint/lintInstall/sconscript delete mode 100644 tests/lint/sconscript delete mode 100644 tests/system/requirements.txt delete mode 100644 tests/system/sconscript delete mode 100644 tests/unit/sconscript create mode 100644 venvUtils/ensureAndActivate.bat create mode 100644 venvUtils/ensureVenv.py create mode 100644 venvUtils/exportPackageList.bat create mode 100644 venvUtils/venvCmd.bat diff --git a/.gitignore b/.gitignore index cd3f1c1eb73..28f1b7b4cf0 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ tests/unit/nvda.ini source/locale/*/cldr.dic .vscode .vs +.venv diff --git a/.gitmodules b/.gitmodules index ad949f1e294..6fb7620b8b9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,28 +15,9 @@ path = include/sonic url = https://github.com/waywardgeek/sonic.git ignore = untracked -[submodule "include/pyserial"] - path = include/pyserial - url = https://github.com/pyserial/pyserial.git - ignore = untracked -[submodule "include/comtypes"] - path = include/comtypes - url = https://github.com/nvaccess/comtypes-bin -[submodule "include/scons"] - path = include/scons - url = https://github.com/SCons/scons [submodule "include/ia2"] path = include/ia2 url = https://github.com/LinuxA11y/IAccessible2.git -[submodule "include/wxPython"] - path = include/wxPython - url = https://github.com/nvaccess/wxPython-bin.git -[submodule "include/configobj"] - path = include/configobj - url = https://github.com/DiffSK/configobj.git -[submodule "include/py2exe"] - path = include/py2exe - url = https://github.com/nvaccess/py2exe-bin [submodule "include/javaAccessBridge32"] path = include/javaAccessBridge32 url = https://github.com/nvaccess/javaAccessBridge32-bin.git diff --git a/appveyor.yml b/appveyor.yml index 52c46ae42c2..4c424a12efe 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -11,7 +11,7 @@ branches: - /release-.*/ environment: - PY_PYTHON: 3.7-32 + PY_PYTHON: 3.8-32 encFileKey: secure: ekOvuyywHuDdGZmRmoj+b3jfrq39A2xlx4RD5ZUGd/8= mozillaSymsAuthToken: @@ -95,12 +95,15 @@ build_script: Set-AppveyorBuildVariable "sconsOutTargets" $sconsOutTargets Set-AppveyorBuildVariable "sconsArgs" $sconsArgs - 'echo scons args: %sconsArgs%' - - py scons.py source %sconsArgs% + - scons source %sconsArgs% + - if ERRORLEVEL 1 exit %ERRORLEVEL% # We don't need launcher to run checkPot, so run the checkPot before launcher. - - py scons.py checkPot %sconsArgs% + - scons checkPot %sconsArgs% + - if ERRORLEVEL 1 exit %ERRORLEVEL% # The pot gets built by tests, but we don't actually need it as a build artifact. - 'echo scons output targets: %sconsOutTargets%' - - py scons.py %sconsOutTargets% %sconsArgs% + - scons %sconsOutTargets% %sconsArgs% + - if ERRORLEVEL 1 exit %ERRORLEVEL% # Build symbol store. - ps: | foreach ($syms in @@ -115,12 +118,10 @@ build_script: } - cd symbols - 7z a -tzip -r ..\output\symbols.zip *.dl_ *.ex_ *.pd_ + - if ERRORLEVEL 1 exit %ERRORLEVEL% - cd .. before_test: - # install required packages - - py -m pip install --upgrade pip - - py -m pip install -r tests/system/requirements.txt -r tests/lint/lintInstall/requirements.txt - mkdir testOutput - mkdir testOutput\unit - mkdir testOutput\system @@ -153,7 +154,7 @@ test_script: $errorCode=0 $outDir = (Resolve-Path .\testOutput\unit\) $unitTestsXml = "$outDir\unitTests.xml" - py -m nose -sv --with-xunit --xunit-file="$unitTestsXml" ./tests/unit + .\rununittests.bat --with-xunit --xunit-file="$unitTestsXml" if($LastExitCode -ne 0) { $errorCode=$LastExitCode Add-AppveyorMessage "Unit test failure" @@ -168,12 +169,14 @@ test_script: if($env:APPVEYOR_PULL_REQUEST_NUMBER) { $lintOutput = (Resolve-Path .\testOutput\lint\) $lintSource = (Resolve-Path .\tests\lint\) - git fetch -q origin $env:APPVEYOR_REPO_BRANCH - $prDiff = "$lintOutput\prDiff.patch" - git diff -U0 FETCH_HEAD...HEAD > $prDiff - $flake8Config = "$lintSource\flake8.ini" - $flake8Output = "$lintOutput\PR-Flake8.txt" - type "$prDiff" | py -Xutf8 -m flake8 --diff --output-file="$flake8Output" --tee --config="$flake8Config" + # When Appveyor runs for a pr, + # the build is made from a new temporary commit, + # resulting from the pr branch being merged into its base branch. + # Therefore to create a diff for linting, we must fetch the head of the base branch. + # In a PR, APPVEYOR_REPO_BRANCH points to the head of the base branch. + git fetch -q origin $env:APPVEYOR_REPO_BRANCH + $flake8Output = "$lintOutput\PR-Flake8.txt" + .\runlint.bat FETCH_HEAD "$flake8Output" if($LastExitCode -ne 0) { $errorCode=$LastExitCode Add-AppveyorMessage "PR introduces Flake8 errors" @@ -191,19 +194,14 @@ test_script: - ps: | $testOutput = (Resolve-Path .\testOutput\) $systemTestOutput = (Resolve-Path "$testOutput\system") - $testSource = ".\tests\system\robot" - - $robotArgs = '--argumentfile', '.\tests\system\robotArgs.robot' - # useInstalledNVDA must come after args file to override the whichNVDA value - $useInstalledNVDA = '--variable', 'whichNVDA:installed' - - py -m robot $robotArgs $useInstalledNVDA $testSource - Compress-Archive -Path "$systemTestOutput\*" -DestinationPath "$testOutput\systemTestResult.zip" - Push-AppveyorArtifact "$testOutput\systemTestResult.zip" + + .\runsystemtests.bat --variable whichNVDA:installed if($LastExitCode -ne 0) { $errorCode=$LastExitCode Add-AppveyorMessage "System test failure" } + Compress-Archive -Path "$systemTestOutput\*" -DestinationPath "$testOutput\systemTestResult.zip" + Push-AppveyorArtifact "$testOutput\systemTestResult.zip" $wc = New-Object 'System.Net.WebClient' $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path "$systemTestOutput\systemTests.xml")) if($errorCode -ne 0) { $host.SetShouldExit($errorCode) } @@ -273,6 +271,9 @@ deploy_script: on_finish: # add a message to point to the NVDA build, to make testing PR's easier. - ps: | + # Save an exact list of the actual Python packages and their versions that got installed, to aide in reproducing a build + .\venvUtils\exportPackageList.bat installed_python_packages.txt + Push-AppveyorArtifact installed_python_packages.txt $appVeyorUrl = "https://ci.appveyor.com" $exe = Get-ChildItem -Name output\*.exe if($?){ diff --git a/devDocs/buildSystemNotes.md b/devDocs/buildSystemNotes.md new file mode 100644 index 00000000000..432b0515e5f --- /dev/null +++ b/devDocs/buildSystemNotes.md @@ -0,0 +1,74 @@ +# Build System Notes +A Python virtual environment is used transparently by the NVDA build system, +and all Python dependencies are installed into this environment using `pip`. + +NVDA's build system commands will handle all aspects of the virtual environment. +Developers should not create or activate the virtual environment manually, unless +working on the build system itself. + +For the documentation on how to _use_ the build system (E.G. building, +running NVDA or tests) see the main repository readme file. + +## How the build system works + +The virtual environment system used is `venv`. +Dependencies are installed with `pip` via the `requirements.txt` file. +Version numbers for dependencies should be used to lock in a version. + +The virtual environment is recreated if it is outdated, either due to: +- Python version. +- `pip` requirements. + +The user is consulted before modifying / removing a virtual environment that can't be identified +as having been created by NVDA's build system. + +### Entry points to the build system + +These are the only files expected to be executed directly by a user/developer: +- `scons.bat` +- `runnvda.bat` +- `rununittests.bat` +- `runsystemtests.bat` +- `runlint.bat` + +**Note:** The `runnvda.bat` script intentionally uses `pyw.exe` to run NVDA as +this is the more common and expected way to run NVDA. +Run NVDA with `py.exe` in order to have standard output/error output to the console. +This is particularly useful if there is an error in NVDA before logging is initialised. +To do this, modify the `runnvda.bat` file. + +**Note:** Executing `source/nvda.pyw` outside of a virtual environment will produce an error message +and early termination. + +### Main implementation files: +The following files contain the main implementation of the virtual environment setup. + +#### `venvUtils/ensureAndActivate.bat` + - Activates the virtual environment. + - If necessary, creates and configures it first. + - The virtual environment is left active. +#### `venvUtils/venvCmd.bat` + - Uses `ensureAndActivate.bat` to run a command within the context + of the virtual environment. + - The virtual environment is deactivated after the command + completes. + - All entry point scripts depend on this. +#### `venvUtils/ensureVenv.py` +- Does the actual work to create and configure the virtual + environment. + +## Motivation for using virtual environments + +Ensures the build environment is clean, and there are no conflicts with other installed packages. + +NVDA and its build system have many Python dependencies. +Using `pip` and a virtual environment means: +- Updating is easier than git submodules. + E.G. wxPython no longer has to be pre-built and stored in our bin repo. +- Developers need to sync/update their submodules less often. +- More consistency for dependencies. +- IDE's can be configured more easily. +- No conflict between NVDA dependencies and Python packages already installed globally on the + developer's system. +- Don't interfere with the developer's system. Installing packages globally may break things + outside of NVDA. diff --git a/devDocs/devDocsInstall/.gitignore b/devDocs/devDocsInstall/.gitignore deleted file mode 100644 index 9ae18088231..00000000000 --- a/devDocs/devDocsInstall/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_executed_requirements.txt diff --git a/devDocs/devDocsInstall/requirements.txt b/devDocs/devDocsInstall/requirements.txt deleted file mode 100644 index 44ee79ace41..00000000000 --- a/devDocs/devDocsInstall/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -sphinx==3.4.1 -sphinx_rtd_theme diff --git a/devDocs/devDocsInstall/sconscript b/devDocs/devDocsInstall/sconscript deleted file mode 100644 index 032cd142541..00000000000 --- a/devDocs/devDocsInstall/sconscript +++ /dev/null @@ -1,35 +0,0 @@ -### -# This file is a part of the NVDA project. -# URL: http://www.nvaccess.org/ -# Copyright 2019 NV Access Limited. -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2.0, as published by -# the Free Software Foundation. -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# This license can be found at: -# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html -### - -import sys - -Import("env") - -doInstall = env.Command( - "_executed_requirements.txt", # $TARGET - "requirements.txt", # $SOURCE - [ - # Install deps from requirements file. - [ - sys.executable, "-m", "pip", - "install", "--no-warn-script-location", "-r", "$SOURCE", - ], - # Copy the requirements file, this is used to stop scons from - # triggering pip from attempting re-installing when nothing has - # changed. Pip takes a long time to determine that deps are met. - Copy('$TARGET', '$SOURCE') - ] -) - -Return('doInstall') diff --git a/devDocs/sconscript b/devDocs/sconscript index c04bd9261b9..12e3f64b8f0 100644 --- a/devDocs/sconscript +++ b/devDocs/sconscript @@ -19,10 +19,6 @@ Import("env", "outputDir", "sourceDir", "t2tBuildConf") env = env.Clone() -devDocsInstall = env.SConscript("devDocsInstall/sconscript", exports=["env"]) -env.Depends(devDocsInstall, env.Dir("devDocs/devDocsInstall/requirements.txt")) -env.Alias("devDocsInstall", devDocsInstall) - devDocsOutputDir=outputDir.Dir('devDocs') #Build the developer guide and move it to the output directory @@ -69,7 +65,6 @@ sphinxAPIDocs = env.Command( ] + [f"{sourceDir}\\{f}" for f in ignorePaths] ] ) -env.Depends(sphinxAPIDocs, devDocsInstall) sphinxHtml = env.Command( "_build", sphinxAPIDocs, diff --git a/include/comtypes b/include/comtypes deleted file mode 160000 index 8c45582fe49..00000000000 --- a/include/comtypes +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8c45582fe497269594f127f93d1a786d6bc868fb diff --git a/include/configobj b/include/configobj deleted file mode 160000 index f9a265c4adf..00000000000 --- a/include/configobj +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f9a265c4adf2e4e64fd65b5555d0a1b2fbf9ec39 diff --git a/include/nvda_dmp b/include/nvda_dmp index b0f8d87c79d..1c601ecd10a 160000 --- a/include/nvda_dmp +++ b/include/nvda_dmp @@ -1 +1 @@ -Subproject commit b0f8d87c79dfa27fc9b33371a3f4a4a5e193c384 +Subproject commit 1c601ecd10a5c0256cbaab61b8d38d46bc7f1cf4 diff --git a/include/py2exe b/include/py2exe deleted file mode 160000 index c496ae65e4c..00000000000 --- a/include/py2exe +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c496ae65e4cb67fb5a1c17087ec33e4fd69be859 diff --git a/include/pyserial b/include/pyserial deleted file mode 160000 index c54c81d933b..00000000000 --- a/include/pyserial +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c54c81d933b847458d465cd77e96cd702ff2e7be diff --git a/include/scons b/include/scons deleted file mode 160000 index dfcc78cf334..00000000000 --- a/include/scons +++ /dev/null @@ -1 +0,0 @@ -Subproject commit dfcc78cf3343debc4e01d846fd286779bba3ff1e diff --git a/include/wxPython b/include/wxPython deleted file mode 160000 index 11de9371b6b..00000000000 --- a/include/wxPython +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 11de9371b6b737673ad5ea2703d214ada69a158a diff --git a/miscDeps b/miscDeps index fc1e9c47dc7..91217045c4d 160000 --- a/miscDeps +++ b/miscDeps @@ -1 +1 @@ -Subproject commit fc1e9c47dc7f797fa0bca6cd91a32590fbf30edf +Subproject commit 91217045c4d3263c0ac9130e6f53e4852060f351 diff --git a/nvdaHelper/espeak/sconscript b/nvdaHelper/espeak/sconscript index a38b590b1b2..7ce88e9df77 100644 --- a/nvdaHelper/espeak/sconscript +++ b/nvdaHelper/espeak/sconscript @@ -1,5 +1,3 @@ -from include.scons.SCons.Util import CLVar - Import([ 'thirdPartyEnv', 'sourceDir', diff --git a/nvdaHelper/liblouis/sconscript b/nvdaHelper/liblouis/sconscript index 793cc1b611a..8404e9c0e67 100644 --- a/nvdaHelper/liblouis/sconscript +++ b/nvdaHelper/liblouis/sconscript @@ -17,8 +17,8 @@ import re import glob from SCons.Tool.MSCommon.vc import find_vc_pdir import typing -from include.scons.SCons.Environment import Environment -from include.scons.SCons.Environment import Base +from SCons.Environment import Environment +from SCons.Environment import Base Import([ "thirdPartyEnv", diff --git a/readme.md b/readme.md index 61910f12878..734c94ca09e 100644 --- a/readme.md +++ b/readme.md @@ -55,9 +55,8 @@ The NVDA source depends on several other packages to run correctly. ### Installed Dependencies The following dependencies need to be installed on your system: -* [Python](https://www.python.org/), version 3.7, 32 bit +* [Python](https://www.python.org/), version 3.8, 32 bit * Use latest minor version if possible. - * Don't use `3.7.6` it causes an error while building, see #10696. * Microsoft Visual Studio 2019 Community, Version 16.3 or later: * Download from https://visualstudio.microsoft.com/vs/ * When installing Visual Studio, you need to enable the following: @@ -75,20 +74,16 @@ The following dependencies need to be installed on your system: ### Git Submodules -Most of the dependencies are contained in Git submodules. +Some of the dependencies are contained in Git submodules. If you didn't pass the `--recursive` option to git clone, you will need to run `git submodule update --init`. Whenever a required submodule commit changes (e.g. after git pull), you will need to run `git submodule update`. If you aren't sure, run `git submodule update` after every git pull, merge or checkout. For reference, the following run time dependencies are included in Git submodules: -* [comtypes](https://github.com/enthought/comtypes), version 1.1.7 -* [wxPython](https://www.wxpython.org/), version 4.0.3 * [eSpeak NG](https://github.com/espeak-ng/espeak-ng), version 1.51-dev commit 82d5b7b04 * [Sonic](https://github.com/waywardgeek/sonic), commit 4f8c1d11 * [IAccessible2](https://wiki.linuxfoundation.org/accessibility/iaccessible2/start), commit cbc1f29631780 -* [ConfigObj](https://github.com/DiffSK/configobj), commit f9a265c -* [Six](https://pypi.python.org/pypi/six), version 1.12.0, required by wxPython and ConfigObj * [liblouis](http://www.liblouis.org/), version 3.16.1 * [Unicode Common Locale Data Repository (CLDR)](http://cldr.unicode.org/), version 38.1 * NVDA images and sounds @@ -97,37 +92,27 @@ For reference, the following run time dependencies are included in Git submodule * [MinHook](https://github.com/RaMMicHaeL/minhook), tagged version 1.2.2 * brlapi Python bindings, version 0.8 or later, distributed with [BRLTTY for Windows](https://brltty.app/download.html), version 6.1 * lilli.dll, version 2.1.0.0 -* [pySerial](https://pypi.python.org/pypi/pyserial), version 3.4 * [Python interface to FTDI driver/chip](http://fluidmotion.dyndns.org/zenphoto/index.php?p=news&title=Python-interface-to-FTDI-driver-chip) * Java Access Bridge 32 bit, from Zulu Community OpenJDK build 13.0.1+10Zulu (13.28.11) -Additionally, the following build time dependencies are included in Git submodules: +Additionally, the following build time dependencies are included in the miscDeps git submodule: -* [Py2Exe](https://github.com/albertosottile/py2exe/), version 0.9.3.2 commit b372a8e -* [Python Windows Extensions](https://sourceforge.net/projects/pywin32/ ), build 224, required by py2exe * [txt2tags](https://txt2tags.org/), version 2.5 -* [SCons](https://www.scons.org/), version 4.0.1 * [Nulsoft Install System](https://nsis.sourceforge.io/Main_Page/), version 2.51 * [NSIS UAC plug-in](https://nsis.sourceforge.io/UAC_plug-in), version 0.2.4, ansi * xgettext and msgfmt from [GNU gettext](https://sourceforge.net/projects/cppcms/files/boost_locale/gettext_for_windows/) * [Boost Optional (stand-alone header)](https://github.com/akrzemi1/Optional), from commit [3922965](https://github.com/akrzemi1/Optional/commit/3922965396fc455c6b1770374b9b4111799588a9) -### Other Dependencies -To lint using Flake 8 locally using our SCons integration, some dependencies are installed (automatically) via pip. -Although this [must be run manually](#linting-your-changes), developers may wish to first configure a Python Virtual Environment to ensure their general install is not affected. -* Flake8 -* Flake8-tabs - - The following dependencies aren't needed by most people, and are not included in Git submodules: - -* To generate developer documentation: [Sphinx](http://sphinx-doc.org/), version 3.4.1 * To generate developer documentation for nvdaHelper: [Doxygen Windows installer](http://www.doxygen.nl/download.html), version 1.8.15: * When you are using Visual Studio Code as your integrated development environment of preference, you can make use of our [prepopulated workspace configuration](https://github.com/nvaccess/vscode-nvda/) for [Visual Studio Code](https://code.visualstudio.com/). While this VSCode project is not included as a submodule in the NVDA repository, you can easily check out the workspace configuration in your repository by executing the following from the root of the repository. ```git clone https://github.com/nvaccess/vscode-nvda.git .vscode``` +### Python dependencies +NVDA and its build system also depend on an extensive list of Python packages. They are all listed with their specific versions in a requirements.txt file in the root of this repository. However, the build system takes care of fetching these itself when needed. these packages will be installed into an isolated Python virtual environment within this repository, and will not affect your system-wide set of packages. + ## Preparing the Source Tree Before you can run the NVDA source code, you must prepare the source tree. You do this by opening a command prompt, changing to the root of the NVDA source distribution and typing: @@ -173,13 +158,8 @@ By default, builds also do not use any compiler optimizations. Please see the `release` keyword argument for what compiler optimizations it will enable. ## Running the Source Code -Most developers run directly from source by: -``` -cd source -pythonw.exe nvda.pyw -``` -Note: Since NVDA is a Windows application (rather than command line), it is best to run it with `pythonw.exe`. -However, if during development you encounter an error early in the startup of NVDA, you can use `python.exe` which is likely to give more information about the error. +It is possible to run NVDA directly from source without having to build the full binary package and launcher. +To launch NVDA from source, using `cmd.exe`, execute `runnvda.bat` in the root of the repository. To view help on the arguments that NVDA will accept, use the `-h` or `--help` option. These arguments are also documented in the user guide. @@ -302,24 +282,27 @@ scons checkPot ### Linting your changes In order to ensure your changes comply with NVDA's coding style you can run the Flake8 linter locally. Some developers have found certain linting error messages misleading, these are clarified in `tests/lint/readme.md`. -Running via SCons will use Flake8 to inspect only the differences between your working directory and the specified `base` branch. +runlint.bat will use Flake8 to inspect only the differences between your working directory and the specified `base` branch. If you create a Pull Request, the `base` branch you use here should be the same as the target you would use for a Pull Request. In most cases it will be `origin/master`. ``` -scons lint base=origin/master +runlint origin/master ``` To be warned about linting errors faster, you may wish to integrate Flake8 other development tools you are using. For more details, see `tests/lint/readme.md` +### Unit Tests +Unit tests can be run with the `rununittests.bat` script. +Internally this script uses the Nose Python test framework to execute the tests. +Any arguments given to rununittests.bat are forwarded onto Nose. +Please refer to Nose's own documentation on how to filter tests etc. + ### System Tests -You may also use `scons` to run the system tests, - though this will still require the dependencies to be set up. +System tests can be run with the `runsystemtests.bat` script. +Internally this script uses the Robot test framework to execute the tests. +Any arguments given to runsystemtests.bat are forwarded onto Robot. For more details (including filtering and exclusion of tests) see `tests/system/readme.md`. -``` -scons systemTests -``` - ## Contributing to NVDA If you would like to contribute code or documentation to NVDA, you can read more information in our [contributing guide](https://github.com/nvaccess/nvda/wiki/Contributing). diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000000..aaf72293f86 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,30 @@ +# NVDA's build system is SCons +SCons==4.1.0.post1 + +# NVDA's runtime dependencies +comtypes==1.1.7 +pyserial==3.5 +wxPython==4.1.1 +git+git://github.com/DiffSK/configobj@3e2f4cc#egg=configobj + +#NVDA_DMP requires diff-match-patch +diff_match_patch_python==1.0.2 + +# Packaging NVDA +py2exe==0.10.1.0 + +# For building developer documentation +sphinx==3.4.1 +sphinx_rtd_theme + +# Requirements for automated linting +flake8 ~= 3.7.7 +flake8-tabs == 2.1.0 + +# Requirements for unit tests +nose==1.3.7 + +# Requirements for system tests +robotframework==3.2.2 +robotremoteserver==1.1 +robotframework-screencaplibrary==1.5.0 diff --git a/runlint.bat b/runlint.bat new file mode 100644 index 00000000000..81dcee35af6 --- /dev/null +++ b/runlint.bat @@ -0,0 +1,9 @@ +@echo off +rem runlint [] +rem Lints any changes after base commit up to and including current HEAD, plus any uncommitted changes. +call "%~dp0\venvUtils\venvCmd.bat" py "%~dp0\tests\lint\genDiff.py" %1 "%~dp0\tests\lint\_lint.diff" + if ERRORLEVEL 1 exit /b %ERRORLEVEL% + set flake8Args=--diff --config="%~dp0\tests\lint\flake8.ini" + if "%2" NEQ "" set flake8Args=%flake8Args% --tee --output-file=%2 + type "%~dp0\tests\lint\_lint.diff" | call "%~dp0\venvUtils\venvCmd.bat" py -Xutf8 -m flake8 %flake8Args% + diff --git a/runnvda.bat b/runnvda.bat new file mode 100644 index 00000000000..771b973a5df --- /dev/null +++ b/runnvda.bat @@ -0,0 +1,2 @@ +@echo off +call "%~dp0\venvUtils\venvCmd.bat" start pyw "%~dp0\source\nvda.pyw" %* diff --git a/runsystemtests.bat b/runsystemtests.bat new file mode 100644 index 00000000000..bb2708e2801 --- /dev/null +++ b/runsystemtests.bat @@ -0,0 +1,3 @@ +@echo off +call "%~dp0\venvUtils\venvCmd.bat" py -m robot --argumentfile "%~dp0\tests\system\robotArgs.robot" %* "%~dp0\tests\system\robot" + diff --git a/rununittests.bat b/rununittests.bat new file mode 100644 index 00000000000..c77cfa38b1a --- /dev/null +++ b/rununittests.bat @@ -0,0 +1,2 @@ +@echo off +call "%~dp0\venvUtils\venvCmd.bat" py -m nose -sv --traverse-namespace -w "%~dp0\tests\unit" %* diff --git a/scons.bat b/scons.bat index 5c7001358e6..849e7c1b354 100644 --- a/scons.bat +++ b/scons.bat @@ -1,13 +1,3 @@ @echo off -rem We need this script because .py probably isn't in pathext. -rem We can't just call python -c because it may not be in the path. -rem Instead, find the python launcher (installed by python 3) -where py 1>nul 2>&1 -if "%ERRORLEVEL%" == "0" ( - rem Python launcher is present in the PATH - rem Call python 3.7 for 32 bits - py -3.7-32 "%~dp0\scons.py" %* -) else ( - rem Python registers itself with the .py extension, so call scons.py. - "%~dp0\scons.py" %* -) +rem Executes SScons within the NVDA build system's Python virtual environment. +call "%~dp0\venvUtils\venvCmd.bat" py -m SCons %* diff --git a/scons.py b/scons.py deleted file mode 100644 index 6deab741492..00000000000 --- a/scons.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Run SCons from the NVDA source environment. -""" - -import sys -import os - -sconsPath = os.path.abspath(os.path.join(os.path.dirname(__file__), "include", "scons")) - -if os.path.exists(sconsPath): - # sys.path[0] will always be the current dir, which should take precedence. - # Insert path to the SCons folder after that. - sys.path[1:1] = (sconsPath,) - import SCons.Script - SCons.Script.Main.main() -else: - raise OSError("Path %s does not exist. Perhaps try running git submodule update --init" % sconsPath) diff --git a/sconstruct b/sconstruct index 41101eba2d6..0e02b1ef472 100755 --- a/sconstruct +++ b/sconstruct @@ -1,5 +1,6 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2010-2020 NV Access Limited, James Teh, Michael Curran, Peter Vágner, Joseph Lee, Reef Turner, Babbage B.V., Leonard de Ruijter, Łukasz Golonka, Accessolutions, Julien Cochuyt # noqa: E501 +# Copyright (C) 2010-2021 NV Access Limited, James Teh, Michael Curran, Peter Vágner, Joseph Lee, +# Reef Turner, Babbage B.V., Leonard de Ruijter, Łukasz Golonka, Accessolutions, Julien Cochuyt # This file may be used under the terms of the GNU General Public License, version 2 or later. # For more details see: https://www.gnu.org/licenses/gpl-2.0.html @@ -7,9 +8,21 @@ import sys import os import platform +# Ensure we are inside the Python virtual environment. +nvdaVenv = os.getenv("NVDA_VENV") +virtualEnv = os.getenv("VIRTUAL_ENV") +if not virtualEnv or not os.path.isdir(virtualEnv): + print( + "Error: SCons cannot detect the NVDA build system Python virtual environment.\n" + "SCons must be executed using scons.bat in the root of this repository." + ) + sys.exit(1) +if nvdaVenv != virtualEnv: + print("Warning: SCons launched within a custom Python virtual environment.") + # Variables for storing required version of Python, and the version which is used to run this script. requiredPythonMajor ="3" -requiredPythonMinor = "7" +requiredPythonMinor = "8" requiredPythonArchitecture = "32bit" installedPythonMajor = str(sys.version_info.major) installedPythonMinor = str(sys.version_info.minor) @@ -34,17 +47,12 @@ if ( requiredPythonArchitecture ) ) -if sys.version_info.micro == 6: - # #10696: Building with Python 3.7.6 fails. Innform user and exit. - Py376FailMsg = "Building with Python 3.7.6 is not possible.\nPlease use more recent version of Python 3." - raise RuntimeError(Py376FailMsg) sourceEnvPath = os.path.abspath(os.path.join(Dir('.').srcnode().path, "source")) sys.path.append(sourceEnvPath) import sourceEnv sys.path.remove(sourceEnvPath) import time from glob import glob -from py2exe.dllfinder import pydll import importlib.util import winreg @@ -102,21 +110,6 @@ vars.Add("certTimestampServer", "The URL of the timestamping server to use to ti vars.Add(PathVariable("outputDir", "The directory where the final built archives and such will be placed", "output",PathVariable.PathIsDirCreate)) vars.Add(ListVariable("nvdaHelperDebugFlags", "a list of debugging features you require", 'none', ["debugCRT","RTC","analyze"])) vars.Add(EnumVariable('nvdaHelperLogLevel','The level of logging you wish to see, lower is more verbose','15',allowed_values=[str(x) for x in range(60)])) -if "tests" in COMMAND_LINE_TARGETS: - vars.Add("unitTests", "A list of unit tests to run", "") -if "lint" in COMMAND_LINE_TARGETS: - vars.Add( # pass through variable that lint is requested - "doLint", - "internal use", - True - ) - vars.Add( - "base", - "Lint is done only on a diff, specify the ref to use as base for the diff.", - None - ) -if "systemTests" in COMMAND_LINE_TARGETS: - vars.Add("filter", "A filter for the name of the system test(s) to run. Wildcards accepted.", "") #Base environment for this and sub sconscripts env = Environment(variables=vars,HOST_ARCH='x86',tools=[ @@ -315,23 +308,27 @@ def NVDADistGenerator(target, source, env, for_signature): action.append(buildCmd) - # Python3 has started signing its main python dll. - # However, Py2exe currently tries to add a string resource to it, invalidating the signature and possibly currupting the certificate. - # Therefore, copy a fresh version of the dll one more time once py2exe has completed. - action.append(Copy(target[0],pydll)) - # #10031: Apps written in Python 3 require Universal CRT to be installed. We cannot assume users have it on their systems. # Therefore , copy required libraries from Windows 10 SDK. try: with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'SOFTWARE\Microsoft\Microsoft SDKs\Windows\v10.0', 0,winreg.KEY_READ|winreg.KEY_WOW64_32KEY) as SDKKey: - CRTDir = os.path.join(winreg.QueryValueEx(SDKKey, 'InstallationFolder')[0], "Redist", "ucrt", "DLLs", "x86") + sdk_installationFolder = winreg.QueryValueEx(SDKKey, 'InstallationFolder')[0] + sdk_productVersion = winreg.QueryValueEx(SDKKey, 'ProductVersion')[0] except WindowsError: raise RuntimeError("Windows 10 SDK not found") - if os.path.isdir(CRTDir): - with os.scandir(CRTDir) as dir: - for file in dir: - if file.name.endswith(".dll") and file.is_file(): - action.append(Copy(target[0], file.path)) + # The Universal CRT should be in an SDK version-specific directory + # But usually has a '.0' appended after the productVersion found in the registry. + # E.g. 10.0.1941 might e actually 10.0.1941.0. + # Thus try both. + CRTDir = os.path.join(sdk_installationFolder, "Redist", sdk_productVersion+".0", "ucrt", "DLLs", "x86") + if not os.path.isdir(CRTDir): + CRTDir = os.path.join(sdk_installationFolder, "Redist", sdk_productVersion, "ucrt", "DLLs", "x86") + if not os.path.isdir(CRTDir): + raise RuntimeError(f"Could not locate CRT dlls in SDK at {CRTDir}") + with os.scandir(CRTDir) as dir: + for file in dir: + if file.name.endswith(".dll") and file.is_file(): + action.append(Copy(target[0], file.path)) if certFile: for prog in "nvda_noUIAccess.exe", "nvda_uiAccess.exe", "nvda_slave.exe", "nvda_eoaProxy.exe": diff --git a/source/addonHandler/__init__.py b/source/addonHandler/__init__.py index bf9bd6d4421..5f505c01a81 100644 --- a/source/addonHandler/__init__.py +++ b/source/addonHandler/__init__.py @@ -1,9 +1,9 @@ # -*- coding: UTF-8 -*- -#addonHandler.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2012-2019 Rui Batista, NV Access Limited, Noelia Ruiz Martínez, Joseph Lee, Babbage B.V., Arnold Loubriat -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2012-2021 NV Access Limited, Rui Batista, Noelia Ruiz Martínez, +# Joseph Lee, Babbage B.V., Arnold Loubriat +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. import sys import os.path @@ -539,8 +539,8 @@ def initTranslation(): try: callerFrame = inspect.currentframe().f_back callerFrame.f_globals['_'] = translations.gettext - # Install our pgettext function. - callerFrame.f_globals['pgettext'] = languageHandler.makePgettext(translations) + # Install pgettext function. + callerFrame.f_globals['pgettext'] = translations.pgettext finally: del callerFrame # Avoid reference problems with frames (per python docs) diff --git a/source/gui/__init__.py b/source/gui/__init__.py index 7a4e46e7851..368e537acd5 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -597,6 +597,21 @@ def initialize(): raise RuntimeError("GUI already initialized") mainFrame = MainFrame() wx.GetApp().SetTopWindow(mainFrame) + # In wxPython >= 4.1, + # wx.CallAfter no longer executes callbacks while NVDA's main thread is within apopup menu or message box. + # To work around this, + # Monkeypatch wx.CallAfter to + # post a WM_NULL message to our top-level window after calling the original CallAfter, + # which causes wx's event loop to wake up enough to execute the callback. + old_wx_CallAfter = wx.CallAfter + + def wx_CallAfter_wrapper(func, *args, **kwargs): + old_wx_CallAfter(func, *args, **kwargs) + # mainFrame may be None as NVDA could be terminating. + topHandle = mainFrame.Handle if mainFrame else None + if topHandle: + winUser.PostMessage(topHandle, winUser.WM_NULL, 0, 0) + wx.CallAfter = wx_CallAfter_wrapper def terminate(): import brailleViewer diff --git a/source/languageHandler.py b/source/languageHandler.py index 7699701ec0e..b134ea0373f 100644 --- a/source/languageHandler.py +++ b/source/languageHandler.py @@ -1,14 +1,12 @@ -#languageHandler.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2007-2018 NV access Limited, Joseph Lee -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2007-2021 NV Access Limited, Joseph Lee +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. """Language and localization support. This module assists in NVDA going global through language services such as converting Windows locale ID's to friendly names and presenting available languages. """ -import builtins import os import sys import ctypes @@ -123,27 +121,6 @@ def getAvailableLanguages(presentational=False): ) return langs -def makePgettext(translations): - """Obtaina pgettext function for use with a gettext translations instance. - pgettext is used to support message contexts, - but Python's gettext module doesn't support this, - so NVDA must provide its own implementation. - """ - if isinstance(translations, gettext.GNUTranslations): - def pgettext(context, message): - try: - # Look up the message with its context. - return translations._catalog[u"%s\x04%s" % (context, message)] - except KeyError: - return message - elif isinstance(translations, gettext.NullTranslations): - # A language with out a translation catalog, such as English. - def pgettext(context, message): - return message - else: - raise ValueError("%s is Not a GNUTranslations or NullTranslations object"%translations) - return pgettext - def getWindowsLanguage(): """ Fetches the locale name of the user's configured language in Windows. @@ -198,10 +175,8 @@ def setLanguage(lang): except IOError: trans=gettext.translation("nvda",fallback=True) curLang="en" - trans.install() - # Install our pgettext function. - import builtins - builtins.pgettext = makePgettext(trans) + # #9207: Python 3.8 adds gettext.pgettext, so add it to the built-in namespace. + trans.install(names=["pgettext"]) def getLanguage(): return curLang diff --git a/source/louisHelper.py b/source/louisHelper.py index c89064a03cc..533086d19c6 100644 --- a/source/louisHelper.py +++ b/source/louisHelper.py @@ -1,12 +1,16 @@ -#louisHelper.py -#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 NV Access Limited, Babbage B.V. +# 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-2021 NV Access Limited, Babbage B.V., Joseph Lee """Helper module to ease communication to and from liblouis.""" -import louis +# Python 3.8 changes the way DLL's are loaded due to security. +# Thus manually add NVDA executable path to DLL lookup path for loading liblouis.dll. +import os +import globalVars +with os.add_dll_directory(globalVars.appDir): + import louis from logHandler import log import config diff --git a/source/nvda.pyw b/source/nvda.pyw index f2e07dfdc38..92117109c51 100755 --- a/source/nvda.pyw +++ b/source/nvda.pyw @@ -10,14 +10,29 @@ import sys import os import globalVars +import ctypes - +customVenvDetected = False if getattr(sys, "frozen", None): # We are running as an executable. # Append the path of the executable to sys so we can import modules from the dist dir. sys.path.append(sys.prefix) appDir = sys.prefix else: + # we are running from source + # Ensure we are inside the NVDA build system's Python virtual environment. + nvdaVenv = os.getenv("NVDA_VENV") + virtualEnv = os.getenv("VIRTUAL_ENV") + if not virtualEnv or not os.path.isdir(virtualEnv): + ctypes.windll.user32.MessageBoxW( + 0, + "NVDA cannot detect the Python virtual environment. " + "To run NVDA from source, please use runnvda.bat in the root of this repository.", + "Error", + 0, + ) + sys.exit(1) + customVenvDetected = nvdaVenv != virtualEnv import sourceEnv #We should always change directory to the location of this module (nvda.pyw), don't rely on sys.path[0] appDir = os.path.normpath(os.path.dirname(__file__)) @@ -25,7 +40,7 @@ appDir = os.path.abspath(appDir) os.chdir(appDir) globalVars.appDir = appDir -import ctypes + import locale import gettext @@ -232,6 +247,8 @@ if logHandler.log.getEffectiveLevel() is log.DEBUG: import buildVersion log.info("Starting NVDA version %s" % buildVersion.version) log.debug("Debug level logging enabled") +if customVenvDetected: + log.warning("NVDA launched using a custom Python virtual environment.") if globalVars.appArgs.changeScreenReaderFlag: winUser.setSystemScreenReaderFlag(True) #Accept wm_quit from other processes, even if running with higher privilages diff --git a/source/setup.py b/source/setup.py index 9db1366581a..387294c15e8 100755 --- a/source/setup.py +++ b/source/setup.py @@ -213,12 +213,13 @@ def getRecursiveDataFiles(dest,source,excludes=()): ], options = {"py2exe": { "bundle_files": 3, - "excludes": ["tkinter", - "serial.loopback_connection", - "serial.rfc2217", - "serial.serialcli", - "serial.serialjava", - "serial.serialposix", + "excludes": [ + "tkinter", + "serial.loopback_connection", + "serial.rfc2217", + "serial.serialcli", + "serial.serialjava", + "serial.serialposix", "serial.socket_connection", # netbios (from pywin32) is optionally used by Python3's uuid module. # This is not needed. @@ -228,6 +229,8 @@ def getRecursiveDataFiles(dest,source,excludes=()): # winxptheme is optionally used by wx.lib.agw.aui. # We don't need this. "winxptheme", + # numpy is an optional dependency of comtypes but we don't require it. + "numpy", ], "packages": [ "NVDAObjects", @@ -242,6 +245,8 @@ def getRecursiveDataFiles(dest,source,excludes=()): "nvdaBuiltin", # #3368: bisect was implicitly included with Python 2.7.3, but isn't with 2.7.5. "bisect", + # robotremoteserver (for system tests) depends on xmlrpc.server + "xmlrpc.server", ], }}, data_files=[ diff --git a/source/sourceEnv.py b/source/sourceEnv.py index dfd7a6b9297..decfc2e5d1d 100644 --- a/source/sourceEnv.py +++ b/source/sourceEnv.py @@ -11,13 +11,9 @@ # Get the path to the top of the repo; i.e. where include and miscDeps are. TOP_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + # Directories containing Python modules included in git submodules. PYTHON_DIRS = ( - os.path.join(TOP_DIR, "include", "pyserial"), - os.path.join(TOP_DIR, "include", "comtypes"), - os.path.join(TOP_DIR, "include", "configobj", "src"), - os.path.join(TOP_DIR, "include", "wxPython"), - os.path.join(TOP_DIR, "include", "py2exe"), os.path.join(TOP_DIR, "miscDeps", "python"), ) diff --git a/tests/lint/lintInstall/.gitignore b/tests/lint/lintInstall/.gitignore deleted file mode 100644 index 9ae18088231..00000000000 --- a/tests/lint/lintInstall/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_executed_requirements.txt diff --git a/tests/lint/lintInstall/requirements.txt b/tests/lint/lintInstall/requirements.txt deleted file mode 100644 index 738eb918933..00000000000 --- a/tests/lint/lintInstall/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -####### requirements.txt ####### -# -###### Requirements for automated lint ###### -flake8 ~= 3.7.7 -flake8-tabs == 2.1.0 diff --git a/tests/lint/lintInstall/sconscript b/tests/lint/lintInstall/sconscript deleted file mode 100644 index b3b27f7b514..00000000000 --- a/tests/lint/lintInstall/sconscript +++ /dev/null @@ -1,35 +0,0 @@ -### -# This file is a part of the NVDA project. -# URL: http://www.nvaccess.org/ -# Copyright 2019 NV Access Limited. -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2.0, as published by -# the Free Software Foundation. -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# This license can be found at: -# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html -### - -import sys - -Import("env") - -doInstall = env.Command( - "_executed_requirements.txt", # $TARGET - "requirements.txt", # $SOURCE - [ - # Install deps from requirements file. - [ - sys.executable, "-m", "pip", - "install", "-r", "$SOURCE", - ], - # Copy the requirements file, this is used to stop scons from - # triggering pip from attempting re-installing when nothing has - # changed. Pip takes a long time to determine that deps are met. - Copy('$TARGET', '$SOURCE') - ] -) - -Return('doInstall') diff --git a/tests/lint/sconscript b/tests/lint/sconscript deleted file mode 100644 index ccd52a1b381..00000000000 --- a/tests/lint/sconscript +++ /dev/null @@ -1,62 +0,0 @@ -### -# This file is a part of the NVDA project. -# URL: http://www.nvaccess.org/ -# Copyright 2019 NV Access Limited. -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2.0, as published by -# the Free Software Foundation. -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# This license can be found at: -# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html -### - -import os -import sys - -import SCons - -Import("env") - -# Add system path to get access to git. -externalEnv = Environment( - ENV = {'PATH' : os.environ['PATH']} -) - -doLint = env.get("doLint") -baseBranch = env.get("base") -if doLint and not baseBranch: - errorMessage = ( - "Lint can not complete without base branch. " - "Try: 'scons lint base=origin/master' " - "See also /tests/lint/readme.md" - ) - raise SCons.SConf.SConfError(errorMessage) - -# Create a unified diff. Written to file for ease of manual inspection -diffTarget = externalEnv.Command( - "current.diff", - None, - [[sys.executable, "tests/lint/genDiff.py", baseBranch, '$TARGET']] -) - -# Pipe diff to flake8 for linting. -lintTarget = externalEnv.Command( - "current.lint", - "current.diff", - [[ - 'type', '$SOURCE', '|', # provide diff to stdin - sys.executable, - "-Xutf8", # UTF-8 mode (PEP 540, Python >= 3.7) - "-m", "flake8", - '--diff', # accept a unified diff from stdin - '--output-file=$TARGET', # output to a file to allow easier inspection - '--tee', # also output to stdout, so results appear in scons output - '--config="tests/lint/flake8.ini"', # use config file of complex options - '--exit-zero', # even if there are errors, don't stop the build. - ]] -) - -env.Depends(lintTarget, diffTarget) -Return('diffTarget', 'lintTarget') diff --git a/tests/sconscript b/tests/sconscript index 2f110d3cebe..187f1cade0a 100644 --- a/tests/sconscript +++ b/tests/sconscript @@ -16,43 +16,9 @@ import checkPot Import("env", "sourceDir", "pot") -unitTests = env.SConscript("unit/sconscript", exports=["env"]) -env.Depends(unitTests, sourceDir) -env.AlwaysBuild(unitTests) - -systemTests = env.SConscript("system/sconscript", exports=["env"]) -env.Depends(systemTests, sourceDir) -env.AlwaysBuild(systemTests) -env.Alias("systemTests", systemTests) - - -lintInstall = env.SConscript("lint/lintInstall/sconscript", exports=["env"]) -env.Depends(lintInstall, env.Dir("tests/lint/lintInstall/requirements.txt")) -env.Alias("lintInstall", lintInstall) - -lint = env.SConscript("lint/sconscript", exports=["env"]) -env.Depends(lint, lintInstall) -env.AlwaysBuild(lint) -env.Alias("lint", lint) - def checkPotAction(target, source, env): return checkPot.checkPot(source[0].abspath) checkPotTarget = env.Command("checkPot", pot, checkPotAction) env.Depends(checkPotTarget, pot) env.AlwaysBuild(checkPotTarget) env.Alias("checkPot", checkPotTarget) - -# Determine the targets for scons tests. -# If specific tests are explicitly specified, only run those. -explicitUnitTests = env.get("unitTests") -explicitCheckPot = "checkPot" in COMMAND_LINE_TARGETS -explicitSystemTests = "systemTests" in COMMAND_LINE_TARGETS -explicit = explicitUnitTests or explicitCheckPot or explicitSystemTests -tests = [] -if not explicit or explicitUnitTests: - tests.append(unitTests) -if not explicit or explicitCheckPot: - tests.append(checkPotTarget) -if explicit and explicitSystemTests: # only run system tests explicitly - tests.append(systemTests) -env.Alias("tests", tests) diff --git a/tests/system/libraries/NvdaLib.py b/tests/system/libraries/NvdaLib.py index 74db03359b1..78da2894405 100644 --- a/tests/system/libraries/NvdaLib.py +++ b/tests/system/libraries/NvdaLib.py @@ -50,8 +50,8 @@ def __init__(self): self.whichNVDA = builtIn.get_variable_value("${whichNVDA}", "source") if self.whichNVDA == "source": - self._runNVDAFilePath = _pJoin(self.repoRoot, "source/nvda.pyw") - self.baseNVDACommandline = f"pyw -3.7-32 {self._runNVDAFilePath}" + self._runNVDAFilePath = _pJoin(self.repoRoot, "runnvda.bat") + self.baseNVDACommandline = self._runNVDAFilePath elif self.whichNVDA == "installed": self._runNVDAFilePath = _pJoin(_expandvars('%PROGRAMFILES%'), 'nvda', 'nvda.exe') self.baseNVDACommandline = f'"{str(self._runNVDAFilePath)}"' diff --git a/tests/system/libraries/SystemTestSpy/configManager.py b/tests/system/libraries/SystemTestSpy/configManager.py index 1db293237a7..46b72e13a78 100644 --- a/tests/system/libraries/SystemTestSpy/configManager.py +++ b/tests/system/libraries/SystemTestSpy/configManager.py @@ -48,11 +48,9 @@ def _installSystemTestSpyToScratchPad(repoRoot: str, scratchPadDir: str): _copyPythonLibs( pythonImports=[ # relative to the python path r"robotremoteserver", - r"xmlrpc", ], libsDest=spyPackageLibsDir ) - # install the global plugin # Despite duplication, specify full paths for clarity. opSys.copy_file( diff --git a/tests/system/readme.md b/tests/system/readme.md index e5af3622138..9116def1685 100644 --- a/tests/system/readme.md +++ b/tests/system/readme.md @@ -2,50 +2,28 @@ ### Dependencies -To install all required packages move to the root directory of this repo and execute: - -`python -m pip install -r tests/system/requirements.txt` - +This build system uses the Robot test framework to execute the system tests. +Dependencies such as Robot are automatically installed for you when NVDA's build system Python virtual environment is set up, when running any of the high-level commands such as runsystemtests.bat, thus a developer should usually not have to worry about dependencies. + ### Running the tests -You can run the tests with `scons` or manually - -#### Scons (easier) -`scons systemTests` - -To run only specific system tests, - specify them using the `filter` variable on the command line. -This filter accepts wildcard characters. - -``` -scons systemTests filter="Read welcome dialog" -``` - -#### Manually (faster) - -SCons takes quite a long time to initialize and actually start running the tests, -if you are running the tests repeatedly consider running them manually. -These tests should be run from the windows command prompt (cmd.exe) from the root directory - of your NVDA repository. - -``` -python -m robot --argumentfile ./tests/system/robotArgs.robot ./tests/system/robot -``` -Note that the path to the tests directory is required and must be the final argument. +You can run the tests with `runsystemtests.bat`. +Running this script with no arguments will run all system tests found in tests\system\robot, against the current source copy of NVDA. +Any extra arguments provided to this script are forwarded on to Robot. To run a single test, add the `--test` argument (wildcards accepted). ``` -python -m robot --test "starts" ... +runsystemtests --test "starts" ... ``` To run all tests with a particular tag use `-i`: ``` -python -m robot -i "chrome" ... +runsystemtests -i "chrome" ... ``` Other options exit for specifying tests to run (e.g. by suite, tag, etc). -Consult `python -m robot --help` +Consult `runsystemtests --help` ### Getting the results @@ -65,11 +43,11 @@ checkbox labelled by inner element checkbox_labelled_by_inner_element ``` -When the tests are run, the option `--exclude excluded_from_build` is given to Robot. +When the tests are run, the option `--exclude excluded_from_build` is given to Robot internally. See [description of test args](#test-args) ### Test args -Common arguments (for both `scons` and AppVeyor) are kept in the `tests\system\robotArgs.robot` file. +Common arguments are kept in the `tests\system\robotArgs.robot` file. The `whichNVDA` argument allows the tests to be run against an installed copy of NVDA (first ensure it is compatible with the tests). Note valid values are: diff --git a/tests/system/requirements.txt b/tests/system/requirements.txt deleted file mode 100644 index dd3c3abbea0..00000000000 --- a/tests/system/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -nose -robotframework -robotremoteserver -robotframework-screencaplibrary diff --git a/tests/system/sconscript b/tests/system/sconscript deleted file mode 100644 index 2c716fa8280..00000000000 --- a/tests/system/sconscript +++ /dev/null @@ -1,43 +0,0 @@ -### -#This file is a part of the NVDA project. -#URL: http://www.nvaccess.org/ -#Copyright 2017 NV Access Limited. -#This program is free software: you can redistribute it and/or modify -#it under the terms of the GNU General Public License version 2.0, as published by -#the Free Software Foundation. -#This program is distributed in the hope that it will be useful, -#but WITHOUT ANY WARRANTY; without even the implied warranty of -#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -#This license can be found at: -#http://www.gnu.org/licenses/old-licenses/gpl-2.0.html -### - -import sys -Import("env") - -import os - -tests = env.get("filter") -# This should probably be limited just to the environment vars needed for NVDA to run. -# EG: testRunnerEnv = Environment( -# ENV={ -# 'PATH': os.environ['PATH'], -# 'TEMP': os.environ['TEMP'], -# 'TMP': os.environ['TMP'], -# }) -# On CI, tests are run directly (using CI env), not via SCons. See appveyor.yml. -# Running tests via SCons is included for developer convenience. Since it is likely that the -# developers environment is appropriate for running NVDA, we will just inherit it. -testRunnerEnv = Environment(ENV=os.environ) # noqa: F821 Flake8 does not recognise scons patterns. - -cmd = [ - sys.executable, "-m", "robot", - "--argumentfile", "./tests/system/robotArgs.robot", -] -if tests: - # run specific tests - cmd += ['--test="{}"'.format(tests)] - -cmd.append("./tests/system/robot") # must be the final argument -target = testRunnerEnv.Command(".", None, [cmd]) -Return('target') \ No newline at end of file diff --git a/tests/unit/sconscript b/tests/unit/sconscript deleted file mode 100644 index 0ac8d561dc5..00000000000 --- a/tests/unit/sconscript +++ /dev/null @@ -1,29 +0,0 @@ -### -#This file is a part of the NVDA project. -#URL: http://www.nvaccess.org/ -#Copyright 2017 NV Access Limited. -#This program is free software: you can redistribute it and/or modify -#it under the terms of the GNU General Public License version 2.0, as published by -#the Free Software Foundation. -#This program is distributed in the hope that it will be useful, -#but WITHOUT ANY WARRANTY; without even the implied warranty of -#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -#This license can be found at: -#http://www.gnu.org/licenses/old-licenses/gpl-2.0.html -### - -import sys - -Import("env") - -cmd = [sys.executable, "-m", "unittest"] -tests = env.get("unitTests") -if tests: - # Run specific tests. - tests = tests.split(",") - cmd += ["tests.unit." + test for test in tests] -else: - # Run all tests. - cmd.extend(("discover", "tests.unit", "-t", ".")) -target = env.Command(".", None, [cmd]) -Return('target') diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 4f24482c071..129a7e381b0 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -30,6 +30,14 @@ What's New in NVDA == Changes for Developers == - Note: this is a Add-on API compatibility breaking release. Add-ons will need to be re-tested and have their manifest updated. +- NVDA now requires Python 3.8. (#12075) +- NVDA's build system now fetches all Python dependencies with pip and stores them in a Python virtual environment. This is all done transparently. + - To build NVDA, SCons should continue to be used in the usual way. E.g. executing scons.bat in the root of the repository. Running ``py -m SCons`` is no longer supported, and ``scons.py`` has also been removed. + - To run NVDA from source, rather than executing ``source/nvda.pyw`` directly, the developer should now use ``runnvda.bat`` in the root of the repository. If you do try to execute ``source/nvda.pyw``, a message box will alert you this is no longer supported. + - To perform unit tests, execute ``rununittests.bat []`` + - To perform system tests: execute ``runsystemtests.bat [] `` + - To perform linting, execute ``runlint.bat `` + - Please refer to readme.md for more details. - `LiveText._getTextLines` has been removed. (#11639) - Instead, override `_getText` which returns a string of all text in the object. - `LiveText` objects can now calculate diffs by character. (#11639) diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index ba1579e2b7d..0c7e548ea8b 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -67,8 +67,8 @@ For details regarding exceptions, access the license document from the NVDA menu + System Requirements +[SystemRequirements] - Operating Systems: all 32-bit and 64-bit editions of Windows 7, Windows 8, Windows 8.1, Windows 10, and all Server Operating Systems starting from Windows Server 2008 R2. - - For Windows 7, NVDA requires Service Pack 1 or higher. - - For Windows Server 2008 R2, NVDA requires Service Pack 1 or higher. + - For Windows 7 and Windows Server 2008 R2 NVDA requires Service Pack 1 and the [KB3063858 https://support.microsoft.com/en-us/kb/3063858] update. + Both of these should be installed if all available updates are applied. - at least 150 MB of storage space. - diff --git a/venvUtils/ensureAndActivate.bat b/venvUtils/ensureAndActivate.bat new file mode 100644 index 00000000000..9d6f3f7664e --- /dev/null +++ b/venvUtils/ensureAndActivate.bat @@ -0,0 +1,9 @@ +@echo off +rem this script ensures the NVDA build system Python virtual environment is created and up to date, +rem and then activates it. +rem this script should be used only in the case where many commands will be executed within the environment and the shell will be eventually thrown away. +rem E.g. an Appveyor build. +py -3.8-32 "%~dp0\ensureVenv.py" +if ERRORLEVEL 1 goto :EOF +call "%~dp0\..\.venv\scripts\activate.bat" +set NVDA_VENV=%VIRTUAL_ENV% diff --git a/venvUtils/ensureVenv.py b/venvUtils/ensureVenv.py new file mode 100644 index 00000000000..3256fdfba4d --- /dev/null +++ b/venvUtils/ensureVenv.py @@ -0,0 +1,137 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2021 NV Access Limited +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + + +import sys +import os +import subprocess +import shutil +from typing import Set + +""" +A script to ensure that the NVDA build system's Python virtual environment is created and up to date. +""" + +top_dir: str = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..") +venv_path: str = os.path.join(top_dir, ".venv") +requirements_path: str = os.path.join(top_dir, "requirements.txt") +venv_orig_requirements_path: str = os.path.join(venv_path, "_requirements.txt") +venv_python_version_path: str = os.path.join(venv_path, "python_version") + + +def askYesNoQuestion(message: str) -> bool: + """ + Displays the given message to the user and accepts y or n as input. + Any other input causes the question to be asked again. + @returns: True for y and n for False. + """ + while True: + answer = input( + message + " [y/n]: " + ) + if answer == 'n': + return False + elif answer == 'y': + return True + else: + continue # ask again + + +def fetchRequirementsSet(path: str) -> Set[str]: + """ + Fetches all the package lines from a pip requirements.txt file + returning them as a set of strings. + The returned set could be compared with a set from another file + which would allow easy identification of which requirements were added or removed. + """ + with open(path, "r") as f: + lines = [x.strip() for x in f.readlines()] + lines = [x for x in lines if x and not x.isspace() and not x.startswith('#')] + return set(lines) + + +def createVenvAndPopulate(): + """ + Creates the NVDA build system's Python virtual environment and installs all required packages. + this function will overwrite any existing virtual environment found at c{venv_path}. + """ + print("Creating virtual environment...", flush=True) + subprocess.run( + [ + sys.executable, + "-m", "venv", + "--clear", + venv_path, + ], + check=True + ) + with open(venv_python_version_path, "w") as f: + f.write(sys.version) + print("Installing packages in virtual environment...", flush=True) + subprocess.run( + [ + # Activate virtual environment + os.path.join(venv_path, "scripts", "activate.bat"), + "&&", + # Ensure we have the latest version of pip + "py", "-m", "pip", + "install", "--upgrade", "pip", + "&&", + # Install required packages with pip + "py", "-m", "pip", + "install", "-r", requirements_path, + ], + check=True, + shell=True, + ) + shutil.copy(requirements_path, venv_orig_requirements_path) + + +def ensureVenvAndRequirements(): + """ + Ensures that the NVDA build system's Python virtual environment is created and up to date. + If a previous virtual environment exists but has a miss-matching Python version + or pip package requirements have changed, + The virtual environment is recreated with the updated version of Python and packages. + If a virtual environment is found but does not seem to be ours, + This function asks the user if it should be overwritten or not. + """ + if not os.path.exists(venv_path): + print("Virtual environment does not exist.") + return createVenvAndPopulate() + if ( + not os.path.exists(venv_python_version_path) + or not os.path.exists(venv_orig_requirements_path) + ): + if askYesNoQuestion( + f"Virtual environment at {venv_path} probably not created by NVDA. " + "This directory must be removed before continuing. Should it be removed?" + ): + return createVenvAndPopulate() + else: + print("Aborting") + sys.exit(1) + venv_python_version = open(venv_python_version_path, "r").read() + if venv_python_version != sys.version: + print(f"Python version changed. Was {venv_python_version}, now is {sys.version}") + return createVenvAndPopulate() + oldRequirements = fetchRequirementsSet(venv_orig_requirements_path) + newRequirements = fetchRequirementsSet(requirements_path) + addedRequirements = newRequirements - oldRequirements + if addedRequirements: + print(f"Added or changed package requirements. {addedRequirements}") + return createVenvAndPopulate() + + +if __name__ == '__main__': + # Ensure we are not inside an already active Python virtual environment. + virtualEnv = os.getenv("VIRTUAL_ENV") + if virtualEnv: + print( + "Error: It looks like another Python virtual environment is already active in this shell.\n" + "Please deactivate the current Python virtual environment and try again." + ) + sys.exit(1) + ensureVenvAndRequirements() diff --git a/venvUtils/exportPackageList.bat b/venvUtils/exportPackageList.bat new file mode 100644 index 00000000000..b98d9ee14fc --- /dev/null +++ b/venvUtils/exportPackageList.bat @@ -0,0 +1,8 @@ +@echo off +setlocal +if "%VIRTUAL_ENV%" == "" ( + call "%~dp0\ensureAndActivate.bat" + if ERRORLEVEL 1 goto :EOF +) +py -m pip freeze >%1 +endlocal diff --git a/venvUtils/venvCmd.bat b/venvUtils/venvCmd.bat new file mode 100644 index 00000000000..c0157c05ab2 --- /dev/null +++ b/venvUtils/venvCmd.bat @@ -0,0 +1,27 @@ +@echo off +rem this script executes the single given command and arguments inside the NVDA build system's Python virtual environment. +rem It activates the environment, creating / updating it first if necessary, +rem then executes the command, +rem and then finally deactivates the environment. + +rem This script also supports running in an already fully activated NVDA Python environment. +rem If this is detected, the command is executed directly instead. +if "%VIRTUAL_ENV%" NEQ "" ( + if "%NVDA_VENV%" NEQ "%VIRTUAL_ENV%" ( + echo Warning: Detected a custom Python virtual environment. + echo It is recommended to run all NVDA build system commands outside of any existing Python virtual environment, unless you really know what you are doing. + ) + echo directly calling %* + call %* + goto :EOF +) + +setlocal +echo Ensuring NVDA Python virtual environment +call "%~dp0\ensureAndActivate.bat" +if ERRORLEVEL 1 goto :EOF +echo call %* +call %* +echo Deactivating NVDA Python virtual environment +call "%~dp0\..\.venv\scripts\deactivate.bat" +endlocal From e2c6f5076be061b0f9636e3fa2935a1308e45bfa Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Thu, 11 Mar 2021 22:42:53 +0100 Subject: [PATCH 076/174] Update comtypes to 1.1.8 (#12155) * Update comtypes to 1.1.8 * Fix check_version monkeypatch * Linting --- requirements.txt | 2 +- source/comtypesMonkeyPatches.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index aaf72293f86..03417fe5e32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ SCons==4.1.0.post1 # NVDA's runtime dependencies -comtypes==1.1.7 +comtypes==1.1.8 pyserial==3.5 wxPython==4.1.1 git+git://github.com/DiffSK/configobj@3e2f4cc#egg=configobj diff --git a/source/comtypesMonkeyPatches.py b/source/comtypesMonkeyPatches.py index 99248a79a1c..089cfe74a56 100644 --- a/source/comtypesMonkeyPatches.py +++ b/source/comtypesMonkeyPatches.py @@ -130,7 +130,9 @@ def newGetTypeInfo(self,index,lcid=0): # comtypes doesn't let us disable this when running from source, so we need to monkey patch. # This is just the code from the original comtypes._check_version excluding the time check. import comtypes -def _check_version(actual): + + +def _check_version(actual, tlib_cached_mtime=None): from comtypes.tools.codegenerator import version as required if actual != required: raise ImportError("Wrong version") From a6ba80c8560434ee853d0f36593a047f17b35d75 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Fri, 12 Mar 2021 07:44:26 +1000 Subject: [PATCH 077/174] What's new: mention upgraded Python dependencies. --- user_docs/en/changes.t2t | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 129a7e381b0..d3e76f4fbb9 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -38,6 +38,11 @@ What's New in NVDA - To perform system tests: execute ``runsystemtests.bat [] `` - To perform linting, execute ``runlint.bat `` - Please refer to readme.md for more details. +- The following Python dependencies have also been upgraded: + - comtypes updated to 1.1.8. + - pySerial updated to 3.5. + - wxPython updated to 4.1.1. + - Py2exe updated to 0.10.1.0. - `LiveText._getTextLines` has been removed. (#11639) - Instead, override `_getText` which returns a string of all text in the object. - `LiveText` objects can now calculate diffs by character. (#11639) From c901b0f85eb9c9d04f6ef2156c9c576eb74531b7 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Fri, 12 Mar 2021 11:05:19 +1100 Subject: [PATCH 078/174] Deprecate import of Commands directly from speech module in favour of speech.commands (#12126) * Include deprecations of import commands directly from speech * fix flake8 issues * changes to match reproduction steps * update changelog --- source/mathPres/__init__.py | 2 +- source/mathPres/mathPlayer.py | 30 +++++--- source/sayAllHandler.py | 16 ++-- source/speech/__init__.py | 20 +---- source/speech/manager.py | 20 +---- source/speechXml.py | 5 +- source/synthDriverHandler.py | 4 +- source/synthDrivers/espeak.py | 43 +++++++---- source/synthDrivers/oneCore.py | 33 +++++--- source/synthDrivers/sapi4.py | 9 ++- source/synthDrivers/sapi5.py | 44 +++++++---- source/synthDrivers/silence.py | 4 +- .../SystemTestSpy/speechSpySynthDriver.py | 4 +- tests/unit/test_speechManager/__init__.py | 76 ++++++++++--------- .../speechManagerTestHarness.py | 14 ++-- tests/unit/test_speechXml.py | 25 ++++-- user_docs/en/changes.t2t | 1 + 17 files changed, 188 insertions(+), 162 deletions(-) diff --git a/source/mathPres/__init__.py b/source/mathPres/__init__.py index ae96e180206..a7a927606ae 100644 --- a/source/mathPres/__init__.py +++ b/source/mathPres/__init__.py @@ -33,7 +33,7 @@ def getSpeechForMathMl(self, mathMl): @param mathMl: The MathML markup. @type mathMl: str @return: A speech sequence. - @rtype: List[str, speech.SpeechCommand] + @rtype: List[str, SpeechCommand] """ raise NotImplementedError diff --git a/source/mathPres/mathPlayer.py b/source/mathPres/mathPlayer.py index 42432083973..128bb8fffb1 100644 --- a/source/mathPres/mathPlayer.py +++ b/source/mathPres/mathPlayer.py @@ -18,6 +18,16 @@ import braille import mathPres +from speech.commands import ( + PitchCommand, + VolumeCommand, + RateCommand, + LangChangeCommand, + BreakCommand, + CharacterModeCommand, + PhonemeCommand, +) + RE_MP_SPEECH = re.compile( # Break. r" ?" @@ -35,9 +45,9 @@ # Actual content. r"|(?P[^<,]+)") PROSODY_COMMANDS = { - "pitch": speech.PitchCommand, - "volume": speech.VolumeCommand, - "rate": speech.RateCommand, + "pitch": PitchCommand, + "volume": VolumeCommand, + "rate": RateCommand, } def _processMpSpeech(text, language): # MathPlayer's default rate is 180 wpm. @@ -47,16 +57,15 @@ def _processMpSpeech(text, language): breakMulti = 180.0 / wpm out = [] if language: - out.append(speech.LangChangeCommand(language)) + out.append(LangChangeCommand(language)) resetProsody = set() for m in RE_MP_SPEECH.finditer(text): if m.lastgroup == "break": - out.append(speech.BreakCommand(time=int(m.group("break")) * breakMulti)) + out.append(BreakCommand(time=int(m.group("break")) * breakMulti)) elif m.lastgroup == "char": - out.extend((speech.CharacterModeCommand(True), - m.group("char"), speech.CharacterModeCommand(False))) + out.extend((CharacterModeCommand(True), m.group("char"), CharacterModeCommand(False))) elif m.lastgroup == "comma": - out.append(speech.BreakCommand(time=100)) + out.append(BreakCommand(time=100)) elif m.lastgroup in PROSODY_COMMANDS: command = PROSODY_COMMANDS[m.lastgroup] out.append(command(multiplier=int(m.group(m.lastgroup)) / 100.0)) @@ -66,12 +75,11 @@ def _processMpSpeech(text, language): out.append(command(multiplier=1)) resetProsody.clear() elif m.lastgroup == "phonemeText": - out.append(speech.PhonemeCommand(m.group("ipa"), - text=m.group("phonemeText"))) + out.append(PhonemeCommand(m.group("ipa"), text=m.group("phonemeText"))) elif m.lastgroup == "content": out.append(m.group(0)) if language: - out.append(speech.LangChangeCommand(None)) + out.append(LangChangeCommand(None)) return out class MathPlayerInteraction(mathPres.MathInteractionNVDAObject): diff --git a/source/sayAllHandler.py b/source/sayAllHandler.py index ddda6c2e233..1490c168916 100644 --- a/source/sayAllHandler.py +++ b/source/sayAllHandler.py @@ -15,6 +15,8 @@ import queueHandler import winKernel +from speech.commands import CallbackCommand, EndUtteranceCommand + CURSOR_CARET = 0 CURSOR_REVIEW = 1 @@ -69,7 +71,7 @@ def next(self): if not obj: return # Call this method again when we start speaking this object. - callbackCommand = speech.CallbackCommand(self.next, name="say-all:next") + callbackCommand = CallbackCommand(self.next, name="say-all:next") speech.speakObject(obj, reason=controlTypes.OutputReason.SAYALL, _prefixSpeechCommand=callbackCommand) def stop(self): @@ -148,8 +150,8 @@ def nextLine(self): # No more text. if isinstance(self.reader.obj, textInfos.DocumentWithPageTurns): # Once the last line finishes reading, try turning the page. - cb = speech.CallbackCommand(self.turnPage, name="say-all:turnPage") - speech.speakWithoutPauses([cb, speech.EndUtteranceCommand()]) + cb = CallbackCommand(self.turnPage, name="say-all:turnPage") + speech.speakWithoutPauses([cb, EndUtteranceCommand()]) else: self.finish() return @@ -163,7 +165,7 @@ def nextLine(self): def _onLineReached(obj=self.reader.obj, state=state): self.lineReached(obj, bookmark, state) - cb = speech.CallbackCommand( + cb = CallbackCommand( _onLineReached, name="say-all:lineReached" ) @@ -239,11 +241,11 @@ def finish(self): # Otherwise, if a different synth is being used for say all, # we might switch synths too early and truncate the final speech. # We do this by putting a CallbackCommand at the start of a new utterance. - cb = speech.CallbackCommand(self.stop, name="say-all:stop") + cb = CallbackCommand(self.stop, name="say-all:stop") speech.speakWithoutPauses([ - speech.EndUtteranceCommand(), + EndUtteranceCommand(), cb, - speech.EndUtteranceCommand() + EndUtteranceCommand() ]) def stop(self): diff --git a/source/speech/__init__.py b/source/speech/__init__.py index f0c74c34e9e..4dc0601947c 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -34,25 +34,7 @@ EndUtteranceCommand, CharacterModeCommand, ) -from .commands import ( # noqa: F401 - # F401 imported but unused: - # The following are imported here because other files that speech.py - # previously relied on "import * from .commands" - # New commands added to commands.py should be directly imported only where needed. - # Usage of these imports is deprecated and will be removed in 2021.1 - SynthCommand, - IndexCommand, - SynthParamCommand, - BreakCommand, - BaseProsodyCommand, - VolumeCommand, - RateCommand, - PhonemeCommand, - BaseCallbackCommand, - CallbackCommand, - WaveFileCommand, - ConfigProfileTriggerCommand, -) + from . import types from .types import ( SpeechSequence, diff --git a/source/speech/manager.py b/source/speech/manager.py index b74da7d081f..efb97c7af53 100644 --- a/source/speech/manager.py +++ b/source/speech/manager.py @@ -18,25 +18,7 @@ IndexCommand, _CancellableSpeechCommand, ) -from .commands import ( # noqa: F401 - # F401 imported but unused: - # These are imported explicitly to maintain backwards compatibility and will be removed in - # 2021.1. Rather than rely on these imports, import directly from the commands module. - # New commands added to commands.py should be directly imported only where needed. - SpeechCommand, - PitchCommand, - LangChangeCommand, - BeepCommand, - CharacterModeCommand, - SynthCommand, - BreakCommand, - BaseProsodyCommand, - VolumeCommand, - RateCommand, - PhonemeCommand, - CallbackCommand, - WaveFileCommand, -) + from .priorities import Spri, SPEECH_PRIORITIES from logHandler import log from synthDriverHandler import getSynth diff --git a/source/speechXml.py b/source/speechXml.py index 0abe0be00ac..7f308694a34 100644 --- a/source/speechXml.py +++ b/source/speechXml.py @@ -13,6 +13,7 @@ from collections import namedtuple, OrderedDict import re import speech +from speech.commands import SpeechCommand from logHandler import log XML_ESCAPES = { @@ -196,7 +197,7 @@ class SpeechXmlConverter(object): Subclasses implement specific XML schemas by implementing methods which convert each speech command. The method for a speech command should be named with the prefix "convert" followed by the command's class name. For example, the handler for C{IndexCommand} should be named C{convertIndexCommand}. - These methods receive the L{speech.SpeechCommand} instance as their only argument. + These methods receive the L{SpeechCommand} instance as their only argument. They should return an appropriate XmlBalancer command. Subclasses may wish to extend L{generateBalancerCommands} to produce additional XmlBalancer commands at the start or end; @@ -210,7 +211,7 @@ def generateBalancerCommands(self, speechSequence): for item in speechSequence: if isinstance(item, str): yield item - elif isinstance(item, speech.SpeechCommand): + elif isinstance(item, SpeechCommand): name = type(item).__name__ # For example: self.convertIndexCommand func = getattr(self, "convert%s" % name, None) diff --git a/source/synthDriverHandler.py b/source/synthDriverHandler.py index a172e824c68..81a76dc1ae5 100644 --- a/source/synthDriverHandler.py +++ b/source/synthDriverHandler.py @@ -62,7 +62,7 @@ class SynthDriver(driverHandler.Driver): e.g. the L{voice} attribute is used for the L{voice} setting. These will usually be properties. L{supportedCommands} should specify what synth commands the synthesizer supports. - At a minimum, L{speech.IndexCommand} must be supported. + At a minimum, L{IndexCommand} must be supported. L{PitchCommand} must also be supported if you want capital pitch change to work; support for the pitch setting is not sufficient. L{supportedNotifications} should specify what notifications the synthesizer provides. @@ -93,7 +93,7 @@ class SynthDriver(driverHandler.Driver): #: @type: str description = "" #: The speech commands supported by the synth. - #: @type: set of L{speech.SynthCommand} subclasses. + #: @type: set of L{SynthCommand} subclasses. supportedCommands = frozenset() #: The notifications provided by the synth. #: @type: set of L{extensionPoints.Action} instances diff --git a/source/synthDrivers/espeak.py b/source/synthDrivers/espeak.py index 2f2ad27b54c..0e87abc60e9 100644 --- a/source/synthDrivers/espeak.py +++ b/source/synthDrivers/espeak.py @@ -15,6 +15,17 @@ from logHandler import log from driverHandler import BooleanDriverSetting +from speech.commands import ( + IndexCommand, + CharacterModeCommand, + LangChangeCommand, + BreakCommand, + PitchCommand, + RateCommand, + VolumeCommand, + PhonemeCommand, +) + class SynthDriver(SynthDriver): name = "espeak" description = "eSpeak NG" @@ -29,14 +40,14 @@ class SynthDriver(SynthDriver): SynthDriver.VolumeSetting(), ) supportedCommands = { - speech.IndexCommand, - speech.CharacterModeCommand, - speech.LangChangeCommand, - speech.BreakCommand, - speech.PitchCommand, - speech.RateCommand, - speech.VolumeCommand, - speech.PhonemeCommand, + IndexCommand, + CharacterModeCommand, + LangChangeCommand, + BreakCommand, + PitchCommand, + RateCommand, + VolumeCommand, + PhonemeCommand, } supportedNotifications = {synthIndexReached, synthDoneSpeaking} @@ -60,9 +71,9 @@ def _get_language(self): return self._language PROSODY_ATTRS = { - speech.PitchCommand: "pitch", - speech.VolumeCommand: "volume", - speech.RateCommand: "rate", + PitchCommand: "pitch", + VolumeCommand: "volume", + RateCommand: "rate", } IPA_TO_ESPEAK = { @@ -91,16 +102,16 @@ def speak(self,speechSequence): for item in speechSequence: if isinstance(item,str): textList.append(self._processText(item)) - elif isinstance(item,speech.IndexCommand): + elif isinstance(item, IndexCommand): textList.append(""%item.index) - elif isinstance(item,speech.CharacterModeCommand): + elif isinstance(item, CharacterModeCommand): textList.append("" if item.state else "") - elif isinstance(item,speech.LangChangeCommand): + elif isinstance(item, LangChangeCommand): if langChanged: textList.append("") textList.append(""%(item.lang if item.lang else defaultLanguage).replace('_','-')) langChanged=True - elif isinstance(item,speech.BreakCommand): + elif isinstance(item, BreakCommand): textList.append('' % item.time) elif type(item) in self.PROSODY_ATTRS: if prosody: @@ -121,7 +132,7 @@ def speak(self,speechSequence): for attr,val in prosody.items(): textList.append(' %s="%d%%"'%(attr,val)) textList.append(">") - elif isinstance(item,speech.PhonemeCommand): + elif isinstance(item, PhonemeCommand): # We can't use str.translate because we want to reject unknown characters. try: phonemes="".join([self.IPA_TO_ESPEAK[char] for char in item.ipa]) diff --git a/source/synthDrivers/oneCore.py b/source/synthDrivers/oneCore.py index 7163122bdc5..e9a9727f126 100644 --- a/source/synthDrivers/oneCore.py +++ b/source/synthDrivers/oneCore.py @@ -25,6 +25,17 @@ import winVersion import NVDAHelper +from speech.commands import ( + IndexCommand, + CharacterModeCommand, + LangChangeCommand, + BreakCommand, + PitchCommand, + RateCommand, + VolumeCommand, + PhonemeCommand, +) + #: The number of 100-nanosecond units in 1 second. HUNDRED_NS_PER_SEC = 10000000 # 1000000000 ns per sec / 100 ns ocSpeech_Callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int, ctypes.c_wchar_p) @@ -78,9 +89,9 @@ def generateBalancerCommands(self, speechSequence): yield next(commands) # OneCore didn't provide a way to set base prosody values before API version 5. # Therefore, the base values need to be set using SSML. - yield self.convertRateCommand(speech.RateCommand(multiplier=1)) - yield self.convertVolumeCommand(speech.VolumeCommand(multiplier=1)) - yield self.convertPitchCommand(speech.PitchCommand(multiplier=1)) + yield self.convertRateCommand(RateCommand(multiplier=1)) + yield self.convertVolumeCommand(VolumeCommand(multiplier=1)) + yield self.convertPitchCommand(PitchCommand(multiplier=1)) for command in commands: yield command @@ -105,14 +116,14 @@ class SynthDriver(SynthDriver): # Translators: Description for a speech synthesizer. description = _("Windows OneCore voices") supportedCommands = { - speech.IndexCommand, - speech.CharacterModeCommand, - speech.LangChangeCommand, - speech.BreakCommand, - speech.PitchCommand, - speech.RateCommand, - speech.VolumeCommand, - speech.PhonemeCommand, + IndexCommand, + CharacterModeCommand, + LangChangeCommand, + BreakCommand, + PitchCommand, + RateCommand, + VolumeCommand, + PhonemeCommand, } supportedNotifications = {synthIndexReached, synthDoneSpeaking} diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py index f130ca8ad09..8cdd5fbecef 100755 --- a/source/synthDrivers/sapi4.py +++ b/source/synthDrivers/sapi4.py @@ -17,6 +17,7 @@ import nvwave import weakref +from speech.commands import IndexCommand, SpeechCommand, CharacterModeCommand class SynthDriverBufSink(COMObject): _com_interfaces_ = [ITTSBufNotifySink] @@ -96,16 +97,16 @@ def speak(self,speechSequence): for item in speechSequence: if isinstance(item,str): textList.append(item.replace('\\','\\\\')) - elif isinstance(item,speech.IndexCommand): + elif isinstance(item, IndexCommand): textList.append("\\mrk=%d\\"%item.index) - elif isinstance(item,speech.CharacterModeCommand): + elif isinstance(item, CharacterModeCommand): textList.append("\\RmS=1\\" if item.state else "\\RmS=0\\") charMode=item.state - elif isinstance(item,speech.SpeechCommand): + elif isinstance(item, SpeechCommand): log.debugWarning("Unsupported speech command: %s"%item) else: log.error("Unknown speech: %s"%item) - if isinstance(item,speech.IndexCommand): + if isinstance(item, IndexCommand): # This is the index denoting the end of the speech sequence. self._finalIndex=item.index if charMode: diff --git a/source/synthDrivers/sapi5.py b/source/synthDrivers/sapi5.py index 45d2e60948b..386b293c33c 100644 --- a/source/synthDrivers/sapi5.py +++ b/source/synthDrivers/sapi5.py @@ -23,6 +23,18 @@ from logHandler import log import weakref +from speech.commands import ( + IndexCommand, + CharacterModeCommand, + LangChangeCommand, + BreakCommand, + PitchCommand, + RateCommand, + VolumeCommand, + PhonemeCommand, + SpeechCommand, +) + # SPAudioState enumeration SPAS_CLOSED=0 SPAS_STOP=1 @@ -124,14 +136,14 @@ def EndStream(self, streamNum, pos): class SynthDriver(SynthDriver): supportedSettings=(SynthDriver.VoiceSetting(),SynthDriver.RateSetting(),SynthDriver.PitchSetting(),SynthDriver.VolumeSetting()) supportedCommands = { - speech.IndexCommand, - speech.CharacterModeCommand, - speech.LangChangeCommand, - speech.BreakCommand, - speech.PitchCommand, - speech.RateCommand, - speech.VolumeCommand, - speech.PhonemeCommand, + IndexCommand, + CharacterModeCommand, + LangChangeCommand, + BreakCommand, + PitchCommand, + RateCommand, + VolumeCommand, + PhonemeCommand, } supportedNotifications = {synthIndexReached, synthDoneSpeaking} @@ -312,9 +324,9 @@ def outputTags(): if isinstance(item, str): outputTags() textList.append(item.replace("<", "<")) - elif isinstance(item, speech.IndexCommand): + elif isinstance(item, IndexCommand): textList.append('' % item.index) - elif isinstance(item, speech.CharacterModeCommand): + elif isinstance(item, CharacterModeCommand): if item.state: tags["spell"] = {} else: @@ -323,12 +335,12 @@ def outputTags(): except KeyError: pass tagsChanged[0] = True - elif isinstance(item, speech.BreakCommand): + elif isinstance(item, BreakCommand): textList.append('' % item.time) - elif isinstance(item, speech.PitchCommand): + elif isinstance(item, PitchCommand): tags["pitch"] = {"absmiddle": self._percentToPitch(int(pitch * item.multiplier))} tagsChanged[0] = True - elif isinstance(item, speech.VolumeCommand): + elif isinstance(item, VolumeCommand): if item.multiplier == 1: try: del tags["volume"] @@ -337,7 +349,7 @@ def outputTags(): else: tags["volume"] = {"level": int(volume * item.multiplier)} tagsChanged[0] = True - elif isinstance(item, speech.RateCommand): + elif isinstance(item, RateCommand): if item.multiplier == 1: try: del tags["rate"] @@ -346,7 +358,7 @@ def outputTags(): else: tags["rate"] = {"absspeed": self._percentToRate(int(rate * item.multiplier))} tagsChanged[0] = True - elif isinstance(item, speech.PhonemeCommand): + elif isinstance(item, PhonemeCommand): try: textList.append(u'%s' % (self._convertPhoneme(item.ipa), item.text or u"")) @@ -354,7 +366,7 @@ def outputTags(): log.debugWarning("Couldn't convert character in IPA string: %s" % item.ipa) if item.text: textList.append(item.text) - elif isinstance(item, speech.SpeechCommand): + elif isinstance(item, SpeechCommand): log.debugWarning("Unsupported speech command: %s" % item) else: log.error("Unknown speech: %s" % item) diff --git a/source/synthDrivers/silence.py b/source/synthDrivers/silence.py index 3f64adc29c2..1d37d69601d 100644 --- a/source/synthDrivers/silence.py +++ b/source/synthDrivers/silence.py @@ -5,7 +5,7 @@ #See the file COPYING for more details. import synthDriverHandler -import speech +from speech.commands import IndexCommand class SynthDriver(synthDriverHandler.SynthDriver): """A dummy synth driver used to disable speech in NVDA. @@ -23,7 +23,7 @@ def check(cls): def speak(self, speechSequence): self.lastIndex = None for item in speechSequence: - if isinstance(item, speech.IndexCommand): + if isinstance(item, IndexCommand): self.lastIndex = item.index def cancel(self): diff --git a/tests/system/libraries/SystemTestSpy/speechSpySynthDriver.py b/tests/system/libraries/SystemTestSpy/speechSpySynthDriver.py index 6f31942bc97..ee7d93c843c 100644 --- a/tests/system/libraries/SystemTestSpy/speechSpySynthDriver.py +++ b/tests/system/libraries/SystemTestSpy/speechSpySynthDriver.py @@ -12,7 +12,7 @@ import synthDriverHandler import extensionPoints -import speech +from speech.commands import IndexCommand # inform those who want to know that there is new speech post_speech = extensionPoints.Action() @@ -36,7 +36,7 @@ def check(cls): def speak(self, speechSequence): for item in speechSequence: - if isinstance(item, speech.IndexCommand): + if isinstance(item, IndexCommand): synthDriverHandler.synthIndexReached.notify(synth=self, index=item.index) synthDriverHandler.synthDoneSpeaking.notify(synth=self) post_speech.notify(speechSequence=speechSequence) diff --git a/tests/unit/test_speechManager/__init__.py b/tests/unit/test_speechManager/__init__.py index 21c3b968d75..499bc108aec 100644 --- a/tests/unit/test_speechManager/__init__.py +++ b/tests/unit/test_speechManager/__init__.py @@ -16,6 +16,8 @@ ) import speech.manager from speech.commands import ( + BeepCommand, + WaveFileCommand, PitchCommand, ConfigProfileTriggerCommand, _CancellableSpeechCommand, @@ -144,13 +146,13 @@ def testAllValuesHaveSymmetry(self): class TestExpectedClasses(unittest.TestCase): def test_expectedProsodyEqualsMatching(self): - p = speech.PitchCommand(offset=2) - e = ExpectedProsody(speech.PitchCommand(offset=2)) + p = PitchCommand(offset=2) + e = ExpectedProsody(PitchCommand(offset=2)) self.assertEqual(p, e) def test_expectedProsodyNotMatching(self): - p = speech.PitchCommand(offset=2) - e = ExpectedProsody(speech.PitchCommand(offset=5)) + p = PitchCommand(offset=2) + e = ExpectedProsody(PitchCommand(offset=5)) self.assertNotEqual(p, e) @@ -491,17 +493,17 @@ def setUp(self): speechDictHandler.initialize() # setting the synth depends on dictionary["voice"] config.conf['featureFlag']['cancelExpiredFocusSpeech'] = 2 # no - @patch.object(speech.commands.WaveFileCommand, 'run') - @patch.object(speech.commands.BeepCommand, 'run') + @patch.object(WaveFileCommand, 'run') + @patch.object(BeepCommand, 'run') def test_1(self, mock_BeepCommand_run, mock_WaveFileCommand_run): r"""Text, beep, beep, sound, text. Manual Test (in NVDA python console): wx.CallLater(500, speech.speak, [ - u"This is some speech and then comes a", speech.BeepCommand(440, 10), - u"beep. If you liked that, let's ", speech.BeepCommand(880, 10), - u"beep again. I'll speak the rest of this in a ", speech.PitchCommand(offset=50), + u"This is some speech and then comes a", BeepCommand(440, 10), + u"beep. If you liked that, let's ", BeepCommand(880, 10), + u"beep again. I'll speak the rest of this in a ", PitchCommand(offset=50), u"higher pitch. And for the finale, let's ", - speech.WaveFileCommand(r"waves\browseMode.wav"), u"play a sound." + WaveFileCommand(r"waves\browseMode.wav"), u"play a sound." ]) """ smi = SpeechManagerInteractions(self) @@ -514,7 +516,7 @@ def test_1(self, mock_BeepCommand_run, mock_WaveFileCommand_run): "beep. If you liked that, let's ", _beepCommand(880, 10, expectedToBecomeIndex=2), "beep again. I'll speak the rest of this in a ", - speech.PitchCommand(offset=50), + PitchCommand(offset=50), "higher pitch. And for the finale, let's ", _waveFileCommand(r"waves\browseMode.wav", expectedToBecomeIndex=3), "play a sound.", @@ -537,7 +539,7 @@ def test2(self): """Text, end utterance, text. Manual Test (in NVDA python console): wx.CallLater(500, speech.speak, [ - u"This is the first utterance", speech.EndUtteranceCommand(), u"And this is the second" + u"This is the first utterance", EndUtteranceCommand(), u"And this is the second" ]) """ smi = SpeechManagerInteractions(self) @@ -558,20 +560,20 @@ def test2(self): def test3(self): """Change pitch, text, end utterance, text. wx.CallLater(500, speech.speak, [ - speech.PitchCommand(offset=50), + PitchCommand(offset=50), u"This is the first utterance in a higher pitch", - speech.EndUtteranceCommand(), u"And this is the second" + EndUtteranceCommand(), u"And this is the second" ]) Expected: All should be higher pitch. """ smi = SpeechManagerInteractions(self) sequence = [ - speech.PitchCommand(offset=50), + PitchCommand(offset=50), u"This is the first utterance in a higher pitch", # EndUtterance effectively splits the sequence, two sequence numbers are returned # from smi.speak for ease of adding expectations. smi.create_EndUtteranceCommand(expectedToBecomeIndex=1), - smi.create_ExpectedProsodyCommand(speech.PitchCommand(offset=50)), + smi.create_ExpectedProsodyCommand(PitchCommand(offset=50)), u"And this is the second", smi.create_ExpectedIndex(expectedToBecomeIndex=2), ] @@ -587,7 +589,7 @@ def test_6_SPRI(self): """Two utterances at SPRI_NORMAL in same sequence. Two separate sequences at SPRI_NEXT. Manual Test (in NVDA python console): wx.CallLater(500, speech.speak, [ - u"1 2 3 ", u"4 5", speech.EndUtteranceCommand(), u"16 17 18 19 20" + u"1 2 3 ", u"4 5", EndUtteranceCommand(), u"16 17 18 19 20" ]) wx.CallLater(510, speech.speak, [u"6 7 8 9 10"], priority=speech.SPRI_NEXT) wx.CallLater(520, speech.speak, [u"11 12 13 14 15"], priority=speech.SPRI_NEXT) @@ -634,12 +636,12 @@ def test_6_SPRI(self): smi.pumpAll() smi.expect_synthSpeak(fin) - @patch.object(speech.commands.BeepCommand, 'run') + @patch.object(BeepCommand, 'run') def test_7_SPRI(self, mock_BeepCommand_run): """Utterance at SPRI_NORMAL including a beep. Utterance at SPRI_NOW. Manual Test (in NVDA python console): wx.CallLater(500, speech.speak, [ - u"Text before the beep ", speech.BeepCommand(440, 10), + u"Text before the beep ", BeepCommand(440, 10), u"text after the beep, text, text, text, text" ]) wx.CallLater(1500, speech.speak, [u"This is an interruption"], priority=speech.SPRI_NOW) @@ -731,7 +733,7 @@ def test_9_SPRI(self): smi.pumpAll() smi.expect_synthSpeak(second) - @patch.object(speech.commands.BeepCommand, 'run') + @patch.object(BeepCommand, 'run') def test_13_SPRI_interruptBeforeIndexReached(self, mock_BeepCommand_run): """The same as the other test_13, but the first index is not reached before the interruption. In this cases speech manager is expected to finish the first utterance before interrupting. @@ -767,14 +769,14 @@ def test_13_SPRI_interruptBeforeIndexReached(self, mock_BeepCommand_run): smi.pumpAll() smi.expect_synthSpeak(first) - @patch.object(speech.commands.BeepCommand, 'run') + @patch.object(BeepCommand, 'run') def test_13_SPRI_interruptAfterIndexReached(self, mock_BeepCommand_run): """Utterance at SPRI_NORMAL including a pitch change and beep. Utterance at SPRI_NOW. Manual Test (in NVDA python console): wx.CallLater(500, speech.speak, [ - speech.PitchCommand(offset=100), + PitchCommand(offset=100), u"Text before the beep ", - speech.BeepCommand(440, 10), + BeepCommand(440, 10), u"text after the beep, text, text, text, text" ]) wx.CallLater(1500, speech.speak, [u"This is an interruption"], priority=speech.SPRI_NOW) @@ -836,9 +838,9 @@ def test_4_profiles(self): t1 = sayAllHandler.SayAllProfileTrigger() t2 = appModuleHandler.AppProfileTrigger("notepad") wx.CallLater(500, speech.speak, [ - u"Testing testing ", speech.PitchCommand(offset=100), "1 2 3 4", - speech.ConfigProfileTriggerCommand(t1, True), speech.ConfigProfileTriggerCommand(t2, True), - u"5 6 7 8", speech.ConfigProfileTriggerCommand(t1, False), u"9 10 11 12" + u"Testing testing ", PitchCommand(offset=100), "1 2 3 4", + ConfigProfileTriggerCommand(t1, True), ConfigProfileTriggerCommand(t2, True), + u"5 6 7 8", ConfigProfileTriggerCommand(t1, False), u"9 10 11 12" ]) Expected: All text after 1 2 3 4 should be higher pitch. @@ -851,7 +853,7 @@ def test_4_profiles(self): smi.addMockCallMonitoring([t1.enter, t1.exit, t2.enter, t2.exit]) seq = [ "Testing testing ", - speech.PitchCommand(offset=100), + PitchCommand(offset=100), "1 2 3 4", smi.create_ExpectedIndex(1), # The preceeding index is expected, @@ -913,8 +915,8 @@ def test_5_profiles(self): import sayAllHandler trigger = sayAllHandler.SayAllProfileTrigger() wx.CallLater(500, speech.speak, [ - speech.ConfigProfileTriggerCommand(trigger, True), u"5 6 7 8", - speech.ConfigProfileTriggerCommand(trigger, False), + ConfigProfileTriggerCommand(trigger, True), u"5 6 7 8", + ConfigProfileTriggerCommand(trigger, False), u"9 10 11 12" ]) Expected: 5 6 7 8 in different profile, 9 10 11 12 with base config. @@ -955,7 +957,7 @@ def test_10_SPRI_profiles(self): import sayAllHandler; trigger = sayAllHandler.SayAllProfileTrigger(); wx.CallLater(500, speech.speak, [ - speech.ConfigProfileTriggerCommand(trigger, True), + ConfigProfileTriggerCommand(trigger, True), u"This is a normal utterance with a different profile" ]) wx.CallLater(1000, speech.speak, [u"This is an interruption"], priority=speech.SPRI_NOW) @@ -975,7 +977,7 @@ def test_10_SPRI_profiles(self): # before the first utterance can finish, it is interrupted. with smi.expectation(): interrupt = [ - speech.ConfigProfileTriggerCommand(t1, True), + ConfigProfileTriggerCommand(t1, True), "This is an interruption with a different profile", smi.create_ExpectedIndex(expectedToBecomeIndex=2) ] @@ -1001,7 +1003,7 @@ def test_11_SPRI_Profile(self): import sayAllHandler trigger = sayAllHandler.SayAllProfileTrigger() wx.CallLater(500, speech.speak, [ - speech.ConfigProfileTriggerCommand(trigger, True), + ConfigProfileTriggerCommand(trigger, True), u"This is a normal utterance with a different profile" ]) wx.CallLater(1000, speech.speak, [u"This is an interruption"], priority=speech.SPRI_NOW) @@ -1015,7 +1017,7 @@ def test_11_SPRI_Profile(self): smi.addMockCallMonitoring([t1.enter, t1.exit]) with smi.expectation(): first = [ - speech.ConfigProfileTriggerCommand(t1, True), + ConfigProfileTriggerCommand(t1, True), "This is a normal utterance with a different profile", smi.create_ExpectedIndex(expectedToBecomeIndex=1) ] @@ -1066,11 +1068,11 @@ def test_12_SPRI_profile(self): t1 = sayAllHandler.SayAllProfileTrigger() t2 = appModuleHandler.AppProfileTrigger("notepad") wx.CallLater(500, speech.speak, [ - speech.ConfigProfileTriggerCommand(t1, True), + ConfigProfileTriggerCommand(t1, True), u"This is a normal utterance with profile 1" ]) wx.CallLater(1000, speech.speak, [ - speech.ConfigProfileTriggerCommand(t2, True), + ConfigProfileTriggerCommand(t2, True), u"This is an interruption with profile 2" ], priority=speech.SPRI_NOW) Expected: Normal speaks with profile 1 but gets interrupted, interruption speaks with profile 2, @@ -1082,7 +1084,7 @@ def test_12_SPRI_profile(self): smi.addMockCallMonitoring([t1.enter, t1.exit, t2.enter, t2.exit]) with smi.expectation(): first = [ - speech.ConfigProfileTriggerCommand(t1, True), + ConfigProfileTriggerCommand(t1, True), "This is a normal utterance with profile 1", smi.create_ExpectedIndex(expectedToBecomeIndex=1) ] @@ -1094,7 +1096,7 @@ def test_12_SPRI_profile(self): # Before the first index is reached, there is an interruption with smi.expectation(): interrupt = [ - speech.ConfigProfileTriggerCommand(t2, True), + ConfigProfileTriggerCommand(t2, True), "This is an interruption with profile 2", smi.create_ExpectedIndex(expectedToBecomeIndex=2) ] diff --git a/tests/unit/test_speechManager/speechManagerTestHarness.py b/tests/unit/test_speechManager/speechManagerTestHarness.py index 102d24efbce..f7f5e370064 100644 --- a/tests/unit/test_speechManager/speechManagerTestHarness.py +++ b/tests/unit/test_speechManager/speechManagerTestHarness.py @@ -30,6 +30,10 @@ BeepCommand, WaveFileCommand, EndUtteranceCommand, + BaseProsodyCommand, + RateCommand, + VolumeCommand, + PitchCommand, ConfigProfileTriggerCommand, ) from speech.types import _IndexT @@ -60,15 +64,15 @@ class ExpectedProsody: commands that are sent to the synth. This may be as a result of resuming a previous utterance. """ expectedProsody: Union[ - speech.commands.PitchCommand, - speech.commands.RateCommand, - speech.commands.VolumeCommand + PitchCommand, + RateCommand, + VolumeCommand ] def __eq__(self, other): if type(self.expectedProsody) != type(other): return False - if isinstance(other, speech.commands.BaseProsodyCommand): + if isinstance(other, BaseProsodyCommand): return repr(other) == repr(self.expectedProsody) return False @@ -223,7 +227,7 @@ def _updateKnownSequences(self, seq) -> List[_SentSequenceIndex]: """ startOfUtteranceIndexes = set( i + 1 for i, item in enumerate(seq) - if isinstance(item, speech.commands.EndUtteranceCommand) + if isinstance(item, EndUtteranceCommand) ) startOfUtteranceIndexes.add(len(seq)) # ensure the last index is included start = 0 diff --git a/tests/unit/test_speechXml.py b/tests/unit/test_speechXml.py index 17966d04d7c..89cf45d6f1c 100644 --- a/tests/unit/test_speechXml.py +++ b/tests/unit/test_speechXml.py @@ -12,6 +12,15 @@ from speechXml import REPLACEMENT_CHAR import speech +from speech.commands import ( + PitchCommand, + VolumeCommand, + LangChangeCommand, + CharacterModeCommand, + IndexCommand, + PhonemeCommand, +) + class TestEscapeXml(unittest.TestCase): """Test the _escapeXml function. """ @@ -229,16 +238,16 @@ def test_convertComplex(self): converter = speechXml.SsmlConverter("en_US") xml = converter.convertToXml([ "t1", - speech.PitchCommand(multiplier=2), - speech.VolumeCommand(multiplier=2), + PitchCommand(multiplier=2), + VolumeCommand(multiplier=2), "t2", - speech.PitchCommand(), - speech.LangChangeCommand("de_DE"), - speech.CharacterModeCommand(True), - speech.IndexCommand(1), + PitchCommand(), + LangChangeCommand("de_DE"), + CharacterModeCommand(True), + IndexCommand(1), "c", - speech.CharacterModeCommand(False), - speech.PhonemeCommand("phIpa", text="phText") + CharacterModeCommand(False), + PhonemeCommand("phIpa", text="phText") ]) self.assertEqual(xml, '' diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index d3e76f4fbb9..d162cfbb980 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -67,6 +67,7 @@ What's New in NVDA - `winKernel.GetDateFormat` has been removed - use `winKernel.GetDateFormatEx` instead (#12139) - `gui.DriverSettingsMixin` has been removed - use `gui.AutoSettingsMixin` (#12144) - `speech.getSpeechForSpelling` has been removed - use `speech.getSpellingSpeech` (#12145) +- Commands cannot be directly imported from speech as `import speech; speech.ExampleCommand()` or `import speech.manager; speech.manager.ExampleCommand()` - use `from speech.commands import ExampleCommand` instead (#12126) = 2020.4 = From 52bf1ed60dab19d4fcd266e8d53b2462e68bbcea Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Fri, 12 Mar 2021 11:52:04 +1100 Subject: [PATCH 079/174] implement deprecation of SAYALL behaviour for speakTextInfo (#12150) * implement deprecation of SAYALL behaviour for speakTextInfo in favour of sayAllHandler * update changelog --- source/speech/__init__.py | 8 -------- user_docs/en/changes.t2t | 1 + 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/source/speech/__init__.py b/source/speech/__init__.py index 4dc0601947c..520d2400001 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -1047,14 +1047,6 @@ def speakTextInfo( suppressBlanks ) - if reason == OutputReason.SAYALL: - log.error( - "Deprecation warning: In 2021.1 speakTextInfo will no longer send speech through " - "speakWithoutPauses if reason is sayAll, as sayAllhandler does this manually now." - ) - flatSpeechGen = list(_flattenNestedSequences(speechGen)) - return _speakWithoutPauses.speakWithoutPauses(flatSpeechGen) - speechGen = GeneratorWithReturn(speechGen) for seq in speechGen: speak(seq, priority=priority) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index d162cfbb980..7d2aebb392d 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -68,6 +68,7 @@ What's New in NVDA - `gui.DriverSettingsMixin` has been removed - use `gui.AutoSettingsMixin` (#12144) - `speech.getSpeechForSpelling` has been removed - use `speech.getSpellingSpeech` (#12145) - Commands cannot be directly imported from speech as `import speech; speech.ExampleCommand()` or `import speech.manager; speech.manager.ExampleCommand()` - use `from speech.commands import ExampleCommand` instead (#12126) +- `speakTextInfo` will no longer send speech through `speakWithoutPauses` if reason is `SAYALL`, as `sayAllhandler` does this manually now. (#12150) = 2020.4 = From 4779af0d4a9ccac05dbaf328e001512639973c2a Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Tue, 16 Mar 2021 19:08:02 +1000 Subject: [PATCH 080/174] Chrome system tests: ensure the page is fully loaded (including all iframes) before testing the page (#12174) * Chrome system tests: add a load status line after the start marker, and wait for it to become "Test page load complete" before continuing with the test. This is set by the boy's onload event. * Core: queue the PostNVDAStartup notification so that it is actually executed from within the core loop once running, and after any initial focus has been reported. --- source/core.py | 5 ++++- tests/system/libraries/ChromeLib.py | 20 +++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/source/core.py b/source/core.py index e2af1a42ab5..0e77a9c6f4e 100644 --- a/source/core.py +++ b/source/core.py @@ -561,7 +561,10 @@ def run(self): log.debug("initializing updateCheck") updateCheck.initialize() log.info("NVDA initialized") - postNvdaStartup.notify() + # Queue the firing of the postNVDAStartup notification. + # This is queued so that it will run from within the core loop, + # and initial focus has been reported. + queueHandler.queueFunction(queueHandler.eventQueue, postNvdaStartup.notify) log.debug("entering wx application main loop") app.MainLoop() diff --git a/tests/system/libraries/ChromeLib.py b/tests/system/libraries/ChromeLib.py index b563fb4f2ec..d99c3fe37e4 100644 --- a/tests/system/libraries/ChromeLib.py +++ b/tests/system/libraries/ChromeLib.py @@ -62,6 +62,7 @@ def start_chrome(self, filePath): _testCaseTitle = "NVDA Browser Test Case" _beforeMarker = "Before Test Case Marker" _afterMarker = "After Test Case Marker" + _loadCompleteString = "Test page load complete" @staticmethod def _writeTestFile(testCase) -> str: @@ -76,8 +77,9 @@ def _writeTestFile(testCase) -> str: {ChromeLib._testCaseTitle} - +

{ChromeLib._beforeMarker}

+

Loading...

{testCase}

{ChromeLib._afterMarker}

@@ -109,7 +111,7 @@ def _waitForStartMarker(self, spy, lastSpeechIndex): else: # Exceeded the number of tries spy.dump_speech_to_log() builtIn.fail( - "Unable to tab to 'before sample' marker." + "Unable to locate 'before sample' marker." f" Too many attempts looking for '{ChromeLib._beforeMarker}'" " See NVDA log for full speech." ) @@ -136,7 +138,19 @@ def prepareChrome(self, testCase: str) -> None: applicationTitle = f"{self._testCaseTitle}" appTitleIndex = spy.wait_for_specific_speech(applicationTitle, afterIndex=lastSpeechIndex) self._waitForStartMarker(spy, appTitleIndex) - + # Move to the loading status line, and wait fore it to become complete + # the page has fully loaded. + spy.emulateKeyPress('downArrow') + for x in range(10): + builtIn.sleep("0.1 seconds") + actualSpeech = ChromeLib.getSpeechAfterKey('NVDA+UpArrow') + if actualSpeech == self._loadCompleteString: + break + else: # Exceeded the number of tries + spy.dump_speech_to_log() + builtIn.fail( + "Failed to wait for Test page load complete." + ) @staticmethod def getSpeechAfterKey(key) -> str: From a20e768134755f37b28aed7ba22769c1df46fb65 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Wed, 17 Mar 2021 08:11:23 +1000 Subject: [PATCH 081/174] Performance increase interacting with Visual Studio (#12170) * NVDAObjects.UIA.visualStudio.findExtraOverlayClasses: avoid fetching parent unless it is actually needed. Provides a significant performance increase. * Update what's new. --- source/NVDAObjects/UIA/VisualStudio.py | 2 +- user_docs/en/changes.t2t | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/source/NVDAObjects/UIA/VisualStudio.py b/source/NVDAObjects/UIA/VisualStudio.py index edd3ec03b8b..2e11df25032 100644 --- a/source/NVDAObjects/UIA/VisualStudio.py +++ b/source/NVDAObjects/UIA/VisualStudio.py @@ -85,7 +85,7 @@ def event_UIA_toolTipOpened(self): def findExtraOverlayClasses(obj, clsList): if obj.UIAAutomationId in _INTELLISENSE_LIST_AUTOMATION_IDS: clsList.insert(0, IntelliSenseList) - elif isinstance(obj.parent, IntelliSenseList) and obj.UIAElement.cachedClassName == "IntellisenseMenuItem": + elif obj.UIAElement.cachedClassName == "IntellisenseMenuItem" and isinstance(obj.parent, IntelliSenseList): clsList.insert(0, IntelliSenseItem) elif ( obj.UIAElement.cachedClassName == "LiveTextBlock" diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 7d2aebb392d..0124ddd8ffd 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -26,6 +26,7 @@ What's New in NVDA - In the Python Console, inserting a tab for indentation at the beginning of a non-empty input line and performing tab-completion in the middle of an input line are now supported. (#11532) - Formatting information and other browseable messages no longer present unexpected blank lines when screen layout is turned off. (#12004) - It is now possible to read comments in MS Word with UIA enabled. (#9285) +- Performance when interacting with Visual Studio has been improved. (#12171) == Changes for Developers == From ee1be353079364d4502c0f389d32ff10452d0533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Wed, 17 Mar 2021 00:14:10 +0100 Subject: [PATCH 082/174] Stop installer from attempting to delete lib directories for the version of NVDA which has just been installed (#12165) * installer: Unify usages of names from ctypes to avoid linter warnings about star imports * installer: Remove unused imports * Ensure that installer does not try to delete lib directories for the version of NVDA which is currently being installed. --- source/installer.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/source/installer.py b/source/installer.py index f1ca65e4277..b8e0ea2f44e 100644 --- a/source/installer.py +++ b/source/installer.py @@ -4,10 +4,8 @@ # See the file COPYING for more details. # Copyright (C) 2011-2019 NV Access Limited, Joseph Lee, Babbage B.V., Łukasz Golonka -from ctypes import * -from ctypes.wintypes import * +import ctypes import winreg -import threading import time import os import tempfile @@ -168,11 +166,16 @@ def removeOldLibFiles(destPath, rebootOK=False): continue for d in subdirs: path = os.path.join(parent, d) - log.debug("Removing old lib directory: %r"%path) - try: - os.rmdir(path) - except OSError: - log.warning("Failed to remove a directory no longer needed. This can be manually removed after a reboot or the installer will try removing it again next time. Directory: %r"%path) + if path != currentLibPath: + log.debug(f"Removing old lib directory: {repr(path)}") + try: + os.rmdir(path) + except OSError: + log.warning( + "Failed to remove a directory no longer needed. " + "This can be manually removed after a reboot or the installer will try" + f" removing it again next time. Directory: {repr(path)}" + ) for f in files: path = os.path.join(parent, f) log.debug("Removing old lib file: %r"%path) @@ -531,8 +534,8 @@ def tryCopyFile(sourceFilePath,destFilePath): sourceFilePath=u"\\\\?\\"+sourceFilePath if not destFilePath.startswith('\\\\'): destFilePath=u"\\\\?\\"+destFilePath - if windll.kernel32.CopyFileW(sourceFilePath,destFilePath,False)==0: - errorCode=GetLastError() + if ctypes.windll.kernel32.CopyFileW(sourceFilePath, destFilePath, False) == 0: + errorCode = ctypes.GetLastError() log.debugWarning("Unable to copy %s, error %d"%(sourceFilePath,errorCode)) if not os.path.exists(destFilePath): raise OSError("error %d copying %s to %s"%(errorCode,sourceFilePath,destFilePath)) @@ -543,8 +546,8 @@ def tryCopyFile(sourceFilePath,destFilePath): log.error("Failed to rename %s after failed overwrite"%destFilePath,exc_info=True) raise RetriableFailure("Failed to rename %s after failed overwrite"%destFilePath) winKernel.moveFileEx(tempPath,None,winKernel.MOVEFILE_DELAY_UNTIL_REBOOT) - if windll.kernel32.CopyFileW(sourceFilePath,destFilePath,False)==0: - errorCode=GetLastError() + if ctypes.windll.kernel32.CopyFileW(sourceFilePath, destFilePath, False) == 0: + errorCode = ctypes.GetLastError() raise OSError("Unable to copy file %s to %s, error %d"%(sourceFilePath,destFilePath,errorCode)) def install(shouldCreateDesktopShortcut=True,shouldRunAtLogon=True): From ef8a0d41be4a92f298f47bfd230da9414a7a44d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Wed, 17 Mar 2021 00:15:12 +0100 Subject: [PATCH 083/174] Add diff used for linting changes to the list of files ignored by git (#12169) --- tests/lint/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/lint/.gitignore b/tests/lint/.gitignore index 74720b11b0e..e7b65a16059 100644 --- a/tests/lint/.gitignore +++ b/tests/lint/.gitignore @@ -1,2 +1,3 @@ current.diff current.lint +_lint.diff From d67c3c7cd3db769aec137db376d57cad9ce1340d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Wed, 17 Mar 2021 00:16:45 +0100 Subject: [PATCH 084/174] controlTypes: Remove deprecated ROLE_EQUATION (#12164) * controlTypes: Remove deprecated ROLE_EQUATION * update changes.t2t Co-authored-by: buddsean --- source/controlTypes.py | 11 +++++------ user_docs/en/changes.t2t | 1 + 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/source/controlTypes.py b/source/controlTypes.py index b29b4da5b68..d128a825505 100644 --- a/source/controlTypes.py +++ b/source/controlTypes.py @@ -1,8 +1,8 @@ -#controlTypes.py -#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) 2007-2016 NV Access Limited, Babbage B.V. +# 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) 2007-2021 NV Access Limited, Babbage B.V. + from typing import Dict, Union, Set, Any, Optional, List from enum import Enum, auto @@ -111,7 +111,6 @@ ROLE_MENUBUTTON=102 ROLE_DROPDOWNBUTTONGRID=103 ROLE_MATH=104 -ROLE_EQUATION=ROLE_MATH # Deprecated; for backwards compatibility. ROLE_GRIP=105 ROLE_HOTKEYFIELD=106 ROLE_INDICATOR=107 diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 0124ddd8ffd..a36b9ca1b40 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -70,6 +70,7 @@ What's New in NVDA - `speech.getSpeechForSpelling` has been removed - use `speech.getSpellingSpeech` (#12145) - Commands cannot be directly imported from speech as `import speech; speech.ExampleCommand()` or `import speech.manager; speech.manager.ExampleCommand()` - use `from speech.commands import ExampleCommand` instead (#12126) - `speakTextInfo` will no longer send speech through `speakWithoutPauses` if reason is `SAYALL`, as `sayAllhandler` does this manually now. (#12150) +- `ROLE_EQUATION` has been removed from controlTypes - use `ROLE_MATH`` instead. (#12164) = 2020.4 = From 3dcba914b4e99c562281ba0661dfd4d5d9a9f960 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Wed, 17 Mar 2021 10:53:47 +1100 Subject: [PATCH 085/174] remove unused flag from horizontal sizer (#12182) --- source/gui/inputGestures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/gui/inputGestures.py b/source/gui/inputGestures.py index 8920275ed82..82dbb9808a9 100644 --- a/source/gui/inputGestures.py +++ b/source/gui/inputGestures.py @@ -608,7 +608,7 @@ def makeSettings(self, settingsSizer): bHelper.sizer.AddStretchSpacer() # Translators: The label of a button to reset all gestures in the Input Gestures dialog. resetButton = wx.Button(self, label=_("Reset to factory &defaults")) - bHelper.sizer.Add(resetButton, flag=wx.ALIGN_RIGHT) + bHelper.sizer.Add(resetButton) resetButton.Bind(wx.EVT_BUTTON, self.onReset) settingsSizer.Add(bHelper.sizer, flag=wx.EXPAND) From 04849b045aca55b1216aa0e0aa9c520df11af6c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Wed, 17 Mar 2021 01:55:23 +0100 Subject: [PATCH 086/174] don't rely on star imports from synthDriverHandler (#12172) * synthDriverHandler: Remove unused imports, standardize copyright header * sayAllHandler: Remove unused import of synthDriverHandler * settingsDialog: don't rely on star imports from synthDriverHandler * globalCommands: don't rely onn star imports from synthDriverHandler, remove unused imports * update changes.t2t Co-authored-by: buddsean --- source/globalCommands.py | 5 +---- source/gui/settingsDialogs.py | 6 +++--- source/sayAllHandler.py | 1 - source/synthDriverHandler.py | 5 ----- user_docs/en/changes.t2t | 1 + 5 files changed, 5 insertions(+), 13 deletions(-) diff --git a/source/globalCommands.py b/source/globalCommands.py index 5b7ccf10454..c85490c8a55 100644 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -6,11 +6,9 @@ # Leonard de Ruijter, Derek Riemer, Babbage B.V., Davy Kager, Ethan Holliger, Łukasz Golonka, Accessolutions, # Julien Cochuyt -import time import itertools from typing import Optional -import tones import audioDucking import touchHandler import keyboardHandler @@ -25,7 +23,6 @@ from NVDAObjects import NVDAObject, NVDAObjectTextInfo import globalVars from logHandler import log -from synthDriverHandler import * import gui import wx import config @@ -34,13 +31,13 @@ import winKernel import treeInterceptorHandler import browseMode +import languageHandler import scriptHandler from scriptHandler import script import ui import braille import brailleInput import inputCore -import virtualBuffers import characterProcessing from baseObject import ScriptableObject import core diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 869eed402d0..4b0a9dc40fa 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -6,8 +6,9 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. import logging -from abc import ABCMeta +from abc import ABCMeta, abstractmethod import copy +import os import wx from vision.providerBase import VisionEnhancementProviderSettings @@ -17,8 +18,7 @@ import winUser import logHandler import installer -from synthDriverHandler import * -from synthDriverHandler import SynthDriver, getSynth +from synthDriverHandler import changeVoice, getSynth, getSynthList, setSynth, SynthDriver import config import languageHandler import speech diff --git a/source/sayAllHandler.py b/source/sayAllHandler.py index 1490c168916..bb3b7f42ba1 100644 --- a/source/sayAllHandler.py +++ b/source/sayAllHandler.py @@ -6,7 +6,6 @@ import weakref import garbageHandler import speech -import synthDriverHandler from logHandler import log import config import controlTypes diff --git a/source/synthDriverHandler.py b/source/synthDriverHandler.py index 81a76dc1ae5..4fa8aaf732d 100644 --- a/source/synthDriverHandler.py +++ b/source/synthDriverHandler.py @@ -1,19 +1,15 @@ -# -*- coding: UTF-8 -*- -# synthDriverHandler.py # 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) 2006-2019 NV Access Limited, Peter Vágner, Aleksey Sadovoy, # Joseph Lee, Arnold Loubriat, Leonard de Ruijter -import os import pkgutil import importlib from typing import Optional from locale import strxfrm import config -import baseObject import winVersion import globalVars from logHandler import log @@ -23,7 +19,6 @@ import extensionPoints import synthDrivers import driverHandler -from driverHandler import StringParameterInfo # noqa: F401 # Backwards compatibility from abc import abstractmethod diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index a36b9ca1b40..cd53924ca7c 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -70,6 +70,7 @@ What's New in NVDA - `speech.getSpeechForSpelling` has been removed - use `speech.getSpellingSpeech` (#12145) - Commands cannot be directly imported from speech as `import speech; speech.ExampleCommand()` or `import speech.manager; speech.manager.ExampleCommand()` - use `from speech.commands import ExampleCommand` instead (#12126) - `speakTextInfo` will no longer send speech through `speakWithoutPauses` if reason is `SAYALL`, as `sayAllhandler` does this manually now. (#12150) +- The `synthDriverHandler` module is no longer star imported into `globalCommands` and `gui.settingsDialogs` - use `from synthDriverHandler import synthFunctionExample` instead. (#12172) - `ROLE_EQUATION` has been removed from controlTypes - use `ROLE_MATH`` instead. (#12164) From b3b2e1ff2467d599452277a7a9f57e5dae7aa462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Wed, 17 Mar 2021 02:42:42 +0100 Subject: [PATCH 087/174] Use DriverSetting classes from autoSettingUtils rather than from driverHandler (#12168) * Use DriverSetting classes from autoSettingUtils rather than from driverHandler * update changes.t2t Co-authored-by: buddsean --- source/braille.py | 20 +++++++---- source/driverHandler.py | 12 ------- source/synthDriverHandler.py | 34 ++++++++++++------- source/synthDrivers/espeak.py | 1 - source/synthSettingsRing.py | 13 +++---- .../NVDAHighlighter.py | 5 ++- .../_exampleProvider_autoGui.py | 14 ++++---- user_docs/en/changes.t2t | 2 ++ 8 files changed, 52 insertions(+), 49 deletions(-) diff --git a/source/braille.py b/source/braille.py index 166f5e75a9c..bbdee9f0d78 100644 --- a/source/braille.py +++ b/source/braille.py @@ -39,6 +39,8 @@ import bdDetect import queueHandler import brailleViewer +from autoSettingsUtils.driverSetting import BooleanDriverSetting, NumericDriverSetting + roleLabels = { # Translators: Displayed in braille for an object which is a @@ -2233,19 +2235,23 @@ def terminate(): class BrailleDisplayDriver(driverHandler.Driver): """Abstract base braille display driver. - Each braille display driver should be a separate Python module in the root brailleDisplayDrivers directory containing a BrailleDisplayDriver class which inherits from this base class. + Each braille display driver should be a separate Python module in the root brailleDisplayDrivers directory + containing a BrailleDisplayDriver class which inherits from this base class. At a minimum, drivers must set L{name} and L{description} and override the L{check} method. To display braille, L{numCells} and L{display} must be implemented. - Drivers should dispatch input such as presses of buttons, wheels or other controls using the L{inputCore} framework. - They should subclass L{BrailleDisplayGesture} and execute instances of those gestures using L{inputCore.manager.executeGesture}. + Drivers should dispatch input such as presses of buttons, wheels or other controls + using the L{inputCore} framework. + They should subclass L{BrailleDisplayGesture} + and execute instances of those gestures using L{inputCore.manager.executeGesture}. These gestures can be mapped in L{gestureMap}. A driver can also inherit L{baseObject.ScriptableObject} to provide display specific scripts. @see: L{hwIo} for raw serial and HID I/O. - There are factory functions to create L{driverHandler.DriverSetting} instances for common display specific settings; e.g. L{DotFirmnessSetting}. + There are factory functions to create L{autoSettingsUtils.driverSetting.DriverSetting} instances + for common display specific settings; e.g. L{DotFirmnessSetting}. """ _configSection = "braille" # Most braille display drivers don't have settings yet. @@ -2463,7 +2469,7 @@ def _handleAck(self): @classmethod def DotFirmnessSetting(cls,defaultVal,minVal,maxVal,useConfig=False): """Factory function for creating dot firmness setting.""" - return driverHandler.NumericDriverSetting( + return NumericDriverSetting( "dotFirmness", # Translators: Label for a setting in braille settings dialog. _("Dot firm&ness"), @@ -2476,7 +2482,7 @@ def DotFirmnessSetting(cls,defaultVal,minVal,maxVal,useConfig=False): @classmethod def BrailleInputSetting(cls, useConfig=True): """Factory function for creating braille input setting.""" - return driverHandler.BooleanDriverSetting( + return BooleanDriverSetting( "brailleInput", # Translators: Label for a setting in braille settings dialog. _("Braille inp&ut"), @@ -2486,7 +2492,7 @@ def BrailleInputSetting(cls, useConfig=True): @classmethod def HIDInputSetting(cls, useConfig): """Factory function for creating HID input setting.""" - return driverHandler.BooleanDriverSetting( + return BooleanDriverSetting( "hidKeyboardInput", # Translators: Label for a setting in braille settings dialog. _("&HID keyboard input simulation"), diff --git a/source/driverHandler.py b/source/driverHandler.py index cee0ea31f53..889e1adaf59 100644 --- a/source/driverHandler.py +++ b/source/driverHandler.py @@ -7,18 +7,6 @@ """Handler for driver functionality that is global to synthesizers and braille displays.""" from autoSettingsUtils.autoSettings import AutoSettings -# F401: the following imports, while unused in this file, are provided for backwards compatibility. -from autoSettingsUtils.driverSetting import ( # noqa: F401 - DriverSetting, - BooleanDriverSetting, - NumericDriverSetting, - AutoPropertyObject, -) -from autoSettingsUtils.utils import ( # noqa: F401 - UnsupportedConfigParameterError, - StringParameterInfo, -) - class Driver(AutoSettings): """ diff --git a/source/synthDriverHandler.py b/source/synthDriverHandler.py index 4fa8aaf732d..69555908325 100644 --- a/source/synthDriverHandler.py +++ b/source/synthDriverHandler.py @@ -19,10 +19,13 @@ import extensionPoints import synthDrivers import driverHandler +from autoSettingsUtils.driverSetting import BooleanDriverSetting, DriverSetting, NumericDriverSetting +from autoSettingsUtils.utils import StringParameterInfo + from abc import abstractmethod -class LanguageInfo(driverHandler.StringParameterInfo): +class LanguageInfo(StringParameterInfo): """Holds information for a particular language""" def __init__(self, id): @@ -31,7 +34,7 @@ def __init__(self, id): super(LanguageInfo, self).__init__(id, displayName) -class VoiceInfo(driverHandler.StringParameterInfo): +class VoiceInfo(StringParameterInfo): """Provides information about a single synthesizer voice. """ @@ -46,13 +49,18 @@ def __init__(self, id, displayName, language=None): class SynthDriver(driverHandler.Driver): - """Abstract base synthesizer driver. - Each synthesizer driver should be a separate Python module in the root synthDrivers directory containing a SynthDriver class which inherits from this base class. + """ + Abstract base synthesizer driver. + Each synthesizer driver should be a separate Python module in the root synthDrivers directory + containing a SynthDriver class + which inherits from this base class. At a minimum, synth drivers must set L{name} and L{description} and override the L{check} method. The methods L{speak}, L{cancel} and L{pause} should be overridden as appropriate. L{supportedSettings} should be set as appropriate for the settings supported by the synthesiser. - There are factory functions to create L{driverHandler.DriverSetting} instances for common settings; e.g. L{VoiceSetting} and L{RateSetting}. + There are factory functions to create L{autoSettingsUtils.driverSetting.DriverSetting} instances + for common settings; + e.g. L{VoiceSetting} and L{RateSetting}. Each setting is retrieved and set using attributes named after the setting; e.g. the L{voice} attribute is used for the L{voice} setting. These will usually be properties. @@ -98,7 +106,7 @@ class SynthDriver(driverHandler.Driver): @classmethod def LanguageSetting(cls): """Factory function for creating a language setting.""" - return driverHandler.DriverSetting( + return DriverSetting( "language", # Translators: Label for a setting in voice settings dialog. _("&Language"), @@ -110,7 +118,7 @@ def LanguageSetting(cls): @classmethod def VoiceSetting(cls): """Factory function for creating voice setting.""" - return driverHandler.DriverSetting( + return DriverSetting( "voice", # Translators: Label for a setting in voice settings dialog. _("&Voice"), @@ -122,7 +130,7 @@ def VoiceSetting(cls): @classmethod def VariantSetting(cls): """Factory function for creating variant setting.""" - return driverHandler.DriverSetting( + return DriverSetting( "variant", # Translators: Label for a setting in voice settings dialog. _("V&ariant"), @@ -134,7 +142,7 @@ def VariantSetting(cls): @classmethod def RateSetting(cls, minStep=1): """Factory function for creating rate setting.""" - return driverHandler.NumericDriverSetting( + return NumericDriverSetting( "rate", # Translators: Label for a setting in voice settings dialog. _("&Rate"), @@ -147,7 +155,7 @@ def RateSetting(cls, minStep=1): @classmethod def RateBoostSetting(cls): """Factory function for creating rate boost setting.""" - return driverHandler.BooleanDriverSetting( + return BooleanDriverSetting( "rateBoost", # Translators: This is the name of the rate boost voice toggle # which further increases the speaking rate when enabled. @@ -160,7 +168,7 @@ def RateBoostSetting(cls): @classmethod def VolumeSetting(cls, minStep=1): """Factory function for creating volume setting.""" - return driverHandler.NumericDriverSetting( + return NumericDriverSetting( "volume", # Translators: Label for a setting in voice settings dialog. _("V&olume"), @@ -174,7 +182,7 @@ def VolumeSetting(cls, minStep=1): @classmethod def PitchSetting(cls, minStep=1): """Factory function for creating pitch setting.""" - return driverHandler.NumericDriverSetting( + return NumericDriverSetting( "pitch", # Translators: Label for a setting in voice settings dialog. _("&Pitch"), @@ -187,7 +195,7 @@ def PitchSetting(cls, minStep=1): @classmethod def InflectionSetting(cls, minStep=1): """Factory function for creating inflection setting.""" - return driverHandler.NumericDriverSetting( + return NumericDriverSetting( "inflection", # Translators: Label for a setting in voice settings dialog. _("&Inflection"), diff --git a/source/synthDrivers/espeak.py b/source/synthDrivers/espeak.py index 0e87abc60e9..8a1b71fade7 100644 --- a/source/synthDrivers/espeak.py +++ b/source/synthDrivers/espeak.py @@ -13,7 +13,6 @@ from synthDriverHandler import SynthDriver, VoiceInfo, synthIndexReached, synthDoneSpeaking import speech from logHandler import log -from driverHandler import BooleanDriverSetting from speech.commands import ( IndexCommand, diff --git a/source/synthSettingsRing.py b/source/synthSettingsRing.py index 215dc1eeca6..39e06fe6f84 100644 --- a/source/synthSettingsRing.py +++ b/source/synthSettingsRing.py @@ -2,16 +2,17 @@ import config import synthDriverHandler import queueHandler -import driverHandler +from autoSettingsUtils.driverSetting import BooleanDriverSetting, NumericDriverSetting + class SynthSetting(baseObject.AutoPropertyObject): """a numeric synth setting. Has functions to set, get, increase and decrease its value """ def __init__(self,synth,setting,min=0,max=100): self.synth = synth self.setting = setting - self.min = setting.minVal if isinstance(setting,driverHandler.NumericDriverSetting) else min - self.max = setting.maxVal if isinstance(setting,driverHandler.NumericDriverSetting) else max - self.step = setting.normalStep if isinstance(setting,driverHandler.NumericDriverSetting) else 1 + self.min = setting.minVal if isinstance(setting, NumericDriverSetting) else min + self.max = setting.maxVal if isinstance(setting, NumericDriverSetting) else max + self.step = setting.normalStep if isinstance(setting, NumericDriverSetting) else 1 def increase(self): val = min(self.max,self.value+self.step) @@ -139,9 +140,9 @@ def updateSupportedSettings(self,synth): if not s.availableInSettingsRing: continue if prevID == s.id: #restore the last setting self._current=len(list) - if isinstance(s,driverHandler.NumericDriverSetting): + if isinstance(s, NumericDriverSetting): cls=SynthSetting - elif isinstance(s,driverHandler.BooleanDriverSetting): + elif isinstance(s, BooleanDriverSetting): cls=BooleanSynthSetting else: cls=StringSynthSetting diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 6e20d72d977..728778b82a7 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -1,4 +1,3 @@ -# visionEnhancementProviders/NVDAHighlighter.py # A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -8,6 +7,7 @@ from typing import Optional, Tuple from autoSettingsUtils.autoSettings import SupportedSettingType +from autoSettingsUtils.driverSetting import BooleanDriverSetting import vision from vision.constants import Context from vision.util import getContextRect @@ -29,7 +29,6 @@ import weakref from colors import RGB import core -import driverHandler class HighlightStyle( @@ -229,7 +228,7 @@ def getDisplayName(cls) -> str: def _get_supportedSettings(self) -> SupportedSettingType: return [ - driverHandler.BooleanDriverSetting( + BooleanDriverSetting( 'highlight%s' % (context[0].upper() + context[1:]), _contextOptionLabelsWithAccelerators[context], defaultVal=True diff --git a/source/visionEnhancementProviders/_exampleProvider_autoGui.py b/source/visionEnhancementProviders/_exampleProvider_autoGui.py index ccce002b353..446bcdaa066 100644 --- a/source/visionEnhancementProviders/_exampleProvider_autoGui.py +++ b/source/visionEnhancementProviders/_exampleProvider_autoGui.py @@ -4,7 +4,7 @@ # Copyright (C) 2019 NV Access Limited from vision import providerBase -import driverHandler +from autoSettingsUtils.driverSetting import BooleanDriverSetting, DriverSetting, NumericDriverSetting import gui from autoSettingsUtils.utils import StringParameterInfo from autoSettingsUtils.autoSettings import SupportedSettingType @@ -58,22 +58,22 @@ def getPreInitSettings(cls) -> SupportedSettingType: This is a class method because it does not rely on any instance state in this class. """ return [ - driverHandler.BooleanDriverSetting( + BooleanDriverSetting( "shouldDoX", # value stored in matching property name on class "Should Do X", defaultVal=True ), - driverHandler.BooleanDriverSetting( + BooleanDriverSetting( "shouldDoY", # value stored in matching property name on class "Should Do Y", defaultVal=False ), - driverHandler.NumericDriverSetting( + NumericDriverSetting( "amountOfZ", # value stored in matching property name on class "Amount of Z", defaultVal=11 ), - driverHandler.DriverSetting( + DriverSetting( # options for this come from a property with name generated by # f"available{settingID.capitalize()}s" # Note: @@ -100,7 +100,7 @@ def _getAvailableRuntimeSettings(self) -> SupportedSettingType: settings = [] if self._hasFeature("runtimeOnlySetting_externalValueLoad"): settings.extend([ - driverHandler.NumericDriverSetting( + NumericDriverSetting( "runtimeOnlySetting_externalValueLoad", # value stored in matching property name on class "Runtime Only amount, external value load", # no GUI default @@ -108,7 +108,7 @@ def _getAvailableRuntimeSettings(self) -> SupportedSettingType: ]) if self._hasFeature("runtimeOnlySetting_localDefault"): settings.extend([ - driverHandler.NumericDriverSetting( + NumericDriverSetting( "runtimeOnlySetting_localDefault", # value stored in matching property name on class "Runtime Only amount, local default", defaultVal=50, diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index cd53924ca7c..4d97c78c507 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -72,6 +72,8 @@ What's New in NVDA - `speakTextInfo` will no longer send speech through `speakWithoutPauses` if reason is `SAYALL`, as `sayAllhandler` does this manually now. (#12150) - The `synthDriverHandler` module is no longer star imported into `globalCommands` and `gui.settingsDialogs` - use `from synthDriverHandler import synthFunctionExample` instead. (#12172) - `ROLE_EQUATION` has been removed from controlTypes - use `ROLE_MATH`` instead. (#12164) +- `autoSettingsUtils.driverSetting` classes are removed from `driverHandler` - please use them from `autoSettingUtils.driverSetting`. (#12168) +- `autoSettingsUtils.utils` classes are removed from `driverHandler` - please use them from `autoSettingUtils.utils`. (#12168) = 2020.4 = From 4b74c7bbf6a02735acddbe5ed609c003a8cd9b2b Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Wed, 17 Mar 2021 15:16:58 +1100 Subject: [PATCH 088/174] TextInfos must inherit from BaseContentRecogTextInfo (#12157) * usage of TextInfos that do not inherit from BaseContentRecogTextInfo is removed * remove unusued code * update changes.t2t --- source/contentRecog/recogUi.py | 21 --------------------- user_docs/en/changes.t2t | 1 + 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/source/contentRecog/recogUi.py b/source/contentRecog/recogUi.py index 888c2d0cc0d..cd21ad15a53 100644 --- a/source/contentRecog/recogUi.py +++ b/source/contentRecog/recogUi.py @@ -52,29 +52,8 @@ def makeTextInfo(self, position): ti.collapse() else: ti = self.result.makeTextInfo(self, position) - if not isinstance(ti, BaseContentRecogTextInfo): - # Support of TextInfos that do not inherit from BaseContentRecogTextInfo is deprecated - # and will be removed in NVDA 2020.1. - log.warning( - f"Deprecation: {type(ti)} must inherit from {BaseContentRecogTextInfo} to avoid reference cycles." - ) - ti = self._patchTextInfo(ti) return ti - def _patchTextInfo(self, info): - # Patch TextInfos so that updateSelection/Caret updates our fake selection. - info.updateCaret = lambda: self._setSelection(info, True) - info.updateSelection = lambda: self._setSelection(info, False) - # Ensure any copies get patched too. - oldCopy = info.copy - info.copy = lambda: self._patchTextInfo(oldCopy()) - return info - - def _setSelection(self, textInfo, collapse): - self._selection = textInfo.copy() - if collapse: - self._selection.collapse() - def setFocus(self): ti = self.parent.treeInterceptor if isinstance(ti, browseMode.BrowseModeDocumentTreeInterceptor): diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 4d97c78c507..cd0f48e3f8b 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -74,6 +74,7 @@ What's New in NVDA - `ROLE_EQUATION` has been removed from controlTypes - use `ROLE_MATH`` instead. (#12164) - `autoSettingsUtils.driverSetting` classes are removed from `driverHandler` - please use them from `autoSettingUtils.driverSetting`. (#12168) - `autoSettingsUtils.utils` classes are removed from `driverHandler` - please use them from `autoSettingUtils.utils`. (#12168) +- Support of `TextInfo`s that do not inherit from `contentRecog.BaseContentRecogTextInfo` is removed. (#12157) = 2020.4 = From 5fbe05d4ddb28a255ec10dde5372f2f3b34be6e3 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 17 Mar 2021 18:56:59 +0800 Subject: [PATCH 089/174] Enable cancellable speech by default (PR #11266) First introduced with "Cancellable speech #10885" Several issues fixed with "Fix several issues in speech manager #11245" --- source/gui/settingsDialogs.py | 2 +- source/speech/manager.py | 4 ++-- user_docs/en/changes.t2t | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 4b0a9dc40fa..df5ce3dd8e6 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2648,7 +2648,7 @@ def __init__(self, parent): expiredFocusSpeechChoices = [ # Translators: Label for the 'Cancel speech for expired &focus events' combobox # in the Advanced settings panel. - _("Default (No)"), + _("Default (Yes)"), # Translators: Label for the 'Cancel speech for expired &focus events' combobox # in the Advanced settings panel. _("Yes"), diff --git a/source/speech/manager.py b/source/speech/manager.py index efb97c7af53..6e4fcd7c519 100644 --- a/source/speech/manager.py +++ b/source/speech/manager.py @@ -34,8 +34,8 @@ def _shouldCancelExpiredFocusEvents(): - # 0: default (no), 1: yes, 2: no - return config.conf["featureFlag"]["cancelExpiredFocusSpeech"] == 1 + # 0: default (yes), 1: yes, 2: no + return config.conf["featureFlag"]["cancelExpiredFocusSpeech"] != 2 def _shouldDoSpeechManagerLogging(): diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index cd0f48e3f8b..9af95ba8e2e 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -16,6 +16,9 @@ What's New in NVDA - Added more mathematical symbols to the symbols dictionary. (#11467) - The user guide, changes file, and key commands listing now have a refreshed appearance. (#12027) - "Unsupported" now reported when attempting to toggle screen layout in applications that do not support it, such as Microsoft Word. (#7297) +- 'Attempt to cancel speech for expired focus events' option in the advanced settings panel now enabled by default. + - This behaviour can be disabled by default with by setting this option to "No". + - Web applications (E.G. Gmail) no longer speak outdated information when moving focus rapidly. (#10885) == Bug Fixes == From 073826ae3eb345712561722181e4d42ca84ef118 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Thu, 18 Mar 2021 15:02:52 +1100 Subject: [PATCH 090/174] Fix right-to-left layout direction issues (#12181) * move to supported usage of wx.StaticBoxSizer standardize LTRStaticBoxSizer usage standardize LTRStaticBoxSizer usage test wx RTL workaround use wxWidgets workaround commit c59e874058cbe0f521806ae3f6ad1c59a68b6324 Author: buddsean Date: Tue Mar 16 14:09:26 2021 +1100 try parent instead of sibling method commit 11017debd9047be24938ce4745d3a234ae3e8829 Author: buddsean Date: Tue Mar 16 13:25:59 2021 +1100 remove LTRStaticBoxSizer workaround commit e17de62433affe91cc3029a25798f759a5c60702 Author: buddsean Date: Tue Mar 16 13:14:39 2021 +1100 try parent instead of sibling method commit 45514f271ec569eb2fa363afbc0f35bd28e7c8f2 Author: buddsean Date: Tue Mar 16 11:48:24 2021 +1100 try parent instead of sibling method commit 9575740f52590b9d4fa75e548b8d2b94f344a6dd Author: buddsean Date: Tue Mar 16 11:23:59 2021 +1100 try parent instead of sibling method commit 608fb66bb6c98225ac6298c22efcbd77d4a04ac6 Author: buddsean Date: Mon Mar 15 14:38:37 2021 +1100 test wx RTL workaround commit f1189bf06b07c97a398169484ffcfe2c117e6bba Author: buddsean Date: Mon Mar 15 14:03:20 2021 +1100 test wx RTL workaround * fix flake8 issues * set layout on mainframe for locale * fix lint * move out wx import * use better typing and naming * remove unused variable * apply DRY * undo risky import change * fix lint * undo risky import change, fix lint * fix nose typing read * remove unneccessary typing import * update changes.t2t --- source/core.py | 40 ++++--- source/gui/__init__.py | 26 ++--- source/gui/configProfiles.py | 17 +-- source/gui/guiHelper.py | 13 ++- source/gui/installerGui.py | 45 ++++---- source/gui/settingsDialogs.py | 191 ++++++++++++++++++---------------- source/languageHandler.py | 3 +- user_docs/en/changes.t2t | 3 + 8 files changed, 189 insertions(+), 149 deletions(-) diff --git a/source/core.py b/source/core.py index 0e77a9c6f4e..61173822c29 100644 --- a/source/core.py +++ b/source/core.py @@ -1,10 +1,11 @@ -# -*- coding: UTF-8 -*- # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2019 NV Access Limited, Aleksey Sadovoy, Christopher Toth, Joseph Lee, Peter Vágner, +# Copyright (C) 2006-2021 NV Access Limited, Aleksey Sadovoy, Christopher Toth, Joseph Lee, Peter Vágner, # Derek Riemer, Babbage B.V., Zahari Yurukov, Łukasz Golonka # This file is covered by the GNU General Public License. # See the file COPYING for more details. +from typing import Optional + """NVDA core""" RPC_E_CALL_CANCELED = -2147418110 @@ -207,6 +208,27 @@ def _setInitialFocus(): except: log.exception("Error retrieving initial focus") + +def getWxLangOrNone() -> Optional['wx.LanguageInfo']: + import languageHandler + import wx + lang = languageHandler.getLanguage() + locale = wx.Locale() + wxLang = locale.FindLanguageInfo(lang) + if not wxLang and '_' in lang: + wxLang = locale.FindLanguageInfo(lang.split('_')[0]) + # #8064: Wx might know the language, but may not actually contain a translation database for that language. + # If we try to initialize this language, wx will show a warning dialog. + # #9089: some languages (such as Aragonese) do not have language info, causing language getter to fail. + # In this case, wxLang is already set to None. + # Therefore treat these situations like wx not knowing the language at all. + if wxLang and not locale.IsAvailable(wxLang.Language): + wxLang = None + if not wxLang: + log.debugWarning("wx does not support language %s" % lang) + return wxLang + + def main(): """NVDA's core main loop. This initializes all modules such as audio, IAccessible, keyboard, mouse, and GUI. @@ -418,26 +440,14 @@ def handlePowerStatusChange(self): # initialize wxpython localization support locale = wx.Locale() - lang=languageHandler.getLanguage() - wxLang=locale.FindLanguageInfo(lang) - if not wxLang and '_' in lang: - wxLang=locale.FindLanguageInfo(lang.split('_')[0]) + wxLang = getWxLangOrNone() if hasattr(sys,'frozen'): locale.AddCatalogLookupPathPrefix(os.path.join(globalVars.appDir, "locale")) - # #8064: Wx might know the language, but may not actually contain a translation database for that language. - # If we try to initialize this language, wx will show a warning dialog. - # #9089: some languages (such as Aragonese) do not have language info, causing language getter to fail. - # In this case, wxLang is already set to None. - # Therefore treat these situations like wx not knowing the language at all. - if wxLang and not locale.IsAvailable(wxLang.Language): - wxLang=None if wxLang: try: locale.Init(wxLang.Language) except: log.error("Failed to initialize wx locale",exc_info=True) - else: - log.debugWarning("wx does not support language %s" % lang) log.debug("Initializing garbageHandler") garbageHandler.initialize() diff --git a/source/gui/__init__.py b/source/gui/__init__.py index 368e537acd5..6cbe8c2ac02 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -596,6 +596,10 @@ def initialize(): if mainFrame: raise RuntimeError("GUI already initialized") mainFrame = MainFrame() + wxLang = core.getWxLangOrNone() + if wxLang: + # otherwise the system default will be used + mainFrame.SetLayoutDirection(wxLang.LayoutDirection) wx.GetApp().SetTopWindow(mainFrame) # In wxPython >= 4.1, # wx.CallAfter no longer executes callbacks while NVDA's main thread is within apopup menu or message box. @@ -725,14 +729,10 @@ def __init__(self, parent): welcomeTextDetail = wx.StaticText(self, wx.ID_ANY, self.WELCOME_MESSAGE_DETAIL) mainSizer.Add(welcomeTextDetail,border=20,flag=wx.EXPAND|wx.LEFT|wx.RIGHT) - optionsSizer = wx.StaticBoxSizer( - wx.StaticBox( - self, - # Translators: The label for a group box containing the NVDA welcome dialog options. - label=_("Options") - ), - wx.VERTICAL - ) + # Translators: The label for a group box containing the NVDA welcome dialog options. + optionsLabel = _("Options") + optionsSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=optionsLabel) + optionsBox = optionsSizer.GetStaticBox() sHelper = guiHelper.BoxSizerHelper(self, sizer=optionsSizer) # Translators: The label of a combobox in the Welcome dialog. kbdLabelText = _("&Keyboard layout:") @@ -747,17 +747,18 @@ def __init__(self, parent): log.error("Could not set Keyboard layout list to current layout",exc_info=True) # Translators: The label of a checkbox in the Welcome dialog. capsAsNVDAModifierText = _("&Use CapsLock as an NVDA modifier key") - self.capsAsNVDAModifierCheckBox = sHelper.addItem(wx.CheckBox(self, label=capsAsNVDAModifierText)) + self.capsAsNVDAModifierCheckBox = sHelper.addItem(wx.CheckBox(optionsBox, label=capsAsNVDAModifierText)) self.capsAsNVDAModifierCheckBox.SetValue(config.conf["keyboard"]["useCapsLockAsNVDAModifierKey"]) # Translators: The label of a checkbox in the Welcome dialog. startAfterLogonText = _("St&art NVDA after I sign in") - self.startAfterLogonCheckBox = sHelper.addItem(wx.CheckBox(self, label=startAfterLogonText)) + self.startAfterLogonCheckBox = sHelper.addItem(wx.CheckBox(optionsBox, label=startAfterLogonText)) self.startAfterLogonCheckBox.Value = config.getStartAfterLogon() if globalVars.appArgs.secure or config.isAppX or not config.isInstalledCopy(): self.startAfterLogonCheckBox.Disable() # Translators: The label of a checkbox in the Welcome dialog. showWelcomeDialogAtStartupText = _("&Show this dialog when NVDA starts") - self.showWelcomeDialogAtStartupCheckBox = sHelper.addItem(wx.CheckBox(self, label=showWelcomeDialogAtStartupText)) + _showWelcomeDialogAtStartupCheckBox = wx.CheckBox(optionsBox, label=showWelcomeDialogAtStartupText) + self.showWelcomeDialogAtStartupCheckBox = sHelper.addItem(_showWelcomeDialogAtStartupCheckBox) self.showWelcomeDialogAtStartupCheckBox.SetValue(config.conf["general"]["showWelcomeDialogAtStartup"]) mainSizer.Add(optionsSizer, border=guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL) mainSizer.Add(self.CreateButtonSizer(wx.OK), border=guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL|wx.ALIGN_RIGHT) @@ -810,7 +811,8 @@ def __init__(self, parent): # Translators: The label of the license text which will be shown when NVDA installation program starts. groupLabel = _("License Agreement") - sizer = sHelper.addItem(wx.StaticBoxSizer(wx.StaticBox(self, label=groupLabel), wx.VERTICAL)) + sizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=groupLabel) + sHelper.addItem(sizer) licenseTextCtrl = wx.TextCtrl(self, size=(500, 400), style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH) licenseTextCtrl.Value = codecs.open(getDocFilePath("copying.txt", False), "r", encoding="UTF-8").read() sizer.Add(licenseTextCtrl) diff --git a/source/gui/configProfiles.py b/source/gui/configProfiles.py index ceb20ae0b3c..63417fad3c3 100644 --- a/source/gui/configProfiles.py +++ b/source/gui/configProfiles.py @@ -42,13 +42,16 @@ def __init__(self, parent): mainSizer = wx.BoxSizer(wx.VERTICAL) sHelper = guiHelper.BoxSizerHelper(self,orientation=wx.VERTICAL) - profilesListGroupSizer = wx.StaticBoxSizer(wx.StaticBox(self), wx.HORIZONTAL) + profilesListGroupSizer = wx.StaticBoxSizer(wx.HORIZONTAL, self) + profilesListBox = profilesListGroupSizer.GetStaticBox() profilesListGroupContents = wx.BoxSizer(wx.HORIZONTAL) #contains the profile list and activation button in vertical arrangement. changeProfilesSizer = wx.BoxSizer(wx.VERTICAL) - item = self.profileList = wx.ListBox(self, - choices=[self.getProfileDisplay(name, includeStates=True) for name in self.profileNames]) + item = self.profileList = wx.ListBox( + profilesListBox, + choices=[self.getProfileDisplay(name, includeStates=True) for name in self.profileNames] + ) self.bindHelpEvent("ProfilesBasicManagement", self.profileList) item.Bind(wx.EVT_LISTBOX, self.onProfileListChoice) item.Selection = self.profileNames.index(config.conf.profiles[-1].name) @@ -56,7 +59,7 @@ def __init__(self, parent): changeProfilesSizer.AddSpacer(guiHelper.SPACE_BETWEEN_BUTTONS_VERTICAL) - self.changeStateButton = wx.Button(self) + self.changeStateButton = wx.Button(profilesListBox) self.bindHelpEvent("ConfigProfileManual", self.changeStateButton) self.changeStateButton.Bind(wx.EVT_BUTTON, self.onChangeState) self.AffirmativeId = self.changeStateButton.Id @@ -68,17 +71,17 @@ def __init__(self, parent): buttonHelper = guiHelper.ButtonHelper(wx.VERTICAL) # Translators: The label of a button to create a new configuration profile. - newButton = buttonHelper.addButton(self, label=_("&New")) + newButton = buttonHelper.addButton(profilesListBox, label=_("&New")) self.bindHelpEvent("ProfilesCreating", newButton) newButton.Bind(wx.EVT_BUTTON, self.onNew) # Translators: The label of a button to rename a configuration profile. - self.renameButton = buttonHelper.addButton(self, label=_("&Rename")) + self.renameButton = buttonHelper.addButton(profilesListBox, label=_("&Rename")) self.bindHelpEvent("ProfilesBasicManagement", self.renameButton) self.renameButton.Bind(wx.EVT_BUTTON, self.onRename) # Translators: The label of a button to delete a configuration profile. - self.deleteButton = buttonHelper.addButton(self, label=_("&Delete")) + self.deleteButton = buttonHelper.addButton(profilesListBox, label=_("&Delete")) self.bindHelpEvent("ProfilesBasicManagement", self.deleteButton) self.deleteButton.Bind(wx.EVT_BUTTON, self.onDelete) diff --git a/source/gui/guiHelper.py b/source/gui/guiHelper.py index b33edad2875..7531d682300 100644 --- a/source/gui/guiHelper.py +++ b/source/gui/guiHelper.py @@ -324,7 +324,10 @@ def addLabeledControl(self, labelText, wxCtrlClass, **kwargs): Relies on guiHelper.LabeledControlHelper and thus guiHelper.associateElements, and therefore inherits any limitations from there. """ - labeledControl = LabeledControlHelper(self._parent, labelText, wxCtrlClass, **kwargs) + parent = self._parent + if isinstance(self.sizer, wx.StaticBoxSizer): + parent = self.sizer.GetStaticBox() + labeledControl = LabeledControlHelper(parent, labelText, wxCtrlClass, **kwargs) if(isinstance(labeledControl.control, (wx.ListCtrl,wx.ListBox,wx.TreeCtrl))): self.addItem(labeledControl.sizer, flag=wx.EXPAND, proportion=1) else: @@ -345,6 +348,9 @@ def addDialogDismissButtons(self, buttons, separated=False): Should be set to L{False} for message or single input dialogs, L{True} otherwise. @type separated: L{bool} """ + parent = self._parent + if isinstance(self.sizer, wx.StaticBoxSizer): + parent = self.sizer.GetStaticBox() if self.sizer.GetOrientation() != wx.VERTICAL: raise NotImplementedError( "Adding dialog dismiss buttons to a horizontal BoxSizerHelper is not implemented." @@ -354,11 +360,11 @@ def addDialogDismissButtons(self, buttons, separated=False): elif isinstance(buttons, (wx.Sizer, wx.Button)): toAdd = buttons elif isinstance(buttons, int): - toAdd = self._parent.CreateButtonSizer(buttons) + toAdd = parent.CreateButtonSizer(buttons) else: raise NotImplementedError("Unknown type: {}".format(buttons)) if separated: - self.addItem(wx.StaticLine(self._parent), flag=wx.EXPAND) + self.addItem(wx.StaticLine(parent), flag=wx.EXPAND) self.addItem(toAdd, flag=wx.ALIGN_RIGHT) self.dialogDismissButtonsAdded = True return buttons @@ -366,4 +372,3 @@ def addDialogDismissButtons(self, buttons, separated=False): class SIPABCMeta(wx.siplib.wrappertype, ABCMeta): """Meta class to be used for wx subclasses with abstract methods.""" pass - diff --git a/source/gui/installerGui.py b/source/gui/installerGui.py index 8c7fd3e6377..8fbd861f935 100644 --- a/source/gui/installerGui.py +++ b/source/gui/installerGui.py @@ -175,18 +175,15 @@ def __init__(self, parent, isUpdate): self.bindHelpEvent("InstallWithIncompatibleAddons", self.confirmationCheckbox) self.confirmationCheckbox.SetFocus() - optionsSizer = guiHelper.BoxSizerHelper(self, sizer=sHelper.addItem(wx.StaticBoxSizer( - wx.StaticBox( - self, - # Translators: The label for a group box containing the NVDA installation dialog options. - label=_("Options") - ), - wx.VERTICAL - ))) + # Translators: The label for a group box containing the NVDA installation dialog options. + optionsLabel = _("Options") + optionsHelper = sHelper.addItem(wx.StaticBoxSizer(wx.VERTICAL, self, label=optionsLabel)) + optionsSizer = guiHelper.BoxSizerHelper(self, sizer=optionsHelper) + optionsBox = optionsSizer.GetStaticBox() # Translators: The label of a checkbox option in the Install NVDA dialog. startOnLogonText = _("Use NVDA during sign-in") - self.startOnLogonCheckbox = optionsSizer.addItem(wx.CheckBox(self, label=startOnLogonText)) + self.startOnLogonCheckbox = optionsSizer.addItem(wx.CheckBox(optionsBox, label=startOnLogonText)) self.bindHelpEvent("StartAtWindowsLogon", self.startOnLogonCheckbox) if globalVars.appArgs.enableStartOnLogon is not None: self.startOnLogonCheckbox.Value = globalVars.appArgs.enableStartOnLogon @@ -197,19 +194,22 @@ def __init__(self, parent, isUpdate): if self.isUpdate and shortcutIsPrevInstalled: # Translators: The label of a checkbox option in the Install NVDA dialog. keepShortCutText = _("&Keep existing desktop shortcut") - self.createDesktopShortcutCheckbox = optionsSizer.addItem(wx.CheckBox(self, label=keepShortCutText)) + keepShortCutBox = wx.CheckBox(optionsBox, label=keepShortCutText) + self.createDesktopShortcutCheckbox = optionsSizer.addItem(keepShortCutBox) else: # Translators: The label of the option to create a desktop shortcut in the Install NVDA dialog. # If the shortcut key has been changed for this locale, # this change must also be reflected here. createShortcutText = _("Create &desktop icon and shortcut key (control+alt+n)") - self.createDesktopShortcutCheckbox = optionsSizer.addItem(wx.CheckBox(self, label=createShortcutText)) + createShortcutBox = wx.CheckBox(optionsBox, label=createShortcutText) + self.createDesktopShortcutCheckbox = optionsSizer.addItem(createShortcutBox) self.bindHelpEvent("CreateDesktopShortcut", self.createDesktopShortcutCheckbox) self.createDesktopShortcutCheckbox.Value = shortcutIsPrevInstalled if self.isUpdate else True # Translators: The label of a checkbox option in the Install NVDA dialog. createPortableText = _("Copy &portable configuration to current user account") - self.copyPortableConfigCheckbox = optionsSizer.addItem(wx.CheckBox(self, label=createPortableText)) + createPortableBox = wx.CheckBox(optionsBox, label=createPortableText) + self.copyPortableConfigCheckbox = optionsSizer.addItem(createPortableBox) self.bindHelpEvent("CopyPortableConfigurationToCurrentUserAccount", self.copyPortableConfigCheckbox) self.copyPortableConfigCheckbox.Value = bool(globalVars.appArgs.copyPortableConfig) if globalVars.appArgs.copyPortableConfig is None: @@ -220,12 +220,12 @@ def __init__(self, parent, isUpdate): bHelper = sHelper.addDialogDismissButtons(guiHelper.ButtonHelper(wx.HORIZONTAL)) if shouldAskAboutAddons: # Translators: The label of a button to launch the add-on compatibility review dialog. - reviewAddonButton = bHelper.addButton(self, label=_("&Review add-ons...")) + reviewAddonButton = bHelper.addButton(optionsBox, label=_("&Review add-ons...")) self.bindHelpEvent("InstallWithIncompatibleAddons", reviewAddonButton) reviewAddonButton.Bind(wx.EVT_BUTTON, self.onReviewAddons) # Translators: The label of a button to continue with the operation. - continueButton = bHelper.addButton(self, label=_("&Continue"), id=wx.ID_OK) + continueButton = bHelper.addButton(optionsBox, label=_("&Continue"), id=wx.ID_OK) continueButton.SetDefault() continueButton.Bind(wx.EVT_BUTTON, self.onInstall) if shouldAskAboutAddons: @@ -235,7 +235,7 @@ def __init__(self, parent, isUpdate): ) continueButton.Enable(False) - bHelper.addButton(self, id=wx.ID_CANCEL) + bHelper.addButton(optionsBox, id=wx.ID_CANCEL) # If we bind this using button.Bind, it fails to trigger when the dialog is closed. self.Bind(wx.EVT_BUTTON, self.onCancel, id=wx.ID_CANCEL) @@ -349,35 +349,38 @@ def __init__(self, parent): # Translators: The label of a grouping containing controls to select the destination directory # in the Create Portable NVDA dialog. directoryGroupText = _("Portable &directory:") - groupHelper = sHelper.addItem(gui.guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(wx.StaticBox(self, label=directoryGroupText), wx.VERTICAL))) + groupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=directoryGroupText) + groupHelper = sHelper.addItem(gui.guiHelper.BoxSizerHelper(self, sizer=groupSizer)) + groupBox = groupSizer.GetStaticBox() # Translators: The label of a button to browse for a directory. browseText = _("Browse...") # Translators: The title of the dialog presented when browsing for the # destination directory when creating a portable copy of NVDA. dirDialogTitle = _("Select portable directory") - directoryEntryControl = groupHelper.addItem(gui.guiHelper.PathSelectionHelper(self, browseText, dirDialogTitle)) + directoryPathHelper = gui.guiHelper.PathSelectionHelper(groupBox, browseText, dirDialogTitle) + directoryEntryControl = groupHelper.addItem(directoryPathHelper) self.portableDirectoryEdit = directoryEntryControl.pathControl if globalVars.appArgs.portablePath: self.portableDirectoryEdit.Value = globalVars.appArgs.portablePath # Translators: The label of a checkbox option in the Create Portable NVDA dialog. copyConfText = _("Copy current &user configuration") - self.copyUserConfigCheckbox = sHelper.addItem(wx.CheckBox(self, label=copyConfText)) + self.copyUserConfigCheckbox = sHelper.addItem(wx.CheckBox(groupBox, label=copyConfText)) self.copyUserConfigCheckbox.Value = False if globalVars.appArgs.launcher: self.copyUserConfigCheckbox.Disable() # Translators: The label of a checkbox option in the Create Portable NVDA dialog. startAfterCreateText = _("&Start the new portable copy after creation") - self.startAfterCreateCheckbox = sHelper.addItem(wx.CheckBox(self, label=startAfterCreateText)) + self.startAfterCreateCheckbox = sHelper.addItem(wx.CheckBox(groupBox, label=startAfterCreateText)) self.startAfterCreateCheckbox.Value = False bHelper = sHelper.addDialogDismissButtons(gui.guiHelper.ButtonHelper(wx.HORIZONTAL), separated=True) - continueButton = bHelper.addButton(self, label=_("&Continue"), id=wx.ID_OK) + continueButton = bHelper.addButton(groupBox, label=_("&Continue"), id=wx.ID_OK) continueButton.SetDefault() continueButton.Bind(wx.EVT_BUTTON, self.onCreatePortable) - bHelper.addButton(self, id=wx.ID_CANCEL) + bHelper.addButton(groupBox, id=wx.ID_CANCEL) # If we bind this using button.Bind, it fails to trigger when the dialog is closed. self.Bind(wx.EVT_BUTTON, self.onCancel, id=wx.ID_CANCEL) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index df5ce3dd8e6..8b012bda4a2 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -909,8 +909,9 @@ def makeSettings(self, settingsSizer): settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) # Translators: A label for the synthesizer on the speech panel. synthLabel = _("&Synthesizer") - synthBox = wx.StaticBox(self, label=synthLabel) - synthGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(synthBox, wx.HORIZONTAL)) + synthBoxSizer = wx.StaticBoxSizer(wx.HORIZONTAL, self, label=synthLabel) + synthBox = synthBoxSizer.GetStaticBox() + synthGroup = guiHelper.BoxSizerHelper(self, sizer=synthBoxSizer) settingsSizerHelper.addItem(synthGroup) # Use a ExpandoTextCtrl because even when readonly it accepts focus from keyboard, which @@ -919,12 +920,17 @@ def makeSettings(self, settingsSizer): # and a vertical scroll bar. This is not neccessary for the single line of text we wish to # display here. synthDesc = getSynth().description - self.synthNameCtrl = ExpandoTextCtrl(self, size=(self.scaleSize(250), -1), value=synthDesc, style=wx.TE_READONLY) + self.synthNameCtrl = ExpandoTextCtrl( + synthBox, + size=(self.scaleSize(250), -1), + value=synthDesc, + style=wx.TE_READONLY, + ) self.synthNameCtrl.Bind(wx.EVT_CHAR_HOOK, self._enterTriggersOnChangeSynth) # Translators: This is the label for the button used to change synthesizer, # it appears in the context of a synthesizer group on the speech settings panel. - changeSynthBtn = wx.Button(self, label=_("C&hange...")) + changeSynthBtn = wx.Button(synthBox, label=_("C&hange...")) self.bindHelpEvent("SpeechSettingsChange", self.synthNameCtrl) self.bindHelpEvent("SpeechSettingsChange", changeSynthBtn) synthGroup.addItem( @@ -2116,32 +2122,34 @@ def makeSettings(self, settingsSizer): # Translators: This is the label for a group of document formatting options in the # document formatting settings panel fontGroupText = _("Font") - fontGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(wx.StaticBox(self, label=fontGroupText), wx.VERTICAL)) + fontGroupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=fontGroupText) + fontGroupBox = fontGroupSizer.GetStaticBox() + fontGroup = guiHelper.BoxSizerHelper(self, sizer=fontGroupSizer) sHelper.addItem(fontGroup) # Translators: This is the label for a checkbox in the # document formatting settings panel. fontNameText = _("&Font name") - self.fontNameCheckBox=fontGroup.addItem(wx.CheckBox(self, label=fontNameText)) + self.fontNameCheckBox = fontGroup.addItem(wx.CheckBox(fontGroupBox, label=fontNameText)) self.fontNameCheckBox.SetValue(config.conf["documentFormatting"]["reportFontName"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. fontSizeText = _("Font &size") - self.fontSizeCheckBox=fontGroup.addItem(wx.CheckBox(self,label=fontSizeText)) + self.fontSizeCheckBox = fontGroup.addItem(wx.CheckBox(fontGroupBox, label=fontSizeText)) self.fontSizeCheckBox.SetValue(config.conf["documentFormatting"]["reportFontSize"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. fontAttributesText = _("Font attrib&utes") - self.fontAttrsCheckBox=fontGroup.addItem(wx.CheckBox(self,label=fontAttributesText)) + self.fontAttrsCheckBox = fontGroup.addItem(wx.CheckBox(fontGroupBox, label=fontAttributesText)) self.fontAttrsCheckBox.SetValue(config.conf["documentFormatting"]["reportFontAttributes"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. superscriptsAndSubscriptsText = _("Su&perscripts and subscripts") self.superscriptsAndSubscriptsCheckBox = fontGroup.addItem( - wx.CheckBox(self, label=superscriptsAndSubscriptsText) + wx.CheckBox(fontGroupBox, label=superscriptsAndSubscriptsText) ) self.superscriptsAndSubscriptsCheckBox.SetValue( config.conf["documentFormatting"]["reportSuperscriptsAndSubscripts"] @@ -2150,14 +2158,14 @@ def makeSettings(self, settingsSizer): # Translators: This is the label for a checkbox in the # document formatting settings panel. emphasisText=_("E&mphasis") - self.emphasisCheckBox=fontGroup.addItem(wx.CheckBox(self,label=emphasisText)) + self.emphasisCheckBox = fontGroup.addItem(wx.CheckBox(fontGroupBox, label=emphasisText)) self.emphasisCheckBox.SetValue(config.conf["documentFormatting"]["reportEmphasis"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. highlightText = _("Mar&ked (highlighted text)") self.highlightCheckBox = fontGroup.addItem( - wx.CheckBox(self, label=highlightText) + wx.CheckBox(fontGroupBox, label=highlightText) ) self.highlightCheckBox.SetValue( config.conf["documentFormatting"]["reportHighlight"] @@ -2166,55 +2174,59 @@ def makeSettings(self, settingsSizer): # Translators: This is the label for a checkbox in the # document formatting settings panel. styleText =_("St&yle") - self.styleCheckBox=fontGroup.addItem(wx.CheckBox(self,label=styleText)) + self.styleCheckBox = fontGroup.addItem(wx.CheckBox(fontGroupBox, label=styleText)) self.styleCheckBox.SetValue(config.conf["documentFormatting"]["reportStyle"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. colorsText = _("&Colors") - self.colorCheckBox=fontGroup.addItem(wx.CheckBox(self,label=colorsText)) + self.colorCheckBox = fontGroup.addItem(wx.CheckBox(fontGroupBox, label=colorsText)) self.colorCheckBox.SetValue(config.conf["documentFormatting"]["reportColor"]) # Translators: This is the label for a group of document formatting options in the # document formatting settings panel documentInfoGroupText = _("Document information") - docInfoGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(wx.StaticBox(self, label=documentInfoGroupText), wx.VERTICAL)) + docInfoSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=documentInfoGroupText) + docInfoBox = docInfoSizer.GetStaticBox() + docInfoGroup = guiHelper.BoxSizerHelper(self, sizer=docInfoSizer) sHelper.addItem(docInfoGroup) # Translators: This is the label for a checkbox in the # document formatting settings panel. commentsText = _("No&tes and comments") - self.commentsCheckBox=docInfoGroup.addItem(wx.CheckBox(self,label=commentsText)) + self.commentsCheckBox = docInfoGroup.addItem(wx.CheckBox(docInfoBox, label=commentsText)) self.commentsCheckBox.SetValue(config.conf["documentFormatting"]["reportComments"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. revisionsText = _("&Editor revisions") - self.revisionsCheckBox=docInfoGroup.addItem(wx.CheckBox(self,label=revisionsText)) + self.revisionsCheckBox = docInfoGroup.addItem(wx.CheckBox(docInfoBox, label=revisionsText)) self.revisionsCheckBox.SetValue(config.conf["documentFormatting"]["reportRevisions"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. spellingErrorText = _("Spelling e&rrors") - self.spellingErrorsCheckBox=docInfoGroup.addItem(wx.CheckBox(self,label=spellingErrorText)) + self.spellingErrorsCheckBox = docInfoGroup.addItem(wx.CheckBox(docInfoBox, label=spellingErrorText)) self.spellingErrorsCheckBox.SetValue(config.conf["documentFormatting"]["reportSpellingErrors"]) # Translators: This is the label for a group of document formatting options in the # document formatting settings panel pageAndSpaceGroupText = _("Pages and spacing") - pageAndSpaceGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(wx.StaticBox(self, label=pageAndSpaceGroupText), wx.VERTICAL)) + pageAndSpaceSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=pageAndSpaceGroupText) + pageAndSpaceBox = pageAndSpaceSizer.GetStaticBox() + pageAndSpaceGroup = guiHelper.BoxSizerHelper(self, sizer=pageAndSpaceSizer) sHelper.addItem(pageAndSpaceGroup) # Translators: This is the label for a checkbox in the # document formatting settings panel. pageText = _("&Pages") - self.pageCheckBox=pageAndSpaceGroup.addItem(wx.CheckBox(self,label=pageText)) + self.pageCheckBox = pageAndSpaceGroup.addItem(wx.CheckBox(pageAndSpaceBox, label=pageText)) self.pageCheckBox.SetValue(config.conf["documentFormatting"]["reportPage"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. lineText = _("Line &numbers") - self.lineNumberCheckBox=pageAndSpaceGroup.addItem(wx.CheckBox(self,label=lineText)) + self.lineNumberCheckBox = pageAndSpaceGroup.addItem(wx.CheckBox(pageAndSpaceBox, label=lineText)) self.lineNumberCheckBox.SetValue(config.conf["documentFormatting"]["reportLineNumber"]) # Translators: This is the label for a combobox controlling the reporting of line indentation in the @@ -2242,40 +2254,46 @@ def makeSettings(self, settingsSizer): # Translators: This message is presented in the document formatting settings panelue # If this option is selected, NVDA will report paragraph indentation if available. paragraphIndentationText = _("&Paragraph indentation") - self.paragraphIndentationCheckBox=pageAndSpaceGroup.addItem(wx.CheckBox(self,label=paragraphIndentationText)) + _paragraphIndentationCheckBox = wx.CheckBox(pageAndSpaceBox, label=paragraphIndentationText) + self.paragraphIndentationCheckBox = pageAndSpaceGroup.addItem(_paragraphIndentationCheckBox) self.paragraphIndentationCheckBox.SetValue(config.conf["documentFormatting"]["reportParagraphIndentation"]) # Translators: This message is presented in the document formatting settings panelue # If this option is selected, NVDA will report line spacing if available. lineSpacingText=_("&Line spacing") - self.lineSpacingCheckBox=pageAndSpaceGroup.addItem(wx.CheckBox(self,label=lineSpacingText)) + _lineSpacingCheckBox = wx.CheckBox(pageAndSpaceBox, label=lineSpacingText) + self.lineSpacingCheckBox = pageAndSpaceGroup.addItem(_lineSpacingCheckBox) self.lineSpacingCheckBox.SetValue(config.conf["documentFormatting"]["reportLineSpacing"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. alignmentText = _("&Alignment") - self.alignmentCheckBox=pageAndSpaceGroup.addItem(wx.CheckBox(self,label=alignmentText)) + self.alignmentCheckBox = pageAndSpaceGroup.addItem(wx.CheckBox(pageAndSpaceBox, label=alignmentText)) self.alignmentCheckBox.SetValue(config.conf["documentFormatting"]["reportAlignment"]) # Translators: This is the label for a group of document formatting options in the # document formatting settings panel tablesGroupText = _("Table information") - tablesGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(wx.StaticBox(self, label=tablesGroupText), wx.VERTICAL)) + tablesGroupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=tablesGroupText) + tablesGroupBox = tablesGroupSizer.GetStaticBox() + tablesGroup = guiHelper.BoxSizerHelper(self, sizer=tablesGroupSizer) sHelper.addItem(tablesGroup) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.tablesCheckBox=tablesGroup.addItem(wx.CheckBox(self,label=_("&Tables"))) + self.tablesCheckBox = tablesGroup.addItem(wx.CheckBox(tablesGroupBox, label=_("&Tables"))) self.tablesCheckBox.SetValue(config.conf["documentFormatting"]["reportTables"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.tableHeadersCheckBox=tablesGroup.addItem(wx.CheckBox(self,label=_("Row/column h&eaders"))) + _tableHeadersCheckBox = wx.CheckBox(tablesGroupBox, label=_("Row/column h&eaders")) + self.tableHeadersCheckBox = tablesGroup.addItem(_tableHeadersCheckBox) self.tableHeadersCheckBox.SetValue(config.conf["documentFormatting"]["reportTableHeaders"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.tableCellCoordsCheckBox=tablesGroup.addItem(wx.CheckBox(self,label=_("Cell c&oordinates"))) + _tableCellCoordsCheckBox = wx.CheckBox(tablesGroupBox, label=_("Cell c&oordinates")) + self.tableCellCoordsCheckBox = tablesGroup.addItem(_tableCellCoordsCheckBox) self.tableCellCoordsCheckBox.SetValue(config.conf["documentFormatting"]["reportTableCellCoords"]) borderChoices=[ @@ -2307,65 +2325,71 @@ def makeSettings(self, settingsSizer): # Translators: This is the label for a group of document formatting options in the # document formatting settings panel elementsGroupText = _("Elements") - elementsGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(wx.StaticBox(self, label=elementsGroupText), wx.VERTICAL)) + elementsGroupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=elementsGroupText) + elementsGroupBox = elementsGroupSizer.GetStaticBox() + elementsGroup = guiHelper.BoxSizerHelper(self, sizer=elementsGroupSizer) sHelper.addItem(elementsGroup, flag=wx.EXPAND, proportion=1) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.headingsCheckBox=elementsGroup.addItem(wx.CheckBox(self,label=_("&Headings"))) + self.headingsCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=_("&Headings"))) self.headingsCheckBox.SetValue(config.conf["documentFormatting"]["reportHeadings"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.linksCheckBox=elementsGroup.addItem(wx.CheckBox(self,label=_("Lin&ks"))) + self.linksCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=_("Lin&ks"))) self.linksCheckBox.SetValue(config.conf["documentFormatting"]["reportLinks"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.graphicsCheckBox = elementsGroup.addItem(wx.CheckBox(self, label=_("&Graphics"))) + self.graphicsCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=_("&Graphics"))) self.graphicsCheckBox.SetValue(config.conf["documentFormatting"]["reportGraphics"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.listsCheckBox=elementsGroup.addItem(wx.CheckBox(self,label=_("&Lists"))) + self.listsCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=_("&Lists"))) self.listsCheckBox.SetValue(config.conf["documentFormatting"]["reportLists"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.blockQuotesCheckBox=elementsGroup.addItem(wx.CheckBox(self,label=_("Block "es"))) + _blockQuotesCheckBox = wx.CheckBox(elementsGroupBox, label=_("Block "es")) + self.blockQuotesCheckBox = elementsGroup.addItem(_blockQuotesCheckBox) self.blockQuotesCheckBox.SetValue(config.conf["documentFormatting"]["reportBlockQuotes"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. groupingsText = _("&Groupings") - self.groupingsCheckBox = elementsGroup.addItem(wx.CheckBox(self, label=groupingsText)) + self.groupingsCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=groupingsText)) self.groupingsCheckBox.SetValue(config.conf["documentFormatting"]["reportGroupings"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. landmarksText = _("Lan&dmarks and regions") - self.landmarksCheckBox = elementsGroup.addItem(wx.CheckBox(self, label=landmarksText)) + self.landmarksCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=landmarksText)) self.landmarksCheckBox.SetValue(config.conf["documentFormatting"]["reportLandmarks"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.articlesCheckBox = elementsGroup.addItem(wx.CheckBox(self, label=_("Arti&cles"))) + self.articlesCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=_("Arti&cles"))) self.articlesCheckBox.SetValue(config.conf["documentFormatting"]["reportArticles"]) # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.framesCheckBox=elementsGroup.addItem(wx.CheckBox(self,label=_("Fra&mes"))) + self.framesCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=_("Fra&mes"))) self.framesCheckBox.Value=config.conf["documentFormatting"]["reportFrames"] # Translators: This is the label for a checkbox in the # document formatting settings panel. - self.clickableCheckBox=elementsGroup.addItem(wx.CheckBox(self,label=_("&Clickable"))) + self.clickableCheckBox = elementsGroup.addItem(wx.CheckBox(elementsGroupBox, label=_("&Clickable"))) self.clickableCheckBox.Value=config.conf["documentFormatting"]["reportClickable"] # Translators: This is the label for a checkbox in the # document formatting settings panel. detectFormatAfterCursorText = _("Report formatting chan&ges after the cursor (can cause a lag)") - self.detectFormatAfterCursorCheckBox=wx.CheckBox(self, label=detectFormatAfterCursorText) + self.detectFormatAfterCursorCheckBox = wx.CheckBox( + elementsGroupBox, + label=detectFormatAfterCursorText + ) self.bindHelpEvent( "DocumentFormattingDetectFormatAfterCursor", self.detectFormatAfterCursorCheckBox @@ -2485,16 +2509,15 @@ def __init__(self, parent): # Translators: This is the label for a group of advanced options in the # Advanced settings panel groupText = _("NVDA Development") - devGroup = guiHelper.BoxSizerHelper( - parent=self, - sizer=wx.StaticBoxSizer(parent=self, label=groupText, orient=wx.VERTICAL) - ) + devGroupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=groupText) + devGroupBox = devGroupSizer.GetStaticBox() + devGroup = guiHelper.BoxSizerHelper(self, sizer=devGroupSizer) sHelper.addItem(devGroup) # Translators: This is the label for a checkbox in the # Advanced settings panel. label = _("Enable loading custom code from Developer Scratchpad directory") - self.scratchpadCheckBox=devGroup.addItem(wx.CheckBox(self, label=label)) + self.scratchpadCheckBox = devGroup.addItem(wx.CheckBox(devGroupBox, label=label)) self.bindHelpEvent("AdvancedSettingsEnableScratchpad", self.scratchpadCheckBox) self.scratchpadCheckBox.SetValue(config.conf["development"]["enableScratchpadDir"]) self.scratchpadCheckBox.defaultValue = self._getDefaultValue(["development", "enableScratchpadDir"]) @@ -2507,7 +2530,7 @@ def __init__(self, parent): # Translators: the label for a button in the Advanced settings category label=_("Open developer scratchpad directory") - self.openScratchpadButton=devGroup.addItem(wx.Button(self, label=label)) + self.openScratchpadButton = devGroup.addItem(wx.Button(devGroupBox, label=label)) self.bindHelpEvent("AdvancedSettingsOpenScratchpadDir", self.openScratchpadButton) self.openScratchpadButton.Enable(config.conf["development"]["enableScratchpadDir"]) self.openScratchpadButton.Bind(wx.EVT_BUTTON,self.onOpenScratchpadDir) @@ -2517,16 +2540,15 @@ def __init__(self, parent): # Translators: This is the label for a group of advanced options in the # Advanced settings panel label = _("Microsoft UI Automation") - UIAGroup = guiHelper.BoxSizerHelper( - parent=self, - sizer=wx.StaticBoxSizer(parent=self, label=label, orient=wx.VERTICAL) - ) + UIASizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=label) + UIABox = UIASizer.GetStaticBox() + UIAGroup = guiHelper.BoxSizerHelper(self, sizer=UIASizer) sHelper.addItem(UIAGroup) # Translators: This is the label for a checkbox in the # Advanced settings panel. label = _("Enable &selective registration for UI Automation events and property changes") - self.selectiveUIAEventRegistrationCheckBox = UIAGroup.addItem(wx.CheckBox(self, label=label)) + self.selectiveUIAEventRegistrationCheckBox = UIAGroup.addItem(wx.CheckBox(UIABox, label=label)) self.bindHelpEvent( "AdvancedSettingsSelectiveUIAEventRegistration", self.selectiveUIAEventRegistrationCheckBox @@ -2539,7 +2561,7 @@ def __init__(self, parent): # Translators: This is the label for a checkbox in the # Advanced settings panel. label = _("Use UI Automation to access Microsoft &Word document controls when available") - self.UIAInMSWordCheckBox=UIAGroup.addItem(wx.CheckBox(self, label=label)) + self.UIAInMSWordCheckBox = UIAGroup.addItem(wx.CheckBox(UIABox, label=label)) self.bindHelpEvent("AdvancedSettingsUseUiaForWord", self.UIAInMSWordCheckBox) self.UIAInMSWordCheckBox.SetValue(config.conf["UIA"]["useInMSWordWhenAvailable"]) self.UIAInMSWordCheckBox.defaultValue = self._getDefaultValue(["UIA", "useInMSWordWhenAvailable"]) @@ -2548,7 +2570,7 @@ def __init__(self, parent): # Advanced settings panel. label = _("Use UI Automation to access the Windows C&onsole when available") consoleUIADevMap = True if config.conf['UIA']['winConsoleImplementation'] == 'UIA' else False - self.ConsoleUIACheckBox = UIAGroup.addItem(wx.CheckBox(self, label=label)) + self.ConsoleUIACheckBox = UIAGroup.addItem(wx.CheckBox(UIABox, label=label)) self.bindHelpEvent("AdvancedSettingsConsoleUIA", self.ConsoleUIACheckBox) self.ConsoleUIACheckBox.SetValue(consoleUIADevMap) self.ConsoleUIACheckBox.defaultValue = self._getDefaultValue(["UIA", "winConsoleImplementation"]) @@ -2556,7 +2578,7 @@ def __init__(self, parent): # Translators: This is the label for a checkbox in the # Advanced settings panel. label = _("Speak &passwords in UIA consoles (may improve performance)") - self.winConsoleSpeakPasswordsCheckBox = UIAGroup.addItem(wx.CheckBox(self, label=label)) + self.winConsoleSpeakPasswordsCheckBox = UIAGroup.addItem(wx.CheckBox(UIABox, label=label)) self.bindHelpEvent("AdvancedSettingsWinConsoleSpeakPasswords", self.winConsoleSpeakPasswordsCheckBox) self.winConsoleSpeakPasswordsCheckBox.SetValue(config.conf["terminals"]["speakPasswords"]) self.winConsoleSpeakPasswordsCheckBox.defaultValue = self._getDefaultValue(["terminals", "speakPasswords"]) @@ -2586,15 +2608,14 @@ def __init__(self, parent): # Translators: This is the label for a group of advanced options in the # Advanced settings panel label = _("Terminal programs") - terminalsGroup = guiHelper.BoxSizerHelper( - parent=self, - sizer=wx.StaticBoxSizer(parent=self, label=label, orient=wx.VERTICAL) - ) + terminalsSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=label) + terminalsBox = terminalsSizer.GetStaticBox() + terminalsGroup = guiHelper.BoxSizerHelper(self, sizer=terminalsSizer) sHelper.addItem(terminalsGroup) # Translators: This is the label for a checkbox in the # Advanced settings panel. label = _("Use the new t&yped character support in Windows Console when available") - self.keyboardSupportInLegacyCheckBox=terminalsGroup.addItem(wx.CheckBox(self, label=label)) + self.keyboardSupportInLegacyCheckBox = terminalsGroup.addItem(wx.CheckBox(terminalsBox, label=label)) self.bindHelpEvent("AdvancedSettingsKeyboardSupportInLegacy", self.keyboardSupportInLegacyCheckBox) self.keyboardSupportInLegacyCheckBox.SetValue(config.conf["terminals"]["keyboardSupportInLegacy"]) self.keyboardSupportInLegacyCheckBox.defaultValue = self._getDefaultValue(["terminals", "keyboardSupportInLegacy"]) @@ -2639,10 +2660,8 @@ def __init__(self, parent): # Translators: This is the label for a group of advanced options in the # Advanced settings panel label = _("Speech") - speechGroup = guiHelper.BoxSizerHelper( - parent=self, - sizer=wx.StaticBoxSizer(parent=self, label=label, orient=wx.VERTICAL) - ) + speechSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=label) + speechGroup = guiHelper.BoxSizerHelper(speechSizer, sizer=speechSizer) sHelper.addItem(speechGroup) expiredFocusSpeechChoices = [ @@ -2675,10 +2694,8 @@ def __init__(self, parent): # Translators: This is the label for a group of advanced options in the # Advanced settings panel label = _("Editable Text") - editableTextGroup = guiHelper.BoxSizerHelper( - self, - sizer=wx.StaticBoxSizer(parent=self, label=label, orient=wx.VERTICAL) - ) + editableSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=label) + editableTextGroup = guiHelper.BoxSizerHelper(editableSizer, sizer=editableSizer) sHelper.addItem(editableTextGroup) # Translators: This is the label for a numeric control in the @@ -2697,10 +2714,8 @@ def __init__(self, parent): # Translators: This is the label for a group of advanced options in the # Advanced settings panel label = _("Debug logging") - debugLogGroup = guiHelper.BoxSizerHelper( - self, - sizer=wx.StaticBoxSizer(parent=self, label=label, orient=wx.VERTICAL) - ) + debugLogSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=label) + debugLogGroup = guiHelper.BoxSizerHelper(self, sizer=debugLogSizer) sHelper.addItem(debugLogGroup) self.logCategories=[ @@ -2824,12 +2839,10 @@ def makeSettings(self, settingsSizer): :type settingsSizer: wx.BoxSizer """ sHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) - warningGroup = guiHelper.BoxSizerHelper( - self, - sizer=wx.StaticBoxSizer(wx.StaticBox(self), wx.VERTICAL) - ) - sHelper.addItem(warningGroup) + warningSizer = wx.StaticBoxSizer(wx.VERTICAL, self) + warningGroup = guiHelper.BoxSizerHelper(self, sizer=warningSizer) warningBox = warningGroup.sizer.GetStaticBox() # type: wx.StaticBox + sHelper.addItem(warningGroup) warningText = wx.StaticText(warningBox, label=self.warningHeader) warningText.SetFont(wx.Font(18, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.BOLD)) @@ -2851,7 +2864,7 @@ def makeSettings(self, settingsSizer): restoreDefaultsButton = warningGroup.addItem( # Translators: This is the label for a button in the Advanced settings panel - wx.Button(self, label=_("Restore defaults")) + wx.Button(warningBox, label=_("Restore defaults")) ) self.bindHelpEvent("AdvancedSettingsRestoringDefaults", restoreDefaultsButton) restoreDefaultsButton.Bind(wx.EVT_BUTTON, lambda evt: self.advancedControls.restoreToDefaults()) @@ -3102,15 +3115,20 @@ def makeSettings(self, settingsSizer): # Translators: A label for the braille display on the braille panel. displayLabel = _("Braille &display") - displayBox = wx.StaticBox(self, label=displayLabel) - displayGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(displayBox, wx.HORIZONTAL)) + displaySizer = wx.StaticBoxSizer(wx.HORIZONTAL, self, label=displayLabel) + displayBox = displaySizer.GetStaticBox() + displayGroup = guiHelper.BoxSizerHelper(self, sizer=displaySizer) settingsSizerHelper.addItem(displayGroup) - self.displayNameCtrl = ExpandoTextCtrl(self, size=(self.scaleSize(250), -1), style=wx.TE_READONLY) + self.displayNameCtrl = ExpandoTextCtrl( + displayBox, + size=(self.scaleSize(250), -1), + style=wx.TE_READONLY + ) self.bindHelpEvent("BrailleSettingsChange", self.displayNameCtrl) self.updateCurrentDisplay() # Translators: This is the label for the button used to change braille display, # it appears in the context of a braille display group on the braille settings panel. - changeDisplayBtn = wx.Button(self, label=_("C&hange...")) + changeDisplayBtn = wx.Button(displayBox, label=_("C&hange...")) self.bindHelpEvent("BrailleSettingsChange", changeDisplayBtn) displayGroup.addItem( guiHelper.associateElements( @@ -3712,7 +3730,7 @@ def makeSettings(self, settingsSizer: wx.BoxSizer): for providerInfo in vision.handler.getProviderList(reloadFromSystem=True): providerSizer = self.settingsSizerHelper.addItem( - wx.StaticBoxSizer(wx.StaticBox(self, label=providerInfo.displayName), wx.VERTICAL), + wx.StaticBoxSizer(wx.VERTICAL, self, label=providerInfo.displayName), flag=wx.EXPAND ) if len(self.providerPanelInstances) > 0: @@ -4089,14 +4107,9 @@ def makeSettings(self, settingsSizer): # Translators: The label for the group of controls in symbol pronunciation dialog to change the pronunciation of a symbol. changeSymbolText = _("Change selected symbol") - changeSymbolHelper = sHelper.addItem(guiHelper.BoxSizerHelper( - parent=self, - sizer=wx.StaticBoxSizer( - parent=self, - label=changeSymbolText, - orient=wx.VERTICAL, - ) - )) + changeSymbolSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=changeSymbolText) + changeSymbolGroup = guiHelper.BoxSizerHelper(self, sizer=changeSymbolSizer) + changeSymbolHelper = sHelper.addItem(changeSymbolGroup) # Used to ensure that event handlers call Skip(). Not calling skip can cause focus problems for controls. More # generally the advice on the wx documentation is: "In general, it is recommended to skip all non-command events diff --git a/source/languageHandler.py b/source/languageHandler.py index b134ea0373f..c3ab3dc4ab0 100644 --- a/source/languageHandler.py +++ b/source/languageHandler.py @@ -178,7 +178,8 @@ def setLanguage(lang): # #9207: Python 3.8 adds gettext.pgettext, so add it to the built-in namespace. trans.install(names=["pgettext"]) -def getLanguage(): + +def getLanguage() -> str: return curLang def normalizeLanguage(lang): diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 9af95ba8e2e..7dbae0b4ab1 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -30,6 +30,9 @@ What's New in NVDA - Formatting information and other browseable messages no longer present unexpected blank lines when screen layout is turned off. (#12004) - It is now possible to read comments in MS Word with UIA enabled. (#9285) - Performance when interacting with Visual Studio has been improved. (#12171) +- Fix graphical bugs such as missing elements when using NVDA with a right-to-left layout. (#8859) +- Respect the GUI layout direction based on the NVDA language, not the system locale. (#638) + - known issue for right-to-left languages: the right border of groupings clips with labels/controls. (#12181) == Changes for Developers == From 7139a9c4d4d3a2aa05dd3c204bd9d7b16bda75e4 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 18 Mar 2021 18:07:41 +1000 Subject: [PATCH 091/174] Allow NVDA build system to still function if NvDA repository is moved or copied to another location (#12184) * venvUtils\ensureAndActivate.bat: rather than calling the standard activate.bat from the generated .venv, set the necessary environment variables manually, and make VIRTUAL_ENV be relative to this script, rather than a hard-coded path. This allows the NVDA repository to be moved or copied somewhere else on the same system and the environment used without having to be recreated. * Don't call the deactivate script from venvCmd.bat. We no longer call activate.bat so deactivate.bat may do the wrong thing. But more importantly, we already call endlocal which dumps all changes to the environment anyway, thus it is not needed. * Add extra comments to venvCmd.bat explaining setlocal / endlocal. * ensureAndActivate.bat: add more clearer comments, mentioning .venv's standard activate.bat. --- venvUtils/ensureAndActivate.bat | 25 ++++++++++++++++++++++--- venvUtils/venvCmd.bat | 4 +++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/venvUtils/ensureAndActivate.bat b/venvUtils/ensureAndActivate.bat index 9d6f3f7664e..b39d1f947ff 100644 --- a/venvUtils/ensureAndActivate.bat +++ b/venvUtils/ensureAndActivate.bat @@ -1,9 +1,28 @@ @echo off rem this script ensures the NVDA build system Python virtual environment is created and up to date, rem and then activates it. -rem this script should be used only in the case where many commands will be executed within the environment and the shell will be eventually thrown away. -rem E.g. an Appveyor build. +rem This is an internal script and should not be used directly. + +rem Ensure the environment is created and up to date py -3.8-32 "%~dp0\ensureVenv.py" if ERRORLEVEL 1 goto :EOF -call "%~dp0\..\.venv\scripts\activate.bat" + +rem Set the necessary environment variables to have Python use this virtual environment. +rem This should set all the necessary environment variables that the standard .venv\scripts\activate.bat does +rem Except that we set VIRTUAL_ENV to a path relative to this script, +rem rather than it being hard-coded to where the virtual environment was first created. + +rem unset the PYTHONHOME variable so as to ensure that Python does not use a customized Python standard library. +set PYTHONHOME= +rem set the VIRTUAL_ENV variable instructing Python to use a virtual environment +rem py.exe will honor VIRTUAL_ENV and launch the python.exe that it finds in %VIRTUAL_ENV%\scripts. +rem %VIRTUAL_ENV%\scripts\python.exe will find pyvenv.cfg in its parent directory, +rem which is actually what then causes Python to use the site-packages found in this virtual environment. +set VIRTUAL_ENV=%~dp0..\.venv +rem Add the virtual environment's scripts directory to the path +set PATH=%VIRTUAL_ENV%\scripts;%PATH% +rem Set an NVDA-specific variable to identify this official NVDA virtual environment from other 3rd party ones set NVDA_VENV=%VIRTUAL_ENV% +rem mention the environment in the prompt to make it obbvious it is active +rem just in case this script is executed outside of a local block and not cleaned up. +set PROMPT=[NVDA Venv] %PROMPT% diff --git a/venvUtils/venvCmd.bat b/venvUtils/venvCmd.bat index c0157c05ab2..06e4572d5e3 100644 --- a/venvUtils/venvCmd.bat +++ b/venvUtils/venvCmd.bat @@ -16,12 +16,14 @@ if "%VIRTUAL_ENV%" NEQ "" ( goto :EOF ) +rem call setlocal to make sure that any environment variable changes made by activating the virtual environment +rem can be completely undone when endlocal is called or this script exits. setlocal echo Ensuring NVDA Python virtual environment call "%~dp0\ensureAndActivate.bat" if ERRORLEVEL 1 goto :EOF echo call %* call %* +rem the virtual environment will now be deactivated as endlocal will be reached. echo Deactivating NVDA Python virtual environment -call "%~dp0\..\.venv\scripts\deactivate.bat" endlocal From 3c1693dd6533bd821cee233236c69da1b847e375 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 18 Mar 2021 18:08:37 +1000 Subject: [PATCH 092/174] Stop printing some exceptions in __del__ to standard error (#12148) * garbageHandler.trackableObject's __del__method: handle the case where the object is being deleted while Python is exiting. In this situation, some global symbols could be None. * comtypesMonkeyPatches: in our override of compointer_base's __del__ method: handle the case where Python is exiting. In this situation some global symbols could become set to None. * Linting * Clarify comment. * comtypesMonkeyPatches._newCpbdel: make isFinalizer check a little easier to read. --- source/comtypesMonkeyPatches.py | 18 ++++++++++++++---- source/garbageHandler.py | 7 ++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/source/comtypesMonkeyPatches.py b/source/comtypesMonkeyPatches.py index 089cfe74a56..beff7550681 100644 --- a/source/comtypesMonkeyPatches.py +++ b/source/comtypesMonkeyPatches.py @@ -8,6 +8,7 @@ import ctypes import _ctypes import importlib +import sys # A version of ctypes.WINFUNCTYPE @@ -95,14 +96,23 @@ def new__call__(self,*args,**kwargs): # This causes Release() to be called more than it should, which is very nasty and will eventually cause us to access pointers which have been freed. from comtypes import _compointer_base -_cpbDel = _compointer_base.__del__ +_compointer_base._oldCpbDel = _compointer_base.__del__ def newCpbDel(self): + # __del__ may be called while Python is exiting. + # In this state, global symbols may be set to None + # Therefore avoid calling into garbageHandler or log, + # unless isFinalizing is checked first to ensure they are still available + # Using local variables or calling other methods on this class is still okay. + isFinalizingFunc = getattr(sys, 'is_finalizing', lambda: True) + isFinalizing = isFinalizingFunc() if hasattr(self, "_deleted"): # Don't allow this to be called more than once. - log.debugWarning("COM pointer %r already deleted" % self) + if not isFinalizing: + log.debugWarning("COM pointer %r already deleted" % self) return - garbageHandler.notifyObjectDeletion(self) - _cpbDel(self) + if not isFinalizing: + garbageHandler.notifyObjectDeletion(self) + self._oldCpbDel() self._deleted = True newCpbDel.__name__ = "__del__" _compointer_base.__del__ = newCpbDel diff --git a/source/garbageHandler.py b/source/garbageHandler.py index b5cb2592ee4..aa90fab0f0a 100644 --- a/source/garbageHandler.py +++ b/source/garbageHandler.py @@ -5,6 +5,7 @@ # See the file COPYING for more details. +import sys import gc import threading from logHandler import log @@ -20,7 +21,11 @@ class TrackedObject: """ def __del__(self): - notifyObjectDeletion(self) + # __del__ may still be called while Python is exiting. + # And therefore some symbols may be set to None. + isFinalizing = getattr(sys, 'is_finalizing', lambda: True)() + if not isFinalizing: + notifyObjectDeletion(self) _collectionThreadID = 0 From b5ad8d6e8b85f4112415998c449d11427b1689f6 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 22 Mar 2021 11:45:33 +0800 Subject: [PATCH 093/174] Update espeak to 1.51-dev commit 53915b (PR #12202) Now using 1.51-dev commit 53915bf0a7cd48f90c4a38ac52fff697723d9f4d --- include/espeak | 2 +- readme.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/espeak b/include/espeak index 82d5b7b0448..53915bf0a7c 160000 --- a/include/espeak +++ b/include/espeak @@ -1 +1 @@ -Subproject commit 82d5b7b04488412845101851f36da6953cac4378 +Subproject commit 53915bf0a7cd48f90c4a38ac52fff697723d9f4d diff --git a/readme.md b/readme.md index 734c94ca09e..3b29634aa3f 100644 --- a/readme.md +++ b/readme.md @@ -81,7 +81,7 @@ If you aren't sure, run `git submodule update` after every git pull, merge or ch For reference, the following run time dependencies are included in Git submodules: -* [eSpeak NG](https://github.com/espeak-ng/espeak-ng), version 1.51-dev commit 82d5b7b04 +* [eSpeak NG](https://github.com/espeak-ng/espeak-ng), version 1.51-dev commit 53915bf0a * [Sonic](https://github.com/waywardgeek/sonic), commit 4f8c1d11 * [IAccessible2](https://wiki.linuxfoundation.org/accessibility/iaccessible2/start), commit cbc1f29631780 * [liblouis](http://www.liblouis.org/), version 3.16.1 From abdf77e91dc6597cba98277da417dc70ae810725 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 22 Mar 2021 11:50:28 +0800 Subject: [PATCH 094/174] Update changes file for PR #12202 --- user_docs/en/changes.t2t | 1 + 1 file changed, 1 insertion(+) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 7dbae0b4ab1..61f620462fa 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -19,6 +19,7 @@ What's New in NVDA - 'Attempt to cancel speech for expired focus events' option in the advanced settings panel now enabled by default. - This behaviour can be disabled by default with by setting this option to "No". - Web applications (E.G. Gmail) no longer speak outdated information when moving focus rapidly. (#10885) +- Espeak-ng has been updated to 1.51-dev commit 53915bf0a7cd48f90c4a38ac52fff697723d9f4d. (#12202) == Bug Fixes == From f2a734b092bc7cc41508436df2f6f5338da5ec5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Mon, 22 Mar 2021 06:07:31 +0100 Subject: [PATCH 095/174] Remove backwards compat aliases from PR 10593 (#12195) Removes code provided for backwards compatibility in PR #10593 ### Summary of the issue: PR #10593 introduced `speech.speakWithoutPauses ` as an alias for `speech._speakWithoutPauses.speakWithoutPauses` and `speech.re_last_pause` as an alias for `speech._speakWithoutPauses.re_last_pause` for backwards compatibility. Since 2021.1 is going to be backwards compatibility breaking release it makes sense to remove these. ### Description of how this pull request fixes the issue: These aliases are removed and their usages are replaces with `speech.SpeechWithoutPauses(speakFunc=speech.speak).speakWithoutPauses` and `speech.SpeechWithoutPauses.re_last_pause` respectively. --- source/sayAllHandler.py | 12 ++++++++---- source/speech/__init__.py | 6 ------ tests/unit/test_SpeechWithoutPauses.py | 4 ++-- user_docs/en/changes.t2t | 2 ++ 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/source/sayAllHandler.py b/source/sayAllHandler.py index bb3b7f42ba1..ff4b5884b7e 100644 --- a/source/sayAllHandler.py +++ b/source/sayAllHandler.py @@ -16,6 +16,10 @@ from speech.commands import CallbackCommand, EndUtteranceCommand + +speakWithoutPauses = speech.SpeechWithoutPauses(speakFunc=speech.speak).speakWithoutPauses + + CURSOR_CARET = 0 CURSOR_REVIEW = 1 @@ -150,7 +154,7 @@ def nextLine(self): if isinstance(self.reader.obj, textInfos.DocumentWithPageTurns): # Once the last line finishes reading, try turning the page. cb = CallbackCommand(self.turnPage, name="say-all:turnPage") - speech.speakWithoutPauses([cb, EndUtteranceCommand()]) + speakWithoutPauses([cb, EndUtteranceCommand()]) else: self.finish() return @@ -182,7 +186,7 @@ def _onLineReached(obj=self.reader.obj, state=state): seq = list(speech._flattenNestedSequences(speechGen)) seq.insert(0, cb) # Speak the speech sequence. - spoke = speech.speakWithoutPauses(seq) + spoke = speakWithoutPauses(seq) # Update the textInfo state ready for when speaking the next line. self.speakTextInfoState = state.copy() @@ -204,7 +208,7 @@ def _onLineReached(obj=self.reader.obj, state=state): else: # We don't want to buffer too much. # Force speech. lineReached will resume things when speech catches up. - speech.speakWithoutPauses(None) + speakWithoutPauses(None) # The first buffered line has now started speaking. self.numBufferedLines -= 1 @@ -241,7 +245,7 @@ def finish(self): # we might switch synths too early and truncate the final speech. # We do this by putting a CallbackCommand at the start of a new utterance. cb = CallbackCommand(self.stop, name="say-all:stop") - speech.speakWithoutPauses([ + speakWithoutPauses([ EndUtteranceCommand(), cb, EndUtteranceCommand() diff --git a/source/speech/__init__.py b/source/speech/__init__.py index 520d2400001..befd122ebfe 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -51,7 +51,6 @@ Generator, Union, Callable, - Iterator, Tuple, ) from logHandler import log @@ -466,7 +465,6 @@ def getObjectSpeech( # noqa: C901 reason: OutputReason = OutputReason.QUERY, _prefixSpeechCommand: Optional[SpeechCommand] = None, ): - from NVDAObjects import NVDAObjectTextInfo role=obj.role # Choose when we should report the content of this object's textInfo, rather than just the object's value import browseMode @@ -2538,10 +2536,6 @@ def _getSpeech( _speakWithoutPauses = SpeechWithoutPauses(speakFunc=speak) -#: Alias for class SpeakWithoutPauses.speakWithoutPauses. Kept for backwards compatibility -speakWithoutPauses = _speakWithoutPauses.speakWithoutPauses -#: Kept for backwards compatibility. -re_last_pause = _speakWithoutPauses.re_last_pause #: The singleton _SpeechManager instance used for speech functions. #: @type: L{manager.SpeechManager} diff --git a/tests/unit/test_SpeechWithoutPauses.py b/tests/unit/test_SpeechWithoutPauses.py index a2559c6dc91..720501495c9 100644 --- a/tests/unit/test_SpeechWithoutPauses.py +++ b/tests/unit/test_SpeechWithoutPauses.py @@ -10,7 +10,7 @@ from speech.types import SpeechSequence from speech.commands import EndUtteranceCommand, LangChangeCommand, CallbackCommand -from speech import re_last_pause, SpeechWithoutPauses +from speech import SpeechWithoutPauses from logHandler import log @@ -89,7 +89,7 @@ def old_speakWithoutPauses( # noqa: C901 for index in range(len(speechSequence) - 1, -1, -1): item = speechSequence[index] if isinstance(item, str): - m = re_last_pause.match(item) + m = SpeechWithoutPauses.re_last_pause.match(item) if m: before, after = m.groups() if after: diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 61f620462fa..e0a21e27e38 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -82,6 +82,8 @@ What's New in NVDA - `autoSettingsUtils.driverSetting` classes are removed from `driverHandler` - please use them from `autoSettingUtils.driverSetting`. (#12168) - `autoSettingsUtils.utils` classes are removed from `driverHandler` - please use them from `autoSettingUtils.utils`. (#12168) - Support of `TextInfo`s that do not inherit from `contentRecog.BaseContentRecogTextInfo` is removed. (#12157) +- `speech.speakWithoutPauses` has been removed - please use `speech.SpeechWithoutPauses(speakFunc=speech.speak).speakWithoutPauses` instead. (#12195) +- `speech.re_last_pause` has been removed - please use `speech.SpeechWithoutPauses.re_last_pause` instead. (#12195) = 2020.4 = From 83738d7d9150fb379083eb3918e9c78c78610489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9-Abush=20Clause?= Date: Mon, 22 Mar 2021 11:15:08 +0100 Subject: [PATCH 096/174] Update liblouis to 3.17.0 (PR #12137) Adds Urdu braille tables to the GUI Co-authored-by: Reef Turner --- include/liblouis | 2 +- readme.md | 2 +- source/brailleTables.py | 12 ++++++++++++ user_docs/en/changes.t2t | 2 ++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/include/liblouis b/include/liblouis index 48633b86de7..2e8b61c5605 160000 --- a/include/liblouis +++ b/include/liblouis @@ -1 +1 @@ -Subproject commit 48633b86de7d4dc86e5aa21855f1974ede1b0c26 +Subproject commit 2e8b61c56055370fba697b3c0ec41c26d159d1b0 diff --git a/readme.md b/readme.md index 3b29634aa3f..22b71927afd 100644 --- a/readme.md +++ b/readme.md @@ -84,7 +84,7 @@ For reference, the following run time dependencies are included in Git submodule * [eSpeak NG](https://github.com/espeak-ng/espeak-ng), version 1.51-dev commit 53915bf0a * [Sonic](https://github.com/waywardgeek/sonic), commit 4f8c1d11 * [IAccessible2](https://wiki.linuxfoundation.org/accessibility/iaccessible2/start), commit cbc1f29631780 -* [liblouis](http://www.liblouis.org/), version 3.16.1 +* [liblouis](http://www.liblouis.org/), version 3.17.0 * [Unicode Common Locale Data Repository (CLDR)](http://cldr.unicode.org/), version 38.1 * NVDA images and sounds * [Adobe Acrobat accessibility interface, version XI](https://download.macromedia.com/pub/developer/acrobat/AcrobatAccess.zip) diff --git a/source/brailleTables.py b/source/brailleTables.py index a640bae931b..e99c0cf4ec8 100644 --- a/source/brailleTables.py +++ b/source/brailleTables.py @@ -114,6 +114,12 @@ def listTables(): addTable("be-in-g1.utb", _("Bengali grade 1")) # Translators: The name of a braille table displayed in the # braille settings dialog. +addTable("bel-comp.utb", _("Belarusian computer braille")) +# Translators: The name of a braille table displayed in the +# braille settings dialog. +addTable("bel.utb", _("Belarusian literary braille"), input=False) +# Translators: The name of a braille table displayed in the +# braille settings dialog. addTable("bg.ctb", _("Bulgarian 8 dot computer braille")) # Translators: The name of a braille table displayed in the # braille settings dialog. @@ -432,6 +438,12 @@ def listTables(): addTable("uk-comp.utb", _("Ukrainian computer braille")) # Translators: The name of a braille table displayed in the # braille settings dialog. +addTable("ur-pk-g1.utb", _("Urdu grade 1")) +# Translators: The name of a braille table displayed in the +# braille settings dialog. +addTable("ur-pk-g2.ctb", _("Urdu grade 2")) +# Translators: The name of a braille table displayed in the +# braille settings dialog. addTable("uz-g1.utb", _("Uzbek grade 1")) # Translators: The name of a braille table displayed in the # braille settings dialog. diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index e0a21e27e38..7d782ab1f69 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -20,6 +20,8 @@ What's New in NVDA - This behaviour can be disabled by default with by setting this option to "No". - Web applications (E.G. Gmail) no longer speak outdated information when moving focus rapidly. (#10885) - Espeak-ng has been updated to 1.51-dev commit 53915bf0a7cd48f90c4a38ac52fff697723d9f4d. (#12202) +- Updated liblouis braille translator to [3.17.0 https://github.com/liblouis/liblouis/releases/tag/v3.17.0]. (#12137) + - New braille tables: Belarusian literary braille, Belarusian computer braille, Urdu grade 1, Urdu grade 2. == Bug Fixes == From fc6b73f5c7d7375c724e91b1e447d538e33585c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Tue, 23 Mar 2021 01:25:49 +0100 Subject: [PATCH 097/174] Move some classes from the main gui module. (#12105) Code layout in the gui module is sup optimal. This can easily cause cyclic imports for one such example see #11950 While the issue mentioned above has been resolved it makes sense to reorganize this part of the code to minimize likelihood of similar problems in the future. Description of how this pull request fixes the issue: - `LauncherDialog`, `WelcomeDialog` and `AskAllowUsageStatsDialog` were moved to the `gui.startupDialogs` module - `getDocFilePath` has been moved into the new `documentationUtils` module as there is no logical connection between it and gui Co-authored-by: buddsean --- source/core.py | 8 +- source/documentationUtils.py | 52 ++++++ source/gui/__init__.py | 298 +---------------------------------- source/gui/contextHelp.py | 5 +- source/gui/startupDialogs.py | 278 ++++++++++++++++++++++++++++++++ user_docs/en/changes.t2t | 2 + 6 files changed, 341 insertions(+), 302 deletions(-) create mode 100644 source/documentationUtils.py create mode 100644 source/gui/startupDialogs.py diff --git a/source/core.py b/source/core.py index 61173822c29..ca36a04dbf2 100644 --- a/source/core.py +++ b/source/core.py @@ -79,7 +79,8 @@ def doStartupDialogs(): _("Configuration File Error"), wx.OK | wx.ICON_EXCLAMATION) if config.conf["general"]["showWelcomeDialogAtStartup"]: - gui.WelcomeDialog.run() + from gui.startupDialogs import WelcomeDialog + WelcomeDialog.run() if config.conf["brailleViewer"]["showBrailleViewerAtStartup"]: gui.mainFrame.onToggleBrailleViewerCommand(evt=None) if config.conf["speechViewer"]["showSpeechViewerAtStartup"]: @@ -105,7 +106,7 @@ def onResult(ID): except: pass # Ask the user if usage stats can be collected. - gui.runScriptModalDialog(gui.AskAllowUsageStatsDialog(None),onResult) + gui.runScriptModalDialog(gui.startupDialogs.AskAllowUsageStatsDialog(None), onResult) def restart(disableAddons=False, debugLogging=False): """Restarts NVDA by starting a new copy.""" @@ -519,7 +520,8 @@ def handlePowerStatusChange(self): except: log.error("", exc_info=True) if globalVars.appArgs.launcher: - gui.LauncherDialog.run() + from gui.startupDialogs import LauncherDialog + LauncherDialog.run() # LauncherDialog will call doStartupDialogs() afterwards if required. else: wx.CallAfter(doStartupDialogs) diff --git a/source/documentationUtils.py b/source/documentationUtils.py new file mode 100644 index 00000000000..db80df9c5eb --- /dev/null +++ b/source/documentationUtils.py @@ -0,0 +1,52 @@ +# -*- coding: UTF-8 -*- +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2006-2021 NV Access Limited, Łukasz Golonka +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + +import os +import sys + +import globalVars +import languageHandler + + +def getDocFilePath(fileName, localized=True): + if not getDocFilePath.rootPath: + if hasattr(sys, "frozen"): + getDocFilePath.rootPath = os.path.join(globalVars.appDir, "documentation") + else: + getDocFilePath.rootPath = os.path.join(globalVars.appDir, "..", "user_docs") + + if localized: + lang = languageHandler.getLanguage() + tryLangs = [lang] + if "_" in lang: + # This locale has a sub-locale, but documentation might not exist for the sub-locale, so try stripping it. + tryLangs.append(lang.split("_")[0]) + # If all else fails, use English. + tryLangs.append("en") + + fileName, fileExt = os.path.splitext(fileName) + for tryLang in tryLangs: + tryDir = os.path.join(getDocFilePath.rootPath, tryLang) + if not os.path.isdir(tryDir): + continue + + # Some out of date translations might include .txt files which are now .html files in newer translations. + # Therefore, ignore the extension and try both .html and .txt. + for tryExt in ("html", "txt"): + tryPath = os.path.join(tryDir, f"{fileName}.{tryExt}") + if os.path.isfile(tryPath): + return tryPath + return None + else: + # Not localized. + if not hasattr(sys, "frozen") and fileName in ("copying.txt", "contributors.txt"): + # If running from source, these two files are in the root dir. + return os.path.join(globalVars.appDir, "..", fileName) + else: + return os.path.join(getDocFilePath.rootPath, fileName) + + +getDocFilePath.rootPath = None diff --git a/source/gui/__init__.py b/source/gui/__init__.py index 6cbe8c2ac02..742e8869dcb 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -5,17 +5,10 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. -from .contextHelp import ( - # several other submodules depend on ContextHelpMixin - # ensure early that it can be imported successfully. - ContextHelpMixin as _ContextHelpMixin, # don't expose from gui, import submodule directly. -) - import time import os import sys import threading -import codecs import ctypes import weakref import wx @@ -23,10 +16,10 @@ import globalVars import tones import ui +from documentationUtils import getDocFilePath from logHandler import log import config import versionInfo -import addonAPIVersion import speech import queueHandler import core @@ -34,14 +27,10 @@ from .settingsDialogs import * from .inputGestures import InputGesturesDialog import speechDictHandler -import languageHandler -import keyboardHandler from . import logViewer import speechViewer import winUser import api -from . import guiHelper -import winVersion try: import updateCheck @@ -57,43 +46,6 @@ mainFrame = None isInMessageBox = False -def getDocFilePath(fileName, localized=True): - if not getDocFilePath.rootPath: - if hasattr(sys, "frozen"): - getDocFilePath.rootPath = os.path.join(NVDA_PATH, "documentation") - else: - getDocFilePath.rootPath = os.path.join(NVDA_PATH, "..", "user_docs") - - if localized: - lang = languageHandler.getLanguage() - tryLangs = [lang] - if "_" in lang: - # This locale has a sub-locale, but documentation might not exist for the sub-locale, so try stripping it. - tryLangs.append(lang.split("_")[0]) - # If all else fails, use English. - tryLangs.append("en") - - fileName, fileExt = os.path.splitext(fileName) - for tryLang in tryLangs: - tryDir = os.path.join(getDocFilePath.rootPath, tryLang) - if not os.path.isdir(tryDir): - continue - - # Some out of date translations might include .txt files which are now .html files in newer translations. - # Therefore, ignore the extension and try both .html and .txt. - for tryExt in ("html", "txt"): - tryPath = os.path.join(tryDir, "%s.%s" % (fileName, tryExt)) - if os.path.isfile(tryPath): - return tryPath - return None - else: - # Not localized. - if not hasattr(sys, "frozen") and fileName in ("copying.txt", "contributors.txt"): - # If running from source, these two files are in the root dir. - return os.path.join(NVDA_PATH, "..", fileName) - else: - return os.path.join(getDocFilePath.rootPath, fileName) -getDocFilePath.rootPath = None class MainFrame(wx.Frame): @@ -530,6 +482,7 @@ def __init__(self, frame): self.Bind(wx.EVT_MENU, lambda evt: os.startfile(getDocFilePath("contributors.txt", False)), item) # Translators: The label for the menu item to open NVDA Welcome Dialog. item = menu_help.Append(wx.ID_ANY, _("We&lcome dialog...")) + from .startupDialogs import WelcomeDialog self.Bind(wx.EVT_MENU, lambda evt: WelcomeDialog.run(), item) menu_help.AppendSeparator() if updateCheck: @@ -694,189 +647,6 @@ def run(): wx.CallAfter(run) -class WelcomeDialog( - _ContextHelpMixin, - wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO -): - """The NVDA welcome dialog. - This provides essential information for new users, such as a description of the NVDA key and instructions on how to activate the NVDA menu. - It also provides quick access to some important configuration options. - This dialog is displayed the first time NVDA is started with a new configuration. - """ - helpId = "WelcomeDialog" - WELCOME_MESSAGE_DETAIL = _( - # Translators: The main message for the Welcome dialog when the user starts NVDA for the first time. - "Most commands for controlling NVDA require you to hold down" - " the NVDA key while pressing other keys.\n" - "By default, the numpad Insert and main Insert keys may both be used as the NVDA key.\n" - "You can also configure NVDA to use the CapsLock as the NVDA key.\n" - "Press NVDA+n at any time to activate the NVDA menu.\n" - "From this menu, you can configure NVDA, get help and access other NVDA functions." - ) - - def __init__(self, parent): - # Translators: The title of the Welcome dialog when user starts NVDA for the first time. - super(WelcomeDialog, self).__init__(parent, wx.ID_ANY, _("Welcome to NVDA")) - - mainSizer=wx.BoxSizer(wx.VERTICAL) - # Translators: The header for the Welcome dialog when user starts NVDA for the first time. This is in larger, - # bold lettering - welcomeTextHeader = wx.StaticText(self, label=_("Welcome to NVDA!")) - welcomeTextHeader.SetFont(wx.Font(18, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.BOLD)) - mainSizer.AddSpacer(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) - mainSizer.Add(welcomeTextHeader,border=20,flag=wx.EXPAND|wx.LEFT|wx.RIGHT) - mainSizer.AddSpacer(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) - welcomeTextDetail = wx.StaticText(self, wx.ID_ANY, self.WELCOME_MESSAGE_DETAIL) - mainSizer.Add(welcomeTextDetail,border=20,flag=wx.EXPAND|wx.LEFT|wx.RIGHT) - - # Translators: The label for a group box containing the NVDA welcome dialog options. - optionsLabel = _("Options") - optionsSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=optionsLabel) - optionsBox = optionsSizer.GetStaticBox() - sHelper = guiHelper.BoxSizerHelper(self, sizer=optionsSizer) - # Translators: The label of a combobox in the Welcome dialog. - kbdLabelText = _("&Keyboard layout:") - layouts = keyboardHandler.KeyboardInputGesture.LAYOUTS - self.kbdNames = sorted(layouts) - kbdChoices = [layouts[layout] for layout in self.kbdNames] - self.kbdList = sHelper.addLabeledControl(kbdLabelText, wx.Choice, choices=kbdChoices) - try: - index = self.kbdNames.index(config.conf["keyboard"]["keyboardLayout"]) - self.kbdList.SetSelection(index) - except: - log.error("Could not set Keyboard layout list to current layout",exc_info=True) - # Translators: The label of a checkbox in the Welcome dialog. - capsAsNVDAModifierText = _("&Use CapsLock as an NVDA modifier key") - self.capsAsNVDAModifierCheckBox = sHelper.addItem(wx.CheckBox(optionsBox, label=capsAsNVDAModifierText)) - self.capsAsNVDAModifierCheckBox.SetValue(config.conf["keyboard"]["useCapsLockAsNVDAModifierKey"]) - # Translators: The label of a checkbox in the Welcome dialog. - startAfterLogonText = _("St&art NVDA after I sign in") - self.startAfterLogonCheckBox = sHelper.addItem(wx.CheckBox(optionsBox, label=startAfterLogonText)) - self.startAfterLogonCheckBox.Value = config.getStartAfterLogon() - if globalVars.appArgs.secure or config.isAppX or not config.isInstalledCopy(): - self.startAfterLogonCheckBox.Disable() - # Translators: The label of a checkbox in the Welcome dialog. - showWelcomeDialogAtStartupText = _("&Show this dialog when NVDA starts") - _showWelcomeDialogAtStartupCheckBox = wx.CheckBox(optionsBox, label=showWelcomeDialogAtStartupText) - self.showWelcomeDialogAtStartupCheckBox = sHelper.addItem(_showWelcomeDialogAtStartupCheckBox) - self.showWelcomeDialogAtStartupCheckBox.SetValue(config.conf["general"]["showWelcomeDialogAtStartup"]) - mainSizer.Add(optionsSizer, border=guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL) - mainSizer.Add(self.CreateButtonSizer(wx.OK), border=guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL|wx.ALIGN_RIGHT) - self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK) - - mainSizer.Fit(self) - self.SetSizer(mainSizer) - self.kbdList.SetFocus() - self.CentreOnScreen() - - def onOk(self, evt): - layout = self.kbdNames[self.kbdList.GetSelection()] - config.conf["keyboard"]["keyboardLayout"] = layout - config.conf["keyboard"]["useCapsLockAsNVDAModifierKey"] = self.capsAsNVDAModifierCheckBox.IsChecked() - if self.startAfterLogonCheckBox.Enabled: - config.setStartAfterLogon(self.startAfterLogonCheckBox.Value) - config.conf["general"]["showWelcomeDialogAtStartup"] = self.showWelcomeDialogAtStartupCheckBox.IsChecked() - try: - config.conf.save() - except: - log.debugWarning("Could not save",exc_info=True) - self.EndModal(wx.ID_OK) - - @classmethod - def run(cls): - """Prepare and display an instance of this dialog. - This does not require the dialog to be instantiated. - """ - mainFrame.prePopup() - d = cls(mainFrame) - d.ShowModal() - d.Destroy() - mainFrame.postPopup() - - -class LauncherDialog( - _ContextHelpMixin, - wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO -): - """The dialog that is displayed when NVDA is started from the launcher. - This displays the license and allows the user to install or create a portable copy of NVDA. - """ - helpId = "InstallingNVDA" - - def __init__(self, parent): - super(LauncherDialog, self).__init__(parent, title=versionInfo.name) - - mainSizer = wx.BoxSizer(wx.VERTICAL) - sHelper = guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) - - # Translators: The label of the license text which will be shown when NVDA installation program starts. - groupLabel = _("License Agreement") - sizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=groupLabel) - sHelper.addItem(sizer) - licenseTextCtrl = wx.TextCtrl(self, size=(500, 400), style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH) - licenseTextCtrl.Value = codecs.open(getDocFilePath("copying.txt", False), "r", encoding="UTF-8").read() - sizer.Add(licenseTextCtrl) - - # Translators: The label for a checkbox in NvDA installation program to agree to the license agreement. - agreeText = _("I &agree") - self.licenseAgreeCheckbox = sHelper.addItem(wx.CheckBox(self, label=agreeText)) - self.licenseAgreeCheckbox.Value = False - self.licenseAgreeCheckbox.Bind(wx.EVT_CHECKBOX, self.onLicenseAgree) - - sizer = sHelper.addItem(wx.GridSizer(2, 2, 0, 0)) - self.actionButtons = [] - # Translators: The label of the button in NVDA installation program to install NvDA on the user's computer. - ctrl = wx.Button(self, label=_("&Install NVDA on this computer")) - sizer.Add(ctrl) - ctrl.Bind(wx.EVT_BUTTON, lambda evt: self.onAction(evt, mainFrame.onInstallCommand)) - self.actionButtons.append(ctrl) - # Translators: The label of the button in NVDA installation program to create a portable version of NVDA. - ctrl = wx.Button(self, label=_("Create &portable copy")) - sizer.Add(ctrl) - ctrl.Bind(wx.EVT_BUTTON, lambda evt: self.onAction(evt, mainFrame.onCreatePortableCopyCommand)) - self.actionButtons.append(ctrl) - # Translators: The label of the button in NVDA installation program to continue using the installation program as a temporary copy of NVDA. - ctrl = wx.Button(self, label=_("&Continue running")) - sizer.Add(ctrl) - ctrl.Bind(wx.EVT_BUTTON, self.onContinueRunning) - self.actionButtons.append(ctrl) - sizer.Add(wx.Button(self, label=_("E&xit"), id=wx.ID_CANCEL)) - # If we bind this on the button, it fails to trigger when the dialog is closed. - self.Bind(wx.EVT_BUTTON, self.onExit, id=wx.ID_CANCEL) - - for ctrl in self.actionButtons: - ctrl.Disable() - - mainSizer.Add(sHelper.sizer, border = guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL) - self.Sizer = mainSizer - mainSizer.Fit(self) - self.CentreOnScreen() - - def onLicenseAgree(self, evt): - for ctrl in self.actionButtons: - ctrl.Enable(evt.IsChecked()) - - def onAction(self, evt, func): - self.Destroy() - func(evt) - - def onContinueRunning(self, evt): - self.Destroy() - core.doStartupDialogs() - - def onExit(self, evt): - wx.GetApp().ExitMainLoop() - - @classmethod - def run(cls): - """Prepare and display an instance of this dialog. - This does not require the dialog to be instantiated. - """ - mainFrame.prePopup() - d = cls(mainFrame) - d.Show() - mainFrame.postPopup() - class ExitDialog(wx.Dialog): _instance = None @@ -1087,67 +857,3 @@ def Notify(self): def _isDebug(): return config.conf["debugLog"]["gui"] - - -class AskAllowUsageStatsDialog( - _ContextHelpMixin, - wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO -): - """A dialog asking if the user wishes to allow NVDA usage stats to be collected by NV Access.""" - - helpId = "UsageStatsDialog" - - def __init__(self, parent): - # Translators: The title of the dialog asking if usage data can be collected - super().__init__(parent, title=_("NVDA Usage Data Collection")) - mainSizer = wx.BoxSizer(wx.VERTICAL) - sHelper = guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) - - # Translators: A message asking the user if they want to allow usage stats gathering - message=_("In order to improve NVDA in the future, NV Access wishes to collect usage data from running copies of NVDA.\n\n" - "Data includes Operating System version, NVDA version, language, country of origin, plus certain NVDA configuration such as current synthesizer, braille display and braille table. " - "No spoken or braille content will be ever sent to NV Access. Please refer to the User Guide for a current list of all data collected.\n\n" - "Do you wish to allow NV Access to periodically collect this data in order to improve NVDA?") - sText=sHelper.addItem(wx.StaticText(self, label=message)) - # the wx.Window must be constructed before we can get the handle. - import windowUtils - self.scaleFactor = windowUtils.getWindowScalingFactor(self.GetHandle()) - sText.Wrap(self.scaleFactor*600) # 600 was fairly arbitrarily chosen by a visual user to look acceptable on their machine. - - bHelper = sHelper.addDialogDismissButtons(guiHelper.ButtonHelper(wx.HORIZONTAL)) - - # Translators: The label of a Yes button in a dialog - yesButton = bHelper.addButton(self, wx.ID_YES, label=_("&Yes")) - yesButton.Bind(wx.EVT_BUTTON, self.onYesButton) - - # Translators: The label of a No button in a dialog - noButton = bHelper.addButton(self, wx.ID_NO, label=_("&No")) - noButton.Bind(wx.EVT_BUTTON, self.onNoButton) - - # Translators: The label of a button to remind the user later about performing some action. - remindMeButton = bHelper.addButton(self, wx.ID_CANCEL, label=_("Remind me &later")) - remindMeButton.Bind(wx.EVT_BUTTON, self.onLaterButton) - remindMeButton.SetFocus() - - mainSizer.Add(sHelper.sizer, border=guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL) - self.Sizer = mainSizer - mainSizer.Fit(self) - self.Center(wx.BOTH | wx.CENTER_ON_SCREEN) - - def onYesButton(self,evt): - log.debug("Usage stats gathering has been allowed") - config.conf['update']['askedAllowUsageStats']=True - config.conf['update']['allowUsageStats']=True - self.EndModal(wx.ID_YES) - - def onNoButton(self,evt): - log.debug("Usage stats gathering has been disallowed") - config.conf['update']['askedAllowUsageStats']=True - config.conf['update']['allowUsageStats']=False - self.EndModal(wx.ID_NO) - - def onLaterButton(self,evt): - log.debug("Usage stats gathering question has been deferred") - # evt.Skip() is called since wx.ID_CANCEL is used as the ID for the Ask Later button, - # wx automatically ends the modal itself. - evt.Skip() diff --git a/source/gui/contextHelp.py b/source/gui/contextHelp.py index 5c562d25f7a..b9b2a7d54bb 100644 --- a/source/gui/contextHelp.py +++ b/source/gui/contextHelp.py @@ -9,6 +9,7 @@ import wx from logHandler import log +import documentationUtils def writeRedirect(helpId: str, helpFilePath: str, contextHelpPath: str): @@ -34,9 +35,7 @@ def showHelp(helpId: str): noHelpMessage = _("No help available here.") queueHandler.queueFunction(queueHandler.eventQueue, ui.message, noHelpMessage) return - - import gui - helpFile = gui.getDocFilePath("userGuide.html") + helpFile = documentationUtils.getDocFilePath("userGuide.html") if helpFile is None: # Translators: Message shown when trying to display context sensitive help, # indicating that the user guide could not be found. diff --git a/source/gui/startupDialogs.py b/source/gui/startupDialogs.py new file mode 100644 index 00000000000..a95ce8fde60 --- /dev/null +++ b/source/gui/startupDialogs.py @@ -0,0 +1,278 @@ +# -*- coding: UTF-8 -*- +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2006-2021 NV Access Limited, Łukasz Golonka +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + +import wx + +import config +import core +from documentationUtils import getDocFilePath +import globalVars +import gui +import keyboardHandler +from logHandler import log +import versionInfo + + +class WelcomeDialog( + gui.contextHelp.ContextHelpMixin, + wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO +): + """The NVDA welcome dialog. + This provides essential information for new users, + such as a description of the NVDA key and instructions on how to activate the NVDA menu. + It also provides quick access to some important configuration options. + This dialog is displayed the first time NVDA is started with a new configuration. + """ + helpId = "WelcomeDialog" + WELCOME_MESSAGE_DETAIL = _( + # Translators: The main message for the Welcome dialog when the user starts NVDA for the first time. + "Most commands for controlling NVDA require you to hold down" + " the NVDA key while pressing other keys.\n" + "By default, the numpad Insert and main Insert keys may both be used as the NVDA key.\n" + "You can also configure NVDA to use the CapsLock as the NVDA key.\n" + "Press NVDA+n at any time to activate the NVDA menu.\n" + "From this menu, you can configure NVDA, get help and access other NVDA functions." + ) + + def __init__(self, parent): + # Translators: The title of the Welcome dialog when user starts NVDA for the first time. + super().__init__(parent, wx.ID_ANY, _("Welcome to NVDA")) + + mainSizer = wx.BoxSizer(wx.VERTICAL) + # Translators: The header for the Welcome dialog when user starts NVDA for the first time. + # This is in larger, bold lettering + welcomeTextHeader = wx.StaticText(self, label=_("Welcome to NVDA!")) + welcomeTextHeader.SetFont(wx.Font(18, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.BOLD)) + mainSizer.AddSpacer(gui.guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) + mainSizer.Add(welcomeTextHeader, border=20, flag=wx.EXPAND | wx.LEFT | wx.RIGHT) + mainSizer.AddSpacer(gui.guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) + welcomeTextDetail = wx.StaticText(self, wx.ID_ANY, self.WELCOME_MESSAGE_DETAIL) + mainSizer.Add(welcomeTextDetail, border=20, flag=wx.EXPAND | wx.LEFT | wx.RIGHT) + + # Translators: The label for a group box containing the NVDA welcome dialog options. + optionsLabel = _("Options") + optionsSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=optionsLabel) + optionsBox = optionsSizer.GetStaticBox() + sHelper = gui.guiHelper.BoxSizerHelper(self, sizer=optionsSizer) + # Translators: The label of a combobox in the Welcome dialog. + kbdLabelText = _("&Keyboard layout:") + layouts = keyboardHandler.KeyboardInputGesture.LAYOUTS + self.kbdNames = sorted(layouts) + kbdChoices = [layouts[layout] for layout in self.kbdNames] + self.kbdList = sHelper.addLabeledControl(kbdLabelText, wx.Choice, choices=kbdChoices) + try: + index = self.kbdNames.index(config.conf["keyboard"]["keyboardLayout"]) + self.kbdList.SetSelection(index) + except (ValueError, KeyError): + log.error("Could not set Keyboard layout list to current layout", exc_info=True) + # Translators: The label of a checkbox in the Welcome dialog. + capsAsNVDAModifierText = _("&Use CapsLock as an NVDA modifier key") + self.capsAsNVDAModifierCheckBox = sHelper.addItem(wx.CheckBox(optionsBox, label=capsAsNVDAModifierText)) + self.capsAsNVDAModifierCheckBox.SetValue(config.conf["keyboard"]["useCapsLockAsNVDAModifierKey"]) + # Translators: The label of a checkbox in the Welcome dialog. + startAfterLogonText = _("St&art NVDA after I sign in") + self.startAfterLogonCheckBox = sHelper.addItem(wx.CheckBox(optionsBox, label=startAfterLogonText)) + self.startAfterLogonCheckBox.Value = config.getStartAfterLogon() + if globalVars.appArgs.secure or config.isAppX or not config.isInstalledCopy(): + self.startAfterLogonCheckBox.Disable() + # Translators: The label of a checkbox in the Welcome dialog. + showWelcomeDialogAtStartupText = _("&Show this dialog when NVDA starts") + _showWelcomeDialogAtStartupCheckBox = wx.CheckBox(optionsBox, label=showWelcomeDialogAtStartupText) + self.showWelcomeDialogAtStartupCheckBox = sHelper.addItem(_showWelcomeDialogAtStartupCheckBox) + self.showWelcomeDialogAtStartupCheckBox.SetValue(config.conf["general"]["showWelcomeDialogAtStartup"]) + mainSizer.Add(optionsSizer, border=gui.guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL) + mainSizer.Add( + self.CreateButtonSizer(wx.OK), + border=gui.guiHelper.BORDER_FOR_DIALOGS, + flag=wx.ALL | wx.ALIGN_RIGHT + ) + self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK) + + mainSizer.Fit(self) + self.SetSizer(mainSizer) + self.kbdList.SetFocus() + self.CentreOnScreen() + + def onOk(self, evt): + layout = self.kbdNames[self.kbdList.GetSelection()] + config.conf["keyboard"]["keyboardLayout"] = layout + config.conf["keyboard"]["useCapsLockAsNVDAModifierKey"] = self.capsAsNVDAModifierCheckBox.IsChecked() + if self.startAfterLogonCheckBox.Enabled: + config.setStartAfterLogon(self.startAfterLogonCheckBox.Value) + config.conf["general"]["showWelcomeDialogAtStartup"] = self.showWelcomeDialogAtStartupCheckBox.IsChecked() + try: + config.conf.save() + except Exception: + log.debugWarning("Could not save", exc_info=True) + self.EndModal(wx.ID_OK) + + @classmethod + def run(cls): + """Prepare and display an instance of this dialog. + This does not require the dialog to be instantiated. + """ + gui.mainFrame.prePopup() + d = cls(gui.mainFrame) + d.ShowModal() + d.Destroy() + gui.mainFrame.postPopup() + + +class LauncherDialog( + gui.contextHelp.ContextHelpMixin, + wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO +): + """The dialog that is displayed when NVDA is started from the launcher. + This displays the license and allows the user to install or create a portable copy of NVDA. + """ + helpId = "InstallingNVDA" + + def __init__(self, parent): + super().__init__(parent, title=versionInfo.name) + + mainSizer = wx.BoxSizer(wx.VERTICAL) + sHelper = gui.guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) + + # Translators: The label of the license text which will be shown when NVDA installation program starts. + groupLabel = _("License Agreement") + sizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=groupLabel) + sHelper.addItem(sizer) + licenseTextCtrl = wx.TextCtrl(self, size=(500, 400), style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH) + licenseTextCtrl.Value = open(getDocFilePath("copying.txt", False), "r", encoding="UTF-8").read() + sizer.Add(licenseTextCtrl) + + # Translators: The label for a checkbox in NvDA installation program to agree to the license agreement. + agreeText = _("I &agree") + self.licenseAgreeCheckbox = sHelper.addItem(wx.CheckBox(self, label=agreeText)) + self.licenseAgreeCheckbox.Value = False + self.licenseAgreeCheckbox.Bind(wx.EVT_CHECKBOX, self.onLicenseAgree) + + sizer = sHelper.addItem(wx.GridSizer(2, 2, 0, 0)) + self.actionButtons = [] + # Translators: The label of the button in NVDA installation program to install NvDA on the user's computer. + ctrl = wx.Button(self, label=_("&Install NVDA on this computer")) + sizer.Add(ctrl) + ctrl.Bind(wx.EVT_BUTTON, lambda evt: self.onAction(evt, gui.mainFrame.onInstallCommand)) + self.actionButtons.append(ctrl) + # Translators: The label of the button in NVDA installation program to create a portable version of NVDA. + ctrl = wx.Button(self, label=_("Create &portable copy")) + sizer.Add(ctrl) + ctrl.Bind(wx.EVT_BUTTON, lambda evt: self.onAction(evt, gui.mainFrame.onCreatePortableCopyCommand)) + self.actionButtons.append(ctrl) + # Translators: The label of the button in NVDA installation program + # to continue using the installation program as a temporary copy of NVDA. + ctrl = wx.Button(self, label=_("&Continue running")) + sizer.Add(ctrl) + ctrl.Bind(wx.EVT_BUTTON, self.onContinueRunning) + self.actionButtons.append(ctrl) + sizer.Add(wx.Button(self, label=_("E&xit"), id=wx.ID_CANCEL)) + # If we bind this on the button, it fails to trigger when the dialog is closed. + self.Bind(wx.EVT_BUTTON, self.onExit, id=wx.ID_CANCEL) + + for ctrl in self.actionButtons: + ctrl.Disable() + + mainSizer.Add(sHelper.sizer, border=gui.guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL) + self.Sizer = mainSizer + mainSizer.Fit(self) + self.CentreOnScreen() + + def onLicenseAgree(self, evt): + for ctrl in self.actionButtons: + ctrl.Enable(evt.IsChecked()) + + def onAction(self, evt, func): + self.Destroy() + func(evt) + + def onContinueRunning(self, evt): + self.Destroy() + core.doStartupDialogs() + + def onExit(self, evt): + wx.GetApp().ExitMainLoop() + + @classmethod + def run(cls): + """Prepare and display an instance of this dialog. + This does not require the dialog to be instantiated. + """ + gui.mainFrame.prePopup() + d = cls(gui.mainFrame) + d.Show() + gui.mainFrame.postPopup() + + +class AskAllowUsageStatsDialog( + gui.contextHelp.ContextHelpMixin, + wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO +): + """A dialog asking if the user wishes to allow NVDA usage stats to be collected by NV Access.""" + + helpId = "UsageStatsDialog" + + def __init__(self, parent): + # Translators: The title of the dialog asking if usage data can be collected + super().__init__(parent, title=_("NVDA Usage Data Collection")) + mainSizer = wx.BoxSizer(wx.VERTICAL) + sHelper = gui.guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) + + message = _( + # Translators: A message asking the user if they want to allow usage stats gathering + "In order to improve NVDA in the future, " + "NV Access wishes to collect usage data from running copies of NVDA.\n\n" + "Data includes Operating System version, NVDA version, language, country of origin, plus " + "certain NVDA configuration such as current synthesizer, braille display and braille table. " + "No spoken or braille content will be ever sent to NV Access. " + "Please refer to the User Guide for a current list of all data collected.\n\n" + "Do you wish to allow NV Access to periodically collect this data in order to improve NVDA?" + ) + sText = sHelper.addItem(wx.StaticText(self, label=message)) + # the wx.Window must be constructed before we can get the handle. + import windowUtils + self.scaleFactor = windowUtils.getWindowScalingFactor(self.GetHandle()) + sText.Wrap( + # 600 was fairly arbitrarily chosen by a visual user to look acceptable on their machine. + self.scaleFactor * 600 + ) + + bHelper = sHelper.addDialogDismissButtons(gui.guiHelper.ButtonHelper(wx.HORIZONTAL)) + + # Translators: The label of a Yes button in a dialog + yesButton = bHelper.addButton(self, wx.ID_YES, label=_("&Yes")) + yesButton.Bind(wx.EVT_BUTTON, self.onYesButton) + + # Translators: The label of a No button in a dialog + noButton = bHelper.addButton(self, wx.ID_NO, label=_("&No")) + noButton.Bind(wx.EVT_BUTTON, self.onNoButton) + + # Translators: The label of a button to remind the user later about performing some action. + remindMeButton = bHelper.addButton(self, wx.ID_CANCEL, label=_("Remind me &later")) + remindMeButton.Bind(wx.EVT_BUTTON, self.onLaterButton) + remindMeButton.SetFocus() + + mainSizer.Add(sHelper.sizer, border=gui.guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL) + self.Sizer = mainSizer + mainSizer.Fit(self) + self.Center(wx.BOTH | wx.CENTER_ON_SCREEN) + + def onYesButton(self, evt): + log.debug("Usage stats gathering has been allowed") + config.conf['update']['askedAllowUsageStats'] = True + config.conf['update']['allowUsageStats'] = True + self.EndModal(wx.ID_YES) + + def onNoButton(self, evt): + log.debug("Usage stats gathering has been disallowed") + config.conf['update']['askedAllowUsageStats'] = True + config.conf['update']['allowUsageStats'] = False + self.EndModal(wx.ID_NO) + + def onLaterButton(self, evt): + log.debug("Usage stats gathering question has been deferred") + # evt.Skip() is called since wx.ID_CANCEL is used as the ID for the Ask Later button, + # wx automatically ends the modal itself. + evt.Skip() diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 7d782ab1f69..a430c2a9903 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -86,6 +86,8 @@ What's New in NVDA - Support of `TextInfo`s that do not inherit from `contentRecog.BaseContentRecogTextInfo` is removed. (#12157) - `speech.speakWithoutPauses` has been removed - please use `speech.SpeechWithoutPauses(speakFunc=speech.speak).speakWithoutPauses` instead. (#12195) - `speech.re_last_pause` has been removed - please use `speech.SpeechWithoutPauses.re_last_pause` instead. (#12195) +- `WelcomeDialog`, `LauncherDialog` and `AskAllowUsageStatsDialog` are moved to the `gui.startupDialogs`. (#12105) +- `getDocFilePath` has been moved from `gui` to the `documentationUtils` module. (#12105) = 2020.4 = From 6ede26783c39e01906fcb82cdb7a455311a38fd1 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Tue, 23 Mar 2021 16:36:08 +0800 Subject: [PATCH 098/174] Fix routing to cell in selection (braille) (PR #12208) When routing to a cell within a selection, an error occurred. When there is a selection self.brailleCursorPos is None. --- source/braille.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/source/braille.py b/source/braille.py index bbdee9f0d78..7c153aa5d62 100644 --- a/source/braille.py +++ b/source/braille.py @@ -407,8 +407,7 @@ def __init__(self): #: @type: [int, ...] self.brailleToRawPos = [] #: The position of the cursor in L{brailleCells}, C{None} if the cursor is not in this region. - #: @type: int - self.brailleCursorPos = None + self.brailleCursorPos: Optional[int] = None #: The position of the selection start in L{brailleCells}, C{None} if there is no selection in this region. #: @type: int self.brailleSelectionStart = None @@ -1094,15 +1093,20 @@ def routeTo(self, braillePos): return dest = self.getTextInfoForBraillePos(braillePos) - cursor = self.getTextInfoForBraillePos(self.brailleCursorPos) - if dest.compareEndPoints(cursor, "startToStart") == 0: - # The cursor is already at this position, - # so activate the position. - try: - self._getSelection().activate() - except NotImplementedError: - pass - return + # When there is a selection, brailleCursorPos will be None + # Don't activate, but move the cursor to the new cell (dropping the + # selection). An alternative behavior may be to activate on the selection. + # Moving the cursor was considered more intuitive. + if self.brailleCursorPos is not None: + cursor = self.getTextInfoForBraillePos(self.brailleCursorPos) + if dest.compareEndPoints(cursor, "startToStart") == 0: + # The cursor is already at this position, + # so activate the position. + try: + self._getSelection().activate() + except NotImplementedError: + pass + return self._setCursor(dest) def nextLine(self): From ae38fcc7fcae1f1888bbdeb1ffc7e3af87ab9ed2 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Tue, 23 Mar 2021 10:46:22 +0100 Subject: [PATCH 099/174] Remove Adobe Flash support from NVDA (PR #12207) Support for Adobe Flash content has been removed from NVDA due to the use of Flash being actively discouraged by Adobe. Fixes #11131 Co-authored-by: Reef Turner --- nvdaHelper/remote/sconscript | 1 - nvdaHelper/remote/vbufRemote.cpp | 1 - .../vbufBackends/adobeFlash/adobeFlash.cpp | 308 ------------------ .../vbufBackends/adobeFlash/adobeFlash.h | 51 --- nvdaHelper/vbufBackends/adobeFlash/sconscript | 21 -- nvdaHelper/vbufBase/backend.h | 1 - readme.md | 1 - source/NVDAObjects/IAccessible/__init__.py | 14 - source/NVDAObjects/IAccessible/adobeFlash.py | 101 ------ source/comInterfaces_sconscript | 1 - source/controlTypes.py | 2 +- source/virtualBuffers/adobeFlash.py | 132 -------- user_docs/en/changes.t2t | 1 + user_docs/en/userGuide.t2t | 3 +- 14 files changed, 3 insertions(+), 635 deletions(-) delete mode 100644 nvdaHelper/vbufBackends/adobeFlash/adobeFlash.cpp delete mode 100644 nvdaHelper/vbufBackends/adobeFlash/adobeFlash.h delete mode 100644 nvdaHelper/vbufBackends/adobeFlash/sconscript delete mode 100644 source/NVDAObjects/IAccessible/adobeFlash.py delete mode 100644 source/virtualBuffers/adobeFlash.py diff --git a/nvdaHelper/remote/sconscript b/nvdaHelper/remote/sconscript index 80a66432707..9604944ed7e 100644 --- a/nvdaHelper/remote/sconscript +++ b/nvdaHelper/remote/sconscript @@ -23,7 +23,6 @@ winIPCUtilsObj=env.Object("./winIPCUtils","../common/winIPCUtils.cpp") vbufBackendLibs=[ env.SConscript('../vbufBase/sconscript'), env.SConscript('../vbufBackends/adobeAcrobat/sconscript'), - env.SConscript('../vbufBackends/adobeFlash/sconscript'), env.SConscript('../vbufBackends/lotusNotesRichText/sconscript'), env.SConscript('../vbufBackends/gecko_ia2/sconscript'), env.SConscript('../vbufBackends/mshtml/sconscript'), diff --git a/nvdaHelper/remote/vbufRemote.cpp b/nvdaHelper/remote/vbufRemote.cpp index 0ce6b35ac16..bfbfb1eefad 100755 --- a/nvdaHelper/remote/vbufRemote.cpp +++ b/nvdaHelper/remote/vbufRemote.cpp @@ -22,7 +22,6 @@ using namespace std; const map VBufBackendFactoryMap { {L"adobeAcrobat",AdobeAcrobatVBufBackend_t_createInstance}, - {L"adobeFlash",AdobeFlashVBufBackend_t_createInstance}, {L"gecko_ia2",GeckoVBufBackend_t_createInstance}, {L"mshtml",MshtmlVBufBackend_t_createInstance}, {L"lotusNotesRichText",lotusNotesRichTextVBufBackend_t_createInstance}, diff --git a/nvdaHelper/vbufBackends/adobeFlash/adobeFlash.cpp b/nvdaHelper/vbufBackends/adobeFlash/adobeFlash.cpp deleted file mode 100644 index d76fd501386..00000000000 --- a/nvdaHelper/vbufBackends/adobeFlash/adobeFlash.cpp +++ /dev/null @@ -1,308 +0,0 @@ -/* -This file is a part of the NVDA project. -URL: http://www.nvda-project.org/ -Copyright 2010-2013 NV Access Limited - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License version 2.0, as published by - the Free Software Foundation. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -This license can be found at: -http://www.gnu.org/licenses/old-licenses/gpl-2.0.html -*/ - -#include -#include -#include -#include -#include -#include -#include -#include -#include "adobeFlash.h" - -using namespace std; - -void AdobeFlashVBufBackend_t::renderThread_initialize() { - registerWinEventHook(renderThread_winEventProcHook); - VBufBackend_t::renderThread_initialize(); -} - -void AdobeFlashVBufBackend_t::renderThread_terminate() { - unregisterWinEventHook(renderThread_winEventProcHook); - if (this->accPropServices) - this->accPropServices->Release(); - VBufBackend_t::renderThread_terminate(); -} - -void CALLBACK AdobeFlashVBufBackend_t::renderThread_winEventProcHook(HWINEVENTHOOK hookID, DWORD eventID, HWND hwnd, long objectID, long childID, DWORD threadID, DWORD time) { - switch(eventID) { - case EVENT_OBJECT_REORDER: - case EVENT_OBJECT_NAMECHANGE: - case EVENT_OBJECT_VALUECHANGE: - case EVENT_OBJECT_STATECHANGE: - break; - default: - return; - } - - int docHandle=HandleToUlong(hwnd); - int ID=(childID==CHILDID_SELF&&objectID>0)?objectID:childID; - for(VBufBackendSet_t::iterator i=runningBackends.begin();i!=runningBackends.end();++i) { - AdobeFlashVBufBackend_t* backend=NULL; - HWND rootWindow=(HWND)UlongToHandle(((*i)->rootDocHandle)); - if(rootWindow!=hwnd) - continue; - backend=static_cast(*i); - VBufStorage_controlFieldNode_t* node=backend->getControlFieldNodeWithIdentifier(docHandle,ID); - if(node) - backend->invalidateSubtree(node); - if(!backend->isWindowless) { - // If this is not windowless, there can only be one backend with this docHandle, - // so stop searching. - break; - } - } -} - -VBufStorage_fieldNode_t* AdobeFlashVBufBackend_t::renderControlContent(VBufStorage_buffer_t* buffer, VBufStorage_controlFieldNode_t* parentNode, VBufStorage_fieldNode_t* previousNode, int docHandle, int id, IAccessible* pacc) { - nhAssert(buffer); - - HRESULT res; - //all IAccessible methods take a variant for childID, get one ready - VARIANT varChild; - varChild.vt=VT_I4; - varChild.lVal=CHILDID_SELF; - - VBufStorage_fieldNode_t* tempNode=NULL; - - //Make sure that we don't already know about this object -- protect from loops - if(buffer->getControlFieldNodeWithIdentifier(docHandle,id)!=NULL) { - return NULL; - } - - //Add this node to the buffer - parentNode=buffer->addControlFieldNode(parentNode,previousNode,docHandle,id,TRUE); - nhAssert(parentNode); //new node must have been created - previousNode=NULL; - - wostringstream s; - - // Get role with accRole - long role = 0; - VARIANT varRole; - VariantInit(&varRole); - if((res=pacc->get_accRole(varChild,&varRole))!=S_OK) { - s<<0; - } else if(varRole.vt==VT_BSTR) { - s << varRole.bstrVal; - } else if(varRole.vt==VT_I4) { - s << varRole.lVal; - role = varRole.lVal; - } - parentNode->addAttribute(L"IAccessible::role",s.str()); - VariantClear(&varRole); - - // Get states with accState - VARIANT varState; - VariantInit(&varState); - if((res=pacc->get_accState(varChild,&varState))!=S_OK) { - varState.vt=VT_I4; - varState.lVal=0; - } - int states=varState.lVal; - VariantClear(&varState); - //Add each state that is on, as an attrib - for(int i=0;i<32;++i) { - int state=1<addAttribute(s.str(),L"1"); - } - } - - BSTR tempBstr=NULL; - wstring name; - wstring value; - wstring content; - - if ((res = pacc->get_accName(varChild, &tempBstr)) == S_OK) { - name = tempBstr; - SysFreeString(tempBstr); - } - if ((res = pacc->get_accValue(varChild, &tempBstr)) == S_OK) { - value = tempBstr; - SysFreeString(tempBstr); - } - if(!value.empty()) { - content=value; - } else if(role!=ROLE_SYSTEM_TEXT&&!name.empty()) { - content=name; - } else if (states & STATE_SYSTEM_FOCUSABLE) { - // This node is focusable, but contains no text. - // Therefore, add it with a space so that the user can get to it. - content = L" "; - } - - if (!content.empty()) { - if (tempNode = buffer->addTextFieldNode(parentNode, previousNode, content)) { - previousNode=tempNode; - } - } - - return parentNode; -} - -long AdobeFlashVBufBackend_t::getAccId(IAccessible* acc) { - IAccIdentity* accId = NULL; - if (acc->QueryInterface(IID_IAccIdentity, (void**)&accId) != S_OK || !accId) - return -1; - BYTE* idString=NULL; - DWORD idLen=0; - HRESULT res; - res = accId->GetIdentityString(CHILDID_SELF, &idString, &idLen); - accId->Release(); - if (res != S_OK || !idString) - return -1; - if (!this->accPropServices) { - // Only retrieve this the first time it's needed. - if (CoCreateInstance(CLSID_AccPropServices, NULL, CLSCTX_SERVER, IID_IAccPropServices, (void**)&this->accPropServices) != S_OK) { - CoTaskMemFree(idString); - return -1; - } - } - HWND hwnd; - DWORD objId; - DWORD childId; - res = this->accPropServices->DecomposeHwndIdentityString(idString, idLen, &hwnd, &objId, &childId); - CoTaskMemFree(idString); - if (res != S_OK) - return -1; - return objId; -} - -void AdobeFlashVBufBackend_t::render(VBufStorage_buffer_t* buffer, int docHandle, int ID, VBufStorage_controlFieldNode_t* oldNode) { - if (!oldNode) { - // This is the initial render. - WCHAR* wclass = (WCHAR*)malloc(sizeof(WCHAR) * 256); - if (!wclass) - return; - if (GetClassName((HWND)UlongToHandle(docHandle), wclass, 256) == 0) { - free(wclass); - return; - } - this->isWindowless = wcscmp(wclass, L"Internet Explorer_Server") == 0; - free(wclass); - } - - DWORD_PTR res=0; - //Get an IAccessible by sending WM_GETOBJECT directly to bypass any proxying, to speed things up. - if (SendMessageTimeout((HWND)UlongToHandle(docHandle), WM_GETOBJECT, 0, isWindowless ? ID : OBJID_CLIENT, SMTO_ABORTIFHUNG, 2000, &res) == 0 || res == 0) { - //Failed to send message or window does not support IAccessible - return; - } - IAccessible* pacc=NULL; - if(ObjectFromLresult(res,IID_IAccessible,0,(void**)&pacc)!=S_OK) { - //Could not get the IAccessible pointer from the WM_GETOBJECT result - return; - } - nhAssert(pacc); //must get a valid IAccessible object - HRESULT hres; - VARIANT varChild; - varChild.vt=VT_I4; - IAccessible* childAcc; - if (ID != CHILDID_SELF && !this->isWindowless) { - // We have the root accessible, but a specific child has been requested. - varChild.lVal = ID; - IDispatch* childDisp = NULL; - hres = pacc->get_accChild(varChild, &childDisp); - pacc->Release(); - if (hres != S_OK || !childDisp) - return; - childAcc = NULL; - hres = childDisp->QueryInterface(IID_IAccessible, (void**)&childAcc); - childDisp->Release(); - if (hres != S_OK || !childAcc) - return; - pacc = childAcc; - } - - if (!oldNode || ID == this->rootID) { - // This is the root node. - VBufStorage_controlFieldNode_t* parentNode=buffer->addControlFieldNode(NULL,NULL,docHandle,ID,TRUE); - parentNode->addAttribute(L"IAccessible::role",L"10"); - VBufStorage_fieldNode_t* previousNode=NULL; - long childCount=0; - pacc->get_accChildCount(&childCount); - - if (this->getAccId(pacc) != -1) { - // We can get IDs from accessibles. - VARIANT* varChildren; - if (!(varChildren = (VARIANT*)malloc(sizeof(VARIANT) * childCount))) - return; - if (FAILED(AccessibleChildren(pacc, 0, childCount, varChildren, &childCount))) - childCount = 0; - for (long i = 0; i < childCount; ++i) { - if (varChildren[i].vt != VT_DISPATCH || !varChildren[i].pdispVal) { - VariantClear(&(varChildren[i])); - continue; - } - childAcc = NULL; - hres = varChildren[i].pdispVal->QueryInterface(IID_IAccessible, (void**)&childAcc); - VariantClear(&(varChildren[i])); - if (hres != S_OK) - continue; - int childId = getAccId(childAcc); - previousNode = this->renderControlContent(buffer, parentNode, previousNode, docHandle, childId, childAcc); - childAcc->Release(); - } - free(varChildren); - - } else { - // We can't get IDs from accessibles. - // The only way to get IDs is to just try them sequentially. - // accessiblesByLocation maps ((x, y), id) to (accessible, id). - // This allows us to order by location and, where that is the same, ID. - // We need this because added children always have larger IDs, - // even if they were inserted between two other children. - map, long>, pair> accessiblesByLocation; - // Keep going until we have childCount children. - for(int i=1;i<1000&&static_cast(accessiblesByLocation.size())get_accChild(varChild, &childDisp) != S_OK || !childDisp) - continue; - childAcc = NULL; - hres = childDisp->QueryInterface(IID_IAccessible, (void**)&childAcc); - childDisp->Release(); - if (hres != S_OK || !childAcc) - continue; - long left=0, top=0, width=0, height=0; - varChild.lVal = CHILDID_SELF; - if (childAcc->accLocation(&left, &top, &width, &height, varChild) != S_OK) - left=top=width=height=0; - accessiblesByLocation[make_pair(make_pair(top + height / 2, left + width / 2), i)] = make_pair(childAcc, i); - } - for (map, long>, pair>::iterator i = accessiblesByLocation.begin(); i != accessiblesByLocation.end(); ++i) { - previousNode = this->renderControlContent(buffer, parentNode, previousNode, docHandle, i->second.second, i->second.first); - i->second.first->Release(); - } - } - - } else { - // This is a child that is being re-rendered. - this->renderControlContent(buffer, NULL, NULL, docHandle, ID, pacc); - } - - pacc->Release(); -} - -AdobeFlashVBufBackend_t::AdobeFlashVBufBackend_t(int docHandle, int ID): VBufBackend_t(docHandle,ID), accPropServices(NULL) { -} - -VBufBackend_t* AdobeFlashVBufBackend_t_createInstance(int docHandle, int ID) { - VBufBackend_t* backend=new AdobeFlashVBufBackend_t(docHandle,ID); - return backend; -} diff --git a/nvdaHelper/vbufBackends/adobeFlash/adobeFlash.h b/nvdaHelper/vbufBackends/adobeFlash/adobeFlash.h deleted file mode 100644 index 39ea00e4c1d..00000000000 --- a/nvdaHelper/vbufBackends/adobeFlash/adobeFlash.h +++ /dev/null @@ -1,51 +0,0 @@ -/* -This file is a part of the NVDA project. -URL: http://www.nvda-project.org/ -Copyright 2010-2013 NV Access Limited - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License version 2.0, as published by - the Free Software Foundation. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -This license can be found at: -http://www.gnu.org/licenses/old-licenses/gpl-2.0.html -*/ - -#ifndef VIRTUALBUFFER_BACKENDS_ADOBEFLASH_H -#define VIRTUALBUFFER_BACKENDS_ADOBEFLASH_H - -#include -#include -#include - -class AdobeFlashVBufBackend_t: public VBufBackend_t { - private: - - VBufStorage_fieldNode_t* renderControlContent(VBufStorage_buffer_t* buffer, VBufStorage_controlFieldNode_t* parentNode, VBufStorage_fieldNode_t* previousNode, int docHandle, int id, IAccessible* pacc); - - IAccPropServices* accPropServices; - - long getAccId(IAccessible* acc); - - bool isWindowless; - - protected: - - static void CALLBACK renderThread_winEventProcHook(HWINEVENTHOOK hookID, DWORD eventID, HWND hwnd, long objectID, long childID, DWORD threadID, DWORD time); - - virtual void renderThread_initialize(); - - virtual void renderThread_terminate(); - - virtual void render(VBufStorage_buffer_t* buffer, int docHandle, int ID, VBufStorage_controlFieldNode_t* oldNode); - - //virtual ~AdobeFlashVBufBackend_t(); - - public: - - AdobeFlashVBufBackend_t(int docHandle, int ID); - -}; - -#endif diff --git a/nvdaHelper/vbufBackends/adobeFlash/sconscript b/nvdaHelper/vbufBackends/adobeFlash/sconscript deleted file mode 100644 index ee49d76bda8..00000000000 --- a/nvdaHelper/vbufBackends/adobeFlash/sconscript +++ /dev/null @@ -1,21 +0,0 @@ -### -#This file is a part of the NVDA project. -#URL: http://www.nvda-project.org/ -#Copyright 2006-2010 NVDA contributers. -#This program is free software: you can redistribute it and/or modify -#it under the terms of the GNU General Public License version 2.0, as published by -#the Free Software Foundation. -#This program is distributed in the hope that it will be useful, -#but WITHOUT ANY WARRANTY; without even the implied warranty of -#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -#This license can be found at: -#http://www.gnu.org/licenses/old-licenses/gpl-2.0.html -### - -Import([ - 'env', -]) - -adobeFlashBackendLib=env.Object("adobeFlash.cpp") - -Return('adobeFlashBackendLib') diff --git a/nvdaHelper/vbufBase/backend.h b/nvdaHelper/vbufBase/backend.h index 8cb96a0844f..e73ae9f8307 100644 --- a/nvdaHelper/vbufBase/backend.h +++ b/nvdaHelper/vbufBase/backend.h @@ -184,7 +184,6 @@ typedef VBufBackend_t*(*VBufBackend_create_proc)(int,int); // The backend creation functions VBufBackend_t* AdobeAcrobatVBufBackend_t_createInstance(int docHandle, int ID); -VBufBackend_t* AdobeFlashVBufBackend_t_createInstance(int docHandle, int ID); VBufBackend_t* GeckoVBufBackend_t_createInstance(int docHandle, int ID); VBufBackend_t* lotusNotesRichTextVBufBackend_t_createInstance(int docHandle, int ID); VBufBackend_t* MshtmlVBufBackend_t_createInstance(int docHandle, int ID); diff --git a/readme.md b/readme.md index 22b71927afd..1c8d3211301 100644 --- a/readme.md +++ b/readme.md @@ -88,7 +88,6 @@ For reference, the following run time dependencies are included in Git submodule * [Unicode Common Locale Data Repository (CLDR)](http://cldr.unicode.org/), version 38.1 * NVDA images and sounds * [Adobe Acrobat accessibility interface, version XI](https://download.macromedia.com/pub/developer/acrobat/AcrobatAccess.zip) -* Adobe FlashAccessibility interface typelib * [MinHook](https://github.com/RaMMicHaeL/minhook), tagged version 1.2.2 * brlapi Python bindings, version 0.8 or later, distributed with [BRLTTY for Windows](https://brltty.app/download.html), version 6.1 * lilli.dll, version 2.1.0.0 diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index 295a982d15b..0486fdce660 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -485,20 +485,6 @@ def findOverlayClasses(self,clsList): elif windowClassName=="GeckoPluginWindow" and self.event_objectID==0 and self.IAccessibleChildID==0: from .mozilla import GeckoPluginWindowRoot clsList.append(GeckoPluginWindowRoot) - maybeFlash = False - if ((windowClassName in ("MozillaWindowClass", "GeckoPluginWindow") and not isinstance(self.IAccessibleObject, IAccessibleHandler.IAccessible2)) - or windowClassName in ("MacromediaFlashPlayerActiveX", "ApolloRuntimeContentWindow", "ShockwaveFlash", "ShockwaveFlashLibrary", "ShockwaveFlashFullScreen", "GeckoFPSandboxChildWindow")): - maybeFlash = True - elif windowClassName == "Internet Explorer_Server" and self.event_objectID is not None and self.event_objectID > 0: - # #2454: In Windows 8 IE, Flash is exposed in the same HWND as web content. - from .MSHTML import MSHTML - # This is only possibly Flash if it isn't MSHTML. - if not isinstance(self, MSHTML): - maybeFlash = True - if maybeFlash: - # This is possibly a Flash object. - from . import adobeFlash - adobeFlash.findExtraOverlayClasses(self, clsList) elif windowClassName.startswith('Mozilla'): from . import mozilla mozilla.findExtraOverlayClasses(self, clsList) diff --git a/source/NVDAObjects/IAccessible/adobeFlash.py b/source/NVDAObjects/IAccessible/adobeFlash.py deleted file mode 100644 index 64c0bf5ae5c..00000000000 --- a/source/NVDAObjects/IAccessible/adobeFlash.py +++ /dev/null @@ -1,101 +0,0 @@ -#NVDAObjects/IAccessible/adobeFlash.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2009 NVDA Contributors -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. - -import winUser -import oleacc -from . import IAccessible, getNVDAObjectFromEvent -from NVDAObjects import NVDAObjectTextInfo -from NVDAObjects.behaviors import EditableTextWithoutAutoSelectDetection -from comtypes import COMError, IServiceProvider, hresult -from comtypes.gen.FlashAccessibility import ISimpleTextSelection, IFlashAccessibility -from logHandler import log - -class InputTextFieldTextInfo(NVDAObjectTextInfo): - - def _getStoryText(self): - return self.obj.value or "" - - def _getRawSelectionOffsets(self): - try: - return self.obj.ISimpleTextSelectionObject.GetSelection() - except COMError as e: - if e.hresult == hresult.E_FAIL: - # The documentation says that an empty field should return 0 for both values, but instead, we seem to get E_FAIL. - # An empty field still has a valid caret. - return 0, 0 - else: - raise RuntimeError - except AttributeError: - raise RuntimeError - - def _getCaretOffset(self): - # We want the active (moving) end of the selection. - return self._getRawSelectionOffsets()[1] - - def _getSelectionOffsets(self): - # This might be a backwards selection, but for now, we should always return the values in ascending order. - return sorted(self._getRawSelectionOffsets()) - -class InputTextField(EditableTextWithoutAutoSelectDetection, IAccessible): - TextInfo = InputTextFieldTextInfo - -class Root(IAccessible): - - def _get_presentationType(self): - return self.presType_content - - def _get_treeInterceptorClass(self): - import virtualBuffers.adobeFlash - return virtualBuffers.adobeFlash.AdobeFlash - - #Flash root client has broken accParent, force to return the flash root window root IAccessible - def _get_parent(self): - return getNVDAObjectFromEvent(self.windowHandle,0,0) - -class PluginClientWithBrokenFocus(IAccessible): - """The client of a Flash plugin with broken focus behaviour. - #2546: In Flash protected mode, the Flash content is in another window beneath the plugin window. - Unfortunately, Flash doesn't bother to set focus to this window. - To work around this, when focus hits this object, focus is forced to the child. - """ - - def event_gainFocus(self): - try: - self.firstChild.firstChild.setFocus() - except AttributeError: - super(PluginClientWithBrokenFocus, self).event_gainFocus() - -def findExtraOverlayClasses(obj, clsList): - """Determine the most appropriate class if this is a Flash object. - This works similarly to L{NVDAObjects.NVDAObject.findOverlayClasses} except that it never calls any other findOverlayClasses method. - """ - iaRole = obj.IAccessibleRole - if obj.windowClassName == "GeckoPluginWindow" and iaRole == oleacc.ROLE_SYSTEM_CLIENT and obj.childCount == 1 and obj.firstChild.windowClassName == "GeckoFPSandboxChildWindow": - clsList.append(PluginClientWithBrokenFocus) - return - - try: - servProv = obj.IAccessibleObject.QueryInterface(IServiceProvider) - except COMError: - return - - # Check whether this is the Flash root accessible. - if iaRole == oleacc.ROLE_SYSTEM_CLIENT: - try: - servProv.QueryService(IFlashAccessibility._iid_, IFlashAccessibility) - clsList.append(Root) - except COMError: - pass - # If this is a client and IFlashAccessibility wasn't present, this is not a Flash object. - return - - # Check whether this is a Flash input text field. - try: - # We have to fetch ISimpleTextSelectionObject in order to check whether this is an input text field, so store it on the instance. - obj.ISimpleTextSelectionObject = servProv.QueryService(ISimpleTextSelection._iid_, ISimpleTextSelection) - clsList.append(InputTextField) - except COMError: - pass diff --git a/source/comInterfaces_sconscript b/source/comInterfaces_sconscript index 3a6d93a5d75..12e5ec73134 100755 --- a/source/comInterfaces_sconscript +++ b/source/comInterfaces_sconscript @@ -51,7 +51,6 @@ COM_INTERFACES = { "tom.py": ('{8CC497C9-A1DF-11CE-8098-00AA0047BE5D}',1,0), "SpeechLib.py": ('{C866CA3A-32F7-11D2-9602-00C04F8EE628}',5,0), "AcrobatAccessLib.py": "typelibs/AcrobatAccess.tlb", - "FlashAccessibility.py": "typelibs/FlashAccessibility.tlb", } for k,v in COM_INTERFACES.items(): diff --git a/source/controlTypes.py b/source/controlTypes.py index d128a825505..1a9395a7cb8 100644 --- a/source/controlTypes.py +++ b/source/controlTypes.py @@ -335,7 +335,7 @@ ROLE_ICON:_("icon"), # Translators: Identifies a directory pane. ROLE_DIRECTORYPANE:_("directory pane"), - # Translators: Identifies an embedded object such as flash content on webpages. + # Translators: Identifies an object that is embedded in a document. ROLE_EMBEDDEDOBJECT:_("embedded object"), # Translators: Identifies an end note. ROLE_ENDNOTE:_("end note"), diff --git a/source/virtualBuffers/adobeFlash.py b/source/virtualBuffers/adobeFlash.py deleted file mode 100644 index 91ec5b68a48..00000000000 --- a/source/virtualBuffers/adobeFlash.py +++ /dev/null @@ -1,132 +0,0 @@ -#virtualBuffers/adobeFlash.py -#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) 2010-2013 NV Access Limited - -from comtypes import COMError -from . import VirtualBuffer, VirtualBufferTextInfo -import controlTypes -import NVDAObjects.IAccessible -import winUser -import mouseHandler -import IAccessibleHandler -import oleacc -from logHandler import log -import textInfos - -class AdobeFlash_TextInfo(VirtualBufferTextInfo): - - def _normalizeControlField(self,attrs): - accRole=attrs['IAccessible::role'] - if accRole.isdigit(): - accRole=int(accRole) - else: - accRole = accRole.lower() - role=IAccessibleHandler.IAccessibleRolesToNVDARoles.get(accRole,controlTypes.ROLE_UNKNOWN) - - states=set(IAccessibleHandler.IAccessibleStatesToNVDAStates[x] for x in [1< 0 - - def __contains__(self,obj): - if self.isWindowless: - if not isinstance(obj, NVDAObjects.IAccessible.IAccessible): - return False - if obj.windowHandle != self.rootDocHandle: - return False - info = obj.IAccessibleIdentity - if not info: - return False - ID=info['objectID'] - try: - self.rootNVDAObject.IAccessibleObject.accChild(ID) - return True - except COMError: - return False - return winUser.isDescendantWindow(self.rootDocHandle, obj.windowHandle) - - def _get_isAlive(self): - if self.isLoading: - return True - root=self.rootNVDAObject - if not root: - return False - if not winUser.isWindow(root.windowHandle) or root.role == controlTypes.ROLE_UNKNOWN: - return False - return True - - def getNVDAObjectFromIdentifier(self, docHandle, ID): - if self.isWindowless: - objId = ID - childId = 0 - else: - objId = winUser.OBJID_CLIENT - childId = ID - return NVDAObjects.IAccessible.getNVDAObjectFromEvent(docHandle, objId, childId) - - def getIdentifierFromNVDAObject(self,obj): - info = obj.IAccessibleIdentity - if info: - # Trust IAccIdentity over the event parameters. - accId = info["objectID"] - else: - accId = obj.event_objectID - if accId is None: - # We don't have event parameters, so we can't get an ID. - raise LookupError - if accId <= 0: - accId = obj.event_childID - return obj.windowHandle, accId - - def _searchableAttribsForNodeType(self,nodeType): - if nodeType=="formField": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_PUSHBUTTON,oleacc.ROLE_SYSTEM_RADIOBUTTON,oleacc.ROLE_SYSTEM_CHECKBUTTON,oleacc.ROLE_SYSTEM_COMBOBOX,oleacc.ROLE_SYSTEM_LIST,oleacc.ROLE_SYSTEM_TEXT]} - elif nodeType=="list": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_LIST]} - elif nodeType=="listItem": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_LISTITEM]} - elif nodeType=="button": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_PUSHBUTTON]} - elif nodeType=="edit": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_TEXT]} - elif nodeType=="radioButton": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_RADIOBUTTON]} - elif nodeType=="comboBox": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_COMBOBOX]} - elif nodeType=="checkBox": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_CHECKBUTTON]} - elif nodeType=="graphic": - attrs={"IAccessible::role":[oleacc.ROLE_SYSTEM_GRAPHIC]} - elif nodeType=="focusable": - attrs={"IAccessible::state_%s"%oleacc.STATE_SYSTEM_FOCUSABLE:[1]} - else: - return None - return attrs - - def _activateNVDAObject(self, obj): - try: - obj.doAction() - return - except: - pass - - log.debugWarning("could not programmatically activate field, trying mouse") - l=obj.location - if not l: - log.debugWarning("no location for field") - return - oldX,oldY=winUser.getCursorPos() - winUser.setCursorPos(*l.center) - mouseHandler.executeMouseEvent(winUser.MOUSEEVENTF_LEFTDOWN,0,0) - mouseHandler.executeMouseEvent(winUser.MOUSEEVENTF_LEFTUP,0,0) - winUser.setCursorPos(oldX,oldY) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index a430c2a9903..bdd98708d67 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -22,6 +22,7 @@ What's New in NVDA - Espeak-ng has been updated to 1.51-dev commit 53915bf0a7cd48f90c4a38ac52fff697723d9f4d. (#12202) - Updated liblouis braille translator to [3.17.0 https://github.com/liblouis/liblouis/releases/tag/v3.17.0]. (#12137) - New braille tables: Belarusian literary braille, Belarusian computer braille, Urdu grade 1, Urdu grade 2. +- Support for Adobe Flash content has been removed from NVDA due to the use of Flash being actively discouraged by Adobe. (#11131) == Bug Fixes == diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 0c7e548ea8b..7efed26778a 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -498,7 +498,6 @@ This includes documents in the following applications: - Microsoft Edge - Adobe Reader - Foxit Reader -- Adobe Flash - Supported books in Amazon Kindle for PC - @@ -596,7 +595,7 @@ Use the following keys for performing searches: %kc:endInclude ++ Embedded Objects ++[ImbeddedObjects] -Pages can include rich content using technologies such as Adobe Flash, Oracle Java and HTML5, as well as applications and dialogs. +Pages can include rich content using technologies such as Oracle Java and HTML5, as well as applications and dialogs. Where these are encountered in browse mode, NVDA will report "embedded object", "application" or "dialog", respectively. You can quickly move to them using the o and shift+o embedded object single letter navigation keys. To interact with these objects, you can press enter on them. From 0316158bc9f43066d6db44431bbd2402c27330b0 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Wed, 24 Mar 2021 10:15:27 +1100 Subject: [PATCH 100/174] fix installation crash from StaticBoxSizer changes (#12199) Summary of the issue: The standard installation process crashes with the following traceback, caused by #12181 Description of how this pull request fixes the issue: Fix various crashing GUI bugs introduced to the installation process in #12181 Add smoke tests for the installation process --- appveyor.yml | 4 +- source/gui/installerGui.py | 26 +++++----- source/gui/startupDialogs.py | 2 +- tests/system/libraries/NvdaLib.py | 64 +++++++++++++++++++++-- tests/system/readme.md | 3 ++ tests/system/robot/NVDAInstaller.py | 70 ++++++++++++++++++++++++++ tests/system/robot/NVDAInstaller.robot | 38 ++++++++++++++ tests/system/robotArgs.robot | 2 + 8 files changed, 190 insertions(+), 19 deletions(-) create mode 100644 tests/system/robot/NVDAInstaller.py create mode 100644 tests/system/robot/NVDAInstaller.robot diff --git a/appveyor.yml b/appveyor.yml index 4c424a12efe..7cfd77970ae 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -133,6 +133,7 @@ before_test: $nvdaLauncherFile+="_snapshot" } $nvdaLauncherFile+="_${env:version}.exe" + Set-AppveyorBuildVariable "nvdaLauncherFile" $nvdaLauncherFile echo NVDALauncherFile: $NVDALauncherFile $outputDir=$(resolve-path .\testOutput) $installerLogFilePath="$outputDir\nvda_install.log" @@ -194,8 +195,7 @@ test_script: - ps: | $testOutput = (Resolve-Path .\testOutput\) $systemTestOutput = (Resolve-Path "$testOutput\system") - - .\runsystemtests.bat --variable whichNVDA:installed + .\runsystemtests.bat --variable whichNVDA:installed --variable installDir:"${env:nvdaLauncherFile}" --include installer if($LastExitCode -ne 0) { $errorCode=$LastExitCode Add-AppveyorMessage "System test failure" diff --git a/source/gui/installerGui.py b/source/gui/installerGui.py index 8fbd861f935..db9a9576284 100644 --- a/source/gui/installerGui.py +++ b/source/gui/installerGui.py @@ -177,13 +177,13 @@ def __init__(self, parent, isUpdate): # Translators: The label for a group box containing the NVDA installation dialog options. optionsLabel = _("Options") - optionsHelper = sHelper.addItem(wx.StaticBoxSizer(wx.VERTICAL, self, label=optionsLabel)) - optionsSizer = guiHelper.BoxSizerHelper(self, sizer=optionsHelper) + optionsSizer = sHelper.addItem(wx.StaticBoxSizer(wx.VERTICAL, self, label=optionsLabel)) + optionsHelper = guiHelper.BoxSizerHelper(self, sizer=optionsSizer) optionsBox = optionsSizer.GetStaticBox() # Translators: The label of a checkbox option in the Install NVDA dialog. startOnLogonText = _("Use NVDA during sign-in") - self.startOnLogonCheckbox = optionsSizer.addItem(wx.CheckBox(optionsBox, label=startOnLogonText)) + self.startOnLogonCheckbox = optionsHelper.addItem(wx.CheckBox(optionsBox, label=startOnLogonText)) self.bindHelpEvent("StartAtWindowsLogon", self.startOnLogonCheckbox) if globalVars.appArgs.enableStartOnLogon is not None: self.startOnLogonCheckbox.Value = globalVars.appArgs.enableStartOnLogon @@ -195,21 +195,21 @@ def __init__(self, parent, isUpdate): # Translators: The label of a checkbox option in the Install NVDA dialog. keepShortCutText = _("&Keep existing desktop shortcut") keepShortCutBox = wx.CheckBox(optionsBox, label=keepShortCutText) - self.createDesktopShortcutCheckbox = optionsSizer.addItem(keepShortCutBox) + self.createDesktopShortcutCheckbox = optionsHelper.addItem(keepShortCutBox) else: # Translators: The label of the option to create a desktop shortcut in the Install NVDA dialog. # If the shortcut key has been changed for this locale, # this change must also be reflected here. createShortcutText = _("Create &desktop icon and shortcut key (control+alt+n)") createShortcutBox = wx.CheckBox(optionsBox, label=createShortcutText) - self.createDesktopShortcutCheckbox = optionsSizer.addItem(createShortcutBox) + self.createDesktopShortcutCheckbox = optionsHelper.addItem(createShortcutBox) self.bindHelpEvent("CreateDesktopShortcut", self.createDesktopShortcutCheckbox) self.createDesktopShortcutCheckbox.Value = shortcutIsPrevInstalled if self.isUpdate else True # Translators: The label of a checkbox option in the Install NVDA dialog. createPortableText = _("Copy &portable configuration to current user account") createPortableBox = wx.CheckBox(optionsBox, label=createPortableText) - self.copyPortableConfigCheckbox = optionsSizer.addItem(createPortableBox) + self.copyPortableConfigCheckbox = optionsHelper.addItem(createPortableBox) self.bindHelpEvent("CopyPortableConfigurationToCurrentUserAccount", self.copyPortableConfigCheckbox) self.copyPortableConfigCheckbox.Value = bool(globalVars.appArgs.copyPortableConfig) if globalVars.appArgs.copyPortableConfig is None: @@ -220,12 +220,12 @@ def __init__(self, parent, isUpdate): bHelper = sHelper.addDialogDismissButtons(guiHelper.ButtonHelper(wx.HORIZONTAL)) if shouldAskAboutAddons: # Translators: The label of a button to launch the add-on compatibility review dialog. - reviewAddonButton = bHelper.addButton(optionsBox, label=_("&Review add-ons...")) + reviewAddonButton = bHelper.addButton(self, label=_("&Review add-ons...")) self.bindHelpEvent("InstallWithIncompatibleAddons", reviewAddonButton) reviewAddonButton.Bind(wx.EVT_BUTTON, self.onReviewAddons) # Translators: The label of a button to continue with the operation. - continueButton = bHelper.addButton(optionsBox, label=_("&Continue"), id=wx.ID_OK) + continueButton = bHelper.addButton(self, label=_("&Continue"), id=wx.ID_OK) continueButton.SetDefault() continueButton.Bind(wx.EVT_BUTTON, self.onInstall) if shouldAskAboutAddons: @@ -235,7 +235,7 @@ def __init__(self, parent, isUpdate): ) continueButton.Enable(False) - bHelper.addButton(optionsBox, id=wx.ID_CANCEL) + bHelper.addButton(self, id=wx.ID_CANCEL) # If we bind this using button.Bind, it fails to trigger when the dialog is closed. self.Bind(wx.EVT_BUTTON, self.onCancel, id=wx.ID_CANCEL) @@ -365,22 +365,22 @@ def __init__(self, parent): # Translators: The label of a checkbox option in the Create Portable NVDA dialog. copyConfText = _("Copy current &user configuration") - self.copyUserConfigCheckbox = sHelper.addItem(wx.CheckBox(groupBox, label=copyConfText)) + self.copyUserConfigCheckbox = sHelper.addItem(wx.CheckBox(self, label=copyConfText)) self.copyUserConfigCheckbox.Value = False if globalVars.appArgs.launcher: self.copyUserConfigCheckbox.Disable() # Translators: The label of a checkbox option in the Create Portable NVDA dialog. startAfterCreateText = _("&Start the new portable copy after creation") - self.startAfterCreateCheckbox = sHelper.addItem(wx.CheckBox(groupBox, label=startAfterCreateText)) + self.startAfterCreateCheckbox = sHelper.addItem(wx.CheckBox(self, label=startAfterCreateText)) self.startAfterCreateCheckbox.Value = False bHelper = sHelper.addDialogDismissButtons(gui.guiHelper.ButtonHelper(wx.HORIZONTAL), separated=True) - continueButton = bHelper.addButton(groupBox, label=_("&Continue"), id=wx.ID_OK) + continueButton = bHelper.addButton(self, label=_("&Continue"), id=wx.ID_OK) continueButton.SetDefault() continueButton.Bind(wx.EVT_BUTTON, self.onCreatePortable) - bHelper.addButton(groupBox, id=wx.ID_CANCEL) + bHelper.addButton(self, id=wx.ID_CANCEL) # If we bind this using button.Bind, it fails to trigger when the dialog is closed. self.Bind(wx.EVT_BUTTON, self.onCancel, id=wx.ID_CANCEL) diff --git a/source/gui/startupDialogs.py b/source/gui/startupDialogs.py index a95ce8fde60..8830a9c1ae9 100644 --- a/source/gui/startupDialogs.py +++ b/source/gui/startupDialogs.py @@ -131,7 +131,7 @@ class LauncherDialog( helpId = "InstallingNVDA" def __init__(self, parent): - super().__init__(parent, title=versionInfo.name) + super().__init__(parent, title=f"{versionInfo.name} {_('Launcher')}") mainSizer = wx.BoxSizer(wx.VERTICAL) sHelper = gui.guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) diff --git a/tests/system/libraries/NvdaLib.py b/tests/system/libraries/NvdaLib.py index 78da2894405..735ea741c70 100644 --- a/tests/system/libraries/NvdaLib.py +++ b/tests/system/libraries/NvdaLib.py @@ -49,12 +49,16 @@ def __init__(self): opSys.directory_should_exist(self.stagingDir) self.whichNVDA = builtIn.get_variable_value("${whichNVDA}", "source") + self._installFilePath = builtIn.get_variable_value("${installDir}", None) + self.NVDAInstallerCommandline = None if self.whichNVDA == "source": self._runNVDAFilePath = _pJoin(self.repoRoot, "runnvda.bat") self.baseNVDACommandline = self._runNVDAFilePath elif self.whichNVDA == "installed": self._runNVDAFilePath = _pJoin(_expandvars('%PROGRAMFILES%'), 'nvda', 'nvda.exe') self.baseNVDACommandline = f'"{str(self._runNVDAFilePath)}"' + if self._installFilePath is not None: + self.NVDAInstallerCommandline = f'"{str(self._installFilePath)}"' else: raise AssertionError("RobotFramework should be run with argument: '-v whichNVDA [source|installed]'") @@ -65,8 +69,15 @@ def __init__(self): "nvdaTestRunLogs" ) + def ensureInstallerPathsExist(self): + fileWarnMsg = f"Unable to run NVDA installer unless path exists. Path given: {self._installFilePath}" + opSys.file_should_exist(self._installFilePath, fileWarnMsg) + opSys.create_directory(self.profileDir) + opSys.create_directory(self.preservedLogsDir) + def ensurePathsExist(self): - opSys.file_should_exist(self._runNVDAFilePath, "Unable to start NVDA unless path exists.") + fileWarnMsg = f"Unable to run NVDA installer unless path exists. Path given: {self._runNVDAFilePath}" + opSys.file_should_exist(self._runNVDAFilePath, fileWarnMsg) opSys.create_directory(self.profileDir) opSys.create_directory(self.preservedLogsDir) @@ -130,7 +141,28 @@ def _startNVDAProcess(self): ) return handle - def _connectToRemoteServer(self): + def _startNVDAInstallerProcess(self): + """Start NVDA Installer. + Use debug logging, replacing any current instance, using the system test profile directory + """ + _locations.ensureInstallerPathsExist() + command = ( + f"{_locations.NVDAInstallerCommandline}" + f" --debug-logging" + f" -r" + f" -c \"{_locations.profileDir}\"" + f" --log-file \"{_locations.logPath}\"" + ) + self.nvdaHandle = handle = process.start_process( + command, + shell=True, + alias=self.nvdaProcessAlias, + stdout=_pJoin(_locations.preservedLogsDir, self._createTestIdFileName("stdout.txt")), + stderr=_pJoin(_locations.preservedLogsDir, self._createTestIdFileName("stderr.txt")), + ) + return handle + + def _connectToRemoteServer(self, connectionTimeoutSecs=10): """Connects to the nvdaSpyServer Because we do not know how far through the startup NVDA is, we have to poll to check that the server is available. Importing the library immediately seems @@ -146,7 +178,7 @@ def _connectToRemoteServer(self): # therefore we use '_testRemoteServer' to ensure that we can in fact connect before proceeding. _blockUntilConditionMet( getValue=lambda: _testRemoteServer(self._spyServerURI, log=False), - giveUpAfterSeconds=10, + giveUpAfterSeconds=connectionTimeoutSecs, errorMessage=f"Unable to connect to {self._spyAlias}", ) builtIn.log(f"Connecting to {self._spyAlias}", level='DEBUG') @@ -193,6 +225,16 @@ def runKeyword(*args, **kwargs): ) return remoteLib + def start_NVDAInstaller(self, settingsFileName): + builtIn.log(f"Starting NVDA with config: {settingsFileName}") + self.setup_nvda_profile(settingsFileName) + nvdaProcessHandle = self._startNVDAInstallerProcess() + process.process_should_be_running(nvdaProcessHandle) + # Timeout is increased due to the installer load time and start up splash sound + self._connectToRemoteServer(connectionTimeoutSecs=30) + self.nvdaSpy.wait_for_NVDA_startup_to_complete() + return nvdaProcessHandle + def start_NVDA(self, settingsFileName): builtIn.log(f"Starting NVDA with config: {settingsFileName}") self.setup_nvda_profile(settingsFileName) @@ -234,6 +276,22 @@ def quit_NVDA(self): # remove the spy so that if nvda is run manually against this config it does not interfere. self.teardown_nvda_profile() + def quit_NVDAInstaller(self): + builtIn.log("Stopping nvdaSpy server: {}".format(self._spyServerURI)) + self.nvdaSpy.emulateKeyPress("insert+q") + self.nvdaSpy.wait_for_specific_speech("Exit NVDA") + self.nvdaSpy.emulateKeyPress("enter", blockUntilProcessed=False) + builtIn.sleep(1) + try: + _stopRemoteServer(self._spyServerURI, log=False) + except Exception: + raise + finally: + self.save_NVDA_log() + # remove the spy so that if nvda is run manually against this config it does not interfere. + self.teardown_nvda_profile() + + def getSpyLib(): """ Gets the spy library instance. This has been augmented with methods for all supported keywords. diff --git a/tests/system/readme.md b/tests/system/readme.md index 9116def1685..03ec0a829bd 100644 --- a/tests/system/readme.md +++ b/tests/system/readme.md @@ -55,6 +55,9 @@ of NVDA (first ensure it is compatible with the tests). Note valid values are: the tests are run from an administrator command prompt. * "source" +The `installDir` argument performs a smoke test on the installation process given a path to the installer exe. For example `--variable installDir:".\path\to\nvda_installer.exe"`. +This should be used with `--variable whichNVDA:installed --include installer`. + ### Overview Robot Framework loads and parses the test files and their libraries. diff --git a/tests/system/robot/NVDAInstaller.py b/tests/system/robot/NVDAInstaller.py new file mode 100644 index 00000000000..4ed1b0d860e --- /dev/null +++ b/tests/system/robot/NVDAInstaller.py @@ -0,0 +1,70 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2020-2021 NV Access Limited +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + +"""Logic for NVDA installation process tests. +""" + +from robot.libraries.BuiltIn import BuiltIn +# relative import not used for 'systemTestUtils' because the folder is added to the path for 'libraries' +# imported methods start with underscore (_) so they don't get imported into robot files as keywords +from SystemTestSpy import ( + _getLib, +) + +# Imported for type information +from robot.libraries.Process import Process as _ProcessLib + +from AssertsLib import AssertsLib as _AssertsLib + +import NvdaLib as _nvdaLib +from NvdaLib import NvdaLib as _nvdaRobotLib +_nvdaProcessAlias = _nvdaRobotLib.nvdaProcessAlias + +_builtIn: BuiltIn = BuiltIn() +_process: _ProcessLib = _getLib("Process") +_asserts: _AssertsLib = _getLib("AssertsLib") + + +def read_install_dialog(): + "Smoke test the launcher dialogs used to install NVDA" + + spy = _nvdaLib.getSpyLib() + launchDialog = spy.wait_for_specific_speech("NVDA Launcher") # ensure the dialog is present. + spy.wait_for_speech_to_finish() + spy.get_speech_at_index_until_now(launchDialog) + + _builtIn.sleep(1) # the dialog is not always receiving keypresses, wait a little longer for it + # agree to the License Agreement + spy.emulateKeyPress("alt+a") + + # start install + spy.emulateKeyPress("alt+i") + + spy.wait_for_specific_speech("To install NVDA to your hard drive, please press the Continue button.") + + # exit NVDA Installer + spy.emulateKeyPress("escape") + + +def read_portable_copy_dialog(): + "Smoke test the launcher dialogs used to create a portable copy of NVDA" + + spy = _nvdaLib.getSpyLib() + launchDialog = spy.wait_for_specific_speech("NVDA Launcher") # ensure the dialog is present. + spy.wait_for_speech_to_finish() + spy.get_speech_at_index_until_now(launchDialog) + + _builtIn.sleep(1) # the dialog is not always receiving keypresses, wait a little longer for it + # agree to the License Agreement + spy.emulateKeyPress("alt+a") + + # start portable copy + spy.emulateKeyPress("alt+p") + + spy.wait_for_specific_speech( + "To create a portable copy of NVDA, please select the path and other options and then press Continue") + + # exit NVDA Installer + spy.emulateKeyPress("escape") diff --git a/tests/system/robot/NVDAInstaller.robot b/tests/system/robot/NVDAInstaller.robot new file mode 100644 index 00000000000..1c4d20f18ff --- /dev/null +++ b/tests/system/robot/NVDAInstaller.robot @@ -0,0 +1,38 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2021 NV Access Limited +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html +*** Settings *** +Documentation Smoke test the installation process +Force Tags smoke test installer +Suite Setup Run Keyword If not r'${installDir}' Fail "--installDir not supplied" + +# for start & quit in Test Setup and Test Teardown +Library NvdaLib.py +Library NVDAInstaller.py +Library ScreenCapLibrary + +Test Setup default startup +Test Teardown default teardown + +*** Keywords *** + +default teardown + ${screenshotName}= create_preserved_test_output_filename failedTest.png + Run Keyword If Test Failed Take Screenshot ${screenShotName} + quit NVDAInstaller + +default startup + start NVDAInstaller standard-dontShowWelcomeDialog.ini + +default pass execution + +*** Test Cases *** + +Read install dialog + [Documentation] Ensure that the install dialog can be read in full + read_install_dialog # run test + +Read install dialog portable copy + [Documentation] Ensure that the portable copy install dialog can be read in full + read_portable_copy_dialog # run test diff --git a/tests/system/robotArgs.robot b/tests/system/robotArgs.robot index 009131d5c21..d2eb08a522f 100644 --- a/tests/system/robotArgs.robot +++ b/tests/system/robotArgs.robot @@ -2,5 +2,7 @@ --outputdir testOutput\system --xunit systemTests.xml --pythonpath .\tests\system\libraries +--include NVDA --exclude excluded_from_build --variable whichNVDA:source +--variable installDir: From ed5fca6b3925ee656dc0ce66e15c24303e1efa09 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 25 Mar 2021 13:21:25 +1000 Subject: [PATCH 101/174] Optional experimental support for Microsoft Excel via UI Automation (#12210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since 2013, Microsoft has been slowly adding a UI Automation implementation to Microsoft Excel. For quite some time, this implementation was missing too much to be at all useful for NVDA. However, in the last year, Microsoft has increased its effort in this area, starting to fill many of the gaps in the implementation, providing more of a reason for NVDA to start consuming this experimentally, with the goal of one day switching to UI Automation in Excel by default. Not only would this provide significant performance improvemts for NVDA with Excel, but it would also enable scenarios such as using Excel in Application Guard, or remotely via Windows Virtual Desktop, where access to the object model and in-process injection is no longer possible. NVDA will now fall back to relying on UI Automation In Microsoft Excel if available, if NVDA is not able to inject in-process. A new option has been added to NVDA's Advanced settings, which when enabled, forces NVDA to use UI Automation in Microsoft Excel always when available. Although so far navigating, editing and querying cells is all supported, the following features are not yet implemented: • NvDA Elements List for listing formulas, comments etc. The needed UI Automation extensions are not yet available. • Browse mode quick nav. Again, same reason as above. • Setting / consuming screen reader specific column and row headers. This feature probably will not be supported. Instead it is recommended to make these particular sets of data into tables, marking the needed rows and columns as headers in the standrd way for modern Microsoft Excel. As some Excel-specific information on cells does not quite fit with standard NVDA concepts. A new Cell Appearance script (NVDA+o) has been added, which presents a browse mode document, listing these specific bits of information. This script may be changed or removed in future as it is possible to find this information out by looking in the Cell formatting dialog etc anyway, but for now it is there to demonstrate NVDa's ability to fetch this kind of information from UI Automation. Many of Excel's specific features have been exposed by Microsoft through UI automation via either annotations, or custom properties. Standard UIA annotations that NvDA consumes for this support are: • comment: For notes and comments. NVDA exposes the "has comment" state on cells, and reports the comment with a reportComment script (NVDA+alt+c). • Data validation error: the error text is included in the cell's description. • Formula error: the error text is included in the cell's description. • Circular reference error: the error text is included in the cell's description. Microsoft has made use of UI Automation's custom property feature in order to expose many more Excel-specific properties: https://docs.microsoft.com/en-us/office/uia/excel/excelcustomproperties This pr adds an infrastructure in UIAHandler / UIA NVDAObjects which allows registering and accessing these custom property IDs in a standard way. The properties that Microsoft exposes in Excel are: • cellFormula: mapped to NvDA's hasFormula state. • cellNumberFormat: exposed in Cell appearence script (NvDA+o). Note however that Excel currently exposes the raw template value and not the friendly name. So right now this is not as useful as it could be. • hasDataValidation: Exposed in Cell appearence script (NvDA+o) • hasDataValidationDropdown: Mapped to NvDA's submenu state. • dataValidationPrompt: exposed in cell description and Cell appearence script (NvDA+o) • hasConditionalFormatting: exposed in Cell appearence script (NvDA+o) • areGridlinesVisible: exposed in Cell appearence script (NvDA+o) • commentReplyCount: Included in reportComment script (NvDA+alt+c). The above annotations and properties have only been available in Microsoft Excel since build 16.0.13522.10000. Thus turning on this option will only be useful for this build or higher. --- source/NVDAObjects/UIA/__init__.py | 96 +++++- source/NVDAObjects/UIA/excel.py | 474 +++++++++++++++++++++++++++++ source/NVDAObjects/__init__.py | 27 +- source/UIAHandler.py | 13 +- source/_UIAConstants.py | 62 ++++ source/_UIACustomProps.py | 90 ++++++ source/_UIAHandler.py | 93 ++++-- source/colors.py | 2 +- source/config/configSpec.py | 1 + source/gui/settingsDialogs.py | 11 + user_docs/en/changes.t2t | 1 + user_docs/en/userGuide.t2t | 8 + 12 files changed, 825 insertions(+), 53 deletions(-) create mode 100644 source/NVDAObjects/UIA/excel.py create mode 100644 source/_UIAConstants.py create mode 100644 source/_UIACustomProps.py diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index b6bd3ba9e8d..da85cfa5bed 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -5,7 +5,7 @@ # Babbage B.V., Leonard de Ruijter, Bill Dengler """Support for UI Automation (UIA) controls.""" - +import typing from ctypes import byref from ctypes.wintypes import POINT, RECT from comtypes import COMError @@ -17,6 +17,7 @@ import colors import languageHandler import UIAHandler +import _UIACustomProps import globalVars import eventHandler import controlTypes @@ -813,6 +814,9 @@ def updateSelection(self): updateCaret = updateSelection class UIA(Window): + _UIACustomProps = _UIACustomProps.CustomPropertiesCommon.get() + + shouldAllowDuplicateUIAFocusEvent = False def _get__coreCycleUIAPropertyCacheElementCache(self): """ @@ -872,6 +876,19 @@ def findOverlayClasses(self,clsList): clsList.append(WpfTextView) elif UIAClassName=="NetUIDropdownAnchor": clsList.append(NetUIDropdownAnchor) + elif self.windowClassName == "EXCEL6" and self.role == controlTypes.ROLE_PANE: + from .excel import BadExcelFormulaEdit + clsList.append(BadExcelFormulaEdit) + elif self.windowClassName == "EXCEL7": + if self.role in (controlTypes.ROLE_DATAITEM, controlTypes.ROLE_HEADERITEM): + from .excel import ExcelCell + clsList.append(ExcelCell) + elif self.role == controlTypes.ROLE_DATAGRID: + from .excel import ExcelWorksheet + clsList.append(ExcelWorksheet) + elif self.role == controlTypes.ROLE_EDITABLETEXT: + from .excel import CellEdit + clsList.append(CellEdit) elif self.TextInfo == UIATextInfo and ( UIAClassName == '_WwG' or self.windowClassName == '_WwG' @@ -1168,8 +1185,64 @@ def _get_UIASelectionItemPattern(self): self.UIASelectionItemPattern=self._getUIAPattern(UIAHandler.UIA_SelectionItemPatternId,UIAHandler.IUIAutomationSelectionItemPattern) return self.UIASelectionItemPattern + def _get_UIASelectionPattern(self): + self.UIASelectionPattern = self._getUIAPattern( + UIAHandler.UIA_SelectionPatternId, + UIAHandler.IUIAutomationSelectionPattern + ) + return self.UIASelectionPattern + + def _get_UIASelectionPattern2(self): + self.UIASelectionPattern2 = self._getUIAPattern( + UIAHandler.UIA_SelectionPattern2Id, + UIAHandler.IUIAutomationSelectionPattern2 + ) + return self.UIASelectionPattern2 + + def getSelectedItemsCount(self, maxItems=None): + p = self.UIASelectionPattern2 + if p: + return p.currentItemCount + return 0 + + #: Typing information for auto-property: _get_selectionContainer + selectionContainer: "typing.Optional[UIA]" + + def _get_selectionContainer(self) -> "typing.Optional[UIA]": + p = self.UIASelectionItemPattern + if not p: + return None + e = p.currentSelectionContainer + e = e.buildUpdatedCache(UIAHandler.handler.baseCacheRequest) + obj = UIA(UIAElement=e) + if obj.UIASelectionPattern2: + return obj + return None + + #: typing for auto-property: UIAAnnotationObjects + UIAAnnotationObjects: typing.Dict[int, UIAHandler.IUIAutomationElement] + + def _get_UIAAnnotationObjects(self) -> typing.Dict[int, UIAHandler.IUIAutomationElement]: + """ + Returns this UIAElement's annotation objects, + in a dict keyed by their annotation type ID. + """ + objsByTypeID = {} + objs = self._getUIACacheablePropertyValue(UIAHandler.UIA_AnnotationObjectsPropertyId) + if objs: + objs = objs.QueryInterface(UIAHandler.IUIAutomationElementArray) + for index in range(objs.length): + obj = objs.getElement(index) + typeID = obj.GetCurrentPropertyValue(UIAHandler.UIA_AnnotationAnnotationTypeIdPropertyId) + objsByTypeID[typeID] = obj + return objsByTypeID + def _get_UIATextPattern(self): - self.UIATextPattern=self._getUIAPattern(UIAHandler.UIA_TextPatternId,UIAHandler.IUIAutomationTextPattern,cache=True) + self.UIATextPattern = self._getUIAPattern( + UIAHandler.UIA_TextPatternId, + UIAHandler.IUIAutomationTextPattern, + cache=False + ) return self.UIATextPattern def _get_UIATextEditPattern(self): @@ -1247,7 +1320,10 @@ def _get_UIAAutomationId(self): # #11445: due to timing errors, elements will be instantiated with no automation Id present. return "" - def _get_name(self): + #: Typing info for auto property _get_name() + name: str + + def _get_name(self) -> str: try: return self._getUIACacheablePropertyValue(UIAHandler.UIA_NamePropertyId) except COMError: @@ -1320,6 +1396,7 @@ def _get_keyboardShortcut(self): UIAHandler.UIA_IsSelectionItemPatternAvailablePropertyId, UIAHandler.UIA_IsEnabledPropertyId, UIAHandler.UIA_IsOffscreenPropertyId, + UIAHandler.UIA_AnnotationTypesPropertyId, } def _get_states(self): @@ -1385,6 +1462,10 @@ def _get_states(self): states.add(controlTypes.STATE_CHECKABLE) if s==UIAHandler.ToggleState_On: states.add(controlTypes.STATE_CHECKED) + annotationTypes = self._getUIACacheablePropertyValue(UIAHandler.UIA_AnnotationTypesPropertyId) + if annotationTypes: + if UIAHandler.AnnotationType_Comment in annotationTypes: + states.add(controlTypes.STATE_HASCOMMENT) return states def _getReadOnlyState(self) -> bool: @@ -1447,7 +1528,10 @@ def _get_previous(self): return None return self.correctAPIForRelation(UIA(UIAElement=previousElement)) - def _get_next(self): + #: Typing information for auto-property: _get_next + next: "typing.Optional[UIA]" + + def _get_next(self) -> "typing.Optional[UIA]": try: nextElement=UIAHandler.handler.baseTreeWalker.GetNextSiblingElementBuildCache(self.UIAElement,UIAHandler.handler.baseCacheRequest) except COMError: @@ -1784,7 +1868,7 @@ def _get_positionInfo(self): info={} itemIndex=0 try: - itemIndex=self._getUIACacheablePropertyValue(UIAHandler.handler.ItemIndex_PropertyId) + itemIndex = self._getUIACacheablePropertyValue(self._UIACustomProps.itemIndex.id) except COMError: pass if itemIndex>0: @@ -1796,7 +1880,7 @@ def _get_positionInfo(self): e=None if e: try: - itemCount=e.getCurrentPropertyValue(UIAHandler.handler.ItemCount_PropertyId) + itemCount = e.getCurrentPropertyValue(self._UIACustomProps.itemCount.id) except COMError: itemCount=0 if itemCount>0: diff --git a/source/NVDAObjects/UIA/excel.py b/source/NVDAObjects/UIA/excel.py new file mode 100644 index 00000000000..84e6eb49912 --- /dev/null +++ b/source/NVDAObjects/UIA/excel.py @@ -0,0 +1,474 @@ +# 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-2021 NV Access Limited + +from typing import Optional, Tuple +import UIAHandler +import _UIAHandler +import _UIAConstants +from _UIAConstants import ( + UIAutomationType, +) +import colors +import locationHelper +import controlTypes +from _UIACustomProps import ( + CustomPropertyInfo, +) +from comtypes import GUID +from scriptHandler import script +import ui +from logHandler import log +from . import UIA + + +class ExcelCustomProperties: + """ UIA 'custom properties' specific to Excel. + Once registered, all subsequent registrations will return the same ID value. + This class should be used as a singleton via ExcelCustomProperties.get() + to prevent unnecessary work by repeatedly interacting with UIA. + """ + #: Singleton instance + _instance: "Optional[ExcelCustomProperties]" = None + + @classmethod + def get(cls) -> "ExcelCustomProperties": + """Get the singleton instance or initialise it. + """ + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def __init__(self): + self.cellFormula = CustomPropertyInfo( + guid=GUID("{E244641A-2785-41E9-A4A7-5BE5FE531507}"), + programmaticName="CellFormula", + uiaType=UIAutomationType.STRING, + ) + + self.cellNumberFormat = CustomPropertyInfo( + guid=GUID("{626CF4A0-A5AE-448B-A157-5EA4D1D057D7}"), + programmaticName="CellNumberFormat", + uiaType=UIAutomationType.STRING, + ) + + self.hasDataValidation = CustomPropertyInfo( + guid=GUID("{29F2E049-5DE9-4444-8338-6784C5D18ADF}"), + programmaticName="HasDataValidation", + uiaType=UIAutomationType.BOOL, + ) + + self.hasDataValidationDropdown = CustomPropertyInfo( + guid=GUID("{1B93A5CD-0956-46ED-9BBF-016C1B9FD75F}"), + programmaticName="HasDataValidationDropdown", + uiaType=UIAutomationType.BOOL, + ) + + self.dataValidationPrompt = CustomPropertyInfo( + guid=GUID("{7AAEE221-E14D-4DA4-83FE-842AAF06A9B7}"), + programmaticName="DataValidationPrompt", + uiaType=UIAutomationType.STRING, + ) + + self.hasConditionalFormatting = CustomPropertyInfo( + guid=GUID("{DFEF6BBD-7A50-41BD-971F-B5D741569A2B}"), + programmaticName="HasConditionalFormatting", + uiaType=UIAutomationType.BOOL, + ) + + self.commentReplyCount = CustomPropertyInfo( + guid=GUID("{312F7536-259A-47C7-B192-AA16352522C4}"), + programmaticName="CommentReplyCount", + uiaType=UIAutomationType.INT, + ) + + self.areGridLinesVisible = CustomPropertyInfo( + guid=GUID("{4BB56516-F354-44CF-A5AA-96B52E968CFD}"), + programmaticName="AreGridlinesVisible", + uiaType=UIAutomationType.BOOL, + ) + + +class ExcelObject(UIA): + """Common base class for all Excel UIA objects + """ + _UIAExcelCustomProps = ExcelCustomProperties.get() + + +class ExcelCell(ExcelObject): + + # selecting cells causes duplicate focus events + shouldAllowDuplicateUIAFocusEvent = True + + name = "" + role = controlTypes.ROLE_TABLECELL + rowHeaderText = None + columnHeaderText = None + + #: Typing information for auto-property: _get_areGridlinesVisible + areGridlinesVisible: bool + + def _get_areGridlinesVisible(self) -> bool: + parent = self.parent + # There will be at least one grid element between the cell and the sheet. + # There could be multiple as there might be a data table defined on the sheet. + while parent and parent.role == controlTypes.ROLE_TABLE: + parent = parent.parent + if parent: + return parent._getUIACacheablePropertyValue(self._UIAExcelCustomProps.areGridLinesVisible.id) + else: + log.debugWarning("Could not locate worksheet element.") + return False + + #: Typing information for auto-property: _get_outlineColor + outlineColor: Optional[Tuple[colors.RGB]] + + def _get_outlineColor(self) -> Optional[Tuple[colors.RGB]]: + val = self._getUIACacheablePropertyValue(_UIAHandler.UIA_OutlineColorPropertyId, True) + if isinstance(val, tuple): + return tuple(colors.RGB.fromCOLORREF(v) for v in val) + return None + + #: Typing information for auto-property: _get_outlineThickness + outlineThickness: Optional[Tuple[float]] + + def _get_outlineThickness(self) -> Optional[Tuple[float]]: + val = self._getUIACacheablePropertyValue(_UIAHandler.UIA_OutlineThicknessPropertyId, True) + if isinstance(val, tuple): + return val + return None + + #: Typing information for auto-property: _get_fillColor + fillColor: Optional[colors.RGB] + + def _get_fillColor(self) -> Optional[colors.RGB]: + val = self._getUIACacheablePropertyValue(_UIAHandler.UIA_FillColorPropertyId, True) + if isinstance(val, int): + return colors.RGB.fromCOLORREF(val) + return None + + #: Typing information for auto-property: _get_fillType + fillType: Optional[_UIAConstants.FillType] + + def _get_fillType(self) -> Optional[_UIAConstants.FillType]: + val = self._getUIACacheablePropertyValue(_UIAHandler.UIA_FillTypePropertyId, True) + if isinstance(val, int): + try: + return _UIAConstants.FillType(val) + except ValueError: + pass + return None + + #: Typing information for auto-property: _get_rotation + rotation: Optional[float] + + def _get_rotation(self) -> Optional[float]: + val = self._getUIACacheablePropertyValue(_UIAHandler.UIA_RotationPropertyId, True) + if isinstance(val, float): + return val + return None + + #: Typing information for auto-property: _get_cellSize + cellSize: locationHelper.Point + + def _get_cellSize(self) -> locationHelper.Point: + val = self._getUIACacheablePropertyValue(_UIAHandler.UIA_SizePropertyId, True) + x = val[0] + y = val[1] + return locationHelper.Point(x, y) + + @script( + description=pgettext( + "excel-UIA", + # Translators: the description of a script + "Shows a browseable message Listing information about a cell's " + "appearance such as outline and fill colors, rotation and size" + ), + gestures=["kb:NVDA+o"], + ) + def script_showCellAppearanceInfo(self, gesture): + infoList = [] + tmpl = pgettext( + "excel-UIA", + # Translators: The width of the cell in points + "Cell width: {0.x:.1f} pt" + ) + infoList.append(tmpl.format(self.cellSize)) + + tmpl = pgettext( + "excel-UIA", + # Translators: The height of the cell in points + "Cell height: {0.y:.1f} pt" + ) + infoList.append(tmpl.format(self.cellSize)) + + if self.rotation is not None: + tmpl = pgettext( + "excel-UIA", + # Translators: The rotation in degrees of an Excel cell + "Rotation: {0} degrees" + ) + infoList.append(tmpl.format(self.rotation)) + + if self.outlineColor is not None: + tmpl = pgettext( + "excel-UIA", + # Translators: The outline (border) colors of an Excel cell. + "Outline color: top={0.name}, bottom={1.name}, left={2.name}, right={3.name}" + ) + infoList.append(tmpl.format(*self.outlineColor)) + + if self.outlineThickness is not None: + tmpl = pgettext( + "excel-UIA", + # Translators: The outline (border) thickness values of an Excel cell. + "Outline thickness: top={0}, bottom={1}, left={2}, right={3}" + ) + infoList.append(tmpl.format(*self.outlineThickness)) + + if self.fillColor is not None: + tmpl = pgettext( + "excel-UIA", + # Translators: The fill color of an Excel cell + "Fill color: {0.name}" + ) + infoList.append(tmpl.format(self.fillColor)) + + if self.fillType is not None: + tmpl = pgettext( + "excel-UIA", + # Translators: The fill type (pattern, gradient etc) of an Excel Cell + "Fill type: {0}" + ) + infoList.append(tmpl.format(_UIAConstants.FillTypeLabels[self.fillType])) + numberFormat = self._getUIACacheablePropertyValue( + self._UIAExcelCustomProps.cellNumberFormat.id + ) + if numberFormat: + # Translators: the number format of an Excel cell + tmpl = _("Number format: {0}") + infoList.append(tmpl.format(numberFormat)) + hasDataValidation = self._getUIACacheablePropertyValue( + self._UIAExcelCustomProps.hasDataValidation.id + ) + if hasDataValidation: + # Translators: If an excel cell has data validation set + tmpl = _("Has data validation") + infoList.append(tmpl) + dataValidationPrompt = self._getUIACacheablePropertyValue( + self._UIAExcelCustomProps.dataValidationPrompt.id + ) + if dataValidationPrompt: + # Translators: the data validation prompt (input message) for an Excel cell + tmpl = _("Data validation prompt: {0}") + infoList.append(tmpl.format(dataValidationPrompt)) + hasConditionalFormatting = self._getUIACacheablePropertyValue( + self._UIAExcelCustomProps.hasConditionalFormatting.id + ) + if hasConditionalFormatting: + # Translators: If an excel cell has conditional formatting + tmpl = _("Has conditional formatting") + infoList.append(tmpl) + if self.areGridlinesVisible: + # Translators: If an excel cell has visible gridlines + tmpl = _("Gridlines are visible") + infoList.append(tmpl) + infoString = "\n".join(infoList) + ui.browseableMessage( + infoString, + title=pgettext( + "excel-UIA", + # Translators: Title for a browsable message that describes the appearance of a cell in Excel + "Cell Appearance" + ) + ) + + def _hasSelection(self): + return ( + self.selectionContainer + and 1 < self.selectionContainer.getSelectedItemsCount() + ) + + def _get_value(self): + if self._hasSelection(): + return + return super().value + + def _get_errorText(self): + for typeId, element in self.UIAAnnotationObjects.items(): + if typeId in { + UIAHandler.AnnotationType_DataValidationError, + UIAHandler.AnnotationType_FormulaError, + UIAHandler.AnnotationType_CircularReferenceError, + }: + return element.GetCurrentPropertyValue(UIAHandler.UIA_FullDescriptionPropertyId) + + def _get_description(self): + """ + Prepends error messages and collaborator presence to any existing description. + """ + descriptionList = [] + if self.errorText: + # Translators: an error message on a cell in Microsoft Excel + descriptionList.append( + # Translators: an error message on a cell in Microsoft Excel. + _("Error: {errorText}").format(errorText=self.errorText) + ) + presence = self.UIAAnnotationObjects.get(UIAHandler.AnnotationType_Author) + if presence: + author = presence.GetCurrentPropertyValue(UIAHandler.UIA_AnnotationAuthorPropertyId) + descriptionList.append( + # Translators: a mesage when another author is editing a cell in a shared Excel spreadsheet. + _("{author} is editing").format( + author=author + ) + ) + baseDescription = super().description + if baseDescription: + descriptionList.append(baseDescription) + return ", ".join(descriptionList) + + #: Typing information for auto-property: _get__isContentTooLargeForCell + _isContentTooLargeForCell: bool + + def _get__isContentTooLargeForCell(self) -> bool: + if not self.UIATextPattern: + return False + r = self.UIATextPattern.documentRange + vr = self.UIATextPattern.getvisibleRanges().getElement(0) + return len(vr.getText(-1)) < len(r.getText(-1)) + + #: Typing information for auto-property: _get__nextCellHasContent + _nextCellHasContent: bool + + def _get__nextCellHasContent(self) -> bool: + nextCell = self.next + if nextCell and nextCell.UIATextPattern: + return bool(nextCell.UIATextPattern.documentRange.getText(-1)) + return False + + def _get_states(self): + states = super().states + if self._isContentTooLargeForCell: + if not self._nextCellHasContent: + states.add(controlTypes.STATE_OVERFLOWING) + else: + states.add(controlTypes.STATE_CROPPED) + if self._getUIACacheablePropertyValue(self._UIAExcelCustomProps.cellFormula.id): + states.add(controlTypes.STATE_HASFORMULA) + if self._getUIACacheablePropertyValue(self._UIAExcelCustomProps.hasDataValidationDropdown.id): + states.add(controlTypes.STATE_HASPOPUP) + return states + + def _get_cellCoordsText(self): + if self._hasSelection(): + sc = self._getUIACacheablePropertyValue( + UIAHandler.UIA_SelectionItemSelectionContainerPropertyId + ).QueryInterface(UIAHandler.IUIAutomationElement) + + firstSelected = sc.GetCurrentPropertyValue( + UIAHandler.UIA_Selection2FirstSelectedItemPropertyId + ).QueryInterface(UIAHandler.IUIAutomationElement) + + firstAddress = firstSelected.GetCurrentPropertyValue( + UIAHandler.UIA_NamePropertyId + ).replace('"', '') + + firstValue = firstSelected.GetCurrentPropertyValue( + UIAHandler.UIA_ValueValuePropertyId + ) + + lastSelected = sc.GetCurrentPropertyValue( + UIAHandler.UIA_Selection2LastSelectedItemPropertyId + ).QueryInterface(UIAHandler.IUIAutomationElement) + + lastAddress = lastSelected.GetCurrentPropertyValue( + UIAHandler.UIA_NamePropertyId + ).replace('"', '') + + lastValue = lastSelected.GetCurrentPropertyValue( + UIAHandler.UIA_ValueValuePropertyId + ) + + cellCoordsTemplate = pgettext( + "excel-UIA", + # Translators: Excel, report range of cell coordinates + "{firstAddress} {firstValue} through {lastAddress} {lastValue}" + ) + return cellCoordsTemplate.format( + firstAddress=firstAddress, + firstValue=firstValue, + lastAddress=lastAddress, + lastValue=lastValue + ) + name = super().name + # Later builds of Excel 2016 quote the letter coordinate. + # We don't want the quotes. + name = name.replace('"', '') + return name + + @script( + # Translators: the description for a script for Excel + description=_("Reports the note or comment thread on the current cell"), + gesture="kb:NVDA+alt+c") + def script_reportComment(self, gesture): + commentsElement = self.UIAAnnotationObjects.get(UIAHandler.AnnotationType_Comment) + if commentsElement: + comment = commentsElement.GetCurrentPropertyValue(UIAHandler.UIA_FullDescriptionPropertyId) + author = commentsElement.GetCurrentPropertyValue(UIAHandler.UIA_AnnotationAuthorPropertyId) + numReplies = commentsElement.GetCurrentPropertyValue(self._UIAExcelCustomProps.commentReplyCount.id) + if numReplies == 0: + # Translators: a comment on a cell in Microsoft excel. + text = _("{comment} by {author}").format( + comment=comment, + author=author + ) + else: + # Translators: a comment on a cell in Microsoft excel. + text = _("{comment} by {author} with {numReplies} replies").format( + comment=comment, + author=author, + numReplies=numReplies + ) + ui.message(text) + else: + # Translators: A message in Excel when there is no note + ui.message(_("No note or comment thread on this cell")) + + +class ExcelWorksheet(ExcelObject): + role = controlTypes.ROLE_TABLE + + # The grid UIAElement dies each time the sheet is scrolled. + # Therefore this grid would be announced in the focus ancestory each time which is bad. + # Suppress this. + isPresentableFocusAncestor = False + + def _get_parent(self): + parent = super().parent + # We want to present the parent (sheet) in the focus ancestry + # As that is what has the sheet name. + # Making this change in an overlay class was considered, however + # the parent (sheet) has no useful properties (such as className) to be easily identified, + # and identifying it by one of its children would be more costly than the current approach. + parent.isPresentableFocusAncestor = True + # However, the selection state on the sheet is not useful, so remove it. + parent.states.discard(controlTypes.STATE_SELECTED) + return parent + + +class CellEdit(ExcelObject): + name = "" + + +class BadExcelFormulaEdit(ExcelObject): + """ + Suppresses focus events on the old formula bar, which does not have a usable UIA implementation. + Excel used to focus the EXCEL6 window with the formula bar when editing a cell. + However, now focus is moved to an edit control within the actual cell being edited, + in the EXCEL7 window. + Sometimes however focus (probably proxied from MSAA) still gets occasionally fired. + """ + + shouldAllowUIAFocusEvent = False diff --git a/source/NVDAObjects/__init__.py b/source/NVDAObjects/__init__.py index b93410d1c0c..56c3206b4da 100644 --- a/source/NVDAObjects/__init__.py +++ b/source/NVDAObjects/__init__.py @@ -11,6 +11,7 @@ import os import time import re +import typing import weakref from logHandler import log import review @@ -446,15 +447,20 @@ def _get_roleTextBraille(self): return f"{braille.roleLabels[controlTypes.ROLE_LANDMARK]} {braille.landmarkLabels[self.landmark]}" return self.roleText - def _get_value(self): - """The value of this object (example: the current percentage of a scrollbar, the selected option in a combo box). - @rtype: str + #: Typing information for auto property _get_value + value: str + + def _get_value(self) -> str: + """The value of this object + (example: the current percentage of a scrollbar, the selected option in a combo box). """ return "" - def _get_description(self): + #: Typing information for auto property _get_description + description: str + + def _get_description(self) -> str: """The description or help text of this object. - @rtype: str """ return "" @@ -499,12 +505,11 @@ def _get_isInForeground(self): raise NotImplementedError # Type info for auto property: - states: set + states: typing.Set[int] - def _get_states(self): + def _get_states(self) -> typing.Set[int]: """Retrieves the current states of this object (example: selected, focused). @return: a set of STATE_* constants from L{controlTypes}. - @rtype: set of int """ return set() @@ -640,13 +645,15 @@ def _get_presentationalColumnNumber(self): """ raise NotImplementedError - def _get_cellCoordsText(self): + #: Typing information for auto-property: _get_cellCoordsText + cellCoordsText: typing.Optional[str] + + def _get_cellCoordsText(self) -> typing.Optional[str]: """ An alternative text representation of cell coordinates e.g. "a1". Will override presentation of rowNumber and columnNumber. Only implement if the representation is really different. """ return None - def _get_rowCount(self): """Retrieves the number of rows this object contains if its a table. diff --git a/source/UIAHandler.py b/source/UIAHandler.py index 528af0a81c0..a866cd4f195 100644 --- a/source/UIAHandler.py +++ b/source/UIAHandler.py @@ -1,20 +1,21 @@ -#UIAHandler.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2008-2018 NV Access Limited, Joseph Lee -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2008-2018 NV Access Limited, Joseph Lee +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +from typing import Optional from comtypes import COMError import config from logHandler import log # Maintain backwards compatibility: F403 (unable to detect undefined names) flake8 warning. from _UIAHandler import * # noqa: F403 +from _UIAHandler import UIAHandler # Make the _UIAHandler._isDebug function available to this module, # ignoring the fact that it is not used here directly. from _UIAHandler import _isDebug # noqa: F401 -handler=None +handler: Optional[UIAHandler] = None def initialize(): global handler diff --git a/source/_UIAConstants.py b/source/_UIAConstants.py new file mode 100644 index 00000000000..f458e8b33bb --- /dev/null +++ b/source/_UIAConstants.py @@ -0,0 +1,62 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2021 NV Access Limited +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. + +import enum + + +class FillType(enum.IntEnum): + NONE = 0 + COLOR = 1 + GRADIENT = 2 + PICTURE = 3 + PATTERN = 4 + + +FillTypeLabels = { + # Translators: a style of fill type (to color the inside of a control or text) + FillType.NONE: pgettext("UIAHandler.FillType", "none"), + # Translators: a style of fill type (to color the inside of a control or text) + FillType.COLOR: pgettext("UIAHandler.FillType", "color"), + # Translators: a style of fill type (to color the inside of a control or text) + FillType.GRADIENT: pgettext("UIAHandler.FillType", "gradient"), + # Translators: a style of fill type (to color the inside of a control or text) + FillType.PICTURE: pgettext("UIAHandler.FillType", "picture"), + # Translators: a style of fill type (to color the inside of a control or text) + FillType.PATTERN: pgettext("UIAHandler.FillType", "pattern"), +} + + +# Some newer UIA constants that could be missing +class UIAutomationType(enum.IntEnum): + INT = 1 + BOOL = 2 + STRING = 3 + DOUBLE = 4 + POINT = 5 + RECT = 6 + ELEMENT = 7 + ARRAY = 8 + OUT = 9 + INT_ARRAY = 10 + BOOL_ARRAY = 11 + STRING_ARRAY = 12 + DOUBLE_ARRAY = 13 + POINT_ARRAY = 14 + RECT_ARRAY = 15 + ELEMENT_ARRAY = 16 + OUT_INT = 17 + OUT_BOOL = 18 + OUT_STRING = 19 + OUT_DOUBLE = 20 + OUT_POINT = 21 + OUT_RECT = 22 + OUT_ELEMENT = 23 + OUT_INT_ARRAY = 24 + OUT_BOOL_ARRAY = 25 + OUT_STRING_ARRAY = 26 + OUT_DOUBLE_ARRAY = 27 + OUT_POINT_ARRAY = 28 + OUT_RECT_ARRAY = 29 + OUT_ELEMENT_ARRAY = 30 diff --git a/source/_UIACustomProps.py b/source/_UIACustomProps.py new file mode 100644 index 00000000000..354c9bf91e2 --- /dev/null +++ b/source/_UIACustomProps.py @@ -0,0 +1,90 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2021 NV Access Limited +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. + +from dataclasses import ( + dataclass, + field, +) +from typing import Optional + +from comtypes import ( + GUID, + byref, +) +from _UIAConstants import ( + UIAutomationType, +) + +""" +This module provides helpers and a common format to define UIA custom properties. +The common custom properties are defined here. +Custom properties specific to an application should be defined within a NVDAObjects/UIA +submodule specific to that application, E.G. 'NVDAObjects/UIA/excel.py' + +UIA originally had hard coded 'static' ID's for properties. +For an example see 'UIA_SelectionPatternId' in +`source/comInterfaces/_944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0.py` +imported via `UIAutomationClient.py`. +When a new property was added the UIA spec had to be updated. +Now a mechanism is in place to allow applications to register "custom properties". +This relies on both the UIA server application and the UIA client application sharing a known +GUID for the property. +""" + + +@dataclass +class CustomPropertyInfo: + """Holds information about a CustomProperty + This makes it easy to define custom properties to be loaded. + """ + guid: GUID + programmaticName: str + uiaType: UIAutomationType + id: int = field(init=False) + + def __post_init__(self) -> None: + """ The id field must be initialised at runtime. + UIA will return the id to use when given the GUID. + Any application can be first to register a custom property, subsequent applications + will be given the same id. + """ + import NVDAHelper + self.id = NVDAHelper.localLib.registerUIAProperty( + byref(self.guid), + self.programmaticName, + self.uiaType + ) + + +class CustomPropertiesCommon: + """UIA 'custom properties' common to all applications. + Once registered, all subsequent registrations will return the same ID value. + This class should be used as a singleton via CustomPropertiesCommon.get() + to prevent unnecessary work by repeatedly interacting with UIA. + """ + #: Singleton instance + _instance: "Optional[CustomPropertiesCommon]" = None + + @classmethod + def get(cls) -> "CustomPropertiesCommon": + """Get the singleton instance or initialise it. + """ + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def __init__(self): + + self.itemIndex = CustomPropertyInfo( + guid=GUID("{92A053DA-2969-4021-BF27-514CFC2E4A69}"), + programmaticName="ItemIndex", + uiaType=UIAutomationType.INT, + ) + + self.itemCount = CustomPropertyInfo( + guid=GUID("{ABBF5C45-5CCC-47b7-BB4E-87CB87BBD162}"), + programmaticName="ItemCount", + uiaType=UIAutomationType.INT, + ) diff --git a/source/_UIAHandler.py b/source/_UIAHandler.py index 30deaa41514..ab1c778f6f9 100644 --- a/source/_UIAHandler.py +++ b/source/_UIAHandler.py @@ -1,55 +1,64 @@ -# _UIAHandler.py # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2011-2020 NV Access Limited, Joseph Lee, Babbage B.V., Leonard de Ruijter +# Copyright (C) 2011-2021 NV Access Limited, Joseph Lee, Babbage B.V., Leonard de Ruijter # This file is covered by the GNU General Public License. # See the file COPYING for more details. -from ctypes import * -from ctypes.wintypes import * -from enum import Enum +import ctypes +import ctypes.wintypes +from ctypes import ( + oledll, + windll, +) +from enum import ( + Enum, +) import comtypes.client from comtypes.automation import VT_EMPTY -from comtypes import COMError -from comtypes import * -import weakref +from comtypes import ( + COMError, + COMObject, + byref, + CLSCTX_INPROC_SERVER, + CoCreateInstance, +) + import threading import time -from collections import namedtuple import config import api import appModuleHandler -import queueHandler import controlTypes -import NVDAHelper import winKernel import winUser import winVersion import eventHandler from logHandler import log import UIAUtils -from comtypes.gen import UIAutomationClient as UIA -from comtypes.gen.UIAutomationClient import * +from comInterfaces import UIAutomationClient as UIA +# F403 unable to detect undefined names +from comInterfaces.UIAutomationClient import * # noqa: F403 import textInfos from typing import Dict from queue import Queue import aria -#Some newer UIA constants that could be missing -ItemIndex_Property_GUID=GUID("{92A053DA-2969-4021-BF27-514CFC2E4A69}") -ItemCount_Property_GUID=GUID("{ABBF5C45-5CCC-47b7-BB4E-87CB87BBD162}") + HorizontalTextAlignment_Left=0 HorizontalTextAlignment_Centered=1 HorizontalTextAlignment_Right=2 HorizontalTextAlignment_Justified=3 + + # The name of the WDAG (Windows Defender Application Guard) process WDAG_PROCESS_NAME=u'hvsirdpclient' goodUIAWindowClassNames=[ # A WDAG (Windows Defender Application Guard) Window is always native UIA, even if it doesn't report as such. 'RAIL_WINDOW', + "EXCEL6", ] badUIAWindowClassNames=[ @@ -67,7 +76,6 @@ "RichEdit20", "RICHEDIT50W", "SysListView32", - "EXCEL7", "Button", # #8944: The Foxit UIA implementation is incomplete and should not be used for now. "FoxitDocWnd", @@ -248,7 +256,13 @@ def __init__(self): raise self.MTAThreadInitException def terminate(self): - MTAThreadHandle=HANDLE(windll.kernel32.OpenThread(winKernel.SYNCHRONIZE,False,self.MTAThread.ident)) + MTAThreadHandle = ctypes.wintypes.HANDLE( + windll.kernel32.OpenThread( + winKernel.SYNCHRONIZE, + False, + self.MTAThread.ident + ) + ) self.MTAThreadQueue.put_nowait(None) #Wait for the MTA thread to die (while still message pumping) if windll.user32.MsgWaitForMultipleObjects(1,byref(MTAThreadHandle),False,200,0)!=0: @@ -309,9 +323,6 @@ def MTAThreadFunc(self): self.UIAWindowHandleCache={} self.baseTreeWalker=self.clientObject.RawViewWalker self.baseCacheRequest=self.windowCacheRequest.Clone() - import UIAHandler - self.ItemIndex_PropertyId=NVDAHelper.localLib.registerUIAProperty(byref(ItemIndex_Property_GUID),u"ItemIndex",1) - self.ItemCount_PropertyId=NVDAHelper.localLib.registerUIAProperty(byref(ItemCount_Property_GUID),u"ItemCount",1) for propertyId in (UIA_FrameworkIdPropertyId,UIA_AutomationIdPropertyId,UIA_ClassNamePropertyId,UIA_ControlTypePropertyId,UIA_ProviderDescriptionPropertyId,UIA_ProcessIdPropertyId,UIA_IsTextPatternAvailablePropertyId,UIA_IsContentElementPropertyId,UIA_IsControlElementPropertyId): self.baseCacheRequest.addProperty(propertyId) self.baseCacheRequest.addPattern(UIA_TextPatternId) @@ -520,14 +531,15 @@ def IUIAutomationFocusChangedEventHandler_HandleFocusChangedEvent(self,sender): return import NVDAObjects.UIA if isinstance(eventHandler.lastQueuedFocusObject,NVDAObjects.UIA.UIA): - lastFocus=eventHandler.lastQueuedFocusObject.UIAElement + lastFocusObj = eventHandler.lastQueuedFocusObject # Ignore duplicate focus events. # It seems that it is possible for compareElements to return True, even though the objects are different. # Therefore, don't ignore the event if the last focus object has lost its hasKeyboardFocus state. try: if ( - self.clientObject.compareElements(sender, lastFocus) - and lastFocus.currentHasKeyboardFocus + not lastFocusObj.shouldAllowDuplicateUIAFocusEvent + and self.clientObject.compareElements(sender, lastFocusObj.UIAElement) + and lastFocusObj.UIAElement.currentHasKeyboardFocus ): if _isDebug(): log.debugWarning("HandleFocusChangedEvent: Ignoring duplicate focus event") @@ -711,21 +723,42 @@ def _isUIAWindowHelper(self,hwnd): # Ask the window if it supports UIA natively res=windll.UIAutomationCore.UiaHasServerSideProvider(hwnd) if res: - # the window does support UIA natively, but - # MS Word documents now have a fairly usable UI Automation implementation. However, - # Builds of MS Office 2016 before build 9000 or so had bugs which we cannot work around. - # And even current builds of Office 2016 are still missing enough info from UIA that it is still impossible to switch to UIA completely. - # Therefore, if we can inject in-process, refuse to use UIA and instead fall back to the MS Word object model. + # The window does support UIA natively, but MS Word documents now + # have a fairly usable UI Automation implementation. + # However, builds of MS Office 2016 before build 9000 or so had bugs which + # we cannot work around. + # And even current builds of Office 2016 are still missing enough info from + # UIA that it is still impossible to switch to UIA completely. + # Therefore, if we can inject in-process, refuse to use UIA and instead + # fall back to the MS Word object model. canUseOlderInProcessApproach = bool(appModule.helperLocalBindingHandle) if ( # An MS Word document window windowClass=="_WwG" # Disabling is only useful if we can inject in-process (and use our older code) and canUseOlderInProcessApproach - # Allow the user to explisitly force UIA support for MS Word documents no matter the Office version + # Allow the user to explicitly force UIA support for MS Word documents + # no matter the Office version and not config.conf['UIA']['useInMSWordWhenAvailable'] ): return False + # MS Excel spreadsheets now have a fairly usable UI Automation implementation. + # However, builds of MS Office 2016 before build 9000 or so had bugs which we + # cannot work around. + # And even current builds of Office 2016 are still missing enough info from UIA + # that it is still impossible to switch to UIA completely. + # Therefore, if we can inject in-process, refuse to use UIA and instead fall + # back to the MS Excel object model. + elif ( + # An MS Excel spreadsheet window + windowClass == "EXCEL7" + # Disabling is only useful if we can inject in-process (and use our older code) + and appModule.helperLocalBindingHandle + # Allow the user to explicitly force UIA support for MS Excel spreadsheets + # no matter the Office version + and not config.conf['UIA']['useInMSExcelWhenAvailable'] + ): + return False # Unless explicitly allowed, all Chromium implementations (including Edge) should not be UIA, # As their IA2 implementation is still better at the moment. elif ( diff --git a/source/colors.py b/source/colors.py index ead8d7b77a0..327eff5a922 100644 --- a/source/colors.py +++ b/source/colors.py @@ -14,7 +14,7 @@ class RGB(namedtuple('RGB',('red','green','blue'))): """Represents a color as an RGB (red green blue) value""" @classmethod - def fromCOLORREF(cls,c): + def fromCOLORREF(cls, c) -> "RGB": """factory method to create an RGB from a COLORREF ctypes instance""" if isinstance(c,COLORREF): c=c.value diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 54dc5a49c6f..6e95716537f 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -219,6 +219,7 @@ [UIA] enabled = boolean(default=true) useInMSWordWhenAvailable = boolean(default=false) + useInMSExcelWhenAvailable = boolean(default=false) winConsoleImplementation= option("auto", "legacy", "UIA", default="auto") selectiveEventRegistration = boolean(default=false) # 0:default, 1:Only when necessary, 2:yes, 3:no diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 8b012bda4a2..6bbb1fc7bd4 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2566,6 +2566,14 @@ def __init__(self, parent): self.UIAInMSWordCheckBox.SetValue(config.conf["UIA"]["useInMSWordWhenAvailable"]) self.UIAInMSWordCheckBox.defaultValue = self._getDefaultValue(["UIA", "useInMSWordWhenAvailable"]) + # Translators: This is the label for a checkbox in the + # Advanced settings panel. + label = _("Use UI Automation to access Microsoft &Excel spreadsheet controls when available") + self.UIAInMSExcelCheckBox = UIAGroup.addItem(wx.CheckBox(self, label=label)) + self.bindHelpEvent("UseUiaForExcel", self.UIAInMSExcelCheckBox) + self.UIAInMSExcelCheckBox.SetValue(config.conf["UIA"]["useInMSExcelWhenAvailable"]) + self.UIAInMSExcelCheckBox.defaultValue = self._getDefaultValue(["UIA", "useInMSExcelWhenAvailable"]) + # Translators: This is the label for a checkbox in the # Advanced settings panel. label = _("Use UI Automation to access the Windows C&onsole when available") @@ -2768,6 +2776,7 @@ def haveConfigDefaultsBeenRestored(self): == self.selectiveUIAEventRegistrationCheckBox.defaultValue ) and self.UIAInMSWordCheckBox.IsChecked() == self.UIAInMSWordCheckBox.defaultValue + and self.UIAInMSExcelCheckBox.IsChecked() == self.UIAInMSExcelCheckBox.defaultValue and self.ConsoleUIACheckBox.IsChecked() == (self.ConsoleUIACheckBox.defaultValue == 'UIA') and self.winConsoleSpeakPasswordsCheckBox.IsChecked() == self.winConsoleSpeakPasswordsCheckBox.defaultValue and self.cancelExpiredFocusSpeechCombo.GetSelection() == self.cancelExpiredFocusSpeechCombo.defaultValue @@ -2783,6 +2792,7 @@ def restoreToDefaults(self): self.scratchpadCheckBox.SetValue(self.scratchpadCheckBox.defaultValue) self.selectiveUIAEventRegistrationCheckBox.SetValue(self.selectiveUIAEventRegistrationCheckBox.defaultValue) self.UIAInMSWordCheckBox.SetValue(self.UIAInMSWordCheckBox.defaultValue) + self.UIAInMSExcelCheckBox.SetValue(self.UIAInMSExcelCheckBox.defaultValue) self.ConsoleUIACheckBox.SetValue(self.ConsoleUIACheckBox.defaultValue == 'UIA') self.UIAInChromiumCombo.SetSelection(self.UIAInChromiumCombo.defaultValue) self.winConsoleSpeakPasswordsCheckBox.SetValue(self.winConsoleSpeakPasswordsCheckBox.defaultValue) @@ -2798,6 +2808,7 @@ def onSave(self): config.conf["development"]["enableScratchpadDir"]=self.scratchpadCheckBox.IsChecked() config.conf["UIA"]["selectiveEventRegistration"] = self.selectiveUIAEventRegistrationCheckBox.IsChecked() config.conf["UIA"]["useInMSWordWhenAvailable"]=self.UIAInMSWordCheckBox.IsChecked() + config.conf["UIA"]["useInMSExcelWhenAvailable"] = self.UIAInMSExcelCheckBox.IsChecked() if self.ConsoleUIACheckBox.IsChecked(): config.conf['UIA']['winConsoleImplementation'] = "UIA" else: diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index bdd98708d67..d4d9edcc55e 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -7,6 +7,7 @@ What's New in NVDA == New Features == - Early support for UIA with Chromium based browsers (such as Edge). (#12025) +- Optional experimental support for Microsoft Excel via UI Automation. Only recommended for Microsoft Excel build 16.0.13522.10000 or higher. (#12210) == Changes == diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 7efed26778a..39bf105e16c 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -1849,6 +1849,14 @@ The combo box has the following options: - No: Don't use UIA, even if NVDA is unable to inject in process. This may be useful for developers debugging issues with IA2 and want to ensure that NVDA does not fall back to UIA. - +==== Use UI automation to access Microsoft Excel spreadsheet controls when available ====[UseUiaForExcel] +When this option is enabled, NVDA will try to use the Microsoft UI Automation accessibility API in order to fetch information from Microsoft Excel Spreadsheet controls. +This is an experimental feature, and some features of Microsoft Excel may not be available in this mode. +For instance, NVDA's Elements List for listing formulas and comments, and Browse mode quick navigation to jump to form fields on a spreadsheet features are not available. +However, for basic spreadsheet navigating / editing, this option may provide a vast performance improvement. +We still do not recommend that the majority of users turn this on by default, though we do welcome users of Microsoft Excel build 16.0.13522.10000 or higher to test this feature and provide feedback. +Microsoft Excel's UI automation implementation is ever changing, and versions of Microsoft Office older than 16.0.13522.10000 may not expose enough information for this option to be of any use. + ==== Use the new typed character support in Windows Console when available ====[AdvancedSettingsKeyboardSupportInLegacy] This option enables an alternative method for detecting typed characters in Windows command consoles. While it improves performance and prevents some console output from being spelled out, it may be incompatible with some terminal programs. From 57e02cae77b33a312b5adc190b150bca9e27967e Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Thu, 25 Mar 2021 05:46:31 +0100 Subject: [PATCH 102/174] use wx.Accessible to improve accessibility of our settings panels and checkable list controls (#12215) To fix some accessibility issues in our settings panel and checkable lists, we had to use a custom com server with comtypes. However, wx has built-in functionality to manipulate accessibility properties. This commit removes our own accprop server in favour of wx' native support. Closes #12209 --- source/gui/accPropServer.py | 178 ---------------------------------- source/gui/nvdaControls.py | 93 ++++-------------- source/gui/settingsDialogs.py | 25 +++-- user_docs/en/changes.t2t | 1 + 4 files changed, 39 insertions(+), 258 deletions(-) delete mode 100644 source/gui/accPropServer.py diff --git a/source/gui/accPropServer.py b/source/gui/accPropServer.py deleted file mode 100644 index aabef6e367b..00000000000 --- a/source/gui/accPropServer.py +++ /dev/null @@ -1,178 +0,0 @@ -#accPropServer.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2017-2018 NV Access Limited, Derek Riemer, Babbage B.V. -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. - -"""Implementation of IAccProcServer, so that customization of a wx control can be done very fast.""" -from ctypes.wintypes import BOOL -from typing import Optional, Tuple, Any, Union, Callable - -from logHandler import log -from comtypes.automation import S_OK, VARIANT, POINTER, c_int, c_double, _oleaut32 -from comtypes import COMObject, GUID -from comInterfaces.Accessibility import IAccPropServer, ANNO_CONTAINER, ANNO_THIS -from abc import ABCMeta, abstractmethod -import weakref -import winUser -import wx - -_VariantInit: Callable[[POINTER(VARIANT),], None] = _oleaut32.VariantInit -_VariantInit.argtypes = (POINTER(VARIANT),) - -AcceptedGetPropTypes = Union[ - bool, - int, c_int, - float, c_double, - str, - VARIANT, - # And others: L{comtpyes.automation.tagVariant._set_value} - ] - -class IAccPropServer_Impl(COMObject, metaclass=ABCMeta): - """Base class for implementing a COM interface for a hwnd based AccPropServer\ - to annotate a WX control. - The AccPropServer registers itself using the window handle of the WX control. - When the WX control is destroyed, the instance is automatically unregistered. - This should eventually be dropped in favor of WX' own annotation support, - blocked by wxWidgets/Phoenix#1129. - Please override the L{_GetPropValue} method, not L{GetPropValue}. - L{GetPropValue} wraps L{_getPropValue} to catch and log exceptions (Which for some reason NVDA's logger misses when they occur in GetPropValue). - You must also provide the L{properties} property. - """ - - _com_interfaces_ = [ - IAccPropServer - ] - - # Constants used with `IAccPropServer::GetPropValue` method see - # https://msdn.microsoft.com/en-us/library/windows/desktop/dd318495(v=vs.85).aspx - HAS_PROP = 1 # TRUE - Constant for `BOOL* pfHasProp` out param of `IAccPropServer::GetPropValue` method - DOES_NOT_HAVE_PROP = 0 # FALSE - Constant for `BOOL* pfHasProp` out param of `IAccPropServer::GetPropValue` method - - # An array with the GUIDs of the properties that an AccPropServer should override - properties_GUIDPTR = [] - properties = [] - - def __init__(self, control, annotateProperties, annotateChildren=False): - """Initialize the instance of AccPropServer. - @param control: the WX control instance, so you can look up things in the _getPropValue method. - It's available on self.control. - @Type control: Subclass of wx.Window - @param annotateProperties The properties that should be annotated, see oleacc.py for constants. - @type annotateProperties List of oleacc constants. Internally these are converted to GUID pointers for the server. - @param annotateChildren: whether the WX control is a container which children should be annotated. - @type annotateChildren: bool - """ - self.properties = annotateProperties - self.properties_GUIDPTR = convertToGUIDPointerList(annotateProperties) - self.control = weakref.ref(control) - self.hwnd = control.GetHandle() - super(IAccPropServer_Impl, self).__init__() - # Import late to avoid circular import - from IAccessibleHandler import accPropServices - accPropServices.SetHwndPropServer( - hwnd=self.hwnd, - idObject=winUser.OBJID_CLIENT, - idChild=0, - paProps=self.properties_GUIDPTR, - cProps=len(self.properties_GUIDPTR), - pServer=self, - AnnoScope=ANNO_CONTAINER if annotateChildren else ANNO_THIS - ) - # clean up of accPropServices needs to happen when the control is destroyed. We can't rely on - # pythons `__del__` method to be called, and the wx framework does not call Destroy on child controls, - # automatically. Instead we can bind to the "window destroy" event for the control to do necessary - # cleanup (wxWidgets/Phoenix/#630). Not performing this cleanup results in a reference to the parent - # window, keeping it from being deleted correctly. This can cause a freeze on exit of NVDA. - control.Bind(wx.EVT_WINDOW_DESTROY, self._onDestroyControl, source=control) - - @abstractmethod - def _getPropValue( - self, - pIDString: str, - dwIDStringLen: int, - idProp: GUID - ) -> Optional[Tuple[BOOL, AcceptedGetPropTypes]]: - """ Use this method to implement GetPropValue. - It is wrapped by the callback GetPropValue to handle exceptions, and ensure valid return types. - For instructions on implementing accPropServers, see https://msdn.microsoft.com/en-us/library/windows/desktop/dd373681(v=vs.85).aspx . - For instructions specifically about this method, see https://msdn.microsoft.com/en-us/library/windows/desktop/dd318495(v=vs.85).aspx . - @param pIDString: Contains a string that identifies the property being requested. - If a single callback object is registered for annotating multiple accessible elements, - the identity string can be used to determine which element the request refers to. - If the accessible element is HWND-based, - IAccessibleHandler.accPropServices.DecomposeHwndIdentityString can be used - to extract the HWND/idObject/idChild from the identity string. - Note that, while one IAccPropServer implementation can annotate - multiple accessible elements, it is still bound to one wx.Control. - @param dwIDStringLen: Specifies the length of the identity string specified by the pIDString parameter. - @param idProp: Specifies a GUID indicating the desired property. One of the values from oleacc.PROPID_* - @return Use L{self._hasProp} to return correct values or return None if unable to supply the property. - """ - raise NotImplementedError - - def _hasProp( - self, - value: AcceptedGetPropTypes - ) -> Optional[Tuple[BOOL, AcceptedGetPropTypes]]: - """Constructs a tuple for the `IAccPropServer::GetPropValue` method, two elements: - 1. `VARIANT pvarValue` - 2. `BOOL pfHasProp` (either self.HAS_PROP or self.DOES_NOT_HAVE_PROP)""" - return value, self.HAS_PROP - - def GetPropValue( - self, this, # unused "this" used to indicate to comTypes we want a low level implementation - pIDString: str, - dwIDStringLen: int, - idProp: GUID, - pvarValue: POINTER(VARIANT), - pfGotProp: POINTER(BOOL) - ) -> int: - """ Exposed method to get a prop value. - see L{_getPropValue} for more details of args. - Uses a low-level approach, because comtypes tries to clear the VARIANT even though it is an out param. - When the pfHasProp part is FALSE / self.DOES_NOT_HAVE_PROP, then the pvarValue.vt part must be VT_EMPTY. - """ - # ensure exceptions don't leave this function. They will get get swallowed by the caller. - # instead catch and log exceptions. - try: - # Preset values for "no prop value", in case we return early. - pfGotProp.contents.value = self.DOES_NOT_HAVE_PROP - _VariantInit(pvarValue) - - ret = self._getPropValue(pIDString, dwIDStringLen, idProp) - if ret is None: - # We don't have the prop value, return early. - return S_OK - elif len(ret) != 2: - # We don't have the prop value, internal error. - raise RuntimeError("_getPropValue implementation must return None or two element tuple") - elif ret[1] != self.HAS_PROP: - # We don't have the prop value, return early. - return S_OK - - # we do have the prop value - pfGotProp.contents.value = self.HAS_PROP - pvarValue.contents.value = ret[0] - except Exception as e: # catch and log all exceptions so they are not swallowed by caller. - log.exception() - return S_OK - - def _onDestroyControl(self, evt): - evt.Skip() # Allow other handlers to process this event. - self._cleanup() - - def _cleanup(self): - # Import late to avoid circular import - from IAccessibleHandler import accPropServices - accPropServices.ClearHwndProps( - hwnd=self.hwnd, - idObject=winUser.OBJID_CLIENT, - idChild=0, - paProps=self.properties_GUIDPTR, - cProps=len(self.properties_GUIDPTR) - ) - -def convertToGUIDPointerList(propList): - return (GUID * len(propList))(*propList) diff --git a/source/gui/nvdaControls.py b/source/gui/nvdaControls.py index a0a08dcf326..b3ae5f682c2 100644 --- a/source/gui/nvdaControls.py +++ b/source/gui/nvdaControls.py @@ -9,7 +9,6 @@ import wx from comtypes import GUID from wx.lib.mixins import listctrl as listmix -from . import accPropServer from .dpiScalingHelper import DpiScalingHelperMixin from . import guiHelper import oleacc @@ -86,85 +85,35 @@ def OnSetFocus(self, evt): self.SetSelection(0, numChars) evt.Skip() -class AccPropertyOverride(accPropServer.IAccPropServer_Impl): - def __init__(self, control, propertyAnnotations): - """ - A simple class for overriding specific values on a control - :type propertyAnnotations: dict - """ - super(AccPropertyOverride, self).__init__( - control, - annotateProperties=list(propertyAnnotations.keys()), - annotateChildren=False - ) - self.propertyAnnotations = propertyAnnotations +class ListCtrlAccessible(wx.Accessible): + """WX Accessible implementation for checkable lists which aren't fully accessible.""" - def _getPropValue(self, pIDString, dwIDStringLen, idProp): - control = self.control() # self.control held as a weak ref, ensure it stays alive for the duration of this method - if control is None or not self.propertyAnnotations: - return None + def GetRole(self, childId): + if childId == winUser.CHILDID_SELF: + return super().GetRole(childId) + return (wx.ACC_OK, wx.ROLE_SYSTEM_CHECKBUTTON) - try: - val = self.propertyAnnotations[idProp] - if callable(val): - val = val() - return self._hasProp(val) - except KeyError: - pass - - return None - - def _cleanup(self): - # could contain references (via lambda) of our owner, set it to None to avoid a circular reference which - # would block destruction. - self.propertyAnnotations = None - super(AccPropertyOverride, self)._cleanup() - -class ListCtrlAccPropServer(accPropServer.IAccPropServer_Impl): - """AccPropServer for wx checkable lists which aren't fully accessible.""" - - def __init__(self, control): - super(ListCtrlAccPropServer, self).__init__( - control, - annotateProperties=[ - oleacc.PROPID_ACC_ROLE, # supposed to be checkbox, rather than list item - oleacc.PROPID_ACC_STATE # should include the checkable state and checked state if the item is checked. - ], - annotateChildren=True - ) + def GetState(self, childId): + if childId == winUser.CHILDID_SELF: + return super().GetState(childId) + states = wx.ACC_STATE_SYSTEM_SELECTABLE | wx.ACC_STATE_SYSTEM_FOCUSABLE + if self.Window.IsChecked(childId - 1): + states |= wx.ACC_STATE_SYSTEM_CHECKED + if self.Window.IsSelected(childId - 1): + # wx doesn't seem to have a method to check whether a list item is focused. + # Therefore, assume that a selected item is focused,which is the case in single select list boxes. + states |= wx.ACC_STATE_SYSTEM_SELECTED | wx.ACC_STATE_SYSTEM_FOCUSED + return (wx.ACC_OK, states) - def _getPropValue(self, pIDString: str, dwIDStringLen: int, idProp: GUID) -> Optional[Tuple[BOOL, Any]]: - control = self.control() # self.control held as a weak ref, ensure it stays alive for the duration of this method - if control is None: - return None - - # Import late to prevent circular import. - from IAccessibleHandler import accPropServices - handle, objid, childid = accPropServices.DecomposeHwndIdentityString(pIDString, dwIDStringLen) - if childid == winUser.CHILDID_SELF: - return None - - if idProp == oleacc.PROPID_ACC_ROLE: - return self._hasProp(oleacc.ROLE_SYSTEM_CHECKBUTTON) - - if idProp == oleacc.PROPID_ACC_STATE: - states = oleacc.STATE_SYSTEM_SELECTABLE|oleacc.STATE_SYSTEM_FOCUSABLE - if control.IsChecked(childid-1): - states |= oleacc.STATE_SYSTEM_CHECKED - if control.IsSelected(childid-1): - # wx doesn't seem to have a method to check whether a list item is focused. - # Therefore, assume that a selected item is focused,which is the case in single select list boxes. - states |= oleacc.STATE_SYSTEM_SELECTED | oleacc.STATE_SYSTEM_FOCUSED - return self._hasProp(states) class CustomCheckListBox(wx.CheckListBox): """Custom checkable list to fix a11y bugs in the standard wx checkable list box.""" def __init__(self, *args, **kwargs): super(CustomCheckListBox, self).__init__(*args, **kwargs) - # Register object with COM to fix accessibility bugs in wx. - self.server = ListCtrlAccPropServer(self) + # Register a custom wx.Accessible implementation to fix accessibility incompleties + self.SetAccessible(ListCtrlAccessible(self)) # Register ourself with ourself's selected event, so that we can notify winEvent of the state change. self.Bind(wx.EVT_CHECKLISTBOX, self.notifyIAccessible) @@ -189,8 +138,8 @@ def __init__(self, parent, id=wx.ID_ANY, autoSizeColumn="LAST", pos=wx.DefaultPo ): AutoWidthColumnListCtrl.__init__(self, parent, id=id, pos=pos, size=size, style=style, autoSizeColumn=autoSizeColumn) listmix.CheckListCtrlMixin.__init__(self, check_image, uncheck_image, imgsz) - # Register object with COM to fix accessibility bugs in wx. - self.server = ListCtrlAccPropServer(self) + # Register a custom wx.Accessible implementation to fix accessibility incompleties + self.SetAccessible(ListCtrlAccessible(self)) # Register our hook to check/uncheck items with space. # Use wx.EVT_CHAR_HOOK, because EVT_LIST_KEY_DOWN isn't triggered for space. self.Bind(wx.EVT_CHAR_HOOK, self.onCharHook) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 6bbb1fc7bd4..01db9055522 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -357,6 +357,22 @@ def _sendLayoutUpdatedEvent(self): event.SetEventObject(self) self.GetEventHandler().ProcessEvent(event) + +class SettingsPanelAccessible(wx.Accessible): + """ + WX Accessible implementation to set the role of a settings panel to property page, + as well as to set the accessible description based on the panel's description. + """ + + Window: SettingsPanel + + def GetRole(self, childId): + return (wx.ACC_OK, wx.ROLE_SYSTEM_PROPERTYPAGE) + + def GetDescription(self, childId): + return (wx.ACC_OK, self.Window.panelDescription) + + class MultiCategorySettingsDialog(SettingsDialog): """A settings dialog with multiple settings categories. A multi category settings dialog consists of a list view with settings categories on the left side, @@ -524,14 +540,7 @@ def _getCategoryPanel(self, catId): ).format(cls, panel.Size[0]) ) panel.SetLabel(panel.title) - import oleacc - panel.server = nvdaControls.AccPropertyOverride( - panel, - propertyAnnotations={ - oleacc.PROPID_ACC_ROLE: oleacc.ROLE_SYSTEM_PROPERTYPAGE, # change the role from pane to property page - oleacc.PROPID_ACC_DESCRIPTION: panel.panelDescription, # set a description - } - ) + panel.SetAccessible(SettingsPanelAccessible(panel)) return panel def postInit(self): diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index d4d9edcc55e..f1d6d595446 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -90,6 +90,7 @@ What's New in NVDA - `speech.re_last_pause` has been removed - please use `speech.SpeechWithoutPauses.re_last_pause` instead. (#12195) - `WelcomeDialog`, `LauncherDialog` and `AskAllowUsageStatsDialog` are moved to the `gui.startupDialogs`. (#12105) - `getDocFilePath` has been moved from `gui` to the `documentationUtils` module. (#12105) +- The gui.accPropServer module as well as the AccPropertyOverride and ListCtrlAccPropServer classes from the gui.nvdaControls module have been removed in favor of WX' native support for overriding accessibility properties. When enhancing accessibility of WX controls, implement wx.Accessible instead. (#12215) = 2020.4 = From 8412adbd2030de9ae87246fc1c5f5adf0301600a Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 25 Mar 2021 15:03:09 +0800 Subject: [PATCH 103/174] Make com interfaces ide friendly (PR #12201) * Make UIAutomationClient work for tools and runtime At runtime the import from comtypes.gen will work, as a fallback for IDE / Tools that can't find that, the relative import is used. Note: IDE's wont be aware of 'protected' symbols (beginning with underscore). Protected symbols aren't imported with the from x import * statment. * Auto fix generated files for IDE / tools. Generated "friendly name" comtypes.gen files are modified so that they can be used by tools / IDEs more easily. The files are read, the module name extracted, and the import statement replaced by a more elaborate version which include a fall back to a do a relative import if the comtypes.gen file is not found. --- source/_UIAHandler.py | 4 +- source/comInterfaces/UIAutomationClient.py | 6 +- source/comInterfaces/readme.md | 16 +++++ source/comInterfaces_sconscript | 80 ++++++++++++++++++---- user_docs/en/changes.t2t | 1 + 5 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 source/comInterfaces/readme.md diff --git a/source/_UIAHandler.py b/source/_UIAHandler.py index ab1c778f6f9..e8dcddfa89d 100644 --- a/source/_UIAHandler.py +++ b/source/_UIAHandler.py @@ -36,8 +36,8 @@ from logHandler import log import UIAUtils from comInterfaces import UIAutomationClient as UIA -# F403 unable to detect undefined names -from comInterfaces.UIAutomationClient import * # noqa: F403 +# F403: unable to detect undefined names +from comInterfaces .UIAutomationClient import * # noqa: F403 import textInfos from typing import Dict from queue import Queue diff --git a/source/comInterfaces/UIAutomationClient.py b/source/comInterfaces/UIAutomationClient.py index a24fbb96e65..2ccf200f4a8 100644 --- a/source/comInterfaces/UIAutomationClient.py +++ b/source/comInterfaces/UIAutomationClient.py @@ -1,3 +1,7 @@ -from comtypes.gen import _944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 +try: + from comtypes.gen import _944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 +except ModuleNotFoundError: + import _944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 + from _944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0 import * globals().update(_944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0.__dict__) __name__ = 'comtypes.gen.UIAutomationClient' \ No newline at end of file diff --git a/source/comInterfaces/readme.md b/source/comInterfaces/readme.md new file mode 100644 index 00000000000..b28d495ea72 --- /dev/null +++ b/source/comInterfaces/readme.md @@ -0,0 +1,16 @@ +The comInterfaces package is generated via SCons. +The logic for this is in `comInterfaces_sconscript`, which uses `comtypes.gen` to read `*.tlb` +files or via interface IDs. + +The interface files have an ID named file (a GUID, followed by a version number) as well as a +"friendly name" file. + +The "friendly name" file generated by comtypes is not consumed easily by tools and IDEs, +runtime logic is used to expose symbols. +To remedy this, the file is then processed by `comInterfaces_sconscript` to extract the module +name and replace the import statement with a more elaborate approach which includes a fallback +for the purposes of IDEs and tools. + +Only UIAutomation.py is not generated, UIA has historically been updated regularly and the version on +Appveyor build servers could not be guaranteed. + diff --git a/source/comInterfaces_sconscript b/source/comInterfaces_sconscript index 12e5ec73134..52ff137407d 100755 --- a/source/comInterfaces_sconscript +++ b/source/comInterfaces_sconscript @@ -11,6 +11,10 @@ #This license can be found at: #http://www.gnu.org/licenses/old-licenses/gpl-2.0.html ### +from typing import List +import fileinput +import re +from SCons.Node.FS import File Import( 'env', @@ -26,18 +30,49 @@ def new_my_import(fullname): return old_my_import(fullname) comtypes.client._generate._my_import = new_my_import -def interfaceAction(target,source,env): - clsid=env.get('clsid') +def makeIDEFriendly(path:str) -> None: + """ + Add a local import of * so that tools and IDE's can find definitions. + Prefer to import from comtypes.gen, at runtime behavior will not have changed. + @param path: Path to the friendly name comInterfaces module. + """ + importTemplate = ( +"""try: + from comtypes.gen import {libIdentifier} +except ModuleNotFoundError: + import {libIdentifier} + from {libIdentifier} import * +""") + + importPattern = re.compile(r"from comtypes\.gen import ([\w]+)\n") + for line in fileinput.input(path, inplace=True): + # Note: The fileinput module temporarily redirects stdout to the file. + # So print is used to write the file. + match = importPattern.match(line) + if match: + libId = match.group(1) + print(importTemplate.format(libIdentifier=libId), end='') + else: + print(line, end='') + +def interfaceAction(target:List[File], source, env): + clsid = env.get('clsid') if clsid: - source=(clsid,env['majorVersion'],env['minorVersion']) + source=(clsid, env['majorVersion'], env['minorVersion']) else: source=str(source[0]) comtypes.client.GetModule(source) + # re-write the the "friendlyNameFile" so that tools/IDEs can find the + # definitions + for t in target: + path: str = t.abspath + if path.endswith(".py"): + makeIDEFriendly(path) interfaceBuilder=env.Builder( action=env.Action(interfaceAction), ) -env['BUILDERS']['comtypesInterface']=interfaceBuilder +env['BUILDERS']['comtypesInterface'] = interfaceBuilder # Force comtypes generated interfaces in to our directory import comtypes.client @@ -47,21 +82,40 @@ COM_INTERFACES = { "IAccessible2Lib.py": "typelibs/ia2.tlb", "ISimpleDOM.py": "typelibs/ISimpleDOMNode.tlb", "mathPlayer.py": "typelibs/mathPlayerDLL.tlb", - #"Accessibility.py": ('{1EA4DBF0-3C3B-11CF-810C-00AA00389B71}',1,0), + "Accessibility.py": ('{1EA4DBF0-3C3B-11CF-810C-00AA00389B71}',1,0), "tom.py": ('{8CC497C9-A1DF-11CE-8098-00AA0047BE5D}',1,0), "SpeechLib.py": ('{C866CA3A-32F7-11D2-9602-00C04F8EE628}',5,0), "AcrobatAccessLib.py": "typelibs/AcrobatAccess.tlb", + # We don't generate UIAutomationClient (and _944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0.py) + # because we don't know what version is available on Appveyor. + # Instead we locally generate the file and include it in the repository. + # Ensuring that adjustments performed by 'makeIDEFriendly' are incorporated. + # "UIAutomationClient.py": ('{944DE083-8FB8-45CF-BCB7-C477ACB2F897}', 1, 0), } for k,v in COM_INTERFACES.items(): - targets=[Dir('comInterfaces').File(k), - # This buillds a .pyc file as well. - Dir('comInterfaces').File(importlib.util.cache_from_source(k))] - source=clsid=majorVersion=None + targets=[ + Dir('comInterfaces').File(k), + # This builds a .pyc file as well. + Dir('comInterfaces').File(importlib.util.cache_from_source(k)) + ] + source = clsid = majorVersion = None if isinstance(v, str): - env.comtypesInterface(targets,v) + env.comtypesInterface(targets, v) else: - env.comtypesInterface(targets,Dir('comInterfaces').File('__init__.py'),clsid=v[0],majorVersion=v[1],minorVersion=v[2]) + env.comtypesInterface( + targets, + Dir('comInterfaces').File('__init__.py'), + clsid=v[0], + majorVersion=v[1], + minorVersion=v[2] + ) + -#When cleaning comInterfaces get rid of everything except for things starting with __ (e.g. __init__.py) -env.Clean(Dir('comInterfaces'),Glob('comInterfaces/[!_]*')+Glob('comInterfaces/_[!_]*')) + +# When cleaning comInterfaces get rid of everything +# except for things starting with __ (e.g. __init__.py) +env.Clean( + Dir('comInterfaces'), + Glob('comInterfaces/[!_]*') + Glob('comInterfaces/_[!_]*') +) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index f1d6d595446..8b0f48b88d4 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -91,6 +91,7 @@ What's New in NVDA - `WelcomeDialog`, `LauncherDialog` and `AskAllowUsageStatsDialog` are moved to the `gui.startupDialogs`. (#12105) - `getDocFilePath` has been moved from `gui` to the `documentationUtils` module. (#12105) - The gui.accPropServer module as well as the AccPropertyOverride and ListCtrlAccPropServer classes from the gui.nvdaControls module have been removed in favor of WX' native support for overriding accessibility properties. When enhancing accessibility of WX controls, implement wx.Accessible instead. (#12215) +- Files in `source/comInterfaces/` are now more easily consumable by developer tools such as IDEs. (#12201) = 2020.4 = From 8665526b573affa35d9d89b0650972482e45dc4c Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Thu, 25 Mar 2021 19:05:36 +1100 Subject: [PATCH 104/174] Exit NVDA safely by closing all top level windows (#12183) Unreleased silent installs of NVDA were causing a crash at the end of the install process. it was found NVDA closes the wxApp by forcibly exiting the MainLoop. This is not recommended and the behaviour of closing the TopWindow is recommended. NVDA now will exit even with windows still open, the exit process now closes all NVDA windows and dialogs --- appveyor.yml | 6 ++++++ source/core.py | 4 ++-- source/gui/__init__.py | 33 ++++++++++++++++++++++----------- source/gui/installerGui.py | 4 ++-- source/gui/startupDialogs.py | 2 +- user_docs/en/changes.t2t | 1 + 6 files changed, 34 insertions(+), 16 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 7cfd77970ae..0072099bee0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -147,6 +147,12 @@ before_test: Add-AppveyorMessage "Unable to install NVDA prior to tests." } Push-AppveyorArtifact $installerLogFilePath + $crashDump = "$outputDir\nvda_crash.dmp" + if (Test-Path -Path $crashDump){ + Push-AppveyorArtifact $crashDump -FileName "nvda_install_crash.dmp" + Add-AppveyorMessage "Install process crashed" + $errorCode=1 + } if($errorCode -ne 0) { $host.SetShouldExit($errorCode) } test_script: diff --git a/source/core.py b/source/core.py index ca36a04dbf2..0bfc8dfcff8 100644 --- a/source/core.py +++ b/source/core.py @@ -111,9 +111,9 @@ def onResult(ID): def restart(disableAddons=False, debugLogging=False): """Restarts NVDA by starting a new copy.""" if globalVars.appArgs.launcher: - import wx + import gui globalVars.exitCode=3 - wx.GetApp().ExitMainLoop() + gui.safeAppExit() return import subprocess import winUser diff --git a/source/gui/__init__.py b/source/gui/__init__.py index 742e8869dcb..43b1c80b5d4 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -75,10 +75,6 @@ def __init__(self): self.Show() self.Hide() - def Destroy(self): - self.sysTrayIcon.Destroy() - super(MainFrame, self).Destroy() - def prePopup(self): """Prepare for a popup. This should be called before any dialog or menu which should pop up for the user. @@ -199,7 +195,7 @@ def onExitCommand(self, evt): d.Show() self.postPopup() else: - wx.GetApp().ExitMainLoop() + safeAppExit() def onNVDASettingsCommand(self,evt): self._popupSettingsDialog(NVDASettingsDialog) @@ -359,6 +355,25 @@ def onConfigProfilesCommand(self, evt): ProfilesDialog(gui.mainFrame).Show() self.postPopup() + +def safeAppExit(): + """ + Ensures the app is exited by all the top windows being destroyed + """ + + for window in wx.GetTopLevelWindows(): + if isinstance(window, wx.Dialog) and window.IsModal(): + log.info(f"ending modal {window} during exit process") + wx.CallAfter(window.EndModal, wx.ID_CLOSE_ALL) + if isinstance(window, MainFrame): + log.info(f"destroying main frame during exit process") + # the MainFrame has EVT_CLOSE bound to the ExitDialog + # which calls this function on exit, so destroy this window + wx.CallAfter(window.Destroy) + else: + log.info(f"closing window {window} during exit process") + wx.CallAfter(window.Close) + class SysTrayIcon(wx.adv.TaskBarIcon): def __init__(self, frame): @@ -527,10 +542,6 @@ def __init__(self, frame): self.Bind(wx.adv.EVT_TASKBAR_LEFT_DOWN, self.onActivate) self.Bind(wx.adv.EVT_TASKBAR_RIGHT_DOWN, self.onActivate) - def Destroy(self): - self.menu.Destroy() - super(SysTrayIcon, self).Destroy() - def onActivate(self, evt): mainFrame.prePopup() import appModules.nvda @@ -585,7 +596,7 @@ def terminate(): # This is called after the main loop exits because WM_QUIT exits the main loop # without destroying all objects correctly and we need to support WM_QUIT. # Therefore, any request to exit should exit the main loop. - wx.CallAfter(mainFrame.Destroy) + safeAppExit() # #4460: We need another iteration of the main loop # so that everything (especially the TaskBarIcon) is cleaned up properly. # ProcessPendingEvents doesn't seem to work, but MainLoop does. @@ -712,7 +723,7 @@ def onOk(self, evt): if action >= 2 and config.isAppX: action += 1 if action == 0: - wx.GetApp().ExitMainLoop() + safeAppExit() elif action == 1: queueHandler.queueFunction(queueHandler.eventQueue,core.restart) elif action == 2: diff --git a/source/gui/installerGui.py b/source/gui/installerGui.py index db9a9576284..36a1f701b8a 100644 --- a/source/gui/installerGui.py +++ b/source/gui/installerGui.py @@ -102,7 +102,7 @@ def doInstall( winUser.SW_SHOWNORMAL ) else: - wx.GetApp().ExitMainLoop() + gui.safeAppExit() def doSilentInstall( @@ -450,7 +450,7 @@ def doCreatePortable(portableDirectory,copyUserConfig=False,silent=False,startAf return d.done() if silent: - wx.GetApp().ExitMainLoop() + gui.safeAppExit() else: # Translators: The message displayed when a portable copy of NVDA has been successfully created. # %s will be replaced with the destination directory. diff --git a/source/gui/startupDialogs.py b/source/gui/startupDialogs.py index 8830a9c1ae9..d26f7a9367a 100644 --- a/source/gui/startupDialogs.py +++ b/source/gui/startupDialogs.py @@ -193,7 +193,7 @@ def onContinueRunning(self, evt): core.doStartupDialogs() def onExit(self, evt): - wx.GetApp().ExitMainLoop() + gui.safeAppExit() @classmethod def run(cls): diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 8b0f48b88d4..7afc23131c1 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -24,6 +24,7 @@ What's New in NVDA - Updated liblouis braille translator to [3.17.0 https://github.com/liblouis/liblouis/releases/tag/v3.17.0]. (#12137) - New braille tables: Belarusian literary braille, Belarusian computer braille, Urdu grade 1, Urdu grade 2. - Support for Adobe Flash content has been removed from NVDA due to the use of Flash being actively discouraged by Adobe. (#11131) +- NVDA will exit even with windows still open, the exit process now closes all NVDA windows and dialogs (#1740) == Bug Fixes == From ff79f51894718978ed8a81a86477a70eb4421d0d Mon Sep 17 00:00:00 2001 From: Joseph Lee Date: Thu, 25 Mar 2021 03:26:45 -0700 Subject: [PATCH 105/174] Replace isWin10 with more flexible Windows version checking (PR #11909) Convenience methods and types have been added to the winVersion module for getting and comparing Windows versions. - isWin10 function found in winVersion module has been removed. - class winVersion.WinVersion is a comparable and order-able type encapsulating Windows version information. - Function winVersion.getWinVer has been added to get a winVersion.WinVersion representing the currently running OS. - Convenience constants have been added for known Windows releases, see winVersion.WIN* constants. Closes #11795 Closes #11837 Closes #11933 Replaces #11796 Replaces #11799 --- source/COMRegistrationFixes/__init__.py | 3 +- source/NVDAObjects/IAccessible/winConsole.py | 4 +- source/NVDAObjects/UIA/__init__.py | 2 +- source/NVDAObjects/UIA/spartanEdge.py | 2 +- source/_UIAHandler.py | 2 +- source/appModuleHandler.py | 6 +- source/appModules/explorer.py | 11 +- source/appModules/putty.py | 4 +- ...bleshell_experiences_textinput_inputapp.py | 26 ++- source/core.py | 2 +- source/easeOfAccess.py | 6 +- source/gui/settingsDialogs.py | 2 +- source/keyboardHandler.py | 2 +- source/synthDriverHandler.py | 2 +- source/synthDrivers/oneCore.py | 2 +- source/touchHandler.py | 2 +- source/updateCheck.py | 13 +- source/winVersion.py | 165 +++++++++++++----- tests/unit/test_winVersion.py | 37 ++++ user_docs/en/changes.t2t | 5 + 20 files changed, 218 insertions(+), 80 deletions(-) create mode 100644 tests/unit/test_winVersion.py diff --git a/source/COMRegistrationFixes/__init__.py b/source/COMRegistrationFixes/__init__.py index 968be869653..dbf5d494ba9 100644 --- a/source/COMRegistrationFixes/__init__.py +++ b/source/COMRegistrationFixes/__init__.py @@ -60,7 +60,8 @@ def fixCOMRegistrations(): Registers most common COM proxies, in case they had accidentally been unregistered or overwritten by 3rd party software installs/uninstalls. """ is64bit=os.environ.get("PROCESSOR_ARCHITEW6432","").endswith('64') - OSMajorMinor=winVersion.winVersion[:2] + winVer = winVersion.getWinVer() + OSMajorMinor = (winVer.major, winVer.minor) log.debug("Fixing COM registration for Windows %s.%s, %s"%(OSMajorMinor[0],OSMajorMinor[1],"64 bit" if is64bit else "32 bit")) # Commands taken from NVDA issue #2807 comment https://github.com/nvaccess/nvda/issues/2807#issuecomment-320149243 # OLEACC (MSAA) proxies diff --git a/source/NVDAObjects/IAccessible/winConsole.py b/source/NVDAObjects/IAccessible/winConsole.py index d2c9343ce3e..9eb85cc8b29 100644 --- a/source/NVDAObjects/IAccessible/winConsole.py +++ b/source/NVDAObjects/IAccessible/winConsole.py @@ -6,7 +6,7 @@ import config from NVDAObjects.behaviors import KeyboardHandlerBasedTypedCharSupport -from winVersion import isWin10 +from winVersion import getWinVer, WIN10_1607 from . import IAccessible from ..window import winConsole @@ -39,7 +39,7 @@ def _get_diffAlgo(self): def findExtraOverlayClasses(obj, clsList): - if isWin10(1607) and config.conf['terminals']['keyboardSupportInLegacy']: + if getWinVer() >= WIN10_1607 and config.conf['terminals']['keyboardSupportInLegacy']: clsList.append(EnhancedLegacyWinConsole) else: clsList.append(LegacyWinConsole) diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index da85cfa5bed..fe72d201230 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -335,7 +335,7 @@ def __init__(self,obj,position,_rangeObj=None): # sometimes rangeFromChild can return a NULL range if not self._rangeObj: raise LookupError elif isinstance(position,locationHelper.Point): - if (winVersion.winVersion.major, winVersion.winVersion.minor) == (6, 1): + if winVersion.getWinVer() <= winVersion.WIN7_SP1: # #9435: RangeFromPoint causes a freeze in UIA client library in the Windows 7 start menu! raise NotImplementedError("RangeFromPoint not supported on Windows 7") self._rangeObj=self.obj.UIATextPattern.RangeFromPoint(position.toPOINT()) diff --git a/source/NVDAObjects/UIA/spartanEdge.py b/source/NVDAObjects/UIA/spartanEdge.py index 7886d922c6e..76020b29b7c 100644 --- a/source/NVDAObjects/UIA/spartanEdge.py +++ b/source/NVDAObjects/UIA/spartanEdge.py @@ -324,7 +324,7 @@ def _getTextWithFieldsForUIARange( # noqa: C901 class EdgeNode(web.UIAWeb): - _edgeIsPreGapRemoval = winVersion.winVersion.build < 15048 + _edgeIsPreGapRemoval = winVersion.getWinVer().build < 15048 _TextInfo = EdgeTextInfo_preGapRemoval if _edgeIsPreGapRemoval else EdgeTextInfo diff --git a/source/_UIAHandler.py b/source/_UIAHandler.py index e8dcddfa89d..8344a84cc09 100644 --- a/source/_UIAHandler.py +++ b/source/_UIAHandler.py @@ -195,7 +195,7 @@ localEventHandlerGroupUIAEventIds = set() autoSelectDetectionAvailable = False -if winVersion.isWin10(): +if winVersion.getWinVer() >= winVersion.WIN10: UIAEventIdsToNVDAEventNames.update({ UIA.UIA_Text_TextChangedEventId: "textChange", UIA.UIA_Text_TextSelectionChangedEventId: "caret", diff --git a/source/appModuleHandler.py b/source/appModuleHandler.py index 0d17bfea131..463d87bb190 100644 --- a/source/appModuleHandler.py +++ b/source/appModuleHandler.py @@ -389,9 +389,7 @@ def _setProductInfo(self): if not self.processHandle: raise RuntimeError("processHandle is 0") # No need to worry about immersive (hosted) apps and friends until Windows 8. - # Python 3.7 introduces platform_version to sys.getwindowsversion tuple, - # which returns major, minor, build. - if winVersion.winVersion.platform_version >= (6, 2, 9200): + if winVersion.getWinVer() >= winVersion.WIN8: # Some apps such as File Explorer says it is an immersive process but error 15700 is shown. # Therefore resort to file version info behavior because it is not a hosted app. # Others such as Store version of Office are not truly hosted apps, @@ -501,7 +499,7 @@ def _get_isWindowsStoreApp(self): e.g. File Explorer reports itself as immersive when it is not. @rtype: bool """ - if winVersion.winVersion.platform_version < (6, 2, 9200): + if winVersion.getWinVer() < winVersion.WIN8: # Windows Store/UWP apps were introduced in Windows 8. self.isWindowsStoreApp = False return False diff --git a/source/appModules/explorer.py b/source/appModules/explorer.py index f155ccce45b..20e025c4c34 100644 --- a/source/appModules/explorer.py +++ b/source/appModules/explorer.py @@ -242,7 +242,7 @@ class MetadataEditField(RichEdit50): but to avoid Windows Explorer crashes we need to use EditTextInfo here. """ @classmethod def _get_TextInfo(cls): - if ((winVersion.winVersion.major, winVersion.winVersion.minor) == (6, 1)): + if winVersion.getWinVer() <= winVersion.WIN7_SP1: cls.TextInfo = EditTextInfo else: cls.TextInfo = super().TextInfo @@ -255,8 +255,8 @@ def event_gainFocus(self): # as it causes 'pane" to be announced when minimizing windows or moving to desktop. # However when closing Windows 7 Start Menu in some cases # focus lands on it instead of the focused desktop item. - # Simply ignore the event if running on anything never than Win 7. - if ((winVersion.winVersion.major, winVersion.winVersion.minor) != (6, 1)): + # Simply ignore the event if running on anything other than Win 7. + if winVersion.getWinVer() > winVersion.WIN7_SP1: return if eventHandler.isPendingEvents("gainFocus"): return @@ -416,7 +416,10 @@ def event_gainFocus(self, obj, nextHandler): def isGoodUIAWindow(self, hwnd): # #9204: shell raises window open event for emoji panel in build 18305 and later. - if winVersion.isWin10(version=1903) and winUser.getClassName(hwnd) == "ApplicationFrameWindow": + if ( + winVersion.getWinVer() >= winVersion.WIN10_1903 + and winUser.getClassName(hwnd) == "ApplicationFrameWindow" + ): return True return False diff --git a/source/appModules/putty.py b/source/appModules/putty.py index cd116342d28..60907e0a831 100644 --- a/source/appModules/putty.py +++ b/source/appModules/putty.py @@ -12,7 +12,7 @@ from NVDAObjects.window import DisplayModelEditableText, DisplayModelLiveText import appModuleHandler from NVDAObjects.IAccessible import IAccessible -from winVersion import isWin10 +from winVersion import getWinVer, WIN10_1607 class AppModule(appModuleHandler.AppModule): # Allow this to be overridden for derived applications. @@ -24,7 +24,7 @@ def chooseNVDAObjectOverlayClasses(self, obj, clsList): clsList.remove(DisplayModelEditableText) except ValueError: pass - if isWin10(1607): + if getWinVer() >= WIN10_1607: clsList[0:0] = (KeyboardHandlerBasedTypedCharSupport, DisplayModelLiveText) else: clsList[0:0] = (Terminal, DisplayModelLiveText) diff --git a/source/appModules/windowsinternal_composableshell_experiences_textinput_inputapp.py b/source/appModules/windowsinternal_composableshell_experiences_textinput_inputapp.py index ab73a7db1f8..04c179d1215 100644 --- a/source/appModules/windowsinternal_composableshell_experiences_textinput_inputapp.py +++ b/source/appModules/windowsinternal_composableshell_experiences_textinput_inputapp.py @@ -1,8 +1,7 @@ -# App module for Composable Shell (CShell) input panel -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2017-2018 NV Access Limited, Joseph Lee -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2017-2021 NV Access Limited, Joseph Lee +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. """App module for Windows 10 Modern Keyboard aka new touch keyboard panel. The chief feature is allowing NVDA to announce selected emoji when using the keyboard to search for and select one. @@ -173,9 +172,17 @@ def event_UIA_window_windowOpen(self, obj, nextHandler): return # #9104: different aspects of modern input panel are represented by automation iD's. childAutomationID = obj.firstChild.UIAElement.cachedAutomationID - # Emoji panel for build 16299 and 17134. + # Emoji panel for 1709 (build 16299) and 1803 (17134). + emojiPanelInitial = winVersion.WIN10_1709 # This event is properly raised in build 17134. - if winVersion.winVersion.build <= 17134 and childAutomationID in ("TEMPLATE_PART_ExpressiveInputFullViewFuntionBarItemControl", "TEMPLATE_PART_ExpressiveInputFullViewFuntionBarCloseButton"): + emojiPanelWindowOpenEvent = winVersion.WIN10_1803 + if ( + emojiPanelInitial <= winVersion.getWinVer() <= emojiPanelWindowOpenEvent + and childAutomationID in ( + "TEMPLATE_PART_ExpressiveInputFullViewFuntionBarItemControl", + "TEMPLATE_PART_ExpressiveInputFullViewFuntionBarCloseButton" + ) + ): self.event_UIA_elementSelected(obj.lastChild.firstChild, nextHandler) # Handle hardware keyboard suggestions. # Treat it the same as CJK composition list - don't announce this if candidate announcement setting is off. @@ -221,8 +228,9 @@ def event_nameChange(self, obj, nextHandler): or (self._recentlySelected is not None and self._recentlySelected in obj.name)): return # The word "blank" is kept announced, so suppress this on build 17666 and later. - if winVersion.winVersion.build > 17134: - # In build 17672 and later, return immediatley when element selected event on clipboard item was fired just prior to this. + if winVersion.getWinVer().build > 17134: + # In build 17672 and later, + # return immediately when element selected event on clipboard item was fired just prior to this. # In some cases, parent will be None, as seen when emoji panel is closed in build 18267. try: if obj.UIAElement.cachedAutomationID == "TEMPLATE_PART_ClipboardItemIndex" or obj.parent.UIAElement.cachedAutomationID == "TEMPLATE_PART_ClipboardItemsList": return diff --git a/source/core.py b/source/core.py index 0bfc8dfcff8..b61d6b4b3a0 100644 --- a/source/core.py +++ b/source/core.py @@ -276,7 +276,7 @@ def main(): languageHandler.setLanguage(lang) except: log.warning("Could not set language to %s"%lang) - log.info("Using Windows version %s" % winVersion.winVersionText) + log.info(f"Windows version: {winVersion.getWinVer()}") log.info("Using Python version %s"%sys.version) log.info("Using comtypes version %s"%comtypes.__version__) import configobj diff --git a/source/easeOfAccess.py b/source/easeOfAccess.py index ba4729c5271..33cfd973600 100644 --- a/source/easeOfAccess.py +++ b/source/easeOfAccess.py @@ -10,12 +10,12 @@ import winreg import ctypes import winUser -from winVersion import winVersion +import winVersion # Windows >= Vista -isSupported = winVersion.major >= 6 +isSupported = winVersion.getWinVer().major >= 6 # Windows >= 8 -canConfigTerminateOnDesktopSwitch = isSupported and (winVersion.major, winVersion.minor) >= (6, 2) +canConfigTerminateOnDesktopSwitch = isSupported and winVersion.getWinVer() >= winVersion.WIN8 ROOT_KEY = r"Software\Microsoft\Windows NT\CurrentVersion\Accessibility" APP_KEY_NAME = "nvda_nvda_v1" diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 01db9055522..0c3caca86eb 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2636,7 +2636,7 @@ def __init__(self, parent): self.bindHelpEvent("AdvancedSettingsKeyboardSupportInLegacy", self.keyboardSupportInLegacyCheckBox) self.keyboardSupportInLegacyCheckBox.SetValue(config.conf["terminals"]["keyboardSupportInLegacy"]) self.keyboardSupportInLegacyCheckBox.defaultValue = self._getDefaultValue(["terminals", "keyboardSupportInLegacy"]) - self.keyboardSupportInLegacyCheckBox.Enable(winVersion.isWin10(1607)) + self.keyboardSupportInLegacyCheckBox.Enable(winVersion.getWinVer() >= winVersion.WIN10_1607) # Translators: This is the label for a combo box for selecting a # method of detecting changed content in terminals in the advanced diff --git a/source/keyboardHandler.py b/source/keyboardHandler.py index 5d685d32fab..89de9ae0348 100644 --- a/source/keyboardHandler.py +++ b/source/keyboardHandler.py @@ -107,7 +107,7 @@ def shouldUseToUnicodeEx(focus=None): from NVDAObjects.behaviors import KeyboardHandlerBasedTypedCharSupport return ( # This is only possible in Windows 10 1607 and above - winVersion.isWin10(1607) + winVersion.getWinVer() >= winVersion.WIN10_1607 and ( # Either of # We couldn't inject in-process, and its not a legacy console window without keyboard support. # console windows have their own specific typed character support. diff --git a/source/synthDriverHandler.py b/source/synthDriverHandler.py index 69555908325..cf8f80023e6 100644 --- a/source/synthDriverHandler.py +++ b/source/synthDriverHandler.py @@ -427,7 +427,7 @@ def getSynthInstance(name): # The synthDrivers that should be used by default. # The first that successfully initializes will be used when config is set to auto (I.e. new installs of NVDA). defaultSynthPriorityList = ['espeak', 'silence'] -if winVersion.winVersion.major >= 10: +if winVersion.getWinVer() >= winVersion.WIN10: # Default to OneCore on Windows 10 and above defaultSynthPriorityList.insert(0, 'oneCore') diff --git a/source/synthDrivers/oneCore.py b/source/synthDrivers/oneCore.py index e9a9727f126..077123029c1 100644 --- a/source/synthDrivers/oneCore.py +++ b/source/synthDrivers/oneCore.py @@ -130,7 +130,7 @@ class SynthDriver(SynthDriver): @classmethod def check(cls): # Only present this as an available synth if this is Windows 10. - return winVersion.isWin10() + return winVersion.getWinVer() >= winVersion.WIN10 def _get_supportsProsodyOptions(self): self.supportsProsodyOptions = self._dll.ocSpeech_supportsProsodyOptions() diff --git a/source/touchHandler.py b/source/touchHandler.py index f3bb6eeb5f3..74cf897a760 100644 --- a/source/touchHandler.py +++ b/source/touchHandler.py @@ -302,7 +302,7 @@ def touchSupported(debugLog: bool = False): if debugLog: log.debugWarning("Touch only supported on installed copies") return False - if winVersion.winVersion.platform_version < (6, 2, 9200): + if winVersion.getWinVer() < winVersion.WIN8: if debugLog: log.debugWarning("Touch only supported on Windows 8 and higher") return False diff --git a/source/updateCheck.py b/source/updateCheck.py index 0fd12aea8bf..d3bb39d3dda 100644 --- a/source/updateCheck.py +++ b/source/updateCheck.py @@ -22,7 +22,7 @@ # Avoid a E402 'module level import not at top of file' warning, because several checks are performed above. import gui.contextHelp # noqa: E402 from gui.dpiScalingHelper import DpiScalingHelperMixin, DpiScalingHelperMixinWithoutInit # noqa: E402 -import winVersion +import sys # noqa: E402 import os import inspect import threading @@ -105,12 +105,21 @@ def checkForUpdate(auto=False): @raise RuntimeError: If there is an error checking for an update. """ allowUsageStats=config.conf["update"]['allowUsageStats'] + # #11837: build version string, service pack, and product type manually + # because winVersion.getWinVer adds Windows release name. + winVersion = sys.getwindowsversion() + winVersionText = "{v.major}.{v.minor}.{v.build}".format(v=winVersion) + if winVersion.service_pack_major != 0: + winVersionText += " service pack %d" % winVersion.service_pack_major + if winVersion.service_pack_minor != 0: + winVersionText += ".%d" % winVersion.service_pack_minor + winVersionText += " %s" % ("workstation", "domain controller", "server")[winVersion.product_type - 1] params = { "autoCheck": auto, "allowUsageStats":allowUsageStats, "version": versionInfo.version, "versionType": versionInfo.updateVersionType, - "osVersion": winVersion.winVersionText, + "osVersion": winVersionText, "x64": os.environ.get("PROCESSOR_ARCHITEW6432") == "AMD64", } if auto and allowUsageStats: diff --git a/source/winVersion.py b/source/winVersion.py index 6a37aa9ecef..05a11efe2cb 100644 --- a/source/winVersion.py +++ b/source/winVersion.py @@ -1,65 +1,142 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2020 NV Access Limited +# Copyright (C) 2006-2021 NV Access Limited, Bill Dengler, Joseph Lee # This file is covered by the GNU General Public License. # See the file COPYING for more details. +"""A module used to record Windows versions. +It is also used to define feature checks such as +making sure NVDA can run on a minimum supported version of Windows. +""" + import sys import os -import winUser +import functools + + +@functools.total_ordering +class WinVersion(object): + """ + Represents a Windows release. + Includes version major, minor, build, service pack information, + as well as tools such as checking for specific Windows 10 releases. + """ + + def __init__( + self, + major: int = 0, + minor: int = 0, + build: int = 0, + servicePack: str = "", + productType: str = "" + ): + self.major = major + self.minor = minor + self.build = build + self.servicePack = servicePack + self.productType = productType + + def _windowsVersionToReleaseName(self): + """Returns release names for a given Windows version. + For example, 6.1 will return 'Windows 7'. + For Windows 10, feature update release name will be included. + On server systems, client release names will be returned. + For example, 'Windows 10 1809' will be returned on Server 2019 systems. + """ + if (self.major, self.minor) == (6, 1): + return "Windows 7" + elif (self.major, self.minor) == (6, 2): + return "Windows 8" + elif (self.major, self.minor) == (6, 3): + return "Windows 8.1" + elif self.major == 10: + buildsToReleases = {build: release for release, build in WIN10_RELEASE_NAME_TO_BUILDS.items()} + if self.build in buildsToReleases: + return f"Windows 10 {buildsToReleases[self.build]}" + else: + # Windows Insider build. + return "Windows 10 prerelease" + else: + raise RuntimeError("Unknown Windows release") + + def __repr__(self): + winVersionText = [self._windowsVersionToReleaseName()] + winVersionText.append(f"({self.major}.{self.minor}.{self.build})") + if self.servicePack != "": + winVersionText.append(f"service pack {self.servicePack}") + if self.productType != "": + winVersionText.append(self.productType) + return " ".join(winVersionText) + + def __eq__(self, other): + return ( + (self.major, self.minor, self.build) + == (other.major, other.minor, other.build) + ) + + def __ge__(self, other): + return ( + (self.major, self.minor, self.build) + >= (other.major, other.minor, other.build) + ) + + +# Windows releases to WinVersion instances for easing comparisons. +WIN7 = WinVersion(major=6, minor=1, build=7600) +WIN7_SP1 = WinVersion(major=6, minor=1, build=7601, servicePack="1") +WIN8 = WinVersion(major=6, minor=2, build=9200) +WIN81 = WinVersion(major=6, minor=3, build=9600) +WIN10 = WIN10_1507 = WinVersion(major=10, minor=0, build=10240) +WIN10_1511 = WinVersion(major=10, minor=0, build=10586) +WIN10_1607 = WinVersion(major=10, minor=0, build=14393) +WIN10_1703 = WinVersion(major=10, minor=0, build=15063) +WIN10_1709 = WinVersion(major=10, minor=0, build=16299) +WIN10_1803 = WinVersion(major=10, minor=0, build=17134) +WIN10_1809 = WinVersion(major=10, minor=0, build=17763) +WIN10_1903 = WinVersion(major=10, minor=0, build=18362) +WIN10_1909 = WinVersion(major=10, minor=0, build=18363) +WIN10_2004 = WinVersion(major=10, minor=0, build=19041) +WIN10_20H2 = WinVersion(major=10, minor=0, build=19042) + + +def getWinVer(): + """Returns a record of current Windows version NVDA is running on. + """ + winVer = sys.getwindowsversion() + return WinVersion( + major=winVer.major, + minor=winVer.minor, + build=winVer.build, + servicePack=winVer.service_pack, + productType=("workstation", "domain controller", "server")[winVer.product_type - 1] + ) -winVersion=sys.getwindowsversion() -winVersionText="{v.major}.{v.minor}.{v.build}".format(v=winVersion) -if winVersion.service_pack_major!=0: - winVersionText+=" service pack %d"%winVersion.service_pack_major - if winVersion.service_pack_minor!=0: - winVersionText+=".%d"%winVersion.service_pack_minor -winVersionText+=" %s" % ("workstation","domain controller","server")[winVersion.product_type-1] def isSupportedOS(): # NVDA can only run on Windows 7 Service pack 1 and above - return (winVersion.major,winVersion.minor,winVersion.service_pack_major) >= (6,1,1) + return getWinVer() >= WIN7_SP1 -def canRunVc2010Builds(): - return isSupportedOS() UWP_OCR_DATA_PATH = os.path.expandvars(r"$windir\OCR") + + def isUwpOcrAvailable(): return os.path.isdir(UWP_OCR_DATA_PATH) -WIN10_VERSIONS_TO_BUILDS = { - 1507: 10240, - 1511: 10586, - 1607: 14393, - 1703: 15063, - 1709: 16299, - 1803: 17134, - 1809: 17763, - 1903: 18362, - 1909: 18363, - 2004: 19041, - 2009: 19042, +WIN10_RELEASE_NAME_TO_BUILDS = { + "1507": 10240, + "1511": 10586, + "1607": 14393, + "1703": 15063, + "1709": 16299, + "1803": 17134, + "1809": 17763, + "1903": 18362, + "1909": 18363, + "2004": 19041, + "20H2": 19042, } -def isWin10(version: int = 1507, atLeast: bool = True): - """ - Returns True if NVDA is running on the supplied release version of Windows 10. If no argument is supplied, returns True for all public Windows 10 releases. - @param version: a release version of Windows 10 (such as 1903). - @param atLeast: return True if NVDA is running on at least this Windows 10 build (i.e. this version or higher). - """ - if winVersion.major != 10: - return False - try: - if atLeast: - return winVersion.build >= WIN10_VERSIONS_TO_BUILDS[version] - else: - return winVersion.build == WIN10_VERSIONS_TO_BUILDS[version] - except KeyError: - from logHandler import log - log.error("Unknown Windows 10 version {}".format(version)) - return False - - def isFullScreenMagnificationAvailable(): - return (winVersion.major, winVersion.minor) >= (6, 2) + return getWinVer() >= WIN8 diff --git a/tests/unit/test_winVersion.py b/tests/unit/test_winVersion.py new file mode 100644 index 00000000000..78bf0e22330 --- /dev/null +++ b/tests/unit/test_winVersion.py @@ -0,0 +1,37 @@ +# 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) 2021 NV Access Limited, Joseph Lee + +"""Unit tests for the Windows version module.""" + +import unittest +import sys +import winVersion + + +class TestWinVersion(unittest.TestCase): + + def test_getWinVer(self): + # Test a 3-tuple consisting of version major, minor, build. + # sys.getwindowsversion() internally returns a named tuple, so comparing tuples is possible. + currentWinVer = winVersion.getWinVer() + winVerPython = sys.getwindowsversion() + self.assertTupleEqual( + (currentWinVer.major, currentWinVer.minor, currentWinVer.build), + winVerPython[:3] + ) + + def test_getWinVerFromNonExistentRelease(self): + # Test the fact that there is no Windows 10 2003 (2004 exists, however). + with self.assertRaises(AttributeError): + # Flake8 F841: local variable name is assigned to but never used + may2020Update = winVersion.WIN10_2003 # NOQA: F841 + + def test_moreRecentWinVer(self): + # Specifically to test operators. + minimumWinVer = winVersion.WIN7_SP1 + audioDuckingAvailable = winVersion.WIN8 + self.assertGreaterEqual( + audioDuckingAvailable, minimumWinVer + ) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 7afc23131c1..1e87ddcee54 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -93,6 +93,11 @@ What's New in NVDA - `getDocFilePath` has been moved from `gui` to the `documentationUtils` module. (#12105) - The gui.accPropServer module as well as the AccPropertyOverride and ListCtrlAccPropServer classes from the gui.nvdaControls module have been removed in favor of WX' native support for overriding accessibility properties. When enhancing accessibility of WX controls, implement wx.Accessible instead. (#12215) - Files in `source/comInterfaces/` are now more easily consumable by developer tools such as IDEs. (#12201) +- Convenience methods and types have been added to the winVersion module for getting and comparing Windows versions. (#11909) + - isWin10 function found in winVersion module has been removed. + - class winVersion.WinVersion is a comparable and order-able type encapsulating Windows version information. + - Function winVersion.getWinVer has been added to get a winVersion.WinVersion representing the currently running OS. + - Convenience constants have been added for known Windows releases, see winVersion.WIN* constants. = 2020.4 = From d90b24e7bb3e0e61814c24f4b163afa85beac3c7 Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Thu, 25 Mar 2021 19:31:08 +0100 Subject: [PATCH 106/174] Restore advanced settings layout. (#12223) The new option introduced by #12210 was not located in the "Microsoft UI Automation" option group as it should. Probably due to missed changes during upmerge of master. It is now Put in this group as intended by using The static box of the sizer as parent for the checkbox rather than the whole panel. --- source/gui/settingsDialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 0c3caca86eb..37c306f0ce7 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2578,7 +2578,7 @@ def __init__(self, parent): # Translators: This is the label for a checkbox in the # Advanced settings panel. label = _("Use UI Automation to access Microsoft &Excel spreadsheet controls when available") - self.UIAInMSExcelCheckBox = UIAGroup.addItem(wx.CheckBox(self, label=label)) + self.UIAInMSExcelCheckBox = UIAGroup.addItem(wx.CheckBox(UIABox, label=label)) self.bindHelpEvent("UseUiaForExcel", self.UIAInMSExcelCheckBox) self.UIAInMSExcelCheckBox.SetValue(config.conf["UIA"]["useInMSExcelWhenAvailable"]) self.UIAInMSExcelCheckBox.defaultValue = self._getDefaultValue(["UIA", "useInMSExcelWhenAvailable"]) From 6a0839d96d90a7ed29b393efc5fc5e431202edc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Fri, 26 Mar 2021 01:35:58 +0100 Subject: [PATCH 107/174] Remove backwards compat code from IAccessibleHandler (#12232) removes code marked as deprecated in #10934 PR #10934 refactored IAccessibleHandler into a package. This necessitated keeping some unused imports but marking them as deprecated. Also various parts of NVDA relied on the fact that IAccessibleHandler star imported all variables from IAccessible and IAccessible2 COM interfaces. Unused imports are removed from IAccessibleHandler NVDA's source has been modified to use IAccessible2 names from the COM interface rather than from IAccessibleHandler. --- source/IAccessibleHandler/__init__.py | 240 ++++++------------ .../internalWinEventHandler.py | 15 +- source/NVDAObjects/IAccessible/__init__.py | 84 +++--- source/NVDAObjects/IAccessible/chromium.py | 1 - .../NVDAObjects/IAccessible/ia2TextMozilla.py | 6 +- source/NVDAObjects/IAccessible/ia2Web.py | 16 +- source/NVDAObjects/IAccessible/mozilla.py | 7 +- source/NVDAObjects/window/edit.py | 1 - source/NVDAObjects/window/scintilla.py | 1 - source/appModules/kindle.py | 23 +- source/appModules/soffice.py | 3 +- source/virtualBuffers/gecko_ia2.py | 60 +++-- user_docs/en/changes.t2t | 1 + 13 files changed, 208 insertions(+), 250 deletions(-) diff --git a/source/IAccessibleHandler/__init__.py b/source/IAccessibleHandler/__init__.py index beba0bab96d..9a32972a1bd 100644 --- a/source/IAccessibleHandler/__init__.py +++ b/source/IAccessibleHandler/__init__.py @@ -1,4 +1,3 @@ -# IAccessibleHandler.py # A part of NonVisual Desktop Access (NVDA) # Copyright (C) 2006-2007 NVDA Contributors # This file is covered by the GNU General Public License. @@ -7,8 +6,6 @@ from typing import Tuple import struct import weakref -# Kept for backwards compatibility -from ctypes import * # noqa: F401, F403 from ctypes import ( wintypes, windll, @@ -26,80 +23,9 @@ import oleacc import UIAHandler -# Kept for backwards compatibility -from comInterfaces.Accessibility import * # noqa: F401, F403 -# Specific imports for items we know we use, hopefully in the future we can remove the import for this module. -from comInterfaces.Accessibility import ( - IAccessible, - IAccIdentity, - CAccPropServices, -) -# Kept for backwards compatibility -from comInterfaces.IAccessible2Lib import * # noqa: F401, F403 -# Specific imports for items we know we use, hopefully in the future we can remove the import for this module. -from comInterfaces.IAccessible2Lib import ( - IAccessibleText, - IAccessibleHypertext, - IAccessible2, - IA2_STATE_REQUIRED, - IA2_STATE_INVALID_ENTRY, - IA2_STATE_MODAL, - IA2_STATE_DEFUNCT, - IA2_STATE_SUPPORTS_AUTOCOMPLETION, - IA2_STATE_MULTI_LINE, - IA2_STATE_ICONIFIED, - IA2_STATE_EDITABLE, - IA2_STATE_PINNED, - IA2_STATE_CHECKABLE, - IA2_ROLE_UNKNOWN, - IA2_ROLE_CANVAS, - IA2_ROLE_CAPTION, - IA2_ROLE_CHECK_MENU_ITEM, - IA2_ROLE_COLOR_CHOOSER, - IA2_ROLE_DATE_EDITOR, - IA2_ROLE_DIRECTORY_PANE, - IA2_ROLE_DESKTOP_PANE, - IA2_ROLE_EDITBAR, - IA2_ROLE_EMBEDDED_OBJECT, - IA2_ROLE_ENDNOTE, - IA2_ROLE_FILE_CHOOSER, - IA2_ROLE_FONT_CHOOSER, - IA2_ROLE_FRAME, - IA2_ROLE_FOOTNOTE, - IA2_ROLE_FORM, - IA2_ROLE_GLASS_PANE, - IA2_ROLE_HEADER, - IA2_ROLE_HEADING, - IA2_ROLE_ICON, - IA2_ROLE_IMAGE_MAP, - IA2_ROLE_INPUT_METHOD_WINDOW, - IA2_ROLE_INTERNAL_FRAME, - IA2_ROLE_LABEL, - IA2_ROLE_LAYERED_PANE, - IA2_ROLE_NOTE, - IA2_ROLE_OPTION_PANE, - IA2_ROLE_PAGE, - IA2_ROLE_PARAGRAPH, - IA2_ROLE_RADIO_MENU_ITEM, - IA2_ROLE_REDUNDANT_OBJECT, - IA2_ROLE_ROOT_PANE, - IA2_ROLE_RULER, - IA2_ROLE_SCROLL_PANE, - IA2_ROLE_SECTION, - IA2_ROLE_SHAPE, - IA2_ROLE_SPLIT_PANE, - IA2_ROLE_TEAR_OFF_MENU, - IA2_ROLE_TERMINAL, - IA2_ROLE_TEXT_FRAME, - IA2_ROLE_TOGGLE_BUTTON, - IA2_ROLE_VIEW_PORT, - IA2_ROLE_CONTENT_DELETION, - IA2_ROLE_CONTENT_INSERTION, - IA2_ROLE_BLOCK_QUOTE, - IA2_ROLE_DESKTOP_ICON, - IA2_ROLE_FOOTER, - IA2_ROLE_MARK, -) +from comInterfaces import Accessibility as IA + +from comInterfaces import IAccessible2Lib as IA2 import config @@ -170,15 +96,7 @@ def isMSAADebugLoggingEnabled(): ] from . import internalWinEventHandler -# Imported for backwards compat -from .internalWinEventHandler import ( # noqa: F401 - winEventHookIDs, - winEventLimiter, - winEventIDsToNVDAEventNames, - _shouldGetEvents, -) -from comInterfaces import IAccessible2Lib as IA2 from logHandler import log import JABHandler import eventHandler @@ -276,55 +194,55 @@ def isMSAADebugLoggingEnabled(): oleacc.ROLE_SYSTEM_OUTLINEBUTTON: controlTypes.ROLE_TREEVIEWBUTTON, oleacc.ROLE_SYSTEM_CLOCK: controlTypes.ROLE_CLOCK, # IAccessible2 roles - IA2_ROLE_UNKNOWN: controlTypes.ROLE_UNKNOWN, - IA2_ROLE_CANVAS: controlTypes.ROLE_CANVAS, - IA2_ROLE_CAPTION: controlTypes.ROLE_CAPTION, - IA2_ROLE_CHECK_MENU_ITEM: controlTypes.ROLE_CHECKMENUITEM, - IA2_ROLE_COLOR_CHOOSER: controlTypes.ROLE_COLORCHOOSER, - IA2_ROLE_DATE_EDITOR: controlTypes.ROLE_DATEEDITOR, - IA2_ROLE_DESKTOP_ICON: controlTypes.ROLE_DESKTOPICON, - IA2_ROLE_DESKTOP_PANE: controlTypes.ROLE_DESKTOPPANE, - IA2_ROLE_DIRECTORY_PANE: controlTypes.ROLE_DIRECTORYPANE, - IA2_ROLE_EDITBAR: controlTypes.ROLE_EDITBAR, - IA2_ROLE_EMBEDDED_OBJECT: controlTypes.ROLE_EMBEDDEDOBJECT, - IA2_ROLE_ENDNOTE: controlTypes.ROLE_ENDNOTE, - IA2_ROLE_FILE_CHOOSER: controlTypes.ROLE_FILECHOOSER, - IA2_ROLE_FONT_CHOOSER: controlTypes.ROLE_FONTCHOOSER, - IA2_ROLE_FOOTER: controlTypes.ROLE_FOOTER, - IA2_ROLE_FOOTNOTE: controlTypes.ROLE_FOOTNOTE, - IA2_ROLE_FORM: controlTypes.ROLE_FORM, - IA2_ROLE_FRAME: controlTypes.ROLE_FRAME, - IA2_ROLE_GLASS_PANE: controlTypes.ROLE_GLASSPANE, - IA2_ROLE_HEADER: controlTypes.ROLE_HEADER, - IA2_ROLE_HEADING: controlTypes.ROLE_HEADING, - IA2_ROLE_ICON: controlTypes.ROLE_ICON, - IA2_ROLE_IMAGE_MAP: controlTypes.ROLE_IMAGEMAP, - IA2_ROLE_INPUT_METHOD_WINDOW: controlTypes.ROLE_INPUTWINDOW, - IA2_ROLE_INTERNAL_FRAME: controlTypes.ROLE_INTERNALFRAME, - IA2_ROLE_LABEL: controlTypes.ROLE_LABEL, - IA2_ROLE_LAYERED_PANE: controlTypes.ROLE_LAYEREDPANE, - IA2_ROLE_NOTE: controlTypes.ROLE_NOTE, - IA2_ROLE_OPTION_PANE: controlTypes.ROLE_OPTIONPANE, - IA2_ROLE_PAGE: controlTypes.ROLE_PAGE, - IA2_ROLE_PARAGRAPH: controlTypes.ROLE_PARAGRAPH, - IA2_ROLE_RADIO_MENU_ITEM: controlTypes.ROLE_RADIOMENUITEM, - IA2_ROLE_REDUNDANT_OBJECT: controlTypes.ROLE_REDUNDANTOBJECT, - IA2_ROLE_ROOT_PANE: controlTypes.ROLE_ROOTPANE, - IA2_ROLE_RULER: controlTypes.ROLE_RULER, - IA2_ROLE_SCROLL_PANE: controlTypes.ROLE_SCROLLPANE, - IA2_ROLE_SECTION: controlTypes.ROLE_SECTION, - IA2_ROLE_SHAPE: controlTypes.ROLE_SHAPE, - IA2_ROLE_SPLIT_PANE: controlTypes.ROLE_SPLITPANE, - IA2_ROLE_TEAR_OFF_MENU: controlTypes.ROLE_TEAROFFMENU, - IA2_ROLE_TERMINAL: controlTypes.ROLE_TERMINAL, - IA2_ROLE_TEXT_FRAME: controlTypes.ROLE_TEXTFRAME, - IA2_ROLE_TOGGLE_BUTTON: controlTypes.ROLE_TOGGLEBUTTON, - IA2_ROLE_VIEW_PORT: controlTypes.ROLE_VIEWPORT, - IA2_ROLE_CONTENT_DELETION: controlTypes.ROLE_DELETED_CONTENT, - IA2_ROLE_CONTENT_INSERTION: controlTypes.ROLE_INSERTED_CONTENT, - IA2_ROLE_BLOCK_QUOTE: controlTypes.ROLE_BLOCKQUOTE, + IA2.IA2_ROLE_UNKNOWN: controlTypes.ROLE_UNKNOWN, + IA2.IA2_ROLE_CANVAS: controlTypes.ROLE_CANVAS, + IA2.IA2_ROLE_CAPTION: controlTypes.ROLE_CAPTION, + IA2.IA2_ROLE_CHECK_MENU_ITEM: controlTypes.ROLE_CHECKMENUITEM, + IA2.IA2_ROLE_COLOR_CHOOSER: controlTypes.ROLE_COLORCHOOSER, + IA2.IA2_ROLE_DATE_EDITOR: controlTypes.ROLE_DATEEDITOR, + IA2.IA2_ROLE_DESKTOP_ICON: controlTypes.ROLE_DESKTOPICON, + IA2.IA2_ROLE_DESKTOP_PANE: controlTypes.ROLE_DESKTOPPANE, + IA2.IA2_ROLE_DIRECTORY_PANE: controlTypes.ROLE_DIRECTORYPANE, + IA2.IA2_ROLE_EDITBAR: controlTypes.ROLE_EDITBAR, + IA2.IA2_ROLE_EMBEDDED_OBJECT: controlTypes.ROLE_EMBEDDEDOBJECT, + IA2.IA2_ROLE_ENDNOTE: controlTypes.ROLE_ENDNOTE, + IA2.IA2_ROLE_FILE_CHOOSER: controlTypes.ROLE_FILECHOOSER, + IA2.IA2_ROLE_FONT_CHOOSER: controlTypes.ROLE_FONTCHOOSER, + IA2.IA2_ROLE_FOOTER: controlTypes.ROLE_FOOTER, + IA2.IA2_ROLE_FOOTNOTE: controlTypes.ROLE_FOOTNOTE, + IA2.IA2_ROLE_FORM: controlTypes.ROLE_FORM, + IA2.IA2_ROLE_FRAME: controlTypes.ROLE_FRAME, + IA2.IA2_ROLE_GLASS_PANE: controlTypes.ROLE_GLASSPANE, + IA2.IA2_ROLE_HEADER: controlTypes.ROLE_HEADER, + IA2.IA2_ROLE_HEADING: controlTypes.ROLE_HEADING, + IA2.IA2_ROLE_ICON: controlTypes.ROLE_ICON, + IA2.IA2_ROLE_IMAGE_MAP: controlTypes.ROLE_IMAGEMAP, + IA2.IA2_ROLE_INPUT_METHOD_WINDOW: controlTypes.ROLE_INPUTWINDOW, + IA2.IA2_ROLE_INTERNAL_FRAME: controlTypes.ROLE_INTERNALFRAME, + IA2.IA2_ROLE_LABEL: controlTypes.ROLE_LABEL, + IA2.IA2_ROLE_LAYERED_PANE: controlTypes.ROLE_LAYEREDPANE, + IA2.IA2_ROLE_NOTE: controlTypes.ROLE_NOTE, + IA2.IA2_ROLE_OPTION_PANE: controlTypes.ROLE_OPTIONPANE, + IA2.IA2_ROLE_PAGE: controlTypes.ROLE_PAGE, + IA2.IA2_ROLE_PARAGRAPH: controlTypes.ROLE_PARAGRAPH, + IA2.IA2_ROLE_RADIO_MENU_ITEM: controlTypes.ROLE_RADIOMENUITEM, + IA2.IA2_ROLE_REDUNDANT_OBJECT: controlTypes.ROLE_REDUNDANTOBJECT, + IA2.IA2_ROLE_ROOT_PANE: controlTypes.ROLE_ROOTPANE, + IA2.IA2_ROLE_RULER: controlTypes.ROLE_RULER, + IA2.IA2_ROLE_SCROLL_PANE: controlTypes.ROLE_SCROLLPANE, + IA2.IA2_ROLE_SECTION: controlTypes.ROLE_SECTION, + IA2.IA2_ROLE_SHAPE: controlTypes.ROLE_SHAPE, + IA2.IA2_ROLE_SPLIT_PANE: controlTypes.ROLE_SPLITPANE, + IA2.IA2_ROLE_TEAR_OFF_MENU: controlTypes.ROLE_TEAROFFMENU, + IA2.IA2_ROLE_TERMINAL: controlTypes.ROLE_TERMINAL, + IA2.IA2_ROLE_TEXT_FRAME: controlTypes.ROLE_TEXTFRAME, + IA2.IA2_ROLE_TOGGLE_BUTTON: controlTypes.ROLE_TOGGLEBUTTON, + IA2.IA2_ROLE_VIEW_PORT: controlTypes.ROLE_VIEWPORT, + IA2.IA2_ROLE_CONTENT_DELETION: controlTypes.ROLE_DELETED_CONTENT, + IA2.IA2_ROLE_CONTENT_INSERTION: controlTypes.ROLE_INSERTED_CONTENT, + IA2.IA2_ROLE_BLOCK_QUOTE: controlTypes.ROLE_BLOCKQUOTE, IA2.IA2_ROLE_LANDMARK: controlTypes.ROLE_LANDMARK, - IA2_ROLE_MARK: controlTypes.ROLE_MARKED_CONTENT, + IA2.IA2_ROLE_MARK: controlTypes.ROLE_MARKED_CONTENT, # some common string roles "frame": controlTypes.ROLE_FRAME, "iframe": controlTypes.ROLE_INTERNALFRAME, @@ -371,32 +289,32 @@ def isMSAADebugLoggingEnabled(): } IAccessible2StatesToNVDAStates = { - IA2_STATE_REQUIRED: controlTypes.STATE_REQUIRED, - IA2_STATE_DEFUNCT: controlTypes.STATE_DEFUNCT, - # IA2_STATE_STALE:controlTypes.STATE_DEFUNCT, - IA2_STATE_INVALID_ENTRY: controlTypes.STATE_INVALID_ENTRY, - IA2_STATE_MODAL: controlTypes.STATE_MODAL, - IA2_STATE_SUPPORTS_AUTOCOMPLETION: controlTypes.STATE_AUTOCOMPLETE, - IA2_STATE_MULTI_LINE: controlTypes.STATE_MULTILINE, - IA2_STATE_ICONIFIED: controlTypes.STATE_ICONIFIED, - IA2_STATE_EDITABLE: controlTypes.STATE_EDITABLE, - IA2_STATE_PINNED: controlTypes.STATE_PINNED, - IA2_STATE_CHECKABLE: controlTypes.STATE_CHECKABLE, + IA2.IA2_STATE_REQUIRED: controlTypes.STATE_REQUIRED, + IA2.IA2_STATE_DEFUNCT: controlTypes.STATE_DEFUNCT, + # IA2.IA2_STATE_STALE:controlTypes.STATE_DEFUNCT, + IA2.IA2_STATE_INVALID_ENTRY: controlTypes.STATE_INVALID_ENTRY, + IA2.IA2_STATE_MODAL: controlTypes.STATE_MODAL, + IA2.IA2_STATE_SUPPORTS_AUTOCOMPLETION: controlTypes.STATE_AUTOCOMPLETE, + IA2.IA2_STATE_MULTI_LINE: controlTypes.STATE_MULTILINE, + IA2.IA2_STATE_ICONIFIED: controlTypes.STATE_ICONIFIED, + IA2.IA2_STATE_EDITABLE: controlTypes.STATE_EDITABLE, + IA2.IA2_STATE_PINNED: controlTypes.STATE_PINNED, + IA2.IA2_STATE_CHECKABLE: controlTypes.STATE_CHECKABLE, } def normalizeIAccessible(pacc, childID=0): - if not isinstance(pacc, IAccessible): + if not isinstance(pacc, IA.IAccessible): try: - pacc = pacc.QueryInterface(IAccessible) + pacc = pacc.QueryInterface(IA.IAccessible) except COMError: raise RuntimeError("%s Not an IAccessible" % pacc) # #2558: IAccessible2 doesn't support simple children. # Therefore, it doesn't make sense to use IA2 if the child ID is non-0. - if childID == 0 and not isinstance(pacc, IAccessible2): + if childID == 0 and not isinstance(pacc, IA2.IAccessible2): try: s = pacc.QueryInterface(IServiceProvider) - pacc2 = s.QueryService(IAccessible._iid_, IAccessible2) + pacc2 = s.QueryService(IA.IAccessible._iid_, IA2.IAccessible2) if not pacc2: # QueryService should fail if IA2 is not supported, but some applications such as AIM 7 misbehave # and return a null COM pointer. Treat this as if QueryService failed. @@ -595,7 +513,7 @@ def winEventToNVDAEvent(eventID, window, objectID, childID, useCache=True): f"Creating NVDA event from winEvent: {getWinEventLogInfo(window, objectID, childID, eventID)}, " f"use cache {useCache}" ) - NVDAEventName = winEventIDsToNVDAEventNames.get(eventID, None) + NVDAEventName = internalWinEventHandler.winEventIDsToNVDAEventNames.get(eventID, None) if not NVDAEventName: log.debugWarning(f"No NVDA event name for {getWinEventName(eventID)}") return None @@ -1036,7 +954,7 @@ def _fakeFocus(oldFocus): def initialize(): global accPropServices try: - accPropServices = comtypes.client.CreateObject(CAccPropServices) + accPropServices = comtypes.client.CreateObject(IA.CAccPropServices) except (WindowsError, COMError) as e: log.debugWarning("AccPropServices is not available: %s" % e) internalWinEventHandler.initialize(processDestroyWinEvent) @@ -1044,7 +962,7 @@ def initialize(): # C901 'pumpAll' is too complex def pumpAll(): # noqa: C901 - if not _shouldGetEvents(): + if not internalWinEventHandler._shouldGetEvents(): return focusWinEvents = [] validFocus = False @@ -1058,7 +976,7 @@ def pumpAll(): # noqa: C901 alwaysAllowedObjects.append((focus.event_windowHandle, focus.event_objectID, focus.event_childID)) # Receive all the winEvents from the limiter for this cycle - winEvents = winEventLimiter.flushEvents(alwaysAllowedObjects) + winEvents = internalWinEventHandler.winEventLimiter.flushEvents(alwaysAllowedObjects) for winEvent in winEvents: isEventOnCaret = winEvent[2] == winUser.OBJID_CARET @@ -1074,7 +992,7 @@ def pumpAll(): # noqa: C901 if not focus.shouldAcceptShowHideCaretEvent: continue elif not eventHandler.shouldAcceptEvent( - winEventIDsToNVDAEventNames[winEvent[0]], + internalWinEventHandler.winEventIDsToNVDAEventNames[winEvent[0]], windowHandle=winEvent[1] ): continue @@ -1128,7 +1046,7 @@ def terminate(): def getIAccIdentity(pacc, childID): - IAccIdentityObject = pacc.QueryInterface(IAccIdentity) + IAccIdentityObject = pacc.QueryInterface(IA.IAccIdentity) stringPtr, stringSize = IAccIdentityObject.getIdentityString(childID) try: if accPropServices: @@ -1186,16 +1104,16 @@ def findGroupboxObject(obj): # C901 'getRecursiveTextFromIAccessibleTextObject' def getRecursiveTextFromIAccessibleTextObject(obj, startOffset=0, endOffset=-1): # noqa: C901 - if not isinstance(obj, IAccessibleText): + if not isinstance(obj, IA2.IAccessibleText): try: - textObject = obj.QueryInterface(IAccessibleText) + textObject = obj.QueryInterface(IA2.IAccessibleText) except: # noqa: E722 Bare except textObject = None else: textObject = obj - if not isinstance(obj, IAccessible): + if not isinstance(obj, IA.IAccessible): try: - accObject = obj.QueryInterface(IAccessible) + accObject = obj.QueryInterface(IA.IAccessible) except: # noqa: E722 Bare except return "" else: @@ -1219,7 +1137,7 @@ def getRecursiveTextFromIAccessibleTextObject(obj, startOffset=0, endOffset=-1): description = None return " ".join([x for x in [name, value, description] if x and not x.isspace()]) try: - hypertextObject = accObject.QueryInterface(IAccessibleHypertext) + hypertextObject = accObject.QueryInterface(IA2.IAccessibleHypertext) except: # noqa: E722 Bare except return text textList = [] @@ -1227,7 +1145,7 @@ def getRecursiveTextFromIAccessibleTextObject(obj, startOffset=0, endOffset=-1): if ord(t) == 0xFFFC: try: index = hypertextObject.hyperlinkIndex(i + startOffset) - childTextObject = hypertextObject.hyperlink(index).QueryInterface(IAccessible) + childTextObject = hypertextObject.hyperlink(index).QueryInterface(IA.IAccessible) t = " %s " % getRecursiveTextFromIAccessibleTextObject(childTextObject) except: # noqa: E722 Bare except pass @@ -1322,7 +1240,7 @@ def isMarshalledIAccessible(IAccessibleObject): """Looks at the location of the first function in the IAccessible object's vtable (IUnknown::AddRef) to see if it was implemented in oleacc.dll (its local) or ole32.dll (its marshalled). """ - if not isinstance(IAccessibleObject, IAccessible): + if not isinstance(IAccessibleObject, IA.IAccessible): raise TypeError("object should be of type IAccessible, not %s" % IAccessibleObject) buf = create_unicode_buffer(1024) addr = POINTER(c_void_p).from_address( diff --git a/source/IAccessibleHandler/internalWinEventHandler.py b/source/IAccessibleHandler/internalWinEventHandler.py index 9765ea8213b..43ab1376b97 100644 --- a/source/IAccessibleHandler/internalWinEventHandler.py +++ b/source/IAccessibleHandler/internalWinEventHandler.py @@ -16,13 +16,8 @@ from . import getWinEventLogInfo from . import isMSAADebugLoggingEnabled +from comInterfaces import IAccessible2Lib as IA2 -from comInterfaces.IAccessible2Lib import ( - IA2_EVENT_TEXT_CARET_MOVED, - IA2_EVENT_DOCUMENT_LOAD_COMPLETE, - IA2_EVENT_OBJECT_ATTRIBUTE_CHANGED, - IA2_EVENT_PAGE_CHANGED, -) from .orderedWinEventLimiter import OrderedWinEventLimiter, MENU_EVENTIDS from logHandler import log @@ -61,10 +56,10 @@ winUser.EVENT_OBJECT_STATECHANGE: "stateChange", winUser.EVENT_OBJECT_VALUECHANGE: "valueChange", winUser.EVENT_OBJECT_LIVEREGIONCHANGED: "liveRegionChange", - IA2_EVENT_TEXT_CARET_MOVED: "caret", - IA2_EVENT_DOCUMENT_LOAD_COMPLETE: "documentLoadComplete", - IA2_EVENT_OBJECT_ATTRIBUTE_CHANGED: "IA2AttributeChange", - IA2_EVENT_PAGE_CHANGED: "pageChange", + IA2.IA2_EVENT_TEXT_CARET_MOVED: "caret", + IA2.IA2_EVENT_DOCUMENT_LOAD_COMPLETE: "documentLoadComplete", + IA2.IA2_EVENT_OBJECT_ATTRIBUTE_CHANGED: "IA2AttributeChange", + IA2.IA2_EVENT_PAGE_CHANGED: "pageChange", } _processDestroyWinEvent = None diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index 0486fdce660..4e1f83f1589 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -12,6 +12,7 @@ import itertools import importlib from comInterfaces.tom import ITextDocument +from comInterfaces import IAccessible2Lib as IA2 import tones import languageHandler import textInfos.offsets @@ -124,7 +125,9 @@ def _get_encoding(self): def _getOffsetFromPoint(self,x,y): if self.obj.IAccessibleTextObject.nCharacters>0: - offset = self.obj.IAccessibleTextObject.OffsetAtPoint(x,y,IAccessibleHandler.IA2_COORDTYPE_SCREEN_RELATIVE) + offset = self.obj.IAccessibleTextObject.OffsetAtPoint( + x, y, IA2.IA2_COORDTYPE_SCREEN_RELATIVE + ) # IA2 specifies that a result of -1 indicates that # the point is invalid or there is no character under the point. # Note that Chromium does not follow the spec and returns 0 for invalid or no character points. @@ -138,7 +141,9 @@ def _getOffsetFromPoint(self,x,y): @classmethod def _getBoundingRectFromOffsetInObject(cls,obj,offset): try: - res=RectLTWH(*obj.IAccessibleTextObject.characterExtents(offset,IAccessibleHandler.IA2_COORDTYPE_SCREEN_RELATIVE)) + res = RectLTWH(*obj.IAccessibleTextObject.characterExtents( + offset, IA2.IA2_COORDTYPE_SCREEN_RELATIVE + )) except COMError: raise NotImplementedError if not any(res[2:]): @@ -259,7 +264,7 @@ def _getCharacterOffsets(self,offset): except COMError: pass try: - start,end,text = self.obj.IAccessibleTextObject.TextAtOffset(offset,IAccessibleHandler.IA2_TEXT_BOUNDARY_CHAR) + start, end, text = self.obj.IAccessibleTextObject.TextAtOffset(offset, IA2.IA2_TEXT_BOUNDARY_CHAR) except COMError: return super(IA2TextTextInfo,self)._getCharacterOffsets(offset) if text and (textUtils.isHighSurrogate(text) or textUtils.isLowSurrogate(text)): @@ -275,7 +280,7 @@ def _getWordOffsets(self,offset): except COMError: pass try: - start,end,text=self.obj.IAccessibleTextObject.TextAtOffset(offset,IAccessibleHandler.IA2_TEXT_BOUNDARY_WORD) + start, end, text = self.obj.IAccessibleTextObject.TextAtOffset(offset, IA2.IA2_TEXT_BOUNDARY_WORD) except COMError: return super(IA2TextTextInfo,self)._getWordOffsets(offset) if start>offset or offset>end: @@ -285,7 +290,7 @@ def _getWordOffsets(self,offset): def _getLineOffsets(self,offset): try: - start,end,text=self.obj.IAccessibleTextObject.TextAtOffset(offset,IAccessibleHandler.IA2_TEXT_BOUNDARY_LINE) + start, end, text = self.obj.IAccessibleTextObject.TextAtOffset(offset, IA2.IA2_TEXT_BOUNDARY_LINE) return start,end except COMError: log.debugWarning("IAccessibleText::textAtOffset failed",exc_info=True) @@ -298,7 +303,7 @@ def _getSentenceOffsets(self,offset): except COMError: pass try: - start,end,text=self.obj.IAccessibleTextObject.TextAtOffset(offset,IAccessibleHandler.IA2_TEXT_BOUNDARY_SENTENCE) + start, end, text = self.obj.IAccessibleTextObject.TextAtOffset(offset, IA2.IA2_TEXT_BOUNDARY_SENTENCE) if start==end: raise NotImplementedError return start,end @@ -312,7 +317,7 @@ def _getParagraphOffsets(self,offset): except COMError: pass try: - start,end,text=self.obj.IAccessibleTextObject.TextAtOffset(offset,IAccessibleHandler.IA2_TEXT_BOUNDARY_PARAGRAPH) + start, end, text = self.obj.IAccessibleTextObject.TextAtOffset(offset, IA2.IA2_TEXT_BOUNDARY_PARAGRAPH) if start>=end: raise RuntimeError("did not expand to paragraph correctly") return start,end @@ -564,7 +569,11 @@ def findOverlayClasses(self,clsList): clsList.append(IAccessible) - if self.event_objectID==winUser.OBJID_CLIENT and self.event_childID==0 and not isinstance(self.IAccessibleObject,IAccessibleHandler.IAccessible2): + if( + self.event_objectID == winUser.OBJID_CLIENT + and self.event_childID == 0 + and not isinstance(self.IAccessibleObject, IA2.IAccessible2) + ): # This is the main (client) area of the window, so we can use other classes at the window level. # #3872: However, don't do this for IAccessible2 because # IA2 supersedes window level APIs and might conflict with them. @@ -588,7 +597,7 @@ def __init__(self,windowHandle=None,IAccessibleObject=None,IAccessibleChildID=No self.IAccessibleChildID=IAccessibleChildID # Try every trick in the book to get the window handle if we don't have it. - if not windowHandle and isinstance(IAccessibleObject,IAccessibleHandler.IAccessible2): + if not windowHandle and isinstance(IAccessibleObject, IA2.IAccessible2): windowHandle=self.IA2WindowHandle try: Identity=IAccessibleHandler.getIAccIdentity(IAccessibleObject,IAccessibleChildID) @@ -614,7 +623,7 @@ def __init__(self,windowHandle=None,IAccessibleObject=None,IAccessibleChildID=No if not windowHandle: raise InvalidNVDAObject("Can't get a window handle from IAccessible") - if isinstance(IAccessibleObject,IAccessibleHandler.IAccessible2): + if isinstance(IAccessibleObject, IA2.IAccessible2): try: self.IA2UniqueID=IAccessibleObject.uniqueID except COMError: @@ -623,7 +632,7 @@ def __init__(self,windowHandle=None,IAccessibleObject=None,IAccessibleChildID=No # Set the event params based on our calculated/construction info if we must. if event_windowHandle is None: event_windowHandle=windowHandle - if event_objectID is None and isinstance(IAccessibleObject,IAccessibleHandler.IAccessible2): + if event_objectID is None and isinstance(IAccessibleObject, IA2.IAccessible2): event_objectID=winUser.OBJID_CLIENT if event_childID is None: if self.IA2UniqueID is not None: @@ -637,18 +646,18 @@ def __init__(self,windowHandle=None,IAccessibleObject=None,IAccessibleChildID=No super(IAccessible,self).__init__(windowHandle=windowHandle) try: - self.IAccessibleActionObject=IAccessibleObject.QueryInterface(IAccessibleHandler.IAccessibleAction) + self.IAccessibleActionObject = IAccessibleObject.QueryInterface(IA2.IAccessibleAction) except COMError: pass try: - self.IAccessibleTable2Object=self.IAccessibleObject.QueryInterface(IAccessibleHandler.IAccessibleTable2) + self.IAccessibleTable2Object = self.IAccessibleObject.QueryInterface(IA2.IAccessibleTable2) except COMError: try: - self.IAccessibleTableObject=self.IAccessibleObject.QueryInterface(IAccessibleHandler.IAccessibleTable) + self.IAccessibleTableObject = self.IAccessibleObject.QueryInterface(IA2.IAccessibleTable) except COMError: pass try: - self.IAccessibleTextObject=IAccessibleObject.QueryInterface(IAccessibleHandler.IAccessibleText) + self.IAccessibleTextObject = IAccessibleObject.QueryInterface(IA2.IAccessibleText) except COMError: pass if None not in (event_windowHandle,event_objectID,event_childID): @@ -698,7 +707,10 @@ def _isEqual(self,other): return False if self.IAccessibleObject==other.IAccessibleObject: return True - if isinstance(self.IAccessibleObject,IAccessibleHandler.IAccessible2) and isinstance(other.IAccessibleObject,IAccessibleHandler.IAccessible2): + if ( + isinstance(self.IAccessibleObject, IA2.IAccessible2) + and isinstance(other.IAccessibleObject, IA2.IAccessible2) + ): # These are both IAccessible2 objects, so we can test unique ID. # Unique ID is only guaranteed to be unique within a given window, so we must check window handle as well. selfIA2Window=self.IA2WindowHandle @@ -810,7 +822,7 @@ def _get_IAccessibleIdentity(self): return self._IAccessibleIdentity def _get_IAccessibleRole(self): - if isinstance(self.IAccessibleObject,IAccessibleHandler.IAccessible2): + if isinstance(self.IAccessibleObject, IA2.IAccessible2): try: role=self.IAccessibleObject.role() except COMError: @@ -857,7 +869,7 @@ def _get_states(self): log.debugWarning("could not get IAccessible states",exc_info=True) else: states.update(IAccessibleHandler.IAccessibleStatesToNVDAStates[x] for x in (y for y in (1< Date: Fri, 26 Mar 2021 05:28:16 +0100 Subject: [PATCH 108/174] Report appointment categories in Microsoft Outlook (PR #11598) Outlook appointments could have multiple categories (or colors) assigned to them. Fetch the categories from the object model and report them after other appointment details. --- source/appModules/outlook.py | 47 ++++++++++++++++++++++++++++++------ source/languageHandler.py | 2 ++ user_docs/en/changes.t2t | 1 + 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/source/appModules/outlook.py b/source/appModules/outlook.py index a40eefae45a..934c9e34028 100644 --- a/source/appModules/outlook.py +++ b/source/appModules/outlook.py @@ -1,8 +1,8 @@ -#appModules/outlook.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2006-2018 NV Access Limited, Yogesh Kumar, Manish Agrawal, Joseph Lee, Davy Kager, Babbage B.V. -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2006-2021 NV Access Limited, Yogesh Kumar, Manish Agrawal, Joseph Lee, Davy Kager, +# Babbage B.V., Leonard de Ruijter +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. from comtypes import COMError from comtypes.hresult import S_OK @@ -36,6 +36,8 @@ from NVDAObjects.behaviors import RowWithFakeNavigation, Dialog from NVDAObjects.UIA import UIA from NVDAObjects.UIA.wordDocument import WordDocument as UIAWordDocument +import languageHandler +from gettext import ngettext PR_LAST_VERB_EXECUTED=0x10810003 VERB_REPLYTOSENDER=102 @@ -334,6 +336,30 @@ def _generateTimeRangeText(self,startTime,endTime): # Translators: a message reporting the time range (i.e. start time to end time) of an Outlook calendar entry return _("{startTime} to {endTime}").format(startTime=startText,endTime=endText) + @staticmethod + def _generateCategoriesText(appointment): + categories = appointment.Categories + if not categories: + return None + # Categories is a delimited string of category names that have been assigned to an Outlook item. + # This property uses the user locale's list separator to separate entries. + # See also https://docs.microsoft.com/en-us/office/vba/api/outlook.appointmentitem.categories + bufLength = 4 + separatorBuf = ctypes.create_unicode_buffer(bufLength) + if ctypes.windll.kernel32.GetLocaleInfoW( + languageHandler.LOCALE_USER_DEFAULT, + languageHandler.LOCALE_SLIST, + separatorBuf, + bufLength + ) == 0: + raise ctypes.WinError() + categoriesCount = len(categories.split(f"{separatorBuf.value} ")) + + # Translators: Part of a message reported when on a calendar appointment with one or more categories + # in Microsoft Outlook. + categoriesText = ngettext("category", "categories", categoriesCount) + return f"{categoriesText} {categories}" + def isDuplicateIAccessibleEvent(self,obj): return False @@ -355,8 +381,15 @@ def reportFocus(self): except COMError: return super(CalendarView,self).reportFocus() t=self._generateTimeRangeText(start,end) - # Translators: A message reported when on a calendar appointment in Microsoft Outlook - ui.message(_("Appointment {subject}, {time}").format(subject=p.subject,time=t)) + # Translators: A message reported when on a calendar appointment with category in Microsoft Outlook + message = _("Appointment {subject}, {time}").format(subject=p.subject, time=t) + try: + categoriesText = self._generateCategoriesText(p) + except COMError: + categoriesText = None + if categoriesText is not None: + message = f"{message}, {categoriesText}" + ui.message(message) else: v=e.currentView try: diff --git a/source/languageHandler.py b/source/languageHandler.py index c3ab3dc4ab0..fbfdf14ec6d 100644 --- a/source/languageHandler.py +++ b/source/languageHandler.py @@ -16,7 +16,9 @@ #a few Windows locale constants LOCALE_SLANGUAGE=0x2 +LOCALE_SLIST = 0xC LOCALE_SLANGDISPLAYNAME=0x6f +LOCALE_USER_DEFAULT = 0x400 LOCALE_CUSTOM_UNSPECIFIED = 0x1000 #: Returned from L{localeNameToWindowsLCID} when the locale name cannot be mapped to a locale identifier. #: This might be because Windows doesn't know about the locale (e.g. "an"), diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 6a98d45dc28..0b9c317a0a5 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -121,6 +121,7 @@ Plus many other important bug fixes and improvements. - Added the --copy-portable-config command line parameter that allows you to automatically copy the provided configuration to the user account when silently installing NVDA. (#9676) - Braille routing is now supported with the Braille Viewer for mouse users, hover to route to a braille cell. (#11804) - NVDA will now automatically detect the Humanware Brailliant BI 40X and 20X devices via both USB and Bluetooth. (#11819) +- NVDA now reports the categories assigned to an appointment in Microsoft Outlook, if any. (#11598) == Changes == From 656ecb5f9bd3301812e8d0f73dbf8cf360f286a5 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Fri, 26 Mar 2021 20:52:32 +1000 Subject: [PATCH 109/174] Allow UIA to work again on Win7 after pr #12210 (#12233) With the merging of pr #12210 it became impossible to interact with UI Automation controls on Windows 7. * Fetching the annotationTypes UIA property raises a COMError if not implemented by the control. On newer Operating Systems UIA core returns a default. * Fetching the SelectionPattern2 interface also causes a COMError on older Operating Systems if not implemented by the control. Therefore: * Catch COMError when fetching UIA annotationTypes. * Catch COMError when fetching the UIA SelectionPattern2 interface. Fixes #12227 --- source/NVDAObjects/UIA/__init__.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index fe72d201230..660bd37d596 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -1193,10 +1193,14 @@ def _get_UIASelectionPattern(self): return self.UIASelectionPattern def _get_UIASelectionPattern2(self): - self.UIASelectionPattern2 = self._getUIAPattern( - UIAHandler.UIA_SelectionPattern2Id, - UIAHandler.IUIAutomationSelectionPattern2 - ) + try: + self.UIASelectionPattern2 = self._getUIAPattern( + UIAHandler.UIA_SelectionPattern2Id, + UIAHandler.IUIAutomationSelectionPattern2 + ) + except COMError: + # SelectionPattern2 is not available on older Operating Systems such as Windows 7 + self.UIASelectionPattern2 = None return self.UIASelectionPattern2 def getSelectedItemsCount(self, maxItems=None): @@ -1462,7 +1466,11 @@ def _get_states(self): states.add(controlTypes.STATE_CHECKABLE) if s==UIAHandler.ToggleState_On: states.add(controlTypes.STATE_CHECKED) - annotationTypes = self._getUIACacheablePropertyValue(UIAHandler.UIA_AnnotationTypesPropertyId) + try: + annotationTypes = self._getUIACacheablePropertyValue(UIAHandler.UIA_AnnotationTypesPropertyId) + except COMError: + # annotationTypes cannot be fetched on older Operating Systems such as Windows 7. + annotationTypes = None if annotationTypes: if UIAHandler.AnnotationType_Comment in annotationTypes: states.add(controlTypes.STATE_HASCOMMENT) From d9ccf26a068f57daf205056c3a45a0790c176453 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Mon, 29 Mar 2021 10:50:00 +1100 Subject: [PATCH 110/174] Patch wx overriding the correct python locale (#12214) The thread executing this did not have the correct locale set by NVDA through languageHandler.setLanguage. This is due to the latest wxPython incorrectly overriding the locale with one not supported by python. The Windows/System language option should also call locale.setlocale in setLanguage and the locale needs to be reset after wx changes it. --- source/core.py | 3 + source/languageHandler.py | 141 ++++++++++++++++++----- tests/unit/test_languageHandler.py | 179 +++++++++++++++++++++++++++-- user_docs/en/changes.t2t | 1 + 4 files changed, 290 insertions(+), 34 deletions(-) diff --git a/source/core.py b/source/core.py index b61d6b4b3a0..9854cdbc12f 100644 --- a/source/core.py +++ b/source/core.py @@ -449,6 +449,9 @@ def handlePowerStatusChange(self): locale.Init(wxLang.Language) except: log.error("Failed to initialize wx locale",exc_info=True) + finally: + # Revert wx's changes to the python locale + languageHandler.setLocale(languageHandler.curLang) log.debug("Initializing garbageHandler") garbageHandler.initialize() diff --git a/source/languageHandler.py b/source/languageHandler.py index fbfdf14ec6d..a3ea42a6f67 100644 --- a/source/languageHandler.py +++ b/source/languageHandler.py @@ -13,6 +13,7 @@ import locale import gettext import globalVars +from logHandler import log #a few Windows locale constants LOCALE_SLANGUAGE=0x2 @@ -148,37 +149,119 @@ def getWindowsLanguage(): localeName="en" return localeName -def setLanguage(lang): + +def setLanguage(lang: str) -> None: + ''' + Sets the following using `lang` such as "en", "ru_RU", or "es-ES". Use "Windows" to use the system locale + - the windows locale for the thread (fallback to system locale) + - the translation service (fallback to English) + - languageHandler.curLang (match the translation service) + - the python locale for the thread (match the translation service, fallback to system default) + ''' global curLang - try: - if lang=="Windows": - localeName=getWindowsLanguage() - trans=gettext.translation('nvda',localedir='locale',languages=[localeName]) - curLang=localeName - else: - trans=gettext.translation("nvda", localedir="locale", languages=[lang]) - curLang=lang - localeChanged=False - #Try setting Python's locale to lang - try: - locale.setlocale(locale.LC_ALL,lang) - localeChanged=True - except: - pass - if not localeChanged and '_' in lang: - #Python couldn'tsupport the language_country locale, just try language. - try: - locale.setlocale(locale.LC_ALL,lang.split('_')[0]) - except: - pass - #Set the windows locale for this thread (NVDA core) to this locale. - LCID=localeNameToWindowsLCID(lang) + if lang == "Windows": + localeName = getWindowsLanguage() + else: + localeName = lang + # Set the windows locale for this thread (NVDA core) to this locale. + try: + LCID = localeNameToWindowsLCID(lang) ctypes.windll.kernel32.SetThreadLocale(LCID) + except IOError: + log.debugWarning(f"couldn't set windows thread locale to {lang}") + + try: + trans = gettext.translation("nvda", localedir="locale", languages=[localeName]) + curLang = localeName except IOError: - trans=gettext.translation("nvda",fallback=True) - curLang="en" + log.debugWarning(f"couldn't set the translation service locale to {localeName}") + trans = gettext.translation("nvda", fallback=True) + curLang = "en" + # #9207: Python 3.8 adds gettext.pgettext, so add it to the built-in namespace. trans.install(names=["pgettext"]) + setLocale(curLang) + + +def setLocale(localeName: str) -> None: + ''' + Set python's locale using a `localeName` such as "en", "ru_RU", or "es-ES". + Will fallback on `curLang` if it cannot be set and finally fallback to the system locale. + ''' + + r''' + Python 3.8's locale system allows you to set locales that you cannot get + so we must test for both ValueErrors and locale.Errors + + >>> import locale + >>> locale.setlocale(locale.LC_ALL, 'foobar') + Traceback (most recent call last): + File "", line 1, in + File "Python38-32\lib\locale.py", line 608, in setlocale + return _setlocale(category, locale) + locale.Error: unsupported locale setting + >>> locale.setlocale(locale.LC_ALL, 'en-GB') + 'en-GB' + >>> locale.getlocale() + Traceback (most recent call last): + File "", line 1, in + File "Python38-32\lib\locale.py", line 591, in getlocale + return _parse_localename(localename) + File "Python38-32\lib\locale.py", line 499, in _parse_localename + raise ValueError('unknown locale: %s' % localename) + ValueError: unknown locale: en-GB + ''' + originalLocaleName = localeName + # Try setting Python's locale to localeName + try: + locale.setlocale(locale.LC_ALL, localeName) + locale.getlocale() + log.debug(f"set python locale to {localeName}") + return + except locale.Error: + log.debugWarning(f"python locale {localeName} could not be set") + except ValueError: + log.debugWarning(f"python locale {localeName} could not be retrieved with getlocale") + + if '-' in localeName: + # Python couldn't support the language-country locale, try language_country. + try: + localeName = localeName.replace('-', '_') + locale.setlocale(locale.LC_ALL, localeName) + locale.getlocale() + log.debug(f"set python locale to {localeName}") + return + except locale.Error: + log.debugWarning(f"python locale {localeName} could not be set") + except ValueError: + log.debugWarning(f"python locale {localeName} could not be retrieved with getlocale") + + if '_' in localeName: + # Python couldn't support the language_country locale, just try language. + try: + localeName = localeName.split('_')[0] + locale.setlocale(locale.LC_ALL, localeName) + locale.getlocale() + log.debug(f"set python locale to {localeName}") + return + except locale.Error: + log.debugWarning(f"python locale {localeName} could not be set") + except ValueError: + log.debugWarning(f"python locale {localeName} could not be retrieved with getlocale") + + try: + locale.getlocale() + except ValueError: + # as the locale may have been changed to something that getlocale() couldn't retrieve + # reset to default locale + if originalLocaleName == curLang: + # reset to system locale default if we can't set the current lang's locale + locale.setlocale(locale.LC_ALL, "") + log.debugWarning(f"set python locale to system default") + else: + log.debugWarning(f"setting python locale to the current language {curLang}") + # fallback and try to reset the locale to the current lang + setLocale(curLang) def getLanguage() -> str: @@ -316,5 +399,9 @@ def normalizeLanguage(lang): 134:'qut', 135:'rw', 136:'wo', - 140:'gbz' + 140: 'gbz', + 1170: 'ckb', + 1109: 'my', + 1143: 'so', + 9242: 'sr', } diff --git a/tests/unit/test_languageHandler.py b/tests/unit/test_languageHandler.py index b142768e554..149f7e4b9ac 100644 --- a/tests/unit/test_languageHandler.py +++ b/tests/unit/test_languageHandler.py @@ -1,20 +1,32 @@ -#tests/unit/test_languageHandler.py -#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) 2017 NV Access Limited +# 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) 2017-2021 NV Access Limited """Unit tests for the languageHandler module. """ import unittest import languageHandler -from languageHandler import LCID_NONE +from languageHandler import LCID_NONE, windowsPrimaryLCIDsToLocaleNames +import locale +import ctypes LCID_ENGLISH_US = 0x0409 +UNSUPPORTED_PYTHON_LOCALES = { + "an", + "ckb", + "kmr", + "mn", + "my", + "ne", + "so", +} +TRANSLATABLE_LANGS = set(l[0] for l in languageHandler.getAvailableLanguages()) - {"Windows"} +WINDOWS_LANGS = set(locale.windows_locale.values()).union(windowsPrimaryLCIDsToLocaleNames.values()) -class TestLocaleNameToWindowsLCID(unittest.TestCase): +class TestLocaleNameToWindowsLCID(unittest.TestCase): def test_knownLocale(self): lcid = languageHandler.localeNameToWindowsLCID("en") self.assertEqual(lcid, LCID_ENGLISH_US) @@ -31,3 +43,156 @@ def test_nonStandardLocale(self): def test_invalidLocale(self): lcid = languageHandler.localeNameToWindowsLCID("zzzz") self.assertEqual(lcid, LCID_NONE) + + +class Test_languageHandler_setLocale(unittest.TestCase): + """Tests for the function languageHandler.setLocale""" + + SUPPORTED_LOCALES = [("en", "en_US"), ("fa-IR", "fa_IR"), ("an-ES", "an_ES")] + + def setUp(self): + """ + `setLocale` doesn't change `languageHandler.curLang`, so reset the locale using `setLanguage` to + the current language for each test. + """ + languageHandler.setLanguage(languageHandler.curLang) + + @classmethod + def tearDownClass(cls): + """ + `setLocale` doesn't change `languageHandler.curLang`, so reset the locale using `setLanguage` to + the current language so the tests can continue normally. + """ + languageHandler.setLanguage(languageHandler.curLang) + + def test_SupportedLocale_LocaleIsSet(self): + """ + Tests several locale formats that should result in an expected python locale being set. + """ + for localeName in self.SUPPORTED_LOCALES: + with self.subTest(localeName=localeName): + languageHandler.setLocale(localeName[0]) + self.assertEqual(locale.getlocale()[0], localeName[1]) + + def test_PythonUnsupportedLocale_LocaleUnchanged(self): + """ + Tests several locale formats that python doesn't support which will result in a return to the + current locale + """ + original_locale = locale.getlocale() + for localeName in UNSUPPORTED_PYTHON_LOCALES: + with self.subTest(localeName=localeName): + languageHandler.setLocale(localeName) + self.assertEqual(locale.getlocale(), original_locale) + + def test_NVDASupportedAndPythonSupportedLocale_LanguageCodeMatches(self): + """ + Tests all the translatable languages that NVDA shows in the user preferences + excludes the locales that python doesn't support, as the expected behaviour is different. + """ + for localeName in TRANSLATABLE_LANGS - UNSUPPORTED_PYTHON_LOCALES: + with self.subTest(localeName=localeName): + languageHandler.setLocale(localeName) + current_locale = locale.getlocale() + + if localeName == "uk": + self.assertEqual(current_locale[0], "English_United Kingdom") + else: + pythonLang = current_locale[0].split("_")[0] + langOnly = localeName.split("_")[0] + self.assertEqual( + langOnly, + pythonLang, + f"full values: {localeName} {current_locale[0]}", + ) + + def test_WindowsLang_LocaleCanBeRetrieved(self): + """ + We don't know whether python supports a specific windows locale so just ensure locale isn't + broken after testing these values. + """ + for localeName in WINDOWS_LANGS: + with self.subTest(localeName=localeName): + languageHandler.setLocale(localeName) + locale.getlocale() + + +class Test_LanguageHandler_SetLanguage(unittest.TestCase): + """Tests for the function languageHandler.setLanguage""" + + UNSUPPORTED_WIN_LANGUAGES = ["an", "kmr"] + + def tearDown(self): + """ + Resets the language to whatever it was before the testing suite begun. + """ + languageHandler.setLanguage(self._prevLang) + + def __init__(self, *args, **kwargs): + self._prevLang = languageHandler.getLanguage() + + ctypes.windll.kernel32.SetThreadLocale(0) + defaultThreadLocale = ctypes.windll.kernel32.GetThreadLocale() + self._defaultThreadLocaleName = languageHandler.windowsLCIDToLocaleName( + defaultThreadLocale + ) + + locale.setlocale(locale.LC_ALL, "") + self._defaultPythonLocale = locale.getlocale() + + languageHandler.setLanguage(self._prevLang) + super().__init__(*args, **kwargs) + + def test_NVDASupportedLanguages_LanguageIsSetCorrectly(self): + """ + Tests languageHandler.setLanguage, using all NVDA supported languages, which should do the following: + - set the translation service and languageHandler.curLang + - set the windows locale for the thread (fallback to system default) + - set the python locale for the thread (match the translation service, fallback to system default) + """ + for localeName in TRANSLATABLE_LANGS: + with self.subTest(localeName=localeName): + langOnly = localeName.split("_")[0] + languageHandler.setLanguage(localeName) + # check curLang/translation service is set + self.assertEqual(languageHandler.curLang, localeName) + + # check Windows thread is set + threadLocale = ctypes.windll.kernel32.GetThreadLocale() + threadLocaleName = languageHandler.windowsLCIDToLocaleName(threadLocale) + threadLocaleLang = threadLocaleName.split("_")[0] + if localeName in self.UNSUPPORTED_WIN_LANGUAGES: + # our translatable locale isn't supported by windows + # check that the system locale is unchanged + self.assertEqual(self._defaultThreadLocaleName, threadLocaleName) + else: + # check that the language codes are correctly set for the thread + self.assertEqual( + langOnly, + threadLocaleLang, + f"full values: {localeName} {threadLocaleName}", + ) + + # check that the python locale is set + python_locale = locale.getlocale() + if localeName in UNSUPPORTED_PYTHON_LOCALES: + # our translatable locale isn't supported by python + # check that the system locale is unchanged + self.assertEqual(self._defaultPythonLocale, python_locale) + elif localeName == "uk": + self.assertEqual(python_locale[0], "English_United Kingdom") + else: + # check that the language codes are correctly set for python + pythonLang = python_locale[0].split("_")[0] + self.assertEqual( + langOnly, pythonLang, f"full values: {localeName} {python_locale}" + ) + + def test_WindowsLanguages_NoErrorsThrown(self): + """ + We don't know whether python or our translator system supports a specific windows locale + so just ensure the setLanguage process doesn't fail. + """ + for localeName in WINDOWS_LANGS: + with self.subTest(localeName=localeName): + languageHandler.setLanguage(localeName) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 0b9c317a0a5..505f76e946f 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -39,6 +39,7 @@ What's New in NVDA - Fix graphical bugs such as missing elements when using NVDA with a right-to-left layout. (#8859) - Respect the GUI layout direction based on the NVDA language, not the system locale. (#638) - known issue for right-to-left languages: the right border of groupings clips with labels/controls. (#12181) +- The python locale is set to match the language selected in preferences consistently, and will occur when using the default language. (#12214) == Changes for Developers == From e1a937f3ae14f556090bea6b394b59d1ec2f9334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Mon, 29 Mar 2021 02:02:07 +0200 Subject: [PATCH 111/174] Fix for #12234 (#12236) --- source/virtualBuffers/gecko_ia2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/virtualBuffers/gecko_ia2.py b/source/virtualBuffers/gecko_ia2.py index b86ebef32a5..da9a2b01dbe 100755 --- a/source/virtualBuffers/gecko_ia2.py +++ b/source/virtualBuffers/gecko_ia2.py @@ -214,8 +214,8 @@ def _iterIdsToTryWithAccChild(cls, obj): yield accId def __contains__(self,obj): - if not ( - ( + if ( + not ( isinstance(obj, NVDAObjects.IAccessible.IAccessible) and isinstance(obj.IAccessibleObject, IA2.IAccessible2) ) From 55a9b32e4d3889c04a1ea4ad5834a59d8851e009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Tue, 30 Mar 2021 02:12:17 +0200 Subject: [PATCH 112/174] Ensure that `speech.cancelSpeech` clears instance of `SpeechWithoutPauses` which has been used for say all. (#12228) Fixes #12225 PR #12195 removed speech.speakWithoutPauses which was an alias for speech._speakWithoutPauses. While all usages of speech.speakWithoutPauses were found and fixed it turned out than when cancelling speech during say all speech._speakWithoutPauses was cleared rather than instance of SpeakWithoutPauses used for say all. Now the instance of SpeechWithoutPauses which is used during say all is cleared. A function is added to sayAllHandler which creates it first if necessary, and ensures that the single instance is being used for say all. --- source/sayAllHandler.py | 24 +++++++++++++++++------- source/speech/__init__.py | 5 +---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/source/sayAllHandler.py b/source/sayAllHandler.py index ff4b5884b7e..ae49cfccc22 100644 --- a/source/sayAllHandler.py +++ b/source/sayAllHandler.py @@ -3,6 +3,7 @@ # This file may be used under the terms of the GNU General Public License, version 2 or later. # For more details see: https://www.gnu.org/licenses/gpl-2.0.html +from typing import Optional import weakref import garbageHandler import speech @@ -17,9 +18,6 @@ from speech.commands import CallbackCommand, EndUtteranceCommand -speakWithoutPauses = speech.SpeechWithoutPauses(speakFunc=speech.speak).speakWithoutPauses - - CURSOR_CARET = 0 CURSOR_REVIEW = 1 @@ -28,6 +26,18 @@ #: This is a weakref because the manager should be allowed to die once say all is complete. _activeSayAll = lambda: None # Return None when called like a dead weakref. + +def getSpeechWithoutPauses() -> "speech.SpeechWithoutPauses": + """Returns an instance of `speech.SpeechWithoutPauses` which should be used for say all + creating it if necessary.""" + if getSpeechWithoutPauses.speechWithoutPausesInstance is None: + getSpeechWithoutPauses.speechWithoutPausesInstance = speech.SpeechWithoutPauses(speakFunc=speech.speak) + return getSpeechWithoutPauses.speechWithoutPausesInstance + + +getSpeechWithoutPauses.speechWithoutPausesInstance: Optional["speech.SpeechWithoutPauses"] = None + + def stop(): active = _activeSayAll() if active: @@ -154,7 +164,7 @@ def nextLine(self): if isinstance(self.reader.obj, textInfos.DocumentWithPageTurns): # Once the last line finishes reading, try turning the page. cb = CallbackCommand(self.turnPage, name="say-all:turnPage") - speakWithoutPauses([cb, EndUtteranceCommand()]) + getSpeechWithoutPauses().speakWithoutPauses([cb, EndUtteranceCommand()]) else: self.finish() return @@ -186,7 +196,7 @@ def _onLineReached(obj=self.reader.obj, state=state): seq = list(speech._flattenNestedSequences(speechGen)) seq.insert(0, cb) # Speak the speech sequence. - spoke = speakWithoutPauses(seq) + spoke = getSpeechWithoutPauses().speakWithoutPauses(seq) # Update the textInfo state ready for when speaking the next line. self.speakTextInfoState = state.copy() @@ -208,7 +218,7 @@ def _onLineReached(obj=self.reader.obj, state=state): else: # We don't want to buffer too much. # Force speech. lineReached will resume things when speech catches up. - speakWithoutPauses(None) + getSpeechWithoutPauses().speakWithoutPauses(None) # The first buffered line has now started speaking. self.numBufferedLines -= 1 @@ -245,7 +255,7 @@ def finish(self): # we might switch synths too early and truncate the final speech. # We do this by putting a CallbackCommand at the start of a new utterance. cb = CallbackCommand(self.stop, name="say-all:stop") - speakWithoutPauses([ + getSpeechWithoutPauses().speakWithoutPauses([ EndUtteranceCommand(), cb, EndUtteranceCommand() diff --git a/source/speech/__init__.py b/source/speech/__init__.py index befd122ebfe..cefdd72e2fe 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -117,7 +117,7 @@ def cancelSpeech(): # Import only for this function to avoid circular import. import sayAllHandler sayAllHandler.stop() - _speakWithoutPauses.reset() + sayAllHandler.getSpeechWithoutPauses().reset() if beenCanceled: return elif speechMode==speechMode_off: @@ -2534,9 +2534,6 @@ def _getSpeech( return finalSpeechSequence -_speakWithoutPauses = SpeechWithoutPauses(speakFunc=speak) - - #: The singleton _SpeechManager instance used for speech functions. #: @type: L{manager.SpeechManager} _manager = manager.SpeechManager() From 2e37433ea402b4f15d655ea995dd4397f965e4c9 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Wed, 31 Mar 2021 19:01:50 +1000 Subject: [PATCH 113/174] Microsoft Edge with UIA: move the browse mode caret to the target of a same-page link when it is activated, by supporting UI Automation's activeTextPositionChanged event. (#12242) When following a same-page link in a web browser, the page is scrolled to the target of the same-page link, and it is expected that the screen reader will move its browse mode caret to that location as well. Recently a new ActiveTextPositionchanged event was added to UI Automation to allow browsers to communicate this to screen readers. Only in the last week or so, Microsoft Edge Canary has started firing this event when same-page links are activated. NVDA should make use of this. NVDA now registers for UI Automation's ActiveTextPositionChanged event globally, and when one is received, fires its own UIA_activetextPositionChanged NvDA event. This NVDA event is implemented on UIABrowseModeDocument, which uses BrowseMode's _handleScrollTo to move the browse mode caret to the new location. _handleScrollTo had to be changed to accept not just an NvDAObject, but now also a TextInfo, as the ActiveTextPositionChanged event is actually passed a IUIAutomationTextRange. --- source/UIABrowseMode.py | 8 ++++++++ source/_UIAHandler.py | 37 ++++++++++++++++++++++++++++++++++++- source/browseMode.py | 21 ++++++++++++++------- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/source/UIABrowseMode.py b/source/UIABrowseMode.py index 7354f2e7a23..275aae674af 100644 --- a/source/UIABrowseMode.py +++ b/source/UIABrowseMode.py @@ -360,6 +360,14 @@ class UIABrowseModeDocument(UIADocumentWithTableNavigation,browseMode.BrowseMode # Because UIA TextRanges are opaque and are tied specifically to one particular document. shouldRememberCaretPositionAcrossLoads=False + def event_UIA_activeTextPositionChanged(self, obj, nextHandler, textRange=None): + if not self.isReady: + self._initialScrollObj = obj + return nextHandler() + scrollInfo = self.makeTextInfo(textRange) + if not self._handleScrollTo(scrollInfo): + return nextHandler() + def _iterNodesByType(self,nodeType,direction="next",pos=None): if nodeType.startswith("heading"): return UIAHeadingQuicknavIterator(nodeType,self,pos,direction=direction) diff --git a/source/_UIAHandler.py b/source/_UIAHandler.py index 8344a84cc09..2b33b5d1c28 100644 --- a/source/_UIAHandler.py +++ b/source/_UIAHandler.py @@ -234,7 +234,8 @@ class UIAHandler(COMObject): UIA.IUIAutomationEventHandler, UIA.IUIAutomationFocusChangedEventHandler, UIA.IUIAutomationPropertyChangedEventHandler, - UIA.IUIAutomationNotificationEventHandler + UIA.IUIAutomationNotificationEventHandler, + UIA.IUIAutomationActiveTextPositionChangedEventHandler, ] def __init__(self): @@ -381,6 +382,12 @@ def _registerGlobalEventHandlers(self): self.baseCacheRequest, self ) + if isinstance(self.clientObject, UIA.IUIAutomation6): + self.globalEventHandlerGroup.AddActiveTextPositionChangedEventHandler( + UIA.TreeScope_Subtree, + self.baseCacheRequest, + self + ) self.addEventHandlerGroup(self.rootElement, self.globalEventHandlerGroup) def _createLocalEventHandlerGroup(self): @@ -670,6 +677,34 @@ def IUIAutomationNotificationEventHandler_HandleNotificationEvent( return eventHandler.queueEvent("UIA_notification",obj, notificationKind=NotificationKind, notificationProcessing=NotificationProcessing, displayString=displayString, activityId=activityId) + def IUIAutomationActiveTextPositionChangedEventHandler_HandleActiveTextPositionChangedEvent( + self, + sender, + textRange + ): + if not self.MTAThreadInitEvent.isSet(): + # UIAHandler hasn't finished initialising yet, so just ignore this event. + if _isDebug(): + log.debug("HandleActiveTextPositionchangedEvent: event received while not fully initialized") + return + import NVDAObjects.UIA + try: + obj = NVDAObjects.UIA.UIA(UIAElement=sender) + except Exception: + if _isDebug(): + log.debugWarning( + "HandleActiveTextPositionChangedEvent: Exception while creating object: ", + exc_info=True + ) + return + if not obj: + if _isDebug(): + log.debug( + "HandleActiveTextPositionchangedEvent: Ignoring because no object: " + ) + return + eventHandler.queueEvent("UIA_activeTextPositionChanged", obj, textRange=textRange) + def _isBadUIAWindowClassName(self, windowClass): "Given a windowClassName, returns True if this is a known problematic UIA implementation." # #7497: Windows 10 Fall Creators Update has an incomplete UIA diff --git a/source/browseMode.py b/source/browseMode.py index e98c2b49252..f4acc4b5005 100644 --- a/source/browseMode.py +++ b/source/browseMode.py @@ -4,6 +4,7 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. +from typing import Union import os import itertools import collections @@ -1633,14 +1634,15 @@ def event_gainFocus(self, obj, nextHandler): event_gainFocus.ignoreIsReady=True - def _handleScrollTo(self, obj): + def _handleScrollTo( + self, + obj: Union[NVDAObject, textInfos.TextInfo], + ) -> bool: """Handle scrolling the browseMode document to a given object in response to an event. Subclasses should call this from an event which indicates that the document has scrolled. @postcondition: The virtual caret is moved to L{obj} and the buffer content for L{obj} is reported. @param obj: The object to which the document should scroll. - @type obj: L{NVDAObjects.NVDAObject} @return: C{True} if the document was scrolled, C{False} if not. - @rtype: bool @note: If C{False} is returned, calling events should probably call their nextHandler. """ if self.programmaticScrollMayFireEvent and self._lastProgrammaticScrollTime and time.time() - self._lastProgrammaticScrollTime < 0.4: @@ -1649,10 +1651,15 @@ def _handleScrollTo(self, obj): # However, pretend we handled it, as we don't want it to be passed on to the object either. return True - try: - scrollInfo = self.makeTextInfo(obj) - except: - return False + if isinstance(obj, NVDAObject): + try: + scrollInfo = self.makeTextInfo(obj) + except (NotImplementedError, RuntimeError): + return False + elif isinstance(obj, textInfos.TextInfo): + scrollInfo = obj.copy() + else: + raise ValueError(f"{obj} is not a supported type") #We only want to update the caret and speak the field if we're not in the same one as before caretInfo=self.makeTextInfo(textInfos.POSITION_CARET) From ebe1ab5478629d037fd45b2b81a4942aadb42c6d Mon Sep 17 00:00:00 2001 From: Ty Gillespie <60036684+trypolis464@users.noreply.github.com> Date: Mon, 5 Apr 2021 20:31:36 -0600 Subject: [PATCH 114/174] Fixed a typo in changes file (autoSettingUtils to autoSettingsUtils). (#12263) Co-authored-by: trypolis <60036684+TyGillespie@users.noreply.github.com> --- user_docs/en/changes.t2t | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 505f76e946f..b87c5dcf0b2 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -85,8 +85,8 @@ What's New in NVDA - `speakTextInfo` will no longer send speech through `speakWithoutPauses` if reason is `SAYALL`, as `sayAllhandler` does this manually now. (#12150) - The `synthDriverHandler` module is no longer star imported into `globalCommands` and `gui.settingsDialogs` - use `from synthDriverHandler import synthFunctionExample` instead. (#12172) - `ROLE_EQUATION` has been removed from controlTypes - use `ROLE_MATH`` instead. (#12164) -- `autoSettingsUtils.driverSetting` classes are removed from `driverHandler` - please use them from `autoSettingUtils.driverSetting`. (#12168) -- `autoSettingsUtils.utils` classes are removed from `driverHandler` - please use them from `autoSettingUtils.utils`. (#12168) +- `autoSettingsUtils.driverSetting` classes are removed from `driverHandler` - please use them from `autoSettingsUtils.driverSetting`. (#12168) +- `autoSettingsUtils.utils` classes are removed from `driverHandler` - please use them from `autoSettingsUtils.utils`. (#12168) - Support of `TextInfo`s that do not inherit from `contentRecog.BaseContentRecogTextInfo` is removed. (#12157) - `speech.speakWithoutPauses` has been removed - please use `speech.SpeechWithoutPauses(speakFunc=speech.speak).speakWithoutPauses` instead. (#12195) - `speech.re_last_pause` has been removed - please use `speech.SpeechWithoutPauses.re_last_pause` instead. (#12195) From 0efeee4b7dbdd243a7b880cf08dcbef284b48896 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Tue, 6 Apr 2021 21:06:04 +1000 Subject: [PATCH 115/174] Ensure TextInfo.getTextInChunks does not freeze, and provide new friendly compareable TextInfo endpoint properties (#12253) TextInfo.getTextInChunks could sometimes end up in an infinit loop when dealing with particular TextInfo implementations such as ITextDocument where setting start to the end of the document results in the start never quite getting there. To work around this specific freeze, getTextInChunks now double checks that the start has really moved to the end of the last chunk. If not, we break out of the loop. However, for a long time now we have wanted to make these TextInfo comparisons much more readable, which can aide in finding logic errors much easier. To that end, TextInfo objects now have start and end properties, which can be compared mathematically, and also set from another, removing the need to use compareEndPoints or setEndPoint. Some examples: Setting a's end to b's start Original code: a.setEndPoint(b, "endToStart") New code: a.end = b.start Is a's start at or past b's end? Original code: a.compareEndPoints(b, "startToEnd") >= 0 New code: a.start >= b.end TextInfo.getTextInChunks has been rewritten to use these new properties. --- source/textInfos/__init__.py | 114 +++++++++++++++++++++++++++++++++-- tests/unit/test_textInfos.py | 42 +++++++++++++ user_docs/en/changes.t2t | 7 +++ 3 files changed, 157 insertions(+), 6 deletions(-) diff --git a/source/textInfos/__init__.py b/source/textInfos/__init__.py index 7bedfa93823..6e6efdb88d1 100755 --- a/source/textInfos/__init__.py +++ b/source/textInfos/__init__.py @@ -12,13 +12,21 @@ from abc import abstractmethod import weakref import re -from typing import Any, Union, List, Optional, Dict +from typing import ( + Any, + Union, + List, + Optional, + Dict, + Tuple, +) import baseObject import config import controlTypes from controlTypes import OutputReason import locationHelper +from logHandler import log SpeechSequence = List[Union[Any, str]] @@ -307,6 +315,24 @@ def __init__(self,obj,position): #: The position with which this instance was constructed. self.basePosition=position + #: Typing information for auto-property: start + start: "TextInfoEndpoint" + + def _get_start(self) -> "TextInfoEndpoint": + return TextInfoEndpoint(self, True) + + def _set_start(self, otherEndpoint: "TextInfoEndpoint"): + self.start.moveTo(otherEndpoint) + + #: Typing information for auto-property: end + end: "TextInfoEndpoint" + + def _get_end(self) -> "TextInfoEndpoint": + return TextInfoEndpoint(self, False) + + def _set_end(self, otherEndpoint: "TextInfoEndpoint"): + self.end.moveTo(otherEndpoint) + def _get_obj(self): """The object containing the range of text being represented.""" return self._obj() @@ -521,15 +547,18 @@ def getTextInChunks(self, unit): """ unitInfo=self.copy() unitInfo.collapse() - while unitInfo.compareEndPoints(self,"startToEnd")<0: + while unitInfo.start < self.end: unitInfo.expand(unit) chunkInfo=unitInfo.copy() - if chunkInfo.compareEndPoints(self,"startToStart")<0: - chunkInfo.setEndPoint(self,"startToStart") - if chunkInfo.compareEndPoints(self,"endToEnd")>0: - chunkInfo.setEndPoint(self,"endToEnd") + if chunkInfo.start < self.start: + chunkInfo.start = self.start + if chunkInfo.end > self.end: + chunkInfo.end = self.end yield chunkInfo.text unitInfo.collapse(end=True) + if unitInfo.start < chunkInfo.end: + log.debugWarning("Could not move TextInfo completely to end, breaking") + break def getControlFieldSpeech( self, @@ -624,3 +653,76 @@ def turnPage(self, previous=False): @raise RuntimeError: If there are no further pages. """ raise NotImplementedError + + +class TextInfoEndpoint: + """ + Represents one end of a TextInfo instance. + This object can be compared with another end from the same or a different TextInfo instance, + Using the standard math comparison operators: + < <= == != >= > + """ + + _whichMap: Dict[Tuple[bool, bool], str] = { + (True, True): "startToStart", + (True, False): "startToEnd", + (False, True): "endToStart", + (False, False): "endToEnd", + } + + def _cmp(self, other: "TextInfoEndpoint") -> int: + """ + A standard cmp function returning: + -1 for less than, 0 for equal and 1 for greater than. + """ + if ( + not isinstance(other, TextInfoEndpoint) + or not isinstance(other.textInfo, type(self.textInfo)) + ): + raise ValueError(f"Cannot compare endpoint with different type: {other}") + return self.textInfo.compareEndPoints(other.textInfo, self._whichMap[self.isStart, other.isStart]) + + def __init__( + self, + textInfo: TextInfo, + isStart: bool + ): + """ + @param textInfo: the TextInfo instance you wish to represent an endpoint of. + @param isStart: true to represent the start, false for the end. + """ + self.textInfo = textInfo + self.isStart = isStart + + def __lt__(self, other) -> bool: + return self._cmp(other) < 0 + + def __le__(self, other) -> bool: + return self._cmp(other) <= 0 + + def __eq__(self, other) -> bool: + return self._cmp(other) == 0 + + def __ne__(self, other) -> bool: + return self._cmp(other) != 0 + + def __ge__(self, other) -> bool: + return self._cmp(other) >= 0 + + def __gt__(self, other) -> bool: + return self._cmp(other) > 0 + + def moveTo(self, other: "TextInfoEndpoint") -> None: + """ + Moves the end of the TextInfo this endpoint represents to the position of the given endpoint. + """ + if ( + not isinstance(other, TextInfoEndpoint) + or not isinstance(other.textInfo, type(self.textInfo)) + ): + raise ValueError(f"Cannot move endpoint to different type: {other}") + self.textInfo.setEndPoint(other.textInfo, self._whichMap[(self.isStart, other.isStart)]) + + def __repr__(self): + endpointLabel = "start" if self.isStart else "end" + return f"{endpointLabel} endpoint of {self.textInfo}" diff --git a/tests/unit/test_textInfos.py b/tests/unit/test_textInfos.py index b9615f5f528..15393e2fd96 100644 --- a/tests/unit/test_textInfos.py +++ b/tests/unit/test_textInfos.py @@ -131,3 +131,45 @@ def test_mixedSurrogatePairsNonSurrogatesAndSingleSurrogatesBackward(self): ti.move(textInfos.UNIT_CHARACTER, -1) ti.expand(textInfos.UNIT_CHARACTER) # Range at a self.assertEqual(ti.offsets, (0, 1)) # One offset + + +class TestEndpoints(unittest.TestCase): + + def test_TextInfoEndpoint_largerAndSmaller(self): + obj = BasicTextProvider(text="abcdef") + ti = obj.makeTextInfo(Offsets(0, 2)) + smaller = ti.start + larger = ti.end + self.assertTrue(smaller < larger) + self.assertFalse(larger < smaller) + self.assertTrue(smaller <= larger) + self.assertFalse(larger <= smaller) + self.assertFalse(smaller >= larger) + self.assertTrue(larger >= smaller) + self.assertFalse(smaller > larger) + self.assertTrue(larger > smaller) + self.assertTrue(smaller != larger) + self.assertTrue(larger != smaller) + + def test_TextInfoEndpoint_equal(self): + obj = BasicTextProvider(text="abcdef") + ti = obj.makeTextInfo(Offsets(1, 1)) + self.assertTrue(ti.start == ti.end) + self.assertFalse(ti.start != ti.end) + self.assertFalse(ti.start < ti.end) + self.assertTrue(ti.start <= ti.end) + self.assertTrue(ti.start >= ti.end) + self.assertFalse(ti.start > ti.end) + + def test_setEndpoint(self): + obj = BasicTextProvider(text="abcdef") + ti1 = obj.makeTextInfo(Offsets(0, 2)) + ti2 = obj.makeTextInfo(Offsets(3, 5)) + ti1.end = ti2.end + self.assertEqual((ti1._startOffset, ti1._endOffset), (0, 5)) + ti1.start = ti2.start + self.assertEqual((ti1._startOffset, ti1._endOffset), (3, 5)) + ti1.end = ti2.start + self.assertEqual((ti1._startOffset, ti1._endOffset), (3, 3)) + ti1.start = ti2.end + self.assertEqual((ti1._startOffset, ti1._endOffset), (5, 5)) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index b87c5dcf0b2..1a4e77befb0 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -40,6 +40,7 @@ What's New in NVDA - Respect the GUI layout direction based on the NVDA language, not the system locale. (#638) - known issue for right-to-left languages: the right border of groupings clips with labels/controls. (#12181) - The python locale is set to match the language selected in preferences consistently, and will occur when using the default language. (#12214) +- - TextInfo.getTextInChunks no longer freezes when called on Rich Edit controls such as the NVDA log viewer. (#11613) == Changes for Developers == @@ -100,6 +101,12 @@ What's New in NVDA - Function winVersion.getWinVer has been added to get a winVersion.WinVersion representing the currently running OS. - Convenience constants have been added for known Windows releases, see winVersion.WIN* constants. - IAccessibleHandler no longer star imports everything from IAccessible and IA2 COM interfaces - please use them directly. (#12232) +- TextInfo objects now have start and end properties which can be compared mathematically with operators such as < <= == != >= >. (#11613) + - E.g. ti1.start <= ti2.end + - This usage is now prefered instead of ti1.compareEndPoints(ti2,"startToEnd") <= 0 +- TextInfo start and end properties can also be set to each other. + - E.g. ti1.start = ti2.end + - This usage is prefered instead of ti1.SetEndPoint(ti2,"startToEnd") = 2020.4 = From 62eaba526a8b9876e3bf8dff3c7e51e7a6608748 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Wed, 7 Apr 2021 16:48:30 +1000 Subject: [PATCH 116/174] UIA NvDAObject's selectionContainer property: protect against a NULL selectionContainer on UIA's SelectionItemPattern, such as for Lutlook Attachment lists. (#12274) After merging of pr #12210 , The attachment list in Outlook no longer read with NVDA, and an error was produced in the log. It seems that Outlook's attachment list, gives back a NULL pointer for the SelectionContainer property on its UIA SelectionItemPattern. This commit Checks to see if CurrentSelectionContainer is NULL and returns None as there is nothing further we can do with that pattern. Fixes #12265 --- source/NVDAObjects/UIA/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index 660bd37d596..31c95b7466e 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -1217,6 +1217,10 @@ def _get_selectionContainer(self) -> "typing.Optional[UIA]": if not p: return None e = p.currentSelectionContainer + if not e: + # Some implementations of SelectionItemPattern, such as the Outlook attachment list + # give back a NULL selectionContainer + return None e = e.buildUpdatedCache(UIAHandler.handler.baseCacheRequest) obj = UIA(UIAElement=e) if obj.UIASelectionPattern2: From 1fdf8ec7c956938f2d9bdf14ffa839e624d6b288 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Thu, 8 Apr 2021 16:59:13 +1000 Subject: [PATCH 117/174] Ensure AppVeyor message is added for translation comment errors (#12283) Post a relevant message to appveyor and the PR when a build fails due to translation comments missing or unexpectedly included --- appveyor.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 0072099bee0..ab587f59eaa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -98,8 +98,13 @@ build_script: - scons source %sconsArgs% - if ERRORLEVEL 1 exit %ERRORLEVEL% # We don't need launcher to run checkPot, so run the checkPot before launcher. - - scons checkPot %sconsArgs% - - if ERRORLEVEL 1 exit %ERRORLEVEL% + - ps: | + cmd.exe /c "scons checkPot $sconsArgs" + if($LastExitCode -ne 0) { + $errorCode=$LastExitCode + Add-AppveyorMessage "Translation comments missing or unexpectedly included." + } + if ($errorCode -ne 0) { $host.SetShouldExit($errorCode) } # The pot gets built by tests, but we don't actually need it as a build artifact. - 'echo scons output targets: %sconsOutTargets%' - scons %sconsOutTargets% %sconsArgs% From 911d712b3153f7f60b96889bbac02776e9f67c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Fri, 9 Apr 2021 02:22:14 +0200 Subject: [PATCH 118/174] Fix NVDA crash when formatting a timestamp of a log entry under some versions of Universal CRT (#12250) Under some versions of Universal CRT Python can crash when formatting time with time.localtime and locale is set to a language which contains underscore it its name. For users this manifests in a crash of NVDA since default log formatter attempts to format current time with time.localtime when writing anything to the log. WX Python no longer tries to set Python locale to an nonexistent one when creating an instance of wx.app. While this is not necessary any longer when time.localtime is not used we are managing locale ourselves in languageHandler so it makes sense to do it just in one place. When formatting time of the log entries time.localtime is not used any more * Backport of `InitLocale` from Wx Python 4.1.2 to workaround crash encountered under particular versions of Universal CRT. * Custom implementation of `formatTime` for log formatter using win32 API calls to avoid crashes under Server 2019 and Windows 10 1809 * Lint for winKernel: - Remove star imports - Remove duplicate definition of openProcess * logHandler: move import of winKernel to the top of the file and remove unused imports. Co-authored-by: buddsean --- source/core.py | 8 +++++ source/logHandler.py | 17 +++++++-- source/winKernel.py | 75 ++++++++++++++++++++++++++++++++++------ user_docs/en/changes.t2t | 1 + 4 files changed, 89 insertions(+), 12 deletions(-) diff --git a/source/core.py b/source/core.py index 9854cdbc12f..5c6f56b8a8f 100644 --- a/source/core.py +++ b/source/core.py @@ -317,6 +317,14 @@ class App(wx.App): def OnAssert(self,file,line,cond,msg): message="{file}, line {line}:\nassert {cond}: {msg}".format(file=file,line=line,cond=cond,msg=msg) log.debugWarning(message,codepath="WX Widgets",stack_info=True) + + def InitLocale(self): + # Backport of `InitLocale` from wx Python 4.1.2 as the current version tries to set a Python + # locale to an nonexistent one when creating an instance of `wx.App`. + # This causes a crash when running under a particular version of Universal CRT (#12160) + import locale + locale.setlocale(locale.LC_ALL, "C") + app = App(redirect=False) # We support queryEndSession events, but in general don't do anything for them. # However, when running as a Windows Store application, we do want to request to be restarted for updates diff --git a/source/logHandler.py b/source/logHandler.py index 6fa34b52a6f..56f4ed98e7d 100755 --- a/source/logHandler.py +++ b/source/logHandler.py @@ -10,13 +10,13 @@ import ctypes import sys import warnings -from encodings import utf_8 import logging import inspect import winsound import traceback -from types import MethodType, FunctionType +from types import FunctionType import globalVars +import winKernel import buildVersion from typing import Optional @@ -288,6 +288,19 @@ class Formatter(logging.Formatter): def formatException(self, ex): return stripBasePathFromTracebackText(super(Formatter, self).formatException(ex)) + def formatTime(self, record: logging.LogRecord, datefmt: Optional[str] = None) -> str: + """Custom implementation of `formatTime` which avoids `time.localtime` + since it causes a crash under some versions of Universal CRT ( #12160, Python issue 36792) + """ + timeAsFileTime = winKernel.time_tToFileTime(record.created) + timeAsSystemTime = winKernel.SYSTEMTIME() + winKernel.FileTimeToSystemTime(timeAsFileTime, timeAsSystemTime) + timeAsLocalTime = winKernel.SYSTEMTIME() + winKernel.SystemTimeToTzSpecificLocalTime(None, timeAsSystemTime, timeAsLocalTime) + res = f"{timeAsLocalTime.wHour:02d}:{timeAsLocalTime.wMinute:02d}:{timeAsLocalTime.wSecond:02d}" + return self.default_msec_format % (res, record.msecs) + + class StreamRedirector(object): """Redirects an output stream to a logger. """ diff --git a/source/winKernel.py b/source/winKernel.py index 75575f8bf8a..4d616e04b04 100644 --- a/source/winKernel.py +++ b/source/winKernel.py @@ -1,17 +1,17 @@ -#winKernel.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2006-2019 NV Access Limited, Rui Batista, Aleksey Sadovoy, Peter Vagner, Mozilla Corporation, Babbage B.V., Joseph Lee -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2006-2021 NV Access Limited, Rui Batista, Aleksey Sadovoy, Peter Vagner, +# Mozilla Corporation, Babbage B.V., Joseph Lee, Łukasz Golonka +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. """Functions that wrap Windows API functions from kernel32.dll and advapi32.dll""" +from typing import Union import contextlib import ctypes import ctypes.wintypes -from ctypes import WinError -from ctypes import * -from ctypes.wintypes import * +from ctypes import byref, c_byte, POINTER, sizeof, Structure, windll, WinError +from ctypes.wintypes import BOOL, DWORD, HANDLE, LARGE_INTEGER, LPWSTR, LPVOID, WORD kernel32=ctypes.windll.kernel32 advapi32 = windll.advapi32 @@ -160,6 +160,63 @@ class SYSTEMTIME(ctypes.Structure): ("wMilliseconds", WORD) ) + +class FILETIME(Structure): + _fields_ = ( + ("dwLowDateTime", DWORD), + ("dwHighDateTime", DWORD) + ) + + +class TIME_ZONE_INFORMATION(Structure): + _fields_ = ( + ("Bias", ctypes.wintypes.LONG), + ("StandardName", ctypes.wintypes.WCHAR * 32), + ("StandardDate", SYSTEMTIME), + ("StandardBias", ctypes.wintypes.LONG), + ("DaylightName", ctypes.wintypes.WCHAR * 32), + ("DaylightDate", SYSTEMTIME), + ("DaylightBias", ctypes.wintypes.LONG) + ) + + +def time_tToFileTime(time_tToConvert: float) -> FILETIME: + """Converts time_t as returned from `time.time` to a FILETIME structure. + Based on a code snipped from: + https://docs.microsoft.com/en-us/windows/win32/sysinfo/converting-a-time-t-value-to-a-file-time + """ + timeAsFileTime = FILETIME() + res = (int(time_tToConvert) * 10000000) + 116444736000000000 + timeAsFileTime.dwLowDateTime = res + timeAsFileTime.dwHighDateTime = res >> 32 + return timeAsFileTime + + +def FileTimeToSystemTime(lpFileTime: FILETIME, lpSystemTime: SYSTEMTIME) -> None: + if kernel32.FileTimeToSystemTime(byref(lpFileTime), byref(lpSystemTime)) == 0: + raise WinError() + + +def SystemTimeToTzSpecificLocalTime( + lpTimeZoneInformation: Union[TIME_ZONE_INFORMATION, None], + lpUniversalTime: SYSTEMTIME, + lpLocalTime: SYSTEMTIME +) -> None: + """Wrapper for `SystemTimeToTzSpecificLocalTime` from kernel32. + :param lpTimeZoneInformation: Either TIME_ZONE_INFORMATION containing info about the desired time zone + or `None` when the current time zone as configured in Windows settings should be used. + :param lpUniversalTime: SYSTEMTIME structure containing time in UTC wwhich you wish to convert. + : param lpLocalTime: A SYSTEMTIME structure in which time converted to the desired time zone would be placed. + :raises WinError + """ + if lpTimeZoneInformation is not None: + lpTimeZoneInformation = byref(lpTimeZoneInformation) + if kernel32.SystemTimeToTzSpecificLocalTime( + lpTimeZoneInformation, byref(lpUniversalTime), byref(lpLocalTime) + ) == 0: + raise WinError() + + def GetDateFormatEx(Locale,dwFlags,date,lpFormat): if date is not None: date=SYSTEMTIME(date.year,date.month,0,date.day,date.hour,date.minute,date.second,0) @@ -182,8 +239,6 @@ def GetTimeFormatEx(Locale,dwFlags,date,lpFormat): kernel32.GetTimeFormatEx(Locale,dwFlags,lpTime,lpFormat, buf, bufferLength) return buf.value -def openProcess(*args): - return kernel32.OpenProcess(*args) def virtualAllocEx(*args): res = kernel32.VirtualAllocEx(*args) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 1a4e77befb0..efa4dd72fb5 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -41,6 +41,7 @@ What's New in NVDA - known issue for right-to-left languages: the right border of groupings clips with labels/controls. (#12181) - The python locale is set to match the language selected in preferences consistently, and will occur when using the default language. (#12214) - - TextInfo.getTextInChunks no longer freezes when called on Rich Edit controls such as the NVDA log viewer. (#11613) +- It is once again possible to use NVDA in a languages containing underscores in the locale name such as de_CH on Windows 10 1803 and 1809. (#12250) == Changes for Developers == From 49a857888990c33fe44b28a309e9aed7b353713f Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Fri, 9 Apr 2021 15:15:02 +0800 Subject: [PATCH 119/174] Update espeak-ng to 1.51-dev commit cad1c8e8 (PR #12280) Adds readme for espeak, including how to update espeak submodule, sourced from: https://github.com/nvaccess/nvda/wiki/Updating-eSpeak-NG --- include/espeak | 2 +- include/espeak.md | 55 ++++++++++++++++++++++++++++++++++++ nvdaHelper/espeak/sconscript | 1 + readme.md | 2 +- user_docs/en/changes.t2t | 2 +- 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 include/espeak.md diff --git a/include/espeak b/include/espeak index 53915bf0a7c..cad1c8e87fc 160000 --- a/include/espeak +++ b/include/espeak @@ -1 +1 @@ -Subproject commit 53915bf0a7cd48f90c4a38ac52fff697723d9f4d +Subproject commit cad1c8e87fcccf677a445202e340f61980450a84 diff --git a/include/espeak.md b/include/espeak.md new file mode 100644 index 00000000000..3fcca61821d --- /dev/null +++ b/include/espeak.md @@ -0,0 +1,55 @@ +# Espeak-ng submodule + +The submodule contained in the espeak directory is a cross platform open source speech synthesizer. + +### Background +The main authority on build requirements should be `/include/espeak/Makefile.am`. +The `*.vcxproj` files in `/include/espeak/src/windows/` can also be considered, +however these are not always kept up to date. + +We don't use the auto make files or the visual studio files, we maintain our own method of building espeak. +Modifications will need to be made in `/nvdaHelper/espeak` +* `sconscript` for the build process. +* `config.h` to set the eSpeak-ng version that NVDA outputs to the log file. + +### Doing the update + +1. Start from a clean branch off of NVDA `master` + 1. Check out the latest NVDA `origin/master` and create a new branch. + 1. Do a git clean to ensure the working directory is clean. +1. Ensure submodules are up to date + 1. Synchronize submodules with `git submodule sync` + 1. Update submodules with `git submodule update --init --recursive` +1. Checkout the new eSpeak-ng revision to try. + 1. Change to the `include/espeak/` directory + 1. Do `git fetch` to get the latest from the espeak-ng repo + 1. Do `git checkout origin/master` or whichever espeak-ng ref you wish. +1. Look at changes in `Makefile.am` and update our build. + 1. Diff `Makefile.am` with the previously used commit of espeak. + 1. Some modules are intentionally excluded from the build. + If unsure, err on the side of including it and raise it as a question when submitting a PR. + 1. Modify the `/nvdaHelper/espeak/config.h` file as required. +1. Update our record of the version number and build. + 1. Change back to the NVDA repo root + 1. Update the package version in `/nvdaHelper/espeak/config.h` + - Compare to espeak source info: `/include/espeak/src/windows/config.h`. + 1. Update NVDA `readme.md` with espeak version and commit. + 1. Build NVDA +1. Run NVDA (set eSpeak-ng as the synthesizer) and test. +1. Ensure that the log file contains the new version number for eSpeak-NG + +### Troubleshooting + +If python crashes while building, check the log. +If the last thing is compiling some dictionary try excluding it. +This can be done in `/nvdaHelper/espeak/sconscript`. +Remember to report this to the eSpeak-ng project. + +If the build fails, take note of the error, compare the diff of the `Makefile.am` file and mirror +any changes in our `sconscript` file. + +### Known issues +Due to problems with emoji support (causing crashes), emoji dictionary files are being excluded +from the build, they are deleted prior to compiling the dictionaries in the +`/nvdaHelper/espeak/sconscript` file. + diff --git a/nvdaHelper/espeak/sconscript b/nvdaHelper/espeak/sconscript index 7ce88e9df77..51838d16811 100644 --- a/nvdaHelper/espeak/sconscript +++ b/nvdaHelper/espeak/sconscript @@ -138,6 +138,7 @@ espeakLib=env.SharedLibrary( "phonemelist.c", "readclause.c", "setlengths.c", + "soundIcon.c", "spect.c", "speech.c", "ssml.c", diff --git a/readme.md b/readme.md index 1c8d3211301..f2abd157982 100644 --- a/readme.md +++ b/readme.md @@ -81,7 +81,7 @@ If you aren't sure, run `git submodule update` after every git pull, merge or ch For reference, the following run time dependencies are included in Git submodules: -* [eSpeak NG](https://github.com/espeak-ng/espeak-ng), version 1.51-dev commit 53915bf0a +* [eSpeak NG](https://github.com/espeak-ng/espeak-ng), version 1.51-dev commit cad1c8e8 * [Sonic](https://github.com/waywardgeek/sonic), commit 4f8c1d11 * [IAccessible2](https://wiki.linuxfoundation.org/accessibility/iaccessible2/start), commit cbc1f29631780 * [liblouis](http://www.liblouis.org/), version 3.17.0 diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index efa4dd72fb5..a4e3e078997 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -20,7 +20,7 @@ What's New in NVDA - 'Attempt to cancel speech for expired focus events' option in the advanced settings panel now enabled by default. - This behaviour can be disabled by default with by setting this option to "No". - Web applications (E.G. Gmail) no longer speak outdated information when moving focus rapidly. (#10885) -- Espeak-ng has been updated to 1.51-dev commit 53915bf0a7cd48f90c4a38ac52fff697723d9f4d. (#12202) +- Espeak-ng has been updated to 1.51-dev commit cad1c8e87fcccf677a445202e340f61980450a84. (#12202, #12280) - Updated liblouis braille translator to [3.17.0 https://github.com/liblouis/liblouis/releases/tag/v3.17.0]. (#12137) - New braille tables: Belarusian literary braille, Belarusian computer braille, Urdu grade 1, Urdu grade 2. - Support for Adobe Flash content has been removed from NVDA due to the use of Flash being actively discouraged by Adobe. (#11131) From db84870b3a045e12939ddfc270d86ebd8c8488be Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Tue, 6 Apr 2021 01:02:58 -0400 Subject: [PATCH 120/174] Bug issue template: Rewrote COM Reg. Fix Tool question, to make user more likely to run it before submitting --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index cf48177814b..b5eaccfb307 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -35,4 +35,4 @@ Please also note that the NVDA project has a Citizen and Contributor Code of Con #### If add-ons are disabled, is your problem still occurring? -#### Did you try to run the COM registry fixing tool in NVDA menu / tools? +#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu?? From bd12c8859f9e6a654ea59049aa157e059391e9e9 Mon Sep 17 00:00:00 2001 From: Luke Davis <8139760+XLTechie@users.noreply.github.com> Date: Tue, 13 Apr 2021 23:10:38 -0400 Subject: [PATCH 121/174] Fixed typo in db84870b3a045e12939 (PR #12276). (#12290) --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index b5eaccfb307..2c86bc249e9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -35,4 +35,4 @@ Please also note that the NVDA project has a Citizen and Contributor Code of Con #### If add-ons are disabled, is your problem still occurring? -#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu?? +#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu? From 35ceac295888a6fef532ffb65e1a09d616944884 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 15 Apr 2021 18:40:32 +0800 Subject: [PATCH 122/174] Improve logging of test for panel destruction (PR #12291) * Reduces the log noise during destruction of SettingsDialogs * Makes the log easier to follow, objects and constants are now more clearly described. * Fixes a space in the import statement, this was a typo and is strange style. --- source/_UIAHandler.py | 2 +- source/gui/__init__.py | 14 ++++++++--- source/gui/settingsDialogs.py | 46 ++++++++++++++++++++++++----------- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/source/_UIAHandler.py b/source/_UIAHandler.py index 2b33b5d1c28..76e0047d0a3 100644 --- a/source/_UIAHandler.py +++ b/source/_UIAHandler.py @@ -37,7 +37,7 @@ import UIAUtils from comInterfaces import UIAutomationClient as UIA # F403: unable to detect undefined names -from comInterfaces .UIAutomationClient import * # noqa: F403 +from comInterfaces.UIAutomationClient import * # noqa: F403 import textInfos from typing import Dict from queue import Queue diff --git a/source/gui/__init__.py b/source/gui/__init__.py index 43b1c80b5d4..b812bab9324 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -5,6 +5,7 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. +import typing import time import os import sys @@ -24,6 +25,7 @@ import queueHandler import core from . import guiHelper +from . import settingsDialogs from .settingsDialogs import * from .inputGestures import InputGesturesDialog import speechDictHandler @@ -585,10 +587,16 @@ def terminate(): import brailleViewer brailleViewer.destroyBrailleViewer() - for instance, state in gui.SettingsDialog._instances.items(): - if state is gui.SettingsDialog._DIALOG_DESTROYED_STATE: + # prevent race condition with object deletion + # prevent deletion of the object while we work on it. + _SettingsDialog = settingsDialogs.SettingsDialog + nonWeak: typing.Dict[_SettingsDialog, _SettingsDialog] = dict(_SettingsDialog._instances) + + for instance, state in nonWeak.items(): + if state is _SettingsDialog.DialogState.DESTROYED: log.error( - "Destroyed but not deleted instance of settings dialog exists: {!r}".format(instance) + "Destroyed but not deleted instance of gui.SettingsDialog exists" + f": {instance.title} - {instance.__class__.__qualname__} - {instance}" ) else: log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance)) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 37c306f0ce7..7201aa2a323 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -9,7 +9,9 @@ from abc import ABCMeta, abstractmethod import copy import os +from enum import IntEnum +import typing import wx from vision.providerBase import VisionEnhancementProviderSettings from wx.lib import scrolledpanel @@ -80,10 +82,12 @@ class SettingsDialog( class MultiInstanceError(RuntimeError): pass - _DIALOG_CREATED_STATE = 0 - _DIALOG_DESTROYED_STATE = 1 + class DialogState(IntEnum): + CREATED = 0 + DESTROYED = 1 + # holds instances of SettingsDialogs as keys, and state as the value - _instances=weakref.WeakKeyDictionary() + _instances = weakref.WeakKeyDictionary() title = "" helpId = "NVDASettings" shouldSuspendConfigProfileTriggers = True @@ -102,25 +106,39 @@ def __new__(cls, *args, **kwargs): "Creating new settings dialog (multiInstanceAllowed:{}). " "State of _instances {!r}".format(multiInstanceAllowed, instancesState) ) - if state is cls._DIALOG_CREATED_STATE and not multiInstanceAllowed: + if state is cls.DialogState.CREATED and not multiInstanceAllowed: raise SettingsDialog.MultiInstanceError("Only one instance of SettingsDialog can exist at a time") - if state is cls._DIALOG_DESTROYED_STATE and not multiInstanceAllowed: + if state is cls.DialogState.DESTROYED and not multiInstanceAllowed: # the dialog has been destroyed by wx, but the instance is still available. This indicates there is something # keeping it alive. log.error("Opening new settings dialog while instance still exists: {!r}".format(firstMatchingInstance)) obj = super(SettingsDialog, cls).__new__(cls, *args, **kwargs) - SettingsDialog._instances[obj] = cls._DIALOG_CREATED_STATE + SettingsDialog._instances[obj] = cls.DialogState.CREATED return obj def _setInstanceDestroyedState(self): - if log.isEnabledFor(log.DEBUG): - instancesState = dict(SettingsDialog._instances) - log.debug( - "Setting state to destroyed for instance: {!r}\n" - "Current _instances {!r}".format(self, instancesState) - ) - if self in SettingsDialog._instances: - SettingsDialog._instances[self] = self._DIALOG_DESTROYED_STATE + # prevent race condition with object deletion + # prevent deletion of the object while we work on it. + nonWeak: typing.Dict[SettingsDialog, SettingsDialog.DialogState] = dict(SettingsDialog._instances) + + if ( + self in SettingsDialog._instances + # Because destroy handlers are use evt.skip, _setInstanceDestroyedState may be called many times + # prevent noisy logging. + and self.DialogState.DESTROYED != SettingsDialog._instances[self] + ): + if log.isEnabledFor(log.DEBUG): + instanceStatesGen = ( + f"{instance.title} - {state.name}" + for instance, state in nonWeak.items() + ) + instancesList = list(instanceStatesGen) + log.debug( + f"Setting state to destroyed for instance: {self.title} - {self.__class__.__qualname__} - {self}\n" + f"Current _instances {instancesList}" + ) + SettingsDialog._instances[self] = self.DialogState.DESTROYED + def __init__( self, parent, From 516706dd96a3cda25af87e325db7f14591533a3d Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Fri, 16 Apr 2021 12:51:53 +0800 Subject: [PATCH 123/174] Work around wxWidgets assertion (PR #12292) See issue #12220 There was a wxAssertion error when closing braille or speech settings panels. ERROR - unhandled exception (09:33:35.263) - MainThread (7112): wx._core.wxAssertionError: C++ assertion ""GetWindow() != 0"" failed at ..\..\src\common\wincmn.cpp(3919) in wxWindowAccessible::GetDescription(): The above exception was the direct cause of the following exception: SystemError: returned a result with an error set There is a lot more information about the investigation on the issue. This PR provides a workaround, the true cause of this assertion has not yet been determined. However in the meantime this PR will prevent the log error, and any additional instability in WX that may occur due to ending up in this situation. --- source/gui/settingsDialogs.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 7201aa2a323..1091005f171 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -1001,9 +1001,17 @@ def onPanelDeactivated(self): super(SpeechSettingsPanel,self).onPanelDeactivated() def onDiscard(self): + # Work around wxAssertion error #12220 + # Manually destroying the ExpandoTextCtrl when the settings dialog is + # exited prevents the wxAssertion. + self.synthNameCtrl.Destroy() self.voicePanel.onDiscard() def onSave(self): + # Work around wxAssertion error #12220 + # Manually destroying the ExpandoTextCtrl when the settings dialog is + # exited prevents the wxAssertion. + self.synthNameCtrl.Destroy() self.voicePanel.onSave() class SynthesizerSelectionDialog(SettingsDialog): @@ -3212,9 +3220,17 @@ def onPanelDeactivated(self): super(BrailleSettingsPanel,self).onPanelDeactivated() def onDiscard(self): + # Work around wxAssertion error #12220 + # Manually destroying the ExpandoTextCtrl when the settings dialog is + # exited prevents the wxAssertion. + self.displayNameCtrl.Destroy() self.brailleSubPanel.onDiscard() def onSave(self): + # Work around wxAssertion error #12220 + # Manually destroying the ExpandoTextCtrl when the settings dialog is + # exited prevents the wxAssertion. + self.displayNameCtrl.Destroy() self.brailleSubPanel.onSave() From a17643ff98e320217eb7ac5b384c6010aedce5e9 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Mon, 19 Apr 2021 17:08:04 +1000 Subject: [PATCH 124/174] centre the progress dialog when updating (#12295) --- source/updateCheck.py | 1 + 1 file changed, 1 insertion(+) diff --git a/source/updateCheck.py b/source/updateCheck.py index d3bb39d3dda..65cefb5904e 100644 --- a/source/updateCheck.py +++ b/source/updateCheck.py @@ -610,6 +610,7 @@ def start(self): # and waits for the user to press the Close button. style=wx.PD_CAN_ABORT | wx.PD_ELAPSED_TIME | wx.PD_REMAINING_TIME | wx.PD_AUTO_HIDE, parent=gui.mainFrame) + self._progressDialog.CentreOnScreen() self._progressDialog.Raise() t = threading.Thread( name=f"{self.__class__.__module__}.{self.start.__qualname__}", From 1d038ff8b5619c9a64060cc5a3afca2d2b05bb23 Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Tue, 20 Apr 2021 01:26:58 +0200 Subject: [PATCH 125/174] Fix for error on Chromium UIA combo-box when restoring Advanced Settings (#12302) Using .Selection on a combo-box rather than .GetSelection was causing an error when restoring Advanced settings to default. Modified .Selection to .GetSelection() --- source/gui/settingsDialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 1091005f171..6c1345a4ce3 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2815,7 +2815,7 @@ def haveConfigDefaultsBeenRestored(self): and self.ConsoleUIACheckBox.IsChecked() == (self.ConsoleUIACheckBox.defaultValue == 'UIA') and self.winConsoleSpeakPasswordsCheckBox.IsChecked() == self.winConsoleSpeakPasswordsCheckBox.defaultValue and self.cancelExpiredFocusSpeechCombo.GetSelection() == self.cancelExpiredFocusSpeechCombo.defaultValue - and self.UIAInChromiumCombo.selection == self.UIAInChromiumCombo.defaultValue + and self.UIAInChromiumCombo.GetSelection() == self.UIAInChromiumCombo.defaultValue and self.keyboardSupportInLegacyCheckBox.IsChecked() == self.keyboardSupportInLegacyCheckBox.defaultValue and self.diffAlgoCombo.GetSelection() == self.diffAlgoCombo.defaultValue and self.caretMoveTimeoutSpinControl.GetValue() == self.caretMoveTimeoutSpinControl.defaultValue From f94aa0394c871265aba9137ac765f8e8f32e5f97 Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Tue, 20 Apr 2021 04:51:29 +0200 Subject: [PATCH 126/174] Fixes in the user guide: broken link and typo. (#12297) --- user_docs/en/userGuide.t2t | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 39bf105e16c..3a691534f2b 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -1746,7 +1746,7 @@ You can configure reporting of: - Pages and spacing - Page numbers - Line numbers - - Line indentation reporting [(Off, Speech, Tones, Both Speech and Tones) #lineIndentationOptions] + - Line indentation reporting [(Off, Speech, Tones, Both Speech and Tones) #DocumentFormattingSettingsLineIndentation] - Paragraph indentation (e.g. hanging indent, first line indent) - Line spacing (single, double, etc.) - Alignment @@ -1754,7 +1754,7 @@ You can configure reporting of: - Tables - Row/column headers - Cell coordinates - - Cell borders [(Off, Styles, Both Colours and Styles) + - Cell borders (Off, Styles, Both Colours and Styles) - Elements - Headings - Links From ce2825619e6c5c7a7a25aca21f583a368799474e Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Tue, 20 Apr 2021 05:01:09 +0200 Subject: [PATCH 127/174] Fix reporting of superscript / subscript in Wordpad (#12262) In IAccessible edit field such as WordPad, enabling "Superscript and subscript" reporting in Document formatting settings was not enough to have them reported. It was also required to have Font attributes reporting enabled. This is due to a forgotten part of the code when reporting of superscript/subscript has been separated from reporting of the other attributes in #10919. Description of how this pull request fixes the issue: Added the missing 'if' statement --- source/NVDAObjects/window/edit.py | 3 +++ user_docs/en/changes.t2t | 1 + 2 files changed, 4 insertions(+) diff --git a/source/NVDAObjects/window/edit.py b/source/NVDAObjects/window/edit.py index 9f4c24db9e4..437007c91b1 100644 --- a/source/NVDAObjects/window/edit.py +++ b/source/NVDAObjects/window/edit.py @@ -507,6 +507,9 @@ def _getFormatFieldAtRange(self, textRange, formatConfig): formatField["italic"]=bool(fontObj.italic) formatField["underline"]=bool(fontObj.underline) formatField["strikethrough"]=bool(fontObj.StrikeThrough) + if formatConfig["reportSuperscriptsAndSubscripts"]: + if not fontObj: + fontObj = textRange.font if fontObj.superscript: formatField["text-position"]="super" elif fontObj.subscript: diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index a4e3e078997..a031beb2eae 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -42,6 +42,7 @@ What's New in NVDA - The python locale is set to match the language selected in preferences consistently, and will occur when using the default language. (#12214) - - TextInfo.getTextInChunks no longer freezes when called on Rich Edit controls such as the NVDA log viewer. (#11613) - It is once again possible to use NVDA in a languages containing underscores in the locale name such as de_CH on Windows 10 1803 and 1809. (#12250) +- In WordPad, configuration of superscript/subscript reporting works as expected. (#12262) == Changes for Developers == From e019a243271a45816223c5f4fa661cda4b75d9fe Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Tue, 20 Apr 2021 14:19:30 +1000 Subject: [PATCH 128/174] Downgrade to Python 3.7 due to stack corruption in Python 3.8+ (#12298) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #12152 Fixes #12154 Since upgrading to Python 3.8, several serious crashes in NVDA have been reported. Specifically: • NVDA crashing when using the SAPI4 speech synthesizer: #12152 • NVDA crashing when using Windows Explorer on Windows Server 2012: #12154 Both of these issues were traced to stack corruption after a Python callback of NVDA's was called from external libraries. In SAPI4's case, after calling NVDA's implementation of ITTSBufNotifySink::TextDataStarted, and in the Windows Server 2012 case: IUIAutomationPropertyChangedEventHandler::handlePropertyChangedEvent. It seems as though libFFI / Python ctypes is not cleaning the stack properly after executing a Python callback with the stdcall calling convention (ctypes WINFUNCTYPE), where the callback contained at least one argument larger than 4 bytes (E.g. a long long, or a VARIANT struct), and the arguments preceding it were such that this argument was not aligned to an 8 byte boundary. E.g. the callback might be: • callback(void*, long long) or • callback(void*, void*, int, VARIANT) See Python bug: https://bugs.python.org/issue38748 On that bug I have attached a minimal testcase. This bug affects both Python 3.8 and Python 3.9. The bug is most likely in the libFFI project used by Python's ctypes module. Python 3.8 switched to a much more recent and official version of libFFI I believe. Although we do really want to move to Python 3.8+ as soon as we can, right now this bug makes it impossible to do so. We could specifically work around the currently known manifestations by moving some of that code into C++, but that brings its own risks, and we still don't know where else this issue may appear in our code. The appropriate thing to do right now is stay on Python 3.7 until we can work with Python / libFFI to get this resolved. Description of how this pull request fixes the issue: Downgrades to Python 3.7 by referencing Python 3.7 (rather than 3.8) in NVDA's build system. The existing PRs that needed to be reverted were: • Updating brlAPI to a Python 3.8 build: nvaccess/nvda-misc-deps#20 • Switching to using Python's own pgettext: #12109 • calling os.add_dll_directory when loading liblouis: #12020 None of these PRs provided any user visible changes. The rest of the Python 3.8 work, including the switch to a virtual environment etc is all compatible with Python 3.7 and can remain. --- appveyor.yml | 2 +- miscDeps | 2 +- readme.md | 2 +- sconstruct | 2 +- source/addonHandler/__init__.py | 7 ++++--- source/languageHandler.py | 32 ++++++++++++++++++++++++++++---- source/louisHelper.py | 10 +++------- user_docs/en/changes.t2t | 1 - user_docs/en/userGuide.t2t | 4 ++-- venvUtils/ensureAndActivate.bat | 2 +- venvUtils/ensureVenv.py | 11 +++++++++++ 11 files changed, 53 insertions(+), 22 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index ab587f59eaa..98e0d1a3f97 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -11,7 +11,7 @@ branches: - /release-.*/ environment: - PY_PYTHON: 3.8-32 + PY_PYTHON: 3.7-32 encFileKey: secure: ekOvuyywHuDdGZmRmoj+b3jfrq39A2xlx4RD5ZUGd/8= mozillaSymsAuthToken: diff --git a/miscDeps b/miscDeps index 91217045c4d..fc1e9c47dc7 160000 --- a/miscDeps +++ b/miscDeps @@ -1 +1 @@ -Subproject commit 91217045c4d3263c0ac9130e6f53e4852060f351 +Subproject commit fc1e9c47dc7f797fa0bca6cd91a32590fbf30edf diff --git a/readme.md b/readme.md index f2abd157982..d01d00f3584 100644 --- a/readme.md +++ b/readme.md @@ -55,7 +55,7 @@ The NVDA source depends on several other packages to run correctly. ### Installed Dependencies The following dependencies need to be installed on your system: -* [Python](https://www.python.org/), version 3.8, 32 bit +* [Python](https://www.python.org/), version 3.7, 32 bit * Use latest minor version if possible. * Microsoft Visual Studio 2019 Community, Version 16.3 or later: * Download from https://visualstudio.microsoft.com/vs/ diff --git a/sconstruct b/sconstruct index 0e02b1ef472..c5905140f0e 100755 --- a/sconstruct +++ b/sconstruct @@ -22,7 +22,7 @@ if nvdaVenv != virtualEnv: # Variables for storing required version of Python, and the version which is used to run this script. requiredPythonMajor ="3" -requiredPythonMinor = "8" +requiredPythonMinor = "7" requiredPythonArchitecture = "32bit" installedPythonMajor = str(sys.version_info.major) installedPythonMinor = str(sys.version_info.minor) diff --git a/source/addonHandler/__init__.py b/source/addonHandler/__init__.py index 5f505c01a81..6248a163ce3 100644 --- a/source/addonHandler/__init__.py +++ b/source/addonHandler/__init__.py @@ -1,6 +1,7 @@ # -*- coding: UTF-8 -*- +# addonHandler.py # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2012-2021 NV Access Limited, Rui Batista, Noelia Ruiz Martínez, +# Copyright (C) 2012-2019 Rui Batista, NV Access Limited, Noelia Ruiz Martínez, # Joseph Lee, Babbage B.V., Arnold Loubriat # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -539,8 +540,8 @@ def initTranslation(): try: callerFrame = inspect.currentframe().f_back callerFrame.f_globals['_'] = translations.gettext - # Install pgettext function. - callerFrame.f_globals['pgettext'] = translations.pgettext + # Install our pgettext function. + callerFrame.f_globals['pgettext'] = languageHandler.makePgettext(translations) finally: del callerFrame # Avoid reference problems with frames (per python docs) diff --git a/source/languageHandler.py b/source/languageHandler.py index a3ea42a6f67..a32fdf73dc9 100644 --- a/source/languageHandler.py +++ b/source/languageHandler.py @@ -1,5 +1,6 @@ +# languageHandler.py # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2007-2021 NV Access Limited, Joseph Lee +# Copyright (C) 2007-2018 NV access Limited, Joseph Lee # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -7,6 +8,7 @@ This module assists in NVDA going global through language services such as converting Windows locale ID's to friendly names and presenting available languages. """ +import builtins import os import sys import ctypes @@ -124,6 +126,28 @@ def getAvailableLanguages(presentational=False): ) return langs + +def makePgettext(translations): + """Obtaina pgettext function for use with a gettext translations instance. + pgettext is used to support message contexts, + but Python's gettext module doesn't support this, + so NVDA must provide its own implementation. + """ + if isinstance(translations, gettext.GNUTranslations): + def pgettext(context, message): + try: + # Look up the message with its context. + return translations._catalog[u"%s\x04%s" % (context, message)] + except KeyError: + return message + elif isinstance(translations, gettext.NullTranslations): + # A language with out a translation catalog, such as English. + def pgettext(context, message): + return message + else: + raise ValueError("%s is Not a GNUTranslations or NullTranslations object" % translations) + return pgettext + def getWindowsLanguage(): """ Fetches the locale name of the user's configured language in Windows. @@ -178,10 +202,10 @@ def setLanguage(lang: str) -> None: trans = gettext.translation("nvda", fallback=True) curLang = "en" - # #9207: Python 3.8 adds gettext.pgettext, so add it to the built-in namespace. - trans.install(names=["pgettext"]) + trans.install() setLocale(curLang) - + # Install our pgettext function. + builtins.pgettext = makePgettext(trans) def setLocale(localeName: str) -> None: ''' diff --git a/source/louisHelper.py b/source/louisHelper.py index 533086d19c6..7a3bdc72631 100644 --- a/source/louisHelper.py +++ b/source/louisHelper.py @@ -1,16 +1,12 @@ +# louisHelper.py # 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-2021 NV Access Limited, Babbage B.V., Joseph Lee +# Copyright (C) 2018 NV Access Limited, Babbage B.V. """Helper module to ease communication to and from liblouis.""" -# Python 3.8 changes the way DLL's are loaded due to security. -# Thus manually add NVDA executable path to DLL lookup path for loading liblouis.dll. -import os -import globalVars -with os.add_dll_directory(globalVars.appDir): - import louis +import louis from logHandler import log import config diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index a031beb2eae..a7697945237 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -47,7 +47,6 @@ What's New in NVDA == Changes for Developers == - Note: this is a Add-on API compatibility breaking release. Add-ons will need to be re-tested and have their manifest updated. -- NVDA now requires Python 3.8. (#12075) - NVDA's build system now fetches all Python dependencies with pip and stores them in a Python virtual environment. This is all done transparently. - To build NVDA, SCons should continue to be used in the usual way. E.g. executing scons.bat in the root of the repository. Running ``py -m SCons`` is no longer supported, and ``scons.py`` has also been removed. - To run NVDA from source, rather than executing ``source/nvda.pyw`` directly, the developer should now use ``runnvda.bat`` in the root of the repository. If you do try to execute ``source/nvda.pyw``, a message box will alert you this is no longer supported. diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 3a691534f2b..c22198ed92d 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -67,8 +67,8 @@ For details regarding exceptions, access the license document from the NVDA menu + System Requirements +[SystemRequirements] - Operating Systems: all 32-bit and 64-bit editions of Windows 7, Windows 8, Windows 8.1, Windows 10, and all Server Operating Systems starting from Windows Server 2008 R2. - - For Windows 7 and Windows Server 2008 R2 NVDA requires Service Pack 1 and the [KB3063858 https://support.microsoft.com/en-us/kb/3063858] update. - Both of these should be installed if all available updates are applied. + - For Windows 7, NVDA requires Service Pack 1 or higher. + - For Windows Server 2008 R2, NVDA requires Service Pack 1 or higher. - at least 150 MB of storage space. - diff --git a/venvUtils/ensureAndActivate.bat b/venvUtils/ensureAndActivate.bat index b39d1f947ff..7d6526a8dd5 100644 --- a/venvUtils/ensureAndActivate.bat +++ b/venvUtils/ensureAndActivate.bat @@ -4,7 +4,7 @@ rem and then activates it. rem This is an internal script and should not be used directly. rem Ensure the environment is created and up to date -py -3.8-32 "%~dp0\ensureVenv.py" +py -3.7-32 "%~dp0\ensureVenv.py" if ERRORLEVEL 1 goto :EOF rem Set the necessary environment variables to have Python use this virtual environment. diff --git a/venvUtils/ensureVenv.py b/venvUtils/ensureVenv.py index 3256fdfba4d..bf11e404cc4 100644 --- a/venvUtils/ensureVenv.py +++ b/venvUtils/ensureVenv.py @@ -134,4 +134,15 @@ def ensureVenvAndRequirements(): "Please deactivate the current Python virtual environment and try again." ) sys.exit(1) + if ( + sys.version_info.minor == 7 + and sys.version_info.micro == 6 + ): + # #10696: Building with Python 3.7.6 fails. Inform user and exit. + Py376FailMsg = ( + "Error: Building with Python 3.7.6 is not possible.\n" + "Please use a more recent version of Python 3." + ) + print(Py376FailMsg) + sys.exit(1) ensureVenvAndRequirements() From da3028a54fd81f7e2080f64dd745ec46976eadd9 Mon Sep 17 00:00:00 2001 From: Michael Fairchild Date: Tue, 20 Apr 2021 00:38:13 -0500 Subject: [PATCH 129/174] Fix Issue 12147: new focus target is not always announced (#12252) fixes #12147 The new focus target is not always announced when: 1. The triggering button is activated while in browse mode 2. The triggering button is removed 3. The focus target is adjacent to where the triggering button was This is because of the overlapping logic for browse mode focus changes that is used to prevent duplicate announcements. That overlapping logic did not account for this scenario. See the linked codepens in #12147 for more examples. I can not specially point to an instance of this in the wild, as all of my use-cases are behind a firewall. However, I know that this behavior is not unusual in SPA-style web apps. Description of how this pull request fixes the issue: This PR fixes the issue by altering the overlapping detecting logic so that: 1. We look at the state of the previous focus object 2. If the state of the previous focus object is DEFUNCT (no longer available) and an overlap was detected, then continue as if there was not an overlap. --- source/browseMode.py | 10 +++++++- tests/system/robot/chromeTests.py | 34 ++++++++++++++++++++++++++++ tests/system/robot/chromeTests.robot | 3 +++ user_docs/en/changes.t2t | 1 + 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/source/browseMode.py b/source/browseMode.py index f4acc4b5005..cae9ee5f741 100644 --- a/source/browseMode.py +++ b/source/browseMode.py @@ -1553,6 +1553,13 @@ def event_gainFocus(self, obj, nextHandler): return if not self.passThrough and self._shouldIgnoreFocus(obj): return + + # If the previous focus object was removed, we might hit a false positive for overlap detection. + # Track the previous focus target so that we can account for this scenario. + previousFocusObjIsDefunct = False + if self._lastFocusObj and controlTypes.STATE_DEFUNCT in self._lastFocusObj.states: + previousFocusObjIsDefunct = True + self._lastFocusObj=obj try: @@ -1570,7 +1577,8 @@ def event_gainFocus(self, obj, nextHandler): caretInfo=self.makeTextInfo(textInfos.POSITION_CARET) # Expand to one character, as isOverlapping() doesn't treat, for example, (4,4) and (4,5) as overlapping. caretInfo.expand(textInfos.UNIT_CHARACTER) - if not self._hadFirstGainFocus or not focusInfo.isOverlapping(caretInfo): + isOverlapping = focusInfo.isOverlapping(caretInfo) + if not self._hadFirstGainFocus or not isOverlapping or (isOverlapping and previousFocusObjIsDefunct): # The virtual caret is not within the focus node. oldPassThrough=self.passThrough passThrough = self.shouldPassThrough(obj, reason=OutputReason.FOCUS) diff --git a/tests/system/robot/chromeTests.py b/tests/system/robot/chromeTests.py index d9cd0aa6b74..f5baab177c9 100644 --- a/tests/system/robot/chromeTests.py +++ b/tests/system/robot/chromeTests.py @@ -345,3 +345,37 @@ def test_ariaCheckbox_browseMode(): actualSpeech, "Sandwich Condiments grouping list with 4 items Lettuce check box not checked" ) + + +def test_i12147(): + """ + New focus target should be announced if the triggering element is removed when activated. + """ + _chrome.prepareChrome( + f""" +
+ +

target 0

+
+ + """ + ) + # Jump to the first button (the trigger) + actualSpeech = _chrome.getSpeechAfterKey("tab") + _asserts.strings_match( + actualSpeech, + "trigger 0 button" + ) + # Activate the button, we should hear the new focus target. + actualSpeech = _chrome.getSpeechAfterKey("enter") + _asserts.strings_match( + actualSpeech, + "target 0 heading level 4" + ) diff --git a/tests/system/robot/chromeTests.robot b/tests/system/robot/chromeTests.robot index 8063420da2a..bcf5b91d000 100644 --- a/tests/system/robot/chromeTests.robot +++ b/tests/system/robot/chromeTests.robot @@ -47,3 +47,6 @@ ARIA checkbox [Documentation] Navigate to an unchecked checkbox in reading mode. [Tags] aria-at test_ariaCheckbox_browseMode +i12147 + [Documentation] New focus target should be announced if the triggering element is removed when activated + test_i12147 diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index a7697945237..ecb87673b9c 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -43,6 +43,7 @@ What's New in NVDA - - TextInfo.getTextInChunks no longer freezes when called on Rich Edit controls such as the NVDA log viewer. (#11613) - It is once again possible to use NVDA in a languages containing underscores in the locale name such as de_CH on Windows 10 1803 and 1809. (#12250) - In WordPad, configuration of superscript/subscript reporting works as expected. (#12262) +- NVDA no longer fails to announce the newly focused content on a web page if the old focus disappears and is replaced by the new focus in the same position. (#12147) == Changes for Developers == From 5de73c14fb09467a0e8f5a4016c79e3875ae6b8b Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Tue, 20 Apr 2021 08:48:53 +0200 Subject: [PATCH 130/174] Add missing formatting information in MS Excel cells (strikethrough, superscript/subscript) (#12264) Some formatting information is not reported when navigating from one cell to another in Excel, although the corresponding category is checked in Document formatting settings. - strikethrough is not reported even if font attributes is checked - superscript or subscript is not reported even if the corresponding option is checked Strikethrough, superscript and subscript are now reported when navigating through Excel cells if the corresponding option is enabled --- source/NVDAObjects/window/excel.py | 8 +++++++- user_docs/en/changes.t2t | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/source/NVDAObjects/window/excel.py b/source/NVDAObjects/window/excel.py index af7f98d335d..63211ea56f0 100755 --- a/source/NVDAObjects/window/excel.py +++ b/source/NVDAObjects/window/excel.py @@ -994,6 +994,12 @@ def _getFormatFieldAndOffsets(self,offset,formatConfig,calculateOffsets=True): formatField['italic']=fontObj.italic underline=fontObj.underline formatField['underline']=False if underline is None or underline==xlUnderlineStyleNone else True + formatField['strikethrough'] = fontObj.strikethrough + if formatConfig['reportSuperscriptsAndSubscripts']: + if fontObj.superscript: + formatField['text-position'] = 'super' + elif fontObj.subscript: + formatField['text-position'] = 'sub' if formatConfig['reportStyle']: try: styleName=self.obj.excelCellObject.style.nameLocal @@ -1471,7 +1477,7 @@ def reportFocus(self): formatField.update(field.field) if not hasattr(self.parent,'_formatFieldSpeechCache'): self.parent._formatFieldSpeechCache = textInfos.Field() - if formatField: + if formatField or self.parent._formatFieldSpeechCache: sequence = speech.getFormatFieldSpeech( formatField, attrsCache=self.parent._formatFieldSpeechCache, diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index ecb87673b9c..65a2db40eb1 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -44,6 +44,7 @@ What's New in NVDA - It is once again possible to use NVDA in a languages containing underscores in the locale name such as de_CH on Windows 10 1803 and 1809. (#12250) - In WordPad, configuration of superscript/subscript reporting works as expected. (#12262) - NVDA no longer fails to announce the newly focused content on a web page if the old focus disappears and is replaced by the new focus in the same position. (#12147) +- Strikethrough, superscript and subscript formatting for entire Excel cells are now reported if the corresponding option is enabled. (#12264) == Changes for Developers == From 381c976850c7417462ffe8152d074f35ad51c537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Tue, 20 Apr 2021 09:11:41 +0200 Subject: [PATCH 131/174] Fix up of 11738: Force UIA for Outlook messages list even though it doesn't support UIA natively (#12241) PR #11738 made it impossible to use UIA for controls which doesn't report as native UIA. This decreased accessibility of Outlook's messages list in some versions of Outlook (certainly for 2010 and possibly for 2013) which reports as non native UIA. Description of how this pull request fixes the issue: Similar to the fix in #11828 for these controls the fact that they're non native UIA is ignored. --- source/appModules/outlook.py | 8 ++++---- user_docs/en/changes.t2t | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/source/appModules/outlook.py b/source/appModules/outlook.py index 934c9e34028..b5e1b7e278a 100644 --- a/source/appModules/outlook.py +++ b/source/appModules/outlook.py @@ -246,10 +246,10 @@ def event_gainFocus(self): return super(SuperGridClient2010,self).event_gainFocus() try: kwargs = {} - UIA.kwargsFromSuper(kwargs, relation="focus") - obj=UIA(**kwargs) - except: - log.debugWarning("Retrieving UIA focus failed", exc_info=True) + UIA.kwargsFromSuper(kwargs, relation="focus", ignoreNonNativeElementsWithFocus=False) + obj = UIA(**kwargs) + except Exception: + log.error("Retrieving UIA focus failed", exc_info=True) return super(SuperGridClient2010,self).event_gainFocus() if not isinstance(obj,UIAGridRow): return super(SuperGridClient2010,self).event_gainFocus() diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 65a2db40eb1..ec4a912c7a9 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -28,6 +28,7 @@ What's New in NVDA == Bug Fixes == +- The list of messages in Outlook 2010 is once again readable. (12241) - In terminal programs on Windows 10 version 1607 and later, when inserting or deleting characters in the middle of a line, the characters to the right of the caret are no longer read out. (#3200) - This experimental fix must be manually enabled in NVDA's advanced settings panel by changing the diff algorithm to Diff Match Patch. - Fixed access to edit fields in MCS Electronics IDE's. (#11966) From 240f6412e5ac3a63ac7e9d2c215b97246e4e4d97 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Tue, 20 Apr 2021 18:21:59 +1000 Subject: [PATCH 132/174] remove wx.CENTER_ON_SCREEN and wx.CENTRE_ON_SCREEN (#12309) We included code to add backwards compatibility for addons for the following deprecated wxPython constants wx.CENTER_ON_SCREEN = wx.CENTRE_ON_SCREEN when used with Dialog.Center() Description of how this pull request fixes the issue: Remove the deprecated code, and it's only usage in AskAllowUsageStatsDialog, replace with self.CentreOnScreen() --- source/core.py | 3 --- source/gui/startupDialogs.py | 2 +- user_docs/en/changes.t2t | 1 + 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/source/core.py b/source/core.py index 5c6f56b8a8f..949a7fcc25d 100644 --- a/source/core.py +++ b/source/core.py @@ -308,9 +308,6 @@ def main(): # Translators: This is spoken when NVDA is starting. speech.speakMessage(_("Loading NVDA. Please wait...")) import wx - # wxPython 4 no longer has either of these constants (despite the documentation saying so), some add-ons may rely on - # them so we add it back into wx. https://wxpython.org/Phoenix/docs/html/wx.Window.html#wx.Window.Centre - wx.CENTER_ON_SCREEN = wx.CENTRE_ON_SCREEN = 0x2 import six log.info("Using wx version %s with six version %s"%(wx.version(), six.__version__)) class App(wx.App): diff --git a/source/gui/startupDialogs.py b/source/gui/startupDialogs.py index d26f7a9367a..aadc038e8c0 100644 --- a/source/gui/startupDialogs.py +++ b/source/gui/startupDialogs.py @@ -257,7 +257,7 @@ def __init__(self, parent): mainSizer.Add(sHelper.sizer, border=gui.guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL) self.Sizer = mainSizer mainSizer.Fit(self) - self.Center(wx.BOTH | wx.CENTER_ON_SCREEN) + self.CentreOnScreen() def onYesButton(self, evt): log.debug("Usage stats gathering has been allowed") diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index ec4a912c7a9..c512b6eb177 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -111,6 +111,7 @@ What's New in NVDA - TextInfo start and end properties can also be set to each other. - E.g. ti1.start = ti2.end - This usage is prefered instead of ti1.SetEndPoint(ti2,"startToEnd") +- `wx.CENTRE_ON_SCREEN` and `wx.CENTER_ON_SCREEN` are removed, use `self.CentreOnScreen()` instead. (#12309) = 2020.4 = From 4781002709821072e2c770c3ff639e33083f2548 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Tue, 20 Apr 2021 16:52:04 +0800 Subject: [PATCH 133/174] Add code owners and CONTRIBUTING (PR #12294) With code owners we can automatically request a code review from a code owner when a PR is no longer a draft. Hopefully this helps contributors communicate they are ready for a review from core developers. To facilitate this, new contributions should start in draft state, and be converted to "ready for review" when initial checks are complete. This adds a code owners file, docs: https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners Docs for auto-assign code reviewer: https://docs.github.com/en/organizations/organizing-members-into-teams/managing-code-review-assignment-for-your-team Adds a CONTRIBUTING file which points to our full guide: https://github.com/nvaccess/nvda/wiki/Contributing --- .github/CODEOWNERS | 46 ++++++++++++++++++++++++++++++++ .github/CONTRIBUTING.md | 4 +++ .github/PULL_REQUEST_TEMPLATE.md | 12 ++++++--- 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/CONTRIBUTING.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..fa74450046a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,46 @@ +# This is a comment. +# Each line is a file pattern followed by one or more owners. + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +# @global-owner1 and @global-owner2 will be requested for +# review when someone opens a pull request. +# * @global-owner1 @global-owner2 + +# Order is important; the last matching pattern takes the most +# precedence. When someone opens a pull request that only +# modifies JS files, only @js-owner and not the global +# owner(s) will be requested for a review. +# *.js @js-owner + +# You can also use email addresses if you prefer. They'll be +# used to look up users just like we do for commit author +# emails. +# *.go docs@example.com + +# In this example, @doctocat owns any files in the build/logs +# directory at the root of the repository and any of its +# subdirectories. +# /build/logs/ @doctocat + +# The `docs/*` pattern will match files like +# `docs/getting-started.md` but not further nested files like +# `docs/build-app/troubleshooting.md`. +# docs/* docs@example.com + +# In this example, @octocat owns any file in an apps directory +# anywhere in your repository. +# apps/ @octocat + +# In this example, @doctocat owns any file in the `/docs` +# directory in the root of your repository and any of its +# subdirectories. +# /docs/ @doctocat + +# Start of NVDA config + +# By default auto request review from NV Access developer team. +* @nvaccess/developers + +# For changes to the userGuide auto request review from userDocs team +/user_docs/en/userGuide.t2t @nvaccess/userDocs diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000000..3797ea2d0a5 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,4 @@ +The NVDA project is open to contributions, thank you for your interest! + +Please first [read our guide to contributing](https://github.com/nvaccess/nvda/wiki/Contributing) on the NVDA +GitHub Wiki. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d6bd5031b62..6da727c361e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,8 @@ ### Link to issue number: @@ -13,17 +15,21 @@ Please also note that the NVDA project has a Citizen and Contributor Code of Con ### Known issues with pull request: -### Change log entry: - -Section: New features, Changes, Bug fixes +### Change log entries: +New features +Changes +Bug fixes +For Developers ### Code Review Checklist: + - [ ] Pull Request description is up to date. - [ ] Unit tests. From 37d66a51ce23e4386182781d303261b56bf6e475 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Tue, 20 Apr 2021 19:06:12 +1000 Subject: [PATCH 134/174] comInterfaces_sconscript: correct case of MathPlayer.py string. (#12310) Fixes #12281 Math can no longer be read in NVDA with mathPlayer. a regression introduced by pr #12201 comInterfaces_sconscript instructs comtypes to generate Python files from typelibs/mathPlayer.tlb. Comtypes produces MathPlayer.py (note the uppercase M). However, the sconscript target was hardcoded as "mathPlayer.py" (note the lowercase m). This has always been the case, and has not been a problem as scons and Windows is case insensitive and so Scons thought the target had been correctly created. However, pr #12201 makes use of the target's abspath property to rewrite the file with some extra imports. However, the path is made from the hardcoded target, so it has the lowercase m and therefore when the file is written out again, it has a lowercase m, and mathPres cannot then import MathPlayer with an uppercase M. Description of how this pull request fixes the issue: Correct the "mathPlayer.py" string in comInterfaces_sconscript to "MathPlayer.py". --- source/comInterfaces_sconscript | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/comInterfaces_sconscript b/source/comInterfaces_sconscript index 52ff137407d..ef4a25b46dc 100755 --- a/source/comInterfaces_sconscript +++ b/source/comInterfaces_sconscript @@ -81,7 +81,7 @@ comtypes.client.gen_dir=Dir('comInterfaces').abspath COM_INTERFACES = { "IAccessible2Lib.py": "typelibs/ia2.tlb", "ISimpleDOM.py": "typelibs/ISimpleDOMNode.tlb", - "mathPlayer.py": "typelibs/mathPlayerDLL.tlb", + "MathPlayer.py": "typelibs/mathPlayerDLL.tlb", "Accessibility.py": ('{1EA4DBF0-3C3B-11CF-810C-00AA00389B71}',1,0), "tom.py": ('{8CC497C9-A1DF-11CE-8098-00AA0047BE5D}',1,0), "SpeechLib.py": ('{C866CA3A-32F7-11D2-9602-00C04F8EE628}',5,0), From f3306af9775387f46f052c4a7d3cd8d92e7bc189 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Wed, 21 Apr 2021 09:40:16 +1000 Subject: [PATCH 135/174] Move report formatting changes box to the correct parent (#12307) In #12181, the setting "Report formatting changes after the cursor" in "Document Formatting" was incorrectly moved to the wrong parent, the static box grouping above it, making it invisible to the user. It was readable using NVDA but not graphically visible to the user. This has been corrected. --- source/gui/settingsDialogs.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 6c1345a4ce3..36c51cd7803 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2421,10 +2421,7 @@ def makeSettings(self, settingsSizer): # Translators: This is the label for a checkbox in the # document formatting settings panel. detectFormatAfterCursorText = _("Report formatting chan&ges after the cursor (can cause a lag)") - self.detectFormatAfterCursorCheckBox = wx.CheckBox( - elementsGroupBox, - label=detectFormatAfterCursorText - ) + self.detectFormatAfterCursorCheckBox = wx.CheckBox(self, label=detectFormatAfterCursorText) self.bindHelpEvent( "DocumentFormattingDetectFormatAfterCursor", self.detectFormatAfterCursorCheckBox From d1c8ccd1ecbafac0c2973617ab75a9d551d76d40 Mon Sep 17 00:00:00 2001 From: Luke Davis <8139760+XLTechie@users.noreply.github.com> Date: Tue, 20 Apr 2021 20:58:46 -0400 Subject: [PATCH 136/174] Updated developer guide to clarify that the Remote Python Console is unavailable in binary builds (#12316) --- devDocs/developerGuide.t2t | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devDocs/developerGuide.t2t b/devDocs/developerGuide.t2t index 31b87a2bddb..a5179638bb6 100644 --- a/devDocs/developerGuide.t2t +++ b/devDocs/developerGuide.t2t @@ -860,11 +860,11 @@ If the input is "nav._", attribute names with a single leading underscore are pr Similarly, if the input is "nav.__", attribute names with two leading underscores are proposed. + Remote Python Console + -A remote Python console is available for situations where remote debugging of NVDA is useful. +A remote Python console is available in source builds of NVDA, for situations where remote debugging of NVDA is useful. It is similar to the [local Python console #PythonConsole] discussed above, but is accessed via TCP. Please be aware that this is a huge security risk. -You should only enable it if you are connected to trusted networks. +It is not available in binary builds distributed by NV Access, and You should only enable it if you are connected to trusted networks. ++ Usage ++ To enable the remote Python console, use the local Python console to import remotePythonConsole and call remotePythonConsole.initialize(). From 07030660b3b60d5c906b20150ca189efe9f0d80f Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Wed, 21 Apr 2021 11:01:11 +1000 Subject: [PATCH 137/174] prevent Chrome losing focus during systests (#12293) Our system tests randomly fail due to chrome not having focus, and so NVDA does not read the expected speech. Sometimes this is due to a Docker popup asking for feedback. Changes: - Log when the event postNvdaStartup fires - sleep for 2s before launching chrome Known issues with pull request: - Docker popups may still block the build - 2s of sleep might not be long enough (we have confidence that 10s is) - Lint checking occasionally blocks builds due to a failed merge (likely due to a rare GitHub issue) --- source/core.py | 8 +++++++- .../libraries/SystemTestSpy/speechSpyGlobalPlugin.py | 3 +-- tests/system/robot/chromeTests.robot | 6 +++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/source/core.py b/source/core.py index 949a7fcc25d..a2ff82453c0 100644 --- a/source/core.py +++ b/source/core.py @@ -581,10 +581,16 @@ def run(self): log.debug("initializing updateCheck") updateCheck.initialize() log.info("NVDA initialized") + # Queue the firing of the postNVDAStartup notification. # This is queued so that it will run from within the core loop, # and initial focus has been reported. - queueHandler.queueFunction(queueHandler.eventQueue, postNvdaStartup.notify) + def _doPostNvdaStartupAction(): + log.debug("Notify of postNvdaStartup action") + postNvdaStartup.notify() + + queueHandler.queueFunction(queueHandler.eventQueue, _doPostNvdaStartupAction) + log.debug("entering wx application main loop") app.MainLoop() diff --git a/tests/system/libraries/SystemTestSpy/speechSpyGlobalPlugin.py b/tests/system/libraries/SystemTestSpy/speechSpyGlobalPlugin.py index 365daee455c..fe5868cd888 100644 --- a/tests/system/libraries/SystemTestSpy/speechSpyGlobalPlugin.py +++ b/tests/system/libraries/SystemTestSpy/speechSpyGlobalPlugin.py @@ -159,8 +159,7 @@ def wait_for_NVDA_startup_to_complete(self): giveUpAfterSeconds=self._minTimeout(10), errorMessage="Unable to connect to nvdaSpy", ) - if self._isNvdaStartupComplete: - self.reset_all_speech_index() + self.reset_all_speech_index() def get_last_speech(self) -> str: return self._getSpeechAtIndex(-1) diff --git a/tests/system/robot/chromeTests.robot b/tests/system/robot/chromeTests.robot index bcf5b91d000..01c33755681 100644 --- a/tests/system/robot/chromeTests.robot +++ b/tests/system/robot/chromeTests.robot @@ -12,7 +12,7 @@ Library NvdaLib.py Library chromeTests.py Library ScreenCapLibrary -Test Setup start NVDA standard-dontShowWelcomeDialog.ini +Test Setup default setup Test Teardown default teardown *** Keywords *** @@ -22,6 +22,10 @@ default teardown exit chrome quit NVDA +default setup + start NVDA standard-dontShowWelcomeDialog.ini + Sleep 2s + *** Test Cases *** checkbox labelled by inner element From bc8eb11e79ec1b9336da8d5c76f73927261eeba0 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Wed, 21 Apr 2021 11:04:41 +1000 Subject: [PATCH 138/174] Ensure settings are scrolled to using tab navigation (#12300) Navigating through settings using tabbing does not visually scroll to the focused control. wxPython ScrolledPanels calculate the position to scroll to based on the relative position to a focus elements parent, rather than the relative position to the ScrolledPanel itself. When fixing right-to-left issues in #12181, another layer of nesting was introduced for controls in our settings panels, which caused the controls to no longer get scrolled to. A patched version of ScrolledPanel is created, which calculates relative position to the ScrolledPanel --- source/gui/nvdaControls.py | 52 +++++++++++++++++++++++++++++------ source/gui/settingsDialogs.py | 3 +- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/source/gui/nvdaControls.py b/source/gui/nvdaControls.py index b3ae5f682c2..4bb34341927 100644 --- a/source/gui/nvdaControls.py +++ b/source/gui/nvdaControls.py @@ -1,21 +1,20 @@ # -*- coding: UTF-8 -*- -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2016-2018 NV Access Limited, Derek Riemer -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2016-2021 NV Access Limited, Derek Riemer +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. -from ctypes.wintypes import BOOL -from typing import Any, Tuple, Optional import wx -from comtypes import GUID +from wx.lib import scrolledpanel from wx.lib.mixins import listctrl as listmix from .dpiScalingHelper import DpiScalingHelperMixin from . import guiHelper -import oleacc import winUser import winsound + from collections.abc import Callable + class AutoWidthColumnListCtrl(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin): """ A list control that allows you to specify a column to resize to take up the remaining width of a wx.ListCtrl. @@ -355,3 +354,40 @@ def onSliderChar(self, evt): evt.Skip() return self.SetValue(newValue) + + +class TabbableScrolledPanel(scrolledpanel.ScrolledPanel): + """ + This class was created to ensure a ScrolledPanel scrolls to nested children of the panel when navigating + with tabs (#12224). A PR to wxPython implementing this fix can be tracked on + https://github.com/wxWidgets/Phoenix/pull/1950 + """ + def GetChildRectRelativeToSelf(self, child: wx.Window) -> wx.Rect: + """ + window.GetRect returns the size of a window, and its position relative to its parent. + When calculating ScrollChildIntoView, the position relative to its parent is not relevant unless the + parent is the ScrolledPanel itself. Instead, calculate the position relative to scrolledPanel + """ + childRectRelativeToScreen = child.GetScreenRect() + scrolledPanelScreenPosition = self.GetScreenPosition() + return wx.Rect( + childRectRelativeToScreen.x - scrolledPanelScreenPosition.x, + childRectRelativeToScreen.y - scrolledPanelScreenPosition.y, + childRectRelativeToScreen.width, + childRectRelativeToScreen.height + ) + + def ScrollChildIntoView(self, child: wx.Window) -> None: + """ + Overrides child.GetRect with `GetChildRectRelativeToSelf` before calling + `super().ScrollChildIntoView`. `super().ScrollChildIntoView` incorrectly uses child.GetRect to + navigate scrolling, which is relative to the parent, where it should instead be relative to this + ScrolledPanel. + """ + oldChildGetRectFunction = child.GetRect + child.GetRect = lambda: self.GetChildRectRelativeToSelf(child) + try: + super().ScrollChildIntoView(child) + finally: + # ensure child.GetRect is reset properly even if super().ScrollChildIntoView throws an exception + child.GetRect = oldChildGetRectFunction diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 36c51cd7803..9863cb6a5cd 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -14,7 +14,6 @@ import typing import wx from vision.providerBase import VisionEnhancementProviderSettings -from wx.lib import scrolledpanel from wx.lib.expando import ExpandoTextCtrl import wx.lib.newevent import winUser @@ -483,7 +482,7 @@ def makeSettings(self, settingsSizer): # The provided column header is just a placeholder, as it is hidden due to the wx.LC_NO_HEADER style flag. self.catListCtrl.InsertColumn(0,categoriesLabelText) - self.container = scrolledpanel.ScrolledPanel( + self.container = nvdaControls.TabbableScrolledPanel( parent = self, style = wx.TAB_TRAVERSAL | wx.BORDER_THEME, size=containerDim From 6200a070a013d9c6322700922cc801194d48aafd Mon Sep 17 00:00:00 2001 From: Joseph Lee Date: Tue, 20 Apr 2021 22:40:17 -0700 Subject: [PATCH 139/174] WinVersion class follow-up: remove ease of access is supported flag (#12222) NVDA requires Windows 7/Server 2008 R2 SP1 or later, therefore Ease of Access is available. This removes easeOfAccess.isSupported flag. --- source/config/__init__.py | 20 ++++++++------------ source/easeOfAccess.py | 13 +++++-------- user_docs/en/changes.t2t | 1 + 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/source/config/__init__.py b/source/config/__init__.py index 61de93ace03..fe3e38958ea 100644 --- a/source/config/__init__.py +++ b/source/config/__init__.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2020 NV Access Limited, Aleksey Sadovoy, Peter Vágner, Rui Batista, Zahari Yurukov, +# Copyright (C) 2006-2021 NV Access Limited, Aleksey Sadovoy, Peter Vágner, Rui Batista, Zahari Yurukov, # Joseph Lee, Babbage B.V., Łukasz Golonka, Julien Cochuyt # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -190,8 +190,10 @@ def initConfigPath(configPath=None): RUN_REGKEY = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Run" def getStartAfterLogon(): - if (easeOfAccess.isSupported and easeOfAccess.canConfigTerminateOnDesktopSwitch - and easeOfAccess.willAutoStart(winreg.HKEY_CURRENT_USER)): + if ( + easeOfAccess.canConfigTerminateOnDesktopSwitch + and easeOfAccess.willAutoStart(winreg.HKEY_CURRENT_USER) + ): return True try: k = winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_REGKEY) @@ -203,7 +205,7 @@ def getStartAfterLogon(): def setStartAfterLogon(enable): if getStartAfterLogon() == enable: return - if easeOfAccess.isSupported and easeOfAccess.canConfigTerminateOnDesktopSwitch: + if easeOfAccess.canConfigTerminateOnDesktopSwitch: easeOfAccess.setAutoStart(winreg.HKEY_CURRENT_USER, enable) if enable: return @@ -230,7 +232,7 @@ def setStartAfterLogon(enable): NVDA_REGKEY = r"SOFTWARE\NVDA" def getStartOnLogonScreen(): - if easeOfAccess.isSupported and easeOfAccess.willAutoStart(winreg.HKEY_LOCAL_MACHINE): + if easeOfAccess.willAutoStart(winreg.HKEY_LOCAL_MACHINE): return True try: k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, NVDA_REGKEY) @@ -239,13 +241,7 @@ def getStartOnLogonScreen(): return False def _setStartOnLogonScreen(enable): - if easeOfAccess.isSupported: - # The installer will have migrated service config to EoA if appropriate, - # so we only need to deal with EoA here. - easeOfAccess.setAutoStart(winreg.HKEY_LOCAL_MACHINE, enable) - else: - k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, NVDA_REGKEY, 0, winreg.KEY_WRITE) - winreg.SetValueEx(k, u"startOnLogonScreen", None, winreg.REG_DWORD, int(enable)) + easeOfAccess.setAutoStart(winreg.HKEY_LOCAL_MACHINE, enable) def setSystemConfigToCurrentConfig(): fromPath = globalVars.appArgs.configPath diff --git a/source/easeOfAccess.py b/source/easeOfAccess.py index 33cfd973600..a1aa4ddae31 100644 --- a/source/easeOfAccess.py +++ b/source/easeOfAccess.py @@ -1,8 +1,7 @@ -#easeOfAccess.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2014 NV Access Limited -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2014-2021 NV Access Limited +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. """Utilities for working with the Windows Ease of Access Center. """ @@ -12,10 +11,8 @@ import winUser import winVersion -# Windows >= Vista -isSupported = winVersion.getWinVer().major >= 6 # Windows >= 8 -canConfigTerminateOnDesktopSwitch = isSupported and winVersion.getWinVer() >= winVersion.WIN8 +canConfigTerminateOnDesktopSwitch: bool = winVersion.getWinVer() >= winVersion.WIN8 ROOT_KEY = r"Software\Microsoft\Windows NT\CurrentVersion\Accessibility" APP_KEY_NAME = "nvda_nvda_v1" diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index c512b6eb177..1ed658bca27 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -112,6 +112,7 @@ What's New in NVDA - E.g. ti1.start = ti2.end - This usage is prefered instead of ti1.SetEndPoint(ti2,"startToEnd") - `wx.CENTRE_ON_SCREEN` and `wx.CENTER_ON_SCREEN` are removed, use `self.CentreOnScreen()` instead. (#12309) +- `easeOfAccess.isSupported` has been removed, NVDA only supports versions of Windows where this evaluates to `True`. (#12222) = 2020.4 = From 46f66acd6b3d52953c86af0247762e5f5ebe9276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Wed, 21 Apr 2021 09:15:09 +0200 Subject: [PATCH 140/174] Fix copy config from portable during installation. (PR #12076) Related PR #9679 Fixes #12071 Fixes #12205 Co-authored-by: Reef Turner --- source/core.py | 11 --------- source/gui/installerGui.py | 46 +++++++++++++++++++++++++------------- user_docs/en/changes.t2t | 1 + 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/source/core.py b/source/core.py index a2ff82453c0..aec9d54b570 100644 --- a/source/core.py +++ b/source/core.py @@ -250,17 +250,6 @@ def main(): log.debug("loading config") import config config.initialize() - if globalVars.appArgs.configPath == config.getUserDefaultConfigPath(useInstalledPathIfExists=True): - # Make sure not to offer the ability to copy the current configuration to the user account. - # This case always applies to the launcher when configPath is not overridden by the user, - # which is the default. - # However, if a user wants to run the launcher with a custom configPath, - # it is likely that he wants to copy that configuration when installing. - # This check also applies to cases where a portable copy is run using the installed configuration, - # in which case we want to avoid copying a configuration to itself. - # We set the value to C{None} in order for the gui to determine - # when to disable the checkbox for this feature. - globalVars.appArgs.copyPortableConfig = None if config.conf['development']['enableScratchpadDir']: log.info("Developer Scratchpad mode enabled") if not globalVars.appArgs.minimal and config.conf["general"]["playStartAndExitSounds"]: diff --git a/source/gui/installerGui.py b/source/gui/installerGui.py index 36a1f701b8a..9b574f27078 100644 --- a/source/gui/installerGui.py +++ b/source/gui/installerGui.py @@ -1,29 +1,45 @@ -#gui/installerGui.py -#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) 2011-2018 NV Access Limited, Babbage B.v. +# 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) 2011-2021 NV Access Limited, Babbage B.v., Cyrille Bougot, Julien Cochuyt, Accessolutions, +# Bill Dengler, Joseph Lee, Takuya Nishimoto import os -import ctypes -import buildVersion import shellapi import winUser import wx import config import globalVars -import versionInfo import installer from logHandler import log import gui from gui import guiHelper import gui.contextHelp from gui.dpiScalingHelper import DpiScalingHelperMixinWithoutInit -import tones import systemUtils +def _canPortableConfigBeCopied() -> bool: + # In some cases even though user requested to copy config from the portable copy during installation + # it should not be done. + if globalVars.appArgs.launcher: + # Normally when running from the launcher + # and configPath is not overridden by the user copying config during installation is rather pointless + # as we would copy it into itself. + # However, if a user wants to run the launcher with a custom configPath, + # it is likely that he wants to copy that configuration when installing. + return globalVars.appArgs.configPath != config.getUserDefaultConfigPath(useInstalledPathIfExists=True) + else: + # For portable copies we want to avoid copying the configuration to itself, + # so return True only if the configPath + # does not point to the config of the installed copy in appdata. + confPath = config.getInstalledUserConfigPath() + if confPath and confPath == globalVars.appArgs.configPath: + return False + return True + + def doInstall( createDesktopShortcut=True, startOnLogon=True, @@ -52,7 +68,8 @@ def doInstall( if copyPortableConfig: installedUserConfigPath=config.getInstalledUserConfigPath() if installedUserConfigPath: - gui.ExecAndPump(installer.copyUserConfig,installedUserConfigPath) + if _canPortableConfigBeCopied(): + gui.ExecAndPump(installer.copyUserConfig, installedUserConfigPath) except Exception as e: res=e log.error("Failed to execute installer",exc_info=True) @@ -211,11 +228,10 @@ def __init__(self, parent, isUpdate): createPortableBox = wx.CheckBox(optionsBox, label=createPortableText) self.copyPortableConfigCheckbox = optionsHelper.addItem(createPortableBox) self.bindHelpEvent("CopyPortableConfigurationToCurrentUserAccount", self.copyPortableConfigCheckbox) - self.copyPortableConfigCheckbox.Value = bool(globalVars.appArgs.copyPortableConfig) - if globalVars.appArgs.copyPortableConfig is None: - # copyPortableConfig is set to C{None} in the main loop, - # when copying the portable configuration should be disabled at all costs. - self.copyPortableConfigCheckbox.Disable() + self.copyPortableConfigCheckbox.Value = ( + bool(globalVars.appArgs.copyPortableConfig) and _canPortableConfigBeCopied() + ) + self.copyPortableConfigCheckbox.Enable(_canPortableConfigBeCopied()) bHelper = sHelper.addDialogDismissButtons(guiHelper.ButtonHelper(wx.HORIZONTAL)) if shouldAskAboutAddons: diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 1ed658bca27..32ced91a63e 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -46,6 +46,7 @@ What's New in NVDA - In WordPad, configuration of superscript/subscript reporting works as expected. (#12262) - NVDA no longer fails to announce the newly focused content on a web page if the old focus disappears and is replaced by the new focus in the same position. (#12147) - Strikethrough, superscript and subscript formatting for entire Excel cells are now reported if the corresponding option is enabled. (#12264) +- Fixed copying config during installation from a portable copy when default destination config directory is empty. (#12071, #12205) == Changes for Developers == From dd950eb1b7ad54010acaeba5726e37f19c451afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Wed, 21 Apr 2021 09:30:37 +0200 Subject: [PATCH 141/174] Reuse FILETIME from winKernel. (PR #12312) * Use FILETIME from winKernel in objidl and SAPI4 internal driver rather than defining it in every file. Follow up of PR #12250: --- source/objidl.py | 47 +++++++++++++++++++++-------------- source/synthDrivers/_sapi4.py | 25 ++++++++++++++----- source/synthDrivers/sapi4.py | 29 ++++++++++++++++++--- 3 files changed, 72 insertions(+), 29 deletions(-) diff --git a/source/objidl.py b/source/objidl.py index a5a5f127f79..884bb37d1d4 100644 --- a/source/objidl.py +++ b/source/objidl.py @@ -1,12 +1,14 @@ -#objidl.py -#A part of NonVisual Desktop Access (NVDA) -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2010-2021 NV Access Limited, Leonard de Ruijter, Joseph Lee +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html -from ctypes import * +from ctypes import c_int, c_longlong, c_ubyte, c_ulong, c_ulonglong, c_wchar_p, POINTER, Structure, windll from ctypes.wintypes import HWND, BOOL from comtypes import HRESULT, GUID, COMMETHOD, IUnknown, tagBIND_OPTS2 from comtypes.persist import IPersist +import winKernel + WSTRING = c_wchar_p class IOleWindow(IUnknown): @@ -30,20 +32,15 @@ class _ULARGE_INTEGER(Structure): ('QuadPart', c_ulonglong), ] -class _FILETIME(Structure): - _fields_ = [ - ('dwLowDateTime', c_ulong), - ('dwHighDateTime', c_ulong), - ] class tagSTATSTG(Structure): _fields_ = [ ('pwcsName', WSTRING), ('type', c_ulong), ('cbSize', _ULARGE_INTEGER), - ('mtime', _FILETIME), - ('ctime', _FILETIME), - ('atime', _FILETIME), + ('mtime', winKernel.FILETIME), + ('ctime', winKernel.FILETIME), + ('atime', winKernel.FILETIME), ('grfMode', c_ulong), ('grfLocksSupported', c_ulong), ('clsid', GUID), @@ -238,10 +235,14 @@ def __next__(self): ( ['in'], POINTER(IBindCtx), 'pbc' ), ( ['in'], POINTER(IMoniker), 'pmkToLeft' ), ( ['in'], POINTER(IMoniker), 'pmkNewlyRunning' )), - COMMETHOD([], HRESULT, 'GetTimeOfLastChange', + COMMETHOD( + [], + HRESULT, + 'GetTimeOfLastChange', ( ['in'], POINTER(IBindCtx), 'pbc' ), ( ['in'], POINTER(IMoniker), 'pmkToLeft' ), - ( ['out'], POINTER(_FILETIME), 'pfiletime' )), + (['out'], POINTER(winKernel.FILETIME), 'pfiletime') + ), COMMETHOD([], HRESULT, 'Inverse', ( ['out'], POINTER(POINTER(IMoniker)), 'ppmk' )), COMMETHOD([], HRESULT, 'CommonPrefixWith', @@ -277,12 +278,20 @@ def __next__(self): COMMETHOD([], HRESULT, 'GetObject', ( ['in'], POINTER(IMoniker), 'pmkObjectName' ), ( ['out'], POINTER(POINTER(IUnknown)), 'ppunkObject' )), - COMMETHOD([], HRESULT, 'NoteChangeTime', + COMMETHOD( + [], + HRESULT, + 'NoteChangeTime', ( ['in'], c_ulong, 'dwRegister' ), - ( ['in'], POINTER(_FILETIME), 'pfiletime' )), - COMMETHOD([], HRESULT, 'GetTimeOfLastChange', + (['in'], POINTER(winKernel.FILETIME), 'pfiletime') + ), + COMMETHOD( + [], + HRESULT, + 'GetTimeOfLastChange', ( ['in'], POINTER(IMoniker), 'pmkObjectName' ), - ( ['out'], POINTER(_FILETIME), 'pfiletime' )), + (['out'], POINTER(winKernel.FILETIME), 'pfiletime') + ), COMMETHOD([], HRESULT, 'EnumRunning', ( ['out'], POINTER(POINTER(IEnumMoniker)), 'ppenumMoniker' )), ] diff --git a/source/synthDrivers/_sapi4.py b/source/synthDrivers/_sapi4.py index 0828ce5a634..32d808e8aca 100755 --- a/source/synthDrivers/_sapi4.py +++ b/source/synthDrivers/_sapi4.py @@ -5,9 +5,24 @@ #This file is covered by the GNU General Public License. #See the file COPYING for more details. -from ctypes import * -from ctypes.wintypes import * -from comtypes import * +from ctypes import ( + cast, + c_int, + c_uint, + c_ulong, + c_ulonglong, + c_wchar, + c_wchar_p, + c_void_p, + HRESULT, + POINTER, + sizeof, + Structure +) +from ctypes.wintypes import BYTE, DWORD, LPCWSTR, WORD +from comtypes import GUID, IUnknown, STDMETHOD + +import winKernel S_OK=0 @@ -37,8 +52,6 @@ class VOICECHARSET(c_int): CHARSET_IPAPHONETIC = 1 CHARSET_ENGINEPHONETIC = 2 -class FILETIME(Structure): - _fields_ = [("dwLowDateTime", DWORD), ("dwHighDateTime", DWORD)] class LANGUAGEW(Structure): _fields_ = [("LanguageID", LANGID), @@ -111,7 +124,7 @@ class ITTSCentralW(IUnknown): STDMETHOD(HRESULT, "Phoneme", [VOICECHARSET, DWORD, SDATA, POINTER(SDATA)]), STDMETHOD(HRESULT, "PosnGet", [POINTER(QWORD)]), STDMETHOD(HRESULT, "TextData", [VOICECHARSET, DWORD, SDATA, c_void_p, GUID]), - STDMETHOD(HRESULT, "ToFileTime", [POINTER(QWORD), POINTER(FILETIME)]), + STDMETHOD(HRESULT, "ToFileTime", [POINTER(QWORD), POINTER(winKernel.FILETIME)]), STDMETHOD(HRESULT, "AudioPause"), STDMETHOD(HRESULT, "AudioResume"), STDMETHOD(HRESULT, "AudioReset"), diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py index 8cdd5fbecef..1739e7f8577 100755 --- a/source/synthDrivers/sapi4.py +++ b/source/synthDrivers/sapi4.py @@ -7,12 +7,33 @@ import locale from collections import OrderedDict import winreg -from comtypes import COMObject, COMError -from ctypes import * +from comtypes import CoCreateInstance, COMObject, COMError, GUID +from ctypes import byref, c_ulong, POINTER +from ctypes.wintypes import DWORD, WORD from synthDriverHandler import SynthDriver,VoiceInfo, synthIndexReached, synthDoneSpeaking from logHandler import log -import speech -from ._sapi4 import * +from ._sapi4 import ( + CLSID_MMAudioDest, + CLSID_TTSEnumerator, + IAudioMultiMediaDevice, + ITTSAttributes, + ITTSBufNotifySink, + ITTSCentralW, + ITTSEnumW, + TextSDATA, + TTSATTR_MAXPITCH, + TTSATTR_MAXSPEED, + TTSATTR_MAXVOLUME, + TTSATTR_MINPITCH, + TTSATTR_MINSPEED, + TTSATTR_MINVOLUME, + TTSDATAFLAG_TAGGED, + TTSFEATURE_PITCH, + TTSFEATURE_SPEED, + TTSFEATURE_VOLUME, + TTSMODEINFO, + VOICECHARSET +) import config import nvwave import weakref From 4d47375fed91b9fb8d2563b43e407b1b50ad8515 Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Wed, 21 Apr 2021 10:52:21 +0200 Subject: [PATCH 142/174] Correctly report accented letters after cap (PR #11977) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #11948 # Summary of the issue: As described in #11948, some letter with diacritic is not spelt correctly with some synth if "Say cap before capitale" is checked, e. g. 'À' is spelt 'cap A' instead of 'cap A grave'. Looking at getSpellingSpeech function in speech\__init__.py, it appears that this is due to the fact that the string 'cap À' is passed to the synth in the speech sequence in such a case. But the synth is only asked to use character mode for the strings whose length is 1. This is wrong in this case, because 'cap' should be spoken with character mode off, but 'À' should be spoken with character mode on. # Description of how this pull request fixes the issue: In getSpellingSpeech, in the string sequence, I have separated the 'cap' message string from the character to be spelt. If the character to be spelt is of length 1, i.e. not replaced by symbol description or modified pronunciation, character mode is activated. If a string in the speech sequence has a length > 1, because it is a capitalization message or because it corresponds to a character description or symbol replacement, character mode is disabled. Note that the 'cap' message can be of two types: - with 'cap' indication before as in English: 'cap %s' - with 'cap' indication after as in French: '%s majuscule' Co-authored-by: Reef Turner --- source/speech/__init__.py | 121 ++++++++++--- tests/unit/__init__.py | 3 +- tests/unit/test_speech.py | 346 ++++++++++++++++++++++++++++++++++++++ user_docs/en/changes.t2t | 1 + 4 files changed, 444 insertions(+), 27 deletions(-) create mode 100644 tests/unit/test_speech.py diff --git a/source/speech/__init__.py b/source/speech/__init__.py index cefdd72e2fe..50888cab5a5 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -211,14 +211,70 @@ def speakSpelling( speak(seq, priority=priority) -# C901 'getSpellingSpeech' is too complex -# Note: when working on getSpellingSpeech, look for opportunities to simplify -# and move logic out into smaller helper functions. -def getSpellingSpeech( # noqa: C901 +def _getSpellingSpeechAddCharMode( + seq: Generator[SequenceItemT, None, None], +) -> Generator[SequenceItemT, None, None]: + """Inserts CharacterMode commands in a speech sequence generator to ensure any single character + is spelt by the synthesizer. + @param seq: The speech sequence to be spelt. + """ + charMode = False + for item in seq: + if isinstance(item, str): + if len(item) == 1: + if not charMode: + yield CharacterModeCommand(True) + charMode = True + elif charMode: + yield CharacterModeCommand(False) + charMode = False + yield item + + +def _getSpellingCharAddCapNotification( + speakCharAs: str, + sayCapForCapitals: bool, + capPitchChange: int, + beepForCapitals: bool, +) -> Generator[SequenceItemT, None, None]: + """This function produces a speech sequence containing a character to be spelt as well as commands + to indicate that this character is uppercase if applicable. + @param speakCharAs: The character as it will be spoken by the synthesizer. + @param sayCapForCapitals: indicates if 'cap' should be reported along with the currently spelt character. + @param capPitchChange: pitch offset to apply while spelling the currently spelt character. + @param beepForCapitals: indicates if a cap notification beep should be produced while spelling the currently + spellt character. + """ + if sayCapForCapitals: + # Translators: cap will be spoken before the given letter when it is capitalized. + capMsg = _("cap %s") + (capMsgBefore, capMsgAfter) = capMsg.split('%s') + else: + capMsgBefore = '' + capMsgAfter = '' + + if capPitchChange: + yield PitchCommand(offset=capPitchChange) + if beepForCapitals: + yield BeepCommand(2000, 50) + if capMsgBefore: + yield capMsgBefore + yield speakCharAs + if capMsgAfter: + yield capMsgAfter + if capPitchChange: + yield PitchCommand() + + +def _getSpellingSpeechWithoutCharMode( text: str, - locale: Optional[str] = None, - useCharacterDescriptions: bool = False + locale: str, + useCharacterDescriptions: bool, + sayCapForCapitals: bool, + capPitchChange: int, + beepForCapitals: bool, ) -> Generator[SequenceItemT, None, None]: + defaultLanguage=getCurrentLanguage() if not locale or (not config.conf['speech']['autoDialectSwitching'] and locale.split('_')[0]==defaultLanguage.split('_')[0]): locale=defaultLanguage @@ -230,9 +286,6 @@ def getSpellingSpeech( # noqa: C901 if not text.isspace(): text=text.rstrip() - synth = getSynth() - synthConfig=config.conf["speech"][synth.name] - charMode = False textLength=len(text) count = 0 localeHasConjuncts = True if locale.split('_',1)[0] in LANGS_WITH_CONJUNCT_CHARS else False @@ -253,27 +306,43 @@ def getSpellingSpeech( # noqa: C901 speakCharAs=charDesc[0] if textLength>1 else IDEOGRAPHIC_COMMA.join(charDesc) else: speakCharAs=characterProcessing.processSpeechSymbol(locale,speakCharAs) - if uppercase and synthConfig["sayCapForCapitals"]: - # Translators: cap will be spoken before the given letter when it is capitalized. - speakCharAs=_("cap %s")%speakCharAs - if uppercase and synth.isSupported("pitch") and synthConfig["capPitchChange"]: - yield PitchCommand(offset=synthConfig["capPitchChange"]) if config.conf['speech']['autoLanguageSwitching']: yield LangChangeCommand(locale) - if len(speakCharAs) == 1 and synthConfig["useSpellingFunctionality"]: - if not charMode: - yield CharacterModeCommand(True) - charMode = True - elif charMode: - yield CharacterModeCommand(False) - charMode = False - if uppercase and synthConfig["beepForCapitals"]: - yield BeepCommand(2000, 50) - yield speakCharAs - if uppercase and synth.isSupported("pitch") and synthConfig["capPitchChange"]: - yield PitchCommand() + yield from _getSpellingCharAddCapNotification( + speakCharAs, + uppercase and sayCapForCapitals, + capPitchChange if uppercase else 0, + uppercase and beepForCapitals, + ) yield EndUtteranceCommand() + +def getSpellingSpeech( + text: str, + locale: Optional[str] = None, + useCharacterDescriptions: bool = False +) -> Generator[SequenceItemT, None, None]: + + synth = getSynth() + synthConfig = config.conf["speech"][synth.name] + + if synth.isSupported("pitch"): + capPitchChange = synthConfig["capPitchChange"] + else: + capPitchChange = 0 + seq = _getSpellingSpeechWithoutCharMode( + text, + locale, + useCharacterDescriptions, + sayCapForCapitals=synthConfig["sayCapForCapitals"], + capPitchChange=capPitchChange, + beepForCapitals=synthConfig["beepForCapitals"], + ) + if synthConfig["useSpellingFunctionality"]: + seq = _getSpellingSpeechAddCharMode(seq) + yield from seq + + def getCharDescListFromText(text,locale): """This method prepares a list, which contains character and its description for all characters the text is made up of, by checking the presence of character descriptions in characterDescriptions.dic of that locale for all possible combination of consecutive characters in the text. This is done to take care of conjunct characters present in several languages such as Hindi, Urdu, etc. diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index 4e4d26c05b4..a5c7efdafc1 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -21,7 +21,8 @@ import gettext #Localization settings locale.setlocale(locale.LC_ALL,'') -gettext.install('nvda') +translations = gettext.NullTranslations() +translations.install() # The path to the unit tests. UNIT_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/tests/unit/test_speech.py b/tests/unit/test_speech.py new file mode 100644 index 00000000000..d6821b72654 --- /dev/null +++ b/tests/unit/test_speech.py @@ -0,0 +1,346 @@ +# 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) 2021 NV Access Limited, Cyrille Bougot + +"""Unit tests for the speech module. +""" +import unittest +import gettext +import typing +import config +from speech import ( + _getSpellingSpeechAddCharMode, + _getSpellingCharAddCapNotification, + _getSpellingSpeechWithoutCharMode, +) +from speech.commands import ( + EndUtteranceCommand, + CharacterModeCommand, + PitchCommand, + BeepCommand, + LangChangeCommand +) + + +class Test_getSpellingSpeechAddCharMode(unittest.TestCase): + def test_symbolNamesAtStartAndEnd(self): + # Spelling ¡hola! + seq = (c for c in [ + 'inverted exclamation point', + EndUtteranceCommand(), + 'h', + EndUtteranceCommand(), + 'o', + EndUtteranceCommand(), + 'l', + EndUtteranceCommand(), + 'a', + EndUtteranceCommand(), + 'bang', + EndUtteranceCommand() + ]) + expected = repr([ + 'inverted exclamation point', + EndUtteranceCommand(), + CharacterModeCommand(True), + 'h', + EndUtteranceCommand(), + 'o', + EndUtteranceCommand(), + 'l', + EndUtteranceCommand(), + 'a', + EndUtteranceCommand(), + CharacterModeCommand(False), + 'bang', + EndUtteranceCommand() + ]) + output = _getSpellingSpeechAddCharMode(seq) + self.assertEqual(repr(list(output)), expected) + + def test_manySymbolNamesInARow(self): + # Spelling a...b + seq = (c for c in [ + 'a', + EndUtteranceCommand(), + 'dot', + EndUtteranceCommand(), + 'dot', + EndUtteranceCommand(), + 'dot', + EndUtteranceCommand(), + 'b', + EndUtteranceCommand() + ]) + expected = repr([ + CharacterModeCommand(True), + 'a', + EndUtteranceCommand(), + CharacterModeCommand(False), + 'dot', + EndUtteranceCommand(), + 'dot', + EndUtteranceCommand(), + 'dot', + EndUtteranceCommand(), + CharacterModeCommand(True), + 'b', + EndUtteranceCommand() + ]) + output = _getSpellingSpeechAddCharMode(seq) + self.assertEqual(repr(list(output)), expected) + + +class Translation_Fake(gettext.NullTranslations): + originalTranslationFunction: gettext.NullTranslations + translationResults: typing.Dict[str, str] + + def __init__(self, originalTranslationFunction: gettext.NullTranslations): + self.originalTranslationFunction = originalTranslationFunction + self.translationResults = {} + super().__init__() + + def gettext(self, msg: str) -> str: + if msg in self.translationResults: + return self.translationResults[msg] + return self.originalTranslationFunction.gettext(msg) + + +class Test_getSpellingCharAddCapNotification(unittest.TestCase): + translationsFake: Translation_Fake + + @classmethod + def setUpClass(cls): + from . import translations as originalTranslationClass + cls.translationsFake = Translation_Fake(originalTranslationClass) + cls.translationsFake.install() + + @classmethod + def tearDownClass(cls): + cls.translationsFake.originalTranslationFunction.install() + + def tearDown(self) -> None: + self.translationsFake.translationResults.clear() + + def test_noNotifications(self): + expected = repr([ + 'A', + ]) + output = _getSpellingCharAddCapNotification( + speakCharAs='A', + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_pitchNotifications(self): + expected = repr([ + PitchCommand(offset=30), + 'A', + PitchCommand() + ]) + output = _getSpellingCharAddCapNotification( + speakCharAs='A', + sayCapForCapitals=False, + capPitchChange=30, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_beepNotifications(self): + expected = repr([ + BeepCommand(2000, 50, left=50, right=50), + 'A', + ]) + output = _getSpellingCharAddCapNotification( + speakCharAs='A', + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=True, + ) + self.assertEqual(repr(list(output)), expected) + + def test_capNotifications(self): + expected = repr([ + 'cap ', + 'A', + ]) + output = _getSpellingCharAddCapNotification( + speakCharAs='A', + sayCapForCapitals=True, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_capNotificationsWithPlaceHolderBefore(self): + self.translationsFake.translationResults["cap %s"] = "%s cap" + expected = repr(['A', ' cap', ]) # for English this would be "cap A" + output = _getSpellingCharAddCapNotification( + speakCharAs='A', + sayCapForCapitals=True, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_allNotifications(self): + expected = repr([ + PitchCommand(offset=30), + BeepCommand(2000, 50, left=50, right=50), + 'cap ', + 'A', + PitchCommand() + ]) + output = _getSpellingCharAddCapNotification( + speakCharAs='A', + sayCapForCapitals=True, + capPitchChange=30, + beepForCapitals=True, + ) + self.assertEqual(repr(list(output)), expected) + + +class Test_getSpellingSpeechWithoutCharMode(unittest.TestCase): + + def setUp(self): + config.conf['speech']['autoLanguageSwitching'] = False + + def tearDown(self): + # Restore default value + config.conf['speech']['autoLanguageSwitching'] = config.conf.getConfigValidation( + ['speech', 'autoLanguageSwitching'] + ).default + + def test_simpleSpelling(self): + expected = repr([ + 'a', + EndUtteranceCommand(), + 'b', + EndUtteranceCommand(), + 'c', + EndUtteranceCommand(), + ]) + output = _getSpellingSpeechWithoutCharMode( + text='abc', + locale=None, + useCharacterDescriptions=False, + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_cap(self): + expected = repr([ + PitchCommand(offset=30), + BeepCommand(2000, 50, left=50, right=50), + 'cap ', + 'A', + PitchCommand(), + EndUtteranceCommand(), + ]) + output = _getSpellingSpeechWithoutCharMode( + text='A', + locale=None, + useCharacterDescriptions=False, + sayCapForCapitals=True, + capPitchChange=30, + beepForCapitals=True, + ) + self.assertEqual(repr(list(output)), expected) + + def test_characterMode(self): + expected = repr([ + 'Alfa', + EndUtteranceCommand(), + ]) + output = _getSpellingSpeechWithoutCharMode( + text='a', + locale='en', + useCharacterDescriptions=True, + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_blank(self): + expected = repr([ + 'blank', + ]) + output = _getSpellingSpeechWithoutCharMode( + text='', + locale=None, + useCharacterDescriptions=False, + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_onlySpaces(self): + expected = repr([ + 'space', + EndUtteranceCommand(), + 'tab', + EndUtteranceCommand(), + ]) + output = _getSpellingSpeechWithoutCharMode( + text=' \t', + locale=None, + useCharacterDescriptions=False, + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_trimRightSpace(self): + expected = repr([ + 'a', + EndUtteranceCommand(), + ]) + output = _getSpellingSpeechWithoutCharMode( + text='a ', + locale=None, + useCharacterDescriptions=False, + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_symbol(self): + expected = repr([ + 'bang', + EndUtteranceCommand(), + ]) + output = _getSpellingSpeechWithoutCharMode( + text='!', + locale=None, + useCharacterDescriptions=False, + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) + + def test_languageDetection(self): + config.conf['speech']['autoLanguageSwitching'] = True + expected = repr([ + LangChangeCommand('fr_FR'), + 'a', + EndUtteranceCommand(), + ]) + output = _getSpellingSpeechWithoutCharMode( + text='a', + locale='fr_FR', + useCharacterDescriptions=False, + sayCapForCapitals=False, + capPitchChange=0, + beepForCapitals=False, + ) + self.assertEqual(repr(list(output)), expected) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 32ced91a63e..ebe8bb7adf9 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -47,6 +47,7 @@ What's New in NVDA - NVDA no longer fails to announce the newly focused content on a web page if the old focus disappears and is replaced by the new focus in the same position. (#12147) - Strikethrough, superscript and subscript formatting for entire Excel cells are now reported if the corresponding option is enabled. (#12264) - Fixed copying config during installation from a portable copy when default destination config directory is empty. (#12071, #12205) +- Fixed incorrect announcement of some letters with accents or diacritic when 'Say cap before capitals' option is checked. (#11948) == Changes for Developers == From 330961cc6174007bc37c407412524071b37c5025 Mon Sep 17 00:00:00 2001 From: Luke Davis <8139760+XLTechie@users.noreply.github.com> Date: Wed, 21 Apr 2021 04:56:00 -0400 Subject: [PATCH 143/174] Fix readme typo/grammar (PR #12301) In reading the README recently, I noticed several areas where minor grammar corrections could be made, and one or two where the grammar in certain paragraphs could be made to flow better for a more clear understanding. Mostly these are rather insignificant changes, and on their own wouldn't be worth a PR, but collected together I thought they might be considered. # Description of how this pull request fixes the issue: Thinking that improving the readability of the readme could aid new devs, I made the following changes: - Inserted a few missing words with my best guess as to what they should have been. - Fixed one capitalization error. - Converted indefinite articles to definite where appropriate. - Changed the format of a link to be like other links on the same site. - In one case, added a missing link. - Rephrased a couple small items, and the Get Support paragraph. --- readme.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index d01d00f3584..ed8de569a4d 100644 --- a/readme.md +++ b/readme.md @@ -4,22 +4,22 @@ NVDA (NonVisual Desktop Access) is a free, open source screen reader for Microso It is developed by NV Access in collaboration with a global community of contributors. To learn more about NVDA or download a copy, visit the main [NV Access](http://www.nvaccess.org/) website. -Please note: the NVDA project has a [Citizen and Contributor Code of Conduct](CODE_OF_CONDUCT.md). NV Access expects that all contributors and other community members read and abide by the rules set out in this document while participating or contributing to this project. +Please note: the NVDA project has a [Citizen and Contributor Code of Conduct](CODE_OF_CONDUCT.md). NV Access expects that all contributors and other community members will read and abide by the rules set out in this document while participating or contributing to this project. ## Get support -Either if you are a beginner, an advanced user, a new or a long time developer, or if you are an organization willing to know more or to contribute to NVDA, you can get support through the documentation in place as well as several communication channels dedicated for the NVDA screen reader. Here is an overview of the most important support sources. +Whether you are a beginner, an advanced user, a new or a long time developer; or if you represent an organization wishing to know more or to contribute to NVDA: you can get support through the included documentation as well as several communication channels dedicated to the NVDA screen reader. Here is an overview of the most important support sources. ### Documentation * [NVDA User Guide](https://www.nvaccess.org/files/nvda/documentation/userGuide.html) * [NVDA Developer Guide](https://www.nvaccess.org/files/nvda/documentation/developerGuide.html) * [NVDA Add-ons Development Internals](https://github.com/nvdaaddons/DevGuide/wiki) * [NVDA ControllerClient manual](https://github.com/nvaccess/nvda/tree/master/extras/controllerClient) -* Further documentation is included in the Wiki of this repository and in the [Community Wiki](https://github.com/nvaccess/nvda-community/wiki) +* Further documentation is available in the NVDA repository's [Wiki](https://github.com/nvaccess/nvda/wiki), and in the [Community Wiki](https://github.com/nvaccess/nvda-community/wiki) ### Communication channels * [NVDA Users Mailing List](https://nvda.groups.io/g/nvda) * [NVDA Developers Mailing List](https://groups.io/g/nvda-devel) -* [NVDA Add-ons Mailing List](https://nvda-addons.groups.io/g/nvda-addons) +* [NVDA Add-ons Mailing List](https://groups.io/g/nvda-addons) * [Instant Messaging channel for NVDA Support](https://gitter.im/nvaccess/NVDA) * [Other sources including groups and profiles on social media channels, language specific websites and mailing lists etc.](https://github.com/nvaccess/nvda-community/wiki/Connect) @@ -110,7 +110,7 @@ The following dependencies aren't needed by most people, and are not included in ```git clone https://github.com/nvaccess/vscode-nvda.git .vscode``` ### Python dependencies -NVDA and its build system also depend on an extensive list of Python packages. They are all listed with their specific versions in a requirements.txt file in the root of this repository. However, the build system takes care of fetching these itself when needed. these packages will be installed into an isolated Python virtual environment within this repository, and will not affect your system-wide set of packages. +NVDA and its build system also depend on an extensive list of Python packages. They are all listed with their specific versions in the requirements.txt file in the root of this repository. However, the build system takes care of fetching these itself when needed. These packages will be installed into an isolated Python virtual environment within this repository, and will not affect your system-wide set of packages. ## Preparing the Source Tree Before you can run the NVDA source code, you must prepare the source tree. @@ -166,7 +166,7 @@ These arguments are also documented in the user guide. ## Building NVDA A binary build of NVDA can be run on a system without Python and all of NVDA's other dependencies installed (as we do for snapshots and releases). -Binary archives and bundles can be created using scons from the root of the NVDA source distribution. To build any of the following, open a command prompt and change to this directory. +Binary archives and bundles can be created using scons from the root of the NVDA source distribution. To build any of the following, open a command prompt and change to that directory. To make a non-archived binary build (equivalent to an extracted portable archive), type: @@ -287,7 +287,7 @@ If you create a Pull Request, the `base` branch you use here should be the same runlint origin/master ``` -To be warned about linting errors faster, you may wish to integrate Flake8 other development tools you are using. +To be warned about linting errors faster, you may wish to integrate Flake8 with other development tools you are using. For more details, see `tests/lint/readme.md` ### Unit Tests From a94f5fe214640d3a02d297cd48ed0b8ae3324d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Thu, 22 Apr 2021 00:33:26 +0200 Subject: [PATCH 144/174] No longer fail to report content of unfocused text fields in Firefox (#12319) Fixes #12114 PR #12025 started catching only very specific exceptions when getting selection of edit fields. However in Firefox attempting to get caret for non focused edit fields results in RuntimeError which made it impossible to speak these controls. Description of how this pull request fixes the issue: When getting content of edit fields in speech, we're now catching RuntimeError as well and treating this situation like no selection. --- source/speech/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/speech/__init__.py b/source/speech/__init__.py index 50888cab5a5..d7a9b72943c 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -568,7 +568,7 @@ def getObjectSpeech( # noqa: C901 if shouldReportTextContent: try: info = obj.makeTextInfo(textInfos.POSITION_SELECTION) - except NotImplementedError: + except (NotImplementedError, RuntimeError): info = None if info and not info.isCollapsed: # if there is selected text, then there is a value and we do not report placeholder From 03746a1dbb542bbd001f2e10b31105817bf96322 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 22 Apr 2021 13:58:01 +1000 Subject: [PATCH 145/174] Fix up from #12210: Excel without UIA enabled: again allow typing and editing in cells (#12321) Fixes #12303 After merging of pr #12210 editing cells in Excel without UIA enabled became im possible as NVDA did not report / track focus had ented the Cell Edit control. This was due to the EXCEL6 window accidentally being marked as having a good UIA implementation. This was testing code left over from the early implementation of #12210. Description of how this pull request fixes the issue: Remove EXCEL6 from the good UIA windows list. Also ensure that MSAA focus events on this window are ignored when using Excel with UIA enabled, as Excel will fire its own UIA focus event on an edit control within the active cell. --- source/_UIAHandler.py | 1 - source/appModules/excel.py | 26 +++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/source/_UIAHandler.py b/source/_UIAHandler.py index 76e0047d0a3..e377a88c045 100644 --- a/source/_UIAHandler.py +++ b/source/_UIAHandler.py @@ -58,7 +58,6 @@ goodUIAWindowClassNames=[ # A WDAG (Windows Defender Application Guard) Window is always native UIA, even if it doesn't report as such. 'RAIL_WINDOW', - "EXCEL6", ] badUIAWindowClassNames=[ diff --git a/source/appModules/excel.py b/source/appModules/excel.py index afe6cac130e..adac3378961 100644 --- a/source/appModules/excel.py +++ b/source/appModules/excel.py @@ -5,6 +5,7 @@ # For more details see: https://www.gnu.org/licenses/gpl-2.0.html import time +import config import eventHandler import api import UIAHandler @@ -15,6 +16,8 @@ from NVDAObjects.window.edit import UnidentifiedEdit from NVDAObjects.window import Window from NVDAObjects.window.excel import ExcelCell +from NVDAObjects.IAccessible import IAccessible + class Excel6(Window): """ @@ -43,6 +46,17 @@ def _get_focusRedirect(self): self.focusRedirect=obj return obj + +class Excel6_WhenUIAEnabled(IAccessible): + """ + #12303: When accessing Microsoft Excel via UI Automation + MSAA focus events on the old formula edit window should be completely ignored. + UI Automation will fire its own ones. + """ + + shouldAllowIAccessibleFocusEvent = False + + class AppModule(appModuleHandler.AppModule): def chooseNVDAObjectOverlayClasses(self, obj, clsList): @@ -55,4 +69,14 @@ def chooseNVDAObjectOverlayClasses(self, obj, clsList): pass clsList.insert(0, DisplayModelEditableText) if windowClass=="EXCEL6": - clsList.insert(0,Excel6) + if config.conf["UIA"]["useInMSExcelWhenAvailable"]: + # #12303: When accessing Microsoft Excel via UI Automation + # MSAA focus events on the old formula edit window should be completely ignored. + # UI Automation will fire its own ones. + clsList.insert(0, Excel6_WhenUIAEnabled) + else: + # #12303: The old Formula Edit window in recent versions of Excel + # may not be accessible with display models as GDI is no longer used. + # However, UI Automation does expose an accessible edit control within the active cell, + # So use a class that will redirect focus to that. + clsList.insert(0, Excel6) From 60c0b76b8271a5b484d4cebf4486772e81828210 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Thu, 22 Apr 2021 16:06:47 +1000 Subject: [PATCH 146/174] refactor the exit of nvda and gui.terminate (#12286) Summary of the issue: When restarting NVDA, WM_QUIT is posted as an event to the window, forcibly exiting the app. This leaves objects such as the system tray icon left behind. Additionally, changes introduced in #12183 - caused the braille viewer to be closed without saving state properly - lost code that destroyed the system tray and menu in some instances - made most of gui.terminate no longer necessary/redundant Description of how this pull request fixes the issue: - A windows event winUser.WM_EXIT_NVDA is registered that triggers safeAppExit and can be called across instances of NVDA. - move the safe destruction of the brailleviewer to safeAppExit so that it is exited properly before destruction - reintroduce the destruction of the system tray icon and menu, and remove the icon manually. - ensured safeAppExit is not called from gui.terminate if it has been called elsewhere to terminate the app. WM_QUIT is the other way to exit the MainLoop other than safeAppExit - removed restarting the MainLoop in gui.terminate to process pending events as this doesn't work. Known issues with pull request: - When starting a new instance of NVDA with an existing instance running, where one is version <2020.4, NVDA will not exit safely. Instead, the running NVDA copy will terminate directly using the behaviour of 2020.4. This is because WM_EXIT_NVDA won't be registered on the older instance. - Issues with terminating NVDA across instances cannot be logged properly as the loghandler hasn't been initialized --- source/core.py | 9 +++++ source/gui/__init__.py | 74 +++++++++++++++++++++--------------- source/gui/startupDialogs.py | 2 +- source/nvda.pyw | 46 ++++++++++++++++++---- source/winUser.py | 38 +++++++++++++++--- user_docs/en/changes.t2t | 1 + 6 files changed, 124 insertions(+), 46 deletions(-) diff --git a/source/core.py b/source/core.py index aec9d54b570..a31efb5b155 100644 --- a/source/core.py +++ b/source/core.py @@ -377,6 +377,12 @@ def __init__(self, windowName=None): self.orientationStateCache = self.ORIENTATION_NOT_INITIALIZED self.orientationCoordsCache = (0,0) self.handlePowerStatusChange() + # Accept WM_EXIT_NVDA from other NVDA instances + import winUser + if not winUser.user32.ChangeWindowMessageFilterEx(self.handle, winUser.WM_EXIT_NVDA, 1, None): + log.error( + f"Unable to set the thread {self.handle} to receive WM_EXIT_NVDA from other processes") + raise winUser.WinError() def windowProc(self, hwnd, msg, wParam, lParam): post_windowMessageReceipt.notify(msg=msg, wParam=wParam, lParam=lParam) @@ -384,6 +390,9 @@ def windowProc(self, hwnd, msg, wParam, lParam): self.handlePowerStatusChange() elif msg == winUser.WM_DISPLAYCHANGE: self.handleScreenOrientationChange(lParam) + elif msg == winUser.WM_EXIT_NVDA: + log.debug("NVDA instance being closed from another instance") + gui.safeAppExit() def handleScreenOrientationChange(self, lParam): import ui diff --git a/source/gui/__init__.py b/source/gui/__init__.py index b812bab9324..5ff80b492ee 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -47,7 +47,7 @@ ### Globals mainFrame = None isInMessageBox = False - +hasAppExited = False class MainFrame(wx.Frame): @@ -360,22 +360,54 @@ def onConfigProfilesCommand(self, evt): def safeAppExit(): """ - Ensures the app is exited by all the top windows being destroyed + Ensures the app is exited by all the top windows being destroyed. + wx objects that don't inherit from wx.Window (eg sysTrayIcon, Menu) need to be manually destroyed. """ + import brailleViewer + brailleViewer.destroyBrailleViewer() + + app = wx.GetApp() + + # prevent race condition with object deletion + # prevent deletion of the object while we work on it. + _SettingsDialog = settingsDialogs.SettingsDialog + nonWeak: typing.Dict[_SettingsDialog, _SettingsDialog] = dict(_SettingsDialog._instances) + + for instance, state in nonWeak.items(): + if state is _SettingsDialog.DialogState.DESTROYED: + log.error( + "Destroyed but not deleted instance of gui.SettingsDialog exists" + f": {instance.title} - {instance.__class__.__qualname__} - {instance}" + ) + else: + log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance)) + + # wx.Windows destroy child Windows automatically but wx.Menu and TaskBarIcon don't inherit from wx.Window. + # They must be manually destroyed when exiting the app. + # Note: this doesn't consistently clean them from the tray and appears to be a wx issue. (#12286, #12238) + log.debug("destroying system tray icon and menu") + app.ScheduleForDestruction(mainFrame.sysTrayIcon.menu) + mainFrame.sysTrayIcon.RemoveIcon() + app.ScheduleForDestruction(mainFrame.sysTrayIcon) + for window in wx.GetTopLevelWindows(): if isinstance(window, wx.Dialog) and window.IsModal(): - log.info(f"ending modal {window} during exit process") + log.debug(f"ending modal {window} during exit process") wx.CallAfter(window.EndModal, wx.ID_CLOSE_ALL) if isinstance(window, MainFrame): - log.info(f"destroying main frame during exit process") + log.debug("destroying main frame during exit process") # the MainFrame has EVT_CLOSE bound to the ExitDialog # which calls this function on exit, so destroy this window - wx.CallAfter(window.Destroy) + app.ScheduleForDestruction(window) else: - log.info(f"closing window {window} during exit process") + log.debug(f"closing window {window} during exit process") wx.CallAfter(window.Close) + global hasAppExited + hasAppExited = True + + class SysTrayIcon(wx.adv.TaskBarIcon): def __init__(self, frame): @@ -584,33 +616,13 @@ def wx_CallAfter_wrapper(func, *args, **kwargs): wx.CallAfter = wx_CallAfter_wrapper def terminate(): - import brailleViewer - brailleViewer.destroyBrailleViewer() + global mainFrame - # prevent race condition with object deletion - # prevent deletion of the object while we work on it. - _SettingsDialog = settingsDialogs.SettingsDialog - nonWeak: typing.Dict[_SettingsDialog, _SettingsDialog] = dict(_SettingsDialog._instances) + # If MainLoop is terminated through WM_QUIT, such as starting an NVDA instance older than 2021.1, + # safeAppExit has not been called yet + if not hasAppExited: + safeAppExit() - for instance, state in nonWeak.items(): - if state is _SettingsDialog.DialogState.DESTROYED: - log.error( - "Destroyed but not deleted instance of gui.SettingsDialog exists" - f": {instance.title} - {instance.__class__.__qualname__} - {instance}" - ) - else: - log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance)) - global mainFrame - # This is called after the main loop exits because WM_QUIT exits the main loop - # without destroying all objects correctly and we need to support WM_QUIT. - # Therefore, any request to exit should exit the main loop. - safeAppExit() - # #4460: We need another iteration of the main loop - # so that everything (especially the TaskBarIcon) is cleaned up properly. - # ProcessPendingEvents doesn't seem to work, but MainLoop does. - # Because the top window gets destroyed, - # MainLoop thankfully returns pretty quickly. - wx.GetApp().MainLoop() mainFrame = None def showGui(): diff --git a/source/gui/startupDialogs.py b/source/gui/startupDialogs.py index aadc038e8c0..1e432e6a060 100644 --- a/source/gui/startupDialogs.py +++ b/source/gui/startupDialogs.py @@ -117,7 +117,7 @@ def run(cls): gui.mainFrame.prePopup() d = cls(gui.mainFrame) d.ShowModal() - d.Destroy() + wx.CallAfter(d.Destroy) gui.mainFrame.postPopup() diff --git a/source/nvda.pyw b/source/nvda.pyw index 92117109c51..58b3c17d99d 100755 --- a/source/nvda.pyw +++ b/source/nvda.pyw @@ -160,18 +160,28 @@ for name in pathAppArgs: newVal = os.path.abspath(origVal) setattr(globalVars.appArgs, name, newVal) -def terminateRunningNVDA(window): + +def safelyTerminateRunningNVDA(window: winUser.HWND): processID,threadID=winUser.getWindowThreadProcessID(window) - winUser.PostMessage(window,winUser.WM_QUIT,0,0) + winUser.PostSafeQuitMessage(window) h=winKernel.openProcess(winKernel.SYNCHRONIZE,False,processID) if not h: # The process is already dead. return try: - res=winKernel.waitForSingleObject(h,4000) + res = winKernel.waitForSingleObject(h, 6000) # give time to exit NVDA safely if res==0: # The process terminated within the timeout period. return + else: + raise OSError("Failed to terminate with WM_EXIT_NVDA") + except OSError: + # allow for updating between NVDA versions, as NVDA <= 2020.4 does not accept WM_EXIT_NVDA messages + print("Failed to post a safe quit message across NVDA instances, sending WM_QUIT", file=sys.stderr) + res = _terminateRunningLegacyNVDA(window) + if res == 0: + # The process terminated within the timeout period. + return finally: winKernel.closeHandle(h) @@ -185,6 +195,20 @@ def terminateRunningNVDA(window): finally: winKernel.closeHandle(h) + +def _terminateRunningLegacyNVDA(window: winUser.HWND) -> int: + ''' + Returns 0 on success, raises an OSError based WinErr if the process isn't killed + ''' + processID, _threadID = winUser.getWindowThreadProcessID(window) + winUser.PostMessage(window, winUser.WM_QUIT, 0, 0) + h = winKernel.openProcess(winKernel.SYNCHRONIZE, False, processID) + if not h: + # The process is already dead. + return 0 + return winKernel.waitForSingleObject(h, 4000) + + #Handle running multiple instances of NVDA try: oldAppWindowHandle=winUser.FindWindow(u'wxWindowClassNR',u'NVDA') @@ -197,8 +221,9 @@ if oldAppWindowHandle and not globalVars.appArgs.easeOfAccess: # NVDA is running. sys.exit(0) try: - terminateRunningNVDA(oldAppWindowHandle) - except: + safelyTerminateRunningNVDA(oldAppWindowHandle) + except Exception as e: + print(f"Couldn't terminate existing NVDA process, abandoning start:\nException: {e}", file=sys.stderr) sys.exit(1) if globalVars.appArgs.quit or (oldAppWindowHandle and globalVars.appArgs.easeOfAccess): sys.exit(0) @@ -251,9 +276,14 @@ if customVenvDetected: log.warning("NVDA launched using a custom Python virtual environment.") if globalVars.appArgs.changeScreenReaderFlag: winUser.setSystemScreenReaderFlag(True) -#Accept wm_quit from other processes, even if running with higher privilages -if not ctypes.windll.user32.ChangeWindowMessageFilter(winUser.WM_QUIT,1): - raise WinError() + +# Accept WM_QUIT from other processes, even if running with higher privilages +# 2020.4 and earlier versions sent a WM_QUIT message when asking NVDA to exit. +# Some users may run several different versions of NVDA, so we continue to support this. +# WM_QUIT does not allow NVDA to shutdown cleanly, now WM_EXIT_NVDA is used instead +if not ctypes.windll.user32.ChangeWindowMessageFilter(winUser.WM_QUIT, winUser.MSGFLT.ALLOW): + log.error("Unable to set the NVDA process to receive WM_QUIT messages from other processes") + raise winUser.WinError() # Make this the last application to be shut down and don't display a retry dialog box. winKernel.SetProcessShutdownParameters(0x100, winKernel.SHUTDOWN_NORETRY) if not isSecureDesktop and not config.isAppX: diff --git a/source/winUser.py b/source/winUser.py index e007c443a81..fd7719ba86c 100644 --- a/source/winUser.py +++ b/source/winUser.py @@ -13,6 +13,7 @@ from ctypes.wintypes import HWND, RECT, DWORD import winKernel from textUtils import WCHAR_ENCODING +import enum #dll handles user32=windll.user32 @@ -114,12 +115,6 @@ class GUITHREADINFO(Structure): CBS_OWNERDRAWFIXED=0x0010 CBS_OWNERDRAWVARIABLE=0x0020 CBS_HASSTRINGS=0x00200 -WM_NULL=0 -WM_QUIT=18 -WM_COPYDATA=74 -WM_NOTIFY=78 -WM_DEVICECHANGE=537 -WM_USER=1024 #PeekMessage PM_REMOVE=1 PM_NOYIELD=2 @@ -146,6 +141,7 @@ class GUITHREADINFO(Structure): WM_NOTIFY = 78 WM_USER = 1024 WM_QUIT = 18 +WM_DEVICECHANGE = 537 WM_DISPLAYCHANGE = 0x7e WM_GETTEXT=13 WM_GETTEXTLENGTH=14 @@ -377,6 +373,27 @@ class GUITHREADINFO(Structure): # The height of the virtual screen, in pixels. SM_CYVIRTUALSCREEN = 79 + +class MSGFLT(enum.IntEnum): + # Actions associated with ChangeWindowMessageFilterEx + # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-changewindowmessagefilterex + # Adds the message to the filter. This has the effect of allowing the message to be received. + ALLOW = 1 + # Removes the message from the filter. This has the effect of blocking the message. + DISALLOW = 2 + # Resets the window message filter to the default. + # Any message allowed globally or process-wide will get through. + RESET = 0 + + +# Registers an application wide Window Message so that NVDA can be exited across instances +WM_EXIT_NVDA = user32.RegisterWindowMessageW("WM_EXIT_NVDA") +if not WM_EXIT_NVDA: + winErr = WinError() + # provides additional information to the OSError based WinError + winErr.filename = "Failed to register Windows application message WM_EXIT_NVDA" + raise winErr + def setSystemScreenReaderFlag(val): user32.SystemParametersInfoW(SPI_SETSCREENREADER,val,0,SPIF_UPDATEINIFILE|SPIF_SENDCHANGE) @@ -601,6 +618,15 @@ def PostMessage(hwnd, msg, wParam, lParam): if not user32.PostMessageW(hwnd, msg, wParam, lParam): raise WinError() + +def PostSafeQuitMessage(hwnd: HWND): + """ + Posts a WM_EXIT_NVDA quit message across windows to exit NVDA safely from another instance + @param hwnd: Target NVDA window id + """ + if not user32.PostMessageW(hwnd, WM_EXIT_NVDA, None, None): + raise WinError() + user32.VkKeyScanExW.restype = SHORT def VkKeyScanEx(ch, hkl): res = user32.VkKeyScanExW(WCHAR(ch), hkl) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index ebe8bb7adf9..0529bda6509 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -115,6 +115,7 @@ What's New in NVDA - This usage is prefered instead of ti1.SetEndPoint(ti2,"startToEnd") - `wx.CENTRE_ON_SCREEN` and `wx.CENTER_ON_SCREEN` are removed, use `self.CentreOnScreen()` instead. (#12309) - `easeOfAccess.isSupported` has been removed, NVDA only supports versions of Windows where this evaluates to `True`. (#12222) +- Do not exit NVDA by sending a `WM_QUIT` message to the process. Instead send `winUser.WM_EXIT_NVDA` to the window handle (found by `winUser.FindWindow('wxWindowClassNR', 'NVDA')`). (#12286) = 2020.4 = From d9afe35d679d92424c76f5ff2bc408336640197b Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Fri, 23 Apr 2021 13:55:55 +1000 Subject: [PATCH 147/174] Revert "refactor the exit of nvda and gui.terminate (#12286)" (#12326) This reverts commit 60c0b76b8271a5b484d4cebf4486772e81828210. --- source/core.py | 9 ----- source/gui/__init__.py | 74 +++++++++++++++--------------------- source/gui/startupDialogs.py | 2 +- source/nvda.pyw | 46 ++++------------------ source/winUser.py | 38 +++--------------- user_docs/en/changes.t2t | 1 - 6 files changed, 46 insertions(+), 124 deletions(-) diff --git a/source/core.py b/source/core.py index a31efb5b155..aec9d54b570 100644 --- a/source/core.py +++ b/source/core.py @@ -377,12 +377,6 @@ def __init__(self, windowName=None): self.orientationStateCache = self.ORIENTATION_NOT_INITIALIZED self.orientationCoordsCache = (0,0) self.handlePowerStatusChange() - # Accept WM_EXIT_NVDA from other NVDA instances - import winUser - if not winUser.user32.ChangeWindowMessageFilterEx(self.handle, winUser.WM_EXIT_NVDA, 1, None): - log.error( - f"Unable to set the thread {self.handle} to receive WM_EXIT_NVDA from other processes") - raise winUser.WinError() def windowProc(self, hwnd, msg, wParam, lParam): post_windowMessageReceipt.notify(msg=msg, wParam=wParam, lParam=lParam) @@ -390,9 +384,6 @@ def windowProc(self, hwnd, msg, wParam, lParam): self.handlePowerStatusChange() elif msg == winUser.WM_DISPLAYCHANGE: self.handleScreenOrientationChange(lParam) - elif msg == winUser.WM_EXIT_NVDA: - log.debug("NVDA instance being closed from another instance") - gui.safeAppExit() def handleScreenOrientationChange(self, lParam): import ui diff --git a/source/gui/__init__.py b/source/gui/__init__.py index 5ff80b492ee..b812bab9324 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -47,7 +47,7 @@ ### Globals mainFrame = None isInMessageBox = False -hasAppExited = False + class MainFrame(wx.Frame): @@ -360,54 +360,22 @@ def onConfigProfilesCommand(self, evt): def safeAppExit(): """ - Ensures the app is exited by all the top windows being destroyed. - wx objects that don't inherit from wx.Window (eg sysTrayIcon, Menu) need to be manually destroyed. + Ensures the app is exited by all the top windows being destroyed """ - import brailleViewer - brailleViewer.destroyBrailleViewer() - - app = wx.GetApp() - - # prevent race condition with object deletion - # prevent deletion of the object while we work on it. - _SettingsDialog = settingsDialogs.SettingsDialog - nonWeak: typing.Dict[_SettingsDialog, _SettingsDialog] = dict(_SettingsDialog._instances) - - for instance, state in nonWeak.items(): - if state is _SettingsDialog.DialogState.DESTROYED: - log.error( - "Destroyed but not deleted instance of gui.SettingsDialog exists" - f": {instance.title} - {instance.__class__.__qualname__} - {instance}" - ) - else: - log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance)) - - # wx.Windows destroy child Windows automatically but wx.Menu and TaskBarIcon don't inherit from wx.Window. - # They must be manually destroyed when exiting the app. - # Note: this doesn't consistently clean them from the tray and appears to be a wx issue. (#12286, #12238) - log.debug("destroying system tray icon and menu") - app.ScheduleForDestruction(mainFrame.sysTrayIcon.menu) - mainFrame.sysTrayIcon.RemoveIcon() - app.ScheduleForDestruction(mainFrame.sysTrayIcon) - for window in wx.GetTopLevelWindows(): if isinstance(window, wx.Dialog) and window.IsModal(): - log.debug(f"ending modal {window} during exit process") + log.info(f"ending modal {window} during exit process") wx.CallAfter(window.EndModal, wx.ID_CLOSE_ALL) if isinstance(window, MainFrame): - log.debug("destroying main frame during exit process") + log.info(f"destroying main frame during exit process") # the MainFrame has EVT_CLOSE bound to the ExitDialog # which calls this function on exit, so destroy this window - app.ScheduleForDestruction(window) + wx.CallAfter(window.Destroy) else: - log.debug(f"closing window {window} during exit process") + log.info(f"closing window {window} during exit process") wx.CallAfter(window.Close) - global hasAppExited - hasAppExited = True - - class SysTrayIcon(wx.adv.TaskBarIcon): def __init__(self, frame): @@ -616,13 +584,33 @@ def wx_CallAfter_wrapper(func, *args, **kwargs): wx.CallAfter = wx_CallAfter_wrapper def terminate(): - global mainFrame + import brailleViewer + brailleViewer.destroyBrailleViewer() - # If MainLoop is terminated through WM_QUIT, such as starting an NVDA instance older than 2021.1, - # safeAppExit has not been called yet - if not hasAppExited: - safeAppExit() + # prevent race condition with object deletion + # prevent deletion of the object while we work on it. + _SettingsDialog = settingsDialogs.SettingsDialog + nonWeak: typing.Dict[_SettingsDialog, _SettingsDialog] = dict(_SettingsDialog._instances) + for instance, state in nonWeak.items(): + if state is _SettingsDialog.DialogState.DESTROYED: + log.error( + "Destroyed but not deleted instance of gui.SettingsDialog exists" + f": {instance.title} - {instance.__class__.__qualname__} - {instance}" + ) + else: + log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance)) + global mainFrame + # This is called after the main loop exits because WM_QUIT exits the main loop + # without destroying all objects correctly and we need to support WM_QUIT. + # Therefore, any request to exit should exit the main loop. + safeAppExit() + # #4460: We need another iteration of the main loop + # so that everything (especially the TaskBarIcon) is cleaned up properly. + # ProcessPendingEvents doesn't seem to work, but MainLoop does. + # Because the top window gets destroyed, + # MainLoop thankfully returns pretty quickly. + wx.GetApp().MainLoop() mainFrame = None def showGui(): diff --git a/source/gui/startupDialogs.py b/source/gui/startupDialogs.py index 1e432e6a060..aadc038e8c0 100644 --- a/source/gui/startupDialogs.py +++ b/source/gui/startupDialogs.py @@ -117,7 +117,7 @@ def run(cls): gui.mainFrame.prePopup() d = cls(gui.mainFrame) d.ShowModal() - wx.CallAfter(d.Destroy) + d.Destroy() gui.mainFrame.postPopup() diff --git a/source/nvda.pyw b/source/nvda.pyw index 58b3c17d99d..92117109c51 100755 --- a/source/nvda.pyw +++ b/source/nvda.pyw @@ -160,28 +160,18 @@ for name in pathAppArgs: newVal = os.path.abspath(origVal) setattr(globalVars.appArgs, name, newVal) - -def safelyTerminateRunningNVDA(window: winUser.HWND): +def terminateRunningNVDA(window): processID,threadID=winUser.getWindowThreadProcessID(window) - winUser.PostSafeQuitMessage(window) + winUser.PostMessage(window,winUser.WM_QUIT,0,0) h=winKernel.openProcess(winKernel.SYNCHRONIZE,False,processID) if not h: # The process is already dead. return try: - res = winKernel.waitForSingleObject(h, 6000) # give time to exit NVDA safely + res=winKernel.waitForSingleObject(h,4000) if res==0: # The process terminated within the timeout period. return - else: - raise OSError("Failed to terminate with WM_EXIT_NVDA") - except OSError: - # allow for updating between NVDA versions, as NVDA <= 2020.4 does not accept WM_EXIT_NVDA messages - print("Failed to post a safe quit message across NVDA instances, sending WM_QUIT", file=sys.stderr) - res = _terminateRunningLegacyNVDA(window) - if res == 0: - # The process terminated within the timeout period. - return finally: winKernel.closeHandle(h) @@ -195,20 +185,6 @@ def safelyTerminateRunningNVDA(window: winUser.HWND): finally: winKernel.closeHandle(h) - -def _terminateRunningLegacyNVDA(window: winUser.HWND) -> int: - ''' - Returns 0 on success, raises an OSError based WinErr if the process isn't killed - ''' - processID, _threadID = winUser.getWindowThreadProcessID(window) - winUser.PostMessage(window, winUser.WM_QUIT, 0, 0) - h = winKernel.openProcess(winKernel.SYNCHRONIZE, False, processID) - if not h: - # The process is already dead. - return 0 - return winKernel.waitForSingleObject(h, 4000) - - #Handle running multiple instances of NVDA try: oldAppWindowHandle=winUser.FindWindow(u'wxWindowClassNR',u'NVDA') @@ -221,9 +197,8 @@ if oldAppWindowHandle and not globalVars.appArgs.easeOfAccess: # NVDA is running. sys.exit(0) try: - safelyTerminateRunningNVDA(oldAppWindowHandle) - except Exception as e: - print(f"Couldn't terminate existing NVDA process, abandoning start:\nException: {e}", file=sys.stderr) + terminateRunningNVDA(oldAppWindowHandle) + except: sys.exit(1) if globalVars.appArgs.quit or (oldAppWindowHandle and globalVars.appArgs.easeOfAccess): sys.exit(0) @@ -276,14 +251,9 @@ if customVenvDetected: log.warning("NVDA launched using a custom Python virtual environment.") if globalVars.appArgs.changeScreenReaderFlag: winUser.setSystemScreenReaderFlag(True) - -# Accept WM_QUIT from other processes, even if running with higher privilages -# 2020.4 and earlier versions sent a WM_QUIT message when asking NVDA to exit. -# Some users may run several different versions of NVDA, so we continue to support this. -# WM_QUIT does not allow NVDA to shutdown cleanly, now WM_EXIT_NVDA is used instead -if not ctypes.windll.user32.ChangeWindowMessageFilter(winUser.WM_QUIT, winUser.MSGFLT.ALLOW): - log.error("Unable to set the NVDA process to receive WM_QUIT messages from other processes") - raise winUser.WinError() +#Accept wm_quit from other processes, even if running with higher privilages +if not ctypes.windll.user32.ChangeWindowMessageFilter(winUser.WM_QUIT,1): + raise WinError() # Make this the last application to be shut down and don't display a retry dialog box. winKernel.SetProcessShutdownParameters(0x100, winKernel.SHUTDOWN_NORETRY) if not isSecureDesktop and not config.isAppX: diff --git a/source/winUser.py b/source/winUser.py index fd7719ba86c..e007c443a81 100644 --- a/source/winUser.py +++ b/source/winUser.py @@ -13,7 +13,6 @@ from ctypes.wintypes import HWND, RECT, DWORD import winKernel from textUtils import WCHAR_ENCODING -import enum #dll handles user32=windll.user32 @@ -115,6 +114,12 @@ class GUITHREADINFO(Structure): CBS_OWNERDRAWFIXED=0x0010 CBS_OWNERDRAWVARIABLE=0x0020 CBS_HASSTRINGS=0x00200 +WM_NULL=0 +WM_QUIT=18 +WM_COPYDATA=74 +WM_NOTIFY=78 +WM_DEVICECHANGE=537 +WM_USER=1024 #PeekMessage PM_REMOVE=1 PM_NOYIELD=2 @@ -141,7 +146,6 @@ class GUITHREADINFO(Structure): WM_NOTIFY = 78 WM_USER = 1024 WM_QUIT = 18 -WM_DEVICECHANGE = 537 WM_DISPLAYCHANGE = 0x7e WM_GETTEXT=13 WM_GETTEXTLENGTH=14 @@ -373,27 +377,6 @@ class GUITHREADINFO(Structure): # The height of the virtual screen, in pixels. SM_CYVIRTUALSCREEN = 79 - -class MSGFLT(enum.IntEnum): - # Actions associated with ChangeWindowMessageFilterEx - # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-changewindowmessagefilterex - # Adds the message to the filter. This has the effect of allowing the message to be received. - ALLOW = 1 - # Removes the message from the filter. This has the effect of blocking the message. - DISALLOW = 2 - # Resets the window message filter to the default. - # Any message allowed globally or process-wide will get through. - RESET = 0 - - -# Registers an application wide Window Message so that NVDA can be exited across instances -WM_EXIT_NVDA = user32.RegisterWindowMessageW("WM_EXIT_NVDA") -if not WM_EXIT_NVDA: - winErr = WinError() - # provides additional information to the OSError based WinError - winErr.filename = "Failed to register Windows application message WM_EXIT_NVDA" - raise winErr - def setSystemScreenReaderFlag(val): user32.SystemParametersInfoW(SPI_SETSCREENREADER,val,0,SPIF_UPDATEINIFILE|SPIF_SENDCHANGE) @@ -618,15 +601,6 @@ def PostMessage(hwnd, msg, wParam, lParam): if not user32.PostMessageW(hwnd, msg, wParam, lParam): raise WinError() - -def PostSafeQuitMessage(hwnd: HWND): - """ - Posts a WM_EXIT_NVDA quit message across windows to exit NVDA safely from another instance - @param hwnd: Target NVDA window id - """ - if not user32.PostMessageW(hwnd, WM_EXIT_NVDA, None, None): - raise WinError() - user32.VkKeyScanExW.restype = SHORT def VkKeyScanEx(ch, hkl): res = user32.VkKeyScanExW(WCHAR(ch), hkl) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 0529bda6509..ebe8bb7adf9 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -115,7 +115,6 @@ What's New in NVDA - This usage is prefered instead of ti1.SetEndPoint(ti2,"startToEnd") - `wx.CENTRE_ON_SCREEN` and `wx.CENTER_ON_SCREEN` are removed, use `self.CentreOnScreen()` instead. (#12309) - `easeOfAccess.isSupported` has been removed, NVDA only supports versions of Windows where this evaluates to `True`. (#12222) -- Do not exit NVDA by sending a `WM_QUIT` message to the process. Instead send `winUser.WM_EXIT_NVDA` to the window handle (found by `winUser.FindWindow('wxWindowClassNR', 'NVDA')`). (#12286) = 2020.4 = From a0089a2fe2583b91536a8f8ececa0027ad1f4425 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Tue, 27 Apr 2021 12:06:29 +1000 Subject: [PATCH 148/174] Increase sleep to 5s after starting NVDA in chromeTests (#12343) --- tests/system/robot/chromeTests.robot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/robot/chromeTests.robot b/tests/system/robot/chromeTests.robot index 01c33755681..7a2e7b03610 100644 --- a/tests/system/robot/chromeTests.robot +++ b/tests/system/robot/chromeTests.robot @@ -24,7 +24,7 @@ default teardown default setup start NVDA standard-dontShowWelcomeDialog.ini - Sleep 2s + Sleep 5s *** Test Cases *** From 260063b8c79c69798f410b1ff5ed2e8ce24c975f Mon Sep 17 00:00:00 2001 From: Luke Davis <8139760+XLTechie@users.noreply.github.com> Date: Wed, 28 Apr 2021 00:45:08 -0400 Subject: [PATCH 149/174] Suppressed the option to file a default issue on NVDA's Github new issue chooser page. (PR #12334) (#12334) The default issue is just a copy of the bug issue template with introductory text suggesting it not be used. Added a config.yml file to the Github issue config folder, which stops the option from being generated. Left the default issue template file in place, in case some Github quirk or error allows its filing in the future. --- .github/ISSUE_TEMPLATE/config.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..ac2e8b47ddd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false From e8c32e3cbc648bded8a8d4fa93acb72cc5493fe9 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Wed, 28 Apr 2021 16:30:11 +1000 Subject: [PATCH 150/174] Create tool for comparing GUI and UI elements in copies of NVDA (#12308) GUI and UI features for NVDA can become lost through code refactors. Generating screenshots and comparing text across copies of NVDA is an annoying process for developers. A tool is created using our system tests to generate screenshots and text of all the content in NVDA settings. This can be expanded upon to include other GUI and UI features of NVDA. --- .gitignore | 2 + runsettingsdiff.bat | 2 + tests/system/guiDiff.robot | 8 +++ tests/system/libraries/NvdaLib.py | 16 ++++- .../libraries/SystemTestSpy/configManager.py | 12 ++++ tests/system/readme.md | 26 +++++++ tests/system/robot/NVDASettings.py | 72 +++++++++++++++++++ tests/system/robot/NVDASettings.robot | 65 +++++++++++++++++ .../system/settingsCache/2020.4/Advanced.txt | 23 ++++++ tests/system/settingsCache/2020.4/Braille.txt | 19 +++++ .../settingsCache/2020.4/Browse Mode.txt | 13 ++++ .../2020.4/Document Formatting.txt | 39 ++++++++++ tests/system/settingsCache/2020.4/General.txt | 12 ++++ .../2020.4/Input Composition.txt | 7 ++ .../system/settingsCache/2020.4/Keyboard.txt | 14 ++++ tests/system/settingsCache/2020.4/Mouse.txt | 9 +++ .../2020.4/Object Presentation.txt | 12 ++++ .../settingsCache/2020.4/Review Cursor.txt | 6 ++ tests/system/settingsCache/2020.4/Speech.txt | 14 ++++ tests/system/settingsCache/2020.4/Vision.txt | 11 +++ 20 files changed, 380 insertions(+), 2 deletions(-) create mode 100644 runsettingsdiff.bat create mode 100644 tests/system/guiDiff.robot create mode 100644 tests/system/robot/NVDASettings.py create mode 100644 tests/system/robot/NVDASettings.robot create mode 100644 tests/system/settingsCache/2020.4/Advanced.txt create mode 100644 tests/system/settingsCache/2020.4/Braille.txt create mode 100644 tests/system/settingsCache/2020.4/Browse Mode.txt create mode 100644 tests/system/settingsCache/2020.4/Document Formatting.txt create mode 100644 tests/system/settingsCache/2020.4/General.txt create mode 100644 tests/system/settingsCache/2020.4/Input Composition.txt create mode 100644 tests/system/settingsCache/2020.4/Keyboard.txt create mode 100644 tests/system/settingsCache/2020.4/Mouse.txt create mode 100644 tests/system/settingsCache/2020.4/Object Presentation.txt create mode 100644 tests/system/settingsCache/2020.4/Review Cursor.txt create mode 100644 tests/system/settingsCache/2020.4/Speech.txt create mode 100644 tests/system/settingsCache/2020.4/Vision.txt diff --git a/.gitignore b/.gitignore index 28f1b7b4cf0..b66066c93cf 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,8 @@ uninstaller/UAC.nsh *.pyo *.dmp tests/unit/nvda.ini +tests/system/settingsCache/* +!tests/system/settingsCache/2020.4/*.txt source/locale/*/cldr.dic .vscode .vs diff --git a/runsettingsdiff.bat b/runsettingsdiff.bat new file mode 100644 index 00000000000..f367c1ecb41 --- /dev/null +++ b/runsettingsdiff.bat @@ -0,0 +1,2 @@ +@echo off +call "%~dp0\venvUtils\venvCmd.bat" py -m robot --argumentfile "%~dp0\tests\system\guiDiff.robot" %* "%~dp0\tests\system\robot" diff --git a/tests/system/guiDiff.robot b/tests/system/guiDiff.robot new file mode 100644 index 00000000000..3c6e84d247b --- /dev/null +++ b/tests/system/guiDiff.robot @@ -0,0 +1,8 @@ +--loglevel DEBUG +--outputdir .\testOutput\system +--xunit systemTests.xml +--pythonpath .\tests\system\libraries +--suite NVDASettings +--variable whichNVDA:source +--variable currentVersion:source +--variable cacheFolder:.\tests\system\settingsCache diff --git a/tests/system/libraries/NvdaLib.py b/tests/system/libraries/NvdaLib.py index 735ea741c70..4155454b3eb 100644 --- a/tests/system/libraries/NvdaLib.py +++ b/tests/system/libraries/NvdaLib.py @@ -55,12 +55,12 @@ def __init__(self): self._runNVDAFilePath = _pJoin(self.repoRoot, "runnvda.bat") self.baseNVDACommandline = self._runNVDAFilePath elif self.whichNVDA == "installed": - self._runNVDAFilePath = _pJoin(_expandvars('%PROGRAMFILES%'), 'nvda', 'nvda.exe') + self._runNVDAFilePath = self.findInstalledNVDAPath() self.baseNVDACommandline = f'"{str(self._runNVDAFilePath)}"' if self._installFilePath is not None: self.NVDAInstallerCommandline = f'"{str(self._installFilePath)}"' else: - raise AssertionError("RobotFramework should be run with argument: '-v whichNVDA [source|installed]'") + raise AssertionError("RobotFramework should be run with argument: '-v whichNVDA:[source|installed]'") self.profileDir = _pJoin(self.stagingDir, "nvdaProfile") self.logPath = _pJoin(self.profileDir, 'nvda.log') @@ -69,6 +69,18 @@ def __init__(self): "nvdaTestRunLogs" ) + def findInstalledNVDAPath(self) -> Optional[str]: + NVDAFilePath = _pJoin(_expandvars('%PROGRAMFILES%'), 'nvda', 'nvda.exe') + legacyNVDAFilePath = _pJoin(_expandvars('%PROGRAMFILES%'), 'NVDA', 'nvda.exe') + exeErrorMsg = f"Unable to find installed NVDA exe. Paths tried: {NVDAFilePath}, {legacyNVDAFilePath}" + try: + opSys.file_should_exist(NVDAFilePath) + return NVDAFilePath + except AssertionError: + # Older versions of NVDA (<=2020.4) install the exe in NVDA\nvda.exe + opSys.file_should_exist(legacyNVDAFilePath, exeErrorMsg) + return legacyNVDAFilePath + def ensureInstallerPathsExist(self): fileWarnMsg = f"Unable to run NVDA installer unless path exists. Path given: {self._installFilePath}" opSys.file_should_exist(self._installFilePath, fileWarnMsg) diff --git a/tests/system/libraries/SystemTestSpy/configManager.py b/tests/system/libraries/SystemTestSpy/configManager.py index 46b72e13a78..cc9f23f91ce 100644 --- a/tests/system/libraries/SystemTestSpy/configManager.py +++ b/tests/system/libraries/SystemTestSpy/configManager.py @@ -51,6 +51,18 @@ def _installSystemTestSpyToScratchPad(repoRoot: str, scratchPadDir: str): ], libsDest=spyPackageLibsDir ) + + try: + opSys.directory_should_exist(_pJoin(spyPackageLibsDir, "xmlrpc")) + except AssertionError: + # installed copies of NVDA <= 2020.4 don't copy this over + _copyPythonLibs( + pythonImports=[ # relative to the python path + "xmlrpc", + ], + libsDest=spyPackageLibsDir + ) + # install the global plugin # Despite duplication, specify full paths for clarity. opSys.copy_file( diff --git a/tests/system/readme.md b/tests/system/readme.md index 03ec0a829bd..920c9251fd1 100644 --- a/tests/system/readme.md +++ b/tests/system/readme.md @@ -103,3 +103,29 @@ NVDA is started with the `-c` option to specify this profile directory to be use Both Robot Framework and NVDA logs are captured in the `testOutput` directory in the repo root. NVDA logs (NVDA log, stdOut, and stdErr for each test) are under the `nvdaTestRunLogs` directory. The log files are named by suite and test name. + +### Comparing changes to NVDA Settings +`.\runsettingsdiff.bat` is a tool used to compare the settings dialog by reading text and generating screenshots for comparison. The default behaviour is to run using the source code and output to `.\tests\system\settingsCache\source`. + + +#### Usage +To check for unreleased changes to the settings dialogs, one can use this tool to compare against two copies of NVDA. + +The following arguments should be used with the script. + +Default arguments used are stored in `.\tests\system\guiDiff.robot` + +- `--variable whichNVDA:[installed|source]` to decide where to run NVDA from +- `--variable cacheFolder:[filePath]` screenshots and text files of each settings panel are generated in `$cacheFolder\$currentVersion` +- `--variable currentVersion:[nvdaVersion]` where `[nvdaVersion]` is used to name the generated screenshot and cache folder +- `--variable compareVersion:[nvdaVersion]` using a `$nvdaVersion` that this script has already been run against, run the system tests and fail if there are differences between the read text. This generates a multiline diff. + +#### Example usage to compare settings between NVDA 2020.4 and the current source + +1. Install NVDA 2020.4 +1. Run `.\runsettingsdiff.bat -v whichNVDA:installed -v currentVersion:2020.4` +1. Run `.\runsettingsdiff.bat -v whichNVDA:source -v currentVersion:source -v compareVersion:2020.4` + - The test will fail and display a diff of any read changes +1. Use a diff tool to compare folders: + - `diff ./tests/system/settingsCache/2020.4 ./tests/system/settingsCache/source` + - [ImageMagick Compare](https://imagemagick.org/script/compare.php) can be used to compare images diff --git a/tests/system/robot/NVDASettings.py b/tests/system/robot/NVDASettings.py new file mode 100644 index 00000000000..db318a6f6d8 --- /dev/null +++ b/tests/system/robot/NVDASettings.py @@ -0,0 +1,72 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2021 NV Access Limited +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + +"""Logic for smoketesting the settings. +""" + +from robot.libraries.BuiltIn import BuiltIn +# relative import not used for 'systemTestUtils' because the folder is added to the path for 'libraries' +# imported methods start with underscore (_) so they don't get imported into robot files as keywords +from SystemTestSpy import ( + _getLib, +) + +# Imported for type information +from robot.libraries.Process import Process as _ProcessLib + +from AssertsLib import AssertsLib as _AssertsLib + +import os +from typing import Optional +import NvdaLib as _nvdaLib +from NvdaLib import NvdaLib as _nvdaRobotLib +_nvdaProcessAlias = _nvdaRobotLib.nvdaProcessAlias + +_builtIn: BuiltIn = BuiltIn() +_process: _ProcessLib = _getLib("Process") +_asserts: _AssertsLib = _getLib("AssertsLib") + + +def navigate_to_settings(settingsName): + spy = _nvdaLib.getSpyLib() + # open settings menu + spy.emulateKeyPress("NVDA+n") + spy.emulateKeyPress("p") + spy.emulateKeyPress("s") + spy.emulateKeyPress("leftWindows+upArrow") # maximise + spy.wait_for_speech_to_finish() + + # naviagte to setting + for letter in settingsName.lower(): + spy.emulateKeyPress(letter) + + spy.wait_for_speech_to_finish() + spy.reset_all_speech_index() + + +def read_settings(settingsName, cacheFolder, currentVersion, compareVersion: Optional[str] = None): + spy = _nvdaLib.getSpyLib() + start_speech_index = spy.get_next_speech_index() + advancedWarning = "I understand that changing these settings may cause NVDA to function incorrectly." + + # read new setting + lastSpeech = "" + while "OK" not in lastSpeech: + spy.emulateKeyPress("tab") + spy.wait_for_speech_to_finish() + lastSpeech = spy.get_last_speech() + if lastSpeech.split(" ")[0] == advancedWarning: + spy.emulateKeyPress("space") + + actualSpeech = spy.get_speech_at_index_until_now(start_speech_index) + os.makedirs(f"{cacheFolder}/{currentVersion}", exist_ok=True) + with open(f"{cacheFolder}/{currentVersion}/{settingsName}.txt", "w") as f: + f.write(actualSpeech) + + if compareVersion: + with open(f"{cacheFolder}/{compareVersion}/{settingsName}.txt", "r") as f: + compareText = f.read() + + _asserts.strings_match(compareText, actualSpeech) diff --git a/tests/system/robot/NVDASettings.robot b/tests/system/robot/NVDASettings.robot new file mode 100644 index 00000000000..354485956ea --- /dev/null +++ b/tests/system/robot/NVDASettings.robot @@ -0,0 +1,65 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2021 NV Access Limited +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html +*** Settings *** +Documentation Smoke test the settings panel, use for checking diffs +Force Tags NVDA smoke test excluded_from_build + +# for start & quit in Test Setup and Test Test Teardown +Library NvdaLib.py +Library NVDASettings.py +Library ScreenCapLibrary + +Test Setup start NVDA standard-dontShowWelcomeDialog.ini +Test Teardown default teardown + +*** Keywords *** +default run read test + [Arguments] ${settingsName} + navigate to settings ${settingsName} + Create Directory ${cacheFolder}/${currentVersion} + Set Screenshot Directory ${cacheFolder}/${currentVersion} + Take Screenshot ${settingsName}.png + read settings ${settingsName} ${cacheFolder} ${currentVersion} ${compareVersion} + Take Screenshot ${settingsName}-end.png + +default teardown + quit NVDA + +*** Test Cases *** +Read General + default run read test General + +Read Speech + default run read test Speech + +Read Braille + default run read test Braille + +Read Vision + default run read test Vision + +Read Keyboard + default run read test Keyboard + +Read Mouse + default run read test Mouse + +Read Review Cursor + default run read test Review Cursor + +Read Input Composition + default run read test Input Composition + +Read Object Presentation + default run read test Object Presentation + +Read Browse Mode + default run read test Browse Mode + +Read Document Formatting + default run read test Document Formatting + +Read Advanced + default run read test Advanced diff --git a/tests/system/settingsCache/2020.4/Advanced.txt b/tests/system/settingsCache/2020.4/Advanced.txt new file mode 100644 index 00000000000..f2a772b3af8 --- /dev/null +++ b/tests/system/settingsCache/2020.4/Advanced.txt @@ -0,0 +1,23 @@ +Advanced property page Warning! The following settings are for advanced users. Changing them may cause NVDA to function incorrectly. Please only change these if you know what you are doing or have been specifically instructed by NVDA developers. +I understand that changing these settings may cause NVDA to function incorrectly. check box not checked +space +checked +Restore defaults button +NVDA Development grouping +Enable loading custom code from Developer Scratchpad directory check box checked +Open developer scratchpad directory button +Microsoft UI Automation grouping +Enable selective registration for UI Automation events and property changes check box not checked Alt plus s +Use UI Automation to access Microsoft Word document controls when available check box not checked Alt plus w +Use UI Automation to access the Windows Console when available check box not checked Alt plus o +Speak passwords in UIA consoles (may improve performance) check box not checked Alt plus p +Terminal programs grouping +Use the new typed character support in Windows Console when available check box checked Alt plus y +Speech grouping +Attempt to cancel speech for expired focus events: combo box Default (No) collapsed +Editable Text grouping +Caret movement timeout (in ms) edit selected 100 +Debug logging grouping +Enabled logging categories list +hw Io check box not checked +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Braille.txt b/tests/system/settingsCache/2020.4/Braille.txt new file mode 100644 index 00000000000..5992820b34d --- /dev/null +++ b/tests/system/settingsCache/2020.4/Braille.txt @@ -0,0 +1,19 @@ +Braille property page +Braille display grouping +Braille display edit read only multi line Alt plus d Automatic +Change... button Alt plus h +Output table: combo box Unified English Braille Code grade 1 collapsed Alt plus o +Input table: combo box Unified English Braille Code grade 1 collapsed Alt plus i +Expand to computer braille for the word at the cursor check box checked Alt plus x +Show cursor check box checked Alt plus s +Blink cursor check box checked +Cursor blink rate (ms) edit selected 500 +Cursor shape for focus: combo box Dots 7 and 8 collapsed Alt plus f +Cursor shape for review: combo box Dot 8 collapsed Alt plus r +Show messages combo box Use timeout collapsed +Message timeout (sec) edit Alt plus t selected 4 +Tether Braille: combo box automatically collapsed Alt plus r +Read by paragraph check box not checked Alt plus p +Avoid splitting words when possible check box checked Alt plus w +Focus context presentation: combo box Fill display for context changes collapsed +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Browse Mode.txt b/tests/system/settingsCache/2020.4/Browse Mode.txt new file mode 100644 index 00000000000..464264b901f --- /dev/null +++ b/tests/system/settingsCache/2020.4/Browse Mode.txt @@ -0,0 +1,13 @@ +Browse Mode property page +Maximum number of characters on one line edit Alt plus m selected 100 +Number of lines per page edit Alt plus n selected 25 +Use screen layout (when supported) check box checked Alt plus s +Enable browse mode on page load check box checked Alt plus e +Automatic Say All on page load check box not checked Alt plus s +Include layout tables check box not checked Alt plus a +Automatic focus mode for focus changes check box checked +Automatic focus mode for caret movement check box not checked +Audio indication of focus and browse modes check box not checked +Trap all non command gestures from reaching the document check box checked Alt plus t +Automatically set system focus to focusable elements check box not checked Alt plus f +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Document Formatting.txt b/tests/system/settingsCache/2020.4/Document Formatting.txt new file mode 100644 index 00000000000..f471a0b14e6 --- /dev/null +++ b/tests/system/settingsCache/2020.4/Document Formatting.txt @@ -0,0 +1,39 @@ +Document Formatting property page The following options control the types of document formatting reported by NVDA. +Font grouping +Font name check box not checked Alt plus f +Font size check box not checked Alt plus s +Font attributes check box not checked Alt plus u +Superscripts and subscripts check box not checked Alt plus p +Emphasis check box not checked Alt plus m +Marked (highlighted text) check box checked Alt plus k +Style check box not checked Alt plus y +Colors check box not checked Alt plus c +Document information grouping +Notes and comments check box checked Alt plus t +Editor revisions check box checked Alt plus e +Spelling errors check box checked Alt plus r +Pages and spacing grouping +Pages check box checked Alt plus p +Line numbers check box not checked Alt plus n +Line indentation reporting: combo box Off collapsed Alt plus i +Paragraph indentation check box not checked Alt plus p +Line spacing check box not checked Alt plus l +Alignment check box not checked Alt plus a +Table information grouping +Tables check box checked Alt plus t +Row slash column headers check box checked Alt plus e +Cell coordinates check box checked Alt plus o +Cell borders: combo box Off collapsed Alt plus b +Elements grouping +Headings check box checked Alt plus h +Links check box checked Alt plus k +Graphics check box checked Alt plus g +Lists check box checked Alt plus l +Block quotes check box checked Alt plus q +Groupings check box checked Alt plus g +Landmarks and regions check box checked Alt plus d +Articles check box not checked Alt plus c +Frames check box checked Alt plus m +Clickable check box checked Alt plus c +Report formatting changes after the cursor (can cause a lag) check box not checked Alt plus g +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/General.txt b/tests/system/settingsCache/2020.4/General.txt new file mode 100644 index 00000000000..cedb018f4f1 --- /dev/null +++ b/tests/system/settingsCache/2020.4/General.txt @@ -0,0 +1,12 @@ +General property page +NVDA Language (requires restart): combo box User default collapsed Alt plus l +Save configuration when exiting NVDA check box checked Alt plus s +Show exit options when exiting NVDA check box checked Alt plus w +Play sounds when starting or exiting NVDA check box checked Alt plus p +Start NVDA after I sign in check box not checked Alt plus a +Use NVDA during sign in (requires administrator privileges) check box not checked +Use currently saved settings during sign in and on secure screens (requires administrator privileges) button +Automatically check for updates to NVDA check box not checked Alt plus u +Notify for pending update on startup check box not checked Alt plus p +Allow the NVDA project to gather NVDA usage statistics check box not checked +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Input Composition.txt b/tests/system/settingsCache/2020.4/Input Composition.txt new file mode 100644 index 00000000000..b41c7345af6 --- /dev/null +++ b/tests/system/settingsCache/2020.4/Input Composition.txt @@ -0,0 +1,7 @@ +Input Composition property page +Automatically report all available candidates check box checked Alt plus c +Announce selected candidate check box checked Alt plus s +Always include short character description when announcing candidates check box checked Alt plus d +Report changes to the reading string check box checked Alt plus r +Report changes to the composition string check box checked Alt plus c +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Keyboard.txt b/tests/system/settingsCache/2020.4/Keyboard.txt new file mode 100644 index 00000000000..15c15719f6c --- /dev/null +++ b/tests/system/settingsCache/2020.4/Keyboard.txt @@ -0,0 +1,14 @@ +Keyboard property page +Keyboard layout: combo box desktop collapsed Alt plus k +Select NVDA Modifier Keys list +caps lock check box not checked +Speak typed characters check box checked Alt plus c +Speak typed words check box not checked Alt plus w +Speech interrupt for typed characters check box checked Alt plus i +Speech interrupt for Enter key check box checked Alt plus n +Allow skim reading in Say All check box not checked Alt plus r +Beep if typing lowercase letters when caps lock is on check box checked Alt plus b +Speak command keys check box not checked Alt plus o +Play sound for spelling errors while typing check box checked Alt plus s +Handle keys from other applications check box checked Alt plus a +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Mouse.txt b/tests/system/settingsCache/2020.4/Mouse.txt new file mode 100644 index 00000000000..d63a58016cd --- /dev/null +++ b/tests/system/settingsCache/2020.4/Mouse.txt @@ -0,0 +1,9 @@ +Mouse property page +Report mouse shape changes check box not checked Alt plus s +Enable mouse tracking check box checked Alt plus t +Text unit resolution: combo box paragraph collapsed Alt plus u +Report role when mouse enters object check box not checked Alt plus r +Play audio coordinates when mouse moves check box not checked Alt plus p +Brightness controls audio coordinates volume check box not checked Alt plus b +Ignore mouse input from other applications check box not checked Alt plus a +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Object Presentation.txt b/tests/system/settingsCache/2020.4/Object Presentation.txt new file mode 100644 index 00000000000..1bc63892cef --- /dev/null +++ b/tests/system/settingsCache/2020.4/Object Presentation.txt @@ -0,0 +1,12 @@ +Object Presentation property page +Report tooltips check box not checked Alt plus t +Report notifications check box checked Alt plus n +Report object shortcut keys check box checked Alt plus k +Report object position information check box checked Alt plus p +Guess object position information when unavailable check box not checked Alt plus g +Report object descriptions check box checked Alt plus d +Progress bar output: combo box Beep collapsed Alt plus b +Report background progress bars check box not checked Alt plus r +Report dynamic content changes check box checked Alt plus c +Play a sound when auto suggestions appear check box checked Alt plus a +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Review Cursor.txt b/tests/system/settingsCache/2020.4/Review Cursor.txt new file mode 100644 index 00000000000..20adaabba38 --- /dev/null +++ b/tests/system/settingsCache/2020.4/Review Cursor.txt @@ -0,0 +1,6 @@ +Review Cursor property page +Follow system focus check box checked Alt plus f +Follow System Caret check box checked Alt plus c +Follow mouse cursor check box not checked Alt plus m +Simple review mode check box checked Alt plus s +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Speech.txt b/tests/system/settingsCache/2020.4/Speech.txt new file mode 100644 index 00000000000..c5f14f02ab0 --- /dev/null +++ b/tests/system/settingsCache/2020.4/Speech.txt @@ -0,0 +1,14 @@ +Speech property page +Synthesizer grouping +Synthesizer edit read only multi line Alt plus s System test speech spy +Change... button Alt plus h +Automatic language switching (when supported) check box checked +Automatic dialect switching (when supported) check box not checked +Punctuation slash symbol level: combo box some collapsed Alt plus l +Trust voice's language when processing characters and symbols check box checked +Include Unicode Consortium data (including emoji) when processing characters and symbols check box checked +Capital pitch change percentage edit selected 30 +Say cap before capitals check box not checked Alt plus c +Beep for capitals check box not checked Alt plus b +Use spelling functionality if supported check box checked Alt plus s +OK button \ No newline at end of file diff --git a/tests/system/settingsCache/2020.4/Vision.txt b/tests/system/settingsCache/2020.4/Vision.txt new file mode 100644 index 00000000000..ed6e7acd75e --- /dev/null +++ b/tests/system/settingsCache/2020.4/Vision.txt @@ -0,0 +1,11 @@ +Vision property page Configure visual aids. +Visual Highlight grouping +Enable Highlighting check box not checked Alt plus e +Highlight system focus check box not checked Alt plus c +Highlight navigator object check box not checked Alt plus o +Highlight browse mode cursor check box not checked Alt plus m +Screen Curtain grouping +Make screen black (immediate effect) check box not checked +Always show a warning when loading Screen Curtain check box checked Alt plus s +Play sound when toggling Screen Curtain check box checked Alt plus p +OK button \ No newline at end of file From badd390c2d5278272eaf3dc730b1dcc6f726675f Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Thu, 29 Apr 2021 01:28:57 +0200 Subject: [PATCH 151/174] Developer Guide: Fix not parsed headers (#12332) The following two snippets can be found in the current Developer Guide as rendered in HTML format: `+++ An Example Manifest File +++` `++ Plugins and Drivers ++` These have been around for quite some time. It seems quite obvious they should be rendered as headers, respectively: `4.2.2. An Example Manifest File` `4.3. Plugins and Drivers` --- devDocs/developerGuide.t2t | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/devDocs/developerGuide.t2t b/devDocs/developerGuide.t2t index a5179638bb6..45e20975111 100644 --- a/devDocs/developerGuide.t2t +++ b/devDocs/developerGuide.t2t @@ -724,12 +724,14 @@ Otherwise, the add-on will not install. - e.g "2019.1.1" - Must be a three part version string I.E. Year.Major.Minor, or a two part version string of Year.Major. In the second case, Minor defaults to 0. - Defaults to "0.0.0" - - Must be less than or equal to `lastTestedNVDAVersion` + - Must be less than or equal to ``lastTestedNVDAVersion`` + - - lastTestedNVDAVersion (string): The last version of NVDA this add-on has been tested with. - e.g "2019.1.0" - Must be a three part version string I.E. Year.Major.Minor, or a two part version string of Year.Major. In the second case, Minor defaults to 0. - Defaults to "0.0.0" - - Must be greater than or equal to `minimumNVDAVersion` + - Must be greater than or equal to ``minimumNVDAVersion`` + - - All string values must be enclosed in quotes as shown in the example below. @@ -751,7 +753,7 @@ docFileName = "readme.html" minimumNVDAVersion = "2018.1.0" lastTestedNVDAVersion = "2019.1.0" --- end --- -``` +``` ++ Plugins and Drivers ++ The following plugins and drivers can be included in an add-on: From f84a7d9f8b7102bac201fab0674cf9ff7ff1987c Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Thu, 29 Apr 2021 02:12:27 +0200 Subject: [PATCH 152/174] Speech Viewer: allow to close with alt+F4 & add a close button on the title bar for use with pointing devices (#10791) (#12330) The Speech Viewer currently has no close button nor can be closed with alt+F4. As described by @Qchristensen in #10791 (comment), most dialogs in NVDA can be closed with alt+F4. As argued by @bhavyashah in #10791 (comment), the Speech Viewer is especially useful for sighted testers who might be more familiar in using pointing devices than keyboard shortcuts. Description of how this pull request fixes the issue: Handle closing with alt+F4 & add a standard close button in the title bar of the dialog. Co-authored-by: Sean Budd --- source/gui/__init__.py | 7 ++++--- source/speechViewer.py | 11 ++++------- user_docs/en/changes.t2t | 1 + 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/source/gui/__init__.py b/source/gui/__init__.py index b812bab9324..4098735ec7e 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -1,7 +1,7 @@ # -*- coding: UTF-8 -*- # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2020 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Mesar Hameed, Joseph Lee, -# Thomas Stivers, Babbage B.V. +# Copyright (C) 2006-2021 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Mesar Hameed, Joseph Lee, +# Thomas Stivers, Babbage B.V., Accessolutions, Julien Cochuyt # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -439,7 +439,8 @@ def __init__(self, frame): item = menu_tools.Append(wx.ID_ANY, _("View log")) self.Bind(wx.EVT_MENU, frame.onViewLogCommand, item) # Translators: The label for the menu item to toggle Speech Viewer. - item=self.menu_tools_toggleSpeechViewer = menu_tools.AppendCheckItem(wx.ID_ANY, _("Speech viewer")) + item = self.menu_tools_toggleSpeechViewer = menu_tools.AppendCheckItem(wx.ID_ANY, _("Speech viewer")) + item.Check(speechViewer.isActive) self.Bind(wx.EVT_MENU, frame.onToggleSpeechViewerCommand, item) self.menu_tools_toggleBrailleViewer: wx.MenuItem = menu_tools.AppendCheckItem( diff --git a/source/speechViewer.py b/source/speechViewer.py index 7109b55eead..7c293d6d466 100644 --- a/source/speechViewer.py +++ b/source/speechViewer.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2020 NV Access Limited, Thomas Stivers +# Copyright (C) 2006-2021 NV Access Limited, Thomas Stivers, Accessolutions, Julien Cochuyt # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -38,7 +38,7 @@ def __init__(self, onDestroyCallBack): title=_("NVDA Speech Viewer"), size=dialogSize, pos=dialogPos, - style=wx.CAPTION | wx.RESIZE_BORDER | wx.STAY_ON_TOP + style=wx.CAPTION | wx.CLOSE_BOX | wx.RESIZE_BORDER | wx.STAY_ON_TOP ) self._isDestroyed = False self.onDestroyCallBack = onDestroyCallBack @@ -97,10 +97,8 @@ def _onDialogActivated(self, evt): self.shouldShowOnStartupCheckBox.SetFocus() def onClose(self, evt): - if not evt.CanVeto(): - deactivate() - return - evt.Veto() + assert isActive, "Cannot close Speech Viewer as it is already inactive" + deactivate() def onShouldShowOnStartupChanged(self, evt): config.conf["speechViewer"]["showSpeechViewerAtStartup"] = self.shouldShowOnStartupCheckBox.IsChecked() @@ -185,4 +183,3 @@ def deactivate(): # #7077: If the window is destroyed, text control will be gone, so save speech viewer position before destroying the window. _guiFrame.savePositionInformation() _guiFrame.Destroy() - isActive = False diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index ebe8bb7adf9..1791789766b 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -25,6 +25,7 @@ What's New in NVDA - New braille tables: Belarusian literary braille, Belarusian computer braille, Urdu grade 1, Urdu grade 2. - Support for Adobe Flash content has been removed from NVDA due to the use of Flash being actively discouraged by Adobe. (#11131) - NVDA will exit even with windows still open, the exit process now closes all NVDA windows and dialogs (#1740) +- The Speech Viewer can now be closed with `alt+F4` and has a standard close button for easier interaction with users of pointing devices. (#12330) == Bug Fixes == From e2d269609ec475816655b64fb0062817af773456 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Thu, 29 Apr 2021 06:43:06 +0200 Subject: [PATCH 153/174] Braille Viewer: Add a close button on the title bar for use with pointing devices (#12328) The Braille Viewer currently presents no close control on its GUI. It can only be closed with alt+F4 or by means of the dedicated NVDA Tools menu entry. Description of how this pull request fixes the issue: Add a standard close button on the title bar of the Braille Viewer dialog. --- source/brailleViewer/brailleViewerGui.py | 4 ++-- user_docs/en/changes.t2t | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/source/brailleViewer/brailleViewerGui.py b/source/brailleViewer/brailleViewerGui.py index b781a29f0ce..33f3ead0a1a 100644 --- a/source/brailleViewer/brailleViewerGui.py +++ b/source/brailleViewer/brailleViewerGui.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2014-2019 NV Access Limited +# Copyright (C) 2014-2021 NV Access Limited, Accessolutions, Julien Cochuyt # This file is covered by the GNU General Public License. # See the file COPYING for more details. import enum @@ -280,7 +280,7 @@ def __init__(self, numCells, onDestroyed): gui.mainFrame, title=self._title, pos=dialogPos, - style=wx.CAPTION | wx.STAY_ON_TOP + style=wx.CAPTION | wx.CLOSE_BOX | wx.STAY_ON_TOP ) self.Bind(wx.EVT_CLOSE, self._onClose) self.Bind(wx.EVT_WINDOW_DESTROY, self._onDestroy) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 1791789766b..d41b7d875ef 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -26,6 +26,7 @@ What's New in NVDA - Support for Adobe Flash content has been removed from NVDA due to the use of Flash being actively discouraged by Adobe. (#11131) - NVDA will exit even with windows still open, the exit process now closes all NVDA windows and dialogs (#1740) - The Speech Viewer can now be closed with `alt+F4` and has a standard close button for easier interaction with users of pointing devices. (#12330) +- The Braille Viewer now has a standard close button for easier interaction with users of pointing devices. (#12328) == Bug Fixes == From a3af16ec54fe6fc474108403dc778c1ebb0a1dba Mon Sep 17 00:00:00 2001 From: Rowen Date: Fri, 30 Apr 2021 16:38:02 +0800 Subject: [PATCH 154/174] Fix Sapi4 pitch change (PR #12354) Fixes: #12311 Co-authored-by: Reef Turner --- source/synthDrivers/sapi4.py | 35 ++++++++++++++++++++++++++++++++++- user_docs/en/changes.t2t | 1 + 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py index 1739e7f8577..e48a5bb11d4 100755 --- a/source/synthDrivers/sapi4.py +++ b/source/synthDrivers/sapi4.py @@ -38,6 +38,7 @@ import nvwave import weakref +from speech.commands import PitchCommand from speech.commands import IndexCommand, SpeechCommand, CharacterModeCommand class SynthDriverBufSink(COMObject): @@ -115,6 +116,11 @@ def speak(self,speechSequence): textList=[] charMode=False item=None + isPitchCommand = False + pitch = WORD() + self._ttsAttrs.PitchGet(byref(pitch)) + oldPitch = pitch.value + for item in speechSequence: if isinstance(item,str): textList.append(item.replace('\\','\\\\')) @@ -123,6 +129,16 @@ def speak(self,speechSequence): elif isinstance(item, CharacterModeCommand): textList.append("\\RmS=1\\" if item.state else "\\RmS=0\\") charMode=item.state + elif isinstance(item, PitchCommand): + offset = int(config.conf["speech"]['sapi4']["capPitchChange"]) + offset = int((self._maxPitch - self._minPitch) * offset / 100) + val = oldPitch + offset + if val > self._maxPitch: + val = self._maxPitch + if val < self._minPitch: + val = self._minPitch + self._ttsAttrs.PitchSet(val) + isPitchCommand = True elif isinstance(item, SpeechCommand): log.debugWarning("Unsupported speech command: %s"%item) else: @@ -139,7 +155,24 @@ def speak(self,speechSequence): textList.append("\\PAU=1\\") text="".join(textList) flags=TTSDATAFLAG_TAGGED - self._ttsCentral.TextData(VOICECHARSET.CHARSET_TEXT, flags,TextSDATA(text),self._bufSinkPtr,ITTSBufNotifySink._iid_) + if isPitchCommand: + self._ttsCentral.TextData( + VOICECHARSET.CHARSET_TEXT, + flags, + TextSDATA(text), + self._bufSinkPtr, + ITTSBufNotifySink._iid_ + ) + self._ttsAttrs.PitchSet(oldPitch) + isPitchCommand = False + else: + self._ttsCentral.TextData( + VOICECHARSET.CHARSET_TEXT, + flags, + TextSDATA(text), + self._bufSinkPtr, + ITTSBufNotifySink._iid_ + ) def cancel(self): self._ttsCentral.AudioReset() diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index d41b7d875ef..4d5c7797e0b 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -50,6 +50,7 @@ What's New in NVDA - Strikethrough, superscript and subscript formatting for entire Excel cells are now reported if the corresponding option is enabled. (#12264) - Fixed copying config during installation from a portable copy when default destination config directory is empty. (#12071, #12205) - Fixed incorrect announcement of some letters with accents or diacritic when 'Say cap before capitals' option is checked. (#11948) +- Fixed the pitch change failure in Sapi4 speech synthesizer. (#12311) == Changes for Developers == From f392c23a34df479861263491f84b67b3c1aec93c Mon Sep 17 00:00:00 2001 From: Luke Davis <8139760+XLTechie@users.noreply.github.com> Date: Fri, 30 Apr 2021 04:43:01 -0400 Subject: [PATCH 155/174] Added guidance on where to provide info in github templates (PR #12333) Added a new paragraph to the bug, feature, and default issue templates, explaining that comments should appear BENEATH the lines with hashmarks. Updated the COM Reg. Fix Tool question in the default issue template, to the current bug template's revision. --- .github/ISSUE_TEMPLATE.md | 4 +++- .github/ISSUE_TEMPLATE/bug_report.md | 2 ++ .github/ISSUE_TEMPLATE/feature_request.md | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 7c030e839bb..2e4343b3d7b 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -10,6 +10,8 @@ Please thoroughly read NVDA's wiki article on how to fill in this template, incl Issues may be closed if the required information is not present. https://github.com/nvaccess/nvda/wiki/Github-issue-template-explanation-and-examples Please also note that the NVDA project has a Citizen and Contributor Code of Conduct which can be found at https://github.com/nvaccess/nvda/blob/master/CODE_OF_CONDUCT.md. NV Access expects that all contributors and other community members read and abide by the rules set out in this document while participating or contributing to this project. This includes creating or commenting on issues and pull requests. + +Each of the questions and sections below start with multiple hash symbols (#). Place your answers and information on the blank line below each question. --> ### Steps to reproduce: @@ -36,4 +38,4 @@ Please also note that the NVDA project has a Citizen and Contributor Code of Con #### If add-ons are disabled, is your problem still occurring? -#### Did you try to run the COM registry fixing tool in NVDA menu / tools? +#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu? diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 2c86bc249e9..967188fb34a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,6 +9,8 @@ Please thoroughly read NVDA's wiki article on how to fill in this template, incl Issues may be closed if the required information is not present. https://github.com/nvaccess/nvda/wiki/Github-issue-template-explanation-and-examples Please also note that the NVDA project has a Citizen and Contributor Code of Conduct which can be found at https://github.com/nvaccess/nvda/blob/master/CODE_OF_CONDUCT.md. NV Access expects that all contributors and other community members read and abide by the rules set out in this document while participating or contributing to this project. This includes creating or commenting on issues and pull requests. + +Each of the questions and sections below start with multiple hash symbols (#). Place your answers and information on the blank line below each question. --> ### Steps to reproduce: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index b2f13d650ce..c2ae3a9ada2 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -9,6 +9,8 @@ Please thoroughly read NVDA's wiki article on how to fill in this template, incl Issues may be closed if the required information is not present. https://github.com/nvaccess/nvda/wiki/Github-issue-template-explanation-and-examples Please also note that the NVDA project has a Citizen and Contributor Code of Conduct which can be found at https://github.com/nvaccess/nvda/blob/master/CODE_OF_CONDUCT.md. NV Access expects that all contributors and other community members read and abide by the rules set out in this document while participating or contributing to this project. This includes creating or commenting on issues and pull requests. + +Each of the questions and sections below start with multiple hash symbols (#). Place your answers and information on the blank line below each question. --> ### Is your feature request related to a problem? Please describe. From 3f6bf0c749c76cb9c588152e2dde0801b8973f69 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 3 May 2021 13:15:51 +0800 Subject: [PATCH 156/174] Fix missing issue auto-link (PR #12358) --- user_docs/en/changes.t2t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 4d5c7797e0b..b5f9bc5a55f 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -30,7 +30,7 @@ What's New in NVDA == Bug Fixes == -- The list of messages in Outlook 2010 is once again readable. (12241) +- The list of messages in Outlook 2010 is once again readable. (#12241) - In terminal programs on Windows 10 version 1607 and later, when inserting or deleting characters in the middle of a line, the characters to the right of the caret are no longer read out. (#3200) - This experimental fix must be manually enabled in NVDA's advanced settings panel by changing the diff algorithm to Diff Match Patch. - Fixed access to edit fields in MCS Electronics IDE's. (#11966) From 86fe20ff6aeb07c22abec29d32fd1bcb19bf41b8 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Tue, 4 May 2021 03:00:30 +0200 Subject: [PATCH 157/174] Elements List: Don't remove the label of a filled spin button input field (#12317) (#12318) In browse mode documents, when a spin button input field is filled with a value, its label goes missing from the Elements List dialog. Description of how this pull request fixes the issue: Remove the role ROLE_SPINBUTTON from the set of exceptions for which the content replaces the label. --- source/browseMode.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/source/browseMode.py b/source/browseMode.py index cae9ee5f741..0729e0aecf8 100644 --- a/source/browseMode.py +++ b/source/browseMode.py @@ -1,6 +1,6 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2007-2020 NV Access Limited, Babbage B.V., James Teh, Leonard de Ruijter, -# Thomas Stivers +# Copyright (C) 2007-2021 NV Access Limited, Babbage B.V., James Teh, Leonard de Ruijter, +# Thomas Stivers, Accessolutions, Julien Cochuyt # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -252,7 +252,15 @@ def _getLabelForProperties(self, labelPropertyGetter): realStates = labelPropertyGetter("states") labeledStates = " ".join(controlTypes.processAndLabelStates(role, realStates, OutputReason.FOCUS)) if self.itemType == "formField": - if role in (controlTypes.ROLE_BUTTON,controlTypes.ROLE_DROPDOWNBUTTON,controlTypes.ROLE_TOGGLEBUTTON,controlTypes.ROLE_SPLITBUTTON,controlTypes.ROLE_MENUBUTTON,controlTypes.ROLE_DROPDOWNBUTTONGRID,controlTypes.ROLE_SPINBUTTON,controlTypes.ROLE_TREEVIEWBUTTON): + if role in ( + controlTypes.ROLE_BUTTON, + controlTypes.ROLE_DROPDOWNBUTTON, + controlTypes.ROLE_TOGGLEBUTTON, + controlTypes.ROLE_SPLITBUTTON, + controlTypes.ROLE_MENUBUTTON, + controlTypes.ROLE_DROPDOWNBUTTONGRID, + controlTypes.ROLE_TREEVIEWBUTTON + ): # Example output: Mute; toggle button; pressed labelParts = (content or name or unlabeled, roleText, labeledStates) else: From bacd1abe02a69d423872d551b624de8e1e4bbf62 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Wed, 5 May 2021 16:02:01 +1000 Subject: [PATCH 158/174] Focus chrome after starting in system tests (#12352) As discussed in #12293, our systems fail randomly. Usually this is due to another window stealing focus, such as the taskbar or Docker. As system tests are run locally, we shouldn't be killing these processes. Description of how this pull request fixes the issue: Use windows API to make the chrome window gain focus Adds logging that lists the foreground window and open windows if chrome doesn't gain focus Removes extra sleep time after starting NVDA --- tests/system/libraries/ChromeLib.py | 57 ++++++++++++--- .../system/libraries/SystemTestSpy/windows.py | 71 +++++++++++++++++++ tests/system/robot/chromeTests.robot | 1 - 3 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 tests/system/libraries/SystemTestSpy/windows.py diff --git a/tests/system/libraries/ChromeLib.py b/tests/system/libraries/ChromeLib.py index d99c3fe37e4..74ed48ecdb7 100644 --- a/tests/system/libraries/ChromeLib.py +++ b/tests/system/libraries/ChromeLib.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2020 NV Access Limited +# Copyright (C) 2020-2021 NV Access Limited # This file may be used under the terms of the GNU General Public License, version 2 or later. # For more details see: https://www.gnu.org/licenses/gpl-2.0.html @@ -12,8 +12,15 @@ import tempfile as _tempfile from typing import Optional as _Optional from SystemTestSpy import ( + _blockUntilConditionMet, _getLib, ) +from SystemTestSpy.windows import ( + GetForegroundWindowTitle, + GetVisibleWindowTitles, + SetForegroundWindow, +) +import re from robot.libraries.BuiltIn import BuiltIn # Imported for type information @@ -64,6 +71,14 @@ def start_chrome(self, filePath): _afterMarker = "After Test Case Marker" _loadCompleteString = "Test page load complete" + @staticmethod + def getUniqueTestCaseTitle(testCase: str) -> str: + return f"{ChromeLib._testCaseTitle} ({abs(hash(testCase))})" + + @staticmethod + def getUniqueTestCaseTitleRegex(testCase: str) -> re.Pattern: + return re.compile(f"^{ChromeLib._testCaseTitle} \\({abs(hash(testCase))}\\)") + @staticmethod def _writeTestFile(testCase) -> str: """ @@ -75,7 +90,7 @@ def _writeTestFile(testCase) -> str: filePath = ChromeLib._getTestCasePath("test.html") fileContents = (f""" - {ChromeLib._testCaseTitle} + {ChromeLib.getUniqueTestCaseTitle(testCase)}

{ChromeLib._beforeMarker}

@@ -116,6 +131,33 @@ def _waitForStartMarker(self, spy, lastSpeechIndex): " See NVDA log for full speech." ) + def _focusChrome(self, startsWithTestCaseTitle: re.Pattern): + """ Ensure chrome started and is focused. + Different versions of chrome have variations in how the title is presented. + This may mean that there is a separator between document name and application name. + E.G. "htmlTest Google Chrome", "html – Google Chrome" or perhaps no application name at all. + Rather than try to get this right, just use the doc title. + If this continues to be unreliable we could use Selenium or similar to start chrome and inform us + when it is ready. + """ + success, _success = _blockUntilConditionMet( + getValue=lambda: SetForegroundWindow(startsWithTestCaseTitle), + giveUpAfterSeconds=3, + intervalBetweenSeconds=0.5 + ) + if success: + return + windowInformation = "" + try: + windowInformation = f"Foreground Window: {GetForegroundWindowTitle()}.\n" + windowInformation += f"Open Windows: {GetVisibleWindowTitles()}" + except OSError as e: + builtIn.log(f"Couldn't retrieve active window information.\nException: {e}") + raise AssertionError( + "Unable to focus Chrome.\n" + f"{windowInformation}" + ) + def prepareChrome(self, testCase: str) -> None: """ Starts Chrome opening a file containing the HTML sample @@ -127,15 +169,8 @@ def prepareChrome(self, testCase: str) -> None: spy.wait_for_speech_to_finish() lastSpeechIndex = spy.get_last_speech_index() self.start_chrome(path) - # Ensure chrome started - # Different versions of chrome have variations in how the title is presented - # This may mean that there is a separator between document name and - # application name. E.G. "htmlTest Google Chrome", "html – Google Chrome" or perhaps no applcation - # name at all. - # Rather than try to get this right, just wait for the doc title. - # If this continues to be unreliable we could use solenium or similar to start chrome and inform us when - # it is ready. - applicationTitle = f"{self._testCaseTitle}" + self._focusChrome(ChromeLib.getUniqueTestCaseTitleRegex(testCase)) + applicationTitle = ChromeLib.getUniqueTestCaseTitle(testCase) appTitleIndex = spy.wait_for_specific_speech(applicationTitle, afterIndex=lastSpeechIndex) self._waitForStartMarker(spy, appTitleIndex) # Move to the loading status line, and wait fore it to become complete diff --git a/tests/system/libraries/SystemTestSpy/windows.py b/tests/system/libraries/SystemTestSpy/windows.py new file mode 100644 index 00000000000..6742bad58eb --- /dev/null +++ b/tests/system/libraries/SystemTestSpy/windows.py @@ -0,0 +1,71 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2021 NV Access Limited +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + +"""This module provides functions to interact with the Windows system. +""" + +from ctypes.wintypes import HWND, LPARAM +from ctypes import c_bool, c_int, create_unicode_buffer, POINTER, WINFUNCTYPE, windll, WinError +import re +from typing import Callable, List, NamedTuple + + +class Window(NamedTuple): + hwnd: HWND + title: str + + +def _GetWindowTitle(hwnd: HWND) -> str: + length = windll.user32.GetWindowTextLengthW(hwnd) + if not length: + return '' + buff = create_unicode_buffer(length + 1) + if not windll.user32.GetWindowTextW(hwnd, buff, length + 1): + raise WinError() + return str(buff.value) + + +def _GetWindows( + filterUsingWindow: Callable[[Window], bool] = lambda _: True, +) -> List[Window]: + windows: List[Window] = [] + EnumWindowsProc = WINFUNCTYPE(c_bool, POINTER(c_int), POINTER(c_int)) + + def _append_title(hwnd: HWND, _lParam: LPARAM) -> bool: + window = Window(hwnd, _GetWindowTitle(hwnd)) + if filterUsingWindow(window): + windows.append(window) + return True + + if not windll.user32.EnumWindows(EnumWindowsProc(_append_title), 0): + raise WinError() + return windows + + +def _GetVisibleWindows() -> List[Window]: + return _GetWindows( + filterUsingWindow=lambda window: windll.user32.IsWindowVisible(window.hwnd) and bool(window.title) + ) + + +def SetForegroundWindow(targetTitle: re.Pattern) -> bool: + if re.match(targetTitle, GetForegroundWindowTitle()): + return True + windows = _GetWindows( + filterUsingWindow=lambda window: re.match(targetTitle, window.title) + ) + for window in windows: + return windll.user32.SetForegroundWindow(window.hwnd) + return False + + +def GetVisibleWindowTitles() -> List[str]: + windows = _GetVisibleWindows() + return [w.title for w in windows] + + +def GetForegroundWindowTitle() -> str: + hwnd = windll.user32.GetForegroundWindow() + return _GetWindowTitle(hwnd) diff --git a/tests/system/robot/chromeTests.robot b/tests/system/robot/chromeTests.robot index 7a2e7b03610..b84747cdba6 100644 --- a/tests/system/robot/chromeTests.robot +++ b/tests/system/robot/chromeTests.robot @@ -24,7 +24,6 @@ default teardown default setup start NVDA standard-dontShowWelcomeDialog.ini - Sleep 5s *** Test Cases *** From 5ce754558d1e6f45b8b3f19e5942b2e4eb1e1a67 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Wed, 5 May 2021 17:14:18 +1000 Subject: [PATCH 159/174] Revert "Microsoft Word documents (both with UIA enabled and disabled) now get a treeInterceptor created straight way, but with passThrough (focus mode) enabled. Thus, NVDA+f7 (elements list) is now available with out having to switch to browse mode in Microsoft Word first. (#12051)" (#12365) This reverts pr #12051 commit db664bef7881d35c6af156287b9b518130aa2a69. Fixes #12117 Summary of the issue: In both Outlook and Windows 10 Mail, a Microsoft Word document control is used to display content of received emails and emails currently being composed. In NVDA 2020.4, NVDA would use browse mode for reading emails, but not for writing emails. However, after merging of pr #12051 browse mode is no longer used by default when reading emails. This is because the base Microsoft Word document NVDAObject now creates a TreeInterceptor all the time, but set to focus mode, so that elements list is always available in Microsoft Word. But as hxMail and Outlook implementations assumed browse mode would be available for the TreeInterceptor always, and only created the TreeInterceptor in the reading pane, Windows 10 mail and Outlook ended up getting no treeInterceptor for writing email (ok) but for reading email it got a treeInterceptor but set to focus mode (not okay). Description of how this pull request fixes the issue: Reverts pr #12051 . --- source/NVDAObjects/IAccessible/winword.py | 7 ++----- source/NVDAObjects/UIA/wordDocument.py | 12 ++---------- source/NVDAObjects/window/winword.py | 8 -------- 3 files changed, 4 insertions(+), 23 deletions(-) diff --git a/source/NVDAObjects/IAccessible/winword.py b/source/NVDAObjects/IAccessible/winword.py index 72da470d693..acc273a6377 100644 --- a/source/NVDAObjects/IAccessible/winword.py +++ b/source/NVDAObjects/IAccessible/winword.py @@ -21,14 +21,11 @@ from NVDAObjects.window import DisplayModelEditableText from ..behaviors import EditableTextWithoutAutoSelectDetection from NVDAObjects.window.winword import * -from NVDAObjects.window.winword import WordDocumentTreeInterceptor - class WordDocument(IAccessible,EditableTextWithoutAutoSelectDetection,WordDocument): - treeInterceptorClass = WordDocumentTreeInterceptor - shouldCreateTreeInterceptor = True - + treeInterceptorClass=WordDocumentTreeInterceptor + shouldCreateTreeInterceptor=False TextInfo=WordDocumentTextInfo def _get_ignoreEditorRevisions(self): diff --git a/source/NVDAObjects/UIA/wordDocument.py b/source/NVDAObjects/UIA/wordDocument.py index 105843d999c..bccd3d03783 100644 --- a/source/NVDAObjects/UIA/wordDocument.py +++ b/source/NVDAObjects/UIA/wordDocument.py @@ -298,14 +298,6 @@ def getTextWithFields(self,formatConfig=None): class WordBrowseModeDocument(UIABrowseModeDocument): - # This treeInterceptor starts in focus mode, thus escape should not switch back to browse mode - disableAutoPassThrough = True - - def __init__(self, rootNVDAObject): - super(WordBrowseModeDocument, self).__init__(rootNVDAObject) - self.passThrough = True - browseMode.reportPassThrough.last = True - def shouldSetFocusToObj(self,obj): # Ignore strange editable text fields surrounding most inner fields (links, table cells etc) if obj.role==controlTypes.ROLE_EDITABLETEXT and obj.UIAElement.cachedAutomationID.startswith('UIA_AutomationId_Word_Content'): @@ -349,8 +341,8 @@ def _get_role(self): return role class WordDocument(UIADocumentWithTableNavigation,WordDocumentNode,WordDocumentBase): - treeInterceptorClass = WordBrowseModeDocument - shouldCreateTreeInterceptor = True + treeInterceptorClass=WordBrowseModeDocument + shouldCreateTreeInterceptor=False announceEntireNewLine=True # Microsoft Word duplicates the full title of the document on this control, which is redundant as it appears in the title of the app itself. diff --git a/source/NVDAObjects/window/winword.py b/source/NVDAObjects/window/winword.py index cd2449b0195..2d83596adba 100755 --- a/source/NVDAObjects/window/winword.py +++ b/source/NVDAObjects/window/winword.py @@ -1069,14 +1069,6 @@ def _get_focusableNVDAObjectAtStart(self): class WordDocumentTreeInterceptor(browseMode.BrowseModeDocumentTreeInterceptor): - # This treeInterceptor starts in focus mode, thus escape should not switch back to browse mode - disableAutoPassThrough = True - - def __init__(self, rootNVDAObject): - super(WordDocumentTreeInterceptor, self).__init__(rootNVDAObject) - self.passThrough = True - browseMode.reportPassThrough.last = True - TextInfo=BrowseModeWordDocumentTextInfo def _activateLongDesc(self,controlField): From 54e18f51b2bbd664edad80188b86812798a164a5 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Thu, 6 May 2021 08:00:16 +0200 Subject: [PATCH 160/174] No startup sound for launcher --minimal (PR #12322) Fixes #12289 Co-authored-by: Reef Turner --- launcher/nvdaLauncher.nsi | 21 +++++++++++++++++---- user_docs/en/changes.t2t | 1 + 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/launcher/nvdaLauncher.nsi b/launcher/nvdaLauncher.nsi index cef8e853cf1..2711ae7e5b8 100644 --- a/launcher/nvdaLauncher.nsi +++ b/launcher/nvdaLauncher.nsi @@ -1,4 +1,5 @@ !include "fileFunc.nsh" +!include "LogicLib.nsh" !include "mui2.nsh" !define launcher_appExe "nvdaLauncher.exe" @@ -72,10 +73,17 @@ Banner::show /nounload BringToFront setOutPath "$PLUGINSDIR" -;Play NVDA logo sound -File "..\miscDeps\launcher\nvda_logo.wav" -Push "$PLUGINSDIR\nvda_logo.wav" -Call PlaySound +; Get the full param string and puts it in register $0. +; So $0 may then contain eg. "--minimal --install" +; Reference: https://nsis.sourceforge.io/Docs/AppendixE.html#getparameters +${GetParameters} $0 +; From the params string, looks for option "--minimal", tries to get it's (unused) value and stores in $1. +; Sets the error flag if the option is missing. +; Reference: https://nsis.sourceforge.io/Docs/AppendixE.html#getoptions +${GetOptions} $0 "--minimal" $1 +${If} ${Errors} + Call PlayLogoSound +${EndIf} CreateDirectory "$PLUGINSDIR\app" setOutPath "$PLUGINSDIR\app" file /R "${NVDADistDir}\" @@ -87,6 +95,11 @@ execWait "$PLUGINSDIR\app\nvda_noUIAccess.exe $0 -r --launcher" $1 intcmp $1 3 exec +1 SectionEnd +Function PlayLogoSound +File "..\miscDeps\launcher\nvda_logo.wav" +Push "$PLUGINSDIR\nvda_logo.wav" +Call PlaySound +FunctionEnd Function PlaySound ; Retrieve the file to play diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index b5f9bc5a55f..0c9fe36fbce 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -51,6 +51,7 @@ What's New in NVDA - Fixed copying config during installation from a portable copy when default destination config directory is empty. (#12071, #12205) - Fixed incorrect announcement of some letters with accents or diacritic when 'Say cap before capitals' option is checked. (#11948) - Fixed the pitch change failure in Sapi4 speech synthesizer. (#12311) +- The NVDA installer now also honors the ``--minimal`` command line parameter and does not play the start-up sound, following the same documented behavior as an installed or portable copy NVDA executable. (#12289) == Changes for Developers == From a3e983a8687e45aeeaf9931b09b6922a8e317cef Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 6 May 2021 16:16:52 +0800 Subject: [PATCH 161/174] Fix wx assertion error (PR #12363) Fixes: #12336 Fixes: #12220 # Summary of the issue: Issue#12220 Causes a wx assertion message when either the Braille or Speech settings panels are open. This seems to be related to the expando text control used on both panels. The assertion is in wx's accessibility code, which has been introduced in our latest upgrade of wxPython. The PR #12292 attempted to fix this by explicitly destroying the expando text control when closing. In #12292 it was missed that the onSave callback was also called for the apply button. # Description of how this pull request fixes the issue: While looking at adding an explicit close callback for panels, I noticed that Destroy was being called manually during the event handler. Scheduling a destroy call after the event handler seems to resolve this issue. As I understand, destroying children explicitly is not required. While here also: - Tidy onSave / onApply - Add type info for 'catIdToInstanceMap' and 'categoryClasses' --- source/gui/settingsDialogs.py | 58 ++++++++++++++--------------------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 9863cb6a5cd..18321a33176 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -237,8 +237,7 @@ def onOk(self, evt): Sub-classes may extend this method. This base method should always be called to clean up the dialog. """ - self.DestroyChildren() - self.Destroy() + self.DestroyLater() self.SetReturnCode(wx.ID_OK) def onCancel(self, evt): @@ -246,8 +245,7 @@ def onCancel(self, evt): Sub-classes may extend this method. This base method should always be called to clean up the dialog. """ - self.DestroyChildren() - self.Destroy() + self.DestroyLater() self.SetReturnCode(wx.ID_CANCEL) def onApply(self, evt): @@ -404,7 +402,7 @@ class MultiCategorySettingsDialog(SettingsDialog): """ title="" - categoryClasses=[] + categoryClasses: typing.List[typing.Type[SettingsPanel]] = [] class CategoryUnavailableError(RuntimeError): pass @@ -428,8 +426,9 @@ def __init__(self, parent, initialCategory=None): self.initialCategory = initialCategory self.currentCategory = None self.setPostInitFocus = None - # dictionary key is index of category in self.catList, value is the instance. Partially filled, check for KeyError - self.catIdToInstanceMap = {} + # dictionary key is index of category in self.catList, value is the instance. + # Partially filled, check for KeyError + self.catIdToInstanceMap: typing.Dict[int, SettingsPanel] = {} super(MultiCategorySettingsDialog, self).__init__( parent, @@ -635,37 +634,42 @@ def onCategoryChange(self, evt): else: evt.Skip() - def _doSave(self): + def _validateAllPanels(self): + """Check if all panels are valid, and can be saved + @note: raises ValueError if a panel is not valid. See c{SettingsPanel.isValid} + """ for panel in self.catIdToInstanceMap.values(): if panel.isValid() is False: raise ValueError("Validation for %s blocked saving settings" % panel.__class__.__name__) + + def _saveAllPanels(self): for panel in self.catIdToInstanceMap.values(): panel.onSave() + + def _notifyAllPanelsSaveOccurred(self): for panel in self.catIdToInstanceMap.values(): panel.postSave() - def onOk(self,evt): + def _doSave(self): try: - self._doSave() + self._validateAllPanels() + self._saveAllPanels() + self._notifyAllPanelsSaveOccurred() except ValueError: - log.debugWarning("", exc_info=True) + log.debugWarning("Error while saving settings:", exc_info=True) return - for panel in self.catIdToInstanceMap.values(): - panel.Destroy() + + def onOk(self, evt): + self._doSave() super(MultiCategorySettingsDialog,self).onOk(evt) def onCancel(self,evt): for panel in self.catIdToInstanceMap.values(): panel.onDiscard() - panel.Destroy() super(MultiCategorySettingsDialog,self).onCancel(evt) def onApply(self,evt): - try: - self._doSave() - except ValueError: - log.debugWarning("", exc_info=True) - return + self._doSave() super(MultiCategorySettingsDialog,self).onApply(evt) @@ -1000,17 +1004,9 @@ def onPanelDeactivated(self): super(SpeechSettingsPanel,self).onPanelDeactivated() def onDiscard(self): - # Work around wxAssertion error #12220 - # Manually destroying the ExpandoTextCtrl when the settings dialog is - # exited prevents the wxAssertion. - self.synthNameCtrl.Destroy() self.voicePanel.onDiscard() def onSave(self): - # Work around wxAssertion error #12220 - # Manually destroying the ExpandoTextCtrl when the settings dialog is - # exited prevents the wxAssertion. - self.synthNameCtrl.Destroy() self.voicePanel.onSave() class SynthesizerSelectionDialog(SettingsDialog): @@ -3216,17 +3212,9 @@ def onPanelDeactivated(self): super(BrailleSettingsPanel,self).onPanelDeactivated() def onDiscard(self): - # Work around wxAssertion error #12220 - # Manually destroying the ExpandoTextCtrl when the settings dialog is - # exited prevents the wxAssertion. - self.displayNameCtrl.Destroy() self.brailleSubPanel.onDiscard() def onSave(self): - # Work around wxAssertion error #12220 - # Manually destroying the ExpandoTextCtrl when the settings dialog is - # exited prevents the wxAssertion. - self.displayNameCtrl.Destroy() self.brailleSubPanel.onSave() From a7f9d47d48578e841e2435c8ec4488123b21f615 Mon Sep 17 00:00:00 2001 From: Joseph Lee Date: Thu, 6 May 2021 03:25:59 -0700 Subject: [PATCH 162/174] Recognize Windows 10 build 19043 as Version 21H1 (May 2021 Update) (PR #12259) * WinVersion: recognize Windows 10 build 19043 as Version 21H1. Add WIN10_21H1 constant and 21H1 key to Windows 10 versions to builds map. * AppX: update max version tested key to 10.0.19043.0 (Version 21H1). --- appx/manifest.xml.subst | 2 +- source/winVersion.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/appx/manifest.xml.subst b/appx/manifest.xml.subst index 6efe97163e1..8b0d8f87149 100644 --- a/appx/manifest.xml.subst +++ b/appx/manifest.xml.subst @@ -24,7 +24,7 @@ diff --git a/source/winVersion.py b/source/winVersion.py index 05a11efe2cb..f3696cc295a 100644 --- a/source/winVersion.py +++ b/source/winVersion.py @@ -96,6 +96,7 @@ def __ge__(self, other): WIN10_1909 = WinVersion(major=10, minor=0, build=18363) WIN10_2004 = WinVersion(major=10, minor=0, build=19041) WIN10_20H2 = WinVersion(major=10, minor=0, build=19042) +WIN10_21H1 = WinVersion(major=10, minor=0, build=19043) def getWinVer(): @@ -135,6 +136,7 @@ def isUwpOcrAvailable(): "1909": 18363, "2004": 19041, "20H2": 19042, + "21H1": 19043, } From a110d14b919dd46451681779c63b588240af3808 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Fri, 7 May 2021 11:06:58 +1000 Subject: [PATCH 163/174] Increase AppVeyor git clone depth and lint try branch builds (#12346) Up the clone depth to unlimited (by removing setting the value) Fetch master directly when performing a try-branch build so that lint checks can now run on try branch builds recursively fetch submodules on demand. --- appveyor.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 98e0d1a3f97..4065e1b4f04 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -56,8 +56,6 @@ init: Set-AppveyorBuildVariable "versionType" $versionType } -clone_depth: 1 - install: - cd appveyor # Decrypt files. @@ -178,20 +176,27 @@ test_script: # Flake8 Linting - ps: | - if($env:APPVEYOR_PULL_REQUEST_NUMBER) { $lintOutput = (Resolve-Path .\testOutput\lint\) $lintSource = (Resolve-Path .\tests\lint\) + $flake8Output = "$lintOutput\Flake8.txt" # When Appveyor runs for a pr, # the build is made from a new temporary commit, # resulting from the pr branch being merged into its base branch. # Therefore to create a diff for linting, we must fetch the head of the base branch. # In a PR, APPVEYOR_REPO_BRANCH points to the head of the base branch. - git fetch -q origin $env:APPVEYOR_REPO_BRANCH - $flake8Output = "$lintOutput\PR-Flake8.txt" + # Additionally, we can not use a clone_depth of 1, but must use an unlimited clone. + if($env:APPVEYOR_PULL_REQUEST_NUMBER) { + git fetch -q origin $env:APPVEYOR_REPO_BRANCH + $msgBaseLabel = "PR" + } else { + # However in a pushed branch, we must fetch master. + git fetch -q origin master:master + $msgBaseLabel = "Branch" + } .\runlint.bat FETCH_HEAD "$flake8Output" if($LastExitCode -ne 0) { - $errorCode=$LastExitCode - Add-AppveyorMessage "PR introduces Flake8 errors" + $errorCode=$LastExitCode + Add-AppveyorMessage "$msgBaseLabel introduces Flake8 errors" } Push-AppveyorArtifact $flake8Output $junitXML = "$lintOutput\PR-Flake8.xml" @@ -200,7 +205,6 @@ test_script: $wc = New-Object 'System.Net.WebClient' $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", $junitXML) if($errorCode -ne 0) { $host.SetShouldExit($errorCode) } - } # System tests - ps: | From 9dc1f3661c2a2fc274f2be49c4fcb7fc62dc7291 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Fri, 7 May 2021 12:31:15 +1000 Subject: [PATCH 164/174] refactor the exit of nvda and gui.terminate (#12342) Summary of the issue: Changes introduced in #12183 - caused the braille viewer to be closed without saving state properly - lost code that destroyed the system tray and menu in some instances - made most of gui.terminate no longer necessary/redundant Description of how this pull request fixes the issue: - Creates `core.triggerNVDAExit` which terminates necessary modules safely and then closes all windows - Destroys the system tray icon and menu - Uses a parser error message if a new NVDA instance fails to end a running instance. - Uses an enum for ChangeWindowMessageFilter filters. Known issues with pull request: WM_QUIT will not exit the app safely (called from a new NVDA instance) when a dialog such as WelcomeDialog is still open --- source/JABHandler.py | 4 +- source/core.py | 89 +++++++++++++++++++++++++++++++++--- source/gui/__init__.py | 56 +++-------------------- source/gui/installerGui.py | 5 +- source/gui/startupDialogs.py | 4 +- source/nvda.pyw | 12 +++-- source/winUser.py | 21 ++++++--- 7 files changed, 117 insertions(+), 74 deletions(-) diff --git a/source/JABHandler.py b/source/JABHandler.py index 9466f4489c9..5a9e745cc64 100644 --- a/source/JABHandler.py +++ b/source/JABHandler.py @@ -795,10 +795,10 @@ def initialize(): ): enableBridge() # Accept wm_copydata and any wm_user messages from other processes even if running with higher privileges - if not windll.user32.ChangeWindowMessageFilter(winUser.WM_COPYDATA, 1): + if not windll.user32.ChangeWindowMessageFilter(winUser.WM_COPYDATA, winUser.MSGFLT.ALLOW): raise WinError() for msg in range(winUser.WM_USER + 1, 0xffff): - if not windll.user32.ChangeWindowMessageFilter(msg, 1): + if not windll.user32.ChangeWindowMessageFilter(msg, winUser.MSGFLT.ALLOW): raise WinError() bridgeDll.Windows_run() # Register java events diff --git a/source/core.py b/source/core.py index aec9d54b570..3304d197ae6 100644 --- a/source/core.py +++ b/source/core.py @@ -43,6 +43,8 @@ class CallCancelled(Exception): # inform those who want to know that NVDA has finished starting up. postNvdaStartup = extensionPoints.Action() +# inform those who want to know that NVDA has begun to exit. +preNVDAExit = extensionPoints.Action() PUMP_MAX_DELAY = 10 @@ -111,9 +113,8 @@ def onResult(ID): def restart(disableAddons=False, debugLogging=False): """Restarts NVDA by starting a new copy.""" if globalVars.appArgs.launcher: - import gui globalVars.exitCode=3 - gui.safeAppExit() + triggerNVDAExit() return import subprocess import winUser @@ -230,6 +231,64 @@ def getWxLangOrNone() -> Optional['wx.LanguageInfo']: return wxLang +def triggerNVDAExit(): + preNVDAExit.notify() + # ensure NVDA only runs exit procedures once + handlers = list(preNVDAExit.handlers) # don't mutate .handlers directly with unregister while iterating + for handler in handlers: + preNVDAExit.unregister(handler) + + +def _closeAllWindows(): + """ + Should only be used by calling triggerNVDAExit and after handleNVDAModuleCleanupBeforeGUIExit. + Ensures the wx mainloop is exited by all the top windows being destroyed. + wx objects that don't inherit from wx.Window (eg sysTrayIcon, Menu) need to be manually destroyed. + """ + import gui + from gui.settingsDialogs import SettingsDialog + from typing import Dict + import wx + + app = wx.GetApp() + + # prevent race condition with object deletion + # prevent deletion of the object while we work on it. + _SettingsDialog = SettingsDialog + nonWeak: Dict[_SettingsDialog, _SettingsDialog] = dict(_SettingsDialog._instances) + + for instance, state in nonWeak.items(): + if state is _SettingsDialog.DialogState.DESTROYED: + log.error( + "Destroyed but not deleted instance of gui.SettingsDialog exists" + f": {instance.title} - {instance.__class__.__qualname__} - {instance}" + ) + else: + log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance)) + + # wx.Windows destroy child Windows automatically but wx.Menu and TaskBarIcon don't inherit from wx.Window. + # They must be manually destroyed when exiting the app. + # Note: this doesn't consistently clean them from the tray and appears to be a wx issue. (#12286, #12238) + log.debug("destroying system tray icon and menu") + app.ScheduleForDestruction(gui.mainFrame.sysTrayIcon.menu) + gui.mainFrame.sysTrayIcon.RemoveIcon() + app.ScheduleForDestruction(gui.mainFrame.sysTrayIcon) + + for window in wx.GetTopLevelWindows(): + if isinstance(window, wx.Dialog) and window.IsModal(): + log.debug(f"ending modal {window} during exit process") + wx.CallAfter(window.EndModal, wx.ID_CLOSE_ALL) + elif not isinstance(window, gui.MainFrame): + log.debug(f"closing window {window} during exit process") + wx.CallAfter(window.Close) + + wx.Yield() # creates a temporary event loop and uses it instead to process pending messages + log.debug("destroying main frame during exit process") + # the MainFrame has EVT_CLOSE bound to the ExitDialog + # which calls this function on exit, so destroy this window + app.ScheduleForDestruction(gui.mainFrame) + + def main(): """NVDA's core main loop. This initializes all modules such as audio, IAccessible, keyboard, mouse, and GUI. @@ -580,16 +639,32 @@ def _doPostNvdaStartupAction(): queueHandler.queueFunction(queueHandler.eventQueue, _doPostNvdaStartupAction) + def handleNVDAModuleCleanupBeforeGUIExit(): + """ Terminates various modules that rely on the GUI. This should be used before closing all windows + and terminating the GUI + """ + import brailleViewer + # before the GUI is terminated we must terminate the update checker + if updateCheck: + _terminate(updateCheck) + + # The core is expected to terminate, so we should not treat this as a crash + _terminate(watchdog) + # plugins must be allowed to close safely before we terminate the GUI as dialogs may be unsaved + _terminate(globalPluginHandler) + # the brailleViewer should be destroyed safely before closing the window + brailleViewer.destroyBrailleViewer() + + preNVDAExit.register(handleNVDAModuleCleanupBeforeGUIExit) + preNVDAExit.register(_closeAllWindows) log.debug("entering wx application main loop") app.MainLoop() log.info("Exiting") - if updateCheck: - _terminate(updateCheck) - - _terminate(watchdog) - _terminate(globalPluginHandler, name="global plugin handler") + # If MainLoop is terminated through WM_QUIT, such as starting an NVDA instance older than 2021.1, + # triggerNVDAExit has not been called yet + triggerNVDAExit() _terminate(gui) config.saveOnExit() diff --git a/source/gui/__init__.py b/source/gui/__init__.py index 4098735ec7e..a8d8fc928a7 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -5,7 +5,6 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. -import typing import time import os import sys @@ -25,7 +24,7 @@ import queueHandler import core from . import guiHelper -from . import settingsDialogs +from .settingsDialogs import SettingsDialog from .settingsDialogs import * from .inputGestures import InputGesturesDialog import speechDictHandler @@ -197,7 +196,7 @@ def onExitCommand(self, evt): d.Show() self.postPopup() else: - safeAppExit() + core.triggerNVDAExit() def onNVDASettingsCommand(self,evt): self._popupSettingsDialog(NVDASettingsDialog) @@ -357,25 +356,6 @@ def onConfigProfilesCommand(self, evt): ProfilesDialog(gui.mainFrame).Show() self.postPopup() - -def safeAppExit(): - """ - Ensures the app is exited by all the top windows being destroyed - """ - - for window in wx.GetTopLevelWindows(): - if isinstance(window, wx.Dialog) and window.IsModal(): - log.info(f"ending modal {window} during exit process") - wx.CallAfter(window.EndModal, wx.ID_CLOSE_ALL) - if isinstance(window, MainFrame): - log.info(f"destroying main frame during exit process") - # the MainFrame has EVT_CLOSE bound to the ExitDialog - # which calls this function on exit, so destroy this window - wx.CallAfter(window.Destroy) - else: - log.info(f"closing window {window} during exit process") - wx.CallAfter(window.Close) - class SysTrayIcon(wx.adv.TaskBarIcon): def __init__(self, frame): @@ -558,6 +538,7 @@ def onActivate(self, evt): appModules.nvda.nvdaMenuIaIdentity = None mainFrame.postPopup() + def initialize(): global mainFrame if mainFrame: @@ -584,34 +565,9 @@ def wx_CallAfter_wrapper(func, *args, **kwargs): winUser.PostMessage(topHandle, winUser.WM_NULL, 0, 0) wx.CallAfter = wx_CallAfter_wrapper + def terminate(): - import brailleViewer - brailleViewer.destroyBrailleViewer() - - # prevent race condition with object deletion - # prevent deletion of the object while we work on it. - _SettingsDialog = settingsDialogs.SettingsDialog - nonWeak: typing.Dict[_SettingsDialog, _SettingsDialog] = dict(_SettingsDialog._instances) - - for instance, state in nonWeak.items(): - if state is _SettingsDialog.DialogState.DESTROYED: - log.error( - "Destroyed but not deleted instance of gui.SettingsDialog exists" - f": {instance.title} - {instance.__class__.__qualname__} - {instance}" - ) - else: - log.debug("Exiting NVDA with an open settings dialog: {!r}".format(instance)) global mainFrame - # This is called after the main loop exits because WM_QUIT exits the main loop - # without destroying all objects correctly and we need to support WM_QUIT. - # Therefore, any request to exit should exit the main loop. - safeAppExit() - # #4460: We need another iteration of the main loop - # so that everything (especially the TaskBarIcon) is cleaned up properly. - # ProcessPendingEvents doesn't seem to work, but MainLoop does. - # Because the top window gets destroyed, - # MainLoop thankfully returns pretty quickly. - wx.GetApp().MainLoop() mainFrame = None def showGui(): @@ -732,7 +688,7 @@ def onOk(self, evt): if action >= 2 and config.isAppX: action += 1 if action == 0: - safeAppExit() + core.triggerNVDAExit() elif action == 1: queueHandler.queueFunction(queueHandler.eventQueue,core.restart) elif action == 2: @@ -754,7 +710,7 @@ def onOk(self, evt): confirmUpdateDialog.ShowModal() else: updateCheck.executePendingUpdate() - self.Destroy() + wx.CallAfter(self.Destroy) def onCancel(self, evt): self.Destroy() diff --git a/source/gui/installerGui.py b/source/gui/installerGui.py index 9b574f27078..7846ff58072 100644 --- a/source/gui/installerGui.py +++ b/source/gui/installerGui.py @@ -10,6 +10,7 @@ import winUser import wx import config +import core import globalVars import installer from logHandler import log @@ -119,7 +120,7 @@ def doInstall( winUser.SW_SHOWNORMAL ) else: - gui.safeAppExit() + core.triggerNVDAExit() def doSilentInstall( @@ -466,7 +467,7 @@ def doCreatePortable(portableDirectory,copyUserConfig=False,silent=False,startAf return d.done() if silent: - gui.safeAppExit() + core.triggerNVDAExit() else: # Translators: The message displayed when a portable copy of NVDA has been successfully created. # %s will be replaced with the destination directory. diff --git a/source/gui/startupDialogs.py b/source/gui/startupDialogs.py index aadc038e8c0..ad6e60a26a0 100644 --- a/source/gui/startupDialogs.py +++ b/source/gui/startupDialogs.py @@ -117,7 +117,7 @@ def run(cls): gui.mainFrame.prePopup() d = cls(gui.mainFrame) d.ShowModal() - d.Destroy() + wx.CallAfter(d.Destroy) gui.mainFrame.postPopup() @@ -193,7 +193,7 @@ def onContinueRunning(self, evt): core.doStartupDialogs() def onExit(self, evt): - gui.safeAppExit() + core.triggerNVDAExit() @classmethod def run(cls): diff --git a/source/nvda.pyw b/source/nvda.pyw index 92117109c51..eddf757e0ec 100755 --- a/source/nvda.pyw +++ b/source/nvda.pyw @@ -198,8 +198,8 @@ if oldAppWindowHandle and not globalVars.appArgs.easeOfAccess: sys.exit(0) try: terminateRunningNVDA(oldAppWindowHandle) - except: - sys.exit(1) + except Exception as e: + parser.error(f"Couldn't terminate existing NVDA process, abandoning start:\nException: {e}") if globalVars.appArgs.quit or (oldAppWindowHandle and globalVars.appArgs.easeOfAccess): sys.exit(0) elif globalVars.appArgs.check_running: @@ -251,9 +251,11 @@ if customVenvDetected: log.warning("NVDA launched using a custom Python virtual environment.") if globalVars.appArgs.changeScreenReaderFlag: winUser.setSystemScreenReaderFlag(True) -#Accept wm_quit from other processes, even if running with higher privilages -if not ctypes.windll.user32.ChangeWindowMessageFilter(winUser.WM_QUIT,1): - raise WinError() + +# Accept WM_QUIT from other processes, even if running with higher privileges +if not ctypes.windll.user32.ChangeWindowMessageFilter(winUser.WM_QUIT, winUser.MSGFLT.ALLOW): + log.error("Unable to set the NVDA process to receive WM_QUIT messages from other processes") + raise winUser.WinError() # Make this the last application to be shut down and don't display a retry dialog box. winKernel.SetProcessShutdownParameters(0x100, winKernel.SHUTDOWN_NORETRY) if not isSecureDesktop and not config.isAppX: diff --git a/source/winUser.py b/source/winUser.py index e007c443a81..d93543f3c38 100644 --- a/source/winUser.py +++ b/source/winUser.py @@ -13,6 +13,7 @@ from ctypes.wintypes import HWND, RECT, DWORD import winKernel from textUtils import WCHAR_ENCODING +import enum #dll handles user32=windll.user32 @@ -114,12 +115,6 @@ class GUITHREADINFO(Structure): CBS_OWNERDRAWFIXED=0x0010 CBS_OWNERDRAWVARIABLE=0x0020 CBS_HASSTRINGS=0x00200 -WM_NULL=0 -WM_QUIT=18 -WM_COPYDATA=74 -WM_NOTIFY=78 -WM_DEVICECHANGE=537 -WM_USER=1024 #PeekMessage PM_REMOVE=1 PM_NOYIELD=2 @@ -146,6 +141,7 @@ class GUITHREADINFO(Structure): WM_NOTIFY = 78 WM_USER = 1024 WM_QUIT = 18 +WM_DEVICECHANGE = 537 WM_DISPLAYCHANGE = 0x7e WM_GETTEXT=13 WM_GETTEXTLENGTH=14 @@ -377,6 +373,19 @@ class GUITHREADINFO(Structure): # The height of the virtual screen, in pixels. SM_CYVIRTUALSCREEN = 79 + +class MSGFLT(enum.IntEnum): + # Actions associated with ChangeWindowMessageFilterEx + # https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-changewindowmessagefilterex + # Adds the message to the filter. This has the effect of allowing the message to be received. + ALLOW = 1 + # Removes the message from the filter. This has the effect of blocking the message. + DISALLOW = 2 + # Resets the window message filter to the default. + # Any message allowed globally or process-wide will get through. + RESET = 0 + + def setSystemScreenReaderFlag(val): user32.SystemParametersInfoW(SPI_SETSCREENREADER,val,0,SPIF_UPDATEINIFILE|SPIF_SENDCHANGE) From 57f2791654b3e10d929680c769c9d2ab09b5c2b3 Mon Sep 17 00:00:00 2001 From: buddsean Date: Mon, 12 Apr 2021 11:05:55 +1000 Subject: [PATCH 165/174] move speech/__init__.py to speech/speech.py and move sayAllHandler.py to speech/sayAll.py --- source/{sayAllHandler.py => speech/sayAll.py} | 0 source/speech/{__init__.py => speech.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename source/{sayAllHandler.py => speech/sayAll.py} (100%) rename source/speech/{__init__.py => speech.py} (100%) diff --git a/source/sayAllHandler.py b/source/speech/sayAll.py similarity index 100% rename from source/sayAllHandler.py rename to source/speech/sayAll.py diff --git a/source/speech/__init__.py b/source/speech/speech.py similarity index 100% rename from source/speech/__init__.py rename to source/speech/speech.py From c22509baa5972c9b546c79ed4896b9bb4076fc55 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Fri, 7 May 2021 08:45:09 +0200 Subject: [PATCH 166/174] Python Console: Jump to previous/next result (PR #9785) Fixes #9784 Summary of the issue: In the output pane of the Python Console, it can be tedious to inspect a series of lengthy output results. Description of how this pull request fixes the issue: Provide key bindings to jump to the previous/next result, select a whole result and clear the output pane. Co-authored-by: Reef Turner --- devDocs/developerGuide.t2t | 2 + source/appModules/nvda.py | 150 +++++++++++++++++++++++++++++++++++-- source/gui/__init__.py | 7 +- source/pythonConsole.py | 20 +++-- user_docs/en/changes.t2t | 3 + 5 files changed, 171 insertions(+), 11 deletions(-) diff --git a/devDocs/developerGuide.t2t b/devDocs/developerGuide.t2t index 45e20975111..2291fd243d1 100644 --- a/devDocs/developerGuide.t2t +++ b/devDocs/developerGuide.t2t @@ -824,6 +824,8 @@ You can navigate through the history of previously entered lines using the up an Output (responses from the interpreter) will be spoken when enter is pressed. The f6 key toggles between the input and output controls. +When on the output control, alt+up/down jumps to the previous/next result (add shift for selecting). +Pressing control+l clears the output. The result of the last executed command is stored in the "_" global variable. This shadows the gettext function which is stored as a built-in with the same name. diff --git a/source/appModules/nvda.py b/source/appModules/nvda.py index ba37154d879..4f0fd2a529b 100755 --- a/source/appModules/nvda.py +++ b/source/appModules/nvda.py @@ -1,20 +1,30 @@ -#appModules/nvda.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2008-2017 NV Access Limited -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2008-2021 NV Access Limited, James Teh, Michael Curran, Leonard de Ruijter, Reef Turner, +# Julien Cochuyt +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + + +import typing import appModuleHandler import api import controlTypes import versionInfo from NVDAObjects.IAccessible import IAccessible +from baseObject import ScriptableObject import gui +from scriptHandler import script import speech +import textInfos import braille import config from logHandler import log +if typing.TYPE_CHECKING: + import inputCore + + nvdaMenuIaIdentity = None class NvdaDialog(IAccessible): @@ -44,6 +54,118 @@ def _get_description(self): """ return "" + +# Translators: The name of a category of NVDA commands. +SCRCAT_PYTHON_CONSOLE = _("Python Console") + + +class NvdaPythonConsoleUIOutputClear(ScriptableObject): + + # Allow the bound gestures to be edited through the Input Gestures dialog (see L{gui.prePopup}) + isPrevFocusOnNvdaPopup = True + + @script( + gesture="kb:control+l", + # Translators: Description of a command to clear the Python Console output pane + description=_("Clear the output pane"), + category=SCRCAT_PYTHON_CONSOLE, + ) + def script_clearOutput(self, gesture: "inputCore.InputGesture"): + from pythonConsole import consoleUI + consoleUI.clear() + + +class NvdaPythonConsoleUIOutputCtrl(ScriptableObject): + + # Allow the bound gestures to be edited through the Input Gestures dialog (see L{gui.prePopup}) + isPrevFocusOnNvdaPopup = True + + @script( + gesture="kb:alt+downArrow", + # Translators: Description of a command to move to the next result in the Python Console output pane + description=_("Move to the next result"), + category=SCRCAT_PYTHON_CONSOLE + ) + def script_moveToNextResult(self, gesture: "inputCore.InputGesture"): + self._resultNavHelper(direction="next", select=False) + + @script( + gesture="kb:alt+upArrow", + # Translators: Description of a command to move to the previous result + # in the Python Console output pane + description=_("Move to the previous result"), + category=SCRCAT_PYTHON_CONSOLE + ) + def script_moveToPrevResult(self, gesture: "inputCore.InputGesture"): + self._resultNavHelper(direction="previous", select=False) + + @script( + gesture="kb:alt+downArrow+shift", + # Translators: Description of a command to select from the current caret position to the end + # of the current result in the Python Console output pane + description=_("Select until the end of the current result"), + category=SCRCAT_PYTHON_CONSOLE + ) + def script_selectToResultEnd(self, gesture: "inputCore.InputGesture"): + self._resultNavHelper(direction="next", select=True) + + @script( + gesture="kb:alt+shift+upArrow", + # Translators: Description of a command to select from the current caret position to the start + # of the current result in the Python Console output pane + description=_("Select until the start of the current result"), + category=SCRCAT_PYTHON_CONSOLE + ) + def script_selectToResultStart(self, gesture: "inputCore.InputGesture"): + self._resultNavHelper(direction="previous", select=True) + + def _resultNavHelper(self, direction: str = "next", select: bool = False): + from pythonConsole import consoleUI + startPos, endPos = consoleUI.outputCtrl.GetSelection() + if self.isTextSelectionAnchoredAtStart: + curPos = endPos + else: + curPos = startPos + if direction == "previous": + for pos in reversed(consoleUI.outputPositions): + if pos < curPos: + break + else: + # Translators: Reported when attempting to move to the previous result in the Python Console + # output pane while there is no previous result. + speech.speakMessage(_("Top")) + return + elif direction == "next": + for pos in consoleUI.outputPositions: + if pos > curPos: + break + else: + # Translators: Reported when attempting to move to the next result in the Python Console + # output pane while there is no next result. + speech.speakMessage(_("Bottom")) + return + else: + raise ValueError(u"Unexpected direction: {!r}".format(direction)) + if select: + consoleUI.outputCtrl.Freeze() + anchorPos = startPos if self.isTextSelectionAnchoredAtStart else endPos + consoleUI.outputCtrl.SetSelection(anchorPos, pos) + consoleUI.outputCtrl.Thaw() + self.detectPossibleSelectionChange() + self.isTextSelectionAnchoredAtStart = anchorPos < pos + else: + consoleUI.outputCtrl.SetSelection(pos, pos) + info = self.makeTextInfo(textInfos.POSITION_CARET) + copy = info.copy() + info.expand(textInfos.UNIT_LINE) + if ( + copy.move(textInfos.UNIT_CHARACTER, 4, endPoint="end") == 4 + and copy.text == ">>> " + ): + info.move(textInfos.UNIT_CHARACTER, 4, endPoint="start") + speech.speakTextInfo(info, reason=controlTypes.OutputReason.CARET) + + class AppModule(appModuleHandler.AppModule): # The configuration profile that has been previously edited. # This ought to be a class property. @@ -108,8 +230,26 @@ def isNvdaSettingsDialog(self, obj): return True return False + def isNvdaPythonConsoleUIInputCtrl(self, obj): + from pythonConsole import consoleUI + if not consoleUI: + return + return obj.windowHandle == consoleUI.inputCtrl.GetHandle() + + def isNvdaPythonConsoleUIOutputCtrl(self, obj): + from pythonConsole import consoleUI + if not consoleUI: + return + return obj.windowHandle == consoleUI.outputCtrl.GetHandle() + def chooseNVDAObjectOverlayClasses(self, obj, clsList): if obj.windowClassName == "#32770" and obj.role == controlTypes.ROLE_DIALOG: clsList.insert(0, NvdaDialog) if self.isNvdaSettingsDialog(obj): clsList.insert(0, NvdaDialogEmptyDescription) + return + if self.isNvdaPythonConsoleUIInputCtrl(obj): + clsList.insert(0, NvdaPythonConsoleUIOutputClear) + elif self.isNvdaPythonConsoleUIOutputCtrl(obj): + clsList.insert(0, NvdaPythonConsoleUIOutputClear) + clsList.insert(0, NvdaPythonConsoleUIOutputCtrl) diff --git a/source/gui/__init__.py b/source/gui/__init__.py index a8d8fc928a7..9aaab0b7a87 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -84,7 +84,12 @@ def prePopup(self): """ nvdaPid = os.getpid() focus = api.getFocusObject() - if focus.processID != nvdaPid: + # Do not set prevFocus if the focus is on a control rendered by NVDA itself, such as the NVDA menu. + # This allows to refer to the control that had focus before opening the menu while still using NVDA + # on its own controls. The L{nvdaPid} check can be bypassed by setting the optional attribute + # L{isPrevFocusOnNvdaPopup} to L{True} when a NVDA dialog offers customizable bound gestures, + # eg. the NVDA Python Console. + if focus.processID != nvdaPid or getattr(focus, "isPrevFocusOnNvdaPopup", False): self.prevFocus = focus self.prevFocusAncestors = api.getFocusAncestors() if winUser.getWindowThreadProcessID(winUser.getForegroundWindow())[0] != nvdaPid: diff --git a/source/pythonConsole.py b/source/pythonConsole.py index 33cc9384d93..4dc8a7590ec 100755 --- a/source/pythonConsole.py +++ b/source/pythonConsole.py @@ -1,8 +1,8 @@ -#pythonConsole.py -#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) 2008-2019 NV Access Limited, Leonard de Ruijter +# pythonConsole.py +# 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) 2008-2020 NV Access Limited, Leonard de Ruijter, Julien Cochuyt import watchdog @@ -12,6 +12,7 @@ import builtins import os +from typing import Sequence import code import codeop import sys @@ -253,6 +254,7 @@ def __init__(self, parent): # Even the most recent line has a position in the history, so initialise with one blank line. self.inputHistory = [""] self.inputHistoryPos = 0 + self.outputPositions: Sequence[int] = [0] def onActivate(self, evt): if evt.GetActive(): @@ -268,6 +270,12 @@ def output(self, data): if data and not data.isspace(): queueHandler.queueFunction(queueHandler.eventQueue, speech.speakText, data) + def clear(self): + """Clear the output. + """ + self.outputCtrl.Clear() + self.outputPositions[:] = [0] + def echo(self, data): self.outputCtrl.write(data) @@ -292,6 +300,8 @@ def execute(self): self.inputHistory.append("") self.inputHistoryPos = len(self.inputHistory) - 1 self.inputCtrl.ChangeValue("") + if self.console.prompt != "...": + self.outputPositions.append(self.outputCtrl.GetInsertionPoint()) def historyMove(self, movement): newIndex = self.inputHistoryPos + movement diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 0c9fe36fbce..f1fca8e0e10 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -8,6 +8,9 @@ What's New in NVDA == New Features == - Early support for UIA with Chromium based browsers (such as Edge). (#12025) - Optional experimental support for Microsoft Excel via UI Automation. Only recommended for Microsoft Excel build 16.0.13522.10000 or higher. (#12210) +- Easier navigation of output in NVDA Python Console. (#9784) + - alt+up/down jumps to the previous/next output result (add shift for selecting). + - control+l clears the output pane. == Changes == From f8c182680978e4e63f28cf381603557dceffed11 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Mon, 10 May 2021 00:50:22 +0200 Subject: [PATCH 167/174] Developer Guide: Minor fixes (#12384) * Fix broken link to 7-zip * Add a missing internal link to the "Packaging code as NVDA Add-ons" section * Remove extraneous or trailing whitespace --- devDocs/developerGuide.t2t | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/devDocs/developerGuide.t2t b/devDocs/developerGuide.t2t index 2291fd243d1..0ab3ebd8a8f 100644 --- a/devDocs/developerGuide.t2t +++ b/devDocs/developerGuide.t2t @@ -179,7 +179,7 @@ However, they do differ in some ways. Custom appModules and globalPlugins can be packaged into NVDA add-ons. This allows easy distribution, and provides a safe way for the user to install and uninstall the custom code. -Please refer to the Add-ons section later on in this document. +Please refer to the [Add-ons section #Addons] later on in this document. In order to test the code while developing, you can place it in a special 'scratchpad' directory in your NVDA user configuration directory. You will also need to configure NVDA to enable loading of custom code from the Developer Scratchpad Directory, by enabling this in the Advanced category of NVDA's Settings dialog. @@ -194,7 +194,7 @@ For example, an App Module for notepad would be called notepad.py, as notepad's For apps hosted inside host executables, see the section on app modules for hosted apps. App Module files must be placed in the appModules subdirectory of an add-on, or of the scratchpad directory of the NVDA user configuration directory. - + App Modules must define a class called AppModule, which inherits from appModuleHandler.AppModule. This class can then define event and script methods, gesture bindings and other code. This will all be covered in depth later. @@ -283,7 +283,7 @@ As wwahost app module provides necessary infrastructure for apps hosted inside, ++ Basics of a Global Plugin ++ Global Plugin files have a .py extension, and should have a short unique name which identifies what they do. -Global plugin files must be placed in the globalPlugins subdirectory of an add-on, or of the scratchpad directory of the NVDA user configuration directory. +Global plugin files must be placed in the globalPlugins subdirectory of an add-on, or of the scratchpad directory of the NVDA user configuration directory. Global Plugins must define a class called GlobalPlugin, which inherits from globalPluginHandler.GlobalPlugin. This class can then define event and script methods, gesture bindings and other code. @@ -692,17 +692,17 @@ class AppModule(appModuleHandler.AppModule): --- end --- ``` -+ Packaging Code as NVDA Add-ons + ++ Packaging Code as NVDA Add-ons +[Addons] To make it easy for users to share and install plugins and drivers, they can be packaged in to a single NVDA add-on package which the user can then install into a copy of NVDA via the Add-ons Manager found under Tools in the NVDA menu. Add-on packages are only supported in NVDA 2012.2 and later. -An add-on package is simply a standard zip archive with the file extension of nvda-addon which contains a manifest file, optional install/uninstall code and one or more directories containing plugins and/or drivers. +An add-on package is simply a standard zip archive with the file extension of nvda-addon which contains a manifest file, optional install/uninstall code and one or more directories containing plugins and/or drivers. ++ Non-ASCII File Names in Zip Archives ++ If your add-on includes files which contain non-ASCII (non-English) characters, you should create the zip archive such that it uses UTF-8 file names. This means that these files can be extracted properly on all systems, regardless of the system's configured language. Unfortunately, many zip archivers do not support this, including Windows Explorer. Generally, it has to be explicitly enabled even in archivers that do support it. -[http://www.7-zip.org/ 7-Zip] supports this, though it must be enabled by specifying the "cu=on" method parameter. +[7-Zip http://www.7-zip.org/] supports this, though it must be enabled by specifying the "cu=on" method parameter. ++ Manifest Files ++ Each add-on package must contain a manifest file named manifest.ini. @@ -764,9 +764,9 @@ The following plugins and drivers can be included in an add-on: - ++ Optional install / Uninstall code ++ -If you need to execute code as your add-on is being installed or uninstalled from NVDA (e.g. to validate license information or to copy files to a custom location), you can provide a Python file called installTasks.py in the archive which contains special functions that NVDA will call while installing or uninstalling your add-on. -This file should avoid loading any modules that are not absolutely necessary, especially Python C extensions or dlls from your own add-on, as this could cause later removal of the add-on to fail. -However, if this does happen, the add-on directory will be renamed and then deleted after the next restart of NVDA. +If you need to execute code as your add-on is being installed or uninstalled from NVDA (e.g. to validate license information or to copy files to a custom location), you can provide a Python file called installTasks.py in the archive which contains special functions that NVDA will call while installing or uninstalling your add-on. +This file should avoid loading any modules that are not absolutely necessary, especially Python C extensions or dlls from your own add-on, as this could cause later removal of the add-on to fail. +However, if this does happen, the add-on directory will be renamed and then deleted after the next restart of NVDA. Finally, it should not depend on the existence or state of other add-ons, as they may not be installed, have already been removed or not yet be initialized. +++ the onInstall function +++ @@ -792,7 +792,7 @@ All other fields will be ignored. +++ Locale-specific Messages +++ Each language directory can also contain gettext information, which is the system used to translate the rest of NVDA's user interface and reported messages. As with the rest of NVDA, an nvda.mo compiled gettext database file should be placed in the LC_MESSAGES directory within this directory. -to allow plugins in your add-on to access gettext message information via calls to _(), you must initialize translations at the top of each Python module by calling addonHandler.initTranslation(). +to allow plugins in your add-on to access gettext message information via calls to _(), you must initialize translations at the top of each Python module by calling addonHandler.initTranslation(). For more information about gettext and NVDA translation in general, please read https://github.com/nvaccess/nvda/wiki/Translating From ef53fb54d37a4e17ff5259751afa8f9f49c54a9e Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Mon, 10 May 2021 00:54:39 +0200 Subject: [PATCH 168/174] EditableText: Do not treat "\r\n" as two characters when `backspace` is pressed (#12379) In Notepad++, when hitting backspace to delete a line break, NVDA announces "blank" instead of "new line". The TextInfo of the Scintilla implementation may contain "\r\n" instead of "\n". The current implementation of EditableText._backspaceScriptHelper treats it as two characters and thus fails to trigger a proper announce. Description of how this pull request fixes the issue: Replace "\r\n" by "\n" before further treatment of the deleted chunk. --- source/editableText.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/editableText.py b/source/editableText.py index b425f8117c0..92f5c8278a0 100755 --- a/source/editableText.py +++ b/source/editableText.py @@ -1,7 +1,7 @@ # 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) 2006-2020 NV Access Limited, Davy Kager, Julien Cochuyt +# Copyright (C) 2006-2021 NV Access Limited, Davy Kager, Julien Cochuyt """Common support for editable text. @note: If you want editable text functionality for an NVDAObject, @@ -260,7 +260,8 @@ def _backspaceScriptHelper(self,unit,gesture): caretMoved,newInfo=self._hasCaretMoved(oldBookmark) if not caretMoved: return - if len(delChunk)>1: + delChunk = delChunk.replace("\r\n", "\n") # Occurs with at least with Scintilla + if len(delChunk) > 1: speech.speakMessage(delChunk) else: speech.speakSpelling(delChunk) From e2c206d0fcce6965d9d82d6d72babe0c501320c4 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Mon, 10 May 2021 01:37:58 +0200 Subject: [PATCH 169/174] Elements List dialog: Remove the accelerator key on the "Activate" button (#6167) (#12369) Fixes #6167 Summary of the issue: In the English locale, there is an accelerator key collision between the "Annotation" element type and the "Activate" button, both set to the letter "A". In the French locale, there is collision between the element type "Form field" ("Champs de formulaire") and the same button ("Activer"), both set to the letter "C". This changes the behavior of the accelerator key that focuses the radio button but does not activate it. This is not a bug at all, but is not ergonomically optimum, especially for less advanced users. Description of how this pull request fixes the issue: As suggested by @Qchristensen, remove the accelerator key setting from the "Activate" button as it is, when available, the default action of the dialog upon pressing the enter key. In most locale, this change should not raise the need for a new translation, as the "Activate" label already exists without an accelerator marker as "a message reported when the action at the position of the review cursor or navigator object is performed.". Just to be sure, I also added a warning in the translators comment for the button label to ask them to beware of the risk of collision. --- source/browseMode.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/source/browseMode.py b/source/browseMode.py index 0729e0aecf8..31cd021f35d 100644 --- a/source/browseMode.py +++ b/source/browseMode.py @@ -972,9 +972,10 @@ def __init__(self, document): contentsSizer.AddSpacer(gui.guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) bHelper = gui.guiHelper.ButtonHelper(wx.HORIZONTAL) - # Translators: The label of a button to activate an element - # in the browse mode Elements List dialog. - self.activateButton = bHelper.addButton(self, label=_("&Activate")) + # Translators: The label of a button to activate an element in the browse mode Elements List dialog. + # Beware not to set an accelerator that would collide with other controls in this dialog, such as an + # element type radio label. + self.activateButton = bHelper.addButton(self, label=_("Activate")) self.activateButton.Bind(wx.EVT_BUTTON, lambda evt: self.onAction(True)) # Translators: The label of a button to move to an element From 8eff942e5aee5eeb230f7c928bf1fa99e284df46 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Mon, 10 May 2021 09:40:24 +1000 Subject: [PATCH 170/174] Update what's new for pr #12369 --- user_docs/en/changes.t2t | 1 + 1 file changed, 1 insertion(+) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index f1fca8e0e10..21e82032ed4 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -30,6 +30,7 @@ What's New in NVDA - NVDA will exit even with windows still open, the exit process now closes all NVDA windows and dialogs (#1740) - The Speech Viewer can now be closed with `alt+F4` and has a standard close button for easier interaction with users of pointing devices. (#12330) - The Braille Viewer now has a standard close button for easier interaction with users of pointing devices. (#12328) +- In the Elements List dialog, the accelerator key on the "Activate" button has been removed in some locales to avoid collision with an element type radio button label. When available, the button is still the default of the dialog and as such can still be invoked by simply pressing enter from the elements list itself. (#6167) == Bug Fixes == From 509b7fa13387b137a558453711d18d4bded59531 Mon Sep 17 00:00:00 2001 From: buddsean Date: Fri, 7 May 2021 15:45:04 +1000 Subject: [PATCH 171/174] move SpeechWithoutPauses out of speech.py --- source/speech/speech.py | 155 ----------------------- source/speech/speechWithoutPauses.py | 180 +++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 155 deletions(-) create mode 100644 source/speech/speechWithoutPauses.py diff --git a/source/speech/speech.py b/source/speech/speech.py index d7a9b72943c..cb69cc8daf8 100755 --- a/source/speech/speech.py +++ b/source/speech/speech.py @@ -2448,161 +2448,6 @@ def getTableInfoSpeech( return textList -def _yieldIfNonEmpty(seq: SpeechSequence): - """Helper method to yield the sequence if it is not None or empty.""" - if seq: - yield seq - - -class SpeechWithoutPauses: - _pendingSpeechSequence: SpeechSequence - re_last_pause = re.compile( - r"^(.*(?<=[^\s.!?])[.!?][\"'”’)]?(?:\s+|$))(.*$)", - re.DOTALL | re.UNICODE - ) - - def __init__( - self, - speakFunc: Callable[[SpeechSequence], None] - ): - """ - :param speakFunc: Function used by L{speakWithoutPauses} to speak. This will likely be speech.speak. - """ - self.speak = speakFunc - self.reset() - - def reset(self): - self._pendingSpeechSequence = [] - - def speakWithoutPauses( - self, - speechSequence: Optional[SpeechSequence], - detectBreaks: bool = True - ) -> bool: - """ - Speaks the speech sequences given over multiple calls, - only sending to the synth at acceptable phrase or sentence boundaries, - or when given None for the speech sequence. - @return: C{True} if something was actually spoken, - C{False} if only buffering occurred. - """ - speech = GeneratorWithReturn(self.getSpeechWithoutPauses( - speechSequence, - detectBreaks - )) - for seq in speech: - self.speak(seq) - return speech.returnValue - - def getSpeechWithoutPauses( # noqa: C901 - self, - speechSequence: Optional[SpeechSequence], - detectBreaks: bool = True - ) -> Generator[SpeechSequence, None, bool]: - """ - Generate speech sequences over multiple calls, - only returning a speech sequence at acceptable phrase or sentence boundaries, - or when given None for the speech sequence. - @return: The speech sequence that can be spoken without pauses. The 'return' for this generator function, - is a bool which indicates whether this sequence should be considered valid speech. Use - L{GeneratorWithReturn} to retain the return value. A generator is used because the previous - implementation had several calls to speech, this approach replicates that. - """ - if speechSequence is not None: - logBadSequenceTypes(speechSequence) - # Break on all explicit break commands - if detectBreaks and speechSequence: - speech = GeneratorWithReturn(self._detectBreaksAndGetSpeech(speechSequence)) - yield from speech - return speech.returnValue # Don't fall through to flush / normal speech - - if speechSequence is None: # Requesting flush - pending = self._flushPendingSpeech() - yield from _yieldIfNonEmpty(pending) - return bool(pending) # Don't fall through to handle normal speech - - # Handling normal speech - speech = self._getSpeech(speechSequence) - yield from _yieldIfNonEmpty(speech) - return bool(speech) - - def _detectBreaksAndGetSpeech( - self, - speechSequence: SpeechSequence - ) -> Generator[SpeechSequence, None, bool]: - lastStartIndex = 0 - sequenceLen = len(speechSequence) - gotValidSpeech = False - for index, item in enumerate(speechSequence): - if isinstance(item, EndUtteranceCommand): - if index > 0 and lastStartIndex < index: - subSequence = speechSequence[lastStartIndex:index] - yield from _yieldIfNonEmpty( - self._getSpeech(subSequence) - ) - yield from _yieldIfNonEmpty( - self._flushPendingSpeech() - ) - gotValidSpeech = True - lastStartIndex = index + 1 - if lastStartIndex < sequenceLen: - subSequence = speechSequence[lastStartIndex:] - seq = self._getSpeech(subSequence) - gotValidSpeech = bool(seq) - yield from _yieldIfNonEmpty(seq) - return gotValidSpeech - - def _flushPendingSpeech(self) -> SpeechSequence: - """ - @return: may be empty sequence - """ - # Place the last incomplete phrase in to finalSpeechSequence to be spoken now - pending = self._pendingSpeechSequence - self._pendingSpeechSequence = [] - return pending - - def _getSpeech( - self, - speechSequence: SpeechSequence - ) -> SpeechSequence: - """ - @return: May be an empty sequence - """ - finalSpeechSequence: SpeechSequence = [] # To be spoken now - pendingSpeechSequence: speechSequence = [] # To be saved off for speaking later - # Scan the given speech and place all completed phrases in finalSpeechSequence to be spoken, - # And place the final incomplete phrase in pendingSpeechSequence - for index in range(len(speechSequence) - 1, -1, -1): - item = speechSequence[index] - if isinstance(item, str): - m = self.re_last_pause.match(item) - if m: - before, after = m.groups() - if after: - pendingSpeechSequence.append(after) - if before: - finalSpeechSequence.extend(self._flushPendingSpeech()) - finalSpeechSequence.extend(speechSequence[0:index]) - finalSpeechSequence.append(before) - # Apply the last language change to the pending sequence. - # This will need to be done for any other speech change commands introduced in future. - for changeIndex in range(index - 1, -1, -1): - change = speechSequence[changeIndex] - if not isinstance(change, LangChangeCommand): - continue - pendingSpeechSequence.append(change) - break - break - else: - pendingSpeechSequence.append(item) - else: - pendingSpeechSequence.append(item) - if pendingSpeechSequence: - pendingSpeechSequence.reverse() - self._pendingSpeechSequence.extend(pendingSpeechSequence) - return finalSpeechSequence - - #: The singleton _SpeechManager instance used for speech functions. #: @type: L{manager.SpeechManager} _manager = manager.SpeechManager() diff --git a/source/speech/speechWithoutPauses.py b/source/speech/speechWithoutPauses.py new file mode 100644 index 00000000000..56e95676b59 --- /dev/null +++ b/source/speech/speechWithoutPauses.py @@ -0,0 +1,180 @@ +# 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) 2006-2021 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Babbage B.V., Bill Dengler, +# Julien Cochuyt + +import re + +from .commands import ( + # Commands that are used in this file. + LangChangeCommand, + EndUtteranceCommand, +) + +from .types import ( + SpeechSequence, + logBadSequenceTypes, + GeneratorWithReturn, +) + +from typing import ( + Optional, + Generator, + Callable, +) + + +def _yieldIfNonEmpty(seq: SpeechSequence): + """Helper method to yield the sequence if it is not None or empty.""" + if seq: + yield seq + + +class SpeechWithoutPauses: + _pendingSpeechSequence: SpeechSequence + re_last_pause = re.compile( + r"^(.*(?<=[^\s.!?])[.!?][\"'”’)]?(?:\s+|$))(.*$)", + re.DOTALL | re.UNICODE + ) + + def __init__( + self, + speakFunc: Callable[[SpeechSequence], None] + ): + """ + :param speakFunc: Function used by L{speakWithoutPauses} to speak. This will likely be speech.speak. + """ + self.speak = speakFunc + self.reset() + + def reset(self): + self._pendingSpeechSequence = [] + + def speakWithoutPauses( + self, + speechSequence: Optional[SpeechSequence], + detectBreaks: bool = True + ) -> bool: + """ + Speaks the speech sequences given over multiple calls, + only sending to the synth at acceptable phrase or sentence boundaries, + or when given None for the speech sequence. + @return: C{True} if something was actually spoken, + C{False} if only buffering occurred. + """ + speech = GeneratorWithReturn(self.getSpeechWithoutPauses( + speechSequence, + detectBreaks + )) + for seq in speech: + self.speak(seq) + return speech.returnValue + + def getSpeechWithoutPauses( # noqa: C901 + self, + speechSequence: Optional[SpeechSequence], + detectBreaks: bool = True + ) -> Generator[SpeechSequence, None, bool]: + """ + Generate speech sequences over multiple calls, + only returning a speech sequence at acceptable phrase or sentence boundaries, + or when given None for the speech sequence. + @return: The speech sequence that can be spoken without pauses. The 'return' for this generator function, + is a bool which indicates whether this sequence should be considered valid speech. Use + L{GeneratorWithReturn} to retain the return value. A generator is used because the previous + implementation had several calls to speech, this approach replicates that. + """ + if speechSequence is not None: + logBadSequenceTypes(speechSequence) + # Break on all explicit break commands + if detectBreaks and speechSequence: + speech = GeneratorWithReturn(self._detectBreaksAndGetSpeech(speechSequence)) + yield from speech + return speech.returnValue # Don't fall through to flush / normal speech + + if speechSequence is None: # Requesting flush + pending = self._flushPendingSpeech() + yield from _yieldIfNonEmpty(pending) + return bool(pending) # Don't fall through to handle normal speech + + # Handling normal speech + speech = self._getSpeech(speechSequence) + yield from _yieldIfNonEmpty(speech) + return bool(speech) + + def _detectBreaksAndGetSpeech( + self, + speechSequence: SpeechSequence + ) -> Generator[SpeechSequence, None, bool]: + lastStartIndex = 0 + sequenceLen = len(speechSequence) + gotValidSpeech = False + for index, item in enumerate(speechSequence): + if isinstance(item, EndUtteranceCommand): + if index > 0 and lastStartIndex < index: + subSequence = speechSequence[lastStartIndex:index] + yield from _yieldIfNonEmpty( + self._getSpeech(subSequence) + ) + yield from _yieldIfNonEmpty( + self._flushPendingSpeech() + ) + gotValidSpeech = True + lastStartIndex = index + 1 + if lastStartIndex < sequenceLen: + subSequence = speechSequence[lastStartIndex:] + seq = self._getSpeech(subSequence) + gotValidSpeech = bool(seq) + yield from _yieldIfNonEmpty(seq) + return gotValidSpeech + + def _flushPendingSpeech(self) -> SpeechSequence: + """ + @return: may be empty sequence + """ + # Place the last incomplete phrase in to finalSpeechSequence to be spoken now + pending = self._pendingSpeechSequence + self._pendingSpeechSequence = [] + return pending + + def _getSpeech( + self, + speechSequence: SpeechSequence + ) -> SpeechSequence: + """ + @return: May be an empty sequence + """ + finalSpeechSequence: SpeechSequence = [] # To be spoken now + pendingSpeechSequence: speechSequence = [] # To be saved off for speaking later + # Scan the given speech and place all completed phrases in finalSpeechSequence to be spoken, + # And place the final incomplete phrase in pendingSpeechSequence + for index in range(len(speechSequence) - 1, -1, -1): + item = speechSequence[index] + if isinstance(item, str): + m = self.re_last_pause.match(item) + if m: + before, after = m.groups() + if after: + pendingSpeechSequence.append(after) + if before: + finalSpeechSequence.extend(self._flushPendingSpeech()) + finalSpeechSequence.extend(speechSequence[0:index]) + finalSpeechSequence.append(before) + # Apply the last language change to the pending sequence. + # This will need to be done for any other speech change commands introduced in future. + for changeIndex in range(index - 1, -1, -1): + change = speechSequence[changeIndex] + if not isinstance(change, LangChangeCommand): + continue + pendingSpeechSequence.append(change) + break + break + else: + pendingSpeechSequence.append(item) + else: + pendingSpeechSequence.append(item) + if pendingSpeechSequence: + pendingSpeechSequence.reverse() + self._pendingSpeechSequence.extend(pendingSpeechSequence) + return finalSpeechSequence From a2a6e23f747fa9ceb641d2a12585817bb12a7d11 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Mon, 10 May 2021 11:03:04 +1000 Subject: [PATCH 172/174] Only run AppVeyor linting check on branches which aren't master (#12378) Master builds are currently failing at the linting step such as https://ci.appveyor.com/project/NVAccess/nvda/builds/39046507 https://ci.appveyor.com/project/NVAccess/nvda/builds/39045879 Lint checking doesn't need to occur for master builds Description of how this pull request fixes the issue: Don't run the appveyor lint checking on master branches --- appveyor.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 4065e1b4f04..a66ec823620 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -176,6 +176,7 @@ test_script: # Flake8 Linting - ps: | + if ($env:APPVEYOR_PULL_REQUEST_NUMBER -or $env:APPVEYOR_REPO_BRANCH.StartsWith("try-")) { $lintOutput = (Resolve-Path .\testOutput\lint\) $lintSource = (Resolve-Path .\tests\lint\) $flake8Output = "$lintOutput\Flake8.txt" @@ -205,6 +206,7 @@ test_script: $wc = New-Object 'System.Net.WebClient' $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", $junitXML) if($errorCode -ne 0) { $host.SetShouldExit($errorCode) } + } # System tests - ps: | From b95743da959148924183effa98ffcc7f6e5f0349 Mon Sep 17 00:00:00 2001 From: buddsean Date: Mon, 12 Apr 2021 11:09:00 +1000 Subject: [PATCH 173/174] refactor of sayAllHandler into speech. (#12251) SpeechWithoutPauses is only used by sayAllHandler, but the code lies in speech\__init__.py. Due to code changes sayAllHandler needs to instantiate a SpeechWithoutPauses instance, and would either introduce a circular dependency or require a singleton to be created when the instance is needed. Description of how this pull request fixes the issue: - sayAllHandler is moved to a new module - speech.sayAll. - SpeechWithoutPauses is moved to it's own module - speech\__init__.py has been moved to speech\speech.py so that speech.sayAll can import the necessary functions from speech. - sayAllHandler top level functions have been refactor into a class. An instance of SayAllHandler is initialised when NVDA is started in a consistent manner with other initialisations in the code base. --- source/NVDAHelper.py | 4 +- source/NVDAObjects/IAccessible/winword.py | 8 +- source/NVDAObjects/window/winword.py | 3 +- source/appModules/eclipse.py | 4 +- source/appModules/kindle.py | 6 +- source/appModules/powerpnt.py | 4 +- source/browseMode.py | 14 +- source/core.py | 3 + source/cursorManager.py | 22 +-- source/editableText.py | 12 +- source/globalCommands.py | 12 +- source/inputCore.py | 6 +- source/scriptHandler.py | 14 +- source/speech/__init__.py | 179 ++++++++++++++++++++++ source/speech/sayAll.py | 142 +++++++++-------- source/speech/speech.py | 21 +-- source/virtualBuffers/__init__.py | 1 - tests/unit/test_SpeechWithoutPauses.py | 6 +- tests/unit/test_scriptHandler.py | 6 +- tests/unit/test_speechManager/__init__.py | 26 ++-- user_docs/en/changes.t2t | 11 +- 21 files changed, 352 insertions(+), 152 deletions(-) create mode 100644 source/speech/__init__.py mode change 100755 => 100644 source/speech/speech.py diff --git a/source/NVDAHelper.py b/source/NVDAHelper.py index 6e088342a74..333ed123ca6 100755 --- a/source/NVDAHelper.py +++ b/source/NVDAHelper.py @@ -386,9 +386,9 @@ def nvdaControllerInternal_inputLangChangeNotify(threadID,hkl,layoutString): #But threadIDs for console windows are always wrong so don't ignore for those. if not isinstance(focus,NVDAObjects.window.Window) or (threadID!=focus.windowThreadID and focus.windowClassName!="ConsoleWindowClass"): return 0 - import sayAllHandler + from speech import sayAll #Never announce changes while in sayAll (#1676) - if sayAllHandler.isRunning(): + if sayAll.SayAllHandler.isRunning(): return 0 import queueHandler import ui diff --git a/source/NVDAObjects/IAccessible/winword.py b/source/NVDAObjects/IAccessible/winword.py index acc273a6377..0f9c5618605 100644 --- a/source/NVDAObjects/IAccessible/winword.py +++ b/source/NVDAObjects/IAccessible/winword.py @@ -21,6 +21,9 @@ from NVDAObjects.window import DisplayModelEditableText from ..behaviors import EditableTextWithoutAutoSelectDetection from NVDAObjects.window.winword import * +from NVDAObjects.window.winword import WordDocumentTreeInterceptor +from speech import sayAll + class WordDocument(IAccessible,EditableTextWithoutAutoSelectDetection,WordDocument): @@ -362,7 +365,7 @@ def script_nextParagraph(self,gesture): info._rangeObj.move(wdParagraph,1) info.updateCaret() self._caretScriptPostMovedHelper(textInfos.UNIT_PARAGRAPH,gesture,None) - script_nextParagraph.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_nextParagraph.resumeSayAllMode = sayAll.CURSOR.CARET def script_previousParagraph(self,gesture): info=self.makeTextInfo(textInfos.POSITION_CARET) @@ -370,7 +373,7 @@ def script_previousParagraph(self,gesture): info._rangeObj.move(wdParagraph,-1) info.updateCaret() self._caretScriptPostMovedHelper(textInfos.UNIT_PARAGRAPH,gesture,None) - script_previousParagraph.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_previousParagraph.resumeSayAllMode = sayAll.CURSOR.CARET def focusOnActiveDocument(self, officeChartObject): rangeStart=officeChartObject.Parent.Range.Start @@ -462,4 +465,3 @@ def event_gainFocus(self): ctypes.windll.user32.AttachThreadInput(curThreadID,document.windowThreadID,False) if not document.WinwordWindowObject.active: document.WinwordWindowObject.activate() - diff --git a/source/NVDAObjects/window/winword.py b/source/NVDAObjects/window/winword.py index 2d83596adba..2af68acbb49 100755 --- a/source/NVDAObjects/window/winword.py +++ b/source/NVDAObjects/window/winword.py @@ -14,7 +14,6 @@ import locale import collections import colorsys -import sayAllHandler import eventHandler import braille from scriptHandler import script @@ -946,7 +945,7 @@ def collapse(self,end=False): newEndOffset = self._rangeObj.end # the new endOffset should not have become smaller than the old endOffset, this could cause an infinite loop in # a case where you called move end then collapse until the size of the range is no longer being reduced. - # For an example of this see sayAll (specifically readTextHelper_generator in sayAllHandler.py) + # For an example of this see sayAll (specifically readTextHelper_generator in sayAll.py) if newEndOffset < oldEndOffset : raise RuntimeError diff --git a/source/appModules/eclipse.py b/source/appModules/eclipse.py index fd1813b5e45..176a40f0ee2 100644 --- a/source/appModules/eclipse.py +++ b/source/appModules/eclipse.py @@ -12,7 +12,7 @@ import braille import ui import api -import sayAllHandler +from speech import sayAll import eventHandler import keyboardHandler from scriptHandler import script @@ -98,7 +98,7 @@ def script_readDocumentation(self, gesture): if documentObj.role == controlTypes.ROLE_DOCUMENT: api.setNavigatorObject(documentObj) braille.handler.handleReviewMove() - sayAllHandler.readText(sayAllHandler.CURSOR_REVIEW) + sayAll.SayAllHandler.readText(sayAll.CURSOR.REVIEW) elif documentObj.role == controlTypes.ROLE_EDITABLETEXT: ui.message(documentObj.value) diff --git a/source/appModules/kindle.py b/source/appModules/kindle.py index 4a282019582..65a29d4a1b9 100644 --- a/source/appModules/kindle.py +++ b/source/appModules/kindle.py @@ -8,7 +8,7 @@ from comtypes.hresult import S_OK import appModuleHandler import speech -import sayAllHandler +from speech import sayAll import api from scriptHandler import willSayAllResume, isScriptWaiting import controlTypes @@ -106,11 +106,11 @@ def _changePageScriptHelper(self,gesture,previous=False): def script_moveByPage_forward(self,gesture): self._changePageScriptHelper(gesture) - script_moveByPage_forward.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_moveByPage_forward.resumeSayAllMode = sayAll.CURSOR.CARET def script_moveByPage_back(self,gesture): self._changePageScriptHelper(gesture,previous=True) - script_moveByPage_back.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_moveByPage_back.resumeSayAllMode = sayAll.CURSOR.CARET def _tabOverride(self,direction): return False diff --git a/source/appModules/powerpnt.py b/source/appModules/powerpnt.py index d05bc596c8f..82b33f1d5ee 100644 --- a/source/appModules/powerpnt.py +++ b/source/appModules/powerpnt.py @@ -15,7 +15,7 @@ import colors import api import speech -import sayAllHandler +from speech import sayAll import NVDAHelper import winUser import msoAutoShapeTypes @@ -1097,7 +1097,7 @@ def makeTextInfo(self,position): def reportNewSlide(self): self.makeTextInfo(textInfos.POSITION_FIRST).updateCaret() - sayAllHandler.readText(sayAllHandler.CURSOR_CARET) + sayAll.SayAllHandler.readText(sayAll.CURSOR.CARET) def script_toggleNotesMode(self,gesture): self.rootNVDAObject.notesMode=not self.rootNVDAObject.notesMode diff --git a/source/browseMode.py b/source/browseMode.py index 0729e0aecf8..f394c0dfbdb 100644 --- a/source/browseMode.py +++ b/source/browseMode.py @@ -33,7 +33,7 @@ import braille import vision import speech -import sayAllHandler +from speech import sayAll import treeInterceptorHandler import inputCore import api @@ -470,7 +470,7 @@ def addQuickNav( script = lambda self,gesture: self._quickNavScript(gesture, itemType, "next", nextError, readUnit) script.__doc__ = nextDoc script.__name__ = funcName - script.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script.resumeSayAllMode = sayAll.CURSOR.CARET setattr(cls, funcName, script) if key is not None: cls.__gestures["kb:%s" % key] = scriptName @@ -479,7 +479,7 @@ def addQuickNav( script = lambda self,gesture: self._quickNavScript(gesture, itemType, "previous", prevError, readUnit) script.__doc__ = prevDoc script.__name__ = funcName - script.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script.resumeSayAllMode = sayAll.CURSOR.CARET setattr(cls, funcName, script) if key is not None: cls.__gestures["kb:shift+%s" % key] = scriptName @@ -1287,7 +1287,7 @@ def event_treeInterceptor_gainFocus(self): if not self.passThrough: if doSayAll: speech.speakObjectProperties(self.rootNVDAObject, name=True, states=True, reason=OutputReason.FOCUS) - sayAllHandler.readText(sayAllHandler.CURSOR_CARET) + sayAll.SayAllHandler.readText(sayAll.CURSOR.CARET) else: # Speak it like we would speak focus on any other document object. # This includes when entering the treeInterceptor for the first time: @@ -1590,7 +1590,7 @@ def event_gainFocus(self, obj, nextHandler): # The virtual caret is not within the focus node. oldPassThrough=self.passThrough passThrough = self.shouldPassThrough(obj, reason=OutputReason.FOCUS) - if not oldPassThrough and (passThrough or sayAllHandler.isRunning()): + if not oldPassThrough and (passThrough or sayAll.SayAllHandler.isRunning()): # If pass-through is disabled, cancel speech, as a focus change should cause page reading to stop. # This must be done before auto-pass-through occurs, as we want to stop page reading even if pass-through will be automatically enabled by this focus change. speech.cancelSpeech() @@ -1821,7 +1821,7 @@ def script_moveToStartOfContainer(self,gesture): if not willSayAllResume(gesture): container.expand(textInfos.UNIT_LINE) speech.speakTextInfo(container, reason=OutputReason.FOCUS) - script_moveToStartOfContainer.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_moveToStartOfContainer.resumeSayAllMode = sayAll.CURSOR.CARET # Translators: Description for the Move to start of container command in browse mode. script_moveToStartOfContainer.__doc__=_("Moves to the start of the container element, such as a list or table") @@ -1846,7 +1846,7 @@ def script_movePastEndOfContainer(self,gesture): if not willSayAllResume(gesture): container.expand(textInfos.UNIT_LINE) speech.speakTextInfo(container, reason=OutputReason.FOCUS) - script_movePastEndOfContainer.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_movePastEndOfContainer.resumeSayAllMode = sayAll.CURSOR.CARET # Translators: Description for the Move past end of container command in browse mode. script_movePastEndOfContainer.__doc__=_("Moves past the end of the container element, such as a list or table") diff --git a/source/core.py b/source/core.py index 3304d197ae6..3f5de929186 100644 --- a/source/core.py +++ b/source/core.py @@ -351,6 +351,9 @@ def main(): import speech log.debug("Initializing speech") speech.initialize() + from speech import sayAll + log.debug("Initializing sayAllHandler") + sayAll.initialize() if not globalVars.appArgs.minimal and (time.time()-globalVars.startTime)>5: log.debugWarning("Slow starting core (%.2f sec)" % (time.time()-globalVars.startTime)) # Translators: This is spoken when NVDA is starting. diff --git a/source/cursorManager.py b/source/cursorManager.py index a297b649de2..63891ba9d21 100644 --- a/source/cursorManager.py +++ b/source/cursorManager.py @@ -16,7 +16,7 @@ import gui from gui import guiHelper import gui.contextHelp -import sayAllHandler +from speech import sayAll import review from scriptHandler import willSayAllResume, script import textInfos @@ -197,7 +197,7 @@ def run(): "find the next occurrence of the previously entered text string from the current cursor's position" ), gesture="kb:NVDA+f3", - resumeSayAllMode=sayAllHandler.CURSOR_CARET, + resumeSayAllMode=sayAll.CURSOR.CARET, ) def script_findNext(self,gesture): if not self._lastFindText: @@ -215,7 +215,7 @@ def script_findNext(self,gesture): "find the previous occurrence of the previously entered text string from the current cursor's position" ), gesture="kb:NVDA+shift+f3", - resumeSayAllMode=sayAllHandler.CURSOR_CARET, + resumeSayAllMode=sayAll.CURSOR.CARET, ) def script_findPrevious(self,gesture): if not self._lastFindText: @@ -230,11 +230,11 @@ def script_findPrevious(self,gesture): def script_moveByPage_back(self,gesture): self._caretMovementScriptHelper(gesture,textInfos.UNIT_LINE,-config.conf["virtualBuffers"]["linesPerPage"],extraDetail=False) - script_moveByPage_back.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_moveByPage_back.resumeSayAllMode = sayAll.CURSOR.CARET def script_moveByPage_forward(self,gesture): self._caretMovementScriptHelper(gesture,textInfos.UNIT_LINE,config.conf["virtualBuffers"]["linesPerPage"],extraDetail=False) - script_moveByPage_forward.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_moveByPage_forward.resumeSayAllMode = sayAll.CURSOR.CARET def script_moveByCharacter_back(self,gesture): self._caretMovementScriptHelper(gesture,textInfos.UNIT_CHARACTER,-1,extraDetail=True,handleSymbols=True) @@ -250,27 +250,27 @@ def script_moveByWord_forward(self,gesture): def script_moveByLine_back(self,gesture): self._caretMovementScriptHelper(gesture,textInfos.UNIT_LINE,-1) - script_moveByLine_back.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_moveByLine_back.resumeSayAllMode = sayAll.CURSOR.CARET def script_moveByLine_forward(self,gesture): self._caretMovementScriptHelper(gesture,textInfos.UNIT_LINE,1) - script_moveByLine_forward.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_moveByLine_forward.resumeSayAllMode = sayAll.CURSOR.CARET def script_moveBySentence_back(self,gesture): self._caretMovementScriptHelper(gesture,textInfos.UNIT_SENTENCE,-1) - script_moveBySentence_back.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_moveBySentence_back.resumeSayAllMode = sayAll.CURSOR.CARET def script_moveBySentence_forward(self,gesture): self._caretMovementScriptHelper(gesture,textInfos.UNIT_SENTENCE,1) - script_moveBySentence_forward.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_moveBySentence_forward.resumeSayAllMode = sayAll.CURSOR.CARET def script_moveByParagraph_back(self,gesture): self._caretMovementScriptHelper(gesture,textInfos.UNIT_PARAGRAPH,-1) - script_moveByParagraph_back.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_moveByParagraph_back.resumeSayAllMode = sayAll.CURSOR.CARET def script_moveByParagraph_forward(self,gesture): self._caretMovementScriptHelper(gesture,textInfos.UNIT_PARAGRAPH,1) - script_moveByParagraph_forward.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_moveByParagraph_forward.resumeSayAllMode = sayAll.CURSOR.CARET def script_startOfLine(self,gesture): self._caretMovementScriptHelper(gesture,textInfos.UNIT_CHARACTER,posUnit=textInfos.UNIT_LINE,extraDetail=True,handleSymbols=True) diff --git a/source/editableText.py b/source/editableText.py index b425f8117c0..f5af01a5c25 100755 --- a/source/editableText.py +++ b/source/editableText.py @@ -9,7 +9,7 @@ """ import time -import sayAllHandler +from speech import sayAll import api import review from baseObject import ScriptableObject @@ -222,7 +222,7 @@ def _caretMoveBySentenceHelper(self, gesture, direction): def script_caret_moveByLine(self,gesture): self._caretMovementScriptHelper(gesture, textInfos.UNIT_LINE) - script_caret_moveByLine.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_caret_moveByLine.resumeSayAllMode = sayAll.CURSOR.CARET def script_caret_moveByCharacter(self,gesture): self._caretMovementScriptHelper(gesture, textInfos.UNIT_CHARACTER) @@ -232,15 +232,15 @@ def script_caret_moveByWord(self,gesture): def script_caret_moveByParagraph(self,gesture): self._caretMovementScriptHelper(gesture, textInfos.UNIT_PARAGRAPH) - script_caret_moveByParagraph.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_caret_moveByParagraph.resumeSayAllMode = sayAll.CURSOR.CARET def script_caret_previousSentence(self,gesture): self._caretMoveBySentenceHelper(gesture, -1) - script_caret_previousSentence.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_caret_previousSentence.resumeSayAllMode = sayAll.CURSOR.CARET def script_caret_nextSentence(self,gesture): self._caretMoveBySentenceHelper(gesture, 1) - script_caret_nextSentence.resumeSayAllMode=sayAllHandler.CURSOR_CARET + script_caret_nextSentence.resumeSayAllMode = sayAll.CURSOR.CARET def _backspaceScriptHelper(self,unit,gesture): try: @@ -394,7 +394,7 @@ def script_caret_changeSelection(self,gesture): try: self.reportSelectionChange(oldInfo) except: - return + return __changeSelectionGestures = ( "kb:shift+upArrow", diff --git a/source/globalCommands.py b/source/globalCommands.py index c85490c8a55..1f6bef8eaa5 100644 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -19,7 +19,7 @@ import api import textInfos import speech -import sayAllHandler +from speech import sayAll from NVDAObjects import NVDAObject, NVDAObjectTextInfo import globalVars from logHandler import log @@ -1253,7 +1253,7 @@ def script_review_top(self,gesture): @script( # Translators: Input help mode message for move review cursor to previous line command. description=_("Moves the review cursor to the previous line of the current navigator object and speaks it"), - resumeSayAllMode=sayAllHandler.CURSOR_REVIEW, + resumeSayAllMode=sayAll.CURSOR.REVIEW, category=SCRCAT_TEXTREVIEW, gestures=("kb:numpad7", "kb(laptop):NVDA+upArrow", "ts(text):flickUp") ) @@ -1294,7 +1294,7 @@ def script_review_currentLine(self,gesture): @script( # Translators: Input help mode message for move review cursor to next line command. description=_("Moves the review cursor to the next line of the current navigator object and speaks it"), - resumeSayAllMode=sayAllHandler.CURSOR_REVIEW, + resumeSayAllMode=sayAll.CURSOR.REVIEW, category=SCRCAT_TEXTREVIEW, gestures=("kb:numpad9", "kb(laptop):NVDA+downArrow", "ts(text):flickDown") ) @@ -1675,7 +1675,7 @@ def script_showGui(self,gesture): gestures=("kb:numpadPlus", "kb(laptop):NVDA+shift+a", "ts(text):3finger_flickDown") ) def script_review_sayAll(self,gesture): - sayAllHandler.readText(sayAllHandler.CURSOR_REVIEW) + sayAll.SayAllHandler.readText(sayAll.CURSOR.REVIEW) @script( # Translators: Input help mode message for say all with system caret command. @@ -1684,7 +1684,7 @@ def script_review_sayAll(self,gesture): gestures=("kb(desktop):NVDA+downArrow", "kb(laptop):NVDA+a") ) def script_sayAll(self,gesture): - sayAllHandler.readText(sayAllHandler.CURSOR_CARET) + sayAll.SayAllHandler.readText(sayAll.CURSOR.CARET) def _reportFormattingHelper(self, info, browseable=False): # Report all formatting-related changes regardless of user settings @@ -1991,7 +1991,7 @@ def script_title(self,gesture): def script_speakForeground(self,gesture): obj=api.getForegroundObject() if obj: - sayAllHandler.readObjects(obj) + sayAll.SayAllHandler.readObjects(obj) @script( gesture="kb(desktop):NVDA+control+f2" diff --git a/source/inputCore.py b/source/inputCore.py index 948b00d5bba..fdc71a4235d 100644 --- a/source/inputCore.py +++ b/source/inputCore.py @@ -18,7 +18,7 @@ from typing import Dict, Any, Tuple, List, Union import configobj -import sayAllHandler +from speech import sayAll import baseObject import scriptHandler import queueHandler @@ -455,12 +455,12 @@ def executeGesture(self, gesture): wasInSayAll=False if gesture.isModifier: if not self.lastModifierWasInSayAll: - wasInSayAll=self.lastModifierWasInSayAll=sayAllHandler.isRunning() + wasInSayAll = self.lastModifierWasInSayAll = sayAll.SayAllHandler.isRunning() elif self.lastModifierWasInSayAll: wasInSayAll=True self.lastModifierWasInSayAll=False else: - wasInSayAll=sayAllHandler.isRunning() + wasInSayAll = sayAll.SayAllHandler.isRunning() if wasInSayAll: gesture.wasInSayAll=True diff --git a/source/scriptHandler.py b/source/scriptHandler.py index cebbf17464b..dc88adb574a 100644 --- a/source/scriptHandler.py +++ b/source/scriptHandler.py @@ -10,7 +10,7 @@ import types import config import speech -import sayAllHandler +from speech import sayAll import appModuleHandler import api import queueHandler @@ -175,7 +175,11 @@ def queueScript(script,gesture): queueHandler.queueFunction(queueHandler.eventQueue,_queueScriptCallback,script,gesture) def willSayAllResume(gesture): - return config.conf['keyboard']['allowSkimReadingInSayAll']and gesture.wasInSayAll and getattr(gesture.script,'resumeSayAllMode',None)==sayAllHandler.lastSayAllMode + return ( + config.conf['keyboard']['allowSkimReadingInSayAll'] + and gesture.wasInSayAll + and getattr(gesture.script, 'resumeSayAllMode', None) == sayAll.SayAllHandler.lastSayAllMode + ) def executeScript(script,gesture): """Executes a given script (function) passing it the given gesture. @@ -195,7 +199,7 @@ def executeScript(script,gesture): _isScriptRunning=True resumeSayAllMode=None if willSayAllResume(gesture): - resumeSayAllMode=sayAllHandler.lastSayAllMode + resumeSayAllMode = sayAll.SayAllHandler.lastSayAllMode try: scriptTime=time.time() scriptRef=weakref.ref(scriptFunc) @@ -211,7 +215,7 @@ def executeScript(script,gesture): finally: _isScriptRunning=False if resumeSayAllMode is not None: - sayAllHandler.readText(resumeSayAllMode) + sayAll.SayAllHandler.readText(resumeSayAllMode) def getLastScriptRepeatCount(): """The count of how many times the most recent script has been executed. @@ -261,7 +265,7 @@ def script( @param bypassInputHelp: Whether this script should run when input help is active. @param allowInSleepMode: Whether this script should run when NVDA is in sleep mode. @param resumeSayAllMode: The say all mode that should be resumed when active before executing this script. - One of the C{sayAllHandler.CURSOR_*} constants. + One of the C{sayAll.CURSOR_*} constants. """ if gestures is None: gestures = [] diff --git a/source/speech/__init__.py b/source/speech/__init__.py new file mode 100644 index 00000000000..5d5944ca956 --- /dev/null +++ b/source/speech/__init__.py @@ -0,0 +1,179 @@ +# 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) 2006-2020 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Babbage B.V., Bill Dengler, +# Julien Cochuyt + +from .speech import ( + _extendSpeechSequence_addMathForTextInfo, + _getSpellingSpeechAddCharMode, + _getSpellingCharAddCapNotification, + _getSpellingSpeechWithoutCharMode, + _getPlaceholderSpeechIfTextEmpty, + _getSelectionMessageSpeech, + _getSpeakMessageSpeech, + _manager, + _objectSpeech_calculateAllowedProps, + _suppressSpeakTypedCharacters, + _suppressSpeakTypedCharactersNumber, + _suppressSpeakTypedCharactersTime, + beenCanceled, + BLANK_CHUNK_CHARS, + cancelSpeech, + CHUNK_SEPARATOR, + clearTypedWordBuffer, + curWordChars, + FIRST_NONCONTROL_CHAR, + getCharDescListFromText, + getControlFieldSpeech, + getCurrentLanguage, + getFormatFieldSpeech, + getIndentationSpeech, + getObjectPropertiesSpeech, + getObjectSpeech, + getPreselectedTextSpeech, + getPropertiesSpeech, + getSpellingSpeech, + getTableInfoSpeech, + getTextInfoSpeech, + IDT_BASE_FREQUENCY, + IDT_MAX_SPACES, + IDT_TONE_DURATION, + isBlank, + isPaused, + LANGS_WITH_CONJUNCT_CHARS, + oldColumnNumber, + oldColumnSpan, + oldRowNumber, + oldRowSpan, + oldTableID, + oldTreeLevel, + pauseSpeech, + processText, + PROTECTED_CHAR, + RE_CONVERT_WHITESPACE, + RE_INDENTATION_CONVERT, + RE_INDENTATION_SPLIT, + speak, + speakMessage, + speakObject, + speakObjectProperties, + speakPreselectedText, + speakSelectionChange, + speakSelectionMessage, + speakSpelling, + speakText, + speakTextInfo, + speakTextSelected, + speakTypedCharacters, + speechMode, + speechMode_beeps, + speechMode_beeps_ms, + speechMode_off, + speechMode_talk, + spellTextInfo, + splitTextIndentation, +) + +from .priorities import Spri + +from .types import ( + SpeechSequence, + SequenceItemT, + logBadSequenceTypes, + GeneratorWithReturn, + _flattenNestedSequences +) + +__all__ = [ + # from .priorities + "Spri", + # from .types + "SpeechSequence", + "SequenceItemT", + "logBadSequenceTypes", + "GeneratorWithReturn", + "_flattenNestedSequences", + # from .speech + "_getSpellingSpeechAddCharMode", + "_getSpellingCharAddCapNotification", + "_getSpellingSpeechWithoutCharMode", + "_extendSpeechSequence_addMathForTextInfo", + "_getPlaceholderSpeechIfTextEmpty", + "_getSelectionMessageSpeech", + "_getSpeakMessageSpeech", + "_manager", + "_objectSpeech_calculateAllowedProps", + "_suppressSpeakTypedCharacters", + "_suppressSpeakTypedCharactersNumber", + "_suppressSpeakTypedCharactersTime", + "beenCanceled", + "BLANK_CHUNK_CHARS", + "cancelSpeech", + "CHUNK_SEPARATOR", + "clearTypedWordBuffer", + "curWordChars", + "FIRST_NONCONTROL_CHAR", + "getCharDescListFromText", + "getControlFieldSpeech", + "getCurrentLanguage", + "getFormatFieldSpeech", + "getIndentationSpeech", + "getObjectPropertiesSpeech", + "getObjectSpeech", + "getPreselectedTextSpeech", + "getPropertiesSpeech", + "getSpellingSpeech", + "getTableInfoSpeech", + "getTextInfoSpeech", + "IDT_BASE_FREQUENCY", + "IDT_MAX_SPACES", + "IDT_TONE_DURATION", + "isBlank", + "isPaused", + "LANGS_WITH_CONJUNCT_CHARS", + "oldColumnNumber", + "oldColumnSpan", + "oldRowNumber", + "oldRowSpan", + "oldTableID", + "oldTreeLevel", + "pauseSpeech", + "processText", + "PROTECTED_CHAR", + "RE_CONVERT_WHITESPACE", + "RE_INDENTATION_CONVERT", + "RE_INDENTATION_SPLIT", + "speak", + "speakMessage", + "speakObject", + "speakObjectProperties", + "speakPreselectedText", + "speakSelectionChange", + "speakSelectionMessage", + "speakSpelling", + "speakText", + "speakTextInfo", + "speakTextSelected", + "speakTypedCharacters", + "speechMode", + "speechMode_beeps", + "speechMode_beeps_ms", + "speechMode_off", + "speechMode_talk", + "spellTextInfo", + "splitTextIndentation", +] + +import synthDriverHandler +import config + + +def initialize(): + """Loads and sets the synth driver configured in nvda.ini.""" + synthDriverHandler.initialize() + synthDriverHandler.setSynth(config.conf["speech"]["synth"]) + + +def terminate(): + synthDriverHandler.setSynth(None) diff --git a/source/speech/sayAll.py b/source/speech/sayAll.py index ae49cfccc22..8ef07e115b8 100644 --- a/source/speech/sayAll.py +++ b/source/speech/sayAll.py @@ -1,12 +1,19 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2017 NV Access Limited -# This file may be used under the terms of the GNU General Public License, version 2 or later. -# For more details see: https://www.gnu.org/licenses/gpl-2.0.html +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2006-2021 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Babbage B.V., Bill Dengler, +# Julien Cochuyt -from typing import Optional +from enum import IntEnum +from typing import TYPE_CHECKING import weakref import garbageHandler -import speech +from .speech import ( + speak, + getTextInfoSpeech, + SpeakTextInfoState, + speakObject, +) from logHandler import log import config import controlTypes @@ -15,55 +22,78 @@ import queueHandler import winKernel -from speech.commands import CallbackCommand, EndUtteranceCommand +from .commands import CallbackCommand, EndUtteranceCommand +from .speechWithoutPauses import SpeechWithoutPauses +from .types import ( + _flattenNestedSequences, +) -CURSOR_CARET = 0 -CURSOR_REVIEW = 1 +if TYPE_CHECKING: + import NVDAObjects -lastSayAllMode = None -#: The active say all manager. -#: This is a weakref because the manager should be allowed to die once say all is complete. -_activeSayAll = lambda: None # Return None when called like a dead weakref. +class CURSOR(IntEnum): + CARET = 0 + REVIEW = 1 -def getSpeechWithoutPauses() -> "speech.SpeechWithoutPauses": - """Returns an instance of `speech.SpeechWithoutPauses` which should be used for say all - creating it if necessary.""" - if getSpeechWithoutPauses.speechWithoutPausesInstance is None: - getSpeechWithoutPauses.speechWithoutPausesInstance = speech.SpeechWithoutPauses(speakFunc=speech.speak) - return getSpeechWithoutPauses.speechWithoutPausesInstance +SayAllHandler = None -getSpeechWithoutPauses.speechWithoutPausesInstance: Optional["speech.SpeechWithoutPauses"] = None +def initialize(): + global SayAllHandler + SayAllHandler = _SayAllHandler(SpeechWithoutPauses(speakFunc=speak)) -def stop(): - active = _activeSayAll() - if active: - active.stop() -def isRunning(): - """Determine whether say all is currently running. - @return: C{True} if say all is currently running, C{False} if not. - @rtype: bool - """ - return bool(_activeSayAll()) +class _SayAllHandler: + def __init__(self, speechWithoutPausesInstance: SpeechWithoutPauses): + self.lastSayAllMode = None + self.speechWithoutPausesInstance = speechWithoutPausesInstance + #: The active say all manager. + #: This is a weakref because the manager should be allowed to die once say all is complete. + self._getActiveSayAll = lambda: None # noqa: Return None when called like a dead weakref. -def readObjects(obj): - global _activeSayAll - reader = _ObjectsReader(obj) - _activeSayAll = weakref.ref(reader) - reader.next() + def stop(self): + ''' + Stops any active objects reader and resets the SayAllHandler's SpeechWithoutPauses instance + ''' + active = self._getActiveSayAll() + if active: + active.stop() + self.speechWithoutPausesInstance.reset() + + def isRunning(self): + """Determine whether say all is currently running. + @return: C{True} if say all is currently running, C{False} if not. + @rtype: bool + """ + return bool(self._getActiveSayAll()) + + def readObjects(self, obj: 'NVDAObjects.NVDAObject'): + reader = _ObjectsReader(self, obj) + self._getActiveSayAll = weakref.ref(reader) + reader.next() + + def readText(self, cursor: CURSOR): + self.lastSayAllMode = cursor + try: + reader = _TextReader(self, cursor) + except NotImplementedError: + log.debugWarning("Unable to make reader", exc_info=True) + return + self._getActiveSayAll = weakref.ref(reader) + reader.nextLine() class _ObjectsReader(garbageHandler.TrackedObject): - def __init__(self, root): + def __init__(self, handler: _SayAllHandler, root: 'NVDAObjects.NVDAObject'): + self.handler = handler self.walker = self.walk(root) self.prevObj = None - def walk(self, obj): + def walk(self, obj: 'NVDAObjects.NVDAObject'): yield obj child=obj.simpleFirstChild while child: @@ -77,7 +107,7 @@ def next(self): return if self.prevObj: # We just started speaking this object, so move the navigator to it. - api.setNavigatorObject(self.prevObj, isFocus=lastSayAllMode==CURSOR_CARET) + api.setNavigatorObject(self.prevObj, isFocus=self.handler.lastSayAllMode == CURSOR.CARET) winKernel.SetThreadExecutionState(winKernel.ES_SYSTEM_REQUIRED) # Move onto the next object. self.prevObj = obj = next(self.walker, None) @@ -85,22 +115,11 @@ def next(self): return # Call this method again when we start speaking this object. callbackCommand = CallbackCommand(self.next, name="say-all:next") - speech.speakObject(obj, reason=controlTypes.OutputReason.SAYALL, _prefixSpeechCommand=callbackCommand) + speakObject(obj, reason=controlTypes.OutputReason.SAYALL, _prefixSpeechCommand=callbackCommand) def stop(self): self.walker = None -def readText(cursor): - global lastSayAllMode, _activeSayAll - lastSayAllMode=cursor - try: - reader = _TextReader(cursor) - except NotImplementedError: - log.debugWarning("Unable to make reader", exc_info=True) - return - _activeSayAll = weakref.ref(reader) - reader.nextLine() - class _TextReader(garbageHandler.TrackedObject): """Manages continuous reading of text. @@ -124,12 +143,13 @@ class _TextReader(garbageHandler.TrackedObject): """ MAX_BUFFERED_LINES = 10 - def __init__(self, cursor): + def __init__(self, handler: _SayAllHandler, cursor: CURSOR): + self.handler = handler self.cursor = cursor self.trigger = SayAllProfileTrigger() self.reader = None # Start at the cursor. - if cursor == CURSOR_CARET: + if cursor == CURSOR.CARET: try: self.reader = api.getCaretObject().makeTextInfo(textInfos.POSITION_CARET) except (NotImplementedError, RuntimeError) as e: @@ -138,7 +158,7 @@ def __init__(self, cursor): self.reader = api.getReviewPosition() # #10899: SayAll profile can't be activated earlier because they may not be anything to read self.trigger.enter() - self.speakTextInfoState = speech.SpeakTextInfoState(self.reader.obj) + self.speakTextInfoState = SpeakTextInfoState(self.reader.obj) self.numBufferedLines = 0 def nextLine(self): @@ -164,7 +184,7 @@ def nextLine(self): if isinstance(self.reader.obj, textInfos.DocumentWithPageTurns): # Once the last line finishes reading, try turning the page. cb = CallbackCommand(self.turnPage, name="say-all:turnPage") - getSpeechWithoutPauses().speakWithoutPauses([cb, EndUtteranceCommand()]) + self.handler.speechWithoutPausesInstance.speakWithoutPauses([cb, EndUtteranceCommand()]) else: self.finish() return @@ -187,16 +207,16 @@ def _onLineReached(obj=self.reader.obj, state=state): # and insert the lineReached callback at the very beginning of the sequence. # _linePrefix on speakTextInfo cannot be used here # As it would be inserted in the sequence after all initial control starts which is too late. - speechGen = speech.getTextInfoSpeech( + speechGen = getTextInfoSpeech( self.reader, unit=textInfos.UNIT_READINGCHUNK, reason=controlTypes.OutputReason.SAYALL, useCache=state ) - seq = list(speech._flattenNestedSequences(speechGen)) + seq = list(_flattenNestedSequences(speechGen)) seq.insert(0, cb) # Speak the speech sequence. - spoke = getSpeechWithoutPauses().speakWithoutPauses(seq) + spoke = self.handler.speechWithoutPausesInstance.speakWithoutPauses(seq) # Update the textInfo state ready for when speaking the next line. self.speakTextInfoState = state.copy() @@ -218,7 +238,7 @@ def _onLineReached(obj=self.reader.obj, state=state): else: # We don't want to buffer too much. # Force speech. lineReached will resume things when speech catches up. - getSpeechWithoutPauses().speakWithoutPauses(None) + self.handler.speechWithoutPausesInstance.speakWithoutPauses(None) # The first buffered line has now started speaking. self.numBufferedLines -= 1 @@ -226,10 +246,10 @@ def lineReached(self, obj, bookmark, state): # We've just started speaking this line, so move the cursor there. state.updateObj() updater = obj.makeTextInfo(bookmark) - if self.cursor == CURSOR_CARET: + if self.cursor == CURSOR.CARET: updater.updateCaret() - if self.cursor != CURSOR_CARET or config.conf["reviewCursor"]["followCaret"]: - api.setReviewPosition(updater, isCaret=self.cursor==CURSOR_CARET) + if self.cursor != CURSOR.CARET or config.conf["reviewCursor"]["followCaret"]: + api.setReviewPosition(updater, isCaret=self.cursor == CURSOR.CARET) winKernel.SetThreadExecutionState(winKernel.ES_SYSTEM_REQUIRED) if self.numBufferedLines == 0: # This was the last line spoken, so move on. @@ -255,7 +275,7 @@ def finish(self): # we might switch synths too early and truncate the final speech. # We do this by putting a CallbackCommand at the start of a new utterance. cb = CallbackCommand(self.stop, name="say-all:stop") - getSpeechWithoutPauses().speakWithoutPauses([ + self.handler.speechWithoutPausesInstance.speakWithoutPauses([ EndUtteranceCommand(), cb, EndUtteranceCommand() diff --git a/source/speech/speech.py b/source/speech/speech.py old mode 100755 new mode 100644 index cb69cc8daf8..7ada56a081e --- a/source/speech/speech.py +++ b/source/speech/speech.py @@ -1,4 +1,3 @@ -# -*- coding: UTF-8 -*- # A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -7,6 +6,7 @@ """High-level functions to speak information. """ +# flake8: noqa ignore lint for mass refactor in (#12251), to be fixed with follow up PR import itertools import weakref @@ -17,8 +17,7 @@ import controlTypes from controlTypes import OutputReason import tones -import synthDriverHandler -from synthDriverHandler import getSynth, setSynth +from synthDriverHandler import getSynth import re import textInfos import speechDictHandler @@ -50,7 +49,6 @@ Any, Generator, Union, - Callable, Tuple, ) from logHandler import log @@ -58,6 +56,7 @@ import aria from .priorities import Spri + speechMode_off=0 speechMode_beeps=1 speechMode_talk=2 @@ -83,15 +82,6 @@ oldColumnNumber=None oldColumnSpan=None -def initialize(): - """Loads and sets the synth driver configured in nvda.ini.""" - synthDriverHandler.initialize() - setSynth(config.conf["speech"]["synth"]) - -def terminate(): - synthDriverHandler.setSynth(None) - speechViewerObj=None - #: If a chunk of text contains only these characters, it will be considered blank. BLANK_CHUNK_CHARS = frozenset((" ", "\n", "\r", "\0", u"\xa0")) def isBlank(text): @@ -115,9 +105,8 @@ def cancelSpeech(): """Interupts the synthesizer from currently speaking""" global beenCanceled, isPaused # Import only for this function to avoid circular import. - import sayAllHandler - sayAllHandler.stop() - sayAllHandler.getSpeechWithoutPauses().reset() + from speech import sayAll + sayAll.SayAllHandler.stop() if beenCanceled: return elif speechMode==speechMode_off: diff --git a/source/virtualBuffers/__init__.py b/source/virtualBuffers/__init__.py index 39330350ad9..0cabd1c78e4 100644 --- a/source/virtualBuffers/__init__.py +++ b/source/virtualBuffers/__init__.py @@ -20,7 +20,6 @@ import speech import NVDAObjects import api -import sayAllHandler import controlTypes import textInfos.offsets import config diff --git a/tests/unit/test_SpeechWithoutPauses.py b/tests/unit/test_SpeechWithoutPauses.py index 720501495c9..9e1d98e5326 100644 --- a/tests/unit/test_SpeechWithoutPauses.py +++ b/tests/unit/test_SpeechWithoutPauses.py @@ -1,16 +1,16 @@ # 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) 2019 NV Access Limited +# Copyright (C) 2019-2021 NV Access Limited -"""Unit tests for speechWithoutPauses""" +"""Unit tests for SpeechWithoutPauses""" import unittest from typing import List from speech.types import SpeechSequence from speech.commands import EndUtteranceCommand, LangChangeCommand, CallbackCommand -from speech import SpeechWithoutPauses +from speech.speechWithoutPauses import SpeechWithoutPauses from logHandler import log diff --git a/tests/unit/test_scriptHandler.py b/tests/unit/test_scriptHandler.py index 70127da27a0..f95e25ed7b3 100644 --- a/tests/unit/test_scriptHandler.py +++ b/tests/unit/test_scriptHandler.py @@ -8,7 +8,7 @@ import unittest from scriptHandler import script from inputCore import SCRCAT_MISC -from sayAllHandler import CURSOR_CARET +from speech.sayAll import CURSOR class TestScriptDecorator(unittest.TestCase): """A test that verifies the functionality of the L{scriptHandler.script} decorator.""" @@ -22,7 +22,7 @@ def test_scriptdecoration(self): canPropagate=True, bypassInputHelp=True, allowInSleepMode=True, - resumeSayAllMode=CURSOR_CARET + resumeSayAllMode=CURSOR.CARET ) def script_test(self, gesture): return @@ -33,4 +33,4 @@ def script_test(self, gesture): self.assertTrue(script_test.canPropagate) self.assertTrue(script_test.bypassInputHelp) self.assertTrue(script_test.allowInSleepMode) - self.assertEqual(script_test.resumeSayAllMode, CURSOR_CARET) + self.assertEqual(script_test.resumeSayAllMode, CURSOR.CARET) diff --git a/tests/unit/test_speechManager/__init__.py b/tests/unit/test_speechManager/__init__.py index 499bc108aec..017578e4acf 100644 --- a/tests/unit/test_speechManager/__init__.py +++ b/tests/unit/test_speechManager/__init__.py @@ -363,7 +363,7 @@ def test_standardSayAll(self): callBack = smi.create_CallBackCommand expectIndex = smi.create_ExpectedIndex - # First sayAllHandler queues up a number of utterances. + # First sayAll queues up a number of utterances. with smi.expectation(): seqNum = smi.speak([ callBack(expectedToBecomeIndex=1), @@ -401,7 +401,7 @@ def test_standardSayAll(self): with smi.expectation(): smi.pumpAll() - # no side effect, sayAllHandler does not send more speech until the final callback index is hit. + # no side effect, sayAll does not send more speech until the final callback index is hit. smi.expect_indexReachedCallback(forIndex=1, sideEffect=None) with smi.expectation(): @@ -485,7 +485,7 @@ class InitialDevelopmentTests(unittest.TestCase): the features they test. Manual test steps are kept in unit tests doc string, they can be run in the NVDA python console after the following imports: - import sayAllHandler, appModuleHandler + from speech import sayAll, appModuleHandler """ def setUp(self): @@ -834,8 +834,8 @@ def hasProfile(self): def test_4_profiles(self): """Text, pitch, text, enter profile1, enter profile2, text, exit profile1, text. Manual Test (in NVDA python console): - import sayAllHandler, appModuleHandler - t1 = sayAllHandler.SayAllProfileTrigger() + from speech import sayAll, appModuleHandler + t1 = sayAll.SayAllProfileTrigger() t2 = appModuleHandler.AppProfileTrigger("notepad") wx.CallLater(500, speech.speak, [ u"Testing testing ", PitchCommand(offset=100), "1 2 3 4", @@ -912,8 +912,8 @@ def test_4_profiles(self): def test_5_profiles(self): """Enter profile, text, exit profile. Manual Test (in NVDA python console): - import sayAllHandler - trigger = sayAllHandler.SayAllProfileTrigger() + from speech import sayAll + trigger = sayAll.SayAllProfileTrigger() wx.CallLater(500, speech.speak, [ ConfigProfileTriggerCommand(trigger, True), u"5 6 7 8", ConfigProfileTriggerCommand(trigger, False), @@ -954,8 +954,8 @@ def test_5_profiles(self): def test_10_SPRI_profiles(self): """Utterance at SPRI_NORMAL. Utterance at SPRI_NOW with profile switch. Manual Test (in NVDA python console): - import sayAllHandler; - trigger = sayAllHandler.SayAllProfileTrigger(); + from speech import sayAll; + trigger = sayAll.SayAllProfileTrigger(); wx.CallLater(500, speech.speak, [ ConfigProfileTriggerCommand(trigger, True), u"This is a normal utterance with a different profile" @@ -1000,8 +1000,8 @@ def test_10_SPRI_profiles(self): def test_11_SPRI_Profile(self): """Utterance at SPRI_NORMAL with profile switch. Utterance at SPRI_NOW. Manual Test (in NVDA python console): - import sayAllHandler - trigger = sayAllHandler.SayAllProfileTrigger() + from speech import sayAll + trigger = sayAll.SayAllProfileTrigger() wx.CallLater(500, speech.speak, [ ConfigProfileTriggerCommand(trigger, True), u"This is a normal utterance with a different profile" @@ -1064,8 +1064,8 @@ def test_11_SPRI_Profile(self): def test_12_SPRI_profile(self): """Utterance at SPRI_NORMAL with profile 1. Utterance at SPRI_NOW with profile 2. Manual Test (in NVDA python console): - import sayAllHandler, appModuleHandler - t1 = sayAllHandler.SayAllProfileTrigger() + from speech import sayAll, appModuleHandler + t1 = sayAll.SayAllProfileTrigger() t2 = appModuleHandler.AppProfileTrigger("notepad") wx.CallLater(500, speech.speak, [ ConfigProfileTriggerCommand(t1, True), diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 0c9fe36fbce..21ffc17a6a7 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -93,14 +93,14 @@ What's New in NVDA - `gui.DriverSettingsMixin` has been removed - use `gui.AutoSettingsMixin` (#12144) - `speech.getSpeechForSpelling` has been removed - use `speech.getSpellingSpeech` (#12145) - Commands cannot be directly imported from speech as `import speech; speech.ExampleCommand()` or `import speech.manager; speech.manager.ExampleCommand()` - use `from speech.commands import ExampleCommand` instead (#12126) -- `speakTextInfo` will no longer send speech through `speakWithoutPauses` if reason is `SAYALL`, as `sayAllhandler` does this manually now. (#12150) +- `speakTextInfo` will no longer send speech through `speakWithoutPauses` if reason is `SAYALL`, as `SayAllHandler` does this manually now. (#12150) - The `synthDriverHandler` module is no longer star imported into `globalCommands` and `gui.settingsDialogs` - use `from synthDriverHandler import synthFunctionExample` instead. (#12172) - `ROLE_EQUATION` has been removed from controlTypes - use `ROLE_MATH`` instead. (#12164) - `autoSettingsUtils.driverSetting` classes are removed from `driverHandler` - please use them from `autoSettingsUtils.driverSetting`. (#12168) - `autoSettingsUtils.utils` classes are removed from `driverHandler` - please use them from `autoSettingsUtils.utils`. (#12168) - Support of `TextInfo`s that do not inherit from `contentRecog.BaseContentRecogTextInfo` is removed. (#12157) -- `speech.speakWithoutPauses` has been removed - please use `speech.SpeechWithoutPauses(speakFunc=speech.speak).speakWithoutPauses` instead. (#12195) -- `speech.re_last_pause` has been removed - please use `speech.SpeechWithoutPauses.re_last_pause` instead. (#12195) +- `speech.speakWithoutPauses` has been removed - please use `speech.speechWithoutPauses.SpeechWithoutPauses(speakFunc=speech.speak).speakWithoutPauses` instead. (#12195, #12251) +- `speech.re_last_pause` has been removed - please use `speech.speechWithoutPauses.SpeechWithoutPauses.re_last_pause` instead. (#12195, #12251) - `WelcomeDialog`, `LauncherDialog` and `AskAllowUsageStatsDialog` are moved to the `gui.startupDialogs`. (#12105) - `getDocFilePath` has been moved from `gui` to the `documentationUtils` module. (#12105) - The gui.accPropServer module as well as the AccPropertyOverride and ListCtrlAccPropServer classes from the gui.nvdaControls module have been removed in favor of WX' native support for overriding accessibility properties. When enhancing accessibility of WX controls, implement wx.Accessible instead. (#12215) @@ -119,6 +119,11 @@ What's New in NVDA - This usage is prefered instead of ti1.SetEndPoint(ti2,"startToEnd") - `wx.CENTRE_ON_SCREEN` and `wx.CENTER_ON_SCREEN` are removed, use `self.CentreOnScreen()` instead. (#12309) - `easeOfAccess.isSupported` has been removed, NVDA only supports versions of Windows where this evaluates to `True`. (#12222) +- `sayAllHandler` has been moved to `speech.sayAll` (#12251): + - `speech.sayAll.SayAllHandler` exposes the functions `stop`, `isRunning`, `readObjects`, `readText`, `lastSayAllMode`. + - `SayAllHandler.stop` also resets the `SayAllHandler` `SpeechWithoutPauses` instance. + - `CURSOR_REVIEW` and `CURSOR_CARET` has been replaced with `CURSOR.REVIEW` and `CURSOR.CARET`. +- `speech.SpeechWithoutPauses` has been moved to `speech.speechWithoutPauses.SpeechWithoutPauses`. (#12251) = 2020.4 = From 01605b61ac537287ae60918e9c2d13273d3574d6 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Mon, 10 May 2021 19:08:16 +1000 Subject: [PATCH 174/174] move changes to correct header (#12390) A changes item was accidentally added to the wrong release header in PR #11598 Description of how this pull request fixes the issue: Move the changelog item to the correct release --- user_docs/en/changes.t2t | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index c7172f54f64..46772bd6061 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -11,6 +11,7 @@ What's New in NVDA - Easier navigation of output in NVDA Python Console. (#9784) - alt+up/down jumps to the previous/next output result (add shift for selecting). - control+l clears the output pane. +- NVDA now reports the categories assigned to an appointment in Microsoft Outlook, if any. (#11598) == Changes == @@ -20,9 +21,9 @@ What's New in NVDA - Added more mathematical symbols to the symbols dictionary. (#11467) - The user guide, changes file, and key commands listing now have a refreshed appearance. (#12027) - "Unsupported" now reported when attempting to toggle screen layout in applications that do not support it, such as Microsoft Word. (#7297) -- 'Attempt to cancel speech for expired focus events' option in the advanced settings panel now enabled by default. +- 'Attempt to cancel speech for expired focus events' option in the advanced settings panel now enabled by default. (#10885) - This behaviour can be disabled by default with by setting this option to "No". - - Web applications (E.G. Gmail) no longer speak outdated information when moving focus rapidly. (#10885) + - Web applications (E.G. Gmail) no longer speak outdated information when moving focus rapidly. - Espeak-ng has been updated to 1.51-dev commit cad1c8e87fcccf677a445202e340f61980450a84. (#12202, #12280) - Updated liblouis braille translator to [3.17.0 https://github.com/liblouis/liblouis/releases/tag/v3.17.0]. (#12137) - New braille tables: Belarusian literary braille, Belarusian computer braille, Urdu grade 1, Urdu grade 2. @@ -150,7 +151,6 @@ Plus many other important bug fixes and improvements. - Added the --copy-portable-config command line parameter that allows you to automatically copy the provided configuration to the user account when silently installing NVDA. (#9676) - Braille routing is now supported with the Braille Viewer for mouse users, hover to route to a braille cell. (#11804) - NVDA will now automatically detect the Humanware Brailliant BI 40X and 20X devices via both USB and Bluetooth. (#11819) -- NVDA now reports the categories assigned to an appointment in Microsoft Outlook, if any. (#11598) == Changes ==