Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
* udev rule for "Xpra Virtual Touchpad"
* make sure we reset the valuators when the device / hierarchy changes
* pass the device information with the events
* parse the motion valuators correctly instead of assuming buttons 2 and 3 for wheel scrolling
* process any non-wheel motion before wheel events so they will land where they should
* support creating the "Xpra Virtual Touchpad" device and corresponding xorg config

git-svn-id: https://xpra.org/svn/Xpra/trunk@18916 3bb7dfac-3a0b-4e04-842a-767bc560f471
  • Loading branch information
totaam committed Mar 30, 2018
1 parent be55856 commit 57f5571
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 137 deletions.
1 change: 1 addition & 0 deletions src/udev/rules.d/71-xpra-virtual-pointer.rules
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# allow xpra to use fine grained scrolling

ACTION=="add|change", ATTRS{name}=="Xpra Virtual Pointer*", PROGRAM="/bin/xpra_udev_product_version", ENV{MOUSE_WHEEL_CLICK_ANGLE}="1", ENV{MOUSE_WHEEL_CLICK_COUNT}="360", MODE="0660", OWNER="%c", GROUP="xpra"
ACTION=="add|change", ATTRS{name}=="Xpra Virtual Touchpad*", PROGRAM="/bin/xpra_udev_product_version", ENV{MOUSE_WHEEL_CLICK_ANGLE}="1", ENV{MOUSE_WHEEL_CLICK_COUNT}="360", MODE="0660", OWNER="%c", GROUP="xpra"
91 changes: 59 additions & 32 deletions src/xpra/platform/xposix/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,15 @@ def configured(self, *_args):
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)
self.XI2.connect(window, "XI_DeviceChanged", self.do_xi_device_changed)
self.XI2.connect(window, "XI_HierarchyChanged", self.do_xi_hierarchy_changed)

def do_xi_device_changed(self, *args):
self.motion_valuators = {}

def do_xi_hierarchy_changed(self, *args):
self.motion_valuators = {}


def get_parent_windows(self, oxid):
windows = [oxid]
Expand All @@ -595,12 +604,12 @@ def get_parent_windows(self, oxid):
return windows


def do_xi_button(self, event):
def do_xi_button(self, event, device):
window = self.window
client = window._client
if client.readonly:
return
xinputlog("do_xi_button(%s) server_input_devices=%s", event, client.server_input_devices)
xinputlog("do_xi_button(%s, %s) server_input_devices=%s", event, device, client.server_input_devices)
if client.server_input_devices=="xi" or (client.server_input_devices=="uinput" and client.server_precise_wheel):
#skip synthetic scroll events,
#as the server should synthesize them from the motion events
Expand All @@ -614,9 +623,10 @@ def do_xi_button(self, event):
args = self.get_pointer_extra_args(event)
window._button_action(button, event, depressed, *args)

def do_xi_motion(self, event):
def do_xi_motion(self, event, device):
window = self.window
if window.moveresize_event:
xinputlog("do_xi_motion(%s, %s) handling as a moveresize event on window %s", event, device, window)
window.motion_moveresize(event)
self.do_motion_notify_event(event)
return
Expand All @@ -626,40 +636,57 @@ def do_xi_motion(self, event):
pointer, modifiers, buttons = window._pointer_modifiers(event)
wid = self.window.get_mouse_event_wid(*pointer)
#log("server_input_devices=%s, server_precise_wheel=%s", client.server_input_devices, client.server_precise_wheel)
if client.server_input_devices=="uinput" and client.server_precise_wheel:
#wheel motion, send it using precise wheel:
#new absolute motion values:
wheel_x = event.valuators.get(2, None)
wheel_y = event.valuators.get(3, None)
valuators = event.valuators
unused_valuators = valuators.copy()
dx, dy = 0, 0
if (valuators and device and device.get("enabled") and
client.server_input_devices=="uinput" and client.server_precise_wheel):
XIModeRelative = 0
classes = device.get("classes")
val_classes = {}
for c in classes.values():
number = c.get("number")
if number is not None and c.get("type")=="valuator" and c.get("mode")==XIModeRelative:
val_classes[number] = c
#previous values:
mv = self.motion_valuators.setdefault(event.device, {})
try:
wx = mv.pop(2)
except:
wx = None
try:
wy = mv.pop(3)
except:
wy = None
last_x, last_y = 0, 0
wheel_x, wheel_y = 0, 0
unused_valuators = {}
for number, value in valuators.items():
valuator = val_classes.get(number)
if valuator:
label = valuator.get("label")
if label:
mouselog("%s: %s", label, value)
if label.lower().find("horiz")>=0:
wheel_x = value
last_x = mv.get(number)
continue
elif label.lower().find("vert")>=0:
wheel_y = value
last_y = mv.get(number)
continue
unused_valuators[number] = value
#new absolute motion values:
#calculate delta if we have both old and new values:
dx, dy = 0, 0
if wx is not None and wheel_x is not None:
dx = wx-wheel_x
if wy is not None and wheel_y is not None:
dy = wy-wheel_y
if last_x is not None and wheel_x is not None:
dx = last_x-wheel_x
if last_y is not None and wheel_y is not None:
dy = last_y-wheel_y
#whatever happens, update our motion cached values:
mv.update(event.valuators)
#now see if we have anything to send as a wheel event:
if dx!=0 or dy!=0:
mouselog("do_xi_motion(%s) wheel deltas: dx=%i, dy=%i", event, dx, dy)
#normalize (xinput is always using 15 degrees?)
client.wheel_event(wid, float(dx)/XINPUT_WHEEL_DIV, float(dy)/XINPUT_WHEEL_DIV, event.device)
if not mv:
#we've dealt with all the valuators
return
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, window._id, event.device, pointer, modifiers, buttons)
packet = ["pointer-position", wid, pointer, modifiers, buttons] + self.get_pointer_extra_args(event)
client.send_mouse_position(packet)
#send plain motion first, if any:
if unused_valuators:
xinputlog("do_xi_motion(%s, %s) wid=%s / focus=%s / window wid=%i, device=%s, pointer=%s, modifiers=%s, buttons=%s", event, device, wid, window._client._focused, window._id, event.device, pointer, modifiers, buttons)
packet = ["pointer-position", wid, pointer, modifiers, buttons] + self.get_pointer_extra_args(event)
xinputlog.info("%s", packet)
client.send_mouse_position(packet)
#now see if we have anything to send as a wheel event:
if dx!=0 or dy!=0:
xinputlog("do_xi_motion(%s, %s) wheel deltas: dx=%i, dy=%i", event, device, dx, dy)
#normalize (xinput is always using 15 degrees?)
client.wheel_event(wid, float(dx)/XINPUT_WHEEL_DIV, float(dy)/XINPUT_WHEEL_DIV, event.device)

def get_pointer_extra_args(self, event):
def intscaled(f):
Expand Down
86 changes: 55 additions & 31 deletions src/xpra/server/server_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,38 +312,18 @@ def has_uinput():
return False
return True

def create_uinput_pointer_device(uuid, uid):
def create_uinput_device(uuid, uid, events, name):
log = get_util_logger()
try:
import uinput
except (ImportError, NameError) as e:
log.error("Error: cannot access python uinput module:")
log.error(" %s", e)
return None
events = (
uinput.REL_X,
uinput.REL_Y,
uinput.REL_WHEEL,
#REL_HIRES_WHEEL = 0x10
#uinput.REL_HWHEEL,
uinput.BTN_LEFT,
uinput.BTN_RIGHT,
uinput.BTN_MIDDLE,
uinput.BTN_SIDE,
uinput.BTN_EXTRA,
uinput.BTN_FORWARD,
uinput.BTN_BACK,
)
import uinput
BUS_USB = 0x03
#BUS_VIRTUAL = 0x06
VENDOR = 0xffff
PRODUCT = 0x1000
#our xpra_udev_product_version script will use the version attribute to set
#the udev OWNER value
VERSION = uid
name = "Xpra Virtual Pointer %s" % uuid
try:
uinput_pointer = uinput.Device(events, name=name, bustype=BUS_USB, vendor=VENDOR, product=PRODUCT, version=VERSION)
device = uinput.Device(events, name=name, bustype=BUS_USB, vendor=VENDOR, product=PRODUCT, version=VERSION)
except OSError as e:
log("uinput.Device creation failed", exc_info=True)
if os.getuid()==0:
Expand All @@ -355,23 +335,67 @@ def create_uinput_pointer_device(uuid, uid):
else:
log.info("cannot access uinput: %s", e)
return None
dev_path = get_uinput_device_path(uinput_pointer)
dev_path = get_uinput_device_path(device)
if not dev_path:
uinput_pointer.destroy()
device.destroy()
return None
return name, uinput_pointer, dev_path
return name, device, dev_path

def create_uinput_pointer_device(uuid, uid):
import uinput
events = (
uinput.REL_X,
uinput.REL_Y,
uinput.REL_WHEEL,
#REL_HIRES_WHEEL = 0x10
#uinput.REL_HWHEEL,
uinput.BTN_LEFT,
uinput.BTN_RIGHT,
uinput.BTN_MIDDLE,
uinput.BTN_SIDE,
uinput.BTN_EXTRA,
uinput.BTN_FORWARD,
uinput.BTN_BACK,
)
name = "Xpra Virtual Pointer %s" % uuid
return create_uinput_device(uuid, uid, events, name)

def create_uinput_touchpad_device(uuid, uid):
import uinput
events = (
uinput.BTN_TOUCH,
uinput.BTN_TOOL_PEN,
uinput.ABS_X + (0, 2**24-1, 0, 0),
uinput.ABS_Y + (0, 2**24-1, 0, 0),
uinput.ABS_PRESSURE + (0, 2**24-1, 0, 0),
)
name = "Xpra Virtual Touchpad %s" % uuid
return create_uinput_device(uuid, uid, events, name)


def create_uinput_devices(uinput_uuid, uid):
d = create_uinput_pointer_device(uinput_uuid, uid)
if not d:
log = get_util_logger()
try:
import uinput
assert uinput
except (ImportError, NameError) as e:
log.error("Error: cannot access python uinput module:")
log.error(" %s", e)
return {}
name, uinput_pointer, dev_path = d
return {
"pointer" : {
pointer = create_uinput_pointer_device(uinput_uuid, uid)
touchpad = create_uinput_touchpad_device(uinput_uuid, uid)
if not pointer or not touchpad:
return {}
def i(device):
name, uinput_pointer, dev_path = device
return {
"name" : name,
"uinput" : uinput_pointer,
"device" : dev_path,
}
return {
"pointer" : i(pointer),
"touchpad" : i(touchpad),
}

def create_input_devices(uinput_uuid, uid):
Expand Down
15 changes: 11 additions & 4 deletions src/xpra/x11/bindings/xi2_bindings.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -328,10 +328,12 @@ cdef class _X11XI2Bindings(_X11CoreBindings):
cdef int opcode
cdef object events
cdef object event_handlers
cdef object device_cache

def __init__(self):
self.opcode = -1
self.event_handlers = {}
self.device_cache = {}
self.reset_events()

def __repr__(self):
Expand Down Expand Up @@ -486,7 +488,6 @@ cdef class _X11XI2Bindings(_X11CoreBindings):
XFlush(self.display)

def parse_xi_event(self, display, uintptr_t _cookie):
log("parse_xi_event(%s)", _cookie)
cdef XGenericEventCookie *cookie = <XGenericEventCookie*> _cookie
cdef XIDeviceEvent *device_e
cdef XIHierarchyEvent *hierarchy_e
Expand All @@ -495,6 +496,7 @@ cdef class _X11XI2Bindings(_X11CoreBindings):
cdef XIRawEvent *raw
cdef int i = 0, j = 0
if not XGetEventData(self.display, cookie):
log("parse_xi_event(%#x) no event data", _cookie)
return None
xie = <XIEvent*> cookie.data
device_e = <XIDeviceEvent*> cookie.data
Expand All @@ -503,13 +505,14 @@ cdef class _X11XI2Bindings(_X11CoreBindings):
global XI_EVENT_NAMES
event_name = XI_EVENT_NAMES.get(xi_type)
if not event_name:
log("unknown XI2 event code: %i", xi_type)
log("parse_xi_event(%#x) unknown XI2 event code: %i", _cookie, 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:
log("parse_xi_event(%#x) repeated %s event skipped (%s)", _cookie, last_event.name, event_name)
return None

pyev = X11Event(event_name)
Expand All @@ -520,12 +523,14 @@ cdef class _X11XI2Bindings(_X11CoreBindings):
pyev.time = int(xie.time)
pyev.window = int(XDefaultRootWindow(self.display))

device_info = None
if xi_type in (XI_KeyPress, XI_KeyRelease,
XI_ButtonPress, XI_ButtonRelease,
XI_Motion,
XI_TouchBegin, XI_TouchUpdate, XI_TouchEnd):
device = <XIDeviceEvent*> cookie.data
#pyev.source = device.sourceid #always 0
device_info = self.device_cache.get(device.deviceid)
pyev.device = device.deviceid
pyev.detail = device.detail
pyev.flags = device.flags
Expand Down Expand Up @@ -583,9 +588,10 @@ cdef class _X11XI2Bindings(_X11CoreBindings):
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)
log("parse_xi_event: %s, device_info=%s, handler=%s", pyev, device_info, handler)
handler(pyev, device_info)
log("parse_xi_event: no handler for %s", event_name)
return None

def get_devices(self, show_all=True, show_disabled=False):
Expand Down Expand Up @@ -623,6 +629,7 @@ cdef class _X11XI2Bindings(_X11CoreBindings):
log("[%i] %s: %s", device.deviceid, device.name, info)
dinfo[device.deviceid] = info
XIFreeDeviceInfo(devices)
self.device_cache = dinfo
return dinfo

def get_device_properties(self, deviceid):
Expand Down
51 changes: 30 additions & 21 deletions src/xpra/x11/vfb_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,32 +65,41 @@ def cleanup_dir():
for d in dirs:
makedir(d)

#create individual device files,
#only pointer for now:
i = 0
dev_type = "pointer"
name = "Xpra Virtual Pointer %s" % device_uuid
conf_files = []
for i, dev_type in (
(0, "pointer"),
#(1, "touchpad"),
) :
f = save_input_conf(xorg_conf_dir, i, dev_type, device_uuid, uid, gid)
conf_files.append(f)
def cleanup_input_conf_files():
for f in conf_files:
os.unlink(f)
cleanups.insert(0, cleanup_input_conf_files)
return cleanups

#create individual device files:
def save_input_conf(xorg_conf_dir, i, dev_type, device_uuid, uid, gid):
upper_dev_type = dev_type[:1].upper()+dev_type[1:] #ie: Pointer
product_name = "Xpra Virtual %s %s" % (upper_dev_type, device_uuid)
identifier = "xpra-virtual-%s" % dev_type
conf_file = os.path.join(xorg_conf_dir, "%02i-%s.conf" % (i, dev_type))
with open(conf_file, "wb") as f:
f.write(b"""Section "InputClass"
Identifier "xpra-virtual-%s"
MatchProduct "%s"
MatchUSBID "ffff:ffff"
MatchIsPointer "True"
Driver "libinput"
Option "AccelProfile" "flat"
Option "Ignore" "False"
Identifier "%s"
MatchProduct "%s"
MatchUSBID "ffff:ffff"
MatchIs%s "True"
Driver "libinput"
Option "AccelProfile" "flat"
Option "Ignore" "False"
EndSection
""" % (dev_type, name))
""" % (identifier, product_name, upper_dev_type))
os.fchown(f.fileno(), uid, gid)
#Option "AccelerationProfile" "-1"
#Option "AccelerationScheme" "none"
#Option "AccelSpeed" "-1"
def cleanup_conf_file():
log("cleanup_conf_file: %s", conf_file)
os.unlink(conf_file)
cleanups.insert(0, cleanup_conf_file)
return cleanups
#Option "AccelerationProfile" "-1"
#Option "AccelerationScheme" "none"
#Option "AccelSpeed" "-1"
return conf_file


def get_xauthority_path(display_name, username, uid, gid):
Expand Down
Loading

0 comments on commit 57f5571

Please sign in to comment.