Browse Source

Implement X Input pointer handling for Unix

Switch from using Core events to using X Input events for pointer
devices in order to differentiate between mouse events and touch events.

Because FLTK doesn't understand X Input 2, we intercept these events and
translate them to core events where possible.
tags/v1.10.90
Aaron Sowry 5 years ago
parent
commit
c79a05dc15

+ 3
- 0
cmake/StaticBuild.cmake View File

@@ -112,6 +112,9 @@ if(BUILD_STATIC)
if(X11_Xrandr_LIB)
set(X11_Xrandr_LIB "-Wl,-Bstatic -lXrandr -lXrender -Wl,-Bdynamic")
endif()
if(X11_Xi_LIB)
set(X11_Xi_LIB "-Wl,-Bstatic -lXi -Wl,-Bdynamic")
endif()
endif()
endif()


+ 5
- 4
vncviewer/CMakeLists.txt View File

@@ -15,6 +15,7 @@ set(VNCVIEWER_SOURCES
Viewport.cxx
parameters.cxx
keysym2ucs.c
touch.cxx
vncviewer.cxx)

if(WIN32)
@@ -32,7 +33,7 @@ if(WIN32)
elseif(APPLE)
set(VNCVIEWER_SOURCES ${VNCVIEWER_SOURCES} cocoa.mm osx_to_qnum.c)
else()
set(VNCVIEWER_SOURCES ${VNCVIEWER_SOURCES} xkb_to_qnum.c)
set(VNCVIEWER_SOURCES ${VNCVIEWER_SOURCES} XInputTouchHandler.cxx xkb_to_qnum.c)
endif()

if(WIN32)
@@ -53,12 +54,12 @@ target_link_libraries(vncviewer rfb network rdr os ${FLTK_LIBRARIES} ${GETTEXT_L

if(WIN32)
target_link_libraries(vncviewer msimg32)
endif()

if(APPLE)
elseif(APPLE)
target_link_libraries(vncviewer "-framework Cocoa")
target_link_libraries(vncviewer "-framework Carbon")
target_link_libraries(vncviewer "-framework IOKit")
else()
target_link_libraries(vncviewer ${X11_Xi_LIB})
endif()

install(TARGETS vncviewer DESTINATION ${CMAKE_INSTALL_FULL_BINDIR})

+ 7
- 15
vncviewer/DesktopWindow.cxx View File

@@ -37,6 +37,7 @@
#include "CConn.h"
#include "Surface.h"
#include "Viewport.h"
#include "touch.h"

#include <FL/Fl.H>
#include <FL/Fl_Image_Surface.H>
@@ -967,23 +968,13 @@ void DesktopWindow::ungrabKeyboard()
void DesktopWindow::grabPointer()
{
#if !defined(WIN32) && !defined(__APPLE__)
int ret;

// We also need to grab the pointer as some WMs like to grab buttons
// combined with modifies (e.g. Alt+Button0 in metacity).
ret = XGrabPointer(fl_display, fl_xid(this), True,
ButtonPressMask|ButtonReleaseMask|
ButtonMotionMask|PointerMotionMask,
GrabModeAsync, GrabModeAsync,
None, None, CurrentTime);
if (ret) {
// Having a button pressed prevents us from grabbing, we make
// a new attempt in fltkHandle()
if (ret == AlreadyGrabbed)
return;
vlog.error(_("Failure grabbing mouse"));

// Having a button pressed prevents us from grabbing, we make
// a new attempt in fltkHandle()
if (!x11_grab_pointer(fl_xid(this)))
return;
}
#endif

mouseGrabbed = true;
@@ -993,8 +984,9 @@ void DesktopWindow::grabPointer()
void DesktopWindow::ungrabPointer()
{
mouseGrabbed = false;

#if !defined(WIN32) && !defined(__APPLE__)
XUngrabPointer(fl_display, CurrentTime);
x11_ungrab_pointer(fl_xid(this));
#endif
}


+ 332
- 0
vncviewer/XInputTouchHandler.cxx View File

@@ -0,0 +1,332 @@
/* Copyright 2019 Aaron Sowry for Cendio AB
* Copyright 2019-2020 Pierre Ossman for Cendio AB
*
* This is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
* USA.
*/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <assert.h>
#include <string.h>

#include <X11/extensions/XInput2.h>
#include <X11/extensions/XI2.h>

#include <FL/x.H>

#include <rfb/LogWriter.h>

#include "i18n.h"
#include "XInputTouchHandler.h"

static rfb::LogWriter vlog("XInputTouchHandler");

static bool grabbed = false;

XInputTouchHandler::XInputTouchHandler(Window wnd)
: wnd(wnd), fakeStateMask(0), trackingTouch(false)
{
XIEventMask eventmask;
unsigned char flags[XIMaskLen(XI_LASTEVENT)] = { 0 };

// Event delivery is broken when somebody else does a pointer grab,
// so we need to listen to all devices and do filtering of master
// devices manually
eventmask.deviceid = XIAllDevices;
eventmask.mask_len = sizeof(flags);
eventmask.mask = flags;

XISetMask(flags, XI_ButtonPress);
XISetMask(flags, XI_Motion);
XISetMask(flags, XI_ButtonRelease);

XISetMask(flags, XI_TouchBegin);
XISetMask(flags, XI_TouchUpdate);
XISetMask(flags, XI_TouchEnd);

// If something has a passive grab of touches (e.g. the window
// manager wants to have its own gestures) then we won't get the
// touch events until everyone who has a grab has indicated they
// don't want these touches (via XIAllowTouchEvents()).
// Unfortunately the touches are then replayed one touch point at
// a time, meaning things will be delayed and out of order,
// completely screwing up our gesture detection. Listening for
// XI_TouchOwnership has the effect of giving us the touch events
// right away, even if grabbing clients are also getting them.
//
// FIXME: We should really wait for the XI_TouchOwnership event
// before it is safe to react to the gesture, otherwise we
// might react to something that the window manager will
// also react to.
//
if (!grabbed)
XISetMask(flags, XI_TouchOwnership);

XISelectEvents(fl_display, wnd, &eventmask, 1);
}

bool XInputTouchHandler::grabPointer()
{
XIEventMask *curmasks;
int num_masks;

int ret, ndevices;

XIDeviceInfo *devices, *device;
bool gotGrab;

// We grab for the same events as the window is currently interested in
curmasks = XIGetSelectedEvents(fl_display, wnd, &num_masks);
if (curmasks == NULL) {
if (num_masks == -1)
vlog.error(_("Unable to get X Input 2 event mask for window 0x%08lu"), wnd);
else
vlog.error(_("Window 0x%08lu has no X Input 2 event mask"), wnd);

return false;
}

// Our windows should only have a single mask, which allows us to
// simplify all the code handling the masks
if (num_masks > 1) {
vlog.error(_("Window 0x%08lu has more than one X Input 2 event mask"), wnd);
return false;
}

devices = XIQueryDevice(fl_display, XIAllMasterDevices, &ndevices);

// Iterate through available devices to find those which
// provide pointer input, and attempt to grab all such devices.
gotGrab = false;
for (int i = 0; i < ndevices; i++) {
device = &devices[i];

if (device->use != XIMasterPointer)
continue;

curmasks[0].deviceid = device->deviceid;

ret = XIGrabDevice(fl_display,
device->deviceid,
wnd,
CurrentTime,
None,
XIGrabModeAsync,
XIGrabModeAsync,
True,
&(curmasks[0]));

if (ret) {
if (ret == XIAlreadyGrabbed) {
continue;
} else {
vlog.error(_("Failure grabbing device %i"), device->deviceid);
continue;
}
}

gotGrab = true;
}

XIFreeDeviceInfo(devices);

// Did we not even grab a single device?
if (!gotGrab)
return false;

grabbed = true;

return true;
}

void XInputTouchHandler::ungrabPointer()
{
int ndevices;
XIDeviceInfo *devices, *device;

devices = XIQueryDevice(fl_display, XIAllMasterDevices, &ndevices);

// Release all devices, hoping they are the same as when we
// grabbed things
for (int i = 0; i < ndevices; i++) {
device = &devices[i];

if (device->use != XIMasterPointer)
continue;

XIUngrabDevice(fl_display, device->deviceid, CurrentTime);
}

XIFreeDeviceInfo(devices);

grabbed = false;
}

void XInputTouchHandler::processEvent(const XIDeviceEvent* devev)
{
// FLTK doesn't understand X Input events, and we've stopped
// delivery of Core events by enabling the X Input ones. Make
// FLTK happy by faking Core events based on the X Input ones.

bool isMaster = devev->deviceid != devev->sourceid;

// We're only interested in events from master devices
if (!isMaster) {
// However we need to accept TouchEnd from slave devices as they
// might not get delivered if there is a pointer grab, see:
// https://gitlab.freedesktop.org/xorg/xserver/-/issues/1016
if (devev->evtype != XI_TouchEnd)
return;
}

// Avoid duplicate TouchEnd events, see above
// FIXME: Doesn't handle floating slave devices
if (isMaster && devev->evtype == XI_TouchEnd)
return;

if (devev->flags & XIPointerEmulated) {
// We still want the server to do the scroll wheel to button thing
// though, so keep those
if (((devev->evtype == XI_ButtonPress) ||
(devev->evtype == XI_ButtonRelease)) &&
(devev->detail >= 4) && (devev->detail <= 7)) {
;
} else {
// Sometimes the server incorrectly sends us various events with
// this flag set, see:
// https://gitlab.freedesktop.org/xorg/xserver/-/issues/1027
return;
}
}

switch (devev->evtype) {
case XI_Enter:
case XI_Leave:
// We get these when the mouse is grabbed implicitly, so just ignore them
// https://gitlab.freedesktop.org/xorg/xserver/-/issues/1026
break;
case XI_Motion:
// FIXME: We also get XI_Motion for scroll wheel events, which
// we might want to ignore
fakeMotionEvent(devev);
break;
case XI_ButtonPress:
fakeButtonEvent(true, devev->detail, devev);
break;
case XI_ButtonRelease:
fakeButtonEvent(false, devev->detail, devev);
break;
case XI_TouchBegin:
if (trackingTouch)
break;

// XInput2 wants us to explicitly accept touch sequences
// for grabbed devices before it will pass events
if (grabbed) {
XIAllowTouchEvents(fl_display,
devev->deviceid,
devev->detail,
devev->event,
XIAcceptTouch);
}

trackingTouch = true;
trackedTouchPoint = devev->detail;

fakeMotionEvent(devev);
fakeButtonEvent(true, Button1, devev);
break;
case XI_TouchUpdate:
if (!trackingTouch || (devev->detail != trackedTouchPoint))
break;
fakeMotionEvent(devev);
break;
case XI_TouchEnd:
if (!trackingTouch || (devev->detail != trackedTouchPoint))
break;
fakeButtonEvent(false, Button1, devev);
trackingTouch = false;
break;
case XI_TouchOwnership:
// FIXME: Currently ignored, see constructor
break;
}
}

void XInputTouchHandler::preparePointerEvent(XEvent* dst, const XIDeviceEvent* src)
{
// XButtonEvent and XMotionEvent are almost identical, so we
// don't have to care which it is for these fields
dst->xbutton.serial = src->serial;
dst->xbutton.display = src->display;
dst->xbutton.window = src->event;
dst->xbutton.root = src->root;
dst->xbutton.subwindow = src->child;
dst->xbutton.time = src->time;
dst->xbutton.x = src->event_x;
dst->xbutton.y = src->event_y;
dst->xbutton.x_root = src->root_x;
dst->xbutton.y_root = src->root_y;
dst->xbutton.state = src->mods.effective;
dst->xbutton.state |= ((src->buttons.mask[0] >> 1) & 0x1f) << 8;
dst->xbutton.same_screen = True;
}

void XInputTouchHandler::fakeMotionEvent(const XIDeviceEvent* origEvent)
{
XEvent fakeEvent;

memset(&fakeEvent, 0, sizeof(XEvent));

fakeEvent.type = MotionNotify;
fakeEvent.xmotion.is_hint = False;
preparePointerEvent(&fakeEvent, origEvent);

fakeEvent.xbutton.state |= fakeStateMask;

pushFakeEvent(&fakeEvent);
}

void XInputTouchHandler::fakeButtonEvent(bool press, int button,
const XIDeviceEvent* origEvent)
{
XEvent fakeEvent;

memset(&fakeEvent, 0, sizeof(XEvent));

fakeEvent.type = press ? ButtonPress : ButtonRelease;
fakeEvent.xbutton.button = button;
preparePointerEvent(&fakeEvent, origEvent);

fakeEvent.xbutton.state |= fakeStateMask;

pushFakeEvent(&fakeEvent);

// Set/unset the bit for the correct button
if (press) {
fakeStateMask |= (1<<(7+button));
} else {
fakeStateMask &= ~(1<<(7+button));
}
}

void XInputTouchHandler::pushFakeEvent(XEvent* event)
{
// Perhaps use XPutBackEvent() to avoid round trip latency?
XSendEvent(fl_display, event->xany.window, true, 0, event);
}

+ 48
- 0
vncviewer/XInputTouchHandler.h View File

@@ -0,0 +1,48 @@
/* Copyright 2019 Aaron Sowry for Cendio AB
* Copyright 2019-2020 Pierre Ossman for Cendio AB
*
* This is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
* USA.
*/

#ifndef __XINPUTTOUCHHANDLER_H__
#define __XINPUTTOUCHHANDLER_H__

class XInputTouchHandler {
public:
XInputTouchHandler(Window wnd);

bool grabPointer();
void ungrabPointer();

void processEvent(const XIDeviceEvent* devev);

protected:
void preparePointerEvent(XEvent* dst, const XIDeviceEvent* src);
void fakeMotionEvent(const XIDeviceEvent* origEvent);
void fakeButtonEvent(bool press, int button,
const XIDeviceEvent* origEvent);

private:
void pushFakeEvent(XEvent* event);

private:
Window wnd;
int fakeStateMask;
bool trackingTouch;
int trackedTouchPoint;
};

#endif

+ 220
- 0
vncviewer/touch.cxx View File

@@ -0,0 +1,220 @@
/* Copyright 2019-2020 Pierre Ossman <ossman@cendio.se> for Cendio AB
* Copyright 2019 Aaron Sowry for Cendio AB
*
* This is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
* USA.
*/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <assert.h>

#include <map>

#if !defined(WIN32) && !defined(__APPLE__)
#include <X11/extensions/XInput2.h>
#include <X11/extensions/XI2.h>
#endif

#include <FL/Fl.H>
#include <FL/x.H>

#include <rfb/LogWriter.h>

#include "i18n.h"
#include "vncviewer.h"
#if !defined(WIN32) && !defined(__APPLE__)
#include "XInputTouchHandler.h"
#endif

#include "touch.h"

static rfb::LogWriter vlog("Touch");

#if !defined(WIN32) && !defined(__APPLE__)
static int xi_major;

typedef std::map<Window, class XInputTouchHandler*> HandlerMap;
static HandlerMap handlers;
#endif

#if !defined(WIN32) && !defined(__APPLE__)
static void x11_change_touch_ownership(bool enable)
{
HandlerMap::const_iterator iter;

XIEventMask *curmasks;
int num_masks;

XIEventMask newmask;
unsigned char mask[XIMaskLen(XI_LASTEVENT)] = { 0 };

newmask.mask = mask;
newmask.mask_len = sizeof(mask);

for (iter = handlers.begin(); iter != handlers.end(); ++iter) {
curmasks = XIGetSelectedEvents(fl_display, iter->first, &num_masks);
if (curmasks == NULL) {
if (num_masks == -1)
vlog.error(_("Unable to get X Input 2 event mask for window 0x%08lu"), iter->first);
continue;
}

// Our windows should only have a single mask, which allows us to
// simplify all the code handling the masks
if (num_masks > 1) {
vlog.error(_("Window 0x%08lu has more than one X Input 2 event mask"), iter->first);
continue;
}

newmask.deviceid = curmasks[0].deviceid;

assert(newmask.mask_len >= curmasks[0].mask_len);
memcpy(newmask.mask, curmasks[0].mask, curmasks[0].mask_len);
if (enable)
XISetMask(newmask.mask, XI_TouchOwnership);
else
XIClearMask(newmask.mask, XI_TouchOwnership);

XISelectEvents(fl_display, iter->first, &newmask, 1);

XFree(curmasks);
}
}

bool x11_grab_pointer(Window window)
{
bool ret;

if (handlers.count(window) == 0) {
vlog.error(_("Invalid window 0x%08lu specified for pointer grab"), window);
return BadWindow;
}

// We need to remove XI_TouchOwnership from our event masks while
// grabbing as otherwise we will get double events (one for the grab,
// and one because of XI_TouchOwnership) with no way of telling them
// apart. See XInputTouchHandler constructor for why we use this
// event.
x11_change_touch_ownership(false);

ret = handlers[window]->grabPointer();

if (!ret)
x11_change_touch_ownership(true);

return ret;
}

void x11_ungrab_pointer(Window window)
{
if (handlers.count(window) == 0) {
vlog.error(_("Invalid window 0x%08lu specified for pointer grab"), window);
return;
}

handlers[window]->ungrabPointer();

// Restore XI_TouchOwnership now that the grab is gone
x11_change_touch_ownership(true);
}
#endif

static int handleTouchEvent(void *event, void *data)
{
#if !defined(WIN32) && !defined(__APPLE__)
XEvent *xevent = (XEvent*)event;

if (xevent->type == MapNotify) {
handlers[xevent->xmap.window] = new XInputTouchHandler(xevent->xmap.window);

// Fall through as we don't want to interfere with whatever someone
// else might want to do with this event

} else if (xevent->type == UnmapNotify) {
delete handlers[xevent->xunmap.window];
handlers.erase(xevent->xunmap.window);
} else if (xevent->type == DestroyNotify) {
delete handlers[xevent->xdestroywindow.window];
handlers.erase(xevent->xdestroywindow.window);
} else if (xevent->type == GenericEvent) {
if (xevent->xgeneric.extension == xi_major) {
XIDeviceEvent *devev;

if (!XGetEventData(fl_display, &xevent->xcookie)) {
vlog.error(_("Failed to get event data for X Input event"));
return 1;
}

devev = (XIDeviceEvent*)xevent->xcookie.data;

if (handlers.count(devev->event) == 0) {
// We get these when the mouse is grabbed implicitly, so just
// ignore them
// https://gitlab.freedesktop.org/xorg/xserver/-/issues/1026
if ((devev->evtype == XI_Enter) || (devev->evtype == XI_Leave))
;
else
vlog.error(_("X Input event for unknown window"));
XFreeEventData(fl_display, &xevent->xcookie);
return 1;
}

handlers[devev->event]->processEvent(devev);

XFreeEventData(fl_display, &xevent->xcookie);

return 1;
}
}
#endif

return 0;
}

void enable_touch()
{
#if !defined(WIN32) && !defined(__APPLE__)
int ev, err;
int major_ver, minor_ver;

fl_open_display();

if (!XQueryExtension(fl_display, "XInputExtension", &xi_major, &ev, &err)) {
exit_vncviewer(_("X Input extension not available."));
return; // Not reached
}

major_ver = 2;
minor_ver = 2;
if (XIQueryVersion(fl_display, &major_ver, &minor_ver) != Success) {
exit_vncviewer(_("X Input 2 (or newer) is not available."));
return; // Not reached
}

if ((major_ver == 2) && (minor_ver < 2))
vlog.error(_("X Input 2.2 (or newer) is not available. Touch gestures will not be supported."));
#endif

Fl::add_system_handler(handleTouchEvent, NULL);
}

void disable_touch()
{
Fl::remove_system_handler(handleTouchEvent);
}


+ 30
- 0
vncviewer/touch.h View File

@@ -0,0 +1,30 @@
/* Copyright 2019-2020 Pierre Ossman <ossman@cendio.se> for Cendio AB
*
* This is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
* USA.
*/

#ifndef __TOUCH_H__
#define __TOUCH_H__

void enable_touch();
void disable_touch();

#if !defined(WIN32) && !defined(__APPLE__)
bool x11_grab_pointer(Window window);
void x11_ungrab_pointer(Window window);
#endif

#endif

+ 2
- 0
vncviewer/vncviewer.cxx View File

@@ -68,6 +68,7 @@
#include "CConn.h"
#include "ServerDialog.h"
#include "UserDialog.h"
#include "touch.h"
#include "vncviewer.h"
#include "fltk_layout.h"

@@ -585,6 +586,7 @@ int main(int argc, char** argv)
#endif

init_fltk();
enable_touch();

// Check if the server name in reality is a configuration file
potentiallyLoadConfigurationFile(vncServerName);

Loading…
Cancel
Save