Skip to content

Commit 20cf56e

Browse files
authored
Merge 1dc834d into 81d81dc
2 parents 81d81dc + 1dc834d commit 20cf56e

5 files changed

Lines changed: 128 additions & 29 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: 27 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,8 @@
1213
import api
1314
import textInfos
1415
import queueHandler
16+
from functools import partial
17+
import time
1518
import winKernel
1619

1720
CURSOR_CARET = 0
@@ -101,6 +104,8 @@ class _TextReader(object):
101104
11. If there is another page, L{turnPage} calls L{nextLine}.
102105
"""
103106
MAX_BUFFERED_LINES = 10
107+
_lastEndOfWhiteSpaceReachedTime = 0
108+
MIN_TIME_BETWEEN_WHITE_SPACE_UPDATES = 0.5
104109

105110
def __init__(self, cursor):
106111
self.cursor = cursor
@@ -160,6 +165,11 @@ def _onLineReached(obj=self.reader.obj, state=self.speakTextInfoState.copy()):
160165
unit=textInfos.UNIT_READINGCHUNK,
161166
reason=controlTypes.REASON_SAYALL,
162167
_prefixSpeechCommand=cb,
168+
_whiteSpaceReachedCallback=(
169+
self.endOfWhiteSpaceReached
170+
if config.conf["speech"]["increaseSayAllCaretUpdates"]
171+
else None
172+
),
163173
useCache=self.speakTextInfoState
164174
)
165175

@@ -185,15 +195,27 @@ def _onLineReached(obj=self.reader.obj, state=self.speakTextInfoState.copy()):
185195
# The first buffered line has now started speaking.
186196
self.numBufferedLines -= 1
187197

188-
def lineReached(self, obj, bookmark, state):
189-
# We've just started speaking this line, so move the cursor there.
190-
state.updateObj()
198+
def _bookmarkReached(self, obj, bookmark):
191199
updater = obj.makeTextInfo(bookmark)
192200
if self.cursor == CURSOR_CARET:
193-
updater.updateCaret()
201+
obj.selection = updater
194202
if self.cursor != CURSOR_CARET or config.conf["reviewCursor"]["followCaret"]:
195203
api.setReviewPosition(updater, isCaret=self.cursor==CURSOR_CARET)
204+
205+
def endOfWhiteSpaceReached(self, bookmark):
206+
curTime = time.time()
207+
if (self._lastEndOfWhiteSpaceReachedTime + self.MIN_TIME_BETWEEN_WHITE_SPACE_UPDATES) > curTime:
208+
return
209+
self._lastEndOfWhiteSpaceReachedTime = curTime
210+
self._bookmarkReached(self.reader.obj, bookmark)
211+
212+
def lineReached(self, obj, bookmark, state):
213+
# We've just started speaking this line, so move the cursor there.
214+
state.updateObj()
215+
self._bookmarkReached(obj, bookmark)
216+
196217
winKernel.SetThreadExecutionState(winKernel.ES_SYSTEM_REQUIRED | winKernel.ES_DISPLAY_REQUIRED)
218+
197219
if self.numBufferedLines == 0:
198220
# This was the last line spoken, so move on.
199221
self.nextLine()

source/speech/__init__.py

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
WaveFileCommand,
5151
ConfigProfileTriggerCommand,
5252
)
53-
53+
from functools import partial
5454
from . import types
5555
from .types import SpeechSequence, SequenceItemT
5656
from typing import Optional, Dict, List, Any, Generator, Union, Callable, Iterator, Tuple
@@ -1026,6 +1026,13 @@ def _extendSpeechSequence_addMathForTextInfo(
10261026
return
10271027

10281028

1029+
re_white_space = re.compile(r"(\s+|$)", re.DOTALL)
1030+
1031+
1032+
class _TextChunk(str):
1033+
"""str subclass to distinguish normal text from field text when processing text info speech."""
1034+
1035+
10291036
class GeneratorWithReturn:
10301037
"""Helper class, used with generator functions to access the 'return' value after there are no more values
10311038
to iterate over.
@@ -1047,6 +1054,7 @@ def speakTextInfo(
10471054
unit: Optional[str] = None,
10481055
reason: OutputReason = controlTypes.REASON_QUERY,
10491056
_prefixSpeechCommand: Optional[SpeechCommand] = None,
1057+
_whiteSpaceReachedCallback: Optional[Callable[[Any], None]] = None,
10501058
onlyInitialFields: bool = False,
10511059
suppressBlanks: bool = False,
10521060
priority: Optional[Spri] = None
@@ -1081,6 +1089,7 @@ def getTextInfoSpeech( # noqa: C901
10811089
suppressBlanks: bool = False
10821090
) -> Generator[SpeechSequence, None, bool]:
10831091
onlyCache=reason==controlTypes.REASON_ONLYCACHE
1092+
processWhiteSpace: bool = _whiteSpaceReachedCallback is not None
10841093
if isinstance(useCache,SpeakTextInfoState):
10851094
speakTextInfoState=useCache
10861095
elif useCache:
@@ -1300,9 +1309,9 @@ def isControlEndFieldCommand(x):
13001309
indentationDone=True
13011310
if command:
13021311
if inTextChunk:
1303-
relativeSpeechSequence[-1]+=command
1312+
relativeSpeechSequence[-1] = _TextChunk(relativeSpeechSequence[-1] + command)
13041313
else:
1305-
relativeSpeechSequence.append(command)
1314+
relativeSpeechSequence.append(_TextChunk(command))
13061315
inTextChunk=True
13071316
elif isinstance(command,textInfos.FieldCommand):
13081317
newLanguage=None
@@ -1384,16 +1393,44 @@ def isControlEndFieldCommand(x):
13841393
else:
13851394
speechSequence.extend(indentationSpeech)
13861395
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)
1396+
# only add this text if it is not blank.
1397+
notBlank = any(
1398+
not isBlank(x) for x in relativeSpeechSequence
1399+
if isinstance(x, str)
1400+
)
1401+
if notBlank:
13951402
shouldConsiderTextInfoBlank = False
1396-
1403+
if not processWhiteSpace:
1404+
speechSequence.extend(relativeSpeechSequence)
1405+
else:
1406+
# Add appropriate white space bookmarks
1407+
whiteSpaceTracker = info.copy()
1408+
whiteSpaceTracker.collapse()
1409+
for index, command in list(enumerate(relativeSpeechSequence)):
1410+
if not isinstance(command, _TextChunk):
1411+
continue
1412+
curCommandSequence = []
1413+
endOfWhiteSpaceIndexes = [m.end() for m in re_white_space.finditer(command)]
1414+
start = 0
1415+
for end in endOfWhiteSpaceIndexes:
1416+
text = command[start:end]
1417+
curCommandSequence.append(text)
1418+
whiteSpaceTracker.move(textInfos.UNIT_CHARACTER, len(text))
1419+
if whiteSpaceTracker.compareEndPoints(info, "startToEnd") > 0:
1420+
break
1421+
bookmark = whiteSpaceTracker.bookmark
1422+
callback = partial(_whiteSpaceReachedCallback, bookmark=bookmark)
1423+
curCommandSequence.append(CallbackCommand(callback))
1424+
# The whiteSpaceTracker shouldn't move past the end of the info we're speaking.
1425+
start = end
1426+
relativeSpeechSequence[index] = curCommandSequence
1427+
expandedRelativeSpeechSequence = []
1428+
for x in relativeSpeechSequence:
1429+
if isinstance(x, list):
1430+
expandedRelativeSpeechSequence.extend(x)
1431+
else:
1432+
expandedRelativeSpeechSequence.append(x)
1433+
speechSequence.extend(expandedRelativeSpeechSequence)
13971434
#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
13981435
if autoLanguageSwitching and lastLanguage is not None:
13991436
speechSequence.append(
@@ -2375,6 +2412,9 @@ def getTableInfoSpeech(
23752412
return textList
23762413

23772414

2415+
re_last_pause = re.compile(r"^(.*?(?<=[^\s.!?])[.!?][\"'”’)]?(?:\s+|$))(.*$)", re.DOTALL)
2416+
2417+
23782418
def _yieldIfNonEmpty(seq: SpeechSequence):
23792419
"""Helper method to yield the sequence if it is not None or empty."""
23802420
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)