Skip to content

Commit f967268

Browse files
authored
Merge 8a71324 into 9172c09
2 parents 9172c09 + 8a71324 commit f967268

8 files changed

Lines changed: 570 additions & 141 deletions

File tree

source/documentBase.py

Lines changed: 216 additions & 94 deletions
Large diffs are not rendered by default.

source/speech/sayAll.py

Lines changed: 119 additions & 39 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,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+
318398
class SayAllProfileTrigger(config.ProfileTrigger):
319399
"""A configuration profile trigger for when say all is in progress.
320400
"""

source/virtualBuffers/__init__.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
# -*- coding: UTF-8 -*-
2-
#virtualBuffers/__init__.py
3-
#A part of NonVisual Desktop Access (NVDA)
4-
#This file is covered by the GNU General Public License.
5-
#See the file COPYING for more details.
6-
#Copyright (C) 2007-2017 NV Access Limited, Peter Vágner
2+
# A part of NonVisual Desktop Access (NVDA)
3+
# This file is covered by the GNU General Public License.
4+
# See the file COPYING for more details.
5+
# Copyright (C) 2007-2022 NV Access Limited, Peter Vágner
76

87
import time
98
import threading
@@ -39,6 +38,8 @@
3938
import treeInterceptorHandler
4039
import watchdog
4140
from abc import abstractmethod
41+
import documentBase
42+
4243

4344
VBufStorage_findDirection_forward=0
4445
VBufStorage_findDirection_back=1
@@ -639,7 +640,16 @@ def _iterTableCells(self, tableID, startPos=None, direction="next", row=None, co
639640
for item in results:
640641
yield item.textInfo
641642

642-
def _getNearestTableCell(self, tableID, startPos, origRow, origCol, origRowSpan, origColSpan, movement, axis):
643+
def _getNearestTableCell(
644+
self,
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.collSpan
652+
)
643653
# Determine destination row and column.
644654
destRow = origRow
645655
destCol = origCol

source/virtualBuffers/gecko_ia2.py

Lines changed: 9 additions & 2 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]:
@@ -546,9 +547,15 @@ def _getTableCellAt(self,tableID,startPos,destRow,destCol):
546547
except (COMError, RuntimeError):
547548
raise LookupError
548549

549-
def _getNearestTableCell(self, tableID, startPos, origRow, origCol, origRowSpan, origColSpan, movement, axis):
550+
def _getNearestTableCell(
551+
self,
552+
startPos: textInfos.TextInfo,
553+
cell: documentBase._TableCell,
554+
movement: documentBase._Movement,
555+
axis: documentBase._Axis,
556+
) -> textInfos.TextInfo:
550557
# Skip the VirtualBuffer implementation as the base BrowseMode implementation is good enough for us here.
551-
return super(VirtualBuffer,self)._getNearestTableCell(tableID, startPos, origRow, origCol, origRowSpan, origColSpan, movement, axis)
558+
return super(VirtualBuffer, self)._getNearestTableCell(startPos, cell, movement, axis)
552559

553560
def _get_documentConstantIdentifier(self):
554561
try:

0 commit comments

Comments
 (0)