Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
58 changes: 55 additions & 3 deletions addon/globalPlugins/beepKeyboard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# Released under GPL 2
#globalPlugins/beepKeyboard.py

import api, codecs, config, globalPluginHandler, gui, keyboardHandler, tones, ui, winreg, winUser, wx, addonHandler
import api, codecs, config, controlTypes, globalPluginHandler, gui, keyboardHandler, synthDriverHandler, tones, ui, winreg, winUser, wx, addonHandler
from .beepKeyboardUtils import configSpec, registerConfig, showDonationsDialog
addonHandler.initTranslation()

Expand Down Expand Up @@ -40,6 +40,46 @@ def beep(l):
if not (AF.disableBeepingOnPasswordFields and api.getFocusObject().isProtected):
tones.beep(*l, right=l[-1])

try:
# available since NVDA 2025.1, used by NVDA itself for the "only in edit controls" typed
# character/word echo option.
from speech.speech import isFocusEditable as _nativeIsFocusEditable
except ImportError:
_nativeIsFocusEditable = None

def isFocusEditable(obj):
""" tells whether obj should be considered an editable text field.
Uses NVDA's own speech.speech.isFocusEditable when available (NVDA 2025.1 and later);
on older NVDA versions, where that function does not exist, falls back to a reimplementation
of the same criteria.
"""
if _nativeIsFocusEditable is not None:
return _nativeIsFocusEditable()
if not obj: return False
controls = (controlTypes.ROLE_EDITABLETEXT, controlTypes.ROLE_DOCUMENT, controlTypes.ROLE_TERMINAL)
return (obj.role in controls or controlTypes.STATE_EDITABLE in obj.states) and controlTypes.STATE_READONLY not in obj.states

def isNativeCapsBeepActive(obj, ch):
""" tells whether NVDA's own "Beep for capitals" voice setting will already play its beep
for this exact typed character, so our own capitals beep would just be a duplicate.
NVDA beeps here when the active synthesizer has "beep for capitals" enabled and the
keyboard "speak typed characters" setting applies to the current focus (replicates the
same decision NVDA's speech.speakTypedCharacters makes before calling speakSpelling).
"""
if not ch.isupper(): return False
synth = synthDriverHandler.getSynth()
if not synth: return False
try:
if not config.conf["speech"][synth.name]["beepForCapitals"]: return False
except KeyError:
return False
mode = config.conf["keyboard"]["speakTypedCharacters"]
if isinstance(mode, bool):
mode = 2 if mode else 0
if mode == 0: return False
if mode == 1: return isFocusEditable(obj)
return True

#saves the original _reportToggleKey function
origReportToggleKey = keyboardHandler.KeyboardInputGesture._reportToggleKey

Expand Down Expand Up @@ -212,9 +252,21 @@ def checkEaseAccessToggleKeys(self):

def event_typedCharacter(self, obj, nextHandler, ch):
nextHandler()
if AF.beepUpperWithCapsLock and winUser.getKeyState(winUser.VK_CAPITAL)&1 and ch.isupper():
if AF.beepUpperWithCapsLock and winUser.getKeyState(winUser.VK_CAPITAL)&1 and ch.isupper() and isFocusEditable(obj) and not isNativeCapsBeepActive(obj, ch):
beep(AF.capsLockUpperTone)
elif AF.beepCharacterWithShift and not winUser.getKeyState(winUser.VK_CONTROL) &32768 and winUser.getKeyState(winUser.VK_SHIFT) &32768 and ch not in self.ignoredCharactersForShift and not (config.conf["keyboard"]["beepForLowercaseWithCapslock"] and ch.islower() and winUser.getKeyState(winUser.VK_CAPITAL)&1):
elif (
AF.beepCharacterWithShift
and not winUser.getKeyState(winUser.VK_CONTROL) &32768
and winUser.getKeyState(winUser.VK_SHIFT) &32768
# ch.isupper() is the ground truth for whether shift actually produced an uppercase
# letter; the live shift key state can lag behind fast typing and briefly still read
# as pressed for a character it did not actually shift (e.g. a caps-lock-lowercased
# letter typed right after a shifted one), which used to cause spurious beeps.
and (ch.isupper() if ch.isalpha() else True)
and ch not in self.ignoredCharactersForShift
and isFocusEditable(obj)
and not isNativeCapsBeepActive(obj, ch)
):
beep(AF.shiftedCharactersTone)
elif ch in self.beepForCharacters: beep(AF.customCharactersTone)

Expand Down
8 changes: 4 additions & 4 deletions buildVars.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
from site_scons.site_tools.NVDATool.utils import _

class Config:
VERSION = "26.5.1"
VERSION = "26.8.0"
MIN_NVDA = "2018.3.0"
LAST_TESTED = "2026.1"
LAST_TESTED = "2026.2"
# Essential Metadata
ID = "beepKeyboard"
# Translators: Summary/title for this add-on
SUMMARY = _("Beep keyboard")
# Translators: Long description to be shown for this add-on on add-on information from add-on store
DESCRIPTION = _("""This add-on beeps with some keyboard events.""")
# Translators: what's new content for the add-on version to be shown in the add-on store
CHANGELOG = _("""Initial migration to the new template.""")
CHANGELOG = _("""Capital letter beeps are now more accurate, only sound in edit fields, and no longer duplicate NVDA's own beep for capitals.""")
author="David CM <dhf360@gmail.com>"
URL = "https://github.com/david-acm/NVDA-beepKeyboard"
URL = "https://github.com/david-acm/beepKeyboard"

from site_scons.site_tools.NVDATool.typings import AddonInfo, BrailleTables, SymbolDictionaries, SpeechDictionaries

Expand Down
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Version 26.8.0

* Capital letter beeps are now more accurate, only sound in edit fields, and no longer duplicate NVDA's own beep for capitals.""")

# version 26.5.1.

Added support for NVDA 2026.1.
Expand Down