1212import os
1313import time
1414import threading
15- import difflib
15+ from diff_match_patch import diff_match_patch
1616import tones
1717import queueHandler
1818import eventHandler
@@ -219,6 +219,10 @@ class LiveText(NVDAObject):
219219 """
220220 #: The time to wait before fetching text after a change event.
221221 STABILIZE_DELAY = 0
222+ #: The maximum amount of time (in seconds) to wait for a diff to complete,
223+ #: 0 for infinity.
224+ #: Longer timeouts increase diff quality at the cost of performance.
225+ DIFF_DEADLINE = 3
222226 # If the text is live, this is definitely content.
223227 presentationType = NVDAObject .presType_content
224228
@@ -228,6 +232,7 @@ def initOverlayClass(self):
228232 self ._event = threading .Event ()
229233 self ._monitorThread = None
230234 self ._keepMonitoring = False
235+ self ._dmp = diff_match_patch ()
231236
232237 def startMonitoring (self ):
233238 """Start monitoring for new text.
@@ -263,15 +268,18 @@ def event_textChange(self):
263268 """
264269 self ._event .set ()
265270
266- def _getTextLines (self ):
267- """Retrieve the text of this object in lines .
271+ def _getText (self ):
272+ """Retrieve the text of this object.
268273 This will be used to determine the new text to speak.
269274 The base implementation uses the L{TextInfo}.
270275 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
276+ @return: The current text ob the object .
277+ @rtype: str
273278 """
274- return list (self .makeTextInfo (textInfos .POSITION_ALL ).getTextInChunks (textInfos .UNIT_LINE ))
279+ if hasattr (self , "_getTextLines" ):
280+ log .warning ("LiveText._getTextLines is deprecated, please override _getText instead." )
281+ return '\n ' .join (self ._getTextLines ())
282+ return self .makeTextInfo (textInfos .POSITION_ALL ).text
275283
276284 def _reportNewLines (self , lines ):
277285 """
@@ -289,10 +297,10 @@ def _reportNewText(self, line):
289297
290298 def _monitor (self ):
291299 try :
292- oldLines = self ._getTextLines ()
300+ oldText = self ._getText ()
293301 except :
294- log .exception ("Error getting initial lines " )
295- oldLines = []
302+ log .exception ("Error getting initial text " )
303+ oldText = ""
296304
297305 while self ._keepMonitoring :
298306 self ._event .wait ()
@@ -307,71 +315,30 @@ def _monitor(self):
307315 self ._event .clear ()
308316
309317 try :
310- newLines = self ._getTextLines ()
318+ newText = self ._getText ()
311319 if config .conf ["presentation" ]["reportDynamicContentChanges" ]:
312- outLines = self ._calculateNewText (newLines , oldLines )
320+ outLines = self ._calculateNewText (newText , oldText )
313321 if len (outLines ) == 1 and len (outLines [0 ].strip ()) == 1 :
314322 # This is only a single character,
315323 # which probably means it is just a typed character,
316324 # so ignore it.
317325 del outLines [0 ]
318326 if outLines :
319327 queueHandler .queueFunction (queueHandler .eventQueue , self ._reportNewLines , outLines )
320- oldLines = newLines
328+ oldText = newText
321329 except :
322- log .exception ("Error getting lines or calculating new text" )
323-
324- def _calculateNewText (self , newLines , oldLines ):
325- outLines = []
326-
327- prevLine = None
328- for line in difflib .ndiff (oldLines , newLines ):
329- if line [0 ] == "?" :
330- # We're never interested in these.
331- continue
332- if line [0 ] != "+" :
333- # We're only interested in new lines.
334- prevLine = line
335- continue
336- text = line [2 :]
337- if not text or text .isspace ():
338- prevLine = line
339- continue
330+ log .exception ("Error getting or calculating new text" )
340331
341- if prevLine and prevLine [0 ] == "-" and len (prevLine ) > 2 :
342- # It's possible that only a few characters have changed in this line.
343- # If so, we want to speak just the changed section, rather than the entire line.
344- prevText = prevLine [2 :]
345- textLen = len (text )
346- prevTextLen = len (prevText )
347- # Find the first character that differs between the two lines.
348- for pos in range (min (textLen , prevTextLen )):
349- if text [pos ] != prevText [pos ]:
350- start = pos
351- break
352- else :
353- # We haven't found a differing character so far and we've hit the end of one of the lines.
354- # This means that the differing text starts here.
355- start = pos + 1
356- # Find the end of the differing text.
357- if textLen != prevTextLen :
358- # The lines are different lengths, so assume the rest of the line changed.
359- end = textLen
360- else :
361- for pos in range (textLen - 1 , start - 1 , - 1 ):
362- if text [pos ] != prevText [pos ]:
363- end = pos + 1
364- break
365-
366- if end - start < 15 :
367- # Less than 15 characters have changed, so only speak the changed chunk.
368- text = text [start :end ]
369-
370- if text and not text .isspace ():
371- outLines .append (text )
372- prevLine = line
332+ def _calculateNewText (self , newText , oldText ):
333+ return [
334+ line
335+ for change , line in self ._dmp .diff_lineMode (
336+ oldText , newText , self .DIFF_DEADLINE
337+ )
338+ if change == self ._dmp .DIFF_INSERT
339+ and not line .isspace ()
340+ ]
373341
374- return outLines
375342
376343class Terminal (LiveText , EditableText ):
377344 """An object which both accepts text input and outputs text which should be reported automatically.
0 commit comments