From 642207f126cb1c5d647afcca98dc82095a463c34 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sat, 28 Mar 2020 21:30:56 +0100 Subject: [PATCH] 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. --- vncviewer/CMakeLists.txt | 2 +- vncviewer/Win32TouchHandler.cxx | 442 ++++++++++++++++++++++++++++++++ vncviewer/Win32TouchHandler.h | 60 +++++ vncviewer/touch.cxx | 71 ++++- 4 files changed, 565 insertions(+), 10 deletions(-) create mode 100644 vncviewer/Win32TouchHandler.cxx create mode 100644 vncviewer/Win32TouchHandler.h diff --git a/vncviewer/CMakeLists.txt b/vncviewer/CMakeLists.txt index 74e2deef..e4fad782 100644 --- a/vncviewer/CMakeLists.txt +++ b/vncviewer/CMakeLists.txt @@ -30,7 +30,7 @@ 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() 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 +#endif + +#include + +#define XK_MISCELLANY +#include +#include +#include + +#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(¤tPos); + + 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 + +#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/touch.cxx b/vncviewer/touch.cxx index 4ad9a0ad..0c15a117 100644 --- a/vncviewer/touch.cxx +++ b/vncviewer/touch.cxx @@ -25,7 +25,10 @@ #include -#if !defined(WIN32) && !defined(__APPLE__) +#if defined(WIN32) +#include +#include +#elif !defined(__APPLE__) #include #include #endif @@ -33,11 +36,15 @@ #include #include +#include #include #include "i18n.h" #include "vncviewer.h" -#if !defined(WIN32) && !defined(__APPLE__) +#include "BaseTouchHandler.h" +#if defined(WIN32) +#include "Win32TouchHandler.h" +#elif !defined(__APPLE__) #include "XInputTouchHandler.h" #endif @@ -47,12 +54,39 @@ static rfb::LogWriter vlog("Touch"); #if !defined(WIN32) && !defined(__APPLE__) static int xi_major; +#endif -typedef std::map HandlerMap; +typedef std::map HandlerMap; static HandlerMap handlers; -#endif -#if !defined(WIN32) && !defined(__APPLE__) +#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 + (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; @@ -112,7 +146,7 @@ bool x11_grab_pointer(Window window) // event. x11_change_touch_ownership(false); - ret = handlers[window]->grabPointer(); + ret = dynamic_cast(handlers[window])->grabPointer(); if (!ret) x11_change_touch_ownership(true); @@ -127,7 +161,7 @@ void x11_ungrab_pointer(Window window) return; } - handlers[window]->ungrabPointer(); + dynamic_cast(handlers[window])->ungrabPointer(); // Restore XI_TouchOwnership now that the grab is gone x11_change_touch_ownership(true); @@ -136,7 +170,26 @@ void x11_ungrab_pointer(Window window) static int handleTouchEvent(void *event, void *data) { -#if !defined(WIN32) && !defined(__APPLE__) +#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) { @@ -174,7 +227,7 @@ static int handleTouchEvent(void *event, void *data) return 1; } - handlers[devev->event]->processEvent(devev); + dynamic_cast(handlers[devev->event])->processEvent(devev); XFreeEventData(fl_display, &xevent->xcookie); -- 2.39.5