Make synth fallback check all other synths, not just later ones (#19603)
Summary of the issue:
If the default speech synthesizer is set to a synth that can't be loaded
for some reason, and the only working synth(s) are before that synth in
`synthDriverHandler.defaultSynthPriorityList`, NVDA will fall back to
`silence`.
That is, as reported in #19582, if the default synth is eSpeak and it
can't be loaded, NVDA doesn't produce speech at all.
Description of user facing changes:
If eSpeak is set as the default synth and it can't be loaded, NVDA will
first try to use OneCore before falling back to no speech.
Description of developer facing changes:
`synthDriverHandler.setSynth` and `findAndSetNextSynth` now optionally
take an internal-only keyword-only `_leftToTry` list of synth names (in
reverse order of priority). The behaviour of `findAndSetNextSynth` has
changed to find the first working synth in `defaultSynthPriorityList`,
rather than only considering synths after `currentSynthName`. As a
result, `setSynth` may fall back to a synth anywhere in
`defaultSynthPriorityList`, and will preference synths earlier therein.
Description of development approach:
Update `synthDriverHandler.setSynth` and `findNextSynth` to take an
optional list of synth names. If `None`, it is calculated in
`findAndSetNextSynth` from `currentSynthName` and
`defaultSynthPriorityList`.
In `findAndSetNextSynth`, rather than finding the index of
`currentSynthName` in `defaultSynthPriorityList` and attempting to use
the next synth, pop a synth name from `leftToTry` and attempt to use
that synth. as `findAndSetNextSynth` calls `setSynth` with the popped
name and the remainder of `leftToTry`, and `setSynth` calls
`findAndSetNextSynth` with what it received as `leftToTry`, this will
consume `leftToTry` tail-first until either a working synth is found or
`leftToTry` is exhausted.
If `findAndSetNextSynth` receives `None` as the value of `leftToTry`, it
uses `defaultSynthPriorityList` in reverse order, excluding
`currentSynthName`. Otherwise, `currentSynthName` is ignored.
Since, if unspecified, `leftToTry` defaults to
`defaultSynthPriorityList`, and `findAndSetNextSynth` consumes
`leftToTry` in reverse, we try synths from `defaultSynthPriorityList` in
order (axiomatically, `reverss(reverse(sequence)) = sequence`).
Testing strategy:
Raised `RuntimeError` from `synthDrivers.espeak.SynthDriver.__init__`
and/or `synthDriverHandler.OneCore.OneCoreSynthDriver.__init__`.
Observed the following behaviour:
* Default synth OneCore:
* OneCore raises: eSpeak NG is used at startup
* oneCore does not raise: OneCore is used at startup
* Default synth eSpeak NG:
* eSpeak NG raises: oneCore is used at startup
* eSpeak NG does not raise: eSpeak NG is used at startup
* Default synth OneCore or eSpeak NG:
* OneCore and eSpeak NG raise: No speech is used at startup
* Default synth SAPI 5:
* eSpeak NG raises: when attempting to select eSpeak NG, SAPI 5
continues to be used and an error is shown.
Known issues with pull request:
None known