Hide keyboard shortcuts

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. 

6 

7import os 

8import re 

9from gi.repository import GLib, Gtk, GdkPixbuf 

10 

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 

31 

32log = Logger("menu") 

33execlog = Logger("exec") 

34clipboardlog = Logger("menu", "clipboard") 

35webcamlog = Logger("menu", "webcam") 

36avsynclog = Logger("menu", "av-sync") 

37bandwidthlog = Logger("bandwidth", "network") 

38 

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) 

56 

57 

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() 

67 

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 

81 

82 

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" 

93 

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) 

101 

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) 

110 

111 

112class GTKTrayMenuBase(MenuHelper): 

113 

114 def setup_menu(self): 

115 return self.do_setup_menu(SHOW_CLOSE) 

116 

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) 

134 

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 

160 

161 

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 

175 

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 

183 

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()) 

198 

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 

225 

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 

251 

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 

270 

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 

297 

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 

323 

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 

346 

347 

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) 

368 

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]) 

381 

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 

402 

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 "") 

409 

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") 

421 

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 

462 

463 

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 

496 

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 

507 

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) 

512 

513 

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 

527 

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 

541 

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 

555 

556 def make_bandwidthlimitmenuitem(self): 

557 bandwidth_limit_menu_item = self.menuitem("Bandwidth Limit", "bandwidth_limit.png") 

558 menu = Gtk.Menu() 

559 menuitems = {} 

560 

561 def bwitem(bwlimit): 

562 c = self.bwitem(menu, bwlimit) 

563 menuitems[bwlimit] = c 

564 return c 

565 

566 menu.append(bwitem(0)) 

567 bandwidth_limit_menu_item.set_submenu(menu) 

568 bandwidth_limit_menu_item.show_all() 

569 

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) 

582 

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)) 

590 

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 

628 

629 

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 

656 

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 

670 

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() 

678 

679 

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 

685 

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 

705 

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 

729 

730 

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 

739 

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 

746 

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() 

761 

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) 

779 

780 

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 

789 

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 

794 

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() 

809 

810 

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") 

822 

823 

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 

833 

834 

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 

859 

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 

885 

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() 

894 

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 

927 

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 

973 

974 

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() 

1012 

1013 def get_active_device_no(): 

1014 if self.client.webcam_device is None: 

1015 return -1 

1016 return self.client.webcam_device_no 

1017 

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() 

1046 

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) 

1057 

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 

1081 

1082 

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 

1093 

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 

1196 

1197 

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 

1208 

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) 

1215 

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) 

1222 

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) 

1230 

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) 

1237 

1238 

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 

1259 

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 

1273 

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 

1288 

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 

1302 

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 

1314 

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 

1329 

1330 

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 

1346 

1347 

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 

1381 

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 

1406 

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 

1437 

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 

1473 

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 

1481 

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 

1498 

1499 

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) 

1504 

1505 

1506 def make_closemenuitem(self): 

1507 return self.menuitem("Close Menu", "close.png", None, self.close_menu)