summaryrefslogtreecommitdiffstats
path: root/vncviewer/Win32TouchHandler.cxx
diff options
context:
space:
mode:
authorSamuel Mannehed <samuel@cendio.se>2020-03-28 21:30:56 +0100
committerNiko Lehto <nikle@cendio.se>2020-05-29 15:28:31 +0200
commit642207f126cb1c5d647afcca98dc82095a463c34 (patch)
treefaca14f568e24f7e0d41119b80e2a29e42ed0157 /vncviewer/Win32TouchHandler.cxx
parent95ad018961f384f6e43c80c4f5473064a6f3aedd (diff)
downloadtigervnc-642207f126cb1c5d647afcca98dc82095a463c34.tar.gz
tigervnc-642207f126cb1c5d647afcca98dc82095a463c34.zip
Support touch gestures on Windows
This adds the same touch gesture support for Windows as already added for Unix. Note that it uses Windows gesture detection instead of our own here though to give the user a familiar experience. Unfortunately that means we lose the three finger tap. This also raises the base requirements to Windows 7 as that's when Windows got proper touch support.
Diffstat (limited to 'vncviewer/Win32TouchHandler.cxx')
-rw-r--r--vncviewer/Win32TouchHandler.cxx442
1 files changed, 442 insertions, 0 deletions
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);
+}