From 336277cea95014cc37a833e05024e680fe413d5a Mon Sep 17 00:00:00 2001 From: Christopher Toth Date: Fri, 22 Nov 2024 21:12:12 -0700 Subject: [PATCH 1/5] Add new extension points for keyboard input and speech pause - Add decide_handleRawKey extension point to allow intercepting raw keyboard events - Add speechPaused extension point for speech pause/resume notifications - Update documentation to reflect new extension points - Add unit tests for speechPaused extension point --- projectDocs/dev/developerGuide/developerGuide.md | 2 ++ source/inputCore.py | 11 +++++++++++ source/keyboardHandler.py | 14 ++++++++++++++ source/speech/__init__.py | 3 ++- source/speech/extensions.py | 8 ++++++++ source/speech/speech.py | 3 ++- tests/unit/test_speech.py | 9 +++++++++ user_docs/en/changes.md | 4 ++++ 8 files changed, 52 insertions(+), 2 deletions(-) diff --git a/projectDocs/dev/developerGuide/developerGuide.md b/projectDocs/dev/developerGuide/developerGuide.md index 918f902bf63..3559a0932c5 100644 --- a/projectDocs/dev/developerGuide/developerGuide.md +++ b/projectDocs/dev/developerGuide/developerGuide.md @@ -1361,6 +1361,7 @@ For examples of how to define and use new extension points, please see the code | Type |Extension Point |Description| |---|---|---| +|`Decider` |`decide_handleRawKey` |Notifies when a raw keyboard event is received, before any NVDA processing, allowing other code to decide if it should be handled.| |`Decider` |`decide_executeGesture` |Notifies when a gesture is about to be executed, allowing other code to decide if it should be.| ### logHandler {#logHandlerExtPts} @@ -1382,6 +1383,7 @@ For examples of how to define and use new extension points, please see the code |`Action` |`speechCanceled` |Triggered when speech is canceled.| |`Action` |`pre_speechCanceled` |Triggered before speech is canceled.| |`Action` |`pre_speech` |Triggered before NVDA handles prepared speech.| +|`Action` |`speechPaused` |Triggered when speech is paused or resumed.| |`Filter` |`filter_speechSequence` |Allows components or add-ons to filter speech sequence before it passes to the synth driver.| ### synthDriverHandler {#synthDriverHandlerExtPts} diff --git a/source/inputCore.py b/source/inputCore.py index 57f1814916f..cf11d6efb45 100644 --- a/source/inputCore.py +++ b/source/inputCore.py @@ -452,6 +452,17 @@ def __eq__(self, other: Any) -> bool: return NotImplemented +decide_handleRawKey = extensionPoints.Decider() +""" +Notifies when a raw keyboard event is received, before any NVDA processing. +Handlers can decide whether the key should be processed by NVDA and/or passed to the OS. +@param vkCode: The virtual key code +@param scanCode: The scan code +@param extended: Whether this is an extended key +@param pressed: Whether this is a key press or release +@return: True to allow normal processing, False to block the key +""" + decide_executeGesture = extensionPoints.Decider() """ Notifies when a gesture is about to be executed, diff --git a/source/keyboardHandler.py b/source/keyboardHandler.py index e70879acb71..d06ddcf08c8 100644 --- a/source/keyboardHandler.py +++ b/source/keyboardHandler.py @@ -165,6 +165,13 @@ def shouldUseToUnicodeEx(focus: Optional["NVDAObject"] = None): def internal_keyDownEvent(vkCode, scanCode, extended, injected): """Event called by winInputHook when it receives a keyDown.""" + if not inputCore.decide_handleRawKey.decide( + vkCode=vkCode, + scanCode=scanCode, + extended=extended, + pressed=True, + ): + return False gestureExecuted = False try: global \ @@ -313,6 +320,13 @@ def internal_keyDownEvent(vkCode, scanCode, extended, injected): def internal_keyUpEvent(vkCode, scanCode, extended, injected): """Event called by winInputHook when it receives a keyUp.""" + if not inputCore.decide_handleRawKey.decide( + vkCode=vkCode, + scanCode=scanCode, + extended=extended, + pressed=False, + ): + return False try: global \ lastNVDAModifier, \ diff --git a/source/speech/__init__.py b/source/speech/__init__.py index 61ea0d35734..5e31b01b002 100644 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -63,7 +63,7 @@ spellTextInfo, splitTextIndentation, ) -from .extensions import speechCanceled +from .extensions import speechCanceled, speechPaused from .priorities import Spri from .types import ( @@ -142,6 +142,7 @@ "spellTextInfo", "splitTextIndentation", "speechCanceled", + "speechPaused", ] import synthDriverHandler diff --git a/source/speech/extensions.py b/source/speech/extensions.py index 13b2e42c4f2..049c5118c66 100644 --- a/source/speech/extensions.py +++ b/source/speech/extensions.py @@ -22,6 +22,14 @@ Handlers are called without arguments. """ +speechPaused = Action() +""" +Notifies when speech is paused. + +@param switch: True if speech is paused, False if speech is resumed. +@type switch: bool +""" + pre_speech = Action() """ Notifies when code attempts to speak text. diff --git a/source/speech/speech.py b/source/speech/speech.py index 32d3fd3cc06..3153208b8dc 100644 --- a/source/speech/speech.py +++ b/source/speech/speech.py @@ -27,7 +27,7 @@ from textUtils import unicodeNormalize from textUtils.uniscribe import splitAtCharacterBoundaries from . import manager -from .extensions import speechCanceled, pre_speechCanceled, pre_speech +from .extensions import speechCanceled, speechPaused, pre_speechCanceled, pre_speech from .extensions import filter_speechSequence from .commands import ( # Commands that are used in this file. @@ -211,6 +211,7 @@ def cancelSpeech(): def pauseSpeech(switch): getSynth().pause(switch) + speechPaused.notify(switch=switch) _speechState.isPaused = switch _speechState.beenCanceled = False diff --git a/tests/unit/test_speech.py b/tests/unit/test_speech.py index e91f4d3b12d..fb0b60eaea1 100644 --- a/tests/unit/test_speech.py +++ b/tests/unit/test_speech.py @@ -16,7 +16,9 @@ _getSpellingSpeechAddCharMode, _getSpellingSpeechWithoutCharMode, cancelSpeech, + pauseSpeech, speechCanceled, + speechPaused, ) from speech.commands import ( BeepCommand, @@ -591,3 +593,10 @@ def test_speechCanceledExtensionPoint(self): speechCanceled, ): cancelSpeech() + + def test_speechPausedExtensionPoint(self): + with actionTester(self, speechPaused, switch=True): + pauseSpeech(True) + + with actionTester(self, speechPaused, switch=False): + pauseSpeech(False) diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index e33d5b9cee9..d4622a9fcb4 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -104,6 +104,10 @@ Add-ons will need to be re-tested and have their manifest updated. * Added the [VS Code workspace configuration for NVDA](https://nvaccess.org/nvaccess/vscode-nvda) as a git submodule. (#17003) * In the `brailleTables` module, a `getDefaultTableForCurrentLang` function has been added (#17222, @nvdaes) * Retrieving the `labeledBy` property now works for UIA elements supporting the corresponding `LabeledBy` UIA property. (#17442, @michaelweghorn) +* Added the following extension points: + * inputCore.decide_handleRawKey: called on each keypress + * speech.extensions.speechPaused: Called when speech is paused or unpaused +pause) #### API Breaking Changes From e2f88b9a978952ad0724f27d0c2bf6a07aa2ae76 Mon Sep 17 00:00:00 2001 From: Christopher Toth Date: Mon, 25 Nov 2024 21:46:27 -0500 Subject: [PATCH 2/5] Address feedback - Add type annotations to decide_handleRawKey extension point parameters - Rename speechPaused to post_speechPaused to follow coding standards for post-event naming - Add PR reference (#17428) to changelog entry - Format extension point names in changelog with backticks --- projectDocs/dev/developerGuide/developerGuide.md | 2 +- source/inputCore.py | 5 +++++ source/speech/__init__.py | 4 ++-- source/speech/extensions.py | 2 +- source/speech/speech.py | 4 ++-- tests/unit/test_speech.py | 8 ++++---- user_docs/en/changes.md | 7 +++---- 7 files changed, 18 insertions(+), 14 deletions(-) diff --git a/projectDocs/dev/developerGuide/developerGuide.md b/projectDocs/dev/developerGuide/developerGuide.md index 3559a0932c5..b471171e2df 100644 --- a/projectDocs/dev/developerGuide/developerGuide.md +++ b/projectDocs/dev/developerGuide/developerGuide.md @@ -1383,7 +1383,7 @@ For examples of how to define and use new extension points, please see the code |`Action` |`speechCanceled` |Triggered when speech is canceled.| |`Action` |`pre_speechCanceled` |Triggered before speech is canceled.| |`Action` |`pre_speech` |Triggered before NVDA handles prepared speech.| -|`Action` |`speechPaused` |Triggered when speech is paused or resumed.| +|`Action` |`post_speechPaused` |Triggered when speech is paused or resumed.| |`Filter` |`filter_speechSequence` |Allows components or add-ons to filter speech sequence before it passes to the synth driver.| ### synthDriverHandler {#synthDriverHandlerExtPts} diff --git a/source/inputCore.py b/source/inputCore.py index cf11d6efb45..413ba2ae9a3 100644 --- a/source/inputCore.py +++ b/source/inputCore.py @@ -457,10 +457,15 @@ def __eq__(self, other: Any) -> bool: Notifies when a raw keyboard event is received, before any NVDA processing. Handlers can decide whether the key should be processed by NVDA and/or passed to the OS. @param vkCode: The virtual key code +@type vkCode: int @param scanCode: The scan code +@type scanCode: int @param extended: Whether this is an extended key +@type extended: bool @param pressed: Whether this is a key press or release +@type pressed: bool @return: True to allow normal processing, False to block the key +@rtype: bool """ decide_executeGesture = extensionPoints.Decider() diff --git a/source/speech/__init__.py b/source/speech/__init__.py index 5e31b01b002..7090bd8ab50 100644 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -63,7 +63,7 @@ spellTextInfo, splitTextIndentation, ) -from .extensions import speechCanceled, speechPaused +from .extensions import speechCanceled, post_speechPaused from .priorities import Spri from .types import ( @@ -142,7 +142,7 @@ "spellTextInfo", "splitTextIndentation", "speechCanceled", - "speechPaused", + "post_speechPaused", ] import synthDriverHandler diff --git a/source/speech/extensions.py b/source/speech/extensions.py index 049c5118c66..f8dd885ba4a 100644 --- a/source/speech/extensions.py +++ b/source/speech/extensions.py @@ -22,7 +22,7 @@ Handlers are called without arguments. """ -speechPaused = Action() +post_speechPaused = Action() """ Notifies when speech is paused. diff --git a/source/speech/speech.py b/source/speech/speech.py index 3153208b8dc..49992c0b03f 100644 --- a/source/speech/speech.py +++ b/source/speech/speech.py @@ -27,7 +27,7 @@ from textUtils import unicodeNormalize from textUtils.uniscribe import splitAtCharacterBoundaries from . import manager -from .extensions import speechCanceled, speechPaused, pre_speechCanceled, pre_speech +from .extensions import speechCanceled, post_speechPaused, pre_speechCanceled, pre_speech from .extensions import filter_speechSequence from .commands import ( # Commands that are used in this file. @@ -211,7 +211,7 @@ def cancelSpeech(): def pauseSpeech(switch): getSynth().pause(switch) - speechPaused.notify(switch=switch) + post_speechPaused.notify(switch=switch) _speechState.isPaused = switch _speechState.beenCanceled = False diff --git a/tests/unit/test_speech.py b/tests/unit/test_speech.py index fb0b60eaea1..a50557f1c4e 100644 --- a/tests/unit/test_speech.py +++ b/tests/unit/test_speech.py @@ -18,7 +18,7 @@ cancelSpeech, pauseSpeech, speechCanceled, - speechPaused, + post_speechPaused, ) from speech.commands import ( BeepCommand, @@ -594,9 +594,9 @@ def test_speechCanceledExtensionPoint(self): ): cancelSpeech() - def test_speechPausedExtensionPoint(self): - with actionTester(self, speechPaused, switch=True): + def test_post_speechPausedExtensionPoint(self): + with actionTester(self, post_speechPaused, switch=True): pauseSpeech(True) - with actionTester(self, speechPaused, switch=False): + with actionTester(self, post_speechPaused, switch=False): pauseSpeech(False) diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index d4622a9fcb4..b629aa3df8f 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -104,10 +104,9 @@ Add-ons will need to be re-tested and have their manifest updated. * Added the [VS Code workspace configuration for NVDA](https://nvaccess.org/nvaccess/vscode-nvda) as a git submodule. (#17003) * In the `brailleTables` module, a `getDefaultTableForCurrentLang` function has been added (#17222, @nvdaes) * Retrieving the `labeledBy` property now works for UIA elements supporting the corresponding `LabeledBy` UIA property. (#17442, @michaelweghorn) -* Added the following extension points: - * inputCore.decide_handleRawKey: called on each keypress - * speech.extensions.speechPaused: Called when speech is paused or unpaused -pause) +* Added the following extension points (#17428): + * `inputCore.decide_handleRawKey`: called on each keypress + * `speech.extensions.post_speechPaused`: Called when speech is paused or unpaused #### API Breaking Changes From 836de545e7773a8dfabb3fdaae301ac09b99d55a Mon Sep 17 00:00:00 2001 From: Christopher Toth Date: Sat, 30 Nov 2024 00:00:04 -0500 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: Sean Budd --- source/inputCore.py | 20 ++++++++++---------- source/speech/extensions.py | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/source/inputCore.py b/source/inputCore.py index 413ba2ae9a3..54a638c739f 100644 --- a/source/inputCore.py +++ b/source/inputCore.py @@ -456,16 +456,16 @@ def __eq__(self, other: Any) -> bool: """ Notifies when a raw keyboard event is received, before any NVDA processing. Handlers can decide whether the key should be processed by NVDA and/or passed to the OS. -@param vkCode: The virtual key code -@type vkCode: int -@param scanCode: The scan code -@type scanCode: int -@param extended: Whether this is an extended key -@type extended: bool -@param pressed: Whether this is a key press or release -@type pressed: bool -@return: True to allow normal processing, False to block the key -@rtype: bool +:param vkCode: The virtual key code +:type vkCode: int +:param scanCode: The scan code +:type scanCode: int +:param extended: Whether this is an extended key +:type extended: bool +:param pressed: Whether this is a key press or release +:type pressed: bool +:return: True to allow normal processing, False to block the key +:rtype: bool """ decide_executeGesture = extensionPoints.Decider() diff --git a/source/speech/extensions.py b/source/speech/extensions.py index f8dd885ba4a..ae74f3115a1 100644 --- a/source/speech/extensions.py +++ b/source/speech/extensions.py @@ -26,8 +26,8 @@ """ Notifies when speech is paused. -@param switch: True if speech is paused, False if speech is resumed. -@type switch: bool +:param switch: True if speech is paused, False if speech is resumed. +:type switch: bool """ pre_speech = Action() From f0dd7feb7ebd6dfa2a725be941d893b183334147 Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Mon, 2 Dec 2024 10:32:03 +1100 Subject: [PATCH 4/5] Update user_docs/en/changes.md --- user_docs/en/changes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index b629aa3df8f..60e2d623d25 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -106,7 +106,7 @@ Add-ons will need to be re-tested and have their manifest updated. * Retrieving the `labeledBy` property now works for UIA elements supporting the corresponding `LabeledBy` UIA property. (#17442, @michaelweghorn) * Added the following extension points (#17428): * `inputCore.decide_handleRawKey`: called on each keypress - * `speech.extensions.post_speechPaused`: Called when speech is paused or unpaused + * `speech.extensions.post_speechPaused`: called when speech is paused or unpaused #### API Breaking Changes From d4904d5dcb5043d7c8c2f879e86b113daf37a884 Mon Sep 17 00:00:00 2001 From: Christopher Toth Date: Mon, 2 Dec 2024 00:08:09 -0500 Subject: [PATCH 5/5] Attribute change Co-authored-by: Cyrille Bougot --- user_docs/en/changes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 60e2d623d25..6416af4ee5d 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -104,7 +104,7 @@ Add-ons will need to be re-tested and have their manifest updated. * Added the [VS Code workspace configuration for NVDA](https://nvaccess.org/nvaccess/vscode-nvda) as a git submodule. (#17003) * In the `brailleTables` module, a `getDefaultTableForCurrentLang` function has been added (#17222, @nvdaes) * Retrieving the `labeledBy` property now works for UIA elements supporting the corresponding `LabeledBy` UIA property. (#17442, @michaelweghorn) -* Added the following extension points (#17428): +* Added the following extension points (#17428, @ctoth): * `inputCore.decide_handleRawKey`: called on each keypress * `speech.extensions.post_speechPaused`: called when speech is paused or unpaused