Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/client/gtk_base/gtk_tray_menu_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# -*- coding: utf-8 -*-
2# This file is part of Xpra.
3# Copyright (C) 2011-2020 Antoine Martin <antoine@xpra.org>
4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
7import os
8import re
9from gi.repository import GLib, Gtk, GdkPixbuf
11from xpra.util import (
12 CLIENT_EXIT,
13 iround, envbool,
14 ellipsizer, repr_ellipsized, reverse_dict, typedict,
15 )
16from xpra.os_util import bytestostr, OSX, WIN32
17from xpra.gtk_common.gtk_util import (
18 get_pixbuf_from_data, scaled_image,
19 )
20from xpra.client.gtk_base.menu_helper import (
21 MenuHelper,
22 ll, set_sensitive, ensure_item_selected,
23 make_encodingsmenu, make_min_auto_menu,
24 )
25from xpra.client.client_base import EXIT_OK
26from xpra.codecs.codec_constants import PREFERRED_ENCODING_ORDER
27from xpra.simple_stats import std_unit_dec
28from xpra.platform.paths import get_icon_dir
29from xpra.client import mixin_features
30from xpra.log import Logger
32log = Logger("menu")
33execlog = Logger("exec")
34clipboardlog = Logger("menu", "clipboard")
35webcamlog = Logger("menu", "webcam")
36avsynclog = Logger("menu", "av-sync")
37bandwidthlog = Logger("bandwidth", "network")
39SHOW_TITLE_ITEM = envbool("XPRA_SHOW_TITLE_ITEM", True)
40SHOW_VERSION_CHECK = envbool("XPRA_SHOW_VERSION_CHECK", True)
41SHOW_QR = envbool("XPRA_SHOW_QR", True)
42SHOW_UPLOAD = envbool("XPRA_SHOW_UPLOAD_MENU", True)
43SHOW_SERVER_LOG = envbool("XPRA_SHOW_SERVER_LOG", True)
44SHOW_DOWNLOAD = envbool("XPRA_SHOW_DOWNLOAD", True)
45STARTSTOP_SOUND_MENU = envbool("XPRA_SHOW_SOUND_MENU", True)
46WEBCAM_MENU = envbool("XPRA_SHOW_WEBCAM_MENU", True)
47RUNCOMMAND_MENU = envbool("XPRA_SHOW_RUNCOMMAND_MENU", True)
48SHOW_SERVER_COMMANDS = envbool("XPRA_SHOW_SERVER_COMMANDS", True)
49SHOW_TRANSFERS = envbool("XPRA_SHOW_TRANSFERS", True)
50SHOW_CLIPBOARD_MENU = envbool("XPRA_SHOW_CLIPBOARD_MENU", True)
51SHOW_CLOSE = envbool("XPRA_SHOW_CLOSE", True)
52SHOW_SHUTDOWN = envbool("XPRA_SHOW_SHUTDOWN", True)
53WINDOWS_MENU = envbool("XPRA_SHOW_WINDOWS_MENU", True)
54START_MENU = envbool("XPRA_SHOW_START_MENU", True)
55MENU_ICONS = envbool("XPRA_MENU_ICONS", True)
58def get_bandwidth_menu_options():
59 options = []
60 for x in os.environ.get("XPRA_BANDWIDTH_MENU_OPTIONS", "1,2,5,10,20,50,100").split(","):
61 try:
62 options.append(int(float(x)*1000*1000))
63 except ValueError:
64 log.warn("Warning: invalid bandwidth menu option '%s'", x)
65 return options
66BANDWIDTH_MENU_OPTIONS = get_bandwidth_menu_options()
68LOSSLESS = "Lossless"
69QUALITY_OPTIONS_COMMON = {
70 50 : "Average",
71 30 : "Low",
72 }
73MIN_QUALITY_OPTIONS = QUALITY_OPTIONS_COMMON.copy()
74MIN_QUALITY_OPTIONS[0] = "None"
75MIN_QUALITY_OPTIONS[75] = "High"
76QUALITY_OPTIONS = QUALITY_OPTIONS_COMMON.copy()
77QUALITY_OPTIONS[0] = "Auto"
78QUALITY_OPTIONS[1] = "Lowest"
79QUALITY_OPTIONS[90] = "Best"
80QUALITY_OPTIONS[100] = LOSSLESS
83SPEED_OPTIONS_COMMON = {
84 70 : "Low Latency",
85 30 : "Low Bandwidth",
86 }
87MIN_SPEED_OPTIONS = SPEED_OPTIONS_COMMON.copy()
88MIN_SPEED_OPTIONS[0] = "None"
89SPEED_OPTIONS = SPEED_OPTIONS_COMMON.copy()
90SPEED_OPTIONS[0] = "Auto"
91SPEED_OPTIONS[1] = "Lowest Bandwidth"
92SPEED_OPTIONS[100] = "Lowest Latency"
94CLIPBOARD_LABELS = ["Clipboard", "Primary", "Secondary"]
95CLIPBOARD_LABEL_TO_NAME = {
96 "Clipboard" : "CLIPBOARD",
97 "Primary" : "PRIMARY",
98 "Secondary" : "SECONDARY"
99 }
100CLIPBOARD_NAME_TO_LABEL = reverse_dict(CLIPBOARD_LABEL_TO_NAME)
102CLIPBOARD_DIRECTION_LABELS = ["Client to server only", "Server to client only", "Both directions", "Disabled"]
103CLIPBOARD_DIRECTION_LABEL_TO_NAME = {
104 "Client to server only" : "to-server",
105 "Server to client only" : "to-client",
106 "Both directions" : "both",
107 "Disabled" : "disabled",
108 }
109CLIPBOARD_DIRECTION_NAME_TO_LABEL = reverse_dict(CLIPBOARD_DIRECTION_LABEL_TO_NAME)
112class GTKTrayMenuBase(MenuHelper):
114 def setup_menu(self):
115 return self.do_setup_menu(SHOW_CLOSE)
117 def do_setup_menu(self, show_close):
118 log("setup_menu(%s)", show_close)
119 menu = Gtk.Menu()
120 title_item = None
121 if SHOW_TITLE_ITEM:
122 title_item = Gtk.MenuItem()
123 title_item.set_label(self.client.session_name or "Xpra")
124 set_sensitive(title_item, False)
125 menu.append(title_item)
126 def set_menu_title(*_args):
127 #set the real name when available:
128 try:
129 title = self.client.get_tray_title()
130 except Exception:
131 title = self.client.session_name or "Xpra"
132 title_item.set_label(title)
133 self.client.after_handshake(set_menu_title)
135 menu.append(self.make_infomenuitem())
136 menu.append(self.make_featuresmenuitem())
137 if mixin_features.windows and self.client.keyboard_helper:
138 menu.append(self.make_keyboardmenuitem())
139 if mixin_features.clipboard and SHOW_CLIPBOARD_MENU:
140 menu.append(self.make_clipboardmenuitem())
141 if mixin_features.windows:
142 menu.append(self.make_picturemenuitem())
143 if mixin_features.audio and STARTSTOP_SOUND_MENU:
144 menu.append(self.make_audiomenuitem())
145 if mixin_features.webcam and WEBCAM_MENU:
146 menu.append(self.make_webcammenuitem())
147 if mixin_features.windows and WINDOWS_MENU:
148 menu.append(self.make_windowsmenuitem())
149 if RUNCOMMAND_MENU or SHOW_SERVER_COMMANDS or SHOW_UPLOAD or SHOW_SHUTDOWN:
150 menu.append(self.make_servermenuitem())
151 if mixin_features.windows and START_MENU:
152 menu.append(self.make_startmenuitem())
153 menu.append(self.make_disconnectmenuitem())
154 if show_close:
155 menu.append(self.make_closemenuitem())
156 menu.connect("deactivate", self.menu_deactivated)
157 menu.show_all()
158 log("setup_menu(%s) done", show_close)
159 return menu
162 def make_infomenuitem(self):
163 info_menu_item = self.menuitem("Information", "information.png")
164 menu = Gtk.Menu()
165 info_menu_item.set_submenu(menu)
166 menu.append(self.make_aboutmenuitem())
167 menu.append(self.make_sessioninfomenuitem())
168 if SHOW_QR:
169 menu.append(self.make_qrmenuitem())
170 if SHOW_VERSION_CHECK:
171 menu.append(self.make_updatecheckmenuitem())
172 menu.append(self.make_bugreportmenuitem())
173 info_menu_item.show_all()
174 return info_menu_item
176 def make_featuresmenuitem(self):
177 features_menu_item = self.handshake_menuitem("Features", "features.png")
178 menu = Gtk.Menu()
179 self.append_featuresmenuitems(menu)
180 features_menu_item.set_submenu(menu)
181 features_menu_item.show_all()
182 return features_menu_item
184 def append_featuresmenuitems(self, menu):
185 menu.append(self.make_sharingmenuitem())
186 menu.append(self.make_lockmenuitem())
187 if mixin_features.windows:
188 menu.append(self.make_readonlymenuitem())
189 menu.append(self.make_bellmenuitem())
190 if mixin_features.notifications:
191 menu.append(self.make_notificationsmenuitem())
192 if mixin_features.windows:
193 menu.append(self.make_cursorsmenuitem())
194 if self.client.client_supports_opengl:
195 menu.append(self.make_openglmenuitem())
196 if mixin_features.windows:
197 menu.append(self.make_modalwindowmenuitem())
199 def make_sharingmenuitem(self):
200 def sharing_toggled(*args):
201 v = self.sharing_menuitem.get_active()
202 self.client.client_supports_sharing = v
203 if self.client.server_sharing_toggle:
204 self.client.send_sharing_enabled()
205 log("sharing_toggled(%s) readonly=%s", args, self.client.readonly)
206 self.sharing_menuitem = self.checkitem("Sharing", sharing_toggled)
207 self.sharing_menuitem.set_tooltip_text("Allow other clients to connect to this session")
208 set_sensitive(self.sharing_menuitem, False)
209 def set_sharing_menuitem(*args):
210 log("set_sharing_menuitem%s client_supports_sharing=%s, server_sharing_toggle=%s, server_sharing=%s",
211 args, self.client.client_supports_sharing,
212 self.client.server_sharing_toggle, self.client.server_sharing)
213 self.sharing_menuitem.set_active(self.client.server_sharing and self.client.client_supports_sharing)
214 set_sensitive(self.sharing_menuitem, self.client.server_sharing_toggle)
215 if not self.client.server_sharing:
216 self.sharing_menuitem.set_tooltip_text("Sharing is disabled on the server")
217 elif not self.client.server_sharing_toggle:
218 self.sharing_menuitem.set_tooltip_text("Sharing cannot be changed on this server")
219 else:
220 self.sharing_menuitem.set_tooltip_text("")
221 self.client.after_handshake(set_sharing_menuitem)
222 self.client.on_server_setting_changed("sharing", set_sharing_menuitem)
223 self.client.on_server_setting_changed("sharing-toggle", set_sharing_menuitem)
224 return self.sharing_menuitem
226 def make_lockmenuitem(self):
227 def lock_toggled(*args):
228 v = self.lock_menuitem.get_active()
229 self.client.client_lock = v
230 if self.client.server_lock_toggle:
231 self.client.send_lock_enabled()
232 log("lock_toggled(%s) lock=%s", args, self.client.client_lock)
233 self.lock_menuitem = self.checkitem("Lock", lock_toggled)
234 self.lock_menuitem.set_tooltip_text("Prevent other clients from stealing this session")
235 set_sensitive(self.lock_menuitem, False)
236 def set_lock_menuitem(*args):
237 log("set_lock_menuitem%s client_lock=%s, server_lock_toggle=%s, server lock=%s",
238 args, self.client.client_lock, self.client.server_lock_toggle, self.client.server_lock)
239 self.lock_menuitem.set_active(self.client.server_lock and self.client.client_lock)
240 set_sensitive(self.lock_menuitem, self.client.server_lock_toggle)
241 if not self.client.server_lock:
242 self.lock_menuitem.set_tooltip_text("Session locking is disabled on this server")
243 elif not self.client.server_lock_toggle:
244 self.lock_menuitem.set_tooltip_text("Session locking cannot be toggled on this server")
245 else:
246 self.lock_menuitem.set_tooltip_text("")
247 self.client.after_handshake(set_lock_menuitem)
248 self.client.on_server_setting_changed("lock", set_lock_menuitem)
249 self.client.on_server_setting_changed("lock-toggle", set_lock_menuitem)
250 return self.lock_menuitem
252 def make_readonlymenuitem(self):
253 def readonly_toggled(*args):
254 v = self.readonly_menuitem.get_active()
255 self.client.readonly = v
256 log("readonly_toggled(%s) readonly=%s", args, self.client.readonly)
257 self.readonly_menuitem = self.checkitem("Read-only", readonly_toggled)
258 set_sensitive(self.readonly_menuitem, False)
259 def set_readonly_menuitem(*args):
260 log("set_readonly_menuitem%s enabled=%s", args, self.client.readonly)
261 self.readonly_menuitem.set_active(self.client.readonly)
262 set_sensitive(self.readonly_menuitem, not self.client.server_readonly)
263 if not self.client.server_readonly:
264 self.readonly_menuitem.set_tooltip_text("Disable all mouse and keyboard input")
265 else:
266 self.readonly_menuitem.set_tooltip_text("Cannot disable readonly mode: "+
267 "the server has locked the session to read only")
268 self.client.after_handshake(set_readonly_menuitem)
269 return self.readonly_menuitem
271 def make_bellmenuitem(self):
272 c = self.client
273 def bell_toggled(*args):
274 can_toggle_bell = c.server_bell and c.client_supports_bell
275 if not can_toggle_bell:
276 return
277 v = self.bell_menuitem.get_active()
278 changed = self.client.bell_enabled != v
279 self.client.bell_enabled = v
280 if changed:
281 self.client.send_bell_enabled()
282 log("bell_toggled(%s) bell_enabled=%s", args, self.client.bell_enabled)
283 self.bell_menuitem = self.checkitem("Bell", bell_toggled)
284 set_sensitive(self.bell_menuitem, False)
285 def set_bell_menuitem(*args):
286 log("set_bell_menuitem%s enabled=%s", args, self.client.bell_enabled)
287 can_toggle_bell = c.server_bell and c.client_supports_bell
288 self.bell_menuitem.set_active(self.client.bell_enabled and can_toggle_bell)
289 set_sensitive(self.bell_menuitem, can_toggle_bell)
290 if can_toggle_bell:
291 self.bell_menuitem.set_tooltip_text("Forward system bell")
292 else:
293 self.bell_menuitem.set_tooltip_text("Cannot forward the system bell: the feature has been disabled")
294 self.client.after_handshake(set_bell_menuitem)
295 self.client.on_server_setting_changed("bell", set_bell_menuitem)
296 return self.bell_menuitem
298 def make_cursorsmenuitem(self):
299 def cursors_toggled(*args):
300 v = self.cursors_menuitem.get_active()
301 changed = self.client.cursors_enabled != v
302 self.client.cursors_enabled = v
303 if changed:
304 self.client.send_cursors_enabled()
305 if not self.client.cursors_enabled:
306 self.client.reset_cursor()
307 log("cursors_toggled(%s) cursors_enabled=%s", args, self.client.cursors_enabled)
308 self.cursors_menuitem = self.checkitem("Cursors", cursors_toggled)
309 set_sensitive(self.cursors_menuitem, False)
310 def set_cursors_menuitem(*args):
311 log("set_cursors_menuitem%s enabled=%s", args, self.client.cursors_enabled)
312 c = self.client
313 can_toggle_cursors = c.server_cursors and c.client_supports_cursors
314 self.cursors_menuitem.set_active(self.client.cursors_enabled and can_toggle_cursors)
315 set_sensitive(self.cursors_menuitem, can_toggle_cursors)
316 if can_toggle_cursors:
317 self.cursors_menuitem.set_tooltip_text("Forward custom mouse cursors")
318 else:
319 self.cursors_menuitem.set_tooltip_text("Cannot forward mouse cursors: the feature has been disabled")
320 self.client.after_handshake(set_cursors_menuitem)
321 self.client.on_server_setting_changed("cursors", set_cursors_menuitem)
322 return self.cursors_menuitem
324 def make_notificationsmenuitem(self):
325 def notifications_toggled(*args):
326 v = self.notifications_menuitem.get_active()
327 changed = self.client.notifications_enabled != v
328 self.client.notifications_enabled = v
329 log("notifications_toggled%s active=%s changed=%s", args, v, changed)
330 if changed:
331 self.client.send_notify_enabled()
332 self.notifications_menuitem = self.checkitem("Notifications", notifications_toggled)
333 set_sensitive(self.notifications_menuitem, False)
334 def set_notifications_menuitem(*args):
335 log("set_notifications_menuitem%s enabled=%s", args, self.client.notifications_enabled)
336 can_notify = self.client.client_supports_notifications
337 self.notifications_menuitem.set_active(self.client.notifications_enabled and can_notify)
338 set_sensitive(self.notifications_menuitem, can_notify)
339 if can_notify:
340 self.notifications_menuitem.set_tooltip_text("Forward system notifications")
341 else:
342 self.notifications_menuitem.set_tooltip_text("Cannot forward system notifications: "+
343 "the feature has been disabled")
344 self.client.after_handshake(set_notifications_menuitem)
345 return self.notifications_menuitem
348 def remote_clipboard_changed(self, item, clipboard_submenu):
349 c = self.client
350 if not c or not c.server_clipboard or not c.client_supports_clipboard:
351 return
352 #prevent infinite recursion where ensure_item_selected
353 #ends up calling here again
354 key = "_in_remote_clipboard_changed"
355 ich = getattr(clipboard_submenu, key, False)
356 clipboardlog("remote_clipboard_changed%s already in change handler: %s, visible=%s",
357 (ll(item), clipboard_submenu), ich, clipboard_submenu.get_visible())
358 if ich: # or not clipboard_submenu.get_visible():
359 return
360 try:
361 setattr(clipboard_submenu, key, True)
362 selected_item = ensure_item_selected(clipboard_submenu, item)
363 selected = selected_item.get_label()
364 remote_clipboard = CLIPBOARD_LABEL_TO_NAME.get(selected)
365 self.set_new_remote_clipboard(remote_clipboard)
366 finally:
367 setattr(clipboard_submenu, key, False)
369 def set_new_remote_clipboard(self, remote_clipboard):
370 clipboardlog("set_new_remote_clipboard(%s)", remote_clipboard)
371 ch = self.client.clipboard_helper
372 local_clipboard = "CLIPBOARD"
373 ch._local_to_remote = {local_clipboard : remote_clipboard}
374 ch._remote_to_local = {remote_clipboard : local_clipboard}
375 selections = [remote_clipboard]
376 clipboardlog.info("server clipboard synchronization changed to %s selection", remote_clipboard)
377 #tell the server what to look for:
378 #(now that "clipboard-toggled" has re-enabled clipboard if necessary)
379 self.client.send_clipboard_selections(selections)
380 ch.send_tokens([local_clipboard])
382 def make_translatedclipboard_optionsmenuitem(self):
383 clipboardlog("make_translatedclipboard_optionsmenuitem()")
384 ch = self.client.clipboard_helper
385 selection_menu = self.menuitem("Selection", None, "Choose which remote clipboard to connect to")
386 selection_submenu = Gtk.Menu()
387 selection_menu.set_submenu(selection_submenu)
388 rc_setting = None
389 if len(ch._local_to_remote)==1:
390 rc_setting = tuple(ch._local_to_remote.values())[0]
391 for label in CLIPBOARD_LABELS:
392 remote_clipboard = CLIPBOARD_LABEL_TO_NAME[label]
393 selection_item = Gtk.CheckMenuItem(label=label)
394 selection_item.set_active(remote_clipboard==rc_setting)
395 selection_item.set_draw_as_radio(True)
396 def remote_clipboard_changed(item):
397 self.remote_clipboard_changed(item, selection_submenu)
398 selection_item.connect("toggled", remote_clipboard_changed)
399 selection_submenu.append(selection_item)
400 selection_submenu.show_all()
401 return selection_menu
403 def clipboard_direction_changed(self, item, submenu):
404 log("clipboard_direction_changed(%s, %s)", item, submenu)
405 sel = ensure_item_selected(submenu, item, recurse=False)
406 if not sel:
407 return
408 self.do_clipboard_direction_changed(sel.get_label() or "")
410 def do_clipboard_direction_changed(self, label):
411 #find the value matching this item label:
412 d = CLIPBOARD_DIRECTION_LABEL_TO_NAME.get(label)
413 if d and d!=self.client.client_clipboard_direction:
414 log.info("clipboard synchronization direction changed to: %s", label.lower())
415 self.client.client_clipboard_direction = d
416 can_send = d in ("to-server", "both")
417 can_receive = d in ("to-client", "both")
418 self.client.clipboard_helper.set_direction(can_send, can_receive)
419 #will send new tokens and may help reset things:
420 self.client.emit("clipboard-toggled")
422 def make_clipboardmenuitem(self):
423 clipboardlog("make_clipboardmenuitem()")
424 self.clipboard_menuitem = self.menuitem("Clipboard", "clipboard.png")
425 set_sensitive(self.clipboard_menuitem, False)
426 def set_clipboard_menu(*args):
427 c = self.client
428 if not c.server_clipboard:
429 self.clipboard_menuitem.set_tooltip_text("Server does not support clipboard synchronization")
430 return
431 ch = c.clipboard_helper
432 if not c.client_supports_clipboard or not ch:
433 self.clipboard_menuitem.set_tooltip_text("Client does not support clipboard synchronization")
434 return
435 #add a submenu:
436 set_sensitive(self.clipboard_menuitem, True)
437 clipboard_submenu = Gtk.Menu()
438 self.clipboard_menuitem.set_submenu(clipboard_submenu)
439 if WIN32 or OSX:
440 #add a submenu to change the selection we synchronize with
441 #since this platform only has a single clipboard
442 try:
443 clipboardlog("set_clipboard_menu(%s) helper=%s, server=%s, client=%s",
444 args, ch, c.server_clipboard, c.client_supports_clipboard)
445 clipboard_submenu.append(self.make_translatedclipboard_optionsmenuitem())
446 clipboard_submenu.append(Gtk.SeparatorMenuItem())
447 except ImportError:
448 clipboardlog.error("make_clipboardmenuitem()", exc_info=True)
449 items = []
450 for label in CLIPBOARD_DIRECTION_LABELS:
451 direction_item = Gtk.CheckMenuItem(label=label)
452 d = CLIPBOARD_DIRECTION_LABEL_TO_NAME.get(label)
453 direction_item.set_active(d==self.client.client_clipboard_direction)
454 clipboard_submenu.append(direction_item)
455 items.append(direction_item)
456 clipboard_submenu.show_all()
457 #connect signals:
458 for direction_item in items:
459 direction_item.connect("toggled", self.clipboard_direction_changed, clipboard_submenu)
460 self.client.after_handshake(set_clipboard_menu)
461 return self.clipboard_menuitem
464 def make_keyboardsyncmenuitem(self):
465 def set_keyboard_sync_tooltip():
466 kh = self.client.keyboard_helper
467 if not kh:
468 text = "Keyboard support is not loaded"
469 elif kh.keyboard_sync:
470 text = "Disable keyboard synchronization "+\
471 "(prevents spurious key repeats on high latency connections)"
472 else:
473 text = "Enable keyboard state synchronization"
474 self.keyboard_sync_menuitem.set_tooltip_text(text)
475 def keyboard_sync_toggled(*args):
476 ks = self.keyboard_sync_menuitem.get_active()
477 if self.client.keyboard_sync!=ks:
478 self.client.keyboard_sync = ks
479 log("keyboard_sync_toggled(%s) keyboard_sync=%s", args, ks)
480 set_keyboard_sync_tooltip()
481 self.client.send_keyboard_sync_enabled_status()
482 self.keyboard_sync_menuitem = self.checkitem("State Synchronization")
483 set_sensitive(self.keyboard_sync_menuitem, False)
484 def set_keyboard_sync_menuitem(*args):
485 kh = self.client.keyboard_helper
486 can_set_sync = kh and self.client.server_keyboard
487 set_sensitive(self.keyboard_sync_menuitem, can_set_sync)
488 if can_set_sync:
489 self.keyboard_sync_menuitem.connect("toggled", keyboard_sync_toggled)
490 if kh:
491 log("set_keyboard_sync_menuitem%s enabled=%s", args, kh.keyboard_sync)
492 self.keyboard_sync_menuitem.set_active(kh and bool(kh.keyboard_sync))
493 set_keyboard_sync_tooltip()
494 self.client.after_handshake(set_keyboard_sync_menuitem)
495 return self.keyboard_sync_menuitem
497 def make_shortcutsmenuitem(self):
498 self.keyboard_shortcuts_menuitem = self.checkitem("Intercept Shortcuts")
499 kh = self.client.keyboard_helper
500 self.keyboard_shortcuts_menuitem.set_active(kh and bool(kh.shortcuts_enabled))
501 def keyboard_shortcuts_toggled(*args):
502 ks = self.keyboard_shortcuts_menuitem.get_active()
503 log("keyboard_shortcuts_toggled%s enabled=%s", args, ks)
504 kh.shortcuts_enabled = ks
505 self.keyboard_shortcuts_menuitem.connect("toggled", keyboard_shortcuts_toggled)
506 return self.keyboard_shortcuts_menuitem
508 def make_viewshortcutsmenuitem(self):
509 def show_shortcuts(*_args):
510 self.client.show_shortcuts()
511 return self.menuitem("View Shortcuts", tooltip="Show all active keyboard shortcuts", cb=show_shortcuts)
514 def make_openglmenuitem(self):
515 gl = self.checkitem("OpenGL")
516 gl.set_tooltip_text("hardware accelerated rendering using OpenGL")
517 def gl_set(*args):
518 log("gl_set(%s) opengl_enabled=%s, ", args, self.client.opengl_enabled)
519 gl.set_active(self.client.opengl_enabled)
520 set_sensitive(gl, self.client.client_supports_opengl)
521 def opengl_toggled(*args):
522 log("opengl_toggled%s", args)
523 self.client.toggle_opengl()
524 gl.connect("toggled", opengl_toggled)
525 self.client.after_handshake(gl_set)
526 return gl
528 def make_modalwindowmenuitem(self):
529 modal = self.checkitem("Modal Windows")
530 modal.set_tooltip_text("honour modal windows")
531 modal.set_active(self.client.modal_windows)
532 set_sensitive(modal, False)
533 def modal_toggled(*args):
534 self.client.modal_windows = modal.get_active()
535 log("modal_toggled%s modal_windows=%s", args, self.client.modal_windows)
536 def set_modal_menuitem(*_args):
537 set_sensitive(modal, True)
538 self.client.after_handshake(set_modal_menuitem)
539 modal.connect("toggled", modal_toggled)
540 return modal
542 def make_picturemenuitem(self):
543 picture_menu_item = self.handshake_menuitem("Picture", "picture.png")
544 menu = Gtk.Menu()
545 picture_menu_item.set_submenu(menu)
546 menu.append(self.make_bandwidthlimitmenuitem())
547 if self.client.windows_enabled and len(self.client.get_encodings())>1:
548 menu.append(self.make_encodingsmenuitem())
549 if self.client.can_scale:
550 menu.append(self.make_scalingmenuitem())
551 menu.append(self.make_qualitymenuitem())
552 menu.append(self.make_speedmenuitem())
553 picture_menu_item.show_all()
554 return picture_menu_item
556 def make_bandwidthlimitmenuitem(self):
557 bandwidth_limit_menu_item = self.menuitem("Bandwidth Limit", "bandwidth_limit.png")
558 menu = Gtk.Menu()
559 menuitems = {}
561 def bwitem(bwlimit):
562 c = self.bwitem(menu, bwlimit)
563 menuitems[bwlimit] = c
564 return c
566 menu.append(bwitem(0))
567 bandwidth_limit_menu_item.set_submenu(menu)
568 bandwidth_limit_menu_item.show_all()
570 def set_bwlimitmenu(*_args):
571 if self.client.mmap_enabled:
572 bandwidth_limit_menu_item.set_tooltip_text("memory mapped transfers are in use, "+
573 "so bandwidth limits are disabled")
574 set_sensitive(bandwidth_limit_menu_item, False)
575 elif not self.client.server_bandwidth_limit_change:
576 bandwidth_limit_menu_item.set_tooltip_text("the server does not support bandwidth-limit")
577 set_sensitive(bandwidth_limit_menu_item, False)
578 else:
579 initial_value = self.client.server_bandwidth_limit or self.client.bandwidth_limit or 0
580 bandwidthlog("set_bwlimitmenu() server_bandwidth_limit=%s, bandwidth_limit=%s, initial value=%s",
581 self.client.server_bandwidth_limit, self.client.bandwidth_limit, initial_value)
583 options = BANDWIDTH_MENU_OPTIONS
584 if initial_value and initial_value not in options:
585 options.append(initial_value)
586 bandwidthlog("bandwidth options=%s", options)
587 menu.append(Gtk.SeparatorMenuItem())
588 for v in sorted(options):
589 menu.append(bwitem(v))
591 sbl = self.client.server_bandwidth_limit
592 for bwlimit, c in menuitems.items():
593 c.set_active(initial_value==bwlimit)
594 #disable any values higher than what the server allows:
595 if bwlimit==0:
596 below_server_limit = sbl==0
597 else:
598 below_server_limit = sbl==0 or bwlimit<=sbl
599 set_sensitive(c, below_server_limit)
600 if not below_server_limit:
601 c.set_tooltip_text("server set the limit to %sbps" % std_unit_dec(sbl))
602 self.client.after_handshake(set_bwlimitmenu)
603 self.client.on_server_setting_changed("bandwidth-limit", set_bwlimitmenu)
604 return bandwidth_limit_menu_item
605 def bwitem(self, menu, bwlimit=0):
606 bandwidthlog("bwitem(%s, %i)", menu, bwlimit)
607 if bwlimit<=0:
608 label = "None"
609 elif bwlimit>=10*1000*1000:
610 label = "%iMbps" % (bwlimit//(1000*1000))
611 else:
612 label = "%sbps" % std_unit_dec(bwlimit)
613 c = Gtk.CheckMenuItem(label=label)
614 c.set_draw_as_radio(True)
615 c.set_active(False)
616 set_sensitive(c, False)
617 def activate_cb(item, *args):
618 if not c.get_active():
619 return
620 bandwidthlog("activate_cb(%s, %s) bwlimit=%s", item, args, bwlimit)
621 ensure_item_selected(menu, item)
622 if (self.client.bandwidth_limit or 0)!=bwlimit:
623 self.client.bandwidth_limit = bwlimit
624 self.client.send_bandwidth_limit()
625 c.connect("toggled", activate_cb)
626 c.show()
627 return c
630 def make_encodingsmenuitem(self):
631 encodings = self.menuitem("Encoding", "encoding.png", "Choose picture data encoding", None)
632 set_sensitive(encodings, False)
633 self.encodings_submenu = None
634 def set_encodingsmenuitem(*args):
635 log("set_encodingsmenuitem%s", args)
636 set_sensitive(encodings, not self.client.mmap_enabled)
637 if self.client.mmap_enabled:
638 #mmap disables encoding and uses raw rgb24
639 encodings.set_label("Encoding")
640 encodings.set_tooltip_text("memory mapped transfers are in use so picture encoding is disabled")
641 else:
642 self.encodings_submenu = self.make_encodingssubmenu()
643 encodings.set_submenu(self.encodings_submenu)
644 self.client.after_handshake(set_encodingsmenuitem)
645 #FUGLY warning: we want to update the menu if we get an 'encodings' packet,
646 #so we inject our handler:
647 saved_process_encodings = getattr(self.client, "_process_encodings")
648 if saved_process_encodings:
649 def process_encodings(*args):
650 #pass it on:
651 saved_process_encodings(*args)
652 #re-generate the menu with the correct server properties:
653 GLib.idle_add(set_encodingsmenuitem)
654 self.client._process_encodings = process_encodings
655 return encodings
657 def make_encodingssubmenu(self):
658 server_encodings = list(self.client.server_encodings)
659 all_encodings = [x for x in PREFERRED_ENCODING_ORDER if x in self.client.get_encodings()]
660 encodings = [x for x in all_encodings if x not in self.client.server_encodings_problematic]
661 if not encodings:
662 #all we have, show the "bad" hidden ones then!
663 encodings = all_encodings
664 encodings.insert(0, "auto")
665 server_encodings.insert(0, "auto")
666 encodings_submenu = make_encodingsmenu(self.get_current_encoding,
667 self.set_current_encoding,
668 encodings, server_encodings)
669 return encodings_submenu
671 def get_current_encoding(self):
672 return self.client.encoding
673 def set_current_encoding(self, enc):
674 self.client.set_encoding(enc)
675 #these menus may need updating now:
676 self.set_qualitymenu()
677 self.set_speedmenu()
680 def make_scalingmenuitem(self):
681 self.scaling = self.menuitem("Scaling", "scaling.png", "Desktop Scaling")
682 scaling_submenu = self.make_scalingmenu()
683 self.scaling.set_submenu(scaling_submenu)
684 return self.scaling
686 def make_scalingmenu(self):
687 scaling_submenu = Gtk.Menu()
688 scaling_submenu.updating = False
689 from xpra.client.mixins.display import SCALING_OPTIONS
690 for x in SCALING_OPTIONS:
691 scaling_submenu.append(self.make_scalingvaluemenuitem(scaling_submenu, x))
692 def scaling_changed(*args):
693 log("scaling_changed%s updating selected tray menu item", args)
694 #find the nearest scaling option to show as current:
695 scaling = (self.client.xscale + self.client.yscale)/2.0
696 by_distance = dict((abs(scaling-x),x) for x in SCALING_OPTIONS)
697 closest = by_distance.get(sorted(by_distance)[0], 1)
698 scaling_submenu.updating = True
699 for x in scaling_submenu.get_children():
700 scalingvalue = getattr(x, "scalingvalue", -1)
701 x.set_active(scalingvalue==closest)
702 scaling_submenu.updating = False
703 self.client.connect("scaling-changed", scaling_changed)
704 return scaling_submenu
706 def make_scalingvaluemenuitem(self, scaling_submenu, scalingvalue=1.0):
707 def scalecmp(v):
708 return abs(self.client.xscale-v)<0.1
709 pct = iround(100.0*scalingvalue)
710 label = {100 : "None"}.get(pct, "%i%%" % pct)
711 c = Gtk.CheckMenuItem(label=label)
712 c.scalingvalue = scalingvalue
713 c.set_draw_as_radio(True)
714 c.set_active(False)
715 def scaling_activated(item):
716 log("scaling_activated(%s) scaling_value=%s, active=%s",
717 item, scalingvalue, item.get_active())
718 if scaling_submenu.updating or not item.get_active():
719 return
720 ensure_item_selected(scaling_submenu, item)
721 self.client.scaleset(item.scalingvalue, item.scalingvalue)
722 c.connect('activate', scaling_activated)
723 def set_active_state():
724 scaling_submenu.updating = True
725 c.set_active(scalecmp(scalingvalue))
726 scaling_submenu.updating = False
727 self.client.after_handshake(set_active_state)
728 return c
731 def make_qualitymenuitem(self):
732 self.quality = self.menuitem("Quality", "slider.png", "Picture quality", None)
733 set_sensitive(self.quality, False)
734 def may_enable_qualitymenu(*_args):
735 self.quality.set_submenu(self.make_qualitysubmenu())
736 self.set_qualitymenu()
737 self.client.after_handshake(may_enable_qualitymenu)
738 return self.quality
740 def make_qualitysubmenu(self):
741 quality_submenu = make_min_auto_menu("Quality", MIN_QUALITY_OPTIONS, QUALITY_OPTIONS,
742 self.get_min_quality, self.get_quality,
743 self.set_min_quality, self.set_quality)
744 quality_submenu.show_all()
745 return quality_submenu
747 def get_min_quality(self):
748 return self.client.min_quality
749 def get_quality(self):
750 return self.client.quality
751 def set_min_quality(self, q):
752 self.client.min_quality = q
753 self.client.quality = -1
754 self.client.send_min_quality()
755 self.client.send_quality()
756 def set_quality(self, q):
757 self.client.min_quality = -1
758 self.client.quality = q
759 self.client.send_min_quality()
760 self.client.send_quality()
762 def set_qualitymenu(self, *_args):
763 if self.quality:
764 can_use = not self.client.mmap_enabled and \
765 (self.client.encoding in self.client.server_encodings_with_quality or self.client.encoding=="auto")
766 set_sensitive(self.quality, can_use)
767 if self.client.mmap_enabled:
768 self.quality.set_tooltip_text("Speed is always 100% with mmap")
769 return
770 if not can_use:
771 self.quality.set_tooltip_text("Not supported with %s encoding" % self.client.encoding)
772 return
773 self.quality.set_tooltip_text("Minimum picture quality")
774 #now check if lossless is supported:
775 if self.quality.get_submenu():
776 can_lossless = self.client.encoding in self.client.server_encodings_with_lossless_mode
777 for q,item in self.quality.get_submenu().menu_items.items():
778 set_sensitive(item, q<100 or can_lossless)
781 def make_speedmenuitem(self):
782 self.speed = self.menuitem("Speed", "speed.png", "Encoding latency vs size", None)
783 set_sensitive(self.speed, False)
784 def may_enable_speedmenu(*_args):
785 self.speed.set_submenu(self.make_speedsubmenu())
786 self.set_speedmenu()
787 self.client.after_handshake(may_enable_speedmenu)
788 return self.speed
790 def make_speedsubmenu(self):
791 speed_submenu = make_min_auto_menu("Speed", MIN_SPEED_OPTIONS, SPEED_OPTIONS,
792 self.get_min_speed, self.get_speed, self.set_min_speed, self.set_speed)
793 return speed_submenu
795 def get_min_speed(self):
796 return self.client.min_speed
797 def get_speed(self):
798 return self.client.speed
799 def set_min_speed(self, s):
800 self.client.min_speed = s
801 self.client.speed = -1
802 self.client.send_min_speed()
803 self.client.send_speed()
804 def set_speed(self, s):
805 self.client.min_speed = -1
806 self.client.speed = s
807 self.client.send_min_speed()
808 self.client.send_speed()
811 def set_speedmenu(self, *_args):
812 if self.speed:
813 can_use = not self.client.mmap_enabled and \
814 (self.client.encoding in self.client.server_encodings_with_speed or self.client.encoding=="auto")
815 set_sensitive(self.speed, can_use)
816 if self.client.mmap_enabled:
817 self.speed.set_tooltip_text("Quality is always 100% with mmap")
818 elif self.client.encoding!="h264":
819 self.speed.set_tooltip_text("Not supported with %s encoding" % self.client.encoding)
820 else:
821 self.speed.set_tooltip_text("Encoding latency vs size")
824 def make_audiomenuitem(self):
825 audio_menu_item = self.handshake_menuitem("Audio", "audio.png")
826 menu = Gtk.Menu()
827 audio_menu_item.set_submenu(menu)
828 menu.append(self.make_speakermenuitem())
829 menu.append(self.make_microphonemenuitem())
830 menu.append(self.make_avsyncmenuitem())
831 audio_menu_item.show_all()
832 return audio_menu_item
835 def spk_on(self, *args):
836 log("spk_on(%s)", args)
837 self.client.start_receiving_sound()
838 def spk_off(self, *args):
839 log("spk_off(%s)", args)
840 self.client.stop_receiving_sound()
841 def make_speakermenuitem(self):
842 speaker = self.menuitem("Speaker", "speaker.png", "Forward sound output from the server")
843 set_sensitive(speaker, False)
844 def is_speaker_on(*_args):
845 return self.client.speaker_enabled
846 def speaker_state(*_args):
847 if not self.client.speaker_allowed:
848 set_sensitive(speaker, False)
849 speaker.set_tooltip_text("Speaker forwarding has been disabled")
850 return
851 if not self.client.server_sound_send:
852 set_sensitive(speaker, False)
853 speaker.set_tooltip_text("Server does not support speaker forwarding")
854 return
855 set_sensitive(speaker, True)
856 speaker.set_submenu(self.make_soundsubmenu(is_speaker_on, self.spk_on, self.spk_off, "speaker-changed"))
857 self.client.after_handshake(speaker_state)
858 return speaker
860 def mic_on(self, *args):
861 log("mic_on(%s)", args)
862 self.client.start_sending_sound()
863 def mic_off(self, *args):
864 log("mic_off(%s)", args)
865 self.client.stop_sending_sound()
866 def make_microphonemenuitem(self):
867 microphone = self.menuitem("Microphone", "microphone.png", "Forward sound input to the server", None)
868 set_sensitive(microphone, False)
869 def is_microphone_on(*_args):
870 return self.client.microphone_enabled
871 def microphone_state(*_args):
872 if not self.client.microphone_allowed:
873 set_sensitive(microphone, False)
874 microphone.set_tooltip_text("Microphone forwarding has been disabled")
875 return
876 if not self.client.server_sound_receive:
877 set_sensitive(microphone, False)
878 microphone.set_tooltip_text("Server does not support microphone forwarding")
879 return
880 set_sensitive(microphone, True)
881 microphone.set_submenu(self.make_soundsubmenu(is_microphone_on,
882 self.mic_on, self.mic_off, "microphone-changed"))
883 self.client.after_handshake(microphone_state)
884 return microphone
886 def sound_submenu_activate(self, item, menu, cb):
887 log("submenu_uncheck(%s, %s, %s) ignore_events=%s, active=%s",
888 item, menu, cb, menu.ignore_events, item.get_active())
889 if menu.ignore_events:
890 return
891 ensure_item_selected(menu, item)
892 if item.get_active():
893 cb()
895 def make_soundsubmenu(self, is_on_cb, on_cb, off_cb, client_signal):
896 menu = Gtk.Menu()
897 menu.ignore_events = False
898 def onoffitem(label, active, cb):
899 c = Gtk.CheckMenuItem(label=label)
900 c.set_draw_as_radio(True)
901 c.set_active(active)
902 set_sensitive(c, True)
903 c.connect('activate', self.sound_submenu_activate, menu, cb)
904 return c
905 is_on = is_on_cb()
906 on = onoffitem("On", is_on, on_cb)
907 off = onoffitem("Off", not is_on, off_cb)
908 menu.append(on)
909 menu.append(off)
910 def update_soundsubmenu_state(*args):
911 menu.ignore_events = True
912 is_on = is_on_cb()
913 log("update_soundsubmenu_state%s is_on=%s", args, is_on)
914 if is_on:
915 if not on.get_active():
916 on.set_active(True)
917 ensure_item_selected(menu, on)
918 else:
919 if not off.get_active():
920 off.set_active(True)
921 ensure_item_selected(menu, off)
922 menu.ignore_events = False
923 self.client.connect(client_signal, update_soundsubmenu_state)
924 self.client.after_handshake(update_soundsubmenu_state)
925 menu.show_all()
926 return menu
928 def make_avsyncmenuitem(self):
929 sync = self.menuitem("Video Sync", "video.png", "Synchronize audio and video", None)
930 menu = Gtk.Menu()
931 current_value = 0
932 if not self.client.av_sync:
933 current_value = None
934 def syncitem(label, delta=0):
935 c = Gtk.CheckMenuItem(label=label)
936 c.set_draw_as_radio(True)
937 c.set_active(current_value==delta)
938 def activate_cb(item, *_args):
939 avsynclog("activate_cb(%s, %s) delta=%s", item, menu, delta)
940 if delta==0:
941 self.client.av_sync = False
942 self.client.send_sound_sync(0)
943 else:
944 self.client.av_sync = True
945 self.client.av_sync_delta = delta
946 #the actual sync value will be calculated and sent
947 #in client._process_sound_data
948 c.connect("toggled", activate_cb, menu)
949 return c
950 menu.append(syncitem("Off", None))
951 menu.append(Gtk.SeparatorMenuItem())
952 menu.append(syncitem("-200", -200))
953 menu.append(syncitem("-100", -100))
954 menu.append(syncitem(" -50", -50))
955 menu.append(syncitem("Auto", 0))
956 menu.append(syncitem(" +50", 50))
957 menu.append(syncitem(" +100", 100))
958 menu.append(syncitem(" +200", 200))
959 sync.set_submenu(menu)
960 sync.show_all()
961 def set_avsyncmenu(*_args):
962 if not self.client.server_av_sync:
963 set_sensitive(sync, False)
964 sync.set_tooltip_text("video-sync is not supported by the server")
965 return
966 if not (self.client.speaker_allowed and self.client.server_sound_send):
967 set_sensitive(sync, False)
968 sync.set_tooltip_text("video-sync requires speaker forwarding")
969 return
970 set_sensitive(sync, True)
971 self.client.after_handshake(set_avsyncmenu)
972 return sync
975 def make_webcammenuitem(self):
976 webcam = self.menuitem("Webcam", "webcam.png")
977 if not self.client.webcam_forwarding:
978 webcam.set_tooltip_text("Webcam forwarding is disabled")
979 set_sensitive(webcam, False)
980 return webcam
981 from xpra.platform.webcam import (
982 get_all_video_devices,
983 get_virtual_video_devices,
984 add_video_device_change_callback,
985 )
986 #TODO: register remove_video_device_change_callback for cleanup
987 menu = Gtk.Menu()
988 #so we can toggle the menu items without causing yet more events and infinite loops:
989 menu.ignore_events = False
990 def deviceitem(label, cb, device_no=0):
991 c = Gtk.CheckMenuItem(label=label)
992 c.set_draw_as_radio(True)
993 c.set_active(get_active_device_no()==device_no)
994 c.device_no = device_no
995 def activate_cb(item, *_args):
996 webcamlog("activate_cb(%s, %s) ignore_events=%s", item, menu, menu.ignore_events)
997 if not menu.ignore_events:
998 try:
999 menu.ignore_events = True
1000 ensure_item_selected(menu, item)
1001 cb(device_no)
1002 finally:
1003 menu.ignore_events = False
1004 c.connect("toggled", activate_cb, menu)
1005 return c
1006 def start_webcam(device_no=0):
1007 webcamlog("start_webcam(%s)", device_no)
1008 self.client.do_start_sending_webcam(device_no)
1009 def stop_webcam(device_no=0):
1010 webcamlog("stop_webcam(%s)", device_no)
1011 self.client.stop_sending_webcam()
1013 def get_active_device_no():
1014 if self.client.webcam_device is None:
1015 return -1
1016 return self.client.webcam_device_no
1018 def populate_webcam_menu():
1019 menu.ignore_events = True
1020 webcamlog("populate_webcam_menu()")
1021 for x in menu.get_children():
1022 menu.remove(x)
1023 all_video_devices = get_all_video_devices() #pylint: disable=assignment-from-none
1024 off_label = "Off"
1025 if all_video_devices is None:
1026 #None means that this platform cannot give us the device names,
1027 #so we just use a single "On" menu item and hope for the best
1028 on = deviceitem("On", start_webcam)
1029 menu.append(on)
1030 else:
1031 on = None
1032 virt_devices = get_virtual_video_devices()
1033 non_virtual = dict((k,v) for k,v in all_video_devices.items() if k not in virt_devices)
1034 for device_no,info in non_virtual.items():
1035 label = bytestostr(info.get("card", info.get("device", str(device_no))))
1036 item = deviceitem(label, start_webcam, device_no)
1037 menu.append(item)
1038 if not non_virtual:
1039 off_label = "No devices found"
1040 off = deviceitem(off_label, stop_webcam, -1)
1041 set_sensitive(off, off_label=="Off")
1042 menu.append(off)
1043 menu.show_all()
1044 menu.ignore_events = False
1045 populate_webcam_menu()
1047 def video_devices_changed(added=None, device=None):
1048 if added is not None and device:
1049 log.info("video device %s: %s", ["removed", "added"][added], device)
1050 else:
1051 log("video_devices_changed")
1052 #this callback runs in another thread,
1053 #and we want to wait for the devices to settle
1054 #so that the file permissions are correct when we try to access it:
1055 GLib.timeout_add(1000, populate_webcam_menu)
1056 add_video_device_change_callback(video_devices_changed)
1058 webcam.set_submenu(menu)
1059 def webcam_changed(*args):
1060 webcamlog("webcam_changed%s webcam_device=%s", args, self.client.webcam_device)
1061 if not self.client.webcam_forwarding:
1062 set_sensitive(webcam, False)
1063 webcam.set_tooltip_text("Webcam forwarding is disabled")
1064 return
1065 if self.client.server_virtual_video_devices<=0 or not self.client.server_webcam:
1066 set_sensitive(webcam, False)
1067 webcam.set_tooltip_text("Server does not support webcam forwarding")
1068 return
1069 webcam.set_tooltip_text("")
1070 set_sensitive(webcam, True)
1071 webcamlog("webcam_changed%s active device no=%s", args, get_active_device_no())
1072 menu.ignore_events = True
1073 for x in menu.get_children():
1074 x.set_active(x.device_no==get_active_device_no())
1075 menu.ignore_events = False
1076 self.client.connect("webcam-changed", webcam_changed)
1077 set_sensitive(webcam, False)
1078 self.client.after_handshake(webcam_changed)
1079 self.client.on_server_setting_changed("webcam", webcam_changed)
1080 return webcam
1083 def make_keyboardmenuitem(self):
1084 keyboard_menu_item = self.handshake_menuitem("Keyboard", "keyboard.png")
1085 menu = Gtk.Menu()
1086 keyboard_menu_item.set_submenu(menu)
1087 menu.append(self.make_keyboardsyncmenuitem())
1088 menu.append(self.make_shortcutsmenuitem())
1089 menu.append(self.make_viewshortcutsmenuitem())
1090 menu.append(self.make_layoutsmenuitem())
1091 keyboard_menu_item.show_all()
1092 return keyboard_menu_item
1094 def make_layoutsmenuitem(self):
1095 keyboard = self.menuitem("Layout", "keyboard.png", "Select your keyboard layout", None)
1096 set_sensitive(keyboard, False)
1097 self.layout_submenu = Gtk.Menu()
1098 keyboard.set_submenu(self.layout_submenu)
1099 def kbitem(title, layout, variant, active=False):
1100 def set_layout(item):
1101 """ this callback updates the client (and server) if needed """
1102 ensure_item_selected(self.layout_submenu, item)
1103 layout = item.keyboard_layout
1104 variant = item.keyboard_variant
1105 kh = self.client.keyboard_helper
1106 kh.locked = layout!="Auto"
1107 if layout!=kh.layout_option or variant!=kh.variant_option:
1108 if layout=="Auto":
1109 #re-detect everything:
1110 msg = "keyboard automatic mode"
1111 else:
1112 #use layout specified and send it:
1113 kh.layout_option = layout
1114 kh.variant_option = variant
1115 msg = "new keyboard layout selected"
1116 kh.update()
1117 kh.send_layout()
1118 kh.send_keymap()
1119 log.info("%s: %s", msg, kh.layout_str())
1120 l = self.checkitem(title, set_layout, active)
1121 l.set_draw_as_radio(True)
1122 l.keyboard_layout = layout
1123 l.keyboard_variant = variant
1124 return l
1125 def keysort(key):
1126 c,l = key
1127 return c.lower()+l.lower()
1128 layout, layouts, variant, variants, _ = self.client.keyboard_helper.get_layout_spec()
1129 layout = bytestostr(layout)
1130 layouts = tuple(bytestostr(x) for x in layouts)
1131 variant = bytestostr(variant or b"")
1132 variants = tuple(bytestostr(x) for x in variants)
1133 log("make_layoutsmenuitem() layout=%s, layouts=%s, variant=%s, variants=%s",
1134 layout, layouts, variant, variants)
1135 full_layout_list = False
1136 if len(layouts)>1:
1137 log("keyboard layouts: %s", ",".join(bytestostr(x) for x in layouts))
1138 #log after removing dupes:
1139 def uniq(seq):
1140 seen = set()
1141 return [x for x in seq if not (x in seen or seen.add(x))]
1142 log("keyboard layouts: %s", ",".join(bytestostr(x) for x in uniq(layouts)))
1143 auto = kbitem("Auto", "Auto", "", True)
1144 self.layout_submenu.append(auto)
1145 if layout:
1146 self.layout_submenu.append(kbitem("%s" % layout, layout, ""))
1147 if variants:
1148 for v in variants:
1149 self.layout_submenu.append(kbitem("%s - %s" % (layout, v), layout, v))
1150 for l in uniq(layouts):
1151 if l!=layout:
1152 self.layout_submenu.append(kbitem("%s" % l, l, ""))
1153 elif layout and variants and len(variants)>1:
1154 #just show all the variants to choose from this layout
1155 default = kbitem("%s - Default" % layout, layout, "", True)
1156 self.layout_submenu.append(default)
1157 for v in variants:
1158 self.layout_submenu.append(kbitem("%s - %s" % (layout, v), layout, v))
1159 else:
1160 full_layout_list = True
1161 from xpra.keyboard.layouts import X11_LAYOUTS
1162 #show all options to choose from:
1163 sorted_keys = list(X11_LAYOUTS.keys())
1164 sorted_keys.sort(key=keysort)
1165 for key in sorted_keys:
1166 country,language = key
1167 layout,variants = X11_LAYOUTS.get(key)
1168 name = "%s - %s" % (country, language)
1169 if len(variants)>1:
1170 #sub-menu for each variant:
1171 variant = self.menuitem(name, tooltip=layout)
1172 variant_submenu = Gtk.Menu()
1173 variant.set_submenu(variant_submenu)
1174 self.layout_submenu.append(variant)
1175 variant_submenu.append(kbitem("%s - Default" % layout, layout, None))
1176 for v in variants:
1177 variant_submenu.append(kbitem("%s - %s" % (layout, v), layout, v))
1178 else:
1179 #no variants:
1180 self.layout_submenu.append(kbitem(name, layout, None))
1181 keyboard_helper = self.client.keyboard_helper
1182 def set_layout_enabled(*args):
1183 log("set_layout_enabled%s full_layout_list=%s",
1184 args, full_layout_list)
1185 log("set_layout_enabled%s layout=%s, print=%s, query=%s",
1186 args, keyboard_helper.xkbmap_layout, keyboard_helper.xkbmap_print, keyboard_helper.xkbmap_query)
1187 if full_layout_list and \
1188 (keyboard_helper.xkbmap_layout or keyboard_helper.xkbmap_print or keyboard_helper.xkbmap_query):
1189 #we have detected a layout
1190 #so no need to show the user the huge layout list
1191 keyboard.hide()
1192 return
1193 set_sensitive(keyboard, True)
1194 self.client.after_handshake(set_layout_enabled)
1195 return keyboard
1198 def make_windowsmenuitem(self):
1199 windows_menu_item = self.handshake_menuitem("Windows", "windows.png")
1200 menu = Gtk.Menu()
1201 windows_menu_item.set_submenu(menu)
1202 menu.append(self.make_raisewindowsmenuitem())
1203 menu.append(self.make_minimizewindowsmenuitem())
1204 menu.append(self.make_refreshmenuitem())
1205 menu.append(self.make_reinitmenuitem())
1206 windows_menu_item.show_all()
1207 return windows_menu_item
1209 def make_refreshmenuitem(self):
1210 def force_refresh(*_args):
1211 log("force refresh")
1212 self.client.send_refresh_all()
1213 self.client.reinit_window_icons()
1214 return self.handshake_menuitem("Refresh", "retry.png", None, force_refresh)
1216 def make_reinitmenuitem(self):
1217 def force_reinit(*_args):
1218 log("force reinit")
1219 self.client.reinit_windows()
1220 self.client.reinit_window_icons()
1221 return self.handshake_menuitem("Re-initialize", "reinitialize.png", None, force_reinit)
1223 def make_raisewindowsmenuitem(self):
1224 def raise_windows(*_args):
1225 for win in self.client._window_to_id:
1226 if not win.is_OR():
1227 win.deiconify()
1228 win.present()
1229 return self.handshake_menuitem("Raise Windows", "raise.png", None, raise_windows)
1231 def make_minimizewindowsmenuitem(self):
1232 def minimize_windows(*_args):
1233 for win in self.client._window_to_id:
1234 if not win.is_OR():
1235 win.iconify()
1236 return self.handshake_menuitem("Minimize Windows", "minimize.png", None, minimize_windows)
1239 def make_servermenuitem(self):
1240 server_menu_item = self.handshake_menuitem("Server", "server.png")
1241 menu = Gtk.Menu()
1242 server_menu_item.set_submenu(menu)
1243 if RUNCOMMAND_MENU:
1244 menu.append(self.make_runcommandmenuitem())
1245 if SHOW_SERVER_COMMANDS:
1246 menu.append(self.make_servercommandsmenuitem())
1247 if SHOW_TRANSFERS:
1248 menu.append(self.make_servertransfersmenuitem())
1249 if SHOW_UPLOAD:
1250 menu.append(self.make_uploadmenuitem())
1251 if SHOW_DOWNLOAD:
1252 menu.append(self.make_downloadmenuitem())
1253 if SHOW_SERVER_LOG:
1254 menu.append(self.make_serverlogmenuitem())
1255 if SHOW_SHUTDOWN:
1256 menu.append(self.make_shutdownmenuitem())
1257 server_menu_item.show_all()
1258 return server_menu_item
1260 def make_servercommandsmenuitem(self):
1261 self.servercommands = self.menuitem("Server Commands", "list.png",
1262 "Commands running on the server",
1263 self.client.show_server_commands)
1264 def enable_servercommands(*args):
1265 log("enable_servercommands%s server-commands-info=%s", args, self.client.server_commands_info)
1266 set_sensitive(self.servercommands, self.client.server_commands_info)
1267 if not self.client.server_commands_info:
1268 self.servercommands.set_tooltip_text("Not supported by the server")
1269 else:
1270 self.servercommands.set_tooltip_text("")
1271 self.client.after_handshake(enable_servercommands)
1272 return self.servercommands
1274 def make_runcommandmenuitem(self):
1275 self.startnewcommand = self.menuitem("Run Command", "forward.png",
1276 "Run a new command on the server",
1277 self.client.show_start_new_command)
1278 def enable_start_new_command(*args):
1279 log("enable_start_new_command%s start_new_command=%s", args, self.client.server_start_new_commands)
1280 set_sensitive(self.startnewcommand, self.client.server_start_new_commands)
1281 if not self.client.server_start_new_commands:
1282 self.startnewcommand.set_tooltip_text("Not supported or enabled on the server")
1283 else:
1284 self.startnewcommand.set_tooltip_text("")
1285 self.client.after_handshake(enable_start_new_command)
1286 self.client.on_server_setting_changed("start-new-commands", enable_start_new_command)
1287 return self.startnewcommand
1289 def make_servertransfersmenuitem(self):
1290 self.transfers = self.menuitem("Transfers", "transfer.png",
1291 "Files and URLs forwarding",
1292 self.client.show_ask_data_dialog)
1293 def enable_transfers(*args):
1294 log("enable_transfers%s ask=%s", args, ())
1295 has_ask = (self.client.remote_file_transfer_ask or
1296 self.client.remote_printing_ask or
1297 self.client.remote_open_files_ask or
1298 self.client.remote_open_url_ask)
1299 set_sensitive(self.transfers, has_ask)
1300 self.client.after_handshake(enable_transfers)
1301 return self.transfers
1303 def make_uploadmenuitem(self):
1304 self.upload = self.menuitem("Upload File", "upload.png", cb=self.client.show_file_upload)
1305 def enable_upload(*args):
1306 log("enable_upload%s server_file_transfer=%s", args, self.client.remote_file_transfer)
1307 set_sensitive(self.upload, self.client.remote_file_transfer)
1308 if not self.client.remote_file_transfer:
1309 self.upload.set_tooltip_text("Not supported by the server")
1310 else:
1311 self.upload.set_tooltip_text("Send a file to the server")
1312 self.client.after_handshake(enable_upload)
1313 return self.upload
1315 def make_downloadmenuitem(self):
1316 self.download = self.menuitem("Download File", "download.png", cb=self.client.send_download_request)
1317 def enable_download(*args):
1318 log("enable_download%s server_file_transfer=%s, server_start_new_commands=%s, subcommands=%s",
1319 args, self.client.remote_file_transfer, self.client.server_start_new_commands, self.client._remote_subcommands)
1320 set_sensitive(self.download, self.client.remote_file_transfer)
1321 if not self.client.remote_file_transfer or not self.client.server_start_new_commands:
1322 self.download.set_tooltip_text("Not supported by the server")
1323 elif "send-file" not in self.client._remote_subcommands:
1324 self.download.set_tooltip_text("'send-file' subcommand is not supported by the server")
1325 else:
1326 self.download.set_tooltip_text("Send a file to the server")
1327 self.client.after_handshake(enable_download)
1328 return self.download
1331 def make_serverlogmenuitem(self):
1332 def download_server_log(*_args):
1333 self.client.download_server_log()
1334 self.download_log = self.menuitem("Download Server Log", "list.png", cb=download_server_log)
1335 def enable_download(*args):
1336 log("enable_download%s server_file_transfer=%s", args, self.client.remote_file_transfer)
1337 set_sensitive(self.download_log, self.client.remote_file_transfer and bool(self.client._remote_server_log))
1338 if not self.client.remote_file_transfer:
1339 self.download_log.set_tooltip_text("Not supported by the server")
1340 elif not self.client._remote_server_log:
1341 self.download_log.set_tooltip_text("Server does not expose its log-file")
1342 else:
1343 self.download_log.set_tooltip_text("Download the server log")
1344 self.client.after_handshake(enable_download)
1345 return self.download_log
1348 def make_shutdownmenuitem(self):
1349 def ask_shutdown_confirm(*_args):
1350 messages = []
1351 #uri = self.client.display_desc.get("display_name")
1352 #if uri:
1353 # messages.append("URI: %s" % uri)
1354 session_name = self.client.session_name or self.client.server_session_name
1355 if session_name:
1356 messages.append("Shutting down the session '%s' may result in data loss," % session_name)
1357 else:
1358 messages.append("Shutting down this session may result in data loss,")
1359 messages.append("are you sure you want to proceed?")
1360 dialog = Gtk.MessageDialog (None, 0, Gtk.MessageType.QUESTION,
1361 Gtk.ButtonsType.NONE,
1362 "\n".join(messages))
1363 dialog.add_button(Gtk.STOCK_CANCEL, 0)
1364 SHUTDOWN = 1
1365 dialog.add_button("Shutdown", SHUTDOWN)
1366 response = dialog.run()
1367 dialog.destroy()
1368 if response == SHUTDOWN:
1369 self.client.send_shutdown_server()
1370 self.shutdown = self.menuitem("Shutdown Server", "shutdown.png", cb=ask_shutdown_confirm)
1371 def enable_shutdown(*args):
1372 log("enable_shutdown%s can_shutdown_server=%s", args, self.client.server_client_shutdown)
1373 set_sensitive(self.shutdown, self.client.server_client_shutdown)
1374 if not self.client.server_client_shutdown:
1375 self.shutdown.set_tooltip_text("Disabled by the server")
1376 else:
1377 self.shutdown.set_tooltip_text("Shutdown this server session")
1378 self.client.after_handshake(enable_shutdown)
1379 self.client.on_server_setting_changed("client-shutdown", enable_shutdown)
1380 return self.shutdown
1382 def make_startmenuitem(self):
1383 start_menu_item = self.handshake_menuitem("Start", "start.png")
1384 start_menu_item.show()
1385 def update_menu_data():
1386 if not self.client.server_start_new_commands:
1387 set_sensitive(start_menu_item, False)
1388 start_menu_item.set_tooltip_text("This server does not support starting new commands")
1389 return
1390 if not self.client.server_xdg_menu:
1391 set_sensitive(start_menu_item, False)
1392 start_menu_item.set_tooltip_text("This server does not provide start menu data")
1393 return
1394 set_sensitive(start_menu_item, True)
1395 menu = self.build_start_menu()
1396 start_menu_item.set_submenu(menu)
1397 start_menu_item.set_tooltip_text(None)
1398 def start_menu_init():
1399 update_menu_data()
1400 def on_xdg_menu_changed(setting, value):
1401 log("on_xdg_menu_changed(%s, %s)", setting, repr_ellipsized(str(value)))
1402 update_menu_data()
1403 self.client.on_server_setting_changed("xdg-menu", on_xdg_menu_changed)
1404 self.client.after_handshake(start_menu_init)
1405 return start_menu_item
1407 def build_start_menu(self):
1408 menu = Gtk.Menu()
1409 execlog("build_start_menu() %i menu items", len(self.client.server_xdg_menu))
1410 execlog("self.client.server_xdg_menu=%s", ellipsizer(self.client.server_xdg_menu))
1411 for cat, category_props in sorted(self.client.server_xdg_menu.items()):
1412 category = cat.decode("utf-8")
1413 execlog(" * category: %s", category)
1414 #log("category_props(%s)=%s", category, category_props)
1415 if not isinstance(category_props, dict):
1416 execlog("category properties is not a dict: %s", type(category_props))
1417 continue
1418 cp = typedict(category_props)
1419 execlog(" category_props(%s)=%s", category, ellipsizer(category_props))
1420 entries = cp.dictget("Entries")
1421 if not entries:
1422 execlog(" no entries for category '%s'", category)
1423 continue
1424 icondata = cp.bytesget("IconData")
1425 category_menu_item = self.start_menuitem(category, icondata)
1426 cat_menu = Gtk.Menu()
1427 category_menu_item.set_submenu(cat_menu)
1428 menu.append(category_menu_item)
1429 for an, cp in sorted(entries.items()):
1430 app_name = an.decode("utf-8")
1431 command_props = typedict(cp)
1432 execlog(" - app_name=%s", app_name)
1433 app_menu_item = self.make_applaunch_menu_item(app_name, command_props)
1434 cat_menu.append(app_menu_item)
1435 menu.show_all()
1436 return menu
1438 def get_appimage(self, app_name, icondata=None):
1439 pixbuf = None
1440 if app_name and not icondata:
1441 #try to load from our icons:
1442 icon_filename = os.path.join(get_icon_dir(), "%s.png" % app_name.decode("utf-8").lower())
1443 if os.path.exists(icon_filename):
1444 pixbuf = GdkPixbuf.Pixbuf.new_from_file(icon_filename)
1445 if not pixbuf and icondata:
1446 #gtk pixbuf loader:
1447 try:
1448 loader = GdkPixbuf.PixbufLoader()
1449 loader.write(icondata)
1450 loader.close()
1451 pixbuf = loader.get_pixbuf()
1452 except Exception as e:
1453 log("pixbuf loader failed", exc_info=True)
1454 log.error("Error: failed to load icon data for '%s':", bytestostr(app_name))
1455 log.error(" %s", e)
1456 log.error(" data=%s", repr_ellipsized(icondata))
1457 if not pixbuf and icondata:
1458 #let's try pillow:
1459 try:
1460 from xpra.codecs.pillow.decoder import open_only
1461 img = open_only(icondata)
1462 has_alpha = img.mode=="RGBA"
1463 width, height = img.size
1464 rowstride = width * (3+int(has_alpha))
1465 pixbuf = get_pixbuf_from_data(img.tobytes(), has_alpha, width, height, rowstride)
1466 return scaled_image(pixbuf, icon_size=self.menu_icon_size)
1467 except Exception:
1468 log.error("Error: failed to load icon data for %s", bytestostr(app_name), exc_info=True)
1469 log.error(" data=%s", repr_ellipsized(icondata))
1470 if pixbuf:
1471 return scaled_image(pixbuf, icon_size=self.menu_icon_size)
1472 return None
1474 def start_menuitem(self, title, icondata=None):
1475 smi = self.handshake_menuitem(title)
1476 if icondata:
1477 image = self.get_appimage(title, icondata)
1478 if image:
1479 smi.set_image(image)
1480 return smi
1482 def make_applaunch_menu_item(self, app_name : str, command_props : typedict):
1483 icondata = command_props.bytesget("IconData")
1484 app_menu_item = self.start_menuitem(app_name, icondata)
1485 def app_launch(*args):
1486 log("app_launch(%s) command_props=%s", args, command_props)
1487 command = command_props.bytesget("command")
1488 try:
1489 command = re.sub(b'\\%[fFuU]', b'', command)
1490 except Exception:
1491 log("re substitution failed", exc_info=True)
1492 command = command.split(b"%", 1)[0]
1493 log("command=%s", command)
1494 if command:
1495 self.client.send_start_command(app_name, command, False, self.client.server_sharing)
1496 app_menu_item.connect("activate", app_launch)
1497 return app_menu_item
1500 def make_disconnectmenuitem(self):
1501 def menu_quit(*_args):
1502 self.client.disconnect_and_quit(EXIT_OK, CLIENT_EXIT)
1503 return self.handshake_menuitem("Disconnect", "quit.png", None, menu_quit)
1506 def make_closemenuitem(self):
1507 return self.menuitem("Close Menu", "close.png", None, self.close_menu)