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.
1211import os
1312import time
1413import threading
15- import difflib
14+ from diff_match_patch import diff_match_patch
1615import tones
1716import queueHandler
1817import eventHandler
@@ -228,6 +227,7 @@ def initOverlayClass(self):
228227 self ._event = threading .Event ()
229228 self ._monitorThread = None
230229 self ._keepMonitoring = False
230+ self ._dmp = diff_match_patch ()
231231
232232 def startMonitoring (self ):
233233 """Start monitoring for new text.
@@ -263,15 +263,18 @@ def event_textChange(self):
263263 """
264264 self ._event .set ()
265265
266- def _getTextLines (self ):
267- """Retrieve the text of this object in lines .
266+ def _getText (self ):
267+ """Retrieve the text of this object.
268268 This will be used to determine the new text to speak.
269269 The base implementation uses the L{TextInfo}.
270270 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
271+ @return: The current text ob the object .
272+ @rtype: str
273273 """
274- return list (self .makeTextInfo (textInfos .POSITION_ALL ).getTextInChunks (textInfos .UNIT_LINE ))
274+ if hasattr (self , "_getTextLines" ):
275+ log .warning ("LiveText._getTextLines is deprecated, please override _getText instead." )
276+ return '\n ' .join (self ._getTextLines ())
277+ return self .makeTextInfo (textInfos .POSITION_ALL ).text
275278
276279 def _reportNewLines (self , lines ):
277280 """
@@ -289,10 +292,10 @@ def _reportNewText(self, line):
289292
290293 def _monitor (self ):
291294 try :
292- oldLines = self ._getTextLines ()
295+ oldText = self ._getText ()
293296 except :
294- log .exception ("Error getting initial lines " )
295- oldLines = []
297+ log .exception ("Error getting initial text " )
298+ oldText = ""
296299
297300 while self ._keepMonitoring :
298301 self ._event .wait ()
@@ -307,59 +310,61 @@ def _monitor(self):
307310 self ._event .clear ()
308311
309312 try :
310- newLines = self ._getTextLines ()
313+ newText = self ._getText ()
311314 if config .conf ["presentation" ]["reportDynamicContentChanges" ]:
312- outLines = self ._calculateNewText (newLines , oldLines )
315+ outLines = self ._calculateNewText (newText , oldText )
313316 if len (outLines ) == 1 and len (outLines [0 ].strip ()) == 1 :
314317 # This is only a single character,
315318 # which probably means it is just a typed character,
316319 # so ignore it.
317320 del outLines [0 ]
318321 if outLines :
319322 queueHandler .queueFunction (queueHandler .eventQueue , self ._reportNewLines , outLines )
320- oldLines = newLines
323+ oldText = newText
321324 except :
322- log .exception ("Error getting lines or calculating new text" )
325+ log .exception ("Error getting or calculating new text" )
323326
324- def _calculateNewText (self , newLines , oldLines ):
325- outLines = []
327+ def _calculateNewText (self , newText , oldText ):
328+ (oldText , newText , linearray ) = self ._dmp .diff_linesToChars (oldText , newText )
329+ diffs = self ._dmp .diff_main (oldText , newText , checklines = False )
330+ # Convert the diff back to original text.
331+ self ._dmp .diff_charsToLines (diffs , linearray )
332+ # Eliminate freak matches (e.g. blank lines)
333+ self ._dmp .diff_cleanupSemantic (diffs )
326334
335+ outLines = []
327336 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 ] != "+" :
337+ prevChange = None
338+ for change , text in diffs :
339+ if change != self ._dmp .DIFF_INSERT :
333340 # We're only interested in new lines.
334- prevLine = line
341+ prevChange , prevLine = change , text
335342 continue
336- text = line [2 :]
337343 if not text or text .isspace ():
338- prevLine = line
344+ prevChange , prevLine = change , text
339345 continue
340346
341- if prevLine and prevLine [ 0 ] == "-" and len ( prevLine ) > 2 :
347+ if prevLine and prevChange == self . _dmp . DIFF_DELETE and not prevLine . isspace () :
342348 # It's possible that only a few characters have changed in this line.
343349 # If so, we want to speak just the changed section, rather than the entire line.
344- prevText = prevLine [2 :]
345350 textLen = len (text )
346- prevTextLen = len (prevText )
351+ prevLineLen = len (prevLine )
347352 # Find the first character that differs between the two lines.
348- for pos in range (min (textLen , prevTextLen )):
349- if text [pos ] != prevText [pos ]:
353+ for pos in range (min (textLen , prevLineLen )):
354+ if text [pos ] != prevLine [pos ]:
350355 start = pos
351356 break
352357 else :
353358 # We haven't found a differing character so far and we've hit the end of one of the lines.
354359 # This means that the differing text starts here.
355360 start = pos + 1
356361 # Find the end of the differing text.
357- if textLen != prevTextLen :
362+ if textLen != prevLineLen :
358363 # The lines are different lengths, so assume the rest of the line changed.
359364 end = textLen
360365 else :
361366 for pos in range (textLen - 1 , start - 1 , - 1 ):
362- if text [pos ] != prevText [pos ]:
367+ if text [pos ] != prevLine [pos ]:
363368 end = pos + 1
364369 break
365370
@@ -369,9 +374,10 @@ def _calculateNewText(self, newLines, oldLines):
369374
370375 if text and not text .isspace ():
371376 outLines .append (text )
372- prevLine = line
377+ prevChange , prevLine = change , text
378+
379+ return [line for outLine in outLines for line in outLine .splitlines () if line and not line .isspace ()]
373380
374- return outLines
375381
376382class Terminal (LiveText , EditableText ):
377383 """An object which both accepts text input and outputs text which should be reported automatically.
0 commit comments