44# Copyright (C) 2006-2022 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
@@ -149,7 +164,7 @@ def stop(self):
149164 self .walker = None
150165
151166
152- class _TextReader (garbageHandler .TrackedObject ):
167+ class _TextReader (garbageHandler .TrackedObject , metaclass = ABCMeta ):
153168 """Manages continuous reading of text.
154169 This is intended for internal use only.
155170
@@ -171,35 +186,32 @@ class _TextReader(garbageHandler.TrackedObject):
171186 """
172187 MAX_BUFFERED_LINES = 10
173188
174- def __init__ (self , handler : _SayAllHandler , cursor : CURSOR ):
189+ def __init__ (self , handler : _SayAllHandler ):
175190 self .handler = handler
176- self .cursor = cursor
177191 self .trigger = SayAllProfileTrigger ()
178- self .reader = None
179- # Start at the cursor.
180- if cursor == CURSOR .CARET :
181- try :
182- self .reader = api .getCaretObject ().makeTextInfo (textInfos .POSITION_CARET )
183- except (NotImplementedError , RuntimeError ) as e :
184- raise NotImplementedError ("Unable to make TextInfo: " + str (e ))
185- else :
186- self .reader = api .getReviewPosition ()
192+ self .reader = self .getInitialTextInfo ()
187193 # #10899: SayAll profile can't be activated earlier because they may not be anything to read
188194 self .trigger .enter ()
189195 self .speakTextInfoState = SayAllHandler ._makeSpeakTextInfoState (self .reader .obj )
190196 self .numBufferedLines = 0
197+ self .initialIteration = True
191198
192- def nextLine (self ):
193- if not self .reader :
194- log .debug ("no self.reader" )
195- # We were stopped.
196- return
197- if not self .reader .obj :
198- log .debug ("no self.reader.obj" )
199- # The object died, so we should too.
200- self .finish ()
201- return
202- bookmark = self .reader .bookmark
199+ @abstractmethod
200+ def getInitialTextInfo (self ) -> textInfos .TextInfo :
201+ ...
202+
203+ @abstractmethod
204+ def updateCaret (self , updater : textInfos .TextInfo ) -> None :
205+ ...
206+
207+ def shouldReadInitialPosition (self ) -> bool :
208+ return False
209+
210+ def nextLineImpl (self ) -> bool :
211+ """
212+ Advances cursor to the next reading chunk (e.g. paragraph).
213+ @return: C{True} if advanced successfully, C{False} otherwise.
214+ """
203215 # Expand to the current line.
204216 # We use move end rather than expand
205217 # because the user might start in the middle of a line
@@ -215,8 +227,40 @@ def nextLine(self):
215227 self .handler .speechWithoutPausesInstance .speakWithoutPauses ([cb , EndUtteranceCommand ()])
216228 else :
217229 self .finish ()
218- return
230+ return False
231+ return True
232+
233+ def collapseLineImpl (self ) -> bool :
234+ """
235+ Collapses to the end of this line, ready to read the next.
236+ @return: C{True} if collapsed successfully, C{False} otherwise.
237+ """
238+ try :
239+ self .reader .collapse (end = True )
240+ return True
241+ except RuntimeError :
242+ # This occurs in Microsoft Word when the range covers the end of the document.
243+ # without this exception to indicate that further collapsing is not possible,
244+ # say all could enter an infinite loop.
245+ self .finish ()
246+ return False
219247
248+ def nextLine (self ):
249+ if not self .reader :
250+ log .debug ("no self.reader" )
251+ # We were stopped.
252+ return
253+ if not self .reader .obj :
254+ log .debug ("no self.reader.obj" )
255+ # The object died, so we should too.
256+ self .finish ()
257+ return
258+ if not self .initialIteration or not self .shouldReadInitialPosition ():
259+ if not self .nextLineImpl ():
260+ self .finish ()
261+ return
262+ self .initialIteration = False
263+ bookmark = self .reader .bookmark
220264 # Copy the speakTextInfoState so that speak callbackCommand
221265 # and its associated callback are using a copy isolated to this specific line.
222266 state = self .speakTextInfoState .copy ()
@@ -248,15 +292,9 @@ def _onLineReached(obj=self.reader.obj, state=state):
248292 # Update the textInfo state ready for when speaking the next line.
249293 self .speakTextInfoState = state .copy ()
250294
251- # Collapse to the end of this line, ready to read the next.
252- try :
253- self .reader .collapse (end = True )
254- except RuntimeError :
255- # This occurs in Microsoft Word when the range covers the end of the document.
256- # without this exception to indicate that further collapsing is not possible,
257- # say all could enter an infinite loop.
258- self .finish ()
295+ if not self .collapseLineImpl ():
259296 return
297+
260298 if not spoke :
261299 # This line didn't include a natural pause, so nothing was spoken.
262300 self .numBufferedLines += 1
@@ -275,10 +313,7 @@ def lineReached(self, obj, bookmark, state):
275313 # We've just started speaking this line, so move the cursor there.
276314 state .updateObj ()
277315 updater = obj .makeTextInfo (bookmark )
278- if self .cursor == CURSOR .CARET :
279- updater .updateCaret ()
280- if self .cursor != CURSOR .CARET or config .conf ["reviewCursor" ]["followCaret" ]:
281- api .setReviewPosition (updater , isCaret = self .cursor == CURSOR .CARET )
316+ self .updateCaret (updater )
282317 winKernel .SetThreadExecutionState (winKernel .ES_SYSTEM_REQUIRED )
283318 if self .numBufferedLines == 0 :
284319 # This was the last line spoken, so move on.
@@ -320,6 +355,62 @@ def stop(self):
320355 def __del__ (self ):
321356 self .stop ()
322357
358+
359+ class _CaretTextReader (_TextReader ):
360+ def getInitialTextInfo (self ) -> textInfos .TextInfo :
361+ try :
362+ return api .getCaretObject ().makeTextInfo (textInfos .POSITION_CARET )
363+ except (NotImplementedError , RuntimeError ) as e :
364+ raise NotImplementedError ("Unable to make TextInfo: " , e )
365+
366+ def updateCaret (self , updater : textInfos .TextInfo ) -> None :
367+ updater .updateCaret ()
368+ if config .conf ["reviewCursor" ]["followCaret" ]:
369+ api .setReviewPosition (updater , isCaret = True )
370+
371+
372+ class _ReviewTextReader (_TextReader ):
373+ def getInitialTextInfo (self ) -> textInfos .TextInfo :
374+ return api .getReviewPosition ()
375+
376+ def updateCaret (self , updater : textInfos .TextInfo ) -> None :
377+ api .setReviewPosition (updater , isCaret = False )
378+
379+
380+ class _TableTextReader (_CaretTextReader ):
381+ def __init__ (
382+ self ,
383+ handler : _SayAllHandler ,
384+ startPos : Optional [textInfos .TextInfo ] = None ,
385+ nextLineFunc : Optional [Callable [[textInfos .TextInfo ], textInfos .TextInfo ]] = None ,
386+ shouldUpdateCaret : bool = True ,
387+ ):
388+ self .startPos = startPos
389+ self .nextLineFunc = nextLineFunc
390+ self .shouldUpdateCaret = shouldUpdateCaret
391+ super ().__init__ (handler )
392+
393+ def getInitialTextInfo (self ) -> textInfos .TextInfo :
394+ return self .startPos or super ().getInitialTextInfo ()
395+
396+ def nextLineImpl (self ) -> bool :
397+ try :
398+ self .reader = self .nextLineFunc (self .reader )
399+ return True
400+ except StopIteration :
401+ return False
402+
403+ def collapseLineImpl (self ) -> bool :
404+ return True
405+
406+ def shouldReadInitialPosition (self ) -> bool :
407+ return True
408+
409+ def updateCaret (self , updater : textInfos .TextInfo ) -> None :
410+ if self .shouldUpdateCaret :
411+ return super ().updateCaret (updater )
412+
413+
323414class SayAllProfileTrigger (config .ProfileTrigger ):
324415 """A configuration profile trigger for when say all is in progress.
325416 """
0 commit comments