From 2e7144c2d71a9f418eacae89525882a7fadf6429 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Tue, 8 Sep 2020 18:11:59 +0200 Subject: [PATCH 01/27] Prepare changes file for 2020.4 --- user_docs/en/changes.t2t | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 4196d3480d4..52a2bb37d92 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -3,6 +3,17 @@ What's New in NVDA %!includeconf: ../changes.t2tconf += 2020.4 = + +== New Features == + + +== Changes == + + +== Bug Fixes == + + = 2020.3 = == New Features == From f0438bfc236ee5dafbebcbd8c3af2f62930c5b8d Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 9 Sep 2020 14:29:15 +0200 Subject: [PATCH 02/27] Update NVDA version --- source/buildVersion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/buildVersion.py b/source/buildVersion.py index 96b5e89ad97..1333239a4cc 100644 --- a/source/buildVersion.py +++ b/source/buildVersion.py @@ -66,7 +66,7 @@ def formatVersionForGUI(year, major, minor): # Version information for NVDA name = "NVDA" version_year = 2020 -version_major = 3 +version_major = 4 version_minor = 0 version_build = 0 # Should not be set manually. Set in 'sconscript' provided by 'appVeyor.yml' version=_formatDevVersionString() From 82ddb3711b1cbe1b7a4d75a924f9e3f4a821843e Mon Sep 17 00:00:00 2001 From: Luke Davis <8139760+XLTechie@users.noreply.github.com> Date: Wed, 9 Sep 2020 09:16:04 -0400 Subject: [PATCH 03/27] Spelling and grammar corrections (PR #11557) --- source/addonHandler/__init__.py | 27 ++++++++++++------------ source/addonHandler/addonVersionCheck.py | 2 +- source/globalCommands.py | 2 +- source/inputCore.py | 5 +++-- 4 files changed, 19 insertions(+), 17 deletions(-) mode change 100755 => 100644 source/globalCommands.py diff --git a/source/addonHandler/__init__.py b/source/addonHandler/__init__.py index d95a3372137..48f127ccf12 100644 --- a/source/addonHandler/__init__.py +++ b/source/addonHandler/__init__.py @@ -41,7 +41,7 @@ state={} -# addons that are blocked from running because they are incompatible +# Add-ons that are blocked from running because they are incompatible _blockedAddons=set() def loadState(): @@ -77,7 +77,7 @@ def saveState(): log.debugWarning("Error saving state", exc_info=True) def getRunningAddons(): - """ Returns currently loaded addons. + """ Returns currently loaded add-ons. """ return getAvailableAddons(filterFunc=lambda addon: addon.isRunning) @@ -96,7 +96,7 @@ def getIncompatibleAddons( )) def completePendingAddonRemoves(): - """Removes any addons that could not be removed on the last run of NVDA""" + """Removes any add-ons that could not be removed on the last run of NVDA""" user_addons = os.path.abspath(os.path.join(globalVars.appArgs.configPath, "addons")) pendingRemovesSet=state['pendingRemovesSet'] for addonName in list(pendingRemovesSet): @@ -166,7 +166,7 @@ def terminate(): def _getDefaultAddonPaths(): """ Returns paths where addons can be found. - For now, only \addons is supported. @rtype: list(string) """ addon_paths = [] @@ -448,10 +448,10 @@ def loadModule(self, name): return None def getTranslationsInstance(self, domain='nvda'): - """ Gets the gettext translation instance for this addon. - \\locale will be used to find .mo files, if exists. If a translation file is not found the default fallback null translation is returned. - @param domain: the tranlation domain to retrieve. The 'nvda' default should be used in most cases. + @param domain: the translation domain to retrieve. The 'nvda' default should be used in most cases. @returns: the gettext translation class. """ localedir = os.path.join(self.path, "locale") @@ -459,7 +459,8 @@ def getTranslationsInstance(self, domain='nvda'): def runInstallTask(self,taskName,*args,**kwargs): """ - Executes the function having the given taskName with the given args and kwargs in the addon's installTasks module if it exists. + Executes the function having the given taskName with the given args and kwargs, + in the add-on's installTasks module if it exists. """ if not hasattr(self,'_installTasksModule'): self._installTasksModule=self.loadModule('installTasks') @@ -502,7 +503,7 @@ def getDocFilePath(self, fileName=None): def getCodeAddon(obj=None, frameDist=1): """ Returns the L{Addon} where C{obj} is defined. If obj is None the caller code frame is assumed to allow simple retrieval of "current calling addon". @param obj: python object or None for default behaviour. - @param frameDist: howmany frames is the caller code. Only change this for functions in this module. + @param frameDist: how many frames is the caller code. Only change this for functions in this module. @return: L{Addon} instance or None if no code does not belong to a add-on package. @rtype: C{Addon} """ @@ -530,7 +531,7 @@ def initTranslation(): addon = getCodeAddon(frameDist=2) translations = addon.getTranslationsInstance() # Point _ to the translation object in the globals namespace of the caller frame - # FIXME: shall we retrieve the caller module object explicitly? + # FIXME: should we retrieve the caller module object explicitly? try: callerFrame = inspect.currentframe().f_back callerFrame.f_globals['_'] = translations.gettext @@ -610,7 +611,7 @@ def createAddonBundleFromPath(path, destDir=None): """ Creates a bundle from a directory that contains a a addon manifest file.""" basedir = os.path.abspath(path) # If caller did not provide a destination directory name - # Put the bundle at the same level of the addon's top directory, + # Put the bundle at the same level as the add-on's top-level directory, # That is, basedir/.. if destDir is None: destDir = os.path.dirname(basedir) @@ -672,7 +673,7 @@ class AddonManifest(ConfigObj): docFileName = string(default=None) # NOTE: apiVersion: -# Eg: 2019.1.0 or 0.0.0 +# EG: 2019.1.0 or 0.0.0 # Must have 3 integers separated by dots. # The first integer must be a Year (4 characters) # "0.0.0" is also valid. @@ -682,7 +683,7 @@ class AddonManifest(ConfigObj): def __init__(self, input, translatedInput=None): """ Constructs an L{AddonManifest} instance from manifest string data - @param input: data to read the manifest informatinon + @param input: data to read the manifest information @type input: a fie-like object. @param translatedInput: translated manifest input @type translatedInput: file-like object diff --git a/source/addonHandler/addonVersionCheck.py b/source/addonHandler/addonVersionCheck.py index 1dec2d872fd..ae4881670a5 100644 --- a/source/addonHandler/addonVersionCheck.py +++ b/source/addonHandler/addonVersionCheck.py @@ -7,7 +7,7 @@ import addonAPIVersion def hasAddonGotRequiredSupport(addon, currentAPIVersion=addonAPIVersion.CURRENT): - """True if NVDA provides the add-on with an API version high enough to meets the addon's minimum requirements + """True if NVDA provides the add-on with an API version high enough to meet the add-on's minimum requirements """ minVersion = addon.minimumNVDAVersion return minVersion <= currentAPIVersion diff --git a/source/globalCommands.py b/source/globalCommands.py old mode 100755 new mode 100644 index ba0ba26fde1..422a67e1f7f --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -3012,7 +3012,7 @@ def _moveGesturesForProfileActivationScript(cls, oldScriptName, newScriptName=No @param oldScriptName: The current name of the profile activation script. @type oldScriptName: str @param newScriptName: The new name for the profile activation script, if any. - if C{None}, the gestures are only removed for the current profile sript. + if C{None}, the gestures are only removed for the current profile script. @type newScriptName: str """ gestureMap = inputCore.manager.userGestureMap diff --git a/source/inputCore.py b/source/inputCore.py index df83933e207..1afaa1969f4 100644 --- a/source/inputCore.py +++ b/source/inputCore.py @@ -64,7 +64,8 @@ class InputGesture(baseObject.AutoPropertyObject): #: @type: bool bypassInputHelp=False - #: Indicates that this gesture should be reported in Input help mode. This would only be false for floodding Gestures like touch screen hovers. + #: Indicates that this gesture should be reported in Input help mode. This would only be false + #: for flooding Gestures like touch screen hovers. #: @type: bool reportInInputHelp=True @@ -796,7 +797,7 @@ def registerGestureSource(source, gestureCls): The specified gesture class will be used for queries regarding all gesture identifiers with the given source prefix. For example, if "kb" is registered with the C{KeyboardInputGesture} class, any queries for "kb:tab" or "kb(desktop):tab" will be directed to the C{KeyboardInputGesture} class. - If there is no exact match for the source, any parenthesised portion is stripped. + If there is no exact match for the source, any parenthesized portion is stripped. For example, for "br(baum):d1", if "br(baum)" isn't registered, "br" will be used if it is registered. This registration is used, for example, to get the display text for a gesture identifier. From 43d3aa1bb6a5c3065c2ad5779f04e1c6550d703c Mon Sep 17 00:00:00 2001 From: Levi <34293774+levyadams@users.noreply.github.com> Date: Wed, 9 Sep 2020 09:17:58 -0400 Subject: [PATCH 04/27] Add "ServiceMark" character to symbols.dic (PR #11559) Add the legal special character "Service Mark" (unicode U+2120) to "some" punctuation setting --- source/locale/en/symbols.dic | 1 + 1 file changed, 1 insertion(+) diff --git a/source/locale/en/symbols.dic b/source/locale/en/symbols.dic index 9d275a758a2..5bffe0d72c9 100644 --- a/source/locale/en/symbols.dic +++ b/source/locale/en/symbols.dic @@ -167,6 +167,7 @@ _ line most ® registered some ™ Trademark some © Copyright some +℠ ServiceMark some ± Plus or Minus some × times some ÷ divide by some From 322370758f3ffa8bb8156fe81a9cc8d276b51209 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Wed, 9 Sep 2020 15:37:53 +0200 Subject: [PATCH 05/27] Update liblouis to version 3.15.0 (PR #11537) Co-authored-by: Reef Turner --- include/liblouis | 2 +- readme.md | 2 +- user_docs/en/changes.t2t | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/include/liblouis b/include/liblouis index 9ecf97a7452..f41b38d5934 160000 --- a/include/liblouis +++ b/include/liblouis @@ -1 +1 @@ -Subproject commit 9ecf97a74523aa0794264b70f8c33504f6d3ee42 +Subproject commit f41b38d59346b60c69cdfe43d777aaa730db01d5 diff --git a/readme.md b/readme.md index d72b016e116..17753fca799 100644 --- a/readme.md +++ b/readme.md @@ -87,7 +87,7 @@ For reference, the following run time dependencies are included in Git submodule * [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.14.0 +* [liblouis](http://www.liblouis.org/), version 3.15.0 * [Unicode Common Locale Data Repository (CLDR)](http://cldr.unicode.org/) Emoji Annotations, version 37.0 * NVDA images and sounds * [Adobe Acrobat accessibility interface, version XI](https://download.macromedia.com/pub/developer/acrobat/AcrobatAccess.zip) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 52a2bb37d92..551e163ab64 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -9,6 +9,7 @@ What's New in NVDA == Changes == +- Updated liblouis braille translator to version 3.15.0 == Bug Fixes == From 423674eae68d4a8020f300fc44ab12cb2bac937d Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 9 Sep 2020 16:41:25 +0200 Subject: [PATCH 06/27] Context sensitive help (PR #11456) Fixes #7757 Fixes #8354 Supersedes PR #8355 Add initial support for opening the user guide at a particular section based on the context when the user presses F1 (in NVDA dialogs). For the sake of simplicity, this is done by opening the user guide in the browser. Unfortunately, this results in a lot of unnecessary verbosity. However, this is still quicker for the user than manually opening the user guide separately (also in a browser) and navigating to the chosen section manually. Co-authored-by: ThomasStivers Co-authored-by: Leonard de Ruijter --- source/browseMode.py | 17 ++- source/gui/__init__.py | 18 ++- source/gui/addonGui.py | 27 +++-- source/gui/configProfiles.py | 31 ++++-- source/gui/contextHelp.py | 95 ++++++++++++++++ source/gui/inputGestures.py | 1 + source/gui/settingsDialogs.py | 204 ++++++++++++++++++++++++++++++++-- source/speechViewer.py | 16 ++- source/updateCheck.py | 17 ++- user_docs/en/changes.t2t | 1 + 10 files changed, 377 insertions(+), 50 deletions(-) create mode 100644 source/gui/contextHelp.py diff --git a/source/browseMode.py b/source/browseMode.py index 4410a3f65f7..3f377057152 100644 --- a/source/browseMode.py +++ b/source/browseMode.py @@ -1,8 +1,7 @@ -#browseMode.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2007-2018 NV Access Limited, 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) 2007-2020 NV Access Limited, Babbage B.V. Thomas Stivers +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. import itertools import collections @@ -38,6 +37,7 @@ import gui.guiHelper from gui.dpiScalingHelper import DpiScalingHelperMixinWithoutInit from NVDAObjects import NVDAObject +import gui.contextHelp from abc import ABCMeta, abstractmethod from typing import Optional @@ -883,7 +883,12 @@ def _get_disableAutoPassThrough(self): del qn -class ElementsListDialog(DpiScalingHelperMixinWithoutInit, wx.Dialog): +class ElementsListDialog( + DpiScalingHelperMixinWithoutInit, + gui.contextHelp.ContextHelpMixin, + wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO +): + helpId = "ElementsList" ELEMENT_TYPES = ( # Translators: The label of a radio button to select the type of element # in the browse mode Elements List dialog. diff --git a/source/gui/__init__.py b/source/gui/__init__.py index 6763b273406..fd0d4f7b3c3 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -25,6 +25,7 @@ import queueHandler import core from . import guiHelper +from .contextHelp import ContextHelpMixin from .settingsDialogs import * from .inputGestures import InputGesturesDialog import speechDictHandler @@ -668,13 +669,17 @@ def run(): dialog.Destroy() wx.CallAfter(run) -class WelcomeDialog(wx.Dialog): + +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" @@ -688,6 +693,7 @@ class WelcomeDialog(wx.Dialog): 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 @@ -766,13 +772,19 @@ def run(cls): d.Destroy() mainFrame.postPopup() -class LauncherDialog(wx.Dialog): + +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) diff --git a/source/gui/addonGui.py b/source/gui/addonGui.py index a6c79d36a4d..84a1f893a15 100644 --- a/source/gui/addonGui.py +++ b/source/gui/addonGui.py @@ -1,8 +1,8 @@ -#gui/addonGui.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) 2012-2019 NV Access Limited, Beqa Gozalishvili, Joseph Lee, Babbage B.V., Ethan Holliger, Arnold Loubriat +# 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) 2012-2019 NV Access Limited, Beqa Gozalishvili, Joseph Lee, +# Babbage B.V., Ethan Holliger, Arnold Loubriat, Thomas Stivers import os import weakref @@ -20,8 +20,8 @@ import buildVersion from . import guiHelper from . import nvdaControls -from .dpiScalingHelper import DpiScalingHelperMixin - +from .dpiScalingHelper import DpiScalingHelperMixin, DpiScalingHelperMixinWithoutInit +import gui.contextHelp def promptUserForRestart(): restartMessage = _( # Translators: A message asking the user if they wish to restart NVDA @@ -136,7 +136,11 @@ def _showAddonInfo(addon): gui.messageBox("\n".join(message), title, wx.OK) -class AddonsDialog(wx.Dialog, DpiScalingHelperMixin): +class AddonsDialog( + DpiScalingHelperMixinWithoutInit, + gui.ContextHelpMixin, + wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO +): @classmethod def _instance(cls): """ type: () -> AddonsDialog @@ -145,6 +149,8 @@ def _instance(cls): """ return None + helpId = "AddonsManager" + def __new__(cls, *args, **kwargs): instance = AddonsDialog._instance() if instance is None: @@ -161,14 +167,11 @@ def __init__(self, parent): title = _("Add-ons Manager") # Translators: The title of the Addons Dialog when add-ons are disabled titleWhenAddonsAreDisabled = _("Add-ons Manager (add-ons disabled)") - wx.Dialog.__init__( - self, + super().__init__( parent, title=title if not globalVars.appArgs.disableAddons else titleWhenAddonsAreDisabled, style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.MAXIMIZE_BOX, ) - DpiScalingHelperMixin.__init__(self, self.GetHandle()) - mainSizer = wx.BoxSizer(wx.VERTICAL) firstTextSizer = wx.BoxSizer(wx.VERTICAL) listAndButtonsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=wx.BoxSizer(wx.HORIZONTAL)) diff --git a/source/gui/configProfiles.py b/source/gui/configProfiles.py index 98974a527de..2b5e63ed2a5 100644 --- a/source/gui/configProfiles.py +++ b/source/gui/configProfiles.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2013-2018 NV Access Limited, Joseph Lee, Julien Cochuyt +# Copyright (C) 2013-2018 NV Access Limited, Joseph Lee, Julien Cochuyt, Thomas Stivers # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -11,11 +11,18 @@ import appModuleHandler import globalVars from . import guiHelper +import gui.contextHelp -class ProfilesDialog(wx.Dialog): + +class ProfilesDialog( + gui.ContextHelpMixin, + wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO +): shouldSuspendConfigProfileTriggers = True + helpId = "ConfigurationProfiles" _instance = None + def __new__(cls, *args, **kwargs): # Make this a singleton. if ProfilesDialog._instance is None: @@ -27,7 +34,7 @@ def __init__(self, parent): return ProfilesDialog._instance = self # Translators: The title of the Configuration Profiles dialog. - super(ProfilesDialog, self).__init__(parent, title=_("Configuration Profiles")) + super().__init__(parent, title=_("Configuration Profiles")) self.currentAppName = (gui.mainFrame.prevFocus or api.getFocusObject()).appModule.appName self.profileNames = [None] @@ -294,11 +301,16 @@ def __init__(self, spec, display, profile): self.display = display self.profile = profile -class TriggersDialog(wx.Dialog): + +class TriggersDialog( + gui.ContextHelpMixin, + wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO +): + helpId = "ConfigProfileTriggers" def __init__(self, parent): # Translators: The title of the configuration profile triggers dialog. - super(TriggersDialog, self).__init__(parent, title=_("Profile Triggers")) + super().__init__(parent, title=_("Profile Triggers")) mainSizer = wx.BoxSizer(wx.VERTICAL) sHelper = guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) @@ -380,11 +392,16 @@ def onClose(self, evt): self.Parent.Enable() self.Destroy() -class NewProfileDialog(wx.Dialog): + +class NewProfileDialog( + gui.ContextHelpMixin, + wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO +): + helpId = "ConfigurationProfiles" def __init__(self, parent): # Translators: The title of the dialog to create a new configuration profile. - super(NewProfileDialog, self).__init__(parent, title=_("New Profile")) + super().__init__(parent, title=_("New Profile")) mainSizer = wx.BoxSizer(wx.VERTICAL) sHelper = guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) diff --git a/source/gui/contextHelp.py b/source/gui/contextHelp.py new file mode 100644 index 00000000000..3caac1d3670 --- /dev/null +++ b/source/gui/contextHelp.py @@ -0,0 +1,95 @@ +# -*- coding: UTF-8 -*- +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2017-2020 NV Access Limited, Thomas Stivers +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +import os +import tempfile +import typing + +import gui +import ui +import wx +from logHandler import log + + +def writeRedirect(helpId: str, helpFilePath: str, contextHelpPath: str): + redirect = rf""" + + + + """ + with open(contextHelpPath, 'w') as f: + f.write(redirect) + + +def showHelp(helpId: str): + """Display the corresponding section of the user guide when either the Help + button in an NVDA dialog is pressed or the F1 key is pressed on a + recognized control. + """ + if not helpId: + # Translators: Message indicating no context sensitive help is available. + noHelpMessage = _("No context sensitive help is available here at this time.") + ui.message(noHelpMessage) + helpFile = gui.getDocFilePath("userGuide.html") + if not os.path.exists(helpFile): + # Translators: Message shown when trying to display context sensitive help, + # indicating that the user guide could not be found. + noHelpMessage = _("No user guide found.") + log.debugWarning("No user guide found: possible cause - running from source without building user docs") + ui.message(noHelpMessage) + log.debug(f"Opening help: helpId = {helpId}, userGuidePath: {helpFile}") + + nvdaTempDir = os.path.join(tempfile.gettempdir(), "nvda") + if not os.path.exists(nvdaTempDir): + os.mkdir(nvdaTempDir) + + contextHelpRedirect = os.path.join(nvdaTempDir, "contextHelp.html") + try: + # a redirect is necessary because not all browsers support opening a fragment URL from the command line. + writeRedirect(helpId, helpFile, contextHelpRedirect) + except Exception: + log.error("Unable to write context help redirect file.", exc_info=True) + return + + try: + os.startfile(f"file://{contextHelpRedirect}") + except Exception: + log.error("Unable to launch context help.", exc_info=True) + + +def bindHelpEvent(helpId: str, window: wx.Window): + window.Unbind(wx.EVT_HELP) + window.Bind( + wx.EVT_HELP, + lambda evt: _onEvtHelp(helpId, evt), + ) + log.debug(f"Did context help binding for {window.__class__.__qualname__}") + + +def _onEvtHelp(helpId: str, evt: wx.HelpEvent): + # Don't call evt.skip. Events bubble upwards through parent controls. + # Context help for more specific controls should override the less specific parent controls. + showHelp(helpId) + + +class ContextHelpMixin: + #: The name of the appropriate anchor in NVDA help that provides help for the wx.Window this mixin is + # used with. + helpId = "" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + helpId = getattr(self, "helpId", None) + if helpId is None or not isinstance(helpId, str): + log.warning(f"No helpId (or incorrect type) for: {self.__class__.__qualname__} helpId: {helpId!r}") + helpId = "" + window = typing.cast(wx.Window, self) + bindHelpEvent(helpId, window) + + def bindHelpEvent(self, helpId: str, window: wx.Window): + """A helper method, to bind helpId strings to sub-controls of this Window. + Useful for adding context help to wx controls directly. + """ + bindHelpEvent(helpId, window) diff --git a/source/gui/inputGestures.py b/source/gui/inputGestures.py index 16304b9526e..8920275ed82 100644 --- a/source/gui/inputGestures.py +++ b/source/gui/inputGestures.py @@ -568,6 +568,7 @@ def doRefresh(self, postFilter=False, focus: Optional[_VmSelection] = None): class InputGesturesDialog(SettingsDialog): # Translators: The title of the Input Gestures dialog where the user can remap input gestures for scripts. title = _("Input Gestures") + helpId = "InputGestures" def __init__(self, parent: "InputGesturesDialog"): #: The index in the _GesturesTree of the prompt for entering a new gesture diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 024c5640afc..85088cd71b1 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -53,9 +53,15 @@ import weakref import time import keyLabels -from .dpiScalingHelper import DpiScalingHelperMixin +from .dpiScalingHelper import DpiScalingHelperMixinWithoutInit -class SettingsDialog(wx.Dialog, DpiScalingHelperMixin, metaclass=guiHelper.SIPABCMeta): + +class SettingsDialog( + DpiScalingHelperMixinWithoutInit, + gui.ContextHelpMixin, + wx.Dialog, # wxPython does not seem to call base class initializer, put last in MRO + metaclass=guiHelper.SIPABCMeta +): """A settings dialog. A settings dialog consists of one or more settings controls and OK and Cancel buttons and an optional Apply button. Action may be taken in response to the OK, Cancel or Apply buttons. @@ -79,6 +85,7 @@ class MultiInstanceError(RuntimeError): pass # holds instances of SettingsDialogs as keys, and state as the value _instances=weakref.WeakKeyDictionary() title = "" + helpId = "NVDASettings" shouldSuspendConfigProfileTriggers = True def __new__(cls, *args, **kwargs): @@ -142,8 +149,7 @@ def __init__( windowStyle = wx.DEFAULT_DIALOG_STYLE if resizeable: windowStyle |= wx.RESIZE_BORDER | wx.MAXIMIZE_BOX - wx.Dialog.__init__(self, parent, title=self.title, style=windowStyle) - DpiScalingHelperMixin.__init__(self, self.GetHandle()) + super().__init__(parent, title=self.title, style=windowStyle) self.hasApply = hasApplyButton @@ -243,7 +249,13 @@ def _onWindowDestroy(self, evt): # redo the layout in whatever way makes sense for their particular content. _RWLayoutNeededEvent, EVT_RW_LAYOUT_NEEDED = wx.lib.newevent.NewCommandEvent() -class SettingsPanel(wx.Panel, DpiScalingHelperMixin, metaclass=guiHelper.SIPABCMeta): + +class SettingsPanel( + DpiScalingHelperMixinWithoutInit, + gui.ContextHelpMixin, + wx.Panel, # wxPython does not seem to call base class initializer, put last in MRO + metaclass=guiHelper.SIPABCMeta +): """A settings panel, to be used in a multi category settings dialog. A settings panel consists of one or more settings controls. Action may be taken in response to the parent dialog's OK or Cancel buttons. @@ -269,8 +281,7 @@ def __init__(self, parent: wx.Window): """ if gui._isDebug(): startTime = time.time() - wx.Panel.__init__(self, parent, wx.ID_ANY) - DpiScalingHelperMixin.__init__(self, self.GetHandle()) + super().__init__(parent) self._buildGui() @@ -631,9 +642,11 @@ def onApply(self,evt): return super(MultiCategorySettingsDialog,self).onApply(evt) + class GeneralSettingsPanel(SettingsPanel): # Translators: This is the label for the general settings panel. title = _("General") + helpId = "GeneralSettingsLanguage" LOG_LEVELS = ( # Translators: One of the log levels of NVDA (the disabled mode turns off logging completely). (log.OFF, _("disabled")), @@ -669,6 +682,7 @@ def makeSettings(self, settingsSizer): # Translators: The label for a setting in general settings to save current configuration when NVDA # exits (if it is not checked, user needs to save configuration before quitting NVDA). self.saveOnExitCheckBox = wx.CheckBox(self, label=_("&Save configuration when exiting NVDA")) + self.saveOnExitCheckBox.SetValue(config.conf["general"]["saveConfigurationOnExit"]) if globalVars.appArgs.secure: self.saveOnExitCheckBox.Disable() @@ -678,9 +692,11 @@ def makeSettings(self, settingsSizer): self.askToExitCheckBox=wx.CheckBox(self,label=_("Sho&w exit options when exiting NVDA")) self.askToExitCheckBox.SetValue(config.conf["general"]["askToExit"]) settingsSizerHelper.addItem(self.askToExitCheckBox) + self.bindHelpEvent("GeneralSettingsShowExitOptions", self.askToExitCheckBox) # Translators: The label for a setting in general settings to play sounds when NVDA starts or exits. self.playStartAndExitSoundsCheckBox=wx.CheckBox(self,label=_("&Play sounds when starting or exiting NVDA")) + self.bindHelpEvent("GeneralSettingsPlaySounds", self.playStartAndExitSoundsCheckBox) self.playStartAndExitSoundsCheckBox.SetValue(config.conf["general"]["playStartAndExitSounds"]) settingsSizerHelper.addItem(self.playStartAndExitSoundsCheckBox) @@ -708,6 +724,7 @@ def makeSettings(self, settingsSizer): if globalVars.appArgs.secure or not config.isInstalledCopy(): self.startAfterLogonCheckBox.Disable() settingsSizerHelper.addItem(self.startAfterLogonCheckBox) + self.bindHelpEvent("GeneralSettingsStartAfterLogOn", self.startAfterLogonCheckBox) self.startOnLogonScreenCheckBox = wx.CheckBox( self, # Translators: The label for a setting in general settings to @@ -716,6 +733,7 @@ def makeSettings(self, settingsSizer): # to allow user to choose the correct account). label=_("Use NVDA during sign-in (requires administrator privileges)") ) + self.bindHelpEvent("GeneralSettingsStartOnLogOnScreen", self.startOnLogonScreenCheckBox) self.startOnLogonScreenCheckBox.SetValue(config.getStartOnLogonScreen()) if globalVars.appArgs.secure or not config.canStartOnSecureScreens(): self.startOnLogonScreenCheckBox.Disable() @@ -732,6 +750,7 @@ def makeSettings(self, settingsSizer): " (requires administrator privileges)" ) ) + self.bindHelpEvent("GeneralSettingsCopySettings", self.copySettingsButton) self.copySettingsButton.Bind(wx.EVT_BUTTON,self.onCopySettings) if globalVars.appArgs.secure or not config.canStartOnSecureScreens(): self.copySettingsButton.Disable() @@ -739,6 +758,7 @@ def makeSettings(self, settingsSizer): if updateCheck: # Translators: The label of a checkbox in general settings to toggle automatic checking for updated versions of NVDA (if not checked, user must check for updates manually). item=self.autoCheckForUpdatesCheckBox=wx.CheckBox(self,label=_("Automatically check for &updates to NVDA")) + self.bindHelpEvent("GeneralSettingsCheckForUpdates", self.autoCheckForUpdatesCheckBox) item.Value=config.conf["update"]["autoCheck"] if globalVars.appArgs.secure: item.Disable() @@ -873,6 +893,7 @@ def onRestartNowButton(self, evt): class SpeechSettingsPanel(SettingsPanel): # Translators: This is the label for the speech panel title = _("Speech") + helpId = "SpeechSettings" def makeSettings(self, settingsSizer): settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) @@ -894,6 +915,8 @@ def makeSettings(self, settingsSizer): # 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...")) + self.bindHelpEvent("SpeechSettingsChange", self.synthNameCtrl) + self.bindHelpEvent("SpeechSettingsChange", changeSynthBtn) synthGroup.addItem( guiHelper.associateElements( self.synthNameCtrl, @@ -943,6 +966,7 @@ def onSave(self): class SynthesizerSelectionDialog(SettingsDialog): # Translators: This is the label for the synthesizer selection dialog title = _("Select Synthesizer") + helpId = "SynthesizerSelection" synthNames = [] def makeSettings(self, settingsSizer): @@ -951,6 +975,7 @@ def makeSettings(self, settingsSizer): # synthesizer combobox in the synthesizer dialog. synthListLabelText=_("&Synthesizer:") self.synthList = settingsSizerHelper.addLabeledControl(synthListLabelText, wx.Choice, choices=[]) + self.bindHelpEvent("SelectSynthesizerSynthesizer", self.synthList) self.updateSynthesizerList() # Translators: This is the label for the select output @@ -964,7 +989,7 @@ def makeSettings(self, settingsSizer): # Translators: name for default (Microsoft Sound Mapper) audio output device. deviceNames[0] = _("Microsoft Sound Mapper") self.deviceList = settingsSizerHelper.addLabeledControl(deviceListLabelText, wx.Choice, choices=deviceNames) - + self.bindHelpEvent("SelectSynthesizerOutputDevice", self.deviceList) try: selection = deviceNames.index(config.conf["speech"]["outputDevice"]) except ValueError: @@ -974,6 +999,7 @@ def makeSettings(self, settingsSizer): # Translators: This is a label for the audio ducking combo box in the Synthesizer Settings dialog. duckingListLabelText = _("Audio d&ucking mode:") self.duckingList=settingsSizerHelper.addLabeledControl(duckingListLabelText, wx.Choice, choices=audioDucking.audioDuckingModes) + self.bindHelpEvent("SelectSynthesizerDuckingMode", self.duckingList) index=config.conf['audio']['audioDuckingMode'] self.duckingList.SetSelection(index) if not audioDucking.isAudioDuckingSupported(): @@ -1190,6 +1216,11 @@ def _makeStringSettingControl( ) lCombo = labeledControl.control setattr(self, f"{setting.id}List", lCombo) + self.bindHelpEvent( + f"SpeechSettings{setting.displayName.capitalize()}", + lCombo + ) + try: cur = getattr(settingsStorage, setting.id) selectionIndex = [ @@ -1218,6 +1249,7 @@ def _makeBooleanSettingControl( checkbox = wx.CheckBox(self, label=setting.displayNameWithAccelerator) setattr(self, f"{setting.id}Checkbox", checkbox) settingsStorageProxy = weakref.proxy(settingsStorage) + self.bindHelpEvent(f"SpeechSettings{setting.displayName.capitalize()}", checkbox) def _onCheckChanged(evt: wx.CommandEvent): evt.Skip() # allow other handlers to also process this event. @@ -1346,6 +1378,7 @@ def onPanelActivated(self): class VoiceSettingsPanel(AutoSettingsMixin, SettingsPanel): # Translators: This is the label for the voice settings panel. title = _("Voice") + helpId = "SpeechSettings" @property def driver(self): @@ -1368,6 +1401,7 @@ def makeSettings(self, settingsSizer): self, label=autoLanguageSwitchingText )) + self.bindHelpEvent("SpeechSettingsLanguageSwitching", self.autoLanguageSwitchingCheckbox) self.autoLanguageSwitchingCheckbox.SetValue( config.conf["speech"]["autoLanguageSwitching"] ) @@ -1379,6 +1413,7 @@ def makeSettings(self, settingsSizer): self.autoDialectSwitchingCheckbox = settingsSizerHelper.addItem( wx.CheckBox(self, label=autoDialectSwitchingText) ) + self.bindHelpEvent("SpeechSettingsDialectSwitching", self.autoDialectSwitchingCheckbox) self.autoDialectSwitchingCheckbox.SetValue( config.conf["speech"]["autoDialectSwitching"] ) @@ -1393,6 +1428,7 @@ def makeSettings(self, settingsSizer): self.symbolLevelList = settingsSizerHelper.addLabeledControl( punctuationLabelText, wx.Choice, choices=symbolLevelChoices ) + self.bindHelpEvent("SpeechSettingsSymbolLevel", self.symbolLevelList) curLevel = config.conf["speech"]["symbolLevel"] self.symbolLevelList.SetSelection( characterProcessing.CONFIGURABLE_SPEECH_SYMBOL_LEVELS.index(curLevel) @@ -1404,6 +1440,7 @@ def makeSettings(self, settingsSizer): self.trustVoiceLanguageCheckbox = settingsSizerHelper.addItem( wx.CheckBox(self, label=trustVoiceLanguageText) ) + self.bindHelpEvent("SpeechSettingsTrust", self.trustVoiceLanguageCheckbox) self.trustVoiceLanguageCheckbox.SetValue(config.conf["speech"]["trustVoiceLanguage"]) includeCLDRText = _( @@ -1434,6 +1471,10 @@ def makeSettings(self, settingsSizer): min=minPitchChange, max=maxPitchChange, initial=config.conf["speech"][self.driver.name]["capPitchChange"]) + self.bindHelpEvent( + "SpeechSettingsCapPitchChange", + self.capPitchChangeEdit + ) # Translators: This is the label for a checkbox in the # voice settings panel. @@ -1441,6 +1482,7 @@ def makeSettings(self, settingsSizer): self.sayCapForCapsCheckBox = settingsSizerHelper.addItem( wx.CheckBox(self, label=sayCapForCapsText) ) + self.bindHelpEvent("SpeechSettingsSayCapBefore", self.sayCapForCapsCheckBox) self.sayCapForCapsCheckBox.SetValue( config.conf["speech"][self.driver.name]["sayCapForCapitals"] ) @@ -1451,6 +1493,10 @@ def makeSettings(self, settingsSizer): self.beepForCapsCheckBox = settingsSizerHelper.addItem( wx.CheckBox(self, label=beepForCapsText) ) + self.bindHelpEvent( + "SpeechSettingsBeepForCaps", + self.beepForCapsCheckBox + ) self.beepForCapsCheckBox.SetValue( config.conf["speech"][self.driver.name]["beepForCapitals"] ) @@ -1461,6 +1507,7 @@ def makeSettings(self, settingsSizer): self.useSpellingFunctionalityCheckBox = settingsSizerHelper.addItem( wx.CheckBox(self, label=useSpellingFunctionalityText) ) + self.bindHelpEvent("SpeechSettingsUseSpelling", self.useSpellingFunctionalityCheckBox) self.useSpellingFunctionalityCheckBox.SetValue( config.conf["speech"][self.driver.name]["useSpellingFunctionality"] ) @@ -1485,6 +1532,7 @@ def onSave(self): class KeyboardSettingsPanel(SettingsPanel): # Translators: This is the label for the keyboard settings panel. title = _("Keyboard") + helpId = "KeyboardSettings" def makeSettings(self, settingsSizer): sHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) @@ -1495,6 +1543,7 @@ def makeSettings(self, settingsSizer): self.kbdNames=sorted(layouts) kbdChoices = [layouts[layout] for layout in self.kbdNames] self.kbdList=sHelper.addLabeledControl(kbdLabelText, wx.Choice, choices=kbdChoices) + self.bindHelpEvent("KeyboardSettingsLayout", self.kbdList) try: index=self.kbdNames.index(config.conf['keyboard']['keyboardLayout']) self.kbdList.SetSelection(index) @@ -1509,28 +1558,37 @@ def makeSettings(self, settingsSizer): checkedItems = [] if config.conf["keyboard"]["useNumpadInsertAsNVDAModifierKey"]: checkedItems.append(keyboardHandler.SUPPORTED_NVDA_MODIFIER_KEYS.index("numpadinsert")) + if config.conf["keyboard"]["useExtendedInsertAsNVDAModifierKey"]: checkedItems.append(keyboardHandler.SUPPORTED_NVDA_MODIFIER_KEYS.index("insert")) if config.conf["keyboard"]["useCapsLockAsNVDAModifierKey"]: checkedItems.append(keyboardHandler.SUPPORTED_NVDA_MODIFIER_KEYS.index("capslock")) self.modifierList.CheckedItems = checkedItems self.modifierList.Select(0) + + self.bindHelpEvent("KeyboardSettingsModifiers", self.modifierList) # Translators: This is the label for a checkbox in the # keyboard settings panel. charsText = _("Speak typed &characters") self.charsCheckBox=sHelper.addItem(wx.CheckBox(self,label=charsText)) + self.bindHelpEvent( + "KeyboardSettingsSpeakTypedCharacters", + self.charsCheckBox + ) self.charsCheckBox.SetValue(config.conf["keyboard"]["speakTypedCharacters"]) # Translators: This is the label for a checkbox in the # keyboard settings panel. speakTypedWordsText = _("Speak typed &words") self.wordsCheckBox=sHelper.addItem(wx.CheckBox(self,label=speakTypedWordsText)) + self.bindHelpEvent("KeyboardSettingsSpeakTypedWords", self.wordsCheckBox) self.wordsCheckBox.SetValue(config.conf["keyboard"]["speakTypedWords"]) # Translators: This is the label for a checkbox in the # keyboard settings panel. speechInterruptForCharText = _("Speech &interrupt for typed characters") self.speechInterruptForCharsCheckBox=sHelper.addItem(wx.CheckBox(self,label=speechInterruptForCharText)) + self.bindHelpEvent("KeyboardSettingsSpeechInteruptForCharacters", self.speechInterruptForCharsCheckBox) self.speechInterruptForCharsCheckBox.SetValue(config.conf["keyboard"]["speechInterruptForCharacters"]) # Translators: This is the label for a checkbox in the @@ -1538,29 +1596,35 @@ def makeSettings(self, settingsSizer): speechInterruptForEnterText = _("Speech i&nterrupt for Enter key") self.speechInterruptForEnterCheckBox=sHelper.addItem(wx.CheckBox(self,label=speechInterruptForEnterText)) self.speechInterruptForEnterCheckBox.SetValue(config.conf["keyboard"]["speechInterruptForEnter"]) + self.bindHelpEvent("KeyboardSettingsSpeechInteruptForEnter", self.speechInterruptForEnterCheckBox) # Translators: This is the label for a checkbox in the # keyboard settings panel. allowSkimReadingInSayAllText = _("Allow skim &reading in Say All") self.skimReadingInSayAllCheckBox=sHelper.addItem(wx.CheckBox(self,label=allowSkimReadingInSayAllText)) + self.bindHelpEvent("KeyboardSettingsSkimReading", self.skimReadingInSayAllCheckBox) + self.skimReadingInSayAllCheckBox.SetValue(config.conf["keyboard"]["allowSkimReadingInSayAll"]) # Translators: This is the label for a checkbox in the # keyboard settings panel. beepForLowercaseWithCapsLockText = _("&Beep if typing lowercase letters when caps lock is on") self.beepLowercaseCheckBox=sHelper.addItem(wx.CheckBox(self,label=beepForLowercaseWithCapsLockText)) + self.bindHelpEvent("SpeechSettingsBeepLowercase", self.beepLowercaseCheckBox) self.beepLowercaseCheckBox.SetValue(config.conf["keyboard"]["beepForLowercaseWithCapslock"]) # Translators: This is the label for a checkbox in the # keyboard settings panel. commandKeysText = _("Speak c&ommand keys") self.commandKeysCheckBox=sHelper.addItem(wx.CheckBox(self,label=commandKeysText)) + self.bindHelpEvent("SpeechSettingsSpeakCommandKeys", self.commandKeysCheckBox) self.commandKeysCheckBox.SetValue(config.conf["keyboard"]["speakCommandKeys"]) # Translators: This is the label for a checkbox in the # keyboard settings panel. alertForSpellingErrorsText = _("Play sound for &spelling errors while typing") self.alertForSpellingErrorsCheckBox=sHelper.addItem(wx.CheckBox(self,label=alertForSpellingErrorsText)) + self.bindHelpEvent("KeyboardSettingsAlertForSpellingErrors", self.alertForSpellingErrorsCheckBox) self.alertForSpellingErrorsCheckBox.SetValue(config.conf["keyboard"]["alertForSpellingErrors"]) if not config.conf["documentFormatting"]["reportSpellingErrors"]: self.alertForSpellingErrorsCheckBox.Disable() @@ -1569,6 +1633,7 @@ def makeSettings(self, settingsSizer): # keyboard settings panel. handleInjectedKeysText = _("Handle keys from other &applications") self.handleInjectedKeysCheckBox=sHelper.addItem(wx.CheckBox(self,label=handleInjectedKeysText)) + self.bindHelpEvent("KeyboardSettingsHandleKeys", self.handleInjectedKeysCheckBox) self.handleInjectedKeysCheckBox.SetValue(config.conf["keyboard"]["handleInjectedKeys"]) def isValid(self): @@ -1602,6 +1667,7 @@ def onSave(self): class MouseSettingsPanel(SettingsPanel): # Translators: This is the label for the mouse settings panel. title = _("Mouse") + helpId = "MouseSettings" def makeSettings(self, settingsSizer): sHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) @@ -1610,12 +1676,14 @@ def makeSettings(self, settingsSizer): # mouse settings panel. shapeChangesText = _("Report mouse &shape changes") self.shapeCheckBox=sHelper.addItem(wx.CheckBox(self,label=shapeChangesText)) + self.bindHelpEvent("MouseSettingsShape", self.shapeCheckBox) self.shapeCheckBox.SetValue(config.conf["mouse"]["reportMouseShapeChanges"]) # Translators: This is the label for a checkbox in the # mouse settings panel. mouseTrackingText=_("Enable mouse &tracking") self.mouseTrackingCheckBox=sHelper.addItem(wx.CheckBox(self,label=mouseTrackingText)) + self.bindHelpEvent("MouseSettingsTracking", self.mouseTrackingCheckBox) self.mouseTrackingCheckBox.SetValue(config.conf["mouse"]["enableMouseTracking"]) # Translators: This is the label for a combobox in the @@ -1625,6 +1693,7 @@ def makeSettings(self, settingsSizer): self.textUnits=textInfos.MOUSE_TEXT_RESOLUTION_UNITS textUnitsChoices = [textInfos.unitLabels[x] for x in self.textUnits] self.textUnitComboBox=sHelper.addLabeledControl(textUnitLabelText, wx.Choice, choices=textUnitsChoices) + self.bindHelpEvent("MouseSettingsTextUnit", self.textUnitComboBox) try: index=self.textUnits.index(config.conf["mouse"]["mouseTextUnit"]) except: @@ -1635,18 +1704,21 @@ def makeSettings(self, settingsSizer): # mouse settings panel. reportObjectRoleText = _("Report &role when mouse enters object") self.reportObjectRoleCheckBox=sHelper.addItem(wx.CheckBox(self,label=reportObjectRoleText)) + self.bindHelpEvent("MouseSettingsRole", self.reportObjectRoleCheckBox) self.reportObjectRoleCheckBox.SetValue(config.conf["mouse"]["reportObjectRoleOnMouseEnter"]) # Translators: This is the label for a checkbox in the # mouse settings panel. audioText = _("&Play audio coordinates when mouse moves") self.audioCheckBox=sHelper.addItem(wx.CheckBox(self,label=audioText)) + self.bindHelpEvent("MouseSettingsAudio", self.audioCheckBox) self.audioCheckBox.SetValue(config.conf["mouse"]["audioCoordinatesOnMouseMove"]) # Translators: This is the label for a checkbox in the # mouse settings panel. audioDetectBrightnessText = _("&Brightness controls audio coordinates volume") self.audioDetectBrightnessCheckBox=sHelper.addItem(wx.CheckBox(self,label=audioDetectBrightnessText)) + self.bindHelpEvent("MouseSettingsBrightness", self.audioDetectBrightnessCheckBox) self.audioDetectBrightnessCheckBox.SetValue(config.conf["mouse"]["audioCoordinates_detectBrightness"]) # Translators: This is the label for a checkbox in the @@ -1667,26 +1739,31 @@ def onSave(self): class ReviewCursorPanel(SettingsPanel): # Translators: This is the label for the review cursor settings panel. title = _("Review Cursor") + helpId = "ReviewCursorSettings" def makeSettings(self, settingsSizer): # Translators: This is the label for a checkbox in the # review cursor settings panel. self.followFocusCheckBox = wx.CheckBox(self, label=_("Follow system &focus")) + self.bindHelpEvent("ReviewCursorFollowFocus", self.followFocusCheckBox) self.followFocusCheckBox.SetValue(config.conf["reviewCursor"]["followFocus"]) settingsSizer.Add(self.followFocusCheckBox,border=10,flag=wx.BOTTOM) # Translators: This is the label for a checkbox in the # review cursor settings panel. self.followCaretCheckBox = wx.CheckBox(self, label=_("Follow System &Caret")) + self.bindHelpEvent("ReviewCursorFollowCaret", self.followCaretCheckBox) self.followCaretCheckBox.SetValue(config.conf["reviewCursor"]["followCaret"]) settingsSizer.Add(self.followCaretCheckBox,border=10,flag=wx.BOTTOM) # Translators: This is the label for a checkbox in the # review cursor settings panel. self.followMouseCheckBox = wx.CheckBox(self, label=_("Follow &mouse cursor")) + self.bindHelpEvent("ReviewCursorFollowMouse", self.followMouseCheckBox) self.followMouseCheckBox.SetValue(config.conf["reviewCursor"]["followMouse"]) settingsSizer.Add(self.followMouseCheckBox,border=10,flag=wx.BOTTOM) # Translators: This is the label for a checkbox in the # review cursor settings panel. self.simpleReviewModeCheckBox = wx.CheckBox(self, label=_("&Simple review mode")) + self.bindHelpEvent("ReviewCursorSimple", self.simpleReviewModeCheckBox) self.simpleReviewModeCheckBox.SetValue(config.conf["reviewCursor"]["simpleReviewMode"]) settingsSizer.Add(self.simpleReviewModeCheckBox,border=10,flag=wx.BOTTOM) @@ -1696,34 +1773,50 @@ def onSave(self): config.conf["reviewCursor"]["followMouse"]=self.followMouseCheckBox.IsChecked() config.conf["reviewCursor"]["simpleReviewMode"]=self.simpleReviewModeCheckBox.IsChecked() + class InputCompositionPanel(SettingsPanel): # Translators: This is the label for the Input Composition settings panel. title = _("Input Composition") + helpId = "InputCompositionSettings" def makeSettings(self, settingsSizer): # Translators: This is the label for a checkbox in the # Input composition settings panel. self.autoReportAllCandidatesCheckBox=wx.CheckBox(self,wx.ID_ANY,label=_("Automatically report all available &candidates")) + self.bindHelpEvent("InputCompositionReportAllCandidates", self.autoReportAllCandidatesCheckBox) self.autoReportAllCandidatesCheckBox.SetValue(config.conf["inputComposition"]["autoReportAllCandidates"]) settingsSizer.Add(self.autoReportAllCandidatesCheckBox,border=10,flag=wx.BOTTOM) # Translators: This is the label for a checkbox in the # Input composition settings panel. self.announceSelectedCandidateCheckBox=wx.CheckBox(self,wx.ID_ANY,label=_("Announce &selected candidate")) + self.bindHelpEvent("InputCompositionAnnounceSelectedCandidate", self.announceSelectedCandidateCheckBox) self.announceSelectedCandidateCheckBox.SetValue(config.conf["inputComposition"]["announceSelectedCandidate"]) settingsSizer.Add(self.announceSelectedCandidateCheckBox,border=10,flag=wx.BOTTOM) # Translators: This is the label for a checkbox in the # Input composition settings panel. self.candidateIncludesShortCharacterDescriptionCheckBox=wx.CheckBox(self,wx.ID_ANY,label=_("Always include short character &description when announcing candidates")) + self.bindHelpEvent( + "InputCompositionCandidateIncludesShortCharacterDescription", + self.candidateIncludesShortCharacterDescriptionCheckBox + ) self.candidateIncludesShortCharacterDescriptionCheckBox.SetValue(config.conf["inputComposition"]["alwaysIncludeShortCharacterDescriptionInCandidateName"]) settingsSizer.Add(self.candidateIncludesShortCharacterDescriptionCheckBox,border=10,flag=wx.BOTTOM) # Translators: This is the label for a checkbox in the # Input composition settings panel. self.reportReadingStringChangesCheckBox=wx.CheckBox(self,wx.ID_ANY,label=_("Report changes to the &reading string")) + self.bindHelpEvent( + "InputCompositionReadingStringChanges", + self.reportReadingStringChangesCheckBox + ) self.reportReadingStringChangesCheckBox.SetValue(config.conf["inputComposition"]["reportReadingStringChanges"]) settingsSizer.Add(self.reportReadingStringChangesCheckBox,border=10,flag=wx.BOTTOM) # Translators: This is the label for a checkbox in the # Input composition settings panel. self.reportCompositionStringChangesCheckBox=wx.CheckBox(self,wx.ID_ANY,label=_("Report changes to the &composition string")) + self.bindHelpEvent( + "InputCompositionCompositionStringChanges", + self.reportCompositionStringChangesCheckBox + ) self.reportCompositionStringChangesCheckBox.SetValue(config.conf["inputComposition"]["reportCompositionStringChanges"]) settingsSizer.Add(self.reportCompositionStringChangesCheckBox,border=10,flag=wx.BOTTOM) @@ -1734,9 +1827,11 @@ def onSave(self): config.conf["inputComposition"]["reportReadingStringChanges"]=self.reportReadingStringChangesCheckBox.IsChecked() config.conf["inputComposition"]["reportCompositionStringChanges"]=self.reportCompositionStringChangesCheckBox.IsChecked() + class ObjectPresentationPanel(SettingsPanel): # Translators: This is the label for the object presentation panel. title = _("Object Presentation") + helpId = "ObjectPresentationSettings" progressLabels = ( # Translators: An option for progress bar output in the Object Presentation dialog # which disables reporting of progress bars. @@ -1762,36 +1857,42 @@ def makeSettings(self, settingsSizer): # object presentation settings panel. reportToolTipsText = _("Report &tooltips") self.tooltipCheckBox=sHelper.addItem(wx.CheckBox(self,label=reportToolTipsText)) + self.bindHelpEvent("ObjectPresentationReportToolTips", self.tooltipCheckBox) self.tooltipCheckBox.SetValue(config.conf["presentation"]["reportTooltips"]) # Translators: This is the label for a checkbox in the # object presentation settings panel. balloonText = _("Report ¬ifications") self.balloonCheckBox=sHelper.addItem(wx.CheckBox(self,label=balloonText)) + self.bindHelpEvent("ObjectPresentationReportBalloons", self.balloonCheckBox) self.balloonCheckBox.SetValue(config.conf["presentation"]["reportHelpBalloons"]) # Translators: This is the label for a checkbox in the # object presentation settings panel. shortcutText = _("Report object shortcut &keys") self.shortcutCheckBox=sHelper.addItem(wx.CheckBox(self,label=shortcutText)) + self.bindHelpEvent("ObjectPresentationShortcutKeys", self.shortcutCheckBox) self.shortcutCheckBox.SetValue(config.conf["presentation"]["reportKeyboardShortcuts"]) # Translators: This is the label for a checkbox in the # object presentation settings panel. positionInfoText = _("Report object &position information") self.positionInfoCheckBox=sHelper.addItem(wx.CheckBox(self,label=positionInfoText)) + self.bindHelpEvent("ObjectPresentationPositionInfo", self.positionInfoCheckBox) self.positionInfoCheckBox.SetValue(config.conf["presentation"]["reportObjectPositionInformation"]) # Translators: This is the label for a checkbox in the # object presentation settings panel. guessPositionInfoText = _("&Guess object position information when unavailable") self.guessPositionInfoCheckBox=sHelper.addItem(wx.CheckBox(self,label=guessPositionInfoText)) + self.bindHelpEvent("ObjectPresentationGuessPositionInfo", self.guessPositionInfoCheckBox) self.guessPositionInfoCheckBox.SetValue(config.conf["presentation"]["guessObjectPositionInformationWhenUnavailable"]) # Translators: This is the label for a checkbox in the # object presentation settings panel. descriptionText = _("Report object &descriptions") self.descriptionCheckBox=sHelper.addItem(wx.CheckBox(self,label=descriptionText)) + self.bindHelpEvent("ObjectPresentationReportDescriptions", self.descriptionCheckBox) self.descriptionCheckBox.SetValue(config.conf["presentation"]["reportObjectDescriptions"]) # Translators: This is the label for a combobox in the @@ -1799,6 +1900,7 @@ def makeSettings(self, settingsSizer): progressLabelText = _("Progress &bar output:") progressChoices = [name for setting, name in self.progressLabels] self.progressList=sHelper.addLabeledControl(progressLabelText, wx.Choice, choices=progressChoices) + self.bindHelpEvent("ObjectPresentationProgressBarOutput", self.progressList) for index, (setting, name) in enumerate(self.progressLabels): if setting == config.conf["presentation"]["progressBarUpdates"]["progressBarOutputMode"]: self.progressList.SetSelection(index) @@ -1810,18 +1912,30 @@ def makeSettings(self, settingsSizer): # object presentation settings panel. reportBackgroundProgressBarsText = _("Report backg&round progress bars") self.reportBackgroundProgressBarsCheckBox=sHelper.addItem(wx.CheckBox(self,label=reportBackgroundProgressBarsText)) + self.bindHelpEvent( + "ObjectPresentationReportBackgroundProgressBars", + self.reportBackgroundProgressBarsCheckBox + ) self.reportBackgroundProgressBarsCheckBox.SetValue(config.conf["presentation"]["progressBarUpdates"]["reportBackgroundProgressBars"]) # Translators: This is the label for a checkbox in the # object presentation settings panel. dynamicContentText = _("Report dynamic &content changes") self.dynamicContentCheckBox=sHelper.addItem(wx.CheckBox(self,label=dynamicContentText)) + self.bindHelpEvent( + "ObjectPresentationReportDynamicContent", + self.dynamicContentCheckBox + ) self.dynamicContentCheckBox.SetValue(config.conf["presentation"]["reportDynamicContentChanges"]) # Translators: This is the label for a checkbox in the # object presentation settings panel. autoSuggestionsLabelText = _("Play a sound when &auto-suggestions appear") self.autoSuggestionSoundsCheckBox=sHelper.addItem(wx.CheckBox(self,label=autoSuggestionsLabelText)) + self.bindHelpEvent( + "ObjectPresentationSuggestionSounds", + self.autoSuggestionSoundsCheckBox + ) self.autoSuggestionSoundsCheckBox.SetValue(config.conf["presentation"]["reportAutoSuggestionsWithSound"]) def onSave(self): @@ -1839,10 +1953,10 @@ def onSave(self): class BrowseModePanel(SettingsPanel): # Translators: This is the label for the browse mode settings panel. title = _("Browse Mode") + helpId = "BrowseModeSettings" def makeSettings(self, settingsSizer): sHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) - # Translators: This is the label for a textfield in the # browse mode settings panel. maxLengthLabelText = _("&Maximum number of characters on one line") @@ -1850,6 +1964,7 @@ def makeSettings(self, settingsSizer): # min and max are not enforced in the config for virtualBuffers.maxLineLength min=10, max=250, initial=config.conf["virtualBuffers"]["maxLineLength"]) + self.bindHelpEvent("BrowseModeSettingsMaxLength", self.maxLengthEdit) # Translators: This is the label for a textfield in the # browse mode settings panel. @@ -1858,11 +1973,13 @@ def makeSettings(self, settingsSizer): # min and max are not enforced in the config for virtualBuffers.linesPerPage min=5, max=150, initial=config.conf["virtualBuffers"]["linesPerPage"]) + self.bindHelpEvent("BrowseModeSettingsPageLines", self.pageLinesEdit) # Translators: This is the label for a checkbox in the # browse mode settings panel. useScreenLayoutText = _("Use &screen layout (when supported)") self.useScreenLayoutCheckBox = sHelper.addItem(wx.CheckBox(self, label=useScreenLayoutText)) + self.bindHelpEvent("BrowseModeSettingsScreenLayout", self.useScreenLayoutCheckBox) self.useScreenLayoutCheckBox.SetValue(config.conf["virtualBuffers"]["useScreenLayout"]) # Translators: The label for a checkbox in browse mode settings to @@ -1875,36 +1992,54 @@ def makeSettings(self, settingsSizer): # browse mode settings panel. autoSayAllText = _("Automatic &Say All on page load") self.autoSayAllCheckBox = sHelper.addItem(wx.CheckBox(self, label=autoSayAllText)) + self.bindHelpEvent("BrowseModeSettingsAutoSayAll", self.autoSayAllCheckBox) self.autoSayAllCheckBox.SetValue(config.conf["virtualBuffers"]["autoSayAllOnPageLoad"]) # Translators: This is the label for a checkbox in the # browse mode settings panel. layoutTablesText = _("Include l&ayout tables") self.layoutTablesCheckBox = sHelper.addItem(wx.CheckBox(self, label =layoutTablesText)) + self.bindHelpEvent("BrowseModeSettingsIncludeLayoutTables", self.layoutTablesCheckBox) self.layoutTablesCheckBox.SetValue(config.conf["documentFormatting"]["includeLayoutTables"]) # Translators: This is the label for a checkbox in the # browse mode settings panel. autoPassThroughOnFocusChangeText = _("Automatic focus mode for focus changes") self.autoPassThroughOnFocusChangeCheckBox = sHelper.addItem(wx.CheckBox(self, label=autoPassThroughOnFocusChangeText)) + self.bindHelpEvent( + "BrowseModeSettingsAutoPassThroughOnFocusChange", + self.autoPassThroughOnFocusChangeCheckBox + ) self.autoPassThroughOnFocusChangeCheckBox.SetValue(config.conf["virtualBuffers"]["autoPassThroughOnFocusChange"]) # Translators: This is the label for a checkbox in the # browse mode settings panel. autoPassThroughOnCaretMoveText = _("Automatic focus mode for caret movement") self.autoPassThroughOnCaretMoveCheckBox = sHelper.addItem(wx.CheckBox(self, label=autoPassThroughOnCaretMoveText)) + self.bindHelpEvent( + "BrowseModeSettingsAutoPassThroughOnCaretMove", + self.autoPassThroughOnCaretMoveCheckBox + ) self.autoPassThroughOnCaretMoveCheckBox.SetValue(config.conf["virtualBuffers"]["autoPassThroughOnCaretMove"]) # Translators: This is the label for a checkbox in the # browse mode settings panel. passThroughAudioIndicationText = _("Audio indication of focus and browse modes") self.passThroughAudioIndicationCheckBox = sHelper.addItem(wx.CheckBox(self, label=passThroughAudioIndicationText)) + self.bindHelpEvent( + "BrowseModeSettingsPassThroughAudioIndication", + self.passThroughAudioIndicationCheckBox + ) self.passThroughAudioIndicationCheckBox.SetValue(config.conf["virtualBuffers"]["passThroughAudioIndication"]) # Translators: This is the label for a checkbox in the # browse mode settings panel. trapNonCommandGesturesText = _("&Trap all non-command gestures from reaching the document") self.trapNonCommandGesturesCheckBox = sHelper.addItem(wx.CheckBox(self, label=trapNonCommandGesturesText)) + self.bindHelpEvent( + "BrowseModeSettingsTrapNonCommandGestures", + self.trapNonCommandGesturesCheckBox + ) self.trapNonCommandGesturesCheckBox.SetValue(config.conf["virtualBuffers"]["trapNonCommandGestures"]) # Translators: This is the label for a checkbox in the @@ -1936,6 +2071,7 @@ def onSave(self): class DocumentFormattingPanel(SettingsPanel): # Translators: This is the label for the document formatting panel. title = _("Document Formatting") + helpId = "DocumentFormattingSettings" # Translators: This is a label appearing on the document formatting settings panel. panelDescription = _("The following options control the types of document formatting reported by NVDA.") @@ -2063,6 +2199,10 @@ def makeSettings(self, settingsSizer): _("Both Speech and Tones") ] self.lineIndentationCombo = pageAndSpaceGroup.addLabeledControl(lineIndentationText, wx.Choice, choices=indentChoices) + self.bindHelpEvent( + "DocumentFormattingSettingsLineIndentation", + self.lineIndentationCombo + ) #We use bitwise operations because it saves us a four way if statement. curChoice = config.conf["documentFormatting"]["reportLineIndentationWithTones"] << 1 | config.conf["documentFormatting"]["reportLineIndentation"] self.lineIndentationCombo.SetSelection(curChoice) @@ -2194,6 +2334,10 @@ def makeSettings(self, settingsSizer): # document formatting settings panel. detectFormatAfterCursorText = _("Report formatting chan&ges after the cursor (can cause a lag)") self.detectFormatAfterCursorCheckBox=wx.CheckBox(self, label=detectFormatAfterCursorText) + self.bindHelpEvent( + "DocumentFormattingDetectFormatAfterCursor", + self.detectFormatAfterCursorCheckBox + ) self.detectFormatAfterCursorCheckBox.SetValue(config.conf["documentFormatting"]["detectFormatAfterCursor"]) sHelper.addItem(self.detectFormatAfterCursorCheckBox) @@ -2258,9 +2402,11 @@ def onSave(self): config.conf["touch"]["touchTyping"] = self.touchTypingCheckBox.IsChecked() touchHandler.setTouchSupport(config.conf["touch"]["enabled"]) + class UwpOcrPanel(SettingsPanel): # Translators: The title of the Windows 10 OCR panel. title = _("Windows 10 OCR") + helpId = "Win10OcrSettings" def makeSettings(self, settingsSizer): sHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) @@ -2273,6 +2419,7 @@ def makeSettings(self, settingsSizer): # Translators: Label for an option in the Windows 10 OCR dialog. languageLabel = _("Recognition &language:") self.languageChoice = sHelper.addLabeledControl(languageLabel, wx.Choice, choices=languageChoices) + self.bindHelpEvent("Win10OcrSettingsRecognitionLanguage", self.languageChoice) try: langIndex = self.languageCodes.index(config.conf["uwpOcr"]["language"]) self.languageChoice.Selection = langIndex @@ -2681,8 +2828,10 @@ def onOk(self,evt): def setType(self, type): self.typeRadioBox.SetSelection(DictionaryEntryDialog.TYPE_LABELS_ORDERING.index(type)) + class DictionaryDialog(SettingsDialog): TYPE_LABELS = {t: l.replace("&", "") for t, l in DictionaryEntryDialog.TYPE_LABELS.items()} + helpId = "SpeechDictionaries" def __init__(self,parent,title,speechDict): self.title = title @@ -2802,11 +2951,14 @@ def OnRemoveClick(self,evt): index=self.dictList.GetNextSelected(index) self.dictList.SetFocus() + class BrailleSettingsPanel(SettingsPanel): # Translators: This is the label for the braille panel title = _("Braille") + helpId = "BrailleSettings" def makeSettings(self, settingsSizer): + settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) # Translators: A label for the braille display on the braille panel. displayLabel = _("Braille &display") @@ -2868,9 +3020,11 @@ def onDiscard(self): def onSave(self): self.brailleSubPanel.onSave() + class BrailleDisplaySelectionDialog(SettingsDialog): # Translators: This is the label for the braille display selection dialog. title = _("Select Braille Display") + helpId = "BrailleSettings" displayNames = [] possiblePorts = [] @@ -2885,6 +3039,7 @@ def makeSettings(self, settingsSizer): # Translators: The label for a setting in braille settings to choose the connection port (if the selected braille display supports port selection). portsLabelText = _("&Port:") self.portsList = sHelper.addLabeledControl(portsLabelText, wx.Choice, choices=[]) + self.bindHelpEvent("BrailleSettingsPort", self.portsList) self.updateBrailleDisplayLists() @@ -2909,6 +3064,7 @@ def updateBrailleDisplayLists(self): displayChoices = [driver[1] for driver in driverList] self.displayList.Clear() self.displayList.AppendItems(displayChoices) + self.bindHelpEvent("BrailleSettingsDisplay", self.displayList) try: if config.conf["braille"]["display"] == braille.AUTO_DISPLAY_NAME: selection = 0 @@ -3000,6 +3156,7 @@ def makeSettings(self, settingsSizer): self.outTableNames = [table.fileName for table in outTables] outTableChoices = [table.displayName for table in outTables] self.outTableList = sHelper.addLabeledControl(outputsLabelText, wx.Choice, choices=outTableChoices) + self.bindHelpEvent("BrailleSettingsOutputTable", self.outTableList) try: selection = self.outTableNames.index(config.conf["braille"]["translationTable"]) self.outTableList.SetSelection(selection) @@ -3016,6 +3173,7 @@ def makeSettings(self, settingsSizer): self.inTables = [table for table in tables if table.input] inTableChoices = [table.displayName for table in self.inTables] self.inTableList = sHelper.addLabeledControl(inputLabelText, wx.Choice, choices=inTableChoices) + self.bindHelpEvent("BrailleSettingsInputTable", self.inTableList) try: selection = self.inTables.index(brailleInput.handler.table) self.inTableList.SetSelection(selection) @@ -3032,11 +3190,13 @@ def makeSettings(self, settingsSizer): self.expandAtCursorCheckBox = sHelper.addItem( wx.CheckBox(self, wx.ID_ANY, label=expandAtCursorText) ) + self.bindHelpEvent("BrailleSettingsExpandToComputerBraille", self.expandAtCursorCheckBox) self.expandAtCursorCheckBox.SetValue(config.conf["braille"]["expandAtCursor"]) # Translators: The label for a setting in braille settings to show the cursor. showCursorLabelText = _("&Show cursor") self.showCursorCheckBox = sHelper.addItem(wx.CheckBox(self, label=showCursorLabelText)) + self.bindHelpEvent("BrailleSettingsShowCursor", self.showCursorCheckBox) self.showCursorCheckBox.Bind(wx.EVT_CHECKBOX, self.onShowCursorChange) self.showCursorCheckBox.SetValue(config.conf["braille"]["showCursor"]) @@ -3045,6 +3205,7 @@ def makeSettings(self, settingsSizer): self.cursorBlinkCheckBox = sHelper.addItem( wx.CheckBox(self, label=cursorBlinkLabelText) ) + self.bindHelpEvent("BrailleSettingsBlinkCursor", self.cursorBlinkCheckBox) self.cursorBlinkCheckBox.Bind(wx.EVT_CHECKBOX, self.onBlinkCursorChange) self.cursorBlinkCheckBox.SetValue(config.conf["braille"]["cursorBlink"]) if not self.showCursorCheckBox.GetValue(): @@ -3063,6 +3224,7 @@ def makeSettings(self, settingsSizer): max=maxBlinkRate, initial=config.conf["braille"]["cursorBlinkRate"] ) + self.bindHelpEvent("BrailleSettingsBlinkRate", self.cursorBlinkRateEdit) if not self.showCursorCheckBox.GetValue() or not self.cursorBlinkCheckBox.GetValue() : self.cursorBlinkRateEdit.Disable() @@ -3072,6 +3234,7 @@ def makeSettings(self, settingsSizer): # Translators: The label for a setting in braille settings to select the cursor shape when tethered to focus. cursorShapeFocusLabelText = _("Cursor shape for &focus:") self.cursorShapeFocusList = sHelper.addLabeledControl(cursorShapeFocusLabelText, wx.Choice, choices=cursorShapeChoices) + self.bindHelpEvent("BrailleSettingsCursorShapeForFocus", self.cursorShapeFocusList) try: selection = self.cursorShapes.index(config.conf["braille"]["cursorShapeFocus"]) self.cursorShapeFocusList.SetSelection(selection) @@ -3083,6 +3246,7 @@ def makeSettings(self, settingsSizer): # Translators: The label for a setting in braille settings to select the cursor shape when tethered to review. cursorShapeReviewLabelText = _("Cursor shape for &review:") self.cursorShapeReviewList = sHelper.addLabeledControl(cursorShapeReviewLabelText, wx.Choice, choices=cursorShapeChoices) + self.bindHelpEvent("BrailleSettingsCursorShapeForReview", self.cursorShapeReviewList) try: selection = self.cursorShapes.index(config.conf["braille"]["cursorShapeReview"]) self.cursorShapeReviewList.SetSelection(selection) @@ -3109,10 +3273,12 @@ def makeSettings(self, settingsSizer): max=maxTimeOut, initial=config.conf["braille"]["messageTimeout"] ) + self.bindHelpEvent("BrailleSettingsMessageTimeout", self.messageTimeoutEdit) # Translators: The label for a setting in braille settings to display a message on the braille display indefinitely. noMessageTimeoutLabelText = _("Show &messages indefinitely") self.noMessageTimeoutCheckBox = sHelper.addItem(wx.CheckBox(self, label=noMessageTimeoutLabelText)) + self.bindHelpEvent("BrailleSettingsNoMessageTimeout", self.noMessageTimeoutCheckBox) self.noMessageTimeoutCheckBox.Bind(wx.EVT_CHECKBOX, self.onNoMessageTimeoutChange) self.noMessageTimeoutCheckBox.SetValue(config.conf["braille"]["noMessageTimeout"]) if self.noMessageTimeoutCheckBox.GetValue(): @@ -3126,6 +3292,7 @@ def makeSettings(self, settingsSizer): # Translators: The value for a setting in the braille settings, to set whether braille should be tethered to focus or review cursor. tetherChoices = [x[1] for x in braille.handler.tetherValues] self.tetherList = sHelper.addLabeledControl(tetherListText, wx.Choice, choices=tetherChoices) + self.bindHelpEvent("BrailleTether", self.tetherList) tetherChoice=braille.handler.TETHER_AUTO if config.conf["braille"]["autoTether"] else config.conf["braille"]["tetherTo"] selection = next((x for x,y in enumerate(braille.handler.tetherValues) if y[0]==tetherChoice)) try: @@ -3138,17 +3305,20 @@ def makeSettings(self, settingsSizer): # Translators: The label for a setting in braille settings to read by paragraph (if it is checked, the commands to move the display by lines moves the display by paragraphs instead). readByParagraphText = _("Read by ¶graph") self.readByParagraphCheckBox = sHelper.addItem(wx.CheckBox(self, label=readByParagraphText)) + self.bindHelpEvent("BrailleSettingsReadByParagraph", self.readByParagraphCheckBox) self.readByParagraphCheckBox.Value = config.conf["braille"]["readByParagraph"] # Translators: The label for a setting in braille settings to enable word wrap (try to avoid spliting words at the end of the braille display). wordWrapText = _("Avoid splitting &words when possible") self.wordWrapCheckBox = sHelper.addItem(wx.CheckBox(self, label=wordWrapText)) + self.bindHelpEvent("BrailleSettingsWordWrap", self.wordWrapCheckBox) self.wordWrapCheckBox.Value = config.conf["braille"]["wordWrap"] # Translators: The label for a setting in braille settings to select how the context for the focus object should be presented on a braille display. focusContextPresentationLabelText = _("Focus context presentation:") self.focusContextPresentationValues = [x[0] for x in braille.focusContextPresentations] focusContextPresentationChoices = [x[1] for x in braille.focusContextPresentations] self.focusContextPresentationList = sHelper.addLabeledControl(focusContextPresentationLabelText, wx.Choice, choices=focusContextPresentationChoices) + self.bindHelpEvent("BrailleSettingsFocusContextPresentation", self.focusContextPresentationList) try: index=self.focusContextPresentationValues.index(config.conf["braille"]["focusContextPresentation"]) except: @@ -3644,6 +3814,10 @@ def _doOnCategoryChange(self): # Translators: The profile name for normal configuration NvdaSettingsDialogActiveConfigProfile = _("normal configuration") self.SetTitle(self._getDialogTitle()) + self.bindHelpEvent( + self.currentCategory.helpId, + self.catListCtrl + ) def _getDialogTitle(self): return u"{dialogTitle}: {panelTitle} ({configProfile})".format( @@ -3664,11 +3838,17 @@ def Destroy(self): NvdaSettingsDialogWindowHandle = None super(NVDASettingsDialog, self).Destroy() -class AddSymbolDialog(wx.Dialog): +class AddSymbolDialog( + gui.ContextHelpMixin, + wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO +): + + helpId = "SymbolPronunciation" + def __init__(self, parent): # Translators: This is the label for the add symbol dialog. - super(AddSymbolDialog,self).__init__(parent, title=_("Add Symbol")) + super().__init__(parent, title=_("Add Symbol")) mainSizer=wx.BoxSizer(wx.VERTICAL) sHelper = guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) @@ -3684,7 +3864,9 @@ def __init__(self, parent): self.identifierTextCtrl.SetFocus() self.CentreOnScreen() + class SpeechSymbolsDialog(SettingsDialog): + helpId = "SymbolPronunciation" def __init__(self,parent): try: diff --git a/source/speechViewer.py b/source/speechViewer.py index 323433e89ca..7109b55eead 100644 --- a/source/speechViewer.py +++ b/source/speechViewer.py @@ -1,21 +1,25 @@ -#speechViewer.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2006-2018 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, Thomas Stivers +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. import wx import gui import config from logHandler import log from speech import SpeechSequence +import gui.contextHelp # Inherit from wx.Frame because these windows show in the alt+tab menu (where miniFrame does not) # We have to manually add a wx.Panel to get correct tab ordering behaviour. # wx.Dialog causes a crash on destruction when multiple were created at the same time (brailleViewer # may start at the same time) -class SpeechViewerFrame(wx.Frame): +class SpeechViewerFrame( + gui.contextHelp.ContextHelpMixin, + wx.Frame # wxPython does not seem to call base class initializer, put last in MRO +): + helpId = "SpeechViewer" def _getDialogSizeAndPosition(self): dialogSize = wx.Size(500, 500) diff --git a/source/updateCheck.py b/source/updateCheck.py index b2d9a669957..d66a993546a 100644 --- a/source/updateCheck.py +++ b/source/updateCheck.py @@ -10,6 +10,7 @@ import garbageHandler import globalVars import config + if globalVars.appArgs.secure: raise RuntimeError("updates disabled in secure mode") elif config.isAppX: @@ -18,7 +19,9 @@ if not versionInfo.updateVersionType: raise RuntimeError("No update version type, update checking not supported") import addonAPIVersion - +# Avoid a E402 'module level import not at top of file' warning, because several checks are performed above. +from gui.contextHelp import ContextHelpMixin # noqa: E402 +from gui.dpiScalingHelper import DpiScalingHelperMixin, DpiScalingHelperMixinWithoutInit # noqa: E402 import winVersion import os import inspect @@ -46,7 +49,6 @@ import winUser import winKernel import fileUtils -from gui.dpiScalingHelper import DpiScalingHelperMixin #: The URL to use for update checks. CHECK_URL = "https://www.nvaccess.org/nvdaUpdateCheck" @@ -315,12 +317,17 @@ def _result(self, info): return wx.CallAfter(UpdateResultDialog, gui.mainFrame, info, True) -class UpdateResultDialog(wx.Dialog, DpiScalingHelperMixin): + +class UpdateResultDialog( + DpiScalingHelperMixinWithoutInit, + ContextHelpMixin, + wx.Dialog # wxPython does not seem to call base class initializer, put last in MRO +): + helpId = "GeneralSettingsCheckForUpdates" def __init__(self, parent, updateInfo, auto): # Translators: The title of the dialog informing the user about an NVDA update. - wx.Dialog.__init__(self, parent, title=_("NVDA Update")) - DpiScalingHelperMixin.__init__(self, self.GetHandle()) + super().__init__(parent, title=_("NVDA Update")) self.updateInfo = updateInfo mainSizer = wx.BoxSizer(wx.VERTICAL) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 551e163ab64..50cf0fa1973 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -6,6 +6,7 @@ What's New in NVDA = 2020.4 = == New Features == +- Pressing F1 inside NVDA dialogs will now open the help file to most relevant section. (#7757) == Changes == From 213a3c2d8574df90826aef0039b3d4509e21756b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Wed, 9 Sep 2020 18:10:50 +0200 Subject: [PATCH 07/27] Use DisplayModelEditableText in Fast Log Entry edit field (PR #11488) Fixes #8996 In #8165 we started using Unidentifiededit for editable text fields in which there is a caret and which seems to support Edit API. While both of these criterion's are true for Fast Log Entry edit fields their WindowText contains complete garbage and UnidentifiedEdit relies on Windowtext being correct. Instead: 1. For edit field in Fast Log Entry DisplayModelEditableText is used similar to the pre #8165 2. DisplayModelEditableText no longer inherits from EditableText rather from EditableTextWithoutAutoSelectDetection which makes selection announced when the color used for highlight in a particular app is the default Windows highlight color - This has been suggested by @jcsteh in #4535 --- source/NVDAObjects/window/__init__.py | 5 +++-- source/appModules/fastlogentry.py | 27 +++++++++++++++++++++++++++ user_docs/en/changes.t2t | 1 + 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 source/appModules/fastlogentry.py diff --git a/source/NVDAObjects/window/__init__.py b/source/NVDAObjects/window/__init__.py index 6f7ee3c6db9..9e73257a1f7 100644 --- a/source/NVDAObjects/window/__init__.py +++ b/source/NVDAObjects/window/__init__.py @@ -16,7 +16,7 @@ import displayModel import eventHandler from NVDAObjects import NVDAObject -from NVDAObjects.behaviors import EditableText, LiveText +from NVDAObjects.behaviors import EditableText, EditableTextWithoutAutoSelectDetection, LiveText import watchdog from locationHelper import RectLTWH @@ -390,7 +390,8 @@ class Desktop(Window): def _get_name(self): return _("Desktop") -class DisplayModelEditableText(EditableText, Window): + +class DisplayModelEditableText(EditableTextWithoutAutoSelectDetection, Window): role=controlTypes.ROLE_EDITABLETEXT TextInfo = displayModel.EditableTextDisplayModelTextInfo diff --git a/source/appModules/fastlogentry.py b/source/appModules/fastlogentry.py new file mode 100644 index 00000000000..ecfae5730cb --- /dev/null +++ b/source/appModules/fastlogentry.py @@ -0,0 +1,27 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2020 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 appModuleHandler +from NVDAObjects.window import DisplayModelEditableText +from NVDAObjects.window.edit import UnidentifiedEdit + + +class TSynMemo(DisplayModelEditableText): + + name = None # Name is complete garbage as well. + + +class AppModule(appModuleHandler.AppModule): + + def chooseNVDAObjectOverlayClasses(self, obj, clsList): + windowClass = obj.windowClassName + if windowClass == "TSynMemo": + # #8996: Edit fields in Fast Log Entry can't use UnidentifiedEdit + # because their WindowText contains complete garbage. + try: + clsList.remove(UnidentifiedEdit) + except ValueError: + pass + clsList.insert(0, TSynMemo) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 50cf0fa1973..0444bc0cb43 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -14,6 +14,7 @@ What's New in NVDA == Bug Fixes == +- NVDA once again works correctly with edit fields when using the Fast Log Entry application. (#8996) = 2020.3 = From a32db1bc80f518db65fc07d82b90ec88c078d032 Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Thu, 10 Sep 2020 07:52:28 +0200 Subject: [PATCH 08/27] Developer guide: document resumeSayAllMode keyword in script decorator parameters. (#11571) --- devDocs/developerGuide.t2t | 3 +++ 1 file changed, 3 insertions(+) diff --git a/devDocs/developerGuide.t2t b/devDocs/developerGuide.t2t index 7be9193dd05..df09a004799 100644 --- a/devDocs/developerGuide.t2t +++ b/devDocs/developerGuide.t2t @@ -468,6 +468,9 @@ 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. +- 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. - Though the script decorator makes the script definition process a lot easier, there are more ways of binding gestures and setting script properties. From e245b98ba3c49da467f1977c6aae6822fe72054f Mon Sep 17 00:00:00 2001 From: Bram Duvigneau Date: Thu, 10 Sep 2020 16:51:29 +0200 Subject: [PATCH 09/27] Improve handling of elapsed/remaining time reporting in Foobar2000 (PR #11337) - Report elapsed time if total time is not available (e.g. when playing a live stream) - Report when remaining/total time is not available instead of keeping silent --- source/appModules/foobar2000.py | 20 ++++++++++++++++---- user_docs/en/changes.t2t | 1 + 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/source/appModules/foobar2000.py b/source/appModules/foobar2000.py index b384007c302..c06ed16a20e 100644 --- a/source/appModules/foobar2000.py +++ b/source/appModules/foobar2000.py @@ -1,5 +1,6 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2009-2018 NV Access Limited, Aleksey Sadovoy, James Teh, Joseph Lee, Tuukka Ojala +# Copyright (C) 2009-2020 NV Access Limited, Aleksey Sadovoy, James Teh, Joseph Lee, Tuukka Ojala, +# Bram Duvigneau # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -54,9 +55,14 @@ def getElapsedAndTotal(self): statusBarContents = self._statusBar.firstChild.name try: playingTimes = statusBarContents.split("|")[4].split("/") - return statusBarTimes(playingTimes[0], playingTimes[1]) except IndexError: return empty + elapsed = playingTimes[0] + if len(playingTimes) > 1: + total = playingTimes[1] + else: + total = None + return statusBarTimes(elapsed, total) def getElapsedAndTotalIfPlaying(self): elapsedAndTotalTime = self.getElapsedAndTotal() @@ -67,12 +73,15 @@ def getElapsedAndTotalIfPlaying(self): def script_reportRemainingTime(self,gesture): elapsedTime, totalTime = self.getElapsedAndTotalIfPlaying() - if elapsedTime is not None and totalTime is not None: + if elapsedTime is None or totalTime is None: + # Translators: Reported if the remaining time can not be calculated in Foobar2000 + msg = _("Unable to determine remaining time") + else: parsedElapsedTime = parseIntervalToTimestamp(elapsedTime) parsedTotalTime = parseIntervalToTimestamp(totalTime) remainingTime = parsedTotalTime - parsedElapsedTime msg = time.strftime(getOutputFormat(remainingTime), time.gmtime(remainingTime)) - ui.message(msg) + ui.message(msg) # Translators: The description of an NVDA command for reading the remaining time of the currently playing track in Foobar 2000. script_reportRemainingTime.__doc__ = _("Reports the remaining time of the currently playing track, if any") @@ -87,6 +96,9 @@ def script_reportTotalTime(self,gesture): totalTime = self.getElapsedAndTotalIfPlaying()[1] if totalTime is not None: ui.message(totalTime) + else: + # Translators: Reported if the total time is not available in Foobar2000 + ui.message(_("Total time not available")) # Translators: The description of an NVDA command for reading the length of the currently playing track in Foobar 2000. script_reportTotalTime.__doc__ = _("Reports the length of the currently playing track, if any") diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 0444bc0cb43..bbf7a863e10 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -15,6 +15,7 @@ What's New in NVDA == Bug Fixes == - NVDA once again works correctly with edit fields when using the Fast Log Entry application. (#8996) +- Report elapsed time in Foobar2000 if no total time is available (e.g. when playing a live stream). (#11337) = 2020.3 = From adff1e4e15b228e32125d4dfab847e21360365e0 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Fri, 11 Sep 2020 08:08:35 +1000 Subject: [PATCH 10/27] emulate key presses for system tests using NVDA directly (#11581) * watchdog: add a isCoreAsleep function. * System tests: send keyboard input via NvDA's own input gesture framework, so that key press strings confirm to NVDA, and we block on key presses until the actual is actually executed. * Fix linting issues * Remove now unused KeyInputLib system test library. * System tests: we no longer require PyGetWindow or PyAutoGui packages. * Add type hint to emulateKeyPress method on System test spy. Co-authored-by: Reef Turner * Clarify docstring for system test spy's emulateKeyPress method. * Update what's new Co-authored-by: Reef Turner --- source/watchdog.py | 17 +++++++- tests/system/libraries/ChromeLib.py | 9 ++-- tests/system/libraries/KeyInputLib.py | 19 --------- .../SystemTestSpy/speechSpyGlobalPlugin.py | 41 +++++++++++++++++++ tests/system/requirements.txt | 6 --- tests/system/robot/startupShutdownNVDA.py | 11 +++-- user_docs/en/changes.t2t | 4 ++ 7 files changed, 69 insertions(+), 38 deletions(-) delete mode 100644 tests/system/libraries/KeyInputLib.py diff --git a/source/watchdog.py b/source/watchdog.py index d3d103aacdd..6a8f1ff9588 100644 --- a/source/watchdog.py +++ b/source/watchdog.py @@ -41,6 +41,7 @@ isRunning=False isAttemptingRecovery = False +_coreIsAsleep = False _coreDeadTimer = windll.kernel32.CreateWaitableTimerW(None, True, None) _suspended = False @@ -68,6 +69,8 @@ def getFormattedStacksForAllThreads(): def alive(): """Inform the watchdog that the core is alive. """ + global _coreIsAsleep + _coreIsAsleep = False # Stop cancelling calls. windll.kernel32.ResetEvent(_cancelCallEvent) # Set the timer so the watcher will take action in MIN_CORE_ALIVE_TIMEOUT @@ -79,11 +82,23 @@ def alive(): def asleep(): """Inform the watchdog that the core is going to sleep. """ - # #5189: Reset in case the core was treated as dead. + global _coreIsAsleep +# #5189: Reset in case the core was treated as dead. alive() # CancelWaitableTimer does not reset the signaled state; if it was signaled, it remains signaled. # However, alive() calls SetWaitableTimer, which resets the timer to unsignaled. windll.kernel32.CancelWaitableTimer(_coreDeadTimer) + _coreIsAsleep = True + + +def isCoreAsleep(): + """ + Finds out if the core is currently asleep (I.e. not in a core cycle). + Note that if the core is actually frozen, this function will return false + as it is frozen in a core cycle while awake. + """ + return _coreIsAsleep + def _isAlive(): # #5189: If the watchdog has been terminated, treat the core as being alive. diff --git a/tests/system/libraries/ChromeLib.py b/tests/system/libraries/ChromeLib.py index 6bcf88b36ac..f86f86a5846 100644 --- a/tests/system/libraries/ChromeLib.py +++ b/tests/system/libraries/ChromeLib.py @@ -19,14 +19,12 @@ # Imported for type information from robot.libraries.OperatingSystem import OperatingSystem as _OpSysLib from robot.libraries.Process import Process as _ProcessLib -from KeyInputLib import KeyInputLib as _KeyInputLib from AssertsLib import AssertsLib as _AssertsLib import NvdaLib as _NvdaLib builtIn: BuiltIn = BuiltIn() opSys: _OpSysLib = _getLib('OperatingSystem') process: _ProcessLib = _getLib('Process') -keyInputLib: _KeyInputLib = _getLib('KeyInputLib') assertsLib: _AssertsLib = _getLib('AssertsLib') @@ -107,7 +105,7 @@ def prepareChrome(self, testCase: str) -> None: for i in range(10): # set a limit on the number of tries. # Small changes in Chrome mean the number of tab presses to get into the document can vary. builtIn.sleep("0.5 seconds") # ensure application has time to receive input - actualSpeech = self.getSpeechAfterKey('F6') + actualSpeech = self.getSpeechAfterKey('f6') if ChromeLib._beforeMarker in actualSpeech: break else: # Exceeded the number of tries @@ -126,8 +124,7 @@ def getSpeechAfterKey(key) -> str: spy = _NvdaLib.getSpyLib() spy.wait_for_speech_to_finish() nextSpeechIndex = spy.get_next_speech_index() - keyInputLib.send(key) - spy.wait_for_speech_to_finish() + spy.emulateKeyPress(key) speech = spy.get_speech_at_index_until_now(nextSpeechIndex) return speech @@ -136,4 +133,4 @@ def getSpeechAfterTab() -> str: """Ensure speech has stopped, press tab, and get speech until it stops. @return: The speech after tab. """ - return ChromeLib.getSpeechAfterKey('\t') + return ChromeLib.getSpeechAfterKey('tab') diff --git a/tests/system/libraries/KeyInputLib.py b/tests/system/libraries/KeyInputLib.py deleted file mode 100644 index edeacf28e78..00000000000 --- a/tests/system/libraries/KeyInputLib.py +++ /dev/null @@ -1,19 +0,0 @@ -# A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2020 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 provides system test library functions for sending keyboard key presses. -""" -import pyautogui -pyautogui.FAILSAFE = False - - -# In Robot libraries, class name must match the name of the module. Use caps for both. -class KeyInputLib: - @staticmethod - def send(*keys): - """Sends the keys as if pressed by the user. - Full list of keys: pyautogui.KEYBOARD_KEY - """ - pyautogui.hotkey(*keys) diff --git a/tests/system/libraries/SystemTestSpy/speechSpyGlobalPlugin.py b/tests/system/libraries/SystemTestSpy/speechSpyGlobalPlugin.py index 2023d28f34b..c842300c789 100644 --- a/tests/system/libraries/SystemTestSpy/speechSpyGlobalPlugin.py +++ b/tests/system/libraries/SystemTestSpy/speechSpyGlobalPlugin.py @@ -16,6 +16,10 @@ from .blockUntilConditionMet import _blockUntilConditionMet from logHandler import log from time import perf_counter as _timer +from keyboardHandler import KeyboardInputGesture +import inputCore +import queueHandler +import watchdog import sys import os @@ -217,6 +221,43 @@ def wait_for_speech_to_finish(self, maxWaitSeconds=5.0): errorMessage="Speech did not finish before timeout" ) + def emulateKeyPress(self, kbIdentifier: str, blockUntilProcessed=True): + """ + Emulates a key press using NVDA's input gesture framework. + The key press will either result in a script being executed, or the key being sent on to the OS. + By default this method will block until any script resulting from this key has been executed, + and the NVDA core has again gone back to sleep. + @param kbIdentifier: an NVDA keyboard gesture identifier. + 0 or more modifier keys followed by a main key, all separated by a plus (+) symbol. + E.g. control+shift+downArrow. + See vkCodes.py in the NVDA source directory for valid key names. + """ + gesture = KeyboardInputGesture.fromName(kbIdentifier) + inputCore.manager.emulateGesture(gesture) + if blockUntilProcessed: + # Emulating may have queued a script or events. + # Insert our own function into the queue after, and wait for that to be also executed. + queueProcessed = set() + + def _setQueueProcessed(): + nonlocal queueProcessed + queueProcessed = True + + queueHandler.queueFunction(queueHandler.eventQueue, _setQueueProcessed) + _blockUntilConditionMet( + getValue=lambda: queueProcessed, + giveUpAfterSeconds=self._minTimeout(5), + errorMessage="Timed out waiting for key to be processed", + ) + # We know that by now the core will have woken up and processed the scripts, events and our own function. + # Wait for the core to go to sleep, + # Which means there is no more things the core is currently processing. + _blockUntilConditionMet( + getValue=lambda: watchdog.isCoreAsleep(), + giveUpAfterSeconds=self._minTimeout(5), + errorMessage="Timed out waiting for core to sleep again", + ) + class SystemTestSpyServer(globalPluginHandler.GlobalPlugin): def __init__(self): diff --git a/tests/system/requirements.txt b/tests/system/requirements.txt index 8386dfe4208..dd3c3abbea0 100644 --- a/tests/system/requirements.txt +++ b/tests/system/requirements.txt @@ -1,9 +1,3 @@ -# Manually grab pyGetWindow 0.0.4 as latest release cannot be installed on Python 2.7. -# See https://github.com/asweigart/PyGetWindow/issues/9 -pygetwindow==0.0.4 -# Latest version of PyAutoGui requires PyGetWindow=0.0.5 which cannot be installed - see above -# Therefore install last working version -pyautogui==0.9.43 nose robotframework robotremoteserver diff --git a/tests/system/robot/startupShutdownNVDA.py b/tests/system/robot/startupShutdownNVDA.py index 02a56b25487..b3118fbde3c 100644 --- a/tests/system/robot/startupShutdownNVDA.py +++ b/tests/system/robot/startupShutdownNVDA.py @@ -15,7 +15,7 @@ # Imported for type information from robot.libraries.Process import Process as _ProcessLib -from KeyInputLib import KeyInputLib as _KeyInputLib + from AssertsLib import AssertsLib as _AssertsLib import NvdaLib as _nvdaLib @@ -24,7 +24,6 @@ _builtIn: BuiltIn = BuiltIn() _process: _ProcessLib = _getLib("Process") -_keyInputs: _KeyInputLib = _getLib("KeyInputLib") _asserts: _AssertsLib = _getLib("AssertsLib") @@ -39,9 +38,9 @@ def quits_from_keyboard(): spy.wait_for_specific_speech("Welcome to NVDA") # ensure the dialog is present. spy.wait_for_speech_to_finish() _builtIn.sleep(1) # the dialog is not always receiving the enter keypress, wait a little longer for it - _keyInputs.send("enter") + spy.emulateKeyPress("enter") - _keyInputs.send("insert", "q") + spy.emulateKeyPress("insert+q") exitTitleIndex = spy.wait_for_specific_speech("Exit NVDA") spy.wait_for_speech_to_finish() @@ -53,7 +52,7 @@ def quits_from_keyboard(): "What would you like to do? combo box Exit collapsed Alt plus d" ) _builtIn.sleep(1) # the dialog is not always receiving the enter keypress, wait a little longer for it - _keyInputs.send("enter") + spy.emulateKeyPress("enter", blockUntilProcessed=False) _process.wait_for_process(_nvdaProcessAlias, timeout="10 sec") _process.process_should_be_stopped(_nvdaProcessAlias) @@ -75,4 +74,4 @@ def read_welcome_dialog(): "Keyboard layout: combo box desktop collapsed Alt plus k" ) _builtIn.sleep(1) # the dialog is not always receiving the enter keypress, wait a little longer for it - _keyInputs.send("enter") + spy.emulateKeyPress("enter") diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index bbf7a863e10..760bc5bbac0 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -18,6 +18,10 @@ What's New in NVDA - Report elapsed time in Foobar2000 if no total time is available (e.g. when playing a live stream). (#11337) +== Changes for Developers == +- System tests can now send keys using spy.emulateKeyPress, which takes a key identifier that conforms to NVDA's own key names, and by default also blocks until the action is executed. (#11581) + + = 2020.3 = == New Features == From 864ff77f08ebe22b2e45e3f9c50829a2d61d2f59 Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Fri, 11 Sep 2020 12:55:24 +0200 Subject: [PATCH 11/27] NVDA find commands no longer stop say all if allow skim reading option is enabled (PR #11564) When reading with say all in browse mode, the find next and find previous commands do not stop reading anymore if 'Allow skim reading option' is enabled; say all rather resumes from after the next or previous found term. Co-authored-by: Reef Turner --- source/cursorManager.py | 44 +++++++++++++++++++++++++++++----------- user_docs/en/changes.t2t | 1 + 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/source/cursorManager.py b/source/cursorManager.py index 2b3ad58293d..8c464a54adf 100644 --- a/source/cursorManager.py +++ b/source/cursorManager.py @@ -17,7 +17,7 @@ from gui import guiHelper import sayAllHandler import review -from scriptHandler import willSayAllResume +from scriptHandler import willSayAllResume, script import textInfos import api import speech @@ -148,7 +148,7 @@ def _caretMovementScriptHelper(self,gesture,unit,direction=None,posConstant=text speech.speakSelectionChange(oldInfo, selection) self.selection = selection - def doFindText(self,text,reverse=False,caseSensitive=False): + def doFindText(self, text, reverse=False, caseSensitive=False, willSayAllResume=False): if not text: return info=self.makeTextInfo(textInfos.POSITION_CARET) @@ -157,7 +157,8 @@ def doFindText(self,text,reverse=False,caseSensitive=False): self.selection=info speech.cancelSpeech() info.move(textInfos.UNIT_LINE,1,endPoint="end") - speech.speakTextInfo(info,reason=controlTypes.REASON_CARET) + if not willSayAllResume: + speech.speakTextInfo(info, reason=controlTypes.REASON_CARET) else: wx.CallAfter(gui.messageBox,_('text "%s" not found')%text,_("Find Error"),wx.OK|wx.ICON_ERROR) CursorManager._lastFindText=text @@ -174,21 +175,42 @@ def run(): # Translators: Input help message for NVDA's find command. script_find.__doc__ = _("find a text string from the current cursor position") + @script( + description=_( + # Translators: Input help message for find next command. + "find the next occurrence of the previously entered text string from the current cursor's position" + ), + gesture="kb:NVDA+f3", + resumeSayAllMode=sayAllHandler.CURSOR_CARET, + ) def script_findNext(self,gesture): if not self._lastFindText: self.script_find(gesture) return - self.doFindText(self._lastFindText, caseSensitive = self._lastCaseSensitivity) - # Translators: Input help message for find next command. - script_findNext.__doc__ = _("find the next occurrence of the previously entered text string from the current cursor's position") - + self.doFindText( + self._lastFindText, + caseSensitive=self._lastCaseSensitivity, + willSayAllResume=willSayAllResume(gesture), + ) + + @script( + description=_( + # Translators: Input help message for find previous command. + "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, + ) def script_findPrevious(self,gesture): if not self._lastFindText: self.script_find(gesture) return - self.doFindText(self._lastFindText,reverse=True, caseSensitive = self._lastCaseSensitivity) - # Translators: Input help message for find previous command. - script_findPrevious.__doc__ = _("find the previous occurrence of the previously entered text string from the current cursor's position") + self.doFindText( + self._lastFindText, + reverse=True, + caseSensitive=self._lastCaseSensitivity, + willSayAllResume=willSayAllResume(gesture), + ) def script_moveByPage_back(self,gesture): self._caretMovementScriptHelper(gesture,textInfos.UNIT_LINE,-config.conf["virtualBuffers"]["linesPerPage"],extraDetail=False) @@ -411,8 +433,6 @@ def reportSelectionChange(self, oldTextInfo): "kb:control+a": "selectAll", "kb:control+c": "copyToClipboard", "kb:NVDA+Control+f": "find", - "kb:NVDA+f3": "findNext", - "kb:NVDA+shift+f3": "findPrevious", "kb:alt+upArrow":"moveBySentence_back", "kb:alt+downArrow":"moveBySentence_forward", } diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 760bc5bbac0..ae49aaab351 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -11,6 +11,7 @@ What's New in NVDA == Changes == - Updated liblouis braille translator to version 3.15.0 +- When reading with say all in browse mode, the find next and find previous commands do not stop reading anymore if Allow skim reading option is enabled; say all rather resumes from after the next or previous found term. (#11563) == Bug Fixes == From 8c3774536b8c82a17bd0678657ec14052434d22a Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Fri, 11 Sep 2020 13:41:35 +0200 Subject: [PATCH 12/27] Add auto complete IntelliSense in SSMS and Visual Studio 2017 (PR #11421) Support for IntelliSense using the UIA Element Selected event. In the past this was very slow, but with selective UIA event registration enabled this is improved. Having said that, even without selective registration enabled it at least reads results, though with somewhat more delay. --- source/NVDAObjects/UIA/VisualStudio.py | 43 ++++++++++++++++++++++++++ source/NVDAObjects/UIA/__init__.py | 4 +++ user_docs/en/changes.t2t | 1 + 3 files changed, 48 insertions(+) create mode 100644 source/NVDAObjects/UIA/VisualStudio.py diff --git a/source/NVDAObjects/UIA/VisualStudio.py b/source/NVDAObjects/UIA/VisualStudio.py new file mode 100644 index 00000000000..2c0324d726a --- /dev/null +++ b/source/NVDAObjects/UIA/VisualStudio.py @@ -0,0 +1,43 @@ +# 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 + +""" +Object overlay classes for Visual Studio components +available in Visual Studio and SQL Server Management Studio. +""" + +from . import UIA +import speech +import braille +import api + + +class IntelliSenseItem(UIA): + + def _get_name(self): + return self.UIAElement.cachedAutomationID + + def event_UIA_elementSelected(self): + # Cancel speech to have speech announce the selection as soon as possible. + # This is needed because L{reportFocus} does not cancel speech. + # Therefore, if speech wouldn't be cancelled, + # selection announcements would queue up when changing selection rapidly. + speech.cancelSpeech() + api.setNavigatorObject(self, isFocus=True) + self.reportFocus() + # Display results as flash messages. + braille.handler.message(braille.getPropertiesBraille( + name=self.name, role=self.role, positionInfo=self.positionInfo, description=self.description + )) + + +class IntelliSenseList(UIA): + ... + + +def findExtraOverlayClasses(obj, clsList): + if obj.UIAElement.cachedAutomationId == "listBoxCompletions": + clsList.insert(0, IntelliSenseList) + elif isinstance(obj.parent, IntelliSenseList) and obj.UIAElement.cachedClassName == "IntellisenseMenuItem": + clsList.insert(0, IntelliSenseItem) diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index 9b9208979c4..c06828a34dc 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -957,6 +957,10 @@ def findOverlayClasses(self,clsList): except ValueError: pass + if self.UIAElement.cachedFrameworkID == "WPF" and self.appModule.appName in ("devenv", "ssms"): + from . import VisualStudio + VisualStudio.findExtraOverlayClasses(self, clsList) + # Support Windows Console's UIA interface if ( self.windowClassName == "ConsoleWindowClass" diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index ae49aaab351..70290392eca 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -7,6 +7,7 @@ What's New in NVDA == New Features == - Pressing F1 inside NVDA dialogs will now open the help file to most relevant section. (#7757) +- Support for auto complete suggestions (IntelliSense) Microsoft SQL Server Management Studio and Visual Studio 2017. (#7504) == Changes == From d25e3dedcd9da818235202a8c5a20e1386ebd118 Mon Sep 17 00:00:00 2001 From: Samuel Thibault Date: Fri, 11 Sep 2020 14:24:05 +0200 Subject: [PATCH 13/27] Symbols: support regex group references in replacements (PR #11116) fixes #11107 --- devDocs/developerGuide.t2t | 10 +++ source/characterProcessing.py | 36 +++++++- tests/unit/test_characterProcessing.py | 120 +++++++++++++++++++++++++ user_docs/en/changes.t2t | 1 + user_docs/en/userGuide.t2t | 3 + 5 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_characterProcessing.py diff --git a/devDocs/developerGuide.t2t b/devDocs/developerGuide.t2t index df09a004799..3da21bcea28 100644 --- a/devDocs/developerGuide.t2t +++ b/devDocs/developerGuide.t2t @@ -72,6 +72,7 @@ Subsequent lines contain a textual identifier used to identify the symbol, a tab For example: ``` . sentence ending (?<=[^\s.])\.(?=[\"')\s]|$) +dates with . \b(\d\d)\.(\d\d)\.(\d{2}|\d{4})\b ``` Again, the English symbols are inherited by all other locales, so you need not include any complex symbols already defined for English. @@ -98,6 +99,8 @@ Certain characters cannot be typed into the file, so the following special seque - \f: form feed - \#: # character (needed because # at the start of a line denotes a comment) - replacement: The text which should be spoken for the symbol. +If the symbol is a complex symbol, \1, \2, etc. can be used to refer to the groups matches, which will be inlined in the replacement, allowing for simpler rules. +This also means that to get a \ character in the replacement, one has to type \\. - level: The symbol level at which the symbol should be spoken. The symbol level is configured by the user and specifies the amount of symbols that should be spoken. This field should contain one of the levels "none", "some", "most", "all" or "char", or "-" to use the default. @@ -133,6 +136,13 @@ It means that the ". sentence ending" complex symbol should be spoken as "point" Level and preserve are not specified, so they will be taken from English. A display name is provided so that French users will know what the symbol represents. +``` +dates with . \1 point \2 point \3 all norep # date avec points +``` +This line appears in the French symbols.dic file. +It means that the first, second, and third groups of the match will be included, separated by the word 'point'. +The effect is thus to replace the dots from the date with the word 'point'. + Please see the file locale\en\symbols.dic for the English definitions which are inherited for all locales. This is also a good full example. diff --git a/source/characterProcessing.py b/source/characterProcessing.py index 21e1e39690e..cf23e26529b 100644 --- a/source/characterProcessing.py +++ b/source/characterProcessing.py @@ -519,6 +519,35 @@ def __init__(self, locale): log.error("Invalid complex symbol regular expression in locale %s: %s" % (locale, e)) raise LookupError + def _replaceGroups(self, m: re.Match, string: str) -> str: + """Replace matching group references (\\1, \\2, ...) with the corresponding matched groups. + Also replace \\\\ with \\ and reject other escapes, for escaping coherency. + @param m: The currently-matched group + @param string: The match replacement string which may contain group references + """ + result = '' + + in_escape = False + for char in string: + if not in_escape: + if char == '\\': + in_escape = True + else: + result += char + else: + if char == '\\': + result += '\\' + elif char >= '0' and char <= '9': + result += m.group(m.lastindex + ord(char) - ord('0')) + else: + log.error("Invalid reference \\%string" % char) + raise LookupError + in_escape = False + if in_escape: + log.error("Unterminated backslash") + raise LookupError + return result + def _regexpRepl(self, m): group = m.lastgroup @@ -540,16 +569,19 @@ def _regexpRepl(self, m): if group == "simple": # Simple symbol. symbol = self.computedSymbols[text] + replacement = symbol.replacement else: # Complex symbol. index = int(group[1:]) symbol = self._computedComplexSymbolsList[index] + replacement = self._replaceGroups(m, symbol.replacement) + if symbol.preserve == SYMPRES_ALWAYS or (symbol.preserve == SYMPRES_NOREP and self._level < symbol.level): suffix = text else: suffix = " " - if self._level >= symbol.level and symbol.replacement: - return u" {repl}{suffix}".format(repl=symbol.replacement, suffix=suffix) + if self._level >= symbol.level and replacement: + return u" {repl}{suffix}".format(repl=replacement, suffix=suffix) else: return suffix diff --git a/tests/unit/test_characterProcessing.py b/tests/unit/test_characterProcessing.py new file mode 100644 index 00000000000..d3d77f49caf --- /dev/null +++ b/tests/unit/test_characterProcessing.py @@ -0,0 +1,120 @@ +# 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 + +"""Unit tests for the characterProcessing module. +""" + +import unittest +import re +from characterProcessing import SpeechSymbolProcessor + + +class TestComplex(unittest.TestCase): + """Test the complex symbols rules. + """ + + def _replace_cb(self, replacement, name=None): + """Return a regexp callback which replaces matches of the given + group name (or all groups if no name is given) with the + replacement string, with support for replacement of group + references. + """ + def replace(m): + if name is None or m.lastgroup == name: + return SpeechSymbolProcessor._replaceGroups(self, m, replacement) + return m.group() + return replace + + def _replace(self, string, pattern, replacement, name=None): + """Perform a pattern-based replacement on a string, for the + given named group (or all groups if no name is given), with + support for replacement of group references. + """ + regexp = re.compile(pattern, re.UNICODE) + return regexp.sub(self._replace_cb(replacement, name), string) + + def test_group_replacement(self): + """Test that plain text gets properly replaced + """ + replaced = self._replace( + string="1", + pattern=r"(\d)", + replacement="a" + ) + self.assertEqual(replaced, "a") + + def test_backslash_replacement(self): + """Test that backslashes get properly replaced + """ + replaced = self._replace( + string="1", + pattern=r"(\d)", + replacement=r"\\" + ) + self.assertEqual(replaced, "\\") + + def test_double_backslash_replacement(self): + """Test that double backslashes get properly replaced + """ + replaced = self._replace( + string="1", + pattern=r"(\d)", + replacement=r"\\\\" + ) + self.assertEqual(replaced, r"\\") + + def test_unknown_escape(self): + """Test that a non-supported escaped character (i.e. not \\1, + \\2, ... \\9 and \\\\) in the replacement raises an error + """ + with self.assertRaises(LookupError): + self._replace( + string="1", + pattern=r"(\d)", + replacement=r"\a" + ) + + def test_missing_group(self): + """Test that a reference in the replacement to an non-existing + group raises an error + """ + with self.assertRaises(IndexError): + self._replace( + string="1", + pattern=r"(\d)", + replacement=r"\2" + ) + + def test_unterminated_escape(self): + """Test that an escape at the end of replacement raises an + error, since there is nothing to be escaped there + """ + with self.assertRaises(LookupError): + self._replace( + string="1", + pattern=r"(\d)", + replacement="\\" + ) + + def test_group_replacements(self): + """Test that group references get properly replaced + """ + replaced = self._replace( + string="bar.BAT", + pattern=r"(([a-z]*)\.([A-Z]*))", + replacement=r"\2>\1" + ) + self.assertEqual(replaced, "BAT>bar") + + def test_multiple_group_replacement(self): + """Test that group indexing is correct with multiple groups + """ + replaced = self._replace( + string="bar.BAT", + pattern=r"(baz)|(?P([a-z]*)\.([A-Z]*))", + replacement=r"\2>\1", + name="foo" + ) + self.assertEqual(replaced, "BAT>bar") diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 70290392eca..44faeb0b426 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -8,6 +8,7 @@ What's New in NVDA == New Features == - Pressing F1 inside NVDA dialogs will now open the help file to most relevant section. (#7757) - Support for auto complete suggestions (IntelliSense) Microsoft SQL Server Management Studio and Visual Studio 2017. (#7504) +- Symbol pronunciation: Support for grouping in a complex symbol definition and support group references in a replacement rule making them simpler and more powerful. (#11107) == Changes == diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index d898e2b0f4d..b2c414bd199 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -1922,6 +1922,9 @@ You can remove a symbol you previously added by pressing the Remove button. When you are finished, press the OK button to save your changes or the Cancel button to discard them. +In the case of complex symbols, the Replacement field may have to include some group references of the matched text. For instance, for a pattern matching a whole date, \1, \2, and \3 would need to appear in the field, to be replaced by the corresponding parts of the date. +Normal backslashes in the Replacement field should thus be doubled, e.g. "a\\b" should be typed in order to get the "a\b" replacement. + +++ Input Gestures +++[InputGestures] In this dialog, you can customize the input gestures (keys on the keyboard, buttons on a braille display, etc.) for NVDA commands. From 8359066f58a46610ee0da185c1e5499cc7cfbca9 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Fri, 11 Sep 2020 15:35:32 +0200 Subject: [PATCH 14/27] Visual Studio appmodule: fix bugs in object model implementation (PR #10387) * Strip down the visual studio appModule * allow using object model on newer versions * Remove focus juggle * Remove ability to override WPF implementation for text controls --- source/appModules/devenv.py | 540 ++++++++---------------------------- 1 file changed, 118 insertions(+), 422 deletions(-) diff --git a/source/appModules/devenv.py b/source/appModules/devenv.py index c647a256850..4cc800fa4ad 100644 --- a/source/appModules/devenv.py +++ b/source/appModules/devenv.py @@ -1,87 +1,33 @@ -#appModules/devenv.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 Soronel Haetir -# -# Suggestions from James Teh have been used. -# -# Visual Studio 2005/2008 support for NVDA. -# I believe this code should work for VS2002/2003 as well but have no way of testing that. -# I have confirmed this script requires at least Visual Studio Standard, as the Express editions -# don't register themselves with the running object table. -# I have tried several means of getting around this, so far without success. -# -# I started with revision 3493 of the main NVDA branch for this work. -# -# !!! IMPORTANT !!! -# -# I had to modify many of the members of IVsTextManager and IVsTextView to cut down on dependencies. -# Specifically any interface pointers other than IVsTextView have been changed to IUnknown -# Also, many structure and enumerations have been replaced with c_int. -# -# If NVDA becomes more dependant on the Visual Studio SDK interfaces the embedded wrappers -# should be dropped in favor of the type library. -# -# !!! END OF IMPORTANT INFORMATION !!! -# -# The Visual Studio 2008 SDK is required if you wish -# to generate python type wrappers. It can be downloaded at: -# http://www.microsoft.com/downloads/details.aspx?familyid=59EC6EC3-4273-48A3-BA25-DC925A45584D&displaylang=en -# Use the MIDL compiler to build textmgr.tlb. -# From \Program Files\Microsoft Visual Studio 2008 SDK\VisualStudioIntegration\Common\IDL: -# midl /I ..\inc textmgr.idl -# and then copy the resulting typelib to your sources\typelibs directory. -# - -import ctypes -import objbase -from comtypes import IUnknown, IServiceProvider , GUID, COMMETHOD, HRESULT, BSTR -from ctypes import POINTER, c_int, c_short, c_ushort, c_ulong -import comtypes.client.dynamic -from comtypes.automation import IDispatch +# 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-2019 NV Access Limited, Soronel Haetir, Babbage B.V., Francisco Del Roio +import objbase +import comtypes +from locationHelper import RectLTWH from logHandler import log import textInfos.offsets -from NVDAObjects.behaviors import EditableTextWithoutAutoSelectDetection +from NVDAObjects.behaviors import EditableText, EditableTextWithoutAutoSelectDetection from NVDAObjects.window import Window - +from comtypes.automation import IDispatch from NVDAObjects.window import DisplayModelEditableText from NVDAObjects.IAccessible import IAccessible - +from NVDAObjects.UIA import UIA +from enum import IntEnum import appModuleHandler import controlTypes +import threading -# # A few helpful constants -# - -VsRootWindowClassName="wndclass_desked_gsk" -VsTextEditPaneClassName="VsTextEditPane" - -SVsTextManager = GUID('{F5E7E71D-1401-11D1-883B-0000F87579D2}') -VsVersion_None = 0 -VsVersion_2002 = 1 -VsVersion_2003 = 2 -VsVersion_2005 = 3 -VsVersion_2008 = 4 - -# Possible values of the VS .Type property of VS windows. -# According to the docs this property should not be used but I have not been able to determine all of the needed values -# of the .Kind property which is the suggested alternative. -# -# I don't have a type library or header defining the VsWindowType enumeration so only .Type values -# I've actually encountered are defined. -# Known missing values are: -# CodeWindow, Designer, Browser, Watch, Locals, -# SolutionExplorer, Properties, Find, FindReplace, Toolbox, LinkedWindowFrame, MainWindow, Preview, -# ColorPalettte, ToolWindowTaskList, Autos, CallStack, Threads, DocumentOutline, RunningDocuments -# Most of these host controls which should hopefully be the "real" window by the time any text needs to be rendered. -VsWindowTypeCommand = 15 -VsWindowTypeDocument = 16 -VsWindowTypeOutput = 17 +# vsWindowType Enum +class VsWindowType(IntEnum): + ToolWindow = 15 + Document = 16 + Output = 17 + # Scroll bar selector SB_HORZ = 0 @@ -89,400 +35,150 @@ class AppModule(appModuleHandler.AppModule): - def chooseNVDAObjectOverlayClasses(self, obj, clsList): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._DTECache = {} vsMajor, vsMinor, rest = self.productVersion.split(".", 2) - vsMajor, vsMinor = int(vsMajor), int(vsMinor) + self.vsMajor, self.vsMinor = int(vsMajor), int(vsMinor) + def chooseNVDAObjectOverlayClasses(self, obj, clsList): # Only use this overlay class if the top level automation object for the IDE can be retrieved, # as it will not work otherwise. - if obj.windowClassName == VsTextEditPaneClassName and self._getDTE(): + if obj.windowClassName == "VsTextEditPane" and self.DTE: try: clsList.remove(DisplayModelEditableText) except ValueError: pass - clsList.insert(0, VsTextEditPane) - - if ((vsMajor == 15 and vsMinor >= 3) - or vsMajor >= 16): + clsList[0:0] = [VsTextEditPane, EditableTextWithoutAutoSelectDetection] + elif ( + (self.vsMajor == 15 and self.vsMinor >= 3) + or self.vsMajor >= 16 + ): if obj.role == controlTypes.ROLE_TREEVIEWITEM and obj.windowClassName == "LiteTreeView32": clsList.insert(0, ObjectsTreeItem) - def _getDTE(self): - # Return the already fetched instance if there is one. - try: - if self._DTE: - return self._DTE - except AttributeError: - pass - - # Retrieve and cache the top level automation object for the IDE - DTEVersion = VsVersion_None + # Retrieve the top level automation object for the IDE bctx = objbase.CreateBindCtx() ROT = objbase.GetRunningObjectTable() for mon in ROT: - # Test for the strings Visual Studio may have registered with. displayName = mon.GetDisplayName(bctx, None) - if "!VisualStudio.DTE.9.0:%d"%self.processID==displayName: - DTEVersion=VsVersion_2008 - elif "!VisualStudio.DTE.8.0:%d"%self.processID==displayName: - DTEVersion = VsVersion_2005 - elif "!VisualStudio.DTE.7.1:%d"%self.processID==displayName: - DTEVersion = VsVersion_2003 - elif "!VisualStudio.DTE:%d"%self.processID==displayName: - DTEVersion = VsVersion_2002 - - if DTEVersion != VsVersion_None: - self._DTEVersion = DTEVersion - self._DTE = comtypes.client.dynamic.Dispatch(ROT.GetObject(mon).QueryInterface(IDispatch)) - break - + if displayName == f"!VisualStudio.DTE.{self.vsMajor}.0:{self.processID}": + return comtypes.client.dynamic.Dispatch(ROT.GetObject(mon).QueryInterface(IDispatch)) else: # None found. - log.debugWarning("No top level automation object found") - self._DTE = None - self._DTEVersion = VsVersion_None + log.debugWarning("No top level automation object found", exc_info=True) + return None - # Loop has completed - return self._DTE + def _get_DTE(self): + thread = threading.get_ident() + # Return the already fetched instance if there is one. + DTE = self._DTECache.get(thread) + if DTE: + return DTE + + DTE = self._DTECache[thread] = self._getDTE() + return DTE - def _getTextManager(self): - try: - if self._textManager: - return self._textManager - except AttributeError: - pass - serviceProvider = self._getDTE().QueryInterface(comtypes.IServiceProvider) - self._textManager = serviceProvider.QueryService(SVsTextManager, IVsTextManager) - return self._textManager class VsTextEditPaneTextInfo(textInfos.offsets.OffsetsTextInfo): - def _InformUnsupportedWindowType(self,type): - log.error("An unsupported window type `%d' was encountered, please inform the NVDA development team." %type) - raise NotImplementedError - - def _getSelectionObject(self): - Selection = None - if self._window.Type == VsWindowTypeDocument: - Selection = self._window.Selection - elif self._window.Type == VsWindowTypeOutput: - Selection = self._window.Object.ActivePane.TextDocument.Selection - elif self._window.Type==VsWindowTypeCommand: - Selection = self._window.Object.TextDocument.Selection + + def _get__selectionObject(self): + window = self.obj._window + if window.Type == VsWindowType.Document: + selection = window.Selection + elif window.Type == VsWindowType.Output: + selection = window.Object.ActivePane.TextDocument.Selection + elif window.Type == VsWindowType.ToolWindow: + selection = window.Object.TextDocument.Selection else: - self._InformUnsupportedWindowType(self._window.Type) - return Selection - + raise RuntimeError(f"Unknown window type: {window.Kind}") + self._selectionObject = selection + return selection + def _createEditPoint(self): - return self._getSelectionObject().ActivePoint.CreateEditPoint() - - def _getOffsetFromPoint(self,x,y): - yMinUnit, yMaxUnit, yVisible, yFirstVisible = self._textView.GetScrollInfo(SB_VERT) - hMinUnit, hMaxUnit, hVisible, hFirstVisible = self._textView.GetScrollInfo(SB_HORZ) - # These should probably be cached as they are fairly unlikely to change, but ... - lineHeight = self._textView.GetLineHeight() - charWidth = self._window.Width // hVisible - - offsetLine = (y - self._window.Top) // lineHeight + yFirstVisible - offsetChar = (x - self._window.Left) // charWidth + hFirstVisible - return self._textView.GetNearestPosition(offsetLine, offsetChar)[0] - - def __init__(self, obj, position): - self._window = obj._window - self._textView = obj._textView - super(VsTextEditPaneTextInfo, self).__init__(obj, position) - + return self._selectionObject.ActivePoint.CreateEditPoint() + def _getCaretOffset(self): - return self._createEditPoint().AbsoluteCharOffset - - def _setCaretOffset(self,offset): - self._getSelectionObject().MoveToAbsoluteOffset(offset) - - def _setSelectionOffsets(self,start,end): - Selection = self._getSelectionObject() - Selection.MoveToAbsoluteOffset(start) - Selection.MoveToAbsoluteOffset(end,True) - + return self._createEditPoint().AbsoluteCharOffset - 1 + + def _setCaretOffset(self, offset): + self._selectionObject.MoveToAbsoluteOffset(offset + 1) + + def _setSelectionOffsets(self, start, end): + self._selectionObject.MoveToAbsoluteOffset(start + 1) + self._selectionObject.MoveToAbsoluteOffset(end + 1, True) + def _getSelectionOffsets(self): - selection = self._getSelectionObject() - startPos = selection.ActivePoint.CreateEditPoint().AbsoluteCharOffset - 1 - endPos = selection.AnchorPoint.CreateEditPoint().AbsoluteCharOffset - 1 - return (startPos,endPos) - - def _getTextRange(self,start,end): - editPointStart = self._createEditPoint() - editPointStart.StartOfDocument() - if start: - editPointStart.MoveToAbsoluteOffset(start) - else: - start = 1 - return editPointStart.GetText(end-start) - - def _getWordOffsets(self,startOffset): + caretPos = self._getCaretOffset() + anchorPos = self._selectionObject.AnchorPoint.CreateEditPoint().AbsoluteCharOffset - 1 + return min(caretPos, anchorPos), max(caretPos, anchorPos) + + def _getTextRange(self, start, end): editPointStart = self._createEditPoint() - editPointEnd = editPointStart.CreateEditPoint() + editPointStart.MoveToAbsoluteOffset(start + 1) + return editPointStart.GetText(end - start) + + def _getWordOffsets(self, offset): + editPointEnd = self._createEditPoint() + editPointEnd.MoveToAbsoluteOffset(offset + 1) editPointEnd.WordRight() - return editPointStart.AbsoluteCharOffset,editPointEnd.AbsoluteCharOffset - - def _getLineOffsets(self,offset): + editPointStart = editPointEnd.CreateEditPoint() + editPointStart.WordLeft() + return editPointStart.AbsoluteCharOffset - 1, editPointEnd.AbsoluteCharOffset - 1 + + def _getLineOffsets(self, offset): editPointStart = self._createEditPoint() - editPointStart.MoveToAbsoluteOffset(offset) + editPointStart.MoveToAbsoluteOffset(offset + 1) editPointStart.StartOfLine() editPointEnd = editPointStart.CreateEditPoint() editPointEnd.EndOfLine() - return (editPointStart.AbsoluteCharOffset,editPointEnd.AbsoluteCharOffset) - - def _getLineNumFromOffset(self,offset): + # Offsets are one based and exclusive + return editPointStart.AbsoluteCharOffset - 1, editPointEnd.AbsoluteCharOffset + + def _getLineNumFromOffset(self, offset): editPoint = self._createEditPoint() - editPoint.MoveToAbsoluteOffset(offset) + editPoint.MoveToAbsoluteOffset(offset + 1) return editPoint.Line - + def _getStoryLength(self): editPoint = self._createEditPoint() editPoint.EndOfDocument() - return editPoint.AbsoluteCharOffset + return editPoint.AbsoluteCharOffset - 1 + +class VsTextEditPane(EditableText, Window): -class VsTextEditPane(EditableTextWithoutAutoSelectDetection,Window): - TextInfo = VsTextEditPaneTextInfo + def _get_TextInfo(self): + try: + if self._window.Type in iter(VsWindowType): + return VsTextEditPaneTextInfo + else: + log.debugWarning( + f"Retrieved Visual Studio window object, but unknown type: {self._window.Type}" + ) + except Exception: + log.debugWarning("Couldn't retrieve Visual Studio window object", exc_info=True) + return super().TextInfo def initOverlayClass(self): - self._window = self.appModule._getDTE().ActiveWindow - self.location = (self._window.Top,self._window.Left,self._window.Width,self._window.Height) - self._textView = self.appModule._getTextManager().GetActiveView(True, None) + self._window = self.appModule.DTE.ActiveWindow + + def _get_location(self): + if not isinstance(self, UIA): + return RectLTWH( + self._window.Left, + self._window.Top, + self._window.Width, + self._window.Height + ) + return super().location def event_valueChange(self): pass -class IVsTextView(IUnknown): - _case_insensitive_ = True - _iid_ = GUID('{BB23A14B-7C61-469A-9890-A95648CED5E6}') - _idlflags_ = [] - - -class IVsTextManager(comtypes.IUnknown): - _case_insensitive_ = True - _iid_ = GUID('{909F83E3-B3FC-4BBF-8820-64378744B39B}') - _idlflags_ = [] - -IVsTextManager._methods_ = [ - COMMETHOD([], HRESULT, 'RegisterView', - ( ['in'], POINTER(IVsTextView), 'pView' ), - ( ['in'], POINTER(IUnknown), 'pBuffer' )), - COMMETHOD([], HRESULT, 'UnregisterView', - ( ['in'], POINTER(IVsTextView), 'pView' )), - COMMETHOD([], HRESULT, 'EnumViews', - ( ['in'], POINTER(IUnknown), 'pBuffer' ), - ( ['out'], POINTER(POINTER(IUnknown)), 'ppEnum' )), - COMMETHOD([], HRESULT, 'CreateSelectionAction', - ( ['in'], POINTER(IUnknown), 'pBuffer' ), - ( ['out'], POINTER(POINTER(IUnknown)), 'ppAction' )), - COMMETHOD([], HRESULT, 'MapFilenameToLanguageSID', - ( ['in'], POINTER(c_ushort), 'pszFileName' ), - ( ['out'], POINTER(GUID), 'pguidLangSID' )), - COMMETHOD([], HRESULT, 'GetRegisteredMarkerTypeID', - ( ['in'], POINTER(GUID), 'pguidMarker' ), - ( ['out'], POINTER(c_int), 'piMarkerTypeID' )), - COMMETHOD([], HRESULT, 'GetMarkerTypeInterface', - ( ['in'], c_int, 'iMarkerTypeID' ), - ( ['out'], POINTER(POINTER(IUnknown)), 'ppMarkerType' )), - COMMETHOD([], HRESULT, 'GetMarkerTypeCount', - ( ['out'], POINTER(c_int), 'piMarkerTypeCount' )), - COMMETHOD([], HRESULT, 'GetActiveView', - ( ['in'], c_int, 'fMustHaveFocus' ), - ( ['in'], POINTER(IUnknown), 'pBuffer' ), - ( ['out'], POINTER(POINTER(IVsTextView)), 'ppView' )), - COMMETHOD([], HRESULT, 'GetUserPreferences', - ( ['out'], POINTER(c_int), 'pViewPrefs' ), - ( ['out'], POINTER(c_int), 'pFramePrefs' ), - ( ['in', 'out'], POINTER(c_int), 'pLangPrefs' ), - ( ['in', 'out'], POINTER(c_int), 'pColorPrefs' )), - COMMETHOD([], HRESULT, 'SetUserPreferences', - ( ['in'], POINTER(c_int), 'pViewPrefs' ), - ( ['in'], POINTER(c_int), 'pFramePrefs' ), - ( ['in'], POINTER(c_int), 'pLangPrefs' ), - ( ['in'], POINTER(c_int), 'pColorPrefs' )), - COMMETHOD([], HRESULT, 'SetFileChangeAdvise', - ( ['in'], POINTER(c_ushort), 'pszFileName' ), - ( ['in'], c_int, 'fStart' )), - COMMETHOD([], HRESULT, 'SuspendFileChangeAdvise', - ( ['in'], POINTER(c_ushort), 'pszFileName' ), - ( ['in'], c_int, 'fSuspend' )), - COMMETHOD([], HRESULT, 'NavigateToPosition', - ( ['in'], POINTER(IUnknown), 'pBuffer' ), - ( ['in'], POINTER(GUID), 'guidDocViewType' ), - ( ['in'], c_int, 'iPos' ), - ( ['in'], c_int, 'iLen' )), - COMMETHOD([], HRESULT, 'NavigateToLineAndColumn', - ( ['in'], POINTER(IUnknown), 'pBuffer' ), - ( ['in'], POINTER(GUID), 'guidDocViewType' ), - ( ['in'], c_int, 'iStartRow' ), - ( ['in'], c_int, 'iStartIndex' ), - ( ['in'], c_int, 'iEndRow' ), - ( ['in'], c_int, 'iEndIndex' )), - COMMETHOD([], HRESULT, 'GetBufferSccStatus', - ( ['in'], POINTER(IUnknown), 'pBufData' ), - ( ['out'], POINTER(c_int), 'pbNonEditable' )), - COMMETHOD([], HRESULT, 'RegisterBuffer', - ( ['in'], POINTER(IUnknown), 'pBuffer' )), - COMMETHOD([], HRESULT, 'UnregisterBuffer', - ( ['in'], POINTER(IUnknown), 'pBuffer' )), - COMMETHOD([], HRESULT, 'EnumBuffers', - ( ['out'], POINTER(POINTER(IUnknown)), 'ppEnum' )), - COMMETHOD([], HRESULT, 'GetPerLanguagePreferences', - ( ['out'], POINTER(c_int), 'pLangPrefs' )), - COMMETHOD([], HRESULT, 'SetPerLanguagePreferences', - ( ['in'], POINTER(c_int), 'pLangPrefs' )), - COMMETHOD([], HRESULT, 'AttemptToCheckOutBufferFromScc', - ( ['in'], POINTER(IUnknown), 'pBufData' ), - ( ['out'], POINTER(c_int), 'pfCheckoutSucceeded' )), - COMMETHOD([], HRESULT, 'GetShortcutManager', - ( ['out'], POINTER(POINTER(IUnknown)), 'ppShortcutMgr' )), - COMMETHOD([], HRESULT, 'RegisterIndependentView', - ( ['in'], POINTER(IUnknown), 'punk' ), - ( ['in'], POINTER(IUnknown), 'pBuffer' )), - COMMETHOD([], HRESULT, 'UnregisterIndependentView', - ( ['in'], POINTER(IUnknown), 'punk' ), - ( ['in'], POINTER(IUnknown), 'pBuffer' )), - COMMETHOD([], HRESULT, 'IgnoreNextFileChange', - ( ['in'], POINTER(IUnknown), 'pBuffer' )), - COMMETHOD([], HRESULT, 'AdjustFileChangeIgnoreCount', - ( ['in'], POINTER(IUnknown), 'pBuffer' ), - ( ['in'], c_int, 'fIgnore' )), - COMMETHOD([], HRESULT, 'GetBufferSccStatus2', - ( ['in'], POINTER(c_ushort), 'pszFileName' ), - ( ['out'], POINTER(c_int), 'pbNonEditable' ), - ( ['out'], POINTER(c_int), 'piStatusFlags' )), - COMMETHOD([], HRESULT, 'AttemptToCheckOutBufferFromScc2', - ( ['in'], POINTER(c_ushort), 'pszFileName' ), - ( ['out'], POINTER(c_int), 'pfCheckoutSucceeded' ), - ( ['out'], POINTER(c_int), 'piStatusFlags' )), - COMMETHOD([], HRESULT, 'EnumLanguageServices', - ( ['out'], POINTER(POINTER(IUnknown)), 'ppEnum' )), - COMMETHOD([], HRESULT, 'EnumIndependentViews', - ( ['in'], POINTER(IUnknown), 'pBuffer' ), - ( ['out'], POINTER(POINTER(IUnknown)), 'ppEnum' )), -] - - -IVsTextView._methods_ = [ - COMMETHOD([], HRESULT, 'Initialize', - ( ['in'], POINTER(IUnknown), 'pBuffer' ), - ( ['in'], comtypes.wireHWND, 'hwndParent' ), - ( ['in'], c_ulong, 'InitFlags' ), - ( ['in'], POINTER(c_int), 'pInitView' )), - COMMETHOD([], HRESULT, 'CloseView'), - COMMETHOD([], HRESULT, 'GetCaretPos', - ( ['out'], POINTER(c_int), 'piLine' ), - ( ['out'], POINTER(c_int), 'piColumn' )), - COMMETHOD([], HRESULT, 'SetCaretPos', - ( ['in'], c_int, 'iLine' ), - ( ['in'], c_int, 'iColumn' )), - COMMETHOD([], HRESULT, 'GetSelectionSpan', - ( ['out'], POINTER(c_int), 'pSpan' )), - COMMETHOD([], HRESULT, 'GetSelection', - ( ['out'], POINTER(c_int), 'piAnchorLine' ), - ( ['out'], POINTER(c_int), 'piAnchorCol' ), - ( ['out'], POINTER(c_int), 'piEndLine' ), - ( ['out'], POINTER(c_int), 'piEndCol' )), - COMMETHOD([], HRESULT, 'SetSelection', - ( ['in'], c_int, 'iAnchorLine' ), - ( ['in'], c_int, 'iAnchorCol' ), - ( ['in'], c_int, 'iEndLine' ), - ( ['in'], c_int, 'iEndCol' )), - COMMETHOD([], c_int, 'GetSelectionMode'), - COMMETHOD([], HRESULT, 'SetSelectionMode', - ( ['in'], c_int, 'iSelMode' )), - COMMETHOD([], HRESULT, 'ClearSelection', - ( ['in'], c_int, 'fMoveToAnchor' )), - COMMETHOD([], HRESULT, 'CenterLines', - ( ['in'], c_int, 'iTopLine' ), - ( ['in'], c_int, 'iCount' )), - COMMETHOD([], HRESULT, 'GetSelectedText', - ( ['out'], POINTER(BSTR), 'pbstrText' )), - COMMETHOD([], HRESULT, 'GetSelectionDataObject', - ( ['out'], POINTER(POINTER(IUnknown)), 'ppIDataObject' )), - COMMETHOD([], HRESULT, 'GetTextStream', - ( ['in'], c_int, 'iTopLine' ), - ( ['in'], c_int, 'iTopCol' ), - ( ['in'], c_int, 'iBottomLine' ), - ( ['in'], c_int, 'iBottomCol' ), - ( ['out'], POINTER(BSTR), 'pbstrText' )), - COMMETHOD([], HRESULT, 'GetBuffer', - ( ['out'], POINTER(POINTER(IUnknown)), 'ppBuffer' )), - COMMETHOD([], HRESULT, 'SetBuffer', - ( ['in'], POINTER(IUnknown), 'pBuffer' )), - COMMETHOD([], comtypes.wireHWND, 'GetWindowHandle'), - COMMETHOD([], HRESULT, 'GetScrollInfo', - ( ['in'], c_int, 'iBar' ), - ( ['out'], POINTER(c_int), 'piMinUnit' ), - ( ['out'], POINTER(c_int), 'piMaxUnit' ), - ( ['out'], POINTER(c_int), 'piVisibleUnits' ), - ( ['out'], POINTER(c_int), 'piFirstVisibleUnit' )), - COMMETHOD([], HRESULT, 'SetScrollPosition', - ( ['in'], c_int, 'iBar' ), - ( ['in'], c_int, 'iFirstVisibleUnit' )), - COMMETHOD([], HRESULT, 'AddCommandFilter', - ( ['in'], POINTER(IUnknown), 'pNewCmdTarg' ), - ( ['out'], POINTER(POINTER(IUnknown)), 'ppNextCmdTarg' )), - COMMETHOD([], HRESULT, 'RemoveCommandFilter', - ( ['in'], POINTER(IUnknown), 'pCmdTarg' )), - COMMETHOD([], HRESULT, 'UpdateCompletionStatus', - ( ['in'], POINTER(IUnknown), 'pCompSet' ), - ( ['in'], c_ulong, 'dwFlags' )), - COMMETHOD([], HRESULT, 'UpdateTipWindow', - ( ['in'], POINTER(IUnknown), 'pTipWindow' ), - ( ['in'], c_ulong, 'dwFlags' )), - COMMETHOD([], HRESULT, 'GetWordExtent', - ( ['in'], c_int, 'iLine' ), - ( ['in'], c_int, 'iCol' ), - ( ['in'], c_ulong, 'dwFlags' ), - ( ['out'], POINTER(c_int), 'pSpan' )), - COMMETHOD([], HRESULT, 'RestrictViewRange', - ( ['in'], c_int, 'iMinLine' ), - ( ['in'], c_int, 'iMaxLine' ), - ( ['in'], POINTER(IUnknown), 'pClient' )), - COMMETHOD([], HRESULT, 'ReplaceTextOnLine', - ( ['in'], c_int, 'iLine' ), - ( ['in'], c_int, 'iStartCol' ), - ( ['in'], c_int, 'iCharsToReplace' ), - ( ['in'], POINTER(c_ushort), 'pszNewText' ), - ( ['in'], c_int, 'iNewLen' )), - COMMETHOD([], HRESULT, 'GetLineAndColumn', - ( ['in'], c_int, 'iPos' ), - ( ['out'], POINTER(c_int), 'piLine' ), - ( ['out'], POINTER(c_int), 'piIndex' )), - COMMETHOD([], HRESULT, 'GetNearestPosition', - ( ['in'], c_int, 'iLine' ), - ( ['in'], c_int, 'iCol' ), - ( ['out'], POINTER(c_int), 'piPos' ), - ( ['out'], POINTER(c_int), 'piVirtualSpaces' )), - COMMETHOD([], HRESULT, 'UpdateViewFrameCaption'), - COMMETHOD([], HRESULT, 'CenterColumns', - ( ['in'], c_int, 'iLine' ), - ( ['in'], c_int, 'iLeftCol' ), - ( ['in'], c_int, 'iColCount' )), - COMMETHOD([], HRESULT, 'EnsureSpanVisible', - ( ['in'], c_int, 'span' )), - COMMETHOD([], HRESULT, 'PositionCaretForEditing', - ( ['in'], c_int, 'iLine' ), - ( ['in'], c_int, 'cIndentLevels' )), - COMMETHOD([], HRESULT, 'GetPointOfLineColumn', - ( ['in'], c_int, 'iLine' ), - ( ['in'], c_int, 'iCol' ), - ( ['retval', 'out'], POINTER(ctypes.wintypes.tagPOINT), 'ppt' )), - COMMETHOD([], HRESULT, 'GetLineHeight', - ( ['retval', 'out'], POINTER(c_int), 'piLineHeight' )), - COMMETHOD([], HRESULT, 'HighlightMatchingBrace', - ( ['in'], c_ulong, 'dwFlags' ), - ( ['in'], c_ulong, 'cSpans' ), - ( ['in'], POINTER(c_int), 'rgBaseSpans' )), - COMMETHOD([], HRESULT, 'SendExplicitFocus'), - COMMETHOD([], HRESULT, 'SetTopLine', - ( ['in'], c_int, 'iBaseLine' )), -] - class ObjectsTreeItem(IAccessible): def _get_focusRedirect(self): @@ -490,9 +186,9 @@ def _get_focusRedirect(self): Returns the correct focused item in the object explorer trees """ - if not controlTypes.STATE_FOCUSED in self.states: + if controlTypes.STATE_FOCUSED not in self.states: # Object explorer tree views have a bad IAccessible implementation. - # When expanding a primary node and going to secondary node, the + # When expanding a primary node and going to secondary node, the # focus is placed to the next root node, so we need to redirect # it to the real focused widget. Fortunately, the states are # still correct and we can detect if this is really focused or not. From c6d6d10fadb81f287978915f1aa3ac11a860abeb Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 14 Sep 2020 09:31:46 +0200 Subject: [PATCH 15/27] Make PR and bug templates more obvious (PR #11595) When landing on the edit field for the first time, often first time contributors do not notice that there is a template. --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 6b5b84b62b5..a4514f3a4f5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,7 +4,7 @@ about: Create a report to help us improve --- - From a82adfcf347f3971526c09a36b4effb70f374ef2 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Wed, 16 Sep 2020 16:20:29 +1000 Subject: [PATCH 16/27] Support aria roleDescription in contentEditables (#11607) * CompoundDocumentTextInfo._getControlFieldForObject: support roleText. * Update what's new --- source/compoundDocuments.py | 1 + user_docs/en/changes.t2t | 1 + 2 files changed, 2 insertions(+) diff --git a/source/compoundDocuments.py b/source/compoundDocuments.py index 029f854d8b5..97a437db056 100644 --- a/source/compoundDocuments.py +++ b/source/compoundDocuments.py @@ -139,6 +139,7 @@ def _getControlFieldForObject(self, obj, ignoreEditableText=True): return None field = textInfos.ControlField() field["role"] = role + field['roleText'] = obj.roleText # The user doesn't care about certain states, as they are obvious. states.discard(controlTypes.STATE_EDITABLE) states.discard(controlTypes.STATE_MULTILINE) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index ae02452ab94..7c260dfe615 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -19,6 +19,7 @@ What's New in NVDA == Bug Fixes == - NVDA once again works correctly with edit fields when using the Fast Log Entry application. (#8996) - Report elapsed time in Foobar2000 if no total time is available (e.g. when playing a live stream). (#11337) +- NVDA now honors the aria-roledescription attribute on elements in editable content in web pages. (#11607) == Changes for Developers == From f1ec7aa471b75ee46ae6716f27940ac86c82eb6c Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Wed, 16 Sep 2020 08:52:28 +0200 Subject: [PATCH 17/27] Use Intellisense menu items to provide instant Intellisense feedback in Visual Studio 2019 (#11609) * Use Intellisense menu items to provide instant Intellisense feedback in Visual Studio 2019 * Update What's new Co-authored-by: Michael Curran --- source/NVDAObjects/UIA/VisualStudio.py | 25 ++++++++++++++++++++++++- user_docs/en/changes.t2t | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/source/NVDAObjects/UIA/VisualStudio.py b/source/NVDAObjects/UIA/VisualStudio.py index 2c0324d726a..380c17cf27a 100644 --- a/source/NVDAObjects/UIA/VisualStudio.py +++ b/source/NVDAObjects/UIA/VisualStudio.py @@ -36,8 +36,31 @@ class IntelliSenseList(UIA): ... +class IntelliSenseLiveRegion(UIA): + """ + Visual Studio uses both Intellisense menu item objects and a live region + to communicate Intellisense selections. + NVDA uses the menu item approach and therefore the live region provides doubled information + and is disabled. + """ + + _shouldAllowUIALiveRegionChangeEvent = False + + +_INTELLISENSE_LIST_AUTOMATION_IDS = { + "listBoxCompletions", + "CompletionList" +} + + def findExtraOverlayClasses(obj, clsList): - if obj.UIAElement.cachedAutomationId == "listBoxCompletions": + if obj.UIAAutomationId in _INTELLISENSE_LIST_AUTOMATION_IDS: clsList.insert(0, IntelliSenseList) elif isinstance(obj.parent, IntelliSenseList) and obj.UIAElement.cachedClassName == "IntellisenseMenuItem": clsList.insert(0, IntelliSenseItem) + elif ( + obj.UIAElement.cachedClassName == "LiveTextBlock" + and obj.previous + and isinstance(obj.previous.previous, IntelliSenseList) + ): + clsList.insert(0, IntelliSenseLiveRegion) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 7c260dfe615..5c87fbf7d19 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -7,7 +7,7 @@ What's New in NVDA == New Features == - Pressing F1 inside NVDA dialogs will now open the help file to most relevant section. (#7757) -- Support for auto complete suggestions (IntelliSense) Microsoft SQL Server Management Studio and Visual Studio 2017. (#7504) +- Support for auto complete suggestions (IntelliSense) in Microsoft SQL Server Management Studio plus Visual Studio 2017 and higher. (#7504) - Symbol pronunciation: Support for grouping in a complex symbol definition and support group references in a replacement rule making them simpler and more powerful. (#11107) From 1216502cb5c1142220374f1bde7ae0c1ac409106 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Wed, 16 Sep 2020 17:59:50 +1000 Subject: [PATCH 18/27] Chrome: Don't announce 'list' on every line of ul, dl and ol tags in contenteditables (#11605) * Ensure that ul, dl and ol tags in Chrome get the readonly state so that 'list' is not announced on every line when in a contenteditable. * Add system test for #7562 * Fix linting issues * Improve docstring * Chrome system tests: The Before and After markers are now paragraphs rather than buttons. They no longer need to be focusable as we use f6 not tab. * System test NVDA configs: disable browse mode passThroughAudioIndication, which then means that switching between browse and focus modes produces a spoken string we can check for. * System test i7562: explicitly force focus mode at the beginning to ensure we don't accidentally test in browse mode. * Fix linting issues * Update what's new --- source/NVDAObjects/IAccessible/chromium.py | 18 ++++++++ tests/system/libraries/ChromeLib.py | 4 +- .../standard-doShowWelcomeDialog.ini | 1 + .../standard-dontShowWelcomeDialog.ini | 1 + tests/system/robot/chromeTests.py | 45 +++++++++++++++++++ tests/system/robot/chromeTests.robot | 3 ++ user_docs/en/changes.t2t | 1 + 7 files changed, 71 insertions(+), 2 deletions(-) diff --git a/source/NVDAObjects/IAccessible/chromium.py b/source/NVDAObjects/IAccessible/chromium.py index 63369e408df..59b0566a61e 100644 --- a/source/NVDAObjects/IAccessible/chromium.py +++ b/source/NVDAObjects/IAccessible/chromium.py @@ -74,6 +74,22 @@ def _get_states(self): return states +class PresentationalList(ia2Web.Ia2Web): + """ + Ensures that lists like UL, DL and OL always have the readonly state. + A work-around for issue #7562 + allowing us to differentiate presentational lists from interactive lists + (such as of size greater 1 and ARIA list boxes). + In firefox, this is possible by the presence of a read-only state, + even in a content editable. + """ + + def _get_states(self): + states = super().states + states.add(controlTypes.STATE_READONLY) + return states + + def findExtraOverlayClasses(obj, clsList): """Determine the most appropriate class(es) for Chromium objects. This works similarly to L{NVDAObjects.NVDAObject.findOverlayClasses} except that it never calls any other findOverlayClasses method. @@ -82,5 +98,7 @@ def findExtraOverlayClasses(obj, clsList): clsList.append(ComboboxListItem) elif obj.role == controlTypes.ROLE_TOGGLEBUTTON: clsList.append(ToggleButton) + elif obj.role == controlTypes.ROLE_LIST and obj.IA2Attributes.get('tag') in ('ul', 'dl', 'ol'): + clsList.append(PresentationalList) ia2Web.findExtraOverlayClasses(obj, clsList, documentClass=Document) diff --git a/tests/system/libraries/ChromeLib.py b/tests/system/libraries/ChromeLib.py index f86f86a5846..811efc44e3e 100644 --- a/tests/system/libraries/ChromeLib.py +++ b/tests/system/libraries/ChromeLib.py @@ -71,9 +71,9 @@ def _writeTestFile(testCase) -> str: {ChromeLib._testCaseTitle} - +

{ChromeLib._beforeMarker}

{testCase} - +

{ChromeLib._afterMarker}

""") with open(file=filePath, mode='w', encoding='UTF-8') as f: diff --git a/tests/system/nvdaSettingsFiles/standard-doShowWelcomeDialog.ini b/tests/system/nvdaSettingsFiles/standard-doShowWelcomeDialog.ini index 8edfec227ba..753266002c7 100644 --- a/tests/system/nvdaSettingsFiles/standard-doShowWelcomeDialog.ini +++ b/tests/system/nvdaSettingsFiles/standard-doShowWelcomeDialog.ini @@ -12,3 +12,4 @@ schemaVersion = 2 enableScratchpadDir = True [virtualBuffers] autoSayAllOnPageLoad = False + passThroughAudioIndication = False diff --git a/tests/system/nvdaSettingsFiles/standard-dontShowWelcomeDialog.ini b/tests/system/nvdaSettingsFiles/standard-dontShowWelcomeDialog.ini index 21dc68221ae..e146b6e06df 100644 --- a/tests/system/nvdaSettingsFiles/standard-dontShowWelcomeDialog.ini +++ b/tests/system/nvdaSettingsFiles/standard-dontShowWelcomeDialog.ini @@ -12,3 +12,4 @@ schemaVersion = 2 enableScratchpadDir = True [virtualBuffers] autoSayAllOnPageLoad = False + passThroughAudioIndication = False diff --git a/tests/system/robot/chromeTests.py b/tests/system/robot/chromeTests.py index 631addc2de6..0bbb8efde7e 100644 --- a/tests/system/robot/chromeTests.py +++ b/tests/system/robot/chromeTests.py @@ -39,3 +39,48 @@ def checkbox_labelled_by_inner_element(): # Instead this should be spoken as: "Simulate evil cat check box not checked" ) + + +def test_i7562(): + """ List should not be announced on every line of a ul in a contenteditable """ + _chrome.prepareChrome( + r""" +
+

before

+
    +
  • frogs
  • +
  • birds
  • +
+

after

+
+ """ + ) + actualSpeech = _chrome.getSpeechAfterKey("NVDA+space") + _asserts.strings_match( + actualSpeech, + "Focus mode" + ) + # Tab into the contenteditable + actualSpeech = _chrome.getSpeechAfterKey("tab") + _asserts.strings_match( + actualSpeech, + "section multi line editable before" + ) + # DownArow into the list. 'list' should be announced when entering. + actualSpeech = _chrome.getSpeechAfterKey("downArrow") + _asserts.strings_match( + actualSpeech, + "list bullet frogs" + ) + # DownArrow to the second list item. 'list' should not be announced. + actualSpeech = _chrome.getSpeechAfterKey("downArrow") + _asserts.strings_match( + actualSpeech, + "bullet birds" + ) + # DownArrow out of the list. 'out of list' should be announced. + actualSpeech = _chrome.getSpeechAfterKey("downArrow") + _asserts.strings_match( + actualSpeech, + "out of list after", + ) diff --git a/tests/system/robot/chromeTests.robot b/tests/system/robot/chromeTests.robot index 593ca012711..ce0e4322067 100644 --- a/tests/system/robot/chromeTests.robot +++ b/tests/system/robot/chromeTests.robot @@ -26,3 +26,6 @@ default teardown checkbox labelled by inner element [Documentation] A checkbox labelled by an inner element should not read the label element twice. checkbox_labelled_by_inner_element +i7562 + [Documentation] List should not be announced on every line of a ul in a contenteditable + test_i7562 diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 5c87fbf7d19..46b3736573d 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -20,6 +20,7 @@ What's New in NVDA - NVDA once again works correctly with edit fields when using the Fast Log Entry application. (#8996) - Report elapsed time in Foobar2000 if no total time is available (e.g. when playing a live stream). (#11337) - NVDA now honors the aria-roledescription attribute on elements in editable content in web pages. (#11607) +- 'list' is no longer announced on every line of a list in Google Docs or other editable content in Google Chrome. (#7562) == Changes for Developers == From 468761f189d26e40254ce98a1eb1a14f560055ec Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Wed, 16 Sep 2020 20:46:13 +1000 Subject: [PATCH 19/27] Announce entering a listItem when moving by character or word in contentEditables and browse mode (#11569) * Announce entering a listItem when moving by character or word in rich text. this includes contentEditables and browse mode. * Fix linting issues. * Add system test for list item change. * Fix linting issues * Improve listItem system test to be much closer to the actual case. I.e. actually uses contentEditable in focus mode, and also tests moving by word. * Fix linting issues. * this system test now forces and verifies focus mode. * Update what's new --- .../NVDAObjects/IAccessible/ia2TextMozilla.py | 13 +++ source/speech/__init__.py | 7 +- source/textInfos/__init__.py | 25 ++++-- tests/system/robot/chromeTests.py | 86 +++++++++++++++++++ tests/system/robot/chromeTests.robot | 3 + user_docs/en/changes.t2t | 1 + 6 files changed, 127 insertions(+), 8 deletions(-) diff --git a/source/NVDAObjects/IAccessible/ia2TextMozilla.py b/source/NVDAObjects/IAccessible/ia2TextMozilla.py index 299d9b8c392..a72e437f736 100644 --- a/source/NVDAObjects/IAccessible/ia2TextMozilla.py +++ b/source/NVDAObjects/IAccessible/ia2TextMozilla.py @@ -57,6 +57,19 @@ def _getEmbedded(obj, offset): class MozillaCompoundTextInfo(CompoundTextInfo): + def _getControlFieldForObject(self, obj, ignoreEditableText=True): + controlField = super()._getControlFieldForObject(obj, ignoreEditableText=ignoreEditableText) + if controlField is None: + return None + # Set the uniqueID of the controlField if we can get one + # which ensures that two controlFields with the same role and states etc are still treated differently + # if they are actually for different objects. + # E.g. two list items in a list. + uniqueID = obj.IA2UniqueID + if uniqueID is not None: + controlField["uniqueID"] = uniqueID + return controlField + def __init__(self, obj, position): super(MozillaCompoundTextInfo, self).__init__(obj, position) if isinstance(position, NVDAObject): diff --git a/source/speech/__init__.py b/source/speech/__init__.py index c52b76511a8..ad51b1da2bb 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -1660,7 +1660,12 @@ def getControlFieldSpeech( # noqa: C901 if not formatConfig: formatConfig=config.conf["documentFormatting"] - presCat=attrs.getPresentationCategory(ancestorAttrs,formatConfig, reason=reason) + presCat = attrs.getPresentationCategory( + ancestorAttrs, + formatConfig, + reason=reason, + extraDetail=extraDetail + ) childControlCount=int(attrs.get('_childcontrolcount',"0")) role = attrs.get('role', controlTypes.ROLE_UNKNOWN) if ( diff --git a/source/textInfos/__init__.py b/source/textInfos/__init__.py index 90abfbd4cbf..93695e6577b 100755 --- a/source/textInfos/__init__.py +++ b/source/textInfos/__init__.py @@ -47,7 +47,13 @@ class ControlField(Field): #: This field is just for layout. PRESCAT_LAYOUT = None - def getPresentationCategory(self, ancestors, formatConfig, reason=controlTypes.REASON_CARET): + def getPresentationCategory( + self, + ancestors, + formatConfig, + reason=controlTypes.REASON_CARET, + extraDetail=False + ): role = self.get("role", controlTypes.ROLE_UNKNOWN) states = self.get("states", set()) @@ -120,12 +126,17 @@ def getPresentationCategory(self, ancestors, formatConfig, reason=controlTypes.R or (role == controlTypes.ROLE_LIST and controlTypes.STATE_READONLY not in states) ): return self.PRESCAT_SINGLELINE - elif role in ( - controlTypes.ROLE_SEPARATOR, - controlTypes.ROLE_FOOTNOTE, - controlTypes.ROLE_ENDNOTE, - controlTypes.ROLE_EMBEDDEDOBJECT, - controlTypes.ROLE_MATH + elif ( + role in ( + controlTypes.ROLE_SEPARATOR, + controlTypes.ROLE_FOOTNOTE, + controlTypes.ROLE_ENDNOTE, + controlTypes.ROLE_EMBEDDEDOBJECT, + controlTypes.ROLE_MATH + ) + or ( + extraDetail and role == controlTypes.ROLE_LISTITEM + ) ): return self.PRESCAT_MARKER elif role in (controlTypes.ROLE_APPLICATION, controlTypes.ROLE_DIALOG): diff --git a/tests/system/robot/chromeTests.py b/tests/system/robot/chromeTests.py index 0bbb8efde7e..ff4289ed073 100644 --- a/tests/system/robot/chromeTests.py +++ b/tests/system/robot/chromeTests.py @@ -41,6 +41,91 @@ def checkbox_labelled_by_inner_element(): ) +def announce_list_item_when_moving_by_word_or_character(): + _chrome.prepareChrome( + r""" +
+

Before list

+
    +
  • small cat
  • +
  • big dog
  • +
+
+ """ + ) + # Force focus mode + actualSpeech = _chrome.getSpeechAfterKey("NVDA+space") + _asserts.strings_match( + actualSpeech, + "Focus mode" + ) + # Tab into the contenteditable + actualSpeech = _chrome.getSpeechAfterKey("tab") + _asserts.strings_match( + actualSpeech, + "section multi line editable Before list" + ) + # Ensure that moving into a list by line, "list item" is not reported. + actualSpeech = _chrome.getSpeechAfterKey("downArrow") + _asserts.strings_match( + actualSpeech, + "list small cat" + ) + # Ensure that when moving by word (control+rightArrow) + # within the list item, "list item" is not announced. + actualSpeech = _chrome.getSpeechAfterKey("control+rightArrow") + _asserts.strings_match( + actualSpeech, + "cat" + ) + # Ensure that when moving by character (rightArrow) + # within the list item, "list item" is not announced. + actualSpeech = _chrome.getSpeechAfterKey("rightArrow") + _asserts.strings_match( + actualSpeech, + "a" + ) + # move to the end of the line (and therefore the list item) + actualSpeech = _chrome.getSpeechAfterKey("end") + _asserts.strings_match( + actualSpeech, + "blank" + ) + # Ensure that when moving by character (rightArrow) + # onto the next list item, "list item" is reported. + actualSpeech = _chrome.getSpeechAfterKey("rightArrow") + _asserts.strings_match( + actualSpeech, + "\n".join([ + "list item level 1 ", + "b" + ]) + ) + # Ensure that when moving by character (leftArrow) + # onto the previous list item, "list item" is reported. + # Note this places us on the end-of-line insertion point of the previous list item. + actualSpeech = _chrome.getSpeechAfterKey("leftArrow") + _asserts.strings_match( + actualSpeech, + "list item level 1" + ) + # Ensure that when moving by word (control+rightArrow) + # onto the next list item, "list item" is reported. + actualSpeech = _chrome.getSpeechAfterKey("control+rightArrow") + _asserts.strings_match( + actualSpeech, + "list item level 1 big" + ) + # Ensure that when moving by word (control+leftArrow) + # onto the previous list item, "list item" is reported. + # Note this places us on the end-of-line insertion point of the previous list item. + actualSpeech = _chrome.getSpeechAfterKey("control+leftArrow") + _asserts.strings_match( + actualSpeech, + "list item level 1" + ) + + def test_i7562(): """ List should not be announced on every line of a ul in a contenteditable """ _chrome.prepareChrome( @@ -55,6 +140,7 @@ def test_i7562(): """ ) + # Force focus mode actualSpeech = _chrome.getSpeechAfterKey("NVDA+space") _asserts.strings_match( actualSpeech, diff --git a/tests/system/robot/chromeTests.robot b/tests/system/robot/chromeTests.robot index ce0e4322067..0c9263736e6 100644 --- a/tests/system/robot/chromeTests.robot +++ b/tests/system/robot/chromeTests.robot @@ -26,6 +26,9 @@ default teardown checkbox labelled by inner element [Documentation] A checkbox labelled by an inner element should not read the label element twice. checkbox_labelled_by_inner_element +Announce list item when moving by word or character + [Documentation] Entering a list item when moving by word or character should be announced, but not by line. + announce_list_item_when_moving_by_word_or_character i7562 [Documentation] List should not be announced on every line of a ul in a contenteditable test_i7562 diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 46b3736573d..a3482cbfb8d 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -21,6 +21,7 @@ What's New in NVDA - Report elapsed time in Foobar2000 if no total time is available (e.g. when playing a live stream). (#11337) - NVDA now honors the aria-roledescription attribute on elements in editable content in web pages. (#11607) - 'list' is no longer announced on every line of a list in Google Docs or other editable content in Google Chrome. (#7562) +- When arrowing by character or word from one list item to another in editable content on the web, entering the new list item is now announced. (#11569) == Changes for Developers == From 0baf0ceb4b8cc9c10c08ccaf5473857a8160660d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Golonka?= Date: Wed, 16 Sep 2020 13:29:09 +0200 Subject: [PATCH 20/27] Redirect focus to Desktop when start menu closed in Win 7 (PR #10567) When Windows 7 start menu closes and focus lands on a WorkewW pane redirect it to the focused desktop item. --- source/appModules/explorer.py | 28 ++++++++++++++++++++++++---- user_docs/en/changes.t2t | 1 + 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/source/appModules/explorer.py b/source/appModules/explorer.py index 20ba74aafc3..f155ccce45b 100644 --- a/source/appModules/explorer.py +++ b/source/appModules/explorer.py @@ -249,6 +249,26 @@ def _get_TextInfo(cls): return cls.TextInfo +class WorkerW(IAccessible): + def event_gainFocus(self): + # #6671: Normally we do not allow WorkerW thread to send gain focus event, + # 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)): + return + if eventHandler.isPendingEvents("gainFocus"): + return + if self.simpleFirstChild: + # If focus is not going to be moved autotically + # we need to forcefully move it to the focused desktop item. + # As we are interested in the first focusable object below the pane use simpleFirstChild. + self.simpleFirstChild.setFocus() + return + super().event_gainFocus() + + class AppModule(appModuleHandler.AppModule): def chooseNVDAObjectOverlayClasses(self, obj, clsList): @@ -305,6 +325,10 @@ def chooseNVDAObjectOverlayClasses(self, obj, clsList): clsList.insert(0, MetadataEditField) return # Optimization: return early to avoid comparing class names and roles that will never match. + if windowClass == "WorkerW" and role == controlTypes.ROLE_PANE and obj.name is None: + clsList.insert(0, WorkerW) + return # Optimization: return early to avoid comparing class names and roles that will never match. + if isinstance(obj, UIA): uiaClassName = obj.UIAElement.cachedClassName if uiaClassName == "GridTileElement": @@ -388,10 +412,6 @@ def event_gainFocus(self, obj, nextHandler): # #8137: also seen when opening quick link menu (Windows+X) on Windows 8 and later. return - if wClass == "WorkerW" and obj.role == controlTypes.ROLE_PANE and obj.name is None: - # #6671: Never allow WorkerW thread to send gain focus event, as it causes 'pane" to be announced when minimizing windows or moving to desktop. - return - nextHandler() def isGoodUIAWindow(self, hwnd): diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index a3482cbfb8d..7f5ae0a4fc8 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -22,6 +22,7 @@ What's New in NVDA - NVDA now honors the aria-roledescription attribute on elements in editable content in web pages. (#11607) - 'list' is no longer announced on every line of a list in Google Docs or other editable content in Google Chrome. (#7562) - When arrowing by character or word from one list item to another in editable content on the web, entering the new list item is now announced. (#11569) +- On Windows 7, opening and closing the start menu from the desktop now sets focus correctly. (#10567) == Changes for Developers == From 33d7b64c2e7fb1e783692796688712c70300e76a Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 17 Sep 2020 06:51:41 +1000 Subject: [PATCH 21/27] Announce the correct line when on the end of a link at the end of a list item in a contenteditable (#11606) * MozillaCompoundTextInfo: when trying to normalize the caret position at the end of an inline element, make sure not to search above the deepest non-inline element. This stops accidentally falling out of the current paragraph or list item etc and then inappropriately reading the next line instead of the current. * Fix linting issues * Add system test. * Fix linting issue * Make comment more explicit. Co-authored-by: Reef Turner * Update what's new Co-authored-by: Reef Turner --- .../NVDAObjects/IAccessible/ia2TextMozilla.py | 10 +++- tests/system/robot/chromeTests.py | 52 +++++++++++++++++++ tests/system/robot/chromeTests.robot | 3 ++ user_docs/en/changes.t2t | 1 + 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/source/NVDAObjects/IAccessible/ia2TextMozilla.py b/source/NVDAObjects/IAccessible/ia2TextMozilla.py index a72e437f736..ba7d7a76b82 100644 --- a/source/NVDAObjects/IAccessible/ia2TextMozilla.py +++ b/source/NVDAObjects/IAccessible/ia2TextMozilla.py @@ -112,7 +112,7 @@ def __init__(self, obj, position): # The caret is at the end of an inline object. # This will report "blank", but we want to report the character just after the caret. try: - caretTi, caretObj = self._findNextContent(caretTi) + caretTi, caretObj = self._findNextContent(caretTi, limitToInline=True) except LookupError: pass self._start = self._end = caretTi @@ -526,7 +526,7 @@ def expand(self, unit): self._end = end self._endObj = endObj - def _findNextContent(self, origin, moveBack=False): + def _findNextContent(self, origin, moveBack=False, limitToInline=False): if isinstance(origin, textInfos.TextInfo): ti = origin obj = ti.obj @@ -542,6 +542,12 @@ def _findNextContent(self, origin, moveBack=False): if obj == self.obj: # We're at the root. Don't go any further. raise LookupError + if limitToInline: + if obj.IA2Attributes.get('display') != 'inline': + # The caller requested to limit to inline objects. + # As this container is not inline, + # We cannot go above this container. + raise LookupError ti = self._getEmbedding(obj) if not ti: raise LookupError diff --git a/tests/system/robot/chromeTests.py b/tests/system/robot/chromeTests.py index ff4289ed073..a14b9be8488 100644 --- a/tests/system/robot/chromeTests.py +++ b/tests/system/robot/chromeTests.py @@ -170,3 +170,55 @@ def test_i7562(): actualSpeech, "out of list after", ) + + +def test_pr11606(): + """ + Announce the correct line when placed at the end of a link at the end of a list item in a contenteditable + """ + _chrome.prepareChrome( + r""" +
+
    +
  • A B
  • +
  • C D
  • +
+
+ """ + ) + # Force focus mode + actualSpeech = _chrome.getSpeechAfterKey("NVDA+space") + _asserts.strings_match( + actualSpeech, + "Focus mode" + ) + # Tab into the contenteditable + actualSpeech = _chrome.getSpeechAfterKey("tab") + _asserts.strings_match( + actualSpeech, + "section multi line editable list bullet link A link B" + ) + # move past the end of the first link. + # This should not be affected due to pr #11606. + actualSpeech = _chrome.getSpeechAfterKey("rightArrow") + _asserts.strings_match( + actualSpeech, + "\n".join([ + "out of link ", + "space" + ]) + ) + # Move to the end of the line (which is also the end of the second link) + # Before pr #11606 this would have announced the bullet on the next line. + actualSpeech = _chrome.getSpeechAfterKey("end") + _asserts.strings_match( + actualSpeech, + "link" + ) + # Read the current line. + # Before pr #11606 the next line ("C D") would have been read. + actualSpeech = _chrome.getSpeechAfterKey("NVDA+upArrow") + _asserts.strings_match( + actualSpeech, + "bullet link A link B" + ) diff --git a/tests/system/robot/chromeTests.robot b/tests/system/robot/chromeTests.robot index 0c9263736e6..46ee5229a8b 100644 --- a/tests/system/robot/chromeTests.robot +++ b/tests/system/robot/chromeTests.robot @@ -32,3 +32,6 @@ Announce list item when moving by word or character i7562 [Documentation] List should not be announced on every line of a ul in a contenteditable test_i7562 +pr11606 + [Documentation] Announce the correct line when placed at the end of a link at the end of a list item in a contenteditable + test_pr11606 diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 7f5ae0a4fc8..991745f20cf 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -22,6 +22,7 @@ What's New in NVDA - NVDA now honors the aria-roledescription attribute on elements in editable content in web pages. (#11607) - 'list' is no longer announced on every line of a list in Google Docs or other editable content in Google Chrome. (#7562) - When arrowing by character or word from one list item to another in editable content on the web, entering the new list item is now announced. (#11569) +- NVDA now reads the correct line when the caret is placed at the end of a link on the end of a list item in Google Docs or other editable content on the web. (#11606) - On Windows 7, opening and closing the start menu from the desktop now sets focus correctly. (#10567) From e587fdc4a575b3754c1982a8f9a6c9c8bf1f48d8 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Fri, 18 Sep 2020 17:27:49 +0200 Subject: [PATCH 22/27] Prevent users from creating regex errors in Speech Dictionary (PR #11409) Fixes #11407 --- source/gui/settingsDialogs.py | 9 ++++++++- user_docs/en/changes.t2t | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 7f41c6fca1a..b9a7f93b707 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2818,7 +2818,14 @@ def onOk(self,evt): self.patternTextCtrl.SetFocus() return try: - self.dictEntry=speechDictHandler.SpeechDictEntry(self.patternTextCtrl.GetValue(),self.replacementTextCtrl.GetValue(),self.commentTextCtrl.GetValue(),bool(self.caseSensitiveCheckBox.GetValue()),self.getType()) + dictEntry = self.dictEntry = speechDictHandler.SpeechDictEntry( + self.patternTextCtrl.GetValue(), + self.replacementTextCtrl.GetValue(), + self.commentTextCtrl.GetValue(), + bool(self.caseSensitiveCheckBox.GetValue()), + self.getType() + ) + dictEntry.sub("test") # Ensure there are no grouping error (#11407) except Exception as e: log.debugWarning("Could not add dictionary entry due to (regex error) : %s" % e) # Translators: This is an error message to let the user know that the dictionary entry is not valid. diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 991745f20cf..b145698df1b 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -9,6 +9,8 @@ What's New in NVDA - Pressing F1 inside NVDA dialogs will now open the help file to most relevant section. (#7757) - Support for auto complete suggestions (IntelliSense) in Microsoft SQL Server Management Studio plus Visual Studio 2017 and higher. (#7504) - Symbol pronunciation: Support for grouping in a complex symbol definition and support group references in a replacement rule making them simpler and more powerful. (#11107) +- Users are now notified when attempting to create Speech Dictionary entries with invalid regular expression substitutions. (#11407) + - Specifically grouping errors are now detected. == Changes == From 36a74bdc3db0f231fa61722cf4588e3521449397 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 21 Sep 2020 14:07:04 +0200 Subject: [PATCH 23/27] Fix Firefox/Chrome tab title speech cancellation (PR #11631) - Fix speech manager logging Despite unit test logging being hardcoded to False, it appeared that these messages were showing in the log. This was caused by the extra frame before the log call. - Fix for switching Tabs in Firefox. In this case the speech is triggered by a tree interceptor (see event_treeInterceptor_gainFocus in source/browseMode.py) which calls speech.speakObject(self.rootNVDAObject ....), and even thought self.rootNVDAObject may refer to the same object, it may not be exactly the same instance. --- source/eventHandler.py | 5 ++++- source/speech/manager.py | 10 ++++++++-- user_docs/en/changes.t2t | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/source/eventHandler.py b/source/eventHandler.py index 3dcdefbdcd1..d0e1a86dd76 100755 --- a/source/eventHandler.py +++ b/source/eventHandler.py @@ -153,6 +153,8 @@ def _trackFocusObject(eventName, obj) -> None: if eventName == "gainFocus": lastQueuedFocusObject = obj setattr(obj, WAS_GAIN_FOCUS_OBJ_ATTR_NAME, True) + if speech.manager._shouldDoSpeechManagerLogging(): + log.debug(f"Changing last queued focus object: {obj!r}") elif not hasattr(obj, WAS_GAIN_FOCUS_OBJ_ATTR_NAME): setattr(obj, WAS_GAIN_FOCUS_OBJ_ATTR_NAME, False) @@ -171,7 +173,8 @@ def previouslyHadFocus(): return getattr(obj, WAS_GAIN_FOCUS_OBJ_ATTR_NAME, False) def isLastFocusObj(): - return obj is lastQueuedFocusObject + # Obj may have been created multiple times pointing to the same underlying object. + return obj is lastQueuedFocusObject or obj == lastQueuedFocusObject def isAncestorOfCurrentFocus(): return obj in api.getFocusAncestors() diff --git a/source/speech/manager.py b/source/speech/manager.py index ec50b1104a4..5b67a64b5f3 100644 --- a/source/speech/manager.py +++ b/source/speech/manager.py @@ -80,7 +80,13 @@ def _speechManagerUnitTest(msg, *args, **kwargs) -> None: When """ if not IS_UNIT_TEST_LOG_ENABLED: - return _speechManagerDebug(msg, *args, **kwargs) + # Don't reuse _speechManagerDebug, it leads to incorrect function names in the log (all + # SpeechManager debug logging appears to come from _speechManagerUnitTest instead of the frame + # one stack higher. The codepath argument for _log could also be used to resolve this, but duplication + # simpler. + if log.isEnabledFor(log.DEBUG) and _shouldDoSpeechManagerLogging(): + log._log(log.DEBUG, f"SpeechManager- " + msg, args, **kwargs) + return log._log(log.INFO, f"SpeechManUnitTest- " + msg, args, **kwargs) # Install the custom log handlers. @@ -420,7 +426,7 @@ def _pushNextSpeech(self, doneSpeaking: bool): if isinstance(item, IndexCommand): self._indexesSpeaking.append(item.index) self._cancelledLastSpeechWithSynth = False - log._speechManagerUnitTest(f"Assert Synth Gets: {seq}") + log._speechManagerUnitTest(f"Synth Gets: {seq}") getSynth().speak(seq) def _getNextPriority(self): diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index b145698df1b..dab7173b344 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -26,6 +26,7 @@ What's New in NVDA - When arrowing by character or word from one list item to another in editable content on the web, entering the new list item is now announced. (#11569) - NVDA now reads the correct line when the caret is placed at the end of a link on the end of a list item in Google Docs or other editable content on the web. (#11606) - On Windows 7, opening and closing the start menu from the desktop now sets focus correctly. (#10567) +- When "attempt to cancel expired focus events" is enabled, the title of the tab is now announced again when switching tabs in Firefox. (#11397) == Changes for Developers == From 66bcf8ea9450c2cd19aacf6863b7941c8a068367 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Tue, 22 Sep 2020 23:13:12 +1000 Subject: [PATCH 24/27] Speech manager no longer sends synths utterances containing only param change and index commands. (#11651) * Speech manager: no longer send pointless sequences to a synth that only contain synth param commands and indexCommands. This was causing some synths to ignore the index. * Fix linting issue * unit test test_4_profiles: use create_expectedIndex rather than ExpectedIndex directly. * Address review comment * Added unit test for pr #11651 that tests that a redundant sequence containing param change and index commands is no longer emmitted after an utterance that contains param change commands and ends in an EndUtterance command E.g. speaking a character. * Fix linting issue * Apply suggestions from code review Co-authored-by: Reef Turner * Update what's new Co-authored-by: Reef Turner --- source/speech/manager.py | 27 ++++++---- tests/unit/test_speechManager/__init__.py | 66 ++++++++++++++++------- user_docs/en/changes.t2t | 1 + 3 files changed, 66 insertions(+), 28 deletions(-) diff --git a/source/speech/manager.py b/source/speech/manager.py index 5b67a64b5f3..42fd3622137 100644 --- a/source/speech/manager.py +++ b/source/speech/manager.py @@ -312,28 +312,36 @@ def _processSpeechSequence(self, inSeq: SpeechSequence): paramTracker = ParamChangeTracker() enteredTriggers = [] outSeqs = [] + paramsToReplay = [] def ensureEndUtterance(seq: SpeechSequence): # We split at EndUtteranceCommands so the ends of utterances are easily found. + # This function ensures the given sequence ends with an EndUtterance command, + # Ensures that the sequence also includes an index command at the end, + # It places the complete sequence in outSeqs, + # It clears the given sequence list ready to build a new one, + # And clears the paramsToReplay list + # and refills it with any params that need to be repeated if a new sequence is going to be built. if seq: # There have been commands since the last split. - outSeqs.append(seq) - lastOutSeq = seq + lastOutSeq = paramsToReplay + seq + outSeqs.append(lastOutSeq) + paramsToReplay.clear() + seq.clear() # Re-apply parameters that have been changed from their defaults. - seq = paramTracker.getChanged() + paramsToReplay.extend(paramTracker.getChanged()) else: lastOutSeq = outSeqs[-1] if outSeqs else None lastCommand = lastOutSeq[-1] if lastOutSeq else None if not lastCommand or isinstance(lastCommand, (EndUtteranceCommand, ConfigProfileTriggerCommand)): # It doesn't make sense to start with or repeat EndUtteranceCommands. # We also don't want an EndUtteranceCommand immediately after a ConfigProfileTriggerCommand. - return seq + return if not isinstance(lastCommand, IndexCommand): # Add an index so we know when we've reached the end of this utterance. reachedIndex = next(self._indexCounter) lastOutSeq.append(IndexCommand(reachedIndex)) outSeqs.append([EndUtteranceCommand()]) - return seq outSeq = [] for command in inSeq: @@ -343,8 +351,9 @@ def ensureEndUtterance(seq: SpeechSequence): outSeq.append(IndexCommand(speechIndex)) self._indexesToCallbacks[speechIndex] = command # We split at indexes so we easily know what has completed speaking. - outSeqs.append(outSeq) - outSeq = [] + outSeqs.append(paramsToReplay + outSeq) + paramsToReplay.clear() + outSeq.clear() continue if isinstance(command, ConfigProfileTriggerCommand): if not command.trigger.hasProfile: @@ -356,7 +365,7 @@ def ensureEndUtterance(seq: SpeechSequence): if not command.enter and command.trigger not in enteredTriggers: log.debugWarning("Request to exit trigger which wasn't entered: %r" % command.trigger.spec) continue - outSeq = ensureEndUtterance(outSeq) + ensureEndUtterance(outSeq) outSeqs.append([command]) if command.enter: enteredTriggers.append(command.trigger) @@ -364,7 +373,7 @@ def ensureEndUtterance(seq: SpeechSequence): enteredTriggers.remove(command.trigger) continue if isinstance(command, EndUtteranceCommand): - outSeq = ensureEndUtterance(outSeq) + ensureEndUtterance(outSeq) continue if isinstance(command, SynthParamCommand): paramTracker.update(command) diff --git a/tests/unit/test_speechManager/__init__.py b/tests/unit/test_speechManager/__init__.py index 8a713f879cd..dc5d526005a 100644 --- a/tests/unit/test_speechManager/__init__.py +++ b/tests/unit/test_speechManager/__init__.py @@ -17,6 +17,8 @@ PitchCommand, ConfigProfileTriggerCommand, _CancellableSpeechCommand, + CharacterModeCommand, + EndUtteranceCommand, ) from .speechManagerTestHarness import ( _IndexT, @@ -826,10 +828,16 @@ def test_4_profiles(self): "Testing testing ", speech.PitchCommand(offset=100), "1 2 3 4", - smi.create_ConfigProfileTriggerCommand(t1, True, expectedToBecomeIndex=1), - smi.create_ConfigProfileTriggerCommand(t2, True, expectedToBecomeIndex=2), + smi.create_ExpectedIndex(1), + # The preceeding index is expected, + # as the following profile trigger commands will cause the utterance to be split here. + ConfigProfileTriggerCommand(t1, True), + ConfigProfileTriggerCommand(t2, True), "5 6 7 8", - smi.create_ConfigProfileTriggerCommand(t1, False, expectedToBecomeIndex=3), + smi.create_ExpectedIndex(2), + # The preceeding index is expected, + # as the following profile trigger commands will cause the utterance to be split here. + ConfigProfileTriggerCommand(t1, False), "9 10 11 12" ] with smi.expectation(): @@ -842,28 +850,17 @@ def test_4_profiles(self): smi.doneSpeaking() smi.pumpAll() smi.expect_synthCancel() - smi.expect_synthSpeak(sequence=[ - seq[1], # PitchCommand - seq[4], # IndexCommand index=2 (derived from ConfigProfileTriggerCommand) - ]) smi.expect_mockCall(t1.enter) - - with smi.expectation(): - smi.indexReached(2) - smi.pumpAll() - with smi.expectation(): - smi.doneSpeaking() - smi.pumpAll() smi.expect_synthCancel() + smi.expect_mockCall(t2.enter) smi.expect_synthSpeak(sequence=[ seq[1], # PitchCommand '5 6 7 8', - seq[6], # IndexCommand index=3 (derived from ConfigProfileTriggerCommand) + seq[7], # IndexCommand index=2 (due to a ConfigProfileTriggerCommand following it) ]) - smi.expect_mockCall(t2.enter) with smi.expectation(): - smi.indexReached(3) + smi.indexReached(2) smi.pumpAll() with smi.expectation(): smi.doneSpeaking() @@ -872,12 +869,12 @@ def test_4_profiles(self): smi.expect_synthSpeak(sequence=[ seq[1], # PitchCommand '9 10 11 12', - smi.create_ExpectedIndex(expectedToBecomeIndex=4) + smi.create_ExpectedIndex(expectedToBecomeIndex=3) ]) smi.expect_mockCall(t1.exit) with smi.expectation(): - smi.indexReached(4) + smi.indexReached(3) smi.pumpAll() with smi.expectation(): smi.doneSpeaking() @@ -1115,3 +1112,34 @@ class InitialDevelopmentTests_withCancellableSpeechEnabled(InitialDevelopmentTes def setUp(self): super().setUp() config.conf['featureFlag']['cancelExpiredFocusSpeech'] = 1 # yes + + +class Test_pr11651(unittest.TestCase): + + def test_redundantSequenceAfterEndUtterance(self): + """ + Tests that redundant param change and index commands are not emitted as an extra utterance + when the preceeding utterance contained param change commands and an EndUtterance command. + E.g. speaking a character. + """ + smi = SpeechManagerInteractions(self) + seq = [ + CharacterModeCommand(True), + "a", + smi.create_ExpectedIndex(1), + EndUtteranceCommand(), + ] + with smi.expectation(): + smi.speak(seq) + # synth should receive the characterMode, the letter 'a' and an index command. + smi.expect_synthSpeak(sequence=seq[:-1]) + with smi.expectation(): + # Previously, this would result in synth.speak receiving + # a call with sequence: + # [CharacterModeCommand(True), IndexCommand(2)] + # This is a problem because it includes an index command but no speech. + # This is inefficient, and also some SAPI5 synths such as Ivona will not + # notify of this bookmark. + smi.indexReached(1) + smi.doneSpeaking() + smi.pumpAll() diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index dab7173b344..a9614e9aede 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -27,6 +27,7 @@ What's New in NVDA - NVDA now reads the correct line when the caret is placed at the end of a link on the end of a list item in Google Docs or other editable content on the web. (#11606) - On Windows 7, opening and closing the start menu from the desktop now sets focus correctly. (#10567) - When "attempt to cancel expired focus events" is enabled, the title of the tab is now announced again when switching tabs in Firefox. (#11397) +- NVDA no longer fails to announce a list item after typing a character in a list when speaking with the SAPI5 Ivona voices. (#11651) == Changes for Developers == From e212721587fa1ddde8168c0cc91152fe2ea021bc Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 24 Sep 2020 12:10:51 +1000 Subject: [PATCH 25/27] Add a code of conduct for the NVDA project adopted by NV Access (#11659) * Code of conduct * Link to code of conduct from readme, issue and pull request templates. --- .github/ISSUE_TEMPLATE.md | 1 + .github/ISSUE_TEMPLATE/bug_report.md | 1 + .github/ISSUE_TEMPLATE/feature_request.md | 1 + .github/PULL_REQUEST_TEMPLATE.md | 1 + CODE_OF_CONDUCT.md | 64 +++++++++++++++++++++++ readme.md | 2 + 6 files changed, 70 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index d6897864855..97c318157dd 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -9,6 +9,7 @@ Direct links to the templates: Please thoroughly read NVDA's wiki article on how to fill in this template, including how to provide the required files. 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. --> ### Steps to reproduce: diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index a4514f3a4f5..0291b356a17 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -8,6 +8,7 @@ about: Create a report to help us improve Please thoroughly read NVDA's wiki article on how to fill in this template, including how to provide the required files. 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. --> ### Steps to reproduce: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 36e6a3f3057..7fde5778b30 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -8,6 +8,7 @@ about: Suggest an idea for this project Please thoroughly read NVDA's wiki article on how to fill in this template, including how to provide the required files. 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. --> ### Is your feature request related to a problem? Please describe. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ac3dce93cd1..359e8f60eda 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,6 @@ ### Link to issue number: diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..97e3b5d3765 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,64 @@ +# Citizen and Contributor Code of Conduct + +## 1. Purpose +A primary goal of NV Access and NVDA is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, nationality, socioeconomic status, education, level of experience and religion (or lack thereof). +This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behaviour. +We invite all those who participate in the NVDA community to help us create safe and positive experiences for everyone. NVDA is a user driven initiative. + +## 2. Open Source Citizenship +A supplemental goal of this Code of Conduct is to increase open source citizenship by encouraging participants to recognise and strengthen the relationships between our actions and their effects on our community. +Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. +If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know. + +## 3. Expected Behaviour +The following behaviours are expected and requested of all community members: +* To participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. +* To exercise consideration and respect in your speech and actions. +* To attempt collaboration before conflict. +* To provide helpful contributions to an issue in order to encourage its progression +* To be respectful of differing viewpoints and experiences +* To gracefully accept constructive criticism +* To respect the need to prioritise issues based on NV Access, user, and community priority +* To be aware that GitHub is a public forum and refrain from publishing personal or sensitive information (your own or others). For example, carefully consider the contents of debug logs when publishing. +* To refrain from demeaning, discriminatory, or harassing behaviour and speech. +* To be mindful of your surroundings and of your fellow participants. Alert NV Access staff if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. + +## 4. Unacceptable Behaviour +The following behaviours are considered harassment and are unacceptable within our community: +* Violence, threats of violence or violent language directed against another person. +* Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. +* Posting or displaying sexually explicit or violent material. +* Posting or threatening to post other people's personally identifying information ("doxing"). +* Trolling, insulting/derogatory comments, and personal or political attacks +* Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. +* Inappropriate photography or recording. +* Unwelcome sexual attention. This includes sexualized comments or jokes, inappropriate touching, groping, and unwelcomed sexual advances. +* Deliberate intimidation, stalking or following (online or in person). +* Advocating for, or encouraging, any of the above behaviour. +* Sustained disruption of community events, including talks and presentations. + +## 5. Consequences of Unacceptable Behaviour +Unacceptable behaviour from any community member, including sponsors and those with decision-making authority, will not be tolerated. +Anyone asked to stop unacceptable behaviour is expected to comply immediately. +If a community member engages in unacceptable behaviour, NV Access Staff may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event). +NV Access Staff have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviours that they deem inappropriate, threatening, offensive, or harmful. + +## 6. Reporting Guidelines +If you are subject to or witness unacceptable behaviour, or have any other concerns, please notify NV Access staff as soon as possible via info@nvaccess.org. The information required for notifications includes PR/Issue Number, nature of breach, community member responsible, date and time of breach. Please note that the details of the reporter will not be disclosed, unless required to by law. + +## 7. Addressing Grievances +If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify NV Access at info@nvaccess.org with a concise description of your grievance. + +## 8. Scope +We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues--online and in-person--as well as in all one-on-one communications pertaining to community business. +This code of conduct and its related procedures also applies to unacceptable behaviour occurring outside the scope of community activities when such behaviour has the potential to adversely affect the safety and well-being of community members. +For others setting up or running an NVDA group of any kind, it is strongly recommended that a complementary Citizen and Contributor Code of Conduct is adopted. + +## 9. Contact info +info@nvaccess.org + +## 10. License and attribution +The Citizen and Contributor Code of Conduct is distributed by NV Access Limited. +Portions of text were derived from the GitHub sample Citizen and Contributor Code of Conduct, which is distributed under a Creative Commons Attribution-ShareAlike license. + +Revision1.0 adopted by NV Access Limited on 2020-09-23 diff --git a/readme.md b/readme.md index 17753fca799..8a377d9bf1f 100644 --- a/readme.md +++ b/readme.md @@ -4,6 +4,8 @@ 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. + ## 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. From ea8606c018f67f932901e8efd09351e360870c17 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 24 Sep 2020 12:14:53 +1000 Subject: [PATCH 26/27] Fix case of code of conduct filename. Fix for pr #11659 --- CODE_OF_CONDUCT.md => CODE_OF_CONDUCT.MD | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename CODE_OF_CONDUCT.md => CODE_OF_CONDUCT.MD (100%) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.MD similarity index 100% rename from CODE_OF_CONDUCT.md rename to CODE_OF_CONDUCT.MD From afb8fce518f4471ab622e0510f1dc97750c4faa4 Mon Sep 17 00:00:00 2001 From: Joseph Lee Date: Mon, 28 Sep 2020 18:25:12 -0700 Subject: [PATCH 27/27] App modules: add commsapps app module for Windows 10 Mail and Calendar in order to restore browse mode functionality when reading emails (#11608) * appModules: add commsapps app module for Windows 10 Mail and Calendar 16005.13110 in order to restore browse mode functionality. Re #11439. In 2020, as part of unifying Mail and Calendar, hxoutlook.exe was renamed to commsapps.exe. Without the new app module, browse mode in Mail will not function, therefore add an alias app module for hxoutlook.py. * appModules/commsapps: flake8 (NOQA: F401, F403 as this is an alias app module) * Update what's new Co-authored-by: Michael Curran --- source/appModules/commsapps.py | 7 +++++++ user_docs/en/changes.t2t | 1 + 2 files changed, 8 insertions(+) create mode 100644 source/appModules/commsapps.py diff --git a/source/appModules/commsapps.py b/source/appModules/commsapps.py new file mode 100644 index 00000000000..48f3c95d19d --- /dev/null +++ b/source/appModules/commsapps.py @@ -0,0 +1,7 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2020 NV Access Limited, Joseph Lee +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. + +# An alias for hxoutlook appModule +from .hxoutlook import * # NOQA: F401, F403 diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 09de41ffb90..fa0141223bc 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -28,6 +28,7 @@ What's New in NVDA - On Windows 7, opening and closing the start menu from the desktop now sets focus correctly. (#10567) - When "attempt to cancel expired focus events" is enabled, the title of the tab is now announced again when switching tabs in Firefox. (#11397) - NVDA no longer fails to announce a list item after typing a character in a list when speaking with the SAPI5 Ivona voices. (#11651) +- It is again possible to use browse mode when reading emails in Windows 10 Mail 16005.13110 and later. (#11439) == Changes for Developers ==