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# This file is part of Xpra. 

2# Copyright (C) 2011 Serviware (Arthur Huillet, <ahuillet@serviware.com>) 

3# Copyright (C) 2010-2020 Antoine Martin <antoine@xpra.org> 

4# Copyright (C) 2008, 2010 Nathaniel Smith <njs@pobox.com> 

5# Xpra is released under the terms of the GNU GPL v2, or, at your option, any 

6# later version. See the file COPYING for details. 

7 

8import os 

9import weakref 

10from gi.repository import Gtk, Gdk, GdkPixbuf 

11 

12from xpra.client.gtk_base.gtk_client_window_base import HAS_X11_BINDINGS, XSHAPE 

13from xpra.util import ( 

14 updict, pver, iround, flatten_dict, 

15 envbool, envint, repr_ellipsized, csv, first_time, typedict, 

16 DEFAULT_METADATA_SUPPORTED, XPRA_OPENGL_NOTIFICATION_ID, 

17 ) 

18from xpra.os_util import ( 

19 bytestostr, strtobytes, hexstr, monotonic_time, load_binary_file, 

20 WIN32, OSX, POSIX, is_Wayland, 

21 ) 

22from xpra.simple_stats import std_unit 

23from xpra.exit_codes import EXIT_PASSWORD_REQUIRED 

24from xpra.scripts.config import TRUE_OPTIONS, FALSE_OPTIONS 

25from xpra.gtk_common.cursor_names import cursor_types 

26from xpra.gtk_common.gtk_util import ( 

27 get_gtk_version_info, scaled_image, get_default_cursor, color_parse, 

28 get_pixbuf_from_data, 

29 get_default_root_window, get_root_size, 

30 get_screen_sizes, GDKWindow, 

31 GRAB_STATUS_STRING, 

32 ) 

33from xpra.gtk_common.gobject_util import no_arg_signal 

34from xpra.client.ui_client_base import UIXpraClient 

35from xpra.client.gobject_client_base import GObjectXpraClient 

36from xpra.client.gtk_base.gtk_keyboard_helper import GTKKeyboardHelper 

37from xpra.client.gtk_base.css_overrides import inject_css_overrides 

38from xpra.client.mixins.window_manager import WindowClient 

39from xpra.platform.paths import get_icon_filename 

40from xpra.platform.gui import force_focus 

41from xpra.platform.gui import ( 

42 get_window_frame_sizes, get_window_frame_size, 

43 system_bell, get_wm_name, get_fixed_cursor_size, 

44 ) 

45from xpra.log import Logger 

46 

47log = Logger("gtk", "client") 

48opengllog = Logger("gtk", "opengl") 

49cursorlog = Logger("gtk", "client", "cursor") 

50framelog = Logger("gtk", "client", "frame") 

51screenlog = Logger("gtk", "client", "screen") 

52filelog = Logger("gtk", "client", "file") 

53clipboardlog = Logger("gtk", "client", "clipboard") 

54notifylog = Logger("gtk", "notify") 

55grablog = Logger("client", "grab") 

56focuslog = Logger("client", "focus") 

57 

58missing_cursor_names = set() 

59 

60METADATA_SUPPORTED = os.environ.get("XPRA_METADATA_SUPPORTED") 

61#on win32, the named cursors work, but they are hard to see 

62#when using the Adwaita theme 

63USE_LOCAL_CURSORS = envbool("XPRA_USE_LOCAL_CURSORS", not WIN32 and not is_Wayland()) 

64EXPORT_ICON_DATA = envbool("XPRA_EXPORT_ICON_DATA", True) 

65SAVE_CURSORS = envbool("XPRA_SAVE_CURSORS", False) 

66CLIPBOARD_NOTIFY = envbool("XPRA_CLIPBOARD_NOTIFY", True) 

67OPENGL_MIN_SIZE = envint("XPRA_OPENGL_MIN_SIZE", 32) 

68NO_OPENGL_WINDOW_TYPES = os.environ.get("XPRA_NO_OPENGL_WINDOW_TYPES", 

69 "DOCK,TOOLBAR,MENU,UTILITY,SPLASH,DROPDOWN_MENU,POPUP_MENU,TOOLTIP,NOTIFICATION,COMBO,DND").split(",") 

70 

71inject_css_overrides() 

72 

73 

74class GTKXpraClient(GObjectXpraClient, UIXpraClient): 

75 __gsignals__ = {} 

76 #add signals from super classes (all no-arg signals) 

77 for signal_name in UIXpraClient.__signals__: 

78 __gsignals__[signal_name] = no_arg_signal 

79 

80 ClientWindowClass = None 

81 GLClientWindowClass = None 

82 

83 def __init__(self): 

84 GObjectXpraClient.__init__(self) 

85 UIXpraClient.__init__(self) 

86 self.shortcuts_info = None 

87 self.session_info = None 

88 self.bug_report = None 

89 self.file_size_dialog = None 

90 self.file_ask_dialog = None 

91 self.file_dialog = None 

92 self.start_new_command = None 

93 self.server_commands = None 

94 self.keyboard_helper_class = GTKKeyboardHelper 

95 self.border = None 

96 self.data_send_requests = {} 

97 #clipboard bits: 

98 self.clipboard_notification_timer = None 

99 self.last_clipboard_notification = 0 

100 #opengl bits: 

101 self.client_supports_opengl = False 

102 self.opengl_force = False 

103 self.opengl_enabled = False 

104 self.opengl_props = {} 

105 self.gl_max_viewport_dims = 0, 0 

106 self.gl_texture_size_limit = 0 

107 self._cursors = weakref.WeakKeyDictionary() 

108 #frame request hidden window: 

109 self.frame_request_window = None 

110 #group leader bits: 

111 self._ref_to_group_leader = {} 

112 self._group_leader_wids = {} 

113 try: 

114 self.connect("scaling-changed", self.reset_windows_cursors) 

115 except TypeError: 

116 log("no 'scaling-changed' signal") 

117 #detect when the UI thread isn't responding: 

118 self.UI_watcher = None 

119 self.connect("first-ui-received", self.start_UI_watcher) 

120 

121 

122 def init(self, opts): 

123 GObjectXpraClient.init(self, opts) 

124 UIXpraClient.init(self, opts) 

125 

126 

127 def setup_frame_request_windows(self): 

128 #query the window manager to get the frame size: 

129 from xpra.gtk_common.error import xsync 

130 from xpra.x11.gtk_x11.send_wm import send_wm_request_frame_extents 

131 self.frame_request_window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL) 

132 self.frame_request_window.set_title("Xpra-FRAME_EXTENTS") 

133 root = self.get_root_window() 

134 self.frame_request_window.realize() 

135 with xsync: 

136 win = self.frame_request_window.get_window() 

137 framelog("setup_frame_request_windows() window=%#x", win.get_xid()) 

138 send_wm_request_frame_extents(root, win) 

139 

140 def run(self): 

141 log("run() HAS_X11_BINDINGS=%s", HAS_X11_BINDINGS) 

142 if HAS_X11_BINDINGS: 

143 self.setup_frame_request_windows() 

144 UIXpraClient.run(self) 

145 self.gtk_main() 

146 log("GTKXpraClient.run_main_loop() main loop ended, returning exit_code=%s", self.exit_code) 

147 return self.exit_code 

148 

149 def gtk_main(self): 

150 log("GTKXpraClient.gtk_main() calling %s", Gtk.main) 

151 Gtk.main() 

152 log("GTKXpraClient.gtk_main() ended") 

153 

154 

155 def quit(self, exit_code=0): 

156 log("GTKXpraClient.quit(%s) current exit_code=%s", exit_code, self.exit_code) 

157 if self.exit_code is None: 

158 self.exit_code = exit_code 

159 if Gtk.main_level()>0: 

160 #if for some reason cleanup() hangs, maybe this will fire... 

161 self.timeout_add(4*1000, self.exit) 

162 #try harder!: 

163 self.timeout_add(5*1000, self.force_quit) 

164 self.cleanup() 

165 log("GTKXpraClient.quit(%s) cleanup done, main_level=%s", 

166 exit_code, Gtk.main_level()) 

167 if Gtk.main_level()>0: 

168 log("GTKXpraClient.quit(%s) main loop at level %s, calling gtk quit via timeout", 

169 exit_code, Gtk.main_level()) 

170 self.timeout_add(500, self.exit) 

171 

172 def force_quit(self): 

173 from xpra.os_util import force_quit 

174 log("GTKXpraClient.force_quit() calling %s", force_quit) 

175 force_quit() 

176 

177 def exit(self): 

178 self.show_progress(100, "terminating") 

179 log("GTKXpraClient.exit() calling %s", Gtk.main_quit) 

180 Gtk.main_quit() 

181 

182 def cleanup(self): 

183 log("GTKXpraClient.cleanup()") 

184 if self.shortcuts_info: 

185 self.shortcuts_info.destroy() 

186 self.shortcuts_info = None 

187 if self.session_info: 

188 self.session_info.destroy() 

189 self.session_info = None 

190 if self.bug_report: 

191 self.bug_report.destroy() 

192 self.bug_report = None 

193 self.close_file_size_warning() 

194 self.close_file_upload_dialog() 

195 self.close_ask_data_dialog() 

196 self.cancel_clipboard_notification_timer() 

197 if self.start_new_command: 

198 self.start_new_command.destroy() 

199 self.start_new_command = None 

200 if self.server_commands: 

201 self.server_commands.destroy() 

202 self.server_commands = None 

203 uw = self.UI_watcher 

204 if uw: 

205 self.UI_watcher = None 

206 uw.stop() 

207 UIXpraClient.cleanup(self) 

208 

209 def start_UI_watcher(self, _client): 

210 from xpra.platform.ui_thread_watcher import get_UI_watcher 

211 self.UI_watcher = get_UI_watcher(self.timeout_add, self.source_remove) 

212 self.UI_watcher.start() 

213 #if server supports it, enable UI thread monitoring workaround when needed: 

214 def UI_resumed(): 

215 self.send("resume", True, tuple(self._id_to_window.keys())) 

216 #maybe the system was suspended? 

217 #so we may want to call WindowClient.resume() 

218 resume = getattr(self, "resume", None) 

219 if resume: 

220 resume() 

221 def UI_failed(): 

222 self.send("suspend", True, tuple(self._id_to_window.keys())) 

223 self.UI_watcher.add_resume_callback(UI_resumed) 

224 self.UI_watcher.add_fail_callback(UI_failed) 

225 

226 

227 def get_notifier_classes(self): 

228 #subclasses may add their toolkit specific variants 

229 #by overriding this method 

230 #use the native ones first: 

231 from xpra.client import mixin_features 

232 assert mixin_features.notifications 

233 from xpra.client.mixins.notifications import NotificationClient 

234 assert isinstance(self, NotificationClient) 

235 ncs = NotificationClient.get_notifier_classes(self) 

236 try: 

237 from xpra.gtk_common.gtk_notifier import GTK_Notifier 

238 ncs.append(GTK_Notifier) 

239 except Exception as e: 

240 notifylog("get_notifier_classes()", exc_info=True) 

241 notifylog.warn("Warning: cannot load GTK notifier:") 

242 notifylog.warn(" %s", e) 

243 return ncs 

244 

245 

246 def _process_startup_complete(self, packet): 

247 UIXpraClient._process_startup_complete(self, packet) 

248 Gdk.notify_startup_complete() 

249 

250 

251 def do_process_challenge_prompt(self, packet, prompt="password"): 

252 self.show_progress(100, "authentication") 

253 dialog = Gtk.Dialog("Server Authentication", 

254 None, 

255 Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT) 

256 dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT) 

257 dialog.add_button(Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT) 

258 def add(widget, padding=0): 

259 a = Gtk.Alignment() 

260 a.set(0.5, 0.5, 1, 1) 

261 a.add(widget) 

262 a.set_padding(padding, padding, padding, padding) 

263 dialog.vbox.pack_start(a) 

264 import gi 

265 gi.require_version("Pango", "1.0") 

266 from gi.repository import Pango 

267 title = Gtk.Label("Server Authentication") 

268 title.modify_font(Pango.FontDescription("sans 14")) 

269 add(title, 16) 

270 add(Gtk.Label(self.get_challenge_prompt(prompt)), 10) 

271 password_input = Gtk.Entry() 

272 password_input.set_max_length(255) 

273 password_input.set_width_chars(32) 

274 password_input.set_visibility(False) 

275 add(password_input, 10) 

276 dialog.vbox.show_all() 

277 dialog.password_input = password_input 

278 def handle_response(dialog, response): 

279 if OSX: 

280 from xpra.platform.darwin.gui import disable_focus_workaround 

281 disable_focus_workaround() 

282 password = dialog.password_input.get_text() 

283 dialog.hide() 

284 dialog.destroy() 

285 if response!=Gtk.ResponseType.ACCEPT or not password: 

286 self.quit(EXIT_PASSWORD_REQUIRED) 

287 return 

288 self.send_challenge_reply(packet, password) 

289 def password_activate(*_args): 

290 handle_response(dialog, Gtk.ResponseType.ACCEPT) 

291 password_input.connect("activate", password_activate) 

292 dialog.connect("response", handle_response) 

293 if OSX: 

294 from xpra.platform.darwin.gui import enable_focus_workaround 

295 enable_focus_workaround() 

296 dialog.show() 

297 return True 

298 

299 

300 def setup_connection(self, conn): 

301 conn = super().setup_connection(conn) 

302 #now that we have display_desc, parse the border again: 

303 self.parse_border(False) 

304 return conn 

305 

306 

307 def show_border_help(self): 

308 if not first_time("border-help"): 

309 return 

310 log.info(" border format: color[,size][:off]") 

311 log.info(" eg: red,10") 

312 log.info(" eg: ,5") 

313 log.info(" eg: auto,5") 

314 log.info(" eg: blue") 

315 

316 def parse_border(self, warn=True): 

317 enabled = not self.border_str.endswith(":off") 

318 parts = [x.strip() for x in self.border_str.replace(":off", "").split(",")] 

319 color_str = parts[0] 

320 if color_str.lower() in ("none", "no", "off", "0"): 

321 return 

322 if color_str.lower()=="help": 

323 self.show_border_help() 

324 return 

325 color_str = color_str.replace(":off", "") 

326 if color_str in ("auto", ""): 

327 from hashlib import md5, sha1 

328 try: 

329 if envbool("XPRA_NOMD5", False): 

330 raise ValueError("md5 explicitly disabled") 

331 m = md5() 

332 except ValueError: 

333 m = sha1() 

334 endpoint = self.display_desc.get("display_name") 

335 if endpoint: 

336 m.update(strtobytes(endpoint)) 

337 color_str = "#%s" % m.hexdigest()[:6] 

338 log("border color derived from %s: %s", endpoint, color_str) 

339 try: 

340 color = color_parse(color_str) 

341 assert color is not None 

342 except Exception as e: 

343 if warn: 

344 log.warn("Warning: invalid border color specified '%s'", color_str) 

345 if str(e): 

346 log.warn(" %s", e) 

347 self.show_border_help() 

348 color = color_parse("red") 

349 alpha = 0.6 

350 size = 4 

351 if len(parts)==2: 

352 size_str = parts[1] 

353 try: 

354 size = int(size_str) 

355 except Exception as e: 

356 if warn: 

357 log.warn("Warning: invalid border size specified '%s'", size_str) 

358 log.warn(" %s", e) 

359 self.show_border_help() 

360 if size<=0: 

361 log("border size is %s, disabling it", size) 

362 return 

363 if size>=45: 

364 log.warn("Warning: border size is too large: %s, clipping it", size) 

365 size = 45 

366 from xpra.client.window_border import WindowBorder 

367 self.border = WindowBorder(enabled, color.red/65536.0, color.green/65536.0, color.blue/65536.0, alpha, size) 

368 log("parse_border(%s)=%s", self.border_str, self.border) 

369 

370 

371 def show_server_commands(self, *_args): 

372 if not self.server_commands_info: 

373 log.warn("Warning: cannot show server commands") 

374 log.warn(" the feature is not available on the server") 

375 return 

376 if self.server_commands is None: 

377 from xpra.client.gtk_base.server_commands import getServerCommandsWindow 

378 self.server_commands = getServerCommandsWindow(self) 

379 self.server_commands.show() 

380 

381 def show_start_new_command(self, *args): 

382 if not self.server_start_new_commands: 

383 log.warn("Warning: cannot start new commands") 

384 log.warn(" the feature is currently disabled on the server") 

385 return 

386 log("show_start_new_command%s current start_new_command=%s, flag=%s", 

387 args, self.start_new_command, self.server_start_new_commands) 

388 if self.start_new_command is None: 

389 from xpra.client.gtk_base.start_new_command import getStartNewCommand 

390 def run_command_cb(command, sharing=True): 

391 self.send_start_command(command, command, False, sharing) 

392 self.start_new_command = getStartNewCommand(run_command_cb, 

393 self.server_sharing, 

394 self.server_xdg_menu) 

395 self.start_new_command.show() 

396 

397 

398 ################################ 

399 # file handling 

400 def ask_data_request(self, cb_answer, send_id, dtype, url, filesize, printit, openit): 

401 self.idle_add(self.do_ask_data_request, cb_answer, send_id, dtype, url, filesize, printit, openit) 

402 

403 def do_ask_data_request(self, cb_answer, send_id, dtype, url, filesize, printit, openit): 

404 from xpra.client.gtk_base.open_requests import getOpenRequestsWindow 

405 timeout = self.remote_file_ask_timeout 

406 def rec_answer(accept, newopenit=openit): 

407 from xpra.net.file_transfer import ACCEPT 

408 if int(accept)==ACCEPT: 

409 #record our response, so we will actually accept the file when the packets arrive: 

410 self.data_send_requests[send_id] = (dtype, url, printit, newopenit) 

411 cb_answer(accept) 

412 self.file_ask_dialog = getOpenRequestsWindow(self.show_file_upload, self.cancel_download) 

413 self.file_ask_dialog.add_request(rec_answer, send_id, dtype, url, filesize, printit, openit, timeout) 

414 self.file_ask_dialog.show() 

415 

416 def close_ask_data_dialog(self): 

417 fad = self.file_ask_dialog 

418 if fad: 

419 self.file_ask_dialog = None 

420 fad.destroy() 

421 

422 def show_ask_data_dialog(self, *_args): 

423 from xpra.client.gtk_base.open_requests import getOpenRequestsWindow 

424 self.file_ask_dialog = getOpenRequestsWindow(self.show_file_upload, self.cancel_download) 

425 self.file_ask_dialog.show() 

426 

427 def transfer_progress_update(self, send=True, transfer_id=0, elapsed=0, position=0, total=0, error=None): 

428 fad = self.file_ask_dialog 

429 if fad: 

430 self.idle_add(fad.transfer_progress_update, send, transfer_id, elapsed, position, total, error) 

431 

432 

433 def accept_data(self, send_id, dtype, url, printit, openit): 

434 #check if we have accepted this file via the GUI: 

435 r = self.data_send_requests.pop(send_id, None) 

436 if not r: 

437 filelog("accept_data: data send request %s not found", send_id) 

438 from xpra.net.file_transfer import FileTransferHandler 

439 return FileTransferHandler.accept_data(self, send_id, dtype, url, printit, openit) 

440 edtype = r[0] 

441 eurl = r[1] 

442 if edtype!=dtype or eurl!=url: 

443 filelog.warn("Warning: the file attributes are different") 

444 filelog.warn(" from the ones that were used to accept the transfer") 

445 s = bytestostr 

446 if edtype!=dtype: 

447 filelog.warn(" expected data type '%s' but got '%s'", s(edtype), s(dtype)) 

448 if eurl!=url: 

449 filelog.warn(" expected url '%s',", s(eurl)) 

450 filelog.warn(" but got url '%s'", s(url)) 

451 return None 

452 #return the printit and openit flag we got from the UI: 

453 return (r[2], r[3]) 

454 

455 def file_size_warning(self, action, location, basefilename, filesize, limit): 

456 if self.file_size_dialog: 

457 #close previous warning 

458 self.file_size_dialog.destroy() 

459 self.file_size_dialog = None 

460 parent = None 

461 msgs = ( 

462 "Warning: cannot %s the file '%s'" % (action, basefilename), 

463 "this file is too large: %sB" % std_unit(filesize), 

464 "the %s file size limit is %iB" % (location, std_unit(limit)), 

465 ) 

466 self.file_size_dialog = Gtk.MessageDialog(parent, Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.INFO, 

467 Gtk.ButtonsType.CLOSE, "\n".join(msgs)) 

468 try: 

469 image = Gtk.Image.new_from_stock(Gtk.STOCK_DIALOG_WARNING, Gtk.IconSize.BUTTON) 

470 self.file_size_dialog.set_image(image) 

471 except Exception as e: 

472 log.warn("Warning: failed to set dialog image: %s", e) 

473 self.file_size_dialog.connect("response", self.close_file_size_warning) 

474 self.file_size_dialog.show() 

475 

476 def close_file_size_warning(self, *_args): 

477 fsd = self.file_size_dialog 

478 if fsd: 

479 self.file_size_dialog = None 

480 fsd.destroy() 

481 

482 def download_server_log(self, callback=None): 

483 filename = "${XPRA_SERVER_LOG}" 

484 if callback: 

485 self.file_request_callback[filename] = callback 

486 self.send_request_file(filename, self.open_files) 

487 

488 def send_download_request(self, *_args): 

489 command = ["xpra", "send-file"] 

490 self.send_start_command("Client-Download-File", command, True) 

491 

492 def show_file_upload(self, *args): 

493 if self.file_dialog: 

494 self.file_dialog.present() 

495 return 

496 filelog("show_file_upload%s can open=%s", args, self.remote_open_files) 

497 buttons = [Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL] 

498 if self.remote_open_files: 

499 buttons += [Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT] 

500 buttons += [Gtk.STOCK_OK, Gtk.ResponseType.OK] 

501 self.file_dialog = Gtk.FileChooserDialog( 

502 "File to upload", 

503 parent=None, 

504 action=Gtk.FileChooserAction.OPEN, 

505 buttons=tuple(buttons)) 

506 self.file_dialog.set_default_response(Gtk.ResponseType.OK) 

507 self.file_dialog.connect("response", self.file_upload_dialog_response) 

508 self.file_dialog.show() 

509 

510 def close_file_upload_dialog(self): 

511 fd = self.file_dialog 

512 if fd: 

513 fd.destroy() 

514 self.file_dialog = None 

515 

516 def file_upload_dialog_response(self, dialog, v): 

517 if v not in (Gtk.ResponseType.OK, Gtk.ResponseType.ACCEPT): 

518 filelog("dialog response code %s", v) 

519 self.close_file_upload_dialog() 

520 return 

521 filename = dialog.get_filename() 

522 filelog("file_upload_dialog_response: filename=%s", filename) 

523 try: 

524 filesize = os.stat(filename).st_size 

525 except OSError: 

526 pass 

527 else: 

528 if not self.check_file_size("upload", filename, filesize): 

529 self.close_file_upload_dialog() 

530 return 

531 gfile = dialog.get_file() 

532 self.close_file_upload_dialog() 

533 filelog("load_contents: filename=%s, response=%s", filename, v) 

534 cancellable = None 

535 user_data = (filename, v==Gtk.ResponseType.ACCEPT) 

536 gfile.load_contents_async(cancellable, self.file_upload_ready, user_data) 

537 

538 def file_upload_ready(self, gfile, result, user_data): 

539 filelog("file_upload_ready%s", (gfile, result, user_data)) 

540 filename, openit = user_data 

541 _, data, entity = gfile.load_contents_finish(result) 

542 filesize = len(data) 

543 filelog("load_contents_finish(%s)=%s", result, (type(data), filesize, entity)) 

544 if not data: 

545 log.warn("Warning: failed to load file '%s'", filename) 

546 return 

547 filelog("load_contents: filename=%s, %i bytes, entity=%s, openit=%s", 

548 filename, filesize, entity, openit) 

549 self.send_file(filename, "", data, filesize=filesize, openit=openit) 

550 

551 

552 def show_about(self, *_args): 

553 from xpra.gtk_common.about import about 

554 force_focus() 

555 about() 

556 

557 def show_shortcuts(self, *_args): 

558 if self.shortcuts_info and not self.shortcuts_info.is_closed: 

559 force_focus() 

560 self.shortcuts_info.present() 

561 return 

562 from xpra.client.gtk3.show_shortcuts import ShortcutInfo 

563 kh = self.keyboard_helper 

564 assert kh, "no keyboard helper" 

565 self.shortcuts_info = ShortcutInfo(kh.shortcut_modifiers, kh.key_shortcuts) 

566 self.shortcuts_info.show_all() 

567 

568 def show_session_info(self, *args): 

569 if self.session_info and not self.session_info.is_closed: 

570 #exists already: just raise its window: 

571 self.session_info.set_args(*args) 

572 force_focus() 

573 self.session_info.present() 

574 return 

575 pixbuf = self.get_pixbuf("statistics.png") 

576 if not pixbuf: 

577 pixbuf = self.get_pixbuf("xpra.png") 

578 p = self._protocol 

579 conn = p._conn if p else None 

580 from xpra.client.gtk_base.session_info import SessionInfo 

581 self.session_info = SessionInfo(self, self.session_name, pixbuf, conn, self.get_pixbuf) 

582 self.session_info.set_args(*args) 

583 force_focus() 

584 self.session_info.show_all() 

585 

586 def show_bug_report(self, *_args): 

587 self.send_info_request() 

588 if self.bug_report: 

589 force_focus() 

590 self.bug_report.show() 

591 return 

592 from xpra.client.gtk_base.bug_report import BugReport 

593 self.bug_report = BugReport() 

594 def init_bug_report(): 

595 #skip things we aren't using: 

596 includes ={ 

597 "keyboard" : bool(self.keyboard_helper), 

598 "opengl" : self.opengl_enabled, 

599 } 

600 def get_server_info(): 

601 return self.server_last_info 

602 self.bug_report.init(show_about=False, 

603 get_server_info=get_server_info, 

604 opengl_info=self.opengl_props, 

605 includes=includes) 

606 self.bug_report.show() 

607 #gives the server time to send an info response.. 

608 #(by the time the user clicks on copy, it should have arrived, we hope!) 

609 def got_server_log(filename, filesize): 

610 log("got_server_log(%s, %s)", filename, filesize) 

611 filedata = load_binary_file(filename) 

612 self.bug_report.set_server_log_data(filedata) 

613 self.download_server_log(got_server_log) 

614 self.timeout_add(200, init_bug_report) 

615 

616 

617 def get_pixbuf(self, icon_name): 

618 try: 

619 if not icon_name: 

620 log("get_pixbuf(%s)=None", icon_name) 

621 return None 

622 icon_filename = get_icon_filename(icon_name) 

623 log("get_pixbuf(%s) icon_filename=%s", icon_name, icon_filename) 

624 if icon_filename: 

625 return GdkPixbuf.Pixbuf.new_from_file(icon_filename) 

626 except Exception: 

627 log.error("get_pixbuf(%s)", icon_name, exc_info=True) 

628 return None 

629 

630 

631 def get_image(self, icon_name, size=None): 

632 try: 

633 pixbuf = self.get_pixbuf(icon_name) 

634 log("get_image(%s, %s) pixbuf=%s", icon_name, size, pixbuf) 

635 if not pixbuf: 

636 return None 

637 return scaled_image(pixbuf, size) 

638 except Exception: 

639 log.error("get_image(%s, %s)", icon_name, size, exc_info=True) 

640 return None 

641 

642 

643 def request_frame_extents(self, window): 

644 from xpra.x11.gtk_x11.send_wm import send_wm_request_frame_extents 

645 from xpra.gtk_common.error import xsync 

646 root = self.get_root_window() 

647 with xsync: 

648 win = window.get_window() 

649 framelog("request_frame_extents(%s) xid=%#x", window, win.get_xid()) 

650 send_wm_request_frame_extents(root, win) 

651 

652 def get_frame_extents(self, window): 

653 #try native platform code first: 

654 x, y = window.get_position() 

655 w, h = window.get_size() 

656 v = get_window_frame_size(x, y, w, h) #pylint: disable=assignment-from-none 

657 framelog("get_window_frame_size%s=%s", (x, y, w, h), v) 

658 if v: 

659 #(OSX does give us these values via Quartz API) 

660 return v 

661 if not HAS_X11_BINDINGS: 

662 #nothing more we can do! 

663 return None 

664 from xpra.x11.gtk_x11.prop import prop_get 

665 gdkwin = window.get_window() 

666 assert gdkwin 

667 v = prop_get(gdkwin, "_NET_FRAME_EXTENTS", ["u32"], ignore_errors=False) 

668 framelog("get_frame_extents(%s)=%s", window.get_title(), v) 

669 return v 

670 

671 def get_window_frame_sizes(self): 

672 wfs = get_window_frame_sizes() 

673 if self.frame_request_window: 

674 v = self.get_frame_extents(self.frame_request_window) 

675 if v: 

676 try: 

677 wm_name = get_wm_name() #pylint: disable=assignment-from-none 

678 except Exception: 

679 wm_name = None 

680 try: 

681 if len(v)==8: 

682 if first_time("invalid-frame-extents"): 

683 framelog.warn("Warning: invalid frame extents value '%s'", v) 

684 if wm_name: 

685 framelog.warn(" this is probably a bug in '%s'", wm_name) 

686 framelog.warn(" using '%s' instead", v[4:]) 

687 v = v[4:] 

688 l, r, t, b = v 

689 wfs["frame"] = (l, r, t, b) 

690 wfs["offset"] = (l, t) 

691 except Exception as e: 

692 framelog.warn("Warning: invalid frame extents value '%s'", v) 

693 framelog.warn(" %s", e) 

694 framelog.warn(" this is probably a bug in '%s'", wm_name) 

695 framelog("get_window_frame_sizes()=%s", wfs) 

696 return wfs 

697 

698 

699 def _add_statusicon_tray(self, tray_list): 

700 #add Gtk.StatusIcon tray: 

701 try: 

702 from xpra.client.gtk_base.statusicon_tray import GTKStatusIconTray 

703 tray_list.append(GTKStatusIconTray) 

704 except Exception as e: 

705 log.warn("failed to load StatusIcon tray: %s" % e) 

706 return tray_list 

707 

708 def get_tray_classes(self): 

709 from xpra.client.mixins.tray import TrayClient 

710 return self._add_statusicon_tray(TrayClient.get_tray_classes(self)) 

711 

712 def get_system_tray_classes(self): 

713 return self._add_statusicon_tray(WindowClient.get_system_tray_classes(self)) 

714 

715 

716 def supports_system_tray(self) -> bool: 

717 #always True: we can always use Gtk.StatusIcon as fallback 

718 return True 

719 

720 

721 def get_root_window(self): 

722 return get_default_root_window() 

723 

724 def get_root_size(self): 

725 return get_root_size() 

726 

727 

728 def get_mouse_position(self): 

729 p = self.get_root_window().get_pointer()[-3:-1] 

730 return self.cp(p[0], p[1]) 

731 

732 def get_current_modifiers(self): 

733 root = self.get_root_window() 

734 if root is None: 

735 return () 

736 modifiers_mask = root.get_pointer()[-1] 

737 return self.mask_to_names(modifiers_mask) 

738 

739 

740 def make_hello(self) -> dict: 

741 capabilities = UIXpraClient.make_hello(self) 

742 capabilities["named_cursors"] = len(cursor_types)>0 

743 capabilities["encoding.transparency"] = self.has_transparency() 

744 capabilities.update(flatten_dict(get_gtk_version_info())) 

745 if EXPORT_ICON_DATA: 

746 #tell the server which icons GTK can use 

747 #so it knows when it should supply one as fallback 

748 it = Gtk.IconTheme.get_default() 

749 if it: 

750 #this would add our bundled icon directory 

751 #to the search path, but I don't think we have 

752 #any extra icons that matter in there: 

753 #from xpra.platform.paths import get_icon_dir 

754 #d = get_icon_dir() 

755 #if d not in it.get_search_path(): 

756 # it.append_search_path(d) 

757 # it.rescan_if_needed() 

758 log("default icon theme: %s", it) 

759 log("icon search path: %s", it.get_search_path()) 

760 log("contexts: %s", it.list_contexts()) 

761 icons = [] 

762 for context in it.list_contexts(): 

763 icons += it.list_icons(context) 

764 log("icons: %s", icons) 

765 capabilities["theme.default.icons"] = tuple(set(icons)) 

766 if METADATA_SUPPORTED: 

767 ms = [x.strip() for x in METADATA_SUPPORTED.split(",")] 

768 else: 

769 #this is currently unused, and slightly redundant because of metadata.supported below: 

770 capabilities["window.states"] = [ 

771 "fullscreen", "maximized", 

772 "sticky", "above", "below", 

773 "shaded", "iconified", 

774 "skip-taskbar", "skip-pager", 

775 ] 

776 ms = list(DEFAULT_METADATA_SUPPORTED) 

777 #added in 0.15: 

778 ms += ["command", "workspace", "above", "below", "sticky", 

779 "set-initial-position", #0.17 

780 "content-type", 

781 ] 

782 if POSIX: 

783 #this is only really supported on X11, but posix is easier to check for.. 

784 #"strut" and maybe even "fullscreen-monitors" could also be supported on other platforms I guess 

785 ms += ["shaded", "bypass-compositor", "strut", "fullscreen-monitors"] 

786 if HAS_X11_BINDINGS: 

787 ms += ["x11-property"] 

788 if XSHAPE: 

789 ms += ["shape"] 

790 log("metadata.supported: %s", ms) 

791 capabilities["metadata.supported"] = ms 

792 updict(capabilities, "pointer", { 

793 "grabs" : True, 

794 "relative" : True, 

795 }) 

796 updict(capabilities, "window", { 

797 "initiate-moveresize" : True, #v4 servers assume this is available 

798 "frame_sizes" : self.get_window_frame_sizes() 

799 }) 

800 updict(capabilities, "encoding", { 

801 "icons.greedy" : True, #we don't set a default window icon any more 

802 "icons.size" : (64, 64), #size we want 

803 "icons.max_size" : (128, 128), #limit 

804 }) 

805 capabilities["opengl"] = self.opengl_props 

806 return capabilities 

807 

808 

809 def has_transparency(self) -> bool: 

810 screen = Gdk.Screen.get_default() 

811 if screen is None: 

812 return is_Wayland() 

813 return screen.get_rgba_visual() is not None 

814 

815 

816 def get_screen_sizes(self, xscale=1, yscale=1): 

817 return get_screen_sizes(xscale, yscale) 

818 

819 

820 def reset_windows_cursors(self, *_args): 

821 cursorlog("reset_windows_cursors() resetting cursors for: %s", tuple(self._cursors.keys())) 

822 for w,cursor_data in tuple(self._cursors.items()): 

823 self.set_windows_cursor([w], cursor_data) 

824 

825 

826 def set_windows_cursor(self, windows, cursor_data): 

827 cursorlog("set_windows_cursor(%s, args[%i])", windows, len(cursor_data)) 

828 cursor = None 

829 if cursor_data: 

830 try: 

831 cursor = self.make_cursor(cursor_data) 

832 cursorlog("make_cursor(..)=%s", cursor) 

833 except Exception as e: 

834 log.warn("error creating cursor: %s (using default)", e, exc_info=True) 

835 if cursor is None: 

836 #use default: 

837 cursor = get_default_cursor() 

838 for w in windows: 

839 w.set_cursor_data(cursor_data) 

840 gdkwin = w.get_window() 

841 #trays don't have a gdk window 

842 if gdkwin: 

843 self._cursors[w] = cursor_data 

844 gdkwin.set_cursor(cursor) 

845 

846 def make_cursor(self, cursor_data): 

847 #if present, try cursor ny name: 

848 display = Gdk.Display.get_default() 

849 cursorlog("make_cursor: has-name=%s, has-cursor-types=%s, xscale=%s, yscale=%s, USE_LOCAL_CURSORS=%s", 

850 len(cursor_data)>=10, bool(cursor_types), self.xscale, self.yscale, USE_LOCAL_CURSORS) 

851 pixbuf = None 

852 if len(cursor_data)>=10 and cursor_types: 

853 cursor_name = bytestostr(cursor_data[9]) 

854 if cursor_name and USE_LOCAL_CURSORS: 

855 try: 

856 cursor = Gdk.Cursor.new_from_name(display, cursor_name) 

857 except TypeError: 

858 cursorlog("Gdk.Cursor.new_from_name%s", (display, cursor_name), exc_info=True) 

859 cursor = None 

860 if cursor: 

861 cursorlog("Gdk.Cursor.new_from_name(%s, %s)=%s", display, cursor_name, cursor) 

862 else: 

863 gdk_cursor = cursor_types.get(cursor_name.upper()) 

864 cursorlog("gdk_cursor(%s)=%s", cursor_name, gdk_cursor) 

865 if gdk_cursor: 

866 try: 

867 cursor = Gdk.Cursor.new_for_display(display, gdk_cursor) 

868 cursorlog("Cursor.new_for_display(%s, %s)=%s", display, gdk_cursor, cursor) 

869 except TypeError as e: 

870 log("new_Cursor_for_display(%s, %s)", display, gdk_cursor, exc_info=True) 

871 if first_time("cursor:%s" % cursor_name.upper()): 

872 log.error("Error creating cursor %s: %s", cursor_name.upper(), e) 

873 global missing_cursor_names 

874 if cursor: 

875 pixbuf = cursor.get_image() 

876 cursorlog("image=%s", pixbuf) 

877 elif cursor_name not in missing_cursor_names: 

878 cursorlog("cursor name '%s' not found", cursor_name) 

879 missing_cursor_names.add(cursor_name) 

880 #create cursor from the pixel data: 

881 encoding, _, _, w, h, xhot, yhot, serial, pixels = cursor_data[0:9] 

882 encoding = bytestostr(encoding) 

883 if encoding!="raw": 

884 cursorlog.warn("Warning: invalid cursor encoding: %s", encoding) 

885 return None 

886 if not pixbuf: 

887 if len(pixels)<w*h*4: 

888 cursorlog.warn("Warning: not enough pixels provided in cursor data") 

889 cursorlog.warn(" %s needed and only %s bytes found:", w*h*4, len(pixels)) 

890 cursorlog.warn(" '%s')", repr_ellipsized(hexstr(pixels))) 

891 return None 

892 pixbuf = get_pixbuf_from_data(pixels, True, w, h, w*4) 

893 else: 

894 w = pixbuf.get_width() 

895 h = pixbuf.get_height() 

896 pixels = pixbuf.get_pixels() 

897 x = max(0, min(xhot, w-1)) 

898 y = max(0, min(yhot, h-1)) 

899 csize = display.get_default_cursor_size() 

900 cmaxw, cmaxh = display.get_maximal_cursor_size() 

901 if len(cursor_data)>=12: 

902 ssize = cursor_data[10] 

903 smax = cursor_data[11] 

904 cursorlog("server cursor sizes: default=%s, max=%s", ssize, smax) 

905 cursorlog("new %s cursor at %s,%s with serial=%#x, dimensions: %sx%s, len(pixels)=%s", 

906 encoding, xhot, yhot, serial, w, h, len(pixels)) 

907 cursorlog("default cursor size is %s, maximum=%s", csize, (cmaxw, cmaxh)) 

908 fw, fh = get_fixed_cursor_size() 

909 if fw>0 and fh>0 and (w!=fw or h!=fh): 

910 #OS wants a fixed cursor size! (win32 does, and GTK doesn't do this for us) 

911 if w<=fw and h<=fh: 

912 cursorlog("pasting %ix%i cursor to fixed OS size %ix%i", w, h, fw, fh) 

913 from PIL import Image 

914 img = Image.frombytes("RGBA", (w, h), pixels, "raw", "BGRA", w*4, 1) 

915 target = Image.new("RGBA", (fw, fh)) 

916 target.paste(img, (0, 0, w, h)) 

917 pixels = img.tobytes("raw", "BGRA") 

918 cursor_pixbuf = get_pixbuf_from_data(pixels, True, w, h, w*4) 

919 else: 

920 cursorlog("scaling cursor from %ix%i to fixed OS size %ix%i", w, h, fw, fh) 

921 cursor_pixbuf = pixbuf.scale_simple(fw, fh, GdkPixbuf.InterpType.BILINEAR) 

922 xratio, yratio = w/fw, h/fh 

923 x, y = iround(x/xratio), iround(y/yratio) 

924 else: 

925 sx, sy, sw, sh = x, y, w, h 

926 #scale the cursors: 

927 if self.xscale!=1 or self.yscale!=1: 

928 sx, sy, sw, sh = self.srect(x, y, w, h) 

929 sw = max(1, sw) 

930 sh = max(1, sh) 

931 #ensure we honour the max size if there is one: 

932 if 0<cmaxw<sw or 0<cmaxh<sh: 

933 ratio = 1.0 

934 if cmaxw>0: 

935 ratio = max(ratio, w/cmaxw) 

936 if cmaxh>0: 

937 ratio = max(ratio, h/cmaxh) 

938 cursorlog("clamping cursor size to %ix%i using ratio=%s", cmaxw, cmaxh, ratio) 

939 sx, sy = iround(x/ratio), iround(y/ratio) 

940 sw, sh = min(cmaxw, iround(w/ratio)), min(cmaxh, iround(h/ratio)) 

941 if sw!=w or sh!=h: 

942 cursorlog("scaling cursor from %ix%i hotspot at %ix%i to %ix%i hotspot at %ix%i", 

943 w, h, x, y, sw, sh, sx, sy) 

944 cursor_pixbuf = pixbuf.scale_simple(sw, sh, GdkPixbuf.InterpType.BILINEAR) 

945 x, y = sx, sy 

946 else: 

947 cursor_pixbuf = pixbuf 

948 if SAVE_CURSORS: 

949 cursor_pixbuf.savev("cursor-%#x.png" % serial, "png", [], []) 

950 #clamp to pixbuf size: 

951 w = cursor_pixbuf.get_width() 

952 h = cursor_pixbuf.get_height() 

953 x = max(0, min(x, w-1)) 

954 y = max(0, min(y, h-1)) 

955 try: 

956 c = Gdk.Cursor.new_from_pixbuf(display, cursor_pixbuf, x, y) 

957 except RuntimeError as e: 

958 log.error("Error: failed to create cursor:") 

959 log.error(" %s", e) 

960 log.error(" Gdk.Cursor.new_from_pixbuf%s", (display, cursor_pixbuf, x, y)) 

961 log.error(" using size %ix%i with hotspot at %ix%i", w, h, x, y) 

962 c = None 

963 return c 

964 

965 

966 def process_ui_capabilities(self, caps : typedict): 

967 UIXpraClient.process_ui_capabilities(self, caps) 

968 #this requires the "DisplayClient" mixin: 

969 if not hasattr(self, "screen_size_changed"): 

970 return 

971 #always one screen per display: 

972 screen = Gdk.Screen.get_default() 

973 screen.connect("size-changed", self.screen_size_changed) 

974 

975 

976 def window_grab(self, window): 

977 em = Gdk.EventMask 

978 event_mask = (em.BUTTON_PRESS_MASK | 

979 em.BUTTON_RELEASE_MASK | 

980 em.POINTER_MOTION_MASK | 

981 em.POINTER_MOTION_HINT_MASK | 

982 em.ENTER_NOTIFY_MASK | 

983 em.LEAVE_NOTIFY_MASK) 

984 confine_to = None 

985 cursor = None 

986 r = Gdk.pointer_grab(window.get_window(), True, event_mask, confine_to, cursor, 0) 

987 grablog("pointer_grab(..)=%s", GRAB_STATUS_STRING.get(r, r)) 

988 #also grab the keyboard so the user won't Alt-Tab away: 

989 r = Gdk.keyboard_grab(window.get_window(), False, 0) 

990 grablog("keyboard_grab(..)=%s", GRAB_STATUS_STRING.get(r, r)) 

991 

992 def window_ungrab(self): 

993 grablog("window_ungrab()") 

994 Gdk.pointer_ungrab(0) 

995 Gdk.keyboard_ungrab(0) 

996 

997 

998 def window_bell(self, window, device, percent, pitch, duration, bell_class, bell_id, bell_name): 

999 gdkwindow = None 

1000 if window: 

1001 gdkwindow = window.get_window() 

1002 if gdkwindow is None: 

1003 gdkwindow = self.get_root_window() 

1004 log("window_bell(..) gdkwindow=%s", gdkwindow) 

1005 if not system_bell(gdkwindow, device, percent, pitch, duration, bell_class, bell_id, bell_name): 

1006 #fallback to simple beep: 

1007 Gdk.beep() 

1008 

1009 

1010 def _process_raise_window(self, packet): 

1011 wid = packet[1] 

1012 window = self._id_to_window.get(wid) 

1013 focuslog("going to raise window %s - %s", wid, window) 

1014 if window: 

1015 if window.has_toplevel_focus(): 

1016 log("window already has top level focus") 

1017 return 

1018 window.present() 

1019 

1020 def _process_restack_window(self, packet): 

1021 wid, detail, other_wid = packet[1:4] 

1022 above = bool(detail==0) 

1023 window = self._id_to_window.get(wid) 

1024 other_window = self._id_to_window.get(other_wid) 

1025 focuslog("restack window %s - %s %s %s", 

1026 wid, window, ["below", "above"][above], other_window) 

1027 if window: 

1028 window.get_window().restack(other_window, above) #Above=0 

1029 

1030 def opengl_setup_failure(self, summary = "Xpra OpenGL Acceleration Failure", body=""): 

1031 def delayed_notify(): 

1032 if self.exit_code is None: 

1033 self.may_notify(XPRA_OPENGL_NOTIFICATION_ID, summary, body, icon_name="opengl") 

1034 #wait for the main loop to run: 

1035 self.timeout_add(2*1000, delayed_notify) 

1036 

1037 #OpenGL bits: 

1038 def init_opengl(self, enable_opengl): 

1039 opengllog("init_opengl(%s)", enable_opengl) 

1040 #enable_opengl can be True, False, force, probe-failed, probe-success, or None (auto-detect) 

1041 #ie: "on:native,gtk", "auto", "no" 

1042 #ie: "probe-failed:SIGSEGV" 

1043 #ie: "probe-success" 

1044 enable_opengl = (enable_opengl or "") 

1045 parts = enable_opengl.split(":", 1) 

1046 enable_option = parts[0].lower() #ie: "on" 

1047 opengllog("init_opengl: enable_option=%s", enable_option) 

1048 if enable_option in ("probe-failed", "probe-error", "probe-crash"): 

1049 msg = enable_option.replace("-", " ") 

1050 if len(parts)>1 and any(len(x) for x in parts[1:]): 

1051 msg += ": %s" % csv(parts[1:]) 

1052 self.opengl_props["info"] = "disabled, %s" % msg 

1053 self.opengl_setup_failure(body=msg) 

1054 return 

1055 if enable_option in FALSE_OPTIONS: 

1056 self.opengl_props["info"] = "disabled by configuration" 

1057 return 

1058 warnings = [] 

1059 self.opengl_props["info"] = "" 

1060 if enable_option=="force": 

1061 self.opengl_force = True 

1062 elif enable_option!="probe-success": 

1063 from xpra.scripts.config import OpenGL_safety_check 

1064 from xpra.platform.gui import gl_check as platform_gl_check 

1065 for check in (OpenGL_safety_check, platform_gl_check): 

1066 opengllog("checking with %s", check) 

1067 warning = check() 

1068 opengllog("%s()=%s", check, warning) 

1069 if warning: 

1070 warnings.append(warning) 

1071 

1072 def err(msg, e): 

1073 opengllog("OpenGL initialization error", exc_info=True) 

1074 self.GLClientWindowClass = None 

1075 self.client_supports_opengl = False 

1076 opengllog.error("%s", msg) 

1077 for x in str(e).split("\n"): 

1078 opengllog.error(" %s", x) 

1079 self.opengl_props["info"] = str(e) 

1080 self.opengl_props["enabled"] = False 

1081 self.opengl_setup_failure(body=str(e)) 

1082 

1083 if warnings: 

1084 if enable_option in ("", "auto"): 

1085 opengllog.warn("OpenGL disabled:") 

1086 for warning in warnings: 

1087 opengllog.warn(" %s", warning) 

1088 self.opengl_props["info"] = "disabled: %s" % csv(warnings) 

1089 return 

1090 if enable_option=="probe-success": 

1091 opengllog.warn("OpenGL enabled, despite some warnings:") 

1092 else: 

1093 opengllog.warn("OpenGL safety warning (enabled at your own risk):") 

1094 for warning in warnings: 

1095 opengllog.warn(" %s", warning) 

1096 self.opengl_props["info"] = "enabled despite: %s" % csv(warnings) 

1097 try: 

1098 opengllog("init_opengl: going to import xpra.client.gl") 

1099 __import__("xpra.client.gl", {}, {}, []) 

1100 from xpra.client.gl.window_backend import ( 

1101 get_gl_client_window_module, 

1102 test_gl_client_window, 

1103 ) 

1104 force_enable = self.opengl_force or (enable_option in TRUE_OPTIONS) 

1105 self.opengl_props, gl_client_window_module = get_gl_client_window_module(force_enable) 

1106 if not gl_client_window_module: 

1107 opengllog.warn("Warning: no OpenGL backend module found") 

1108 self.client_supports_opengl = False 

1109 self.opengl_props["info"] = "disabled: no module found" 

1110 return 

1111 opengllog("init_opengl: found props %s", self.opengl_props) 

1112 self.GLClientWindowClass = gl_client_window_module.GLClientWindow 

1113 self.client_supports_opengl = True 

1114 #only enable opengl by default if force-enabled or if safe to do so: 

1115 self.opengl_enabled = enable_option in (list(TRUE_OPTIONS)+["auto"]) or self.opengl_props.get("safe", False) 

1116 self.gl_texture_size_limit = self.opengl_props.get("texture-size-limit", 16*1024) 

1117 self.gl_max_viewport_dims = self.opengl_props.get("max-viewport-dims", 

1118 (self.gl_texture_size_limit, self.gl_texture_size_limit)) 

1119 driver_info = self.opengl_props.get("renderer") or self.opengl_props.get("vendor") or "unknown card" 

1120 if min(self.gl_max_viewport_dims)<4*1024: 

1121 opengllog.warn("Warning: OpenGL is disabled:") 

1122 opengllog.warn(" the maximum viewport size is too low: %s", self.gl_max_viewport_dims) 

1123 self.opengl_enabled = False 

1124 elif self.gl_texture_size_limit<4*1024: 

1125 opengllog.warn("Warning: OpenGL is disabled:") 

1126 opengllog.warn(" the texture size limit is too low: %s", self.gl_texture_size_limit) 

1127 self.opengl_enabled = False 

1128 elif driver_info.startswith("SVGA3D") and os.environ.get("WAYLAND_DISPLAY"): 

1129 opengllog.warn("Warning: OpenGL is disabled:") 

1130 opengllog.warn(" SVGA3D driver is buggy under Wayland") 

1131 self.opengl_enabled = False 

1132 self.GLClientWindowClass.MAX_VIEWPORT_DIMS = self.gl_max_viewport_dims 

1133 self.GLClientWindowClass.MAX_BACKING_DIMS = self.gl_texture_size_limit, self.gl_texture_size_limit 

1134 mww, mwh = self.max_window_size 

1135 opengllog("OpenGL: enabled=%s, texture-size-limit=%s, max-window-size=%s", 

1136 self.opengl_enabled, self.gl_texture_size_limit, self.max_window_size) 

1137 if self.opengl_enabled and self.gl_texture_size_limit<16*1024 and (mww==0 or mwh==0 or self.gl_texture_size_limit<mww or self.gl_texture_size_limit<mwh): 

1138 #log at warn level if the limit is low: 

1139 #(if we're likely to hit it - if the screen is as big or bigger) 

1140 w, h = self.get_root_size() 

1141 l = opengllog.info 

1142 if w*2<=self.gl_texture_size_limit and h*2<=self.gl_texture_size_limit: 

1143 l = opengllog 

1144 if w>=self.gl_texture_size_limit or h>=self.gl_texture_size_limit: 

1145 l = opengllog.warn 

1146 l("Warning: OpenGL windows will be clamped to the maximum texture size %ix%i", 

1147 self.gl_texture_size_limit, self.gl_texture_size_limit) 

1148 l(" for OpenGL %s renderer '%s'", pver(self.opengl_props.get("opengl", "")), self.opengl_props.get("renderer", "unknown")) 

1149 if self.opengl_enabled and enable_opengl!="probe-success" and not self.opengl_force: 

1150 draw_result = test_gl_client_window(self.GLClientWindowClass, max_window_size=self.max_window_size, pixel_depth=self.pixel_depth) 

1151 if not draw_result.get("success", False): 

1152 err("OpenGL test rendering failed:", draw_result.get("message", "unknown error")) 

1153 return 

1154 log("OpenGL test rendering succeeded") 

1155 if self.opengl_enabled: 

1156 opengllog.info("OpenGL enabled with %s", driver_info) 

1157 #don't try to handle video dimensions bigger than this: 

1158 mvs = min(8192, self.gl_texture_size_limit) 

1159 self.video_max_size = (mvs, mvs) 

1160 elif self.client_supports_opengl: 

1161 opengllog("OpenGL supported with %s, but not enabled", driver_info) 

1162 self.opengl_props["enabled"] = self.opengl_enabled 

1163 except ImportError as e: 

1164 err("OpenGL accelerated rendering is not available:", e) 

1165 except RuntimeError as e: 

1166 err("OpenGL support could not be enabled on this hardware:", e) 

1167 except Exception as e: 

1168 err("Error loading OpenGL support:", e) 

1169 opengllog("init_opengl(%s)", enable_opengl, exc_info=True) 

1170 

1171 def get_client_window_classes(self, w : int, h : int, metadata : typedict, override_redirect : bool): 

1172 log("get_client_window_class%s ClientWindowClass=%s, GLClientWindowClass=%s, opengl_enabled=%s, mmap_enabled=%s, encoding=%s", 

1173 (w, h, metadata, override_redirect), 

1174 self.ClientWindowClass, self.GLClientWindowClass, 

1175 self.opengl_enabled, self.mmap_enabled, self.encoding) 

1176 if self.can_use_opengl(w, h, metadata, override_redirect): 

1177 return (self.GLClientWindowClass, self.ClientWindowClass) 

1178 return (self.ClientWindowClass,) 

1179 

1180 def can_use_opengl(self, w : int, h : int, metadata : typedict, override_redirect : bool): 

1181 if self.GLClientWindowClass is None or not self.opengl_enabled: 

1182 return False 

1183 if not self.opengl_force: 

1184 #verify texture limits: 

1185 ms = min(self.sx(self.gl_texture_size_limit), *self.gl_max_viewport_dims) 

1186 if w>ms or h>ms: 

1187 return False 

1188 #avoid opengl for small windows: 

1189 if w<=OPENGL_MIN_SIZE or h<=OPENGL_MIN_SIZE: 

1190 log("not using opengl for small window: %ix%i", w, h) 

1191 return False 

1192 #avoid opengl for tooltips: 

1193 window_types = metadata.strtupleget("window-type") 

1194 if any(x in (NO_OPENGL_WINDOW_TYPES) for x in window_types): 

1195 log("not using opengl for %s window-type", csv(window_types)) 

1196 return False 

1197 if metadata.intget("transient-for", 0)>0: 

1198 log("not using opengl for transient-for window") 

1199 return False 

1200 if metadata.strget("content-type")=="text": 

1201 return False 

1202 if WIN32: 

1203 #these checks can't be forced ('opengl_force') 

1204 #win32 opengl just doesn't do alpha or undecorated windows properly: 

1205 if override_redirect: 

1206 return False 

1207 if metadata.boolget("has-alpha", False): 

1208 return False 

1209 if not metadata.boolget("decorations", True): 

1210 return False 

1211 hbl = (self.headerbar or "").lower().strip() 

1212 if hbl not in FALSE_OPTIONS: 

1213 #any risk that we may end up using headerbar, 

1214 #means we can't enable opengl 

1215 return False 

1216 return True 

1217 

1218 def toggle_opengl(self, *_args): 

1219 self.opengl_enabled = not self.opengl_enabled 

1220 opengllog("opengl_toggled: %s", self.opengl_enabled) 

1221 #now replace all the windows with new ones: 

1222 for wid, window in tuple(self._id_to_window.items()): 

1223 self.reinit_window(wid, window) 

1224 opengllog("replaced all the windows with opengl=%s: %s", self.opengl_enabled, self._id_to_window) 

1225 self.reinit_window_icons() 

1226 

1227 

1228 def get_group_leader(self, wid, metadata, _override_redirect): 

1229 transient_for = metadata.intget("transient-for", -1) 

1230 log("get_group_leader: transient_for=%s", transient_for) 

1231 if transient_for>0: 

1232 client_window = self._id_to_window.get(transient_for) 

1233 if client_window: 

1234 gdk_window = client_window.get_window() 

1235 if gdk_window: 

1236 return gdk_window 

1237 pid = metadata.intget("pid", -1) 

1238 leader_xid = metadata.intget("group-leader-xid", -1) 

1239 leader_wid = metadata.intget("group-leader-wid", -1) 

1240 group_leader_window = self._id_to_window.get(leader_wid) 

1241 if group_leader_window: 

1242 #leader is another managed window 

1243 log("found group leader window %s for wid=%s", group_leader_window, leader_wid) 

1244 return group_leader_window 

1245 log("get_group_leader: leader pid=%s, xid=%s, wid=%s", pid, leader_xid, leader_wid) 

1246 reftype = "xid" 

1247 ref = leader_xid 

1248 if ref<0: 

1249 reftype = "leader-wid" 

1250 ref = leader_wid 

1251 if ref<0: 

1252 ci = metadata.strtupleget("class-instance") 

1253 if ci: 

1254 reftype = "class" 

1255 ref = "|".join(ci) 

1256 elif pid>0: 

1257 reftype = "pid" 

1258 ref = pid 

1259 elif transient_for>0: 

1260 #this should have matched a client window above.. 

1261 #but try to use it anyway: 

1262 reftype = "transient-for" 

1263 ref = transient_for 

1264 else: 

1265 #no reference to use 

1266 return None 

1267 refkey = "%s:%s" % (reftype, ref) 

1268 group_leader_window = self._ref_to_group_leader.get(refkey) 

1269 if group_leader_window: 

1270 log("found existing group leader window %s using ref=%s", group_leader_window, refkey) 

1271 return group_leader_window 

1272 #we need to create one: 

1273 title = "%s group leader for %s" % (self.session_name or "Xpra", pid) 

1274 #group_leader_window = Gdk.Window(None, 1, 1, Gtk.WindowType.TOPLEVEL, 0, Gdk.INPUT_ONLY, title) 

1275 #static new(parent, attributes, attributes_mask) 

1276 group_leader_window = GDKWindow(wclass=Gdk.WindowWindowClass.INPUT_ONLY, title=title) 

1277 self._ref_to_group_leader[refkey] = group_leader_window 

1278 #avoid warning on win32... 

1279 if not WIN32: 

1280 #X11 spec says window should point to itself: 

1281 group_leader_window.set_group(group_leader_window) 

1282 log("new hidden group leader window %s for ref=%s", group_leader_window, refkey) 

1283 self._group_leader_wids.setdefault(group_leader_window, []).append(wid) 

1284 return group_leader_window 

1285 

1286 def destroy_window(self, wid, window): 

1287 #override so we can cleanup the group-leader if needed, 

1288 WindowClient.destroy_window(self, wid, window) 

1289 group_leader = window.group_leader 

1290 if group_leader is None or not self._group_leader_wids: 

1291 return 

1292 wids = self._group_leader_wids.get(group_leader) 

1293 if wids is None: 

1294 #not recorded any window ids on this group leader 

1295 #means it is another managed window, leave it alone 

1296 return 

1297 if wid in wids: 

1298 wids.remove(wid) 

1299 if wids: 

1300 #still has another window pointing to it 

1301 return 

1302 #the last window has gone, we can remove the group leader, 

1303 #find all the references to this group leader: 

1304 del self._group_leader_wids[group_leader] 

1305 refs = [] 

1306 for ref, gl in self._ref_to_group_leader.items(): 

1307 if gl==group_leader: 

1308 refs.append(ref) 

1309 for ref in refs: 

1310 del self._ref_to_group_leader[ref] 

1311 log("last window for refs %s is gone, destroying the group leader %s", refs, group_leader) 

1312 group_leader.destroy() 

1313 

1314 

1315 def setup_clipboard_helper(self, helperClass): 

1316 from xpra.client.mixins.clipboard import ClipboardClient 

1317 ch = ClipboardClient.setup_clipboard_helper(self, helperClass) 

1318 #check for loops after handshake: 

1319 def register_clipboard_toggled(*_args): 

1320 def clipboard_toggled(*_args): 

1321 #reset tray icon: 

1322 self.local_clipboard_requests = 0 

1323 self.remote_clipboard_requests = 0 

1324 self.clipboard_notify(0) 

1325 self.connect("clipboard-toggled", clipboard_toggled) 

1326 def loop_disabled_notify(): 

1327 ch = self.clipboard_helper 

1328 if ch and ch.disabled_by_loop and self.notifier: 

1329 icon = None 

1330 try: 

1331 from xpra.notifications.common import parse_image_path 

1332 icon = parse_image_path(get_icon_filename("clipboard")) 

1333 except ImportError: 

1334 pass 

1335 summary = "Clipboard Synchronization Error" 

1336 body = "A synchronization loop has been detected,\n" + \ 

1337 "to prevent further issues clipboard synchronization has been disabled." 

1338 self.notifier.show_notify("", self.tray, 0, "Xpra", 0, "", summary, body, [], {}, 10*10000, icon) 

1339 return False 

1340 self.timeout_add(5*1000, loop_disabled_notify) 

1341 self.after_handshake(register_clipboard_toggled) 

1342 if self.server_clipboard: 

1343 #from now on, we will send a message to the server whenever the clipboard flag changes: 

1344 self.connect("clipboard-toggled", self.clipboard_toggled) 

1345 return ch 

1346 

1347 def cancel_clipboard_notification_timer(self): 

1348 cnt = self.clipboard_notification_timer 

1349 if cnt: 

1350 self.clipboard_notification_timer = None 

1351 self.source_remove(cnt) 

1352 

1353 def clipboard_notify(self, n): 

1354 tray = self.tray 

1355 if not tray or not CLIPBOARD_NOTIFY: 

1356 return 

1357 clipboardlog("clipboard_notify(%s) notification timer=%s", n, self.clipboard_notification_timer) 

1358 self.cancel_clipboard_notification_timer() 

1359 if n>0 and self.clipboard_enabled: 

1360 self.last_clipboard_notification = monotonic_time() 

1361 tray.set_icon("clipboard") 

1362 tray.set_tooltip("%s clipboard requests in progress" % n) 

1363 tray.set_blinking(True) 

1364 else: 

1365 #no more pending clipboard transfers, 

1366 #reset the tray icon, 

1367 #but wait at least N seconds after the last clipboard transfer: 

1368 N = 1 

1369 delay = int(max(0, 1000*(self.last_clipboard_notification+N-monotonic_time()))) 

1370 def reset_tray_icon(): 

1371 self.clipboard_notification_timer = None 

1372 tray = self.tray 

1373 if not tray: 

1374 return 

1375 tray.set_icon(None) #None means back to default icon 

1376 tray.set_tooltip(self.get_tray_title()) 

1377 tray.set_blinking(False) 

1378 self.clipboard_notification_timer = self.timeout_add(delay, reset_tray_icon)