Skip to content

Commit c059805

Browse files
authored
Merge 33cdb13 into 8c9efe8
2 parents 8c9efe8 + 33cdb13 commit c059805

15 files changed

Lines changed: 341 additions & 21 deletions

File tree

source/config/configFlags.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,23 @@ def _displayStringLabels(self):
301301
}
302302

303303

304+
class ReportNotSupportedLanguage(DisplayStringStrEnum):
305+
SPEECH = "speech"
306+
BEEP = "beep"
307+
OFF = "off"
308+
309+
@property
310+
def _displayStringLabels(self) -> dict["ReportNotSupportedLanguage", str]:
311+
return {
312+
# Translators: A label for an option to report when the language of the text being read is not supported by the current synthesizer.
313+
self.SPEECH: pgettext("reportLanguage", "Speech"),
314+
# Translators: A label for an option to report when the language of the text being read is not supported by the current synthesizer.
315+
self.BEEP: pgettext("reportLanguage", "Beep"),
316+
# Translators: A label for an option to report when the language of the text being read is not supported by the current synthesizer.
317+
self.OFF: pgettext("reportLanguage", "Off"),
318+
}
319+
320+
304321
@verify(CONTINUOUS)
305322
class RemoteConnectionMode(DisplayStringIntEnum):
306323
"""Enumeration containing the possible remote connection modes (roles for connected clients).

source/config/configSpec.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
beepSpeechModePitch = integer(default=10000,min=50,max=11025)
4343
autoLanguageSwitching = boolean(default=true)
4444
autoDialectSwitching = boolean(default=false)
45+
reportLanguage = boolean(default=false)
46+
reportNotSupportedLanguage = option("speech", "beep", "off", default="speech")
4547
delayedCharacterDescriptions = boolean(default=false)
4648
excludedSpeechModes = int_list(default=list())
4749
trimLeadingSilence = boolean(default=true)

source/globalCommands.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from speech import (
3232
sayAll,
3333
shortcutKeys,
34+
languageHandling,
3435
)
3536
from NVDAObjects import NVDAObject, NVDAObjectTextInfo
3637
import globalVars
@@ -69,6 +70,7 @@
6970
import vision
7071
from utils.security import objectBelowLockScreenAndWindowsIsLocked
7172
import audio
73+
import synthDriverHandler
7274
from utils.displayString import DisplayStringEnum
7375
import _remoteClient
7476

@@ -2323,7 +2325,7 @@ def script_review_endOfSelection(self, gesture: inputCore.InputGesture):
23232325

23242326
def _getCurrentLanguageForTextInfo(self, info):
23252327
curLanguage = None
2326-
if config.conf["speech"]["autoLanguageSwitching"]:
2328+
if languageHandling.shouldMakeLangChangeCommand():
23272329
for field in info.getTextWithFields({}):
23282330
if isinstance(field, textInfos.FieldCommand) and field.command == "formatChange":
23292331
curLanguage = field.field.get("language")
@@ -4887,6 +4889,43 @@ def script_cycleParagraphStyle(self, gesture: "inputCore.InputGesture") -> None:
48874889
def script_cycleSoundSplit(self, gesture: "inputCore.InputGesture") -> None:
48884890
audio._toggleSoundSplitState()
48894891

4892+
@script(
4893+
description=pgettext(
4894+
"reportLanguage",
4895+
# Translators: Input help mode message for report language for caret command.
4896+
"Reports the language for the text under the caret. "
4897+
"If pressed twice, presents the information in browse mode",
4898+
),
4899+
category=SCRCAT_SYSTEMCARET,
4900+
speakOnDemand=True,
4901+
)
4902+
def script_reportCaretLanguage(self, gesture: "inputCore.InputGesture"):
4903+
info = self._getTIAtCaret()
4904+
info.expand(textInfos.UNIT_CHARACTER)
4905+
curLanguage = self._getCurrentLanguageForTextInfo(info)
4906+
langToReport = languageHandling.getLangToReport(curLanguage)
4907+
languageDescription = languageHandler.getLanguageDescription(langToReport)
4908+
if languageDescription is None:
4909+
languageDescription = langToReport
4910+
message = languageDescription
4911+
if languageHandling.shouldReportNotSupported():
4912+
curSynth = synthDriverHandler.getSynth()
4913+
if not curSynth.languageIsSupported(langToReport):
4914+
message = pgettext(
4915+
"reportLanguage",
4916+
# Translators: Language of the character at caret position when it's not supported by the current synthesizer.
4917+
"{languageDescription} (not supported)",
4918+
).format(
4919+
languageDescription=languageDescription,
4920+
)
4921+
repeats = scriptHandler.getLastScriptRepeatCount()
4922+
if repeats == 0:
4923+
ui.message(message)
4924+
elif repeats == 1:
4925+
# Translators: title for report caret language dialog.
4926+
title = pgettext("reportLanguage", "Language at caret position")
4927+
ui.browseableMessage(message, title, copyButton=True, closeButton=True)
4928+
48904929
@script(
48914930
# Translators: Documentation string for the script that toggles whether the output from the remote computer is muted.
48924931
description=pgettext("remote", "Mutes or unmutes the speech coming from the remote computer"),

source/gui/settingsDialogs.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
ReportCellBorders,
4646
OutputMode,
4747
TypingEcho,
48+
ReportNotSupportedLanguage,
4849
)
4950
import languageHandler
5051
import speech
@@ -1694,6 +1695,7 @@ def makeSettings(self, settingsSizer):
16941695
self.autoLanguageSwitchingCheckbox.SetValue(
16951696
config.conf["speech"]["autoLanguageSwitching"],
16961697
)
1698+
self.autoLanguageSwitchingCheckbox.Bind(wx.EVT_CHECKBOX, self.onAutoLanguageSwitchingChange)
16971699

16981700
# Translators: This is the label for a checkbox in the
16991701
# voice settings panel (if checked, different voices for dialects will be used to
@@ -1706,6 +1708,39 @@ def makeSettings(self, settingsSizer):
17061708
self.autoDialectSwitchingCheckbox.SetValue(
17071709
config.conf["speech"]["autoDialectSwitching"],
17081710
)
1711+
# Translators: This is the label for a checkbox in the voice settings panel. If checked, the language of the text been read will be reported.
1712+
reportLanguageText = pgettext("reportLanguage", "Report lan&guage changes")
1713+
self.reportLanguageCheckbox = settingsSizerHelper.addItem(
1714+
wx.CheckBox(
1715+
self,
1716+
label=reportLanguageText,
1717+
),
1718+
)
1719+
self.bindHelpEvent("ReportLanguage", self.reportLanguageCheckbox)
1720+
self.reportLanguageCheckbox.SetValue(
1721+
config.conf["speech"]["reportLanguage"],
1722+
)
1723+
1724+
labelText = pgettext(
1725+
"reportLanguage",
1726+
# Translators: This is a label for a combobox in the Voice settings panel to select
1727+
# reporting when the language of the text being read is not supported by the current synthesizer.
1728+
"Report when switching to language is not s&upported by synthesizer",
1729+
)
1730+
self.reportNotSupportedLanguageCombo = settingsSizerHelper.addLabeledControl(
1731+
labelText,
1732+
wx.Choice,
1733+
choices=[option.displayString for option in ReportNotSupportedLanguage],
1734+
)
1735+
self.bindHelpEvent(
1736+
"ReportIfLanguageIsNotSupportedBySynthesizer",
1737+
self.reportNotSupportedLanguageCombo,
1738+
)
1739+
reportNotSupportedLanguage = config.conf["speech"]["reportNotSupportedLanguage"]
1740+
self.reportNotSupportedLanguageCombo.SetSelection(
1741+
[option.value for option in ReportNotSupportedLanguage].index(reportNotSupportedLanguage),
1742+
)
1743+
self.reportNotSupportedLanguageCombo.Enable(self.autoLanguageSwitchingCheckbox.IsChecked())
17091744

17101745
# Translators: This is the label for a combobox in the
17111746
# voice settings panel (possible choices are none, some, most and all).
@@ -1874,11 +1909,19 @@ def _appendDelayedCharacterDescriptions(self, settingsSizerHelper: guiHelper.Box
18741909
config.conf["speech"]["delayedCharacterDescriptions"],
18751910
)
18761911

1912+
def onAutoLanguageSwitchingChange(self, evt: wx.CommandEvent):
1913+
"""Take action when the autoLanguageSwitching checkbox is pressed."""
1914+
self.reportNotSupportedLanguageCombo.Enable(self.autoLanguageSwitchingCheckbox.IsChecked())
1915+
18771916
def onSave(self):
18781917
AutoSettingsMixin.onSave(self)
18791918

18801919
config.conf["speech"]["autoLanguageSwitching"] = self.autoLanguageSwitchingCheckbox.IsChecked()
18811920
config.conf["speech"]["autoDialectSwitching"] = self.autoDialectSwitchingCheckbox.IsChecked()
1921+
config.conf["speech"]["reportLanguage"] = self.reportLanguageCheckbox.IsChecked()
1922+
config.conf["speech"]["reportNotSupportedLanguage"] = [
1923+
option.value for option in ReportNotSupportedLanguage
1924+
][self.reportNotSupportedLanguageCombo.GetSelection()]
18821925
config.conf["speech"]["symbolLevel"] = characterProcessing.CONFIGURABLE_SPEECH_SYMBOL_LEVELS[
18831926
self.symbolLevelList.GetSelection()
18841927
].value

source/speech/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@
6363
spellTextInfo,
6464
splitTextIndentation,
6565
)
66-
from .extensions import speechCanceled, post_speechPaused, pre_speechQueued
66+
from .extensions import speechCanceled, post_speechPaused, pre_speechQueued, filter_speechSequence
67+
from .languageHandling import getSpeechSequenceWithLangs
6768
from .priorities import Spri
6869

6970
from .types import (
@@ -165,7 +166,9 @@ def initialize():
165166
getTextInfoSpeech,
166167
SpeakTextInfoState,
167168
)
169+
filter_speechSequence.register(getSpeechSequenceWithLangs)
168170

169171

170172
def terminate():
171173
synthDriverHandler.setSynth(None)
174+
filter_speechSequence.unregister(getSpeechSequenceWithLangs)

source/speech/commands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,9 @@ def __eq__(self, __o: object) -> bool:
180180
class LangChangeCommand(SynthParamCommand):
181181
"""A command to switch the language within speech."""
182182

183-
def __init__(self, lang: Optional[str]):
183+
def __init__(self, lang: str | None):
184184
"""
185-
@param lang: the language to switch to: If None then the NVDA locale will be used.
185+
:param lang: The language to switch to: If None then the NVDA locale will be used.
186186
"""
187187
self.lang = lang
188188
self.isDefault = not lang

source/speech/languageHandling.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# A part of NonVisual Desktop Access (NVDA)
2+
# Copyright (C) 2025 NV Access Limited, Noelia Ruiz Martínez
3+
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
4+
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt
5+
6+
import languageHandler
7+
import synthDriverHandler
8+
import config
9+
from config.configFlags import ReportNotSupportedLanguage
10+
from . import speech
11+
from .types import SpeechSequence
12+
from .commands import LangChangeCommand, BeepCommand
13+
14+
15+
def getSpeechSequenceWithLangs(speechSequence: SpeechSequence) -> SpeechSequence:
16+
"""Get a speech sequence with the language description for each non default language of the read text.
17+
18+
:param speechSequence: The original speech sequence.
19+
:return: A speech sequence containing descriptions for each non default language, indicating if the language is not supported by the current synthesizer.
20+
"""
21+
if not shouldMakeLangChangeCommand():
22+
return speechSequence
23+
curSynth = synthDriverHandler.getSynth()
24+
filteredSpeechSequence = list()
25+
for index, item in enumerate(speechSequence):
26+
if (
27+
not isinstance(item, LangChangeCommand)
28+
or item.isDefault
29+
or getLangToReport(item.lang) == speech._speechState.lastReportedLanguage
30+
or index == len(speechSequence) - 1
31+
):
32+
filteredSpeechSequence.append(item)
33+
continue
34+
langDesc = languageHandler.getLanguageDescription(getLangToReport(item.lang))
35+
if langDesc is None:
36+
langDesc = getLangToReport(item.lang)
37+
# Ensure that the language description is pronnounced in the default language.
38+
filteredSpeechSequence.append(LangChangeCommand(None))
39+
if shouldReportNotSupported() and not curSynth.languageIsSupported(getLangToReport(item.lang)):
40+
if config.conf["speech"]["reportNotSupportedLanguage"] == ReportNotSupportedLanguage.SPEECH.value:
41+
filteredSpeechSequence.append(
42+
# Translators: Reported when the language of the text being read is not supported by the current synthesizer.
43+
pgettext("languageNotSupported", "{lang} (not supported)").format(lang=langDesc),
44+
)
45+
else: # Beep
46+
filteredSpeechSequence.append(langDesc)
47+
filteredSpeechSequence.append(BeepCommand(500, 50))
48+
elif config.conf["speech"]["reportLanguage"]:
49+
filteredSpeechSequence.append(langDesc)
50+
speech._speechState.lastReportedLanguage = getLangToReport(item.lang)
51+
filteredSpeechSequence.append(item)
52+
return filteredSpeechSequence
53+
54+
55+
def shouldSwitchVoice() -> bool:
56+
"""Determines if the current synthesizer should switch to the voice corresponding to the language of the text been read."""
57+
return config.conf["speech"]["autoLanguageSwitching"]
58+
59+
60+
def shouldMakeLangChangeCommand() -> bool:
61+
"""Determines if NVDA should get the language of the text been read."""
62+
return config.conf["speech"]["autoLanguageSwitching"] or config.conf["speech"]["reportLanguage"]
63+
64+
65+
def shouldReportNotSupported() -> bool:
66+
"""Determines if NVDA should report if the language is not supported by the synthesizer."""
67+
return (
68+
config.conf["speech"]["autoLanguageSwitching"]
69+
and config.conf["speech"]["reportNotSupportedLanguage"] != ReportNotSupportedLanguage.OFF.value
70+
)
71+
72+
73+
def getLangToReport(lang: str) -> str:
74+
"""Gets the language to report by NVDA, according to speech settings.
75+
76+
:param lang: A language code corresponding to the text been read.
77+
:return: A language code corresponding to the language to be reported.
78+
"""
79+
if config.conf["speech"]["autoLanguageSwitching"] and not config.conf["speech"]["autoDialectSwitching"]:
80+
return lang.split("_")[0]
81+
return lang

source/speech/manager.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
EndUtteranceCommand,
1515
SuppressUnicodeNormalizationCommand,
1616
SynthParamCommand,
17+
LangChangeCommand,
1718
BaseCallbackCommand,
1819
ConfigProfileTriggerCommand,
1920
IndexCommand,
2021
_CancellableSpeechCommand,
2122
)
2223
from .extensions import pre_speechQueued
2324
from .priorities import Spri, SPEECH_PRIORITIES
25+
from .languageHandling import shouldSwitchVoice
2426
from logHandler import log
2527
from synthDriverHandler import getSynth, pre_synthSpeak
2628
from typing import (
@@ -366,6 +368,9 @@ def _processSpeechSequence(self, inSeq: SpeechSequence):
366368
self._ensureEndUtterance(outSeq, outSeqs, paramsToReplay, paramTracker)
367369
continue
368370
if isinstance(command, SynthParamCommand):
371+
if isinstance(command, LangChangeCommand) and not shouldSwitchVoice():
372+
# Language change shouldn't be passed to synthesizer.
373+
continue
369374
paramTracker.update(command)
370375
if isinstance(command, SuppressUnicodeNormalizationCommand):
371376
continue # Not handled by speech manager

0 commit comments

Comments
 (0)