From 845a589baa38b0958974a0bec00b93f4c64ce4a2 Mon Sep 17 00:00:00 2001 From: Joseph Lee Date: Fri, 26 Jul 2019 06:48:22 -0700 Subject: [PATCH 01/12] NVDAObjects/UIA: recognize UWP tooltips (PR #8124) Fixes #8118 Tooltips from universal apps are powered by XAML and has a specific class name (for now). This allows us to recognize them as proper tool tips. --- source/NVDAObjects/UIA/__init__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index 695b1a811fe..745cddea033 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -28,7 +28,7 @@ from UIAUtils import * from NVDAObjects.window import Window from NVDAObjects import NVDAObjectTextInfo, InvalidNVDAObject -from NVDAObjects.behaviors import ProgressBar, EditableTextWithoutAutoSelectDetection, Dialog, Notification, EditableTextWithSuggestions +from NVDAObjects.behaviors import ProgressBar, EditableTextWithoutAutoSelectDetection, Dialog, Notification, EditableTextWithSuggestions, ToolTip import braille import locationHelper import ui @@ -793,6 +793,10 @@ def findOverlayClasses(self,clsList): clsList.append(Toast_win8) elif self.windowClassName=="Windows.UI.Core.CoreWindow" and UIAControlType==UIAHandler.UIA_WindowControlTypeId and "ToastView" in self.UIAElement.cachedAutomationId: # Windows 10 clsList.append(Toast_win10) + # #8118: treat UIA tool tips (including those found in UWP apps) as proper tool tips, especially those found in Microsoft Edge and other apps. + # Windows 8.x toast, although a form of tool tip, is covered separately. + elif UIAControlType==UIAHandler.UIA_ToolTipControlTypeId: + clsList.append(ToolTip) elif self.UIAElement.cachedFrameworkID in ("InternetExplorer","MicrosoftEdge"): from . import edge if UIAClassName in ("Internet Explorer_Server","WebView") and self.role==controlTypes.ROLE_PANE: @@ -1647,6 +1651,12 @@ def event_UIA_window_windowOpen(self): self.__class__._lastToastRuntimeID = toastRuntimeID Notification.event_alert(self) + +class ToolTip(ToolTip, UIA): + + event_UIA_toolTipOpened=ToolTip.event_show + + #WpfTextView fires name state changes once a second, plus when IUIAutomationTextRange::GetAttributeValue is called. #This causes major lags when using this control with Braille in NVDA. (#2759) #For now just ignore the events. From af9b85de15eb5e60be7c2875134775dbf75945d1 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Fri, 26 Jul 2019 15:49:11 +0200 Subject: [PATCH 02/12] Update changes file for PR #8124 --- user_docs/en/changes.t2t | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 6cac926706a..2b1f23a2153 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -3,10 +3,11 @@ What's New in NVDA %!includeconf: ../changes.t2tconf -= threshold release = += 2019.3 = == Bug Fixes == - Emoji and other 32 bit unicode characters now take less space on a braille display when they are shown as hexadecimal values. (#6695) +- In Windows 10, NVDA will announce tooltips from universal apps if NVDA is configured to report tooltips in object presentation dialog. (#8118) == Changes for Developers == From ff0fd1cc8c50ac5cc2f54ac3b143086f5c5a002c Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Mon, 29 Jul 2019 09:34:45 +0200 Subject: [PATCH 03/12] Fix division error in papenmeier_serial (PR #9983) --- source/brailleDisplayDrivers/papenmeier_serial.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/brailleDisplayDrivers/papenmeier_serial.py b/source/brailleDisplayDrivers/papenmeier_serial.py index a708f6b00b6..c542a66d3c5 100644 --- a/source/brailleDisplayDrivers/papenmeier_serial.py +++ b/source/brailleDisplayDrivers/papenmeier_serial.py @@ -182,8 +182,8 @@ def _handleKeyPresses(self): #called by the keycheck timer if(self._dev!=None): data = brl_poll(self._dev) if len(data) == 10 and data[1] == ord(b'K'): - pos = data[2] * 256 + data[3] - pos = (pos-768)/3 + pos = (data[2] << 8) + data[3] + pos = (pos-768) // 3 pressed = data[6] keys = data[8] self._repeatcount = 0 From be63d19d3ab9d86f9fd2a985d9f75bcf3184f45b Mon Sep 17 00:00:00 2001 From: Marco Zehe Date: Mon, 29 Jul 2019 14:54:56 +0200 Subject: [PATCH 04/12] Properly check if the object is an instance of IAccessible2. (PR #9984) Instead of using hasattr, use instanceof to check that the IAccessible object is an IA2 object before checking for the IA2 specific states. This prevents: - "_ctypes.COMError: -2147467262": E_NOINTERFACE 0x80004002 - No such interface supported - "_ctypes.COMError: -2147467259": E_FAIL 0x80004005 - Unspecified error Fixes #9980 Fixes #9988 --- source/NVDAObjects/IAccessible/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index d238d1e40cd..742889ffd9a 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -863,7 +863,7 @@ def _get_states(self): log.debugWarning("could not get IAccessible states",exc_info=True) else: states.update(IAccessibleHandler.IAccessibleStatesToNVDAStates[x] for x in (y for y in (1< Date: Wed, 31 Jul 2019 06:03:35 -0400 Subject: [PATCH 05/12] Disable caret events in the UIA console (PR #9986) A regression caused during Python 3 migration. NVDA was announcing the final character of the prompt after backspacing text quickly. This can result in NVDA announcing "greater" after backspacing a line of text. Disable caret events in the UIA console and legacy consoles. This PR disables the caret movement event detection from #9933 --- source/NVDAObjects/UIA/winConsoleUIA.py | 5 +++++ source/NVDAObjects/window/winConsole.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/source/NVDAObjects/UIA/winConsoleUIA.py b/source/NVDAObjects/UIA/winConsoleUIA.py index 41b379717bd..7499a832248 100644 --- a/source/NVDAObjects/UIA/winConsoleUIA.py +++ b/source/NVDAObjects/UIA/winConsoleUIA.py @@ -287,6 +287,11 @@ def script_flush_queuedChars(self, gesture): self._queuedChars = [] speech.clearTypedWordBuffer() + def _get_caretMovementDetectionUsesEvents(self): + """Using caret events in consoles sometimes causes the last character of the + prompt to be read when quickly deleting text.""" + return False + def _getTextLines(self): # Filter out extraneous empty lines from UIA ptr = self.UIATextPattern.GetVisibleRanges() diff --git a/source/NVDAObjects/window/winConsole.py b/source/NVDAObjects/window/winConsole.py index 0780c6053e8..d7c074223ff 100644 --- a/source/NVDAObjects/window/winConsole.py +++ b/source/NVDAObjects/window/winConsole.py @@ -73,6 +73,11 @@ def reconnect(): self.startMonitoring() core.callLater(200,reconnect) + def _get_caretMovementDetectionUsesEvents(self): + """Using caret events in consoles sometimes causes the last character of the + prompt to be read when quickly deleting text.""" + return False + @script(gestures=[ "kb:enter", "kb:numpadEnter", From 4e634980ca810eacf7ede4f83568efa73dd0181c Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Wed, 31 Jul 2019 06:28:58 -0400 Subject: [PATCH 06/12] Ignore _expandCollapseBeforeReview in script_reviewStartOfLine. (PR #9944) This fixes an issue in UIA consoles, invoking the "review start of line" script (`shift+numpad 1` by default) does not move the review cursor. Expanding and collapsing the textInfo is essential for the functionality of this script (to move the textInfo to the start of the line), we should ignore the _expandCollapseBeforeReview flag. The _expandCollapseBeforeReview flag was added in #9614. It is True everywhere except in UIA consoles. --- source/globalCommands.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/source/globalCommands.py b/source/globalCommands.py index cc6b4e53930..8312dd3003d 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -1096,9 +1096,8 @@ def script_review_nextWord(self,gesture): def script_review_startOfLine(self,gesture): info=api.getReviewPosition().copy() - if info._expandCollapseBeforeReview: - info.expand(textInfos.UNIT_LINE) - info.collapse() + info.expand(textInfos.UNIT_LINE) + info.collapse() api.setReviewPosition(info) info.expand(textInfos.UNIT_CHARACTER) ui.reviewMessage(_("Left")) From cffbaa7f92be8851e20cdeb664e14874adcb3b87 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Thu, 1 Aug 2019 10:54:37 -0400 Subject: [PATCH 07/12] Improve keyboard support for some terminal programs (PR #9915) Previously: NVDA failed to announce typed characters and/or words in Mintty, and spells output close to the caret in legacy Windows consoles. This commit factors out much of the code for handling typed characters in UIA consoles into a new `NVDAObjects.behaviors.TerminalWithoutTypedCharDetection class`. The class is now used in Mintty (PuTTY, Git Bash) and legacy Windows consoles on Windows 10 version 1607 and later. In legacy Windows consoles, the old keyboard handling code is disabled when the class is in use, and the new support can be disabled in the advanced preferences in case it is incompatible with some programs or if suppression of passwords is critical. Since legacy Windows consoles fire textChange rather slowly, this commit prefers faster responsiveness at the cost of offscreen characters (such as passwords) always being reported. Users may disable "speak typed characters" and/or "speak typed words" (using the existing scripts) when entering passwords to suppress this output. On Windows 10 version 1607 with the new keyboard support enabled, spurious characters are reported when the dead key (if available) is pressed. Fixes #513 Fixes #1348 Related to #9614 --- source/NVDAObjects/IAccessible/__init__.py | 7 +- source/NVDAObjects/IAccessible/winConsole.py | 16 ++- source/NVDAObjects/UIA/winConsoleUIA.py | 75 +------------- source/NVDAObjects/behaviors.py | 101 +++++++++++++++++++ source/NVDAObjects/window/winConsole.py | 10 +- source/appModules/putty.py | 10 +- source/config/configSpec.py | 3 + source/gui/settingsDialogs.py | 19 ++++ source/keyboardHandler.py | 12 +-- source/winConsoleHandler.py | 10 +- user_docs/en/userGuide.t2t | 7 ++ 11 files changed, 181 insertions(+), 89 deletions(-) diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index 742889ffd9a..d7ed8416876 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -542,6 +542,12 @@ def findOverlayClasses(self,clsList): elif windowClassName.startswith("Chrome_"): from . import chromium chromium.findExtraOverlayClasses(self, clsList) + if ( + windowClassName == "ConsoleWindowClass" + and role == oleacc.ROLE_SYSTEM_CLIENT + ): + from . import winConsole + winConsole.findExtraOverlayClasses(self,clsList) #Support for Windowless richEdit @@ -2023,5 +2029,4 @@ def event_alert(self): ("NUIDialog",oleacc.ROLE_SYSTEM_CLIENT):"NUIDialogClient", ("_WwB",oleacc.ROLE_SYSTEM_CLIENT):"winword.ProtectedDocumentPane", ("MsoCommandBar",oleacc.ROLE_SYSTEM_LISTITEM):"msOffice.CommandBarListItem", - ("ConsoleWindowClass",oleacc.ROLE_SYSTEM_CLIENT):"winConsole.WinConsole", } diff --git a/source/NVDAObjects/IAccessible/winConsole.py b/source/NVDAObjects/IAccessible/winConsole.py index 59b33186edd..9347169bc5d 100644 --- a/source/NVDAObjects/IAccessible/winConsole.py +++ b/source/NVDAObjects/IAccessible/winConsole.py @@ -4,9 +4,19 @@ #See the file COPYING for more details. #Copyright (C) 2007-2019 NV Access Limited, Bill Dengler +import config + +from winVersion import isWin10 + from . import IAccessible -from ..window.winConsole import WinConsole +from ..window import winConsole -class WinConsole(WinConsole, IAccessible): +class WinConsole(winConsole.WinConsole, IAccessible): "The legacy console implementation for situations where UIA isn't supported." - pass \ No newline at end of file + pass + +def findExtraOverlayClasses(obj, clsList): + if isWin10(1607) and config.conf['terminals']['keyboardSupportInLegacy']: + from NVDAObjects.behaviors import KeyboardHandlerBasedTypedCharSupport + clsList.append(KeyboardHandlerBasedTypedCharSupport) + clsList.append(WinConsole) diff --git a/source/NVDAObjects/UIA/winConsoleUIA.py b/source/NVDAObjects/UIA/winConsoleUIA.py index 7499a832248..b0b0896c82e 100644 --- a/source/NVDAObjects/UIA/winConsoleUIA.py +++ b/source/NVDAObjects/UIA/winConsoleUIA.py @@ -4,18 +4,14 @@ # See the file COPYING for more details. # Copyright (C) 2019 Bill Dengler -import config import ctypes import NVDAHelper -import speech -import time import textInfos import UIAHandler -from scriptHandler import script from winVersion import isWin10 from . import UIATextInfo -from ..behaviors import Terminal +from ..behaviors import KeyboardHandlerBasedTypedCharSupport from ..window import Window @@ -223,70 +219,16 @@ def _get_focusRedirect(self): return None -class WinConsoleUIA(Terminal): +class WinConsoleUIA(KeyboardHandlerBasedTypedCharSupport): #: Disable the name as it won't be localized name = "" #: Only process text changes every 30 ms, in case the console is getting #: a lot of text. STABILIZE_DELAY = 0.03 _TextInfo = consoleUIATextInfo - #: A queue of typed characters, to be dispatched on C{textChange}. - #: This queue allows NVDA to suppress typed passwords when needed. - _queuedChars = [] - #: Whether the console got new text lines in its last update. - #: Used to determine if typed character/word buffers should be flushed. - _hasNewLines = False #: the caret in consoles can take a while to move on Windows 10 1903 and later. _caretMovementTimeoutMultiplier = 1.5 - def _reportNewText(self, line): - # Additional typed character filtering beyond that in LiveText - if len(line.strip()) < max(len(speech.curWordChars) + 1, 3): - return - if self._hasNewLines: - # Clear the queued characters buffer for new text lines. - self._queuedChars = [] - super(WinConsoleUIA, self)._reportNewText(line) - - def event_typedCharacter(self, ch): - if ch == '\t': - # Clear the typed word buffer for tab completion. - speech.clearTypedWordBuffer() - if ( - ( - config.conf['keyboard']['speakTypedCharacters'] - or config.conf['keyboard']['speakTypedWords'] - ) - and not config.conf['UIA']['winConsoleSpeakPasswords'] - ): - self._queuedChars.append(ch) - else: - super(WinConsoleUIA, self).event_typedCharacter(ch) - - def event_textChange(self): - while self._queuedChars: - ch = self._queuedChars.pop(0) - super(WinConsoleUIA, self).event_typedCharacter(ch) - super(WinConsoleUIA, self).event_textChange() - - @script(gestures=[ - "kb:enter", - "kb:numpadEnter", - "kb:tab", - "kb:control+c", - "kb:control+d", - "kb:control+pause" - ]) - def script_flush_queuedChars(self, gesture): - """ - Flushes the typed word buffer and queue of typedCharacter events if present. - Since these gestures clear the current word/line, we should flush the - queue to avoid erroneously reporting these chars. - """ - gesture.send() - self._queuedChars = [] - speech.clearTypedWordBuffer() - def _get_caretMovementDetectionUsesEvents(self): """Using caret events in consoles sometimes causes the last character of the prompt to be read when quickly deleting text.""" @@ -298,19 +240,6 @@ def _getTextLines(self): res = [ptr.GetElement(i).GetText(-1) for i in range(ptr.length)] return res - def _calculateNewText(self, newLines, oldLines): - self._hasNewLines = ( - self._findNonBlankIndices(newLines) - != self._findNonBlankIndices(oldLines) - ) - return super(WinConsoleUIA, self)._calculateNewText(newLines, oldLines) - - def _findNonBlankIndices(self, lines): - """ - Given a list of strings, returns a list of indices where the strings - are not empty. - """ - return [index for index, line in enumerate(lines) if line] def findExtraOverlayClasses(obj, clsList): if obj.UIAElement.cachedAutomationId == "Text Area": diff --git a/source/NVDAObjects/behaviors.py b/source/NVDAObjects/behaviors.py index 312ef21b289..60a751439a3 100755 --- a/source/NVDAObjects/behaviors.py +++ b/source/NVDAObjects/behaviors.py @@ -24,6 +24,7 @@ import textInfos import editableText from logHandler import log +from scriptHandler import script import api import ui import braille @@ -366,6 +367,106 @@ def event_loseFocus(self): super(Terminal, self).event_loseFocus() self.stopMonitoring() + +class KeyboardHandlerBasedTypedCharSupport(Terminal): + """A Terminal object that also provides typed character support for + console applications via keyboardHandler events. + These events are queued from NVDA's global keyboard hook. + Therefore, an event is fired for every single character that is being typed, + even when a character is not written to the console (e.g. in read only console applications). + This approach is an alternative to monitoring the console output for + characters close to the caret, or injecting in-process with NVDAHelper. + This class relies on the toUnicodeEx Windows function, and in particular + the flag to preserve keyboard state available in Windows 10 1607 + and later.""" + #: Whether this object quickly and reliably sends textChange events + #: when its contents update. + #: Timely and reliable textChange events are required + #: to support password suppression. + _supportsTextChange = True + #: A queue of typed characters, to be dispatched on C{textChange}. + #: This queue allows NVDA to suppress typed passwords when needed. + _queuedChars = [] + #: Whether the last typed character is a tab. + #: If so, we should temporarily disable filtering as completions may + #: be short. + _hasTab = False + + def _reportNewText(self, line): + # Perform typed character filtering, as typed characters are handled with events. + if ( + not self._hasTab + and len(line.strip()) < max(len(speech.curWordChars) + 1, 3) + ): + return + super()._reportNewText(line) + + def event_typedCharacter(self, ch): + if ch == '\t': + self._hasTab = True + # Clear the typed word buffer for tab completion. + speech.clearTypedWordBuffer() + else: + self._hasTab = False + if ( + ( + config.conf['keyboard']['speakTypedCharacters'] + or config.conf['keyboard']['speakTypedWords'] + ) + and not config.conf['UIA']['winConsoleSpeakPasswords'] + and self._supportsTextChange + ): + self._queuedChars.append(ch) + else: + super().event_typedCharacter(ch) + + def event_textChange(self): + self._dispatchQueue() + super().event_textChange() + + @script(gestures=[ + "kb:enter", + "kb:numpadEnter", + "kb:tab", + "kb:control+c", + "kb:control+d", + "kb:control+pause" + ]) + def script_flush_queuedChars(self, gesture): + """ + Flushes the typed word buffer and queue of typedCharacter events if present. + Since these gestures clear the current word/line, we should flush the + queue to avoid erroneously reporting these chars. + """ + self._queuedChars = [] + speech.clearTypedWordBuffer() + gesture.send() + + def _calculateNewText(self, newLines, oldLines): + hasNewLines = ( + self._findNonBlankIndices(newLines) + != self._findNonBlankIndices(oldLines) + ) + if hasNewLines: + # Clear the typed word buffer for new text lines. + speech.clearTypedWordBuffer() + self._queuedChars = [] + return super()._calculateNewText(newLines, oldLines) + + def _dispatchQueue(self): + """Sends queued typedCharacter events through to NVDA.""" + while self._queuedChars: + ch = self._queuedChars.pop(0) + super().event_typedCharacter(ch) + + def _findNonBlankIndices(self, lines): + """ + Given a list of strings, returns a list of indices where the strings + are not empty. + """ + return [index for index, line in enumerate(lines) if line] + + class CandidateItem(NVDAObject): def getFormattedCandidateName(self,number,candidate): diff --git a/source/NVDAObjects/window/winConsole.py b/source/NVDAObjects/window/winConsole.py index d7c074223ff..cb942e16161 100644 --- a/source/NVDAObjects/window/winConsole.py +++ b/source/NVDAObjects/window/winConsole.py @@ -2,11 +2,11 @@ #A part of NonVisual Desktop Access (NVDA) #This file is covered by the GNU General Public License. #See the file COPYING for more details. -#Copyright (C) 2007-2012 NV Access Limited +#Copyright (C) 2007-2019 NV Access Limited, Bill Dengler import winConsoleHandler from . import Window -from ..behaviors import Terminal, EditableTextWithoutAutoSelectDetection +from ..behaviors import Terminal, EditableTextWithoutAutoSelectDetection, KeyboardHandlerBasedTypedCharSupport import api import core from scriptHandler import script @@ -20,6 +20,12 @@ class WinConsole(Terminal, EditableTextWithoutAutoSelectDetection, Window): """ STABILIZE_DELAY = 0.03 + def initOverlayClass(self): + # Legacy consoles take quite a while to send textChange events. + # This significantly impacts typing performance, so don't queue chars. + if isinstance(self, KeyboardHandlerBasedTypedCharSupport): + self._supportsTextChange = False + def _get_TextInfo(self): consoleObject=winConsoleHandler.consoleObject if consoleObject and self.windowHandle == consoleObject.windowHandle: diff --git a/source/appModules/putty.py b/source/appModules/putty.py index b84ccee57e5..cd116342d28 100644 --- a/source/appModules/putty.py +++ b/source/appModules/putty.py @@ -2,16 +2,17 @@ #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-2014 NV Access Limited +#Copyright (C) 2010-2019 NV Access Limited, Bill Dengler """App module for PuTTY """ import oleacc -from NVDAObjects.behaviors import Terminal +from NVDAObjects.behaviors import KeyboardHandlerBasedTypedCharSupport, Terminal from NVDAObjects.window import DisplayModelEditableText, DisplayModelLiveText import appModuleHandler from NVDAObjects.IAccessible import IAccessible +from winVersion import isWin10 class AppModule(appModuleHandler.AppModule): # Allow this to be overridden for derived applications. @@ -23,4 +24,7 @@ def chooseNVDAObjectOverlayClasses(self, obj, clsList): clsList.remove(DisplayModelEditableText) except ValueError: pass - clsList[0:0] = (Terminal, DisplayModelLiveText) + if isWin10(1607): + clsList[0:0] = (KeyboardHandlerBasedTypedCharSupport, DisplayModelLiveText) + else: + clsList[0:0] = (Terminal, DisplayModelLiveText) diff --git a/source/config/configSpec.py b/source/config/configSpec.py index ab9c894269b..b33cebc9409 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -190,6 +190,9 @@ winConsoleImplementation= option("auto", "legacy", "UIA", default="auto") winConsoleSpeakPasswords = boolean(default=false) +[terminals] + keyboardSupportInLegacy = boolean(default=True) + [update] autoCheck = boolean(default=true) startupNotification = boolean(default=true) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index f9e6c2504f1..a574b0f7694 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2068,6 +2068,22 @@ def __init__(self, parent): self.winConsoleSpeakPasswordsCheckBox.SetValue(config.conf["UIA"]["winConsoleSpeakPasswords"]) self.winConsoleSpeakPasswordsCheckBox.defaultValue = self._getDefaultValue(["UIA", "winConsoleSpeakPasswords"]) + # Translators: This is the label for a group of advanced options in the + # Advanced settings panel + label = _("Terminal programs") + terminalsGroup = guiHelper.BoxSizerHelper( + parent=self, + sizer=wx.StaticBoxSizer(parent=self, label=label, orient=wx.VERTICAL) + ) + 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 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"]) + self.keyboardSupportInLegacyCheckBox.Enable(winVersion.isWin10(1607)) + # Translators: This is the label for a group of advanced options in the # Advanced settings panel label = _("Browse mode") @@ -2154,6 +2170,7 @@ def haveConfigDefaultsBeenRestored(self): 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 @@ -2165,6 +2182,7 @@ def restoreToDefaults(self): self.UIAInMSWordCheckBox.SetValue(self.UIAInMSWordCheckBox.defaultValue) self.ConsoleUIACheckBox.SetValue(self.ConsoleUIACheckBox.defaultValue=='UIA') self.winConsoleSpeakPasswordsCheckBox.SetValue(self.winConsoleSpeakPasswordsCheckBox.defaultValue) + self.keyboardSupportInLegacyCheckBox.SetValue(self.keyboardSupportInLegacyCheckBox.defaultValue) self.autoFocusFocusableElementsCheckBox.SetValue(self.autoFocusFocusableElementsCheckBox.defaultValue) self.caretMoveTimeoutSpinControl.SetValue(self.caretMoveTimeoutSpinControl.defaultValue) self.logCategoriesList.CheckedItems = self.logCategoriesList.defaultCheckedItems @@ -2179,6 +2197,7 @@ def onSave(self): else: config.conf['UIA']['winConsoleImplementation'] = "auto" config.conf["UIA"]["winConsoleSpeakPasswords"]=self.winConsoleSpeakPasswordsCheckBox.IsChecked() + config.conf["terminals"]["keyboardSupportInLegacy"]=self.keyboardSupportInLegacyCheckBox.IsChecked() config.conf["virtualBuffers"]["autoFocusFocusableElements"] = self.autoFocusFocusableElementsCheckBox.IsChecked() config.conf["editableText"]["caretMoveTimeoutMs"]=self.caretMoveTimeoutSpinControl.GetValue() for index,key in enumerate(self.logCategories): diff --git a/source/keyboardHandler.py b/source/keyboardHandler.py index 7fa2214cc31..05190ad3aa9 100644 --- a/source/keyboardHandler.py +++ b/source/keyboardHandler.py @@ -197,22 +197,22 @@ def internal_keyDownEvent(vkCode,scanCode,extended,injected): # #6017: handle typed characters in Win10 RS2 and above where we can't detect typed characters in-process # This code must be in the 'finally' block as code above returns in several places yet we still want to execute this particular code. focus=api.getFocusObject() - from NVDAObjects.UIA.winConsoleUIA import WinConsoleUIA + from NVDAObjects.behaviors import KeyboardHandlerBasedTypedCharSupport if ( - # This is only possible in Windows 10 RS2 and above - winVersion.isWin10(1703) + # This is only possible in Windows 10 1607 and above + winVersion.isWin10(1607) # And we only want to do this if the gesture did not result in an executed action and not gestureExecuted # and not if this gesture is a modifier key and not isNVDAModifierKey(vkCode,extended) and not vkCode in KeyboardInputGesture.NORMAL_MODIFIER_KEYS and ( # Either of - # We couldn't inject in-process, and its not a legacy console window. + # We couldn't inject in-process, and its not a legacy console window without keyboard support. # console windows have their own specific typed character support. (not focus.appModule.helperLocalBindingHandle and focus.windowClassName!='ConsoleWindowClass') # or the focus is within a UWP app, where WM_CHAR never gets sent or focus.windowClassName.startswith('Windows.UI.Core') - #Or this is a UIA console window, where WM_CHAR messages are doubled - or isinstance(focus, WinConsoleUIA) + #Or this is a console with keyboard support, where WM_CHAR messages are doubled + or isinstance(focus, KeyboardHandlerBasedTypedCharSupport) ) ): keyStates=(ctypes.c_byte*256)() diff --git a/source/winConsoleHandler.py b/source/winConsoleHandler.py index 5ae4ee2d47c..9ad690e3398 100755 --- a/source/winConsoleHandler.py +++ b/source/winConsoleHandler.py @@ -134,6 +134,7 @@ def getConsoleVisibleLines(): @winUser.WINEVENTPROC def consoleWinEventHook(handle,eventID,window,objectID,childID,threadID,timestamp): + from NVDAObjects.behaviors import KeyboardHandlerBasedTypedCharSupport #We don't want to do anything with the event if the event is not for the window this console is in if window!=consoleObject.windowHandle: return @@ -146,7 +147,14 @@ def consoleWinEventHook(handle,eventID,window,objectID,childID,threadID,timestam x=winUser.GET_X_LPARAM(objectID) y=winUser.GET_Y_LPARAM(objectID) consoleScreenBufferInfo=wincon.GetConsoleScreenBufferInfo(consoleOutputHandle) - if x Date: Thu, 1 Aug 2019 16:54:59 +0200 Subject: [PATCH 08/12] Update changes file for PR #9915 --- user_docs/en/changes.t2t | 2 ++ 1 file changed, 2 insertions(+) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 2b1f23a2153..692a790fa80 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -8,6 +8,8 @@ What's New in NVDA == Bug Fixes == - Emoji and other 32 bit unicode characters now take less space on a braille display when they are shown as hexadecimal values. (#6695) - In Windows 10, NVDA will announce tooltips from universal apps if NVDA is configured to report tooltips in object presentation dialog. (#8118) +- On Windows 10 version 1607 and later, typed text is now reported in Mintty. (#1348) +- On Windows 10 version 1607 and later, output in the Windows Console that appears close to the caret is no longer spelled out. (#513) == Changes for Developers == From e68ce2d0733bb9c75229452a1828d9a0e530c3f0 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 1 Aug 2019 17:04:52 +0200 Subject: [PATCH 09/12] Integrate Flake8 linting with SCons (PR #9958) Code contributors regularly have to deal with ill-defined and inconsistently enforced code style requirements. Code reviewers spend much of their time reporting minor issues, time that would be better spent looking for architectural problems / product issues / logic errors. In this commit we introduce automated checking of python style. The diff from new PR's will be tested for compliance with Flake8. The NVDA Python code already contains several inconsistent styles, so rather than try to match it I have tried to configure Flake8 to use the default style guidelines as much as possible. Added two new SCons build targets: - `lint` - creates a unified diff with `git diff -U0 $(git merge-base )` - A helper script is used to generate this diff (`tests\lint\genDiff.py`) - The diff is piped to `flake8` to perform the linting. - The output is printed to stdout and also to `tests/lint/current.lint` - `lintInstall` - required by `lint`. - Uses pip to install dependencies from a `requirements.txt` file. AppVeyor changes: - Adds a new script for tests phase of build - Mostly does what SCons does, does not need to worry about getting working tree / uncommit changes into the diff. - In order to preserve the availability of artifacts, these are uploaded from a `on_finish` phase rather than `artifacts` phase. - This acts like a "finally" block, and happens regardless of whether the build passes or fails. - The installer artifact is often used to test if a change fixes an issue before the PR is polished off / reviewed. It also can help reviewers to test a change locally without having to build the branch. - A message is sent when there are linting errors. - A failed lint will still halt the build, system tests are not run. Closes #5918 --- appveyor.yml | 38 +++++++++++--- readme.md | 32 ++++++++++-- sconstruct | 11 ++++ source/pylintrc | 67 ------------------------- tests/lint/.gitignore | 2 + tests/lint/createJunitReport.py | 65 ++++++++++++++++++++++++ tests/lint/flake8.ini | 35 +++++++++++++ tests/lint/genDiff.py | 65 ++++++++++++++++++++++++ tests/lint/lintInstall/.gitignore | 1 + tests/lint/lintInstall/requirements.txt | 5 ++ tests/lint/lintInstall/sconscript | 35 +++++++++++++ tests/lint/readme.md | 46 +++++++++++++++++ tests/lint/sconscript | 60 ++++++++++++++++++++++ tests/sconscript | 10 ++++ 14 files changed, 396 insertions(+), 76 deletions(-) delete mode 100755 source/pylintrc create mode 100644 tests/lint/.gitignore create mode 100644 tests/lint/createJunitReport.py create mode 100644 tests/lint/flake8.ini create mode 100644 tests/lint/genDiff.py create mode 100644 tests/lint/lintInstall/.gitignore create mode 100644 tests/lint/lintInstall/requirements.txt create mode 100644 tests/lint/lintInstall/sconscript create mode 100644 tests/lint/readme.md create mode 100644 tests/lint/sconscript diff --git a/appveyor.yml b/appveyor.yml index e2d23a3e65d..39e4d1f612b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,6 +23,8 @@ environment: init: - ps: | # iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) + $pythonVersion = (py --version) + echo $pythonVersion if ($env:APPVEYOR_REPO_TAG_NAME -and $env:APPVEYOR_REPO_TAG_NAME.StartsWith("release-")) { # Strip "release-" prefix. $version = $env:APPVEYOR_REPO_TAG_NAME.Substring(8) @@ -121,10 +123,11 @@ build_script: before_test: # install required packages - - py -m pip install -r tests/system/requirements.txt + - py -m pip install -r tests/system/requirements.txt -r tests/lint/lintInstall/requirements.txt - mkdir testOutput - mkdir testOutput\unit - mkdir testOutput\system + - mkdir testOutput\lint - ps: | $errorCode=0 $nvdaLauncherFile=".\output\nvda" @@ -157,6 +160,28 @@ test_script: $wc = New-Object 'System.Net.WebClient' $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", $unitTestsXml) if($errorCode -ne 0) { $host.SetShouldExit($errorCode) } + - ps: | + if($env:APPVEYOR_PULL_REQUEST_NUMBER) { + $lintOutput = (Resolve-Path .\testOutput\lint\) + $lintSource = (Resolve-Path .\tests\lint\) + git fetch -q origin $env:APPVEYOR_REPO_BRANCH + $prDiff = "$lintOutput\prDiff.patch" + git diff -U0 FETCH_HEAD...HEAD > $prDiff + $flake8Config = "$lintSource\flake8.ini" + $flake8Output = "$lintOutput\PR-Flake8.txt" + type "$prDiff" | py -m flake8 --diff --output-file="$flake8Output" --tee --config="$flake8Config" + if($LastExitCode -ne 0) { + $errorCode=$LastExitCode + Add-AppveyorMessage "PR introduces Flake8 errors" + } + Push-AppveyorArtifact $flake8Output + $junitXML = "$lintOutput\PR-Flake8.xml" + py "$lintSource\createJunitReport.py" "$flake8Output" "$junitXML" + Push-AppveyorArtifact $junitXML + $wc = New-Object 'System.Net.WebClient' + $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", $junitXML) + if($errorCode -ne 0) { $host.SetShouldExit($errorCode) } + } - ps: | $testOutput = (Resolve-Path .\testOutput\) $systemTestOutput = (Resolve-Path "$testOutput\system") @@ -169,9 +194,10 @@ test_script: $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path "$systemTestOutput\systemTests.xml")) if($errorCode -ne 0) { $host.SetShouldExit($errorCode) } -artifacts: - - path: output\* - - path: output\*\* +on_finish: + - ps: | + Get-ChildItem output\* | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } + Get-ChildItem output\*\* | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } deploy_script: - ps: | @@ -180,9 +206,9 @@ deploy_script: # Notify our server. $exe = Get-ChildItem -Name output\*.exe $hash = (Get-FileHash "output\$exe" -Algorithm SHA1).Hash.ToLower() - $apiVersion = (python -c "import sys; sys.path.append('source'); from addonAPIVersion import CURRENT; print('{}.{}.{}'.format(*CURRENT))") + $apiVersion = (py -c "import sys; sys.path.append('source'); from addonAPIVersion import CURRENT; print('{}.{}.{}'.format(*CURRENT))") echo apiversion: $apiVersion - $apiCompatTo = (python -c "import sys; sys.path.append('source'); from addonAPIVersion import BACK_COMPAT_TO; print('{}.{}.{}'.format(*BACK_COMPAT_TO))") + $apiCompatTo = (py -c "import sys; sys.path.append('source'); from addonAPIVersion import BACK_COMPAT_TO; print('{}.{}.{}'.format(*BACK_COMPAT_TO))") echo apiBackCompatTo: $apiCompatTo $data = @{ jobId=$env:APPVEYOR_JOB_ID; diff --git a/readme.md b/readme.md index 6153c6981a4..06c18b4295d 100644 --- a/readme.md +++ b/readme.md @@ -91,7 +91,13 @@ Additionally, the following build time dependencies are included in Git submodul * [Boost Optional (stand-alone header)](https://github.com/akrzemi1/Optional), from commit [3922965](https://github.com/akrzemi1/Optional/commit/3922965396fc455c6b1770374b9b4111799588a9) ### Other Dependencies -These dependencies are not included in Git submodules, but aren't needed by most people. +To lint using Flake 8 locally using our SCons integration, some dependencies are installed (automatically) via pip. +Although this [must be run manually](#linting-your-changes), developers may wish to first configure a Python Virtual Environment to ensure their general install is not affected. +* Flake8 +* Flake8-tabs + + +The following dependencies aren't needed by most people, and are not included in Git submodules: * To generate developer documentation for nvdaHelper: [Doxygen Windows installer](http://www.doxygen.nl/download.html), version 1.8.15: @@ -155,6 +161,8 @@ scons dist The build will be created in the dist directory. +### Building the installer + To create a launcher archive (one executable allowing for installation or portable dist generation), type: ``` @@ -163,6 +171,8 @@ scons launcher The archive will be placed in the output directory. +### Building the developer documentation + To generate the NVDA developer guide, type: ``` @@ -180,6 +190,7 @@ scons devDocs_nvdaHelper The documentation will be placed in the `devDocs\nvdaHelper` folder in the output directory. +### Generate debug symbols archive To generate an archive of debug symbols for the various dll/exe binaries, type: ``` @@ -188,12 +199,14 @@ scons symbolsArchive The archive will be placed in the output directory. +### Generate translation template To generate a gettext translation template (for translators), type: ``` scons pot ``` +### Customising the build Optionally, the build can be customised by providing variables on the command line: * version: The version of this build. @@ -216,15 +229,15 @@ scons launcher version=test1 ## Running Automated Tests If you make a change to the NVDA code, you should run NVDA's automated tests. These tests help to ensure that code changes do not unintentionally break functionality that was previously working. -Currently, NVDA has two kinds of automated testing: unit tests and translatable string checks. -To run the tests, first change directory to the root of the NVDA source distribution as above. +To run the tests (unit tests, translatable string checks), first change directory to the root of the NVDA source distribution as above. Then, run: ``` scons tests ``` +### Unit tests To run only specific unit tests, specify them using the `unitTests` variable on the command line. The tests should be provided as a comma separated list. Each test should be specified as a Python module, class or method relative to the `tests\unit` directory. @@ -234,12 +247,25 @@ For example, to run only methods in the `TestMove` and `TestSelection` classes i scons tests unitTests=test_cursorManager.TestMove,test_cursorManager.TestSelection ``` +### Translatable string checks To run only the translatable string checks (which check that all translatable strings have translator comments), run: ``` scons checkPot ``` +### Linting your changes +In order to ensure your changes comply with NVDA's coding style you can run the Flake8 linter locally. +Running via SCons will use Flake8 to inspect only the differences between your working directory and the specified `base` branch. +If you create a Pull Request, the `base` branch you use here should be the same as the target you would use for a Pull Request. In most cases it will be `origin/master`. +``` +scons lint base=origin/master +``` + +To be warned about linting errors faster, you may wish to integrate Flake8 other development tools you are using. +For more details, see `tests/lint/readme.md` + +### System Tests You may also use scons to run the system tests, though this will still rely on having set up the dependencies (see `tests/system/readme.md`). ``` diff --git a/sconstruct b/sconstruct index f7179eec952..6132a8fe360 100755 --- a/sconstruct +++ b/sconstruct @@ -76,6 +76,17 @@ vars.Add(ListVariable("nvdaHelperDebugFlags", "a list of debugging features you vars.Add(EnumVariable('nvdaHelperLogLevel','The level of logging you wish to see, lower is more verbose','15',allowed_values=[str(x) for x in range(60)])) if "tests" in COMMAND_LINE_TARGETS: vars.Add("unitTests", "A list of unit tests to run", "") +if "lint" in COMMAND_LINE_TARGETS: + vars.Add( # pass through variable that lint is requested + "doLint", + "internal use", + True + ) + vars.Add( + "base", + "Lint is done only on a diff, specify the ref to use as base for the diff.", + None + ) if "systemTests" in COMMAND_LINE_TARGETS: vars.Add("filter", "A filter for the name of the system test(s) to run. Wildcards accepted.", "") diff --git a/source/pylintrc b/source/pylintrc deleted file mode 100755 index 1c7ba81510c..00000000000 --- a/source/pylintrc +++ /dev/null @@ -1,67 +0,0 @@ -[MASTER] -profile=no -ignore=.svn -ignore=comInterfaces,config -persistent=yes -cache-size=500 -init-hook=_=lambda x: '' - -[MESSAGES CONTROL] -disable-check=SIMILARITIES -disable-msg=c0301,w0142,c0103,c0121,e1101,e0602,e0611,w0603,f0401,w0702,w0704,c0322,c0323,c0324,w0511, - -[REPORTS] -output-format=text -include-ids=yes -files-output=no -reports=no - -[BASIC] -no-docstring-rgx=__.*__ -module-rgx=(([a-z_][a-zA-Z0-9_-]*)|([A-Z][a-zA-Z0-9_-]+))$ -const-rgx=(([a-zA-Z_][a-zA-Z1-9_]*)|(__.*__))$ -class-rgx=[A-Z_][a-zA-Z0-9]+$ -function-rgx=[a-z_][a-zA-Z0-9_]*$ -method-rgx=[a-z_][a-zA-Z0-9_]*$ -attr-rgx=[a-z_][a-zA-Z0-9_]*$ -argument-rgx=[a-z_][a-zA-Z0-9_]*$ -variable-rgx=[a-z_][a-zA-Z0-9_]*$ -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ -good-names=_ -bad-functions=map,apply,input - -[VARIABLES] -init-import=yes -dummy-variables-rgx=_|dummy -additional-builtins=_ - -[CLASSES] -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__ - -[DESIGN] -max-args=7 -max-locals=15 -max-returns=6 -max-branchs=20 -max-statements=50 -max-parents=7 -max-attributes=7 -min-public-methods=0 -max-public-methods=20 - -[IMPORTS] -deprecated-modules=regsub,string,TERMIOS,Bastion,rexec - -[FORMAT] -max-line-length=80 -max-module-lines=1000 -indent-string='\t' - -[MISCELLANEOUS] -notes=FIXME,XXX,TODO - -[SIMILARITIES] -min-similarity-lines=4 -ignore-comments=yes -ignore-docstrings=yes diff --git a/tests/lint/.gitignore b/tests/lint/.gitignore new file mode 100644 index 00000000000..74720b11b0e --- /dev/null +++ b/tests/lint/.gitignore @@ -0,0 +1,2 @@ +current.diff +current.lint diff --git a/tests/lint/createJunitReport.py b/tests/lint/createJunitReport.py new file mode 100644 index 00000000000..5b31b1c3937 --- /dev/null +++ b/tests/lint/createJunitReport.py @@ -0,0 +1,65 @@ +# -*- coding: UTF-8 -*- +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2019 NV Access Limited + +import os +from sys import argv + +NO_ERROR = r''' + + + + +''' + +# With Error: +WE_PRE = r''' + + + + + + + +''' + + +def makeJunitXML(inFileName, outFileName): + with open(inFileName, 'rt', encoding='UTF-8') as flake8In: + errorText = flake8In.read() + if len(errorText) > 0: + # make "with error" xml content + outContents = f'{WE_PRE}{errorText}{WE_POST}' + else: + # make "no error" xml content + outContents = NO_ERROR + + with open(outFileName, 'wt', encoding='UTF-8') as out: + out.write(outContents) + + +def main(): + try: + if len(argv) != 3: + raise RuntimeError( + f"{argv[0]} expects two arguments: flake8_output_file_name junit_file_name" + ) + scriptName, flake8OutputFileName, junitFileName = argv + if not os.path.isfile(flake8OutputFileName): + raise RuntimeError( + f"Flake8_output_file does not exist at {flake8OutputFileName}" + ) + makeJunitXML(flake8OutputFileName, junitFileName) + except Exception as e: + print(e) + raise e + + +if __name__ == "__main__": + # execute only if run as a script + main() diff --git a/tests/lint/flake8.ini b/tests/lint/flake8.ini new file mode 100644 index 00000000000..895c230d689 --- /dev/null +++ b/tests/lint/flake8.ini @@ -0,0 +1,35 @@ +[flake8] + +# Plugins +use-flake8-tabs = True +use-pycodestyle-indent = True + +# Reporting +statistics = True +doctests = True +show-source = True + +# Options +max-complexity = 15 +max-line-length = 110 +hang-closing = True + +ignore = + W191, # indentation contains tabs + E126, # continuation line over-indented for hanging indent + E133, # closing bracket is missing indentation + W503, # line break before binary operator. As opposed to W504 (line break after binary operator) which we want to check for. + +builtins = # inform flake8 about functions we consider built-in. + _, # translation lookup + pgettext, # translation lookup + +exclude = # don't bother looking in the following subdirectories / files. + .git, + __pycache__, + .tox, + build, + output, + include, + miscDeps, + source/louis, diff --git a/tests/lint/genDiff.py b/tests/lint/genDiff.py new file mode 100644 index 00000000000..adf0f1bd4a8 --- /dev/null +++ b/tests/lint/genDiff.py @@ -0,0 +1,65 @@ +# -*- coding: UTF-8 -*- +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2019 NV Access Limited + +import subprocess +from typing import Tuple, List + +_mergeBaseCommand = " ".join([ + "git", + # merge-base is used to limit changes to only those that are new + # on HEAD. + "merge-base", + "{baseBranch}", # this is the target branch used in a PR + "HEAD" +]) +_diffCommand = " ".join([ + "git", "diff", + # Only include changed lines (no context) in the diff. Otherwise + # developers may end up getting warnings for code adjacent to code they + # touched. This could result in very large change sets in order to get a + # clean build. + "-U0", + # We don't use triple dot syntax ('...') because it will not + # report changes in your working tree. + "{mergeBase}" +]) + + +def getDiff(baseBranch: str) -> bytes: + mergeBaseCommand = _mergeBaseCommand.format(baseBranch=baseBranch) + mergeBase: bytes = subprocess.check_output(mergeBaseCommand) + mergeBase: str = mergeBase.decode(encoding='ANSI').strip() + + diffCommand = _diffCommand.format(mergeBase=mergeBase) + diff: bytes = subprocess.check_output(diffCommand) + return diff + + +def main(baseBranch: str, outFileName: str): + diff = getDiff(baseBranch) + with(open(outFileName, mode="bw")) as outFile: + outFile.write(diff) + + +def getArgs(argv: List[bytes]) -> Tuple[str, str]: + try: + if len(argv) != 3: + raise RuntimeError( + f"{argv[0]} expects two arguments: baseBranch outFile" + ) + scriptName, baseBranch, outFileName = argv + assert isinstance(baseBranch, str) + assert isinstance(outFileName, str) + return baseBranch, outFileName + except Exception as e: + print(e) + raise e + + +if __name__ == "__main__": + # execute only if run as a script + from sys import argv + main(*getArgs(argv)) diff --git a/tests/lint/lintInstall/.gitignore b/tests/lint/lintInstall/.gitignore new file mode 100644 index 00000000000..9b82c51b601 --- /dev/null +++ b/tests/lint/lintInstall/.gitignore @@ -0,0 +1 @@ +_executed_requirements.txt \ No newline at end of file diff --git a/tests/lint/lintInstall/requirements.txt b/tests/lint/lintInstall/requirements.txt new file mode 100644 index 00000000000..1a8f9f19bb3 --- /dev/null +++ b/tests/lint/lintInstall/requirements.txt @@ -0,0 +1,5 @@ +####### requirements.txt ####### +# +###### Requirements for automated lint ###### +flake8 ~= 3.7.7 +flake8-tabs == 2.0.0 diff --git a/tests/lint/lintInstall/sconscript b/tests/lint/lintInstall/sconscript new file mode 100644 index 00000000000..b3b27f7b514 --- /dev/null +++ b/tests/lint/lintInstall/sconscript @@ -0,0 +1,35 @@ +### +# This file is a part of the NVDA project. +# URL: http://www.nvaccess.org/ +# Copyright 2019 NV Access Limited. +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2.0, as published by +# the Free Software Foundation. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# This license can be found at: +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html +### + +import sys + +Import("env") + +doInstall = env.Command( + "_executed_requirements.txt", # $TARGET + "requirements.txt", # $SOURCE + [ + # Install deps from requirements file. + [ + sys.executable, "-m", "pip", + "install", "-r", "$SOURCE", + ], + # Copy the requirements file, this is used to stop scons from + # triggering pip from attempting re-installing when nothing has + # changed. Pip takes a long time to determine that deps are met. + Copy('$TARGET', '$SOURCE') + ] +) + +Return('doInstall') diff --git a/tests/lint/readme.md b/tests/lint/readme.md new file mode 100644 index 00000000000..965dd8072c1 --- /dev/null +++ b/tests/lint/readme.md @@ -0,0 +1,46 @@ + +## Lint overview +Our linting process with Flake8 consists of two main steps: +- Generating a diff +- Running Flake8 on the diff + +## Scons lint +Executed with SCons. +``` +scons lint base=origin/master +``` +- Remember to set the the `base` branch (the target you would use for a PR). +- This SCons target will generate a diff file (`tests\lint\current.diff`) and run Flake8 on it. +- The output from Flake8 will be displayed with other SCons output, and will also be written to file (`tests\lint\current.lint`) +- This uses the NVDA flake8 configuration file (`tests\lint\flake8.ini`) + +## Lint integration + +For faster lint results, or greater integration with your tools you may want to execute this on the command line or via your IDE. + +### Generate a diff + +You can use the `tests/lint/genDiff.py` script to generate the diff, or create the diff on the commandline: +- Get the merge base + - `git merge-base HEAD` + - `merge-base` is used to limit changes to only those that are new on HEAD. +- Create a diff with your working tree + - `git diff -U0 ` + - `-U0`: Only include changed lines (no context) in the diff. + - Otherwise developers may end up getting warnings for code adjacent to code they touched. + - This could result in very large change sets in order to get a clean build. + - Note: We don't use triple dot syntax (`...`) because it will not report changes in your working tree. + +### Pipe to Flake8 + +Flake8 can accept a unified diff, however only via stdin. + +In cmd: +``` +type current.diff | py -3 -m flake8 --diff --config="tests/lint/flake8.ini" +``` + +In bash: +``` +cat current.diff | py -3 -m flake8 --diff --config="tests/lint/flake8.ini" +``` diff --git a/tests/lint/sconscript b/tests/lint/sconscript new file mode 100644 index 00000000000..a7d9a6b0a86 --- /dev/null +++ b/tests/lint/sconscript @@ -0,0 +1,60 @@ +### +# This file is a part of the NVDA project. +# URL: http://www.nvaccess.org/ +# Copyright 2019 NV Access Limited. +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2.0, as published by +# the Free Software Foundation. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# This license can be found at: +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html +### + +import os +import sys + +import SCons + +Import("env") + +# Add system path to get access to git. +externalEnv = Environment( + ENV = {'PATH' : os.environ['PATH']} +) + +doLint = env.get("doLint") +baseBranch = env.get("base") +if doLint and not baseBranch: + errorMessage = ( + "Lint can not complete without base branch. " + "Try: 'scons lint base=origin/master' " + "See also /tests/lint/readme.md" + ) + raise SCons.SConf.SConfError(errorMessage) + +# Create a unified diff. Written to file for ease of manual inspection +diffTarget = externalEnv.Command( + "current.diff", + None, + [[sys.executable, "tests/lint/genDiff.py", baseBranch, '$TARGET']] +) + +# Pipe diff to flake8 for linting. +lintTarget = externalEnv.Command( + "current.lint", + "current.diff", + [[ + 'type', '$SOURCE', '|', # provide diff to stdin + sys.executable, "-m", "flake8", + '--diff', # accept a unified diff from stdin + '--output-file=$TARGET', # output to a file to allow easier inspection + '--tee', # also output to stdout, so results appear in scons output + '--config="tests/lint/flake8.ini"', # use config file of complex options + '--exit-zero', # even if there are errors, don't stop the build. + ]] +) + +env.Depends(lintTarget, diffTarget) +Return('diffTarget', 'lintTarget') diff --git a/tests/sconscript b/tests/sconscript index 68ff8bad232..2f110d3cebe 100644 --- a/tests/sconscript +++ b/tests/sconscript @@ -25,6 +25,16 @@ env.Depends(systemTests, sourceDir) env.AlwaysBuild(systemTests) env.Alias("systemTests", systemTests) + +lintInstall = env.SConscript("lint/lintInstall/sconscript", exports=["env"]) +env.Depends(lintInstall, env.Dir("tests/lint/lintInstall/requirements.txt")) +env.Alias("lintInstall", lintInstall) + +lint = env.SConscript("lint/sconscript", exports=["env"]) +env.Depends(lint, lintInstall) +env.AlwaysBuild(lint) +env.Alias("lint", lint) + def checkPotAction(target, source, env): return checkPot.checkPot(source[0].abspath) checkPotTarget = env.Command("checkPot", pot, checkPotAction) From 83d72334518ff7d26449c9676e39dd948533c6a6 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 1 Aug 2019 17:05:53 +0200 Subject: [PATCH 10/12] Update changes file for PR #9958 --- user_docs/en/changes.t2t | 1 + 1 file changed, 1 insertion(+) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 692a790fa80..99e5510b8d0 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -28,6 +28,7 @@ What's New in NVDA - SynthDriver classes must now notify the synthDriverHandler.synthDoneSpeaking action, once all audio from a SynthDriver.speak call has completed playing. - SynthDriver classes must support the speech.PitchCommand in their speak method, as changes in pitch for speak spelling now depends on this functionality. - The tab-completion in the Python console only suggests attributes starting with an underscore if the underscore is first typed. (#9918) +- Flake8 linting tool has been integrated with SCons reflecting code requirements for Pull Requests. (#5918) = 2019.2 = From 8a8c1c2509aaa54d1fe1d4365c0ac99c5d8f9395 Mon Sep 17 00:00:00 2001 From: Bill Dengler Date: Thu, 1 Aug 2019 11:28:42 -0400 Subject: [PATCH 11/12] UI Automation in Windows Console: improve reliability of visible range checks (PR #9957) Builds on #9614 Supersedes #9735 and #9899 Closes #9891 Previously, after the console window was maximized (or the review cursor is otherwise placed outside the visible text), text review is no longer functional. The review top line and review bottom line scripts do not function as intended. This commit changes: - The isOffscreen property has been implemented as UIAUtils.isTextRangeOffscreen. - When checking if the text range is out of bounds, we now also check that oldRange is in bounds before stopping movement. - Re-implemented POSITION_FIRST and POSITION_LAST in terms of visible ranges. --- source/NVDAObjects/UIA/winConsoleUIA.py | 48 +++++++++++++++++-------- source/UIAUtils.py | 17 +++++++++ user_docs/en/userGuide.t2t | 15 ++++++++ 3 files changed, 65 insertions(+), 15 deletions(-) diff --git a/source/NVDAObjects/UIA/winConsoleUIA.py b/source/NVDAObjects/UIA/winConsoleUIA.py index b0b0896c82e..e7719489f5a 100644 --- a/source/NVDAObjects/UIA/winConsoleUIA.py +++ b/source/NVDAObjects/UIA/winConsoleUIA.py @@ -9,6 +9,8 @@ import textInfos import UIAHandler +from comtypes import COMError +from UIAUtils import isTextRangeOffscreen from winVersion import isWin10 from . import UIATextInfo from ..behaviors import KeyboardHandlerBasedTypedCharSupport @@ -22,6 +24,21 @@ class consoleUIATextInfo(UIATextInfo): #: to do much good either. _expandCollapseBeforeReview = False + def __init__(self,obj,position,_rangeObj=None): + super(consoleUIATextInfo, self).__init__(obj, position, _rangeObj) + # Re-implement POSITION_FIRST and POSITION_LAST in terms of + # visible ranges to fix review top/bottom scripts. + if position==textInfos.POSITION_FIRST: + visiRanges = self.obj.UIATextPattern.GetVisibleRanges() + firstVisiRange = visiRanges.GetElement(0) + self._rangeObj = firstVisiRange + self.collapse() + elif position==textInfos.POSITION_LAST: + visiRanges = self.obj.UIATextPattern.GetVisibleRanges() + lastVisiRange = visiRanges.GetElement(visiRanges.length - 1) + self._rangeObj = lastVisiRange + self.collapse(True) + def collapse(self,end=False): """Works around a UIA bug on Windows 10 1903 and later.""" if not isWin10(1903): @@ -46,8 +63,6 @@ def move(self, unit, direction, endPoint=None): visiRanges = self.obj.UIATextPattern.GetVisibleRanges() visiLength = visiRanges.length if visiLength > 0: - firstVisiRange = visiRanges.GetElement(0) - lastVisiRange = visiRanges.GetElement(visiLength - 1) oldRange = self._rangeObj.clone() if unit == textInfos.UNIT_WORD and direction != 0: # UIA doesn't implement word movement, so we need to do it manually. @@ -90,7 +105,6 @@ def move(self, unit, direction, endPoint=None): lineInfo.expand(textInfos.UNIT_LINE) offset = self._getCurrentOffsetInThisLine(lineInfo) # Finally using the new offset, - # Calculate the current word offsets and move to the start of # this word if we are not already there. start, end = self._getWordOffsetsInThisLine(offset, lineInfo) @@ -104,15 +118,16 @@ def move(self, unit, direction, endPoint=None): else: # moving by a unit other than word res = super(consoleUIATextInfo, self).move(unit, direction, endPoint) - if oldRange and ( - self._rangeObj.CompareEndPoints( - UIAHandler.TextPatternRangeEndpoint_Start, firstVisiRange, - UIAHandler.TextPatternRangeEndpoint_Start) < 0 - or self._rangeObj.CompareEndPoints( - UIAHandler.TextPatternRangeEndpoint_Start, lastVisiRange, - UIAHandler.TextPatternRangeEndpoint_End) >= 0): - self._rangeObj = oldRange - return 0 + try: + if ( + oldRange + and isTextRangeOffscreen(self._rangeObj, visiRanges) + and not isTextRangeOffscreen(oldRange, visiRanges) + ): + self._rangeObj = oldRange + return 0 + except (COMError, RuntimeError): + pass return res def expand(self, unit): @@ -236,9 +251,12 @@ def _get_caretMovementDetectionUsesEvents(self): def _getTextLines(self): # Filter out extraneous empty lines from UIA - ptr = self.UIATextPattern.GetVisibleRanges() - res = [ptr.GetElement(i).GetText(-1) for i in range(ptr.length)] - return res + return ( + self.makeTextInfo(textInfos.POSITION_ALL) + ._rangeObj.getText(-1) + .rstrip() + .split("\r\n") + ) def findExtraOverlayClasses(obj, clsList): diff --git a/source/UIAUtils.py b/source/UIAUtils.py index b2e6e1c2f5e..6c4e8519e18 100644 --- a/source/UIAUtils.py +++ b/source/UIAUtils.py @@ -172,6 +172,23 @@ def getChildrenWithCacheFromUIATextRange(textRange,cacheRequest): c=CacheableUIAElementArray(c) return c +def isTextRangeOffscreen(textRange, visiRanges): + """Given a UIA text range and a visible textRanges array (returned from obj.UIATextPattern.GetVisibleRanges), determines if the given textRange is not within the visible textRanges.""" + visiLength = visiRanges.length + if visiLength > 0: + firstVisiRange = visiRanges.GetElement(0) + lastVisiRange = visiRanges.GetElement(visiLength - 1) + return textRange.CompareEndPoints( + UIAHandler.TextPatternRangeEndpoint_Start, firstVisiRange, + UIAHandler.TextPatternRangeEndpoint_Start + ) < 0 or textRange.CompareEndPoints( + UIAHandler.TextPatternRangeEndpoint_Start, lastVisiRange, + UIAHandler.TextPatternRangeEndpoint_End) >= 0 + else: + # Visible textRanges not available. + raise RuntimeError("Visible textRanges array is empty or invalid.") + + class UIATextRangeAttributeValueFetcher(object): def __init__(self,textRange): diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 846c676ae88..afed9c7cc78 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -942,6 +942,21 @@ When in the table view of added books: | Context menu | applications | Opens the context menu for the selected book. | %kc:endInclude +++ Windows Console ++[WinConsole] +NVDA provides support for the Windows command console used by Command Prompt, PowerShell, and the Windows Subsystem for Linux. +The console window is of fixed size, typically much smaller than the buffer that holds the output. +As new text is written, the content scroll upwards and previous text is no longer visible. +Text that is not visibly displayed in the window is not accessible with NVDA's text review commands. +Therefore, it is necessary to scroll the console window to read earlier text. +%kc:beginInclude +The following built-in Windows Console keyboard shortcuts may be useful when [reviewing text #ReviewingText] with NVDA: +|| Name | Key | Description | +| Scroll up | control+upArrow | Scrolls the console window up, so earlier text can be read. | +| Scroll down | control+downArrow | Scrolls the console window down, so later text can be read. | +| Scroll to start | control+home | Scrolls the console window to the beginning of the buffer. | +| Scroll to end | control+end | Scrolls the console window to the end of the buffer. | +%kc:endInclude + + Configuring NVDA +[ConfiguringNVDA] Most configuration can be performed using dialog boxes accessed through the Preferences sub-menu of the NVDA menu. Many of these settings can be found in the multi-page [NVDA Settings dialog #NVDASettings]. From 1af8775930e53766f67c28dea42ee9c958a7c12d Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 1 Aug 2019 17:28:48 +0200 Subject: [PATCH 12/12] Update changes file for PR #9957 --- user_docs/en/changes.t2t | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 99e5510b8d0..bad93d54281 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -5,6 +5,10 @@ What's New in NVDA = 2019.3 = +== Changes == +- The user guide now describes how to use NVDA in the Windows Console. (#9957) + + == Bug Fixes == - Emoji and other 32 bit unicode characters now take less space on a braille display when they are shown as hexadecimal values. (#6695) - In Windows 10, NVDA will announce tooltips from universal apps if NVDA is configured to report tooltips in object presentation dialog. (#8118)