Skip to content

Commit 1e65d27

Browse files
authored
Merge 59cfbb5 into 37c7a29
2 parents 37c7a29 + 59cfbb5 commit 1e65d27

7 files changed

Lines changed: 146 additions & 34 deletions

File tree

source/NVDAObjects/UIA/winConsoleUIA.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# NVDAObjects/UIA/winConsoleUIA.py
21
# A part of NonVisual Desktop Access (NVDA)
32
# This file is covered by the GNU General Public License.
43
# See the file COPYING for more details.
@@ -366,13 +365,6 @@ def _get_TextInfo(self):
366365
movement."""
367366
return consoleUIATextInfo if self.is21H1Plus else consoleUIATextInfoPre21H1
368367

369-
def _getTextLines(self):
370-
# This override of _getTextLines takes advantage of the fact that
371-
# the console text contains linefeeds for every line
372-
# Thus a simple string splitlines is much faster than splitting by unit line.
373-
ti = self.makeTextInfo(textInfos.POSITION_ALL)
374-
text = ti.text or ""
375-
return text.splitlines()
376368

377369
def findExtraOverlayClasses(obj, clsList):
378370
if obj.UIAElement.cachedAutomationId == "Text Area":

source/NVDAObjects/behaviors.py

Lines changed: 94 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# -*- coding: UTF-8 -*-
2-
# NVDAObjects/behaviors.py
32
# A part of NonVisual Desktop Access (NVDA)
43
# This file is covered by the GNU General Public License.
54
# See the file COPYING for more details.
@@ -10,9 +9,11 @@
109
"""
1110

1211
import os
12+
import sys
1313
import time
1414
import threading
15-
import difflib
15+
import struct
16+
import subprocess
1617
import tones
1718
import queueHandler
1819
import eventHandler
@@ -29,6 +30,7 @@
2930
import ui
3031
import braille
3132
import nvwave
33+
from typing import List
3234

3335
class ProgressBar(NVDAObject):
3436

@@ -219,6 +221,9 @@ class LiveText(NVDAObject):
219221
"""
220222
#: The time to wait before fetching text after a change event.
221223
STABILIZE_DELAY = 0
224+
#: Whether this object supports Diff-Match-Patch character diffing.
225+
#: Set to False to use line diffing.
226+
_supportsDMP = True
222227
# If the text is live, this is definitely content.
223228
presentationType = NVDAObject.presType_content
224229

@@ -228,6 +233,7 @@ def initOverlayClass(self):
228233
self._event = threading.Event()
229234
self._monitorThread = None
230235
self._keepMonitoring = False
236+
self._dmp = None
231237

232238
def startMonitoring(self):
233239
"""Start monitoring for new text.
@@ -263,15 +269,19 @@ def event_textChange(self):
263269
"""
264270
self._event.set()
265271

266-
def _getTextLines(self):
267-
"""Retrieve the text of this object in lines.
272+
def _get_shouldUseDMP(self):
273+
return self._supportsDMP and config.conf["terminals"]["useDMPWhenAvailable"]
274+
275+
def _getText(self) -> str:
276+
"""Retrieve the text of this object.
268277
This will be used to determine the new text to speak.
269278
The base implementation uses the L{TextInfo}.
270279
However, subclasses should override this if there is a better way to retrieve the text.
271-
@return: The current lines of text.
272-
@rtype: list of str
273280
"""
274-
return list(self.makeTextInfo(textInfos.POSITION_ALL).getTextInChunks(textInfos.UNIT_LINE))
281+
if hasattr(self, "_getTextLines"):
282+
log.warning("LiveText._getTextLines is deprecated, please override _getText instead.")
283+
return '\n'.join(self._getTextLines())
284+
return self.makeTextInfo(textInfos.POSITION_ALL).text
275285

276286
def _reportNewLines(self, lines):
277287
"""
@@ -287,12 +297,30 @@ def _reportNewText(self, line):
287297
"""
288298
speech.speakText(line)
289299

300+
def _initializeDMP(self):
301+
self._dmp = subprocess.Popen(
302+
(sys.executable, "nvda_dmp.py"),
303+
bufsize=0,
304+
stdin=subprocess.PIPE,
305+
stdout=subprocess.PIPE
306+
)
307+
308+
def _terminateDMP(self):
309+
self._dmp.stdin.write(struct.pack("=II", 0, 0))
310+
self._dmp = None
311+
290312
def _monitor(self):
313+
if self.shouldUseDMP:
314+
try:
315+
self._initializeDMP()
316+
except Exception:
317+
log.exception("Error initializing DMP, falling back to difflib")
318+
self._supportsDmp = False
291319
try:
292-
oldLines = self._getTextLines()
320+
oldText = self._getText()
293321
except:
294-
log.exception("Error getting initial lines")
295-
oldLines = []
322+
log.exception("Error getting initial text")
323+
oldText = ""
296324

297325
while self._keepMonitoring:
298326
self._event.wait()
@@ -307,25 +335,65 @@ def _monitor(self):
307335
self._event.clear()
308336

309337
try:
310-
newLines = self._getTextLines()
338+
newText = self._getText()
311339
if config.conf["presentation"]["reportDynamicContentChanges"]:
312-
outLines = self._calculateNewText(newLines, oldLines)
340+
outLines = self._calculateNewText(newText, oldText)
313341
if len(outLines) == 1 and len(outLines[0].strip()) == 1:
314342
# This is only a single character,
315343
# which probably means it is just a typed character,
316344
# so ignore it.
317345
del outLines[0]
318346
if outLines:
319347
queueHandler.queueFunction(queueHandler.eventQueue, self._reportNewLines, outLines)
320-
oldLines = newLines
348+
oldText = newText
321349
except:
322-
log.exception("Error getting lines or calculating new text")
350+
log.exception("Error getting or calculating new text")
351+
352+
if self.shouldUseDMP:
353+
try:
354+
self._terminateDMP()
355+
except Exception:
356+
log.exception("Error stopping DMP")
323357

324-
def _calculateNewText(self, newLines, oldLines):
358+
def _calculateNewText_dmp(self, newText: str, oldText: str) -> List[str]:
359+
try:
360+
if not newText and not oldText:
361+
# Return an empty list here to avoid exiting
362+
# nvda_dmp uses two zero-length texts as a sentinal value
363+
return []
364+
old = oldText.encode("utf-8")
365+
new = newText.encode("utf-8")
366+
tl = struct.pack("=II", len(old), len(new))
367+
self._dmp.stdin.write(tl)
368+
self._dmp.stdin.write(old)
369+
self._dmp.stdin.write(new)
370+
buf = b""
371+
sizeb = b""
372+
SIZELEN = 4
373+
while len(sizeb) < SIZELEN:
374+
sizeb = self._dmp.stdout.read(SIZELEN - len(sizeb))
375+
if sizeb is None:
376+
sizeb = b""
377+
(size,) = struct.unpack("=I", sizeb)
378+
while len(buf) < size:
379+
buf += self._dmp.stdout.read(size - len(buf))
380+
return [
381+
line
382+
for line in buf.decode("utf-8").splitlines()
383+
if line and not line.isspace()
384+
]
385+
except Exception:
386+
log.exception("Exception in DMP, falling back to difflib")
387+
self._supportsDMP = False
388+
return self._calculateNewText_difflib(newText, oldText)
389+
390+
def _calculateNewText_difflib(self, newLines: List[str], oldLines: List[str]) -> List[str]:
325391
outLines = []
326392

327393
prevLine = None
328-
for line in difflib.ndiff(oldLines, newLines):
394+
from difflib import ndiff
395+
396+
for line in ndiff(oldLines, newLines):
329397
if line[0] == "?":
330398
# We're never interested in these.
331399
continue
@@ -373,6 +441,16 @@ def _calculateNewText(self, newLines, oldLines):
373441

374442
return outLines
375443

444+
def _calculateNewText(self, newText: str, oldText: str) -> List[str]:
445+
return (
446+
self._calculateNewText_dmp(newText, oldText)
447+
if self.shouldUseDMP
448+
else self._calculateNewText_difflib(
449+
newText.splitlines(), oldText.splitlines()
450+
)
451+
)
452+
453+
376454
class Terminal(LiveText, EditableText):
377455
"""An object which both accepts text input and outputs text which should be reported automatically.
378456
This is an L{EditableText} object,

source/NVDAObjects/window/winConsole.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
#NVDAObjects/WinConsole.py
2-
#A part of NonVisual Desktop Access (NVDA)
3-
#This file is covered by the GNU General Public License.
4-
#See the file COPYING for more details.
5-
#Copyright (C) 2007-2019 NV Access Limited, Bill Dengler
1+
# A part of NonVisual Desktop Access (NVDA)
2+
# This file is covered by the GNU General Public License.
3+
# See the file COPYING for more details.
4+
# Copyright (C) 2007-2020 NV Access Limited, Bill Dengler
65

76
import winConsoleHandler
87
from . import Window
@@ -21,10 +20,14 @@ class WinConsole(Terminal, EditableTextWithoutAutoSelectDetection, Window):
2120
STABILIZE_DELAY = 0.03
2221

2322
def initOverlayClass(self):
24-
# Legacy consoles take quite a while to send textChange events.
25-
# This significantly impacts typing performance, so don't queue chars.
2623
if isinstance(self, KeyboardHandlerBasedTypedCharSupport):
24+
# Legacy consoles take quite a while to send textChange events.
25+
# This significantly impacts typing performance, so don't queue chars.
2726
self._supportsTextChange = False
27+
else:
28+
# Use line diffing to report changes in the middle of lines
29+
# in non-enhanced legacy consoles.
30+
self._supportsDMP = False
2831

2932
def _get_windowThreadID(self):
3033
# #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):
6972
def event_nameChange(self):
7073
pass
7174

72-
def _getTextLines(self):
73-
return winConsoleHandler.getConsoleVisibleLines()
75+
def _getText(self):
76+
return '\n'.join(winConsoleHandler.getConsoleVisibleLines())
7477

7578
def script_caret_backspaceCharacter(self, gesture):
7679
super(WinConsole, self).script_caret_backspaceCharacter(gesture)

source/config/configSpec.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@
219219
[terminals]
220220
speakPasswords = boolean(default=false)
221221
keyboardSupportInLegacy = boolean(default=True)
222+
useDMPWhenAvailable = boolean(default=True)
222223
223224
[update]
224225
autoCheck = boolean(default=true)

source/gui/settingsDialogs.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2527,6 +2527,14 @@ def __init__(self, parent):
25272527
self.keyboardSupportInLegacyCheckBox.defaultValue = self._getDefaultValue(["terminals", "keyboardSupportInLegacy"])
25282528
self.keyboardSupportInLegacyCheckBox.Enable(winVersion.isWin10(1607))
25292529

2530+
# Translators: This is the label for a checkbox in the
2531+
# Advanced settings panel.
2532+
label = _("Detect changes by c&haracter when available")
2533+
self.useDMPWhenAvailableCheckBox = terminalsGroup.addItem(wx.CheckBox(self, label=label))
2534+
self.useDMPWhenAvailableCheckBox.SetValue(config.conf["terminals"]["useDMPWhenAvailable"])
2535+
self.useDMPWhenAvailableCheckBox.defaultValue = self._getDefaultValue(["terminals", "useDMPWhenAvailable"])
2536+
self.useDMPWhenAvailableCheckBox.Enable(winVersion.isWin10(1607))
2537+
25302538
# Translators: This is the label for a group of advanced options in the
25312539
# Advanced settings panel
25322540
label = _("Speech")
@@ -2644,6 +2652,7 @@ def haveConfigDefaultsBeenRestored(self):
26442652
and self.winConsoleSpeakPasswordsCheckBox.IsChecked() == self.winConsoleSpeakPasswordsCheckBox.defaultValue
26452653
and self.cancelExpiredFocusSpeechCombo.GetSelection() == self.cancelExpiredFocusSpeechCombo.defaultValue
26462654
and self.keyboardSupportInLegacyCheckBox.IsChecked() == self.keyboardSupportInLegacyCheckBox.defaultValue
2655+
and self.useDMPWhenAvailableCheckBox.IsChecked() == self.useDMPWhenAvailableCheckBox.defaultValue
26472656
and self.caretMoveTimeoutSpinControl.GetValue() == self.caretMoveTimeoutSpinControl.defaultValue
26482657
and set(self.logCategoriesList.CheckedItems) == set(self.logCategoriesList.defaultCheckedItems)
26492658
and True # reduce noise in diff when the list is extended.
@@ -2657,6 +2666,7 @@ def restoreToDefaults(self):
26572666
self.winConsoleSpeakPasswordsCheckBox.SetValue(self.winConsoleSpeakPasswordsCheckBox.defaultValue)
26582667
self.cancelExpiredFocusSpeechCombo.SetSelection(self.cancelExpiredFocusSpeechCombo.defaultValue)
26592668
self.keyboardSupportInLegacyCheckBox.SetValue(self.keyboardSupportInLegacyCheckBox.defaultValue)
2669+
self.useDMPWhenAvailableCheckBox.SetValue(self.useDMPWhenAvailableCheckBox.defaultValue)
26602670
self.caretMoveTimeoutSpinControl.SetValue(self.caretMoveTimeoutSpinControl.defaultValue)
26612671
self.logCategoriesList.CheckedItems = self.logCategoriesList.defaultCheckedItems
26622672
self._defaultsRestored = True
@@ -2673,6 +2683,7 @@ def onSave(self):
26732683
config.conf["terminals"]["speakPasswords"] = self.winConsoleSpeakPasswordsCheckBox.IsChecked()
26742684
config.conf["featureFlag"]["cancelExpiredFocusSpeech"] = self.cancelExpiredFocusSpeechCombo.GetSelection()
26752685
config.conf["terminals"]["keyboardSupportInLegacy"]=self.keyboardSupportInLegacyCheckBox.IsChecked()
2686+
config.conf["terminals"]["useDMPWhenAvailable"] = self.useDMPWhenAvailableCheckBox.IsChecked()
26762687
config.conf["editableText"]["caretMoveTimeoutMs"]=self.caretMoveTimeoutSpinControl.GetValue()
26772688
for index,key in enumerate(self.logCategories):
26782689
config.conf['debugLog'][key]=self.logCategoriesList.IsChecked(index)

source/nvda_dmp.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import struct
2+
import sys
3+
4+
from diff_match_patch import diff
5+
6+
7+
if __name__ == "__main__":
8+
while True:
9+
oldLen, newLen = struct.unpack("=II", sys.stdin.buffer.read(8))
10+
if not oldLen and not newLen:
11+
break
12+
oldText = sys.stdin.buffer.read(oldLen).decode("utf-8")
13+
newText = sys.stdin.buffer.read(newLen).decode("utf-8")
14+
res = ""
15+
for op, text in diff(oldText, newText, counts_only=False):
16+
if (op == "=" and text.isspace()) or op == "+":
17+
res += text
18+
sys.stdout.buffer.write(struct.pack("=I", len(res)))
19+
sys.stdout.buffer.write(res.encode("utf-8"))
20+
sys.stdin.flush()
21+
sys.stdout.flush()

user_docs/en/userGuide.t2t

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1835,10 +1835,16 @@ This setting controls whether characters are spoken by [speak typed characters #
18351835
==== Use the new typed character support in Windows Console when available ====[AdvancedSettingsKeyboardSupportInLegacy]
18361836
This option enables an alternative method for detecting typed characters in Windows command consoles.
18371837
While it improves performance and prevents some console output from being spelled out, it may be incompatible with some terminal programs.
1838-
This feature is available and enabled by default on Windows 10 versions 1607 and later when UI Automation is unavailable or disabled.
1838+
This feature is available and enabled by default on Windows 10 versions 1607and later when UI Automation is unavailable or disabled.
18391839
Warning: with this option enabled, typed characters that do not appear onscreen, such as passwords, will not be suppressed.
18401840
In untrusted environments, you may temporarily disable [speak typed characters #KeyboardSettingsSpeakTypedCharacters] and [speak typed words #KeyboardSettingsSpeakTypedWords] when entering passwords.
18411841

1842+
==== Detect changes by character when available ====[AdvancedSettingsUseDMPWhenAvailable]
1843+
This option enables an alternative method for detecting output changes in terminals.
1844+
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.
1845+
However, it may be incompatible with some applications.
1846+
This feature is available and enabled by default on Windows 10 versions 1607 and later.
1847+
18421848
==== Attempt to cancel speech for expired focus events ====[CancelExpiredFocusSpeech]
18431849
This option enables behaviour which attempts to cancel speech for expired focus events.
18441850
In particular moving quickly through messages in Gmail with Chrome can cause NVDA to speak outdated information.

0 commit comments

Comments
 (0)