diff options
Diffstat (limited to 'vncviewer/GestureHandler.cxx')
-rw-r--r-- | vncviewer/GestureHandler.cxx | 512 |
1 files changed, 512 insertions, 0 deletions
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 <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; +} |