Skip to content

Commit 4092c7d

Browse files
authored
Merge 60f53b5 into 81d81dc
2 parents 81d81dc + 60f53b5 commit 4092c7d

5 files changed

Lines changed: 132 additions & 28 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
@@ -1451,6 +1451,16 @@ def makeSettings(self, settingsSizer):
14511451
config.conf["speech"][self.driver.name]["useSpellingFunctionality"]
14521452
)
14531453

1454+
# Translators: This is the label for a checkbox in the
1455+
# voice settings panel.
1456+
increaseSayAllCaretUpdatesText = _("Increase caret updates during say all")
1457+
self.increaseSayAllCaretUpdatesCheckBox = settingsSizerHelper.addItem(
1458+
wx.CheckBox(self, label=increaseSayAllCaretUpdatesText)
1459+
)
1460+
self.increaseSayAllCaretUpdatesCheckBox.SetValue(
1461+
config.conf["speech"][self.driver.name]["increaseSayAllCaretUpdates"]
1462+
)
1463+
14541464
def onSave(self):
14551465
AutoSettingsMixin.onSave(self)
14561466

@@ -1467,6 +1477,10 @@ def onSave(self):
14671477
config.conf["speech"][self.driver.name]["sayCapForCapitals"]=self.sayCapForCapsCheckBox.IsChecked()
14681478
config.conf["speech"][self.driver.name]["beepForCapitals"]=self.beepForCapsCheckBox.IsChecked()
14691479
config.conf["speech"][self.driver.name]["useSpellingFunctionality"]=self.useSpellingFunctionalityCheckBox.IsChecked()
1480+
config.conf["speech"][self.driver.name]["increaseSayAllCaretUpdates"] = (
1481+
self.increaseSayAllCaretUpdatesCheckBox.IsChecked()
1482+
)
1483+
14701484

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

source/sayAllHandler.py

Lines changed: 26 additions & 5 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,7 @@
1213
import api
1314
import textInfos
1415
import queueHandler
16+
import time
1517
import winKernel
1618

1719
CURSOR_CARET = 0
@@ -101,6 +103,8 @@ class _TextReader(object):
101103
11. If there is another page, L{turnPage} calls L{nextLine}.
102104
"""
103105
MAX_BUFFERED_LINES = 10
106+
_lastEndOfWhiteSpaceReachedTime = 0
107+
MIN_TIME_BETWEEN_WHITE_SPACE_UPDATES = 0.5
104108

105109
def __init__(self, cursor):
106110
self.cursor = cursor
@@ -160,6 +164,11 @@ def _onLineReached(obj=self.reader.obj, state=self.speakTextInfoState.copy()):
160164
unit=textInfos.UNIT_READINGCHUNK,
161165
reason=controlTypes.REASON_SAYALL,
162166
_prefixSpeechCommand=cb,
167+
_whiteSpaceReachedCallback=(
168+
self.endOfWhiteSpaceReached
169+
if config.conf["speech"][synthDriverHandler.getSynth().name]["increaseSayAllCaretUpdates"]
170+
else None
171+
),
163172
useCache=self.speakTextInfoState
164173
)
165174

@@ -185,15 +194,27 @@ def _onLineReached(obj=self.reader.obj, state=self.speakTextInfoState.copy()):
185194
# The first buffered line has now started speaking.
186195
self.numBufferedLines -= 1
187196

188-
def lineReached(self, obj, bookmark, state):
189-
# We've just started speaking this line, so move the cursor there.
190-
state.updateObj()
197+
def _bookmarkReached(self, obj, bookmark):
191198
updater = obj.makeTextInfo(bookmark)
192199
if self.cursor == CURSOR_CARET:
193-
updater.updateCaret()
200+
obj.selection = updater
194201
if self.cursor != CURSOR_CARET or config.conf["reviewCursor"]["followCaret"]:
195202
api.setReviewPosition(updater, isCaret=self.cursor==CURSOR_CARET)
203+
204+
def endOfWhiteSpaceReached(self, bookmark):
205+
curTime = time.time()
206+
if (self._lastEndOfWhiteSpaceReachedTime + self.MIN_TIME_BETWEEN_WHITE_SPACE_UPDATES) > curTime:
207+
return
208+
self._lastEndOfWhiteSpaceReachedTime = curTime
209+
self._bookmarkReached(self.reader.obj, bookmark)
210+
211+
def lineReached(self, obj, bookmark, state):
212+
# We've just started speaking this line, so move the cursor there.
213+
state.updateObj()
214+
self._bookmarkReached(obj, bookmark)
215+
196216
winKernel.SetThreadExecutionState(winKernel.ES_SYSTEM_REQUIRED | winKernel.ES_DISPLAY_REQUIRED)
217+
197218
if self.numBufferedLines == 0:
198219
# This was the last line spoken, so move on.
199220
self.nextLine()

source/speech/__init__.py

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
WaveFileCommand,
5151
ConfigProfileTriggerCommand,
5252
)
53-
5453
from . import types
5554
from .types import SpeechSequence, SequenceItemT
5655
from typing import Optional, Dict, List, Any, Generator, Union, Callable, Iterator, Tuple
@@ -1026,6 +1025,13 @@ def _extendSpeechSequence_addMathForTextInfo(
10261025
return
10271026

10281027

1028+
re_white_space = re.compile(r"(\s+|$)", re.DOTALL)
1029+
1030+
1031+
class _TextChunk(str):
1032+
"""str subclass to distinguish normal text from field text when processing text info speech."""
1033+
1034+
10291035
class GeneratorWithReturn:
10301036
"""Helper class, used with generator functions to access the 'return' value after there are no more values
10311037
to iterate over.
@@ -1047,6 +1053,7 @@ def speakTextInfo(
10471053
unit: Optional[str] = None,
10481054
reason: OutputReason = controlTypes.REASON_QUERY,
10491055
_prefixSpeechCommand: Optional[SpeechCommand] = None,
1056+
_whiteSpaceReachedCallback: Optional[Callable[[Any], None]] = None,
10501057
onlyInitialFields: bool = False,
10511058
suppressBlanks: bool = False,
10521059
priority: Optional[Spri] = None
@@ -1058,6 +1065,7 @@ def speakTextInfo(
10581065
unit,
10591066
reason,
10601067
_prefixSpeechCommand,
1068+
_whiteSpaceReachedCallback,
10611069
onlyInitialFields,
10621070
suppressBlanks
10631071
)
@@ -1077,10 +1085,12 @@ def getTextInfoSpeech( # noqa: C901
10771085
unit: Optional[str] = None,
10781086
reason: OutputReason = controlTypes.REASON_QUERY,
10791087
_prefixSpeechCommand: Optional[SpeechCommand] = None,
1088+
_whiteSpaceReachedCallback: Optional[Callable[[Any], None]] = None,
10801089
onlyInitialFields: bool = False,
10811090
suppressBlanks: bool = False
10821091
) -> Generator[SpeechSequence, None, bool]:
10831092
onlyCache=reason==controlTypes.REASON_ONLYCACHE
1093+
processWhiteSpace: bool = _whiteSpaceReachedCallback is not None
10841094
if isinstance(useCache,SpeakTextInfoState):
10851095
speakTextInfoState=useCache
10861096
elif useCache:
@@ -1300,9 +1310,9 @@ def isControlEndFieldCommand(x):
13001310
indentationDone=True
13011311
if command:
13021312
if inTextChunk:
1303-
relativeSpeechSequence[-1]+=command
1313+
relativeSpeechSequence[-1] = _TextChunk(relativeSpeechSequence[-1] + command)
13041314
else:
1305-
relativeSpeechSequence.append(command)
1315+
relativeSpeechSequence.append(_TextChunk(command))
13061316
inTextChunk=True
13071317
elif isinstance(command,textInfos.FieldCommand):
13081318
newLanguage=None
@@ -1384,16 +1394,49 @@ def isControlEndFieldCommand(x):
13841394
else:
13851395
speechSequence.extend(indentationSpeech)
13861396
if speakTextInfoState: speakTextInfoState.indentationCache=allIndentation
1387-
# Don't add this text if it is blank.
1388-
relativeBlank=True
1389-
for x in relativeSpeechSequence:
1390-
if isinstance(x,str) and not isBlank(x):
1391-
relativeBlank=False
1392-
break
1393-
if not relativeBlank:
1394-
speechSequence.extend(relativeSpeechSequence)
1397+
# only add this text if it is not blank.
1398+
notBlank = any(
1399+
not isBlank(x) for x in relativeSpeechSequence
1400+
if isinstance(x, str)
1401+
)
1402+
if notBlank:
13951403
shouldConsiderTextInfoBlank = False
1404+
if not processWhiteSpace:
1405+
speechSequence.extend(relativeSpeechSequence)
1406+
else:
1407+
# Add appropriate white space bookmarks
1408+
whiteSpaceTracker = info.copy()
1409+
whiteSpaceTracker.collapse()
1410+
for index, command in list(enumerate(relativeSpeechSequence)):
1411+
if not isinstance(command, _TextChunk):
1412+
continue
1413+
curCommandSequence = []
1414+
endOfWhiteSpaceIndexes = [m.end() for m in re_white_space.finditer(command)]
1415+
start = 0
1416+
for end in endOfWhiteSpaceIndexes:
1417+
text = command[start:end]
1418+
curCommandSequence.append(text)
1419+
whiteSpaceTracker.move(textInfos.UNIT_CHARACTER, len(text))
1420+
if whiteSpaceTracker.compareEndPoints(info, "startToEnd") > 0:
1421+
break
1422+
bookmark = whiteSpaceTracker.bookmark
13961423

1424+
def _onWhiteSpaceReached(bookmark=bookmark):
1425+
return _whiteSpaceReachedCallback(bookmark=bookmark)
1426+
1427+
curCommandSequence.append(
1428+
CallbackCommand(_onWhiteSpaceReached, name="getTextInfoSpeech:whiteSpaceReached")
1429+
)
1430+
# The whiteSpaceTracker shouldn't move past the end of the info we're speaking.
1431+
start = end
1432+
relativeSpeechSequence[index] = curCommandSequence
1433+
expandedRelativeSpeechSequence = []
1434+
for x in relativeSpeechSequence:
1435+
if isinstance(x, list):
1436+
expandedRelativeSpeechSequence.extend(x)
1437+
else:
1438+
expandedRelativeSpeechSequence.append(x)
1439+
speechSequence.extend(expandedRelativeSpeechSequence)
13971440
#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
13981441
if autoLanguageSwitching and lastLanguage is not None:
13991442
speechSequence.append(
@@ -2375,6 +2418,9 @@ def getTableInfoSpeech(
23752418
return textList
23762419

23772420

2421+
re_last_pause = re.compile(r"^(.*?(?<=[^\s.!?])[.!?][\"'”’)]?(?:\s+|$))(.*$)", re.DOTALL)
2422+
2423+
23782424
def _yieldIfNonEmpty(seq: SpeechSequence):
23792425
"""Helper method to yield the sequence if it is not None or empty."""
23802426
if seq:

source/speech/commands.py

Lines changed: 34 additions & 12 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,18 +262,31 @@ 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, name: Optional[str] = None):
265+
def __init__(self, callback: Union[SUPPORTED_CALLBACK_TYPES], name: Optional[str] = None):
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
258-
self._name = name if name else repr(callback)
272+
if name:
273+
self._name = name
274+
elif isinstance(callback, partial):
275+
self._name = "partial[{}]".format(callback.func.__qualname__)
276+
else:
277+
self.name = callback.__qualname__
278+
self._signature = str(signature(callback))
259279

260280
def run(self,*args, **kwargs):
261281
return self._callback(*args,**kwargs)
262282

263283
def __repr__(self):
264-
return "CallbackCommand(name={name})".format(
265-
name=self._name
284+
return "CallbackCommand({name}{signature})".format(
285+
name=self._name,
286+
signature=self._signature
266287
)
267288

289+
268290
class BeepCommand(BaseCallbackCommand):
269291
"""Produce a beep.
270292
"""

0 commit comments

Comments
 (0)