Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions addon/globalPlugins/rdAccess/settingsPanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
Expand Down
46 changes: 46 additions & 0 deletions addon/globalPlugins/rdAccess/synthDetect.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import threading
import typing
from collections.abc import Callable
from concurrent.futures import Future, ThreadPoolExecutor

import addonHandler
Expand All @@ -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

Expand All @@ -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
Comment thread
Copilot marked this conversation as resolved.
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)
Expand Down Expand Up @@ -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)
2 changes: 1 addition & 1 deletion buildVars.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=_(""),
Expand Down
10 changes: 8 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down