Skip to content

Commit

Permalink
Add "Only in edit controls" mode for typing echo (#17505)
Browse files Browse the repository at this point in the history
Fixes #16848,
related #10331, #3027

Summary of the issue:
Currently NVDA can only toggle typing echo (characters and words) on or off globally. Users want more granular control to only have typing feedback in edit controls, while keeping it off in other contexts like listss or non-edit areas.

Description of user facing changes
- Added a new option "Only in edit controls" for both "Speak typed characters" and "Speak typed words" settings in Keyboard Settings
- Instead of checkboxes, these are now combo boxes with three options:
  - Off: No typing echo
  - Only in edit controls: Only echo text typed in edit fields
  - Always: Echo all typed text
- By default, "Speak typed characters" is now set to "Only in edit controls".
- Updated relevant documentation in the user guide

Description of development approach
The implementation:
1. Added a TypingEcho enum in configFlags.py with values:
   - OFF (0)
   - EDIT_CONTROLS (1)
   - ALWAYS (2) 
2. Changed keyboard typing echo configuration from boolean to integer values
3. Updated speech.py, behaviors.py and inputComposition.py to use the new enum
4. Modified settings dialog to use combo box instead of checkbox
5. Updated documentation

Testing strategy:
Tested the following scenarios:
1. Basic functionality:
- Open Notepad (edit control)
  - Set to "Only in edit controls"
  - Type text - should be announced
  - Verify both character and word echo settings
- Explorer file lists(non-edit control)
  - Type text - should not be announced
- Test all three modes (Off, On, Only in edit controls)
2. Different contexts:
- Web browser input fields
- Rich text editors
- Read-only text areas
- Terminal windows

Known issues with pull request:
None identified.
  • Loading branch information
cary-rowen authored Jan 22, 2025
1 parent b8cf4cf commit 605db3e
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 68 deletions.
10 changes: 7 additions & 3 deletions source/NVDAObjects/behaviors.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,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-2023 NV Access Limited, Peter Vágner, Joseph Lee, Bill Dengler,
# Burman's Computer and Education Ltd.
# Copyright (C) 2006-2025 NV Access Limited, Peter Vágner, Joseph Lee, Bill Dengler,
# Burman's Computer and Education Ltd, Cary-rowen

"""Mix-in classes which provide common behaviour for particular types of controls across different APIs.
Behaviors described in this mix-in include providing table navigation commands for certain table rows, terminal input and output support, announcing notifications and suggestion items and so on.
Expand Down Expand Up @@ -31,6 +31,7 @@
import globalVars
from typing import List, Union
import diffHandler
from config.configFlags import TypingEcho


class ProgressBar(NVDAObject):
Expand Down Expand Up @@ -571,7 +572,10 @@ def event_typedCharacter(self, ch):
else:
self._hasTab = False
if (
(config.conf["keyboard"]["speakTypedCharacters"] or config.conf["keyboard"]["speakTypedWords"])
(
config.conf["keyboard"]["speakTypedCharacters"] != TypingEcho.OFF.value
or config.conf["keyboard"]["speakTypedWords"] != TypingEcho.OFF.value
)
and not config.conf["terminals"]["speakPasswords"]
and self._supportsTextChange
):
Expand Down
11 changes: 10 additions & 1 deletion source/NVDAObjects/inputComposition.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2012-2025 NV Access Limited, Cary-Rowen
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

import eventHandler
import queueHandler
import controlTypes
from config.configFlags import TypingEcho
import characterProcessing
import speech
import config
Expand Down Expand Up @@ -73,7 +79,10 @@ def findOverlayClasses(self, clsList):
return clsList

def reportNewText(self, oldString, newString):
if config.conf["keyboard"]["speakTypedCharacters"] or config.conf["keyboard"]["speakTypedWords"]:
if (
config.conf["keyboard"]["speakTypedCharacters"] != TypingEcho.OFF.value
or config.conf["keyboard"]["speakTypedWords"] != TypingEcho.OFF.value
):
newText = calculateInsertedChars(oldString.strip("\u3000"), newString.strip("\u3000"))
if newText:
queueHandler.queueFunction(
Expand Down
26 changes: 25 additions & 1 deletion source/config/configFlags.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2022-2024 NV Access Limited, Cyrille Bougot
# Copyright (C) 2022-2025 NV Access Limited, Cyrille Bougot, Cary-rowen
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

Expand Down Expand Up @@ -45,6 +45,30 @@ def _displayStringLabels(self):
}


@unique
class TypingEcho(DisplayStringIntEnum):
"""Enumeration containing the possible config values for typing echo (characters and words).
Use TypingEcho.MEMBER.value to compare with the config;
use TypingEcho.MEMBER.displayString in the UI for a translatable description of this member.
"""

OFF = 0
EDIT_CONTROLS = 1
ALWAYS = 2

@property
def _displayStringLabels(self):
return {
# Translators: One of the choices for typing echo in keyboard settings
TypingEcho.OFF: _("Off"),
# Translators: One of the choices for typing echo in keyboard settings
TypingEcho.EDIT_CONTROLS: _("Only in edit controls"),
# Translators: One of the choices for typing echo in keyboard settings
TypingEcho.ALWAYS: _("Always"),
}


@unique
class ShowMessages(DisplayStringIntEnum):
"""Enumeration containing the possible config values for "Show messages" option in braille settings.
Expand Down
10 changes: 6 additions & 4 deletions source/config/configSpec.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2006-2025 NV Access Limited, Babbage B.V., Davy Kager, Bill Dengler, Julien Cochuyt,
# Joseph Lee, Dawid Pieper, mltony, Bram Duvigneau, Cyrille Bougot, Rob Meredith,
# Burman's Computer and Education Ltd., Leonard de Ruijter, Łukasz Golonka
# Burman's Computer and Education Ltd., Leonard de Ruijter, Łukasz Golonka, Cary-rowen
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

Expand All @@ -13,7 +13,7 @@
#: provide an upgrade step (@see profileUpgradeSteps.py). An upgrade step does not need to be added when
#: just adding a new element to (or removing from) the schema, only when old versions of the config
#: (conforming to old schema versions) will not work correctly with the new schema.
latestSchemaVersion = 14
latestSchemaVersion = 15

#: The configuration specification string
#: @type: String
Expand Down Expand Up @@ -178,8 +178,10 @@
# Default = 6: NumpadInsert + ExtendedInsert
NVDAModifierKeys = integer(1, 7, default=6)
keyboardLayout = string(default="desktop")
speakTypedCharacters = boolean(default=true)
speakTypedWords = boolean(default=false)
# 0: Off, 1: Only in edit controls, 2: Always
speakTypedCharacters = integer(default=1,min=0,max=2)
# 0: Off, 1: Only in edit controls, 2: Always
speakTypedWords = integer(default=0,min=0,max=2)
beepForLowercaseWithCapslock = boolean(default=true)
speakCommandKeys = boolean(default=false)
speechInterruptForCharacters = boolean(default=true)
Expand Down
30 changes: 29 additions & 1 deletion source/config/profileUpgradeSteps.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2016-2024 NV Access Limited, Bill Dengler, Cyrille Bougot, Łukasz Golonka, Leonard de Ruijter
# Copyright (C) 2016-2025 NV Access Limited, Bill Dengler, Cyrille Bougot, Łukasz Golonka, Leonard de Ruijter, Cary-rowen
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

Expand All @@ -22,6 +22,7 @@
ReportTableHeaders,
ReportCellBorders,
OutputMode,
TypingEcho,
)
import configobj.validate
from configobj import ConfigObj
Expand Down Expand Up @@ -471,3 +472,30 @@ def _friendlyNameToEndpointId(friendlyName: str) -> str | None:
# Proceed to the next device state.
continue
return None


def upgradeConfigFrom_14_to_15(profile: ConfigObj):
"""Convert keyboard typing echo configurations from boolean to integer values."""
_convertTypingEcho(profile, "speakTypedCharacters")
_convertTypingEcho(profile, "speakTypedWords")


def _convertTypingEcho(profile: ConfigObj, key: str) -> None:
"""
Convert a keyboard typing echo configuration from boolean to integer values.
:param profile: The `ConfigObj` instance representing the user's NVDA configuration file.
:param key: The configuration key to convert.
"""
try:
oldValue: bool = profile["keyboard"].as_bool(key)
except KeyError:
log.debug(f"'{key}' not present in config, no action taken.")
return
except ValueError:
log.error(f"'{key}' is not a boolean, no action taken.")
return
else:
newValue = TypingEcho.EDIT_CONTROLS.value if oldValue else TypingEcho.OFF.value
profile["keyboard"][key] = newValue
log.debug(f"Converted '{key}' from {oldValue!r} to {newValue} ({TypingEcho(newValue).name}).")
75 changes: 50 additions & 25 deletions source/globalCommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# Copyright (C) 2006-2025 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Rui Batista, Joseph Lee,
# Leonard de Ruijter, Derek Riemer, Babbage B.V., Davy Kager, Ethan Holliger, Łukasz Golonka, Accessolutions,
# Julien Cochuyt, Jakub Lukowicz, Bill Dengler, Cyrille Bougot, Rob Meredith, Luke Davis,
# Burman's Computer and Education Ltd.
# Burman's Computer and Education Ltd, Cary-rowen.

import itertools
from typing import (
Expand Down Expand Up @@ -44,6 +44,7 @@
ShowMessages,
BrailleMode,
OutputMode,
TypingEcho,
)
from config.featureFlag import FeatureFlag
from config.featureFlagEnums import BoolFlag
Expand All @@ -69,6 +70,7 @@
from utils.security import objectBelowLockScreenAndWindowsIsLocked
import audio
from audio import appsVolume
from utils.displayString import DisplayStringEnum


#: Script category for text review commands.
Expand Down Expand Up @@ -150,6 +152,31 @@ def toggleBooleanValue(
ui.message(msg)


def toggleIntegerValue(
configSection: str,
configKey: str,
enumClass: "DisplayStringEnum",
messageTemplate: str,
) -> None:
"""
Cycles through integer configuration values and displays the corresponding message.
:param configSection: The configuration section containing the integer key.
:param configKey: The configuration key associated with the integer value.
:param enumClass: The enumeration class representing possible states.
:param messageTemplate: The message template with a placeholder, `{mode}`, for the state.
:return: None.
"""
currentValue = config.conf[configSection][configKey]
numVals = len(enumClass)
newValue = (currentValue + 1) % numVals
config.conf[configSection][configKey] = newValue

state = enumClass(newValue)
msg = messageTemplate.format(mode=state.displayString)
ui.message(msg)


class GlobalCommands(ScriptableObject):
"""Commands that are available at all times, regardless of the current focus."""

Expand Down Expand Up @@ -536,38 +563,36 @@ def script_previousSynthSetting(self, gesture):
ui.message("%s %s" % (previousSettingName, previousSettingValue))

@script(
# Translators: Input help mode message for toggle speaked typed characters command.
description=_("Toggles on and off the speaking of typed characters"),
# Translators: Input help mode message for cycling the reporting of typed characters.
description=_("Cycles through options for when to speak typed characters."),
category=SCRCAT_SPEECH,
gesture="kb:NVDA+2",
)
def script_toggleSpeakTypedCharacters(self, gesture):
if config.conf["keyboard"]["speakTypedCharacters"]:
# Translators: The message announced when toggling the speak typed characters keyboard setting.
state = _("speak typed characters off")
config.conf["keyboard"]["speakTypedCharacters"] = False
else:
# Translators: The message announced when toggling the speak typed characters keyboard setting.
state = _("speak typed characters on")
config.conf["keyboard"]["speakTypedCharacters"] = True
ui.message(state)
def script_toggleSpeakTypedCharacters(self, gesture: "inputCore.InputGesture") -> None:
toggleIntegerValue(
configSection="keyboard",
configKey="speakTypedCharacters",
enumClass=TypingEcho,
# Translators: Reported when the user cycles through speak typed characters modes.
# {mode} will be replaced with the mode; e.g. Off, On, Only in edit controls.
messageTemplate=_("Speak typed characters {mode}"),
)

@script(
# Translators: Input help mode message for toggle speak typed words command.
description=_("Toggles on and off the speaking of typed words"),
# Translators: Input help mode message for cycling the reporting of typed words.
description=_("Cycles through options for when to speak typed words."),
category=SCRCAT_SPEECH,
gesture="kb:NVDA+3",
)
def script_toggleSpeakTypedWords(self, gesture):
if config.conf["keyboard"]["speakTypedWords"]:
# Translators: The message announced when toggling the speak typed words keyboard setting.
state = _("speak typed words off")
config.conf["keyboard"]["speakTypedWords"] = False
else:
# Translators: The message announced when toggling the speak typed words keyboard setting.
state = _("speak typed words on")
config.conf["keyboard"]["speakTypedWords"] = True
ui.message(state)
def script_toggleSpeakTypedWords(self, gesture: "inputCore.InputGesture") -> None:
toggleIntegerValue(
configSection="keyboard",
configKey="speakTypedWords",
enumClass=TypingEcho,
# Translators: Reported when the user cycles through speak typed words modes.
# {mode} will be replaced with the mode; e.g. Off, On, Only in edit controls.
messageTemplate=_("Speak typed words {mode}"),
)

@script(
# Translators: Input help mode message for toggle speak command keys command.
Expand Down
42 changes: 24 additions & 18 deletions source/gui/settingsDialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# Thomas Stivers, Julien Cochuyt, Peter Vágner, Cyrille Bougot, Mesar Hameed,
# Łukasz Golonka, Aaron Cannon, Adriani90, André-Abush Clause, Dawid Pieper,
# Takuya Nishimoto, jakubl7545, Tony Malykh, Rob Meredith,
# Burman's Computer and Education Ltd, hwf1324.
# Burman's Computer and Education Ltd, hwf1324, Cary-rowen.
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
import logging
Expand Down Expand Up @@ -39,6 +39,7 @@
ReportTableHeaders,
ReportCellBorders,
OutputMode,
TypingEcho,
)
import languageHandler
import speech
Expand Down Expand Up @@ -1974,24 +1975,29 @@ def makeSettings(self, settingsSizer):
checkedItems.append(n)
self.modifierList.CheckedItems = checkedItems
self.modifierList.Select(0)

self.bindHelpEvent("KeyboardSettingsModifiers", self.modifierList)
# Translators: This is the label for a checkbox in the
# keyboard settings panel.
charsText = _("Speak typed &characters")
self.charsCheckBox = sHelper.addItem(wx.CheckBox(self, label=charsText))
self.bindHelpEvent(
"KeyboardSettingsSpeakTypedCharacters",
self.charsCheckBox,

# Translators: This is the label for a combobox in the keyboard settings panel.
speakTypedCharsLabelText = _("Speak typed &characters:")
speakTypedCharsChoices = [mode.displayString for mode in TypingEcho]
self.speakTypedCharsList = sHelper.addLabeledControl(
speakTypedCharsLabelText,
wx.Choice,
choices=speakTypedCharsChoices,
)
self.charsCheckBox.SetValue(config.conf["keyboard"]["speakTypedCharacters"])
self.bindHelpEvent("KeyboardSettingsSpeakTypedCharacters", self.speakTypedCharsList)
self.speakTypedCharsList.SetSelection(config.conf["keyboard"]["speakTypedCharacters"])

# Translators: This is the label for a checkbox in the
# keyboard settings panel.
speakTypedWordsText = _("Speak typed &words")
self.wordsCheckBox = sHelper.addItem(wx.CheckBox(self, label=speakTypedWordsText))
self.bindHelpEvent("KeyboardSettingsSpeakTypedWords", self.wordsCheckBox)
self.wordsCheckBox.SetValue(config.conf["keyboard"]["speakTypedWords"])
# Translators: This is the label for a combobox in the keyboard settings panel.
speakTypedWordsLabelText = _("Speak typed &words:")
speakTypedWordsChoices = [mode.displayString for mode in TypingEcho]
self.speakTypedWordsList = sHelper.addLabeledControl(
speakTypedWordsLabelText,
wx.Choice,
choices=speakTypedWordsChoices,
)
self.bindHelpEvent("KeyboardSettingsSpeakTypedWords", self.speakTypedWordsList)
self.speakTypedWordsList.SetSelection(config.conf["keyboard"]["speakTypedWords"])

# Translators: This is the label for a checkbox in the
# keyboard settings panel.
Expand Down Expand Up @@ -2091,8 +2097,8 @@ def onSave(self):
config.conf["keyboard"]["NVDAModifierKeys"] = sum(
key.value for (n, key) in enumerate(NVDAKey) if self.modifierList.IsChecked(n)
)
config.conf["keyboard"]["speakTypedCharacters"] = self.charsCheckBox.IsChecked()
config.conf["keyboard"]["speakTypedWords"] = self.wordsCheckBox.IsChecked()
config.conf["keyboard"]["speakTypedCharacters"] = self.speakTypedCharsList.GetSelection()
config.conf["keyboard"]["speakTypedWords"] = self.speakTypedWordsList.GetSelection()
config.conf["keyboard"]["speechInterruptForCharacters"] = (
self.speechInterruptForCharsCheckBox.IsChecked()
)
Expand Down
Loading

0 comments on commit 605db3e

Please sign in to comment.