4040 SuppressUnicodeNormalizationCommand ,
4141 CharacterModeCommand ,
4242 WaveFileCommand ,
43+ CallbackCommand ,
44+ _CancellableSpeechCommand ,
4345)
4446from .shortcutKeys import getKeyboardShortcutsSpeech
4547
8789_speechState : Optional ["SpeechState" ] = None
8890_curWordChars : List [str ] = []
8991IDEOGRAPHIC_COMMA : Final [str ] = "\u3001 "
92+ _lastSpeech : tuple [SpeechSequence , characterProcessing .SymbolLevel | None ] | None = None
93+ """Last spoken text and the symbol level with which it was spoken."""
9094
9195
9296class SpeechMode (DisplayStringIntEnum ):
@@ -141,6 +145,24 @@ def setSpeechMode(newMode: SpeechMode):
141145 _speechState .speechMode = newMode
142146
143147
148+ def _setLastSpeechString (
149+ speechSequence : SpeechSequence ,
150+ symbolLevel : characterProcessing .SymbolLevel | None ,
151+ priority : Spri ,
152+ ):
153+ # Check if the speech sequence contains text to speak
154+ if any (isinstance (item , str ) for item in speechSequence ):
155+ global _lastSpeech
156+ _lastSpeech = (
157+ [
158+ item
159+ for item in speechSequence
160+ if not isinstance (item , (CallbackCommand , _CancellableSpeechCommand ))
161+ ],
162+ symbolLevel ,
163+ )
164+
165+
144166def initialize ():
145167 global _speechState
146168 _speechState = SpeechState ()
@@ -365,6 +387,8 @@ def _getSpellingSpeechAddCharMode(
365387 yield CharacterModeCommand (False )
366388 charMode = False
367389 yield item
390+ if charMode :
391+ yield CharacterModeCommand (False )
368392
369393
370394def _getSpellingCharAddCapNotification (
@@ -425,6 +449,7 @@ def _getSpellingSpeechWithoutCharMode(
425449 fallbackToCharIfNoDescription : bool = True ,
426450 unicodeNormalization : bool = False ,
427451 reportNormalizedForCharacterNavigation : bool = False ,
452+ endsUtterance : bool = True ,
428453) -> Generator [SequenceItemT , None , None ]:
429454 """
430455 Processes text when spelling by character.
@@ -446,6 +471,7 @@ def _getSpellingSpeechWithoutCharMode(
446471 :param unicodeNormalization: Whether to use Unicode normalization for the given text.
447472 :param reportNormalizedForCharacterNavigation: When unicodeNormalization is true, indicates if 'normalized'
448473 should be reported along with the currently spelled character.
474+ :param endsUtterance: Whether an EndUtteranceCommand should be yielded at the end.
449475 :returns: A speech sequence generator.
450476 """
451477 defaultLanguage = getCurrentLanguage ()
@@ -521,7 +547,8 @@ def _getSpellingSpeechWithoutCharMode(
521547 uppercase and beepForCapitals ,
522548 itemIsNormalized and reportNormalizedForCharacterNavigation ,
523549 )
524- yield EndUtteranceCommand ()
550+ if endsUtterance :
551+ yield EndUtteranceCommand ()
525552
526553
527554def getSingleCharDescriptionDelayMS () -> int :
@@ -574,9 +601,20 @@ def getSingleCharDescription(
574601
575602def getSpellingSpeech (
576603 text : str ,
577- locale : Optional [ str ] = None ,
604+ locale : str | None = None ,
578605 useCharacterDescriptions : bool = False ,
606+ endsUtterance : bool = True ,
607+ useCharMode : bool = True ,
579608) -> Generator [SequenceItemT , None , None ]:
609+ """
610+ Gets a speech sequence for spelling text.
611+ :param text: The text to be spelled.
612+ :param locale: The locale to use for character descriptions, if applicable.
613+ :param useCharacterDescriptions: Whether or not to use character descriptions, e.g. speak "a" as "alpha".
614+ :param endsUtterance: Whether an EndUtteranceCommand should be yielded at the end.
615+ :param useCharMode: Whether to wrap the sequence in CharacterModeCommand.
616+ :returns: A speech sequence generator.
617+ """
580618 synth = getSynth ()
581619 synthConfig = config .conf ["speech" ][synth .name ]
582620
@@ -598,8 +636,9 @@ def getSpellingSpeech(
598636 reportNormalizedForCharacterNavigation = config .conf ["speech" ][
599637 "reportNormalizedForCharacterNavigation"
600638 ],
639+ endsUtterance = endsUtterance ,
601640 )
602- if synthConfig ["useSpellingFunctionality" ]:
641+ if useCharMode and synthConfig ["useSpellingFunctionality" ]:
603642 seq = _getSpellingSpeechAddCharMode (seq )
604643 # This function applies Unicode normalization as appropriate.
605644 # Therefore, suppress the global normalization that might still occur
@@ -1189,7 +1228,7 @@ def speak( # noqa: C901
11891228
11901229def speakPreselectedText (
11911230 text : str ,
1192- priority : Optional [ Spri ] = None ,
1231+ priority : Spri | None = None ,
11931232):
11941233 """Helper method to announce that a newly focused control already has
11951234 text selected. This method is in contrast with L{speakTextSelected}.
@@ -1228,8 +1267,8 @@ def getPreselectedTextSpeech(
12281267
12291268
12301269def speakTextSelected (
1231- text : str ,
1232- priority : Optional [ Spri ] = None ,
1270+ text : str | SpeechSequence ,
1271+ priority : Spri | None = None ,
12331272):
12341273 """Helper method to announce that the user has caused text to be selected.
12351274 This method is in contrast with L{speakPreselectedText}.
@@ -1246,8 +1285,8 @@ def speakTextSelected(
12461285
12471286def speakSelectionMessage (
12481287 message : str ,
1249- text : str ,
1250- priority : Optional [ Spri ] = None ,
1288+ text : str | SpeechSequence ,
1289+ priority : Spri | None = None ,
12511290):
12521291 seq = _getSelectionMessageSpeech (message , text )
12531292 if seq :
@@ -1259,8 +1298,26 @@ def speakSelectionMessage(
12591298
12601299def _getSelectionMessageSpeech (
12611300 message : str ,
1262- text : str ,
1301+ text : str | SpeechSequence ,
12631302) -> SpeechSequence :
1303+ if isinstance (text , list ):
1304+ # If text is a speech sequence, we can't use string formatting.
1305+ # Instead, split the message by %s and insert the sequence.
1306+ # This allows for correct localization order (e.g. prefix vs suffix).
1307+ prefix , sep , suffix = message .partition ("%s" )
1308+ if not sep :
1309+ log .warning ("Selection message '%s' does not contain '%%s'" , message )
1310+ return _getSpeakMessageSpeech (message ) + text
1311+
1312+ seq = list (text )
1313+ # Insert prefix/suffix as separate items so they remain outside any
1314+ # speech commands (e.g. PitchCommand for capitals).
1315+ if prefix :
1316+ seq .insert (0 , prefix )
1317+ if suffix :
1318+ seq .append (suffix )
1319+ return seq
1320+
12641321 if len (text ) < MAX_LENGTH_FOR_SELECTION_REPORTING :
12651322 return _getSpeakMessageSpeech (message % text )
12661323 textLength = len (text )
@@ -1279,7 +1336,7 @@ def speakSelectionChange( # noqa: C901
12791336 speakSelected : bool = True ,
12801337 speakUnselected : bool = True ,
12811338 generalize : bool = False ,
1282- priority : Optional [ Spri ] = None ,
1339+ priority : Spri | None = None ,
12831340):
12841341 """Speaks a change in selection, either selected or unselected text.
12851342 @param oldInfo: a TextInfo instance representing what the selection was before
@@ -1327,25 +1384,25 @@ def speakSelectionChange( # noqa: C901
13271384 if not generalize :
13281385 for text in selectedTextList :
13291386 if len (text ) == 1 :
1330- text = characterProcessing . processSpeechSymbol ( locale , text )
1387+ text = list ( getSpellingSpeech ( text , locale , endsUtterance = False , useCharMode = False ) )
13311388 speakTextSelected (text , priority = priority )
13321389 elif len (selectedTextList ) > 0 :
13331390 text = newInfo .text
13341391 if len (text ) == 1 :
1335- text = characterProcessing . processSpeechSymbol ( locale , text )
1392+ text = list ( getSpellingSpeech ( text , locale , endsUtterance = False , useCharMode = False ) )
13361393 speakTextSelected (text , priority = priority )
13371394 if speakUnselected :
13381395 if not generalize :
13391396 for text in unselectedTextList :
13401397 if len (text ) == 1 :
1341- text = characterProcessing . processSpeechSymbol ( locale , text )
1398+ text = list ( getSpellingSpeech ( text , locale , endsUtterance = False , useCharMode = False ) )
13421399 # Translators: This is spoken to indicate what has been unselected. for example 'hello unselected'
13431400 speakSelectionMessage (_ ("%s unselected" ), text , priority = priority )
13441401 elif len (unselectedTextList ) > 0 :
13451402 if not newInfo .isCollapsed :
13461403 text = newInfo .text
13471404 if len (text ) == 1 :
1348- text = characterProcessing . processSpeechSymbol ( locale , text )
1405+ text = list ( getSpellingSpeech ( text , locale , endsUtterance = False , useCharMode = False ) )
13491406 # Translators: This is spoken to indicate when the previous selection was removed and a new selection was made. for example 'hello world selected instead'
13501407 speakSelectionMessage (_ ("%s selected instead" ), text , priority = priority )
13511408 else :
@@ -3018,7 +3075,7 @@ def getFormatFieldSpeech( # noqa: C901
30183075 if formatConfig ["reportSpellingErrors2" ] & ReportSpellingErrors .SPEECH .value :
30193076 # Translators: Reported when text contains a spelling error.
30203077 texts .append (_ ("spelling error" ))
3021- elif extraDetail :
3078+ elif extraDetail and formatConfig [ "reportSpellingErrors2" ] & ReportSpellingErrors . SPEECH . value :
30223079 # Translators: Reported when moving out of text containing a spelling error.
30233080 texts .append (_ ("out of spelling error" ))
30243081 textList .extend (texts )
@@ -3032,7 +3089,7 @@ def getFormatFieldSpeech( # noqa: C901
30323089 if formatConfig ["reportSpellingErrors2" ] & ReportSpellingErrors .SPEECH .value :
30333090 # Translators: Reported when text contains a grammar error.
30343091 texts .append (_ ("grammar error" ))
3035- elif extraDetail :
3092+ elif extraDetail and formatConfig [ "reportSpellingErrors2" ] & ReportSpellingErrors . SPEECH . value :
30363093 # Translators: Reported when moving out of text containing a grammar error.
30373094 texts .append (_ ("out of grammar error" ))
30383095 textList .extend (texts )
0 commit comments