Skip to content

Commit 460a4ef

Browse files
authored
Merge e54f8ac into a7fa0d6
2 parents a7fa0d6 + e54f8ac commit 460a4ef

37 files changed

Lines changed: 4721 additions & 2 deletions

projectDocs/dev/developerGuide/developerGuide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1384,6 +1384,7 @@ For examples of how to define and use new extension points, please see the code
13841384
|`Action` |`pre_speechCanceled` |Triggered before speech is canceled.|
13851385
|`Action` |`pre_speech` |Triggered before NVDA handles prepared speech.|
13861386
|`Action` |`post_speechPaused` |Triggered when speech is paused or resumed.|
1387+
|`Action` |`pre_speechQueued` |Triggered after speech is processed and normalized and directly before it is enqueued.|
13871388
|`Filter` |`filter_speechSequence` |Allows components or add-ons to filter speech sequence before it passes to the synth driver.|
13881389

13891390
### synthDriverHandler {#synthDriverHandlerExtPts}

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ SCons==4.8.1
33

44
# NVDA's runtime dependencies
55
comtypes==1.4.6
6+
cryptography==44.0.0
67
pyserial==3.5
78
wxPython==4.2.2
89
configobj @ git+https://github.com/DiffSK/configobj@8be54629ee7c26acb5c865b74c76284e80f3aa31#egg=configobj

source/core.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,12 @@ def main():
897897

898898
log.debug("Initializing global plugin handler")
899899
globalPluginHandler.initialize()
900+
901+
log.debug("Initializing remote client")
902+
import remoteClient
903+
904+
remoteClient.initialize()
905+
900906
if globalVars.appArgs.install or globalVars.appArgs.installSilent:
901907
import gui.installerGui
902908

@@ -1049,6 +1055,7 @@ def _doPostNvdaStartupAction():
10491055
" This likely indicates NVDA is exiting due to WM_QUIT.",
10501056
)
10511057
queueHandler.pumpAll()
1058+
_terminate(remoteClient)
10521059
_terminate(gui)
10531060
config.saveOnExit()
10541061

source/globalCommands.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@
119119
#: Script category for audio streaming commands.
120120
# Translators: The name of a category of NVDA commands.
121121
SCRCAT_AUDIO = _("Audio")
122+
#: Script category for Remote commands.
123+
# Translators: The name of a category of NVDA commands.
124+
SCRCAT_REMOTE = _("Remote")
122125

123126
# Translators: Reported when there are no settings to configure in synth settings ring
124127
# (example: when there is no setting for language).
@@ -4888,6 +4891,69 @@ def script_toggleApplicationsVolumeAdjuster(self, gesture: "inputCore.InputGestu
48884891
def script_toggleApplicationsMute(self, gesture: "inputCore.InputGesture") -> None:
48894892
appsVolume._toggleAppsVolumeMute()
48904893

4894+
@script(
4895+
# Translators: Describes a command.
4896+
description=_("""Mute or unmute the speech coming from the remote computer"""),
4897+
category=SCRCAT_REMOTE,
4898+
)
4899+
def script_toggle_remote_mute(self, gesture):
4900+
globalVars.remoteClient.toggleMute()
4901+
4902+
@script(
4903+
gesture="kb:control+shift+NVDA+c",
4904+
category=SCRCAT_REMOTE,
4905+
# Translators: Documentation string for the script that sends the contents of the clipboard to the remote machine.
4906+
description=_("Sends the contents of the clipboard to the remote machine"),
4907+
)
4908+
def script_push_clipboard(self, gesture):
4909+
globalVars.remoteClient.pushClipboard()
4910+
4911+
@script(
4912+
# Translators: Documentation string for the script that copies a link to the remote session to the clipboard.
4913+
description=_("""Copies a link to the remote session to the clipboard"""),
4914+
category=SCRCAT_REMOTE,
4915+
)
4916+
def script_copy_link(self, gesture):
4917+
globalVars.remoteClient.copyLink()
4918+
# Translators: A message indicating that a link has been copied to the clipboard.
4919+
ui.message(_("Copied link"))
4920+
4921+
@script(
4922+
gesture="kb:alt+NVDA+pageDown",
4923+
category=SCRCAT_REMOTE,
4924+
# Translators: Documentation string for the script that disconnects a remote session.
4925+
description=_("""Disconnect a remote session"""),
4926+
)
4927+
@gui.blockAction.when(gui.blockAction.Context.SECURE_MODE)
4928+
def script_disconnectFromRemote(self, gesture):
4929+
if not globalVars.remoteClient.isConnected:
4930+
# Translators: A message indicating that the remote client is not connected.
4931+
ui.message(_("Not connected."))
4932+
return
4933+
globalVars.remoteClient.disconnect()
4934+
4935+
@script(
4936+
gesture="kb:alt+NVDA+pageUp",
4937+
# Translators: Documentation string for the script that invokes the remote session.
4938+
description=_("""Connect to a remote computer"""),
4939+
category=SCRCAT_REMOTE,
4940+
)
4941+
@gui.blockAction.when(gui.blockAction.Context.MODAL_DIALOG_OPEN)
4942+
@gui.blockAction.when(gui.blockAction.Context.SECURE_MODE)
4943+
def script_connectToRemote(self, gesture):
4944+
if globalVars.remoteClient.isConnected() or globalVars.remoteClient.connecting:
4945+
return
4946+
globalVars.remoteClient.doConnect()
4947+
4948+
@script(
4949+
# Translators: Documentation string for the script that toggles the control between guest and host machine.
4950+
description=_("Toggles the control between guest and host machine"),
4951+
category=SCRCAT_REMOTE,
4952+
gesture="kb:f11",
4953+
)
4954+
def script_sendKeys(self, gesture):
4955+
globalVars.remoteClient.toggleRemoteKeyControl(gesture)
4956+
48914957

48924958
#: The single global commands instance.
48934959
#: @type: L{GlobalCommands}

source/gui/settingsDialogs.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import gui.contextHelp
4747
import globalVars
4848
from logHandler import log
49+
from remoteClient import configuration
4950
import nvwave
5051
import audio
5152
import audioDucking
@@ -3345,6 +3346,159 @@ def onSave(self):
33453346
config.conf["addonStore"]["automaticUpdates"] = [x.value for x in AddonsAutomaticUpdate][index]
33463347

33473348

3349+
class RemoteSettingsPanel(SettingsPanel):
3350+
# Translators: This is the label for the remote settings category in NVDA Settings screen.
3351+
title = _("Remote")
3352+
autoconnect: wx.CheckBox
3353+
client_or_server: wx.RadioBox
3354+
connection_type: wx.RadioBox
3355+
host: wx.TextCtrl
3356+
port: wx.SpinCtrl
3357+
key: wx.TextCtrl
3358+
play_sounds: wx.CheckBox
3359+
delete_fingerprints: wx.Button
3360+
3361+
def makeSettings(self, settingsSizer):
3362+
self.config = configuration.get_config()
3363+
sHelper = gui.guiHelper.BoxSizerHelper(self, sizer=settingsSizer)
3364+
self.autoconnect = wx.CheckBox(
3365+
parent=self,
3366+
id=wx.ID_ANY,
3367+
# Translators: A checkbox in add-on options dialog to set whether NVDA should automatically connect to a control server on startup.
3368+
label=_("Auto-connect to control server on startup"),
3369+
)
3370+
self.autoconnect.Bind(wx.EVT_CHECKBOX, self.on_autoconnect)
3371+
sHelper.addItem(self.autoconnect)
3372+
# Translators: Whether or not to use a relay server when autoconnecting
3373+
self.client_or_server = wx.RadioBox(
3374+
self,
3375+
wx.ID_ANY,
3376+
choices=(
3377+
# Translators: Use a remote control server
3378+
_("Use Remote Control Server"),
3379+
# Translators: Host a control server
3380+
_("Host Control Server"),
3381+
),
3382+
style=wx.RA_VERTICAL,
3383+
)
3384+
self.client_or_server.Bind(wx.EVT_RADIOBOX, self.on_client_or_server)
3385+
self.client_or_server.SetSelection(0)
3386+
self.client_or_server.Enable(False)
3387+
sHelper.addItem(self.client_or_server)
3388+
choices = [
3389+
# Translators: Radio button to allow this machine to be controlled
3390+
_("Allow this machine to be controlled"),
3391+
# Translators: Radio button to allow this machine to control another machine
3392+
_("Control another machine"),
3393+
]
3394+
self.connection_type = wx.RadioBox(self, wx.ID_ANY, choices=choices, style=wx.RA_VERTICAL)
3395+
self.connection_type.SetSelection(0)
3396+
self.connection_type.Enable(False)
3397+
sHelper.addItem(self.connection_type)
3398+
sHelper.addItem(wx.StaticText(self, wx.ID_ANY, label=_("&Host:")))
3399+
self.host = wx.TextCtrl(self, wx.ID_ANY)
3400+
self.host.Enable(False)
3401+
sHelper.addItem(self.host)
3402+
sHelper.addItem(wx.StaticText(self, wx.ID_ANY, label=_("&Port:")))
3403+
self.port = wx.SpinCtrl(self, wx.ID_ANY, min=1, max=65535)
3404+
self.port.Enable(False)
3405+
sHelper.addItem(self.port)
3406+
sHelper.addItem(wx.StaticText(self, wx.ID_ANY, label=_("&Key:")))
3407+
self.key = wx.TextCtrl(self, wx.ID_ANY)
3408+
self.key.Enable(False)
3409+
sHelper.addItem(self.key)
3410+
# Translators: A checkbox in add-on options dialog to set whether sounds play instead of beeps.
3411+
self.play_sounds = wx.CheckBox(self, wx.ID_ANY, label=_("Play sounds instead of beeps"))
3412+
sHelper.addItem(self.play_sounds)
3413+
# Translators: A button in add-on options dialog to delete all fingerprints of unauthorized certificates.
3414+
self.delete_fingerprints = wx.Button(self, wx.ID_ANY, label=_("Delete all trusted fingerprints"))
3415+
self.delete_fingerprints.Bind(wx.EVT_BUTTON, self.on_delete_fingerprints)
3416+
sHelper.addItem(self.delete_fingerprints)
3417+
self.set_from_config()
3418+
3419+
def on_autoconnect(self, evt: wx.CommandEvent) -> None:
3420+
self.set_controls()
3421+
3422+
def set_controls(self) -> None:
3423+
state = bool(self.autoconnect.GetValue())
3424+
self.client_or_server.Enable(state)
3425+
self.connection_type.Enable(state)
3426+
self.key.Enable(state)
3427+
self.host.Enable(not bool(self.client_or_server.GetSelection()) and state)
3428+
self.port.Enable(bool(self.client_or_server.GetSelection()) and state)
3429+
3430+
def on_client_or_server(self, evt: wx.CommandEvent) -> None:
3431+
evt.Skip()
3432+
self.set_controls()
3433+
3434+
def set_from_config(self) -> None:
3435+
cs = self.config["controlserver"]
3436+
self_hosted = cs["self_hosted"]
3437+
connection_type = cs["connection_type"]
3438+
self.autoconnect.SetValue(cs["autoconnect"])
3439+
self.client_or_server.SetSelection(int(self_hosted))
3440+
self.connection_type.SetSelection(connection_type)
3441+
self.host.SetValue(cs["host"])
3442+
self.port.SetValue(str(cs["port"]))
3443+
self.key.SetValue(cs["key"])
3444+
self.set_controls()
3445+
self.play_sounds.SetValue(self.config["ui"]["play_sounds"])
3446+
3447+
def on_delete_fingerprints(self, evt: wx.CommandEvent) -> None:
3448+
if (
3449+
gui.messageBox(
3450+
_(
3451+
# Translators: This message is presented when the user tries to delete all stored trusted fingerprints.
3452+
"When connecting to an unauthorized server, you will again be prompted to accepts its certificate.",
3453+
),
3454+
# Translators: This is the title of the dialog presented when the user tries to delete all stored trusted fingerprints.
3455+
_("Are you sure you want to delete all stored trusted fingerprints?"),
3456+
wx.YES | wx.NO | wx.NO_DEFAULT | wx.ICON_WARNING,
3457+
)
3458+
== wx.YES
3459+
):
3460+
self.config["trusted_certs"].clear()
3461+
evt.Skip()
3462+
3463+
def isValid(self) -> bool:
3464+
if self.autoconnect.GetValue():
3465+
if not self.client_or_server.GetSelection() and (
3466+
not self.host.GetValue() or not self.key.GetValue()
3467+
):
3468+
gui.messageBox(
3469+
# Translators: This message is presented when the user tries to save the settings with the host or key field empty.
3470+
_("Both host and key must be set in the Remote section."),
3471+
# Translators: This is the title of the dialog presented when the user tries to save the settings with the host or key field empty.
3472+
_("Remote Error"),
3473+
wx.OK | wx.ICON_ERROR,
3474+
)
3475+
return False
3476+
elif self.client_or_server.GetSelection() and not self.port.GetValue() or not self.key.GetValue():
3477+
gui.messageBox(
3478+
# Translators: This message is presented when the user tries to save the settings with the port or key field empty.
3479+
_("Both port and key must be set in the Remote section."),
3480+
# Translators: This is the title of the dialog presented when the user tries to save the settings with the port or key field empty.
3481+
_("Remote Error"),
3482+
wx.OK | wx.ICON_ERROR,
3483+
)
3484+
return False
3485+
return True
3486+
3487+
def onSave(self):
3488+
cs = self.config["controlserver"]
3489+
cs["autoconnect"] = self.autoconnect.GetValue()
3490+
self_hosted = bool(self.client_or_server.GetSelection())
3491+
connection_type = self.connection_type.GetSelection()
3492+
cs["self_hosted"] = self_hosted
3493+
cs["connection_type"] = connection_type
3494+
if not self_hosted:
3495+
cs["host"] = self.host.GetValue()
3496+
else:
3497+
cs["port"] = int(self.port.GetValue())
3498+
cs["key"] = self.key.GetValue()
3499+
self.config["ui"]["play_sounds"] = self.play_sounds.GetValue()
3500+
3501+
33483502
class TouchInteractionPanel(SettingsPanel):
33493503
# Translators: This is the label for the touch interaction settings panel.
33503504
title = _("Touch Interaction")
@@ -5212,6 +5366,8 @@ class NVDASettingsDialog(MultiCategorySettingsDialog):
52125366
DocumentNavigationPanel,
52135367
AddonStorePanel,
52145368
]
5369+
if not globalVars.appArgs.secure:
5370+
categoryClasses.append(RemoteSettingsPanel)
52155371
if touchHandler.touchSupported():
52165372
categoryClasses.append(TouchInteractionPanel)
52175373
if winVersion.isUwpOcrAvailable():

source/remoteClient/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from .client import RemoteClient
2+
3+
4+
def initialize():
5+
"""Initialise the remote client."""
6+
import globalVars
7+
import globalCommands
8+
9+
globalVars.remoteClient = RemoteClient()
10+
globalVars.remoteClient.registerLocalScript(globalCommands.commands.script_sendKeys)
11+
12+
13+
def terminate():
14+
"""Terminate the remote client."""
15+
import globalVars
16+
17+
globalVars.remoteClient.terminate()
18+
globalVars.remoteClient = None
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import collections.abc
2+
import threading
3+
import time
4+
from typing import Tuple, Union
5+
6+
import tones
7+
8+
local_beep = tones.beep
9+
10+
BeepElement = Union[int, Tuple[int, int]] # Either delay_ms or (frequency_hz, duration_ms)
11+
BeepSequence = collections.abc.Iterable[BeepElement]
12+
13+
14+
def beepSequence(*sequence: BeepElement) -> None:
15+
"""Play a simple synchronous monophonic beep sequence
16+
A beep sequence is an iterable containing one of two kinds of elements.
17+
An element consisting of a tuple of two items is interpreted as a frequency and duration. Note, this function plays beeps synchronously, unlike tones.beep
18+
A single integer is assumed to be a delay in ms.
19+
"""
20+
for element in sequence:
21+
if not isinstance(element, collections.abc.Sequence):
22+
time.sleep(float(element) / 1000)
23+
else:
24+
tone, duration = element
25+
time.sleep(float(duration) / 1000)
26+
local_beep(tone, duration)
27+
28+
29+
def beepSequenceAsync(*sequence: BeepElement) -> threading.Thread:
30+
"""Play an asynchronous beep sequence.
31+
This is the same as `beepSequence`, except it runs in a thread."""
32+
thread = threading.Thread(target=beepSequence, args=sequence)
33+
thread.daemon = True
34+
thread.start()
35+
return thread

0 commit comments

Comments
 (0)