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()
Viewport.cxx
parameters.cxx
keysym2ucs.c
+ touch.cxx
vncviewer.cxx)
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)
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})
#include "CConn.h"
#include "Surface.h"
#include "Viewport.h"
+#include "touch.h"
#include <FL/Fl.H>
#include <FL/Fl_Image_Surface.H>
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;
void DesktopWindow::ungrabPointer()
{
mouseGrabbed = false;
+
#if !defined(WIN32) && !defined(__APPLE__)
- XUngrabPointer(fl_display, CurrentTime);
+ x11_ungrab_pointer(fl_xid(this));
#endif
}
--- /dev/null
+/* 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);
+}
--- /dev/null
+/* 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
--- /dev/null
+/* 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);
+}
+
--- /dev/null
+/* 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
#include "CConn.h"
#include "ServerDialog.h"
#include "UserDialog.h"
+#include "touch.h"
#include "vncviewer.h"
#include "fltk_layout.h"
#endif
init_fltk();
+ enable_touch();
// Check if the server name in reality is a configuration file
potentiallyLoadConfigurationFile(vncServerName);