From 0d870ac69010cca1b1cd98af94dc0b8776849816 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Thu, 23 Jan 2020 14:48:36 -0500 Subject: [PATCH 1/8] Role back #10684. --- source/NVDAObjects/UIA/__init__.py | 3 +- source/UIAUtils.py | 23 +++++- source/_UIAHandler.py | 10 ++- source/config/configSpec.py | 2 +- source/gui/settingsDialogs.py | 109 +++++++++++++++++++---------- user_docs/en/userGuide.t2t | 22 ++++-- 6 files changed, 117 insertions(+), 52 deletions(-) diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index 2cd862ac9ad..3c78ae7dd36 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -26,6 +26,7 @@ import textInfos from logHandler import log from UIAUtils import * +from UIAUtils import shouldUseUIAConsole from NVDAObjects.window import Window from NVDAObjects import NVDAObjectTextInfo, InvalidNVDAObject from NVDAObjects.behaviors import ( @@ -929,7 +930,7 @@ def findOverlayClasses(self,clsList): # Support Windows Console's UIA interface if ( self.windowClassName == "ConsoleWindowClass" - and config.conf['UIA']['winConsoleImplementation'] == "UIA" + and shouldUseUIAConsole() ): from . import winConsoleUIA winConsoleUIA.findExtraOverlayClasses(self, clsList) diff --git a/source/UIAUtils.py b/source/UIAUtils.py index 248765486ac..a20ea4b49e7 100644 --- a/source/UIAUtils.py +++ b/source/UIAUtils.py @@ -1,12 +1,14 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2015-2016 NV Access Limited +# Copyright (C) 2015-2019 NV Access Limited, Bill Dengler # This file is covered by the GNU General Public License. # See the file COPYING for more details. import operator from comtypes import COMError +import config import ctypes import UIAHandler +from winVersion import isWin10 def createUIAMultiPropertyCondition(*dicts): """ @@ -224,3 +226,22 @@ def getValue(self,ID,ignoreMixedValues=False): if not ignoreMixedValues and val==UIAHandler.handler.ReservedMixedAttributeValue: raise UIAMixedAttributeError return val + + +def shouldUseUIAConsole(setting=None): + """Determines whether to use UIA in the Windows Console. +@param setting: the config value to base this check on (if not provided, +it is retrieved from config). + """ + if not setting: + setting = config.conf['UIA']['winConsoleImplementation'] + if setting == "legacy": + return False + elif setting == "UIA": + return True + # #7497: Windows 10 Fall Creators Update has an incomplete UIA + # implementation for console windows, therefore for now we should + # ignore it. + # It does not implement caret/selection, and probably has no + # new text events. + return isWin10(1809) diff --git a/source/_UIAHandler.py b/source/_UIAHandler.py index 03f44ac24e0..6d6058c580f 100644 --- a/source/_UIAHandler.py +++ b/source/_UIAHandler.py @@ -500,12 +500,10 @@ def IUIAutomationNotificationEventHandler_HandleNotificationEvent( def _isBadUIAWindowClassName(self, windowClass): "Given a windowClassName, returns True if this is a known problematic UIA implementation." - # #7497: Windows 10 Fall Creators Update has an incomplete UIA - # implementation for console windows, therefore for now we should - # ignore it. - # It does not implement caret/selection, and probably has no new text - # events. - if windowClass == "ConsoleWindowClass" and config.conf['UIA']['winConsoleImplementation'] != "UIA": + if ( + windowClass == "ConsoleWindowClass" + and not UIAUtils.shouldUseUIAConsole() + ): return True return windowClass in badUIAWindowClassNames diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 02cc2d6a575..a74c60fab10 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -209,7 +209,7 @@ [UIA] enabled = boolean(default=true) useInMSWordWhenAvailable = boolean(default=false) - winConsoleImplementation= option("auto", "legacy", "UIA", default="auto") + winConsoleImplementation= option("auto", "legacy", "UIA", default="legacy") [terminals] speakPasswords = boolean(default=false) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index ca61be49f7a..7ef81bc593a 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -1,9 +1,9 @@ # -*- coding: UTF-8 -*- # settingsDialogs.py # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2020 NV Access Limited, Peter Vágner, Aleksey Sadovoy, +# Copyright (C) 2006-2019 NV Access Limited, Peter Vágner, Aleksey Sadovoy, # Rui Batista, Joseph Lee, Heiko Folkerts, Zahari Yurukov, Leonard de Ruijter, -# Derek Riemer, Babbage B.V., Davy Kager, Ethan Holliger, Bill Dengler +# Derek Riemer, Babbage B.V., Davy Kager, Ethan Holliger, Bill Dengler # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -51,6 +51,7 @@ from autoSettingsUtils.utils import UnsupportedConfigParameterError from autoSettingsUtils.autoSettings import AutoSettings from autoSettingsUtils.driverSetting import BooleanDriverSetting, NumericDriverSetting, DriverSetting +from UIAUtils import shouldUseUIAConsole import touchHandler import winVersion import weakref @@ -2270,20 +2271,43 @@ def __init__(self, parent): self.UIAInMSWordCheckBox.SetValue(config.conf["UIA"]["useInMSWordWhenAvailable"]) self.UIAInMSWordCheckBox.defaultValue = self._getDefaultValue(["UIA", "useInMSWordWhenAvailable"]) - # Translators: This is the label for a checkbox in the - # Advanced settings panel. - label = _("Use UI Automation to access the Windows C&onsole when available") - consoleUIADevMap = True if config.conf['UIA']['winConsoleImplementation'] == 'UIA' else False - self.ConsoleUIACheckBox = UIAGroup.addItem(wx.CheckBox(self, label=label)) - self.ConsoleUIACheckBox.SetValue(consoleUIADevMap) - self.ConsoleUIACheckBox.defaultValue = self._getDefaultValue(["UIA", "winConsoleImplementation"]) - - # Translators: This is the label for a checkbox in the - # Advanced settings panel. - label = _("Speak &passwords in UIA consoles (may improve performance)") - self.winConsoleSpeakPasswordsCheckBox = UIAGroup.addItem(wx.CheckBox(self, label=label)) - self.winConsoleSpeakPasswordsCheckBox.SetValue(config.conf["terminals"]["speakPasswords"]) - self.winConsoleSpeakPasswordsCheckBox.defaultValue = self._getDefaultValue(["terminals", "speakPasswords"]) + # Translators: This is the label for a combo box for selecting the + # active console implementation in the advanced settings panel. + # Choices are automatic, prefer UIA, and legacy. + consoleComboText = _("Windows C&onsole support:") + consoleChoices = [ + # Translators: A choice in a combo box in the advanced settings + # panel to have NVDA determine its Windows Console implementation + # automatically. + _("Automatic"), + # Translators: A choice in a combo box in the advanced settings + # panel to have NVDA use UIA in the Windows Console when available. + _("Prefer UIA"), + # Translators: A choice in a combo box in the advanced settings + # panel to have NVDA use its legacy Windoes Console support + # in all cases. + _("Legacy") + ] + #: The possible console config values, in the order they appear + #: in the combo box. + self.consoleVals = ( + "auto", + "UIA", + "legacy" + ) + self.consoleCombo = UIAGroup.addLabeledControl(consoleComboText, wx.Choice, choices=consoleChoices) + self.consoleCombo.Bind( + wx.EVT_CHOICE, + self.enableConsolePasswordsCheckBox, + self.consoleCombo + ) + curChoice = self.consoleVals.index( + config.conf['UIA']['winConsoleImplementation'] + ) + self.consoleCombo.SetSelection(curChoice) + self.consoleCombo.defaultValue = self.consoleVals.index( + self._getDefaultValue(["UIA", "winConsoleImplementation"]) + ) # Translators: This is the label for a group of advanced options in the # Advanced settings panel @@ -2295,7 +2319,14 @@ def __init__(self, parent): sHelper.addItem(terminalsGroup) # Translators: This is the label for a checkbox in the # Advanced settings panel. - label = _("Use the new t&yped character support in Windows Console when available") + label = _("Speak &passwords in Windows Console (may improve performance)") + self.speakPasswordsCheckBox = terminalsGroup.addItem(wx.CheckBox(self, label=label)) + self.speakPasswordsCheckBox.SetValue(config.conf["terminals"]["speakPasswords"]) + self.speakPasswordsCheckBox.defaultValue = self._getDefaultValue(["terminals", "speakPasswords"]) + self.enableConsolePasswordsCheckBox() + # Translators: This is the label for a checkbox in the + # Advanced settings panel. + label = _("Use the new t&yped character support in legacy Windows consoles when available") self.keyboardSupportInLegacyCheckBox=terminalsGroup.addItem(wx.CheckBox(self, label=label)) self.keyboardSupportInLegacyCheckBox.SetValue(config.conf["terminals"]["keyboardSupportInLegacy"]) self.keyboardSupportInLegacyCheckBox.defaultValue = self._getDefaultValue(["terminals", "keyboardSupportInLegacy"]) @@ -2376,6 +2407,13 @@ def __init__(self, parent): ] self.Layout() + def enableConsolePasswordsCheckBox(self, evt=None): + return self.speakPasswordsCheckBox.Enable( + shouldUseUIAConsole(self.consoleVals[ + self.consoleCombo.GetSelection() + ]) + ) + def onOpenScratchpadDir(self,evt): path=config.getScratchpadDir(ensureExists=True) os.startfile(path) @@ -2385,26 +2423,23 @@ def _getDefaultValue(self, configPath): def haveConfigDefaultsBeenRestored(self): return ( - self._defaultsRestored - and self.scratchpadCheckBox.IsChecked() == self.scratchpadCheckBox.defaultValue - and self.UIAInMSWordCheckBox.IsChecked() == self.UIAInMSWordCheckBox.defaultValue - and self.ConsoleUIACheckBox.IsChecked() == (self.ConsoleUIACheckBox.defaultValue == 'UIA') - and self.winConsoleSpeakPasswordsCheckBox.IsChecked() == self.winConsoleSpeakPasswordsCheckBox.defaultValue - and self.keyboardSupportInLegacyCheckBox.IsChecked() == self.keyboardSupportInLegacyCheckBox.defaultValue - and ( - self.autoFocusFocusableElementsCheckBox.IsChecked() - == self.autoFocusFocusableElementsCheckBox.defaultValue - ) - and self.caretMoveTimeoutSpinControl.GetValue() == self.caretMoveTimeoutSpinControl.defaultValue - and set(self.logCategoriesList.CheckedItems) == set(self.logCategoriesList.defaultCheckedItems) - and True # reduce noise in diff when the list is extended. + self._defaultsRestored and + self.scratchpadCheckBox.IsChecked() == self.scratchpadCheckBox.defaultValue and + self.UIAInMSWordCheckBox.IsChecked() == self.UIAInMSWordCheckBox.defaultValue and + self.consoleCombo.GetSelection() == self.consoleCombo.defaultValue and + self.speakPasswordsCheckBox.IsChecked() == self.speakPasswordsCheckBox.defaultValue and + self.keyboardSupportInLegacyCheckBox.IsChecked() == self.keyboardSupportInLegacyCheckBox.defaultValue and + self.autoFocusFocusableElementsCheckBox.IsChecked() == self.autoFocusFocusableElementsCheckBox.defaultValue and + self.caretMoveTimeoutSpinControl.GetValue() == self.caretMoveTimeoutSpinControl.defaultValue and + set(self.logCategoriesList.CheckedItems) == set(self.logCategoriesList.defaultCheckedItems) and + True # reduce noise in diff when the list is extended. ) def restoreToDefaults(self): self.scratchpadCheckBox.SetValue(self.scratchpadCheckBox.defaultValue) self.UIAInMSWordCheckBox.SetValue(self.UIAInMSWordCheckBox.defaultValue) - self.ConsoleUIACheckBox.SetValue(self.ConsoleUIACheckBox.defaultValue == 'UIA') - self.winConsoleSpeakPasswordsCheckBox.SetValue(self.winConsoleSpeakPasswordsCheckBox.defaultValue) + self.consoleCombo.SetSelection(self.consoleCombo.defaultValue == 'UIA') + self.speakPasswordsCheckBox.SetValue(self.speakPasswordsCheckBox.defaultValue) self.keyboardSupportInLegacyCheckBox.SetValue(self.keyboardSupportInLegacyCheckBox.defaultValue) self.autoFocusFocusableElementsCheckBox.SetValue(self.autoFocusFocusableElementsCheckBox.defaultValue) self.caretMoveTimeoutSpinControl.SetValue(self.caretMoveTimeoutSpinControl.defaultValue) @@ -2415,11 +2450,11 @@ def onSave(self): log.debug("Saving advanced config") config.conf["development"]["enableScratchpadDir"]=self.scratchpadCheckBox.IsChecked() config.conf["UIA"]["useInMSWordWhenAvailable"]=self.UIAInMSWordCheckBox.IsChecked() - if self.ConsoleUIACheckBox.IsChecked(): - config.conf['UIA']['winConsoleImplementation'] = "UIA" - else: - config.conf['UIA']['winConsoleImplementation'] = "auto" - config.conf["terminals"]["speakPasswords"] = self.winConsoleSpeakPasswordsCheckBox.IsChecked() + consoleChoice = self.consoleCombo.GetSelection() + config.conf['UIA']['winConsoleImplementation'] = ( + self.consoleVals[consoleChoice] + ) + config.conf["terminals"]["speakPasswords"] = self.speakPasswordsCheckBox.IsChecked() config.conf["terminals"]["keyboardSupportInLegacy"]=self.keyboardSupportInLegacyCheckBox.IsChecked() config.conf["virtualBuffers"]["autoFocusFocusableElements"] = self.autoFocusFocusableElementsCheckBox.IsChecked() config.conf["editableText"]["caretMoveTimeoutMs"]=self.caretMoveTimeoutSpinControl.GetValue() diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index d2636244d2e..ae6658b3e74 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -1779,14 +1779,24 @@ This includes Microsoft Word itself, and also the Microsoft Outlook message view However, There may be some information which is either not exposed, or exposed incorrectly in some versions of Microsoft Office, which means this UI automation support cannot always be relied upon. We still do not recommend that the majority of users turn this on by default, though we do welcome users of Office 2016/365 to test this feature and provide feedback. -==== Use UI Automation to access the Windows Console when available ====[AdvancedSettingsConsoleUIA] -When this option is enabled, NVDA will use a new, work in progress version of its support for Windows Console which takes advantage of [accessibility improvements made by Microsoft https://devblogs.microsoft.com/commandline/whats-new-in-windows-console-in-windows-10-fall-creators-update/]. This feature is highly experimental and is still incomplete, so its use is not yet recommended. However, once completed, it is anticipated that this new support will become the default, improving NVDA's performance and stability in Windows command consoles. +==== Windows Console support ====[AdvancedSettingsConsoleUIA] +This option selects how NVDA interacts with the Windows Console used by command prompt, PowerShell, and the Windows Subsystem for Linux. +It does not affect the modern Windows Terminal. +In Windows 10 version 1709, Microsoft [added support for its UI Automation API to the console https://devblogs.microsoft.com/commandline/whats-new-in-windows-console-in-windows-10-fall-creators-update/], bringing vastly improved performance and stability for screen readers that support it. +In situations where UI Automation is unavailable or known to result in an inferior user experience, NVDA's legacy console support is available as a fallback. +The Windows Console support combo box has three options: +- Automatic: Uses UI Automation in consoles on Windows 10 version 1809 and later. This option is recommended and set by default. +- Prefer UIA: Uses UI Automation in consoles if available, even on Windows versions with incomplete or buggy implementations. While this limited functionality may be useful (and even sufficient for your usage), use of this option is entirely at your own risk and no support for it will be provided. +- Legacy: UI Automation in the Windows Console will be completely disabled, so the legacy fallback will always be used. +- -==== Speak passwords in UIA consoles ====[AdvancedSettingsWinConsoleSpeakPasswords] -This setting controls whether characters are spoken by [speak typed characters #KeyboardSettingsSpeakTypedCharacters] or [speak typed words #KeyboardSettingsSpeakTypedWords] in situations where the screen does not update (such as password entry) in the Windows Console with UI automation support enabled. For security purposes, this setting should be left disabled. However, you may wish to enable it if you experience performance issues or instability with typed character and/or word reporting while using NVDA's new experimental console support. +==== Speak passwords in Windows Console ====[AdvancedSettingsWinConsoleSpeakPasswords] +This setting controls whether characters are spoken by [speak typed characters #KeyboardSettingsSpeakTypedCharacters] or [speak typed words #KeyboardSettingsSpeakTypedWords] in situations where the screen does not update (such as password entry) in some terminal programs, such as the Windows Console with UI automation support enabled and Mintty. +For security purposes, this setting should be left disabled. +However, you may wish to enable it if you experience performance issues or instability with typed character and/or word reporting in consoles, or work in trusted environments and prefer password announcement. -==== Use the new typed character support in Windows Console when available ====[AdvancedSettingsKeyboardSupportInLegacy] -This option enables an alternative method for detecting typed characters in Windows command consoles. +==== Use the new typed character support in legacy Windows consoles when available ====[AdvancedSettingsKeyboardSupportInLegacy] +This option enables an alternative method for detecting typed characters in legacy Windows consoles. While it improves performance and prevents some console output from being spelled out, it may be incompatible with some terminal programs. This feature is available and enabled by default on Windows 10 versions 1607, 1703, 1709 and 1803 as well as on newer Windows 10 releases when UI Automation is unavailable or disabled. Warning: with this option enabled, typed characters that do not appear onscreen, such as passwords, will not be suppressed. From 08b6892eba15a765268fbf48b34f78089b9b0f5f Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Thu, 23 Jan 2020 14:50:16 -0500 Subject: [PATCH 2/8] Role back #10682. This reverts commit b00c3f1528c8e23939656b4316dae593f1f90a05. --- source/config/configSpec.py | 2 +- user_docs/en/changes.t2t | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/source/config/configSpec.py b/source/config/configSpec.py index a74c60fab10..02cc2d6a575 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -209,7 +209,7 @@ [UIA] enabled = boolean(default=true) useInMSWordWhenAvailable = boolean(default=false) - winConsoleImplementation= option("auto", "legacy", "UIA", default="legacy") + winConsoleImplementation= option("auto", "legacy", "UIA", default="auto") [terminals] speakPasswords = boolean(default=false) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 58305d85c50..149fc28be67 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -15,6 +15,9 @@ Although these changes do break compatibility with older NVDA add-ons, the upgra Other highlights in this release include 64 bit support for Java VMs, Screen Curtain and Focus Highlight functionality, support for more braille displays and a new Braille viewer, and many many other bug fixes. == New Features == +- In Command Prompt, PowerShell, and the Windows Subsystem for Linux on Windows 10 October 2018 Update and later: + - Vastly improved performance and stability. (#9771) + - Reporting of typed text that does not appear onscreen (such as passwords) can now be enabled via an option in NVDA's advanced settings panel. (#9649) - The accuracy of the move mouse to navigator object command has been improved in text fields in Java applications. (#10157) - Added support for the following Handy Tech Braille displays (#8955): - Basic Braille Plus 40 From 36751bb249d2595afdbd9768f9de9e46ae869015 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Thu, 23 Jan 2020 15:38:15 -0500 Subject: [PATCH 3/8] Refactor consoleUIATextInfo to trust the improved UIA implementation on Windows 10 2004. --- source/NVDAObjects/UIA/winConsoleUIA.py | 234 +++++++++++++----------- source/winVersion.py | 1 + 2 files changed, 124 insertions(+), 111 deletions(-) diff --git a/source/NVDAObjects/UIA/winConsoleUIA.py b/source/NVDAObjects/UIA/winConsoleUIA.py index a987c4105e3..557640e1f43 100644 --- a/source/NVDAObjects/UIA/winConsoleUIA.py +++ b/source/NVDAObjects/UIA/winConsoleUIA.py @@ -12,13 +12,13 @@ from comtypes import COMError from UIAUtils import isTextRangeOffscreen +from winVersion import isWin10 from . import UIATextInfo from ..behaviors import KeyboardHandlerBasedTypedCharSupport from ..window import Window class consoleUIATextInfo(UIATextInfo): - def __init__(self, obj, position, _rangeObj=None): # We want to limit textInfos to just the visible part of the console. # Therefore we specifically handle POSITION_FIRST, POSITION_LAST and POSITION_ALL. @@ -48,8 +48,58 @@ def __init__(self, obj, position, _rangeObj=None): _rangeObj = first._rangeObj super(consoleUIATextInfo, self).__init__(obj, position, _rangeObj) + def move(self, unit, direction, endPoint=None): + oldInfo = None + if self.basePosition != textInfos.POSITION_CARET: + # Insure we haven't gone beyond the visible text. + # UIA adds thousands of blank lines to the end of the console. + boundingInfo = self.obj.makeTextInfo(textInfos.POSITION_ALL) + oldInfo = self.copy() + res = self._move(unit, direction, endPoint) + # Console textRanges have access to the entire console buffer. + # However, we want to limit ourselves to onscreen text. + # Therefore, if the textInfo was originally visible, + # but we are now above or below the visible range, + # Restore the original textRange and pretend the move didn't work. + if oldInfo: + try: + if ( + ( + self.compareEndPoints(boundingInfo, "startToStart") < 0 + or self.compareEndPoints(boundingInfo, "startToEnd") >= 0 + ) + and not ( + oldInfo.compareEndPoints(boundingInfo, "startToStart") < 0 + or oldInfo.compareEndPoints(boundingInfo, "startToEnd") >= 0 + ) + ): + self._rangeObj = oldInfo._rangeObj + return 0 + except (COMError, RuntimeError): + pass + return res + + def _move(self, unit, direction, endPoint=None): + "Perform a move without respect to bounding." + return super(consoleUIATextInfo, self).move(unit, direction, endPoint) + + def __ne__(self, other): + """Support more accurate caret move detection.""" + return not self == other + + def _get_text(self): + # #10036: return a space if the text range is empty. + # Consoles don't actually store spaces, the character is merely left blank. + res = super(consoleUIATextInfo, self)._get_text() + if not res: + return ' ' + else: + return res + + +class consoleUIATextInfoPre2004(consoleUIATextInfo): def collapse(self, end=False): - """Works around a UIA bug on Windows 10 1803 and later.""" + """Works around a UIA bug on Windows 10 1803 to 2004.""" # When collapsing, consoles seem to incorrectly push the start of the # textRange back one character. # Correct this by bringing the start back up to where the end is. @@ -62,13 +112,63 @@ def collapse(self, end=False): UIAHandler.TextPatternRangeEndpoint_Start ) - def move(self, unit, direction, endPoint=None): - oldInfo = None - if self.basePosition != textInfos.POSITION_CARET: - # Insure we haven't gone beyond the visible text. - # UIA adds thousands of blank lines to the end of the console. - boundingInfo = self.obj.makeTextInfo(textInfos.POSITION_ALL) - oldInfo = self.copy() + + def compareEndPoints(self, other, which): + """Works around a UIA bug on Windows 10 1803 to 2004.""" + # Even when a console textRange's start and end have been moved to the + # same position, the console incorrectly reports the end as being + # past the start. + # Compare to the start (not the end) when collapsed. + selfEndPoint, otherEndPoint = which.split("To") + if selfEndPoint == "end" and self._isCollapsed(): + selfEndPoint = "start" + if otherEndPoint == "End" and other._isCollapsed(): + otherEndPoint = "Start" + which = f"{selfEndPoint}To{otherEndPoint}" + return super().compareEndPoints(other, which=which) + + def setEndPoint(self, other, which): + """Override of L{textInfos.TextInfo.setEndPoint}. + Works around a UIA bug on Windows 10 1803 to 2004 that means we can + not trust the "end" endpoint of a collapsed (empty) text range + for comparisons. + """ + selfEndPoint, otherEndPoint = which.split("To") + # In this case, there is no need to check if self is collapsed + # since the point of this method is to change its text range, modifying the "end" endpoint of a collapsed + # text range is fine. + if otherEndPoint == "End" and other._isCollapsed(): + otherEndPoint = "Start" + which = f"{selfEndPoint}To{otherEndPoint}" + return super().setEndPoint(other, which=which) + + def expand(self, unit): + if unit == textInfos.UNIT_WORD: + # UIA doesn't implement word movement, so we need to do it manually. + lineInfo = self.copy() + lineInfo.expand(textInfos.UNIT_LINE) + offset = self._getCurrentOffsetInThisLine(lineInfo) + start, end = self._getWordOffsetsInThisLine(offset, lineInfo) + wordEndPoints = ( + (offset - start) * -1, + end - offset - 1 + ) + if wordEndPoints[0]: + self._rangeObj.MoveEndpointByUnit( + UIAHandler.TextPatternRangeEndpoint_Start, + UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER], + wordEndPoints[0] + ) + if wordEndPoints[1]: + self._rangeObj.MoveEndpointByUnit( + UIAHandler.TextPatternRangeEndpoint_End, + UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER], + wordEndPoints[1] + ) + else: + return super(consoleUIATextInfo, self).expand(unit) + + def _move(self, unit, direction, endPoint=None): if unit == textInfos.UNIT_WORD and direction != 0: # UIA doesn't implement word movement, so we need to do it manually. # Relative to the current line, calculate our offset @@ -128,98 +228,8 @@ def move(self, unit, direction, endPoint=None): # after moving. # Therefore manually collapse. self.collapse() - # Console textRanges have access to the entire console buffer. - # However, we want to limit ourselves to onscreen text. - # Therefore, if the textInfo was originally visible, - # but we are now above or below the visible range, - # Restore the original textRange and pretend the move didn't work. - if oldInfo: - try: - if ( - ( - self.compareEndPoints(boundingInfo, "startToStart") < 0 - or self.compareEndPoints(boundingInfo, "startToEnd") >= 0 - ) - and not ( - oldInfo.compareEndPoints(boundingInfo, "startToStart") < 0 - or oldInfo.compareEndPoints(boundingInfo, "startToEnd") >= 0 - ) - ): - self._rangeObj = oldInfo._rangeObj - return 0 - except (COMError, RuntimeError): - pass return res - def expand(self, unit): - if unit == textInfos.UNIT_WORD: - # UIA doesn't implement word movement, so we need to do it manually. - lineInfo = self.copy() - lineInfo.expand(textInfos.UNIT_LINE) - offset = self._getCurrentOffsetInThisLine(lineInfo) - start, end = self._getWordOffsetsInThisLine(offset, lineInfo) - wordEndPoints = ( - (offset - start) * -1, - end - offset - 1 - ) - if wordEndPoints[0]: - self._rangeObj.MoveEndpointByUnit( - UIAHandler.TextPatternRangeEndpoint_Start, - UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER], - wordEndPoints[0] - ) - if wordEndPoints[1]: - self._rangeObj.MoveEndpointByUnit( - UIAHandler.TextPatternRangeEndpoint_End, - UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER], - wordEndPoints[1] - ) - else: - return super(consoleUIATextInfo, self).expand(unit) - - def compareEndPoints(self, other, which): - """Works around a UIA bug on Windows 10 1803 and later.""" - # Even when a console textRange's start and end have been moved to the - # same position, the console incorrectly reports the end as being - # past the start. - # Compare to the start (not the end) when collapsed. - selfEndPoint, otherEndPoint = which.split("To") - if selfEndPoint == "end" and self._isCollapsed(): - selfEndPoint = "start" - if otherEndPoint == "End" and other._isCollapsed(): - otherEndPoint = "Start" - which = f"{selfEndPoint}To{otherEndPoint}" - return super().compareEndPoints(other, which=which) - - def setEndPoint(self, other, which): - """Override of L{textInfos.TextInfo.setEndPoint}. - Works around a UIA bug on Windows 10 1803 and later that means we can - not trust the "end" endpoint of a collapsed (empty) text range - for comparisons. - """ - selfEndPoint, otherEndPoint = which.split("To") - # In this case, there is no need to check if self is collapsed - # since the point of this method is to change its text range, modifying the "end" endpoint of a collapsed - # text range is fine. - if otherEndPoint == "End" and other._isCollapsed(): - otherEndPoint = "Start" - which = f"{selfEndPoint}To{otherEndPoint}" - return super().setEndPoint(other, which=which) - - def _isCollapsed(self): - """Works around a UIA bug on Windows 10 1803 and later that means we - cannot trust the "end" endpoint of a collapsed (empty) text range - for comparisons. - Instead we check to see if we can get the first character from the - text range. A collapsed range will not have any characters - and will return an empty string.""" - return not bool(self._rangeObj.getText(1)) - - def _get_isCollapsed(self): - # To decide if the textRange is collapsed, - # Check if it has no text. - return self._isCollapsed() - def _getCurrentOffsetInThisLine(self, lineInfo): """ Given a caret textInfo expanded to line, returns the index into the @@ -258,18 +268,20 @@ def _getWordOffsetsInThisLine(self, offset, lineInfo): min(end.value, max(1, lineTextLen - 2)) ) - def __ne__(self, other): - """Support more accurate caret move detection.""" - return not self == other + def _isCollapsed(self): + """Works around a UIA bug on Windows 10 1803 to 2004 that means we + cannot trust the "end" endpoint of a collapsed (empty) text range + for comparisons. + Instead we check to see if we can get the first character from the + text range. A collapsed range will not have any characters + and will return an empty string.""" + return not bool(self._rangeObj.getText(1)) - def _get_text(self): - # #10036: return a space if the text range is empty. - # Consoles don't actually store spaces, the character is merely left blank. - res = super(consoleUIATextInfo, self)._get_text() - if not res: - return ' ' - else: - return res + + def _get_isCollapsed(self): + # To decide if the textRange is collapsed, + # Check if it has no text. + return self._isCollapsed() class consoleUIAWindow(Window): @@ -302,7 +314,7 @@ def _get_TextInfo(self): on NVDAObjects.UIA.UIA consoleUIATextInfo fixes expand/collapse, implements word movement, and bounds review to the visible text.""" - return consoleUIATextInfo + return consoleUIATextInfo if isWin10(2004) else consoleUIATextInfoPre2004 def _getTextLines(self): # This override of _getTextLines takes advantage of the fact that diff --git a/source/winVersion.py b/source/winVersion.py index 2e551f5ffca..cd7b2df42a1 100644 --- a/source/winVersion.py +++ b/source/winVersion.py @@ -37,6 +37,7 @@ def isUwpOcrAvailable(): 1809: 17763, 1903: 18362, 1909: 18363, + 2004: 19551, # Placeholder, fix once it goes stable } From c2e7f1093e77e415f53c8a626561e351e5bacb32 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Thu, 23 Jan 2020 15:44:47 -0500 Subject: [PATCH 4/8] Fix what's new --- user_docs/en/changes.t2t | 3 --- 1 file changed, 3 deletions(-) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 149fc28be67..58305d85c50 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -15,9 +15,6 @@ Although these changes do break compatibility with older NVDA add-ons, the upgra Other highlights in this release include 64 bit support for Java VMs, Screen Curtain and Focus Highlight functionality, support for more braille displays and a new Braille viewer, and many many other bug fixes. == New Features == -- In Command Prompt, PowerShell, and the Windows Subsystem for Linux on Windows 10 October 2018 Update and later: - - Vastly improved performance and stability. (#9771) - - Reporting of typed text that does not appear onscreen (such as passwords) can now be enabled via an option in NVDA's advanced settings panel. (#9649) - The accuracy of the move mouse to navigator object command has been improved in text fields in Java applications. (#10157) - Added support for the following Handy Tech Braille displays (#8955): - Basic Braille Plus 40 From 8d41600f5a77afbf8cd9de88b4ccbcff2d402ac0 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Thu, 23 Jan 2020 15:45:48 -0500 Subject: [PATCH 5/8] Enable UIA by default on Windows 10 version 2004. --- source/UIAUtils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/UIAUtils.py b/source/UIAUtils.py index a20ea4b49e7..efd0e36cac3 100644 --- a/source/UIAUtils.py +++ b/source/UIAUtils.py @@ -244,4 +244,4 @@ def shouldUseUIAConsole(setting=None): # ignore it. # It does not implement caret/selection, and probably has no # new text events. - return isWin10(1809) + return isWin10(2004) From 8d4a2b34b806460a09e678f5155884c838c7e54e Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Thu, 23 Jan 2020 15:48:52 -0500 Subject: [PATCH 6/8] Update user guide. --- user_docs/en/userGuide.t2t | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index ae6658b3e74..35209f935d9 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -1785,7 +1785,7 @@ It does not affect the modern Windows Terminal. In Windows 10 version 1709, Microsoft [added support for its UI Automation API to the console https://devblogs.microsoft.com/commandline/whats-new-in-windows-console-in-windows-10-fall-creators-update/], bringing vastly improved performance and stability for screen readers that support it. In situations where UI Automation is unavailable or known to result in an inferior user experience, NVDA's legacy console support is available as a fallback. The Windows Console support combo box has three options: -- Automatic: Uses UI Automation in consoles on Windows 10 version 1809 and later. This option is recommended and set by default. +- Automatic: Uses UI Automation in consoles on Windows 10 version 2004 and later. This option is recommended and set by default. - Prefer UIA: Uses UI Automation in consoles if available, even on Windows versions with incomplete or buggy implementations. While this limited functionality may be useful (and even sufficient for your usage), use of this option is entirely at your own risk and no support for it will be provided. - Legacy: UI Automation in the Windows Console will be completely disabled, so the legacy fallback will always be used. - @@ -1798,7 +1798,7 @@ However, you may wish to enable it if you experience performance issues or insta ==== Use the new typed character support in legacy Windows consoles when available ====[AdvancedSettingsKeyboardSupportInLegacy] This option enables an alternative method for detecting typed characters in legacy Windows consoles. While it improves performance and prevents some console output from being spelled out, it may be incompatible with some terminal programs. -This feature is available and enabled by default on Windows 10 versions 1607, 1703, 1709 and 1803 as well as on newer Windows 10 releases when UI Automation is unavailable or disabled. +This feature is available and enabled by default on Windows 10 version 1607 through 1909 as well as on newer Windows 10 releases when UI Automation is unavailable or disabled. Warning: with this option enabled, typed characters that do not appear onscreen, such as passwords, will not be suppressed. In untrusted environments, you may temporarily disable [speak typed characters #KeyboardSettingsSpeakTypedCharacters] and [speak typed words #KeyboardSettingsSpeakTypedWords] when entering passwords. From cea6aca08226b6f03d677150e3a45e0fc98756cc Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Thu, 23 Jan 2020 23:43:12 -0500 Subject: [PATCH 7/8] Style. --- source/NVDAObjects/UIA/winConsoleUIA.py | 4 +--- source/UIAUtils.py | 2 +- source/gui/settingsDialogs.py | 25 ++++++++++++++----------- source/winVersion.py | 4 ++-- user_docs/en/userGuide.t2t | 2 +- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/source/NVDAObjects/UIA/winConsoleUIA.py b/source/NVDAObjects/UIA/winConsoleUIA.py index 557640e1f43..3be86a6795d 100644 --- a/source/NVDAObjects/UIA/winConsoleUIA.py +++ b/source/NVDAObjects/UIA/winConsoleUIA.py @@ -2,7 +2,7 @@ # A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. # See the file COPYING for more details. -# Copyright (C) 2019 Bill Dengler +# Copyright (C) 2019-2020 Bill Dengler import ctypes import NVDAHelper @@ -112,7 +112,6 @@ def collapse(self, end=False): UIAHandler.TextPatternRangeEndpoint_Start ) - def compareEndPoints(self, other, which): """Works around a UIA bug on Windows 10 1803 to 2004.""" # Even when a console textRange's start and end have been moved to the @@ -277,7 +276,6 @@ def _isCollapsed(self): and will return an empty string.""" return not bool(self._rangeObj.getText(1)) - def _get_isCollapsed(self): # To decide if the textRange is collapsed, # Check if it has no text. diff --git a/source/UIAUtils.py b/source/UIAUtils.py index efd0e36cac3..84f914546ec 100644 --- a/source/UIAUtils.py +++ b/source/UIAUtils.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2015-2019 NV Access Limited, Bill Dengler +# Copyright (C) 2015-2020 NV Access Limited, Bill Dengler # This file is covered by the GNU General Public License. # See the file COPYING for more details. diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 7ef81bc593a..d364022c96c 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -1,7 +1,7 @@ # -*- coding: UTF-8 -*- # settingsDialogs.py # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2019 NV Access Limited, Peter Vágner, Aleksey Sadovoy, +# Copyright (C) 2006-2020 NV Access Limited, Peter Vágner, Aleksey Sadovoy, # Rui Batista, Joseph Lee, Heiko Folkerts, Zahari Yurukov, Leonard de Ruijter, # Derek Riemer, Babbage B.V., Davy Kager, Ethan Holliger, Bill Dengler # This file is covered by the GNU General Public License. @@ -2423,16 +2423,19 @@ def _getDefaultValue(self, configPath): def haveConfigDefaultsBeenRestored(self): return ( - self._defaultsRestored and - self.scratchpadCheckBox.IsChecked() == self.scratchpadCheckBox.defaultValue and - self.UIAInMSWordCheckBox.IsChecked() == self.UIAInMSWordCheckBox.defaultValue and - self.consoleCombo.GetSelection() == self.consoleCombo.defaultValue and - self.speakPasswordsCheckBox.IsChecked() == self.speakPasswordsCheckBox.defaultValue and - self.keyboardSupportInLegacyCheckBox.IsChecked() == self.keyboardSupportInLegacyCheckBox.defaultValue and - self.autoFocusFocusableElementsCheckBox.IsChecked() == self.autoFocusFocusableElementsCheckBox.defaultValue and - self.caretMoveTimeoutSpinControl.GetValue() == self.caretMoveTimeoutSpinControl.defaultValue and - set(self.logCategoriesList.CheckedItems) == set(self.logCategoriesList.defaultCheckedItems) and - True # reduce noise in diff when the list is extended. + self._defaultsRestored + and self.scratchpadCheckBox.IsChecked() == self.scratchpadCheckBox.defaultValue + and self.UIAInMSWordCheckBox.IsChecked() == self.UIAInMSWordCheckBox.defaultValue + and self.consoleCombo.GetSelection() == self.consoleCombo.defaultValue + and self.speakPasswordsCheckBox.IsChecked() == self.speakPasswordsCheckBox.defaultValue + and self.keyboardSupportInLegacyCheckBox.IsChecked() == self.keyboardSupportInLegacyCheckBox.defaultValue + and ( + self.autoFocusFocusableElementsCheckBox.IsChecked() + == self.autoFocusFocusableElementsCheckBox.defaultValue + ) + and self.caretMoveTimeoutSpinControl.GetValue() == self.caretMoveTimeoutSpinControl.defaultValue + and set(self.logCategoriesList.CheckedItems) == set(self.logCategoriesList.defaultCheckedItems) + and True # reduce noise in diff when the list is extended. ) def restoreToDefaults(self): diff --git a/source/winVersion.py b/source/winVersion.py index cd7b2df42a1..82d26d517fd 100644 --- a/source/winVersion.py +++ b/source/winVersion.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2019 NV Access Limited +# Copyright (C) 2006-2020 NV Access Limited, Bill Dengler # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -37,7 +37,7 @@ def isUwpOcrAvailable(): 1809: 17763, 1903: 18362, 1909: 18363, - 2004: 19551, # Placeholder, fix once it goes stable + 2004: 19551, # Placeholder, fix once it goes stable } diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 35209f935d9..0f7016b1188 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -1798,7 +1798,7 @@ However, you may wish to enable it if you experience performance issues or insta ==== Use the new typed character support in legacy Windows consoles when available ====[AdvancedSettingsKeyboardSupportInLegacy] This option enables an alternative method for detecting typed characters in legacy Windows consoles. While it improves performance and prevents some console output from being spelled out, it may be incompatible with some terminal programs. -This feature is available and enabled by default on Windows 10 version 1607 through 1909 as well as on newer Windows 10 releases when UI Automation is unavailable or disabled. +This feature is available and enabled by default on Windows 10 versions 1607 through 1909 as well as on newer Windows 10 releases when UI Automation is unavailable or disabled. Warning: with this option enabled, typed characters that do not appear onscreen, such as passwords, will not be suppressed. In untrusted environments, you may temporarily disable [speak typed characters #KeyboardSettingsSpeakTypedCharacters] and [speak typed words #KeyboardSettingsSpeakTypedWords] when entering passwords. From eaedc21eb8b14bf5741232b56fb5792692f617c2 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Fri, 24 Jan 2020 00:16:03 -0500 Subject: [PATCH 8/8] Style --- source/winVersion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/winVersion.py b/source/winVersion.py index 82d26d517fd..a44b127e19e 100644 --- a/source/winVersion.py +++ b/source/winVersion.py @@ -37,7 +37,7 @@ def isUwpOcrAvailable(): 1809: 17763, 1903: 18362, 1909: 18363, - 2004: 19551, # Placeholder, fix once it goes stable + 2004: 19551, # Placeholder, fix once it goes stable }