def set_xsettings(self, v): if not self._xsettings_enabled: return log("set_xsettings(%s)", v) if self._xsettings_manager is None: from xpra.x11.xsettings import XSettingsManager self._xsettings_manager = XSettingsManager() self._xsettings_manager.set_settings(v)
class X11ServerBase(X11ServerCore): """ Base class for X11 servers, adds uinput, icc and xsettings synchronization to the X11ServerCore class (see XpraServer or DesktopServer for actual implementations) """ def __init__(self, clobber): self.clobber = clobber X11ServerCore.__init__(self) self._default_xsettings = {} self._settings = {} self.double_click_time = 0 self.double_click_distance = 0 self.dpi = 0 self.default_dpi = 0 self._xsettings_manager = None self._xsettings_enabled = False self.display_pid = 0 self.icc_profile = b"" def do_init(self, opts): super().do_init(opts) self._xsettings_enabled = opts.xsettings if self._xsettings_enabled: from xpra.x11.xsettings import XSettingsHelper self._default_xsettings = XSettingsHelper().get_settings() log("_default_xsettings=%s", self._default_xsettings) self.init_all_server_settings() def init_display_pid(self, pid): if pid: from xpra.scripts.server import _save_int _save_int(b"_XPRA_SERVER_PID", pid) elif self.clobber: from xpra.scripts.server import _get_int pid = _get_int(b"_XPRA_SERVER_PID") if not pid: log.info("xvfb pid not found") else: log.info("xvfb pid=%i", pid) self.display_pid = pid def kill_display(self): if self.display_pid: from xpra.server import EXITING_CODE if self._upgrading == EXITING_CODE: log.info("exiting: not cleaning up Xvfb") elif self._upgrading: log.info("upgrading: not cleaning up Xvfb") else: from xpra.x11.vfb_util import kill_xvfb kill_xvfb(self.display_pid) def do_cleanup(self): X11ServerCore.do_cleanup(self) self.kill_display() def configure_best_screen_size(self): root_w, root_h = X11ServerCore.configure_best_screen_size(self) if self.touchpad_device: self.touchpad_device.root_w = root_w self.touchpad_device.root_h = root_h return root_w, root_h def init_dbus(self, dbus_pid, dbus_env): try: from xpra.server.dbus import dbus_start assert dbus_start except ImportError as e: log("init_dbus(%s, %s)", dbus_pid, dbus_env, exc_info=True) log.warn("Warning: cannot initialize dbus") log.warn(" %s", e) return from xpra.server.dbus.dbus_start import ( get_saved_dbus_pid, get_saved_dbus_env, save_dbus_pid, save_dbus_env, ) super().init_dbus(dbus_pid, dbus_env) if self.clobber: #get the saved pids and env self.dbus_pid = get_saved_dbus_pid() self.dbus_env = get_saved_dbus_env() dbuslog("retrieved existing dbus attributes: %s, %s", self.dbus_pid, self.dbus_env) if self.dbus_env: os.environ.update(self.dbus_env) else: #now we can save values on the display #(we cannot access gtk3 until dbus has started up) if self.dbus_pid: save_dbus_pid(self.dbus_pid) if self.dbus_env: save_dbus_env(self.dbus_env) def last_client_exited(self): self.reset_settings() X11ServerCore.last_client_exited(self) def init_virtual_devices(self, devices): #(this runs in the main thread - before the main loop starts) #for the time being, we only use the pointer if there is one: pointer = devices.get("pointer") touchpad = devices.get("touchpad") mouselog("init_virtual_devices(%s) got pointer=%s, touchpad=%s", devices, pointer, touchpad) self.input_devices = "xtest" if pointer: uinput_device = pointer.get("uinput") device_path = pointer.get("device") if uinput_device: from xpra.x11.uinput_device import UInputPointerDevice self.input_devices = "uinput" self.pointer_device = UInputPointerDevice( uinput_device, device_path) self.verify_uinput_pointer_device() if self.input_devices == "uinput" and touchpad: uinput_device = touchpad.get("uinput") device_path = touchpad.get("device") if uinput_device: from xpra.x11.uinput_device import UInputTouchpadDevice root_w, root_h = self.get_root_window_size() self.touchpad_device = UInputTouchpadDevice( uinput_device, device_path, root_w, root_h) try: mouselog.info( "pointer device emulation using %s", str(self.pointer_device).replace("PointerDevice", "")) except Exception as e: mouselog("cannot get pointer device class from %s: %s", self.pointer_device, e) def verify_uinput_pointer_device(self): xtest = XTestPointerDevice() ox, oy = 100, 100 with xlog: xtest.move_pointer(0, ox, oy) nx, ny = 200, 200 self.pointer_device.move_pointer(0, nx, ny) def verify_uinput_moved(): pos = None #@UnusedVariable with xswallow: pos = X11Keyboard.query_pointer() mouselog("X11Keyboard.query_pointer=%s", pos) if pos == (ox, oy): mouselog.warn("Warning: %s failed verification", self.pointer_device) mouselog.warn(" expected pointer at %s, now at %s", (nx, ny), pos) mouselog.warn(" usign XTest fallback") self.pointer_device = xtest self.input_devices = "xtest" self.timeout_add(1000, verify_uinput_moved) def dpi_changed(self): #re-apply the same settings, which will apply the new dpi override to it: self.update_server_settings() def get_info(self, proto=None, client_uuids=None): info = super().get_info(proto=proto, client_uuids=client_uuids) info.setdefault("display", {})["icc"] = self.get_icc_info() return info def get_icc_info(self) -> dict: icc_info = { "sync": SYNC_ICC, } if SYNC_ICC: icc_info["profile"] = hexstr(self.icc_profile) return icc_info def set_icc_profile(self): if not SYNC_ICC: return ui_clients = [s for s in self._server_sources.values() if s.ui_client] if len(ui_clients) != 1: screenlog("%i UI clients, resetting ICC profile to default", len(ui_clients)) self.reset_icc_profile() return icc = typedict(ui_clients[0].icc) data = None for x in ("data", "icc-data", "icc-profile"): data = icc.strget(x) if data: break if not data: screenlog("no icc data found in %s", icc) self.reset_icc_profile() return screenlog("set_icc_profile() icc data for %s: %s (%i bytes)", ui_clients[0], hexstr(data or ""), len(data or "")) self.icc_profile = data from xpra.x11.gtk_x11.prop import prop_set prop_set(self.root_window, "_ICC_PROFILE", ["u32"], [ord(x) for x in data]) prop_set(self.root_window, "_ICC_PROFILE_IN_X_VERSION", "u32", 0 * 100 + 4) #0.4 -> 0*100+4*1 def reset_icc_profile(self): screenlog("reset_icc_profile()") from xpra.x11.gtk_x11.prop import prop_del prop_del(self.root_window, "_ICC_PROFILE") prop_del(self.root_window, "_ICC_PROFILE_IN_X_VERSION") self.icc_profile = b"" def reset_settings(self): if not self._xsettings_enabled: return log("resetting xsettings to: %s", self._default_xsettings) self.set_xsettings(self._default_xsettings or (0, ())) def set_xsettings(self, v): if not self._xsettings_enabled: return log("set_xsettings(%s)", v) with xsync: if self._xsettings_manager is None: from xpra.x11.xsettings import XSettingsManager self._xsettings_manager = XSettingsManager() self._xsettings_manager.set_settings(v) def init_all_server_settings(self): log("init_all_server_settings() dpi=%i, default_dpi=%i", self.dpi, self.default_dpi) #almost like update_all, except we use the default_dpi, #since this is called before the first client connects self.do_update_server_settings( { b"resource-manager": b"", b"xsettings-blob": (0, []) }, reset=True, dpi=self.default_dpi, cursor_size=24) def update_all_server_settings(self, reset=False): self.update_server_settings( { b"resource-manager": b"", b"xsettings-blob": (0, []), }, reset=reset) def update_server_settings(self, settings=None, reset=False): self.do_update_server_settings(settings or self._settings, reset, self.dpi, self.double_click_time, self.double_click_distance, self.antialias, self.cursor_size) def do_update_server_settings(self, settings, reset=False, dpi=0, double_click_time=0, double_click_distance=(-1, -1), antialias={}, cursor_size=-1): if not self._xsettings_enabled: log("ignoring xsettings update: %s", settings) return if reset: #FIXME: preserve serial? (what happens when we change values which had the same serial?) self.reset_settings() self._settings = {} if self._default_xsettings: #try to parse default xsettings into a dict: try: for _, prop_name, value, _ in self._default_xsettings[1]: self._settings[prop_name] = value except Exception as e: log("failed to parse %s", self._default_xsettings) log.warn("Warning: failed to parse default XSettings:") log.warn(" %s", e) old_settings = dict(self._settings) log("server_settings: old=%s, updating with=%s", nonl(old_settings), nonl(settings)) log( "overrides: dpi=%s, double click time=%s, double click distance=%s", dpi, double_click_time, double_click_distance) log("overrides: antialias=%s", antialias) self._settings.update(settings) for k, v in settings.items(): #cook the "resource-manager" value to add the DPI and/or antialias values: if k == b"resource-manager" and (dpi > 0 or antialias or cursor_size > 0): value = bytestostr(v) #parse the resources into a dict: values = {} options = value.split("\n") for option in options: if not option: continue parts = option.split(":\t", 1) if len(parts) != 2: log("skipped invalid option: '%s'", option) continue if parts[0] in BLACKLISTED_XSETTINGS: log("skipped blacklisted option: '%s'", option) continue values[parts[0]] = parts[1] if cursor_size > 0: values["Xcursor.size"] = cursor_size if dpi > 0: values["Xft.dpi"] = dpi values["Xft/DPI"] = dpi * 1024 values["gnome.Xft/DPI"] = dpi * 1024 if antialias: ad = typedict(antialias) subpixel_order = "none" sss = tuple(self._server_sources.values()) if len(sss) == 1: #only honour sub-pixel hinting if a single client is connected #and only when it is not using any scaling (or overriden with SCALED_FONT_ANTIALIAS): ss = sss[0] ds_unscaled = getattr(ss, "desktop_size_unscaled", None) ds_scaled = getattr(ss, "desktop_size", None) if SCALED_FONT_ANTIALIAS or (not ds_unscaled or ds_unscaled == ds_scaled): subpixel_order = ad.strget("orientation", "none").lower() values.update({ "Xft.antialias": ad.intget("enabled", -1), "Xft.hinting": ad.intget("hinting", -1), "Xft.rgba": subpixel_order, "Xft.hintstyle": _get_antialias_hintstyle(ad) }) log("server_settings: resource-manager values=%s", nonl(values)) #convert the dict back into a resource string: value = '' for vk, vv in values.items(): value += "%s:\t%s\n" % (vk, vv) #record the actual value used self._settings[b"resource-manager"] = value v = value.encode("utf-8") #cook xsettings to add various settings: #(as those may not be present in xsettings on some platforms.. like win32 and osx) if k==b"xsettings-blob" and \ (self.double_click_time>0 or self.double_click_distance!=(-1, -1) or antialias or dpi>0): #start by removing blacklisted options: def filter_blacklisted(): serial, values = v new_values = [] for _t, _n, _v, _s in values: if bytestostr(_n) in BLACKLISTED_XSETTINGS: log("skipped blacklisted option %s", (_t, _n, _v, _s)) else: new_values.append((_t, _n, _v, _s)) return serial, new_values v = filter_blacklisted() def set_xsettings_value(name, value_type, value): #remove existing one, if any: serial, values = v new_values = [(_t, _n, _v, _s) for (_t, _n, _v, _s) in values if _n != name] new_values.append((value_type, name, value, 0)) return serial, new_values def set_xsettings_int(name, value): if value < 0: #not set, return v unchanged return v return set_xsettings_value(name, XSettingsTypeInteger, value) if dpi > 0: v = set_xsettings_int(b"Xft/DPI", dpi * 1024) if double_click_time > 0: v = set_xsettings_int(b"Net/DoubleClickTime", self.double_click_time) if antialias: ad = typedict(antialias) v = set_xsettings_int(b"Xft/Antialias", ad.intget("enabled", -1)) v = set_xsettings_int(b"Xft/Hinting", ad.intget("hinting", -1)) v = set_xsettings_value( b"Xft/RGBA", XSettingsTypeString, ad.strget("orientation", "none").lower()) v = set_xsettings_value(b"Xft/HintStyle", XSettingsTypeString, _get_antialias_hintstyle(ad)) if double_click_distance != (-1, -1): #some platforms give us a value for each axis, #but X11 only has one, so take the average try: x, y = double_click_distance if x > 0 and y > 0: d = iround((x + y) / 2.0) d = max(1, min(128, d)) #sanitize it a bit v = set_xsettings_int(b"Net/DoubleClickDistance", d) except Exception as e: log.warn( "error setting double click distance from %s: %s", double_click_distance, e) if k not in old_settings or v != old_settings[k]: if k == b"xsettings-blob": self.set_xsettings(v) elif k == b"resource-manager": from xpra.x11.gtk_x11.prop import prop_set p = "RESOURCE_MANAGER" log("server_settings: setting %s to %s", nonl(p), nonl(v)) prop_set(self.root_window, p, "latin1", strtobytes(v).decode("latin1")) else: log.warn("Warning: unexpected setting '%s'", bytestostr(k))
class XpraServer(gobject.GObject, X11ServerBase): __gsignals__ = { "xpra-child-map-event": one_arg_signal, "xpra-cursor-event": one_arg_signal, } def __init__(self, clobber): gobject.GObject.__init__(self) X11ServerBase.__init__(self, clobber) def init(self, opts): self.xsettings_enabled = opts.xsettings self.wm_name = opts.wm_name X11ServerBase.init(self, opts) def x11_init(self): X11ServerBase.x11_init(self) assert init_x11_filter() is True self._has_grab = 0 self._has_focus = 0 # Do this before creating the Wm object, to avoid clobbering its # selecting SubstructureRedirect. root = gtk.gdk.get_default_root_window() root.set_events(root.get_events() | gtk.gdk.SUBSTRUCTURE_MASK) root.property_change(gtk.gdk.atom_intern("XPRA_SERVER", False), gtk.gdk.atom_intern("STRING", False), 8, gtk.gdk.PROP_MODE_REPLACE, xpra.__version__) add_event_receiver(root, self) ### Create the WM object self._wm = Wm(self.clobber, self.wm_name) self._wm.connect("new-window", self._new_window_signaled) self._wm.connect("bell", self._bell_signaled) self._wm.connect("quit", lambda _: self.clean_quit(True)) self._wm.connect("show-desktop", self._show_desktop) #save default xsettings: self.default_xsettings = XSettingsHelper().get_settings() settingslog("default_xsettings=%s", self.default_xsettings) self._settings = {} self._xsettings_manager = None #for handling resize synchronization between client and server (this is not xsync!): self.last_client_configure_event = 0 self.snc_timer = 0 #cursor: self.default_cursor_data = None self.last_cursor_serial = None self.last_cursor_data = None self.send_cursor_pending = False def get_default_cursor(): self.default_cursor_data = X11Keyboard.get_cursor_image() cursorlog("get_default_cursor=%s", self.default_cursor_data) trap.swallow_synced(get_default_cursor) self._wm.enableCursors(True) def make_hello(self, source): capabilities = X11ServerBase.make_hello(self, source) if source.wants_features: capabilities["window.raise"] = True capabilities["window.resize-counter"] = True capabilities["window.configure.skip-geometry"] = True capabilities["pointer.grabs"] = True return capabilities def do_get_info(self, proto, server_sources, window_ids): info = X11ServerBase.do_get_info(self, proto, server_sources, window_ids) info["focused"] = self._has_focus info["grabbed"] = self._has_grab log("do_get_info: adding cursor=%s", self.last_cursor_data) #copy to prevent race: cd = self.last_cursor_data if cd is None: info["cursor"] = "None" else: info["cursor.is_default"] = bool( self.default_cursor_data and len(self.default_cursor_data) >= 8 and len(cd) >= 8 and cd[7] == cd[7]) #all but pixels: for i, x in enumerate(("x", "y", "width", "height", "xhot", "yhot", "serial", None, "name")): if x: v = cd[i] or "" info["cursor." + x] = v return info def get_ui_info(self, proto, wids, *args): info = X11ServerBase.get_ui_info(self, proto, wids, *args) #_NET_WM_NAME: wm = self._wm if wm: info["window-manager-name"] = wm.get_net_wm_name() #now cursor size info: display = gtk.gdk.display_get_default() pos = display.get_default_screen().get_root_window().get_pointer()[:2] info["cursor.position"] = pos for prop, size in { "default": display.get_default_cursor_size(), "max": display.get_maximal_cursor_size() }.items(): if size is None: continue info["cursor.%s_size" % prop] = size return info def get_window_info(self, window): info = X11ServerBase.get_window_info(self, window) info["focused"] = self._has_focus and self._window_to_id.get( window, -1) == self._has_focus info["grabbed"] = self._has_grab and self._window_to_id.get( window, -1) == self._has_grab return info def set_desktops(self, names): if self._wm: self._wm.set_desktop_list(names) def set_workarea(self, workarea): if self._wm: self._wm.set_workarea(workarea.x, workarea.y, workarea.width, workarea.height) def set_desktop_geometry(self, width, height): if self._wm: self._wm.set_desktop_geometry(width, height) def set_dpi(self, xdpi, ydpi): if self._wm: self._wm.set_dpi(xdpi, ydpi) def get_transient_for(self, window): transient_for = window.get_property("transient-for") log("get_transient_for window=%s, transient_for=%s", window, transient_for) if transient_for is None: return None xid = transient_for.xid log("transient_for.xid=%#x", xid) for w, wid in self._window_to_id.items(): if w.get_property("xid") == xid: log("found match, window id=%s", wid) return wid root = gtk.gdk.get_default_root_window() if root.xid == xid: log("transient-for using root") return -1 #-1 is the backwards compatible marker for root... log("not found transient_for=%s, xid=%#x", transient_for, xid) return None def is_shown(self, window): return self._desktop_manager.is_shown(window) def cleanup(self, *args): if self._tray: self._tray.cleanup() self._tray = None X11ServerBase.cleanup(self) cleanup_x11_filter() cleanup_all_event_receivers() if self._wm: self._wm.cleanup() self._wm = None if self._has_grab: #normally we set this value when we receive the NotifyUngrab #but at this point in the cleanup, we probably won't, so force set it: self._has_grab = 0 self.X11_ungrab() def cleanup_source(self, protocol): had_client = len(self._server_sources) > 0 X11ServerBase.cleanup_source(self, protocol) has_client = len(self._server_sources) > 0 if had_client and not has_client: #last client is gone: self.reset_settings() if self._has_grab: self.X11_ungrab() def load_existing_windows(self, system_tray): # Tray handler: self._tray = None if system_tray: try: self._tray = SystemTray() except Exception as e: log.error("cannot setup tray forwarding: %s", e, exc_info=True) ### Create our window managing data structures: self._desktop_manager = DesktopManager() self._wm.get_property("toplevel").add(self._desktop_manager) self._desktop_manager.show_all() ### Load in existing windows: for window in self._wm.get_property("windows"): self._add_new_window(window) root = gtk.gdk.get_default_root_window() for window in get_children(root): if X11Window.is_override_redirect( window.xid) and X11Window.is_mapped(window.xid): self._add_new_or_window(window) def send_windows_and_cursors(self, ss, sharing=False): # We send the new-window packets sorted by id because this sorts them # from oldest to newest -- and preserving window creation order means # that the earliest override-redirect windows will be on the bottom, # which is usually how things work. (I don't know that anyone cares # about this kind of correctness at all, but hey, doesn't hurt.) windowlog("send_windows_and_cursors(%s, %s) will send: %s", ss, sharing, self._id_to_window) for wid in sorted(self._id_to_window.keys()): window = self._id_to_window[wid] if not window.is_managed(): #we keep references to windows that aren't meant to be displayed.. continue #most of the code here is duplicated from the send functions #so we can send just to the new client and request damage #just for the new client too: if window.is_tray(): #code more or less duplicated from _send_new_tray_window_packet: w, h = window.get_property("geometry")[2:4] if ss.system_tray: ss.new_tray(wid, window, w, h) ss.damage(wid, window, 0, 0, w, h) elif not sharing: #park it outside the visible area window.move_resize(-200, -200, w, h) elif window.is_OR(): #code more or less duplicated from _send_new_or_window_packet: x, y, w, h = window.get_property("geometry") wprops = self.client_properties.get("%s|%s" % (wid, ss.uuid)) ss.new_window("new-override-redirect", wid, window, x, y, w, h, wprops) ss.damage(wid, window, 0, 0, w, h) else: #code more or less duplicated from send_new_window_packet: if not sharing: self._desktop_manager.hide_window(window) x, y, w, h = self._desktop_manager.window_geometry(window) wprops = self.client_properties.get("%s|%s" % (wid, ss.uuid)) ss.new_window("new-window", wid, window, x, y, w, h, wprops) #cursors: get sizes and send: display = gtk.gdk.display_get_default() self.cursor_sizes = display.get_default_cursor_size( ), display.get_maximal_cursor_size() cursorlog("cursor_sizes=%s", self.cursor_sizes) ss.send_cursor() def _new_window_signaled(self, wm, window): self._add_new_window(window) def do_xpra_child_map_event(self, event): windowlog("do_xpra_child_map_event(%s)", event) if event.override_redirect: self._add_new_or_window(event.window) def _add_new_window_common(self, window): windowlog("adding window %s", window) for prop in window.get_dynamic_property_names(): window.connect("notify::%s" % prop, self._update_metadata) wid = X11ServerBase._add_new_window_common(self, window) window.managed_connect("client-contents-changed", self._contents_changed) window.managed_connect("unmanaged", self._lost_window) window.managed_connect("raised", self._raised_window) window.managed_connect("initiate-moveresize", self._initiate_moveresize) window.managed_connect("grab", self._window_grab) window.managed_connect("ungrab", self._window_ungrab) return wid def _add_new_window(self, window): self._add_new_window_common(window) _, _, w, h, _ = window.get_property("client-window").get_geometry() x, y, _, _, _ = window.corral_window.get_geometry() windowlog("Discovered new ordinary window: %s (geometry=%s)", window, (x, y, w, h)) self._desktop_manager.add_window(window, x, y, w, h) window.connect("notify::geometry", self._window_resized_signaled) window.connect("notify::iconic", self._iconic_changed) self._send_new_window_packet(window) def _iconic_changed(self, window, pspec): #only defined for debugging purposes log("_iconic_changed(%s, %s) iconic=%s, shown=%s", window, pspec, window.get_property("iconic"), self._desktop_manager.is_shown(window)) def _window_resized_signaled(self, window, *args): nw, nh = window.get_property("actual-size") x, y = window.get_position() geom = self._desktop_manager.window_geometry(window) windowlog( "XpraServer._window_resized_signaled(%s,%s) position=%sx%s, actual-size=%sx%s, current geometry=%s", window, args, x, y, nw, nh, geom) if geom[:4] == [x, y, nw, nh]: #unchanged return geom[:4] = [x, y, nw, nh] lcce = self.last_client_configure_event if not self._desktop_manager.is_shown(window): self.size_notify_clients(window) return if self.snc_timer > 0: gobject.source_remove(self.snc_timer) #TODO: find a better way to choose the timer delay: #for now, we wait at least 100ms, up to 250ms if the client has just sent us a resize: #(lcce should always be in the past, so min(..) should be redundant here) delay = max(100, min(250, 250 + 1000 * (lcce - time.time()))) self.snc_timer = gobject.timeout_add(int(delay), self.size_notify_clients, window, lcce) def size_notify_clients(self, window, lcce=-1): log("size_notify_clients(%s, %s) last_client_configure_event=%s", window, lcce, self.last_client_configure_event) self.snc_timer = 0 wid = self._window_to_id.get(window) if lcce > 0 and lcce != self.last_client_configure_event or not wid: #we have received a new client resize since, #or the window is simply gone return geom = self._desktop_manager.window_geometry(window) x, y, nw, nh = geom[:4] resize_counter = self._desktop_manager.get_resize_counter(window, 1) for ss in self._server_sources.values(): ss.move_resize_window(self._window_to_id[window], window, x, y, nw, nh, resize_counter) #refresh to ensure the client gets the new window contents: #TODO: to save bandwidth, we should compare the dimensions and skip the refresh #if the window is smaller than before, or at least only send the new edges rather than the whole window ss.damage(wid, window, 0, 0, nw, nh, {}) def _add_new_or_window(self, raw_window): xid = raw_window.xid if raw_window.get_window_type() == gtk.gdk.WINDOW_TEMP: #ignoring one of gtk's temporary windows #all the windows we manage should be gtk.gdk.WINDOW_FOREIGN windowlog("ignoring TEMP window %#x", xid) return WINDOW_MODEL_KEY = "_xpra_window_model_" wid = raw_window.get_data(WINDOW_MODEL_KEY) window = self._id_to_window.get(wid) if window: if window.is_managed(): windowlog( "found existing window model %s for %#x, will refresh it", type(window), xid) geometry = window.get_property("geometry") _, _, w, h = geometry self._damage(window, 0, 0, w, h, options={"min_delay": 50}) return windowlog( "found existing model %s (but no longer managed!) for %#x", type(window), xid) #we could try to re-use the existing model and window ID, #but for now it is just easier to create a new one: self._lost_window(window) tray_window = get_tray_window(raw_window) windowlog("Discovered new override-redirect window: %#x (tray=%s)", xid, tray_window) try: if tray_window is not None: assert self._tray window = SystemTrayWindowModel(raw_window) wid = self._add_new_window_common(window) raw_window.set_data(WINDOW_MODEL_KEY, wid) window.call_setup() self._send_new_tray_window_packet(wid, window) else: window = OverrideRedirectWindowModel(raw_window) wid = self._add_new_window_common(window) raw_window.set_data(WINDOW_MODEL_KEY, wid) window.call_setup() window.connect("notify::geometry", self._or_window_geometry_changed) self._send_new_or_window_packet(window) except Unmanageable as e: if window: windowlog("window %s is not manageable: %s", window, e) #if window is set, we failed after instantiating it, #so we need to fail it manually: window.setup_failed(e) if window in self._window_to_id: self._lost_window(window, False) else: windowlog.warn("cannot add window %#x: %s", xid, e) #from now on, we return to the gtk main loop, #so we *should* get a signal when the window goes away def _or_window_geometry_changed(self, window, pspec=None): (x, y, w, h) = window.get_property("geometry")[:4] if w >= 32768 or h >= 32768: windowlog.error( "not sending new invalid window dimensions: %ix%i !", w, h) return windowlog("or_window_geometry_changed: %s (window=%s)", window.get_property("geometry"), window) wid = self._window_to_id[window] for ss in self._server_sources.values(): ss.or_window_geometry(wid, window, x, y, w, h) def do_xpra_cursor_event(self, event): if not self.cursors: return if self.last_cursor_serial == event.cursor_serial: cursorlog( "ignoring cursor event %s with the same serial number %s", event, self.last_cursor_serial) return cursorlog("cursor_event: %s", event) self.last_cursor_serial = event.cursor_serial for ss in self._server_sources.values(): ss.send_cursor() return False def _bell_signaled(self, wm, event): log("bell signaled on window %s", event.window.xid) if not self.bell: return wid = 0 if event.window != gtk.gdk.get_default_root_window( ) and event.window_model is not None: try: wid = self._window_to_id[event.window_model] except: pass log("_bell_signaled(%s,%r) wid=%s", wm, event, wid) for ss in self._server_sources.values(): ss.bell(wid, event.device, event.percent, event.pitch, event.duration, event.bell_class, event.bell_id, event.bell_name or "") def _show_desktop(self, wm, show): log("show_desktop(%s, %s)", wm, show) for ss in self._server_sources.values(): ss.show_desktop(show) def _focus(self, server_source, wid, modifiers): focuslog("focus wid=%s has_focus=%s", wid, self._has_focus) if self._has_focus == wid: #nothing to do! return had_focus = self._id_to_window.get(self._has_focus) def reset_focus(): toplevel = None if self._wm: toplevel = self._wm.get_property("toplevel") focuslog("reset_focus() %s / %s had focus (toplevel=%s)", self._has_focus, had_focus, toplevel) self._clear_keys_pressed() # FIXME: kind of a hack: self._has_focus = 0 #toplevel may be None during cleanup! if toplevel: toplevel.reset_x_focus() if wid == 0: #wid==0 means root window return reset_focus() window = self._id_to_window.get(wid) if not window: #not found! (go back to root) return reset_focus() if window.is_OR(): focuslog.warn("focus(..) cannot focus OR window: %s", window) return focuslog("focus: giving focus to %s", window) #using idle_add seems to prevent some focus races: def give_focus(): if not window.is_managed(): return window.raise_window() window.give_client_focus() gobject.idle_add(give_focus) if server_source and modifiers is not None: focuslog("focus: will set modified mask to %s", modifiers) server_source.make_keymask_match(modifiers) self._has_focus = wid def get_focus(self): return self._has_focus def _send_new_window_packet(self, window): geometry = self._desktop_manager.window_geometry(window) self._do_send_new_window_packet("new-window", window, geometry) def _send_new_or_window_packet(self, window, options=None): geometry = window.get_property("geometry") self._do_send_new_window_packet("new-override-redirect", window, geometry) (_, _, w, h) = geometry self._damage(window, 0, 0, w, h, options=options) def _send_new_tray_window_packet(self, wid, window, options=None): (_, _, w, h) = window.get_property("geometry") for ss in self._server_sources.values(): ss.new_tray(wid, window, w, h) self._damage(window, 0, 0, w, h, options=options) def _update_metadata(self, window, pspec): windowlog("updating metadata on %s: %s", window, pspec) wid = self._window_to_id[window] for ss in self._server_sources.values(): ss.window_metadata(wid, window, pspec.name) def _lost_window(self, window, wm_exiting=False): wid = self._window_to_id[window] windowlog("lost_window: %s - %s", wid, window) for ss in self._server_sources.values(): ss.lost_window(wid, window) del self._window_to_id[window] del self._id_to_window[wid] for ss in self._server_sources.values(): ss.remove_window(wid, window) def _contents_changed(self, window, event): if window.is_OR() or self._desktop_manager.visible(window): self._damage(window, event.x, event.y, event.width, event.height) def _window_grab(self, window, event): grab_id = self._window_to_id.get(window, -1) grablog( "window_grab(%s, %s) has_grab=%s, has focus=%s, grab window=%s", window, event, self._has_grab, self._has_focus, grab_id) if grab_id < 0 or self._has_grab == grab_id: return self._has_grab = grab_id for ss in self._server_sources.values(): ss.pointer_grab(self._has_grab) def _window_ungrab(self, window, event): grab_id = self._window_to_id.get(window, -1) grablog( "window_ungrab(%s, %s) has_grab=%s, has focus=%s, grab window=%s", window, event, self._has_grab, self._has_focus, grab_id) if not self._has_grab: return self._has_grab = 0 for ss in self._server_sources.values(): ss.pointer_ungrab(grab_id) def _initiate_moveresize(self, window, event): log("initiate_moveresize(%s, %s)", window, event) assert len(event.data) == 5 #x_root, y_root, direction, button, source_indication = event.data wid = self._window_to_id[window] for ss in self._server_sources.values(): ss.initiate_moveresize(wid, window, *event.data) def _raised_window(self, window, event): wid = self._window_to_id[window] windowlog("raised window: %s (%s) wid=%s, current focus=%s", window, event, wid, self._has_focus) if self._has_focus == wid: return for ss in self._server_sources.values(): ss.raise_window(wid, window) def _set_window_state(self, proto, wid, window, new_window_state): metadatalog("set_window_state%s", (wid, window, new_window_state)) changes = [] for k in ("maximized", "above", "below", "fullscreen", "sticky", "shaded", "skip-pager", "skip-taskbar", "iconified"): if k in new_window_state: #stupid naming conflict (should have used the same at both ends): wpropname = {"iconified": "iconic"}.get(k, k) new_state = bool(new_window_state.get(k, False)) cur_state = bool(window.get_property(wpropname)) metadatalog( "set window state for '%s': current state=%s, new state=%s", k, cur_state, new_state) if cur_state != new_state: window.set_property(wpropname, new_state) changes.append(k) metadatalog("set_window_state: changes=%s", changes) return changes def _process_map_window(self, proto, packet): wid, x, y, w, h = packet[1:6] window = self._id_to_window.get(wid) if not window: windowlog("cannot map window %s: already removed!", wid) return assert not window.is_OR() windowlog("client mapped window %s - %s, at: %s", wid, window, (x, y, w, h)) if len(packet) >= 8: self._set_window_state(proto, wid, window, packet[7]) ax, ay, aw, ah = self._clamp_window(proto, wid, window, x, y, w, h) self._desktop_manager.configure_window(window, ax, ay, aw, ah) self._desktop_manager.show_window(window) if len(packet) >= 7: self._set_client_properties(proto, wid, window, packet[6]) self._damage(window, 0, 0, w, h) def _process_unmap_window(self, proto, packet): wid = packet[1] window = self._id_to_window.get(wid) if not window: log("cannot map window %s: already removed!", wid) return if len(packet) >= 4: #optional window_state added in 0.15 to update flags #during iconification events: self._set_window_state(proto, wid, window, packet[3]) assert not window.is_OR() windowlog("client unmapped window %s - %s", wid, window) for ss in self._server_sources.values(): ss.unmap_window(wid, window) window.unmap() iconified = len(packet) >= 3 and bool(packet[2]) if iconified and not window.get_property("iconic"): window.set_property("iconic", True) self._desktop_manager.hide_window(window) def _clamp_window(self, proto, wid, window, x, y, w, h): rw, rh = self.get_root_window_size() #clamp to root window size if x >= rw or y >= rh: log("clamping window position %ix%i to root window size %ix%i", x, y, rw, rh) x = max(0, min(x, rw - w)) y = max(0, min(y, rh - h)) #tell this client to honour the new location ss = self._server_sources.get(proto) if ss: resize_counter = self._desktop_manager.get_resize_counter( window, 1) ss.move_resize_window(wid, window, x, y, w, h, resize_counter) return x, y, w, h def _process_configure_window(self, proto, packet): wid, x, y, w, h = packet[1:6] resize_counter = 0 if len(packet) >= 8: resize_counter = packet[7] #some "configure-window" packets are only meant for metadata updates: skip_geometry = len(packet) >= 10 and packet[9] window = self._id_to_window.get(wid) windowlog("client configured window %s - %s, at: %s", wid, window, (x, y, w, h)) if not window: windowlog("cannot map window %s: already removed!", wid) return damage = False if window.is_tray(): assert self._tray if not skip_geometry: traylog("tray %s configured to: %s", window, (x, y, w, h)) self._tray.move_resize(window, x, y, w, h) damage = True else: assert not window.is_OR( ) or skip_geometry, "received a configure packet with geometry for OR window %s from %s: %s" % ( window, proto, packet) self.last_client_configure_event = time.time() if len(packet) >= 9: changes = self._set_window_state(proto, wid, window, packet[8]) damage = len(changes) > 0 if not skip_geometry: owx, owy, oww, owh = self._desktop_manager.window_geometry( window) windowlog( "_process_configure_window(%s) old window geometry: %s", packet[1:], (owx, owy, oww, owh)) ax, ay, aw, ah = self._clamp_window(proto, wid, window, x, y, w, h) self._desktop_manager.configure_window(window, ax, ay, aw, ah, resize_counter) damage |= owx != ax or owy != ay or oww != aw or owh != ah if len(packet) >= 7: self._set_client_properties(proto, wid, window, packet[6]) if damage: self._damage(window, 0, 0, w, h) def _set_client_properties(self, proto, wid, window, new_client_properties): """ Override so we can update the workspace on the window directly, instead of storing it as a client property """ workspace = new_client_properties.get("workspace") workspacelog("workspace from client properties %s: %s", new_client_properties, workspace) if workspace is not None: window.move_to_workspace(workspace) #we have handled it on the window directly, so remove it from client properties del new_client_properties["workspace"] #handle the rest as normal: X11ServerBase._set_client_properties(self, proto, wid, window, new_client_properties) """ override so we can raise the window under the cursor (gtk raise does not change window stacking, just focus) """ def _move_pointer(self, wid, pos): window = self._id_to_window.get(wid) if not window: mouselog("_move_pointer(%s, %s) invalid window id", wid, pos) else: mouselog("raising %s", window) window.raise_window() X11ServerBase._move_pointer(self, wid, pos) def _process_close_window(self, proto, packet): wid = packet[1] window = self._id_to_window.get(wid, None) windowlog("client closed window %s - %s", wid, window) if window: window.request_close() else: windowlog("cannot close window %s: it is already gone!", wid) def make_screenshot_packet(self): try: return self.do_make_screenshot_packet() except: log.error("make_screenshot_packet()", exc_info=True) return None def do_make_screenshot_packet(self): log("grabbing screenshot") regions = [] OR_regions = [] for wid in reversed(sorted(self._id_to_window.keys())): window = self._id_to_window.get(wid) log("screenshot: window(%s)=%s", wid, window) if window is None: continue if window.is_tray(): log("screenshot: skipping tray window %s", wid) continue if not window.is_managed(): log("screenshot: window %s is not/no longer managed", wid) continue if window.is_OR(): x, y = window.get_property("geometry")[:2] else: x, y = self._desktop_manager.window_geometry(window)[:2] log("screenshot: position(%s)=%s,%s", window, x, y) w, h = window.get_dimensions() log("screenshot: size(%s)=%sx%s", window, w, h) try: with xsync: img = window.get_image(0, 0, w, h) except: log.warn("screenshot: window %s could not be captured", wid) continue if img is None: log.warn("screenshot: no pixels for window %s", wid) continue log("screenshot: image=%s, size=%s", (img, img.get_size())) if img.get_pixel_format() not in ("RGB", "RGBA", "XRGB", "BGRX", "ARGB", "BGRA"): log.warn( "window pixels for window %s using an unexpected rgb format: %s", wid, img.get_pixel_format()) continue item = (wid, x, y, img) if window.is_OR(): OR_regions.append(item) elif self._has_focus == wid: #window with focus first (drawn last) regions.insert(0, item) else: regions.append(item) all_regions = OR_regions + regions if len(all_regions) == 0: log("screenshot: no regions found, returning empty 0x0 image!") return ["screenshot", 0, 0, "png", -1, ""] log("screenshot: found regions=%s, OR_regions=%s", len(regions), len(OR_regions)) #in theory, we could run the rest in a non-UI thread since we're done with GTK.. minx = min([x for (_, x, _, _) in all_regions]) miny = min([y for (_, _, y, _) in all_regions]) maxx = max([(x + img.get_width()) for (_, x, _, img) in all_regions]) maxy = max([(y + img.get_height()) for (_, _, y, img) in all_regions]) width = maxx - minx height = maxy - miny log("screenshot: %sx%s, min x=%s y=%s", width, height, minx, miny) from PIL import Image #@UnresolvedImport screenshot = Image.new("RGBA", (width, height)) for wid, x, y, img in reversed(all_regions): pixel_format = img.get_pixel_format() target_format = { "XRGB": "RGB", "BGRX": "RGB", "BGRA": "RGBA" }.get(pixel_format, pixel_format) pixels = img.get_pixels() #PIL cannot use the memoryview directly: if _memoryview and isinstance(pixels, _memoryview): pixels = pixels.tobytes() try: window_image = Image.frombuffer(target_format, (w, h), pixels, "raw", pixel_format, img.get_rowstride()) except: log.error("Error parsing window pixels in %s format", pixel_format, exc_info=True) continue tx = x - minx ty = y - miny screenshot.paste(window_image, (tx, ty)) buf = StringIOClass() screenshot.save(buf, "png") data = buf.getvalue() buf.close() packet = [ "screenshot", width, height, "png", width * 4, Compressed("png", data) ] log("screenshot: %sx%s %s", packet[1], packet[2], packet[-1]) return packet def reset_settings(self): if not self.xsettings_enabled: return settingslog("resetting xsettings to: %s", self.default_xsettings) self.set_xsettings(self.default_xsettings or (0, ())) def set_xsettings(self, v): if self._xsettings_manager is None: self._xsettings_manager = XSettingsManager() self._xsettings_manager.set_settings(v) def _get_antialias_hintstyle(self): ad = typedict(self.antialias) hintstyle = ad.strget("hintstyle", "").lower() if hintstyle in ("hintnone", "hintslight", "hintmedium", "hintfull"): #X11 clients can give us what we need directly: return hintstyle #win32 style contrast value: contrast = ad.intget("contrast", -1) if contrast > 1600: return "hintfull" elif contrast > 1000: return "hintmedium" elif contrast > 0: return "hintslight" return "hintnone" def update_server_settings(self, settings, reset=False): if not self.xsettings_enabled: settingslog("ignoring xsettings update: %s", settings) return if reset: #FIXME: preserve serial? (what happens when we change values which had the same serial?) self.reset_settings() self._settings = {} if self.default_xsettings: self._settings = self._default_xsettings[1] old_settings = dict(self._settings) settingslog("server_settings: old=%s, updating with=%s", nonl(old_settings), nonl(settings)) settingslog( "overrides: dpi=%s, double click time=%s, double click distance=%s", self.dpi, self.double_click_time, self.double_click_distance) settingslog("overrides: antialias=%s", self.antialias) self._settings.update(settings) root = gtk.gdk.get_default_root_window() for k, v in settings.items(): #cook the "resource-manager" value to add the DPI: if k == "resource-manager" and self.dpi > 0: value = v.decode("utf-8") #parse the resources into a dict: values = {} options = value.split("\n") for option in options: if not option: continue parts = option.split(":\t") if len(parts) != 2: continue values[parts[0]] = parts[1] values["Xft.dpi"] = self.dpi values["Xft/DPI"] = self.dpi * 1024 values["gnome.Xft/DPI"] = self.dpi * 1024 if self.antialias: ad = typedict(self.antialias) values.update({ "Xft.antialias": ad.intget("enabled", -1), "Xft.hinting": ad.intget("hinting", -1), "Xft.rgba": ad.strget("orientation", "none").lower(), "Xft.hintstyle": self._get_antialias_hintstyle() }) settingslog("server_settings: resource-manager values=%s", nonl(values)) #convert the dict back into a resource string: value = '' for vk, vv in values.items(): value += "%s:\t%s\n" % (vk, vv) #record the actual value used self._settings["resource-manager"] = value v = value.encode("utf-8") #cook xsettings to add double-click settings: #(as those may not be present in xsettings on some platforms.. like win32 and osx) if k == "xsettings-blob" and (self.double_click_time > 0 or self.double_click_distance != (-1, -1)): from xpra.x11.xsettings_prop import XSettingsTypeInteger, XSettingsTypeString def set_xsettings_value(name, value_type, value): #remove existing one, if any: serial, values = v new_values = [(_t, _n, _v, _s) for (_t, _n, _v, _s) in values if _n != name] new_values.append((value_type, name, value, 0)) return serial, new_values def set_xsettings_int(name, value): if value < 0: #not set, return v unchanged return v return set_xsettings_value(name, XSettingsTypeInteger, value) if self.dpi > 0: v = set_xsettings_int("Xft/DPI", self.dpi * 1024) if self.double_click_time > 0: v = set_xsettings_int("Net/DoubleClickTime", self.double_click_time) if self.antialias: ad = typedict(self.antialias) v = set_xsettings_int("Xft/Antialias", ad.intget("enabled", -1)) v = set_xsettings_int("Xft/Hinting", ad.intget("hinting", -1)) v = set_xsettings_value( "Xft/RGBA", XSettingsTypeString, ad.strget("orientation", "none").lower()) v = set_xsettings_value("Xft/HintStyle", XSettingsTypeString, self._get_antialias_hintstyle()) if self.double_click_distance != (-1, -1): #some platforms give us a value for each axis, #but X11 only has one, so take the average try: x, y = self.double_click_distance if x > 0 and y > 0: d = (x + y) // 2 d = max(1, min(128, d)) #sanitize it a bit v = set_xsettings_int("Net/DoubleClickDistance", d) except Exception as e: log.warn( "error setting double click distance from %s: %s", self.double_click_distance, e) if k not in old_settings or v != old_settings[k]: def root_set(p): settingslog("server_settings: setting %s to %s", nonl(p), nonl(v)) prop_set(root, p, "latin1", v.decode("utf-8")) if k == "xsettings-blob": self.set_xsettings(v) elif k == "resource-manager": root_set("RESOURCE_MANAGER") elif self.pulseaudio: if k == "pulse-cookie": root_set("PULSE_COOKIE") elif k == "pulse-id": root_set("PULSE_ID") elif k == "pulse-server": root_set("PULSE_SERVER")
def set_xsettings(self, v): if self._xsettings_manager is None: self._xsettings_manager = XSettingsManager() self._xsettings_manager.set_settings(v)
def test_basic_set_get(self): blob = "asdfwheeeee" manager = XSettingsManager() manager.set_blob_in_place(blob) watcher = XSettingsWatcher() assert watcher.get_settings_blob() == blob
def test_watching(self): blob1 = "blob1" manager1 = XSettingsManager() manager1.set_blob_in_place(blob1) watcher = XSettingsWatcher() assert watcher.get_settings_blob() == blob1 blob2 = "blob2" manager2 = XSettingsManager() manager2.set_blob_in_place(blob2) assert_mainloop_emits(watcher, "xsettings-changed") assert watcher.get_settings_blob() == blob2 # It's likely that (due to how the GTK+ clipboard code works # underneath) all of the managers that we create within a single # process are actually using the same selection window, and thus the # previous tests could work right even if we only watch for # PropertyNotify *or* only watch for selection owner changes. # Test where the property change but no manager change message # is sent: blob3 = "blob3" manager2.set_blob_in_place(blob3) assert_mainloop_emits(watcher, "xsettings-changed") assert watcher.get_settings_blob() == blob3 # Test where the property does not change, but a manager change # message is sent: manager3 = XSettingsManager() manager3.set_blob_in_place(blob3) assert_mainloop_emits(watcher, "xsettings-changed") assert watcher.get_settings_blob() == blob3
class X11ServerBase(X11ServerCore): """ Base class for X11 servers, adds uinput, icc and xsettings synchronization to the X11ServerCore class (see XpraServer or DesktopServer for actual implementations) """ def __init__(self): X11ServerCore.__init__(self) self._default_xsettings = {} self._settings = {} self._xsettings_manager = None self._xsettings_enabled = False def do_init(self, opts): X11ServerCore.do_init(self, opts) self._xsettings_enabled = opts.xsettings if self._xsettings_enabled: from xpra.x11.xsettings import XSettingsHelper self._default_xsettings = XSettingsHelper().get_settings() log("_default_xsettings=%s", self._default_xsettings) self.init_all_server_settings() def last_client_exited(self): self.reset_settings() X11ServerCore.last_client_exited(self) def init_virtual_devices(self, devices): #(this runs in the main thread - before the main loop starts) #for the time being, we only use the pointer if there is one: pointer = devices.get("pointer") self.input_devices = "xtest" if pointer: mouselog("init_virtual_devices(%s) got pointer=%s", devices, pointer) uinput_device = pointer.get("uinput") #name = pointer.get("name") device_path = pointer.get("device") if uinput_device: self.input_devices = "uinput" xtest = XTestPointerDevice() ox, oy = 100, 100 xtest.move_pointer(0, ox, oy) self.pointer_device = UInputPointerDevice( uinput_device, device_path) nx, ny = 200, 200 self.pointer_device.move_pointer(0, nx, ny) def verify_uinput_moved(): pos = None #@UnusedVariable with xswallow: pos = X11Keyboard.query_pointer() mouselog("X11Keyboard.query_pointer=%s", pos) if pos == (ox, oy): mouselog.warn("Warning: %s failed verification", self.pointer_device) mouselog.warn(" expected pointer at %s, now at %s", (nx, ny), pos) mouselog.warn(" usign XTest fallback") self.pointer_device = xtest self.input_devices = "xtest" self.timeout_add(1000, verify_uinput_moved) try: mouselog.info( "pointer device emulation using %s", str(self.pointer_device).replace("PointerDevice", "")) except Exception as e: mouselog("cannot get pointer device class from %s: %s", self.pointer_device, e) def dpi_changed(self): #re-apply the same settings, which will apply the new dpi override to it: self.update_server_settings() def set_icc_profile(self): ui_clients = [s for s in self._server_sources.values() if s.ui_client] if len(ui_clients) != 1: screenlog("%i UI clients, not setting ICC profile") self.reset_icc_profile() return icc = ui_clients[0].icc data = None for x in ("data", "icc-data", "icc-profile"): if x in icc: data = icc.get(x) break if not data: screenlog("no icc data found in %s", icc) self.reset_icc_profile() return screenlog("set_icc_profile() icc data for %s: %s (%i bytes)", ui_clients[0], hexstr(data or ""), len(data or "")) from xpra.x11.gtk_x11.prop import prop_set #each CARD32 contains just one 8-bit value - don't ask me why prop_set(self.root_window, "_ICC_PROFILE", ["u32"], [ord(x) for x in data]) prop_set(self.root_window, "_ICC_PROFILE_IN_X_VERSION", "u32", 0 * 100 + 4) #0.4 -> 0*100+4*1 def reset_icc_profile(self): screenlog("reset_icc_profile()") from xpra.x11.gtk_x11.prop import prop_del prop_del(self.root_window, "_ICC_PROFILE") prop_del(self.root_window, "_ICC_PROFILE_IN_X_VERSION") def reset_settings(self): if not self._xsettings_enabled: return log("resetting xsettings to: %s", self._default_xsettings) self.set_xsettings(self._default_xsettings or (0, ())) def set_xsettings(self, v): if not self._xsettings_enabled: return log("set_xsettings(%s)", v) if self._xsettings_manager is None: from xpra.x11.xsettings import XSettingsManager self._xsettings_manager = XSettingsManager() self._xsettings_manager.set_settings(v) def init_all_server_settings(self): log("init_all_server_settings() dpi=%i, default_dpi=%i", self.dpi, self.default_dpi) #almost like update_all, except we use the default_dpi, #since this is called before the first client connects self.do_update_server_settings( { "resource-manager": "", "xsettings-blob": (0, []) }, reset=True, dpi=self.default_dpi, cursor_size=24) def update_all_server_settings(self, reset=False): self.update_server_settings( { "resource-manager": "", "xsettings-blob": (0, []), }, reset=reset) def update_server_settings(self, settings=None, reset=False): self.do_update_server_settings(settings or self._settings, reset, self.dpi, self.double_click_time, self.double_click_distance, self.antialias, self.cursor_size) def do_update_server_settings(self, settings, reset=False, dpi=0, double_click_time=0, double_click_distance=(-1, -1), antialias={}, cursor_size=-1): if not self._xsettings_enabled: log("ignoring xsettings update: %s", settings) return if reset: #FIXME: preserve serial? (what happens when we change values which had the same serial?) self.reset_settings() self._settings = {} if self._default_xsettings: #try to parse default xsettings into a dict: try: for _, prop_name, value, _ in self._default_xsettings[1]: self._settings[prop_name] = value except Exception as e: log("failed to parse %s", self._default_xsettings) log.warn("Warning: failed to parse default XSettings:") log.warn(" %s", e) old_settings = dict(self._settings) log("server_settings: old=%s, updating with=%s", nonl(old_settings), nonl(settings)) log( "overrides: dpi=%s, double click time=%s, double click distance=%s", dpi, double_click_time, double_click_distance) log("overrides: antialias=%s", antialias) self._settings.update(settings) for k, v in settings.items(): #cook the "resource-manager" value to add the DPI and/or antialias values: if k == "resource-manager" and (dpi > 0 or antialias or cursor_size > 0): value = v.decode("utf-8") #parse the resources into a dict: values = {} options = value.split("\n") for option in options: if not option: continue parts = option.split(":\t", 1) if len(parts) != 2: log("skipped invalid option: '%s'", option) continue values[parts[0]] = parts[1] if cursor_size > 0: values["Xcursor.size"] = cursor_size if dpi > 0: values["Xft.dpi"] = dpi values["Xft/DPI"] = dpi * 1024 values["gnome.Xft/DPI"] = dpi * 1024 if antialias: ad = typedict(antialias) subpixel_order = "none" sss = self._server_sources.values() if len(sss) == 1: #only honour sub-pixel hinting if a single client is connected #and only when it is not using any scaling (or overriden with SCALED_FONT_ANTIALIAS): ss = sss[0] if SCALED_FONT_ANTIALIAS or ( not ss.desktop_size_unscaled or ss.desktop_size_unscaled == ss.desktop_size): subpixel_order = ad.strget("orientation", "none").lower() values.update({ "Xft.antialias": ad.intget("enabled", -1), "Xft.hinting": ad.intget("hinting", -1), "Xft.rgba": subpixel_order, "Xft.hintstyle": _get_antialias_hintstyle(ad) }) log("server_settings: resource-manager values=%s", nonl(values)) #convert the dict back into a resource string: value = '' for vk, vv in values.items(): value += "%s:\t%s\n" % (vk, vv) #record the actual value used self._settings["resource-manager"] = value v = value.encode("utf-8") #cook xsettings to add various settings: #(as those may not be present in xsettings on some platforms.. like win32 and osx) if k == "xsettings-blob" and (self.double_click_time > 0 or self.double_click_distance != (-1, -1) or antialias or dpi > 0): from xpra.x11.xsettings_prop import XSettingsTypeInteger, XSettingsTypeString def set_xsettings_value(name, value_type, value): #remove existing one, if any: serial, values = v new_values = [(_t, _n, _v, _s) for (_t, _n, _v, _s) in values if _n != name] new_values.append((value_type, name, value, 0)) return serial, new_values def set_xsettings_int(name, value): if value < 0: #not set, return v unchanged return v return set_xsettings_value(name, XSettingsTypeInteger, value) if dpi > 0: v = set_xsettings_int("Xft/DPI", dpi * 1024) if double_click_time > 0: v = set_xsettings_int("Net/DoubleClickTime", self.double_click_time) if antialias: ad = typedict(antialias) v = set_xsettings_int("Xft/Antialias", ad.intget("enabled", -1)) v = set_xsettings_int("Xft/Hinting", ad.intget("hinting", -1)) v = set_xsettings_value( "Xft/RGBA", XSettingsTypeString, ad.strget("orientation", "none").lower()) v = set_xsettings_value("Xft/HintStyle", XSettingsTypeString, _get_antialias_hintstyle(ad)) if double_click_distance != (-1, -1): #some platforms give us a value for each axis, #but X11 only has one, so take the average try: x, y = double_click_distance if x > 0 and y > 0: d = iround((x + y) / 2.0) d = max(1, min(128, d)) #sanitize it a bit v = set_xsettings_int("Net/DoubleClickDistance", d) except Exception as e: log.warn( "error setting double click distance from %s: %s", double_click_distance, e) if k not in old_settings or v != old_settings[k]: def root_set(p): from xpra.x11.gtk_x11.prop import prop_set log("server_settings: setting %s to %s", nonl(p), nonl(v)) prop_set(self.root_window, p, "latin1", v.decode("utf-8")) if k == "xsettings-blob": self.set_xsettings(v) elif k == "resource-manager": root_set("RESOURCE_MANAGER")
class XpraServer(gobject.GObject, X11ServerBase): __gsignals__ = { "xpra-child-map-event": one_arg_signal, "xpra-cursor-event": one_arg_signal, } def __init__(self, clobber): gobject.GObject.__init__(self) X11ServerBase.__init__(self, clobber) def init(self, opts): self.xsettings_enabled = opts.xsettings self.wm_name = opts.wm_name X11ServerBase.init(self, opts) def x11_init(self): X11ServerBase.x11_init(self) assert init_x11_filter() is True self._has_grab = 0 self._has_focus = 0 # Do this before creating the Wm object, to avoid clobbering its # selecting SubstructureRedirect. root = gtk.gdk.get_default_root_window() root.set_events(root.get_events() | gtk.gdk.SUBSTRUCTURE_MASK) root.property_change(gtk.gdk.atom_intern("XPRA_SERVER", False), gtk.gdk.atom_intern("STRING", False), 8, gtk.gdk.PROP_MODE_REPLACE, xpra.__version__) add_event_receiver(root, self) ### Create the WM object self._wm = Wm(self.clobber, self.wm_name) self._wm.connect("new-window", self._new_window_signaled) self._wm.connect("bell", self._bell_signaled) self._wm.connect("quit", lambda _: self.clean_quit(True)) self._wm.connect("show-desktop", self._show_desktop) #save default xsettings: self.default_xsettings = XSettingsHelper().get_settings() settingslog("default_xsettings=%s", self.default_xsettings) self._settings = {} self._xsettings_manager = None #for handling resize synchronization between client and server (this is not xsync!): self.last_client_configure_event = 0 self.snc_timer = 0 #cursor: self.default_cursor_data = None self.last_cursor_serial = None self.last_cursor_data = None self.send_cursor_pending = False def get_default_cursor(): self.default_cursor_data = X11Keyboard.get_cursor_image() cursorlog("get_default_cursor=%s", self.default_cursor_data) trap.swallow_synced(get_default_cursor) self._wm.enableCursors(True) def make_hello(self, source): capabilities = X11ServerBase.make_hello(self, source) if source.wants_features: capabilities["window.raise"] = True capabilities["window.resize-counter"] = True capabilities["window.configure.skip-geometry"] = True capabilities["pointer.grabs"] = True return capabilities def do_get_info(self, proto, server_sources, window_ids): info = X11ServerBase.do_get_info(self, proto, server_sources, window_ids) info["focused"] = self._has_focus info["grabbed"] = self._has_grab log("do_get_info: adding cursor=%s", self.last_cursor_data) #copy to prevent race: cd = self.last_cursor_data if cd is None: info["cursor"] = "None" else: info["cursor.is_default"] = bool(self.default_cursor_data and len(self.default_cursor_data)>=8 and len(cd)>=8 and cd[7]==cd[7]) #all but pixels: for i, x in enumerate(("x", "y", "width", "height", "xhot", "yhot", "serial", None, "name")): if x: v = cd[i] or "" info["cursor." + x] = v return info def get_ui_info(self, proto, wids, *args): info = X11ServerBase.get_ui_info(self, proto, wids, *args) #_NET_WM_NAME: wm = self._wm if wm: info["window-manager-name"] = wm.get_net_wm_name() #now cursor size info: display = gtk.gdk.display_get_default() pos = display.get_default_screen().get_root_window().get_pointer()[:2] info["cursor.position"] = pos for prop, size in {"default" : display.get_default_cursor_size(), "max" : display.get_maximal_cursor_size()}.items(): if size is None: continue info["cursor.%s_size" % prop] = size return info def get_window_info(self, window): info = X11ServerBase.get_window_info(self, window) info["focused"] = self._has_focus and self._window_to_id.get(window, -1)==self._has_focus info["grabbed"] = self._has_grab and self._window_to_id.get(window, -1)==self._has_grab return info def set_desktops(self, names): if self._wm: self._wm.set_desktop_list(names) def set_workarea(self, workarea): if self._wm: self._wm.set_workarea(workarea.x, workarea.y, workarea.width, workarea.height) def set_desktop_geometry(self, width, height): if self._wm: self._wm.set_desktop_geometry(width, height) def set_dpi(self, xdpi, ydpi): if self._wm: self._wm.set_dpi(xdpi, ydpi) def get_transient_for(self, window): transient_for = window.get_property("transient-for") log("get_transient_for window=%s, transient_for=%s", window, transient_for) if transient_for is None: return None xid = transient_for.xid log("transient_for.xid=%#x", xid) for w,wid in self._window_to_id.items(): if w.get_property("xid")==xid: log("found match, window id=%s", wid) return wid root = gtk.gdk.get_default_root_window() if root.xid==xid: log("transient-for using root") return -1 #-1 is the backwards compatible marker for root... log("not found transient_for=%s, xid=%#x", transient_for, xid) return None def is_shown(self, window): return self._desktop_manager.is_shown(window) def cleanup(self, *args): if self._tray: self._tray.cleanup() self._tray = None X11ServerBase.cleanup(self) cleanup_x11_filter() cleanup_all_event_receivers() if self._wm: self._wm.cleanup() self._wm = None if self._has_grab: #normally we set this value when we receive the NotifyUngrab #but at this point in the cleanup, we probably won't, so force set it: self._has_grab = 0 self.X11_ungrab() def cleanup_source(self, protocol): had_client = len(self._server_sources)>0 X11ServerBase.cleanup_source(self, protocol) has_client = len(self._server_sources)>0 if had_client and not has_client: #last client is gone: self.reset_settings() if self._has_grab: self.X11_ungrab() def load_existing_windows(self, system_tray): # Tray handler: self._tray = None if system_tray: try: self._tray = SystemTray() except Exception as e: log.error("cannot setup tray forwarding: %s", e, exc_info=True) ### Create our window managing data structures: self._desktop_manager = DesktopManager() self._wm.get_property("toplevel").add(self._desktop_manager) self._desktop_manager.show_all() ### Load in existing windows: for window in self._wm.get_property("windows"): self._add_new_window(window) root = gtk.gdk.get_default_root_window() for window in get_children(root): if X11Window.is_override_redirect(window.xid) and X11Window.is_mapped(window.xid): self._add_new_or_window(window) def send_windows_and_cursors(self, ss, sharing=False): # We send the new-window packets sorted by id because this sorts them # from oldest to newest -- and preserving window creation order means # that the earliest override-redirect windows will be on the bottom, # which is usually how things work. (I don't know that anyone cares # about this kind of correctness at all, but hey, doesn't hurt.) windowlog("send_windows_and_cursors(%s, %s) will send: %s", ss, sharing, self._id_to_window) for wid in sorted(self._id_to_window.keys()): window = self._id_to_window[wid] if not window.is_managed(): #we keep references to windows that aren't meant to be displayed.. continue #most of the code here is duplicated from the send functions #so we can send just to the new client and request damage #just for the new client too: if window.is_tray(): #code more or less duplicated from _send_new_tray_window_packet: w, h = window.get_property("geometry")[2:4] if ss.system_tray: ss.new_tray(wid, window, w, h) ss.damage(wid, window, 0, 0, w, h) elif not sharing: #park it outside the visible area window.move_resize(-200, -200, w, h) elif window.is_OR(): #code more or less duplicated from _send_new_or_window_packet: x, y, w, h = window.get_property("geometry") wprops = self.client_properties.get("%s|%s" % (wid, ss.uuid)) ss.new_window("new-override-redirect", wid, window, x, y, w, h, wprops) ss.damage(wid, window, 0, 0, w, h) else: #code more or less duplicated from send_new_window_packet: if not sharing: self._desktop_manager.hide_window(window) x, y, w, h = self._desktop_manager.window_geometry(window) wprops = self.client_properties.get("%s|%s" % (wid, ss.uuid)) ss.new_window("new-window", wid, window, x, y, w, h, wprops) #cursors: get sizes and send: display = gtk.gdk.display_get_default() self.cursor_sizes = display.get_default_cursor_size(), display.get_maximal_cursor_size() cursorlog("cursor_sizes=%s", self.cursor_sizes) ss.send_cursor() def _new_window_signaled(self, wm, window): self._add_new_window(window) def do_xpra_child_map_event(self, event): windowlog("do_xpra_child_map_event(%s)", event) if event.override_redirect: self._add_new_or_window(event.window) def _add_new_window_common(self, window): windowlog("adding window %s", window) for prop in window.get_dynamic_property_names(): window.connect("notify::%s" % prop, self._update_metadata) wid = X11ServerBase._add_new_window_common(self, window) window.managed_connect("client-contents-changed", self._contents_changed) window.managed_connect("unmanaged", self._lost_window) window.managed_connect("raised", self._raised_window) window.managed_connect("initiate-moveresize", self._initiate_moveresize) window.managed_connect("grab", self._window_grab) window.managed_connect("ungrab", self._window_ungrab) return wid def _add_new_window(self, window): self._add_new_window_common(window) _, _, w, h, _ = window.get_property("client-window").get_geometry() x, y, _, _, _ = window.corral_window.get_geometry() windowlog("Discovered new ordinary window: %s (geometry=%s)", window, (x, y, w, h)) self._desktop_manager.add_window(window, x, y, w, h) window.connect("notify::geometry", self._window_resized_signaled) window.connect("notify::iconic", self._iconic_changed) self._send_new_window_packet(window) def _iconic_changed(self, window, pspec): #only defined for debugging purposes log("_iconic_changed(%s, %s) iconic=%s, shown=%s", window, pspec, window.get_property("iconic"), self._desktop_manager.is_shown(window)) def _window_resized_signaled(self, window, *args): nw, nh = window.get_property("actual-size") x, y = window.get_position() geom = self._desktop_manager.window_geometry(window) windowlog("XpraServer._window_resized_signaled(%s,%s) position=%sx%s, actual-size=%sx%s, current geometry=%s", window, args, x, y, nw, nh, geom) if geom[:4]==[x, y, nw, nh]: #unchanged return geom[:4] = [x, y, nw, nh] lcce = self.last_client_configure_event if not self._desktop_manager.is_shown(window): self.size_notify_clients(window) return if self.snc_timer>0: gobject.source_remove(self.snc_timer) #TODO: find a better way to choose the timer delay: #for now, we wait at least 100ms, up to 250ms if the client has just sent us a resize: #(lcce should always be in the past, so min(..) should be redundant here) delay = max(100, min(250, 250 + 1000 * (lcce-time.time()))) self.snc_timer = gobject.timeout_add(int(delay), self.size_notify_clients, window, lcce) def size_notify_clients(self, window, lcce=-1): log("size_notify_clients(%s, %s) last_client_configure_event=%s", window, lcce, self.last_client_configure_event) self.snc_timer = 0 wid = self._window_to_id.get(window) if lcce>0 and lcce!=self.last_client_configure_event or not wid: #we have received a new client resize since, #or the window is simply gone return geom = self._desktop_manager.window_geometry(window) x, y, nw, nh = geom[:4] resize_counter = self._desktop_manager.get_resize_counter(window, 1) for ss in self._server_sources.values(): ss.move_resize_window(self._window_to_id[window], window, x, y, nw, nh, resize_counter) #refresh to ensure the client gets the new window contents: #TODO: to save bandwidth, we should compare the dimensions and skip the refresh #if the window is smaller than before, or at least only send the new edges rather than the whole window ss.damage(wid, window, 0, 0, nw, nh, {}) def _add_new_or_window(self, raw_window): xid = raw_window.xid if raw_window.get_window_type()==gtk.gdk.WINDOW_TEMP: #ignoring one of gtk's temporary windows #all the windows we manage should be gtk.gdk.WINDOW_FOREIGN windowlog("ignoring TEMP window %#x", xid) return WINDOW_MODEL_KEY = "_xpra_window_model_" wid = raw_window.get_data(WINDOW_MODEL_KEY) window = self._id_to_window.get(wid) if window: if window.is_managed(): windowlog("found existing window model %s for %#x, will refresh it", type(window), xid) geometry = window.get_property("geometry") _, _, w, h = geometry self._damage(window, 0, 0, w, h, options={"min_delay" : 50}) return windowlog("found existing model %s (but no longer managed!) for %#x", type(window), xid) #we could try to re-use the existing model and window ID, #but for now it is just easier to create a new one: self._lost_window(window) tray_window = get_tray_window(raw_window) windowlog("Discovered new override-redirect window: %#x (tray=%s)", xid, tray_window) try: if tray_window is not None: assert self._tray window = SystemTrayWindowModel(raw_window) wid = self._add_new_window_common(window) raw_window.set_data(WINDOW_MODEL_KEY, wid) window.call_setup() self._send_new_tray_window_packet(wid, window) else: window = OverrideRedirectWindowModel(raw_window) wid = self._add_new_window_common(window) raw_window.set_data(WINDOW_MODEL_KEY, wid) window.call_setup() window.connect("notify::geometry", self._or_window_geometry_changed) self._send_new_or_window_packet(window) except Unmanageable as e: if window: windowlog("window %s is not manageable: %s", window, e) #if window is set, we failed after instantiating it, #so we need to fail it manually: window.setup_failed(e) if window in self._window_to_id: self._lost_window(window, False) else: windowlog.warn("cannot add window %#x: %s", xid, e) #from now on, we return to the gtk main loop, #so we *should* get a signal when the window goes away def _or_window_geometry_changed(self, window, pspec=None): (x, y, w, h) = window.get_property("geometry")[:4] if w>=32768 or h>=32768: windowlog.error("not sending new invalid window dimensions: %ix%i !", w, h) return windowlog("or_window_geometry_changed: %s (window=%s)", window.get_property("geometry"), window) wid = self._window_to_id[window] for ss in self._server_sources.values(): ss.or_window_geometry(wid, window, x, y, w, h) def do_xpra_cursor_event(self, event): if not self.cursors: return if self.last_cursor_serial==event.cursor_serial: cursorlog("ignoring cursor event %s with the same serial number %s", event, self.last_cursor_serial) return cursorlog("cursor_event: %s", event) self.last_cursor_serial = event.cursor_serial for ss in self._server_sources.values(): ss.send_cursor() return False def _bell_signaled(self, wm, event): log("bell signaled on window %s", event.window.xid) if not self.bell: return wid = 0 if event.window!=gtk.gdk.get_default_root_window() and event.window_model is not None: try: wid = self._window_to_id[event.window_model] except: pass log("_bell_signaled(%s,%r) wid=%s", wm, event, wid) for ss in self._server_sources.values(): ss.bell(wid, event.device, event.percent, event.pitch, event.duration, event.bell_class, event.bell_id, event.bell_name or "") def _show_desktop(self, wm, show): log("show_desktop(%s, %s)", wm, show) for ss in self._server_sources.values(): ss.show_desktop(show) def _focus(self, server_source, wid, modifiers): focuslog("focus wid=%s has_focus=%s", wid, self._has_focus) if self._has_focus==wid: #nothing to do! return had_focus = self._id_to_window.get(self._has_focus) def reset_focus(): toplevel = None if self._wm: toplevel = self._wm.get_property("toplevel") focuslog("reset_focus() %s / %s had focus (toplevel=%s)", self._has_focus, had_focus, toplevel) self._clear_keys_pressed() # FIXME: kind of a hack: self._has_focus = 0 #toplevel may be None during cleanup! if toplevel: toplevel.reset_x_focus() if wid == 0: #wid==0 means root window return reset_focus() window = self._id_to_window.get(wid) if not window: #not found! (go back to root) return reset_focus() if window.is_OR(): focuslog.warn("focus(..) cannot focus OR window: %s", window) return focuslog("focus: giving focus to %s", window) #using idle_add seems to prevent some focus races: def give_focus(): if not window.is_managed(): return window.raise_window() window.give_client_focus() gobject.idle_add(give_focus) if server_source and modifiers is not None: focuslog("focus: will set modified mask to %s", modifiers) server_source.make_keymask_match(modifiers) self._has_focus = wid def get_focus(self): return self._has_focus def _send_new_window_packet(self, window): geometry = self._desktop_manager.window_geometry(window) self._do_send_new_window_packet("new-window", window, geometry) def _send_new_or_window_packet(self, window, options=None): geometry = window.get_property("geometry") self._do_send_new_window_packet("new-override-redirect", window, geometry) (_, _, w, h) = geometry self._damage(window, 0, 0, w, h, options=options) def _send_new_tray_window_packet(self, wid, window, options=None): (_, _, w, h) = window.get_property("geometry") for ss in self._server_sources.values(): ss.new_tray(wid, window, w, h) self._damage(window, 0, 0, w, h, options=options) def _update_metadata(self, window, pspec): windowlog("updating metadata on %s: %s", window, pspec) wid = self._window_to_id[window] for ss in self._server_sources.values(): ss.window_metadata(wid, window, pspec.name) def _lost_window(self, window, wm_exiting=False): wid = self._window_to_id[window] windowlog("lost_window: %s - %s", wid, window) for ss in self._server_sources.values(): ss.lost_window(wid, window) del self._window_to_id[window] del self._id_to_window[wid] for ss in self._server_sources.values(): ss.remove_window(wid, window) def _contents_changed(self, window, event): if window.is_OR() or self._desktop_manager.visible(window): self._damage(window, event.x, event.y, event.width, event.height) def _window_grab(self, window, event): grab_id = self._window_to_id.get(window, -1) grablog("window_grab(%s, %s) has_grab=%s, has focus=%s, grab window=%s", window, event, self._has_grab, self._has_focus, grab_id) if grab_id<0 or self._has_grab==grab_id: return self._has_grab = grab_id for ss in self._server_sources.values(): ss.pointer_grab(self._has_grab) def _window_ungrab(self, window, event): grab_id = self._window_to_id.get(window, -1) grablog("window_ungrab(%s, %s) has_grab=%s, has focus=%s, grab window=%s", window, event, self._has_grab, self._has_focus, grab_id) if not self._has_grab: return self._has_grab = 0 for ss in self._server_sources.values(): ss.pointer_ungrab(grab_id) def _initiate_moveresize(self, window, event): log("initiate_moveresize(%s, %s)", window, event) assert len(event.data)==5 #x_root, y_root, direction, button, source_indication = event.data wid = self._window_to_id[window] for ss in self._server_sources.values(): ss.initiate_moveresize(wid, window, *event.data) def _raised_window(self, window, event): wid = self._window_to_id[window] windowlog("raised window: %s (%s) wid=%s, current focus=%s", window, event, wid, self._has_focus) if self._has_focus==wid: return for ss in self._server_sources.values(): ss.raise_window(wid, window) def _set_window_state(self, proto, wid, window, new_window_state): metadatalog("set_window_state%s", (wid, window, new_window_state)) changes = [] for k in ("maximized", "above", "below", "fullscreen", "sticky", "shaded", "skip-pager", "skip-taskbar", "iconified"): if k in new_window_state: #stupid naming conflict (should have used the same at both ends): wpropname = {"iconified" : "iconic"}.get(k, k) new_state = bool(new_window_state.get(k, False)) cur_state = bool(window.get_property(wpropname)) metadatalog("set window state for '%s': current state=%s, new state=%s", k, cur_state, new_state) if cur_state!=new_state: window.set_property(wpropname, new_state) changes.append(k) metadatalog("set_window_state: changes=%s", changes) return changes def _process_map_window(self, proto, packet): wid, x, y, w, h = packet[1:6] window = self._id_to_window.get(wid) if not window: windowlog("cannot map window %s: already removed!", wid) return assert not window.is_OR() windowlog("client mapped window %s - %s, at: %s", wid, window, (x, y, w, h)) if len(packet)>=8: self._set_window_state(proto, wid, window, packet[7]) ax, ay, aw, ah = self._clamp_window(proto, wid, window, x, y, w, h) self._desktop_manager.configure_window(window, ax, ay, aw, ah) self._desktop_manager.show_window(window) if len(packet)>=7: self._set_client_properties(proto, wid, window, packet[6]) self._damage(window, 0, 0, w, h) def _process_unmap_window(self, proto, packet): wid = packet[1] window = self._id_to_window.get(wid) if not window: log("cannot map window %s: already removed!", wid) return if len(packet)>=4: #optional window_state added in 0.15 to update flags #during iconification events: self._set_window_state(proto, wid, window, packet[3]) assert not window.is_OR() windowlog("client unmapped window %s - %s", wid, window) for ss in self._server_sources.values(): ss.unmap_window(wid, window) window.unmap() iconified = len(packet)>=3 and bool(packet[2]) if iconified and not window.get_property("iconic"): window.set_property("iconic", True) self._desktop_manager.hide_window(window) def _clamp_window(self, proto, wid, window, x, y, w, h): rw, rh = self.get_root_window_size() #clamp to root window size if x>=rw or y>=rh: log("clamping window position %ix%i to root window size %ix%i", x, y, rw, rh) x = max(0, min(x, rw-w)) y = max(0, min(y, rh-h)) #tell this client to honour the new location ss = self._server_sources.get(proto) if ss: resize_counter = self._desktop_manager.get_resize_counter(window, 1) ss.move_resize_window(wid, window, x, y, w, h, resize_counter) return x, y, w, h def _process_configure_window(self, proto, packet): wid, x, y, w, h = packet[1:6] resize_counter = 0 if len(packet)>=8: resize_counter = packet[7] #some "configure-window" packets are only meant for metadata updates: skip_geometry = len(packet)>=10 and packet[9] window = self._id_to_window.get(wid) windowlog("client configured window %s - %s, at: %s", wid, window, (x, y, w, h)) if not window: windowlog("cannot map window %s: already removed!", wid) return damage = False if window.is_tray(): assert self._tray if not skip_geometry: traylog("tray %s configured to: %s", window, (x, y, w, h)) self._tray.move_resize(window, x, y, w, h) damage = True else: assert not window.is_OR() or skip_geometry, "received a configure packet with geometry for OR window %s from %s: %s" % (window, proto, packet) self.last_client_configure_event = time.time() if len(packet)>=9: changes = self._set_window_state(proto, wid, window, packet[8]) damage = len(changes)>0 if not skip_geometry: owx, owy, oww, owh = self._desktop_manager.window_geometry(window) windowlog("_process_configure_window(%s) old window geometry: %s", packet[1:], (owx, owy, oww, owh)) ax, ay, aw, ah = self._clamp_window(proto, wid, window, x, y, w, h) self._desktop_manager.configure_window(window, ax, ay, aw, ah, resize_counter) damage |= owx!=ax or owy!=ay or oww!=aw or owh!=ah if len(packet)>=7: self._set_client_properties(proto, wid, window, packet[6]) if damage: self._damage(window, 0, 0, w, h) def _set_client_properties(self, proto, wid, window, new_client_properties): """ Override so we can update the workspace on the window directly, instead of storing it as a client property """ workspace = new_client_properties.get("workspace") workspacelog("workspace from client properties %s: %s", new_client_properties, workspace) if workspace is not None: window.move_to_workspace(workspace) #we have handled it on the window directly, so remove it from client properties del new_client_properties["workspace"] #handle the rest as normal: X11ServerBase._set_client_properties(self, proto, wid, window, new_client_properties) """ override so we can raise the window under the cursor (gtk raise does not change window stacking, just focus) """ def _move_pointer(self, wid, pos): window = self._id_to_window.get(wid) if not window: mouselog("_move_pointer(%s, %s) invalid window id", wid, pos) else: mouselog("raising %s", window) window.raise_window() X11ServerBase._move_pointer(self, wid, pos) def _process_close_window(self, proto, packet): wid = packet[1] window = self._id_to_window.get(wid, None) windowlog("client closed window %s - %s", wid, window) if window: window.request_close() else: windowlog("cannot close window %s: it is already gone!", wid) def make_screenshot_packet(self): try: return self.do_make_screenshot_packet() except: log.error("make_screenshot_packet()", exc_info=True) return None def do_make_screenshot_packet(self): log("grabbing screenshot") regions = [] OR_regions = [] for wid in reversed(sorted(self._id_to_window.keys())): window = self._id_to_window.get(wid) log("screenshot: window(%s)=%s", wid, window) if window is None: continue if window.is_tray(): log("screenshot: skipping tray window %s", wid) continue if not window.is_managed(): log("screenshot: window %s is not/no longer managed", wid) continue if window.is_OR(): x, y = window.get_property("geometry")[:2] else: x, y = self._desktop_manager.window_geometry(window)[:2] log("screenshot: position(%s)=%s,%s", window, x, y) w, h = window.get_dimensions() log("screenshot: size(%s)=%sx%s", window, w, h) try: with xsync: img = window.get_image(0, 0, w, h) except: log.warn("screenshot: window %s could not be captured", wid) continue if img is None: log.warn("screenshot: no pixels for window %s", wid) continue log("screenshot: image=%s, size=%s", (img, img.get_size())) if img.get_pixel_format() not in ("RGB", "RGBA", "XRGB", "BGRX", "ARGB", "BGRA"): log.warn("window pixels for window %s using an unexpected rgb format: %s", wid, img.get_pixel_format()) continue item = (wid, x, y, img) if window.is_OR(): OR_regions.append(item) elif self._has_focus==wid: #window with focus first (drawn last) regions.insert(0, item) else: regions.append(item) all_regions = OR_regions+regions if len(all_regions)==0: log("screenshot: no regions found, returning empty 0x0 image!") return ["screenshot", 0, 0, "png", -1, ""] log("screenshot: found regions=%s, OR_regions=%s", len(regions), len(OR_regions)) #in theory, we could run the rest in a non-UI thread since we're done with GTK.. minx = min([x for (_,x,_,_) in all_regions]) miny = min([y for (_,_,y,_) in all_regions]) maxx = max([(x+img.get_width()) for (_,x,_,img) in all_regions]) maxy = max([(y+img.get_height()) for (_,_,y,img) in all_regions]) width = maxx-minx height = maxy-miny log("screenshot: %sx%s, min x=%s y=%s", width, height, minx, miny) from PIL import Image #@UnresolvedImport screenshot = Image.new("RGBA", (width, height)) for wid, x, y, img in reversed(all_regions): pixel_format = img.get_pixel_format() target_format = { "XRGB" : "RGB", "BGRX" : "RGB", "BGRA" : "RGBA"}.get(pixel_format, pixel_format) pixels = img.get_pixels() #PIL cannot use the memoryview directly: if _memoryview and isinstance(pixels, _memoryview): pixels = pixels.tobytes() try: window_image = Image.frombuffer(target_format, (w, h), pixels, "raw", pixel_format, img.get_rowstride()) except: log.error("Error parsing window pixels in %s format", pixel_format, exc_info=True) continue tx = x-minx ty = y-miny screenshot.paste(window_image, (tx, ty)) buf = StringIOClass() screenshot.save(buf, "png") data = buf.getvalue() buf.close() packet = ["screenshot", width, height, "png", width*4, Compressed("png", data)] log("screenshot: %sx%s %s", packet[1], packet[2], packet[-1]) return packet def reset_settings(self): if not self.xsettings_enabled: return settingslog("resetting xsettings to: %s", self.default_xsettings) self.set_xsettings(self.default_xsettings or (0, ())) def set_xsettings(self, v): if self._xsettings_manager is None: self._xsettings_manager = XSettingsManager() self._xsettings_manager.set_settings(v) def _get_antialias_hintstyle(self): ad = typedict(self.antialias) hintstyle = ad.strget("hintstyle", "").lower() if hintstyle in ("hintnone", "hintslight", "hintmedium", "hintfull"): #X11 clients can give us what we need directly: return hintstyle #win32 style contrast value: contrast = ad.intget("contrast", -1) if contrast>1600: return "hintfull" elif contrast>1000: return "hintmedium" elif contrast>0: return "hintslight" return "hintnone" def update_server_settings(self, settings, reset=False): if not self.xsettings_enabled: settingslog("ignoring xsettings update: %s", settings) return if reset: #FIXME: preserve serial? (what happens when we change values which had the same serial?) self.reset_settings() self._settings = {} if self.default_xsettings: self._settings = self._default_xsettings[1] old_settings = dict(self._settings) settingslog("server_settings: old=%s, updating with=%s", nonl(old_settings), nonl(settings)) settingslog("overrides: dpi=%s, double click time=%s, double click distance=%s", self.dpi, self.double_click_time, self.double_click_distance) settingslog("overrides: antialias=%s", self.antialias) self._settings.update(settings) root = gtk.gdk.get_default_root_window() for k, v in settings.items(): #cook the "resource-manager" value to add the DPI: if k=="resource-manager" and self.dpi>0: value = v.decode("utf-8") #parse the resources into a dict: values={} options = value.split("\n") for option in options: if not option: continue parts = option.split(":\t") if len(parts)!=2: continue values[parts[0]] = parts[1] values["Xft.dpi"] = self.dpi values["Xft/DPI"] = self.dpi*1024 values["gnome.Xft/DPI"] = self.dpi*1024 if self.antialias: ad = typedict(self.antialias) values.update({ "Xft.antialias" : ad.intget("enabled", -1), "Xft.hinting" : ad.intget("hinting", -1), "Xft.rgba" : ad.strget("orientation", "none").lower(), "Xft.hintstyle" : self._get_antialias_hintstyle()}) settingslog("server_settings: resource-manager values=%s", nonl(values)) #convert the dict back into a resource string: value = '' for vk, vv in values.items(): value += "%s:\t%s\n" % (vk, vv) #record the actual value used self._settings["resource-manager"] = value v = value.encode("utf-8") #cook xsettings to add double-click settings: #(as those may not be present in xsettings on some platforms.. like win32 and osx) if k=="xsettings-blob" and (self.double_click_time>0 or self.double_click_distance!=(-1, -1)): from xpra.x11.xsettings_prop import XSettingsTypeInteger, XSettingsTypeString def set_xsettings_value(name, value_type, value): #remove existing one, if any: serial, values = v new_values = [(_t,_n,_v,_s) for (_t,_n,_v,_s) in values if _n!=name] new_values.append((value_type, name, value, 0)) return serial, new_values def set_xsettings_int(name, value): if value<0: #not set, return v unchanged return v return set_xsettings_value(name, XSettingsTypeInteger, value) if self.dpi>0: v = set_xsettings_int("Xft/DPI", self.dpi*1024) if self.double_click_time>0: v = set_xsettings_int("Net/DoubleClickTime", self.double_click_time) if self.antialias: ad = typedict(self.antialias) v = set_xsettings_int("Xft/Antialias", ad.intget("enabled", -1)) v = set_xsettings_int("Xft/Hinting", ad.intget("hinting", -1)) v = set_xsettings_value("Xft/RGBA", XSettingsTypeString, ad.strget("orientation", "none").lower()) v = set_xsettings_value("Xft/HintStyle", XSettingsTypeString, self._get_antialias_hintstyle()) if self.double_click_distance!=(-1, -1): #some platforms give us a value for each axis, #but X11 only has one, so take the average try: x,y = self.double_click_distance if x>0 and y>0: d = (x+y)//2 d = max(1, min(128, d)) #sanitize it a bit v = set_xsettings_int("Net/DoubleClickDistance", d) except Exception as e: log.warn("error setting double click distance from %s: %s", self.double_click_distance, e) if k not in old_settings or v != old_settings[k]: def root_set(p): settingslog("server_settings: setting %s to %s", nonl(p), nonl(v)) prop_set(root, p, "latin1", v.decode("utf-8")) if k == "xsettings-blob": self.set_xsettings(v) elif k == "resource-manager": root_set("RESOURCE_MANAGER") elif self.pulseaudio: if k == "pulse-cookie": root_set("PULSE_COOKIE") elif k == "pulse-id": root_set("PULSE_ID") elif k == "pulse-server": root_set("PULSE_SERVER")