forked from nvaccess/nvda
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlocalMachine.py
More file actions
261 lines (206 loc) · 8.62 KB
/
Copy pathlocalMachine.py
File metadata and controls
261 lines (206 loc) · 8.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2015-2025 NV Access Limited, Christopher Toth, Tyler Spivey, Babbage B.V., David Sexton and others.
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
"""Local machine interface for NVDA Remote.
This module provides functionality for controlling the local NVDA instance
in response to commands received from remote connections.
The main class :class:`LocalMachine` implements all local control operations
that can be triggered by remote NVDA instances. It includes safety features like
muting and uses wxPython's CallAfter for thread synchronization.
.. note::
This module is part of the NVDA Remote protocol implementation and should
not be used directly outside of the remote connection infrastructure.
"""
import ctypes
import logging
import os
from typing import Any, Dict, List, Optional
import api
import braille
import inputCore
import nvwave
import speech
import tones
import wx
from speech.priorities import Spri
from speech.types import SpeechSequence
from systemUtils import hasUiAccess
import ui
from . import cues, input
logger = logging.getLogger("local_machine")
def setSpeechCancelledToFalse() -> None:
"""Reset the speech cancellation flag to allow new speech.
:note: Updates NVDA's internal speech state to ensure future speech will not be cancelled.
Required when receiving remote speech commands.
:warning: This is a temporary workaround that modifies internal NVDA state.
May break in future NVDA versions if the speech subsystem changes.
:seealso: :meth:`LocalMachine.speak`
"""
# workaround as beenCanceled is readonly as of NVDA#12395
speech.speech._speechState.beenCanceled = False
class LocalMachine:
"""Controls the local NVDA instance based on remote commands.
This class implements the local side of remote control functionality,
serving as the bridge between network commands and local NVDA operations.
It ensures thread-safe execution and proper state management.
:note: This class is instantiated by the remote session manager and should not
be created directly. All methods are called in response to remote messages.
:seealso:
- :class:`session.FollowerSession` - Manages remote connections
- :mod:`transport` - Network transport layer
"""
def __init__(self) -> None:
"""Initialize the local machine controller.
:note: The local machine starts unmuted with local braille enabled.
"""
self.isMuted: bool = False
"""When True, most remote commands will be ignored"""
self.receivingBraille: bool = False
"""When True, braille output comes from remote"""
self._cachedSizes: Optional[List[int]] = None
"""Cached braille display sizes from remote machines"""
braille.decide_enabled.register(self.handleDecideEnabled)
def terminate(self) -> None:
"""Clean up resources when the local machine controller is terminated.
:note: Unregisters the braille display handler to prevent memory leaks and
ensure proper cleanup when the remote connection ends.
"""
braille.decide_enabled.unregister(self.handleDecideEnabled)
def playWave(self, fileName: str) -> None:
"""Play a wave file on the local machine.
:param fileName: Path to the wave file to play
:note: Sound playback is ignored if the local machine is muted.
The file must exist on the local system.
"""
if self.isMuted:
return
if os.path.exists(fileName):
nvwave.playWaveFile(fileName=fileName, asynchronous=True)
def beep(self, hz: float, length: int, left: int = 50, right: int = 50) -> None:
"""Play a beep sound on the local machine.
:param hz: Frequency of the beep in Hertz
:param length: Duration of the beep in milliseconds
:param left: Left channel volume (0-100)
:param right: Right channel volume (0-100)
:note: Beeps are ignored if the local machine is muted.
"""
if self.isMuted:
return
tones.beep(hz, length, left, right)
def cancelSpeech(self) -> None:
"""Cancel any ongoing speech on the local machine.
:note: Speech cancellation is ignored if the local machine is muted.
Uses wx.CallAfter to ensure thread-safe execution.
"""
if self.isMuted:
return
wx.CallAfter(speech._manager.cancel)
def pauseSpeech(self, switch: bool) -> None:
"""Pause or resume speech on the local machine.
:param switch: True to pause speech, False to resume
:note: Speech control is ignored if the local machine is muted.
Uses wx.CallAfter to ensure thread-safe execution.
"""
if self.isMuted:
return
wx.CallAfter(speech.pauseSpeech, switch)
def speak(
self,
sequence: SpeechSequence,
priority: Spri = Spri.NORMAL,
) -> None:
"""Process a speech sequence from a remote machine.
Safely queues speech from remote NVDA instances into the local speech
subsystem, handling priority and ensuring proper cancellation state.
:param sequence: List of speech sequences (text and commands) to speak
:param priority: Speech priority level
:note: Speech is always queued asynchronously via wx.CallAfter to ensure
thread safety, as this may be called from network threads.
"""
if self.isMuted:
return
setSpeechCancelledToFalse()
wx.CallAfter(speech._manager.speak, sequence, priority)
def display(self, cells: List[int]) -> None:
"""Update the local braille display with cells from remote.
Safely writes braille cells from a remote machine to the local braille
display, handling display size differences and padding.
:param cells: List of braille cells as integers (0-255)
:note: Only processes cells when:
- receivingBraille is True (display sharing is enabled)
- Local display is connected (displaySize > 0)
- Remote cells fit on local display
Cells are padded with zeros if remote data is shorter than local display.
Uses thread-safe _writeCells method for compatibility with all displays.
"""
if (
self.receivingBraille
and braille.handler.displaySize > 0
and len(cells) <= braille.handler.displaySize
):
cells = cells + [0] * (braille.handler.displaySize - len(cells))
wx.CallAfter(braille.handler._writeCells, cells)
def brailleInput(self, **kwargs: Dict[str, Any]) -> None:
"""Process braille input gestures from a remote machine.
Executes braille input commands locally using NVDA's input gesture system.
Handles both display routing and braille keyboard input.
:param kwargs: Gesture parameters passed to BrailleInputGesture
:note: Silently ignores gestures that have no associated action.
"""
try:
inputCore.manager.executeGesture(input.BrailleInputGesture(**kwargs))
except inputCore.NoInputGestureAction:
pass
def setBrailleDisplaySize(self, sizes: List[int]) -> None:
"""Cache remote braille display sizes for size negotiation.
:param sizes: List of display sizes (cells) from remote machines
"""
self._cachedSizes = sizes
def handleFilterDisplaySize(self, value: int) -> int:
"""Filter the local display size based on remote display sizes.
Determines the optimal display size when sharing braille output by
finding the smallest positive size among local and remote displays.
:param value: Local display size in cells
:return: The negotiated display size to use
"""
if not self._cachedSizes:
return value
sizes = self._cachedSizes + [value]
try:
return min(i for i in sizes if i > 0)
except ValueError:
return value
def handleDecideEnabled(self) -> bool:
"""Determine if the local braille display should be enabled.
:return: False if receiving remote braille, True otherwise
"""
return not self.receivingBraille
def sendKey(
self,
vk_code: Optional[int] = None,
extended: Optional[bool] = None,
pressed: Optional[bool] = None,
) -> None:
"""Simulate a keyboard event on the local machine.
:param vk_code: Virtual key code to simulate
:param extended: Whether this is an extended key
:param pressed: True for key press, False for key release
"""
wx.CallAfter(input.sendKey, vk_code, None, extended, pressed)
def setClipboardText(self, text: str) -> None:
"""Set the local clipboard text from a remote machine.
:param text: Text to copy to the clipboard
"""
cues.clipboardReceived()
api.copyToClip(text=text)
def sendSAS(self) -> None:
"""Simulate a secure attention sequence (e.g. CTRL+ALT+DEL).
:note: SendSAS requires UI Access. If this fails, a warning is displayed.
"""
if hasUiAccess():
ctypes.windll.sas.SendSAS(0)
else:
# Translators: Message displayed when a remote machine tries to send a SAS but UI Access is disabled.
ui.message(_("Unable to trigger Alt Control Delete from remote"))
logger.warning("UI Access is disabled on this machine so cannot trigger CTRL+ALT+DEL")