class GTKXpraClient(UIXpraClient, GObjectXpraClient): __gsignals__ = UIXpraClient.__gsignals__ ClientWindowClass = None GLClientWindowClass = None def __init__(self): GObjectXpraClient.__init__(self) UIXpraClient.__init__(self) self.session_info = None self.bug_report = None self.start_new_command = None #opengl bits: self.client_supports_opengl = False self.opengl_enabled = False self.opengl_props = {} self.gl_max_viewport_dims = 0, 0 self.gl_texture_size_limit = 0 self._cursors = weakref.WeakKeyDictionary() #frame request hidden window: self.frame_request_window = None #group leader bits: self._ref_to_group_leader = {} self._group_leader_wids = {} self._set_window_menu = get_menu_support_function() self.connect("scaling-changed", self.reset_windows_cursors) def init(self, opts): GObjectXpraClient.init(self, opts) UIXpraClient.init(self, opts) def setup_frame_request_windows(self): #query the window manager to get the frame size: from xpra.gtk_common.error import xsync from xpra.x11.gtk_x11.send_wm import send_wm_request_frame_extents self.frame_request_window = gtk.Window(WINDOW_TOPLEVEL) self.frame_request_window.set_title("Xpra-FRAME_EXTENTS") root = self.get_root_window() self.frame_request_window.realize() with xsync: win = self.frame_request_window.get_window() framelog("setup_frame_request_windows() window=%#x", get_xwindow(win)) send_wm_request_frame_extents(root, win) def run(self): log("run() HAS_X11_BINDINGS=%s", HAS_X11_BINDINGS) if HAS_X11_BINDINGS: self.setup_frame_request_windows() UIXpraClient.run(self) gtk_main_quit_on_fatal_exceptions_enable() self.gtk_main() log( "GTKXpraClient.run_main_loop() main loop ended, returning exit_code=%s", self.exit_code) return self.exit_code def gtk_main(self): raise NotImplementedError() def quit(self, exit_code=0): log("GTKXpraClient.quit(%s) current exit_code=%s", exit_code, self.exit_code) if self.exit_code is None: self.exit_code = exit_code if gtk.main_level() > 0: #if for some reason cleanup() hangs, maybe this will fire... self.timeout_add(4 * 1000, self.exit) #try harder!: def force_quit(): from xpra import os_util os_util.force_quit() self.timeout_add(5 * 1000, force_quit) self.cleanup() log("GTKXpraClient.quit(%s) cleanup done, main_level=%s", exit_code, gtk.main_level()) if gtk.main_level() > 0: log( "GTKXpraClient.quit(%s) main loop at level %s, calling gtk quit via timeout", exit_code, gtk.main_level()) self.timeout_add(500, self.exit) def exit(self): log("GTKXpraClient.exit() calling %s", gtk_main_quit_really) gtk_main_quit_really() def cleanup(self): if self.session_info: self.session_info.destroy() self.session_info = None if self.bug_report: self.bug_report.destroy() self.bug_report = None if self.start_new_command: self.start_new_command.destroy() self.start_new_command = None UIXpraClient.cleanup(self) def show_start_new_command(self, *args): log("show_start_new_command%s current start_new_command=%s, flag=%s", args, self.start_new_command, self.start_new_commands) if self.start_new_command is None: from xpra.client.gtk_base.start_new_command import getStartNewCommand def run_command_cb(command, sharing=True): self.send_start_command(command, command, False, sharing) self.start_new_command = getStartNewCommand( run_command_cb, self.server_supports_sharing and self.server_supports_window_filters) self.start_new_command.show() return self.start_new_command def show_file_upload(self, *args): filelog("show_file_upload%s can open=%s", args, self.server_open_files) buttons = [gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL] if self.server_open_files: buttons += [gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT] buttons += [gtk.STOCK_OK, gtk.RESPONSE_OK] dialog = gtk.FileChooserDialog("File to upload", parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN, buttons=tuple(buttons)) dialog.set_default_response(gtk.RESPONSE_OK) v = dialog.run() if v not in (gtk.RESPONSE_OK, gtk.RESPONSE_ACCEPT): filelog("dialog response code %s", v) dialog.destroy() return filename = dialog.get_filename() gfile = dialog.get_file() data, filesize, entity = gfile.load_contents() filelog("load_contents: filename=%s, %i bytes, entity=%s, response=%s", filename, filesize, entity, v) dialog.destroy() self.send_file(filename, data, filesize, openit=(v == gtk.RESPONSE_ACCEPT)) def show_about(self, *args): from xpra.gtk_common.about import about about() def show_session_info(self, *args): if self.session_info and not self.session_info.is_closed: #exists already: just raise its window: self.session_info.set_args(*args) self.session_info.present() return pixbuf = self.get_pixbuf("statistics.png") if not pixbuf: pixbuf = self.get_pixbuf("xpra.png") self.session_info = SessionInfo(self, self.session_name, pixbuf, self._protocol._conn, self.get_pixbuf) self.session_info.set_args(*args) self.session_info.show_all() def show_bug_report(self, *args): self.send_info_request() if self.bug_report: self.bug_report.show() return from xpra.client.gtk_base.bug_report import BugReport self.bug_report = BugReport() def init_bug_report(): #skip things we aren't using: includes = { "keyboard": bool(self.keyboard_helper), "opengl": self.opengl_enabled, } def get_server_info(): return self.server_last_info self.bug_report.init(show_about=False, get_server_info=get_server_info, opengl_info=self.opengl_props, includes=includes) self.bug_report.show() #gives the server time to send an info response.. #(by the time the user clicks on copy, it should have arrived, we hope!) self.timeout_add(200, init_bug_report) def get_pixbuf(self, icon_name): try: if not icon_name: log("get_pixbuf(%s)=None", icon_name) return None icon_filename = get_icon_filename(icon_name) log("get_pixbuf(%s) icon_filename=%s", icon_name, icon_filename) if icon_filename: return pixbuf_new_from_file(icon_filename) except: log.error("get_pixbuf(%s)", icon_name, exc_info=True) return None def get_image(self, icon_name, size=None): try: pixbuf = self.get_pixbuf(icon_name) log("get_image(%s, %s) pixbuf=%s", icon_name, size, pixbuf) if not pixbuf: return None return scaled_image(pixbuf, size) except: log.error("get_image(%s, %s)", icon_name, size, exc_info=True) return None def request_frame_extents(self, window): from xpra.x11.gtk_x11.send_wm import send_wm_request_frame_extents from xpra.gtk_common.error import xsync root = self.get_root_window() with xsync: win = window.get_window() framelog("request_frame_extents(%s) xid=%#x", window, get_xwindow(win)) send_wm_request_frame_extents(root, win) def get_frame_extents(self, window): #try native platform code first: x, y = window.get_position() w, h = window.get_size() v = get_window_frame_size(x, y, w, h) framelog("get_window_frame_size%s=%s", (x, y, w, h), v) if v: #(OSX does give us these values via Quartz API) return v if not HAS_X11_BINDINGS: #nothing more we can do! return None from xpra.x11.gtk_x11.prop import prop_get gdkwin = window.get_window() assert gdkwin v = prop_get(gdkwin, "_NET_FRAME_EXTENTS", ["u32"], ignore_errors=False) framelog("get_frame_extents(%s)=%s", window.get_title(), v) return v def get_window_frame_sizes(self): wfs = get_window_frame_sizes() if self.frame_request_window: v = self.get_frame_extents(self.frame_request_window) if v: try: l, r, t, b = v wfs["frame"] = (l, r, t, b) wfs["offset"] = (l, t) except Exception as e: framelog.warn("Warning: invalid frame extents value '%s'", v) framelog.warn(" %s", e) wm_name = get_wm_name() if wm_name: framelog.warn(" this is probably a bug in '%s'", wm_name) framelog("get_window_frame_sizes()=%s", wfs) return wfs def make_keyboard_helper(self, keyboard_sync, key_shortcuts): return GTKKeyboardHelper(self.send, keyboard_sync, key_shortcuts) def _add_statusicon_tray(self, tray_list): #add gtk.StatusIcon tray: try: from xpra.client.gtk_base.statusicon_tray import GTKStatusIconTray tray_list.append(GTKStatusIconTray) except Exception as e: log.warn("failed to load StatusIcon tray: %s" % e) return tray_list def get_tray_classes(self): return self._add_statusicon_tray(UIXpraClient.get_tray_classes(self)) def get_system_tray_classes(self): return self._add_statusicon_tray( UIXpraClient.get_system_tray_classes(self)) def supports_system_tray(self): #always True: we can always use gtk.StatusIcon as fallback return True def cook_metadata(self, new_window, metadata): metadata = UIXpraClient.cook_metadata(self, new_window, metadata) #ensures we will call set_window_menu for this window when we create it: if new_window and b"menu" not in metadata and self._set_window_menu: metadata[b"menu"] = {} return metadata def set_window_menu(self, add, wid, menus, application_action_callback=None, window_action_callback=None): assert self._set_window_menu model = self._id_to_window.get(wid) window = None if model: window = model.get_window() self._set_window_menu(add, wid, window, menus, application_action_callback, window_action_callback) def get_root_window(self): return get_default_root_window() def get_root_size(self): return get_root_size() def get_mouse_position(self): p = self.get_root_window().get_pointer() return self.cp(p[0], p[1]) def get_current_modifiers(self): modifiers_mask = self.get_root_window().get_pointer()[-1] return self.mask_to_names(modifiers_mask) def make_hello(self): capabilities = UIXpraClient.make_hello(self) capabilities["named_cursors"] = len(cursor_types) > 0 capabilities.update(flatten_dict(get_gtk_version_info())) #tell the server which icons GTK can use #so it knows when it should supply one as fallback it = icon_theme_get_default() #this would add our bundled icon directory #to the search path, but I don't think we have #any extra icons that matter in there: #from xpra.platform.paths import get_icon_dir #d = get_icon_dir() #if d not in it.get_search_path(): # it.append_search_path(d) # it.rescan_if_needed() log("default icon theme: %s", it) log("icon search path: %s", it.get_search_path()) log("contexts: %s", it.list_contexts()) icons = [] for context in it.list_contexts(): icons += it.list_icons(context) log("icons: %s", icons) capabilities["theme.default.icons"] = list(set(icons)) if METADATA_SUPPORTED: ms = [x.strip() for x in METADATA_SUPPORTED.split(",")] else: #this is currently unused, and slightly redundant because of metadata.supported below: capabilities["window.states"] = [ "fullscreen", "maximized", "sticky", "above", "below", "shaded", "iconified", "skip-taskbar", "skip-pager" ] ms = list(DEFAULT_METADATA_SUPPORTED) #added in 0.15: ms += [ "command", "workspace", "above", "below", "sticky", "set-initial-position" ] #0.17 if os.name == "posix": #this is only really supported on X11, but posix is easier to check for.. #"strut" and maybe even "fullscreen-monitors" could also be supported on other platforms I guess ms += [ "shaded", "bypass-compositor", "strut", "fullscreen-monitors" ] if HAS_X11_BINDINGS: ms += ["shape"] if self._set_window_menu: ms += ["menu"] #figure out if we can handle the "global menu" stuff: if os.name == "posix" and not sys.platform.startswith("darwin"): try: from xpra.dbus.helper import DBusHelper assert DBusHelper except: pass log("metadata.supported: %s", ms) capabilities["metadata.supported"] = ms #we need the bindings to support initiate-moveresize (posix only for now): updict( capabilities, "window", { "initiate-moveresize": HAS_X11_BINDINGS, "configure.pointer": True, "frame_sizes": self.get_window_frame_sizes() }) from xpra.client.window_backing_base import DELTA_BUCKETS updict( capabilities, "encoding", { "icons.greedy": True, #we don't set a default window icon any more "icons.size": (64, 64), #size we want "icons.max_size": (128, 128), #limit "delta_buckets": DELTA_BUCKETS, }) return capabilities def has_transparency(self): return screen_get_default().get_rgba_visual() is not None def get_screen_sizes(self, xscale=1, yscale=1): def xs(v): return iround(v / xscale) def ys(v): return iround(v / yscale) def swork(*workarea): return xs(workarea[0]), ys(workarea[1]), xs(workarea[2]), ys( workarea[3]) display = display_get_default() i = 0 screen_sizes = [] n_screens = display.get_n_screens() screenlog("get_screen_sizes(%f, %f) found %s screens", xscale, yscale, n_screens) while i < n_screens: screen = display.get_screen(i) j = 0 monitors = [] workareas = [] #native "get_workareas()" is only valid for a single screen (but describes all the monitors) #and it is only implemented on win32 right now #other platforms only implement "get_workarea()" instead, which is reported against the screen n_monitors = screen.get_n_monitors() screenlog(" screen %s has %s monitors", i, n_monitors) if n_screens == 1: workareas = get_workareas() if len(workareas) != n_monitors: screenlog(" workareas: %s", workareas) screenlog( " number of monitors does not match number of workareas!" ) workareas = [] while j < screen.get_n_monitors(): geom = screen.get_monitor_geometry(j) plug_name = "" if hasattr(screen, "get_monitor_plug_name"): plug_name = screen.get_monitor_plug_name(j) or "" wmm = -1 if hasattr(screen, "get_monitor_width_mm"): wmm = screen.get_monitor_width_mm(j) hmm = -1 if hasattr(screen, "get_monitor_height_mm"): hmm = screen.get_monitor_height_mm(j) monitor = [ plug_name, xs(geom.x), ys(geom.y), xs(geom.width), ys(geom.height), wmm, hmm ] screenlog(" monitor %s: %s", j, monitor) if workareas: w = workareas[j] monitor += list(swork(*w)) monitors.append(tuple(monitor)) j += 1 work_x, work_y, work_width, work_height = swork( 0, 0, screen.get_width(), screen.get_height()) workarea = get_workarea() if workarea: work_x, work_y, work_width, work_height = swork(*workarea) screenlog(" workarea=%s", workarea) item = (screen.make_display_name(), xs(screen.get_width()), ys(screen.get_height()), screen.get_width_mm(), screen.get_height_mm(), monitors, work_x, work_y, work_width, work_height) screenlog(" screen %s: %s", i, item) screen_sizes.append(item) i += 1 return screen_sizes def reset_windows_cursors(self, *args): cursorlog("reset_windows_cursors() resetting cursors for: %s", self._cursors.keys()) for w, cursor_data in list(self._cursors.items()): self.set_windows_cursor([w], cursor_data) def set_windows_cursor(self, windows, cursor_data): cursorlog("set_windows_cursor(%s, ..)", windows) cursor = None if cursor_data: try: cursor = self.make_cursor(cursor_data) cursorlog("make_cursor(..)=%s", cursor) except Exception as e: log.warn("error creating cursor: %s (using default)", e, exc_info=True) if cursor is None: #use default: cursor = get_default_cursor() for w in windows: gdkwin = w.get_window() #trays don't have a gdk window if gdkwin: self._cursors[w] = cursor_data gdkwin.set_cursor(cursor) def make_cursor(self, cursor_data): #if present, try cursor ny name: display = display_get_default() cursorlog( "make_cursor: has-name=%s, has-cursor-types=%s, xscale=%s, yscale=%s, USE_LOCAL_CURSORS=%s", len(cursor_data) >= 9, bool(cursor_types), self.xscale, self.yscale, USE_LOCAL_CURSORS) #named cursors cannot be scaled (round to 10 to compare so 0.95 and 1.05 are considered the same as 1.0, no scaling): if len(cursor_data) >= 9 and cursor_types and iround( self.xscale * 10) == 10 and iround(self.yscale * 10) == 10: cursor_name = bytestostr(cursor_data[8]) if cursor_name and USE_LOCAL_CURSORS: gdk_cursor = cursor_types.get(cursor_name.upper()) if gdk_cursor is not None: cursorlog("setting new cursor by name: %s=%s", cursor_name, gdk_cursor) return new_Cursor_for_display(display, gdk_cursor) else: global missing_cursor_names if cursor_name not in missing_cursor_names: cursorlog("cursor name '%s' not found", cursor_name) missing_cursor_names.add(cursor_name) #create cursor from the pixel data: w, h, xhot, yhot, serial, pixels = cursor_data[2:8] if len(pixels) < w * h * 4: import binascii cursorlog.warn( "not enough pixels provided in cursor data: %s needed and only %s bytes found (%s)", w * h * 4, len(pixels), binascii.hexlify(pixels)[:100]) return pixbuf = get_pixbuf_from_data(pixels, True, w, h, w * 4) x = max(0, min(xhot, w - 1)) y = max(0, min(yhot, h - 1)) csize = display.get_default_cursor_size() cmaxw, cmaxh = display.get_maximal_cursor_size() if len(cursor_data) >= 11: ssize = cursor_data[9] smax = cursor_data[10] cursorlog("server cursor sizes: default=%s, max=%s", ssize, smax) cursorlog( "new cursor at %s,%s with serial=%s, dimensions: %sx%s, len(pixels)=%s, default cursor size is %s, maximum=%s", xhot, yhot, serial, w, h, len(pixels), csize, (cmaxw, cmaxh)) fw, fh = get_fixed_cursor_size() if fw > 0 and fh > 0 and (w != fw or h != fh): #OS wants a fixed cursor size! (win32 does, and GTK doesn't do this for us) if w <= fw and h <= fh: cursorlog( "pasting cursor of size %ix%i onto clear pixbuf of size %ix%i", w, h, fw, fh) cursor_pixbuf = get_pixbuf_from_data("\0" * fw * fh * 4, True, fw, fh, fw * 4) pixbuf.copy_area(0, 0, w, h, cursor_pixbuf, 0, 0) else: cursorlog("scaling cursor from %ix%i to fixed OS size %ix%i", w, h, fw, fh) cursor_pixbuf = pixbuf.scale_simple(fw, fh, INTERP_BILINEAR) xratio, yratio = float(w) / fw, float(h) / fh x, y = iround(x / xratio), iround(y / yratio) else: sx, sy, sw, sh = x, y, w, h #scale the cursors: if self.xscale != 1 or self.yscale != 1: sx, sy, sw, sh = self.srect(x, y, w, h) sw = max(1, sw) sh = max(1, sh) #ensure we honour the max size if there is one: if (cmaxw > 0 and sw > cmaxw) or (cmaxh > 0 and sh > cmaxh): ratio = 1.0 if cmaxw > 0: ratio = max(ratio, float(w) / cmaxw) if cmaxh > 0: ratio = max(ratio, float(h) / cmaxh) cursorlog("clamping cursor size to %ix%i using ratio=%s", cmaxw, cmaxh, ratio) sx, sy, sw, sh = iround(x / ratio), iround(y / ratio), min( cmaxw, iround(w / ratio)), min(cmaxh, iround(h / ratio)) if sw != w or sh != h: cursorlog( "scaling cursor from %ix%i hotspot at %ix%i to %ix%i hotspot at %ix%i", w, h, x, y, sw, sh, sx, sy) cursor_pixbuf = pixbuf.scale_simple(sw, sh, INTERP_BILINEAR) x, y = sx, sy else: cursor_pixbuf = pixbuf #clamp to pixbuf size: w = cursor_pixbuf.get_width() h = cursor_pixbuf.get_height() x = max(0, min(x, w - 1)) y = max(0, min(y, h - 1)) try: c = new_Cursor_from_pixbuf(display, cursor_pixbuf, x, y) except RuntimeError as e: log.error("Error: failed to create cursor:") log.error(" %s", e) log.error(" using %s of size %ix%i with hotspot at %ix%i", cursor_pixbuf, w, h, x, y) c = None return c def process_ui_capabilities(self): UIXpraClient.process_ui_capabilities(self) if self.server_randr: display = display_get_default() i = 0 while i < display.get_n_screens(): screen = display.get_screen(i) screen.connect("size-changed", self.screen_size_changed) i += 1 def window_bell(self, window, device, percent, pitch, duration, bell_class, bell_id, bell_name): gdkwindow = None if window: gdkwindow = window.get_window() if gdkwindow is None: gdkwindow = self.get_root_window() log("window_bell(..) gdkwindow=%s", gdkwindow) if not system_bell(gdkwindow, device, percent, pitch, duration, bell_class, bell_id, bell_name): #fallback to simple beep: gdk.beep() #OpenGL bits: def init_opengl(self, enable_opengl): opengllog("init_opengl(%s)", enable_opengl) #enable_opengl can be True, False or None (auto-detect) if enable_opengl is False: self.opengl_props["info"] = "disabled by configuration" return from xpra.scripts.config import OpenGL_safety_check from xpra.platform.gui import gl_check as platform_gl_check warnings = [] for check in (OpenGL_safety_check, platform_gl_check): opengllog("checking with %s", check) warning = check() opengllog("%s()=%s", check, warning) if warning: warnings.append(warning) self.opengl_props["info"] = "" if warnings: if enable_opengl is True: opengllog.warn( "OpenGL safety warning (enabled at your own risk):") for warning in warnings: opengllog.warn(" %s", warning) self.opengl_props["info"] = "forced enabled despite: %s" % ( ", ".join(warnings)) else: opengllog.warn("OpenGL disabled:", warning) for warning in warnings: opengllog.warn(" %s", warning) self.opengl_props["info"] = "disabled: %s" % ( ", ".join(warnings)) return try: opengllog("init_opengl: going to import xpra.client.gl") __import__("xpra.client.gl", {}, {}, []) __import__("xpra.client.gl.gtk_compat", {}, {}, []) gl_check = __import__("xpra.client.gl.gl_check", {}, {}, ["check_support"]) opengllog("init_opengl: gl_check=%s", gl_check) self.opengl_props = gl_check.check_support( force_enable=(enable_opengl is True)) opengllog("init_opengl: found props %s", self.opengl_props) GTK_GL_CLIENT_WINDOW_MODULE = "xpra.client.gl.gtk%s.gl_client_window" % ( 2 + int(is_gtk3())) opengllog( "init_opengl: trying to load GL client window module '%s'", GTK_GL_CLIENT_WINDOW_MODULE) gl_client_window = __import__(GTK_GL_CLIENT_WINDOW_MODULE, {}, {}, ["GLClientWindow"]) self.GLClientWindowClass = gl_client_window.GLClientWindow self.client_supports_opengl = True #only enable opengl by default if force-enabled or if safe to do so: self.opengl_enabled = ( enable_opengl is True) or self.opengl_props.get("safe", False) self.gl_texture_size_limit = self.opengl_props.get( "texture-size-limit", 16 * 1024) self.gl_max_viewport_dims = self.opengl_props.get( "max-viewport-dims", (self.gl_texture_size_limit, self.gl_texture_size_limit)) if min(self.gl_max_viewport_dims) < 4 * 1024: opengllog.warn("Warning: OpenGL is disabled:") opengllog.warn(" the maximum viewport size is too low: %s", self.gl_max_viewport_dims) self.opengl_enabled = False elif self.gl_texture_size_limit < 4 * 1024: opengllog.warn("Warning: OpenGL is disabled:") opengllog.warn(" the texture size limit is too low: %s", self.gl_texture_size_limit) self.opengl_enabled = False self.GLClientWindowClass.MAX_VIEWPORT_DIMS = self.gl_max_viewport_dims self.GLClientWindowClass.MAX_BACKING_DIMS = self.gl_texture_size_limit, self.gl_texture_size_limit self.GLClientWindowClass.MAX_VIEWPORT_DIMS = 8192, 8192 self.GLClientWindowClass.MAX_BACKING_DIMS = 4096, 4096 mww, mwh = self.max_window_size opengllog( "OpenGL: enabled=%s, texture-size-limit=%s, max-window-size=%s", self.opengl_enabled, self.gl_texture_size_limit, self.max_window_size) 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): #log at warn level if the limit is low: #(if we're likely to hit it - if the screen is as big or bigger) w, h = self.get_root_size() l = opengllog.info if w >= self.gl_texture_size_limit or h >= self.gl_texture_size_limit: l = log.warn l( "Warning: OpenGL windows will be clamped to the maximum texture size %ix%i", self.gl_texture_size_limit, self.gl_texture_size_limit) l(" for OpenGL %s renderer '%s'", pver(self.opengl_props.get("opengl", "")), self.opengl_props.get("renderer", "unknown")) driver_info = self.opengl_props.get( "renderer") or self.opengl_props.get( "vendor") or "unknown card" if self.opengl_enabled: opengllog.info("OpenGL enabled with %s", driver_info) elif self.client_supports_opengl: opengllog("OpenGL supported with %s, but not enabled", driver_info) except ImportError as e: opengllog.warn("OpenGL support is missing:") opengllog.warn(" %s", e) self.opengl_props["info"] = str(e) except RuntimeError as e: opengllog.warn( "OpenGL support could not be enabled on this hardware:") opengllog.warn(" %s", e) self.opengl_props["info"] = str(e) except Exception as e: opengllog.error("Error loading OpenGL support:") opengllog.error(" %s", e, exc_info=True) self.opengl_props["info"] = str(e) def get_client_window_classes(self, w, h, metadata, override_redirect): log( "get_client_window_class(%i, %i, %s, %s) GLClientWindowClass=%s, opengl_enabled=%s, mmap_enabled=%s, encoding=%s", w, h, metadata, override_redirect, self.GLClientWindowClass, self.opengl_enabled, self.mmap_enabled, self.encoding) ms = min(self.sx(self.gl_texture_size_limit), *self.gl_max_viewport_dims) if self.GLClientWindowClass is None or not self.opengl_enabled or w > ms or h > ms: return [self.ClientWindowClass] return [self.GLClientWindowClass, self.ClientWindowClass] def toggle_opengl(self, *args): self.opengl_enabled = not self.opengl_enabled opengllog("opengl_toggled: %s", self.opengl_enabled) def fake_send(*args): opengllog("fake_send(%s)", args) #now replace all the windows with new ones: for wid, window in self._id_to_window.items(): if window.is_tray(): #trays are never GL enabled, so don't bother re-creating them #(might cause problems anyway if we did) continue #ignore packets from old window: window.send = fake_send #copy attributes: x, y = window._pos ww, wh = window._size try: bw, bh = window._backing.size except: bw, bh = ww, wh client_properties = window._client_properties metadata = window._metadata override_redirect = window._override_redirect backing = window._backing video_decoder = None csc_decoder = None decoder_lock = None try: if backing: video_decoder = backing._video_decoder csc_decoder = backing._csc_decoder decoder_lock = backing._decoder_lock if decoder_lock: decoder_lock.acquire() opengllog( "toggle_opengl() will preserve video=%s and csc=%s for %s", video_decoder, csc_decoder, wid) backing._video_decoder = None backing._csc_decoder = None backing._decoder_lock = None backing.close() #now we can unmap it: self.destroy_window(wid, window) #explicitly tell the server we have unmapped it: #(so it will reset the video encoders, etc) self.send("unmap-window", wid) try: del self._id_to_window[wid] except: pass try: del self._window_to_id[window] except: pass #create the new window, which should honour the new state of the opengl_enabled flag: window = self.make_new_window(wid, x, y, ww, wh, bw, bh, metadata, override_redirect, client_properties) if video_decoder or csc_decoder: backing = window._backing backing._video_decoder = video_decoder backing._csc_decoder = csc_decoder backing._decoder_lock = decoder_lock finally: if decoder_lock: decoder_lock.release() opengllog("replaced all the windows with opengl=%s: %s", self.opengl_enabled, self._id_to_window) def get_group_leader(self, wid, metadata, override_redirect): transient_for = metadata.intget("transient-for", -1) log("get_group_leader: transient_for=%s", transient_for) if transient_for > 0: client_window = self._id_to_window.get(transient_for) if client_window: gdk_window = client_window.get_window() if gdk_window: return gdk_window pid = metadata.intget("pid", -1) leader_xid = metadata.intget("group-leader-xid", -1) leader_wid = metadata.intget("group-leader-wid", -1) group_leader_window = self._id_to_window.get(leader_wid) if group_leader_window: #leader is another managed window log("found group leader window %s for wid=%s", group_leader_window, leader_wid) return group_leader_window log("get_group_leader: leader pid=%s, xid=%s, wid=%s", pid, leader_xid, leader_wid) reftype = "xid" ref = leader_xid if ref < 0: reftype = "leader-wid" ref = leader_wid if ref < 0: ci = metadata.strlistget("class-instance") if ci: reftype = "class" ref = "|".join(ci) elif pid > 0: reftype = "pid" ref = pid elif transient_for > 0: #this should have matched a client window above.. #but try to use it anyway: reftype = "transient-for" ref = transient_for else: #no reference to use return None refkey = "%s:%s" % (reftype, ref) group_leader_window = self._ref_to_group_leader.get(refkey) if group_leader_window: log("found existing group leader window %s using ref=%s", group_leader_window, refkey) return group_leader_window #we need to create one: title = "%s group leader for %s" % (self.session_name or "Xpra", pid) #group_leader_window = gdk.Window(None, 1, 1, gdk.WINDOW_TOPLEVEL, 0, gdk.INPUT_ONLY, title) #static new(parent, attributes, attributes_mask) if is_gtk3(): #long winded and annoying attributes = gdk.WindowAttr() attributes.width = 1 attributes.height = 1 attributes.title = title attributes.wclass = gdk.WindowWindowClass.INPUT_ONLY attributes.event_mask = 0 attributes_mask = gdk.WindowAttributesType.TITLE | gdk.WindowAttributesType.WMCLASS group_leader_window = gdk.Window(None, attributes, attributes_mask) group_leader_window.resize(1, 1) else: #gtk2: group_leader_window = gdk.Window(None, 1, 1, gdk.WINDOW_TOPLEVEL, 0, gdk.INPUT_ONLY, title) self._ref_to_group_leader[refkey] = group_leader_window #avoid warning on win32... if not sys.platform.startswith("win"): #X11 spec says window should point to itself: group_leader_window.set_group(group_leader_window) log("new hidden group leader window %s for ref=%s", group_leader_window, refkey) self._group_leader_wids.setdefault(group_leader_window, []).append(wid) return group_leader_window def destroy_window(self, wid, window): #override so we can cleanup the group-leader if needed, UIXpraClient.destroy_window(self, wid, window) group_leader = window.group_leader if group_leader is None or len(self._group_leader_wids) == 0: return wids = self._group_leader_wids.get(group_leader) if wids is None: #not recorded any window ids on this group leader #means it is another managed window, leave it alone return if wid in wids: wids.remove(wid) if len(wids) > 0: #still has another window pointing to it return #the last window has gone, we can remove the group leader, #find all the references to this group leader: del self._group_leader_wids[group_leader] refs = [] for ref, gl in self._ref_to_group_leader.items(): if gl == group_leader: refs.append(ref) for ref in refs: del self._ref_to_group_leader[ref] log("last window for refs %s is gone, destroying the group leader %s", refs, group_leader) group_leader.destroy()
class GTKTrayMenuBase(object): def __init__(self, client): self.client = client self.session_info = None self.menu = None self.menu_shown = False def build(self): if self.menu is None: show_close = True #or sys.platform.startswith("win") self.menu = self.setup_menu(show_close) return self.menu def show_session_info(self, *args): if self.session_info and not self.session_info.is_closed: self.session_info.present() else: pixbuf = self.client.get_pixbuf("statistics.png") if not pixbuf: pixbuf = self.client.get_pixbuf("xpra.png") self.session_info = SessionInfo(self.client, self.client.session_name, pixbuf, self.client._protocol._conn, self.client.get_pixbuf) self.session_info.show_all() def get_image(self, *args): return self.client.get_image(*args) def setup_menu(self, show_close=True): self.menu_shown = False menu = gtk.Menu() menu.set_title(self.client.session_name or "Xpra") def set_menu_title(*args): #set the real name when available: self.menu.set_title(self.client.session_name) self.client.connect("handshake-complete", set_menu_title) menu.append(self.make_aboutmenuitem()) menu.append(self.make_sessioninfomenuitem()) menu.append(gtk.SeparatorMenuItem()) menu.append(self.make_bellmenuitem()) if self.client.windows_enabled: menu.append(self.make_cursorsmenuitem()) menu.append(self.make_notificationsmenuitem()) if not self.client.readonly: menu.append(self.make_clipboardmenuitem()) if self.client.opengl_enabled: menu.append(self.make_openglmenuitem()) if self.client.windows_enabled and len(self.client.get_encodings())>1: menu.append(self.make_encodingsmenuitem()) menu.append(self.make_qualitymenuitem()) menu.append(self.make_speedmenuitem()) if self.client.speaker_allowed and STARTSTOP_SOUND_MENU: menu.append(self.make_speakermenuitem()) if self.client.microphone_allowed and STARTSTOP_SOUND_MENU: menu.append(self.make_microphonemenuitem()) if SHOW_COMPRESSION_MENU: menu.append(self.make_compressionmenu()) if not self.client.readonly and self.client.keyboard_helper: menu.append(self.make_layoutsmenuitem()) if self.client.windows_enabled and not self.client.readonly: menu.append(self.make_keyboardsyncmenuitem()) if self.client.windows_enabled: menu.append(self.make_refreshmenuitem()) menu.append(self.make_raisewindowsmenuitem()) #menu.append(item("Options", "configure", None, self.options)) menu.append(gtk.SeparatorMenuItem()) menu.append(self.make_disconnectmenuitem()) if show_close: menu.append(self.make_closemenuitem()) self.popup_menu_workaround(menu) menu.connect("deactivate", self.menu_deactivated) menu.show_all() return menu def cleanup(self): debug("cleanup() session_info=%s", self.session_info) if self.session_info: self.session_info.destroy() self.session_info = None self.close_menu() close_about() def close_menu(self, *args): if self.menu_shown: self.menu.popdown() self.menu_shown = False def menu_deactivated(self, *args): self.menu_shown = False def activate(self): self.show_menu(1, 0) def popup(self, button, time): self.show_menu(button, time) def show_menu(self, button, time): raise Exception("override me!") def handshake_menuitem(self, *args, **kwargs): """ Same as menuitem() but this one will be disabled until we complete the server handshake """ mi = self.menuitem(*args, **kwargs) mi.set_sensitive(False) def enable_menuitem(*args): mi.set_sensitive(True) self.client.connect("handshake-complete", enable_menuitem) return mi def menuitem(self, title, icon_name=None, tooltip=None, cb=None): """ Utility method for easily creating an ImageMenuItem """ image = None if icon_name: image = self.get_image(icon_name, 24) return menuitem(title, image, tooltip, cb) def checkitem(self, title, cb=None): """ Utility method for easily creating a CheckMenuItem """ check_item = CheckMenuItem(title) if cb: check_item.connect("toggled", cb) check_item.show() return check_item def make_aboutmenuitem(self): return self.menuitem("About Xpra", "information.png", None, about) def make_sessioninfomenuitem(self): title = "Session Info" if self.client.session_name and self.client.session_name!="Xpra session": title = "Info: %s" % self.client.session_name return self.handshake_menuitem(title, "statistics.png", None, self.show_session_info) def make_bellmenuitem(self): def bell_toggled(*args): v = self.bell_menuitem.get_active() changed = self.client.bell_enabled != v self.client.bell_enabled = v if changed and self.client.toggle_cursors_bell_notify: self.client.send_bell_enabled() debug("bell_toggled(%s) bell_enabled=%s", args, self.client.bell_enabled) self.bell_menuitem = self.checkitem("Bell", bell_toggled) self.bell_menuitem.set_sensitive(False) def set_bell_menuitem(*args): self.bell_menuitem.set_active(self.client.bell_enabled) c = self.client can_toggle_bell = c.toggle_cursors_bell_notify and c.server_supports_bell and c.client_supports_bell self.bell_menuitem.set_sensitive(can_toggle_bell) if can_toggle_bell: set_tooltip_text(self.bell_menuitem, "Forward system bell") else: set_tooltip_text(self.bell_menuitem, "Cannot forward the system bell: the feature has been disabled") self.client.connect("handshake-complete", set_bell_menuitem) return self.bell_menuitem def make_cursorsmenuitem(self): def cursors_toggled(*args): v = self.cursors_menuitem.get_active() changed = self.client.cursors_enabled != v self.client.cursors_enabled = v if changed and self.client.toggle_cursors_bell_notify: self.client.send_cursors_enabled() if not self.client.cursors_enabled: self.client.reset_cursor() debug("cursors_toggled(%s) cursors_enabled=%s", args, self.client.cursors_enabled) self.cursors_menuitem = self.checkitem("Cursors", cursors_toggled) self.cursors_menuitem.set_sensitive(False) def set_cursors_menuitem(*args): self.cursors_menuitem.set_active(self.client.cursors_enabled) c = self.client can_toggle_cursors = c.toggle_cursors_bell_notify and c.server_supports_cursors and c.client_supports_cursors self.cursors_menuitem.set_sensitive(can_toggle_cursors) if can_toggle_cursors: set_tooltip_text(self.cursors_menuitem, "Forward custom mouse cursors") else: set_tooltip_text(self.cursors_menuitem, "Cannot forward mouse cursors: the feature has been disabled") self.client.connect("handshake-complete", set_cursors_menuitem) return self.cursors_menuitem def make_notificationsmenuitem(self): def notifications_toggled(*args): v = self.notifications_menuitem.get_active() changed = self.client.notifications_enabled != v self.client.notifications_enabled = v if changed and self.client.toggle_cursors_bell_notify: self.client.send_notify_enabled() debug("notifications_toggled(%s) notifications_enabled=%s", args, self.client.notifications_enabled) self.notifications_menuitem = self.checkitem("Notifications", notifications_toggled) self.notifications_menuitem.set_sensitive(False) def set_notifications_menuitem(*args): self.notifications_menuitem.set_active(self.client.notifications_enabled) c = self.client can_notify = c.toggle_cursors_bell_notify and c.server_supports_notifications and c.client_supports_notifications self.notifications_menuitem.set_sensitive(can_notify) if can_notify: set_tooltip_text(self.notifications_menuitem, "Forward system notifications") else: set_tooltip_text(self.notifications_menuitem, "Cannot forward system notifications: the feature has been disabled") self.client.connect("handshake-complete", set_notifications_menuitem) return self.notifications_menuitem def make_clipboard_togglemenuitem(self): def clipboard_toggled(*args): new_state = self.clipboard_menuitem.get_active() debug("clipboard_toggled(%s) clipboard_enabled=%s, new_state=%s", args, self.client.clipboard_enabled, new_state) if self.client.clipboard_enabled!=new_state: self.client.clipboard_enabled = new_state self.client.emit("clipboard-toggled") self.clipboard_menuitem = self.checkitem("Clipboard", clipboard_toggled) self.clipboard_menuitem.set_sensitive(False) def set_clipboard_menuitem(*args): self.clipboard_menuitem.set_active(self.client.clipboard_enabled) c = self.client can_clipboard = c.server_supports_clipboard and c.client_supports_clipboard self.clipboard_menuitem.set_sensitive(can_clipboard) if can_clipboard: set_tooltip_text(self.clipboard_menuitem, "Enable clipboard synchronization") else: set_tooltip_text(self.clipboard_menuitem, "Clipboard synchronization cannot be enabled: disabled by server") self.client.connect("handshake-complete", set_clipboard_menuitem) return self.clipboard_menuitem def make_translatedclipboard_optionsmenuitem(self): clipboard_menu = self.menuitem("Clipboard", "clipboard.png", "Choose which remote clipboard to connect to", None) clipboard_menu.set_sensitive(False) def set_clipboard_menu(*args): clipboard_submenu = gtk.Menu() clipboard_menu.set_submenu(clipboard_submenu) self.popup_menu_workaround(clipboard_submenu) c = self.client can_clipboard = c.server_supports_clipboard and c.client_supports_clipboard and c.server_supports_clipboard debug("set_clipboard_menu(%s) can_clipboard=%s, server=%s, client=%s", args, can_clipboard, c.server_supports_clipboard, c.client_supports_clipboard) clipboard_menu.set_sensitive(can_clipboard) LABEL_TO_NAME = {"Disabled" : None, "Clipboard" : "CLIPBOARD", "Primary" : "PRIMARY", "Secondary" : "SECONDARY"} from xpra.clipboard.translated_clipboard import TranslatedClipboardProtocolHelper for label, remote_clipboard in LABEL_TO_NAME.items(): clipboard_item = CheckMenuItem(label) def remote_clipboard_changed(item): assert can_clipboard item = ensure_item_selected(clipboard_submenu, item) label = item.get_label() remote_clipboard = LABEL_TO_NAME.get(label) old_state = self.client.clipboard_enabled debug("remote_clipboard_changed(%s) remote_clipboard=%s, old_state=%s", item, remote_clipboard, old_state) send_tokens = False if remote_clipboard is not None: #clipboard is not disabled if self.client.clipboard_helper is None: self.client.setup_clipboard_helper(TranslatedClipboardProtocolHelper) self.client.clipboard_helper.remote_clipboard = remote_clipboard send_tokens = True new_state = True else: self.client.clipboard_helper = None send_tokens = False new_state = False debug("remote_clipboard_changed(%s) label=%s, remote_clipboard=%s, old_state=%s, new_state=%s", item, label, remote_clipboard, old_state, new_state) if new_state!=old_state: self.client.clipboard_enabled = new_state self.client.emit("clipboard-toggled") send_tokens = True if send_tokens and self.client.clipboard_helper: self.client.clipboard_helper.send_all_tokens() active = isinstance(self.client.clipboard_helper, TranslatedClipboardProtocolHelper) \ and self.client.clipboard_helper.remote_clipboard==remote_clipboard clipboard_item.set_active(active) clipboard_item.set_sensitive(can_clipboard) clipboard_item.set_draw_as_radio(True) clipboard_item.connect("toggled", remote_clipboard_changed) clipboard_submenu.append(clipboard_item) clipboard_submenu.show_all() self.client.connect("handshake-complete", set_clipboard_menu) return clipboard_menu def make_clipboardmenuitem(self): try: if self.client.clipboard_helper: from xpra.clipboard.translated_clipboard import TranslatedClipboardProtocolHelper if isinstance(self.client.clipboard_helper, TranslatedClipboardProtocolHelper): return self.make_translatedclipboard_optionsmenuitem() except: log.error("make_clipboardmenuitem()", exc_info=True) return self.make_clipboard_togglemenuitem() def make_keyboardsyncmenuitem(self): def set_keyboard_sync_tooltip(): if not self.client.keyboard_helper: set_tooltip_text(self.keyboard_sync_menuitem, "Keyboard support is not loaded") elif not self.client.toggle_keyboard_sync: set_tooltip_text(self.keyboard_sync_menuitem, "This server does not support changes to keyboard synchronization") elif self.client.keyboard_helper.keyboard_sync: set_tooltip_text(self.keyboard_sync_menuitem, "Disable keyboard synchronization (prevents spurious key repeats on high latency connections)") else: set_tooltip_text(self.keyboard_sync_menuitem, "Enable keyboard state synchronization") def keyboard_sync_toggled(*args): self.client.keyboard_sync = self.keyboard_sync_menuitem.get_active() debug("keyboard_sync_toggled(%s) keyboard_sync=%s", args, self.client.keyboard_sync) set_keyboard_sync_tooltip() self.client.emit("keyboard-sync-toggled") self.keyboard_sync_menuitem = self.checkitem("Keyboard Synchronization", keyboard_sync_toggled) self.keyboard_sync_menuitem.set_sensitive(False) def set_keyboard_sync_menuitem(*args): self.keyboard_sync_menuitem.set_active(self.client.keyboard_helper.keyboard_sync) self.keyboard_sync_menuitem.set_sensitive(self.client.toggle_keyboard_sync) set_keyboard_sync_tooltip() self.client.connect("handshake-complete", set_keyboard_sync_menuitem) return self.keyboard_sync_menuitem def make_openglmenuitem(self): gl = self.checkitem("OpenGL") def gl_set(*args): debug("gl_set(%s) opengl_enabled=%s, window_unmap=%s", args, self.client.opengl_enabled, self.client.window_unmap) gl.set_active(self.client.opengl_enabled) gl.set_sensitive(self.client.window_unmap) if not self.client.window_unmap: set_tooltip_text(gl, "no server support for runtime switching") return def opengl_toggled(*args): self.client.toggle_opengl() gl.connect("toggled", opengl_toggled) self.client.connect("handshake-complete", gl_set) return gl def make_encodingsmenuitem(self): encodings = self.menuitem("Encoding", "encoding.png", "Choose picture data encoding", None) encodings.set_sensitive(False) def set_encodingsmenuitem(*args): encodings.set_sensitive(not self.client.mmap_enabled) if self.client.mmap_enabled: #mmap disables encoding and uses raw rgb24 encodings.set_label("Encoding") set_tooltip_text(encodings, "memory mapped transfers are in use so picture encoding is disabled") else: encodings.set_submenu(self.make_encodingssubmenu()) self.client.connect("handshake-complete", set_encodingsmenuitem) return encodings def make_encodingssubmenu(self, handshake_complete=True): encodings = [x for x in PREFERED_ENCODING_ORDER if x in self.client.get_encodings()] encodings_submenu = make_encodingsmenu(self.get_current_encoding, self.set_current_encoding, encodings, self.client.server_encodings) self.popup_menu_workaround(encodings_submenu) return encodings_submenu def get_current_encoding(self): return self.client.encoding def set_current_encoding(self, enc): self.client.set_encoding(enc) #these menus may need updating now: self.set_qualitymenu() self.set_speedmenu() def reset_encoding_options(self, encodings_menu): for x in encodings_menu.get_children(): if isinstance(x, gtk.CheckMenuItem): encoding = x.get_label() active = encoding==self.client.encoding if active!=x.get_active(): x.set_active(active) x.set_sensitive(encoding in self.client.server_encodings) def make_qualitymenuitem(self): self.quality = self.menuitem("Quality", "slider.png", "Picture quality", None) self.quality.set_sensitive(False) def may_enable_qualitymenu(*args): self.quality.set_submenu(self.make_qualitysubmenu()) self.set_qualitymenu() self.client.connect("handshake-complete", may_enable_qualitymenu) return self.quality def make_qualitysubmenu(self): quality_submenu = make_min_auto_menu("Quality", MIN_QUALITY_OPTIONS, QUALITY_OPTIONS, self.get_min_quality, self.get_quality, self.set_min_quality, self.set_quality) #WARNING: this changes "min-quality", not "quality" (or at least it tries to..) self.popup_menu_workaround(quality_submenu) quality_submenu.show_all() return quality_submenu def get_min_quality(self): return self.client.min_quality def get_quality(self): return self.client.quality def set_min_quality(self, q): self.client.min_quality = q self.client.quality = 0 self.client.send_min_quality() self.client.send_quality() def set_quality(self, q): self.client.min_quality = 0 self.client.quality = q self.client.send_min_quality() self.client.send_quality() def set_qualitymenu(self, *args): if self.quality: can_use = not self.client.mmap_enabled and self.client.encoding in self.client.server_encodings_with_quality self.quality.set_sensitive(can_use) if not can_use: set_tooltip_text(self.quality, "Not supported with %s encoding" % self.client.encoding) return set_tooltip_text(self.quality, "Minimum picture quality") #now check if lossless is supported: if self.quality.get_submenu(): can_lossless = self.client.encoding in self.client.server_encodings_with_lossless_mode for q,item in self.quality.get_submenu().menu_items.items(): item.set_sensitive(q<100 or can_lossless) def make_speedmenuitem(self): self.speed = self.menuitem("Speed", "speed.png", "Encoding latency vs size", None) self.speed.set_sensitive(False) def may_enable_speedmenu(*args): self.speed.set_submenu(self.make_speedsubmenu()) self.set_speedmenu() self.client.connect("handshake-complete", may_enable_speedmenu) return self.speed def make_speedsubmenu(self): speed_submenu = make_min_auto_menu("Speed", MIN_SPEED_OPTIONS, SPEED_OPTIONS, self.get_min_speed, self.get_speed, self.set_min_speed, self.set_speed) self.popup_menu_workaround(speed_submenu) return speed_submenu def get_min_speed(self): return self.client.min_speed def get_speed(self): return self.client.speed def set_min_speed(self, s): self.client.min_speed = s self.client.speed = 0 self.client.send_min_speed() self.client.send_speed() def set_speed(self, s): self.client.min_speed = 0 self.client.speed = s self.client.send_min_speed() self.client.send_speed() def set_speedmenu(self, *args): if self.speed: can_use = not self.client.mmap_enabled and self.client.encoding in self.client.server_encodings_with_speed and self.client.change_speed self.speed.set_sensitive(can_use) if self.client.mmap_enabled: set_tooltip_text(self.speed, "Quality is always 100% with mmap") elif not self.client.change_speed: set_tooltip_text(self.speed, "Server does not support changing speed") elif self.client.encoding!="x264": set_tooltip_text(self.speed, "Not supported with %s encoding" % self.client.encoding) else: set_tooltip_text(self.speed, "Encoding latency vs size") def spk_on(self, *args): debug("spk_on(%s)", args) self.client.start_receiving_sound() def spk_off(self, *args): debug("spk_off(%s)", args) self.client.stop_receiving_sound() def make_speakermenuitem(self): speaker = self.menuitem("Speaker", "speaker.png", "Forward sound output from the server") speaker.set_sensitive(False) def is_speaker_on(*args): return self.client.speaker_enabled def speaker_state(*args): if not self.client.server_sound_send: speaker.set_sensitive(False) set_tooltip_text(speaker, "Server does not support speaker forwarding") return speaker.set_sensitive(True) speaker.set_submenu(self.make_soundsubmenu(is_speaker_on, self.spk_on, self.spk_off, "speaker-changed")) self.client.connect("handshake-complete", speaker_state) return speaker def mic_on(self, *args): debug("mic_on(%s)", args) self.client.start_sending_sound() def mic_off(self, *args): debug("mic_off(%s)", args) self.client.stop_sending_sound() def make_microphonemenuitem(self): microphone = self.menuitem("Microphone", "microphone.png", "Forward sound input to the server", None) microphone.set_sensitive(False) def is_microphone_on(*args): return self.client.microphone_enabled def microphone_state(*args): if not self.client.server_sound_receive: microphone.set_sensitive(False) set_tooltip_text(microphone, "Server does not support microphone forwarding") return microphone.set_sensitive(True) microphone.set_submenu(self.make_soundsubmenu(is_microphone_on, self.mic_on, self.mic_off, "microphone-changed")) self.client.connect("handshake-complete", microphone_state) return microphone def make_soundsubmenu(self, is_on_cb, on_cb, off_cb, client_signal): menu = gtk.Menu() menu.ignore_events = False def onoffitem(label, active, cb): c = CheckMenuItem(label) c.set_draw_as_radio(True) c.set_active(active) def submenu_uncheck(item, menu): if not menu.ignore_events: ensure_item_selected(menu, item) c.connect('activate', submenu_uncheck, menu) def check_enabled(item): if not menu.ignore_events and item.get_active(): cb() c.connect('activate', check_enabled) return c is_on = is_on_cb() on = onoffitem("On", is_on, on_cb) off = onoffitem("Off", not is_on, off_cb) menu.append(on) menu.append(off) def client_signalled_change(obj): menu.ignore_events = True is_on = is_on_cb() debug("sound: client_signalled_change(%s) is_on=%s", obj, is_on) if is_on: if not on.get_active(): on.set_active(True) ensure_item_selected(menu, on) else: if not off.get_active(): off.set_active(True) ensure_item_selected(menu, off) menu.ignore_events = False self.client.connect(client_signal, client_signalled_change) #menu.append(gtk.SeparatorMenuItem()) #... self.popup_menu_workaround(menu) menu.show_all() return menu def make_layoutsmenuitem(self): keyboard = self.menuitem("Keyboard", "keyboard.png", "Select your keyboard layout", None) keyboard.set_sensitive(False) self.layout_submenu = gtk.Menu() keyboard.set_submenu(self.layout_submenu) self.popup_menu_workaround(self.layout_submenu) def kbitem(title, layout, variant): def set_layout(item): """ this callback updates the client (and server) if needed """ item = ensure_item_selected(self.layout_submenu, item) layout = item.keyboard_layout variant = item.keyboard_variant if layout!=self.client.xkbmap_layout or variant!=self.client.xkbmap_variant: debug("keyboard layout selected: %s / %s", layout, variant) self.client.xkbmap_layout = layout self.client.xkbmap_variant = variant self.client.send_layout() l = self.checkitem(title, set_layout) l.set_draw_as_radio(True) l.keyboard_layout = layout l.keyboard_variant = variant return l def keysort(key): c,l = key return c.lower()+l.lower() layout,variant,variants = self.client.keyboard_helper.keyboard.get_layout_spec() if layout and len(variants)>1: #just show all the variants to choose from this layout self.layout_submenu.append(kbitem("%s - Default" % layout, layout, None)) for v in variants: self.layout_submenu.append(kbitem("%s - %s" % (layout, v), layout, v)) else: from xpra.keyboard.layouts import X11_LAYOUTS #show all options to choose from: sorted_keys = list(X11_LAYOUTS.keys()) sorted_keys.sort(key=keysort) for key in sorted_keys: country,language = key layout,variants = X11_LAYOUTS.get(key) name = "%s - %s" % (country, language) if len(variants)>1: #sub-menu for each variant: variant = self.menuitem(name, tooltip=layout) variant_submenu = gtk.Menu() variant.set_submenu(variant_submenu) self.popup_menu_workaround(variant_submenu) self.layout_submenu.append(variant) variant_submenu.append(kbitem("%s - Default" % layout, layout, None)) for v in variants: variant_submenu.append(kbitem("%s - %s" % (layout, v), layout, v)) else: #no variants: self.layout_submenu.append(kbitem(name, layout, None)) keyboard_helper = self.client.keyboard_helper def set_selected_layout(*args): if keyboard_helper.xkbmap_layout or keyboard_helper.xkbmap_print or keyboard_helper.xkbmap_query: #we have detected a layout #so no need to let the user override it keyboard.hide() return keyboard.set_sensitive(True) layout = keyboard_helper.xkbmap_layout variant = keyboard_helper.xkbmap_variant def is_match(checkitem): return checkitem.keyboard_layout==layout and checkitem.keyboard_variant==variant set_checkeditems(self.layout_submenu, is_match) self.client.connect("handshake-complete", set_selected_layout) return keyboard def make_compressionmenu(self): self.compression = self.menuitem("Compression", "compressed.png", "Network packet compression", None) self.compression.set_sensitive(False) self.compression_submenu = gtk.Menu() self.compression.set_submenu(self.compression_submenu) self.popup_menu_workaround(self.compression_submenu) compression_options = {0 : "None"} def set_compression(item): item = ensure_item_selected(self.compression_submenu, item) c = int(item.get_label().replace("None", "0")) if c!=self.client.compression_level: debug("setting compression level to %s", c) self.client.set_deflate_level(c) for i in range(0, 10): c = CheckMenuItem(str(compression_options.get(i, i))) c.set_draw_as_radio(True) c.set_active(i==self.client.compression_level) c.connect('activate', set_compression) self.compression_submenu.append(c) def enable_compressionmenu(self): self.compression.set_sensitive(True) self.compression_submenu.show_all() self.client.connect("handshake-complete", enable_compressionmenu) return self.compression def make_refreshmenuitem(self): def force_refresh(*args): debug("force refresh") self.client.send_refresh_all() return self.handshake_menuitem("Refresh", "retry.png", None, force_refresh) def make_raisewindowsmenuitem(self): def raise_windows(*args): for win in self.client._window_to_id.keys(): if not win.is_OR(): win.present() return self.handshake_menuitem("Raise Windows", "raise.png", None, raise_windows) def make_disconnectmenuitem(self): def menu_quit(*args): self.client.quit(0) return self.handshake_menuitem("Disconnect", "quit.png", None, menu_quit) def make_closemenuitem(self): return self.menuitem("Close Menu", "close.png", None, self.close_menu) def popup_menu_workaround(self, menu): #win32 workaround: if sys.platform.startswith("win"): self.add_popup_menu_workaround(menu) def add_popup_menu_workaround(self, menu): """ windows does not automatically close the popup menu when we click outside it so we workaround it by using a timer and closing the menu when the mouse has stayed outside it for more than 0.5s. This code must be added to all the sub-menus of the popup menu too! """ def enter_menu(*args): debug("mouse_in_tray_menu=%s", self.mouse_in_tray_menu) self.mouse_in_tray_menu_counter += 1 self.mouse_in_tray_menu = True def leave_menu(*args): debug("mouse_in_tray_menu=%s", self.mouse_in_tray_menu) self.mouse_in_tray_menu_counter += 1 self.mouse_in_tray_menu = False def check_menu_left(expected_counter): if self.mouse_in_tray_menu: return False if expected_counter!=self.mouse_in_tray_menu_counter: return False #counter has changed self.close_menu() gobject.timeout_add(500, check_menu_left, self.mouse_in_tray_menu_counter) self.mouse_in_tray_menu_counter = 0 self.mouse_in_tray_menu = False debug("popup_menu_workaround: adding events callbacks") menu.connect("enter-notify-event", enter_menu) menu.connect("leave-notify-event", leave_menu)
class GTKXpraClient(UIXpraClient, GObjectXpraClient): __gsignals__ = UIXpraClient.__gsignals__ def __init__(self): GObjectXpraClient.__init__(self) UIXpraClient.__init__(self) self.session_info = None def init(self, opts): GObjectXpraClient.init(self, opts) UIXpraClient.init(self, opts) def run(self): UIXpraClient.run(self) gtk_main_quit_on_fatal_exceptions_enable() self.gtk_main() log("GTKXpraClient.run_main_loop() main loop ended, returning exit_code=%s", self.exit_code) return self.exit_code def gtk_main(self): raise NotImplementedError() def quit(self, exit_code=0): log("GTKXpraClient.quit(%s) current exit_code=%s", exit_code, self.exit_code) if self.exit_code is None: self.exit_code = exit_code if gtk.main_level()>0: #if for some reason cleanup() hangs, maybe this will fire... gobject.timeout_add(4*1000, gtk_main_quit_really) #try harder!: gobject.timeout_add(5*1000, os._exit, 1) self.cleanup() if gtk.main_level()>0: log("GTKXpraClient.quit(%s) main loop at level %s, calling gtk quit via timeout", exit_code, gtk.main_level()) gobject.timeout_add(500, gtk_main_quit_really) def cleanup(self): if self.session_info: self.session_info.destroy() UIXpraClient.cleanup(self) def show_session_info(self, *args): if self.session_info and not self.session_info.is_closed: #exists already: just raise its window: self.session_info.set_args(*args) self.session_info.present() return pixbuf = self.get_pixbuf("statistics.png") if not pixbuf: pixbuf = self.get_pixbuf("xpra.png") self.session_info = SessionInfo(self, self.session_name, pixbuf, self._protocol._conn, self.get_pixbuf) self.session_info.set_args(*args) self.session_info.show_all() def get_pixbuf(self, icon_name): try: if not icon_name: log("get_pixbuf(%s)=None", icon_name) return None icon_filename = get_icon_filename(icon_name) log("get_pixbuf(%s) icon_filename=%s", icon_name, icon_filename) if icon_filename: return self.do_get_pixbuf(icon_filename) except: log.error("get_pixbuf(%s)", icon_name, exc_info=True) return None def do_get_pixbuf(self, icon_filename): raise Exception("override me!") def get_image(self, icon_name, size=None): try: pixbuf = self.get_pixbuf(icon_name) log("get_image(%s, %s) pixbuf=%s", icon_name, size, pixbuf) if not pixbuf: return None return self.do_get_image(pixbuf, size) except: log.error("get_image(%s, %s)", icon_name, size, exc_info=True) return None def do_get_image(self, pixbuf, size=None): raise Exception("override me!") def make_keyboard_helper(self, keyboard_sync, key_shortcuts): return GTKKeyboardHelper(self.send, keyboard_sync, key_shortcuts, self.send_layout, self.send_keymap) def _add_statusicon_tray(self, tray_list): #add gtk.StatusIcon tray: try: from xpra.client.gtk_base.statusicon_tray import GTKStatusIconTray tray_list.append(GTKStatusIconTray) except Exception, e: log.warn("failed to load StatusIcon tray: %s" % e) return tray_list
class GTKXpraClient(UIXpraClient, GObjectXpraClient): __gsignals__ = UIXpraClient.__gsignals__ def __init__(self): GObjectXpraClient.__init__(self) UIXpraClient.__init__(self) self.session_info = None self.bug_report = None def init(self, opts): GObjectXpraClient.init(self, opts) UIXpraClient.init(self, opts) def init_ui(self, opts, extra_args=[]): UIXpraClient.init_ui(self, opts, extra_args) icon = self.get_pixbuf("xpra.png") if icon: window_set_default_icon(icon) def run(self): UIXpraClient.run(self) gtk_main_quit_on_fatal_exceptions_enable() self.gtk_main() log( "GTKXpraClient.run_main_loop() main loop ended, returning exit_code=%s", self.exit_code) return self.exit_code def gtk_main(self): raise NotImplementedError() def quit(self, exit_code=0): log("GTKXpraClient.quit(%s) current exit_code=%s", exit_code, self.exit_code) if self.exit_code is None: self.exit_code = exit_code if gtk.main_level() > 0: #if for some reason cleanup() hangs, maybe this will fire... gobject.timeout_add(4 * 1000, gtk_main_quit_really) #try harder!: def force_quit(): from xpra import os_util os_util.force_quit() gobject.timeout_add(5 * 1000, force_quit) self.cleanup() if gtk.main_level() > 0: log( "GTKXpraClient.quit(%s) main loop at level %s, calling gtk quit via timeout", exit_code, gtk.main_level()) gobject.timeout_add(500, gtk_main_quit_really) def cleanup(self): if self.session_info: self.session_info.destroy() self.session_info = None if self.bug_report: self.bug_report.destroy() self.bug_report = None UIXpraClient.cleanup(self) def show_session_info(self, *args): if self.session_info and not self.session_info.is_closed: #exists already: just raise its window: self.session_info.set_args(*args) self.session_info.present() return pixbuf = self.get_pixbuf("statistics.png") if not pixbuf: pixbuf = self.get_pixbuf("xpra.png") self.session_info = SessionInfo(self, self.session_name, pixbuf, self._protocol._conn, self.get_pixbuf) self.session_info.set_args(*args) self.session_info.show_all() def show_bug_report(self, *args): self.send_info_request() if self.bug_report: self.bug_report.show() return from xpra.client.gtk_base.bug_report import BugReport self.bug_report = BugReport() def init_bug_report(): #skip things we aren't using: includes = { "keyboard": bool(self.keyboard_helper), "opengl": self.opengl_enabled, } self.bug_report.init(show_about=False, xpra_info=self.server_last_info, opengl_info=self.opengl_props, includes=includes) self.bug_report.show() #ugly: gives the server time to send an info response.. self.timeout_add(1500, init_bug_report) def get_pixbuf(self, icon_name): try: if not icon_name: log("get_pixbuf(%s)=None", icon_name) return None icon_filename = get_icon_filename(icon_name) log("get_pixbuf(%s) icon_filename=%s", icon_name, icon_filename) if icon_filename: return pixbuf_new_from_file(icon_filename) except: log.error("get_pixbuf(%s)", icon_name, exc_info=True) return None def get_image(self, icon_name, size=None): try: pixbuf = self.get_pixbuf(icon_name) log("get_image(%s, %s) pixbuf=%s", icon_name, size, pixbuf) if not pixbuf: return None return scaled_image(pixbuf, size) except: log.error("get_image(%s, %s)", icon_name, size, exc_info=True) return None def make_keyboard_helper(self, keyboard_sync, key_shortcuts): return GTKKeyboardHelper(self.send, keyboard_sync, key_shortcuts) def _add_statusicon_tray(self, tray_list): #add gtk.StatusIcon tray: try: from xpra.client.gtk_base.statusicon_tray import GTKStatusIconTray tray_list.append(GTKStatusIconTray) except Exception, e: log.warn("failed to load StatusIcon tray: %s" % e) return tray_list
class GTKXpraClient(UIXpraClient, GObjectXpraClient): __gsignals__ = UIXpraClient.__gsignals__ ClientWindowClass = None GLClientWindowClass = None def __init__(self): GObjectXpraClient.__init__(self) UIXpraClient.__init__(self) self.session_info = None self.bug_report = None self.start_new_command = None #opengl bits: self.client_supports_opengl = False self.opengl_enabled = False self.opengl_props = {} self.gl_max_viewport_dims = 0, 0 self.gl_texture_size_limit = 0 self._cursors = weakref.WeakKeyDictionary() #frame request hidden window: self.frame_request_window = None #group leader bits: self._ref_to_group_leader = {} self._group_leader_wids = {} self._set_window_menu = get_menu_support_function() self.connect("scaling-changed", self.reset_windows_cursors) def init(self, opts): GObjectXpraClient.init(self, opts) UIXpraClient.init(self, opts) def setup_frame_request_windows(self): #query the window manager to get the frame size: from xpra.gtk_common.error import xsync from xpra.x11.gtk_x11.send_wm import send_wm_request_frame_extents self.frame_request_window = gtk.Window(WINDOW_TOPLEVEL) self.frame_request_window.set_title("Xpra-FRAME_EXTENTS") root = self.get_root_window() self.frame_request_window.realize() with xsync: win = self.frame_request_window.get_window() framelog("setup_frame_request_windows() window=%#x", get_xwindow(win)) send_wm_request_frame_extents(root, win) def run(self): log("run() HAS_X11_BINDINGS=%s", HAS_X11_BINDINGS) if HAS_X11_BINDINGS: self.setup_frame_request_windows() UIXpraClient.run(self) gtk_main_quit_on_fatal_exceptions_enable() self.gtk_main() log("GTKXpraClient.run_main_loop() main loop ended, returning exit_code=%s", self.exit_code) return self.exit_code def gtk_main(self): raise NotImplementedError() def quit(self, exit_code=0): log("GTKXpraClient.quit(%s) current exit_code=%s", exit_code, self.exit_code) if self.exit_code is None: self.exit_code = exit_code if gtk.main_level()>0: #if for some reason cleanup() hangs, maybe this will fire... self.timeout_add(4*1000, self.exit) #try harder!: def force_quit(): from xpra import os_util os_util.force_quit() self.timeout_add(5*1000, force_quit) self.cleanup() log("GTKXpraClient.quit(%s) cleanup done, main_level=%s", exit_code, gtk.main_level()) if gtk.main_level()>0: log("GTKXpraClient.quit(%s) main loop at level %s, calling gtk quit via timeout", exit_code, gtk.main_level()) self.timeout_add(500, self.exit) def exit(self): log("GTKXpraClient.exit() calling %s", gtk_main_quit_really) gtk_main_quit_really() def cleanup(self): if self.session_info: self.session_info.destroy() self.session_info = None if self.bug_report: self.bug_report.destroy() self.bug_report = None if self.start_new_command: self.start_new_command.destroy() self.start_new_command = None UIXpraClient.cleanup(self) def show_start_new_command(self, *args): log("show_start_new_command%s current start_new_command=%s, flag=%s", args, self.start_new_command, self.start_new_commands) if self.start_new_command is None: from xpra.client.gtk_base.start_new_command import getStartNewCommand def run_command_cb(command, sharing=True): self.send_start_command(command, command, False, sharing) self.start_new_command = getStartNewCommand(run_command_cb, self.server_supports_sharing and self.server_supports_window_filters) self.start_new_command.show() return self.start_new_command def show_file_upload(self, *args): filelog("show_file_upload%s can open=%s", args, self.server_open_files) buttons = [gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL] if self.server_open_files: buttons += [gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT] buttons += [gtk.STOCK_OK, gtk.RESPONSE_OK] dialog = gtk.FileChooserDialog("File to upload", parent=None, action=gtk.FILE_CHOOSER_ACTION_OPEN, buttons=tuple(buttons)) dialog.set_default_response(gtk.RESPONSE_OK) v = dialog.run() if v not in (gtk.RESPONSE_OK, gtk.RESPONSE_ACCEPT): filelog("dialog response code %s", v) dialog.destroy() return filename = dialog.get_filename() gfile = dialog.get_file() data, filesize, entity = gfile.load_contents() filelog("load_contents: filename=%s, %i bytes, entity=%s, response=%s", filename, filesize, entity, v) dialog.destroy() self.send_file(filename, data, filesize, openit=(v==gtk.RESPONSE_ACCEPT)) def show_about(self, *args): from xpra.client.gtk_base.about import about about() def show_session_info(self, *args): if self.session_info and not self.session_info.is_closed: #exists already: just raise its window: self.session_info.set_args(*args) self.session_info.present() return pixbuf = self.get_pixbuf("statistics.png") if not pixbuf: pixbuf = self.get_pixbuf("xpra.png") self.session_info = SessionInfo(self, self.session_name, pixbuf, self._protocol._conn, self.get_pixbuf) self.session_info.set_args(*args) self.session_info.show_all() def show_bug_report(self, *args): self.send_info_request() if self.bug_report: self.bug_report.show() return from xpra.client.gtk_base.bug_report import BugReport self.bug_report = BugReport() def init_bug_report(): #skip things we aren't using: includes ={ "keyboard" : bool(self.keyboard_helper), "opengl" : self.opengl_enabled, } def get_server_info(): return self.server_last_info self.bug_report.init(show_about=False, get_server_info=get_server_info, opengl_info=self.opengl_props, includes=includes) self.bug_report.show() #gives the server time to send an info response.. #(by the time the user clicks on copy, it should have arrived, we hope!) self.timeout_add(200, init_bug_report) def get_pixbuf(self, icon_name): try: if not icon_name: log("get_pixbuf(%s)=None", icon_name) return None icon_filename = get_icon_filename(icon_name) log("get_pixbuf(%s) icon_filename=%s", icon_name, icon_filename) if icon_filename: return pixbuf_new_from_file(icon_filename) except: log.error("get_pixbuf(%s)", icon_name, exc_info=True) return None def get_image(self, icon_name, size=None): try: pixbuf = self.get_pixbuf(icon_name) log("get_image(%s, %s) pixbuf=%s", icon_name, size, pixbuf) if not pixbuf: return None return scaled_image(pixbuf, size) except: log.error("get_image(%s, %s)", icon_name, size, exc_info=True) return None def request_frame_extents(self, window): from xpra.x11.gtk_x11.send_wm import send_wm_request_frame_extents from xpra.gtk_common.error import xsync root = self.get_root_window() with xsync: win = window.get_window() framelog("request_frame_extents(%s) xid=%#x", window, get_xwindow(win)) send_wm_request_frame_extents(root, win) def get_frame_extents(self, window): #try native platform code first: x, y = window.get_position() w, h = window.get_size() v = get_window_frame_size(x, y, w, h) framelog("get_window_frame_size%s=%s", (x, y, w, h), v) if v: #(OSX does give us these values via Quartz API) return v if not HAS_X11_BINDINGS: #nothing more we can do! return None from xpra.x11.gtk_x11.prop import prop_get gdkwin = window.get_window() assert gdkwin v = prop_get(gdkwin, "_NET_FRAME_EXTENTS", ["u32"], ignore_errors=False) framelog("get_frame_extents(%s)=%s", window.get_title(), v) return v def get_window_frame_sizes(self): wfs = get_window_frame_sizes() if self.frame_request_window: v = self.get_frame_extents(self.frame_request_window) if v: try: l, r, t, b = v wfs["frame"] = (l, r, t, b) wfs["offset"] = (l, t) except Exception as e: framelog.warn("Warning: invalid frame extents value '%s'", v) framelog.warn(" %s", e) pass framelog("get_window_frame_sizes()=%s", wfs) return wfs def make_keyboard_helper(self, keyboard_sync, key_shortcuts): return GTKKeyboardHelper(self.send, keyboard_sync, key_shortcuts) def _add_statusicon_tray(self, tray_list): #add gtk.StatusIcon tray: try: from xpra.client.gtk_base.statusicon_tray import GTKStatusIconTray tray_list.append(GTKStatusIconTray) except Exception as e: log.warn("failed to load StatusIcon tray: %s" % e) return tray_list def get_tray_classes(self): return self._add_statusicon_tray(UIXpraClient.get_tray_classes(self)) def get_system_tray_classes(self): return self._add_statusicon_tray(UIXpraClient.get_system_tray_classes(self)) def supports_system_tray(self): #always True: we can always use gtk.StatusIcon as fallback return True def cook_metadata(self, new_window, metadata): metadata = UIXpraClient.cook_metadata(self, new_window, metadata) #ensures we will call set_window_menu for this window when we create it: if new_window and b"menu" not in metadata and self._set_window_menu: metadata[b"menu"] = {} return metadata def set_window_menu(self, add, wid, menus, application_action_callback=None, window_action_callback=None): assert self._set_window_menu model = self._id_to_window.get(wid) window = None if model: window = model.get_window() self._set_window_menu(add, wid, window, menus, application_action_callback, window_action_callback) def get_root_window(self): return get_default_root_window() def get_root_size(self): return get_root_size() def get_mouse_position(self): p = self.get_root_window().get_pointer() return self.cp(p[0], p[1]) def get_current_modifiers(self): modifiers_mask = self.get_root_window().get_pointer()[-1] return self.mask_to_names(modifiers_mask) def make_hello(self): capabilities = UIXpraClient.make_hello(self) capabilities["named_cursors"] = len(cursor_types)>0 capabilities.update(get_gtk_version_info()) #tell the server which icons GTK can use #so it knows when it should supply one as fallback it = icon_theme_get_default() #this would add our bundled icon directory #to the search path, but I don't think we have #any extra icons that matter in there: #from xpra.platform.paths import get_icon_dir #d = get_icon_dir() #if d not in it.get_search_path(): # it.append_search_path(d) # it.rescan_if_needed() log("default icon theme: %s", it) log("icon search path: %s", it.get_search_path()) log("contexts: %s", it.list_contexts()) icons = [] for context in it.list_contexts(): icons += it.list_icons(context) log("icons: %s", icons) capabilities["theme.default.icons"] = list(set(icons)) if METADATA_SUPPORTED: ms = [x.strip() for x in METADATA_SUPPORTED.split(",")] else: #this is currently unused, and slightly redundant because of metadata.supported below: capabilities["window.states"] = ["fullscreen", "maximized", "sticky", "above", "below", "shaded", "iconified", "skip-taskbar", "skip-pager"] ms = list(DEFAULT_METADATA_SUPPORTED) #added in 0.15: ms += ["command", "workspace", "above", "below", "sticky"] if os.name=="posix": #this is only really supported on X11, but posix is easier to check for.. #"strut" and maybe even "fullscreen-monitors" could also be supported on other platforms I guess ms += ["shaded", "bypass-compositor", "strut", "fullscreen-monitors"] if HAS_X11_BINDINGS: ms += ["shape"] if self._set_window_menu: ms += ["menu"] #figure out if we can handle the "global menu" stuff: if os.name=="posix" and not sys.platform.startswith("darwin"): try: from xpra.dbus.helper import DBusHelper assert DBusHelper except: pass log("metadata.supported: %s", ms) capabilities["metadata.supported"] = ms #we need the bindings to support initiate-moveresize (posix only for now): updict(capabilities, "window", { "initiate-moveresize" : HAS_X11_BINDINGS, "configure.pointer" : True, "frame_sizes" : self.get_window_frame_sizes() }) from xpra.client.window_backing_base import DELTA_BUCKETS updict(capabilities, "encoding", { "icons.greedy" : True, #we don't set a default window icon any more "icons.size" : (64, 64), #size we want "icons.max_size" : (128, 128), #limit "delta_buckets" : DELTA_BUCKETS, }) return capabilities def has_transparency(self): return screen_get_default().get_rgba_visual() is not None def get_screen_sizes(self, xscale=1, yscale=1): def xs(v): return iround(v/xscale) def ys(v): return iround(v/yscale) def swork(*workarea): return xs(workarea[0]), ys(workarea[1]), xs(workarea[2]), ys(workarea[3]) display = display_get_default() i=0 screen_sizes = [] n_screens = display.get_n_screens() screenlog("get_screen_sizes(%f, %f) found %s screens", xscale, yscale, n_screens) while i<n_screens: screen = display.get_screen(i) j = 0 monitors = [] workareas = [] #native "get_workareas()" is only valid for a single screen (but describes all the monitors) #and it is only implemented on win32 right now #other platforms only implement "get_workarea()" instead, which is reported against the screen n_monitors = screen.get_n_monitors() screenlog(" screen %s has %s monitors", i, n_monitors) if n_screens==1: workareas = get_workareas() if len(workareas)!=n_monitors: screenlog(" workareas: %s", workareas) screenlog(" number of monitors does not match number of workareas!") workareas = [] while j<screen.get_n_monitors(): geom = screen.get_monitor_geometry(j) plug_name = "" if hasattr(screen, "get_monitor_plug_name"): plug_name = screen.get_monitor_plug_name(j) or "" wmm = -1 if hasattr(screen, "get_monitor_width_mm"): wmm = screen.get_monitor_width_mm(j) hmm = -1 if hasattr(screen, "get_monitor_height_mm"): hmm = screen.get_monitor_height_mm(j) monitor = [plug_name, xs(geom.x), ys(geom.y), xs(geom.width), ys(geom.height), wmm, hmm] screenlog(" monitor %s: %s", j, monitor) if workareas: w = workareas[j] monitor += list(swork(*w)) monitors.append(tuple(monitor)) j += 1 work_x, work_y, work_width, work_height = swork(0, 0, screen.get_width(), screen.get_height()) workarea = get_workarea() if workarea: work_x, work_y, work_width, work_height = swork(*workarea) screenlog(" workarea=%s", workarea) item = (screen.make_display_name(), xs(screen.get_width()), ys(screen.get_height()), screen.get_width_mm(), screen.get_height_mm(), monitors, work_x, work_y, work_width, work_height) screenlog(" screen %s: %s", i, item) screen_sizes.append(item) i += 1 return screen_sizes def reset_windows_cursors(self, *args): cursorlog("reset_windows_cursors() resetting cursors for: %s", self._cursors.keys()) for w,cursor_data in list(self._cursors.items()): self.set_windows_cursor([w], cursor_data) def set_windows_cursor(self, windows, cursor_data): cursorlog("set_windows_cursor(%s, ..)", windows) cursor = None if cursor_data: try: cursor = self.make_cursor(cursor_data) cursorlog("make_cursor(..)=%s", cursor) except Exception as e: log.warn("error creating cursor: %s (using default)", e, exc_info=True) if cursor is None: #use default: cursor = get_default_cursor() for w in windows: gdkwin = w.get_window() #trays don't have a gdk window if gdkwin: self._cursors[w] = cursor_data gdkwin.set_cursor(cursor) def make_cursor(self, cursor_data): #if present, try cursor ny name: display = display_get_default() cursorlog("make_cursor: has-name=%s, has-cursor-types=%s, xscale=%s, yscale=%s, USE_LOCAL_CURSORS=%s", len(cursor_data)>=9, bool(cursor_types), self.xscale, self.yscale, USE_LOCAL_CURSORS) #named cursors cannot be scaled (round to 10 to compare so 0.95 and 1.05 are considered the same as 1.0, no scaling): if len(cursor_data)>=9 and cursor_types and iround(self.xscale*10)==10 and iround(self.yscale*10)==10: cursor_name = bytestostr(cursor_data[8]) if cursor_name and USE_LOCAL_CURSORS: gdk_cursor = cursor_types.get(cursor_name.upper()) if gdk_cursor is not None: cursorlog("setting new cursor by name: %s=%s", cursor_name, gdk_cursor) return new_Cursor_for_display(display, gdk_cursor) else: global missing_cursor_names if cursor_name not in missing_cursor_names: cursorlog("cursor name '%s' not found", cursor_name) missing_cursor_names.add(cursor_name) #create cursor from the pixel data: w, h, xhot, yhot, serial, pixels = cursor_data[2:8] if len(pixels)<w*h*4: import binascii cursorlog.warn("not enough pixels provided in cursor data: %s needed and only %s bytes found (%s)", w*h*4, len(pixels), binascii.hexlify(pixels)[:100]) return pixbuf = get_pixbuf_from_data(pixels, True, w, h, w*4) x = max(0, min(xhot, w-1)) y = max(0, min(yhot, h-1)) csize = display.get_default_cursor_size() cmaxw, cmaxh = display.get_maximal_cursor_size() if len(cursor_data)>=11: ssize = cursor_data[9] smax = cursor_data[10] cursorlog("server cursor sizes: default=%s, max=%s", ssize, smax) cursorlog("new cursor at %s,%s with serial=%s, dimensions: %sx%s, len(pixels)=%s, default cursor size is %s, maximum=%s", xhot,yhot, serial, w,h, len(pixels), csize, (cmaxw, cmaxh)) fw, fh = get_fixed_cursor_size() if fw>0 and fh>0 and (w!=fw or h!=fh): #OS wants a fixed cursor size! (win32 does, and GTK doesn't do this for us) if w<=fw and h<=fh: cursorlog("pasting cursor of size %ix%i onto clear pixbuf of size %ix%i", w, h, fw, fh) cursor_pixbuf = get_pixbuf_from_data("\0"*fw*fh*4, True, fw, fh, fw*4) pixbuf.copy_area(0, 0, w, h, cursor_pixbuf, 0, 0) else: cursorlog("scaling cursor from %ix%i to fixed OS size %ix%i", w, h, fw, fh) cursor_pixbuf = pixbuf.scale_simple(fw, fh, INTERP_BILINEAR) xratio, yratio = float(w)/fw, float(h)/fh x, y = iround(x/xratio), iround(y/yratio) else: sx, sy, sw, sh = x, y, w, h #scale the cursors: if self.xscale!=1 or self.yscale!=1: sx, sy, sw, sh = self.srect(x, y, w, h) sw = max(1, sw) sh = max(1, sh) #ensure we honour the max size if there is one: if (cmaxw>0 and sw>cmaxw) or (cmaxh>0 and sh>cmaxh): ratio = 1.0 if cmaxw>0: ratio = max(ratio, float(w)/cmaxw) if cmaxh>0: ratio = max(ratio, float(h)/cmaxh) cursorlog("clamping cursor size to %ix%i using ratio=%s", cmaxw, cmaxh, ratio) sx, sy, sw, sh = iround(x/ratio), iround(y/ratio), min(cmaxw, iround(w/ratio)), min(cmaxh, iround(h/ratio)) if sw!=w or sh!=h: cursorlog("scaling cursor from %ix%i hotspot at %ix%i to %ix%i hotspot at %ix%i", w, h, x, y, sw, sh, sx, sy) cursor_pixbuf = pixbuf.scale_simple(sw, sh, INTERP_BILINEAR) x, y = sx, sy else: cursor_pixbuf = pixbuf #clamp to pixbuf size: w = cursor_pixbuf.get_width() h = cursor_pixbuf.get_height() x = max(0, min(x, w-1)) y = max(0, min(y, h-1)) try: c = new_Cursor_from_pixbuf(display, cursor_pixbuf, x, y) except RuntimeError as e: log.error("Error: failed to create cursor:") log.error(" %s", e) log.error(" using %s of size %ix%i with hotspot at %ix%i", cursor_pixbuf, w, h, x, y) c = None return c def process_ui_capabilities(self): UIXpraClient.process_ui_capabilities(self) if self.server_randr: display = display_get_default() i=0 while i<display.get_n_screens(): screen = display.get_screen(i) screen.connect("size-changed", self.screen_size_changed) i += 1 def window_bell(self, window, device, percent, pitch, duration, bell_class, bell_id, bell_name): gdkwindow = None if window: gdkwindow = window.get_window() if gdkwindow is None: gdkwindow = self.get_root_window() log("window_bell(..) gdkwindow=%s", gdkwindow) if not system_bell(gdkwindow, device, percent, pitch, duration, bell_class, bell_id, bell_name): #fallback to simple beep: gdk.beep() #OpenGL bits: def init_opengl(self, enable_opengl): opengllog("init_opengl(%s)", enable_opengl) #enable_opengl can be True, False or None (auto-detect) if enable_opengl is False: self.opengl_props["info"] = "disabled by configuration" return from xpra.scripts.config import OpenGL_safety_check from xpra.platform.gui import gl_check as platform_gl_check warnings = [] for check in (OpenGL_safety_check, platform_gl_check): opengllog("checking with %s", check) warning = check() opengllog("%s()=%s", check, warning) if warning: warnings.append(warning) self.opengl_props["info"] = "" if warnings: if enable_opengl is True: opengllog.warn("OpenGL safety warning (enabled at your own risk):") for warning in warnings: opengllog.warn(" %s", warning) self.opengl_props["info"] = "forced enabled despite: %s" % (", ".join(warnings)) else: opengllog.warn("OpenGL disabled:", warning) for warning in warnings: opengllog.warn(" %s", warning) self.opengl_props["info"] = "disabled: %s" % (", ".join(warnings)) return try: opengllog("init_opengl: going to import xpra.client.gl") __import__("xpra.client.gl", {}, {}, []) __import__("xpra.client.gl.gtk_compat", {}, {}, []) gl_check = __import__("xpra.client.gl.gl_check", {}, {}, ["check_support"]) opengllog("init_opengl: gl_check=%s", gl_check) self.opengl_props = gl_check.check_support(force_enable=(enable_opengl is True)) opengllog("init_opengl: found props %s", self.opengl_props) GTK_GL_CLIENT_WINDOW_MODULE = "xpra.client.gl.gtk%s.gl_client_window" % (2+int(is_gtk3())) opengllog("init_opengl: trying to load GL client window module '%s'", GTK_GL_CLIENT_WINDOW_MODULE) gl_client_window = __import__(GTK_GL_CLIENT_WINDOW_MODULE, {}, {}, ["GLClientWindow"]) self.GLClientWindowClass = gl_client_window.GLClientWindow self.client_supports_opengl = True #only enable opengl by default if force-enabled or if safe to do so: self.opengl_enabled = (enable_opengl is True) or self.opengl_props.get("safe", False) self.gl_texture_size_limit = self.opengl_props.get("texture-size-limit", 16*1024) self.gl_max_viewport_dims = self.opengl_props.get("max-viewport-dims", (self.gl_texture_size_limit, self.gl_texture_size_limit)) if min(self.gl_max_viewport_dims)<4*1024: opengllog.warn("Warning: OpenGL is disabled:") opengllog.warn(" the maximum viewport size is too low: %s", self.gl_max_viewport_dims) self.opengl_enabled = False elif self.gl_texture_size_limit<4*1024: opengllog.warn("Warning: OpenGL is disabled:") opengllog.warn(" the texture size limit is too low: %s", self.gl_texture_size_limit) self.opengl_enabled = False self.GLClientWindowClass.MAX_VIEWPORT_DIMS = self.gl_max_viewport_dims self.GLClientWindowClass.MAX_BACKING_DIMS = self.gl_texture_size_limit, self.gl_texture_size_limit self.GLClientWindowClass.MAX_VIEWPORT_DIMS = 8192, 8192 self.GLClientWindowClass.MAX_BACKING_DIMS = 4096, 4096 mww, mwh = self.max_window_size opengllog("OpenGL: enabled=%s, texture-size-limit=%s, max-window-size=%s", self.opengl_enabled, self.gl_texture_size_limit, self.max_window_size) 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): #log at warn level if the limit is low: #(if we're likely to hit it - if the screen is as big or bigger) w, h = self.get_root_size() l = opengllog.info if w>=self.gl_texture_size_limit or h>=self.gl_texture_size_limit: l = log.warn l("Warning: OpenGL windows will be clamped to the maximum texture size %ix%i", self.gl_texture_size_limit, self.gl_texture_size_limit) l(" for OpenGL %s renderer '%s'", pver(self.opengl_props.get("opengl", "")), self.opengl_props.get("renderer", "unknown")) driver_info = self.opengl_props.get("renderer") or self.opengl_props.get("vendor") or "unknown card" if self.opengl_enabled: opengllog.info("OpenGL enabled with %s", driver_info) elif self.client_supports_opengl: opengllog("OpenGL supported with %s, but not enabled", driver_info) except ImportError as e: opengllog.warn("OpenGL support is missing:") opengllog.warn(" %s", e) self.opengl_props["info"] = str(e) except RuntimeError as e: opengllog.warn("OpenGL support could not be enabled on this hardware:") opengllog.warn(" %s", e) self.opengl_props["info"] = str(e) except Exception as e: opengllog.error("Error loading OpenGL support:") opengllog.error(" %s", e, exc_info=True) self.opengl_props["info"] = str(e) def get_client_window_classes(self, w, h, metadata, override_redirect): log("get_client_window_class(%i, %i, %s, %s) GLClientWindowClass=%s, opengl_enabled=%s, mmap_enabled=%s, encoding=%s", w, h, metadata, override_redirect, self.GLClientWindowClass, self.opengl_enabled, self.mmap_enabled, self.encoding) ms = min(self.sx(self.gl_texture_size_limit), *self.gl_max_viewport_dims) if self.GLClientWindowClass is None or not self.opengl_enabled or w>ms or h>ms: return [self.ClientWindowClass] return [self.GLClientWindowClass, self.ClientWindowClass] def toggle_opengl(self, *args): assert self.window_unmap, "server support for 'window_unmap' is required for toggling opengl at runtime" self.opengl_enabled = not self.opengl_enabled opengllog("opengl_toggled: %s", self.opengl_enabled) def fake_send(*args): opengllog("fake_send(%s)", args) #now replace all the windows with new ones: for wid, window in self._id_to_window.items(): if window.is_tray(): #trays are never GL enabled, so don't bother re-creating them #(might cause problems anyway if we did) continue #ignore packets from old window: window.send = fake_send #copy attributes: x, y = window._pos ww, wh = window._size try: bw, bh = window._backing.size except: bw, bh = ww, wh client_properties = window._client_properties metadata = window._metadata override_redirect = window._override_redirect backing = window._backing video_decoder = None csc_decoder = None decoder_lock = None try: if backing: video_decoder = backing._video_decoder csc_decoder = backing._csc_decoder decoder_lock = backing._decoder_lock if decoder_lock: decoder_lock.acquire() opengllog("toggle_opengl() will preserve video=%s and csc=%s for %s", video_decoder, csc_decoder, wid) backing._video_decoder = None backing._csc_decoder = None backing._decoder_lock = None backing.close() #now we can unmap it: self.destroy_window(wid, window) #explicitly tell the server we have unmapped it: #(so it will reset the video encoders, etc) self.send("unmap-window", wid) try: del self._id_to_window[wid] except: pass try: del self._window_to_id[window] except: pass #create the new window, which should honour the new state of the opengl_enabled flag: window = self.make_new_window(wid, x, y, ww, wh, bw, bh, metadata, override_redirect, client_properties) if video_decoder or csc_decoder: backing = window._backing backing._video_decoder = video_decoder backing._csc_decoder = csc_decoder backing._decoder_lock = decoder_lock finally: if decoder_lock: decoder_lock.release() opengllog("replaced all the windows with opengl=%s: %s", self.opengl_enabled, self._id_to_window) def get_group_leader(self, wid, metadata, override_redirect): transient_for = metadata.intget("transient-for", -1) log("get_group_leader: transient_for=%s", transient_for) if transient_for>0: client_window = self._id_to_window.get(transient_for) if client_window: gdk_window = client_window.get_window() if gdk_window: return gdk_window pid = metadata.intget("pid", -1) leader_xid = metadata.intget("group-leader-xid", -1) leader_wid = metadata.intget("group-leader-wid", -1) group_leader_window = self._id_to_window.get(leader_wid) if group_leader_window: #leader is another managed window log("found group leader window %s for wid=%s", group_leader_window, leader_wid) return group_leader_window log("get_group_leader: leader pid=%s, xid=%s, wid=%s", pid, leader_xid, leader_wid) reftype = "xid" ref = leader_xid if ref<0: reftype = "leader-wid" ref = leader_wid if ref<0: ci = metadata.strlistget("class-instance") if ci: reftype = "class" ref = "|".join(ci) elif pid>0: reftype = "pid" ref = pid elif transient_for>0: #this should have matched a client window above.. #but try to use it anyway: reftype = "transient-for" ref = transient_for else: #no reference to use return None refkey = "%s:%s" % (reftype, ref) group_leader_window = self._ref_to_group_leader.get(refkey) if group_leader_window: log("found existing group leader window %s using ref=%s", group_leader_window, refkey) return group_leader_window #we need to create one: title = "%s group leader for %s" % (self.session_name or "Xpra", pid) #group_leader_window = gdk.Window(None, 1, 1, gdk.WINDOW_TOPLEVEL, 0, gdk.INPUT_ONLY, title) #static new(parent, attributes, attributes_mask) if is_gtk3(): #long winded and annoying attributes = gdk.WindowAttr() attributes.width = 1 attributes.height = 1 attributes.title = title attributes.wclass = gdk.WindowWindowClass.INPUT_ONLY attributes.event_mask = 0 attributes_mask = gdk.WindowAttributesType.TITLE | gdk.WindowAttributesType.WMCLASS group_leader_window = gdk.Window(None, attributes, attributes_mask) group_leader_window.resize(1, 1) else: #gtk2: group_leader_window = gdk.Window(None, 1, 1, gdk.WINDOW_TOPLEVEL, 0, gdk.INPUT_ONLY, title) self._ref_to_group_leader[refkey] = group_leader_window #avoid warning on win32... if not sys.platform.startswith("win"): #X11 spec says window should point to itself: group_leader_window.set_group(group_leader_window) log("new hidden group leader window %s for ref=%s", group_leader_window, refkey) self._group_leader_wids.setdefault(group_leader_window, []).append(wid) return group_leader_window def destroy_window(self, wid, window): #override so we can cleanup the group-leader if needed, UIXpraClient.destroy_window(self, wid, window) group_leader = window.group_leader if group_leader is None or len(self._group_leader_wids)==0: return wids = self._group_leader_wids.get(group_leader) if wids is None: #not recorded any window ids on this group leader #means it is another managed window, leave it alone return if wid in wids: wids.remove(wid) if len(wids)>0: #still has another window pointing to it return #the last window has gone, we can remove the group leader, #find all the references to this group leader: del self._group_leader_wids[group_leader] refs = [] for ref, gl in self._ref_to_group_leader.items(): if gl==group_leader: refs.append(ref) for ref in refs: del self._ref_to_group_leader[ref] log("last window for refs %s is gone, destroying the group leader %s", refs, group_leader) group_leader.destroy()
class GTKXpraClient(UIXpraClient, GObjectXpraClient): __gsignals__ = UIXpraClient.__gsignals__ def __init__(self): GObjectXpraClient.__init__(self) UIXpraClient.__init__(self) self.session_info = None def init(self, opts): GObjectXpraClient.init(self, opts) UIXpraClient.init(self, opts) def run(self): UIXpraClient.run(self) gtk_main_quit_on_fatal_exceptions_enable() self.gtk_main() log( "GTKXpraClient.run_main_loop() main loop ended, returning exit_code=%s", self.exit_code) return self.exit_code def gtk_main(self): raise NotImplementedError() def quit(self, exit_code=0): log("GTKXpraClient.quit(%s) current exit_code=%s", exit_code, self.exit_code) if self.exit_code is None: self.exit_code = exit_code if gtk.main_level() > 0: #if for some reason cleanup() hangs, maybe this will fire... gobject.timeout_add(4 * 1000, gtk_main_quit_really) #try harder!: gobject.timeout_add(5 * 1000, os._exit, 1) self.cleanup() if gtk.main_level() > 0: log( "GTKXpraClient.quit(%s) main loop at level %s, calling gtk quit via timeout", exit_code, gtk.main_level()) gobject.timeout_add(500, gtk_main_quit_really) def cleanup(self): if self.session_info: self.session_info.destroy() UIXpraClient.cleanup(self) def show_session_info(self, *args): if self.session_info and not self.session_info.is_closed: #exists already: just raise its window: self.session_info.set_args(*args) self.session_info.present() return pixbuf = self.get_pixbuf("statistics.png") if not pixbuf: pixbuf = self.get_pixbuf("xpra.png") self.session_info = SessionInfo(self, self.session_name, pixbuf, self._protocol._conn, self.get_pixbuf) self.session_info.set_args(*args) self.session_info.show_all() def get_pixbuf(self, icon_name): try: if not icon_name: log("get_pixbuf(%s)=None", icon_name) return None icon_filename = get_icon_filename(icon_name) log("get_pixbuf(%s) icon_filename=%s", icon_name, icon_filename) if icon_filename: return pixbuf_new_from_file(icon_filename) except: log.error("get_pixbuf(%s)", icon_name, exc_info=True) return None def get_image(self, icon_name, size=None): try: pixbuf = self.get_pixbuf(icon_name) log("get_image(%s, %s) pixbuf=%s", icon_name, size, pixbuf) if not pixbuf: return None return scaled_image(pixbuf, size) except: log.error("get_image(%s, %s)", icon_name, size, exc_info=True) return None def make_keyboard_helper(self, keyboard_sync, key_shortcuts): return GTKKeyboardHelper(self.send, keyboard_sync, key_shortcuts, self.send_layout, self.send_keymap) def _add_statusicon_tray(self, tray_list): #add gtk.StatusIcon tray: try: from xpra.client.gtk_base.statusicon_tray import GTKStatusIconTray tray_list.append(GTKStatusIconTray) except Exception, e: log.warn("failed to load StatusIcon tray: %s" % e) return tray_list
class GTKXpraClient(UIXpraClient, GObjectXpraClient): __gsignals__ = UIXpraClient.__gsignals__ def __init__(self): GObjectXpraClient.__init__(self) UIXpraClient.__init__(self) self.session_info = None self.bug_report = None def init(self, opts): GObjectXpraClient.init(self, opts) UIXpraClient.init(self, opts) def init_ui(self, opts, extra_args=[]): UIXpraClient.init_ui(self, opts, extra_args) icon = self.get_pixbuf("xpra.png") if icon: window_set_default_icon(icon) def run(self): UIXpraClient.run(self) gtk_main_quit_on_fatal_exceptions_enable() self.gtk_main() log("GTKXpraClient.run_main_loop() main loop ended, returning exit_code=%s", self.exit_code) return self.exit_code def gtk_main(self): raise NotImplementedError() def quit(self, exit_code=0): log("GTKXpraClient.quit(%s) current exit_code=%s", exit_code, self.exit_code) if self.exit_code is None: self.exit_code = exit_code if gtk.main_level()>0: #if for some reason cleanup() hangs, maybe this will fire... gobject.timeout_add(4*1000, gtk_main_quit_really) #try harder!: def force_quit(): from xpra import os_util os_util.force_quit() gobject.timeout_add(5*1000, force_quit) self.cleanup() if gtk.main_level()>0: log("GTKXpraClient.quit(%s) main loop at level %s, calling gtk quit via timeout", exit_code, gtk.main_level()) gobject.timeout_add(500, gtk_main_quit_really) def cleanup(self): if self.session_info: self.session_info.destroy() self.session_info = None if self.bug_report: self.bug_report.destroy() self.bug_report = None UIXpraClient.cleanup(self) def show_session_info(self, *args): if self.session_info and not self.session_info.is_closed: #exists already: just raise its window: self.session_info.set_args(*args) self.session_info.present() return pixbuf = self.get_pixbuf("statistics.png") if not pixbuf: pixbuf = self.get_pixbuf("xpra.png") self.session_info = SessionInfo(self, self.session_name, pixbuf, self._protocol._conn, self.get_pixbuf) self.session_info.set_args(*args) self.session_info.show_all() def show_bug_report(self, *args): self.send_info_request() if self.bug_report: self.bug_report.show() return from xpra.client.gtk_base.bug_report import BugReport self.bug_report = BugReport() def init_bug_report(): #skip things we aren't using: includes ={ "keyboard" : bool(self.keyboard_helper), "opengl" : self.opengl_enabled, } self.bug_report.init(show_about=False, xpra_info=self.server_last_info, opengl_info=self.opengl_props, includes=includes) self.bug_report.show() #ugly: gives the server time to send an info response.. self.timeout_add(1500, init_bug_report) def get_pixbuf(self, icon_name): try: if not icon_name: log("get_pixbuf(%s)=None", icon_name) return None icon_filename = get_icon_filename(icon_name) log("get_pixbuf(%s) icon_filename=%s", icon_name, icon_filename) if icon_filename: return pixbuf_new_from_file(icon_filename) except: log.error("get_pixbuf(%s)", icon_name, exc_info=True) return None def get_image(self, icon_name, size=None): try: pixbuf = self.get_pixbuf(icon_name) log("get_image(%s, %s) pixbuf=%s", icon_name, size, pixbuf) if not pixbuf: return None return scaled_image(pixbuf, size) except: log.error("get_image(%s, %s)", icon_name, size, exc_info=True) return None def make_keyboard_helper(self, keyboard_sync, key_shortcuts): return GTKKeyboardHelper(self.send, keyboard_sync, key_shortcuts) def _add_statusicon_tray(self, tray_list): #add gtk.StatusIcon tray: try: from xpra.client.gtk_base.statusicon_tray import GTKStatusIconTray tray_list.append(GTKStatusIconTray) except Exception, e: log.warn("failed to load StatusIcon tray: %s" % e) return tray_list
class GTKTrayMenuBase(object): def __init__(self, client): self.client = client self.session_info = None self.menu = None self.menu_shown = False def build(self): if self.menu is None: show_close = True #or sys.platform.startswith("win") self.menu = self.setup_menu(show_close) return self.menu def show_session_info(self, *args): if self.session_info and not self.session_info.is_closed: self.session_info.present() else: pixbuf = self.client.get_pixbuf("statistics.png") if not pixbuf: pixbuf = self.client.get_pixbuf("xpra.png") self.session_info = SessionInfo(self.client, self.client.session_name, pixbuf, self.client._protocol._conn, self.client.get_pixbuf) self.session_info.show_all() def get_image(self, *args): return self.client.get_image(*args) def setup_menu(self, show_close=True): self.menu_shown = False menu = gtk.Menu() menu.set_title(self.client.session_name or "Xpra") def set_menu_title(*args): #set the real name when available: self.menu.set_title(self.client.session_name) self.client.connect("handshake-complete", set_menu_title) menu.append(self.make_aboutmenuitem()) menu.append(self.make_sessioninfomenuitem()) menu.append(gtk.SeparatorMenuItem()) menu.append(self.make_bellmenuitem()) if self.client.windows_enabled: menu.append(self.make_cursorsmenuitem()) menu.append(self.make_notificationsmenuitem()) if not self.client.readonly: menu.append(self.make_clipboardmenuitem()) if self.client.opengl_enabled: menu.append(self.make_openglmenuitem()) if self.client.windows_enabled and len( self.client.get_encodings()) > 1: menu.append(self.make_encodingsmenuitem()) menu.append(self.make_qualitymenuitem()) menu.append(self.make_speedmenuitem()) if self.client.speaker_allowed and STARTSTOP_SOUND_MENU: menu.append(self.make_speakermenuitem()) if self.client.microphone_allowed and STARTSTOP_SOUND_MENU: menu.append(self.make_microphonemenuitem()) if SHOW_COMPRESSION_MENU: menu.append(self.make_compressionmenu()) if not self.client.readonly and self.client.keyboard_helper: menu.append(self.make_layoutsmenuitem()) if self.client.windows_enabled and not self.client.readonly: menu.append(self.make_keyboardsyncmenuitem()) if self.client.windows_enabled: menu.append(self.make_refreshmenuitem()) menu.append(self.make_raisewindowsmenuitem()) #menu.append(item("Options", "configure", None, self.options)) menu.append(gtk.SeparatorMenuItem()) menu.append(self.make_disconnectmenuitem()) if show_close: menu.append(self.make_closemenuitem()) self.popup_menu_workaround(menu) menu.connect("deactivate", self.menu_deactivated) menu.show_all() return menu def cleanup(self): debug("cleanup() session_info=%s", self.session_info) if self.session_info: self.session_info.destroy() self.session_info = None self.close_menu() close_about() def close_menu(self, *args): if self.menu_shown: self.menu.popdown() self.menu_shown = False def menu_deactivated(self, *args): self.menu_shown = False def activate(self): self.show_menu(1, 0) def popup(self, button, time): self.show_menu(button, time) def show_menu(self, button, time): raise Exception("override me!") def handshake_menuitem(self, *args, **kwargs): """ Same as menuitem() but this one will be disabled until we complete the server handshake """ mi = self.menuitem(*args, **kwargs) mi.set_sensitive(False) def enable_menuitem(*args): mi.set_sensitive(True) self.client.connect("handshake-complete", enable_menuitem) return mi def menuitem(self, title, icon_name=None, tooltip=None, cb=None): """ Utility method for easily creating an ImageMenuItem """ image = None if icon_name: image = self.get_image(icon_name, 24) return menuitem(title, image, tooltip, cb) def checkitem(self, title, cb=None): """ Utility method for easily creating a CheckMenuItem """ check_item = CheckMenuItem(title) if cb: check_item.connect("toggled", cb) check_item.show() return check_item def make_aboutmenuitem(self): return self.menuitem("About Xpra", "information.png", None, about) def make_sessioninfomenuitem(self): title = "Session Info" if self.client.session_name and self.client.session_name != "Xpra session": title = "Info: %s" % self.client.session_name return self.handshake_menuitem(title, "statistics.png", None, self.show_session_info) def make_bellmenuitem(self): def bell_toggled(*args): v = self.bell_menuitem.get_active() changed = self.client.bell_enabled != v self.client.bell_enabled = v if changed and self.client.toggle_cursors_bell_notify: self.client.send_bell_enabled() debug("bell_toggled(%s) bell_enabled=%s", args, self.client.bell_enabled) self.bell_menuitem = self.checkitem("Bell", bell_toggled) self.bell_menuitem.set_sensitive(False) def set_bell_menuitem(*args): self.bell_menuitem.set_active(self.client.bell_enabled) c = self.client can_toggle_bell = c.toggle_cursors_bell_notify and c.server_supports_bell and c.client_supports_bell self.bell_menuitem.set_sensitive(can_toggle_bell) if can_toggle_bell: set_tooltip_text(self.bell_menuitem, "Forward system bell") else: set_tooltip_text( self.bell_menuitem, "Cannot forward the system bell: the feature has been disabled" ) self.client.connect("handshake-complete", set_bell_menuitem) return self.bell_menuitem def make_cursorsmenuitem(self): def cursors_toggled(*args): v = self.cursors_menuitem.get_active() changed = self.client.cursors_enabled != v self.client.cursors_enabled = v if changed and self.client.toggle_cursors_bell_notify: self.client.send_cursors_enabled() if not self.client.cursors_enabled: self.client.reset_cursor() debug("cursors_toggled(%s) cursors_enabled=%s", args, self.client.cursors_enabled) self.cursors_menuitem = self.checkitem("Cursors", cursors_toggled) self.cursors_menuitem.set_sensitive(False) def set_cursors_menuitem(*args): self.cursors_menuitem.set_active(self.client.cursors_enabled) c = self.client can_toggle_cursors = c.toggle_cursors_bell_notify and c.server_supports_cursors and c.client_supports_cursors self.cursors_menuitem.set_sensitive(can_toggle_cursors) if can_toggle_cursors: set_tooltip_text(self.cursors_menuitem, "Forward custom mouse cursors") else: set_tooltip_text( self.cursors_menuitem, "Cannot forward mouse cursors: the feature has been disabled" ) self.client.connect("handshake-complete", set_cursors_menuitem) return self.cursors_menuitem def make_notificationsmenuitem(self): def notifications_toggled(*args): v = self.notifications_menuitem.get_active() changed = self.client.notifications_enabled != v self.client.notifications_enabled = v if changed and self.client.toggle_cursors_bell_notify: self.client.send_notify_enabled() debug("notifications_toggled(%s) notifications_enabled=%s", args, self.client.notifications_enabled) self.notifications_menuitem = self.checkitem("Notifications", notifications_toggled) self.notifications_menuitem.set_sensitive(False) def set_notifications_menuitem(*args): self.notifications_menuitem.set_active( self.client.notifications_enabled) c = self.client can_notify = c.toggle_cursors_bell_notify and c.server_supports_notifications and c.client_supports_notifications self.notifications_menuitem.set_sensitive(can_notify) if can_notify: set_tooltip_text(self.notifications_menuitem, "Forward system notifications") else: set_tooltip_text( self.notifications_menuitem, "Cannot forward system notifications: the feature has been disabled" ) self.client.connect("handshake-complete", set_notifications_menuitem) return self.notifications_menuitem def make_clipboard_togglemenuitem(self): def clipboard_toggled(*args): new_state = self.clipboard_menuitem.get_active() debug("clipboard_toggled(%s) clipboard_enabled=%s, new_state=%s", args, self.client.clipboard_enabled, new_state) if self.client.clipboard_enabled != new_state: self.client.clipboard_enabled = new_state self.client.emit("clipboard-toggled") self.clipboard_menuitem = self.checkitem("Clipboard", clipboard_toggled) self.clipboard_menuitem.set_sensitive(False) def set_clipboard_menuitem(*args): self.clipboard_menuitem.set_active(self.client.clipboard_enabled) c = self.client can_clipboard = c.server_supports_clipboard and c.client_supports_clipboard self.clipboard_menuitem.set_sensitive(can_clipboard) if can_clipboard: set_tooltip_text(self.clipboard_menuitem, "Enable clipboard synchronization") else: set_tooltip_text( self.clipboard_menuitem, "Clipboard synchronization cannot be enabled: disabled by server" ) self.client.connect("handshake-complete", set_clipboard_menuitem) return self.clipboard_menuitem def make_translatedclipboard_optionsmenuitem(self): clipboard_menu = self.menuitem( "Clipboard", "clipboard.png", "Choose which remote clipboard to connect to", None) clipboard_menu.set_sensitive(False) def set_clipboard_menu(*args): clipboard_submenu = gtk.Menu() clipboard_menu.set_submenu(clipboard_submenu) self.popup_menu_workaround(clipboard_submenu) c = self.client can_clipboard = c.server_supports_clipboard and c.client_supports_clipboard and c.server_supports_clipboard debug( "set_clipboard_menu(%s) can_clipboard=%s, server=%s, client=%s", args, can_clipboard, c.server_supports_clipboard, c.client_supports_clipboard) clipboard_menu.set_sensitive(can_clipboard) LABEL_TO_NAME = { "Disabled": None, "Clipboard": "CLIPBOARD", "Primary": "PRIMARY", "Secondary": "SECONDARY" } from xpra.clipboard.translated_clipboard import TranslatedClipboardProtocolHelper for label, remote_clipboard in LABEL_TO_NAME.items(): clipboard_item = CheckMenuItem(label) def remote_clipboard_changed(item): assert can_clipboard item = ensure_item_selected(clipboard_submenu, item) label = item.get_label() remote_clipboard = LABEL_TO_NAME.get(label) old_state = self.client.clipboard_enabled debug( "remote_clipboard_changed(%s) remote_clipboard=%s, old_state=%s", item, remote_clipboard, old_state) send_tokens = False if remote_clipboard is not None: #clipboard is not disabled if self.client.clipboard_helper is None: self.client.setup_clipboard_helper( TranslatedClipboardProtocolHelper) self.client.clipboard_helper.remote_clipboard = remote_clipboard send_tokens = True new_state = True else: self.client.clipboard_helper = None send_tokens = False new_state = False debug( "remote_clipboard_changed(%s) label=%s, remote_clipboard=%s, old_state=%s, new_state=%s", item, label, remote_clipboard, old_state, new_state) if new_state != old_state: self.client.clipboard_enabled = new_state self.client.emit("clipboard-toggled") send_tokens = True if send_tokens and self.client.clipboard_helper: self.client.clipboard_helper.send_all_tokens() active = isinstance(self.client.clipboard_helper, TranslatedClipboardProtocolHelper) \ and self.client.clipboard_helper.remote_clipboard==remote_clipboard clipboard_item.set_active(active) clipboard_item.set_sensitive(can_clipboard) clipboard_item.set_draw_as_radio(True) clipboard_item.connect("toggled", remote_clipboard_changed) clipboard_submenu.append(clipboard_item) clipboard_submenu.show_all() self.client.connect("handshake-complete", set_clipboard_menu) return clipboard_menu def make_clipboardmenuitem(self): try: if self.client.clipboard_helper: from xpra.clipboard.translated_clipboard import TranslatedClipboardProtocolHelper if isinstance(self.client.clipboard_helper, TranslatedClipboardProtocolHelper): return self.make_translatedclipboard_optionsmenuitem() except: log.error("make_clipboardmenuitem()", exc_info=True) return self.make_clipboard_togglemenuitem() def make_keyboardsyncmenuitem(self): def set_keyboard_sync_tooltip(): if not self.client.keyboard_helper: set_tooltip_text(self.keyboard_sync_menuitem, "Keyboard support is not loaded") elif not self.client.toggle_keyboard_sync: set_tooltip_text( self.keyboard_sync_menuitem, "This server does not support changes to keyboard synchronization" ) elif self.client.keyboard_helper.keyboard_sync: set_tooltip_text( self.keyboard_sync_menuitem, "Disable keyboard synchronization (prevents spurious key repeats on high latency connections)" ) else: set_tooltip_text(self.keyboard_sync_menuitem, "Enable keyboard state synchronization") def keyboard_sync_toggled(*args): self.client.keyboard_sync = self.keyboard_sync_menuitem.get_active( ) debug("keyboard_sync_toggled(%s) keyboard_sync=%s", args, self.client.keyboard_sync) set_keyboard_sync_tooltip() self.client.emit("keyboard-sync-toggled") self.keyboard_sync_menuitem = self.checkitem( "Keyboard Synchronization", keyboard_sync_toggled) self.keyboard_sync_menuitem.set_sensitive(False) def set_keyboard_sync_menuitem(*args): self.keyboard_sync_menuitem.set_active( self.client.keyboard_helper.keyboard_sync) self.keyboard_sync_menuitem.set_sensitive( self.client.toggle_keyboard_sync) set_keyboard_sync_tooltip() self.client.connect("handshake-complete", set_keyboard_sync_menuitem) return self.keyboard_sync_menuitem def make_openglmenuitem(self): gl = self.checkitem("OpenGL") def gl_set(*args): debug("gl_set(%s) opengl_enabled=%s, window_unmap=%s", args, self.client.opengl_enabled, self.client.window_unmap) gl.set_active(self.client.opengl_enabled) gl.set_sensitive(self.client.window_unmap) if not self.client.window_unmap: set_tooltip_text(gl, "no server support for runtime switching") return def opengl_toggled(*args): self.client.toggle_opengl() gl.connect("toggled", opengl_toggled) self.client.connect("handshake-complete", gl_set) return gl def make_encodingsmenuitem(self): encodings = self.menuitem("Encoding", "encoding.png", "Choose picture data encoding", None) encodings.set_sensitive(False) def set_encodingsmenuitem(*args): encodings.set_sensitive(not self.client.mmap_enabled) if self.client.mmap_enabled: #mmap disables encoding and uses raw rgb24 encodings.set_label("Encoding") set_tooltip_text( encodings, "memory mapped transfers are in use so picture encoding is disabled" ) else: encodings.set_submenu(self.make_encodingssubmenu()) self.client.connect("handshake-complete", set_encodingsmenuitem) return encodings def make_encodingssubmenu(self, handshake_complete=True): encodings = [ x for x in PREFERED_ENCODING_ORDER if x in self.client.get_encodings() ] encodings_submenu = make_encodingsmenu(self.get_current_encoding, self.set_current_encoding, encodings, self.client.server_encodings) self.popup_menu_workaround(encodings_submenu) return encodings_submenu def get_current_encoding(self): return self.client.encoding def set_current_encoding(self, enc): self.client.set_encoding(enc) #these menus may need updating now: self.set_qualitymenu() self.set_speedmenu() def reset_encoding_options(self, encodings_menu): for x in encodings_menu.get_children(): if isinstance(x, gtk.CheckMenuItem): encoding = x.get_label() active = encoding == self.client.encoding if active != x.get_active(): x.set_active(active) x.set_sensitive(encoding in self.client.server_encodings) def make_qualitymenuitem(self): self.quality = self.menuitem("Quality", "slider.png", "Picture quality", None) self.quality.set_sensitive(False) def may_enable_qualitymenu(*args): self.quality.set_submenu(self.make_qualitysubmenu()) self.set_qualitymenu() self.client.connect("handshake-complete", may_enable_qualitymenu) return self.quality def make_qualitysubmenu(self): quality_submenu = make_min_auto_menu("Quality", MIN_QUALITY_OPTIONS, QUALITY_OPTIONS, self.get_min_quality, self.get_quality, self.set_min_quality, self.set_quality) #WARNING: this changes "min-quality", not "quality" (or at least it tries to..) self.popup_menu_workaround(quality_submenu) quality_submenu.show_all() return quality_submenu def get_min_quality(self): return self.client.min_quality def get_quality(self): return self.client.quality def set_min_quality(self, q): self.client.min_quality = q self.client.quality = 0 self.client.send_min_quality() self.client.send_quality() def set_quality(self, q): self.client.min_quality = 0 self.client.quality = q self.client.send_min_quality() self.client.send_quality() def set_qualitymenu(self, *args): if self.quality: can_use = not self.client.mmap_enabled and self.client.encoding in self.client.server_encodings_with_quality self.quality.set_sensitive(can_use) if not can_use: set_tooltip_text( self.quality, "Not supported with %s encoding" % self.client.encoding) return set_tooltip_text(self.quality, "Minimum picture quality") #now check if lossless is supported: if self.quality.get_submenu(): can_lossless = self.client.encoding in self.client.server_encodings_with_lossless_mode for q, item in self.quality.get_submenu().menu_items.items(): item.set_sensitive(q < 100 or can_lossless) def make_speedmenuitem(self): self.speed = self.menuitem("Speed", "speed.png", "Encoding latency vs size", None) self.speed.set_sensitive(False) def may_enable_speedmenu(*args): self.speed.set_submenu(self.make_speedsubmenu()) self.set_speedmenu() self.client.connect("handshake-complete", may_enable_speedmenu) return self.speed def make_speedsubmenu(self): speed_submenu = make_min_auto_menu("Speed", MIN_SPEED_OPTIONS, SPEED_OPTIONS, self.get_min_speed, self.get_speed, self.set_min_speed, self.set_speed) self.popup_menu_workaround(speed_submenu) return speed_submenu def get_min_speed(self): return self.client.min_speed def get_speed(self): return self.client.speed def set_min_speed(self, s): self.client.min_speed = s self.client.speed = 0 self.client.send_min_speed() self.client.send_speed() def set_speed(self, s): self.client.min_speed = 0 self.client.speed = s self.client.send_min_speed() self.client.send_speed() def set_speedmenu(self, *args): if self.speed: can_use = not self.client.mmap_enabled and self.client.encoding in self.client.server_encodings_with_speed and self.client.change_speed self.speed.set_sensitive(can_use) if self.client.mmap_enabled: set_tooltip_text(self.speed, "Quality is always 100% with mmap") elif not self.client.change_speed: set_tooltip_text(self.speed, "Server does not support changing speed") elif self.client.encoding != "x264": set_tooltip_text( self.speed, "Not supported with %s encoding" % self.client.encoding) else: set_tooltip_text(self.speed, "Encoding latency vs size") def spk_on(self, *args): debug("spk_on(%s)", args) self.client.start_receiving_sound() def spk_off(self, *args): debug("spk_off(%s)", args) self.client.stop_receiving_sound() def make_speakermenuitem(self): speaker = self.menuitem("Speaker", "speaker.png", "Forward sound output from the server") speaker.set_sensitive(False) def is_speaker_on(*args): return self.client.speaker_enabled def speaker_state(*args): if not self.client.server_sound_send: speaker.set_sensitive(False) set_tooltip_text(speaker, "Server does not support speaker forwarding") return speaker.set_sensitive(True) speaker.set_submenu( self.make_soundsubmenu(is_speaker_on, self.spk_on, self.spk_off, "speaker-changed")) self.client.connect("handshake-complete", speaker_state) return speaker def mic_on(self, *args): debug("mic_on(%s)", args) self.client.start_sending_sound() def mic_off(self, *args): debug("mic_off(%s)", args) self.client.stop_sending_sound() def make_microphonemenuitem(self): microphone = self.menuitem("Microphone", "microphone.png", "Forward sound input to the server", None) microphone.set_sensitive(False) def is_microphone_on(*args): return self.client.microphone_enabled def microphone_state(*args): if not self.client.server_sound_receive: microphone.set_sensitive(False) set_tooltip_text( microphone, "Server does not support microphone forwarding") return microphone.set_sensitive(True) microphone.set_submenu( self.make_soundsubmenu(is_microphone_on, self.mic_on, self.mic_off, "microphone-changed")) self.client.connect("handshake-complete", microphone_state) return microphone def make_soundsubmenu(self, is_on_cb, on_cb, off_cb, client_signal): menu = gtk.Menu() menu.ignore_events = False def onoffitem(label, active, cb): c = CheckMenuItem(label) c.set_draw_as_radio(True) c.set_active(active) def submenu_uncheck(item, menu): if not menu.ignore_events: ensure_item_selected(menu, item) c.connect('activate', submenu_uncheck, menu) def check_enabled(item): if not menu.ignore_events and item.get_active(): cb() c.connect('activate', check_enabled) return c is_on = is_on_cb() on = onoffitem("On", is_on, on_cb) off = onoffitem("Off", not is_on, off_cb) menu.append(on) menu.append(off) def client_signalled_change(obj): menu.ignore_events = True is_on = is_on_cb() debug("sound: client_signalled_change(%s) is_on=%s", obj, is_on) if is_on: if not on.get_active(): on.set_active(True) ensure_item_selected(menu, on) else: if not off.get_active(): off.set_active(True) ensure_item_selected(menu, off) menu.ignore_events = False self.client.connect(client_signal, client_signalled_change) #menu.append(gtk.SeparatorMenuItem()) #... self.popup_menu_workaround(menu) menu.show_all() return menu def make_layoutsmenuitem(self): keyboard = self.menuitem("Keyboard", "keyboard.png", "Select your keyboard layout", None) keyboard.set_sensitive(False) self.layout_submenu = gtk.Menu() keyboard.set_submenu(self.layout_submenu) self.popup_menu_workaround(self.layout_submenu) def kbitem(title, layout, variant): def set_layout(item): """ this callback updates the client (and server) if needed """ item = ensure_item_selected(self.layout_submenu, item) layout = item.keyboard_layout variant = item.keyboard_variant if layout != self.client.xkbmap_layout or variant != self.client.xkbmap_variant: debug("keyboard layout selected: %s / %s", layout, variant) self.client.xkbmap_layout = layout self.client.xkbmap_variant = variant self.client.send_layout() l = self.checkitem(title, set_layout) l.set_draw_as_radio(True) l.keyboard_layout = layout l.keyboard_variant = variant return l def keysort(key): c, l = key return c.lower() + l.lower() layout, variant, variants = self.client.keyboard_helper.keyboard.get_layout_spec( ) if layout and len(variants) > 1: #just show all the variants to choose from this layout self.layout_submenu.append( kbitem("%s - Default" % layout, layout, None)) for v in variants: self.layout_submenu.append( kbitem("%s - %s" % (layout, v), layout, v)) else: from xpra.keyboard.layouts import X11_LAYOUTS #show all options to choose from: sorted_keys = list(X11_LAYOUTS.keys()) sorted_keys.sort(key=keysort) for key in sorted_keys: country, language = key layout, variants = X11_LAYOUTS.get(key) name = "%s - %s" % (country, language) if len(variants) > 1: #sub-menu for each variant: variant = self.menuitem(name, tooltip=layout) variant_submenu = gtk.Menu() variant.set_submenu(variant_submenu) self.popup_menu_workaround(variant_submenu) self.layout_submenu.append(variant) variant_submenu.append( kbitem("%s - Default" % layout, layout, None)) for v in variants: variant_submenu.append( kbitem("%s - %s" % (layout, v), layout, v)) else: #no variants: self.layout_submenu.append(kbitem(name, layout, None)) keyboard_helper = self.client.keyboard_helper def set_selected_layout(*args): if keyboard_helper.xkbmap_layout or keyboard_helper.xkbmap_print or keyboard_helper.xkbmap_query: #we have detected a layout #so no need to let the user override it keyboard.hide() return keyboard.set_sensitive(True) layout = keyboard_helper.xkbmap_layout variant = keyboard_helper.xkbmap_variant def is_match(checkitem): return checkitem.keyboard_layout == layout and checkitem.keyboard_variant == variant set_checkeditems(self.layout_submenu, is_match) self.client.connect("handshake-complete", set_selected_layout) return keyboard def make_compressionmenu(self): self.compression = self.menuitem("Compression", "compressed.png", "Network packet compression", None) self.compression.set_sensitive(False) self.compression_submenu = gtk.Menu() self.compression.set_submenu(self.compression_submenu) self.popup_menu_workaround(self.compression_submenu) compression_options = {0: "None"} def set_compression(item): item = ensure_item_selected(self.compression_submenu, item) c = int(item.get_label().replace("None", "0")) if c != self.client.compression_level: debug("setting compression level to %s", c) self.client.set_deflate_level(c) for i in range(0, 10): c = CheckMenuItem(str(compression_options.get(i, i))) c.set_draw_as_radio(True) c.set_active(i == self.client.compression_level) c.connect('activate', set_compression) self.compression_submenu.append(c) def enable_compressionmenu(self): self.compression.set_sensitive(True) self.compression_submenu.show_all() self.client.connect("handshake-complete", enable_compressionmenu) return self.compression def make_refreshmenuitem(self): def force_refresh(*args): debug("force refresh") self.client.send_refresh_all() return self.handshake_menuitem("Refresh", "retry.png", None, force_refresh) def make_raisewindowsmenuitem(self): def raise_windows(*args): for win in self.client._window_to_id.keys(): if not win.is_OR(): win.present() return self.handshake_menuitem("Raise Windows", "raise.png", None, raise_windows) def make_disconnectmenuitem(self): def menu_quit(*args): self.client.quit(0) return self.handshake_menuitem("Disconnect", "quit.png", None, menu_quit) def make_closemenuitem(self): return self.menuitem("Close Menu", "close.png", None, self.close_menu) def popup_menu_workaround(self, menu): #win32 workaround: if sys.platform.startswith("win"): self.add_popup_menu_workaround(menu) def add_popup_menu_workaround(self, menu): """ windows does not automatically close the popup menu when we click outside it so we workaround it by using a timer and closing the menu when the mouse has stayed outside it for more than 0.5s. This code must be added to all the sub-menus of the popup menu too! """ def enter_menu(*args): debug("mouse_in_tray_menu=%s", self.mouse_in_tray_menu) self.mouse_in_tray_menu_counter += 1 self.mouse_in_tray_menu = True def leave_menu(*args): debug("mouse_in_tray_menu=%s", self.mouse_in_tray_menu) self.mouse_in_tray_menu_counter += 1 self.mouse_in_tray_menu = False def check_menu_left(expected_counter): if self.mouse_in_tray_menu: return False if expected_counter != self.mouse_in_tray_menu_counter: return False #counter has changed self.close_menu() gobject.timeout_add(500, check_menu_left, self.mouse_in_tray_menu_counter) self.mouse_in_tray_menu_counter = 0 self.mouse_in_tray_menu = False debug("popup_menu_workaround: adding events callbacks") menu.connect("enter-notify-event", enter_menu) menu.connect("leave-notify-event", leave_menu)
class GTKXpraClient(UIXpraClient, GObjectXpraClient): __gsignals__ = UIXpraClient.__gsignals__ ClientWindowClass = None GLClientWindowClass = None def __init__(self): GObjectXpraClient.__init__(self) UIXpraClient.__init__(self) self.session_info = None self.bug_report = None self.start_new_command = None #opengl bits: self.client_supports_opengl = False self.opengl_enabled = False self.opengl_props = {} self.gl_texture_size_limit = 0 def init(self, opts): GObjectXpraClient.init(self, opts) UIXpraClient.init(self, opts) def run(self): UIXpraClient.run(self) gtk_main_quit_on_fatal_exceptions_enable() self.gtk_main() log("GTKXpraClient.run_main_loop() main loop ended, returning exit_code=%s", self.exit_code) return self.exit_code def gtk_main(self): raise NotImplementedError() def quit(self, exit_code=0): log("GTKXpraClient.quit(%s) current exit_code=%s", exit_code, self.exit_code) if self.exit_code is None: self.exit_code = exit_code if gtk.main_level()>0: #if for some reason cleanup() hangs, maybe this will fire... gobject.timeout_add(4*1000, self.exit) #try harder!: def force_quit(): from xpra import os_util os_util.force_quit() gobject.timeout_add(5*1000, force_quit) self.cleanup() if gtk.main_level()>0: log("GTKXpraClient.quit(%s) main loop at level %s, calling gtk quit via timeout", exit_code, gtk.main_level()) gobject.timeout_add(500, self.exit) def exit(self): log("GTKXpraClient.exit() calling %s", gtk_main_quit_really) gtk_main_quit_really() def cleanup(self): if self.session_info: self.session_info.destroy() self.session_info = None if self.bug_report: self.bug_report.destroy() self.bug_report = None if self.start_new_command: self.start_new_command.destroy() self.start_new_command = None UIXpraClient.cleanup(self) def show_start_new_command(self, *args): log("show_start_new_command%s current start_new_command=%s, flag=%s", args, self.start_new_command, self.start_new_commands) if self.start_new_command is None: from xpra.client.gtk_base.start_new_command import getStartNewCommand def run_command_cb(command): self.send_start_command(command, command, False) self.start_new_command = getStartNewCommand(run_command_cb) self.start_new_command.show() return self.start_new_command def show_session_info(self, *args): if self.session_info and not self.session_info.is_closed: #exists already: just raise its window: self.session_info.set_args(*args) self.session_info.present() return pixbuf = self.get_pixbuf("statistics.png") if not pixbuf: pixbuf = self.get_pixbuf("xpra.png") self.session_info = SessionInfo(self, self.session_name, pixbuf, self._protocol._conn, self.get_pixbuf) self.session_info.set_args(*args) self.session_info.show_all() def show_bug_report(self, *args): self.send_info_request() if self.bug_report: self.bug_report.show() return from xpra.client.gtk_base.bug_report import BugReport self.bug_report = BugReport() def init_bug_report(): #skip things we aren't using: includes ={ "keyboard" : bool(self.keyboard_helper), "opengl" : self.opengl_enabled, } def get_server_info(): return self.server_last_info self.bug_report.init(show_about=False, get_server_info=get_server_info, opengl_info=self.opengl_props, includes=includes) self.bug_report.show() #gives the server time to send an info response.. #(by the time the user clicks on copy, it should have arrived, we hope!) self.timeout_add(200, init_bug_report) def get_pixbuf(self, icon_name): try: if not icon_name: log("get_pixbuf(%s)=None", icon_name) return None icon_filename = get_icon_filename(icon_name) log("get_pixbuf(%s) icon_filename=%s", icon_name, icon_filename) if icon_filename: return pixbuf_new_from_file(icon_filename) except: log.error("get_pixbuf(%s)", icon_name, exc_info=True) return None def get_image(self, icon_name, size=None): try: pixbuf = self.get_pixbuf(icon_name) log("get_image(%s, %s) pixbuf=%s", icon_name, size, pixbuf) if not pixbuf: return None return scaled_image(pixbuf, size) except: log.error("get_image(%s, %s)", icon_name, size, exc_info=True) return None def make_keyboard_helper(self, keyboard_sync, key_shortcuts): return GTKKeyboardHelper(self.send, keyboard_sync, key_shortcuts) def _add_statusicon_tray(self, tray_list): #add gtk.StatusIcon tray: try: from xpra.client.gtk_base.statusicon_tray import GTKStatusIconTray tray_list.append(GTKStatusIconTray) except Exception as e: log.warn("failed to load StatusIcon tray: %s" % e) return tray_list def get_tray_classes(self): return self._add_statusicon_tray(UIXpraClient.get_tray_classes(self)) def get_system_tray_classes(self): return self._add_statusicon_tray(UIXpraClient.get_system_tray_classes(self)) def supports_system_tray(self): #always True: we can always use gtk.StatusIcon as fallback return True def get_root_window(self): raise Exception("override me!") def get_root_size(self): raise Exception("override me!") def get_mouse_position(self): return self.get_root_window().get_pointer()[:2] def get_current_modifiers(self): modifiers_mask = self.get_root_window().get_pointer()[-1] return self.mask_to_names(modifiers_mask) def make_hello(self): capabilities = UIXpraClient.make_hello(self) capabilities["named_cursors"] = len(cursor_types)>0 capabilities.update(get_gtk_version_info()) #tell the server which icons GTK can use #so it knows when it should supply one as fallback it = icon_theme_get_default() #this would add our bundled icon directory #to the search path, but I don't think we have #any extra icons that matter in there: #from xpra.platform.paths import get_icon_dir #d = get_icon_dir() #if d not in it.get_search_path(): # it.append_search_path(d) # it.rescan_if_needed() log("default icon theme: %s", it) log("icon search path: %s", it.get_search_path()) log("contexts: %s", it.list_contexts()) icons = [] for context in it.list_contexts(): icons += it.list_icons(context) log("icons: %s", icons) capabilities["theme.default.icons"] = list(set(icons)) if METADATA_SUPPORTED: ms = [x.strip() for x in METADATA_SUPPORTED.split(",")] else: #this is currently unused, and slightly redundant because of metadata.supported below: capabilities["window.states"] = ["fullscreen", "maximized", "sticky", "above", "below", "shaded", "iconified", "skip-taskbar", "skip-pager"] ms = list(DEFAULT_METADATA_SUPPORTED) #added in 0.15: ms += ["command", "workspace", "above", "below", "sticky"] if os.name=="posix": #this is only really supported on X11, but posix is easier to check for.. #"strut" and maybe even "fullscreen-monitors" could also be supported on other platforms I guess ms += ["shaded", "bypass-compositor", "strut", "fullscreen-monitors"] log("metadata.supported: %s", ms) capabilities["metadata.supported"] = ms #we need the bindings to support initiate-moveresize (posix only for now): from xpra.client.gtk_base.gtk_client_window_base import HAS_X11_BINDINGS capabilities["window.initiate-moveresize"] = HAS_X11_BINDINGS #window icon bits capabilities["encoding.icons.greedy"] = True #we don't set a default window icon any more capabilities["encoding.icons.size"] = 64, 64 #size we want capabilities["encoding.icons.max_size"] = 128, 128 #limit from xpra.client.window_backing_base import DELTA_BUCKETS capabilities["encoding.delta_buckets"] = DELTA_BUCKETS return capabilities def has_transparency(self): return screen_get_default().get_rgba_visual() is not None def get_screen_sizes(self): display = display_get_default() i=0 screen_sizes = [] n_screens = display.get_n_screens() screenlog("get_screen_sizes() found %s screens", n_screens) while i<n_screens: screen = display.get_screen(i) j = 0 monitors = [] workareas = [] #native "get_workareas()" is only valid for a single screen (but describes all the monitors) #and it is only implemented on win32 right now #other platforms only implement "get_workarea()" instead, which is reported against the screen n_monitors = screen.get_n_monitors() screenlog("get_screen_sizes() screen %s has %s monitors", i, n_monitors) if n_screens==1: workareas = get_workareas() if len(workareas)!=n_monitors: screenlog("number of monitors does not match number of workareas!") workareas = [] while j<screen.get_n_monitors(): geom = screen.get_monitor_geometry(j) plug_name = "" if hasattr(screen, "get_monitor_plug_name"): plug_name = screen.get_monitor_plug_name(j) or "" wmm = -1 if hasattr(screen, "get_monitor_width_mm"): wmm = screen.get_monitor_width_mm(j) hmm = -1 if hasattr(screen, "get_monitor_height_mm"): hmm = screen.get_monitor_height_mm(j) monitor = [plug_name, geom.x, geom.y, geom.width, geom.height, wmm, hmm] screenlog("get_screen_sizes() monitor %s: %s", j, monitor) if workareas: w = workareas[j] monitor += list(w) monitors.append(tuple(monitor)) j += 1 work_x, work_y = 0, 0 work_width, work_height = screen.get_width(), screen.get_height() workarea = get_workarea() if workarea: work_x, work_y, work_width, work_height = workarea screenlog("get_screen_sizes() workarea=%s", workarea) item = (screen.make_display_name(), screen.get_width(), screen.get_height(), screen.get_width_mm(), screen.get_height_mm(), monitors, work_x, work_y, work_width, work_height) screenlog("get_screen_sizes() screen %s: %s", i, item) screen_sizes.append(item) i += 1 return screen_sizes def set_windows_cursor(self, windows, cursor_data): cursorlog("set_windows_cursor(%s, ..)", windows) cursor = None if cursor_data: try: cursor = self.make_cursor(cursor_data) cursorlog("make_cursor(..)=%s", cursor) except Exception as e: log.warn("error creating cursor: %s (using default)", e, exc_info=True) if cursor is None: #use default: cursor = default_Cursor for w in windows: gdkwin = w.get_window() #trays don't have a gdk window if gdkwin: gdkwin.set_cursor(cursor) def make_cursor(self, cursor_data): #if present, try cursor ny name: display = display_get_default() if len(cursor_data)>=9 and cursor_types: cursor_name = bytestostr(cursor_data[8]) if cursor_name: gdk_cursor = cursor_types.get(cursor_name.upper()) if gdk_cursor is not None: cursorlog("setting new cursor by name: %s=%s", cursor_name, gdk_cursor) return new_Cursor_for_display(display, gdk_cursor) else: global missing_cursor_names if cursor_name not in missing_cursor_names: cursorlog("cursor name '%s' not found", cursor_name) missing_cursor_names.add(cursor_name) #create cursor from the pixel data: w, h, xhot, yhot, serial, pixels = cursor_data[2:8] if len(pixels)<w*h*4: import binascii cursorlog.warn("not enough pixels provided in cursor data: %s needed and only %s bytes found (%s)", w*h*4, len(pixels), binascii.hexlify(pixels)[:100]) return pixbuf = get_pixbuf_from_data(pixels, True, w, h, w*4) x = max(0, min(xhot, w-1)) y = max(0, min(yhot, h-1)) csize = display.get_default_cursor_size() cmaxw, cmaxh = display.get_maximal_cursor_size() if len(cursor_data)>=11: ssize = cursor_data[9] smax = cursor_data[10] cursorlog("server cursor sizes: default=%s, max=%s", ssize, smax) cursorlog("new cursor at %s,%s with serial=%s, dimensions: %sx%s, len(pixels)=%s, default cursor size is %s, maximum=%s", xhot,yhot, serial, w,h, len(pixels), csize, (cmaxw, cmaxh)) fw, fh = get_fixed_cursor_size() if fw>0 and fh>0 and (w!=fw or h!=fh): #OS wants a fixed cursor size! (win32 does, and GTK doesn't do this for us) if w<=fw and h<=fh: cursorlog("pasting cursor of size %ix%i onto clear pixbuf of size %ix%i", w, h, fw, fh) cursor_pixbuf = get_pixbuf_from_data("\0"*fw*fh*4, True, fw, fh, fw*4) pixbuf.copy_area(0, 0, w, h, cursor_pixbuf, 0, 0) else: cursorlog("scaling cursor from %ix%i to fixed OS size %ix%i", w, h, fw, fh) cursor_pixbuf = pixbuf.scale_simple(fw, fh, INTERP_BILINEAR) xratio, yratio = float(w)/fw, float(h)/fh x, y = int(x/xratio), int(y/yratio) elif w>cmaxw or h>cmaxh or (csize>0 and (csize<w or csize<h)): ratio = max(float(w)/cmaxw, float(h)/cmaxh, float(max(w,h))/csize) x, y, w, h = int(x/ratio), int(y/ratio), int(w/ratio), int(h/ratio) cursorlog("downscaling cursor %s by %.2f: %sx%s", pixbuf, ratio, w, h) cursor_pixbuf = pixbuf.scale_simple(w, h, INTERP_BILINEAR) else: cursor_pixbuf = pixbuf #clamp to pixbuf size: w = cursor_pixbuf.get_width() h = cursor_pixbuf.get_height() x = max(0, min(x, w-1)) y = max(0, min(y, h-1)) return new_Cursor_from_pixbuf(display, cursor_pixbuf, x, y) def process_ui_capabilities(self): UIXpraClient.process_ui_capabilities(self) if self.server_randr: display = display_get_default() i=0 while i<display.get_n_screens(): screen = display.get_screen(i) screen.connect("size-changed", self.screen_size_changed) i += 1 def window_bell(self, window, device, percent, pitch, duration, bell_class, bell_id, bell_name): gdkwindow = None if window: gdkwindow = window.get_window() if gdkwindow is None: gdkwindow = self.get_root_window() log("window_bell(..) gdkwindow=%s", gdkwindow) if not system_bell(gdkwindow, device, percent, pitch, duration, bell_class, bell_id, bell_name): #fallback to simple beep: gdk.beep() #OpenGL bits: def init_opengl(self, enable_opengl): opengllog("init_opengl(%s)", enable_opengl) #enable_opengl can be True, False or None (auto-detect) if enable_opengl is False: self.opengl_props["info"] = "disabled by configuration" return from xpra.scripts.config import OpenGL_safety_check from xpra.platform.gui import gl_check as platform_gl_check warnings = [] for check in (OpenGL_safety_check, platform_gl_check): warning = check() if warning: warnings.append(warning) self.opengl_props["info"] = "" if warnings: if enable_opengl is True: opengllog.warn("OpenGL safety warning (enabled at your own risk):") for warning in warnings: opengllog.warn(" %s", warning) self.opengl_props["info"] = "forced enabled despite: %s" % (", ".join(warnings)) else: opengllog.warn("OpenGL disabled:", warning) for warning in warnings: opengllog.warn(" %s", warning) self.opengl_props["info"] = "disabled: %s" % (", ".join(warnings)) return try: opengllog("init_opengl: going to import xpra.client.gl") __import__("xpra.client.gl", {}, {}, []) __import__("xpra.client.gl.gtk_compat", {}, {}, []) gl_check = __import__("xpra.client.gl.gl_check", {}, {}, ["check_support"]) opengllog("init_opengl: gl_check=%s", gl_check) self.opengl_props = gl_check.check_support(force_enable=(enable_opengl is True)) opengllog("init_opengl: found props %s", self.opengl_props) GTK_GL_CLIENT_WINDOW_MODULE = "xpra.client.gl.gtk%s.gl_client_window" % (2+int(is_gtk3())) opengllog("init_opengl: trying to load GL client window module '%s'", GTK_GL_CLIENT_WINDOW_MODULE) gl_client_window = __import__(GTK_GL_CLIENT_WINDOW_MODULE, {}, {}, ["GLClientWindow"]) self.GLClientWindowClass = gl_client_window.GLClientWindow self.client_supports_opengl = True #only enable opengl by default if force-enabled or if safe to do so: self.opengl_enabled = (enable_opengl is True) or self.opengl_props.get("safe", False) self.gl_texture_size_limit = self.opengl_props.get("texture-size-limit", 16*1024) if self.gl_texture_size_limit<4*1024: opengllog.warn("OpenGL disabled: the texture size limit is too low (%s)", self.gl_texture_size_limit) self.opengl_enabled = False self.GLClientWindowClass.MAX_TEXTURE_SIZE = self.gl_texture_size_limit mww, mwh = self.max_window_size opengllog("OpenGL: enabled=%s, texture-size-limit=%s, max-window-size=%s", self.opengl_enabled, self.gl_texture_size_limit, self.max_window_size) 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): #log at warn level if the limit is low: #(if we're likely to hit it - if the screen is as big or bigger) w, h = self.get_root_size() l = opengllog.info if w>=self.gl_texture_size_limit or h>=self.gl_texture_size_limit: l = log.warn l("Warning: OpenGL windows will be clamped to the maximum texture size %ix%i", self.gl_texture_size_limit, self.gl_texture_size_limit) l(" for OpenGL %s renderer '%s'", pver(self.opengl_props.get("opengl", "")), self.opengl_props.get("renderer", "unknown")) driver_info = self.opengl_props.get("renderer") or self.opengl_props.get("vendor") or "unknown card" if self.opengl_enabled: opengllog.info("OpenGL enabled with %s", driver_info) elif self.client_supports_opengl: opengllog("OpenGL supported with %s, but not enabled", driver_info) except ImportError as e: opengllog.warn("OpenGL support could not be enabled:") opengllog.warn(" %s", e) self.opengl_props["info"] = str(e) except Exception as e: opengllog.error("Error loading OpenGL support:") opengllog.error(" %s", e, exc_info=True) self.opengl_props["info"] = str(e) def get_client_window_classes(self, w, h, metadata, override_redirect): log("get_client_window_class(%i, %i, %s, %s) GLClientWindowClass=%s, opengl_enabled=%s, mmap_enabled=%s, encoding=%s", w, h, metadata, override_redirect, self.GLClientWindowClass, self.opengl_enabled, self.mmap_enabled, self.encoding) if self.GLClientWindowClass is None or not self.opengl_enabled or w>self.gl_texture_size_limit or h>self.gl_texture_size_limit: return [self.ClientWindowClass] return [self.GLClientWindowClass, self.ClientWindowClass] def toggle_opengl(self, *args): assert self.window_unmap, "server support for 'window_unmap' is required for toggling opengl at runtime" self.opengl_enabled = not self.opengl_enabled opengllog("opengl_toggled: %s", self.opengl_enabled) def fake_send(*args): opengllog("fake_send(%s)", args) #now replace all the windows with new ones: for wid, window in self._id_to_window.items(): if window.is_tray(): #trays are never GL enabled, so don't bother re-creating them #(might cause problems anyway if we did) continue #ignore packets from old window: window.send = fake_send #copy attributes: x, y = window._pos w, h = window._size client_properties = window._client_properties metadata = window._metadata override_redirect = window._override_redirect backing = window._backing video_decoder = None csc_decoder = None decoder_lock = None try: if backing: video_decoder = backing._video_decoder csc_decoder = backing._csc_decoder decoder_lock = backing._decoder_lock if decoder_lock: decoder_lock.acquire() opengllog("toggle_opengl() will preserve video=%s and csc=%s for %s", video_decoder, csc_decoder, wid) backing._video_decoder = None backing._csc_decoder = None backing._decoder_lock = None #now we can unmap it: self.destroy_window(wid, window) #explicitly tell the server we have unmapped it: #(so it will reset the video encoders, etc) self.send("unmap-window", wid) try: del self._id_to_window[wid] except: pass try: del self._window_to_id[window] except: pass #create the new window, which should honour the new state of the opengl_enabled flag: window = self.make_new_window(wid, x, y, w, h, metadata, override_redirect, client_properties) if video_decoder or csc_decoder: backing = window._backing backing._video_decoder = video_decoder backing._csc_decoder = csc_decoder backing._decoder_lock = decoder_lock finally: if decoder_lock: decoder_lock.release() opengllog("replaced all the windows with opengl=%s: %s", self.opengl_enabled, self._id_to_window)
class GTKXpraClient(UIXpraClient, GObjectXpraClient): __gsignals__ = UIXpraClient.__gsignals__ ClientWindowClass = None GLClientWindowClass = None def __init__(self): GObjectXpraClient.__init__(self) UIXpraClient.__init__(self) self.session_info = None self.bug_report = None self.start_new_command = None #opengl bits: self.client_supports_opengl = False self.opengl_enabled = False self.opengl_props = {} self.gl_texture_size_limit = 0 def init(self, opts): GObjectXpraClient.init(self, opts) UIXpraClient.init(self, opts) def run(self): UIXpraClient.run(self) gtk_main_quit_on_fatal_exceptions_enable() self.gtk_main() log( "GTKXpraClient.run_main_loop() main loop ended, returning exit_code=%s", self.exit_code) return self.exit_code def gtk_main(self): raise NotImplementedError() def quit(self, exit_code=0): log("GTKXpraClient.quit(%s) current exit_code=%s", exit_code, self.exit_code) if self.exit_code is None: self.exit_code = exit_code if gtk.main_level() > 0: #if for some reason cleanup() hangs, maybe this will fire... gobject.timeout_add(4 * 1000, self.exit) #try harder!: def force_quit(): from xpra import os_util os_util.force_quit() gobject.timeout_add(5 * 1000, force_quit) self.cleanup() if gtk.main_level() > 0: log( "GTKXpraClient.quit(%s) main loop at level %s, calling gtk quit via timeout", exit_code, gtk.main_level()) gobject.timeout_add(500, self.exit) def exit(self): log("GTKXpraClient.exit() calling %s", gtk_main_quit_really) gtk_main_quit_really() def cleanup(self): if self.session_info: self.session_info.destroy() self.session_info = None if self.bug_report: self.bug_report.destroy() self.bug_report = None if self.start_new_command: self.start_new_command.destroy() self.start_new_command = None UIXpraClient.cleanup(self) def show_start_new_command(self, *args): log("show_start_new_command%s current start_new_command=%s, flag=%s", args, self.start_new_command, self.start_new_commands) if self.start_new_command is None: from xpra.client.gtk_base.start_new_command import getStartNewCommand def run_command_cb(command): self.send_start_command(command, command, False) self.start_new_command = getStartNewCommand(run_command_cb) self.start_new_command.show() return self.start_new_command def show_session_info(self, *args): if self.session_info and not self.session_info.is_closed: #exists already: just raise its window: self.session_info.set_args(*args) self.session_info.present() return pixbuf = self.get_pixbuf("statistics.png") if not pixbuf: pixbuf = self.get_pixbuf("xpra.png") self.session_info = SessionInfo(self, self.session_name, pixbuf, self._protocol._conn, self.get_pixbuf) self.session_info.set_args(*args) self.session_info.show_all() def show_bug_report(self, *args): self.send_info_request() if self.bug_report: self.bug_report.show() return from xpra.client.gtk_base.bug_report import BugReport self.bug_report = BugReport() def init_bug_report(): #skip things we aren't using: includes = { "keyboard": bool(self.keyboard_helper), "opengl": self.opengl_enabled, } def get_server_info(): return self.server_last_info self.bug_report.init(show_about=False, get_server_info=get_server_info, opengl_info=self.opengl_props, includes=includes) self.bug_report.show() #gives the server time to send an info response.. #(by the time the user clicks on copy, it should have arrived, we hope!) self.timeout_add(200, init_bug_report) def get_pixbuf(self, icon_name): try: if not icon_name: log("get_pixbuf(%s)=None", icon_name) return None icon_filename = get_icon_filename(icon_name) log("get_pixbuf(%s) icon_filename=%s", icon_name, icon_filename) if icon_filename: return pixbuf_new_from_file(icon_filename) except: log.error("get_pixbuf(%s)", icon_name, exc_info=True) return None def get_image(self, icon_name, size=None): try: pixbuf = self.get_pixbuf(icon_name) log("get_image(%s, %s) pixbuf=%s", icon_name, size, pixbuf) if not pixbuf: return None return scaled_image(pixbuf, size) except: log.error("get_image(%s, %s)", icon_name, size, exc_info=True) return None def make_keyboard_helper(self, keyboard_sync, key_shortcuts): return GTKKeyboardHelper(self.send, keyboard_sync, key_shortcuts) def _add_statusicon_tray(self, tray_list): #add gtk.StatusIcon tray: try: from xpra.client.gtk_base.statusicon_tray import GTKStatusIconTray tray_list.append(GTKStatusIconTray) except Exception as e: log.warn("failed to load StatusIcon tray: %s" % e) return tray_list def get_tray_classes(self): return self._add_statusicon_tray(UIXpraClient.get_tray_classes(self)) def get_system_tray_classes(self): return self._add_statusicon_tray( UIXpraClient.get_system_tray_classes(self)) def supports_system_tray(self): #always True: we can always use gtk.StatusIcon as fallback return True def get_root_window(self): raise Exception("override me!") def get_root_size(self): raise Exception("override me!") def get_mouse_position(self): return self.get_root_window().get_pointer()[:2] def get_current_modifiers(self): modifiers_mask = self.get_root_window().get_pointer()[-1] return self.mask_to_names(modifiers_mask) def make_hello(self): capabilities = UIXpraClient.make_hello(self) capabilities["named_cursors"] = len(cursor_types) > 0 capabilities.update(get_gtk_version_info()) #tell the server which icons GTK can use #so it knows when it should supply one as fallback it = icon_theme_get_default() #this would add our bundled icon directory #to the search path, but I don't think we have #any extra icons that matter in there: #from xpra.platform.paths import get_icon_dir #d = get_icon_dir() #if d not in it.get_search_path(): # it.append_search_path(d) # it.rescan_if_needed() log("default icon theme: %s", it) log("icon search path: %s", it.get_search_path()) log("contexts: %s", it.list_contexts()) icons = [] for context in it.list_contexts(): icons += it.list_icons(context) log("icons: %s", icons) capabilities["theme.default.icons"] = list(set(icons)) if METADATA_SUPPORTED: ms = [x.strip() for x in METADATA_SUPPORTED.split(",")] else: #this is currently unused, and slightly redundant because of metadata.supported below: capabilities["window.states"] = [ "fullscreen", "maximized", "sticky", "above", "below", "shaded", "iconified", "skip-taskbar", "skip-pager" ] ms = list(DEFAULT_METADATA_SUPPORTED) #added in 0.15: ms += ["command", "workspace", "above", "below", "sticky"] if os.name == "posix": #this is only really supported on X11, but posix is easier to check for.. #"strut" and maybe even "fullscreen-monitors" could also be supported on other platforms I guess ms += [ "shaded", "bypass-compositor", "strut", "fullscreen-monitors" ] log("metadata.supported: %s", ms) capabilities["metadata.supported"] = ms #we need the bindings to support initiate-moveresize (posix only for now): from xpra.client.gtk_base.gtk_client_window_base import HAS_X11_BINDINGS capabilities["window.initiate-moveresize"] = HAS_X11_BINDINGS #window icon bits capabilities[ "encoding.icons.greedy"] = True #we don't set a default window icon any more capabilities["encoding.icons.size"] = 64, 64 #size we want capabilities["encoding.icons.max_size"] = 128, 128 #limit from xpra.client.window_backing_base import DELTA_BUCKETS capabilities["encoding.delta_buckets"] = DELTA_BUCKETS return capabilities def has_transparency(self): return screen_get_default().get_rgba_visual() is not None def get_screen_sizes(self): display = display_get_default() i = 0 screen_sizes = [] n_screens = display.get_n_screens() screenlog("get_screen_sizes() found %s screens", n_screens) while i < n_screens: screen = display.get_screen(i) j = 0 monitors = [] workareas = [] #native "get_workareas()" is only valid for a single screen (but describes all the monitors) #and it is only implemented on win32 right now #other platforms only implement "get_workarea()" instead, which is reported against the screen n_monitors = screen.get_n_monitors() screenlog("get_screen_sizes() screen %s has %s monitors", i, n_monitors) if n_screens == 1: workareas = get_workareas() if len(workareas) != n_monitors: screenlog( "number of monitors does not match number of workareas!" ) workareas = [] while j < screen.get_n_monitors(): geom = screen.get_monitor_geometry(j) plug_name = "" if hasattr(screen, "get_monitor_plug_name"): plug_name = screen.get_monitor_plug_name(j) or "" wmm = -1 if hasattr(screen, "get_monitor_width_mm"): wmm = screen.get_monitor_width_mm(j) hmm = -1 if hasattr(screen, "get_monitor_height_mm"): hmm = screen.get_monitor_height_mm(j) monitor = [ plug_name, geom.x, geom.y, geom.width, geom.height, wmm, hmm ] screenlog("get_screen_sizes() monitor %s: %s", j, monitor) if workareas: w = workareas[j] monitor += list(w) monitors.append(tuple(monitor)) j += 1 work_x, work_y = 0, 0 work_width, work_height = screen.get_width(), screen.get_height() workarea = get_workarea() if workarea: work_x, work_y, work_width, work_height = workarea screenlog("get_screen_sizes() workarea=%s", workarea) item = (screen.make_display_name(), screen.get_width(), screen.get_height(), screen.get_width_mm(), screen.get_height_mm(), monitors, work_x, work_y, work_width, work_height) screenlog("get_screen_sizes() screen %s: %s", i, item) screen_sizes.append(item) i += 1 return screen_sizes def set_windows_cursor(self, windows, cursor_data): cursorlog("set_windows_cursor(%s, ..)", windows) cursor = None if cursor_data: try: cursor = self.make_cursor(cursor_data) cursorlog("make_cursor(..)=%s", cursor) except Exception as e: log.warn("error creating cursor: %s (using default)", e, exc_info=True) if cursor is None: #use default: cursor = default_Cursor for w in windows: gdkwin = w.get_window() #trays don't have a gdk window if gdkwin: gdkwin.set_cursor(cursor) def make_cursor(self, cursor_data): #if present, try cursor ny name: display = display_get_default() if len(cursor_data) >= 9 and cursor_types: cursor_name = bytestostr(cursor_data[8]) if cursor_name: gdk_cursor = cursor_types.get(cursor_name.upper()) if gdk_cursor is not None: cursorlog("setting new cursor by name: %s=%s", cursor_name, gdk_cursor) return new_Cursor_for_display(display, gdk_cursor) else: global missing_cursor_names if cursor_name not in missing_cursor_names: cursorlog("cursor name '%s' not found", cursor_name) missing_cursor_names.add(cursor_name) #create cursor from the pixel data: w, h, xhot, yhot, serial, pixels = cursor_data[2:8] if len(pixels) < w * h * 4: import binascii cursorlog.warn( "not enough pixels provided in cursor data: %s needed and only %s bytes found (%s)", w * h * 4, len(pixels), binascii.hexlify(pixels)[:100]) return pixbuf = get_pixbuf_from_data(pixels, True, w, h, w * 4) x = max(0, min(xhot, w - 1)) y = max(0, min(yhot, h - 1)) csize = display.get_default_cursor_size() cmaxw, cmaxh = display.get_maximal_cursor_size() if len(cursor_data) >= 11: ssize = cursor_data[9] smax = cursor_data[10] cursorlog("server cursor sizes: default=%s, max=%s", ssize, smax) cursorlog( "new cursor at %s,%s with serial=%s, dimensions: %sx%s, len(pixels)=%s, default cursor size is %s, maximum=%s", xhot, yhot, serial, w, h, len(pixels), csize, (cmaxw, cmaxh)) fw, fh = get_fixed_cursor_size() if fw > 0 and fh > 0 and (w != fw or h != fh): #OS wants a fixed cursor size! (win32 does, and GTK doesn't do this for us) if w <= fw and h <= fh: cursorlog( "pasting cursor of size %ix%i onto clear pixbuf of size %ix%i", w, h, fw, fh) cursor_pixbuf = get_pixbuf_from_data("\0" * fw * fh * 4, True, fw, fh, fw * 4) pixbuf.copy_area(0, 0, w, h, cursor_pixbuf, 0, 0) else: cursorlog("scaling cursor from %ix%i to fixed OS size %ix%i", w, h, fw, fh) cursor_pixbuf = pixbuf.scale_simple(fw, fh, INTERP_BILINEAR) xratio, yratio = float(w) / fw, float(h) / fh x, y = int(x / xratio), int(y / yratio) elif w > cmaxw or h > cmaxh or (csize > 0 and (csize < w or csize < h)): ratio = max( float(w) / cmaxw, float(h) / cmaxh, float(max(w, h)) / csize) x, y, w, h = int(x / ratio), int(y / ratio), int(w / ratio), int( h / ratio) cursorlog("downscaling cursor %s by %.2f: %sx%s", pixbuf, ratio, w, h) cursor_pixbuf = pixbuf.scale_simple(w, h, INTERP_BILINEAR) else: cursor_pixbuf = pixbuf #clamp to pixbuf size: w = cursor_pixbuf.get_width() h = cursor_pixbuf.get_height() x = max(0, min(x, w - 1)) y = max(0, min(y, h - 1)) return new_Cursor_from_pixbuf(display, cursor_pixbuf, x, y) def process_ui_capabilities(self): UIXpraClient.process_ui_capabilities(self) if self.server_randr: display = display_get_default() i = 0 while i < display.get_n_screens(): screen = display.get_screen(i) screen.connect("size-changed", self.screen_size_changed) i += 1 def window_bell(self, window, device, percent, pitch, duration, bell_class, bell_id, bell_name): gdkwindow = None if window: gdkwindow = window.get_window() if gdkwindow is None: gdkwindow = self.get_root_window() log("window_bell(..) gdkwindow=%s", gdkwindow) if not system_bell(gdkwindow, device, percent, pitch, duration, bell_class, bell_id, bell_name): #fallback to simple beep: gdk.beep() #OpenGL bits: def init_opengl(self, enable_opengl): opengllog("init_opengl(%s)", enable_opengl) #enable_opengl can be True, False or None (auto-detect) if enable_opengl is False: self.opengl_props["info"] = "disabled by configuration" return from xpra.scripts.config import OpenGL_safety_check from xpra.platform.gui import gl_check as platform_gl_check warnings = [] for check in (OpenGL_safety_check, platform_gl_check): warning = check() if warning: warnings.append(warning) self.opengl_props["info"] = "" if warnings: if enable_opengl is True: opengllog.warn( "OpenGL safety warning (enabled at your own risk):") for warning in warnings: opengllog.warn(" %s", warning) self.opengl_props["info"] = "forced enabled despite: %s" % ( ", ".join(warnings)) else: opengllog.warn("OpenGL disabled:", warning) for warning in warnings: opengllog.warn(" %s", warning) self.opengl_props["info"] = "disabled: %s" % ( ", ".join(warnings)) return try: opengllog("init_opengl: going to import xpra.client.gl") __import__("xpra.client.gl", {}, {}, []) __import__("xpra.client.gl.gtk_compat", {}, {}, []) gl_check = __import__("xpra.client.gl.gl_check", {}, {}, ["check_support"]) opengllog("init_opengl: gl_check=%s", gl_check) self.opengl_props = gl_check.check_support( force_enable=(enable_opengl is True)) opengllog("init_opengl: found props %s", self.opengl_props) GTK_GL_CLIENT_WINDOW_MODULE = "xpra.client.gl.gtk%s.gl_client_window" % ( 2 + int(is_gtk3())) opengllog( "init_opengl: trying to load GL client window module '%s'", GTK_GL_CLIENT_WINDOW_MODULE) gl_client_window = __import__(GTK_GL_CLIENT_WINDOW_MODULE, {}, {}, ["GLClientWindow"]) self.GLClientWindowClass = gl_client_window.GLClientWindow self.client_supports_opengl = True #only enable opengl by default if force-enabled or if safe to do so: self.opengl_enabled = ( enable_opengl is True) or self.opengl_props.get("safe", False) self.gl_texture_size_limit = self.opengl_props.get( "texture-size-limit", 16 * 1024) if self.gl_texture_size_limit < 4 * 1024: opengllog.warn( "OpenGL disabled: the texture size limit is too low (%s)", self.gl_texture_size_limit) self.opengl_enabled = False self.GLClientWindowClass.MAX_TEXTURE_SIZE = self.gl_texture_size_limit mww, mwh = self.max_window_size opengllog( "OpenGL: enabled=%s, texture-size-limit=%s, max-window-size=%s", self.opengl_enabled, self.gl_texture_size_limit, self.max_window_size) 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): #log at warn level if the limit is low: #(if we're likely to hit it - if the screen is as big or bigger) w, h = self.get_root_size() l = opengllog.info if w >= self.gl_texture_size_limit or h >= self.gl_texture_size_limit: l = log.warn l( "Warning: OpenGL windows will be clamped to the maximum texture size %ix%i", self.gl_texture_size_limit, self.gl_texture_size_limit) l(" for OpenGL %s renderer '%s'", pver(self.opengl_props.get("opengl", "")), self.opengl_props.get("renderer", "unknown")) driver_info = self.opengl_props.get( "renderer") or self.opengl_props.get( "vendor") or "unknown card" if self.opengl_enabled: opengllog.info("OpenGL enabled with %s", driver_info) elif self.client_supports_opengl: opengllog("OpenGL supported with %s, but not enabled", driver_info) except ImportError as e: opengllog.warn("OpenGL support could not be enabled:") opengllog.warn(" %s", e) self.opengl_props["info"] = str(e) except Exception as e: opengllog.error("Error loading OpenGL support:") opengllog.error(" %s", e, exc_info=True) self.opengl_props["info"] = str(e) def get_client_window_classes(self, w, h, metadata, override_redirect): log( "get_client_window_class(%i, %i, %s, %s) GLClientWindowClass=%s, opengl_enabled=%s, mmap_enabled=%s, encoding=%s", w, h, metadata, override_redirect, self.GLClientWindowClass, self.opengl_enabled, self.mmap_enabled, self.encoding) if self.GLClientWindowClass is None or not self.opengl_enabled or w > self.gl_texture_size_limit or h > self.gl_texture_size_limit: return [self.ClientWindowClass] return [self.GLClientWindowClass, self.ClientWindowClass] def toggle_opengl(self, *args): assert self.window_unmap, "server support for 'window_unmap' is required for toggling opengl at runtime" self.opengl_enabled = not self.opengl_enabled opengllog("opengl_toggled: %s", self.opengl_enabled) def fake_send(*args): opengllog("fake_send(%s)", args) #now replace all the windows with new ones: for wid, window in self._id_to_window.items(): if window.is_tray(): #trays are never GL enabled, so don't bother re-creating them #(might cause problems anyway if we did) continue #ignore packets from old window: window.send = fake_send #copy attributes: x, y = window._pos w, h = window._size client_properties = window._client_properties metadata = window._metadata override_redirect = window._override_redirect backing = window._backing video_decoder = None csc_decoder = None decoder_lock = None try: if backing: video_decoder = backing._video_decoder csc_decoder = backing._csc_decoder decoder_lock = backing._decoder_lock if decoder_lock: decoder_lock.acquire() opengllog( "toggle_opengl() will preserve video=%s and csc=%s for %s", video_decoder, csc_decoder, wid) backing._video_decoder = None backing._csc_decoder = None backing._decoder_lock = None #now we can unmap it: self.destroy_window(wid, window) #explicitly tell the server we have unmapped it: #(so it will reset the video encoders, etc) self.send("unmap-window", wid) try: del self._id_to_window[wid] except: pass try: del self._window_to_id[window] except: pass #create the new window, which should honour the new state of the opengl_enabled flag: window = self.make_new_window(wid, x, y, w, h, metadata, override_redirect, client_properties) if video_decoder or csc_decoder: backing = window._backing backing._video_decoder = video_decoder backing._csc_decoder = csc_decoder backing._decoder_lock = decoder_lock finally: if decoder_lock: decoder_lock.release() opengllog("replaced all the windows with opengl=%s: %s", self.opengl_enabled, self._id_to_window)