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

Speak all symbols when moving by words (#11779) #11856

Merged
merged 33 commits into from Jul 7, 2021
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
86592b0
Speak all symbols when moving by words (#11779)
JulienCochuyt Nov 23, 2020
cbbbacc
Merge branch 'master' into i11779-moveByWordSymbolLevel
JulienCochuyt Dec 13, 2020
2614f46
Review action https://github.com/nvaccess/nvda/pull/11856#issuecommen…
JulienCochuyt Dec 13, 2020
56d97fe
Add an unassigned global command to toggle symbolLevelWord
JulienCochuyt Dec 14, 2020
db91a19
Add missing translators comment
JulienCochuyt Dec 14, 2020
14f12ca
Merge branch 'master' into i11779-moveByWordSymbolLevel
JulienCochuyt Dec 16, 2020
7f18e8d
User Guide: SpeechSettingsSymbolLevelWord
JulienCochuyt Dec 16, 2020
b92b18a
Merge remote-tracking branch 'upstream/master' into i11779-moveByWord…
JulienCochuyt Apr 23, 2021
300e174
Merge remote-tracking branch 'upstream/master' into i11779-moveByWord…
JulienCochuyt May 7, 2021
46e8163
Merge remote-tracking branch 'upstream/master' into i11779-moveByWord…
JulienCochuyt May 19, 2021
f4b99b4
Review action https://github.com/nvaccess/nvda/pull/11856#discussion_…
JulienCochuyt May 24, 2021
30cbe69
Review action https://github.com/nvaccess/nvda/pull/11856#discussion_…
JulienCochuyt May 24, 2021
9bc1df2
Review action https://github.com/nvaccess/nvda/pull/11856#discussion_…
JulienCochuyt May 24, 2021
06cf4df
Review action https://github.com/nvaccess/nvda/pull/11856#discussion_…
JulienCochuyt May 24, 2021
576ce99
Review action https://github.com/nvaccess/nvda/pull/11856#discussion_…
JulienCochuyt May 24, 2021
bb53010
Merge remote-tracking branch 'upstream/master' into i11779-moveByWord…
JulienCochuyt May 24, 2021
698f5b7
Merge branch 'master' into i11779-moveByWordSymbolLevel
seanbudd Jun 7, 2021
d89a533
add notepad system tests for symbol level word
seanbudd Jun 7, 2021
304d940
unbreak sentence in userguide for translators
seanbudd Jun 7, 2021
2f61ca6
convert symlvl to intenum
seanbudd Jun 7, 2021
5cc726b
update changes
seanbudd Jun 7, 2021
1709685
update changes
seanbudd Jun 7, 2021
e58f0e2
Merge branch 'i11779-moveByWordSymbolLevel' of github.com:accessoluti…
seanbudd Jun 7, 2021
d4f18b8
undo speech chmod
seanbudd Jun 8, 2021
76dec02
fix typing
seanbudd Jun 8, 2021
d4ccb85
eliminate the tristate checkbox
seanbudd Jun 8, 2021
2853f5d
remove sanity check in tests
seanbudd Jun 8, 2021
5972e84
translation category
seanbudd Jun 8, 2021
9f03857
add translator comment
seanbudd Jun 9, 2021
a818141
Merge branch 'master' into i11779-moveByWordSymbolLevel
seanbudd Jun 15, 2021
c4a5ee9
Merge branch 'master' into i11779-moveByWordSymbolLevel
seanbudd Jul 7, 2021
2daaca2
fixup! Merge branch 'master' into i11779-moveByWordSymbolLevel
seanbudd Jul 7, 2021
cf1b1ed
change system tests
seanbudd Jul 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
72 changes: 43 additions & 29 deletions source/characterProcessing.py
@@ -1,10 +1,11 @@
#characterProcessing.py
#A part of NonVisual Desktop Access (NVDA)
#Copyright (C) 2010-2018 NV Access Limited, World Light Information Limited, Hong Kong Blind Union, Babbage B.V.
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.

import time
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2010-2021 NV Access Limited, World Light Information Limited,
# Hong Kong Blind Union, Babbage B.V., Julien Cochuyt
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

from versionInfo import version_year
from enum import IntEnum
import os
import codecs
import collections
Expand Down Expand Up @@ -124,26 +125,40 @@ def getCharacterDescription(locale,character):
desc=getCharacterDescription('en',character)
return desc


# Speech symbol levels
SYMLVL_NONE = 0
SYMLVL_SOME = 100
SYMLVL_MOST = 200
SYMLVL_ALL = 300
SYMLVL_CHAR = 1000
class SYMLVL(IntEnum):
NONE = 0
SOME = 100
MOST = 200
ALL = 300
CHAR = 1000
UNCHANGED = -1


# The following SYMLVL_ constants are deprecated in #11856 but remain to maintain backwards compatibility.
# Remove these in 2022.1 and replace instances using them with the SYMLVL IntEnum.
if version_year < 2022:
seanbudd marked this conversation as resolved.
Show resolved Hide resolved
SYMLVL_NONE = SYMLVL.NONE
SYMLVL_SOME = SYMLVL.SOME
SYMLVL_MOST = SYMLVL.MOST
SYMLVL_ALL = SYMLVL.ALL
SYMLVL_CHAR = SYMLVL.CHAR
seanbudd marked this conversation as resolved.
Show resolved Hide resolved

SPEECH_SYMBOL_LEVEL_LABELS = {
# Translators: The level at which the given symbol will be spoken.
SYMLVL_NONE: pgettext("symbolLevel", "none"),
SYMLVL.NONE: pgettext("symbolLevel", "none"),
# Translators: The level at which the given symbol will be spoken.
SYMLVL_SOME: pgettext("symbolLevel", "some"),
SYMLVL.SOME: pgettext("symbolLevel", "some"),
# Translators: The level at which the given symbol will be spoken.
SYMLVL_MOST: pgettext("symbolLevel", "most"),
SYMLVL.MOST: pgettext("symbolLevel", "most"),
# Translators: The level at which the given symbol will be spoken.
SYMLVL_ALL: pgettext("symbolLevel", "all"),
SYMLVL.ALL: pgettext("symbolLevel", "all"),
# Translators: The level at which the given symbol will be spoken.
SYMLVL_CHAR: pgettext("symbolLevel", "character"),
SYMLVL.CHAR: pgettext("symbolLevel", "character"),
}
CONFIGURABLE_SPEECH_SYMBOL_LEVELS = (SYMLVL_NONE, SYMLVL_SOME, SYMLVL_MOST, SYMLVL_ALL)
SPEECH_SYMBOL_LEVELS = CONFIGURABLE_SPEECH_SYMBOL_LEVELS + (SYMLVL_CHAR,)
CONFIGURABLE_SPEECH_SYMBOL_LEVELS = (SYMLVL.NONE, SYMLVL.SOME, SYMLVL.MOST, SYMLVL.ALL)
SPEECH_SYMBOL_LEVELS = CONFIGURABLE_SPEECH_SYMBOL_LEVELS + (SYMLVL.CHAR,)

# Speech symbol preserve modes
SYMPRES_NEVER = 0
Expand Down Expand Up @@ -254,11 +269,11 @@ def _loadSymbolField(self, input, inputMap=None):
}
IDENTIFIER_ESCAPES_OUTPUT = {v: k for k, v in IDENTIFIER_ESCAPES_INPUT.items()}
LEVEL_INPUT = {
"none": SYMLVL_NONE,
"some": SYMLVL_SOME,
"most": SYMLVL_MOST,
"all": SYMLVL_ALL,
"char": SYMLVL_CHAR,
"none": SYMLVL.NONE,
"some": SYMLVL.SOME,
"most": SYMLVL.MOST,
"all": SYMLVL.ALL,
"char": SYMLVL.CHAR,
}
LEVEL_OUTPUT = {v:k for k, v in LEVEL_INPUT.items()}
PRESERVE_INPUT = {
Expand Down Expand Up @@ -484,7 +499,7 @@ def __init__(self, locale):
pass
continue
if symbol.level is None:
symbol.level = SYMLVL_ALL
symbol.level = SYMLVL.ALL
if symbol.preserve is None:
symbol.preserve = SYMPRES_NEVER
if symbol.displayName is None:
Expand Down Expand Up @@ -660,13 +675,12 @@ def isBuiltin(self, symbolIdentifier):

_localeSpeechSymbolProcessors = LocaleDataMap(SpeechSymbolProcessor)

def processSpeechSymbols(locale, text, level):

def processSpeechSymbols(locale: str, text: str, level: SYMLVL):
"""Process some text, converting symbols according to desired pronunciation.
@param locale: The locale of the text.
@type locale: str
@param text: The text to process.
@type text: str
@param level: The symbol level to use; one of the SYMLVL_* constants.
@param level: The symbol level to use.
"""
try:
ss = _localeSpeechSymbolProcessors.fetchLocaleData(locale)
Expand Down
4 changes: 3 additions & 1 deletion source/config/configSpec.py
@@ -1,6 +1,6 @@
# -*- coding: UTF-8 -*-
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2006-2020 NV Access Limited, Babbage B.V., Davy Kager, Bill Dengler
# Copyright (C) 2006-2020 NV Access Limited, Babbage B.V., Davy Kager, Bill Dengler, Julien Cochuyt
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

Expand Down Expand Up @@ -31,6 +31,8 @@
# The synthesizer to use
synth = string(default=auto)
symbolLevel = integer(default=100)
# Speak all symbols when reviewing by word, uses editor specific implementation if false
symbolLevelWordAll = boolean(default=true)
trustVoiceLanguage = boolean(default=true)
includeCLDR = boolean(default=True)
beepSpeechModePitch = integer(default=10000,min=50,max=11025)
Expand Down
21 changes: 21 additions & 0 deletions source/globalCommands.py
Expand Up @@ -895,6 +895,27 @@ def script_cycleSpeechSymbolLevel(self,gesture):
# %s will be replaced with the symbol level; e.g. none, some, most and all.
ui.message(_("Symbol level %s") % name)

@script(
# Translators: Input help mode message for a command.
description=_("Toggle the announcement of all punctuation and symbols when reviewing by word"),
category=SCRCAT_SPEECH
)
def script_toggleSpeechSymbolLevelWordAll(self, gesture):
symbolLevelWordAll = config.conf["speech"]["symbolLevelWordAll"]
if symbolLevelWordAll:
# Translators: Used to report the new state of a setting which is toggled via a command.
reportedState = pgettext("command toggle", "off")
else:
# Translators: Used to report the new state of a setting which is toggled via a command.
reportedState = pgettext("command toggle", "on")
config.conf["speech"]["symbolLevelWordAll"] = not symbolLevelWordAll
ui.message(
# Translators: Reported when toggling a speech setting
_("Speak all punctuation and symbols when reviewing by word: {state}").format(
state=reportedState
)
)

@script(
# Translators: Input help mode message for move mouse to navigator object command.
description=_("Moves the mouse pointer to the current navigator object"),
Expand Down
20 changes: 17 additions & 3 deletions source/gui/settingsDialogs.py
Expand Up @@ -2,7 +2,8 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2006-2020 NV Access Limited, Peter Vágner, Aleksey Sadovoy,
# Rui Batista, Joseph Lee, Heiko Folkerts, Zahari Yurukov, Leonard de Ruijter,
# Derek Riemer, Babbage B.V., Davy Kager, Ethan Holliger, Bill Dengler, Thomas Stivers
# Derek Riemer, Babbage B.V., Davy Kager, Ethan Holliger, Bill Dengler, Thomas Stivers,
# Julien Cochuyt
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
import logging
Expand Down Expand Up @@ -1492,6 +1493,16 @@ def makeSettings(self, settingsSizer):
characterProcessing.CONFIGURABLE_SPEECH_SYMBOL_LEVELS.index(curLevel)
)

self.symbolLevelWordAll = settingsSizerHelper.addItem(
wx.CheckBox(
self,
# Translators: The label for a setting in the Speech category
label=_("Speak all punctuation and symbols when reviewing by &word"),
)
)
self.bindHelpEvent("SpeechSettingsSymbolLevelWord", self.symbolLevelWordAll)
self.symbolLevelWordAll.SetValue(config.conf["speech"]["symbolLevelWordAll"])

# Translators: This is the label for a checkbox in the
# voice settings panel (if checked, text will be read using the voice for the language of the text).
trustVoiceLanguageText = _("Trust voice's language when processing characters and symbols")
Expand Down Expand Up @@ -1579,8 +1590,11 @@ def onSave(self):

config.conf["speech"]["autoLanguageSwitching"] = self.autoLanguageSwitchingCheckbox.IsChecked()
config.conf["speech"]["autoDialectSwitching"] = self.autoDialectSwitchingCheckbox.IsChecked()
config.conf["speech"]["symbolLevel"]=characterProcessing.CONFIGURABLE_SPEECH_SYMBOL_LEVELS[self.symbolLevelList.GetSelection()]
config.conf["speech"]["trustVoiceLanguage"]=self.trustVoiceLanguageCheckbox.IsChecked()
config.conf["speech"]["symbolLevel"] = characterProcessing.CONFIGURABLE_SPEECH_SYMBOL_LEVELS[
self.symbolLevelList.GetSelection()
]
config.conf["speech"]["symbolLevelWordAll"] = self.symbolLevelWordAll.IsChecked()
config.conf["speech"]["trustVoiceLanguage"] = self.trustVoiceLanguageCheckbox.IsChecked()
currentIncludeCLDR = config.conf["speech"]["includeCLDR"]
config.conf["speech"]["includeCLDR"] = newIncludeCldr = self.includeCLDRCheckbox.IsChecked()
if currentIncludeCLDR is not newIncludeCldr:
Expand Down
10 changes: 8 additions & 2 deletions source/speech/speech.py
Expand Up @@ -1126,8 +1126,14 @@ def speakTextInfo(
)

speechGen = GeneratorWithReturn(speechGen)
symbolLevel: Optional[characterProcessing.SYMLVL] = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given SYMLVL has a value for None, it would be great if this was not optional anymore. Instead just always had a valid (typed) value.

Copy link
Member

@seanbudd seanbudd Jun 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hesitant to make this change as characterProcessing.SYMLVL.NONE == 0 and isn't the same as None from what I can tell. There are cases where we do if symbolLevel is None.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose it could be option to represent "not set" yet (eg in a GUI). Is this actually possible though?

Otherwise, shouldn't there always be some value?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an unset value when determining the level of speech to report, that is processed later in speak. It's not driven by a UI, it's contextual in the processing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a default level? Presumably it would be an error to come out the other end of the processing as None

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is later set to config.conf["speech"]["symbolLevel"] if it is not otherwise set earlier

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So from the docs of def speak:

@param symbolLevel: The symbol verbosity level; C{None} (default) to use the user's configuration.

if unit == textInfos.UNIT_CHARACTER:
symbolLevel = characterProcessing.SYMLVL.ALL
elif unit == textInfos.UNIT_WORD:
if config.conf["speech"]["symbolLevelWordAll"]:
symbolLevel = characterProcessing.SYMLVL.ALL
for seq in speechGen:
speak(seq, priority=priority)
speak(seq, symbolLevel=symbolLevel, priority=priority)
return speechGen.returnValue


Expand Down Expand Up @@ -1323,7 +1329,7 @@ def isControlEndFieldCommand(x):
if onlyInitialFields or (
isWordOrCharUnit
and len(textWithFields) > 0
and len(textWithFields[0].strip() if not textWithFields[0].isspace() else textWithFields[0]) == 1
and len(textWithFields[0]) == 1
and all(isControlEndFieldCommand(x) for x in itertools.islice(textWithFields, 1, None))
):
if not onlyCache:
Expand Down
136 changes: 136 additions & 0 deletions tests/system/libraries/NotepadLib.py
@@ -0,0 +1,136 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2021 NV Access Limited
# 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.plaintext

""" This module provides the NotepadLib Robot Framework Library which allows system tests to start
Windows Notepad with a text sample and assert NVDA interacts with it in the expected way.
"""

# imported methods start with underscore (_) so they don't get imported into robot files as keywords
from os.path import join as _pJoin
import tempfile as _tempfile
from typing import Optional as _Optional
from SystemTestSpy import (
_blockUntilConditionMet,
_getLib,
)
from SystemTestSpy.windows import (
GetForegroundWindowTitle,
GetVisibleWindowTitles,
SetForegroundWindow,
)
import re
from robot.libraries.BuiltIn import BuiltIn

# Imported for type information
from robot.libraries.OperatingSystem import OperatingSystem as _OpSysLib
from robot.libraries.Process import Process as _ProcessLib
from AssertsLib import AssertsLib as _AssertsLib
import NvdaLib as _NvdaLib

builtIn: BuiltIn = BuiltIn()
opSys: _OpSysLib = _getLib('OperatingSystem')
process: _ProcessLib = _getLib('Process')
assertsLib: _AssertsLib = _getLib('AssertsLib')


# In Robot libraries, class name must match the name of the module. Use caps for both.
class NotepadLib:
_testFileStagingPath = _tempfile.mkdtemp()
_testCaseTitle = "test"

def __init__(self):
self.notepadHandle: _Optional[int] = None

@staticmethod
def _getTestCasePath(filename):
return _pJoin(NotepadLib._testFileStagingPath, filename)

def exit_notepad(self):
spy = _NvdaLib.getSpyLib()
spy.emulateKeyPress('alt+f4')
process.wait_for_process(self.notepadHandle, timeout="1 minute", on_timeout="continue")

def start_notepad(self, filePath):
builtIn.log(f"starting notepad: {filePath}")
self.notepadHandle = process.start_process(
"start notepad"
f' "{filePath}"',
shell=True,
alias='NotepadAlias',
)
process.process_should_be_running(self.notepadHandle)
return self.notepadHandle

@staticmethod
def getUniqueTestCaseTitle(testCase: str) -> str:
return f"{NotepadLib._testCaseTitle} ({abs(hash(testCase))}).txt"

@staticmethod
def getUniqueTestCaseTitleRegex(testCase: str) -> re.Pattern:
return re.compile(f"^{NotepadLib._testCaseTitle} \\({abs(hash(testCase))}\\)")

@staticmethod
def _writeTestFile(testCase) -> str:
"""
Creates a file for a plaintext test case.
@param testCase: The plaintext sample that is to be tested.
@return: path to the plaintext file.
"""
filePath = NotepadLib._getTestCasePath(NotepadLib.getUniqueTestCaseTitle(testCase))
with open(file=filePath, mode='w', encoding='UTF-8') as f:
f.write(testCase)
return filePath

def _focusNotepad(self, startsWithTestCaseTitle: re.Pattern):
""" Ensure Notepad started and is focused.
"""
success, _success = _blockUntilConditionMet(
getValue=lambda: SetForegroundWindow(startsWithTestCaseTitle, builtIn.log),
giveUpAfterSeconds=3,
intervalBetweenSeconds=0.5
)
if success:
return
windowInformation = ""
try:
windowInformation = f"Foreground Window: {GetForegroundWindowTitle()}.\n"
windowInformation += f"Open Windows: {GetVisibleWindowTitles()}"
except OSError as e:
builtIn.log(f"Couldn't retrieve active window information.\nException: {e}")
raise AssertionError(
"Unable to focus Notepad.\n"
f"{windowInformation}"
)

def prepareNotepad(self, testCase: str) -> None:
"""
Starts Notepad opening a file containing the plaintext sample.
Different versions of notepad/windows have variations in how the title is presented.
This may mean that there is a file extension in the title.
E.G. "test.txt - Notepad" or "test – Notepad".
@param testCase - The plaintext sample to test.
"""
spy = _NvdaLib.getSpyLib()
path = self._writeTestFile(testCase)

spy.wait_for_speech_to_finish()
self.start_notepad(path)
self._focusNotepad(NotepadLib.getUniqueTestCaseTitleRegex(testCase))
# Move to the start of file
spy.emulateKeyPress('home')
spy.wait_for_speech_to_finish()

@staticmethod
def getSpeechAfterKey(key) -> str:
"""Ensure speech has stopped, press key, and get speech until it stops.
@return: The speech after key press.
"""
spy = _NvdaLib.getSpyLib()
spy.wait_for_speech_to_finish()
nextSpeechIndex = spy.get_next_speech_index()
spy.emulateKeyPress(key)
spy.wait_for_speech_to_finish(speechStartedIndex=nextSpeechIndex)
speech = spy.get_speech_at_index_until_now(nextSpeechIndex)
return speech
18 changes: 18 additions & 0 deletions tests/system/nvdaSettingsFiles/symbolLevelWordOff.ini
@@ -0,0 +1,18 @@
schemaVersion = 2
[general]
showWelcomeDialogAtStartup = False
[update]
askedAllowUsageStats = True
autoCheck = False
startupNotification = False
allowUsageStats = False
[speech]
synth = speechSpySynthDriver
symbolLevelWordAll = False
[development]
enableScratchpadDir = True
[virtualBuffers]
autoSayAllOnPageLoad = False
passThroughAudioIndication = False
[annotations]
reportDetails = True