]> source.dussan.org Git - tigervnc.git/commitdiff
Implement touch gesture handling on Unix
authorAaron Sowry <aaron@cendio.se>
Fri, 24 May 2019 00:00:47 +0000 (12:00 +1200)
committerNiko Lehto <nikle@cendio.se>
Fri, 29 May 2020 13:26:33 +0000 (15:26 +0200)
Allows the user to perform certain important mouse operations using
touch gestures instead.

cmake/StaticBuild.cmake
tests/unit/CMakeLists.txt
tests/unit/gesturehandler.cxx [new file with mode: 0644]
vncviewer/BaseTouchHandler.cxx [new file with mode: 0644]
vncviewer/BaseTouchHandler.h [new file with mode: 0644]
vncviewer/CMakeLists.txt
vncviewer/GestureEvent.h [new file with mode: 0644]
vncviewer/GestureHandler.cxx [new file with mode: 0644]
vncviewer/GestureHandler.h [new file with mode: 0644]
vncviewer/XInputTouchHandler.cxx
vncviewer/XInputTouchHandler.h

index 9129f9dbf2e85a005d2a34cb6137bacc33c85a1d..97cfcb279f8e2f0f06a6976c25f08474284def28 100644 (file)
@@ -123,7 +123,7 @@ if(BUILD_STATIC_GCC)
   set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -nodefaultlibs")
   set(STATIC_BASE_LIBRARIES "")
   if(ENABLE_ASAN AND NOT WIN32 AND NOT APPLE)
-    set(STATIC_BASE_LIBRARIES "${STATIC_BASE_LIBRARIES} -Wl,-Bstatic -lasan -Wl,-Bdynamic -ldl -lm -lpthread")
+    set(STATIC_BASE_LIBRARIES "${STATIC_BASE_LIBRARIES} -Wl,-Bstatic -lasan -Wl,-Bdynamic -ldl -lpthread")
   endif()
   if(ENABLE_TSAN AND NOT WIN32 AND NOT APPLE AND CMAKE_SIZEOF_VOID_P MATCHES 8)
     # libtsan redefines some C++ symbols which then conflict with a
@@ -139,7 +139,7 @@ if(BUILD_STATIC_GCC)
     # these things again
     set(STATIC_BASE_LIBRARIES "${STATIC_BASE_LIBRARIES} -lmingw32 -lgcc_eh -lgcc -lmoldname -lmingwex -lmsvcrt")
   else()
-    set(STATIC_BASE_LIBRARIES "${STATIC_BASE_LIBRARIES} -lgcc -lgcc_eh -lc")
+    set(STATIC_BASE_LIBRARIES "${STATIC_BASE_LIBRARIES} -lm -lgcc -lgcc_eh -lc")
   endif()
   set(CMAKE_C_LINK_EXECUTABLE "${CMAKE_C_LINK_EXECUTABLE} ${STATIC_BASE_LIBRARIES}")
   set(CMAKE_CXX_LINK_EXECUTABLE "${CMAKE_CXX_LINK_EXECUTABLE} -Wl,-Bstatic -lstdc++ -Wl,-Bdynamic ${STATIC_BASE_LIBRARIES}")
index e568660a3b8cbff9cf5479dcedb9f8d1af358cb4..806d7272d9381d60b5c2c958a3fa92ac2505424e 100644 (file)
@@ -7,6 +7,9 @@ target_link_libraries(conv rfb)
 add_executable(convertlf convertlf.cxx)
 target_link_libraries(convertlf rfb)
 
+add_executable(gesturehandler gesturehandler.cxx ../../vncviewer/GestureHandler.cxx)
+target_link_libraries(gesturehandler rfb)
+
 add_executable(hostport hostport.cxx)
 target_link_libraries(hostport rfb)
 
diff --git a/tests/unit/gesturehandler.cxx b/tests/unit/gesturehandler.cxx
new file mode 100644 (file)
index 0000000..a07ecf4
--- /dev/null
@@ -0,0 +1,1245 @@
+/* Copyright 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.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+
+#include <vector>
+
+#include "../../vncviewer/GestureHandler.h"
+
+class TestClass : public GestureHandler
+{
+  protected:
+    virtual void handleGestureEvent(const GestureEvent& event);
+
+  public:
+    std::vector<GestureEvent> events;
+};
+
+void TestClass::handleGestureEvent(const GestureEvent& event)
+{
+  events.push_back(event);
+}
+
+// FIXME: handle doubles
+#define ASSERT_EQ(expr, val) if ((expr) != (val)) { \
+  printf("FAILED on line %d (%s equals %d, expected %d)\n", __LINE__, #expr, (int)(expr), (int)(val)); \
+  return; \
+}
+
+void testOneTapNormal()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchEnd(1);
+
+  ASSERT_EQ(test.events.size(), 2);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureOneTap);
+  ASSERT_EQ(test.events[0].eventX, 20.0);
+  ASSERT_EQ(test.events[0].eventY, 30.0);
+
+  ASSERT_EQ(test.events[1].type, GestureEnd);
+  ASSERT_EQ(test.events[1].gesture, GestureOneTap);
+  ASSERT_EQ(test.events[1].eventX, 20.0);
+  ASSERT_EQ(test.events[1].eventY, 30.0);
+
+  printf("OK\n");
+}
+
+void testTwoTapNormal()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+  test.handleTouchBegin(2, 30.0, 50.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchEnd(1);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchEnd(2);
+
+  ASSERT_EQ(test.events.size(), 2);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureTwoTap);
+  ASSERT_EQ(test.events[0].eventX, 25.0);
+  ASSERT_EQ(test.events[0].eventY, 40.0);
+
+  ASSERT_EQ(test.events[1].type, GestureEnd);
+  ASSERT_EQ(test.events[1].gesture, GestureTwoTap);
+  ASSERT_EQ(test.events[1].eventX, 25.0);
+  ASSERT_EQ(test.events[1].eventY, 40.0);
+
+  printf("OK\n");
+}
+
+void testTwoTapSlowBegin()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+
+  usleep(500000);
+  rfb::Timer::checkTimeouts();
+
+  test.handleTouchBegin(2, 30.0, 50.0);
+  test.handleTouchEnd(1);
+  test.handleTouchEnd(2);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  printf("OK\n");
+}
+
+void testTwoTapSlowEnd()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+  test.handleTouchBegin(2, 30.0, 50.0);
+  test.handleTouchEnd(1);
+
+  usleep(500000);
+  rfb::Timer::checkTimeouts();
+
+  test.handleTouchEnd(2);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  printf("OK\n");
+}
+
+void testTwoTapTimeout()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+  test.handleTouchBegin(2, 30.0, 50.0);
+
+  usleep(1500000);
+  rfb::Timer::checkTimeouts();
+
+  test.handleTouchEnd(1);
+  test.handleTouchEnd(2);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  printf("OK\n");
+}
+
+void testThreeTapNormal()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+  test.handleTouchBegin(2, 30.0, 50.0);
+  test.handleTouchBegin(3, 40.0, 40.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchEnd(1);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchEnd(2);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchEnd(3);
+
+  ASSERT_EQ(test.events.size(), 2);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureThreeTap);
+  ASSERT_EQ(test.events[0].eventX, 30.0);
+  ASSERT_EQ(test.events[0].eventY, 40.0);
+
+  ASSERT_EQ(test.events[1].type, GestureEnd);
+  ASSERT_EQ(test.events[1].gesture, GestureThreeTap);
+  ASSERT_EQ(test.events[1].eventX, 30.0);
+  ASSERT_EQ(test.events[1].eventY, 40.0);
+
+  printf("OK\n");
+}
+
+void testThreeTapSlowBegin()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+  test.handleTouchBegin(2, 30.0, 50.0);
+
+  usleep(500000);
+  rfb::Timer::checkTimeouts();
+
+  test.handleTouchBegin(3, 40.0, 40.0);
+  test.handleTouchEnd(1);
+  test.handleTouchEnd(2);
+  test.handleTouchEnd(3);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  printf("OK\n");
+}
+
+void testThreeTapSlowEnd()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+  test.handleTouchBegin(2, 30.0, 50.0);
+  test.handleTouchBegin(3, 40.0, 40.0);
+  test.handleTouchEnd(1);
+  test.handleTouchEnd(2);
+
+  usleep(500000);
+  rfb::Timer::checkTimeouts();
+
+  test.handleTouchEnd(3);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  printf("OK\n");
+}
+
+void testThreeTapDrag()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+  test.handleTouchBegin(2, 30.0, 50.0);
+  test.handleTouchBegin(3, 40.0, 40.0);
+
+  test.handleTouchUpdate(1, 120.0, 130.0);
+  test.handleTouchUpdate(2, 130.0, 150.0);
+  test.handleTouchUpdate(3, 140.0, 140.0);
+
+  test.handleTouchEnd(1);
+  test.handleTouchEnd(2);
+  test.handleTouchEnd(3);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  printf("OK\n");
+}
+
+void testThreeTapTimeout()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+  test.handleTouchBegin(2, 30.0, 50.0);
+  test.handleTouchBegin(3, 40.0, 40.0);
+
+  usleep(1500000);
+  rfb::Timer::checkTimeouts();
+
+  test.handleTouchEnd(1);
+  test.handleTouchEnd(2);
+  test.handleTouchEnd(3);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  printf("OK\n");
+}
+
+void testDragHoriz()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(1, 40.0, 30.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(1, 80.0, 30.0);
+
+  ASSERT_EQ(test.events.size(), 2);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureDrag);
+  ASSERT_EQ(test.events[0].eventX, 20.0);
+  ASSERT_EQ(test.events[0].eventY, 30.0);
+
+  ASSERT_EQ(test.events[1].type, GestureUpdate);
+  ASSERT_EQ(test.events[1].gesture, GestureDrag);
+  ASSERT_EQ(test.events[1].eventX, 80.0);
+  ASSERT_EQ(test.events[1].eventY, 30.0);
+
+  test.events.clear();
+
+  test.handleTouchEnd(1);
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureEnd);
+  ASSERT_EQ(test.events[0].gesture, GestureDrag);
+  ASSERT_EQ(test.events[0].eventX, 80.0);
+  ASSERT_EQ(test.events[0].eventY, 30.0);
+
+  printf("OK\n");
+}
+
+void testDragVert()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(1, 20.0, 50.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(1, 20.0, 90.0);
+
+  ASSERT_EQ(test.events.size(), 2);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureDrag);
+  ASSERT_EQ(test.events[0].eventX, 20.0);
+  ASSERT_EQ(test.events[0].eventY, 30.0);
+
+  ASSERT_EQ(test.events[1].type, GestureUpdate);
+  ASSERT_EQ(test.events[1].gesture, GestureDrag);
+  ASSERT_EQ(test.events[1].eventX, 20.0);
+  ASSERT_EQ(test.events[1].eventY, 90.0);
+
+  test.events.clear();
+
+  test.handleTouchEnd(1);
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureEnd);
+  ASSERT_EQ(test.events[0].gesture, GestureDrag);
+  ASSERT_EQ(test.events[0].eventX, 20.0);
+  ASSERT_EQ(test.events[0].eventY, 90.0);
+
+  printf("OK\n");
+}
+
+void testDragDiag()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 120.0, 130.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(1, 90.0, 100.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(1, 60.0, 70.0);
+
+  ASSERT_EQ(test.events.size(), 2);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureDrag);
+  ASSERT_EQ(test.events[0].eventX, 120.0);
+  ASSERT_EQ(test.events[0].eventY, 130.0);
+
+  ASSERT_EQ(test.events[1].type, GestureUpdate);
+  ASSERT_EQ(test.events[1].gesture, GestureDrag);
+  ASSERT_EQ(test.events[1].eventX, 60.0);
+  ASSERT_EQ(test.events[1].eventY, 70.0);
+
+  test.events.clear();
+
+  test.handleTouchEnd(1);
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureEnd);
+  ASSERT_EQ(test.events[0].gesture, GestureDrag);
+  ASSERT_EQ(test.events[0].eventX, 60.0);
+  ASSERT_EQ(test.events[0].eventY, 70.0);
+
+  printf("OK\n");
+}
+
+void testLongPressNormal()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  usleep(1500000);
+  rfb::Timer::checkTimeouts();
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureLongPress);
+  ASSERT_EQ(test.events[0].eventX, 20.0);
+  ASSERT_EQ(test.events[0].eventY, 30.0);
+
+  test.events.clear();
+
+  test.handleTouchEnd(1);
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureEnd);
+  ASSERT_EQ(test.events[0].gesture, GestureLongPress);
+  ASSERT_EQ(test.events[0].eventX, 20.0);
+  ASSERT_EQ(test.events[0].eventY, 30.0);
+
+  printf("OK\n");
+}
+
+void testLongPressDrag()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  usleep(1500000);
+  rfb::Timer::checkTimeouts();
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureLongPress);
+  ASSERT_EQ(test.events[0].eventX, 20.0);
+  ASSERT_EQ(test.events[0].eventY, 30.0);
+
+  test.events.clear();
+
+  test.handleTouchUpdate(1, 120.0, 50.0);
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureUpdate);
+  ASSERT_EQ(test.events[0].gesture, GestureLongPress);
+  ASSERT_EQ(test.events[0].eventX, 120.0);
+  ASSERT_EQ(test.events[0].eventY, 50.0);
+
+  test.events.clear();
+
+  test.handleTouchEnd(1);
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureEnd);
+  ASSERT_EQ(test.events[0].gesture, GestureLongPress);
+  ASSERT_EQ(test.events[0].eventX, 120.0);
+  ASSERT_EQ(test.events[0].eventY, 50.0);
+
+  printf("OK\n");
+}
+
+void testTwoDragFastHoriz()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+  test.handleTouchBegin(2, 30.0, 30.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(1, 40.0, 30.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(2, 50.0, 30.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(2, 90.0, 30.0);
+  test.handleTouchUpdate(1, 80.0, 30.0);
+
+  ASSERT_EQ(test.events.size(), 2);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureTwoDrag);
+  ASSERT_EQ(test.events[0].eventX, 25.0);
+  ASSERT_EQ(test.events[0].eventY, 30.0);
+  ASSERT_EQ(test.events[0].magnitudeX, 0.0);
+  ASSERT_EQ(test.events[0].magnitudeY, 0.0);
+
+  ASSERT_EQ(test.events[1].type, GestureUpdate);
+  ASSERT_EQ(test.events[1].gesture, GestureTwoDrag);
+  ASSERT_EQ(test.events[1].eventX, 25.0);
+  ASSERT_EQ(test.events[1].eventY, 30.0);
+  ASSERT_EQ(test.events[1].magnitudeX, 60.0);
+  ASSERT_EQ(test.events[1].magnitudeY, 0.0);
+
+  test.events.clear();
+
+  test.handleTouchEnd(1);
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureEnd);
+  ASSERT_EQ(test.events[0].gesture, GestureTwoDrag);
+  ASSERT_EQ(test.events[0].eventX, 25.0);
+  ASSERT_EQ(test.events[0].eventY, 30.0);
+  ASSERT_EQ(test.events[0].magnitudeX, 60.0);
+  ASSERT_EQ(test.events[0].magnitudeY, 0.0);
+
+  printf("OK\n");
+}
+
+void testTwoDragFastVert()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+  test.handleTouchBegin(2, 30.0, 30.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(1, 20.0, 100.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(2, 30.0, 40.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(2, 30.0, 90.0);
+
+  ASSERT_EQ(test.events.size(), 2);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureTwoDrag);
+  ASSERT_EQ(test.events[0].eventX, 25.0);
+  ASSERT_EQ(test.events[0].eventY, 30.0);
+  ASSERT_EQ(test.events[0].magnitudeX, 0.0);
+  ASSERT_EQ(test.events[0].magnitudeY, 0.0);
+
+  ASSERT_EQ(test.events[1].type, GestureUpdate);
+  ASSERT_EQ(test.events[1].gesture, GestureTwoDrag);
+  ASSERT_EQ(test.events[1].eventX, 25.0);
+  ASSERT_EQ(test.events[1].eventY, 30.0);
+  ASSERT_EQ(test.events[1].magnitudeX, 0.0);
+  ASSERT_EQ(test.events[1].magnitudeY, 65.0);
+
+  test.events.clear();
+
+  test.handleTouchEnd(2);
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureEnd);
+  ASSERT_EQ(test.events[0].gesture, GestureTwoDrag);
+  ASSERT_EQ(test.events[0].eventX, 25.0);
+  ASSERT_EQ(test.events[0].eventY, 30.0);
+  ASSERT_EQ(test.events[0].magnitudeX, 0.0);
+  ASSERT_EQ(test.events[0].magnitudeY, 65.0);
+
+  printf("OK\n");
+}
+
+void testTwoDragFastDiag()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 120.0, 130.0);
+  test.handleTouchBegin(2, 130.0, 130.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(1, 80.0, 90.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(2, 100.0, 130.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(2, 60.0, 70.0);
+
+  ASSERT_EQ(test.events.size(), 2);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureTwoDrag);
+  ASSERT_EQ(test.events[0].eventX, 125.0);
+  ASSERT_EQ(test.events[0].eventY, 130.0);
+  ASSERT_EQ(test.events[0].magnitudeX, 0.0);
+  ASSERT_EQ(test.events[0].magnitudeY, 0.0);
+
+  ASSERT_EQ(test.events[1].type, GestureUpdate);
+  ASSERT_EQ(test.events[1].gesture, GestureTwoDrag);
+  ASSERT_EQ(test.events[1].eventX, 125.0);
+  ASSERT_EQ(test.events[1].eventY, 130.0);
+  ASSERT_EQ(test.events[1].magnitudeX, -55.0);
+  ASSERT_EQ(test.events[1].magnitudeY, -50.0);
+
+  test.events.clear();
+
+  test.handleTouchEnd(2);
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureEnd);
+  ASSERT_EQ(test.events[0].gesture, GestureTwoDrag);
+  ASSERT_EQ(test.events[0].eventX, 125.0);
+  ASSERT_EQ(test.events[0].eventY, 130.0);
+  ASSERT_EQ(test.events[0].magnitudeX, -55.0);
+  ASSERT_EQ(test.events[0].magnitudeY, -50.0);
+
+  printf("OK\n");
+}
+
+void testTwoDragEndBeforeTimeout()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+  test.handleTouchBegin(2, 30.0, 30.0);
+  test.handleTouchUpdate(1, 80.0, 30.0);
+  test.handleTouchUpdate(2, 70.0, 30.0);
+  test.handleTouchEnd(1);
+  test.handleTouchEnd(2);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  usleep(500000);
+  rfb::Timer::checkTimeouts();
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  printf("OK\n");
+}
+
+void testTwoDragStartHorizFromTimeout()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 50.0, 40.0);
+  test.handleTouchBegin(2, 60.0, 40.0);
+  test.handleTouchUpdate(2, 80.0, 40.0);
+  test.handleTouchUpdate(1, 110.0, 40.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  usleep(60000); // 60ms
+  rfb::Timer::checkTimeouts();
+
+  ASSERT_EQ(test.events.size(), 2);
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureTwoDrag);
+  ASSERT_EQ(test.events[0].eventX, 55.0);
+  ASSERT_EQ(test.events[0].eventY, 40.0);
+  ASSERT_EQ(test.events[0].magnitudeX, 0.0);
+  ASSERT_EQ(test.events[0].magnitudeY, 0.0);
+
+  ASSERT_EQ(test.events[1].type, GestureUpdate);
+  ASSERT_EQ(test.events[1].gesture, GestureTwoDrag);
+  ASSERT_EQ(test.events[1].eventX, 55.0);
+  ASSERT_EQ(test.events[1].eventY, 40.0);
+  ASSERT_EQ(test.events[1].magnitudeX, 40.0);
+  ASSERT_EQ(test.events[1].magnitudeY, 0.0);
+
+  printf("OK\n");
+}
+
+void testTwoDragStartVertFromTimeout()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 40.0, 40.0);
+  test.handleTouchBegin(2, 40.0, 60.0);
+  test.handleTouchUpdate(2, 40.0, 80.0);
+  test.handleTouchUpdate(1, 40.0, 100.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  usleep(60000); // 60ms
+  rfb::Timer::checkTimeouts();
+
+  ASSERT_EQ(test.events.size(), 2);
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureTwoDrag);
+  ASSERT_EQ(test.events[0].eventX, 40.0);
+  ASSERT_EQ(test.events[0].eventY, 50.0);
+  ASSERT_EQ(test.events[0].magnitudeX, 0.0);
+  ASSERT_EQ(test.events[0].magnitudeY, 0.0);
+
+  ASSERT_EQ(test.events[1].type, GestureUpdate);
+  ASSERT_EQ(test.events[1].gesture, GestureTwoDrag);
+  ASSERT_EQ(test.events[1].eventX, 40.0);
+  ASSERT_EQ(test.events[1].eventY, 50.0);
+  ASSERT_EQ(test.events[1].magnitudeX, 0.0);
+  ASSERT_EQ(test.events[1].magnitudeY, 40.0);
+
+  printf("OK\n");
+}
+
+void testTwoDragStartDiagFromTimeout()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 50.0, 40.0);
+  test.handleTouchBegin(2, 40.0, 60.0);
+  test.handleTouchUpdate(1, 70.0, 60.0);
+  test.handleTouchUpdate(2, 90.0, 110.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  usleep(60000); // 60ms
+  rfb::Timer::checkTimeouts();
+
+  ASSERT_EQ(test.events.size(), 2);
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureTwoDrag);
+  ASSERT_EQ(test.events[0].eventX, 45.0);
+  ASSERT_EQ(test.events[0].eventY, 50.0);
+  ASSERT_EQ(test.events[0].magnitudeX, 0.0);
+  ASSERT_EQ(test.events[0].magnitudeY, 0.0);
+
+  ASSERT_EQ(test.events[1].type, GestureUpdate);
+  ASSERT_EQ(test.events[1].gesture, GestureTwoDrag);
+  ASSERT_EQ(test.events[1].eventX, 45.0);
+  ASSERT_EQ(test.events[1].eventY, 50.0);
+  ASSERT_EQ(test.events[1].magnitudeX, 35.0);
+  ASSERT_EQ(test.events[1].magnitudeY, 35.0);
+
+  printf("OK\n");
+}
+
+void testTwoDragTooSlow()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+
+  usleep(500000);
+  rfb::Timer::checkTimeouts();
+
+  test.handleTouchBegin(2, 30.0, 30.0);
+  test.handleTouchUpdate(2, 50.0, 30.0);
+  test.handleTouchUpdate(1, 80.0, 30.0);
+  test.handleTouchEnd(1);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  printf("OK\n");
+}
+
+void testPinchFastIn()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 0.0, 0.0);
+  test.handleTouchBegin(2, 130.0, 130.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(1, 50.0, 40.0);
+  test.handleTouchUpdate(2, 100.0, 130.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(2, 60.0, 70.0);
+
+  ASSERT_EQ(test.events.size(), 2);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GesturePinch);
+  ASSERT_EQ(test.events[0].eventX, 65.0);
+  ASSERT_EQ(test.events[0].eventY, 65.0);
+  ASSERT_EQ(test.events[0].magnitudeX, 130.0);
+  ASSERT_EQ(test.events[0].magnitudeY, 130.0);
+
+  ASSERT_EQ(test.events[1].type, GestureUpdate);
+  ASSERT_EQ(test.events[1].gesture, GesturePinch);
+  ASSERT_EQ(test.events[1].eventX, 65.0);
+  ASSERT_EQ(test.events[1].eventY, 65.0);
+  ASSERT_EQ(test.events[1].magnitudeX, 10.0);
+  ASSERT_EQ(test.events[1].magnitudeY, 30.0);
+
+  test.events.clear();
+
+  test.handleTouchEnd(2);
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureEnd);
+  ASSERT_EQ(test.events[0].gesture, GesturePinch);
+  ASSERT_EQ(test.events[0].eventX, 65.0);
+  ASSERT_EQ(test.events[0].eventY, 65.0);
+  ASSERT_EQ(test.events[0].magnitudeX, 10.0);
+  ASSERT_EQ(test.events[0].magnitudeY, 30.0);
+
+  printf("OK\n");
+}
+
+void testPinchFastOut()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 100.0, 100.0);
+  test.handleTouchBegin(2, 110.0, 100.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(1, 130.0, 70.0);
+  test.handleTouchUpdate(2, 20.0, 200.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(1, 180.0, 20.0);
+
+  ASSERT_EQ(test.events.size(), 2);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GesturePinch);
+  ASSERT_EQ(test.events[0].eventX, 105.0);
+  ASSERT_EQ(test.events[0].eventY, 100.0);
+  ASSERT_EQ(test.events[0].magnitudeX, 10.0);
+  ASSERT_EQ(test.events[0].magnitudeY, 0.0);
+
+  ASSERT_EQ(test.events[1].type, GestureUpdate);
+  ASSERT_EQ(test.events[1].gesture, GesturePinch);
+  ASSERT_EQ(test.events[1].eventX, 105.0);
+  ASSERT_EQ(test.events[1].eventY, 100.0);
+  ASSERT_EQ(test.events[1].magnitudeX, 160.0);
+  ASSERT_EQ(test.events[1].magnitudeY, 180.0);
+
+  test.events.clear();
+
+  test.handleTouchEnd(2);
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureEnd);
+  ASSERT_EQ(test.events[0].gesture, GesturePinch);
+  ASSERT_EQ(test.events[0].eventX, 105.0);
+  ASSERT_EQ(test.events[0].eventY, 100.0);
+  ASSERT_EQ(test.events[0].magnitudeX, 160.0);
+  ASSERT_EQ(test.events[0].magnitudeY, 180.0);
+
+  printf("OK\n");
+}
+
+void testPinchEndBeforeTimeout()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+  test.handleTouchBegin(2, 130.0, 130.0);
+  test.handleTouchUpdate(1, 80.0, 70.0);
+  test.handleTouchEnd(1);
+  test.handleTouchEnd(2);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  usleep(500000);
+  rfb::Timer::checkTimeouts();
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  printf("OK\n");
+}
+
+void testPinchStartInFromTimeout()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 0.0, 0.0);
+  test.handleTouchBegin(2, 130.0, 130.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(1, 50.0, 40.0);
+  test.handleTouchUpdate(2, 100.0, 130.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  usleep(60000); // 60ms
+  rfb::Timer::checkTimeouts();
+
+  ASSERT_EQ(test.events.size(), 2);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GesturePinch);
+  ASSERT_EQ(test.events[0].eventX, 65.0);
+  ASSERT_EQ(test.events[0].eventY, 65.0);
+  ASSERT_EQ(test.events[0].magnitudeX, 130.0);
+  ASSERT_EQ(test.events[0].magnitudeY, 130.0);
+
+  ASSERT_EQ(test.events[1].type, GestureUpdate);
+  ASSERT_EQ(test.events[1].gesture, GesturePinch);
+  ASSERT_EQ(test.events[1].eventX, 65.0);
+  ASSERT_EQ(test.events[1].eventY, 65.0);
+  ASSERT_EQ(test.events[1].magnitudeX, 50.0);
+  ASSERT_EQ(test.events[1].magnitudeY, 90.0);
+
+  printf("OK\n");
+}
+
+void testPinchStartOutFromTimeout()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 100.0, 130.0);
+  test.handleTouchBegin(2, 110.0, 130.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(2, 200.0, 130.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  usleep(60000); // 60ms
+  rfb::Timer::checkTimeouts();
+
+  ASSERT_EQ(test.events.size(), 2);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GesturePinch);
+  ASSERT_EQ(test.events[0].eventX, 105.0);
+  ASSERT_EQ(test.events[0].eventY, 130.0);
+  ASSERT_EQ(test.events[0].magnitudeX, 10.0);
+  ASSERT_EQ(test.events[0].magnitudeY, 0.0);
+
+  ASSERT_EQ(test.events[1].type, GestureUpdate);
+  ASSERT_EQ(test.events[1].gesture, GesturePinch);
+  ASSERT_EQ(test.events[1].eventX, 105.0);
+  ASSERT_EQ(test.events[1].eventY, 130.0);
+  ASSERT_EQ(test.events[1].magnitudeX, 100.0);
+  ASSERT_EQ(test.events[1].magnitudeY, 0.0);
+
+  printf("OK\n");
+}
+
+void testPinchTooSlow()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 0.0, 0.0);
+
+  usleep(60000); // 60ms
+  rfb::Timer::checkTimeouts();
+
+  test.handleTouchBegin(2, 130.0, 130.0);
+  test.handleTouchUpdate(2, 100.0, 130.0);
+  test.handleTouchUpdate(1, 50.0, 40.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  printf("OK\n");
+}
+
+void testExtraIgnore()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+  test.handleTouchUpdate(1, 40.0, 30.0);
+  test.handleTouchUpdate(1, 80.0, 30.0);
+
+  ASSERT_EQ(test.events.size(), 2);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureDrag);
+
+  ASSERT_EQ(test.events[1].type, GestureUpdate);
+  ASSERT_EQ(test.events[1].gesture, GestureDrag);
+
+  test.events.clear();
+
+  test.handleTouchBegin(2, 10.0, 10.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(1, 100.0, 50.0);
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureUpdate);
+  ASSERT_EQ(test.events[0].gesture, GestureDrag);
+  ASSERT_EQ(test.events[0].eventX, 100.0);
+  ASSERT_EQ(test.events[0].eventY, 50.0);
+
+  test.events.clear();
+
+  test.handleTouchEnd(1);
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureEnd);
+  ASSERT_EQ(test.events[0].gesture, GestureDrag);
+  ASSERT_EQ(test.events[0].eventX, 100.0);
+  ASSERT_EQ(test.events[0].eventY, 50.0);
+
+  printf("OK\n");
+}
+
+void testIgnoreWhenAwaitingGestureEnd()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+  test.handleTouchBegin(2, 30.0, 30.0);
+  test.handleTouchUpdate(1, 40.0, 30.0);
+  test.handleTouchUpdate(2, 90.0, 30.0);
+  test.handleTouchUpdate(1, 80.0, 30.0);
+
+  ASSERT_EQ(test.events.size(), 2);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureTwoDrag);
+
+  ASSERT_EQ(test.events[1].type, GestureUpdate);
+  ASSERT_EQ(test.events[1].gesture, GestureTwoDrag);
+
+  test.events.clear();
+
+  test.handleTouchEnd(1);
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureEnd);
+  ASSERT_EQ(test.events[0].gesture, GestureTwoDrag);
+
+  test.events.clear();
+
+  test.handleTouchBegin(3, 10.0, 10.0);
+  test.handleTouchEnd(3);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  printf("OK\n");
+}
+
+void testIgnoreAfterGesture()
+{
+  TestClass test;
+
+  printf("%s: ", __func__);
+
+  test.handleTouchBegin(1, 20.0, 30.0);
+  test.handleTouchUpdate(1, 40.0, 30.0);
+  test.handleTouchUpdate(1, 80.0, 30.0);
+
+  ASSERT_EQ(test.events.size(), 2);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureDrag);
+
+  ASSERT_EQ(test.events[1].type, GestureUpdate);
+  ASSERT_EQ(test.events[1].gesture, GestureDrag);
+
+  test.events.clear();
+
+  // Start ignored event
+  test.handleTouchBegin(2, 10.0, 10.0);
+
+  ASSERT_EQ(test.events.size(), 0);
+
+  test.handleTouchUpdate(1, 100.0, 50.0);
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureUpdate);
+  ASSERT_EQ(test.events[0].gesture, GestureDrag);
+  ASSERT_EQ(test.events[0].eventX, 100.0);
+  ASSERT_EQ(test.events[0].eventY, 50.0);
+
+  test.events.clear();
+
+  test.handleTouchEnd(1);
+
+  ASSERT_EQ(test.events.size(), 1);
+
+  ASSERT_EQ(test.events[0].type, GestureEnd);
+  ASSERT_EQ(test.events[0].gesture, GestureDrag);
+  ASSERT_EQ(test.events[0].eventX, 100.0);
+  ASSERT_EQ(test.events[0].eventY, 50.0);
+
+  // End ignored event
+  test.handleTouchEnd(2);
+
+  // Check that everything is reseted after trailing ignores are released
+  test.events.clear();
+
+  test.handleTouchBegin(3, 20.0, 30.0);
+  test.handleTouchEnd(3);
+
+  ASSERT_EQ(test.events.size(), 2);
+
+  ASSERT_EQ(test.events[0].type, GestureBegin);
+  ASSERT_EQ(test.events[0].gesture, GestureOneTap);
+  ASSERT_EQ(test.events[1].type, GestureEnd);
+  ASSERT_EQ(test.events[1].gesture, GestureOneTap);
+
+  printf("OK\n");
+}
+
+void testOneTap()
+{
+  testOneTapNormal();
+}
+
+void testTwoTap()
+{
+  testTwoTapNormal();
+  testTwoTapSlowBegin();
+  testTwoTapSlowEnd();
+  testTwoTapTimeout();
+}
+
+void testThreeTap()
+{
+  testThreeTapNormal();
+  testThreeTapSlowBegin();
+  testThreeTapSlowEnd();
+  testThreeTapDrag();
+  testThreeTapTimeout();
+}
+
+void testDrag()
+{
+  testDragHoriz();
+  testDragVert();
+  testDragDiag();
+}
+
+void testLongPress()
+{
+  testLongPressNormal();
+  testLongPressDrag();
+}
+
+void testTwoDrag()
+{
+  testTwoDragFastHoriz();
+  testTwoDragFastVert();
+  testTwoDragFastDiag();
+  testTwoDragEndBeforeTimeout();
+  testTwoDragStartHorizFromTimeout();
+  testTwoDragStartVertFromTimeout();
+  testTwoDragStartDiagFromTimeout();
+  testTwoDragTooSlow();
+}
+
+void testPinch()
+{
+  testPinchFastIn();
+  testPinchFastOut();
+  testPinchEndBeforeTimeout();
+  testPinchStartInFromTimeout();
+  testPinchStartOutFromTimeout();
+  testPinchTooSlow();
+}
+
+void testIgnore()
+{
+  testExtraIgnore();
+  testIgnoreWhenAwaitingGestureEnd();
+  testIgnoreAfterGesture();
+}
+
+int main(int argc, char** argv)
+{
+  testOneTap();
+  testTwoTap();
+  testThreeTap();
+  testDrag();
+  testLongPress();
+  testTwoDrag();
+  testPinch();
+  testIgnore();
+
+  return 0;
+}
diff --git a/vncviewer/BaseTouchHandler.cxx b/vncviewer/BaseTouchHandler.cxx
new file mode 100644 (file)
index 0000000..ad27ffd
--- /dev/null
@@ -0,0 +1,198 @@
+/* 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 = sqrt(ev.magnitudeX * ev.magnitudeX +
+                            ev.magnitudeY * 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 = sqrt(ev.magnitudeX * ev.magnitudeX +
+                       ev.magnitudeY * 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 = sqrt((dx * dx) + (dy * 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 (file)
index 0000000..d6d1a43
--- /dev/null
@@ -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
index 92b516cba18f1c7a59ea9c82e057965e7c7afff3..74e2deefd7b9958425de2817d8b5051b8c4d8399 100644 (file)
@@ -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
@@ -33,7 +34,7 @@ if(WIN32)
 elseif(APPLE)
   set(VNCVIEWER_SOURCES ${VNCVIEWER_SOURCES} cocoa.mm osx_to_qnum.c)
 else()
-  set(VNCVIEWER_SOURCES ${VNCVIEWER_SOURCES} XInputTouchHandler.cxx xkb_to_qnum.c)
+  set(VNCVIEWER_SOURCES ${VNCVIEWER_SOURCES} GestureHandler.cxx XInputTouchHandler.cxx xkb_to_qnum.c)
 endif()
 
 if(WIN32)
diff --git a/vncviewer/GestureEvent.h b/vncviewer/GestureEvent.h
new file mode 100644 (file)
index 0000000..146680b
--- /dev/null
@@ -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 (file)
index 0000000..6d9d8a4
--- /dev/null
@@ -0,0 +1,512 @@
+/* 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;
+
+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(50);
+      }
+    }
+
+    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 gesture 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 release 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 (file)
index 0000000..372b786
--- /dev/null
@@ -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__
index f64b06adb504f293c9eaca119e6b93dbaf922ccd..359802ea865bbc7226906b0a0f0708741cd5e6d9 100644 (file)
 
 #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"
@@ -39,7 +44,7 @@ static rfb::LogWriter vlog("XInputTouchHandler");
 static bool grabbed = false;
 
 XInputTouchHandler::XInputTouchHandler(Window wnd)
-  : wnd(wnd), fakeStateMask(0), trackingTouch(false)
+  : wnd(wnd), fakeStateMask(0)
 {
   XIEventMask eventmask;
   unsigned char flags[XIMaskLen(XI_LASTEVENT)] = { 0 };
@@ -232,9 +237,6 @@ void XInputTouchHandler::processEvent(const XIDeviceEvent* devev)
     fakeButtonEvent(false, devev->detail, devev);
     break;
   case XI_TouchBegin:
-    if (trackingTouch)
-      break;
-
     // XInput2 wants us to explicitly accept touch sequences
     // for grabbed devices before it will pass events
     if (grabbed) {
@@ -245,22 +247,13 @@ void XInputTouchHandler::processEvent(const XIDeviceEvent* devev)
                          XIAcceptTouch);
     }
 
-    trackingTouch = true;
-    trackedTouchPoint = devev->detail;
-
-    fakeMotionEvent(devev);
-    fakeButtonEvent(true, Button1, devev);
+    handleTouchBegin(devev->detail, devev->event_x, devev->event_y);
     break;
   case XI_TouchUpdate:
-    if (!trackingTouch || (devev->detail != trackedTouchPoint))
-      break;
-    fakeMotionEvent(devev);
+    handleTouchUpdate(devev->detail, devev->event_x, devev->event_y);
     break;
   case XI_TouchEnd:
-    if (!trackingTouch || (devev->detail != trackedTouchPoint))
-      break;
-    fakeButtonEvent(false, Button1, devev);
-    trackingTouch = false;
+    handleTouchEnd(devev->detail);
     break;
   case XI_TouchOwnership:
     // FIXME: Currently ignored, see constructor
@@ -297,13 +290,76 @@ void XInputTouchHandler::fakeMotionEvent(const XIDeviceEvent* origEvent)
   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 XIDeviceEvent* origEvent)
+                                         const GestureEvent origEvent)
 {
   XEvent fakeEvent;
 
@@ -315,6 +371,8 @@ void XInputTouchHandler::fakeButtonEvent(bool press, int button,
 
   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
@@ -325,6 +383,77 @@ void XInputTouchHandler::fakeButtonEvent(bool press, int 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?
index 065728874a970b2f1a659354cb05c9d62160ac91..6360b974ae90e0b857963d009f17d666111cd096 100644 (file)
 #ifndef __XINPUTTOUCHHANDLER_H__
 #define __XINPUTTOUCHHANDLER_H__
 
-class XInputTouchHandler {
+#include "BaseTouchHandler.h"
+#include "GestureHandler.h"
+
+class XInputTouchHandler: public BaseTouchHandler, GestureHandler {
   public:
     XInputTouchHandler(Window wnd);
 
@@ -35,14 +38,21 @@ class XInputTouchHandler {
     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;
-    bool trackingTouch;
-    int trackedTouchPoint;
 };
 
 #endif