Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Say all, move caret per word instead of per line/sentence, taken over after abandoned 9937 #11658

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8f0921c
Split lineReached into a separate _bookmarkReached function
Jul 15, 2019
ce53201
Change the re_last_pause regex to consume less of a line containing m…
Jul 17, 2019
ff1872c
Add a nice repr to callback commands for easier debugging
Jul 17, 2019
07b90c8
Make the caret follow say all more closely
Jul 16, 2019
3225897
Say all, use obj.selection = updater instead of updater.updateCaret()
Jul 17, 2019
3341210
Update source/speech/__init__.py
LeonarddeR Aug 5, 2019
db74dc6
Apply suggestions from code review
LeonarddeR Aug 5, 2019
7bf925c
Merge remote-tracking branch 'origin/master' into sayAll
Aug 9, 2019
795b6ca
Make the caret per word setting a synth setting instead of global to …
Aug 9, 2019
c1d0fb2
Rename of private _TextCommand class to _TextChunk
Aug 9, 2019
f67bbd3
Rename cb to callback
Aug 9, 2019
4947f97
Add type info to speech.speakTextInfo
Aug 9, 2019
28abd3c
re.UNICODE is now obsolete, no longer use it
Aug 9, 2019
c832f7e
Fix copyright of speech/commands. This is part of speech refactor so …
Aug 9, 2019
7af96e2
linter satisfaction
Aug 9, 2019
3b40b04
Merge remote-tracking branch 'origin/master' into sayAll
Oct 23, 2019
000d680
Merge remote-tracking branch 'origin/master' into sayAll
Nov 6, 2019
4f5d791
Merge remote-tracking branch 'origin/master' into sayAll
LeonarddeR Nov 6, 2019
9e9918b
Merge remote-tracking branch 'origin/master' into sayAll
Nov 21, 2019
21c310d
Linting fix
Nov 21, 2019
1dc834d
Merge remote-tracking branch 'origin/master' into sayAll
LeonarddeR Feb 17, 2020
60f53b5
Fix more merge conflicts
LeonarddeR Feb 18, 2020
5f2154c
Support test_SpeechWithoutPauses
LeonarddeR Feb 18, 2020
aad4c99
Merge remote-tracking branch 'origin/master' into sayAll
LeonarddeR May 2, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions source/config/configSpec.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
sayCapForCapitals = boolean(default=false)
beepForCapitals = boolean(default=false)
useSpellingFunctionality = boolean(default=true)
increaseSayAllCaretUpdates = boolean(default=false)

# Audio settings
[audio]
Expand Down
14 changes: 14 additions & 0 deletions source/gui/settingsDialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1468,6 +1468,16 @@ def makeSettings(self, settingsSizer):
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.SetValue(
config.conf["speech"][self.driver.name]["increaseSayAllCaretUpdates"]
)

def onSave(self):
AutoSettingsMixin.onSave(self)

Expand All @@ -1484,6 +1494,10 @@ 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()
)


class KeyboardSettingsPanel(SettingsPanel):
# Translators: This is the label for the keyboard settings panel.
Expand Down
32 changes: 27 additions & 5 deletions source/sayAllHandler.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -12,6 +13,7 @@
import api
import textInfos
import queueHandler
import time
import winKernel

CURSOR_CARET = 0
Expand Down Expand Up @@ -101,6 +103,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
Expand Down Expand Up @@ -167,6 +171,12 @@ def _onLineReached(obj=self.reader.obj, state=state):
self.reader,
unit=textInfos.UNIT_READINGCHUNK,
reason=controlTypes.REASON_SAYALL,
_prefixSpeechCommand=cb,
_whiteSpaceReachedCallback=(
self.endOfWhiteSpaceReached
if config.conf["speech"][synthDriverHandler.getSynth().name]["increaseSayAllCaretUpdates"]
else None
),
useCache=state
)
seq = list(speech._flattenNestedSequences(speechGen))
Expand Down Expand Up @@ -198,15 +208,27 @@ def _onLineReached(obj=self.reader.obj, state=state):
# 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()
obj.selection = updater
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()
self._bookmarkReached(obj, bookmark)

winKernel.SetThreadExecutionState(winKernel.ES_SYSTEM_REQUIRED | winKernel.ES_DISPLAY_REQUIRED)

if self.numBufferedLines == 0:
# This was the last line spoken, so move on.
self.nextLine()
Expand Down
69 changes: 58 additions & 11 deletions source/speech/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
WaveFileCommand,
ConfigProfileTriggerCommand,
)

from . import types
from .types import (
SpeechSequence,
SequenceItemT,
Expand Down Expand Up @@ -1042,13 +1042,21 @@ def _extendSpeechSequence_addMathForTextInfo(
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(
info: textInfos.TextInfo,
useCache: Union[bool, SpeakTextInfoState] = True,
formatConfig: Dict[str, bool] = None,
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,
priority: Optional[Spri] = None
Expand All @@ -1060,6 +1068,7 @@ def speakTextInfo(
unit,
reason,
_prefixSpeechCommand,
_whiteSpaceReachedCallback,
onlyInitialFields,
suppressBlanks
)
Expand Down Expand Up @@ -1088,10 +1097,12 @@ 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]:
onlyCache=reason==controlTypes.REASON_ONLYCACHE
processWhiteSpace: bool = _whiteSpaceReachedCallback is not None
if isinstance(useCache,SpeakTextInfoState):
speakTextInfoState=useCache
elif useCache:
Expand Down Expand Up @@ -1313,9 +1324,9 @@ def isControlEndFieldCommand(x):
indentationDone=True
if command:
if inTextChunk:
relativeSpeechSequence[-1]+=command
relativeSpeechSequence[-1] = _TextChunk(relativeSpeechSequence[-1] + command)
else:
relativeSpeechSequence.append(command)
relativeSpeechSequence.append(_TextChunk(command))
inTextChunk=True
elif isinstance(command,textInfos.FieldCommand):
newLanguage=None
Expand Down Expand Up @@ -1397,16 +1408,49 @@ def isControlEndFieldCommand(x):
else:
speechSequence.extend(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:
shouldConsiderTextInfoBlank = 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, _TextChunk):
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

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
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(
Expand Down Expand Up @@ -2382,6 +2426,9 @@ def getTableInfoSpeech(
return textList


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


def _yieldIfNonEmpty(seq: SpeechSequence):
"""Helper method to yield the sequence if it is not None or empty."""
if seq:
Expand Down
46 changes: 34 additions & 12 deletions source/speech/commands.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
# -*- 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 abc import ABCMeta, abstractmethod
from typing import Optional

import config
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.
Expand All @@ -32,12 +37,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 ValueError("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):
Expand Down Expand Up @@ -245,6 +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.
Expand All @@ -253,18 +262,31 @@ class CallbackCommand(BaseCallbackCommand):
otherwise it will block production of further speech and or other functionality in NVDA.
"""

def __init__(self, callback, name: Optional[str] = None):
def __init__(self, callback: Union[SUPPORTED_CALLBACK_TYPES], name: Optional[str] = None):
if not isinstance(callback, SUPPORTED_CALLBACK_TYPES):
raise TypeError(
"callback must be one of %s, not %s"
% (", ".join(str(t) for t in SUPPORTED_CALLBACK_TYPES), type(callback))
)
self._callback = callback
self._name = name if name else repr(callback)
if name:
self._name = name
elif isinstance(callback, partial):
self._name = "partial[{}]".format(callback.func.__qualname__)
else:
self.name = callback.__qualname__
self._signature = str(signature(callback))

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

def __repr__(self):
return "CallbackCommand(name={name})".format(
name=self._name
return "CallbackCommand({name}{signature})".format(
name=self._name,
signature=self._signature
)


class BeepCommand(BaseCallbackCommand):
"""Produce a beep.
"""
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_SpeechWithoutPauses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down