From 8f0921cdcd47e93cab787b53a9d6d787fe660425 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Mon, 15 Jul 2019 16:32:21 +0200 Subject: [PATCH 01/17] Split lineReached into a separate _bookmarkReached function --- source/sayAllHandler.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/source/sayAllHandler.py b/source/sayAllHandler.py index d34b91970db..2a00b9dedbd 100644 --- a/source/sayAllHandler.py +++ b/source/sayAllHandler.py @@ -125,7 +125,7 @@ def nextLine(self): return bookmark = self.reader.bookmark # Expand to the current line. - # We use move end rather than expand + # We use move end rather than expand⠀ # because the user might start in the middle of a line # and we don't want to read from the start of the line in that case. # For lines after the first, it's also more efficient because @@ -168,14 +168,17 @@ def nextLine(self): # The first buffered line has now started speaking. self.numBufferedLines -= 1 - def lineReached(self, obj, bookmark, state): - # We've just started speaking this line, so move the cursor there. - state.updateObj() + def _bookmarkReached(self, obj, bookmark): 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) + + def lineReached(self, obj, bookmark, state): + # We've just started speaking this line, so move the cursor there. + state.updateObj() + self._bookmarkReached(obj, bookmark) if self.numBufferedLines == 0: # This was the last line spoken, so move on. self.nextLine() From ce53201233e2facb933bd8602ae6e392d2332d8e Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Wed, 17 Jul 2019 11:01:56 +0200 Subject: [PATCH 02/17] Change the re_last_pause regex to consume less of a line containing multiple sentences --- source/speech/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/speech/__init__.py b/source/speech/__init__.py index 010cee29b18..06ca091786b 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -1763,7 +1763,7 @@ def getTableInfoSpeech(tableInfo,oldTableInfo,extraDetail=False): textList.append(_("row %s")%rowNumber) return " ".join(textList) -re_last_pause=re.compile(r"^(.*(?<=[^\s.!?])[.!?][\"'”’)]?(?:\s+|$))(.*$)",re.DOTALL|re.UNICODE) +re_last_pause=re.compile(r"^(.*?(?<=[^\s.!?])[.!?][\"'”’)]?(?:\s+|$))(.*$)",re.DOTALL|re.UNICODE) def speakWithoutPauses(speechSequence,detectBreaks=True): """ From ff1872c203825367347eb682f5c70269ebbaff72 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Wed, 17 Jul 2019 10:57:35 +0200 Subject: [PATCH 03/17] Add a nice repr to callback commands for easier debugging --- source/speech/commands.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/source/speech/commands.py b/source/speech/commands.py index 47e83ad7115..2d6f98fd7a9 100644 --- a/source/speech/commands.py +++ b/source/speech/commands.py @@ -5,11 +5,15 @@ #Copyright (C) 2006-2019 NV Access Limited """Commands that can be embedded in a speech sequence for changing synth parameters, playing sounds or running other callbacks.""" - + from abc import ABCMeta, abstractmethod import config import languageHandler from synthDriverHandler import getSynth +from inspect import signature +from functools import partial +from types import FunctionType, MethodType +from typing import Union class SpeechCommand(object): """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): @param index: the value of this index @type index: integer """ - if not isinstance(index,int): raise ValueError("index must be int, not %s"%type(index)) + if not isinstance(index,int): + raise TypeError("index must be int, not %s"%type(index)) self.index=index def __repr__(self): @@ -245,6 +250,8 @@ def run(self): otherwise it will block production of further speech and or other functionality in NVDA. """ +SUPPORTED_CALLBACK_TYPES = (FunctionType, MethodType, partial) + class CallbackCommand(BaseCallbackCommand): """ Call a function when speech reaches this point. @@ -253,12 +260,27 @@ class CallbackCommand(BaseCallbackCommand): otherwise it will block production of further speech and or other functionality in NVDA. """ - def __init__(self, callback): + def __init__(self, callback: Union[SUPPORTED_CALLBACK_TYPES]): + if not isinstance(callback, SUPPORTED_CALLBACK_TYPES): + raise TypeError( + "callback must be one of %s, not %s" + % (", ".join(SUPPORTED_CALLBACK_TYPES), type(callback)) + ) self._callback = callback def run(self,*args, **kwargs): return self._callback(*args,**kwargs) + def __repr__(self): + if isinstance(self._callback, partial): + callBackName = "partial[{}]".format(self._callback.func.__qualname__) + else: + callBackName = self._callback.__qualname__ + return "CallbackCommand({name}{signature})".format( + name=callBackName, + signature=str(signature(self._callback)) + ) + class BeepCommand(BaseCallbackCommand): """Produce a beep. """ From 07b90c8c9ae1ba911016313b5951f1ce50284cbb Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Tue, 16 Jul 2019 14:51:13 +0200 Subject: [PATCH 04/17] Make the caret follow say all more closely --- source/config/configSpec.py | 1 + source/gui/settingsDialogs.py | 7 ++++ source/sayAllHandler.py | 28 ++++++++++++-- source/speech/__init__.py | 72 +++++++++++++++++++++++++++++------ 4 files changed, 93 insertions(+), 15 deletions(-) diff --git a/source/config/configSpec.py b/source/config/configSpec.py index ab9c894269b..f97740cfa09 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -37,6 +37,7 @@ outputDevice = string(default=default) autoLanguageSwitching = boolean(default=true) autoDialectSwitching = boolean(default=false) + increaseSayAllCaretUpdates = boolean(default=false) [[__many__]] capPitchChange = integer(default=30,min=-100,max=100) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index f9e6c2504f1..36c3854272a 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -1248,6 +1248,12 @@ def makeSettings(self, settingsSizer): self.useSpellingFunctionalityCheckBox = settingsSizerHelper.addItem(wx.CheckBox(self, label = useSpellingFunctionalityText)) self.useSpellingFunctionalityCheckBox.SetValue(config.conf["speech"][self.driver.name]["useSpellingFunctionality"]) + # Translators: This is the label for a checkbox in the + # voice settings panel. + increaseSayAllCaretUpdatesText = _("Increase caret updates during say all") + self.increaseSayAllCaretUpdatesCheckBox = settingsSizerHelper.addItem(wx.CheckBox(self, label = increaseSayAllCaretUpdatesText)) + self.increaseSayAllCaretUpdatesCheckBox.Value = config.conf["speech"]["increaseSayAllCaretUpdates"] + def onSave(self): DriverSettingsMixin.onSave(self) @@ -1264,6 +1270,7 @@ def onSave(self): config.conf["speech"][self.driver.name]["sayCapForCapitals"]=self.sayCapForCapsCheckBox.IsChecked() config.conf["speech"][self.driver.name]["beepForCapitals"]=self.beepForCapsCheckBox.IsChecked() config.conf["speech"][self.driver.name]["useSpellingFunctionality"]=self.useSpellingFunctionalityCheckBox.IsChecked() + config.conf["speech"]["increaseSayAllCaretUpdates"] = self.increaseSayAllCaretUpdatesCheckBox.IsChecked() class KeyboardSettingsPanel(SettingsPanel): # Translators: This is the label for the keyboard settings panel. diff --git a/source/sayAllHandler.py b/source/sayAllHandler.py index 2a00b9dedbd..0874a292038 100644 --- a/source/sayAllHandler.py +++ b/source/sayAllHandler.py @@ -1,5 +1,6 @@ +# sayAllHandler.py # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2017 NV Access Limited +# Copyright (C) 2006-2019 NV Access Limited, Babbage B.V. # This file may be used under the terms of the GNU General Public License, version 2 or later. # For more details see: https://www.gnu.org/licenses/gpl-2.0.html @@ -12,6 +13,8 @@ import api import textInfos import queueHandler +from functools import partial +import time CURSOR_CARET = 0 CURSOR_REVIEW = 1 @@ -99,6 +102,8 @@ class _TextReader(object): 11. If there is another page, L{turnPage} calls L{nextLine}. """ MAX_BUFFERED_LINES = 10 + _lastEndOfWhiteSpaceReachedTime = 0 + MIN_TIME_BETWEEN_WHITE_SPACE_UPDATES = 0.5 def __init__(self, cursor): self.cursor = cursor @@ -125,7 +130,7 @@ def nextLine(self): return bookmark = self.reader.bookmark # Expand to the current line. - # We use move end rather than expand⠀ + # We use move end rather than expand # because the user might start in the middle of a line # and we don't want to read from the start of the line in that case. # For lines after the first, it's also more efficient because @@ -142,9 +147,19 @@ def nextLine(self): return # Call lineReached when we start speaking this line. # lineReached will move the cursor and trigger reading of the next line. - cb = speech.CallbackCommand(lambda obj=self.reader.obj, state=self.speakTextInfoState.copy(): self.lineReached(obj,bookmark, state)) + cb = speech.CallbackCommand(partial( + self.lineReached, + obj=self.reader.obj, + bookmark=bookmark, + state=self.speakTextInfoState.copy() + )) spoke = speech.speakTextInfo(self.reader, unit=textInfos.UNIT_READINGCHUNK, reason=controlTypes.REASON_SAYALL, _prefixSpeechCommand=cb, + _whiteSpaceReachedCallback=( + self.endOfWhiteSpaceReached + if config.conf["speech"]["increaseSayAllCaretUpdates"] + else None + ), useCache=self.speakTextInfoState) # Collapse to the end of this line, ready to read the next. try: @@ -175,6 +190,13 @@ def _bookmarkReached(self, obj, bookmark): if self.cursor != CURSOR_CARET or config.conf["reviewCursor"]["followCaret"]: api.setReviewPosition(updater, isCaret=self.cursor==CURSOR_CARET) + def endOfWhiteSpaceReached(self, bookmark): + curTime = time.time() + if (self._lastEndOfWhiteSpaceReachedTime + self.MIN_TIME_BETWEEN_WHITE_SPACE_UPDATES) > curTime: + return + self._lastEndOfWhiteSpaceReachedTime = curTime + self._bookmarkReached(self.reader.obj, bookmark) + def lineReached(self, obj, bookmark, state): # We've just started speaking this line, so move the cursor there. state.updateObj() diff --git a/source/speech/__init__.py b/source/speech/__init__.py index 06ca091786b..72bbac9fa6f 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -30,6 +30,9 @@ import characterProcessing import languageHandler from .commands import * +from typing import Optional, Callable, Any +from functools import partial +from types import GeneratorType speechMode_off=0 speechMode_beeps=1 @@ -766,8 +769,25 @@ def _speakTextInfo_addMath(speechSequence, info, field): except (NotImplementedError, LookupError): return -def speakTextInfo(info, useCache=True, formatConfig=None, unit=None, reason=controlTypes.REASON_QUERY, _prefixSpeechCommand=None, onlyInitialFields=False, suppressBlanks=False,priority=None): +re_white_space = re.compile(r"(\s+|$)", re.DOTALL) + +class _TextCommand(str): + """str subclass to distinguish normal text from field text when processing text info speech.""" + +def speakTextInfo( + info, + useCache=True, + formatConfig=None, + unit=None, + reason=controlTypes.REASON_QUERY, + _prefixSpeechCommand=None, + _whiteSpaceReachedCallback: Optional[Callable[[Any], None]] = None, + onlyInitialFields=False, + suppressBlanks=False, + priority=None +): onlyCache=reason==controlTypes.REASON_ONLYCACHE + processWhiteSPace: bool = _whiteSpaceReachedCallback is not None if isinstance(useCache,SpeakTextInfoState): speakTextInfoState=useCache elif useCache: @@ -943,9 +963,9 @@ def speakTextInfo(info, useCache=True, formatConfig=None, unit=None, reason=cont indentationDone=True if command: if inTextChunk: - relativeSpeechSequence[-1]+=command + relativeSpeechSequence[-1] = _TextCommand(relativeSpeechSequence[-1] + command) else: - relativeSpeechSequence.append(command) + relativeSpeechSequence.append(_TextCommand(command)) inTextChunk=True elif isinstance(command,textInfos.FieldCommand): newLanguage=None @@ -1007,16 +1027,44 @@ def speakTextInfo(info, useCache=True, formatConfig=None, unit=None, reason=cont else: speechSequence.append(indentationSpeech) if speakTextInfoState: speakTextInfoState.indentationCache=allIndentation - # Don't add this text if it is blank. - relativeBlank=True - for x in relativeSpeechSequence: - if isinstance(x,str) and not isBlank(x): - relativeBlank=False - break - if not relativeBlank: - speechSequence.extend(relativeSpeechSequence) + # only add this text if it is not blank. + notBlank = any( + not isBlank(x) for x in relativeSpeechSequence + if isinstance(x,str) + ) + if notBlank: isTextBlank=False - + if not processWhiteSPace: + speechSequence.extend(relativeSpeechSequence) + else: + # Add appropriate white space bookmarks + whiteSpaceTracker = info.copy() + whiteSpaceTracker.collapse() + for index, command in list(enumerate(relativeSpeechSequence)): + if not isinstance(command, _TextCommand): + continue + curCommandSequence = [] + endOfWhiteSpaceIndexes = [m.end() for m in re_white_space.finditer(command)] + start = 0 + for end in endOfWhiteSpaceIndexes: + text = command[start:end] + curCommandSequence.append(text) + whiteSpaceTracker.move(textInfos.UNIT_CHARACTER, len(text)) + if whiteSpaceTracker.compareEndPoints(info, "startToEnd") > 0: + break + bookmark = whiteSpaceTracker.bookmark + cb = partial(_whiteSpaceReachedCallback, bookmark=bookmark) + curCommandSequence.append(CallbackCommand(cb)) + # The whiteSpaceTracker shouldn't move past the end of the info we're speaking. + start = end + relativeSpeechSequence[index] = curCommandSequence + expandedRelativeSpeechSequence = [] + for x in relativeSpeechSequence: + if isinstance(x, list): + expandedRelativeSpeechSequence.extend(x) + else: + expandedRelativeSpeechSequence.append(x) + speechSequence.extend(expandedRelativeSpeechSequence) #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 if autoLanguageSwitching and lastLanguage is not None: speechSequence.append(LangChangeCommand(None)) From 322589704e8afe19c6b4ac77c7d893a64bcf3bfb Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Wed, 17 Jul 2019 16:09:57 +0200 Subject: [PATCH 05/17] Say all, use obj.selection = updater instead of updater.updateCaret() --- source/sayAllHandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/sayAllHandler.py b/source/sayAllHandler.py index 0874a292038..d736edde923 100644 --- a/source/sayAllHandler.py +++ b/source/sayAllHandler.py @@ -186,7 +186,7 @@ def nextLine(self): def _bookmarkReached(self, obj, bookmark): updater = obj.makeTextInfo(bookmark) if self.cursor == CURSOR_CARET: - updater.updateCaret() + obj.selection = updater if self.cursor != CURSOR_CARET or config.conf["reviewCursor"]["followCaret"]: api.setReviewPosition(updater, isCaret=self.cursor==CURSOR_CARET) From 3341210d1210d9507383bd7e3a0debe7f4d2c411 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Mon, 5 Aug 2019 13:24:21 +0200 Subject: [PATCH 06/17] Update source/speech/__init__.py Fixed typo Co-Authored-By: Derek Riemer --- source/speech/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/speech/__init__.py b/source/speech/__init__.py index 72bbac9fa6f..7fdec1ba42b 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -787,7 +787,7 @@ def speakTextInfo( priority=None ): onlyCache=reason==controlTypes.REASON_ONLYCACHE - processWhiteSPace: bool = _whiteSpaceReachedCallback is not None + processWhiteSpace: bool = _whiteSpaceReachedCallback is not None if isinstance(useCache,SpeakTextInfoState): speakTextInfoState=useCache elif useCache: From db74dc67cb8307f2dfb5820ae3db62db7070b8ed Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Mon, 5 Aug 2019 13:25:54 +0200 Subject: [PATCH 07/17] Apply suggestions from code review Co-Authored-By: Derek Riemer --- source/speech/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/speech/__init__.py b/source/speech/__init__.py index 7fdec1ba42b..4ed14944e90 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -1034,7 +1034,7 @@ def speakTextInfo( ) if notBlank: isTextBlank=False - if not processWhiteSPace: + if not processWhiteSpace: speechSequence.extend(relativeSpeechSequence) else: # Add appropriate white space bookmarks From 795b6caa3c5257f5fb2fe05ea96b9fc9c5206b77 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Fri, 9 Aug 2019 16:10:30 +0200 Subject: [PATCH 08/17] Make the caret per word setting a synth setting instead of global to speech --- source/config/configSpec.py | 2 +- source/gui/settingsDialogs.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 8eaeb0a2e81..c4c06ee2396 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -37,13 +37,13 @@ outputDevice = string(default=default) autoLanguageSwitching = boolean(default=true) autoDialectSwitching = boolean(default=false) - increaseSayAllCaretUpdates = boolean(default=false) [[__many__]] capPitchChange = integer(default=30,min=-100,max=100) sayCapForCapitals = boolean(default=false) beepForCapitals = boolean(default=false) useSpellingFunctionality = boolean(default=true) + increaseSayAllCaretUpdates = boolean(default=false) # Audio settings [audio] diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 97a1c1d47f2..c44b75e05bf 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -1255,7 +1255,7 @@ def makeSettings(self, settingsSizer): # voice settings panel. increaseSayAllCaretUpdatesText = _("Increase caret updates during say all") self.increaseSayAllCaretUpdatesCheckBox = settingsSizerHelper.addItem(wx.CheckBox(self, label = increaseSayAllCaretUpdatesText)) - self.increaseSayAllCaretUpdatesCheckBox.Value = config.conf["speech"]["increaseSayAllCaretUpdates"] + self.increaseSayAllCaretUpdatesCheckBox.Value = config.conf["speech"][self.driver.name]["increaseSayAllCaretUpdates"] def onSave(self): DriverSettingsMixin.onSave(self) @@ -1273,7 +1273,8 @@ def onSave(self): config.conf["speech"][self.driver.name]["sayCapForCapitals"]=self.sayCapForCapsCheckBox.IsChecked() config.conf["speech"][self.driver.name]["beepForCapitals"]=self.beepForCapsCheckBox.IsChecked() config.conf["speech"][self.driver.name]["useSpellingFunctionality"]=self.useSpellingFunctionalityCheckBox.IsChecked() - config.conf["speech"]["increaseSayAllCaretUpdates"] = self.increaseSayAllCaretUpdatesCheckBox.IsChecked() + config.conf["speech"][self.driver.name]["increaseSayAllCaretUpdates"] = self.increaseSayAllCaretUpdatesCheckBox.IsChecked() + class KeyboardSettingsPanel(SettingsPanel): # Translators: This is the label for the keyboard settings panel. From c1d0fb254d7557038b565f5227e31a23541129bf Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Fri, 9 Aug 2019 16:12:45 +0200 Subject: [PATCH 09/17] Rename of private _TextCommand class to _TextChunk --- source/speech/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/source/speech/__init__.py b/source/speech/__init__.py index 4ed14944e90..91364fa9dc8 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -771,7 +771,7 @@ def _speakTextInfo_addMath(speechSequence, info, field): re_white_space = re.compile(r"(\s+|$)", re.DOTALL) -class _TextCommand(str): +class _TextChunk(str): """str subclass to distinguish normal text from field text when processing text info speech.""" def speakTextInfo( @@ -963,9 +963,9 @@ def speakTextInfo( indentationDone=True if command: if inTextChunk: - relativeSpeechSequence[-1] = _TextCommand(relativeSpeechSequence[-1] + command) + relativeSpeechSequence[-1] = _TextChunk(relativeSpeechSequence[-1] + command) else: - relativeSpeechSequence.append(_TextCommand(command)) + relativeSpeechSequence.append(_TextChunk(command)) inTextChunk=True elif isinstance(command,textInfos.FieldCommand): newLanguage=None @@ -1041,7 +1041,7 @@ def speakTextInfo( whiteSpaceTracker = info.copy() whiteSpaceTracker.collapse() for index, command in list(enumerate(relativeSpeechSequence)): - if not isinstance(command, _TextCommand): + if not isinstance(command, _TextChunk): continue curCommandSequence = [] endOfWhiteSpaceIndexes = [m.end() for m in re_white_space.finditer(command)] From f67bbd35c47841549b62ecb24c60b7e326a5c5d8 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Fri, 9 Aug 2019 16:14:55 +0200 Subject: [PATCH 10/17] Rename cb to callback --- source/speech/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/speech/__init__.py b/source/speech/__init__.py index 91364fa9dc8..10831f0165b 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -1053,8 +1053,8 @@ def speakTextInfo( if whiteSpaceTracker.compareEndPoints(info, "startToEnd") > 0: break bookmark = whiteSpaceTracker.bookmark - cb = partial(_whiteSpaceReachedCallback, bookmark=bookmark) - curCommandSequence.append(CallbackCommand(cb)) + callback = partial(_whiteSpaceReachedCallback, bookmark=bookmark) + curCommandSequence.append(CallbackCommand(callback)) # The whiteSpaceTracker shouldn't move past the end of the info we're speaking. start = end relativeSpeechSequence[index] = curCommandSequence From 4947f9794619faa84dab0475d320fba0194515a5 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Fri, 9 Aug 2019 16:19:30 +0200 Subject: [PATCH 11/17] Add type info to speech.speakTextInfo --- source/speech/__init__.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/source/speech/__init__.py b/source/speech/__init__.py index 10831f0165b..cb506bf4c12 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -775,16 +775,16 @@ class _TextChunk(str): """str subclass to distinguish normal text from field text when processing text info speech.""" def speakTextInfo( - info, - useCache=True, - formatConfig=None, - unit=None, - reason=controlTypes.REASON_QUERY, - _prefixSpeechCommand=None, - _whiteSpaceReachedCallback: Optional[Callable[[Any], None]] = None, - onlyInitialFields=False, - suppressBlanks=False, - priority=None + info: textInfos.TextInfo, + useCache: bool = True, + formatConfig: Optional[dict]=None, + unit: Optional[str] = None, + reason: str = controlTypes.REASON_QUERY, + _prefixSpeechCommand: SpeechCommand = None, + _whiteSpaceReachedCallback: Optional[Callable[[Any], None]] = None, + onlyInitialFields: bool = False, + suppressBlanks: bool = False, + priority: int = None ): onlyCache=reason==controlTypes.REASON_ONLYCACHE processWhiteSpace: bool = _whiteSpaceReachedCallback is not None From 28abd3c150e1717d47e8435dd0805645c60a31e3 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Fri, 9 Aug 2019 16:20:59 +0200 Subject: [PATCH 12/17] re.UNICODE is now obsolete, no longer use it --- source/speech/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/speech/__init__.py b/source/speech/__init__.py index cb506bf4c12..b1402dae402 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -1811,7 +1811,7 @@ def getTableInfoSpeech(tableInfo,oldTableInfo,extraDetail=False): textList.append(_("row %s")%rowNumber) return " ".join(textList) -re_last_pause=re.compile(r"^(.*?(?<=[^\s.!?])[.!?][\"'”’)]?(?:\s+|$))(.*$)",re.DOTALL|re.UNICODE) +re_last_pause=re.compile(r"^(.*?(?<=[^\s.!?])[.!?][\"'”’)]?(?:\s+|$))(.*$)", re.DOTALL) def speakWithoutPauses(speechSequence,detectBreaks=True): """ From c832f7e6ff86f21a3bc3f55ddfa7826f42de21d4 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Fri, 9 Aug 2019 16:22:50 +0200 Subject: [PATCH 13/17] Fix copyright of speech/commands. This is part of speech refactor so is code from 2017 and later --- source/speech/commands.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/source/speech/commands.py b/source/speech/commands.py index 2d6f98fd7a9..93e876553ec 100644 --- a/source/speech/commands.py +++ b/source/speech/commands.py @@ -1,8 +1,9 @@ # -*- coding: UTF-8 -*- -#A part of NonVisual Desktop Access (NVDA) -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. -#Copyright (C) 2006-2019 NV Access Limited +# speech/commands.py +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2017-2019 NV Access Limited, Babbage B.V. """Commands that can be embedded in a speech sequence for changing synth parameters, playing sounds or running other callbacks.""" From 7af96e2887c216175ca91cd5ccb72f9f65d68f61 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Fri, 9 Aug 2019 16:50:48 +0200 Subject: [PATCH 14/17] linter satisfaction --- source/gui/settingsDialogs.py | 12 +++++++++--- source/sayAllHandler.py | 10 +++++++--- source/speech/__init__.py | 24 +++++++++++++++++++----- source/speech/commands.py | 9 +++++---- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index c44b75e05bf..8a4a047f09b 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -1254,8 +1254,12 @@ def makeSettings(self, settingsSizer): # Translators: This is the label for a checkbox in the # voice settings panel. increaseSayAllCaretUpdatesText = _("Increase caret updates during say all") - self.increaseSayAllCaretUpdatesCheckBox = settingsSizerHelper.addItem(wx.CheckBox(self, label = increaseSayAllCaretUpdatesText)) - self.increaseSayAllCaretUpdatesCheckBox.Value = config.conf["speech"][self.driver.name]["increaseSayAllCaretUpdates"] + self.increaseSayAllCaretUpdatesCheckBox = settingsSizerHelper.addItem( + wx.CheckBox(self, label=increaseSayAllCaretUpdatesText) + ) + self.increaseSayAllCaretUpdatesCheckBox.SetValue( + config.conf["speech"][self.driver.name]["increaseSayAllCaretUpdates"] + ) def onSave(self): DriverSettingsMixin.onSave(self) @@ -1273,7 +1277,9 @@ def onSave(self): config.conf["speech"][self.driver.name]["sayCapForCapitals"]=self.sayCapForCapsCheckBox.IsChecked() config.conf["speech"][self.driver.name]["beepForCapitals"]=self.beepForCapsCheckBox.IsChecked() config.conf["speech"][self.driver.name]["useSpellingFunctionality"]=self.useSpellingFunctionalityCheckBox.IsChecked() - config.conf["speech"][self.driver.name]["increaseSayAllCaretUpdates"] = self.increaseSayAllCaretUpdatesCheckBox.IsChecked() + config.conf["speech"][self.driver.name]["increaseSayAllCaretUpdates"] = ( + self.increaseSayAllCaretUpdatesCheckBox.IsChecked() + ) class KeyboardSettingsPanel(SettingsPanel): diff --git a/source/sayAllHandler.py b/source/sayAllHandler.py index d736edde923..c6f0151e389 100644 --- a/source/sayAllHandler.py +++ b/source/sayAllHandler.py @@ -153,14 +153,18 @@ def nextLine(self): bookmark=bookmark, state=self.speakTextInfoState.copy() )) - spoke = speech.speakTextInfo(self.reader, unit=textInfos.UNIT_READINGCHUNK, - reason=controlTypes.REASON_SAYALL, _prefixSpeechCommand=cb, + spoke = speech.speakTextInfo( + self.reader, + unit=textInfos.UNIT_READINGCHUNK, + reason=controlTypes.REASON_SAYALL, + _prefixSpeechCommand=cb, _whiteSpaceReachedCallback=( self.endOfWhiteSpaceReached if config.conf["speech"]["increaseSayAllCaretUpdates"] else None ), - useCache=self.speakTextInfoState) + useCache=self.speakTextInfoState + ) # Collapse to the end of this line, ready to read the next. try: self.reader.collapse(end=True) diff --git a/source/speech/__init__.py b/source/speech/__init__.py index b1402dae402..99449e34639 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -30,9 +30,18 @@ import characterProcessing import languageHandler from .commands import * +from .commands import ( + SpeechCommand, + PitchCommand, + LangChangeCommand, + CharacterModeCommand, + BeepCommand, + EndUtteranceCommand, + CallbackCommand, +) from typing import Optional, Callable, Any from functools import partial -from types import GeneratorType + speechMode_off=0 speechMode_beeps=1 @@ -769,15 +778,19 @@ def _speakTextInfo_addMath(speechSequence, info, field): except (NotImplementedError, LookupError): return + re_white_space = re.compile(r"(\s+|$)", re.DOTALL) + class _TextChunk(str): """str subclass to distinguish normal text from field text when processing text info speech.""" -def speakTextInfo( + +# Ignore C901: 'speakTextInfo' is too complex +def speakTextInfo( # noqa: C901 info: textInfos.TextInfo, useCache: bool = True, - formatConfig: Optional[dict]=None, + formatConfig: Optional[dict] = None, unit: Optional[str] = None, reason: str = controlTypes.REASON_QUERY, _prefixSpeechCommand: SpeechCommand = None, @@ -1030,7 +1043,7 @@ def speakTextInfo( # only add this text if it is not blank. notBlank = any( not isBlank(x) for x in relativeSpeechSequence - if isinstance(x,str) + if isinstance(x, str) ) if notBlank: isTextBlank=False @@ -1811,7 +1824,8 @@ def getTableInfoSpeech(tableInfo,oldTableInfo,extraDetail=False): textList.append(_("row %s")%rowNumber) return " ".join(textList) -re_last_pause=re.compile(r"^(.*?(?<=[^\s.!?])[.!?][\"'”’)]?(?:\s+|$))(.*$)", re.DOTALL) + +re_last_pause = re.compile(r"^(.*?(?<=[^\s.!?])[.!?][\"'”’)]?(?:\s+|$))(.*$)", re.DOTALL) def speakWithoutPauses(speechSequence,detectBreaks=True): """ diff --git a/source/speech/commands.py b/source/speech/commands.py index 93e876553ec..ea6c92278ca 100644 --- a/source/speech/commands.py +++ b/source/speech/commands.py @@ -36,13 +36,12 @@ class IndexCommand(SynthCommand): NVDA handles the indexing and dispatches callbacks as appropriate. """ - def __init__(self,index): + def __init__(self, index: int): """ @param index: the value of this index - @type index: integer """ - if not isinstance(index,int): - raise TypeError("index must be int, not %s"%type(index)) + if not isinstance(index, int): + raise TypeError(f"index must be int, not {type(index)}") self.index=index def __repr__(self): @@ -251,8 +250,10 @@ def run(self): otherwise it will block production of further speech and or other functionality in NVDA. """ + SUPPORTED_CALLBACK_TYPES = (FunctionType, MethodType, partial) + class CallbackCommand(BaseCallbackCommand): """ Call a function when speech reaches this point. From 21c310d9b1704c8f380af55925e3b9873685769a Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Thu, 21 Nov 2019 11:26:14 +0100 Subject: [PATCH 15/17] Linting fix --- source/speech/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/source/speech/__init__.py b/source/speech/__init__.py index 8c804291e9b..5e2d8c5b126 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -49,7 +49,6 @@ WaveFileCommand, ConfigProfileTriggerCommand, ) -import languageHandler from functools import partial from . import types from .types import SpeechSequence From 60f53b5cabe26801e0fab5db76df0777d5d5ecc9 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Tue, 18 Feb 2020 08:51:54 +0100 Subject: [PATCH 16/17] Fix more merge conflicts --- source/sayAllHandler.py | 3 +-- source/speech/__init__.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/source/sayAllHandler.py b/source/sayAllHandler.py index d995d086bfb..71d656a0b95 100644 --- a/source/sayAllHandler.py +++ b/source/sayAllHandler.py @@ -13,7 +13,6 @@ import api import textInfos import queueHandler -from functools import partial import time import winKernel @@ -167,7 +166,7 @@ def _onLineReached(obj=self.reader.obj, state=self.speakTextInfoState.copy()): _prefixSpeechCommand=cb, _whiteSpaceReachedCallback=( self.endOfWhiteSpaceReached - if config.conf["speech"]["increaseSayAllCaretUpdates"] + if config.conf["speech"][synthDriverHandler.getSynth().name]["increaseSayAllCaretUpdates"] else None ), useCache=self.speakTextInfoState diff --git a/source/speech/__init__.py b/source/speech/__init__.py index ba0e68d0cd8..077dadbec35 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -50,7 +50,6 @@ WaveFileCommand, ConfigProfileTriggerCommand, ) -from functools import partial from . import types from .types import SpeechSequence, SequenceItemT from typing import Optional, Dict, List, Any, Generator, Union, Callable, Iterator, Tuple @@ -1066,6 +1065,7 @@ def speakTextInfo( unit, reason, _prefixSpeechCommand, + _whiteSpaceReachedCallback, onlyInitialFields, suppressBlanks ) @@ -1085,6 +1085,7 @@ def getTextInfoSpeech( # noqa: C901 unit: Optional[str] = None, reason: OutputReason = controlTypes.REASON_QUERY, _prefixSpeechCommand: Optional[SpeechCommand] = None, + _whiteSpaceReachedCallback: Optional[Callable[[Any], None]] = None, onlyInitialFields: bool = False, suppressBlanks: bool = False ) -> Generator[SpeechSequence, None, bool]: @@ -1419,8 +1420,13 @@ def isControlEndFieldCommand(x): if whiteSpaceTracker.compareEndPoints(info, "startToEnd") > 0: break bookmark = whiteSpaceTracker.bookmark - callback = partial(_whiteSpaceReachedCallback, bookmark=bookmark) - curCommandSequence.append(CallbackCommand(callback)) + + def _onWhiteSpaceReached(bookmark=bookmark): + return _whiteSpaceReachedCallback(bookmark=bookmark) + + curCommandSequence.append( + CallbackCommand(_onWhiteSpaceReached, name="getTextInfoSpeech:whiteSpaceReached") + ) # The whiteSpaceTracker shouldn't move past the end of the info we're speaking. start = end relativeSpeechSequence[index] = curCommandSequence From 5f2154c481c2f956b0e974b83c2d352c22d379bc Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Tue, 18 Feb 2020 11:16:09 +0100 Subject: [PATCH 17/17] Support test_SpeechWithoutPauses --- source/speech/commands.py | 2 +- tests/unit/test_SpeechWithoutPauses.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/speech/commands.py b/source/speech/commands.py index d4c37ccdcab..b185ef55d2a 100644 --- a/source/speech/commands.py +++ b/source/speech/commands.py @@ -266,7 +266,7 @@ def __init__(self, callback: Union[SUPPORTED_CALLBACK_TYPES], name: Optional[str if not isinstance(callback, SUPPORTED_CALLBACK_TYPES): raise TypeError( "callback must be one of %s, not %s" - % (", ".join(SUPPORTED_CALLBACK_TYPES), type(callback)) + % (", ".join(str(t) for t in SUPPORTED_CALLBACK_TYPES), type(callback)) ) self._callback = callback if name: diff --git a/tests/unit/test_SpeechWithoutPauses.py b/tests/unit/test_SpeechWithoutPauses.py index a2559c6dc91..6539f80fadb 100644 --- a/tests/unit/test_SpeechWithoutPauses.py +++ b/tests/unit/test_SpeechWithoutPauses.py @@ -128,7 +128,7 @@ class TestOldImplVsNew(unittest.TestCase): """A test that verifies that the new implementation of SpeechWithoutPauses matches the old behavior. """ def test_stopsSpeakingCase(self): - callbackCommand = CallbackCommand(name="dummy", callback=None) + callbackCommand = CallbackCommand(name="dummy", callback=lambda: None) lang_en = LangChangeCommand('en') lang_default = LangChangeCommand(None)