Skip to content

Commit ada9083

Browse files
authored
Merge 9e9918b into 02f1dff
2 parents 02f1dff + 9e9918b commit ada9083

5 files changed

Lines changed: 138 additions & 32 deletions

File tree

source/config/configSpec.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
sayCapForCapitals = boolean(default=false)
4444
beepForCapitals = boolean(default=false)
4545
useSpellingFunctionality = boolean(default=true)
46+
increaseSayAllCaretUpdates = boolean(default=false)
4647
4748
# Audio settings
4849
[audio]

source/gui/settingsDialogs.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,6 +1447,16 @@ def makeSettings(self, settingsSizer):
14471447
config.conf["speech"][self.driver.name]["useSpellingFunctionality"]
14481448
)
14491449

1450+
# Translators: This is the label for a checkbox in the
1451+
# voice settings panel.
1452+
increaseSayAllCaretUpdatesText = _("Increase caret updates during say all")
1453+
self.increaseSayAllCaretUpdatesCheckBox = settingsSizerHelper.addItem(
1454+
wx.CheckBox(self, label=increaseSayAllCaretUpdatesText)
1455+
)
1456+
self.increaseSayAllCaretUpdatesCheckBox.SetValue(
1457+
config.conf["speech"][self.driver.name]["increaseSayAllCaretUpdates"]
1458+
)
1459+
14501460
def onSave(self):
14511461
AutoSettingsMixin.onSave(self)
14521462

@@ -1463,6 +1473,10 @@ def onSave(self):
14631473
config.conf["speech"][self.driver.name]["sayCapForCapitals"]=self.sayCapForCapsCheckBox.IsChecked()
14641474
config.conf["speech"][self.driver.name]["beepForCapitals"]=self.beepForCapsCheckBox.IsChecked()
14651475
config.conf["speech"][self.driver.name]["useSpellingFunctionality"]=self.useSpellingFunctionalityCheckBox.IsChecked()
1476+
config.conf["speech"][self.driver.name]["increaseSayAllCaretUpdates"] = (
1477+
self.increaseSayAllCaretUpdatesCheckBox.IsChecked()
1478+
)
1479+
14661480

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

source/sayAllHandler.py

Lines changed: 38 additions & 9 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,10 +147,24 @@ 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))
146-
spoke = speech.speakTextInfo(self.reader, unit=textInfos.UNIT_READINGCHUNK,
147-
reason=controlTypes.REASON_SAYALL, _prefixSpeechCommand=cb,
148-
useCache=self.speakTextInfoState)
150+
cb = speech.CallbackCommand(partial(
151+
self.lineReached,
152+
obj=self.reader.obj,
153+
bookmark=bookmark,
154+
state=self.speakTextInfoState.copy()
155+
))
156+
spoke = speech.speakTextInfo(
157+
self.reader,
158+
unit=textInfos.UNIT_READINGCHUNK,
159+
reason=controlTypes.REASON_SAYALL,
160+
_prefixSpeechCommand=cb,
161+
_whiteSpaceReachedCallback=(
162+
self.endOfWhiteSpaceReached
163+
if config.conf["speech"]["increaseSayAllCaretUpdates"]
164+
else None
165+
),
166+
useCache=self.speakTextInfoState
167+
)
149168
# Collapse to the end of this line, ready to read the next.
150169
try:
151170
self.reader.collapse(end=True)
@@ -168,14 +187,24 @@ def nextLine(self):
168187
# The first buffered line has now started speaking.
169188
self.numBufferedLines -= 1
170189

171-
def lineReached(self, obj, bookmark, state):
172-
# We've just started speaking this line, so move the cursor there.
173-
state.updateObj()
190+
def _bookmarkReached(self, obj, bookmark):
174191
updater = obj.makeTextInfo(bookmark)
175192
if self.cursor == CURSOR_CARET:
176-
updater.updateCaret()
193+
obj.selection = updater
177194
if self.cursor != CURSOR_CARET or config.conf["reviewCursor"]["followCaret"]:
178195
api.setReviewPosition(updater, isCaret=self.cursor==CURSOR_CARET)
196+
197+
def endOfWhiteSpaceReached(self, bookmark):
198+
curTime = time.time()
199+
if (self._lastEndOfWhiteSpaceReachedTime + self.MIN_TIME_BETWEEN_WHITE_SPACE_UPDATES) > curTime:
200+
return
201+
self._lastEndOfWhiteSpaceReachedTime = curTime
202+
self._bookmarkReached(self.reader.obj, bookmark)
203+
204+
def lineReached(self, obj, bookmark, state):
205+
# We've just started speaking this line, so move the cursor there.
206+
state.updateObj()
207+
self._bookmarkReached(obj, bookmark)
179208
if self.numBufferedLines == 0:
180209
# This was the last line spoken, so move on.
181210
self.nextLine()

source/speech/__init__.py

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,11 @@
4949
WaveFileCommand,
5050
ConfigProfileTriggerCommand,
5151
)
52-
52+
import languageHandler
53+
from functools import partial
5354
from . import types
5455
from .types import SpeechSequence
55-
from typing import Optional, Dict, List, Any
56+
from typing import Optional, Dict, List, Any, Callable
5657
from logHandler import log
5758
import config
5859
import aria
@@ -911,6 +912,13 @@ def _speakTextInfo_addMath(
911912
return
912913

913914

915+
re_white_space = re.compile(r"(\s+|$)", re.DOTALL)
916+
917+
918+
class _TextChunk(str):
919+
"""str subclass to distinguish normal text from field text when processing text info speech."""
920+
921+
914922
# C901 'speakTextInfo' is too complex
915923
# Note: when working on speakTextInfo, look for opportunities to simplify
916924
# and move logic out into smaller helper functions.
@@ -921,11 +929,13 @@ def speakTextInfo( # noqa: C901
921929
unit: Optional[str] = None,
922930
reason: str = controlTypes.REASON_QUERY,
923931
_prefixSpeechCommand: Optional[SpeechCommand] = None,
932+
_whiteSpaceReachedCallback: Optional[Callable[[Any], None]] = None,
924933
onlyInitialFields: bool = False,
925934
suppressBlanks: bool = False,
926935
priority: Optional[Spri] = None
927936
) -> bool:
928937
onlyCache=reason==controlTypes.REASON_ONLYCACHE
938+
processWhiteSpace: bool = _whiteSpaceReachedCallback is not None
929939
if isinstance(useCache,SpeakTextInfoState):
930940
speakTextInfoState=useCache
931941
elif useCache:
@@ -1132,9 +1142,9 @@ def speakTextInfo( # noqa: C901
11321142
indentationDone=True
11331143
if command:
11341144
if inTextChunk:
1135-
relativeSpeechSequence[-1]+=command
1145+
relativeSpeechSequence[-1] = _TextChunk(relativeSpeechSequence[-1] + command)
11361146
else:
1137-
relativeSpeechSequence.append(command)
1147+
relativeSpeechSequence.append(_TextChunk(command))
11381148
inTextChunk=True
11391149
elif isinstance(command,textInfos.FieldCommand):
11401150
newLanguage=None
@@ -1216,16 +1226,44 @@ def speakTextInfo( # noqa: C901
12161226
else:
12171227
speechSequence.extend(indentationSpeech)
12181228
if speakTextInfoState: speakTextInfoState.indentationCache=allIndentation
1219-
# Don't add this text if it is blank.
1220-
relativeBlank=True
1221-
for x in relativeSpeechSequence:
1222-
if isinstance(x,str) and not isBlank(x):
1223-
relativeBlank=False
1224-
break
1225-
if not relativeBlank:
1226-
speechSequence.extend(relativeSpeechSequence)
1229+
# only add this text if it is not blank.
1230+
notBlank = any(
1231+
not isBlank(x) for x in relativeSpeechSequence
1232+
if isinstance(x, str)
1233+
)
1234+
if notBlank:
12271235
shouldConsiderTextInfoBlank = False
1228-
1236+
if not processWhiteSpace:
1237+
speechSequence.extend(relativeSpeechSequence)
1238+
else:
1239+
# Add appropriate white space bookmarks
1240+
whiteSpaceTracker = info.copy()
1241+
whiteSpaceTracker.collapse()
1242+
for index, command in list(enumerate(relativeSpeechSequence)):
1243+
if not isinstance(command, _TextChunk):
1244+
continue
1245+
curCommandSequence = []
1246+
endOfWhiteSpaceIndexes = [m.end() for m in re_white_space.finditer(command)]
1247+
start = 0
1248+
for end in endOfWhiteSpaceIndexes:
1249+
text = command[start:end]
1250+
curCommandSequence.append(text)
1251+
whiteSpaceTracker.move(textInfos.UNIT_CHARACTER, len(text))
1252+
if whiteSpaceTracker.compareEndPoints(info, "startToEnd") > 0:
1253+
break
1254+
bookmark = whiteSpaceTracker.bookmark
1255+
callback = partial(_whiteSpaceReachedCallback, bookmark=bookmark)
1256+
curCommandSequence.append(CallbackCommand(callback))
1257+
# The whiteSpaceTracker shouldn't move past the end of the info we're speaking.
1258+
start = end
1259+
relativeSpeechSequence[index] = curCommandSequence
1260+
expandedRelativeSpeechSequence = []
1261+
for x in relativeSpeechSequence:
1262+
if isinstance(x, list):
1263+
expandedRelativeSpeechSequence.extend(x)
1264+
else:
1265+
expandedRelativeSpeechSequence.append(x)
1266+
speechSequence.extend(expandedRelativeSpeechSequence)
12291267
#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
12301268
if autoLanguageSwitching and lastLanguage is not None:
12311269
speechSequence.append(
@@ -2182,7 +2220,7 @@ def getTableInfoSpeech(
21822220
return textList
21832221

21842222

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

21872225

21882226
def speakWithoutPauses( # noqa: C901

source/speech/commands.py

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
# -*- coding: UTF-8 -*-
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) 2006-2019 NV Access Limited
2+
# speech/commands.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) 2017-2019 NV Access Limited, Babbage B.V.
67

78
"""Commands that can be embedded in a speech sequence for changing synth parameters, playing sounds or running other callbacks."""
8-
9+
910
from abc import ABCMeta, abstractmethod
1011
from typing import Optional
1112

1213
import config
1314
from synthDriverHandler import getSynth
15+
from inspect import signature
16+
from functools import partial
17+
from types import FunctionType, MethodType
18+
from typing import Union
1419

1520
class SpeechCommand(object):
1621
"""The base class for objects that can be inserted between strings of text to perform actions, change voice parameters, etc.
@@ -32,12 +37,12 @@ class IndexCommand(SynthCommand):
3237
NVDA handles the indexing and dispatches callbacks as appropriate.
3338
"""
3439

35-
def __init__(self,index):
40+
def __init__(self, index: int):
3641
"""
3742
@param index: the value of this index
38-
@type index: integer
3943
"""
40-
if not isinstance(index,int): raise ValueError("index must be int, not %s"%type(index))
44+
if not isinstance(index, int):
45+
raise TypeError(f"index must be int, not {type(index)}")
4146
self.index=index
4247

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

253+
254+
SUPPORTED_CALLBACK_TYPES = (FunctionType, MethodType, partial)
255+
256+
248257
class CallbackCommand(BaseCallbackCommand):
249258
"""
250259
Call a function when speech reaches this point.
@@ -253,12 +262,27 @@ class CallbackCommand(BaseCallbackCommand):
253262
otherwise it will block production of further speech and or other functionality in NVDA.
254263
"""
255264

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

259273
def run(self,*args, **kwargs):
260274
return self._callback(*args,**kwargs)
261275

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

0 commit comments

Comments
 (0)