Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/client/gtk_base/gtk_client_base.py : 41%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of Xpra.
2# Copyright (C) 2011 Serviware (Arthur Huillet, <ahuillet@serviware.com>)
3# Copyright (C) 2010-2020 Antoine Martin <antoine@xpra.org>
4# Copyright (C) 2008, 2010 Nathaniel Smith <njs@pobox.com>
5# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
6# later version. See the file COPYING for details.
8import os
9import weakref
10from gi.repository import Gtk, Gdk, GdkPixbuf
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
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")
58missing_cursor_names = set()
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(",")
71inject_css_overrides()
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
80 ClientWindowClass = None
81 GLClientWindowClass = None
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)
122 def init(self, opts):
123 GObjectXpraClient.init(self, opts)
124 UIXpraClient.init(self, opts)
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)
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
149 def gtk_main(self):
150 log("GTKXpraClient.gtk_main() calling %s", Gtk.main)
151 Gtk.main()
152 log("GTKXpraClient.gtk_main() ended")
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)
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()
177 def exit(self):
178 self.show_progress(100, "terminating")
179 log("GTKXpraClient.exit() calling %s", Gtk.main_quit)
180 Gtk.main_quit()
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)
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)
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
246 def _process_startup_complete(self, packet):
247 UIXpraClient._process_startup_complete(self, packet)
248 Gdk.notify_startup_complete()
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
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
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")
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)
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()
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()
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)
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()
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()
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()
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)
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])
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()
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()
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)
488 def send_download_request(self, *_args):
489 command = ["xpra", "send-file"]
490 self.send_start_command("Client-Download-File", command, True)
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()
510 def close_file_upload_dialog(self):
511 fd = self.file_dialog
512 if fd:
513 fd.destroy()
514 self.file_dialog = None
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)
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)
552 def show_about(self, *_args):
553 from xpra.gtk_common.about import about
554 force_focus()
555 about()
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()
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()
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)
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
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
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)
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
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
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
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))
712 def get_system_tray_classes(self):
713 return self._add_statusicon_tray(WindowClient.get_system_tray_classes(self))
716 def supports_system_tray(self) -> bool:
717 #always True: we can always use Gtk.StatusIcon as fallback
718 return True
721 def get_root_window(self):
722 return get_default_root_window()
724 def get_root_size(self):
725 return get_root_size()
728 def get_mouse_position(self):
729 p = self.get_root_window().get_pointer()[-3:-1]
730 return self.cp(p[0], p[1])
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)
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
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
816 def get_screen_sizes(self, xscale=1, yscale=1):
817 return get_screen_sizes(xscale, yscale)
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)
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)
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
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)
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))
992 def window_ungrab(self):
993 grablog("window_ungrab()")
994 Gdk.pointer_ungrab(0)
995 Gdk.keyboard_ungrab(0)
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()
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()
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
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)
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)
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))
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)
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,)
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
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()
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
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()
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
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)
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)