Skip to content

Commit 23b7076

Browse files
authored
Merge db74dc6 into 7b5cd2d
2 parents 7b5cd2d + db74dc6 commit 23b7076

5 files changed

Lines changed: 125 additions & 22 deletions

File tree

source/config/configSpec.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
outputDevice = string(default=default)
3838
autoLanguageSwitching = boolean(default=true)
3939
autoDialectSwitching = boolean(default=false)
40+
increaseSayAllCaretUpdates = boolean(default=false)
4041
4142
[[__many__]]
4243
capPitchChange = integer(default=30,min=-100,max=100)

source/gui/settingsDialogs.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,6 +1251,12 @@ def makeSettings(self, settingsSizer):
12511251
self.useSpellingFunctionalityCheckBox = settingsSizerHelper.addItem(wx.CheckBox(self, label = useSpellingFunctionalityText))
12521252
self.useSpellingFunctionalityCheckBox.SetValue(config.conf["speech"][self.driver.name]["useSpellingFunctionality"])
12531253

1254+
# Translators: This is the label for a checkbox in the
1255+
# voice settings panel.
1256+
increaseSayAllCaretUpdatesText = _("Increase caret updates during say all")
1257+
self.increaseSayAllCaretUpdatesCheckBox = settingsSizerHelper.addItem(wx.CheckBox(self, label = increaseSayAllCaretUpdatesText))
1258+
self.increaseSayAllCaretUpdatesCheckBox.Value = config.conf["speech"]["increaseSayAllCaretUpdates"]
1259+
12541260
def onSave(self):
12551261
DriverSettingsMixin.onSave(self)
12561262

@@ -1267,6 +1273,7 @@ def onSave(self):
12671273
config.conf["speech"][self.driver.name]["sayCapForCapitals"]=self.sayCapForCapsCheckBox.IsChecked()
12681274
config.conf["speech"][self.driver.name]["beepForCapitals"]=self.beepForCapsCheckBox.IsChecked()
12691275
config.conf["speech"][self.driver.name]["useSpellingFunctionality"]=self.useSpellingFunctionalityCheckBox.IsChecked()
1276+
config.conf["speech"]["increaseSayAllCaretUpdates"] = self.increaseSayAllCaretUpdatesCheckBox.IsChecked()
12701277

12711278
class KeyboardSettingsPanel(SettingsPanel):
12721279
# Translators: This is the label for the keyboard settings panel.

source/sayAllHandler.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
# sayAllHandler.py
12
# A part of NonVisual Desktop Access (NVDA)
2-
# Copyright (C) 2006-2017 NV Access Limited
3+
# Copyright (C) 2006-2019 NV Access Limited, Babbage B.V.
34
# This file may be used under the terms of the GNU General Public License, version 2 or later.
45
# For more details see: https://www.gnu.org/licenses/gpl-2.0.html
56

@@ -12,6 +13,8 @@
1213
import api
1314
import textInfos
1415
import queueHandler
16+
from functools import partial
17+
import time
1518

1619
CURSOR_CARET = 0
1720
CURSOR_REVIEW = 1
@@ -99,6 +102,8 @@ class _TextReader(object):
99102
11. If there is another page, L{turnPage} calls L{nextLine}.
100103
"""
101104
MAX_BUFFERED_LINES = 10
105+
_lastEndOfWhiteSpaceReachedTime = 0
106+
MIN_TIME_BETWEEN_WHITE_SPACE_UPDATES = 0.5
102107

103108
def __init__(self, cursor):
104109
self.cursor = cursor
@@ -142,9 +147,19 @@ def nextLine(self):
142147
return
143148
# Call lineReached when we start speaking this line.
144149
# lineReached will move the cursor and trigger reading of the next line.
145-
cb = speech.CallbackCommand(lambda obj=self.reader.obj, state=self.speakTextInfoState.copy(): self.lineReached(obj,bookmark, state))
150+
cb = speech.CallbackCommand(partial(
151+
self.lineReached,
152+
obj=self.reader.obj,
153+
bookmark=bookmark,
154+
state=self.speakTextInfoState.copy()
155+
))
146156
spoke = speech.speakTextInfo(self.reader, unit=textInfos.UNIT_READINGCHUNK,
147157
reason=controlTypes.REASON_SAYALL, _prefixSpeechCommand=cb,
158+
_whiteSpaceReachedCallback=(
159+
self.endOfWhiteSpaceReached
160+
if config.conf["speech"]["increaseSayAllCaretUpdates"]
161+
else None
162+
),
148163
useCache=self.speakTextInfoState)
149164
# Collapse to the end of this line, ready to read the next.
150165
try:
@@ -168,14 +183,24 @@ def nextLine(self):
168183
# The first buffered line has now started speaking.
169184
self.numBufferedLines -= 1
170185

171-
def lineReached(self, obj, bookmark, state):
172-
# We've just started speaking this line, so move the cursor there.
173-
state.updateObj()
186+
def _bookmarkReached(self, obj, bookmark):
174187
updater = obj.makeTextInfo(bookmark)
175188
if self.cursor == CURSOR_CARET:
176-
updater.updateCaret()
189+
obj.selection = updater
177190
if self.cursor != CURSOR_CARET or config.conf["reviewCursor"]["followCaret"]:
178191
api.setReviewPosition(updater, isCaret=self.cursor==CURSOR_CARET)
192+
193+
def endOfWhiteSpaceReached(self, bookmark):
194+
curTime = time.time()
195+
if (self._lastEndOfWhiteSpaceReachedTime + self.MIN_TIME_BETWEEN_WHITE_SPACE_UPDATES) > curTime:
196+
return
197+
self._lastEndOfWhiteSpaceReachedTime = curTime
198+
self._bookmarkReached(self.reader.obj, bookmark)
199+
200+
def lineReached(self, obj, bookmark, state):
201+
# We've just started speaking this line, so move the cursor there.
202+
state.updateObj()
203+
self._bookmarkReached(obj, bookmark)
179204
if self.numBufferedLines == 0:
180205
# This was the last line spoken, so move on.
181206
self.nextLine()

source/speech/__init__.py

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
import characterProcessing
3131
import languageHandler
3232
from .commands import *
33+
from typing import Optional, Callable, Any
34+
from functools import partial
35+
from types import GeneratorType
3336

3437
speechMode_off=0
3538
speechMode_beeps=1
@@ -766,8 +769,25 @@ def _speakTextInfo_addMath(speechSequence, info, field):
766769
except (NotImplementedError, LookupError):
767770
return
768771

769-
def speakTextInfo(info, useCache=True, formatConfig=None, unit=None, reason=controlTypes.REASON_QUERY, _prefixSpeechCommand=None, onlyInitialFields=False, suppressBlanks=False,priority=None):
772+
re_white_space = re.compile(r"(\s+|$)", re.DOTALL)
773+
774+
class _TextCommand(str):
775+
"""str subclass to distinguish normal text from field text when processing text info speech."""
776+
777+
def speakTextInfo(
778+
info,
779+
useCache=True,
780+
formatConfig=None,
781+
unit=None,
782+
reason=controlTypes.REASON_QUERY,
783+
_prefixSpeechCommand=None,
784+
_whiteSpaceReachedCallback: Optional[Callable[[Any], None]] = None,
785+
onlyInitialFields=False,
786+
suppressBlanks=False,
787+
priority=None
788+
):
770789
onlyCache=reason==controlTypes.REASON_ONLYCACHE
790+
processWhiteSpace: bool = _whiteSpaceReachedCallback is not None
771791
if isinstance(useCache,SpeakTextInfoState):
772792
speakTextInfoState=useCache
773793
elif useCache:
@@ -943,9 +963,9 @@ def speakTextInfo(info, useCache=True, formatConfig=None, unit=None, reason=cont
943963
indentationDone=True
944964
if command:
945965
if inTextChunk:
946-
relativeSpeechSequence[-1]+=command
966+
relativeSpeechSequence[-1] = _TextCommand(relativeSpeechSequence[-1] + command)
947967
else:
948-
relativeSpeechSequence.append(command)
968+
relativeSpeechSequence.append(_TextCommand(command))
949969
inTextChunk=True
950970
elif isinstance(command,textInfos.FieldCommand):
951971
newLanguage=None
@@ -1007,16 +1027,44 @@ def speakTextInfo(info, useCache=True, formatConfig=None, unit=None, reason=cont
10071027
else:
10081028
speechSequence.append(indentationSpeech)
10091029
if speakTextInfoState: speakTextInfoState.indentationCache=allIndentation
1010-
# Don't add this text if it is blank.
1011-
relativeBlank=True
1012-
for x in relativeSpeechSequence:
1013-
if isinstance(x,str) and not isBlank(x):
1014-
relativeBlank=False
1015-
break
1016-
if not relativeBlank:
1017-
speechSequence.extend(relativeSpeechSequence)
1030+
# only add this text if it is not blank.
1031+
notBlank = any(
1032+
not isBlank(x) for x in relativeSpeechSequence
1033+
if isinstance(x,str)
1034+
)
1035+
if notBlank:
10181036
isTextBlank=False
1019-
1037+
if not processWhiteSpace:
1038+
speechSequence.extend(relativeSpeechSequence)
1039+
else:
1040+
# Add appropriate white space bookmarks
1041+
whiteSpaceTracker = info.copy()
1042+
whiteSpaceTracker.collapse()
1043+
for index, command in list(enumerate(relativeSpeechSequence)):
1044+
if not isinstance(command, _TextCommand):
1045+
continue
1046+
curCommandSequence = []
1047+
endOfWhiteSpaceIndexes = [m.end() for m in re_white_space.finditer(command)]
1048+
start = 0
1049+
for end in endOfWhiteSpaceIndexes:
1050+
text = command[start:end]
1051+
curCommandSequence.append(text)
1052+
whiteSpaceTracker.move(textInfos.UNIT_CHARACTER, len(text))
1053+
if whiteSpaceTracker.compareEndPoints(info, "startToEnd") > 0:
1054+
break
1055+
bookmark = whiteSpaceTracker.bookmark
1056+
cb = partial(_whiteSpaceReachedCallback, bookmark=bookmark)
1057+
curCommandSequence.append(CallbackCommand(cb))
1058+
# The whiteSpaceTracker shouldn't move past the end of the info we're speaking.
1059+
start = end
1060+
relativeSpeechSequence[index] = curCommandSequence
1061+
expandedRelativeSpeechSequence = []
1062+
for x in relativeSpeechSequence:
1063+
if isinstance(x, list):
1064+
expandedRelativeSpeechSequence.extend(x)
1065+
else:
1066+
expandedRelativeSpeechSequence.append(x)
1067+
speechSequence.extend(expandedRelativeSpeechSequence)
10201068
#Finally get speech text for any fields left in new controlFieldStack that are common with the old controlFieldStack (for closing), if extra detail is not requested
10211069
if autoLanguageSwitching and lastLanguage is not None:
10221070
speechSequence.append(LangChangeCommand(None))
@@ -1763,7 +1811,7 @@ def getTableInfoSpeech(tableInfo,oldTableInfo,extraDetail=False):
17631811
textList.append(_("row %s")%rowNumber)
17641812
return " ".join(textList)
17651813

1766-
re_last_pause=re.compile(r"^(.*(?<=[^\s.!?])[.!?][\"'”’)]?(?:\s+|$))(.*$)",re.DOTALL|re.UNICODE)
1814+
re_last_pause=re.compile(r"^(.*?(?<=[^\s.!?])[.!?][\"'”’)]?(?:\s+|$))(.*$)",re.DOTALL|re.UNICODE)
17671815

17681816
def speakWithoutPauses(speechSequence,detectBreaks=True):
17691817
"""

source/speech/commands.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@
55
#Copyright (C) 2006-2019 NV Access Limited
66

77
"""Commands that can be embedded in a speech sequence for changing synth parameters, playing sounds or running other callbacks."""
8-
8+
99
from abc import ABCMeta, abstractmethod
1010
import config
1111
import languageHandler
1212
from synthDriverHandler import getSynth
13+
from inspect import signature
14+
from functools import partial
15+
from types import FunctionType, MethodType
16+
from typing import Union
1317

1418
class SpeechCommand(object):
1519
"""The base class for objects that can be inserted between strings of text to perform actions, change voice parameters, etc.
@@ -36,7 +40,8 @@ def __init__(self,index):
3640
@param index: the value of this index
3741
@type index: integer
3842
"""
39-
if not isinstance(index,int): raise ValueError("index must be int, not %s"%type(index))
43+
if not isinstance(index,int):
44+
raise TypeError("index must be int, not %s"%type(index))
4045
self.index=index
4146

4247
def __repr__(self):
@@ -245,6 +250,8 @@ def run(self):
245250
otherwise it will block production of further speech and or other functionality in NVDA.
246251
"""
247252

253+
SUPPORTED_CALLBACK_TYPES = (FunctionType, MethodType, partial)
254+
248255
class CallbackCommand(BaseCallbackCommand):
249256
"""
250257
Call a function when speech reaches this point.
@@ -253,12 +260,27 @@ class CallbackCommand(BaseCallbackCommand):
253260
otherwise it will block production of further speech and or other functionality in NVDA.
254261
"""
255262

256-
def __init__(self, callback):
263+
def __init__(self, callback: Union[SUPPORTED_CALLBACK_TYPES]):
264+
if not isinstance(callback, SUPPORTED_CALLBACK_TYPES):
265+
raise TypeError(
266+
"callback must be one of %s, not %s"
267+
% (", ".join(SUPPORTED_CALLBACK_TYPES), type(callback))
268+
)
257269
self._callback = callback
258270

259271
def run(self,*args, **kwargs):
260272
return self._callback(*args,**kwargs)
261273

274+
def __repr__(self):
275+
if isinstance(self._callback, partial):
276+
callBackName = "partial[{}]".format(self._callback.func.__qualname__)
277+
else:
278+
callBackName = self._callback.__qualname__
279+
return "CallbackCommand({name}{signature})".format(
280+
name=callBackName,
281+
signature=str(signature(self._callback))
282+
)
283+
262284
class BeepCommand(BaseCallbackCommand):
263285
"""Produce a beep.
264286
"""

0 commit comments

Comments
 (0)