44# Copyright (C) 2006-2021 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Babbage B.V., Bill Dengler,
55# Julien Cochuyt
66
7+ from abc import ABCMeta , abstractmethod
78from enum import IntEnum
8- from typing import Callable , TYPE_CHECKING
9+ from typing import Callable , TYPE_CHECKING , Optional
910import weakref
1011import garbageHandler
1112from logHandler import log
3637class CURSOR (IntEnum ):
3738 CARET = 0
3839 REVIEW = 1
40+ TABLE = 2
3941
4042
4143SayAllHandler = None
@@ -95,10 +97,23 @@ def readObjects(self, obj: 'NVDAObjects.NVDAObject'):
9597 self ._getActiveSayAll = weakref .ref (reader )
9698 reader .next ()
9799
98- def readText (self , cursor : CURSOR ):
100+ def readText (
101+ self ,
102+ cursor : CURSOR ,
103+ startPos : Optional [textInfos .TextInfo ] = None ,
104+ nextLineFunc : Optional [Callable [[textInfos .TextInfo ], textInfos .TextInfo ]] = None ,
105+ shouldUpdateCaret : bool = True ,
106+ ) -> None :
99107 self .lastSayAllMode = cursor
100108 try :
101- reader = _TextReader (self , cursor )
109+ if cursor == CURSOR .CARET :
110+ reader = _CaretTextReader (self )
111+ elif cursor == CURSOR .REVIEW :
112+ reader = _ReviewTextReader (self )
113+ elif cursor == CURSOR .TABLE :
114+ reader = _TableTextReader (self , startPos , nextLineFunc , shouldUpdateCaret )
115+ else :
116+ raise RuntimeError (f"Unknown cursor { cursor } " )
102117 except NotImplementedError :
103118 log .debugWarning ("Unable to make reader" , exc_info = True )
104119 return
@@ -145,7 +160,7 @@ def stop(self):
145160 self .walker = None
146161
147162
148- class _TextReader (garbageHandler .TrackedObject ):
163+ class _TextReader (garbageHandler .TrackedObject , metaclass = ABCMeta ):
149164 """Manages continuous reading of text.
150165 This is intended for internal use only.
151166
@@ -167,35 +182,41 @@ class _TextReader(garbageHandler.TrackedObject):
167182 """
168183 MAX_BUFFERED_LINES = 10
169184
170- def __init__ (self , handler : _SayAllHandler , cursor : CURSOR ):
185+ def __init__ (self , handler : _SayAllHandler ):
171186 self .handler = handler
172- self .cursor = cursor
173187 self .trigger = SayAllProfileTrigger ()
174- self .reader = None
175- # Start at the cursor.
176- if cursor == CURSOR .CARET :
177- try :
178- self .reader = api .getCaretObject ().makeTextInfo (textInfos .POSITION_CARET )
179- except (NotImplementedError , RuntimeError ) as e :
180- raise NotImplementedError ("Unable to make TextInfo: " + str (e ))
181- else :
182- self .reader = api .getReviewPosition ()
188+ self .reader = self .getInitialTextInfo ()
183189 # #10899: SayAll profile can't be activated earlier because they may not be anything to read
184190 self .trigger .enter ()
185191 self .speakTextInfoState = SayAllHandler ._makeSpeakTextInfoState (self .reader .obj )
186192 self .numBufferedLines = 0
193+ self .initialIteration = True
187194
188- def nextLine (self ):
189- if not self .reader :
190- log .debug ("no self.reader" )
191- # We were stopped.
192- return
193- if not self .reader .obj :
194- log .debug ("no self.reader.obj" )
195- # The object died, so we should too.
196- self .finish ()
197- return
198- bookmark = self .reader .bookmark
195+ @abstractmethod
196+ def getInitialTextInfo (self ) -> textInfos .TextInfo :
197+ ...
198+
199+ @abstractmethod
200+ def updateCaret (self , updater : textInfos .TextInfo ) -> None :
201+ ...
202+
203+ def shouldReadInitialPosition (self ) -> bool :
204+ return False
205+
206+ def nextLineImpl (self ) -> bool :
207+ """
208+ Advances cursor to the next reading chunk (e.g. paragraph).
209+ @return: C{True} if advanced successfully, C{False} otherwise.
210+ """
211+ # Collapse to the end of this line, ready to read the next.
212+ try :
213+ self .reader .collapse (end = True )
214+ except RuntimeError :
215+ # This occurs in Microsoft Word when the range covers the end of the document.
216+ # without this exception to indicate that further collapsing is not possible,
217+ # say all could enter an infinite loop.
218+
219+ return False
199220 # Expand to the current line.
200221 # We use move end rather than expand
201222 # because the user might start in the middle of a line
@@ -211,8 +232,25 @@ def nextLine(self):
211232 self .handler .speechWithoutPausesInstance .speakWithoutPauses ([cb , EndUtteranceCommand ()])
212233 else :
213234 self .finish ()
214- return
235+ return False
236+ return True
215237
238+ def nextLine (self ):
239+ if not self .reader :
240+ log .debug ("no self.reader" )
241+ # We were stopped.
242+ return
243+ if not self .reader .obj :
244+ log .debug ("no self.reader.obj" )
245+ # The object died, so we should too.
246+ self .finish ()
247+ return
248+ if not self .initialIteration or not self .shouldReadInitialPosition ():
249+ if not self .nextLineImpl ():
250+ self .finish ()
251+ return
252+ self .initialIteration = False
253+ bookmark = self .reader .bookmark
216254 # Copy the speakTextInfoState so that speak callbackCommand
217255 # and its associated callback are using a copy isolated to this specific line.
218256 state = self .speakTextInfoState .copy ()
@@ -244,14 +282,6 @@ def _onLineReached(obj=self.reader.obj, state=state):
244282 # Update the textInfo state ready for when speaking the next line.
245283 self .speakTextInfoState = state .copy ()
246284
247- # Collapse to the end of this line, ready to read the next.
248- try :
249- self .reader .collapse (end = True )
250- except RuntimeError :
251- # This occurs in Microsoft Word when the range covers the end of the document.
252- # without this exception to indicate that further collapsing is not possible, say all could enter an infinite loop.
253- self .finish ()
254- return
255285 if not spoke :
256286 # This line didn't include a natural pause, so nothing was spoken.
257287 self .numBufferedLines += 1
@@ -270,10 +300,7 @@ def lineReached(self, obj, bookmark, state):
270300 # We've just started speaking this line, so move the cursor there.
271301 state .updateObj ()
272302 updater = obj .makeTextInfo (bookmark )
273- if self .cursor == CURSOR .CARET :
274- updater .updateCaret ()
275- if self .cursor != CURSOR .CARET or config .conf ["reviewCursor" ]["followCaret" ]:
276- api .setReviewPosition (updater , isCaret = self .cursor == CURSOR .CARET )
303+ self .updateCaret (updater )
277304 winKernel .SetThreadExecutionState (winKernel .ES_SYSTEM_REQUIRED )
278305 if self .numBufferedLines == 0 :
279306 # This was the last line spoken, so move on.
@@ -315,6 +342,59 @@ def stop(self):
315342 def __del__ (self ):
316343 self .stop ()
317344
345+
346+ class _CaretTextReader (_TextReader ):
347+ def getInitialTextInfo (self ) -> textInfos .TextInfo :
348+ try :
349+ return api .getCaretObject ().makeTextInfo (textInfos .POSITION_CARET )
350+ except (NotImplementedError , RuntimeError ) as e :
351+ raise NotImplementedError ("Unable to make TextInfo: " , e )
352+
353+ def updateCaret (self , updater : textInfos .TextInfo ) -> None :
354+ updater .updateCaret ()
355+ if config .conf ["reviewCursor" ]["followCaret" ]:
356+ api .setReviewPosition (updater , isCaret = True )
357+
358+
359+ class _ReviewTextReader (_TextReader ):
360+ def getInitialTextInfo (self ) -> textInfos .TextInfo :
361+ return api .getReviewPosition ()
362+
363+ def updateCaret (self , updater : textInfos .TextInfo ) -> None :
364+ api .setReviewPosition (updater , isCaret = False )
365+
366+
367+ class _TableTextReader (_CaretTextReader ):
368+ def __init__ (
369+ self ,
370+ handler : _SayAllHandler ,
371+ startPos : Optional [textInfos .TextInfo ] = None ,
372+ nextLineFunc : Optional [Callable [[textInfos .TextInfo ], textInfos .TextInfo ]] = None ,
373+ shouldUpdateCaret : bool = True ,
374+ ):
375+ self .startPos = startPos
376+ self .nextLineFunc = nextLineFunc
377+ self .shouldUpdateCaret = shouldUpdateCaret
378+ super ().__init__ (handler )
379+
380+ def getInitialTextInfo (self ) -> textInfos .TextInfo :
381+ return self .startPos or super ().getInitialTextInfo ()
382+
383+ def nextLineImpl (self ) -> bool :
384+ try :
385+ self .reader = self .nextLineFunc (self .reader )
386+ return True
387+ except StopIteration :
388+ return False
389+
390+ def shouldReadInitialPosition (self ) -> bool :
391+ return True
392+
393+ def updateCaret (self , updater : textInfos .TextInfo ) -> None :
394+ if self .shouldUpdateCaret :
395+ return super ().updateCaret (updater )
396+
397+
318398class SayAllProfileTrigger (config .ProfileTrigger ):
319399 """A configuration profile trigger for when say all is in progress.
320400 """
0 commit comments