diff --git a/addon/globalPlugins/rdAccess/settingsPanel.py b/addon/globalPlugins/rdAccess/settingsPanel.py index c009f61..1df1aa4 100644 --- a/addon/globalPlugins/rdAccess/settingsPanel.py +++ b/addon/globalPlugins/rdAccess/settingsPanel.py @@ -58,9 +58,9 @@ def makeSettings(self, sizer: wx.BoxSizer): serverGroup = guiHelper.BoxSizerHelper(self, sizer=serverGroupSizer) # ty: ignore[invalid-argument-type] sizer_helper.addItem(serverGroup) - # Translators: The label for a setting in RDAccess settings to enable - # automatic recovery of remote speech when the connection was lost. - recoverRemoteSpeechText = _("&Automatically recover remote speech after connection loss") + # Translators: The label for a setting in RDAccess settings to let NVDA + # activate remote speech whenever a remote session offers it. + recoverRemoteSpeechText = _("&Automatically switch to remote speech when available") self.recoverRemoteSpeechCheckbox = serverGroup.addItem( wx.CheckBox(serverGroupBox, label=recoverRemoteSpeechText), ) diff --git a/addon/globalPlugins/rdAccess/synthDetect.py b/addon/globalPlugins/rdAccess/synthDetect.py index 468db3c..0b98504 100644 --- a/addon/globalPlugins/rdAccess/synthDetect.py +++ b/addon/globalPlugins/rdAccess/synthDetect.py @@ -4,6 +4,7 @@ import threading import typing +from collections.abc import Callable from concurrent.futures import Future, ThreadPoolExecutor import addonHandler @@ -25,11 +26,15 @@ class SynthDetector(AutoPropertyObject): + _nvdaHandlePostConfigProfileSwitch: Callable[[bool], None] | None = None + """NVDA's own synthesizer profile switch handler, while replaced by ours.""" + def __init__(self): remoteSynthDriver.synthRemoteDisconnected.register(self._handleRemoteDisconnect) self._executor = ThreadPoolExecutor(1, thread_name_prefix=self.__class__.__name__) self._queuedFuture: Future | None = None self._stopEvent = threading.Event() + self._takeOverPostConfigProfileSwitch() currentSynthesizer: synthDriverHandler.SynthDriver @@ -54,6 +59,46 @@ def _get_isRemoteSynthConfigured(self): assert config.conf is not None return config.conf[remoteSynthDriver._configSection]["synth"] == remoteSynthDriver.name + def _takeOverPostConfigProfileSwitch(self): + """Puts our own handler in NVDA's place, both on L{config.post_configProfileSwitch} + and as L{synthDriverHandler.handlePostConfigProfileSwitch}, + at the start of the registration order. + """ + if self._nvdaHandlePostConfigProfileSwitch is not None: + return + original = synthDriverHandler.handlePostConfigProfileSwitch + if not config.post_configProfileSwitch.unregister(original): + log.debugWarning("NVDA's synthesizer profile switch handler was not registered") + return + self._nvdaHandlePostConfigProfileSwitch = original + handler = self._handlePostConfigProfileSwitch + config.post_configProfileSwitch.register(handler) + config.post_configProfileSwitch.moveToEnd(handler, last=False) + synthDriverHandler.handlePostConfigProfileSwitch = handler # ty: ignore[invalid-assignment] + + def _restorePostConfigProfileSwitch(self): + """Reverses L{_takeOverPostConfigProfileSwitch}. + The module attribute is only restored when it still holds our handler. + """ + original = self._nvdaHandlePostConfigProfileSwitch + if original is None: + return + self._nvdaHandlePostConfigProfileSwitch = None + handler = self._handlePostConfigProfileSwitch + config.post_configProfileSwitch.unregister(handler) + config.post_configProfileSwitch.register(original) + config.post_configProfileSwitch.moveToEnd(original, last=False) + if synthDriverHandler.handlePostConfigProfileSwitch != handler: + return + synthDriverHandler.handlePostConfigProfileSwitch = original # ty: ignore[invalid-assignment] + + def _handlePostConfigProfileSwitch(self, resetSpeechIfNeeded: bool = True): + """Skips NVDA's synthesizer reload while remote speech is active without being configured.""" + if self.isRemoteSynthActive and not self.isRemoteSynthConfigured: + return + assert self._nvdaHandlePostConfigProfileSwitch is not None + self._nvdaHandlePostConfigProfileSwitch(resetSpeechIfNeeded) + def _handleRemoteDisconnect(self, synth: remoteSynthDriver): log.error(f"Handling remote disconnect for {synth!r}") queueHandler.queueFunction(queueHandler.eventQueue, self._fallback) @@ -112,6 +157,7 @@ def rescan(self, force: bool = False): self._queueBgScan(force) def terminate(self): + self._restorePostConfigProfileSwitch() remoteSynthDriver.synthRemoteDisconnected.unregister(self._handleRemoteDisconnect) self._stopBgScan() self._executor.shutdown(wait=False) diff --git a/buildVars.py b/buildVars.py index 927d2e7..e01c732 100644 --- a/buildVars.py +++ b/buildVars.py @@ -30,7 +30,7 @@ + "Citrix Workspace, Parallels RAS and VMware Horizon", ), # version - addon_version="2.0.0", + addon_version="2.0.1", # Brief changelog for this version # Translators: what's new content for the add-on version to be shown in the add-on store addon_changelog=_(""), diff --git a/readme.md b/readme.md index 4d7dee9..417e484 100644 --- a/readme.md +++ b/readme.md @@ -22,6 +22,11 @@ This enables a user experience where managing a remote system feels as seamless ## Changelog +### Version 2.0.1 + +* When remote speech is switched on automatically, it now keeps speaking after a configuration profile switch. Previously, NVDA fell back to the synthesizer you have configured as soon as a profile was activated, for example when you moved to an application with its own profile. +* Renamed the option "Automatically recover remote speech after connection loss" to "Automatically switch to remote speech when available", which better describes what it does. + ### Version 2.0 * Speech and braille coming from the remote system are now presented sooner, which makes working in a remote session feel more responsive. @@ -109,10 +114,11 @@ Choose between: To ensure a smooth start with the add-on, all options are enabled by default. However, you are encouraged to disable server or client mode as appropriate. -### Automatically Recover Remote Speech after Connection Loss +### Automatically Switch to Remote Speech when Available This option is only available in server mode. -It ensures that the connection will automatically be re-established when the Remote Speech synthesizer is active and the connection is lost, similar to braille display auto-detection. +It ensures that Remote Speech is activated as soon as a remote desktop client offers it, similar to braille display auto-detection, and that the connection is automatically re-established when it is lost. +While Remote Speech is active this way, your configured synthesizer is left untouched, and switching configuration profiles no longer falls back to it. This option is enabled by default. It is strongly encouraged to leave this option enabled if the Remote Desktop server has no audio output.