diff options
Diffstat (limited to 'vncviewer')
-rw-r--r-- | vncviewer/BaseTouchHandler.cxx | 196 | ||||
-rw-r--r-- | vncviewer/BaseTouchHandler.h | 51 | ||||
-rw-r--r-- | vncviewer/CMakeLists.txt | 12 | ||||
-rw-r--r-- | vncviewer/DesktopWindow.cxx | 28 | ||||
-rw-r--r-- | vncviewer/EmulateMB.cxx | 72 | ||||
-rw-r--r-- | vncviewer/EmulateMB.h | 4 | ||||
-rw-r--r-- | vncviewer/GestureEvent.h | 51 | ||||
-rw-r--r-- | vncviewer/GestureHandler.cxx | 515 | ||||
-rw-r--r-- | vncviewer/GestureHandler.h | 81 | ||||
-rw-r--r-- | vncviewer/Viewport.cxx | 48 | ||||
-rw-r--r-- | vncviewer/Viewport.h | 1 | ||||
-rw-r--r-- | vncviewer/Win32TouchHandler.cxx | 442 | ||||
-rw-r--r-- | vncviewer/Win32TouchHandler.h | 60 | ||||
-rw-r--r-- | vncviewer/XInputTouchHandler.cxx | 461 | ||||
-rw-r--r-- | vncviewer/XInputTouchHandler.h | 58 | ||||
-rw-r--r-- | vncviewer/i18n.h | 4 | ||||
-rw-r--r-- | vncviewer/touch.cxx | 273 | ||||
-rw-r--r-- | vncviewer/touch.h | 30 | ||||
-rw-r--r-- | vncviewer/vncviewer.cxx | 46 |
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(¤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/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; |