From: Aaron Sowry Date: Fri, 24 May 2019 00:00:47 +0000 (+1200) Subject: Implement touch gesture handling on Unix X-Git-Tag: v1.10.90~27^2~1 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=95ad018961f384f6e43c80c4f5473064a6f3aedd;p=tigervnc.git Implement touch gesture handling on Unix Allows the user to perform certain important mouse operations using touch gestures instead. --- diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index 9129f9db..97cfcb27 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -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}") diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index e568660a..806d7272 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -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 index 00000000..a07ecf40 --- /dev/null +++ b/tests/unit/gesturehandler.cxx @@ -0,0 +1,1245 @@ +/* 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. + */ + +#include +#include + +#include + +#include "../../vncviewer/GestureHandler.h" + +class TestClass : public GestureHandler +{ + protected: + virtual void handleGestureEvent(const GestureEvent& event); + + public: + std::vector 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 index 00000000..ad27ffde --- /dev/null +++ b/vncviewer/BaseTouchHandler.cxx @@ -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 +#endif + +#include +#include + +#define XK_MISCELLANY +#include +#include + +#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 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 92b516cb..74e2deef 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 @@ -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 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..6d9d8a4e --- /dev/null +++ b/vncviewer/GestureHandler.cxx @@ -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 + #endif + +#include +#include + +#include +#include + +#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::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::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::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::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 +#include + +#include + +#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 tracked; + std::set ignored; + + bool waitingRelease; + struct timeval releaseStart; + + rfb::Timer longpressTimer; + rfb::Timer twoTouchTimer; +}; + +#endif // __GESTUREHANDLER_H__ diff --git a/vncviewer/XInputTouchHandler.cxx b/vncviewer/XInputTouchHandler.cxx index f64b06ad..359802ea 100644 --- a/vncviewer/XInputTouchHandler.cxx +++ b/vncviewer/XInputTouchHandler.cxx @@ -26,9 +26,14 @@ #include #include +#include #include +#ifndef XK_MISCELLANY +#define XK_MISCELLANY +#include +#endif #include #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? diff --git a/vncviewer/XInputTouchHandler.h b/vncviewer/XInputTouchHandler.h index 06572887..6360b974 100644 --- a/vncviewer/XInputTouchHandler.h +++ b/vncviewer/XInputTouchHandler.h @@ -20,7 +20,10 @@ #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