From: Aaron Sowry Date: Thu, 25 Apr 2019 00:31:23 +0000 (+1200) Subject: Implement X Input pointer handling for Unix X-Git-Tag: v1.10.90~27^2~3 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=c79a05dc152a6a51d38b8db9f085339bd193241a;p=tigervnc.git 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. --- diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index 43a0786e..9129f9db 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -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() diff --git a/vncviewer/CMakeLists.txt b/vncviewer/CMakeLists.txt index a2048f29..92b516cb 100644 --- a/vncviewer/CMakeLists.txt +++ b/vncviewer/CMakeLists.txt @@ -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}) diff --git a/vncviewer/DesktopWindow.cxx b/vncviewer/DesktopWindow.cxx index f616f8c8..6dc85f4a 100644 --- a/vncviewer/DesktopWindow.cxx +++ b/vncviewer/DesktopWindow.cxx @@ -37,6 +37,7 @@ #include "CConn.h" #include "Surface.h" #include "Viewport.h" +#include "touch.h" #include #include @@ -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 } diff --git a/vncviewer/XInputTouchHandler.cxx b/vncviewer/XInputTouchHandler.cxx new file mode 100644 index 00000000..f64b06ad --- /dev/null +++ b/vncviewer/XInputTouchHandler.cxx @@ -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 +#endif + +#include +#include + +#include +#include + +#include + +#include + +#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); +} diff --git a/vncviewer/XInputTouchHandler.h b/vncviewer/XInputTouchHandler.h new file mode 100644 index 00000000..06572887 --- /dev/null +++ b/vncviewer/XInputTouchHandler.h @@ -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 diff --git a/vncviewer/touch.cxx b/vncviewer/touch.cxx new file mode 100644 index 00000000..4ad9a0ad --- /dev/null +++ b/vncviewer/touch.cxx @@ -0,0 +1,220 @@ +/* Copyright 2019-2020 Pierre Ossman 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 +#endif + +#include + +#include + +#if !defined(WIN32) && !defined(__APPLE__) +#include +#include +#endif + +#include +#include + +#include + +#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 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); +} + diff --git a/vncviewer/touch.h b/vncviewer/touch.h new file mode 100644 index 00000000..dad79b4e --- /dev/null +++ b/vncviewer/touch.h @@ -0,0 +1,30 @@ +/* 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 __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 diff --git a/vncviewer/vncviewer.cxx b/vncviewer/vncviewer.cxx index 7b1ac92e..d4dd3063 100644 --- a/vncviewer/vncviewer.cxx +++ b/vncviewer/vncviewer.cxx @@ -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);