diff --git a/.gitmodules b/.gitmodules index 41835aa2324..ad949f1e294 100644 --- a/.gitmodules +++ b/.gitmodules @@ -40,6 +40,9 @@ [submodule "include/javaAccessBridge32"] path = include/javaAccessBridge32 url = https://github.com/nvaccess/javaAccessBridge32-bin.git +[submodule "include/nvda_dmp"] + path = include/nvda_dmp + url = https://github.com/codeofdusk/nvda_dmp [submodule "include/w3c-aria-practices"] path = include/w3c-aria-practices url = https://github.com/w3c/aria-practices diff --git a/include/nvda_dmp b/include/nvda_dmp new file mode 160000 index 00000000000..b2ccf200866 --- /dev/null +++ b/include/nvda_dmp @@ -0,0 +1 @@ +Subproject commit b2ccf2008669e1acb0a7181c268ce6a1311d4d7e diff --git a/source/NVDAObjects/IAccessible/winConsole.py b/source/NVDAObjects/IAccessible/winConsole.py index 9347169bc5d..d2c9343ce3e 100644 --- a/source/NVDAObjects/IAccessible/winConsole.py +++ b/source/NVDAObjects/IAccessible/winConsole.py @@ -1,22 +1,45 @@ -#NVDAObjects/IAccessible/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 config +from NVDAObjects.behaviors import KeyboardHandlerBasedTypedCharSupport from winVersion import isWin10 from . import IAccessible from ..window import winConsole -class WinConsole(winConsole.WinConsole, IAccessible): - "The legacy console implementation for situations where UIA isn't supported." - pass + +class EnhancedLegacyWinConsole(KeyboardHandlerBasedTypedCharSupport, winConsole.WinConsole, IAccessible): + """ + A hybrid approach to console access, using legacy APIs to read output + and KeyboardHandlerBasedTypedCharSupport for input. + """ + #: Legacy consoles take quite a while to send textChange events. + #: This significantly impacts typing performance, so don't queue chars. + _supportsTextChange = False + + +class LegacyWinConsole(winConsole.WinConsole, IAccessible): + """ + NVDA's original console support, used by default on Windows versions + before 1607. + """ + + def _get_diffAlgo(self): + # Non-enhanced legacy consoles use caret proximity to detect + # typed/deleted text. + # Single-character changes are not reported as + # they are confused for typed characters. + # Force difflib to keep meaningful edit reporting in these consoles. + from diffHandler import get_difflib_algo + return get_difflib_algo() + def findExtraOverlayClasses(obj, clsList): if isWin10(1607) and config.conf['terminals']['keyboardSupportInLegacy']: - from NVDAObjects.behaviors import KeyboardHandlerBasedTypedCharSupport - clsList.append(KeyboardHandlerBasedTypedCharSupport) - clsList.append(WinConsole) + clsList.append(EnhancedLegacyWinConsole) + else: + clsList.append(LegacyWinConsole) diff --git a/source/NVDAObjects/UIA/winConsoleUIA.py b/source/NVDAObjects/UIA/winConsoleUIA.py index 03513bf48f1..c0d847b0bd3 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. @@ -363,20 +362,6 @@ def _get_TextInfo(self): movement.""" return consoleUIATextInfo if self.is21H1Plus else consoleUIATextInfoPre21H1 - def _getTextLines(self): - if self.is21H1Plus: - # #11760: the 21H1 UIA console wraps across lines. - # When text wraps, NVDA starts reading from the beginning of the visible text for every new line of output. - # Use the superclass _getTextLines instead. - return super()._getTextLines() - # 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 detectPossibleSelectionChange(self): try: return super().detectPossibleSelectionChange() diff --git a/source/NVDAObjects/behaviors.py b/source/NVDAObjects/behaviors.py index e09d93ec28e..6713ef9c296 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,6 @@ import os import time import threading -import difflib import tones import queueHandler import eventHandler @@ -30,6 +28,8 @@ import braille import nvwave import globalVars +from typing import List +import diffHandler class ProgressBar(NVDAObject): @@ -265,15 +265,31 @@ def event_textChange(self): """ self._event.set() - def _getTextLines(self): - """Retrieve the text of this object in lines. + def _get_diffAlgo(self): + """ + This property controls which diffing algorithm should be used by + this object. Most subclasses should simply use the base + implementation, which returns DMP (character-based diffing). + + @Note: DMP is experimental, and can be disallowed via user + preference. In this case, the prior stable implementation, Difflib + (line-based diffing), will be used. + """ + return diffHandler.get_dmp_algo() + + def _get_devInfo(self): + info = super().devInfo + info.append(f"diffing algorithm: {self.diffAlgo}") + return info + + 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)) + ti = self.makeTextInfo(textInfos.POSITION_ALL) + return self.diffAlgo._getText(ti) def _reportNewLines(self, lines): """ @@ -291,10 +307,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() @@ -309,9 +325,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, @@ -319,61 +335,13 @@ 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") - - def _calculateNewText(self, newLines, oldLines): - outLines = [] - - prevLine = None - for line in difflib.ndiff(oldLines, newLines): - if line[0] == "?": - # We're never interested in these. - continue - if line[0] != "+": - # We're only interested in new lines. - prevLine = line - continue - text = line[2:] - if not text or text.isspace(): - prevLine = line - continue - - if prevLine and prevLine[0] == "-" and len(prevLine) > 2: - # It's possible that only a few characters have changed in this line. - # If so, we want to speak just the changed section, rather than the entire line. - prevText = prevLine[2:] - textLen = len(text) - prevTextLen = len(prevText) - # Find the first character that differs between the two lines. - for pos in range(min(textLen, prevTextLen)): - if text[pos] != prevText[pos]: - start = pos - break - else: - # We haven't found a differing character so far and we've hit the end of one of the lines. - # This means that the differing text starts here. - start = pos + 1 - # Find the end of the differing text. - if textLen != prevTextLen: - # The lines are different lengths, so assume the rest of the line changed. - end = textLen - else: - for pos in range(textLen - 1, start - 1, -1): - if text[pos] != prevText[pos]: - end = pos + 1 - break - - if end - start < 15: - # Less than 15 characters have changed, so only speak the changed chunk. - text = text[start:end] + log.exception("Error getting or calculating new text") - if text and not text.isspace(): - outLines.append(text) - prevLine = line + def _calculateNewText(self, newText: str, oldText: str) -> List[str]: + return self.diffAlgo.diff(newText, oldText) - return outLines class Terminal(LiveText, EditableText): """An object which both accepts text input and outputs text which should be reported automatically. diff --git a/source/NVDAObjects/window/winConsole.py b/source/NVDAObjects/window/winConsole.py index 96d3d93155b..01ecec4e75e 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 @@ -14,18 +13,12 @@ class WinConsole(Terminal, EditableTextWithoutAutoSelectDetection, Window): """ - NVDA's legacy Windows Console support. + Base class for NVDA's legacy Windows Console support. This is used in situations where UIA isn't available. Please consider using NVDAObjects.UIA.winConsoleUIA instead. """ 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_windowThreadID(self): # #10113: Windows forces the thread of console windows to match the thread of the first attached process. # However, To correctly handle speaking of typed characters, @@ -69,8 +62,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 9e23fbe4fe7..97f5f284582 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -225,6 +225,7 @@ [terminals] speakPasswords = boolean(default=false) keyboardSupportInLegacy = boolean(default=True) + diffAlgo = option("auto", "dmp", "difflib", default="auto") [update] autoCheck = boolean(default=true) diff --git a/source/core.py b/source/core.py index 00d34fdf9aa..e2af1a42ab5 100644 --- a/source/core.py +++ b/source/core.py @@ -605,6 +605,14 @@ def run(self): _terminate(speech) _terminate(addonHandler) _terminate(garbageHandler) + # DMP is only started if needed. + # Terminate manually (and let it write to the log if necessary) + # as core._terminate always writes an entry. + try: + import diffHandler + diffHandler._dmp._terminate() + except Exception: + log.exception("Exception while terminating DMP") if not globalVars.appArgs.minimal and config.conf["general"]["playStartAndExitSounds"]: try: diff --git a/source/diffHandler.py b/source/diffHandler.py new file mode 100644 index 00000000000..5d37065c054 --- /dev/null +++ b/source/diffHandler.py @@ -0,0 +1,192 @@ +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2020 Bill Dengler + +import config +import globalVars +import os +import struct +import subprocess +import sys +from abc import abstractmethod +from baseObject import AutoPropertyObject +from difflib import ndiff +from logHandler import log +from textInfos import TextInfo, UNIT_LINE +from threading import Lock +from typing import List + + +class DiffAlgo(AutoPropertyObject): + @abstractmethod + def diff(self, newText: str, oldText: str) -> List[str]: + raise NotImplementedError + + @abstractmethod + def _getText(self, ti: TextInfo) -> str: + raise NotImplementedError + + +class DiffMatchPatch(DiffAlgo): + """A character-based diffing approach, using the Google Diff Match Patch + library in a proxy process (to work around a licence conflict). + """ + #: A subprocess.Popen object for the nvda_dmp process. + _proc = None + #: A lock to control access to the nvda_dmp process. + #: Control access to avoid synchronization problems if multiple threads + #: attempt to use nvda_dmp at the same time. + _lock = Lock() + + def _initialize(self): + """Start the nvda_dmp process if it is not already running. + @note: This should be run from within the context of an acquired lock.""" + if not DiffMatchPatch._proc: + log.debug("Starting diff-match-patch proxy") + if hasattr(sys, "frozen"): + dmp_path = (os.path.join(globalVars.appDir, "nvda_dmp.exe"),) + else: + dmp_path = (sys.executable, os.path.join( + globalVars.appDir, "..", "include", "nvda_dmp", "nvda_dmp.py" + )) + DiffMatchPatch._proc = subprocess.Popen( + dmp_path, + creationflags=subprocess.CREATE_NO_WINDOW, + bufsize=0, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE + ) + + def _getText(self, ti: TextInfo) -> str: + return ti.text + + def diff(self, newText: str, oldText: str) -> List[str]: + try: + if not newText and not oldText: + # Return an empty list here to avoid exiting + # nvda_dmp uses two zero-length texts as a sentinal value + return [] + with DiffMatchPatch._lock: + self._initialize() + old = oldText.encode("utf-8") + new = newText.encode("utf-8") + # Sizes are packed as 32-bit ints in native byte order. + # Since nvda and nvda_dmp are running on the same Python + # platform/version, this is okay. + tl = struct.pack("=II", len(old), len(new)) + DiffMatchPatch._proc.stdin.write(tl) + DiffMatchPatch._proc.stdin.write(old) + DiffMatchPatch._proc.stdin.write(new) + buf = b"" + sizeb = b"" + SIZELEN = 4 + while len(sizeb) < SIZELEN: + try: + sizeb += DiffMatchPatch._proc.stdout.read(SIZELEN - len(sizeb)) + except TypeError: + pass + (size,) = struct.unpack("=I", sizeb) + while len(buf) < size: + buf += DiffMatchPatch._proc.stdout.read(size - len(buf)) + return [ + line + for line in buf.decode("utf-8").splitlines() + if line and not line.isspace() + ] + except Exception: + log.exception("Exception in DMP, falling back to difflib") + return Difflib().diff(newText, oldText) + + def _terminate(self): + with DiffMatchPatch._lock: + if DiffMatchPatch._proc: + log.debug("Terminating diff-match-patch proxy") + # nvda_dmp exits when it receives two zero-length texts. + DiffMatchPatch._proc.stdin.write(struct.pack("=II", 0, 0)) + DiffMatchPatch._proc.wait(timeout=5) + + +class Difflib(DiffAlgo): + "A line-based diffing approach in pure Python, using the Python standard library." + + def diff(self, newText: str, oldText: str) -> List[str]: + newLines = newText.splitlines() + oldLines = oldText.splitlines() + outLines = [] + + prevLine = None + + for line in ndiff(oldLines, newLines): + if line[0] == "?": + # We're never interested in these. + continue + if line[0] != "+": + # We're only interested in new lines. + prevLine = line + continue + text = line[2:] + if not text or text.isspace(): + prevLine = line + continue + + if prevLine and prevLine[0] == "-" and len(prevLine) > 2: + # It's possible that only a few characters have changed in this line. + # If so, we want to speak just the changed section, rather than the entire line. + prevText = prevLine[2:] + textLen = len(text) + prevTextLen = len(prevText) + # Find the first character that differs between the two lines. + for pos in range(min(textLen, prevTextLen)): + if text[pos] != prevText[pos]: + start = pos + break + else: + # We haven't found a differing character so far and we've hit the end of one of the lines. + # This means that the differing text starts here. + start = pos + 1 + # Find the end of the differing text. + if textLen != prevTextLen: + # The lines are different lengths, so assume the rest of the line changed. + end = textLen + else: + for pos in range(textLen - 1, start - 1, -1): + if text[pos] != prevText[pos]: + end = pos + 1 + break + + if end - start < 15: + # Less than 15 characters have changed, so only speak the changed chunk. + text = text[start:end] + + if text and not text.isspace(): + outLines.append(text) + prevLine = line + + return outLines + + def _getText(self, ti: TextInfo) -> str: + return "\n".join(ti.getTextInChunks(UNIT_LINE)) + + +def get_dmp_algo(): + """ + This function returns a Diff Match Patch object if allowed by the user. + DMP is experimental and can be explicitly enabled/disabled by a user + setting to opt in or out of the experiment. If config does not allow + DMP, this function returns a Difflib instance instead. + """ + return ( + _dmp + if config.conf["terminals"]["diffAlgo"] == "dmp" + else _difflib + ) + + +def get_difflib_algo(): + "Returns an instance of the difflib diffAlgo." + return _difflib + + +_difflib = Difflib() +_dmp = DiffMatchPatch() diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index b2647c7dfbb..4267534c0fc 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2580,6 +2580,41 @@ def __init__(self, parent): self.keyboardSupportInLegacyCheckBox.defaultValue = self._getDefaultValue(["terminals", "keyboardSupportInLegacy"]) self.keyboardSupportInLegacyCheckBox.Enable(winVersion.isWin10(1607)) + # Translators: This is the label for a combo box for selecting a + # method of detecting changed content in terminals in the advanced + # settings panel. + # Choices are automatic, allow Diff Match Patch, and force Difflib. + diffAlgoComboText = _("&Diff algorithm:") + diffAlgoChoices = [ + # Translators: A choice in a combo box in the advanced settings + # panel to have NVDA determine the method of detecting changed + # content in terminals automatically. + _("Automatic (Difflib)"), + # Translators: A choice in a combo box in the advanced settings + # panel to have NVDA detect changes in terminals + # by character when supported, using the diff match patch algorithm. + _("allow Diff Match Patch"), + # Translators: A choice in a combo box in the advanced settings + # panel to have NVDA detect changes in terminals + # by line, using the difflib algorithm. + _("force Difflib") + ] + #: The possible diffAlgo config values, in the order they appear + #: in the combo box. + self.diffAlgoVals = ( + "auto", + "dmp", + "difflib" + ) + self.diffAlgoCombo = terminalsGroup.addLabeledControl(diffAlgoComboText, wx.Choice, choices=diffAlgoChoices) + curChoice = self.diffAlgoVals.index( + config.conf['terminals']['diffAlgo'] + ) + self.diffAlgoCombo.SetSelection(curChoice) + self.diffAlgoCombo.defaultValue = self.diffAlgoVals.index( + self._getDefaultValue(["terminals", "diffAlgo"]) + ) + # Translators: This is the label for a group of advanced options in the # Advanced settings panel label = _("Speech") @@ -2701,6 +2736,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.diffAlgoCombo.GetSelection() == self.diffAlgoCombo.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. @@ -2714,6 +2750,7 @@ def restoreToDefaults(self): self.winConsoleSpeakPasswordsCheckBox.SetValue(self.winConsoleSpeakPasswordsCheckBox.defaultValue) self.cancelExpiredFocusSpeechCombo.SetSelection(self.cancelExpiredFocusSpeechCombo.defaultValue) self.keyboardSupportInLegacyCheckBox.SetValue(self.keyboardSupportInLegacyCheckBox.defaultValue) + self.diffAlgoCombo.SetSelection(self.diffAlgoCombo.defaultValue == 'auto') self.caretMoveTimeoutSpinControl.SetValue(self.caretMoveTimeoutSpinControl.defaultValue) self.logCategoriesList.CheckedItems = self.logCategoriesList.defaultCheckedItems self._defaultsRestored = True @@ -2730,6 +2767,10 @@ def onSave(self): config.conf["terminals"]["speakPasswords"] = self.winConsoleSpeakPasswordsCheckBox.IsChecked() config.conf["featureFlag"]["cancelExpiredFocusSpeech"] = self.cancelExpiredFocusSpeechCombo.GetSelection() config.conf["terminals"]["keyboardSupportInLegacy"]=self.keyboardSupportInLegacyCheckBox.IsChecked() + diffAlgoChoice = self.diffAlgoCombo.GetSelection() + config.conf['terminals']['diffAlgo'] = ( + self.diffAlgoVals[diffAlgoChoice] + ) 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/setup.py b/source/setup.py index ba6b0639e8c..9db1366581a 100755 --- a/source/setup.py +++ b/source/setup.py @@ -6,6 +6,7 @@ #See the file COPYING for more details. import os +import sys import copy import gettext gettext.install("nvda") @@ -13,12 +14,21 @@ import py2exe as py2exeModule from glob import glob import fnmatch +# versionInfo names must be imported after Gettext +# Suppress E402 (module level import not at top of file) +from versionInfo import ( + formatBuildVersionString, + name, + version, + publisher +) # noqa: E402 from versionInfo import * from py2exe import distutils_buildexe from py2exe.dllfinder import DllFinder import wx import importlib.machinery - +# Explicitly put the nvda_dmp dir on the build path so the DMP library is included +sys.path.append(os.path.join("..", "include", "nvda_dmp")) RT_MANIFEST = 24 manifest_template = """\ @@ -187,6 +197,20 @@ def getRecursiveDataFiles(dest,source,excludes=()): "company_name": publisher, }, ], + console=[ + { + "script": os.path.join("..", "include", "nvda_dmp", "nvda_dmp.py"), + "uiAccess": False, + "icon_resources": [(1, "images/nvda.ico")], + "other_resources": [], # Populated at runtime + "version":formatBuildVersionString(), + "description": "NVDA Diff-match-patch proxy", + "product_name": name, + "product_version": version, + "copyright": f"{copyright}, Bill Dengler", + "company_name": f"Bill Dengler, {publisher}", + }, + ], options = {"py2exe": { "bundle_files": 3, "excludes": ["tkinter", diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 25f92ff365b..1ce7ab07062 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -12,10 +12,16 @@ What's New in NVDA == Bug Fixes == +- In terminal programs on Windows 10 version 1607 and later, when inserting or deleting characters in the middle of a line, the characters to the right of the caret are no longer read out. (#3200) + - This experimental fix must be manually enabled in NVDA's advanced settings panel by changing the diff algorithm to Diff Match Patch. == Changes for Developers == - Note: this is a Add-on API compatibility breaking release. Add-ons will need to be re-tested and have their manifest updated. +- `LiveText._getTextLines` has been removed. (#11639) + - Instead, override `_getText` which returns a string of all text in the object. +- `LiveText` objects can now calculate diffs by character. (#11639) + - To alter the diff behaviour for some object, override the `diffAlgo` property (see the docstring for details). = 2020.4 = diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 93fd0f6b898..8ec6f64f7e7 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -1843,6 +1843,20 @@ This feature is available and enabled by default on Windows 10 versions 1607 and 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. +==== Diff algorithm ====[AdvancedSettingsDiffAlgo] +This setting controls how NVDA determines the new text to speak in terminals. +The diff algorithm combo box has three options: +- Automatic: as of NVDA 2021.1, this option is equivalent to Difflib. +In a future release, it may be changed to Diff Match Patch pending positive user testing. +- allow Diff Match Patch: This option causes NVDA to calculate changes to terminal text by character. +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 supported in Windows Console on Windows 10 versions 1607 and later. +Additionally, it may be available in other terminals on earlier Windows releases. +- force Difflib: this option causes NVDA to calculate changes to terminal text by line. +It is identical to NVDA's behaviour in versions 2020.3 and earlier. +- + ==== 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.