Changed how speech interruptions are handled hopefully improved things being interrupted when they shouldn't be.

This commit is contained in:
Storm Dragon
2026-05-19 15:26:44 -04:00
parent a84aec94a9
commit 009938c495
9 changed files with 130 additions and 25 deletions
+5 -3
View File
@@ -3470,7 +3470,7 @@ class Script(script.Script):
return True
def presentMessage(self, fullMessage, briefMessage=None, voice=None, resetStyles=True,
force=False, interrupt=True):
force=False, interrupt=False):
"""Convenience method to speak a message and 'flash' it in braille.
Arguments:
@@ -3484,6 +3484,8 @@ class Script(script.Script):
the briefMessage should set briefMessage to an empty string.
- voice: The voice to use when speaking this message. By default, the
"system" voice will be used.
- interrupt: If True, any current speech should be interrupted
prior to speaking the new text. The default queues the message.
"""
if not fullMessage:
@@ -3959,7 +3961,7 @@ class Script(script.Script):
voice = self.speechGenerator.voice(string=character)
speech.speakCharacter(character, voice)
def speakMessage(self, string, voice=None, interrupt=True, resetStyles=True, force=False):
def speakMessage(self, string, voice=None, interrupt=False, resetStyles=True, force=False):
"""Method to speak a single string. Scripts should use this
method rather than calling speech.speak directly.
@@ -3967,7 +3969,7 @@ class Script(script.Script):
- voice: The voice to use. By default, the "system" voice will
be used.
- interrupt: If True, any current speech should be interrupted
prior to speaking the new text.
prior to speaking the new text. The default queues the message.
"""
if not cthulhu.cthulhuApp.settingsManager.getSetting('enableSpeech') \
+12 -7
View File
@@ -215,7 +215,7 @@ class Script(default.Script):
self._clearSyntheticWebSelection()
if oldContents:
self.speakContents(oldContents)
self.speakMessage(messages.TEXT_UNSELECTED, interrupt=False)
self.speakMessage(messages.TEXT_UNSELECTED)
return True
self.pointOfReference["syntheticWebSelection"] = {
@@ -247,7 +247,7 @@ class Script(default.Script):
if deltaContents:
self.speakContents(deltaContents)
self.speakMessage(message, interrupt=False)
self.speakMessage(message)
return True
@@ -1083,7 +1083,7 @@ class Script(default.Script):
"""Speaks the specified contents."""
utterances = self.speechGenerator.generateContents(contents, **args)
speech.speak(utterances, interrupt=args.get("interrupt", True))
speech.speak(utterances, interrupt=args.get("interrupt", False))
def sayCharacter(self, obj):
"""Speaks the character at the current caret position."""
@@ -1504,7 +1504,6 @@ class Script(default.Script):
def togglePresentationMode(self, inputEvent, documentFrame=None):
[obj, characterOffset] = self.utilities.getCaretContext(documentFrame)
interrupt = inputEvent is not None
if self._inFocusMode:
parent = AXObject.get_parent(obj)
if AXUtilities.is_list_box(parent):
@@ -1512,7 +1511,10 @@ class Script(default.Script):
elif AXUtilities.is_menu(parent):
self.utilities.setCaretContext(AXObject.get_parent(parent), -1)
if not self._loadingDocumentContent:
self.presentMessage(messages.MODE_BROWSE, interrupt=interrupt)
if inputEvent is not None:
self.presentMessage(messages.MODE_BROWSE, interrupt=True)
else:
self.presentMessage(messages.MODE_BROWSE)
if not self._shouldSuppressBrowseModeSound(obj, inputEvent):
sound_theme_manager.getManager().playBrowseModeSound()
else:
@@ -1522,7 +1524,10 @@ class Script(default.Script):
or inputEvent):
self.utilities.grabFocus(obj)
self.presentMessage(messages.MODE_FOCUS, interrupt=interrupt)
if inputEvent is not None:
self.presentMessage(messages.MODE_FOCUS, interrupt=True)
else:
self.presentMessage(messages.MODE_FOCUS)
sound_theme_manager.getManager().playFocusModeSound()
self._inFocusMode = not self._inFocusMode
self._focusModeIsSticky = False
@@ -2689,7 +2694,7 @@ class Script(default.Script):
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return True
self.presentMessage(event.any_data, interrupt=False)
self.presentMessage(event.any_data)
return True
def onNameChanged(self, event):
+3 -2
View File
@@ -425,10 +425,11 @@ def _speak(text: str, acss: Optional[Any], interrupt: bool) -> None:
debug.printMessage(debug.LEVEL_INFO, msg, True)
_speechserver.speak(text, resolvedVoice, interrupt) # type: ignore
def speak(content: Union[str, List[Any]], acss: Optional[Any] = None, interrupt: bool = True) -> None:
def speak(content: Union[str, List[Any]], acss: Optional[Any] = None, interrupt: bool = False) -> None:
"""Speaks the given content. The content can be either a simple
string or an array of arrays of objects returned by a speech
generator."""
generator. Speech queues by default; callers that need to cancel
current output should pass interrupt=True or call presentationInterrupt()."""
if settings.silenceSpeech:
return
+1 -1
View File
@@ -632,7 +632,7 @@ class SpeechServer(speechserver.SpeechServer):
return families
def speak(self, text=None, acss=None, interrupt=True):
def speak(self, text=None, acss=None, interrupt=False):
if not text:
return
+3 -4
View File
@@ -946,7 +946,7 @@ class StructuralNavigation:
for match in matches:
if _isValidMatch(match):
structuralNavigationObject.present(match, arg)
self._script.presentMessage(wrapMessage, interrupt=False)
self._script.presentMessage(wrapMessage)
return
structuralNavigationObject.present(None, arg)
@@ -2219,14 +2219,13 @@ class StructuralNavigation:
if settings.speakCellCoordinates:
[row, col] = self.getCellCoordinates(cell)
self._script.presentMessage(
messages.TABLE_CELL_COORDINATES % {"row": row + 1, "column": col + 1},
interrupt=False,
messages.TABLE_CELL_COORDINATES % {"row": row + 1, "column": col + 1}
)
rowspan, colspan = self._script.utilities.rowAndColumnSpan(cell)
spanString = messages.cellSpan(rowspan, colspan)
if spanString and settings.speakCellSpan:
self._script.presentMessage(spanString, interrupt=False)
self._script.presentMessage(spanString)
########################
# #
@@ -0,0 +1,91 @@
import sys
import unittest
from pathlib import Path
from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from cthulhu import settings
from cthulhu import speech
from cthulhu import cthulhu_state
from cthulhu.scripts import default
class SpeechDefaultPolicyRegressionTests(unittest.TestCase):
def _make_settings_manager(self):
values = {
"enableSpeech": True,
"onlySpeakDisplayedText": False,
"messagesAreDetailed": True,
"enableBraille": False,
"enableBrailleMonitor": False,
"enableFlashMessages": False,
"voices": {settings.SYSTEM_VOICE: "system"},
"capitalizationStyle": settings.CAPITALIZATION_STYLE_NONE,
"verbalizePunctuationStyle": settings.PUNCTUATION_STYLE_NONE,
}
manager = mock.Mock()
manager.getSetting.side_effect = values.get
return manager
def _make_script(self):
testScript = default.Script.__new__(default.Script)
testScript.speechAndVerbosityManager = mock.Mock()
return testScript
def test_speech_speak_queues_by_default(self):
server = mock.Mock()
with (
mock.patch.object(speech, "_speechserver", server),
mock.patch.object(speech, "_write_to_monitor"),
mock.patch.object(speech.speech_history, "add"),
mock.patch.object(cthulhu_state, "activeScript", None),
):
speech.speak("status")
server.speak.assert_called_once()
self.assertFalse(server.speak.call_args.args[2])
def test_speech_speak_explicit_interrupt_is_preserved(self):
server = mock.Mock()
with (
mock.patch.object(speech, "_speechserver", server),
mock.patch.object(speech, "_write_to_monitor"),
mock.patch.object(speech.speech_history, "add"),
mock.patch.object(cthulhu_state, "activeScript", None),
):
speech.speak("status", interrupt=True)
server.speak.assert_called_once()
self.assertTrue(server.speak.call_args.args[2])
def test_present_message_queues_by_default(self):
testScript = self._make_script()
manager = self._make_settings_manager()
with (
mock.patch.object(default.cthulhu.cthulhuApp, "settingsManager", manager),
mock.patch.object(default.speech, "speak") as speak,
):
default.Script.presentMessage(testScript, "Status")
speak.assert_called_once_with("Status", "system", False)
def test_present_message_explicit_interrupt_is_preserved(self):
testScript = self._make_script()
manager = self._make_settings_manager()
with (
mock.patch.object(default.cthulhu.cthulhuApp, "settingsManager", manager),
mock.patch.object(default.speech, "speak") as speak,
):
default.Script.presentMessage(testScript, "Status", interrupt=True)
speak.assert_called_once_with("Status", "system", True)
if __name__ == "__main__":
unittest.main()
@@ -30,6 +30,14 @@ class SpeechDispatcherInterruptRegressionTests(unittest.TestCase):
server._cancel.assert_called_once_with()
server._speak.assert_called_once_with("long utterance", None)
def test_string_speech_queues_by_default(self):
server = self._make_server()
server.speak("next")
server._cancel.assert_not_called()
server._speak.assert_called_once_with("next", None)
def test_recent_key_echo_suppresses_backend_cancel(self):
server = self._make_server()
server._lastKeyEchoTime = time.time()
@@ -38,8 +38,7 @@ class StructuralNavigationTableRegressionTests(unittest.TestCase):
navigator._presentObject.assert_called_once_with("cell", 0)
navigator._script.presentMessage.assert_called_once_with(
messages.TABLE_CELL_COORDINATES % {"row": 2, "column": 3},
interrupt=False,
messages.TABLE_CELL_COORDINATES % {"row": 2, "column": 3}
)
def test_wrapping_announcement_does_not_interrupt_wrapped_object(self):
@@ -78,7 +77,7 @@ class StructuralNavigationTableRegressionTests(unittest.TestCase):
events,
[
("present", first, "arg"),
("message", messages.WRAPPING_TO_TOP, {"interrupt": False}),
("message", messages.WRAPPING_TO_TOP, {}),
],
)
+5 -5
View File
@@ -182,7 +182,7 @@ class WebPresentationModeSpeechRegressionTests(unittest.TestCase):
):
web_script.Script.togglePresentationMode(testScript, None, "document")
testScript.presentMessage.assert_called_once_with(messages.MODE_BROWSE, interrupt=False)
testScript.presentMessage.assert_called_once_with(messages.MODE_BROWSE)
soundManager.playBrowseModeSound.assert_called_once_with()
self.assertFalse(testScript._inFocusMode)
self.assertFalse(testScript._focusModeIsSticky)
@@ -200,7 +200,7 @@ class WebPresentationModeSpeechRegressionTests(unittest.TestCase):
):
web_script.Script.togglePresentationMode(testScript, None, "document")
testScript.presentMessage.assert_called_once_with(messages.MODE_FOCUS, interrupt=False)
testScript.presentMessage.assert_called_once_with(messages.MODE_FOCUS)
soundManager.playFocusModeSound.assert_called_once_with()
self.assertTrue(testScript._inFocusMode)
self.assertFalse(testScript._focusModeIsSticky)
@@ -311,7 +311,7 @@ class WebDescriptionChangeRegressionTests(unittest.TestCase):
result = web_script.Script.onDescriptionChanged(testScript, event)
self.assertTrue(result)
testScript.presentMessage.assert_called_once_with("Helpful tooltip", interrupt=False)
testScript.presentMessage.assert_called_once_with("Helpful tooltip")
if __name__ == "__main__":
@@ -378,7 +378,7 @@ class WebSelectionRegressionTests(unittest.TestCase):
testScript.utilities.setCaretContext.assert_called_once_with(section, 1, document)
setLocusOfFocus.assert_called_once_with(event, section, False, True)
testScript.speakContents.assert_called_once_with([[section, 0, 1, "D"]])
testScript.speakMessage.assert_called_once_with(messages.TEXT_SELECTED, interrupt=False)
testScript.speakMessage.assert_called_once_with(messages.TEXT_SELECTED)
self.assertEqual(
testScript.pointOfReference["syntheticWebSelection"]["string"],
"D",
@@ -414,7 +414,7 @@ class WebSelectionRegressionTests(unittest.TestCase):
testScript.utilities.setCaretContext.assert_called_once_with(link, 0, document)
setLocusOfFocus.assert_called_once_with(event, link, False, True)
testScript.speakContents.assert_called_once_with([[link, 0, 1, "S"]])
testScript.speakMessage.assert_called_once_with(messages.TEXT_SELECTED, interrupt=False)
testScript.speakMessage.assert_called_once_with(messages.TEXT_SELECTED)
self.assertEqual(
testScript.pointOfReference["syntheticWebSelection"]["string"],
"S",