aboutsummaryrefslogtreecommitdiffstats
path: root/vncviewer
diff options
context:
space:
mode:
Diffstat (limited to 'vncviewer')
-rw-r--r--vncviewer/BaseTouchHandler.cxx196
-rw-r--r--vncviewer/BaseTouchHandler.h51
-rw-r--r--vncviewer/CMakeLists.txt12
-rw-r--r--vncviewer/DesktopWindow.cxx28
-rw-r--r--vncviewer/EmulateMB.cxx72
-rw-r--r--vncviewer/EmulateMB.h4
-rw-r--r--vncviewer/GestureEvent.h51
-rw-r--r--vncviewer/GestureHandler.cxx515
-rw-r--r--vncviewer/GestureHandler.h81
-rw-r--r--vncviewer/Viewport.cxx48
-rw-r--r--vncviewer/Viewport.h1
-rw-r--r--vncviewer/Win32TouchHandler.cxx442
-rw-r--r--vncviewer/Win32TouchHandler.h60
-rw-r--r--vncviewer/XInputTouchHandler.cxx461
-rw-r--r--vncviewer/XInputTouchHandler.h58
-rw-r--r--vncviewer/i18n.h4
-rw-r--r--vncviewer/touch.cxx273
-rw-r--r--vncviewer/touch.h30
-rw-r--r--vncviewer/vncviewer.cxx46
19 files changed, 2361 insertions, 72 deletions
diff --git a/vncviewer/BaseTouchHandler.cxx b/vncviewer/BaseTouchHandler.cxx
new file mode 100644
index 00000000..1bcf66c9
--- /dev/null
+++ b/vncviewer/BaseTouchHandler.cxx
@@ -0,0 +1,196 @@
+/* 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 <stdlib.h>
+#include <math.h>
+
+#define XK_MISCELLANY
+#include <rfb/keysymdef.h>
+#include <rfb/util.h>
+
+#include "GestureHandler.h"
+#include "BaseTouchHandler.h"
+
+// Sensitivity threshold for gestures
+static const int ZOOMSENS = 30;
+static const int SCRLSENS = 50;
+
+static const unsigned DOUBLE_TAP_TIMEOUT = 1000;
+static const unsigned DOUBLE_TAP_THRESHOLD = 50;
+
+BaseTouchHandler::BaseTouchHandler()
+{
+ gettimeofday(&lastTapTime, NULL);
+}
+
+BaseTouchHandler::~BaseTouchHandler()
+{
+}
+
+void BaseTouchHandler::handleGestureEvent(const GestureEvent& ev)
+{
+ double magnitude;
+
+ switch (ev.type) {
+ case GestureBegin:
+ switch (ev.gesture) {
+ case GestureOneTap:
+ handleTapEvent(ev, 1);
+ break;
+ case GestureTwoTap:
+ handleTapEvent(ev, 3);
+ break;
+ case GestureThreeTap:
+ handleTapEvent(ev, 2);
+ break;
+ case GestureDrag:
+ fakeMotionEvent(ev);
+ fakeButtonEvent(true, 1, ev);
+ break;
+ case GestureLongPress:
+ fakeMotionEvent(ev);
+ fakeButtonEvent(true, 3, ev);
+ break;
+ case GestureTwoDrag:
+ lastMagnitudeX = ev.magnitudeX;
+ lastMagnitudeY = ev.magnitudeY;
+ fakeMotionEvent(ev);
+ break;
+ case GesturePinch:
+ lastMagnitudeX = hypot(ev.magnitudeX, ev.magnitudeY);
+ fakeMotionEvent(ev);
+ break;
+ }
+ break;
+
+ case GestureUpdate:
+ switch (ev.gesture) {
+ case GestureOneTap:
+ case GestureTwoTap:
+ case GestureThreeTap:
+ break;
+ case GestureDrag:
+ case GestureLongPress:
+ fakeMotionEvent(ev);
+ break;
+ case GestureTwoDrag:
+ // Always scroll in the same position.
+ // We don't know if the mouse was moved so we need to move it
+ // every update.
+ fakeMotionEvent(ev);
+ while ((ev.magnitudeY - lastMagnitudeY) > SCRLSENS) {
+ fakeButtonEvent(true, 4, ev);
+ fakeButtonEvent(false, 4, ev);
+ lastMagnitudeY += SCRLSENS;
+ }
+ while ((ev.magnitudeY - lastMagnitudeY) < -SCRLSENS) {
+ fakeButtonEvent(true, 5, ev);
+ fakeButtonEvent(false, 5, ev);
+ lastMagnitudeY -= SCRLSENS;
+ }
+ while ((ev.magnitudeX - lastMagnitudeX) > SCRLSENS) {
+ fakeButtonEvent(true, 6, ev);
+ fakeButtonEvent(false, 6, ev);
+ lastMagnitudeX += SCRLSENS;
+ }
+ while ((ev.magnitudeX - lastMagnitudeX) < -SCRLSENS) {
+ fakeButtonEvent(true, 7, ev);
+ fakeButtonEvent(false, 7, ev);
+ lastMagnitudeX -= SCRLSENS;
+ }
+ break;
+ case GesturePinch:
+ // Always scroll in the same position.
+ // We don't know if the mouse was moved so we need to move it
+ // every update.
+ fakeMotionEvent(ev);
+ magnitude = hypot(ev.magnitudeX, ev.magnitudeY);
+ if (abs(magnitude - lastMagnitudeX) > ZOOMSENS) {
+ fakeKeyEvent(true, XK_Control_L, ev);
+
+ while ((magnitude - lastMagnitudeX) > ZOOMSENS) {
+ fakeButtonEvent(true, 4, ev);
+ fakeButtonEvent(false, 4, ev);
+ lastMagnitudeX += ZOOMSENS;
+ }
+ while ((magnitude - lastMagnitudeX) < -ZOOMSENS) {
+ fakeButtonEvent(true, 5, ev);
+ fakeButtonEvent(false, 5, ev);
+ lastMagnitudeX -= ZOOMSENS;
+ }
+
+ fakeKeyEvent(false, XK_Control_L, ev);
+ }
+ }
+ break;
+
+ case GestureEnd:
+ switch (ev.gesture) {
+ case GestureOneTap:
+ case GestureTwoTap:
+ case GestureThreeTap:
+ case GesturePinch:
+ case GestureTwoDrag:
+ break;
+ case GestureDrag:
+ fakeMotionEvent(ev);
+ fakeButtonEvent(false, 1, ev);
+ break;
+ case GestureLongPress:
+ fakeMotionEvent(ev);
+ fakeButtonEvent(false, 3, ev);
+ break;
+ }
+ break;
+ }
+}
+
+void BaseTouchHandler::handleTapEvent(const GestureEvent& ev,
+ int buttonEvent)
+{
+ GestureEvent newEv = ev;
+
+ // If the user quickly taps multiple times we assume they meant to
+ // hit the same spot, so slightly adjust coordinates
+ if ((rfb::msSince(&lastTapTime) < DOUBLE_TAP_TIMEOUT) &&
+ (firstDoubleTapEvent.type == ev.type)) {
+
+ double dx = firstDoubleTapEvent.eventX - ev.eventX;
+ double dy = firstDoubleTapEvent.eventY - ev.eventY;
+ double distance = hypot(dx, dy);
+
+ if (distance < DOUBLE_TAP_THRESHOLD) {
+ newEv.eventX = firstDoubleTapEvent.eventX;
+ newEv.eventY = firstDoubleTapEvent.eventY;
+ } else {
+ firstDoubleTapEvent = ev;
+ }
+ } else {
+ firstDoubleTapEvent = ev;
+ }
+ gettimeofday(&lastTapTime, NULL);
+
+ fakeMotionEvent(newEv);
+ fakeButtonEvent(true, buttonEvent, newEv);
+ fakeButtonEvent(false, buttonEvent, newEv);
+}
diff --git a/vncviewer/BaseTouchHandler.h b/vncviewer/BaseTouchHandler.h
new file mode 100644
index 00000000..d6d1a43d
--- /dev/null
+++ b/vncviewer/BaseTouchHandler.h
@@ -0,0 +1,51 @@
+/* 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 __BASETOUCHHANDLER_H__
+#define __BASETOUCHHANDLER_H__
+
+#include "GestureEvent.h"
+
+class BaseTouchHandler {
+ public:
+ virtual ~BaseTouchHandler();
+
+ protected:
+ BaseTouchHandler();
+
+ protected:
+ virtual void fakeMotionEvent(const GestureEvent origEvent) = 0;
+ virtual void fakeButtonEvent(bool press, int button,
+ const GestureEvent origEvent) = 0;
+ virtual void fakeKeyEvent(bool press, int keycode,
+ const GestureEvent origEvent) = 0;
+
+ virtual void handleGestureEvent(const GestureEvent& event);
+
+ private:
+ void handleTapEvent(const GestureEvent& ev, int buttonEvent);
+
+ double lastMagnitudeX;
+ double lastMagnitudeY;
+
+ GestureEvent firstDoubleTapEvent;
+ struct timeval lastTapTime;
+};
+
+#endif
diff --git a/vncviewer/CMakeLists.txt b/vncviewer/CMakeLists.txt
index a2048f29..e4fad782 100644
--- a/vncviewer/CMakeLists.txt
+++ b/vncviewer/CMakeLists.txt
@@ -4,6 +4,7 @@ include_directories(${GETTEXT_INCLUDE_DIR})
include_directories(${CMAKE_SOURCE_DIR}/common)
set(VNCVIEWER_SOURCES
menukey.cxx
+ BaseTouchHandler.cxx
CConn.cxx
DesktopWindow.cxx
EmulateMB.cxx
@@ -15,6 +16,7 @@ set(VNCVIEWER_SOURCES
Viewport.cxx
parameters.cxx
keysym2ucs.c
+ touch.cxx
vncviewer.cxx)
if(WIN32)
@@ -28,11 +30,11 @@ if(WIN32)
endif()
if(WIN32)
- set(VNCVIEWER_SOURCES ${VNCVIEWER_SOURCES} win32.c)
+ set(VNCVIEWER_SOURCES ${VNCVIEWER_SOURCES} Win32TouchHandler.cxx win32.c)
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} GestureHandler.cxx XInputTouchHandler.cxx xkb_to_qnum.c)
endif()
if(WIN32)
@@ -53,12 +55,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 2ca13eea..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 <FL/Fl.H>
#include <FL/Fl_Image_Surface.H>
@@ -755,6 +756,12 @@ int DesktopWindow::fltkHandle(int event, Fl_Window *win)
{
int ret;
+ // FLTK keeps spamming bogus FL_MOVE events if _any_ X event is
+ // received with the mouse pointer outside our windows
+ // https://github.com/fltk/fltk/issues/76
+ if ((event == FL_MOVE) && (win == NULL))
+ return 0;
+
ret = Fl::handle_(event, win);
// This is hackish and the result of the dodgy focus handling in FLTK.
@@ -961,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;
@@ -987,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/EmulateMB.cxx b/vncviewer/EmulateMB.cxx
index 22332745..3cd3fea6 100644
--- a/vncviewer/EmulateMB.cxx
+++ b/vncviewer/EmulateMB.cxx
@@ -205,6 +205,7 @@ void EmulateMB::filterPointerEvent(const rfb::Point& pos, int buttonMask)
int action1, action2;
int lastState;
+ // Just pass through events if the emulate setting is disabled
if (!emulateMiddleButton) {
sendPointerEvent(pos, buttonMask);
return;
@@ -225,16 +226,39 @@ void EmulateMB::filterPointerEvent(const rfb::Point& pos, int buttonMask)
throw rfb::Exception(_("Invalid state for 3 button emulation"));
action1 = stateTab[state][btstate][0];
- if (action1 != 0)
- sendAction(pos, buttonMask, action1);
+
+ if (action1 != 0) {
+ // Some presses are delayed, that means we have to check if that's
+ // the case and send the position corresponding to where the event
+ // first was initiated
+ if ((stateTab[state][4][2] >= 0) && action1 > 0)
+ // We have a timeout state and a button press (a delayed press),
+ // always use the original position when leaving a timeout state,
+ // whether the timeout was triggered or not
+ sendAction(origPos, buttonMask, action1);
+ else
+ // Normal non-delayed event
+ sendAction(pos, buttonMask, action1);
+ }
action2 = stateTab[state][btstate][1];
- if (action2 != 0)
- sendAction(pos, buttonMask, action2);
- if ((action1 == 0) && (action2 == 0)) {
- buttonMask &= ~0x5;
- buttonMask |= emulatedButtonMask;
+ // In our case with the state machine, action2 always occurs during a button
+ // release but if this change we need handle action2 accordingly
+ if (action2 != 0) {
+ if ((stateTab[state][4][2] >= 0) && action2 > 0)
+ sendAction(origPos, buttonMask, action2);
+ else
+ // Normal non-delayed event
+ sendAction(pos, buttonMask, action2);
+ }
+
+ // Still send a pointer move event even if there are no actions.
+ // However if the timer is running then we are supressing _all_
+ // events, even movement. The pointer's actual position will be
+ // sent once the timer fires or is abandoned.
+ if ((action1 == 0) && (action2 == 0) && !timer.isStarted()) {
+ buttonMask = createButtonMask(buttonMask);
sendPointerEvent(pos, buttonMask);
}
@@ -244,14 +268,19 @@ void EmulateMB::filterPointerEvent(const rfb::Point& pos, int buttonMask)
if (lastState != state) {
timer.stop();
- if (stateTab[state][4][2] >= 0)
+ if (stateTab[state][4][2] >= 0) {
+ // We need to save the original position so that
+ // drags start from the correct position
+ origPos = pos;
timer.start(50);
+ }
}
}
bool EmulateMB::handleTimeout(rfb::Timer *t)
{
int action1, action2;
+ int buttonMask;
if (&timer != t)
return false;
@@ -259,15 +288,26 @@ bool EmulateMB::handleTimeout(rfb::Timer *t)
if ((state > 10) || (state < 0))
throw rfb::Exception(_("Invalid state for 3 button emulation"));
+ // Timeout shouldn't trigger when there's no timeout action
assert(stateTab[state][4][2] >= 0);
action1 = stateTab[state][4][0];
if (action1 != 0)
- sendAction(lastPos, lastButtonMask, action1);
+ sendAction(origPos, lastButtonMask, action1);
action2 = stateTab[state][4][1];
if (action2 != 0)
- sendAction(lastPos, lastButtonMask, action2);
+ sendAction(origPos, lastButtonMask, action2);
+
+ buttonMask = lastButtonMask;
+
+ // Pointer move events are not sent when waiting for the timeout.
+ // However, we can't let the position get out of sync so when
+ // the pointer has moved we have to send the latest position here.
+ if (!origPos.equals(lastPos)) {
+ buttonMask = createButtonMask(buttonMask);
+ sendPointerEvent(lastPos, buttonMask);
+ }
state = stateTab[state][4][2];
@@ -283,7 +323,15 @@ void EmulateMB::sendAction(const rfb::Point& pos, int buttonMask, int action)
else
emulatedButtonMask |= (1 << (action - 1));
- buttonMask &= ~0x5;
- buttonMask |= emulatedButtonMask;
+ buttonMask = createButtonMask(buttonMask);
sendPointerEvent(pos, buttonMask);
}
+
+int EmulateMB::createButtonMask(int buttonMask)
+{
+ // Unset left and right buttons in the mask
+ buttonMask &= ~0x5;
+
+ // Set the left and right buttons according to the action
+ return buttonMask |= emulatedButtonMask;
+} \ No newline at end of file
diff --git a/vncviewer/EmulateMB.h b/vncviewer/EmulateMB.h
index e2a70d8e..132f44fe 100644
--- a/vncviewer/EmulateMB.h
+++ b/vncviewer/EmulateMB.h
@@ -36,11 +36,13 @@ protected:
private:
void sendAction(const rfb::Point& pos, int buttonMask, int action);
+ int createButtonMask(int buttonMask);
+
private:
int state;
int emulatedButtonMask;
int lastButtonMask;
- rfb::Point lastPos;
+ rfb::Point lastPos, origPos;
rfb::Timer timer;
};
diff --git a/vncviewer/GestureEvent.h b/vncviewer/GestureEvent.h
new file mode 100644
index 00000000..146680b3
--- /dev/null
+++ b/vncviewer/GestureEvent.h
@@ -0,0 +1,51 @@
+/* Copyright 2019 Aaron Sowry for Cendio AB
+ * Copyright 2020 Samuel Mannehed 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 __GESTUREEVENT_H__
+#define __GESTUREEVENT_H__
+
+enum GestureEventGesture {
+ GestureOneTap,
+ GestureTwoTap,
+ GestureThreeTap,
+ GestureDrag,
+ GestureLongPress,
+ GestureTwoDrag,
+ GesturePinch,
+};
+
+enum GestureEventType {
+ GestureBegin,
+ GestureUpdate,
+ GestureEnd,
+};
+
+// magnitude is used by two gestures:
+// GestureTwoDrag: distance moved since GestureBegin
+// GesturePinch: distance between fingers
+struct GestureEvent {
+ double eventX;
+ double eventY;
+ double magnitudeX;
+ double magnitudeY;
+ GestureEventGesture gesture;
+ GestureEventType type;
+};
+
+#endif // __GESTUREEVENT_H__
diff --git a/vncviewer/GestureHandler.cxx b/vncviewer/GestureHandler.cxx
new file mode 100644
index 00000000..c3cc1531
--- /dev/null
+++ b/vncviewer/GestureHandler.cxx
@@ -0,0 +1,515 @@
+/* Copyright 2019 Aaron Sowry for Cendio AB
+ * Copyright 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 <math.h>
+
+#include <rfb/util.h>
+#include <rfb/LogWriter.h>
+
+#include "GestureHandler.h"
+
+static rfb::LogWriter vlog("GestureHandler");
+
+static const unsigned char GH_NOGESTURE = 0;
+static const unsigned char GH_ONETAP = 1;
+static const unsigned char GH_TWOTAP = 2;
+static const unsigned char GH_THREETAP = 4;
+static const unsigned char GH_DRAG = 8;
+static const unsigned char GH_LONGPRESS = 16;
+static const unsigned char GH_TWODRAG = 32;
+static const unsigned char GH_PINCH = 64;
+
+static const unsigned char GH_INITSTATE = 127;
+
+const unsigned GH_MOVE_THRESHOLD = 50;
+const unsigned GH_ANGLE_THRESHOLD = 90; // Degrees
+
+// Timeout when waiting for gestures (ms)
+const unsigned GH_MULTITOUCH_TIMEOUT = 250;
+
+// Maximum time between press and release for a tap (ms)
+const unsigned GH_TAP_TIMEOUT = 1000;
+
+// Timeout when waiting for longpress (ms)
+const unsigned GH_LONGPRESS_TIMEOUT = 1000;
+
+// Timeout when waiting to decide between PINCH and TWODRAG (ms)
+const unsigned GH_TWOTOUCH_TIMEOUT = 50;
+
+GestureHandler::GestureHandler() :
+ state(GH_INITSTATE), waitingRelease(false),
+ longpressTimer(this), twoTouchTimer(this)
+{
+}
+
+GestureHandler::~GestureHandler()
+{
+}
+
+void GestureHandler::handleTouchBegin(int id, double x, double y)
+{
+ GHTouch ght;
+
+ // Ignore any new touches if there is already an active gesture,
+ // or we're in a cleanup state
+ if (hasDetectedGesture() || (state == GH_NOGESTURE)) {
+ ignored.insert(id);
+ return;
+ }
+
+ // Did it take too long between touches that we should no longer
+ // consider this a single gesture?
+ if ((tracked.size() > 0) &&
+ (rfb::msSince(&tracked.begin()->second.started) > GH_MULTITOUCH_TIMEOUT)) {
+ state = GH_NOGESTURE;
+ ignored.insert(id);
+ return;
+ }
+
+ // If we're waiting for fingers to release then we should no longer
+ // recognize new touches
+ if (waitingRelease) {
+ state = GH_NOGESTURE;
+ ignored.insert(id);
+ return;
+ }
+
+ gettimeofday(&ght.started, NULL);
+ ght.active = true;
+ ght.lastX = ght.firstX = x;
+ ght.lastY = ght.firstY = y;
+ ght.angle = 0;
+
+ tracked[id] = ght;
+
+ switch (tracked.size()) {
+ case 1:
+ longpressTimer.start(GH_LONGPRESS_TIMEOUT);
+ break;
+
+ case 2:
+ state &= ~(GH_ONETAP | GH_DRAG | GH_LONGPRESS);
+ longpressTimer.stop();
+ break;
+
+ case 3:
+ state &= ~(GH_TWOTAP | GH_TWODRAG | GH_PINCH);
+ break;
+
+ default:
+ state = GH_NOGESTURE;
+ }
+}
+
+void GestureHandler::handleTouchUpdate(int id, double x, double y)
+{
+ GHTouch *touch, *prevTouch;
+ double deltaX, deltaY, prevDeltaMove;
+ unsigned deltaAngle;
+
+ // If this is an update for a touch we're not tracking, ignore it
+ if (tracked.count(id) == 0)
+ return;
+
+ touch = &tracked[id];
+
+ // Update the touches last position with the event coordinates
+ touch->lastX = x;
+ touch->lastY = y;
+
+ deltaX = x - touch->firstX;
+ deltaY = y - touch->firstY;
+
+ // Update angle when the touch has moved
+ if ((touch->firstX != touch->lastX) ||
+ (touch->firstY != touch->lastY))
+ touch->angle = atan2(deltaY, deltaX) * 180 / M_PI;
+
+ if (!hasDetectedGesture()) {
+ // Ignore moves smaller than the minimum threshold
+ if (hypot(deltaX, deltaY) < GH_MOVE_THRESHOLD)
+ return;
+
+ // Can't be a tap or long press as we've seen movement
+ state &= ~(GH_ONETAP | GH_TWOTAP | GH_THREETAP | GH_LONGPRESS);
+ longpressTimer.stop();
+
+ if (tracked.size() != 1)
+ state &= ~(GH_DRAG);
+ if (tracked.size() != 2)
+ state &= ~(GH_TWODRAG | GH_PINCH);
+
+ // We need to figure out which of our different two touch gestures
+ // this might be
+ if (tracked.size() == 2) {
+
+ // The other touch can be first or last in tracked
+ // depending on which event came first
+ prevTouch = &tracked.rbegin()->second;
+ if (prevTouch == touch)
+ prevTouch = &tracked.begin()->second;
+
+ // How far the previous touch point has moved since start
+ prevDeltaMove = hypot(prevTouch->firstX - prevTouch->lastX,
+ prevTouch->firstY - prevTouch->lastY);
+
+ // We know that the current touch moved far enough,
+ // but unless both touches moved further than their
+ // threshold we don't want to disqualify any gestures
+ if (prevDeltaMove > GH_MOVE_THRESHOLD) {
+
+ // The angle difference between the direction of the touch points
+ deltaAngle = fabs(touch->angle - prevTouch->angle);
+ deltaAngle = fabs(((deltaAngle + 180) % 360) - 180);
+
+ // PINCH or TWODRAG can be eliminated depending on the angle
+ if (deltaAngle > GH_ANGLE_THRESHOLD)
+ state &= ~GH_TWODRAG;
+ else
+ state &= ~GH_PINCH;
+
+ if (twoTouchTimer.isStarted())
+ twoTouchTimer.stop();
+
+ } else if (!twoTouchTimer.isStarted()) {
+ // We can't determine the gesture right now, let's
+ // wait and see if more events are on their way
+ twoTouchTimer.start(GH_TWOTOUCH_TIMEOUT);
+ }
+ }
+
+ if (!hasDetectedGesture())
+ return;
+
+ pushEvent(GestureBegin);
+ }
+
+ pushEvent(GestureUpdate);
+}
+
+void GestureHandler::handleTouchEnd(int id)
+{
+ std::map<int, GHTouch>::const_iterator iter;
+
+ // Check if this is an ignored touch
+ if (ignored.count(id)) {
+ ignored.erase(id);
+ if (ignored.empty() && tracked.empty()) {
+ state = GH_INITSTATE;
+ waitingRelease = false;
+ }
+ return;
+ }
+
+ // We got a TouchEnd before the timer triggered,
+ // this cannot result in a gesture anymore.
+ if (!hasDetectedGesture() && twoTouchTimer.isStarted()) {
+ twoTouchTimer.stop();
+ state = GH_NOGESTURE;
+ }
+
+ // Some gestures don't trigger until a touch is released
+ if (!hasDetectedGesture()) {
+ // Can't be a gesture that relies on movement
+ state &= ~(GH_DRAG | GH_TWODRAG | GH_PINCH);
+ // Or something that relies on more time
+ state &= ~GH_LONGPRESS;
+ longpressTimer.stop();
+
+ if (!waitingRelease) {
+ gettimeofday(&releaseStart, NULL);
+ waitingRelease = true;
+
+ // Can't be a tap that requires more touches than we current have
+ switch (tracked.size()) {
+ case 1:
+ state &= ~(GH_TWOTAP | GH_THREETAP);
+ break;
+
+ case 2:
+ state &= ~(GH_ONETAP | GH_THREETAP);
+ break;
+ }
+ }
+ }
+
+ // Waiting for all touches to release? (i.e. some tap)
+ if (waitingRelease) {
+ // Were all touches released at roughly the same time?
+ if (rfb::msSince(&releaseStart) > GH_MULTITOUCH_TIMEOUT)
+ state = GH_NOGESTURE;
+
+ // Did too long time pass between press and release?
+ for (iter = tracked.begin(); iter != tracked.end(); ++iter) {
+ if (rfb::msSince(&iter->second.started) > GH_TAP_TIMEOUT) {
+ state = GH_NOGESTURE;
+ break;
+ }
+ }
+
+ tracked[id].active = false;
+
+ // Are we still waiting for more releases?
+ if (hasDetectedGesture()) {
+ pushEvent(GestureBegin);
+ } else {
+ // Have we reached a dead end?
+ if (state != GH_NOGESTURE)
+ return;
+ }
+ }
+
+ if (hasDetectedGesture())
+ pushEvent(GestureEnd);
+
+ // Ignore any remaining touches until they are ended
+ for (iter = tracked.begin(); iter != tracked.end(); ++iter) {
+ if (iter->second.active)
+ ignored.insert(iter->first);
+ }
+ tracked.clear();
+
+ state = GH_NOGESTURE;
+
+ ignored.erase(id);
+ if (ignored.empty()) {
+ state = GH_INITSTATE;
+ waitingRelease = false;
+ }
+}
+
+bool GestureHandler::hasDetectedGesture()
+{
+ if (state == GH_NOGESTURE)
+ return false;
+ // Check to see if the bitmask value is a power of 2
+ // (i.e. only one bit set). If it is, we have a state.
+ if (state & (state - 1))
+ return false;
+
+ // For taps we also need to have all touches released
+ // before we've fully detected the gesture
+ if (state & (GH_ONETAP | GH_TWOTAP | GH_THREETAP)) {
+ std::map<int, GHTouch>::const_iterator iter;
+
+ // Any touch still active/pressed?
+ for (iter = tracked.begin(); iter != tracked.end(); ++iter) {
+ if (iter->second.active)
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool GestureHandler::handleTimeout(rfb::Timer* t)
+{
+ if (t == &longpressTimer)
+ longpressTimeout();
+ else if (t == &twoTouchTimer)
+ twoTouchTimeout();
+
+ return false;
+}
+
+void GestureHandler::longpressTimeout()
+{
+ assert(!hasDetectedGesture());
+
+ state = GH_LONGPRESS;
+ pushEvent(GestureBegin);
+}
+
+void GestureHandler::twoTouchTimeout()
+{
+ double avgMoveH, avgMoveV, fdx, fdy, ldx, ldy, deltaTouchDistance;
+
+ assert(!tracked.empty());
+
+ // How far each touch point has moved since start
+ getAverageMovement(&avgMoveH, &avgMoveV);
+ avgMoveH = fabs(avgMoveH);
+ avgMoveV = fabs(avgMoveV);
+
+ // The difference in the distance between where
+ // the touch points started and where they are now
+ getAverageDistance(&fdx, &fdy, &ldx, &ldy);
+ deltaTouchDistance = fabs(hypot(fdx, fdy) - hypot(ldx, ldy));
+
+ if ((avgMoveV < deltaTouchDistance) &&
+ (avgMoveH < deltaTouchDistance))
+ state = GH_PINCH;
+ else
+ state = GH_TWODRAG;
+
+ pushEvent(GestureBegin);
+ pushEvent(GestureUpdate);
+}
+
+void GestureHandler::pushEvent(GestureEventType t)
+{
+ GestureEvent gev;
+ double avgX, avgY;
+
+ gev.type = t;
+ gev.gesture = stateToGesture(state);
+
+ // For most gesture events the current (average) position is the
+ // most useful
+ getPosition(NULL, NULL, &avgX, &avgY);
+
+ // However we have a slight distance to detect gestures, so for the
+ // first gesture event we want to use the first positions we saw
+ if (t == GestureBegin)
+ getPosition(&avgX, &avgY, NULL, NULL);
+
+ // For these gestures, we always want the event coordinates
+ // to be where the gesture began, not the current touch location.
+ switch (state) {
+ case GH_TWODRAG:
+ case GH_PINCH:
+ getPosition(&avgX, &avgY, NULL, NULL);
+ break;
+ }
+
+ gev.eventX = avgX;
+ gev.eventY = avgY;
+
+ // Some gestures also have a magnitude
+ if (state == GH_PINCH) {
+ if (t == GestureBegin)
+ getAverageDistance(&gev.magnitudeX, &gev.magnitudeY,
+ NULL, NULL);
+ else
+ getAverageDistance(NULL, NULL,
+ &gev.magnitudeX, &gev.magnitudeY);
+ } else if (state == GH_TWODRAG) {
+ if (t == GestureBegin)
+ gev.magnitudeX = gev.magnitudeY = 0;
+ else
+ getAverageMovement(&gev.magnitudeX, &gev.magnitudeY);
+ }
+
+ handleGestureEvent(gev);
+}
+
+GestureEventGesture GestureHandler::stateToGesture(unsigned char state)
+{
+ switch (state) {
+ case GH_ONETAP:
+ return GestureOneTap;
+ case GH_TWOTAP:
+ return GestureTwoTap;
+ case GH_THREETAP:
+ return GestureThreeTap;
+ case GH_DRAG:
+ return GestureDrag;
+ case GH_LONGPRESS:
+ return GestureLongPress;
+ case GH_TWODRAG:
+ return GestureTwoDrag;
+ case GH_PINCH:
+ return GesturePinch;
+ }
+
+ assert(false);
+
+ return (GestureEventGesture)0;
+}
+
+void GestureHandler::getPosition(double *firstX, double *firstY,
+ double *lastX, double *lastY)
+{
+ size_t size;
+ double fx = 0, fy = 0, lx = 0, ly = 0;
+
+ assert(!tracked.empty());
+
+ size = tracked.size();
+
+ std::map<int, GHTouch>::const_iterator iter;
+ for (iter = tracked.begin(); iter != tracked.end(); ++iter) {
+ fx += iter->second.firstX;
+ fy += iter->second.firstY;
+ lx += iter->second.lastX;
+ ly += iter->second.lastY;
+ }
+
+ if (firstX)
+ *firstX = fx / size;
+ if (firstY)
+ *firstY = fy / size;
+ if (lastX)
+ *lastX = lx / size;
+ if (lastY)
+ *lastY = ly / size;
+}
+
+void GestureHandler::getAverageMovement(double *h, double *v)
+{
+ double totalH, totalV;
+ size_t size;
+
+ assert(!tracked.empty());
+
+ totalH = totalV = 0;
+ size = tracked.size();
+
+ std::map<int, GHTouch>::const_iterator iter;
+ for (iter = tracked.begin(); iter != tracked.end(); ++iter) {
+ totalH += iter->second.lastX - iter->second.firstX;
+ totalV += iter->second.lastY - iter->second.firstY;
+ }
+
+ if (h)
+ *h = totalH / size;
+ if (v)
+ *v = totalV / size;
+}
+
+void GestureHandler::getAverageDistance(double *firstX, double *firstY,
+ double *lastX, double *lastY)
+{
+ double dx, dy;
+
+ assert(!tracked.empty());
+
+ // Distance between the first and last tracked touches
+
+ dx = fabs(tracked.rbegin()->second.firstX - tracked.begin()->second.firstX);
+ dy = fabs(tracked.rbegin()->second.firstY - tracked.begin()->second.firstY);
+
+ if (firstX)
+ *firstX = dx;
+ if (firstY)
+ *firstY = dy;
+
+ dx = fabs(tracked.rbegin()->second.lastX - tracked.begin()->second.lastX);
+ dy = fabs(tracked.rbegin()->second.lastY - tracked.begin()->second.lastY);
+
+ if (lastX)
+ *lastX = dx;
+ if (lastY)
+ *lastY = dy;
+}
diff --git a/vncviewer/GestureHandler.h b/vncviewer/GestureHandler.h
new file mode 100644
index 00000000..372b7865
--- /dev/null
+++ b/vncviewer/GestureHandler.h
@@ -0,0 +1,81 @@
+/* Copyright 2019 Aaron Sowry for Cendio AB
+ * Copyright 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 __GESTUREHANDLER_H__
+#define __GESTUREHANDLER_H__
+
+#include <set>
+#include <map>
+
+#include <rfb/Timer.h>
+
+#include "GestureEvent.h"
+
+class GestureHandler : public rfb::Timer::Callback {
+ public:
+ GestureHandler();
+ virtual ~GestureHandler();
+
+ void handleTouchBegin(int id, double x, double y);
+ void handleTouchUpdate(int id, double x, double y);
+ void handleTouchEnd(int id);
+
+ protected:
+ virtual void handleGestureEvent(const GestureEvent& event) = 0;
+
+ private:
+ bool hasDetectedGesture();
+
+ virtual bool handleTimeout(rfb::Timer* t);
+ void longpressTimeout();
+ void twoTouchTimeout();
+
+ void pushEvent(GestureEventType t);
+ GestureEventGesture stateToGesture(unsigned char state);
+
+ void getPosition(double *firstX, double *firstY,
+ double *lastX, double *lastY);
+ void getAverageMovement(double *h, double *v);
+ void getAverageDistance(double *firstX, double *firstY,
+ double *lastX, double *lastY);
+
+ private:
+ struct GHTouch {
+ struct timeval started;
+ bool active;
+ double firstX;
+ double firstY;
+ double lastX;
+ double lastY;
+ int angle;
+ };
+
+ unsigned char state;
+
+ std::map<int, GHTouch> tracked;
+ std::set<int> ignored;
+
+ bool waitingRelease;
+ struct timeval releaseStart;
+
+ rfb::Timer longpressTimer;
+ rfb::Timer twoTouchTimer;
+};
+
+#endif // __GESTUREHANDLER_H__
diff --git a/vncviewer/Viewport.cxx b/vncviewer/Viewport.cxx
index b0b2a15a..84777be1 100644
--- a/vncviewer/Viewport.cxx
+++ b/vncviewer/Viewport.cxx
@@ -939,7 +939,22 @@ int Viewport::handleSystemEvent(void *event, void *data)
#if defined(WIN32)
MSG *msg = (MSG*)event;
- if ((msg->message == WM_KEYDOWN) || (msg->message == WM_SYSKEYDOWN)) {
+ if ((msg->message == WM_MOUSEMOVE) ||
+ (msg->message == WM_LBUTTONDOWN) ||
+ (msg->message == WM_LBUTTONUP) ||
+ (msg->message == WM_RBUTTONDOWN) ||
+ (msg->message == WM_RBUTTONUP) ||
+ (msg->message == WM_MBUTTONDOWN) ||
+ (msg->message == WM_MBUTTONUP) ||
+ (msg->message == WM_MOUSEWHEEL) ||
+ (msg->message == WM_MOUSEHWHEEL)) {
+ // We can't get a mouse event in the middle of an AltGr sequence, so
+ // abort that detection
+ if (self->altGrArmed)
+ self->resolveAltGrDetection(false);
+
+ return 0; // We didn't really consume the mouse event
+ } else if ((msg->message == WM_KEYDOWN) || (msg->message == WM_SYSKEYDOWN)) {
UINT vKey;
bool isExtended;
int keyCode;
@@ -963,16 +978,11 @@ int Viewport::handleSystemEvent(void *event, void *data)
// by seeing the two key events directly after each other with a very
// short time between them (<50ms) and supress the Ctrl event.
if (self->altGrArmed) {
- self->altGrArmed = false;
- Fl::remove_timeout(handleAltGrTimeout);
-
- if (isExtended && (keyCode == 0x38) && (vKey == VK_MENU) &&
- ((msg->time - self->altGrCtrlTime) < 50)) {
- // Alt seen, so this is an AltGr sequence
- } else {
- // Not Alt, so fire the queued up Ctrl event
- self->handleKeyPress(0x1d, XK_Control_L);
- }
+ bool altPressed = isExtended &&
+ (keyCode == 0x38) &&
+ (vKey == VK_MENU) &&
+ ((msg->time - self->altGrCtrlTime) < 50);
+ self->resolveAltGrDetection(altPressed);
}
if (keyCode == SCAN_FAKE) {
@@ -1069,11 +1079,8 @@ int Viewport::handleSystemEvent(void *event, void *data)
// We can't get a release in the middle of an AltGr sequence, so
// abort that detection
- if (self->altGrArmed) {
- self->altGrArmed = false;
- Fl::remove_timeout(handleAltGrTimeout);
- self->handleKeyPress(0x1d, XK_Control_L);
- }
+ if (self->altGrArmed)
+ self->resolveAltGrDetection(false);
if (keyCode == SCAN_FAKE) {
vlog.debug("Ignoring fake key release (virtual key 0x%02x)", vKey);
@@ -1197,6 +1204,15 @@ void Viewport::handleAltGrTimeout(void *data)
self->altGrArmed = false;
self->handleKeyPress(0x1d, XK_Control_L);
}
+
+void Viewport::resolveAltGrDetection(bool isAltGrSequence)
+{
+ altGrArmed = false;
+ Fl::remove_timeout(handleAltGrTimeout);
+ // when it's not an AltGr sequence we can't supress the Ctrl anymore
+ if (!isAltGrSequence)
+ handleKeyPress(0x1d, XK_Control_L);
+}
#endif
void Viewport::initContextMenu()
diff --git a/vncviewer/Viewport.h b/vncviewer/Viewport.h
index 306beee9..19def92c 100644
--- a/vncviewer/Viewport.h
+++ b/vncviewer/Viewport.h
@@ -91,6 +91,7 @@ private:
#ifdef WIN32
static void handleAltGrTimeout(void *data);
+ void resolveAltGrDetection(bool isAltGrSequence);
#endif
void pushLEDState();
diff --git a/vncviewer/Win32TouchHandler.cxx b/vncviewer/Win32TouchHandler.cxx
new file mode 100644
index 00000000..dccf10a8
--- /dev/null
+++ b/vncviewer/Win32TouchHandler.cxx
@@ -0,0 +1,442 @@
+/* Copyright 2020 Samuel Mannehed 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 <math.h>
+
+#define XK_MISCELLANY
+#include <rfb/keysymdef.h>
+#include <rfb/Exception.h>
+#include <rfb/LogWriter.h>
+
+#include "i18n.h"
+#include "Win32TouchHandler.h"
+
+static rfb::LogWriter vlog("Win32TouchHandler");
+
+static const DWORD MOUSEMOVE_FLAGS = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE |
+ MOUSEEVENTF_VIRTUALDESK;
+
+static const unsigned SINGLE_PAN_THRESHOLD = 50;
+
+Win32TouchHandler::Win32TouchHandler(HWND hWnd) :
+ hWnd(hWnd), gesturesConfigured(false), gestureActive(false),
+ ignoringGesture(false), fakeButtonMask(0)
+{
+ // If window is registered as touch we can not receive gestures,
+ // this should not happen
+ if (IsTouchWindow(hWnd, NULL))
+ throw rfb::Exception(_("Window is registered for touch instead of gestures"));
+
+ // We will not receive any touch/gesture events if this service
+ // isn't running - Logging is enough
+ if (!GetSystemMetrics(SM_DIGITIZER))
+ vlog.debug("The 'Tablet PC Input' service is required for touch");
+
+ // When we have less than two touch points we won't receive all
+ // gesture events - Logging is enough
+ int supportedTouches = GetSystemMetrics(SM_MAXIMUMTOUCHES);
+ if (supportedTouches < 2)
+ vlog.debug("Two touch points required, system currently supports: %d",
+ supportedTouches);
+}
+
+bool Win32TouchHandler::processEvent(UINT Msg, WPARAM wParam, LPARAM lParam)
+{
+ GESTUREINFO gi;
+
+ DWORD panWant = GC_PAN_WITH_SINGLE_FINGER_VERTICALLY |
+ GC_PAN_WITH_SINGLE_FINGER_HORIZONTALLY |
+ GC_PAN;
+ DWORD panBlock = GC_PAN_WITH_INERTIA | GC_PAN_WITH_GUTTER;
+
+ GESTURECONFIG gc[] = {{GID_ZOOM, GC_ZOOM, 0},
+ {GID_PAN, panWant, panBlock},
+ {GID_TWOFINGERTAP, GC_TWOFINGERTAP, 0}};
+
+ switch(Msg) {
+ case WM_GESTURENOTIFY:
+ if (gesturesConfigured)
+ return false;
+
+ if (!SetGestureConfig(hWnd, 0, 3, gc, sizeof(GESTURECONFIG))) {
+ vlog.error(_("Failed to set gesture configuration (error 0x%x)"),
+ (int)GetLastError());
+ }
+ gesturesConfigured = true;
+ // Windows expects all handler functions to always
+ // pass this message on, and not consume it
+ return false;
+ case WM_GESTURE:
+ ZeroMemory(&gi, sizeof(GESTUREINFO));
+ gi.cbSize = sizeof(GESTUREINFO);
+
+ if (!GetGestureInfo((HGESTUREINFO)lParam, &gi)) {
+ vlog.error(_("Failed to get gesture information (error 0x%x)"),
+ (int)GetLastError());
+ return true;
+ }
+
+ handleWin32GestureEvent(gi);
+
+ CloseGestureInfoHandle((HGESTUREINFO)lParam);
+ return true;
+ }
+
+ return false;
+}
+
+void Win32TouchHandler::handleWin32GestureEvent(GESTUREINFO gi)
+{
+ GestureEvent gev;
+ POINT pos;
+
+ if (gi.dwID == GID_BEGIN) {
+ // FLTK gets very confused if the cursor position is outside
+ // of the window when getting mouse events, so we start by
+ // moving the cursor to something proper.
+ // FIXME: Only do this when necessary?
+ // FIXME: There is some odd delay before Windows fully updates
+ // the state of the cursor position. By doing it here in
+ // GID_BEGIN we hope to do it early enough that we don't
+ // get any odd effects.
+ // FIXME: GF_BEGIN position can differ from GID_BEGIN pos.
+
+ SetCursorPos(gi.ptsLocation.x, gi.ptsLocation.y);
+ return;
+ } else if (gi.dwID == GID_END) {
+ gestureActive = false;
+ ignoringGesture = false;
+ return;
+ }
+
+ // The GID_BEGIN msg means that no fingers were previously touching,
+ // and a completely new set of gestures is beginning.
+ // The GF_BEGIN flag means a new type of gesture was detected. This
+ // flag can be set on a msg when changing between gestures within
+ // one set of touches.
+ //
+ // We don't support dynamically changing between gestures
+ // without lifting the finger(s).
+ if ((gi.dwFlags & GF_BEGIN) && gestureActive)
+ ignoringGesture = true;
+ if (ignoringGesture)
+ return;
+
+ if (gi.dwFlags & GF_BEGIN) {
+ gev.type = GestureBegin;
+ } else if (gi.dwFlags & GF_END) {
+ gev.type = GestureEnd;
+ } else {
+ gev.type = GestureUpdate;
+ }
+
+ // Convert to relative coordinates
+ pos.x = gi.ptsLocation.x;
+ pos.y = gi.ptsLocation.y;
+ ScreenToClient(gi.hwndTarget, &pos);
+ gev.eventX = pos.x;
+ gev.eventY = pos.y;
+
+ switch(gi.dwID) {
+
+ case GID_ZOOM:
+ gev.gesture = GesturePinch;
+ if (gi.dwFlags & GF_BEGIN) {
+ gestureStart.x = pos.x;
+ gestureStart.y = pos.y;
+ } else {
+ gev.eventX = gestureStart.x;
+ gev.eventY = gestureStart.y;
+ }
+ gev.magnitudeX = gi.ullArguments;
+ gev.magnitudeY = 0;
+ break;
+
+ case GID_PAN:
+ if (isSinglePan(gi)) {
+ if (gi.dwFlags & GF_BEGIN) {
+ gestureStart.x = pos.x;
+ gestureStart.y = pos.y;
+ startedSinglePan = false;
+
+ }
+
+ // FIXME: Add support for sending a OneFingerTap gesture here.
+ // When the movement was very small and we get a GF_END
+ // within a short time we should consider it a tap.
+
+ if (!startedSinglePan &&
+ ((unsigned)abs(gestureStart.x - pos.x) < SINGLE_PAN_THRESHOLD) &&
+ ((unsigned)abs(gestureStart.y - pos.y) < SINGLE_PAN_THRESHOLD))
+ return;
+
+ // Here we know we got a single pan!
+
+ // Change the first GestureUpdate to GestureBegin
+ // after we passed the threshold
+ if (!startedSinglePan) {
+ startedSinglePan = true;
+ gev.type = GestureBegin;
+ gev.eventX = gestureStart.x;
+ gev.eventY = gestureStart.y;
+ }
+
+ gev.gesture = GestureDrag;
+
+ } else {
+ if (gi.dwFlags & GF_BEGIN) {
+ gestureStart.x = pos.x;
+ gestureStart.y = pos.y;
+ gev.magnitudeX = 0;
+ gev.magnitudeY = 0;
+ } else {
+ gev.eventX = gestureStart.x;
+ gev.eventY = gestureStart.y;
+ gev.magnitudeX = pos.x - gestureStart.x;
+ gev.magnitudeY = pos.y - gestureStart.y;
+ }
+
+ gev.gesture = GestureTwoDrag;
+ }
+ break;
+
+ case GID_TWOFINGERTAP:
+ gev.gesture = GestureTwoTap;
+ break;
+
+ }
+
+ gestureActive = true;
+
+ BaseTouchHandler::handleGestureEvent(gev);
+
+ // Since we have a threshold for GestureDrag we need to generate
+ // a second event right away with the current position
+ if ((gev.type == GestureBegin) && (gev.gesture == GestureDrag)) {
+ gev.type = GestureUpdate;
+ gev.eventX = pos.x;
+ gev.eventY = pos.y;
+ BaseTouchHandler::handleGestureEvent(gev);
+ }
+
+ // FLTK tends to reset the cursor to the real position so we
+ // need to make sure that we update that position
+ if (gev.type == GestureEnd) {
+ POINT expectedPos;
+ POINT currentPos;
+
+ expectedPos = lastFakeMotionPos;
+ ClientToScreen(hWnd, &expectedPos);
+ GetCursorPos(&currentPos);
+
+ if ((expectedPos.x != currentPos.x) ||
+ (expectedPos.y != currentPos.y))
+ SetCursorPos(expectedPos.x, expectedPos.y);
+ }
+}
+
+bool Win32TouchHandler::isSinglePan(GESTUREINFO gi)
+{
+ // To differentiate between a single and a double pan we can look
+ // at ullArguments. This shows the distance between the touch points,
+ // but in the case of single pan, it seems to show the monitor's
+ // origin value (this is not documented by microsoft). This origin
+ // value seems to be relative to the screen's position in a multi
+ // monitor setup. For example if the touch monitor is secondary and
+ // positioned to the left of the primary, the origin is negative.
+ //
+ // To use this we need to get the monitor's origin value and check
+ // if it is the same as ullArguments. If they match, we have a
+ // single pan.
+
+ POINT coordinates;
+ HMONITOR monitorHandler;
+ MONITORINFO mi;
+ LONG lowestX;
+
+ // Find the monitor with the touch event
+ coordinates.x = gi.ptsLocation.x;
+ coordinates.y = gi.ptsLocation.y;
+ monitorHandler = MonitorFromPoint(coordinates,
+ MONITOR_DEFAULTTOPRIMARY);
+
+ // Find the monitor's origin
+ ZeroMemory(&mi, sizeof(MONITORINFO));
+ mi.cbSize = sizeof(MONITORINFO);
+ GetMonitorInfo(monitorHandler, &mi);
+ lowestX = mi.rcMonitor.left;
+
+ return lowestX == (LONG)gi.ullArguments;
+}
+
+void Win32TouchHandler::fakeMotionEvent(const GestureEvent origEvent)
+{
+ UINT Msg = WM_MOUSEMOVE;
+ WPARAM wParam = MAKEWPARAM(fakeButtonMask, 0);
+ LPARAM lParam = MAKELPARAM(origEvent.eventX, origEvent.eventY);
+
+ pushFakeEvent(Msg, wParam, lParam);
+ lastFakeMotionPos.x = origEvent.eventX;
+ lastFakeMotionPos.y = origEvent.eventY;
+}
+
+void Win32TouchHandler::fakeButtonEvent(bool press, int button,
+ const GestureEvent origEvent)
+{
+ UINT Msg;
+ WPARAM wParam;
+ LPARAM lParam;
+ int delta;
+
+ switch (button) {
+
+ case 1: // left mousebutton
+ if (press) {
+ Msg = WM_LBUTTONDOWN;
+ fakeButtonMask |= MK_LBUTTON;
+ } else {
+ Msg = WM_LBUTTONUP;
+ fakeButtonMask &= ~MK_LBUTTON;
+ }
+ break;
+ case 2: // middle mousebutton
+ if (press) {
+ Msg = WM_MBUTTONDOWN;
+ fakeButtonMask |= MK_MBUTTON;
+ } else {
+ Msg = WM_MBUTTONUP;
+ fakeButtonMask &= ~MK_MBUTTON;
+ }
+ break;
+ case 3: // right mousebutton
+ if (press) {
+ Msg = WM_RBUTTONDOWN;
+ fakeButtonMask |= MK_RBUTTON;
+ } else {
+ Msg = WM_RBUTTONUP;
+ fakeButtonMask &= ~MK_RBUTTON;
+ }
+ break;
+
+ case 4: // scroll up
+ Msg = WM_MOUSEWHEEL;
+ delta = WHEEL_DELTA;
+ break;
+ case 5: // scroll down
+ Msg = WM_MOUSEWHEEL;
+ delta = -WHEEL_DELTA;
+ break;
+ case 6: // scroll left
+ Msg = WM_MOUSEHWHEEL;
+ delta = -WHEEL_DELTA;
+ break;
+ case 7: // scroll right
+ Msg = WM_MOUSEHWHEEL;
+ delta = WHEEL_DELTA;
+ break;
+
+ default:
+ vlog.error(_("Invalid mouse button %d, must be a number between 1 and 7."),
+ button);
+ return;
+ }
+
+ if (1 <= button && button <= 3) {
+ wParam = MAKEWPARAM(fakeButtonMask, 0);
+
+ // Regular mouse events expect client coordinates
+ lParam = MAKELPARAM(origEvent.eventX, origEvent.eventY);
+ } else {
+ POINT pos;
+
+ // Only act on wheel press, not on release
+ if (!press)
+ return;
+
+ wParam = MAKEWPARAM(fakeButtonMask, delta);
+
+ // Wheel events require screen coordinates
+ pos.x = (LONG)origEvent.eventX;
+ pos.y = (LONG)origEvent.eventY;
+
+ ClientToScreen(hWnd, &pos);
+ lParam = MAKELPARAM(pos.x, pos.y);
+ }
+
+ pushFakeEvent(Msg, wParam, lParam);
+}
+
+void Win32TouchHandler::fakeKeyEvent(bool press, int keysym,
+ const GestureEvent origEvent)
+{
+ UINT Msg = press ? WM_KEYDOWN : WM_KEYUP;
+ WPARAM wParam;
+ LPARAM lParam;
+ int vKey;
+ int scanCode;
+ int previousKeyState = press ? 0 : 1;
+ int transitionState = press ? 0 : 1;
+
+ switch(keysym) {
+
+ case XK_Control_L:
+ vKey = VK_CONTROL;
+ scanCode = 0x1d;
+ if (press)
+ fakeButtonMask |= MK_CONTROL;
+ else
+ fakeButtonMask &= ~MK_CONTROL;
+ break;
+
+ // BaseTouchHandler will currently not send SHIFT but we keep it for
+ // completeness sake. This way we have coverage for all wParam's MK_-bits.
+ case XK_Shift_L:
+ vKey = VK_SHIFT;
+ scanCode = 0x2a;
+ if (press)
+ fakeButtonMask |= MK_SHIFT;
+ else
+ fakeButtonMask &= ~MK_SHIFT;
+ break;
+
+ default:
+ //FIXME: consider adding generic handling
+ vlog.error(_("Unhandled key 0x%x - can't generate keyboard event."),
+ keysym);
+ return;
+ }
+
+ wParam = MAKEWPARAM(vKey, 0);
+
+ scanCode <<= 0;
+ previousKeyState <<= 14;
+ transitionState <<= 15;
+ lParam = MAKELPARAM(1, // RepeatCount
+ (scanCode | previousKeyState | transitionState));
+
+ pushFakeEvent(Msg, wParam, lParam);
+}
+
+void Win32TouchHandler::pushFakeEvent(UINT Msg, WPARAM wParam, LPARAM lParam)
+{
+ PostMessage(hWnd, Msg, wParam, lParam);
+}
diff --git a/vncviewer/Win32TouchHandler.h b/vncviewer/Win32TouchHandler.h
new file mode 100644
index 00000000..05039c88
--- /dev/null
+++ b/vncviewer/Win32TouchHandler.h
@@ -0,0 +1,60 @@
+/* Copyright 2020 Samuel Mannehed 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 __WIN32TOUCHHANDLER_H__
+#define __WIN32TOUCHHANDLER_H__
+
+#include <windows.h>
+
+#include "BaseTouchHandler.h"
+#include "GestureEvent.h"
+
+class Win32TouchHandler: public BaseTouchHandler {
+ public:
+ Win32TouchHandler(HWND hWnd);
+
+ bool processEvent(UINT Msg, WPARAM wParam, LPARAM lParam);
+
+ private:
+ void handleWin32GestureEvent(GESTUREINFO gi);
+ bool isSinglePan(GESTUREINFO gi);
+
+ protected:
+ virtual void fakeMotionEvent(const GestureEvent origEvent);
+ virtual void fakeButtonEvent(bool press, int button,
+ const GestureEvent origEvent);
+ virtual void fakeKeyEvent(bool press, int keycode,
+ const GestureEvent origEvent);
+ private:
+ void pushFakeEvent(UINT Msg, WPARAM wParam, LPARAM lParam);
+
+ private:
+ HWND hWnd;
+
+ bool gesturesConfigured;
+ bool startedSinglePan;
+ POINT gestureStart;
+
+ bool gestureActive;
+ bool ignoringGesture;
+
+ int fakeButtonMask;
+ POINT lastFakeMotionPos;
+};
+
+#endif // __WIN32TOUCHHANDLER_H__
diff --git a/vncviewer/XInputTouchHandler.cxx b/vncviewer/XInputTouchHandler.cxx
new file mode 100644
index 00000000..6203bda5
--- /dev/null
+++ b/vncviewer/XInputTouchHandler.cxx
@@ -0,0 +1,461 @@
+/* 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 <X11/XKBlib.h>
+
+#include <FL/x.H>
+
+#ifndef XK_MISCELLANY
+#define XK_MISCELLANY
+#include <rfb/keysymdef.h>
+#endif
+#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)
+{
+ 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%08lx"), wnd);
+ else
+ vlog.error(_("Window 0x%08lx 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%08lx 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:
+ // 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);
+ }
+
+ handleTouchBegin(devev->detail, devev->event_x, devev->event_y);
+ break;
+ case XI_TouchUpdate:
+ handleTouchUpdate(devev->detail, devev->event_x, devev->event_y);
+ break;
+ case XI_TouchEnd:
+ handleTouchEnd(devev->detail);
+ 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);
+
+ 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;
+
+ // Apply the fake mask before pushing so it will be in sync
+ fakeEvent.xbutton.state |= fakeStateMask;
+ preparePointerEvent(&fakeEvent, origEvent);
+
+ pushFakeEvent(&fakeEvent);
+}
+
+void XInputTouchHandler::preparePointerEvent(XEvent* dst, const GestureEvent src)
+{
+ Window root, child;
+ int rootX, rootY;
+ XkbStateRec state;
+
+ // We don't have a real event to steal things from, so we'll have
+ // to fake these events based on the current state of things
+
+ root = XDefaultRootWindow(fl_display);
+ XTranslateCoordinates(fl_display, wnd, root,
+ src.eventX,
+ src.eventY,
+ &rootX, &rootY, &child);
+ XkbGetState(fl_display, XkbUseCoreKbd, &state);
+
+ // XButtonEvent and XMotionEvent are almost identical, so we
+ // don't have to care which it is for these fields
+ dst->xbutton.serial = XLastKnownRequestProcessed(fl_display);
+ dst->xbutton.display = fl_display;
+ dst->xbutton.window = wnd;
+ dst->xbutton.root = root;
+ dst->xbutton.subwindow = None;
+ dst->xbutton.time = fl_event_time;
+ dst->xbutton.x = src.eventX;
+ dst->xbutton.y = src.eventY;
+ dst->xbutton.x_root = rootX;
+ dst->xbutton.y_root = rootY;
+ dst->xbutton.state = state.mods;
+ dst->xbutton.state |= ((state.ptr_buttons >> 1) & 0x1f) << 8;
+ dst->xbutton.same_screen = True;
+}
+
+void XInputTouchHandler::fakeMotionEvent(const GestureEvent 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 GestureEvent origEvent)
+{
+ XEvent fakeEvent;
+
+ memset(&fakeEvent, 0, sizeof(XEvent));
+
+ fakeEvent.type = press ? ButtonPress : ButtonRelease;
+ fakeEvent.xbutton.button = button;
+ preparePointerEvent(&fakeEvent, origEvent);
+
+ fakeEvent.xbutton.state |= fakeStateMask;
+
+ // The button mask should indicate the button state just prior to
+ // the event, we update the button mask after pushing
+ pushFakeEvent(&fakeEvent);
+
+ // Set/unset the bit for the correct button
+ if (press) {
+ fakeStateMask |= (1<<(7+button));
+ } else {
+ fakeStateMask &= ~(1<<(7+button));
+ }
+}
+
+void XInputTouchHandler::fakeKeyEvent(bool press, int keysym,
+ const GestureEvent origEvent)
+{
+ XEvent fakeEvent;
+
+ Window root, child;
+ int rootX, rootY;
+ XkbStateRec state;
+
+ int modmask;
+
+ root = XDefaultRootWindow(fl_display);
+ XTranslateCoordinates(fl_display, wnd, root,
+ origEvent.eventX,
+ origEvent.eventY,
+ &rootX, &rootY, &child);
+ XkbGetState(fl_display, XkbUseCoreKbd, &state);
+
+ KeyCode kc = XKeysymToKeycode(fl_display, keysym);
+
+ memset(&fakeEvent, 0, sizeof(XEvent));
+
+ fakeEvent.type = press ? KeyPress : KeyRelease;
+ fakeEvent.xkey.type = press ? KeyPress : KeyRelease;
+ fakeEvent.xkey.keycode = kc;
+ fakeEvent.xkey.serial = XLastKnownRequestProcessed(fl_display);
+ fakeEvent.xkey.display = fl_display;
+ fakeEvent.xkey.window = wnd;
+ fakeEvent.xkey.root = root;
+ fakeEvent.xkey.subwindow = None;
+ fakeEvent.xkey.time = fl_event_time;
+ fakeEvent.xkey.x = origEvent.eventX;
+ fakeEvent.xkey.y = origEvent.eventY;
+ fakeEvent.xkey.x_root = rootX;
+ fakeEvent.xkey.y_root = rootY;
+ fakeEvent.xkey.state = state.mods;
+ fakeEvent.xkey.state |= ((state.ptr_buttons >> 1) & 0x1f) << 8;
+ fakeEvent.xkey.same_screen = True;
+
+ // Apply our fake mask
+ fakeEvent.xkey.state |= fakeStateMask;
+
+ pushFakeEvent(&fakeEvent);
+
+ switch(keysym) {
+ case XK_Shift_L:
+ case XK_Shift_R:
+ modmask = ShiftMask;
+ break;
+ case XK_Caps_Lock:
+ modmask = LockMask;
+ break;
+ case XK_Control_L:
+ case XK_Control_R:
+ modmask = ControlMask;
+ break;
+ default:
+ modmask = 0;
+ }
+
+ if (press)
+ fakeStateMask |= modmask;
+ else
+ fakeStateMask &= ~modmask;
+}
+
+void XInputTouchHandler::handleGestureEvent(const GestureEvent& event)
+{
+ BaseTouchHandler::handleGestureEvent(event);
+}
+
+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..6360b974
--- /dev/null
+++ b/vncviewer/XInputTouchHandler.h
@@ -0,0 +1,58 @@
+/* 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__
+
+#include "BaseTouchHandler.h"
+#include "GestureHandler.h"
+
+class XInputTouchHandler: public BaseTouchHandler, GestureHandler {
+ 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);
+
+ void preparePointerEvent(XEvent* dst, const GestureEvent src);
+ virtual void fakeMotionEvent(const GestureEvent origEvent);
+ virtual void fakeButtonEvent(bool press, int button,
+ const GestureEvent origEvent);
+ virtual void fakeKeyEvent(bool press, int keycode,
+ const GestureEvent origEvent);
+
+ virtual void handleGestureEvent(const GestureEvent& event);
+
+ private:
+ void pushFakeEvent(XEvent* event);
+
+ private:
+ Window wnd;
+ int fakeStateMask;
+};
+
+#endif
diff --git a/vncviewer/i18n.h b/vncviewer/i18n.h
index ee0e020d..4a50b17c 100644
--- a/vncviewer/i18n.h
+++ b/vncviewer/i18n.h
@@ -20,8 +20,6 @@
#ifndef _I18N_H
#define _I18N_H 1
-#include "gettext.h"
-
/* Need to tell gcc that pgettext() doesn't screw up format strings */
#ifdef __GNUC__
static const char *
@@ -30,6 +28,8 @@ pgettext_aux (const char *domain,
int category) __attribute__ ((format_arg (3)));
#endif
+#include "gettext.h"
+
#define _(String) gettext (String)
#define p_(Context, String) pgettext (Context, String)
#define N_(String) gettext_noop (String)
diff --git a/vncviewer/touch.cxx b/vncviewer/touch.cxx
new file mode 100644
index 00000000..d169deb9
--- /dev/null
+++ b/vncviewer/touch.cxx
@@ -0,0 +1,273 @@
+/* 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)
+#include <windows.h>
+#include <commctrl.h>
+#elif !defined(__APPLE__)
+#include <X11/extensions/XInput2.h>
+#include <X11/extensions/XI2.h>
+#endif
+
+#include <FL/Fl.H>
+#include <FL/x.H>
+
+#include <rfb/Exception.h>
+#include <rfb/LogWriter.h>
+
+#include "i18n.h"
+#include "vncviewer.h"
+#include "BaseTouchHandler.h"
+#if defined(WIN32)
+#include "Win32TouchHandler.h"
+#elif !defined(__APPLE__)
+#include "XInputTouchHandler.h"
+#endif
+
+#include "touch.h"
+
+static rfb::LogWriter vlog("Touch");
+
+#if !defined(WIN32) && !defined(__APPLE__)
+static int xi_major;
+#endif
+
+typedef std::map<Window, class BaseTouchHandler*> HandlerMap;
+static HandlerMap handlers;
+
+#if defined(WIN32)
+LRESULT CALLBACK win32WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam,
+ LPARAM lParam, UINT_PTR uIdSubclass,
+ DWORD_PTR dwRefData)
+{
+ bool handled = false;
+
+ if (uMsg == WM_NCDESTROY) {
+ delete handlers[hWnd];
+ handlers.erase(hWnd);
+ RemoveWindowSubclass(hWnd, &win32WindowProc, 1);
+ } else {
+ if (handlers.count(hWnd) == 0) {
+ vlog.error(_("Got message (0x%x) for an unhandled window"), uMsg);
+ } else {
+ handled = dynamic_cast<Win32TouchHandler*>
+ (handlers[hWnd])->processEvent(uMsg, wParam, lParam);
+ }
+ }
+
+ // Only run the normal WndProc handlers for unhandled events
+ if (handled)
+ return 0;
+ else
+ return DefSubclassProc(hWnd, uMsg, wParam, lParam);
+}
+
+#elif !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%08lx"), 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%08lx 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%08lx 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 = dynamic_cast<XInputTouchHandler*>(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%08lx specified for pointer grab"), window);
+ return;
+ }
+
+ dynamic_cast<XInputTouchHandler*>(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)
+ MSG *msg = (MSG*)event;
+
+ // Trigger on the first WM_PAINT event. We can't trigger on WM_CREATE
+ // events since FLTK's system handlers trigger before WndProc.
+ // WM_CREATE events are sent directly to WndProc.
+ if (msg->message == WM_PAINT && handlers.count(msg->hwnd) == 0) {
+ try {
+ handlers[msg->hwnd] = new Win32TouchHandler(msg->hwnd);
+ } catch (rfb::Exception& e) {
+ vlog.error(_("Failed to create touch handler: %s"), e.str());
+ exit_vncviewer(e.str());
+ }
+ // Add a special hook-in for handling events sent directly to WndProc
+ if (!SetWindowSubclass(msg->hwnd, &win32WindowProc, 1, 0)) {
+ vlog.error(_("Couldn't attach event handler to window (error 0x%x)"),
+ (int)GetLastError());
+ }
+ }
+#elif !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;
+ }
+
+ dynamic_cast<XInputTouchHandler*>(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 <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
diff --git a/vncviewer/vncviewer.cxx b/vncviewer/vncviewer.cxx
index 39a267c0..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"
@@ -86,6 +87,7 @@ char vncServerName[VNCSERVERNAMELEN] = { '\0' };
static const char *argv0 = NULL;
+static bool inMainloop = false;
static bool exitMainloop = false;
static const char *exitError = NULL;
@@ -114,7 +116,14 @@ void exit_vncviewer(const char *error)
if ((error != NULL) && (exitError == NULL))
exitError = strdup(error);
- exitMainloop = true;
+ if (inMainloop)
+ exitMainloop = true;
+ else {
+ // We're early in the startup. Assume we can just exit().
+ if (alertOnFatalError)
+ fl_alert("%s", exitError);
+ exit(EXIT_FAILURE);
+ }
}
bool should_exit()
@@ -415,9 +424,7 @@ potentiallyLoadConfigurationFile(char *vncServerName)
vncServerName[VNCSERVERNAMELEN-1] = '\0';
} catch (rfb::Exception& e) {
vlog.error("%s", e.str());
- if (alertOnFatalError)
- fl_alert("%s", e.str());
- exit(EXIT_FAILURE);
+ exit_vncviewer(e.str());
}
}
}
@@ -533,8 +540,6 @@ int main(int argc, char** argv)
signal(SIGINT, CleanupSignalHandler);
signal(SIGTERM, CleanupSignalHandler);
- init_fltk();
-
Configuration::enableViewerParams();
/* Load the default parameter settings */
@@ -548,8 +553,6 @@ int main(int argc, char** argv)
}
} catch (rfb::Exception& e) {
vlog.error("%s", e.str());
- if (alertOnFatalError)
- fl_alert("%s", e.str());
}
for (int i = 1; i < argc;) {
@@ -574,11 +577,6 @@ int main(int argc, char** argv)
i++;
}
- // Check if the server name in reality is a configuration file
- potentiallyLoadConfigurationFile(vncServerName);
-
- mkvnchomedir();
-
#if !defined(WIN32) && !defined(__APPLE__)
if (strcmp(display, "") != 0) {
Fl::display(display);
@@ -587,6 +585,14 @@ int main(int argc, char** argv)
XkbSetDetectableAutoRepeat(fl_display, True, NULL);
#endif
+ init_fltk();
+ enable_touch();
+
+ // Check if the server name in reality is a configuration file
+ potentiallyLoadConfigurationFile(vncServerName);
+
+ mkvnchomedir();
+
CSecurity::upg = &dlg;
#ifdef HAVE_GNUTLS
CSecurityTLS::msg = &dlg;
@@ -600,10 +606,8 @@ int main(int argc, char** argv)
// TRANSLATORS: "Parameters" are command line arguments, or settings
// from a file or the Windows registry.
vlog.error(_("Parameters -listen and -via are incompatible"));
- if (alertOnFatalError)
- fl_alert(_("Parameters -listen and -via are incompatible"));
- exit_vncviewer();
- return 1;
+ exit_vncviewer(_("Parameters -listen and -via are incompatible"));
+ return 1; /* Not reached */
}
#endif
@@ -649,10 +653,8 @@ int main(int argc, char** argv)
}
} catch (rdr::Exception& e) {
vlog.error("%s", e.str());
- if (alertOnFatalError)
- fl_alert("%s", e.str());
- exit_vncviewer();
- return 1;
+ exit_vncviewer(e.str());
+ return 1; /* Not reached */
}
while (!listeners.empty()) {
@@ -674,8 +676,10 @@ int main(int argc, char** argv)
CConn *cc = new CConn(vncServerName, sock);
+ inMainloop = true;
while (!exitMainloop)
run_mainloop();
+ inMainloop = false;
delete cc;