Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/client/ui_client_base.py : 68%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of Xpra.
2# Copyright (C) 2011 Serviware (Arthur Huillet, <ahuillet@serviware.com>)
3# Copyright (C) 2010-2020 Antoine Martin <antoine@xpra.org>
4# Copyright (C) 2008, 2010 Nathaniel Smith <njs@pobox.com>
5# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
6# later version. See the file COPYING for details.
8import os
9import sys
11from xpra.client.client_base import XpraClientBase
12from xpra.client.keyboard_helper import KeyboardHelper
13from xpra.platform import set_name
14from xpra.platform.gui import ready as gui_ready, get_wm_name, get_session_type, ClientExtras
15from xpra.version_util import full_version_str
16from xpra.net import compression, packet_encoding
17from xpra.net.net_util import get_info as get_net_info
18from xpra.child_reaper import reaper_cleanup
19from xpra.platform.info import get_sys_info
20from xpra.os_util import (
21 platform_name, bytestostr, strtobytes,
22 BITS, POSIX, is_Wayland,
23 get_frame_info, get_info_env, get_sysconfig_info,
24 )
25from xpra.util import (
26 std, envbool, envint, typedict, updict, repr_ellipsized, ellipsizer, log_screen_sizes, engs, csv,
27 merge_dicts,
28 XPRA_AUDIO_NOTIFICATION_ID, XPRA_DISCONNECT_NOTIFICATION_ID,
29 )
30from xpra.exit_codes import EXIT_CONNECTION_FAILED, EXIT_OK, EXIT_CONNECTION_LOST
31from xpra.version_util import get_version_info_full, get_platform_info
32from xpra.client import mixin_features
33from xpra.log import Logger
36CLIENT_BASES = [XpraClientBase]
37if mixin_features.display:
38 from xpra.client.mixins.display import DisplayClient
39 CLIENT_BASES.append(DisplayClient)
40if mixin_features.windows:
41 from xpra.client.mixins.window_manager import WindowClient
42 CLIENT_BASES.append(WindowClient)
43if mixin_features.webcam:
44 from xpra.client.mixins.webcam import WebcamForwarder
45 CLIENT_BASES.append(WebcamForwarder)
46if mixin_features.audio:
47 from xpra.client.mixins.audio import AudioClient
48 CLIENT_BASES.append(AudioClient)
49if mixin_features.clipboard:
50 from xpra.client.mixins.clipboard import ClipboardClient
51 CLIENT_BASES.append(ClipboardClient)
52if mixin_features.notifications:
53 from xpra.client.mixins.notifications import NotificationClient
54 CLIENT_BASES.append(NotificationClient)
55if mixin_features.dbus:
56 from xpra.client.mixins.rpc import RPCClient
57 CLIENT_BASES.append(RPCClient)
58if mixin_features.mmap:
59 from xpra.client.mixins.mmap import MmapClient
60 CLIENT_BASES.append(MmapClient)
61if mixin_features.logging:
62 from xpra.client.mixins.remote_logging import RemoteLogging
63 CLIENT_BASES.append(RemoteLogging)
64if mixin_features.network_state:
65 from xpra.client.mixins.network_state import NetworkState
66 CLIENT_BASES.append(NetworkState)
67if mixin_features.network_listener:
68 from xpra.client.mixins.network_listener import NetworkListener
69 CLIENT_BASES.append(NetworkListener)
70if mixin_features.encoding:
71 from xpra.client.mixins.encodings import Encodings
72 CLIENT_BASES.append(Encodings)
73if mixin_features.tray:
74 from xpra.client.mixins.tray import TrayClient
75 CLIENT_BASES.append(TrayClient)
77CLIENT_BASES = tuple(CLIENT_BASES)
78ClientBaseClass = type('ClientBaseClass', CLIENT_BASES, {})
80log = Logger("client")
81keylog = Logger("client", "keyboard")
82log("UIXpraClient%s: %s", ClientBaseClass, CLIENT_BASES)
84NOTIFICATION_EXIT_DELAY = envint("XPRA_NOTIFICATION_EXIT_DELAY", 2)
85MOUSE_DELAY_AUTO = envbool("XPRA_MOUSE_DELAY_AUTO", True)
86SYSCONFIG = envbool("XPRA_SYSCONFIG", False)
89"""
90Utility superclass for client classes which have a UI.
91See gtk_client_base and its subclasses.
92"""
93class UIXpraClient(ClientBaseClass):
94 #NOTE: these signals aren't registered here because this class
95 #does not extend GObject,
96 #the gtk client subclasses will take care of it.
97 #these are all "no-arg" signals
98 __signals__ = ["first-ui-received",]
99 for c in CLIENT_BASES:
100 if c!=XpraClientBase:
101 __signals__ += c.__signals__
103 def __init__(self):
104 log.info("Xpra %s client version %s %i-bit", self.client_toolkit(), full_version_str(), BITS)
105 #mmap_enabled belongs in the MmapClient mixin,
106 #but it is used outside it, so make sure we define it:
107 self.mmap_enabled = False
108 #same for tray:
109 self.tray = None
110 for c in CLIENT_BASES:
111 log("calling %s.__init__()", c)
112 c.__init__(self)
113 try:
114 pinfo = get_platform_info()
115 osinfo = "%s" % platform_name(sys.platform, pinfo.get("linux_distribution") or pinfo.get("sysrelease", ""))
116 log.info(" running on %s", osinfo)
117 except Exception:
118 log("platform name error:", exc_info=True)
119 wm = get_wm_name() #pylint: disable=assignment-from-none
120 if wm:
121 log.info(" window manager is '%s'", wm)
123 self._ui_events = 0
124 self.title = ""
125 self.session_name = ""
127 self.server_platform = ""
128 self.server_session_name = None
130 #features:
131 self.opengl_enabled = False
132 self.opengl_props = {}
133 self.readonly = False
134 self.xsettings_enabled = False
135 self.server_start_new_commands = False
136 self.server_xdg_menu = None
137 self.start_new_commands = []
138 self.start_child_new_commands = []
139 self.headerbar = None
141 #in WindowClient - should it be?
142 #self.server_is_desktop = False
143 self.server_sharing = False
144 self.server_sharing_toggle = False
145 self.server_lock = False
146 self.server_lock_toggle = False
147 self.server_keyboard = True
148 self.server_pointer = True
150 self.client_supports_opengl = False
151 self.client_supports_sharing = False
152 self.client_lock = False
154 #helpers and associated flags:
155 self.client_extras = None
156 self.keyboard_helper_class = KeyboardHelper
157 self.keyboard_helper = None
158 self.keyboard_grabbed = False
159 self.keyboard_sync = False
160 self.pointer_grabbed = False
161 self.kh_warning = False
162 self.menu_helper = None
164 #state:
165 self._on_handshake = []
166 self._on_server_setting_changed = {}
169 def init(self, opts):
170 """ initialize variables from configuration """
171 for c in CLIENT_BASES:
172 log("init: %s", c)
173 c.init(self, opts)
175 self.title = opts.title
176 self.session_name = bytestostr(opts.session_name)
177 self.xsettings_enabled = opts.xsettings
178 self.readonly = opts.readonly
179 self.client_supports_sharing = opts.sharing is True
180 self.client_lock = opts.lock is True
181 self.headerbar = opts.headerbar
184 def init_ui(self, opts):
185 """ initialize user interface """
186 if not self.readonly:
187 def noauto(v):
188 if not v:
189 return None
190 if str(v).lower()=="auto":
191 return None
192 return v
193 overrides = [noauto(getattr(opts, "keyboard_%s" % x)) for x in (
194 "layout", "layouts", "variant", "variants", "options",
195 )]
196 def send_keyboard(*parts):
197 self.after_handshake(self.send, *parts)
198 try:
199 self.keyboard_helper = self.keyboard_helper_class(send_keyboard, opts.keyboard_sync,
200 opts.shortcut_modifiers,
201 opts.key_shortcut,
202 opts.keyboard_raw, *overrides)
203 except ImportError as e:
204 keylog("error instantiating %s", self.keyboard_helper_class, exc_info=True)
205 keylog.warn("Warning: no keyboard support, %s", e)
207 if mixin_features.windows:
208 self.init_opengl(opts.opengl)
210 if ClientExtras is not None:
211 self.client_extras = ClientExtras(self, opts) #pylint: disable=not-callable
213 if opts.start or opts.start_child:
214 from xpra.scripts.main import strip_defaults_start_child
215 from xpra.scripts.config import make_defaults_struct
216 defaults = make_defaults_struct()
217 self.start_new_commands = strip_defaults_start_child(opts.start, defaults.start) #pylint: disable=no-member
218 self.start_child_new_commands = strip_defaults_start_child(opts.start_child, defaults.start_child) #pylint: disable=no-member
220 if MOUSE_DELAY_AUTO:
221 try:
222 from xpra.platform.gui import get_vrefresh
223 v = get_vrefresh()
224 if v<=0:
225 #some platforms don't detect the vrefresh correctly
226 #(ie: macos in virtualbox?), so use a sane default:
227 v = 60
228 self._mouse_position_delay = 1000//v//2
229 log("mouse delay: %s", self._mouse_position_delay)
230 except Exception:
231 log("failed to calculate automatic delay", exc_info=True)
234 def run(self):
235 if self.client_extras:
236 self.idle_add(self.client_extras.ready)
237 for c in CLIENT_BASES:
238 c.run(self)
241 def quit(self, _exit_code=0):
242 raise NotImplementedError()
244 def cleanup(self):
245 log("UIXpraClient.cleanup()")
246 for c in CLIENT_BASES:
247 c.cleanup(self)
248 for x in (self.keyboard_helper, self.tray, self.menu_helper, self.client_extras):
249 if x is None:
250 continue
251 log("UIXpraClient.cleanup() calling %s.cleanup()", type(x))
252 try:
253 x.cleanup()
254 except Exception:
255 log.error("error on %s cleanup", type(x), exc_info=True)
256 #the protocol has been closed, it is now safe to close all the windows:
257 #(cleaner and needed when we run embedded in the client launcher)
258 reaper_cleanup()
259 log("UIXpraClient.cleanup() done")
262 def signal_cleanup(self):
263 log("UIXpraClient.signal_cleanup()")
264 XpraClientBase.signal_cleanup(self)
265 reaper_cleanup()
266 log("UIXpraClient.signal_cleanup() done")
269 def get_info(self):
270 info = {
271 "pid" : os.getpid(),
272 "threads" : get_frame_info(),
273 "env" : get_info_env(),
274 "sys" : get_sys_info(),
275 "network" : get_net_info(),
276 }
277 if SYSCONFIG:
278 info["sysconfig"] = get_sysconfig_info()
279 for c in CLIENT_BASES:
280 try:
281 i = c.get_info(self)
282 info = merge_dicts(info, i)
283 except Exception:
284 log.error("Error collection information from %s", c, exc_info=True)
285 return info
288 def show_about(self, *_args):
289 log.warn("show_about() is not implemented in %s", self)
291 def show_session_info(self, *_args):
292 log.warn("show_session_info() is not implemented in %s", self)
294 def show_bug_report(self, *_args):
295 log.warn("show_bug_report() is not implemented in %s", self)
298 def init_opengl(self, _enable_opengl):
299 self.opengl_enabled = False
300 self.client_supports_opengl = False
301 self.opengl_props = {"info" : "not supported"}
304 def _ui_event(self):
305 if self._ui_events==0:
306 self.emit("first-ui-received")
307 self._ui_events += 1
310 def get_mouse_position(self):
311 raise NotImplementedError()
313 def get_current_modifiers(self):
314 raise NotImplementedError()
317 def send_start_new_commands(self):
318 log("send_start_new_commands() start_new_commands=%s, start_child_new_commands=%s",
319 self.start_new_commands, self.start_child_new_commands)
320 import shlex
321 for cmd in self.start_new_commands:
322 cmd_parts = shlex.split(cmd)
323 self.send_start_command(cmd_parts[0], cmd, True)
324 for cmd in self.start_child_new_commands:
325 cmd_parts = shlex.split(cmd)
326 self.send_start_command(cmd_parts[0], cmd, False)
328 def send_start_command(self, name, command, ignore, sharing=True):
329 log("send_start_command(%s, %s, %s, %s)", name, command, ignore, sharing)
330 assert name is not None and command is not None and ignore is not None
331 self.send("start-command", name, command, ignore, sharing)
333 def get_version_info(self) -> dict:
334 return get_version_info_full()
337 ######################################################################
338 # trigger notifications on disconnection,
339 # and wait before actually exiting so the notification has a chance of being seen
340 def server_disconnect_warning(self, reason, *info):
341 if self.exit_code is None:
342 body = "\n".join(info)
343 if self.connection_established:
344 title = "Xpra Session Disconnected: %s" % reason
345 self.exit_code = EXIT_CONNECTION_LOST
346 else:
347 title = "Connection Failed: %s" % reason
348 self.exit_code = EXIT_CONNECTION_FAILED
349 self.may_notify(XPRA_DISCONNECT_NOTIFICATION_ID,
350 title, body, icon_name="disconnected")
351 #show text notification then quit:
352 delay = NOTIFICATION_EXIT_DELAY*mixin_features.notifications
353 self.timeout_add(delay*1000, XpraClientBase.server_disconnect_warning, self, reason, *info)
354 self.cleanup()
356 def server_disconnect(self, reason, *info):
357 body = "\n".join(info)
358 self.may_notify(XPRA_DISCONNECT_NOTIFICATION_ID,
359 "Xpra Session Disconnected: %s" % reason, body, icon_name="disconnected")
360 self.exit_code = EXIT_OK
361 delay = NOTIFICATION_EXIT_DELAY*mixin_features.notifications
362 self.timeout_add(delay*1000, XpraClientBase.server_disconnect, self, reason, *info)
363 self.cleanup()
366 ######################################################################
367 # hello:
368 def make_hello(self):
369 caps = XpraClientBase.make_hello(self)
370 caps["session-type"] = get_session_type()
371 #don't try to find the server uuid if this platform cannot run servers..
372 #(doing so causes lockups on win32 and startup errors on osx)
373 if POSIX and not is_Wayland():
374 #we may be running inside another server!
375 try:
376 from xpra.server.server_uuid import get_uuid
377 caps["server_uuid"] = get_uuid() or ""
378 except ImportError:
379 pass
380 for x in (#generic feature flags:
381 "wants_events", "setting-change",
382 "xdg-menu-update",
383 ):
384 caps[x] = True
385 caps.update({
386 #generic server flags:
387 "share" : self.client_supports_sharing,
388 "lock" : self.client_lock,
389 })
390 caps.update({"mouse" : True})
391 caps.update(self.get_keyboard_caps())
392 for c in CLIENT_BASES:
393 caps.update(c.get_caps(self))
394 def u(prefix, c):
395 updict(caps, prefix, c, flatten_dicts=False)
396 u("control_commands", self.get_control_commands_caps())
397 u("platform", get_platform_info())
398 u("opengl", self.opengl_props)
399 return caps
403 ######################################################################
404 # connection setup:
405 def setup_connection(self, conn):
406 protocol = super().setup_connection(conn)
407 for c in CLIENT_BASES:
408 if c!=XpraClientBase:
409 c.setup_connection(self, conn)
410 return protocol
412 def server_connection_established(self, caps : typedict):
413 if not XpraClientBase.server_connection_established(self, caps):
414 return False
415 #process the rest from the UI thread:
416 self.idle_add(self.process_ui_capabilities, caps)
417 return True
420 def parse_server_capabilities(self, c : typedict) -> bool:
421 for cb in CLIENT_BASES:
422 if not cb.parse_server_capabilities(self, c):
423 log.info("failed to parse server capabilities in %s", cb)
424 return False
425 self.server_session_name = strtobytes(c.rawget("session_name", b"")).decode("utf-8")
426 set_name("Xpra", self.session_name or self.server_session_name or "Xpra")
427 self.server_platform = c.strget("platform")
428 self.server_sharing = c.boolget("sharing")
429 self.server_sharing_toggle = c.boolget("sharing-toggle")
430 self.server_lock = c.boolget("lock")
431 self.server_lock_toggle = c.boolget("lock-toggle")
432 self.server_keyboard = c.boolget("keyboard", True)
433 self.server_pointer = c.boolget("pointer", True)
434 self.server_start_new_commands = c.boolget("start-new-commands")
435 if self.server_start_new_commands:
436 self.server_xdg_menu = c.dictget("xdg-menu", None)
437 if self.start_new_commands or self.start_child_new_commands:
438 if self.server_start_new_commands:
439 self.after_handshake(self.send_start_new_commands)
440 else:
441 log.warn("Warning: cannot start new commands")
442 log.warn(" the feature is currently disabled on the server")
443 self.server_commands_info = c.boolget("server-commands-info")
444 self.server_commands_signals = c.strtupleget("server-commands-signals")
445 self.server_readonly = c.boolget("readonly")
446 if self.server_readonly and not self.readonly:
447 log.info("server is read only")
448 self.readonly = True
449 if not self.server_keyboard and self.keyboard_helper:
450 #swallow packets:
451 def nosend(*_args):
452 pass
453 self.keyboard_helper.send = nosend
455 i = platform_name(self._remote_platform,
456 c.strtupleget("platform.linux_distribution") or c.strget("platform.release", ""))
457 r = self._remote_version
458 if self._remote_revision:
459 r += "-r%s" % self._remote_revision
460 mode = c.strget("server.mode", "server")
461 bits = c.intget("python.bits", 32)
462 log.info("Xpra %s server version %s %i-bit", mode, std(r), bits)
463 if i:
464 log.info(" running on %s", std(i))
465 if c.boolget("desktop") or c.boolget("shadow"):
466 v = c.intpair("actual_desktop_size")
467 if v:
468 w, h = v
469 ss = c.tupleget("screen_sizes")
470 if ss:
471 log.info(" remote desktop size is %sx%s with %s screen%s:", w, h, len(ss), engs(ss))
472 log_screen_sizes(w, h, ss)
473 else:
474 log.info(" remote desktop size is %sx%s", w, h)
475 if c.boolget("proxy"):
476 proxy_hostname = c.strget("proxy.hostname")
477 proxy_platform = c.strget("proxy.platform")
478 proxy_release = c.strget("proxy.platform.release")
479 proxy_version = c.strget("proxy.version")
480 proxy_version = c.strget("proxy.build.version", proxy_version)
481 proxy_distro = c.strget("proxy.linux_distribution")
482 msg = "via: %s proxy version %s" % (
483 platform_name(proxy_platform, proxy_distro or proxy_release),
484 std(proxy_version or "unknown")
485 )
486 if proxy_hostname:
487 msg += " on '%s'" % std(proxy_hostname)
488 log.info(msg)
489 return True
491 def process_ui_capabilities(self, caps : typedict):
492 for c in CLIENT_BASES:
493 if c!=XpraClientBase:
494 c.process_ui_capabilities(self, caps)
495 #keyboard:
496 if self.keyboard_helper:
497 modifier_keycodes = caps.dictget("modifier_keycodes", {})
498 if modifier_keycodes:
499 self.keyboard_helper.set_modifier_mappings(modifier_keycodes)
500 self.key_repeat_delay, self.key_repeat_interval = caps.intpair("key_repeat", (-1,-1))
501 self.handshake_complete()
504 def _process_startup_complete(self, packet):
505 log("all the existing windows and system trays have been received")
506 XpraClientBase._process_startup_complete(self, packet)
507 gui_ready()
508 if self.tray:
509 self.tray.ready()
510 self.send_info_request()
511 msg = "running"
512 try:
513 windows = tuple(self._id_to_window.values())
514 except AttributeError:
515 pass
516 else:
517 trays = sum(1 for w in windows if w.is_tray())
518 wins = sum(1 for w in windows if not w.is_tray())
519 if wins:
520 msg += ", %i window%s" % (wins, engs(wins))
521 if trays:
522 msg += ", %i tray%s" % (trays, engs(trays))
523 log.info(msg)
525 def handshake_complete(self):
526 oh = self._on_handshake
527 self._on_handshake = None
528 for cb, args in oh:
529 try:
530 cb(*args)
531 except Exception:
532 log.error("Error processing handshake callback %s", cb, exc_info=True)
534 def after_handshake(self, cb, *args):
535 log("after_handshake(%s, %s) on_handshake=%s", cb, args, ellipsizer(self._on_handshake))
536 if self._on_handshake is None:
537 #handshake has already occurred, just call it:
538 self.idle_add(cb, *args)
539 else:
540 self._on_handshake.append((cb, args))
543 ######################################################################
544 # server messages:
545 def _process_server_event(self, packet):
546 log(": ".join((str(x) for x in packet[1:])))
548 def on_server_setting_changed(self, setting, cb):
549 self._on_server_setting_changed.setdefault(setting, []).append(cb)
551 def _process_setting_change(self, packet):
552 setting, value = packet[1:3]
553 setting = bytestostr(setting)
554 #convert "hello" / "setting" variable names to client variables:
555 if setting in (
556 "clipboard-limits",
557 ):
558 pass
559 elif setting in (
560 "bell", "randr", "cursors", "notifications", "dbus-proxy", "clipboard",
561 "clipboard-direction", "session_name",
562 "sharing", "sharing-toggle", "lock", "lock-toggle",
563 "start-new-commands", "client-shutdown", "webcam",
564 "bandwidth-limit", "clipboard-limits",
565 "xdg-menu",
566 ):
567 setattr(self, "server_%s" % setting.replace("-", "_"), value)
568 else:
569 log.info("unknown server setting changed: %s=%s", setting, repr_ellipsized(bytestostr(value)))
570 return
571 log("_process_setting_change: %s=%s", setting, value)
572 #xdg-menu is too big to log, and we have to update our attribute:
573 if setting=="xdg-menu":
574 self.server_xdg_menu = value
575 else:
576 log.info("server setting changed: %s=%s", setting, repr_ellipsized(value))
577 self.server_setting_changed(setting, value)
579 def server_setting_changed(self, setting, value):
580 log("setting_changed(%s, %s)", setting, value)
581 cbs = self._on_server_setting_changed.get(setting)
582 if cbs:
583 for cb in cbs:
584 log("setting_changed(%s, %s) calling %s", setting, value, cb)
585 cb(setting, value)
588 def get_control_commands_caps(self):
589 caps = ["show_session_info", "show_bug_report", "debug"]
590 for x in compression.get_enabled_compressors():
591 caps.append("enable_"+x)
592 for x in packet_encoding.get_enabled_encoders():
593 caps.append("enable_"+x)
594 log("get_control_commands_caps()=%s", caps)
595 return {"" : caps}
597 def _process_control(self, packet):
598 command = bytestostr(packet[1])
599 if command=="show_session_info":
600 args = packet[2:]
601 log("calling %s%s on server request", self.show_session_info, args)
602 self.show_session_info(*args)
603 elif command=="show_bug_report":
604 self.show_bug_report()
605 elif command in ("enable_%s" % x for x in compression.get_enabled_compressors()):
606 compressor = command.split("_")[1]
607 log.info("switching to %s on server request", compressor)
608 self._protocol.enable_compressor(compressor)
609 elif command in ("enable_%s" % x for x in packet_encoding.get_enabled_encoders()):
610 pe = command.split("_")[1]
611 log.info("switching to %s on server request", pe)
612 self._protocol.enable_encoder(pe)
613 elif command=="name":
614 assert len(args)>=3
615 self.server_session_name = args[2]
616 log.info("session name updated from server: %s", self.server_session_name)
617 #TODO: reset tray tooltip, session info title, etc..
618 elif command=="debug":
619 args = packet[2:]
620 if not args:
621 log.warn("not enough arguments for debug control command")
622 return
623 from xpra.log import (
624 add_debug_category, add_disabled_category,
625 enable_debug_for, disable_debug_for,
626 get_all_loggers,
627 )
628 log_cmd = bytestostr(args[0])
629 if log_cmd=="status":
630 dloggers = [x for x in get_all_loggers() if x.is_debug_enabled()]
631 if dloggers:
632 log.info("logging is enabled for:")
633 for l in dloggers:
634 log.info(" - %s", l)
635 else:
636 log.info("logging is not enabled for any loggers")
637 return
638 log_cmd = bytestostr(args[0])
639 if log_cmd not in ("enable", "disable"):
640 log.warn("invalid debug control mode: '%s' (must be 'enable' or 'disable')", log_cmd)
641 return
642 if len(args)<2:
643 log.warn("not enough arguments for '%s' debug control command" % log_cmd)
644 return
645 loggers = []
646 #each argument is a group
647 groups = [bytestostr(x) for x in args[1:]]
648 for group in groups:
649 #and each group is a list of categories
650 #preferably separated by "+",
651 #but we support "," for backwards compatibility:
652 categories = [v.strip() for v in group.replace("+", ",").split(",")]
653 if log_cmd=="enable":
654 add_debug_category(*categories)
655 loggers += enable_debug_for(*categories)
656 else:
657 assert log_cmd=="disable"
658 add_disabled_category(*categories)
659 loggers += disable_debug_for(*categories)
660 if not loggers:
661 log.info("%s debugging, no new loggers matching: %s", log_cmd, csv(groups))
662 else:
663 log.info("%sd debugging for:", log_cmd)
664 for l in loggers:
665 log.info(" - %s", l)
666 else:
667 log.warn("received invalid control command from server: %s", command)
670 def may_notify_audio(self, summary, body):
671 self.may_notify(XPRA_AUDIO_NOTIFICATION_ID, summary, body, icon_name="audio")
674 ######################################################################
675 # features:
676 def send_sharing_enabled(self):
677 assert self.server_sharing and self.server_sharing_toggle
678 self.send("sharing-toggle", self.client_supports_sharing)
680 def send_lock_enabled(self):
681 assert self.server_lock_toggle
682 self.send("lock-toggle", self.client_lock)
684 def send_notify_enabled(self):
685 assert self.client_supports_notifications, "cannot toggle notifications: the feature is disabled by the client"
686 self.send("set-notify", self.notifications_enabled)
688 def send_bell_enabled(self):
689 assert self.client_supports_bell, "cannot toggle bell: the feature is disabled by the client"
690 assert self.server_bell, "cannot toggle bell: the feature is disabled by the server"
691 self.send("set-bell", self.bell_enabled)
693 def send_cursors_enabled(self):
694 assert self.client_supports_cursors, "cannot toggle cursors: the feature is disabled by the client"
695 assert self.server_cursors, "cannot toggle cursors: the feature is disabled by the server"
696 self.send("set-cursors", self.cursors_enabled)
698 def send_force_ungrab(self, wid):
699 self.send("force-ungrab", wid)
701 def send_keyboard_sync_enabled_status(self, *_args):
702 self.send("set-keyboard-sync-enabled", self.keyboard_sync)
705 ######################################################################
706 # keyboard:
707 def get_keyboard_caps(self):
708 caps = {}
709 if self.readonly or not self.keyboard_helper:
710 #don't bother sending keyboard info, as it won't be used
711 caps["keyboard"] = False
712 else:
713 caps.update(self.get_keymap_properties())
714 #show the user a summary of what we have detected:
715 self.keyboard_helper.log_keyboard_info()
717 caps["modifiers"] = self.get_current_modifiers()
718 delay_ms, interval_ms = self.keyboard_helper.key_repeat_delay, self.keyboard_helper.key_repeat_interval
719 if delay_ms>0 and interval_ms>0:
720 caps["key_repeat"] = (delay_ms,interval_ms)
721 else:
722 #cannot do keyboard_sync without a key repeat value!
723 #(maybe we could just choose one?)
724 self.keyboard_helper.keyboard_sync = False
725 caps["keyboard_sync"] = self.keyboard_helper.keyboard_sync
726 log("keyboard capabilities: %s", caps)
727 return caps
729 def window_keyboard_layout_changed(self, window):
730 #win32 can change the keyboard mapping per window...
731 keylog("window_keyboard_layout_changed(%s)", window)
732 if self.keyboard_helper:
733 self.keyboard_helper.keymap_changed()
735 def get_keymap_properties(self):
736 if not self.keyboard_helper:
737 return {}
738 props = self.keyboard_helper.get_keymap_properties()
739 props["modifiers"] = self.get_current_modifiers()
740 return props
742 def handle_key_action(self, window, key_event):
743 if self.readonly or self.keyboard_helper is None:
744 return False
745 wid = self._window_to_id[window]
746 keylog("handle_key_action(%s, %s) wid=%s", window, key_event, wid)
747 return self.keyboard_helper.handle_key_action(window, wid, key_event)
749 def mask_to_names(self, mask):
750 if self.keyboard_helper is None:
751 return []
752 return self.keyboard_helper.mask_to_names(mask)
755 ######################################################################
756 # windows overrides
757 def cook_metadata(self, _new_window, metadata):
758 #convert to a typedict and apply client-side overrides:
759 metadata = typedict(metadata)
760 if self.server_is_desktop and self.desktop_fullscreen:
761 #force it fullscreen:
762 metadata.pop("size-constraints", None)
763 metadata["fullscreen"] = True
764 #FIXME: try to figure out the monitors we go fullscreen on for X11:
765 #if POSIX:
766 # metadata["fullscreen-monitors"] = [0, 1, 0, 1]
767 return metadata
769 ######################################################################
770 # network and status:
771 def server_connection_state_change(self):
772 if not self._server_ok:
773 log.info("server is not responding, drawing spinners over the windows")
774 def timer_redraw():
775 if self._protocol is None:
776 #no longer connected!
777 return False
778 ok = self.server_ok()
779 self.redraw_spinners()
780 if ok:
781 log.info("server is OK again")
782 return not ok #repaint again until ok
783 self.idle_add(self.redraw_spinners)
784 self.timeout_add(250, timer_redraw)
786 def redraw_spinners(self):
787 #draws spinner on top of the window, or not (plain repaint)
788 #depending on whether the server is ok or not
789 ok = self.server_ok()
790 log("redraw_spinners() ok=%s", ok)
791 for w in self._id_to_window.values():
792 if not w.is_tray():
793 w.spinner(ok)
796 ######################################################################
797 # packets:
798 def init_authenticated_packet_handlers(self):
799 log("init_authenticated_packet_handlers()")
800 for c in CLIENT_BASES:
801 c.init_authenticated_packet_handlers(self)
802 #run from the UI thread:
803 self.add_packet_handlers({
804 "startup-complete": self._process_startup_complete,
805 "setting-change": self._process_setting_change,
806 "control" : self._process_control,
807 })
808 #run directly from the network thread:
809 self.add_packet_handler("server-event", self._process_server_event, False)
812 def process_packet(self, proto, packet):
813 self.check_server_echo(0)
814 XpraClientBase.process_packet(self, proto, packet)