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/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index d238d1e40cd..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 @@ -863,7 +869,7 @@ def _get_states(self): log.debugWarning("could not get IAccessible states",exc_info=True) else: states.update(IAccessibleHandler.IAccessibleStatesToNVDAStates[x] for x in (y for y in (1< 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. @@ -94,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) @@ -108,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): @@ -223,89 +234,30 @@ 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.""" + return False 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 - - def _calculateNewText(self, newLines, oldLines): - self._hasNewLines = ( - self._findNonBlankIndices(newLines) - != self._findNonBlankIndices(oldLines) + return ( + self.makeTextInfo(textInfos.POSITION_ALL) + ._rangeObj.getText(-1) + .rstrip() + .split("\r\n") ) - 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 0780c6053e8..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: @@ -73,6 +79,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", 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/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/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 diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 6df8c663884..a92a710f523 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/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")) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 9ae92da0dc2..b0f6c71c100 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2075,6 +2075,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") @@ -2161,6 +2177,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 @@ -2172,6 +2189,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 @@ -2186,6 +2204,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/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/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 + + + + +''' + +# 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) diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 6cac926706a..bad93d54281 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -3,10 +3,17 @@ What's New in NVDA %!includeconf: ../changes.t2tconf -= threshold release = += 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) +- 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 == @@ -25,6 +32,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 = diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 054088ae829..e36a10aa6c3 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]. @@ -1682,6 +1697,13 @@ When this option is enabled, NVDA will use a new, work in progress version of it ==== Speak passwords in UIA consoles ====[AdvancedSettingsWinConsoleSpeakPasswords] This setting controls whether characters are spoken by [speak typed characters #KeyboardSettingsSpeakTypedCharacters] or [speak typed words #KeyboardSettingsSpeakTypedWords] in situations where the screen does not update (such as password entry) in the Windows Console with UI automation support enabled. For security purposes, this setting should be left disabled. However, you may wish to enable it if you experience performance issues or instability with typed character and/or word reporting while using NVDA's new experimental console support. +==== Use 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 and 1709 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. + ==== Automatically set system focus to focusable elements in Browse Mode ====[BrowseModeSettingsAutoFocusFocusableElements] Key: NVDA+8