diff --git a/src/setup.py b/src/setup.py index 560c25c5c9..a9196a6449 100755 --- a/src/setup.py +++ b/src/setup.py @@ -147,6 +147,7 @@ def is_RH(): client_ENABLED = DEFAULT x11_ENABLED = DEFAULT and not WIN32 and not OSX +xinput_ENABLED = x11_ENABLED dbus_ENABLED = DEFAULT and x11_ENABLED and not (OSX or WIN32) gtk_x11_ENABLED = DEFAULT and not WIN32 and not OSX gtk2_ENABLED = DEFAULT and client_ENABLED and not PYTHON3 @@ -218,7 +219,8 @@ def is_RH(): "csc_libyuv", "bencode", "cython_bencode", "vsock", "mdns", "clipboard", - "server", "client", "dbus", "x11", "gtk_x11", "service", + "server", "client", "dbus", "x11", "xinput", + "gtk_x11", "service", "gtk2", "gtk3", "html5", "minify", "html5_gzip", "html5_brotli", "pam", @@ -916,6 +918,7 @@ def pkgconfig(*pkgs_options, **ekw): "xpra/x11/bindings/core_bindings.c", "xpra/x11/bindings/posix_display_source.c", "xpra/x11/bindings/ximage.c", + "xpra/x11/bindings/xi2_bindings.c", "xpra/platform/win32/propsys.cpp", "xpra/platform/darwin/gdk_bindings.c", "xpra/net/bencode/cython_bencode.c", @@ -1740,6 +1743,11 @@ def osx_pkgconfig(*pkgs_options, **ekw): ["xpra/x11/bindings/ximage.pyx"], **pkgconfig("x11", "xcomposite", "xdamage", "xext") )) +if xinput_ENABLED: + cython_add(Extension("xpra.x11.bindings.xi2_bindings", + ["xpra/x11/bindings/xi2_bindings.pyx"], + **pkgconfig("x11", "xi") + )) toggle_packages(gtk_x11_ENABLED, "xpra.x11.gtk_x11") if gtk_x11_ENABLED: diff --git a/src/xpra/client/client_base.py b/src/xpra/client/client_base.py index 2f851ee30b..857dcee64f 100644 --- a/src/xpra/client/client_base.py +++ b/src/xpra/client/client_base.py @@ -1,5 +1,5 @@ # This file is part of Xpra. -# Copyright (C) 2010-2016 Antoine Martin +# Copyright (C) 2010-2017 Antoine Martin # Copyright (C) 2008, 2010 Nathaniel Smith # Xpra is released under the terms of the GNU GPL v2, or, at your option, any # later version. See the file COPYING for details. @@ -234,6 +234,7 @@ def setup_connection(self, conn): self._protocol.large_packets.append("keymap-changed") self._protocol.large_packets.append("server-settings") self._protocol.large_packets.append("logging") + self._protocol.large_packets.append("input-devices") self._protocol.set_compression_level(self.compression_level) self._protocol.receive_aliases.update(self._aliases) self._protocol.enable_default_encoder() diff --git a/src/xpra/client/client_window_base.py b/src/xpra/client/client_window_base.py index cc80da1711..df91fe9e49 100644 --- a/src/xpra/client/client_window_base.py +++ b/src/xpra/client/client_window_base.py @@ -659,7 +659,7 @@ def _device_info(self, event): except: return "" - def _button_action(self, button, event, depressed): + def _button_action(self, button, event, depressed, *args): if self._client.readonly: return pointer, modifiers, buttons = self._pointer_modifiers(event) @@ -680,7 +680,7 @@ def _button_action(self, button, event, depressed): b = sb server_buttons.append(b) def send_button(pressed): - self._client.send_button(wid, server_button, pressed, pointer, modifiers, server_buttons) + self._client.send_button(wid, server_button, pressed, pointer, modifiers, server_buttons, *args) pressed_state = self.button_state.get(button, False) if SIMULATE_MOUSE_DOWN and pressed_state is False and depressed is False: mouselog("button action: simulating a missing mouse-down event for window %s before sending the mouse-up event", wid) diff --git a/src/xpra/client/gtk_base/gtk_client_window_base.py b/src/xpra/client/gtk_base/gtk_client_window_base.py index 7883407c77..4e2073881e 100644 --- a/src/xpra/client/gtk_base/gtk_client_window_base.py +++ b/src/xpra/client/gtk_base/gtk_client_window_base.py @@ -1,6 +1,6 @@ # This file is part of Xpra. # Copyright (C) 2011 Serviware (Arthur Huillet, ) -# Copyright (C) 2010-2016 Antoine Martin +# Copyright (C) 2010-2017 Antoine Martin # Copyright (C) 2008, 2010 Nathaniel Smith # Xpra is released under the terms of the GNU GPL v2, or, at your option, any # later version. See the file COPYING for details. @@ -934,75 +934,78 @@ def call_action(self, action_type, action, state, pdata): def do_motion_notify_event(self, event): if self.moveresize_event: - x_root, y_root, direction, button, start_buttons, wx, wy, ww, wh = self.moveresize_event - dirstr = MOVERESIZE_DIRECTION_STRING.get(direction, direction) - buttons = self._event_buttons(event) - if start_buttons is None: - #first time around, store the buttons - start_buttons = buttons - self.moveresize_event[4] = buttons - if (button>0 and button not in buttons) or (button==0 and start_buttons!=buttons): - geomlog("%s for window button %i is no longer pressed (buttons=%s) cancelling moveresize", dirstr, button, buttons) - self.moveresize_event = None - else: - x = event.x_root - y = event.y_root - dx = x-x_root - dy = y-y_root - #clamp resizing using size hints, - #or sane defaults: minimum of (1x1) and maximum of (2*15x2*25) - minw = self.geometry_hints.get("min_width", 1) - minh = self.geometry_hints.get("min_height", 1) - maxw = self.geometry_hints.get("max_width", 2**15) - maxh = self.geometry_hints.get("max_height", 2**15) - geomlog("%s: min=%ix%i, max=%ix%i, window=%ix%i, delta=%ix%i", dirstr, minw, minh, maxw, maxh, ww, wh, dx, dy) - if direction in (MOVERESIZE_SIZE_BOTTOMRIGHT, MOVERESIZE_SIZE_BOTTOM, MOVERESIZE_SIZE_BOTTOMLEFT): - #height will be set to: wh+dy - dy = max(minh-wh, dy) - dy = min(maxh-wh, dy) - elif direction in (MOVERESIZE_SIZE_TOPRIGHT, MOVERESIZE_SIZE_TOP, MOVERESIZE_SIZE_TOPLEFT): - #height will be set to: wh-dy - dy = min(wh-minh, dy) - dy = max(wh-maxh, dy) - if direction in (MOVERESIZE_SIZE_BOTTOMRIGHT, MOVERESIZE_SIZE_RIGHT, MOVERESIZE_SIZE_TOPRIGHT): - #width will be set to: ww+dx - dx = max(minw-ww, dx) - dx = min(maxw-ww, dx) - elif direction in (MOVERESIZE_SIZE_BOTTOMLEFT, MOVERESIZE_SIZE_LEFT, MOVERESIZE_SIZE_TOPLEFT): - #width will be set to: ww-dx - dx = min(ww-minw, dx) - dx = max(ww-maxw, dx) - #calculate move + resize: - if direction==MOVERESIZE_MOVE: - data = (wx+dx, wy+dy), None - elif direction==MOVERESIZE_SIZE_BOTTOMRIGHT: - data = None, (ww+dx, wh+dy) - elif direction==MOVERESIZE_SIZE_BOTTOM: - data = None, (ww, wh+dy) - elif direction==MOVERESIZE_SIZE_BOTTOMLEFT: - data = (wx+dx, wy), (ww-dx, wh+dy) - elif direction==MOVERESIZE_SIZE_RIGHT: - data = None, (ww+dx, wh) - elif direction==MOVERESIZE_SIZE_LEFT: - data = (wx+dx, wy), (ww-dx, wh) - elif direction==MOVERESIZE_SIZE_TOPRIGHT: - data = (wx, wy+dy), (ww+dx, wh-dy) - elif direction==MOVERESIZE_SIZE_TOP: - data = (wx, wy+dy), (ww, wh-dy) - elif direction==MOVERESIZE_SIZE_TOPLEFT: - data = (wx+dx, wy+dy), (ww-dx, wh-dy) - else: - #not handled yet! - data = None - geomlog("%s for window %ix%i: started at %s, now at %s, delta=%s, button=%s, buttons=%s, data=%s", dirstr, ww, wh, (x_root, y_root), (x, y), (dx, dy), button, buttons, data) - if data: - #modifying the window is slower than moving the pointer, - #do it via a timer to batch things together - self.moveresize_data = data - if self.moveresize_timer is None: - self.moveresize_timer = self.timeout_add(20, self.do_moveresize) + self.motion_moveresize(event) ClientWindowBase.do_motion_notify_event(self, event) + def motion_moveresize(self, event): + x_root, y_root, direction, button, start_buttons, wx, wy, ww, wh = self.moveresize_event + dirstr = MOVERESIZE_DIRECTION_STRING.get(direction, direction) + buttons = self._event_buttons(event) + if start_buttons is None: + #first time around, store the buttons + start_buttons = buttons + self.moveresize_event[4] = buttons + if (button>0 and button not in buttons) or (button==0 and start_buttons!=buttons): + geomlog("%s for window button %i is no longer pressed (buttons=%s) cancelling moveresize", dirstr, button, buttons) + self.moveresize_event = None + else: + x = event.x_root + y = event.y_root + dx = x-x_root + dy = y-y_root + #clamp resizing using size hints, + #or sane defaults: minimum of (1x1) and maximum of (2*15x2*25) + minw = self.geometry_hints.get("min_width", 1) + minh = self.geometry_hints.get("min_height", 1) + maxw = self.geometry_hints.get("max_width", 2**15) + maxh = self.geometry_hints.get("max_height", 2**15) + geomlog("%s: min=%ix%i, max=%ix%i, window=%ix%i, delta=%ix%i", dirstr, minw, minh, maxw, maxh, ww, wh, dx, dy) + if direction in (MOVERESIZE_SIZE_BOTTOMRIGHT, MOVERESIZE_SIZE_BOTTOM, MOVERESIZE_SIZE_BOTTOMLEFT): + #height will be set to: wh+dy + dy = max(minh-wh, dy) + dy = min(maxh-wh, dy) + elif direction in (MOVERESIZE_SIZE_TOPRIGHT, MOVERESIZE_SIZE_TOP, MOVERESIZE_SIZE_TOPLEFT): + #height will be set to: wh-dy + dy = min(wh-minh, dy) + dy = max(wh-maxh, dy) + if direction in (MOVERESIZE_SIZE_BOTTOMRIGHT, MOVERESIZE_SIZE_RIGHT, MOVERESIZE_SIZE_TOPRIGHT): + #width will be set to: ww+dx + dx = max(minw-ww, dx) + dx = min(maxw-ww, dx) + elif direction in (MOVERESIZE_SIZE_BOTTOMLEFT, MOVERESIZE_SIZE_LEFT, MOVERESIZE_SIZE_TOPLEFT): + #width will be set to: ww-dx + dx = min(ww-minw, dx) + dx = max(ww-maxw, dx) + #calculate move + resize: + if direction==MOVERESIZE_MOVE: + data = (wx+dx, wy+dy), None + elif direction==MOVERESIZE_SIZE_BOTTOMRIGHT: + data = None, (ww+dx, wh+dy) + elif direction==MOVERESIZE_SIZE_BOTTOM: + data = None, (ww, wh+dy) + elif direction==MOVERESIZE_SIZE_BOTTOMLEFT: + data = (wx+dx, wy), (ww-dx, wh+dy) + elif direction==MOVERESIZE_SIZE_RIGHT: + data = None, (ww+dx, wh) + elif direction==MOVERESIZE_SIZE_LEFT: + data = (wx+dx, wy), (ww-dx, wh) + elif direction==MOVERESIZE_SIZE_TOPRIGHT: + data = (wx, wy+dy), (ww+dx, wh-dy) + elif direction==MOVERESIZE_SIZE_TOP: + data = (wx, wy+dy), (ww, wh-dy) + elif direction==MOVERESIZE_SIZE_TOPLEFT: + data = (wx+dx, wy+dy), (ww-dx, wh-dy) + else: + #not handled yet! + data = None + geomlog("%s for window %ix%i: started at %s, now at %s, delta=%s, button=%s, buttons=%s, data=%s", dirstr, ww, wh, (x_root, y_root), (x, y), (dx, dy), button, buttons, data) + if data: + #modifying the window is slower than moving the pointer, + #do it via a timer to batch things together + self.moveresize_data = data + if self.moveresize_timer is None: + self.moveresize_timer = self.timeout_add(20, self.do_moveresize) + def do_moveresize(self): self.moveresize_timer = None mrd = self.moveresize_data @@ -1323,6 +1326,7 @@ def _get_pointer(self, event): def _pointer_modifiers(self, event): x, y = self._get_pointer(event) pointer = self._pointer(x, y) + #FIXME: state is used for both mods and buttons?? modifiers = self._client.mask_to_names(event.state) buttons = self._event_buttons(event) v = pointer, modifiers, buttons diff --git a/src/xpra/client/ui_client_base.py b/src/xpra/client/ui_client_base.py index 80ebed2183..d74cba523b 100644 --- a/src/xpra/client/ui_client_base.py +++ b/src/xpra/client/ui_client_base.py @@ -266,6 +266,7 @@ def __init__(self): self.server_is_desktop = False self.server_supports_sharing = False self.server_supports_window_filters = False + self.server_input_devices = None #what we told the server about our encoding defaults: self.encoding_defaults = {} @@ -424,6 +425,7 @@ def vinfo(k): self.client_supports_sharing = opts.sharing self.log_both = (opts.remote_logging or "").lower()=="both" self.client_supports_remote_logging = self.log_both or parse_bool("remote-logging", opts.remote_logging) + self.input_devices = opts.input_devices #mouse wheel: mw = (opts.mousewheel or "").lower().replace("-", "") if mw not in FALSE_OPTIONS: @@ -1201,17 +1203,17 @@ def init_opengl(self, enable_opengl): def scale_pointer(self, pointer): return int(pointer[0]/self.xscale), int(pointer[1]/self.yscale) - def send_button(self, wid, button, pressed, pointer, modifiers, buttons): - def send_button(state): - self.send_positional(["button-action", wid, - button, state, - pointer, modifiers, buttons]) + def send_button(self, wid, button, pressed, pointer, modifiers, buttons, *args): pressed_state = self._button_state.get(button, False) if PYTHON3 and WIN32 and pressed_state==pressed: mouselog("button action: unchanged state, ignoring event") return self._button_state[button] = pressed - send_button(pressed) + packet = ["button-action", wid, + button, pressed, + pointer, modifiers, buttons] + list(args) + mouselog("button packet: %s", packet) + self.send_positional(packet) def window_keyboard_layout_changed(self, window): @@ -1941,6 +1943,9 @@ def process_ui_capabilities(self): if self.webcam_option=="on" or self.webcam_option.find("/dev/video")>=0: self.start_sending_webcam() + #input devices: + self.server_input_devices = c.strget("input-devices") + #sound: self.server_pulseaudio_id = c.strget("sound.pulseaudio.id") self.server_pulseaudio_server = c.strget("sound.pulseaudio.server") @@ -2144,6 +2149,11 @@ def _process_control(self, packet): log.warn("received invalid control command from server: %s", command) + def send_input_devices(self, fmt, input_devices): + assert self.server_input_devices + self.send("input-devices", fmt, input_devices) + + def start_sending_webcam(self): with self.webcam_lock: self.do_start_sending_webcam(self.webcam_option) diff --git a/src/xpra/log.py b/src/xpra/log.py index c6b97fb6b7..8735a900cd 100644 --- a/src/xpra/log.py +++ b/src/xpra/log.py @@ -1,6 +1,6 @@ # This file is part of Xpra. # Copyright (C) 2008, 2009 Nathaniel Smith -# Copyright (C) 2012-2016 Antoine Martin +# Copyright (C) 2012-2017 Antoine Martin # Xpra is released under the terms of the GNU GPL v2, or, at your option, any # later version. See the file COPYING for details. @@ -264,6 +264,7 @@ def enable_format(format_string): ])), ("X11", OrderedDict([ ("x11" , "All X11 code"), + ("xinput" , "XInput bindings"), ("bindings" , "X11 Cython bindings"), ("core" , "X11 core bindings"), ("randr" , "X11 RandR bindings"), diff --git a/src/xpra/platform/darwin/shadow_server.py b/src/xpra/platform/darwin/shadow_server.py index c42fc0447e..b0338ca1d5 100644 --- a/src/xpra/platform/darwin/shadow_server.py +++ b/src/xpra/platform/darwin/shadow_server.py @@ -139,7 +139,7 @@ def stop_refresh(self): GTKShadowServerBase.stop_refresh(self) - def do_process_mouse_common(self, proto, wid, pointer): + def do_process_mouse_common(self, proto, wid, pointer, *args): CG.CGWarpMouseCursorPosition(pointer) def fake_key(self, keycode, press): diff --git a/src/xpra/platform/features.py b/src/xpra/platform/features.py index 6b0fbe1951..426b2797ed 100644 --- a/src/xpra/platform/features.py +++ b/src/xpra/platform/features.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # This file is part of Xpra. # Copyright (C) 2010 Nathaniel Smith -# Copyright (C) 2011-2015 Antoine Martin +# Copyright (C) 2011-2017 Antoine Martin # Xpra is released under the terms of the GNU GPL v2, or, at your option, any # later version. See the file COPYING for details. @@ -15,6 +15,8 @@ SYSTEM_TRAY_SUPPORTED = False REINIT_WINDOWS = False +INPUT_DEVICES = ["auto"] + CLIPBOARDS = [] CLIPBOARD_WANT_TARGETS = envbool("XPRA_CLIPBOARD_WANT_TARGETS") CLIPBOARD_GREEDY = envbool("XPRA_CLIPBOARD_GREEDY") @@ -62,6 +64,7 @@ "CLIPBOARD_NATIVE_CLASS", "UI_THREAD_POLLING", "CLIENT_MODULES", + "INPUT_DEVICES", ] from xpra.platform import platform_import platform_import(globals(), "features", False, diff --git a/src/xpra/platform/win32/shadow_server.py b/src/xpra/platform/win32/shadow_server.py index ff57549bdf..82cf87283e 100644 --- a/src/xpra/platform/win32/shadow_server.py +++ b/src/xpra/platform/win32/shadow_server.py @@ -341,7 +341,7 @@ def refresh(self): log("refresh()=%s", v) return v - def do_process_mouse_common(self, proto, wid, pointer): + def do_process_mouse_common(self, proto, wid, pointer, *args): #adjust pointer position for offset in client: try: SetCursorPos(*pointer) diff --git a/src/xpra/platform/xposix/features.py b/src/xpra/platform/xposix/features.py index f911431f32..678f229150 100644 --- a/src/xpra/platform/xposix/features.py +++ b/src/xpra/platform/xposix/features.py @@ -21,3 +21,5 @@ DEFAULT_SSH_CMD = "ssh" CLIPBOARDS=["CLIPBOARD", "PRIMARY", "SECONDARY"] + +INPUT_DEVICES = ["auto", "xi"] diff --git a/src/xpra/platform/xposix/gui.py b/src/xpra/platform/xposix/gui.py index 4f4d54b4cc..46aca4866a 100644 --- a/src/xpra/platform/xposix/gui.py +++ b/src/xpra/platform/xposix/gui.py @@ -1,6 +1,6 @@ # This file is part of Xpra. # Copyright (C) 2010 Nathaniel Smith -# Copyright (C) 2011-2016 Antoine Martin +# Copyright (C) 2011-2017 Antoine Martin # Xpra is released under the terms of the GNU GPL v2, or, at your option, any # later version. See the file COPYING for details. @@ -16,11 +16,20 @@ dbuslog = Logger("posix", "dbus") traylog = Logger("posix", "menu") menulog = Logger("posix", "menu") +mouselog = Logger("posix", "mouse") from xpra.os_util import strtobytes, bytestostr -from xpra.util import iround, envbool +from xpra.util import iround, envbool, csv from xpra.gtk_common.gobject_compat import get_xid, is_gtk3 +try: + from xpra.x11.bindings.window_bindings import X11WindowBindings + from xpra.x11.bindings.xi2_bindings import X11XI2Bindings #@UnresolvedImport +except Exception as e: + log.error("no X11 bindings", exc_info=True) + X11WindowBindings = None + X11XI2Bindings = None + device_bell = None GTK_MENUS = envbool("XPRA_GTK_MENUS", False) RANDR_DPI = envbool("XPRA_RANDR_DPI", True) @@ -93,11 +102,11 @@ def get_session_type(): def _get_X11_window_property(xid, name, req_type): try: from xpra.gtk_common.error import xsync - from xpra.x11.bindings.window_bindings import X11WindowBindings, PropertyError #@UnresolvedImport - window_bindings = X11WindowBindings() + from xpra.x11.bindings.window_bindings import PropertyError #@UnresolvedImport try: + X11Window = X11WindowBindings() with xsync: - prop = window_bindings.XGetWindowProperty(xid, name, req_type) + prop = X11Window.XGetWindowProperty(xid, name, req_type) log("_get_X11_window_property(%#x, %s, %s)=%s, len=%s", xid, name, req_type, type(prop), len(prop or [])) return prop except PropertyError as e: @@ -108,9 +117,8 @@ def _get_X11_window_property(xid, name, req_type): return None def _get_X11_root_property(name, req_type): try: - from xpra.x11.bindings.window_bindings import X11WindowBindings #@UnresolvedImport - window_bindings = X11WindowBindings() - root_xid = window_bindings.getDefaultRootWindow() + X11Window = X11WindowBindings() + root_xid = X11Window.getDefaultRootWindow() return _get_X11_window_property(root_xid, name, req_type) except Exception as e: log.warn("Warning: failed to get X11 root property '%s'", name) @@ -170,18 +178,17 @@ def get_menu_support_function(): def _get_xsettings(): try: - from xpra.x11.bindings.window_bindings import X11WindowBindings #@UnresolvedImport - window_bindings = X11WindowBindings() + X11Window = X11WindowBindings() selection = "_XSETTINGS_S0" - owner = window_bindings.XGetSelectionOwner(selection) + owner = X11Window.XGetSelectionOwner(selection) if not owner: return None XSETTINGS = "_XSETTINGS_SETTINGS" - data = window_bindings.XGetWindowProperty(owner, XSETTINGS, XSETTINGS) + data = X11Window.XGetWindowProperty(owner, XSETTINGS, XSETTINGS) if not data: return None from xpra.x11.xsettings_prop import get_settings - return get_settings(window_bindings.get_display_name(), data) + return get_settings(X11Window.get_display_name(), data) except Exception as e: log("_get_xsettings error: %s", e) return None @@ -461,7 +468,7 @@ def _send_client_message(window, message_type, *values): try: from xpra.x11.gtk2 import gdk_display_source assert gdk_display_source - from xpra.x11.bindings.window_bindings import constants, X11WindowBindings #@UnresolvedImport + from xpra.x11.bindings.window_bindings import constants #@UnresolvedImport X11Window = X11WindowBindings() root_xid = X11Window.getDefaultRootWindow() if window: @@ -499,6 +506,22 @@ def set_shaded(window, shaded): _toggle_wm_state(window, "_NET_WM_STATE_SHADED", shaded) + +WINDOW_ADD_HOOKS = [] +def add_window_hooks(window): + global WINDOW_ADD_HOOKS + for x in WINDOW_ADD_HOOKS: + x(window) + log("add_window_hooks(%s) added %s", window, WINDOW_ADD_HOOKS) + +WINDOW_REMOVE_HOOKS = [] +def remove_window_hooks(window): + global WINDOW_REMOVE_HOOKS + for x in WINDOW_REMOVE_HOOKS: + x(window) + log("remove_window_hooks(%s) added %s", window, WINDOW_REMOVE_HOOKS) + + def get_info(): from xpra.platform.gui import get_info_base i = get_info_base() @@ -516,6 +539,105 @@ def get_info(): return i +class XI2_Window(object): + def __init__(self, window): + log("XI2_Window(%s)", window) + self.XI2 = X11XI2Bindings() + self.X11Window = X11WindowBindings() + self.window = window + self.xid = window.get_window().xid + self.windows = () + window.connect("configure-event", self.configured) + self.configured() + #replace event handlers with XI2 version: + self.do_motion_notify_event = window.do_motion_notify_event + window.do_motion_notify_event = self.noop + window.do_button_press_event = self.noop + window.do_button_release_event = self.noop + window.do_scroll_event = self.noop + window.connect("destroy", self.cleanup) + + def noop(self, *args): + pass + + def cleanup(self, *args): + for window in self.windows: + self.XI2.disconnect(window) + self.windows = [] + self.window = None + + def configured(self, *args): + self.windows = self.get_parent_windows(self.xid) + for window in self.windows: + self.XI2.connect(window, "XI_Motion", self.do_xi_motion) + self.XI2.connect(window, "XI_ButtonPress", self.do_xi_button) + self.XI2.connect(window, "XI_ButtonRelease", self.do_xi_button) + + def get_parent_windows(self, oxid): + windows = [oxid] + root = self.X11Window.getDefaultRootWindow() + xid = oxid + while True: + xid = self.X11Window.getParent(xid) + if xid==0 or xid==root: + break + windows.append(xid) + log("get_parent_windows(%#x)=%s", oxid, csv(hex(x) for x in windows)) + return windows + + + def do_xi_button(self, event): + window = self.window + client = window._client + if client.readonly: + return + if client.server_input_devices=="xi": + #skip synthetic scroll events for two-finger scroll, + #as the server should synthesize them from the motion events + #those have the same serial: + matching_motion = self.XI2.find_event("XI_Motion", event.serial) + #maybe we need more to distinguish? + if matching_motion: + return + button = event.detail + depressed = (event.name == "XI_ButtonPress") + args = self.get_pointer_extra_args(event) + window._button_action(button, event, depressed, *args) + + def do_xi_motion(self, event): + window = self.window + if window.moveresize_event: + window.motion_moveresize(event) + self.do_motion_notify_event(event) + return + if window._client.readonly: + return + #find the motion events in the xi2 event list: + pointer, modifiers, buttons = window._pointer_modifiers(event) + wid = self.window.get_mouse_event_wid(*pointer) + mouselog("do_motion_notify_event(%s) wid=%s / focus=%s / window wid=%i, device=%s, pointer=%s, modifiers=%s, buttons=%s", event, wid, window._client._focused, self.window._id, event.device, pointer, modifiers, buttons) + packet = ["pointer-position", wid, pointer, modifiers, buttons] + self.get_pointer_extra_args(event) + window._client.send_mouse_position(packet) + + def get_pointer_extra_args(self, event): + def intscaled(f): + return int(f*1000000), 1000000 + def dictscaled(v): + return dict((k,intscaled(v)) for k,v in v.items()) + raw_valuators = {} + raw_event_name = event.name.replace("XI_", "XI_Raw") #ie: XI_Motion -> XI_RawMotion + raw = self.XI2.find_event(raw_event_name, event.serial) + #mouselog("raw(%s)=%s", raw_event_name, raw) + if raw: + raw_valuators = raw.raw_valuators + args = [event.device] + for x in ("x", "y", "x_root", "y_root"): + args.append(intscaled(getattr(event, x))) + for v in [event.valuators, raw_valuators]: + args.append(dictscaled(v)) + return args + + class ClientExtras(object): def __init__(self, client, opts): self.client = client @@ -528,6 +650,10 @@ def __init__(self, client, opts): self.x11_filter = None if client.xsettings_enabled: self.setup_xprops() + if client.input_devices=="xi": + #this would trigger warnings with our temporary opengl windows: + #only enable it after we have connected: + self.client.after_handshake(self.setup_xi) self.setup_dbus_signals() def ready(self): @@ -565,6 +691,8 @@ def cleanup(self): bus._clean_up_signal_match(self.upower_sleeping_match) if self.login1_match: bus._clean_up_signal_match(self.login1_match) + global WINDOW_METHOD_OVERRIDES + WINDOW_METHOD_OVERRIDES = {} def resuming_callback(self, *args): eventlog("resuming_callback%s", args) @@ -647,6 +775,44 @@ def do_setup_xprops(self, *args): except ImportError as e: log.error("failed to load X11 properties/settings bindings: %s - root window properties will not be propagated", e) + + def do_xi_devices_changed(self, event): + log("do_xi_devices_changed(%s)", event) + XI2 = X11XI2Bindings() + devices = XI2.get_devices() + if devices: + self.client.send_input_devices("xi", devices) + + def setup_xi(self): + if self.client.server_input_devices!="xi": + log.info("server does not support xi input devices") + try: + from xpra.gtk_common.error import xsync + with xsync: + assert X11WindowBindings, "no X11 window bindings" + assert X11XI2Bindings, "no XI2 window bindings" + X11XI2Bindings().gdk_inject() + self.init_x11_filter() + XI2 = X11XI2Bindings() + XI2.select_xi2_events() + if self.client.server_input_devices: + XI2.connect(0, "XI_HierarchyChanged", self.do_xi_devices_changed) + devices = XI2.get_devices() + if devices: + self.client.send_input_devices("xi", devices) + except Exception as e: + log("enable_xi2()", exc_info=True) + log.error("Error: cannot enable XI2 events") + log.error(" %s", e) + else: + #register our enhanced event handlers: + self.add_xi2_method_overrides() + + def add_xi2_method_overrides(self): + global WINDOW_ADD_HOOKS + WINDOW_ADD_HOOKS = [XI2_Window] + + def _get_xsettings(self): try: return self._xsettings_watcher.get_settings() diff --git a/src/xpra/scripts/config.py b/src/xpra/scripts/config.py index 7aa9f88ef2..ff3756faf2 100755 --- a/src/xpra/scripts/config.py +++ b/src/xpra/scripts/config.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # This file is part of Xpra. -# Copyright (C) 2010-2016 Antoine Martin +# Copyright (C) 2010-2017 Antoine Martin # Xpra is released under the terms of the GNU GPL v2, or, at your option, any # later version. See the file COPYING for details. @@ -489,6 +489,7 @@ def may_create_user_config(xpra_conf_filename=DEFAULT_XPRA_CONF_FILENAME): "dbus-launch" : str, "webcam" : str, "mousewheel" : str, + "input-devices" : str, #ssl options: "ssl" : str, "ssl-key" : str, @@ -619,7 +620,7 @@ def may_create_user_config(xpra_conf_filename=DEFAULT_XPRA_CONF_FILENAME): "quality", "min-quality", "speed", "min-speed", "compression_level", "dpi", "video-scaling", "auto-refresh-delay", - "webcam", "mousewheel", "pings", + "webcam", "mousewheel", "input-devices", "pings", "tray", "keyboard-sync", "cursors", "bell", "notifications", "xsettings", "system-tray", "sharing", "delay-tray", "windows", "readonly", @@ -811,6 +812,7 @@ def addtrailingslash(v): "dbus-launch" : "dbus-launch --close-stderr", "webcam" : ["auto", "no"][OSX], "mousewheel" : "on", + "input-devices" : "auto", #ssl options: "ssl" : "auto", "ssl-key" : "", diff --git a/src/xpra/scripts/main.py b/src/xpra/scripts/main.py index 2d826e156b..4935fcc1a7 100755 --- a/src/xpra/scripts/main.py +++ b/src/xpra/scripts/main.py @@ -562,6 +562,13 @@ def ignore(defaults): group.add_option("--mousewheel", action="store", dest="mousewheel", default=defaults.mousewheel, help="Mouse wheel forwarding, can be used to disable the device or invert some axes. Default: %s." % defaults.webcam) + from xpra.platform.features import INPUT_DEVICES + if len(INPUT_DEVICES)>1: + group.add_option("--input-devices", action="store", metavar="APINAME", + dest="input_devices", default=defaults.input_devices, + help="Which API to use for input devices. Default: %s." % defaults.input_devices) + else: + ignore({"input-devices" : INPUT_DEVICES[0]}) legacy_bool_parse("global-menus") group.add_option("--global-menus", action="store", dest="global_menus", default=defaults.global_menus, metavar="yes|no", diff --git a/src/xpra/server/gtk_server_base.py b/src/xpra/server/gtk_server_base.py index 29062ffbed..860aa9da7c 100644 --- a/src/xpra/server/gtk_server_base.py +++ b/src/xpra/server/gtk_server_base.py @@ -168,7 +168,7 @@ def set_dpi(self, xdpi, ydpi): pass - def _move_pointer(self, wid, pos): + def _move_pointer(self, wid, pos, *args): x, y = pos display = gdk.display_get_default() display.warp_pointer(display.get_default_screen(), x, y) diff --git a/src/xpra/server/server_base.py b/src/xpra/server/server_base.py index e951cf9b83..7077acbe92 100644 --- a/src/xpra/server/server_base.py +++ b/src/xpra/server/server_base.py @@ -143,6 +143,9 @@ def __init__(self): self.webcam_encodings = [] self.webcam_forwarding_device = None self.virtual_video_devices = 0 + self.input_devices = "auto" + self.input_devices_format = None + self.input_devices_data = None self.mem_bytes = 0 #sound: @@ -256,6 +259,7 @@ def set_reaper_callback(): self.notifications = opts.notifications self.scaling_control = parse_bool_or_int("video-scaling", opts.video_scaling) self.webcam_forwarding = opts.webcam.lower() not in FALSE_OPTIONS + self.input_devices = opts.input_devices or "auto" #sound: self.pulseaudio = opts.pulseaudio @@ -752,6 +756,7 @@ def init_packet_handlers(self): "info-request": self._process_info_request, "start-command": self._process_start_command, "print": self._process_print, + "input-devices": self._process_input_devices, # Note: "clipboard-*" packets are handled via a special case.. }) @@ -1376,7 +1381,8 @@ def get_server_features(self): #newer flags: "av-sync", "auto-video-encoding", - "window-filters")) + "window-filters", + )) f["sound"] = { "ogg-latency-fix" : True, "eos-sequence" : True, @@ -1411,6 +1417,7 @@ def make_hello(self, source): "webcam" : self.webcam_forwarding, "webcam.encodings" : self.webcam_encodings, "virtual-video-devices" : self.virtual_video_devices, + "input-devices" : self.input_devices, }) capabilities.update(self.file_transfer.get_file_transfer_features()) capabilities.update(flatten_dict(self.get_server_features())) @@ -2857,7 +2864,7 @@ def _process_key_repeat(self, proto, packet): ss.user_event() - def _move_pointer(self, wid, pos): + def _move_pointer(self, wid, pos, *args): raise NotImplementedError() def _adjust_pointer(self, proto, wid, pointer): @@ -2878,12 +2885,13 @@ def _adjust_pointer(self, proto, wid, pointer): return px+(wx-cx), py+(wy-cy) return pointer - def _process_mouse_common(self, proto, wid, pointer): + def _process_mouse_common(self, proto, wid, pointer, *args): pointer = self._adjust_pointer(proto, wid, pointer) - self.do_process_mouse_common(proto, wid, pointer) + #TODO: adjust args too + self.do_process_mouse_common(proto, wid, pointer, *args) return pointer - def do_process_mouse_common(self, proto, wid, pointer): + def do_process_mouse_common(self, proto, wid, pointer, *args): pass @@ -2915,7 +2923,7 @@ def _process_pointer_position(self, proto, packet): if self.ui_driver and self.ui_driver!=ss.uuid: return self._update_modifiers(proto, wid, modifiers) - self._process_mouse_common(proto, wid, pointer) + self._process_mouse_common(proto, wid, pointer, *packet[5:]) def _process_damage_sequence(self, proto, packet): @@ -3176,6 +3184,16 @@ def _process_webcam_frame(self, proto, packet): ss.send_webcam_stop(device, str(e)) self.stop_virtual_webcam() + def _process_input_devices(self, ss, packet): + self.input_devices_format = packet[1] + self.input_devices_data = packet[2] + from xpra.util import print_nested_dict + mouselog("client %s input devices:", self.input_devices_format) + print_nested_dict(self.input_devices_data, print_fn=mouselog) + self.setup_input_devices() + + def setup_input_devices(self): + pass def process_clipboard_packet(self, ss, packet): if self.readonly: diff --git a/src/xpra/x11/bindings/xi2_bindings.pyx b/src/xpra/x11/bindings/xi2_bindings.pyx new file mode 100644 index 0000000000..7b48cef618 --- /dev/null +++ b/src/xpra/x11/bindings/xi2_bindings.pyx @@ -0,0 +1,739 @@ +# This file is part of Xpra. +# Copyright (C) 2017 Antoine Martin +# Xpra is released under the terms of the GNU GPL v2, or, at your option, any +# later version. See the file COPYING for details. + +import os +import time +import struct +import binascii +import collections + +from xpra.log import Logger +log = Logger("x11", "bindings", "xinput") + +from xpra.x11.gtk2.common import X11Event + +from libc.stdint cimport uintptr_t + + +################################### +# Headers, python magic +################################### +cdef extern from "string.h": + void* memset(void * ptr, int value, size_t num) + +cdef extern from "X11/Xutil.h": + pass + +###### +# Xlib primitives and constants +###### + +include "constants.pxi" +ctypedef unsigned long CARD32 + +cdef extern from "X11/Xlib.h": + ctypedef struct Display: + pass + + ctypedef CARD32 XID + ctypedef int Bool + ctypedef int Status + ctypedef CARD32 Atom + ctypedef XID Window + ctypedef CARD32 Time + + ctypedef struct XGenericEventCookie: + int type # of event. Always GenericEvent + unsigned long serial + Bool send_event + Display *display + int extension #major opcode of extension that caused the event + int evtype #actual event type + unsigned int cookie + void *data + + int XIAnyPropertyType + + Atom XInternAtom(Display * display, char * atom_name, Bool only_if_exists) + int XFree(void * data) + + Bool XQueryExtension(Display * display, char *name, + int *major_opcode_return, int *first_event_return, int *first_error_return) + + Bool XGetEventData(Display *display, XGenericEventCookie *cookie) + void XFreeEventData(Display *display, XGenericEventCookie *cookie) + + Window XDefaultRootWindow(Display * display) + + Bool XQueryPointer(Display *display, Window w, Window *root_return, Window *child_return, int *root_x_return, int *root_y_return, + int *win_x_return, int *win_y_return, unsigned int *mask_return) + int XFlush(Display *dpy) + +cdef extern from "X11/extensions/XInput2.h": + int XI_LASTEVENT + int XI_DeviceChanged + int XI_KeyPress + int XI_KeyRelease + int XI_ButtonPress + int XI_ButtonRelease + int XI_Motion + int XI_Enter + int XI_Leave + int XI_FocusIn + int XI_FocusOut + int XI_HierarchyChanged + int XI_PropertyEvent + int XI_RawKeyPress + int XI_RawKeyRelease + int XI_RawButtonPress + int XI_RawButtonRelease + int XI_RawMotion + int XI_TouchBegin + int XI_TouchUpdate + int XI_TouchEnd + int XI_TouchOwnership + int XI_RawTouchBegin + int XI_RawTouchUpdate + int XI_RawTouchEnd + + int XIMasterPointer + int XIMasterKeyboard + int XISlavePointer + int XISlaveKeyboard + int XIFloatingSlave + + int XIButtonClass + int XIKeyClass + int XIValuatorClass + int XIScrollClass + int XITouchClass + + int XIAllDevices + int XIAllMasterDevices + + ctypedef struct XIValuatorState: + int mask_len + unsigned char *mask + double *values + + ctypedef struct XIEvent: + int type + unsigned long serial + Bool send_event + Display *display + int extension + int evtype + Time time + + ctypedef struct XIRawEvent: + int type #GenericEvent + unsigned long serial + Bool send_event + Display *display + int extension #XI extension offset + int evtype #XI_RawKeyPress, XI_RawKeyRelease, etc + Time time + int deviceid + int sourceid + int detail + int flags + XIValuatorState valuators + double *raw_values + + ctypedef struct XIButtonState: + int mask_len + unsigned char *mask + + ctypedef struct XIModifierState: + int base + int latched + int locked + int effective + + ctypedef XIModifierState XIGroupState + + ctypedef struct XIDeviceEvent: + int type + unsigned long serial + Bool send_event + Display *display + int extension + int evtype + Time time + int deviceid + int sourceid + int detail + Window root + Window event + Window child + double root_x + double root_y + double event_x + double event_y + int flags + XIButtonState buttons + XIValuatorState valuators + XIModifierState mods + XIGroupState group + + ctypedef struct XIHierarchyInfo: + int deviceid + int attachment + int use + Bool enabled + int flags + + ctypedef struct XIHierarchyEvent: + int type + unsigned long serial + Bool send_event + Display *display + int extension + int evtype #XI_HierarchyChanged + Time time + int flags + int num_info + XIHierarchyInfo *info + + ctypedef struct XIEventMask: + int deviceid + int mask_len + unsigned char* mask + + ctypedef struct XIAnyClassInfo: + int type + int sourceid + + ctypedef struct XIDeviceInfo: + int deviceid + char *name + int use + int attachment + Bool enabled + int num_classes + XIAnyClassInfo **classes + + ctypedef struct XIButtonClassInfo: + int type + int sourceid + int num_buttons + Atom *labels + XIButtonState state + + ctypedef struct XIKeyClassInfo: + int type + int sourceid + int num_keycodes + int *keycodes + + ctypedef struct XIValuatorClassInfo: + int type + int sourceid + int number + Atom label + double min + double max + double value + int resolution + int mode + + ctypedef struct XIScrollClassInfo: + int type + int sourceid + int number + int scroll_type + double increment + int flags + + ctypedef struct XITouchClassInfo: + int type + int sourceid + int mode + int num_touches + + Status XIQueryVersion(Display *display, int *major_version_inout, int *minor_version_inout) + Status XISelectEvents(Display *display, Window win, XIEventMask *masks, int num_masks) + XIDeviceInfo* XIQueryDevice(Display *display, int deviceid, int *ndevices_return) + void XIFreeDeviceInfo(XIDeviceInfo *info) + Atom *XIListProperties(Display *display, int deviceid, int *num_props_return) + Status XIGetProperty(Display *display, int deviceid, Atom property, long offset, long length, + Bool delete_property, Atom type, Atom *type_return, + int *format_return, unsigned long *num_items_return, + unsigned long *bytes_after_return, unsigned char **data) + + +DEF MAX_XI_EVENTS = 64 +DEF XI_EVENT_MASK_SIZE = (MAX_XI_EVENTS+7)//8 + +XI_EVENT_NAMES = { + XI_DeviceChanged : "XI_DeviceChanged", + XI_KeyPress : "XI_KeyPress", + XI_KeyRelease : "XI_KeyRelease", + XI_ButtonPress : "XI_ButtonPress", + XI_ButtonRelease : "XI_ButtonRelease", + XI_Motion : "XI_Motion", + XI_Enter : "XI_Enter", + XI_Leave : "XI_Leave", + XI_FocusIn : "XI_FocusIn", + XI_FocusOut : "XI_FocusOut", + XI_HierarchyChanged : "XI_HierarchyChanged", + XI_PropertyEvent : "XI_PropertyEvent", + XI_RawKeyPress : "XI_RawKeyPress", + XI_RawKeyRelease : "XI_RawKeyRelease", + XI_RawButtonPress : "XI_RawButtonPress", + XI_RawButtonRelease : "XI_RawButtonRelease", + XI_RawMotion : "XI_RawMotion", + XI_TouchBegin : "XI_TouchBegin", + XI_TouchUpdate : "XI_TouchUpdate", + XI_TouchEnd : "XI_TouchEnd", + XI_TouchOwnership : "XI_TouchOwnership", + XI_RawTouchBegin : "XI_RawTouchBegin", + XI_RawTouchUpdate : "XI_RawTouchUpdate", + XI_RawTouchEnd : "XI_RawTouchEnd", + } + +XI_USE = { + XIMasterPointer : "master pointer", + XIMasterKeyboard : "master keyboard", + XISlavePointer : "slave pointer", + XISlaveKeyboard : "slave keyboard", + XIFloatingSlave : "floating slave", + } + +CLASS_INFO = { + XIButtonClass : "button", + XIKeyClass : "key", + XIValuatorClass : "valuator", + XIScrollClass : "scroll", + XITouchClass : "touch", + } + + +from core_bindings cimport _X11CoreBindings + +cdef _X11XI2Bindings singleton = None +def X11XI2Bindings(): + global singleton + if singleton is None: + singleton = _X11XI2Bindings() + return singleton + +cdef class _X11XI2Bindings(_X11CoreBindings): + + cdef int opcode + cdef object events + cdef object event_handlers + + def __init__(self): + self.opcode = -1 + self.event_handlers = {} + self.reset_events() + + def __repr__(self): + return "X11XI2Bindings(%s)" % self.display_name + + def connect(self, window, event, handler): + self.event_handlers.setdefault(window, {})[event] = handler + + def disconnect(self, window): + try: + del self.event_handlers[window] + except: + pass + + + def reset_events(self): + self.events = collections.deque(maxlen=100) + + def find_event(self, event_name, serial): + for x in reversed(self.events): + #log.info("find_event(%s, %#x) checking %s", event_name, serial, x) + if x.name==event_name and x.serial==serial: + #log.info("matched") + return x + if x.serial0 and found==window) or (found==0 and window in windows)): + matches.append(x) + found = window + elif found: + break + return matches + + cdef int get_xi_opcode(self, int major=2, int minor=2): + if self.opcode!=-1: + return self.opcode + cdef int opcode, event, error + if not XQueryExtension(self.display, "XInputExtension", &opcode, &event, &error): + log.warn("Warning: XI2 events are not supported") + self.opcode = 0 + return 0 + cdef int rmajor = major, rminor = minor + cdef int rc = XIQueryVersion(self.display, &rmajor, &rminor) + if rc == BadRequest: + log.warn("Warning: no XI2 %i.%i support,", major, minor) + log.warn(" server supports version %i.%i only", rmajor, rminor) + self.opcode = 0 + return 0 + elif rc: + log.warn("Warning: Xlib bug querying XI2, code %i", rc) + self.opcode = 0 + return 0 + self.opcode = opcode + log("get_xi_opcode%s=%i", (major, minor), opcode) + return opcode + + cdef register_parser(self): + log("register_parser()") + if self.opcode>0: + from xpra.x11.gtk2.gdk_bindings import add_x_event_parser + add_x_event_parser(self.opcode, self.parse_xi_event) + + cdef register_gdk_events(self): + log("register_gdk_events()") + if self.opcode<=0: + return + global XI_EVENT_NAMES + from xpra.x11.gtk2.gdk_bindings import add_x_event_signal, add_x_event_type_name + for e, xi_event_name in { + XI_DeviceChanged : "device-changed", + XI_KeyPress : "key-press", + XI_KeyRelease : "key-release", + XI_ButtonPress : "button-press", + XI_ButtonRelease : "button-release", + XI_Motion : "motion", + XI_Enter : "enter", + XI_Leave : "leave", + XI_FocusIn : "focus-in", + XI_FocusOut : "focus-out", + XI_HierarchyChanged : "focus-changed", + XI_PropertyEvent : "property-event", + XI_RawKeyPress : "raw-key-press", + XI_RawKeyRelease : "raw-key-release", + XI_RawButtonPress : "raw-button-press", + XI_RawButtonRelease : "raw-button-release", + XI_RawMotion : "raw-motion", + XI_TouchBegin : "touch-begin", + XI_TouchUpdate : "touch-update", + XI_TouchEnd : "touch-end", + XI_TouchOwnership : "touch-ownership", + XI_RawTouchBegin : "raw-touch-begin", + XI_RawTouchUpdate : "raw-touch-update", + XI_RawTouchEnd : "raw-touch-end", + }.items(): + event = self.opcode+e + add_x_event_signal(event, ("xi-%s" % xi_event_name, None)) + name = XI_EVENT_NAMES[e] + add_x_event_type_name(event, name) + + def select_xi2_events(self): + cdef Window win = XDefaultRootWindow(self.display) + log("select_xi2_events() root window=%#x", win) + assert XI_LASTEVENT>3] |= (1 << ((event) & 7))) + #XISetMask(mask1, XI_RawMotion) + for e in ( + XI_KeyPress, XI_KeyRelease, + XI_Motion, + XI_HierarchyChanged, + XI_ButtonPress, XI_ButtonRelease, + XI_RawButtonPress, XI_RawButtonRelease, + XI_TouchBegin, XI_TouchUpdate, XI_TouchEnd, + XI_RawTouchBegin, XI_RawTouchUpdate, XI_RawTouchEnd, + XI_RawMotion, + ): + mask1[e>>3] |= (1<< (e & 0x7)) + evmasks[0].deviceid = XIAllDevices #XIAllMasterDevices #XIAllDevices + evmasks[0].mask_len = XI_EVENT_MASK_SIZE + evmasks[0].mask = mask1 + XISelectEvents(self.display, win, evmasks, 1) + XFlush(self.display) + + def parse_xi_event(self, display, uintptr_t _cookie): + log("parse_xi_event(%s)", _cookie) + cdef XGenericEventCookie *cookie = _cookie + cdef XIDeviceEvent *device_e + cdef XIHierarchyEvent *hierarchy_e + cdef XIHierarchyInfo *hierarchy_info + cdef XIEvent *xie + cdef XIRawEvent *raw + cdef int i = 0, j = 0 + if not XGetEventData(self.display, cookie): + return None + xie = cookie.data + device_e = cookie.data + cdef int xi_type = cookie.evtype + etype = self.opcode+xi_type + global XI_EVENT_NAMES + event_name = XI_EVENT_NAMES.get(xi_type) + if not event_name: + log("unknown XI2 event code: %i", xi_type) + return None + + #don't parse the same thing again: + if len(self.events)>0: + last_event = self.events[-1] + if last_event.serial==xie.serial and last_event.type==etype: + return None + + pyev = X11Event(event_name) + pyev.type = etype + pyev.display = display + pyev.send_event = bool(xie.send_event) + pyev.serial = xie.serial + pyev.time = int(xie.time) + pyev.window = int(XDefaultRootWindow(self.display)) + + if xi_type in (XI_KeyPress, XI_KeyRelease, + XI_ButtonPress, XI_ButtonRelease, + XI_Motion, + XI_TouchBegin, XI_TouchUpdate, XI_TouchEnd): + device = cookie.data + #pyev.source = device.sourceid #always 0 + pyev.device = device.deviceid + pyev.detail = device.detail + pyev.flags = device.flags + pyev.window = int(device.child or device.event or device.root) + pyev.x_root = device.root_x + pyev.y_root = device.root_y + pyev.x = device.event_x + pyev.y = device.event_y + #mask = [] + valuators = {} + valuator = 0 + for i in range(device.valuators.mask_len*8): + if device.valuators.mask[i>>3] & (1 << (i & 0x7)): + valuators[i] = device.valuators.values[valuator] + valuator += 1 + pyev.valuators = valuators + buttons = [] + for i in range(device.buttons.mask_len): + if device.buttons.mask[i>>3] & (1<< (i & 0x7)): + buttons.append(i) + pyev.buttons = buttons + state = [] + pyev.state = state + pyev.modifiers = { + "base" : device.mods.base, + "latched" : device.mods.latched, + "locked" : device.mods.locked, + "effective" : device.mods.effective, + } + #make it compatible with gdk events: + pyev.state = device.mods.effective + elif xi_type in (XI_RawKeyPress, XI_RawKeyRelease, + XI_RawButtonPress, XI_RawButtonRelease, + XI_RawMotion, + XI_RawTouchBegin, XI_RawTouchUpdate, XI_RawTouchEnd): + raw = cookie.data + valuators = {} + raw_valuators = {} + valuator = 0 + for i in range(raw.valuators.mask_len*8): + if raw.valuators.mask[i>>3] & (1 << (i & 0x7)): + valuators[i] = raw.valuators.values[valuator] + raw_valuators[i] = raw.raw_values[valuator] + valuator += 1 + pyev.valuators = valuators + pyev.raw_valuators = raw_valuators + elif xi_type == XI_HierarchyChanged: + hierarchy_e = cookie.data + pyev.window = 0 + pyev.flags = hierarchy_e.flags + #for i in range(hierarchy_e.num_info): + #XIHierarchyInfo *info + XFreeEventData(self.display, cookie) + pyev.xid = pyev.window + self.events.append(pyev) + + handler = self.event_handlers.get(pyev.window, {}).get(event_name) + log("parse_xi_event: %s, handler=%s", pyev, handler) + if handler: + handler(pyev) + return None + + def get_devices(self, show_all=True, show_disabled=False): + log("get_devices(%s, %s)", show_all, show_disabled) + global XI_USE + cdef int ndevices, i, j + cdef XIDeviceInfo *devices + cdef XIDeviceInfo *device + cdef XIAnyClassInfo *clazz + if show_all: + device_types = XIAllDevices + else: + device_types = XIAllMasterDevices + devices = XIQueryDevice(self.display, device_types, &ndevices) + dinfo = {} + for i in range(ndevices): + device = &devices[i] + if not device.enabled and not show_disabled: + continue + info = { + "name" : device.name, + "use" : XI_USE.get(device.use, "unknown use: %i" % device.use), + "attachment" : device.attachment, + "enabled" : device.enabled, + } + classes = {} + for j in range(device.num_classes): + clazz = device.classes[j] + classes[j] = self.get_class_info(clazz) + info["classes"] = classes + properties = self.get_device_properties(device.deviceid) + if properties: + info["properties"] = properties + log("[%i] %s: %s", device.deviceid, device.name, info) + dinfo[device.deviceid] = info + XIFreeDeviceInfo(devices) + return dinfo + + def get_device_properties(self, deviceid): + cdef Atom *atoms + cdef int nprops, i + atoms = XIListProperties(self.display, deviceid, &nprops) + if atoms==NULL or nprops==0: + return None + props = {} + for i in range(nprops): + value = self.get_device_property(deviceid, atoms[i]) + if value is not None: + prop_name = self.XGetAtomName(atoms[i]) + props[prop_name] = value + return props + + cdef get_device_property(self, int deviceid, Atom property, req_type=0): + #code mostly duplicated from window_bindings XGetWindowProperty: + cdef int buffer_size = 64 * 1024 + cdef Atom xactual_type = 0 + cdef int actual_format = 0 + cdef long offset = 0 + cdef unsigned long nitems = 0, bytes_after = 0 + cdef unsigned char *prop = NULL + cdef Status status + cdef Atom xreq_type = XIAnyPropertyType + if req_type: + xreq_type = self.get_xatom(req_type) + + status = XIGetProperty(self.display, + deviceid, property, + 0, + buffer_size//4, + False, + xreq_type, &xactual_type, + &actual_format, &nitems, &bytes_after, &prop) + if status != Success: + raise Exception("failed to retrieve XI property") + if xactual_type == XNone: + return None + if xreq_type and xreq_type != xactual_type: + raise Exception("expected %s but got %s" % (req_type, self.XGetAtomName(xactual_type))) + # This should only occur for bad property types: + assert not (bytes_after and not nitems) + if bytes_after: + raise Exception("reserved %i bytes for buffer, but data is bigger by %i bytes!" % (buffer_size, bytes_after)) + assert actual_format > 0 + #unlike XGetProperty, we don't need to special case 64-bit: + cdef int bytes_per_item = actual_format // 8 + cdef int nbytes = bytes_per_item * nitems + data = ( prop)[:nbytes] + XFree(prop) + prop_type = self.XGetAtomName(xactual_type) + log("hex=%s (type=%s, nitems=%i, bytes per item=%i, actual format=%i)", binascii.hexlify(data), prop_type, nitems, bytes_per_item, actual_format) + fmt = None + if prop_type=="INTEGER": + fmt = { + 8 : "b", + 16 : "h", + 32 : "i", + }.get(actual_format) + elif prop_type=="CARDINAL": + fmt = { + 8 : "B", + 16 : "H", + 32 : "I", + }.get(actual_format) + elif prop_type=="FLOAT": + fmt = "f" + if fmt: + value = struct.unpack(fmt*nitems, data) + if nitems==1: + return value[0] + return value + return data + + cdef get_class_info(self, XIAnyClassInfo *class_info): + cdef int i + cdef XIButtonClassInfo *button + cdef XIKeyClassInfo *key + cdef XIValuatorClassInfo *valuator + cdef XIScrollClassInfo *scroll + cdef XITouchClassInfo *touch + info = { + "type" : CLASS_INFO.get(class_info.type, "unknown type: %i" % class_info.type), + "sourceid" : class_info.sourceid, + } + if class_info.type==XIButtonClass: + button = class_info + buttons = [] + for i in range(button.num_buttons): + if button.labels[i]>0: + buttons.append(self.XGetAtomName(button.labels[i])) + info["buttons"] = buttons + #XIButtonState state + elif class_info.type==XIKeyClass: + key = class_info + keys = [] + for i in range(key.num_keycodes): + keys.append(key.keycodes[i]) + elif class_info.type==XIValuatorClass: + valuator = class_info + info.update({ + "number" : valuator.number, + "min" : valuator.min, + "max" : valuator.max, + "value" : valuator.value, + "resolution": valuator.resolution, + "mode" : valuator.mode, + }) + if valuator.label: + info["label"] = self.XGetAtomName(valuator.label) + elif class_info.type==XIScrollClass: + scroll = class_info + info.update({ + "number" : scroll.number, + "scroll-type" : scroll.scroll_type, + "increment" : scroll.increment, + "flags" : scroll.flags, + }) + elif class_info.type==XITouchClass: + touch = class_info + info.update({ + "mode" : touch.mode, + "num-touches" : touch.num_touches, + }) + return info + + + def gdk_inject(self): + self.get_xi_opcode() + #log.info("XInput Devices:") + #from xpra.util import print_nested_dict + #print_nested_dict(self.get_devices(), print_fn=log.info) + self.register_parser() + self.register_gdk_events() + #self.select_xi2_events() diff --git a/src/xpra/x11/desktop_server.py b/src/xpra/x11/desktop_server.py index cb5b9206e1..35757f6ad4 100644 --- a/src/xpra/x11/desktop_server.py +++ b/src/xpra/x11/desktop_server.py @@ -1,6 +1,6 @@ # coding=utf8 # This file is part of Xpra. -# Copyright (C) 2016 Antoine Martin +# Copyright (C) 2016-2017 Antoine Martin # Xpra is released under the terms of the GNU GPL v2, or, at your option, any # later version. See the file COPYING for details. @@ -352,7 +352,7 @@ def _process_configure_window(self, proto, packet): self._damage(window, 0, 0, w, h) - def _move_pointer(self, wid, pos): + def _move_pointer(self, wid, pos, *args): if wid>=0: window = self._id_to_window.get(wid) if not window: @@ -360,7 +360,7 @@ def _move_pointer(self, wid, pos): else: #TODO: just like shadow server, adjust for window position pass - X11ServerBase._move_pointer(self, wid, pos) + X11ServerBase._move_pointer(self, wid, pos, *args) def _process_close_window(self, proto, packet): diff --git a/src/xpra/x11/gtk2/gdk_bindings.pyx b/src/xpra/x11/gtk2/gdk_bindings.pyx index 213172950b..4450dacedd 100644 --- a/src/xpra/x11/gtk2/gdk_bindings.pyx +++ b/src/xpra/x11/gtk2/gdk_bindings.pyx @@ -980,7 +980,6 @@ cdef object _gw(display, Window xwin): verbose("cannot get gdk window for %s, %s: %s", display, xwin, get_error_text(error)) raise XError(error) if win is None: - log.warn("window failed: %#x", xwin) verbose("cannot get gdk window for %s, %s", display, xwin) raise XError(BadWindow) return win @@ -1045,11 +1044,7 @@ cdef parse_xevent(GdkXEvent * e_gdk) with gil: parser = x_event_parsers.get(e.xcookie.extension) if parser: #log("calling %s%s", parser, (d, &e.xcookie)) - pyev = parser(d, &e.xcookie) - #log("pyev=%s (window=%#x)", pyev, e.xany.window) - pyev.window = _gw(d, pyev.window) - pyev.delivered_to = pyev.window - return pyev + return parser(d, &e.xcookie) return None event_args = x_event_signals.get(etype) @@ -1224,9 +1219,9 @@ cdef parse_xevent(GdkXEvent * e_gdk) with gil: log.info("not handled: %s", x_event_type_names.get(etype, etype)) return None except XError as ex: - log.warn("XError: %s processing %s", ex, event_type, exc_info=True) + log("XError: %s processing %s", ex, event_type, exc_info=True) if ex.msg==BadWindow: - if etype == DestroyNotify: + if etype==DestroyNotify: #happens too often, don't bother with the debug message pass else: diff --git a/src/xpra/x11/server.py b/src/xpra/x11/server.py index 6b053e3f58..53c4636106 100644 --- a/src/xpra/x11/server.py +++ b/src/xpra/x11/server.py @@ -948,7 +948,7 @@ def _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): + def _move_pointer(self, wid, pos, *args): if wid>=0: window = self._id_to_window.get(wid) if not window: @@ -956,7 +956,7 @@ def _move_pointer(self, wid, pos): else: mouselog("raising %s", window) window.raise_window() - X11ServerBase._move_pointer(self, wid, pos) + X11ServerBase._move_pointer(self, wid, pos, *args) def _process_close_window(self, proto, packet): diff --git a/src/xpra/x11/x11_server_base.py b/src/xpra/x11/x11_server_base.py index 5af6562392..1eb4fb9903 100644 --- a/src/xpra/x11/x11_server_base.py +++ b/src/xpra/x11/x11_server_base.py @@ -43,6 +43,7 @@ grablog = Logger("server", "grab") cursorlog = Logger("server", "cursor") screenlog = Logger("server", "screen") +xinputlog = Logger("xinput") gllog = Logger("screen", "opengl") from xpra.util import iround, envbool, envint @@ -74,6 +75,23 @@ def dump_windows(): log("found window: %s", window_info(window)) +class XTestPointerDevice(object): + + def __repr__(self): + return "XTestPointerDevice" + + def move_pointer(self, screen_no, x, y, *args): + mouselog("xtest_fake_motion(%i, %s, %s)", screen_no, x, y) + X11Keyboard.xtest_fake_motion(screen_no, x, y) + + def click(self, button, pressed, *args): + mouselog("xtest_fake_button(%i, %s)", button, pressed) + X11Keyboard.xtest_fake_button(button, pressed) + + def close(self): + pass + + class X11ServerBase(GTKServerBase): """ Base class for X11 servers, @@ -93,6 +111,7 @@ def init(self, opts): self.current_xinerama_config = None self.x11_init() GTKServerBase.init(self, opts) + self.pointer_device = XTestPointerDevice() def x11_init(self): if self.fake_xinerama: @@ -618,6 +637,7 @@ def reset_icc_profile(self): def do_cleanup(self, *args): GTKServerBase.do_cleanup(self) cleanup_fakeXinerama() + self.cleanup_input_devices() def _process_server_settings(self, proto, packet): @@ -710,23 +730,42 @@ def get_screen_number(self, wid): #-1 uses the current screen return -1 - def _move_pointer(self, wid, pos): + + def cleanup_input_devices(self): + pass + + + def setup_input_devices(self): + xinputlog("setup_input_devices() format=%s, input_devices=%s", self.input_devices_format, self.input_devices) + + def _move_pointer(self, wid, pos, deviceid, *args): + #args = #(this is called within an xswallow context) screen_no = self.get_screen_number(wid) - mouselog("move_pointer(%s, %s) screen_no=%i", wid, pos, screen_no) + device = self.pointer_device + mouselog("move_pointer(%s, %s, %s) screen_no=%i, device=%s", wid, pos, deviceid, screen_no, device) x, y = pos - X11Keyboard.xtest_fake_motion(screen_no, x, y) + try: + device.move_pointer(screen_no, x, y, *args) + except Exception as e: + mouselog.error("Error: failed to move the pointer to %sx%s using %s", x, y, device) + mouselog.error(" %s", e) - def do_process_mouse_common(self, proto, wid, pointer): + def do_process_mouse_common(self, proto, wid, pointer, deviceid=-1, *args): + log("do_process_mouse_common%s", tuple([proto, wid, pointer, deviceid]+list(args))) if self.readonly: return + if self.input_devices_data: + device_data = self.input_devices_data.get(deviceid) + if device_data: + mouselog("process_mouse_common from device=%s", device_data.get("name")) pos = self.root_window.get_pointer()[:2] - if pos!=pointer: + if pos!=pointer or self.input_devices=="xi": ss = self._server_sources.get(proto) assert ss, "source not found for %s" % proto self.last_mouse_user = ss.uuid with xswallow: - self._move_pointer(wid, pointer) + self._move_pointer(wid, pointer, deviceid, *args) def _update_modifiers(self, proto, wid, modifiers): if self.readonly: @@ -739,13 +778,15 @@ def _update_modifiers(self, proto, wid, modifiers): if wid==self.get_focus(): ss.user_event() - def do_process_button_action(self, proto, wid, button, pressed, pointer, modifiers, *args): + def do_process_button_action(self, proto, wid, button, pressed, pointer, modifiers, buttons=[], deviceid=-1, *args): self._update_modifiers(proto, wid, modifiers) - self._process_mouse_common(proto, wid, pointer) - mouselog("xtest_fake_button(%s, %s) at %s", button, pressed, pointer) + #TODO: pass extra args + self._process_mouse_common(proto, wid, pointer, deviceid) + device = self.pointer_device + assert device, "pointer device %s not found" % deviceid try: with xsync: - X11Keyboard.xtest_fake_button(button, pressed) + device.click(button, pressed, *args) except XError: err = "Failed to pass on (un)press of mouse button %s" % button if button>=4: