@@ -1725,8 +1725,7 @@ def getPropertiesSpeech( # noqa: C901
17251725 textList .append (description )
17261726 # sometimes keyboardShortcut key is present but value is None
17271727 keyboardShortcut : Optional [str ] = propertyValues .get ('keyboardShortcut' )
1728- if keyboardShortcut :
1729- textList .append (keyboardShortcut )
1728+ textList .extend (getKeyboardShortcutsSpeech (keyboardShortcut ))
17301729 if includeTableCellCoords and cellCoordsText :
17311730 textList .append (cellCoordsText )
17321731 if cellCoordsText or rowNumber or columnNumber :
@@ -1861,6 +1860,105 @@ def getPropertiesSpeech( # noqa: C901
18611860 return textList
18621861
18631862
1863+ def speakKeyboardShortcuts (keyboardShortcutsStr : Optional [str ]) -> SpeechSequence :
1864+ speak (getKeyboardShortcutsSpeech (keyboardShortcutsStr ))
1865+
1866+
1867+ def getKeyboardShortcutsSpeech (keyboardShortcutsStr : Optional [str ]) -> SpeechSequence :
1868+ """Gets the speech sequence for a shortcuts string containing one or more shortcuts.
1869+ @param keyboardShortcutsStr: the shortcuts string.
1870+ """
1871+
1872+ SHORTCUT_KEY_LIST_SEPARATOR = ' '
1873+ seq = []
1874+ if not keyboardShortcutsStr :
1875+ return seq
1876+ try :
1877+ for shortcutKeyStr in keyboardShortcutsStr .split (SHORTCUT_KEY_LIST_SEPARATOR ):
1878+ seq .extend (_getKeyboardShortcutSpeech (shortcutKeyStr ))
1879+ seq .append (SHORTCUT_KEY_LIST_SEPARATOR )
1880+ seq .pop () # Remove last SHORTCUT_KEY_LIST_SEPARATOR in the sequence
1881+ except Exception :
1882+ log .warning (
1883+ f'Error parsing keyboard shortcut "{ keyboardShortcutsStr } ", reporting the string as a fallback.'
1884+ )
1885+ return [keyboardShortcutsStr ]
1886+
1887+ # Merge consecutive strings in the list when possible
1888+ seqOut = []
1889+ for item in seq :
1890+ if len (seqOut ) > 0 and isinstance (seqOut [- 1 ], str ) and isinstance (item , str ):
1891+ seqOut [- 1 ] = seqOut [- 1 ] + item
1892+ else :
1893+ seqOut .append (item )
1894+
1895+ return seqOut
1896+
1897+
1898+ def _getKeyboardShortcutSpeech (keyboardShortcut : str ) -> SpeechSequence :
1899+ """Gets the speech sequence for a single shortcut string.
1900+ @param keyboardShortcutStr: the shortcuts string.
1901+ """
1902+
1903+ keyList , separators = _splitShortcut (keyboardShortcut )
1904+ seq = []
1905+ for key , sep in zip (keyList [:- 1 ], separators ):
1906+ seq .extend (_getKeySpeech (key ))
1907+ seq .append (sep )
1908+ seq .extend (_getKeySpeech (keyList [- 1 ]))
1909+ return seq
1910+
1911+
1912+ def _getKeySpeech (key : str ) -> SpeechSequence :
1913+ """Gets the speech sequence for a string describing a key.
1914+ @param keyStr: the key string.
1915+ """
1916+ if len (key ) > 1 :
1917+ return [key ]
1918+ locale = getCurrentLanguage ()
1919+ keySymbol = characterProcessing .processSpeechSymbol (locale , key )
1920+ if keySymbol != key :
1921+ return [keySymbol ]
1922+ return [
1923+ CharacterModeCommand (True ),
1924+ key ,
1925+ CharacterModeCommand (False ),
1926+ ]
1927+
1928+
1929+ def _splitShortcut (shortcut : str ) -> SpeechSequence :
1930+ if ', ' in shortcut :
1931+ return _splitSequentialShortcut (shortcut )
1932+ if ' + ' in shortcut :
1933+ separator = ' + '
1934+ elif '+' in shortcut :
1935+ separator = '+'
1936+ else :
1937+ separator = None
1938+ if separator :
1939+ keyList = shortcut .split (separator )
1940+ else :
1941+ keyList = [shortcut ]
1942+ separators = [separator ] * (len (keyList ) - 1 )
1943+ return keyList , separators
1944+
1945+
1946+ def _splitSequentialShortcut (shortcut : str ) -> SpeechSequence :
1947+ keys = []
1948+ separators = []
1949+ RE_SEQ_SHORTCUT_SPLITTING = re .compile (r'^(?P<key>[^, ]+)(?P<sep> |, )(?P<tail>.+)' )
1950+ tail = shortcut
1951+ while len (tail ) > 0 :
1952+ m = RE_SEQ_SHORTCUT_SPLITTING .match (tail )
1953+ if not m :
1954+ keys .append (tail )
1955+ return keys , separators
1956+ keys .append (m ['key' ])
1957+ separators .append (m ['sep' ])
1958+ tail = m ['tail' ]
1959+ raise RuntimeError (f'Wrong sequential shortcut string format: { shortcut } ' )
1960+
1961+
18641962def _shouldSpeakContentFirst (
18651963 reason : OutputReason ,
18661964 role : int ,
0 commit comments