diff options
Diffstat (limited to 'vncviewer')
-rw-r--r-- | vncviewer/CMakeLists.txt | 2 | ||||
-rw-r--r-- | vncviewer/Win32TouchHandler.cxx | 442 | ||||
-rw-r--r-- | vncviewer/Win32TouchHandler.h | 60 | ||||
-rw-r--r-- | vncviewer/touch.cxx | 71 |
4 files changed, 565 insertions, 10 deletions
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 <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(¤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 <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/touch.cxx b/vncviewer/touch.cxx index 4ad9a0ad..0c15a117 100644 --- a/vncviewer/touch.cxx +++ b/vncviewer/touch.cxx @@ -25,7 +25,10 @@ #include <map> -#if !defined(WIN32) && !defined(__APPLE__) +#if defined(WIN32) +#include <windows.h> +#include <commctrl.h> +#elif !defined(__APPLE__) #include <X11/extensions/XInput2.h> #include <X11/extensions/XI2.h> #endif @@ -33,11 +36,15 @@ #include <FL/Fl.H> #include <FL/x.H> +#include <rfb/Exception.h> #include <rfb/LogWriter.h> #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<Window, class XInputTouchHandler*> HandlerMap; +typedef std::map<Window, class BaseTouchHandler*> 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<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; @@ -112,7 +146,7 @@ bool x11_grab_pointer(Window window) // event. x11_change_touch_ownership(false); - ret = handlers[window]->grabPointer(); + ret = dynamic_cast<XInputTouchHandler*>(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<XInputTouchHandler*>(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<XInputTouchHandler*>(handlers[devev->event])->processEvent(devev); XFreeEventData(fl_display, &xevent->xcookie); |