Skip to content

Commit

Permalink
Re-Introduce Table SayAll commands (#14070)
Browse files Browse the repository at this point in the history
Fixes #13469.
Fixes #13927.

Summary of the issue:
Previous PR #13956 broke sayAll functionality in BookWorm (#13927) and therefore was reverted.
This PR undoes reverting, in other words it reintroduces table sayAll commands. It also contains a minor change that fixes sayAll in BookWorm.

Description of user facing changes
Reintroduces table sayAll commands.

Description of development approach
In my original PR #13956 I did a minor refactoring of SayAll classes, in order to avoid code reduplication and have a nice class inheritance. In order to achieve this I slightly rearranged calls to textInfo. This didn't affect functionality anywhere except for page turn detector in BookWorm.
Reverting to the original order of TextInfo calls at the cost of slightly less elegant code.
  • Loading branch information
mltony authored Aug 30, 2022
1 parent 0b72324 commit 61428bc
Show file tree
Hide file tree
Showing 8 changed files with 567 additions and 159 deletions.
302 changes: 210 additions & 92 deletions source/documentBase.py

Large diffs are not rendered by default.

169 changes: 130 additions & 39 deletions source/speech/sayAll.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
# Copyright (C) 2006-2022 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Babbage B.V., Bill Dengler,
# Julien Cochuyt

from abc import ABCMeta, abstractmethod
from enum import IntEnum
from typing import Callable, TYPE_CHECKING
from typing import Callable, TYPE_CHECKING, Optional
import weakref
import garbageHandler
from logHandler import log
Expand Down Expand Up @@ -36,6 +37,7 @@
class CURSOR(IntEnum):
CARET = 0
REVIEW = 1
TABLE = 2


SayAllHandler = None
Expand Down Expand Up @@ -95,10 +97,23 @@ def readObjects(self, obj: 'NVDAObjects.NVDAObject'):
self._getActiveSayAll = weakref.ref(reader)
reader.next()

def readText(self, cursor: CURSOR):
def readText(
self,
cursor: CURSOR,
startPos: Optional[textInfos.TextInfo] = None,
nextLineFunc: Optional[Callable[[textInfos.TextInfo], textInfos.TextInfo]] = None,
shouldUpdateCaret: bool = True,
) -> None:
self.lastSayAllMode = cursor
try:
reader = _TextReader(self, cursor)
if cursor == CURSOR.CARET:
reader = _CaretTextReader(self)
elif cursor == CURSOR.REVIEW:
reader = _ReviewTextReader(self)
elif cursor == CURSOR.TABLE:
reader = _TableTextReader(self, startPos, nextLineFunc, shouldUpdateCaret)
else:
raise RuntimeError(f"Unknown cursor {cursor}")
except NotImplementedError:
log.debugWarning("Unable to make reader", exc_info=True)
return
Expand Down Expand Up @@ -149,7 +164,7 @@ def stop(self):
self.walker = None


class _TextReader(garbageHandler.TrackedObject):
class _TextReader(garbageHandler.TrackedObject, metaclass=ABCMeta):
"""Manages continuous reading of text.
This is intended for internal use only.
Expand All @@ -171,35 +186,32 @@ class _TextReader(garbageHandler.TrackedObject):
"""
MAX_BUFFERED_LINES = 10

def __init__(self, handler: _SayAllHandler, cursor: CURSOR):
def __init__(self, handler: _SayAllHandler):
self.handler = handler
self.cursor = cursor
self.trigger = SayAllProfileTrigger()
self.reader = None
# Start at the cursor.
if cursor == CURSOR.CARET:
try:
self.reader = api.getCaretObject().makeTextInfo(textInfos.POSITION_CARET)
except (NotImplementedError, RuntimeError) as e:
raise NotImplementedError("Unable to make TextInfo: " + str(e))
else:
self.reader = api.getReviewPosition()
self.reader = self.getInitialTextInfo()
# #10899: SayAll profile can't be activated earlier because they may not be anything to read
self.trigger.enter()
self.speakTextInfoState = SayAllHandler._makeSpeakTextInfoState(self.reader.obj)
self.numBufferedLines = 0
self.initialIteration = True

def nextLine(self):
if not self.reader:
log.debug("no self.reader")
# We were stopped.
return
if not self.reader.obj:
log.debug("no self.reader.obj")
# The object died, so we should too.
self.finish()
return
bookmark = self.reader.bookmark
@abstractmethod
def getInitialTextInfo(self) -> textInfos.TextInfo:
...

@abstractmethod
def updateCaret(self, updater: textInfos.TextInfo) -> None:
...

def shouldReadInitialPosition(self) -> bool:
return False

def nextLineImpl(self) -> bool:
"""
Advances cursor to the next reading chunk (e.g. paragraph).
@return: C{True} if advanced successfully, C{False} otherwise.
"""
# Expand to the current line.
# We use move end rather than expand
# because the user might start in the middle of a line
Expand All @@ -215,8 +227,40 @@ def nextLine(self):
self.handler.speechWithoutPausesInstance.speakWithoutPauses([cb, EndUtteranceCommand()])
else:
self.finish()
return
return False
return True

def collapseLineImpl(self) -> bool:
"""
Collapses to the end of this line, ready to read the next.
@return: C{True} if collapsed successfully, C{False} otherwise.
"""
try:
self.reader.collapse(end=True)
return True
except RuntimeError:
# This occurs in Microsoft Word when the range covers the end of the document.
# without this exception to indicate that further collapsing is not possible,
# say all could enter an infinite loop.
self.finish()
return False

def nextLine(self):
if not self.reader:
log.debug("no self.reader")
# We were stopped.
return
if not self.reader.obj:
log.debug("no self.reader.obj")
# The object died, so we should too.
self.finish()
return
if not self.initialIteration or not self.shouldReadInitialPosition():
if not self.nextLineImpl():
self.finish()
return
self.initialIteration = False
bookmark = self.reader.bookmark
# Copy the speakTextInfoState so that speak callbackCommand
# and its associated callback are using a copy isolated to this specific line.
state = self.speakTextInfoState.copy()
Expand Down Expand Up @@ -248,15 +292,9 @@ def _onLineReached(obj=self.reader.obj, state=state):
# Update the textInfo state ready for when speaking the next line.
self.speakTextInfoState = state.copy()

# Collapse to the end of this line, ready to read the next.
try:
self.reader.collapse(end=True)
except RuntimeError:
# This occurs in Microsoft Word when the range covers the end of the document.
# without this exception to indicate that further collapsing is not possible,
# say all could enter an infinite loop.
self.finish()
if not self.collapseLineImpl():
return

if not spoke:
# This line didn't include a natural pause, so nothing was spoken.
self.numBufferedLines += 1
Expand All @@ -275,10 +313,7 @@ def lineReached(self, obj, bookmark, state):
# We've just started speaking this line, so move the cursor there.
state.updateObj()
updater = obj.makeTextInfo(bookmark)
if self.cursor == CURSOR.CARET:
updater.updateCaret()
if self.cursor != CURSOR.CARET or config.conf["reviewCursor"]["followCaret"]:
api.setReviewPosition(updater, isCaret=self.cursor == CURSOR.CARET)
self.updateCaret(updater)
winKernel.SetThreadExecutionState(winKernel.ES_SYSTEM_REQUIRED)
if self.numBufferedLines == 0:
# This was the last line spoken, so move on.
Expand Down Expand Up @@ -320,6 +355,62 @@ def stop(self):
def __del__(self):
self.stop()


class _CaretTextReader(_TextReader):
def getInitialTextInfo(self) -> textInfos.TextInfo:
try:
return api.getCaretObject().makeTextInfo(textInfos.POSITION_CARET)
except (NotImplementedError, RuntimeError) as e:
raise NotImplementedError("Unable to make TextInfo: ", e)

def updateCaret(self, updater: textInfos.TextInfo) -> None:
updater.updateCaret()
if config.conf["reviewCursor"]["followCaret"]:
api.setReviewPosition(updater, isCaret=True)


class _ReviewTextReader(_TextReader):
def getInitialTextInfo(self) -> textInfos.TextInfo:
return api.getReviewPosition()

def updateCaret(self, updater: textInfos.TextInfo) -> None:
api.setReviewPosition(updater, isCaret=False)


class _TableTextReader(_CaretTextReader):
def __init__(
self,
handler: _SayAllHandler,
startPos: Optional[textInfos.TextInfo] = None,
nextLineFunc: Optional[Callable[[textInfos.TextInfo], textInfos.TextInfo]] = None,
shouldUpdateCaret: bool = True,
):
self.startPos = startPos
self.nextLineFunc = nextLineFunc
self.shouldUpdateCaret = shouldUpdateCaret
super().__init__(handler)

def getInitialTextInfo(self) -> textInfos.TextInfo:
return self.startPos or super().getInitialTextInfo()

def nextLineImpl(self) -> bool:
try:
self.reader = self.nextLineFunc(self.reader)
return True
except StopIteration:
return False

def collapseLineImpl(self) -> bool:
return True

def shouldReadInitialPosition(self) -> bool:
return True

def updateCaret(self, updater: textInfos.TextInfo) -> None:
if self.shouldUpdateCaret:
return super().updateCaret(updater)


class SayAllProfileTrigger(config.ProfileTrigger):
"""A configuration profile trigger for when say all is in progress.
"""
Expand Down
19 changes: 10 additions & 9 deletions source/virtualBuffers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
import treeInterceptorHandler
import watchdog
from abc import abstractmethod
import documentBase


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

def _getNearestTableCell(
self,
tableID,
startPos,
origRow,
origCol,
origRowSpan,
origColSpan,
movement,
axis
):
startPos: textInfos.TextInfo,
cell: documentBase._TableCell,
movement: documentBase._Movement,
axis: documentBase._Axis,
) -> textInfos.TextInfo:
tableID, origRow, origCol, origRowSpan, origColSpan = (
cell.tableID, cell.row, cell.col, cell.rowSpan, cell.colSpan
)
# Determine destination row and column.
destRow = origRow
destCol = origCol
Expand Down
26 changes: 7 additions & 19 deletions source/virtualBuffers/gecko_ia2.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import aria
import config
from NVDAObjects.IAccessible import normalizeIA2TextFormatField, IA2TextTextInfo
import documentBase


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

def _getNearestTableCell(
self,
tableID,
startPos,
origRow,
origCol,
origRowSpan,
origColSpan,
movement,
axis,
):
startPos: textInfos.TextInfo,
cell: documentBase._TableCell,
movement: documentBase._Movement,
axis: documentBase._Axis,
) -> textInfos.TextInfo:
# Skip the VirtualBuffer implementation as the base BrowseMode implementation is good enough for us here.
return super(VirtualBuffer, self)._getNearestTableCell(
tableID,
startPos,
origRow,
origCol,
origRowSpan,
origColSpan,
movement,
axis
)
return super(VirtualBuffer, self)._getNearestTableCell(startPos, cell, movement, axis)

def _get_documentConstantIdentifier(self):
try:
Expand Down
Loading

0 comments on commit 61428bc

Please sign in to comment.