diff --git a/.gitmodules b/.gitmodules index 83bdb851c61..c46dc5d364c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -43,3 +43,6 @@ [submodule "include/javaAccessBridge32"] path = include/javaAccessBridge32 url = https://github.com/nvaccess/javaAccessBridge32-bin.git +[submodule "include/dmp"] + path = include/dmp + url = https://github.com/diff-match-patch-python/diff-match-patch diff --git a/copying.txt b/copying.txt index a6660123844..9bc5153f48b 100644 --- a/copying.txt +++ b/copying.txt @@ -2089,6 +2089,213 @@ This applies to wxPython, available from http://www.wxpython.org/ code and/or adjust the licensing conditions notice accordingly. ``` += Diff-Match-Patch = +This applies to Diff-Match-Patch, primarily available from https://github.com/google/diff-match-patch + +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +``` + = Acknowledgements = These components do not require reproduction of their license text: - Nullsoft Scriptable Install System, available from http://nsis.sourceforge.net/ diff --git a/include/dmp b/include/dmp new file mode 160000 index 00000000000..f63dfed2d99 --- /dev/null +++ b/include/dmp @@ -0,0 +1 @@ +Subproject commit f63dfed2d998bbacf6dc7e529505ab74835b2b49 diff --git a/readme.md b/readme.md index d72b016e116..724d4a563d9 100644 --- a/readme.md +++ b/readme.md @@ -98,6 +98,7 @@ For reference, the following run time dependencies are included in Git submodule * [pySerial](https://pypi.python.org/pypi/pyserial), version 3.4 * [Python interface to FTDI driver/chip](http://fluidmotion.dyndns.org/zenphoto/index.php?p=news&title=Python-interface-to-FTDI-driver-chip) * Java Access Bridge 32 bit, from Zulu Community OpenJDK build 13.0.1+10Zulu (13.28.11) +* [Diff-match-patch](https://github.com/diff-match-patch-python/diff-match-patch), latest master. Additionally, the following build time dependencies are included in Git submodules: diff --git a/source/NVDAObjects/UIA/winConsoleUIA.py b/source/NVDAObjects/UIA/winConsoleUIA.py index ce7a4d275b4..d6cf2700eb8 100644 --- a/source/NVDAObjects/UIA/winConsoleUIA.py +++ b/source/NVDAObjects/UIA/winConsoleUIA.py @@ -1,4 +1,3 @@ -# NVDAObjects/UIA/winConsoleUIA.py # A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -366,13 +365,6 @@ def _get_TextInfo(self): movement.""" return consoleUIATextInfo if self.is21H1Plus else consoleUIATextInfoPre21H1 - def _getTextLines(self): - # This override of _getTextLines takes advantage of the fact that - # the console text contains linefeeds for every line - # Thus a simple string splitlines is much faster than splitting by unit line. - ti = self.makeTextInfo(textInfos.POSITION_ALL) - text = ti.text or "" - return text.splitlines() def findExtraOverlayClasses(obj, clsList): if obj.UIAElement.cachedAutomationId == "Text Area": diff --git a/source/NVDAObjects/behaviors.py b/source/NVDAObjects/behaviors.py index a3dfbe9a549..ae8f55c88d7 100755 --- a/source/NVDAObjects/behaviors.py +++ b/source/NVDAObjects/behaviors.py @@ -1,5 +1,4 @@ # -*- coding: UTF-8 -*- -# NVDAObjects/behaviors.py # A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -12,7 +11,7 @@ import os import time import threading -import difflib +from diff_match_patch import diff_match_patch import tones import queueHandler import eventHandler @@ -29,6 +28,7 @@ import ui import braille import nvwave +from typing import List class ProgressBar(NVDAObject): @@ -219,6 +219,9 @@ class LiveText(NVDAObject): """ #: The time to wait before fetching text after a change event. STABILIZE_DELAY = 0 + #: Whether this object supports Diff-Match-Patch character diffing. + #: Set to False to use line diffing. + _supportsDMP = True # If the text is live, this is definitely content. presentationType = NVDAObject.presType_content @@ -228,6 +231,7 @@ def initOverlayClass(self): self._event = threading.Event() self._monitorThread = None self._keepMonitoring = False + self._dmp = diff_match_patch() def startMonitoring(self): """Start monitoring for new text. @@ -263,15 +267,16 @@ def event_textChange(self): """ self._event.set() - def _getTextLines(self): - """Retrieve the text of this object in lines. + def _getText(self) -> str: + """Retrieve the text of this object. This will be used to determine the new text to speak. The base implementation uses the L{TextInfo}. However, subclasses should override this if there is a better way to retrieve the text. - @return: The current lines of text. - @rtype: list of str """ - return list(self.makeTextInfo(textInfos.POSITION_ALL).getTextInChunks(textInfos.UNIT_LINE)) + if hasattr(self, "_getTextLines"): + log.warning("LiveText._getTextLines is deprecated, please override _getText instead.") + return '\n'.join(self._getTextLines()) + return self.makeTextInfo(textInfos.POSITION_ALL).text def _reportNewLines(self, lines): """ @@ -289,10 +294,10 @@ def _reportNewText(self, line): def _monitor(self): try: - oldLines = self._getTextLines() + oldText = self._getText() except: - log.exception("Error getting initial lines") - oldLines = [] + log.exception("Error getting initial text") + oldText = "" while self._keepMonitoring: self._event.wait() @@ -307,9 +312,9 @@ def _monitor(self): self._event.clear() try: - newLines = self._getTextLines() + newText = self._getText() if config.conf["presentation"]["reportDynamicContentChanges"]: - outLines = self._calculateNewText(newLines, oldLines) + outLines = self._calculateNewText(newText, oldText) if len(outLines) == 1 and len(outLines[0].strip()) == 1: # This is only a single character, # which probably means it is just a typed character, @@ -317,15 +322,28 @@ def _monitor(self): del outLines[0] if outLines: queueHandler.queueFunction(queueHandler.eventQueue, self._reportNewLines, outLines) - oldLines = newLines + oldText = newText except: - log.exception("Error getting lines or calculating new text") + log.exception("Error getting or calculating new text") + + def _calculateNewText_dmp(self, newText: str, oldText: str) -> List[str]: + diffs = self._dmp.diff_main(oldText, newText) + res = "" + self._dmp.diff_cleanupSemantic(diffs) + for op, change in diffs: + if op == self._dmp.DIFF_INSERT or ( + change == self._dmp.DIFF_EQUAL and change.isspace() + ): + res += change + return [line for line in res.splitlines() if line and not line.isspace()] - def _calculateNewText(self, newLines, oldLines): + def _calculateNewText_difflib(self, newLines: List[str], oldLines: List[str]) -> List[str]: outLines = [] prevLine = None - for line in difflib.ndiff(oldLines, newLines): + from difflib import ndiff + + for line in ndiff(oldLines, newLines): if line[0] == "?": # We're never interested in these. continue @@ -373,6 +391,16 @@ def _calculateNewText(self, newLines, oldLines): return outLines + def _calculateNewText(self, newText: str, oldText: str) -> List[str]: + return ( + self._calculateNewText_dmp(newText, oldText) + if self._supportsDMP and config.conf["terminals"]["useDMPWhenAvailable"] + else self._calculateNewText_difflib( + newText.splitlines(), oldText.splitlines() + ) + ) + + class Terminal(LiveText, EditableText): """An object which both accepts text input and outputs text which should be reported automatically. This is an L{EditableText} object, diff --git a/source/NVDAObjects/window/winConsole.py b/source/NVDAObjects/window/winConsole.py index 96d3d93155b..c2ffb03d285 100644 --- a/source/NVDAObjects/window/winConsole.py +++ b/source/NVDAObjects/window/winConsole.py @@ -1,8 +1,7 @@ -#NVDAObjects/WinConsole.py -#A part of NonVisual Desktop Access (NVDA) -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. -#Copyright (C) 2007-2019 NV Access Limited, Bill Dengler +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2007-2020 NV Access Limited, Bill Dengler import winConsoleHandler from . import Window @@ -21,10 +20,14 @@ 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): + # Legacy consoles take quite a while to send textChange events. + # This significantly impacts typing performance, so don't queue chars. self._supportsTextChange = False + else: + # Use line diffing to report changes in the middle of lines + # in non-enhanced legacy consoles. + self._supportsDMP = False def _get_windowThreadID(self): # #10113: Windows forces the thread of console windows to match the thread of the first attached process. @@ -69,8 +72,8 @@ def event_loseFocus(self): def event_nameChange(self): pass - def _getTextLines(self): - return winConsoleHandler.getConsoleVisibleLines() + def _getText(self): + return '\n'.join(winConsoleHandler.getConsoleVisibleLines()) def script_caret_backspaceCharacter(self, gesture): super(WinConsole, self).script_caret_backspaceCharacter(gesture) diff --git a/source/config/configSpec.py b/source/config/configSpec.py index a170574c675..25f43a03705 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -219,6 +219,7 @@ [terminals] speakPasswords = boolean(default=false) keyboardSupportInLegacy = boolean(default=True) + useDMPWhenAvailable = boolean(default=True) [update] autoCheck = boolean(default=true) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 941bbc73ba8..932b47c4e2f 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2380,6 +2380,14 @@ def __init__(self, parent): self.keyboardSupportInLegacyCheckBox.defaultValue = self._getDefaultValue(["terminals", "keyboardSupportInLegacy"]) self.keyboardSupportInLegacyCheckBox.Enable(winVersion.isWin10(1607)) + # Translators: This is the label for a checkbox in the + # Advanced settings panel. + label = _("Detect changes by c&haracter when available") + self.useDMPWhenAvailableCheckBox = terminalsGroup.addItem(wx.CheckBox(self, label=label)) + self.useDMPWhenAvailableCheckBox.SetValue(config.conf["terminals"]["useDMPWhenAvailable"]) + self.useDMPWhenAvailableCheckBox.defaultValue = self._getDefaultValue(["terminals", "useDMPWhenAvailable"]) + self.useDMPWhenAvailableCheckBox.Enable(winVersion.isWin10(1607)) + # Translators: This is the label for a group of advanced options in the # Advanced settings panel label = _("Speech") @@ -2495,6 +2503,7 @@ def haveConfigDefaultsBeenRestored(self): and self.winConsoleSpeakPasswordsCheckBox.IsChecked() == self.winConsoleSpeakPasswordsCheckBox.defaultValue and self.cancelExpiredFocusSpeechCombo.GetSelection() == self.cancelExpiredFocusSpeechCombo.defaultValue and self.keyboardSupportInLegacyCheckBox.IsChecked() == self.keyboardSupportInLegacyCheckBox.defaultValue + and self.useDMPWhenAvailableCheckBox.IsChecked() == self.useDMPWhenAvailableCheckBox.defaultValue and self.caretMoveTimeoutSpinControl.GetValue() == self.caretMoveTimeoutSpinControl.defaultValue and set(self.logCategoriesList.CheckedItems) == set(self.logCategoriesList.defaultCheckedItems) and True # reduce noise in diff when the list is extended. @@ -2508,6 +2517,7 @@ def restoreToDefaults(self): self.winConsoleSpeakPasswordsCheckBox.SetValue(self.winConsoleSpeakPasswordsCheckBox.defaultValue) self.cancelExpiredFocusSpeechCombo.SetSelection(self.cancelExpiredFocusSpeechCombo.defaultValue) self.keyboardSupportInLegacyCheckBox.SetValue(self.keyboardSupportInLegacyCheckBox.defaultValue) + self.useDMPWhenAvailableCheckBox.SetValue(self.useDMPWhenAvailableCheckBox.defaultValue) self.caretMoveTimeoutSpinControl.SetValue(self.caretMoveTimeoutSpinControl.defaultValue) self.logCategoriesList.CheckedItems = self.logCategoriesList.defaultCheckedItems self._defaultsRestored = True @@ -2524,6 +2534,7 @@ def onSave(self): config.conf["terminals"]["speakPasswords"] = self.winConsoleSpeakPasswordsCheckBox.IsChecked() config.conf["featureFlag"]["cancelExpiredFocusSpeech"] = self.cancelExpiredFocusSpeechCombo.GetSelection() config.conf["terminals"]["keyboardSupportInLegacy"]=self.keyboardSupportInLegacyCheckBox.IsChecked() + config.conf["terminals"]["useDMPWhenAvailable"] = self.useDMPWhenAvailableCheckBox.IsChecked() config.conf["editableText"]["caretMoveTimeoutMs"]=self.caretMoveTimeoutSpinControl.GetValue() for index,key in enumerate(self.logCategories): config.conf['debugLog'][key]=self.logCategoriesList.IsChecked(index) diff --git a/source/sourceEnv.py b/source/sourceEnv.py index dfd7a6b9297..30c40fe5744 100644 --- a/source/sourceEnv.py +++ b/source/sourceEnv.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2013-2020 NV Access Limited, Leonard de Ruijter +# Copyright (C) 2013-2020 NV Access Limited, Leonard de Ruijter, Bill Dengler # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -18,6 +18,7 @@ os.path.join(TOP_DIR, "include", "configobj", "src"), os.path.join(TOP_DIR, "include", "wxPython"), os.path.join(TOP_DIR, "include", "py2exe"), + os.path.join(TOP_DIR, "include", "dmp", "diff_match_patch"), os.path.join(TOP_DIR, "miscDeps", "python"), ) diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index c5cb33a173d..b33381b2a09 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -1843,6 +1843,12 @@ This feature is available and enabled by default on Windows 10 versions 1607and 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. +==== Detect changes by character when available ====[AdvancedSettingsUseDMPWhenAvailable] +This option enables an alternative method for detecting output changes in terminals. +It may improve performance when large volumes of text are written to the console and allow more accurate reporting of changes made in the middle of lines. +However, it may be incompatible with some applications. +This feature is available and enabled by default on Windows 10 versions 1607 and later. + ==== Attempt to cancel speech for expired focus events ====[CancelExpiredFocusSpeech] This option enables behaviour which attempts to cancel speech for expired focus events. In particular moving quickly through messages in Gmail with Chrome can cause NVDA to speak outdated information.