22# This file is covered by the GNU General Public License.
33# See the file COPYING for more details.
44# Copyright (C) 2006-2022 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Babbage B.V., Bill Dengler,
5- # Julien Cochuyt
5+ # Julien Cochuyt, Derek Riemer
66
77"""High-level functions to speak information.
88"""
2626from . import manager
2727from .commands import (
2828 # Commands that are used in this file.
29+ BreakCommand ,
2930 SpeechCommand ,
3031 PitchCommand ,
3132 LangChangeCommand ,
@@ -338,6 +339,48 @@ def _getSpellingSpeechWithoutCharMode(
338339 yield EndUtteranceCommand ()
339340
340341
342+ def getSingleCharDescriptionDelayMS () -> int :
343+ """
344+ @returns: 1 second, a default delay.
345+ In the future, this should fetch its value from a user defined NVDA idle time.
346+ Blocked by: https://github.com/nvaccess/nvda/issues/13915
347+ """
348+ return 1000
349+
350+
351+ def getSingleCharDescription (
352+ text : str ,
353+ locale : Optional [str ] = None ,
354+ ) -> Generator [SequenceItemT , None , None ]:
355+ # This should only be used for single chars.
356+ if not len (text ) == 1 :
357+ return
358+ synth = getSynth ()
359+ synthConfig = config .conf ["speech" ][synth .name ]
360+ if synth .isSupported ("pitch" ):
361+ capPitchChange = synthConfig ["capPitchChange" ]
362+ else :
363+ capPitchChange = 0
364+ defaultLanguage = getCurrentLanguage ()
365+ if not locale or (
366+ not config .conf ['speech' ]['autoDialectSwitching' ]
367+ and locale .split ('_' )[0 ] == defaultLanguage .split ('_' )[0 ]
368+ ):
369+ locale = defaultLanguage
370+ # If the description for the locale is unknown, we yield nothing.
371+ char , description = getCharDescListFromText (text , locale = locale )[0 ]
372+ uppercase = char .isupper ()
373+ if description is None :
374+ return
375+ yield BreakCommand (getSingleCharDescriptionDelayMS ())
376+ yield from _getSpellingCharAddCapNotification (
377+ description [0 ],
378+ sayCapForCapitals = uppercase and synthConfig ["sayCapForCapitals" ],
379+ capPitchChange = (capPitchChange if uppercase else 0 ),
380+ beepForCapitals = uppercase and synthConfig ["beepForCapitals" ],
381+ )
382+
383+
341384def getSpellingSpeech (
342385 text : str ,
343386 locale : Optional [str ] = None ,
@@ -1169,7 +1212,6 @@ def getTextInfoSpeech( # noqa: C901
11691212 onlyInitialFields : bool = False ,
11701213 suppressBlanks : bool = False
11711214) -> Generator [SpeechSequence , None , bool ]:
1172- onlyCache = reason == OutputReason .ONLYCACHE
11731215 if isinstance (useCache ,SpeakTextInfoState ):
11741216 speakTextInfoState = useCache
11751217 elif useCache :
@@ -1209,8 +1251,8 @@ def getTextInfoSpeech( # noqa: C901
12091251 except KeyError :
12101252 pass
12111253
1212- #Make a new controlFieldStack and formatField from the textInfo's initialFields
1213- newControlFieldStack = []
1254+ # Make a new controlFieldStack and formatField from the textInfo's initialFields
1255+ newControlFieldStack : List [ textInfos . ControlField ] = []
12141256 newFormatField = textInfos .FormatField ()
12151257 initialFields = []
12161258 for field in textWithFields :
@@ -1340,32 +1382,29 @@ def getTextInfoSpeech( # noqa: C901
13401382 language = newFormatField .get ('language' )
13411383 speechSequence .append (LangChangeCommand (language ))
13421384 lastLanguage = language
1343-
1344- def isControlEndFieldCommand (x ):
1345- return isinstance (x , textInfos .FieldCommand ) and x .command == "controlEnd"
1346-
13471385 isWordOrCharUnit = unit in (textInfos .UNIT_CHARACTER , textInfos .UNIT_WORD )
13481386 if onlyInitialFields or (
13491387 isWordOrCharUnit
13501388 and len (textWithFields ) > 0
13511389 and len (textWithFields [0 ].strip () if not textWithFields [0 ].isspace () else textWithFields [0 ]) == 1
1352- and all (isControlEndFieldCommand (x ) for x in itertools .islice (textWithFields , 1 , None ))
1390+ and all (_isControlEndFieldCommand (x ) for x in itertools .islice (textWithFields , 1 , None ))
13531391 ):
1354- if not onlyCache :
1355- if onlyInitialFields or any (isinstance (x , str ) for x in speechSequence ):
1356- yield speechSequence
1357- if not onlyInitialFields :
1358- spellingSequence = list (getSpellingSpeech (
1359- textWithFields [0 ],
1360- locale = language
1361- ))
1362- logBadSequenceTypes (spellingSequence )
1363- yield spellingSequence
1392+ if reason != OutputReason .ONLYCACHE :
1393+ yield from list (_getTextInfoSpeech_considerSpelling (
1394+ unit ,
1395+ onlyInitialFields ,
1396+ textWithFields ,
1397+ reason ,
1398+ speechSequence ,
1399+ language ,
1400+ ))
13641401 if useCache :
1365- speakTextInfoState .controlFieldStackCache = newControlFieldStack
1366- speakTextInfoState .formatFieldAttributesCache = formatFieldAttributesCache
1367- if not isinstance (useCache ,SpeakTextInfoState ):
1368- speakTextInfoState .updateObj ()
1402+ _getTextInfoSpeech_updateCache (
1403+ useCache ,
1404+ speakTextInfoState ,
1405+ newControlFieldStack ,
1406+ formatFieldAttributesCache ,
1407+ )
13691408 return False
13701409
13711410 # Similar to before, but If the most inner clickable is exited, then we allow announcing clickable for the next lot of clickable fields entered.
@@ -1511,12 +1550,14 @@ def isControlEndFieldCommand(x):
15111550 # Translators: This is spoken when the line is considered blank.
15121551 speechSequence .append (_ ("blank" ))
15131552
1514- #Cache a copy of the new controlFieldStack for future use
1553+ # Cache a copy of the new controlFieldStack for future use
15151554 if useCache :
1516- speakTextInfoState .controlFieldStackCache = list (newControlFieldStack )
1517- speakTextInfoState .formatFieldAttributesCache = formatFieldAttributesCache
1518- if not isinstance (useCache ,SpeakTextInfoState ):
1519- speakTextInfoState .updateObj ()
1555+ _getTextInfoSpeech_updateCache (
1556+ useCache ,
1557+ speakTextInfoState ,
1558+ newControlFieldStack ,
1559+ formatFieldAttributesCache ,
1560+ )
15201561
15211562 if reason == OutputReason .ONLYCACHE or not speechSequence :
15221563 return False
@@ -1525,6 +1566,51 @@ def isControlEndFieldCommand(x):
15251566 return True
15261567
15271568
1569+ def _isControlEndFieldCommand (command : Union [str , textInfos .FieldCommand ]):
1570+ return isinstance (command , textInfos .FieldCommand ) and command .command == "controlEnd"
1571+
1572+
1573+ def _getTextInfoSpeech_considerSpelling (
1574+ unit : Optional [textInfos .TextInfo ],
1575+ onlyInitialFields : bool ,
1576+ textWithFields : textInfos .TextInfo .TextWithFieldsT ,
1577+ reason : OutputReason ,
1578+ speechSequence : SpeechSequence ,
1579+ language : str ,
1580+ ) -> Generator [SpeechSequence , None , None ]:
1581+ if onlyInitialFields or any (isinstance (x , str ) for x in speechSequence ):
1582+ yield speechSequence
1583+ if not onlyInitialFields :
1584+ spellingSequence = list (getSpellingSpeech (
1585+ textWithFields [0 ],
1586+ locale = language
1587+ ))
1588+ logBadSequenceTypes (spellingSequence )
1589+ yield spellingSequence
1590+ if (
1591+ reason == OutputReason .CARET
1592+ and unit == textInfos .UNIT_CHARACTER
1593+ and config .conf ["speech" ]["delayedCharacterDescriptions" ]
1594+ ):
1595+ descriptionSequence = list (getSingleCharDescription (
1596+ textWithFields [0 ],
1597+ locale = language ,
1598+ ))
1599+ yield descriptionSequence
1600+
1601+
1602+ def _getTextInfoSpeech_updateCache (
1603+ useCache : Union [bool , SpeakTextInfoState ],
1604+ speakTextInfoState : SpeakTextInfoState ,
1605+ newControlFieldStack : List [textInfos .ControlField ],
1606+ formatFieldAttributesCache : textInfos .Field ,
1607+ ):
1608+ speakTextInfoState .controlFieldStackCache = newControlFieldStack
1609+ speakTextInfoState .formatFieldAttributesCache = formatFieldAttributesCache
1610+ if not isinstance (useCache , SpeakTextInfoState ):
1611+ speakTextInfoState .updateObj ()
1612+
1613+
15281614# C901 'getPropertiesSpeech' is too complex
15291615# Note: when working on getPropertiesSpeech, look for opportunities to simplify
15301616# and move logic out into smaller helper functions.
0 commit comments