Skip to content

Commit 8900133

Browse files
authored
Merge 5b46850 into 42b1d2a
2 parents 42b1d2a + 5b46850 commit 8900133

8 files changed

Lines changed: 557 additions & 160 deletions

File tree

source/documentBase.py

Lines changed: 210 additions & 92 deletions
Large diffs are not rendered by default.

source/speech/sayAll.py

Lines changed: 119 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
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
78
from enum import IntEnum
8-
from typing import Callable, TYPE_CHECKING
9+
from typing import Callable, TYPE_CHECKING, Optional
910
import weakref
1011
import garbageHandler
1112
from logHandler import log
@@ -36,6 +37,7 @@
3637
class CURSOR(IntEnum):
3738
CARET = 0
3839
REVIEW = 1
40+
TABLE = 2
3941

4042

4143
SayAllHandler = 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,15 +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,
253-
# say all could enter an infinite loop.
254-
self.finish()
255-
return
256285
if not spoke:
257286
# This line didn't include a natural pause, so nothing was spoken.
258287
self.numBufferedLines += 1
@@ -271,10 +300,7 @@ def lineReached(self, obj, bookmark, state):
271300
# We've just started speaking this line, so move the cursor there.
272301
state.updateObj()
273302
updater = obj.makeTextInfo(bookmark)
274-
if self.cursor == CURSOR.CARET:
275-
updater.updateCaret()
276-
if self.cursor != CURSOR.CARET or config.conf["reviewCursor"]["followCaret"]:
277-
api.setReviewPosition(updater, isCaret=self.cursor == CURSOR.CARET)
303+
self.updateCaret(updater)
278304
winKernel.SetThreadExecutionState(winKernel.ES_SYSTEM_REQUIRED)
279305
if self.numBufferedLines == 0:
280306
# This was the last line spoken, so move on.
@@ -316,6 +342,59 @@ def stop(self):
316342
def __del__(self):
317343
self.stop()
318344

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+
319398
class SayAllProfileTrigger(config.ProfileTrigger):
320399
"""A configuration profile trigger for when say all is in progress.
321400
"""

source/virtualBuffers/__init__.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
import treeInterceptorHandler
3939
import watchdog
4040
from abc import abstractmethod
41+
import documentBase
42+
4143

4244
VBufStorage_findDirection_forward=0
4345
VBufStorage_findDirection_back=1
@@ -640,15 +642,14 @@ def _iterTableCells(self, tableID, startPos=None, direction="next", row=None, co
640642

641643
def _getNearestTableCell(
642644
self,
643-
tableID,
644-
startPos,
645-
origRow,
646-
origCol,
647-
origRowSpan,
648-
origColSpan,
649-
movement,
650-
axis
651-
):
645+
startPos: textInfos.TextInfo,
646+
cell: documentBase._TableCell,
647+
movement: documentBase._Movement,
648+
axis: documentBase._Axis,
649+
) -> textInfos.TextInfo:
650+
tableID, origRow, origCol, origRowSpan, origColSpan = (
651+
cell.tableID, cell.row, cell.col, cell.rowSpan, cell.colSpan
652+
)
652653
# Determine destination row and column.
653654
destRow = origRow
654655
destCol = origCol

source/virtualBuffers/gecko_ia2.py

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import aria
2424
import config
2525
from NVDAObjects.IAccessible import normalizeIA2TextFormatField, IA2TextTextInfo
26+
import documentBase
2627

2728

2829
def _getNormalizedCurrentAttrs(attrs: textInfos.ControlField) -> typing.Dict[str, typing.Any]:
@@ -548,26 +549,13 @@ def _getTableCellAt(self,tableID,startPos,destRow,destCol):
548549

549550
def _getNearestTableCell(
550551
self,
551-
tableID,
552-
startPos,
553-
origRow,
554-
origCol,
555-
origRowSpan,
556-
origColSpan,
557-
movement,
558-
axis,
559-
):
552+
startPos: textInfos.TextInfo,
553+
cell: documentBase._TableCell,
554+
movement: documentBase._Movement,
555+
axis: documentBase._Axis,
556+
) -> textInfos.TextInfo:
560557
# Skip the VirtualBuffer implementation as the base BrowseMode implementation is good enough for us here.
561-
return super(VirtualBuffer, self)._getNearestTableCell(
562-
tableID,
563-
startPos,
564-
origRow,
565-
origCol,
566-
origRowSpan,
567-
origColSpan,
568-
movement,
569-
axis
570-
)
558+
return super(VirtualBuffer, self)._getNearestTableCell(startPos, cell, movement, axis)
571559

572560
def _get_documentConstantIdentifier(self):
573561
try:

0 commit comments

Comments
 (0)