123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- /* Copyright 2019 Aaron Sowry for Cendio AB
- * Copyright 2020 Pierre Ossman for Cendio AB
- *
- * This is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This software is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this software; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
- * USA.
- */
-
- #ifdef HAVE_CONFIG_H
- #include <config.h>
- #endif
-
- #include <assert.h>
- #include <math.h>
-
- #include <rfb/util.h>
- #include <rfb/LogWriter.h>
-
- #include "GestureHandler.h"
-
- static rfb::LogWriter vlog("GestureHandler");
-
- static const unsigned char GH_NOGESTURE = 0;
- static const unsigned char GH_ONETAP = 1;
- static const unsigned char GH_TWOTAP = 2;
- static const unsigned char GH_THREETAP = 4;
- static const unsigned char GH_DRAG = 8;
- static const unsigned char GH_LONGPRESS = 16;
- static const unsigned char GH_TWODRAG = 32;
- static const unsigned char GH_PINCH = 64;
-
- static const unsigned char GH_INITSTATE = 127;
-
- const unsigned GH_MOVE_THRESHOLD = 50;
- const unsigned GH_ANGLE_THRESHOLD = 90; // Degrees
-
- // Timeout when waiting for gestures (ms)
- const unsigned GH_MULTITOUCH_TIMEOUT = 250;
-
- // Maximum time between press and release for a tap (ms)
- const unsigned GH_TAP_TIMEOUT = 1000;
-
- // Timeout when waiting for longpress (ms)
- const unsigned GH_LONGPRESS_TIMEOUT = 1000;
-
- // Timeout when waiting to decide between PINCH and TWODRAG (ms)
- const unsigned GH_TWOTOUCH_TIMEOUT = 50;
-
- GestureHandler::GestureHandler() :
- state(GH_INITSTATE), waitingRelease(false),
- longpressTimer(this), twoTouchTimer(this)
- {
- }
-
- GestureHandler::~GestureHandler()
- {
- }
-
- void GestureHandler::handleTouchBegin(int id, double x, double y)
- {
- GHTouch ght;
-
- // Ignore any new touches if there is already an active gesture,
- // or we're in a cleanup state
- if (hasDetectedGesture() || (state == GH_NOGESTURE)) {
- ignored.insert(id);
- return;
- }
-
- // Did it take too long between touches that we should no longer
- // consider this a single gesture?
- if ((tracked.size() > 0) &&
- (rfb::msSince(&tracked.begin()->second.started) > GH_MULTITOUCH_TIMEOUT)) {
- state = GH_NOGESTURE;
- ignored.insert(id);
- return;
- }
-
- // If we're waiting for fingers to release then we should no longer
- // recognize new touches
- if (waitingRelease) {
- state = GH_NOGESTURE;
- ignored.insert(id);
- return;
- }
-
- gettimeofday(&ght.started, NULL);
- ght.active = true;
- ght.lastX = ght.firstX = x;
- ght.lastY = ght.firstY = y;
- ght.angle = 0;
-
- tracked[id] = ght;
-
- switch (tracked.size()) {
- case 1:
- longpressTimer.start(GH_LONGPRESS_TIMEOUT);
- break;
-
- case 2:
- state &= ~(GH_ONETAP | GH_DRAG | GH_LONGPRESS);
- longpressTimer.stop();
- break;
-
- case 3:
- state &= ~(GH_TWOTAP | GH_TWODRAG | GH_PINCH);
- break;
-
- default:
- state = GH_NOGESTURE;
- }
- }
-
- void GestureHandler::handleTouchUpdate(int id, double x, double y)
- {
- GHTouch *touch, *prevTouch;
- double deltaX, deltaY, prevDeltaMove;
- unsigned deltaAngle;
-
- // If this is an update for a touch we're not tracking, ignore it
- if (tracked.count(id) == 0)
- return;
-
- touch = &tracked[id];
-
- // Update the touches last position with the event coordinates
- touch->lastX = x;
- touch->lastY = y;
-
- deltaX = x - touch->firstX;
- deltaY = y - touch->firstY;
-
- // Update angle when the touch has moved
- if ((touch->firstX != touch->lastX) ||
- (touch->firstY != touch->lastY))
- touch->angle = atan2(deltaY, deltaX) * 180 / M_PI;
-
- if (!hasDetectedGesture()) {
- // Ignore moves smaller than the minimum threshold
- if (hypot(deltaX, deltaY) < GH_MOVE_THRESHOLD)
- return;
-
- // Can't be a tap or long press as we've seen movement
- state &= ~(GH_ONETAP | GH_TWOTAP | GH_THREETAP | GH_LONGPRESS);
- longpressTimer.stop();
-
- if (tracked.size() != 1)
- state &= ~(GH_DRAG);
- if (tracked.size() != 2)
- state &= ~(GH_TWODRAG | GH_PINCH);
-
- // We need to figure out which of our different two touch gestures
- // this might be
- if (tracked.size() == 2) {
-
- // The other touch can be first or last in tracked
- // depending on which event came first
- prevTouch = &tracked.rbegin()->second;
- if (prevTouch == touch)
- prevTouch = &tracked.begin()->second;
-
- // How far the previous touch point has moved since start
- prevDeltaMove = hypot(prevTouch->firstX - prevTouch->lastX,
- prevTouch->firstY - prevTouch->lastY);
-
- // We know that the current touch moved far enough,
- // but unless both touches moved further than their
- // threshold we don't want to disqualify any gestures
- if (prevDeltaMove > GH_MOVE_THRESHOLD) {
-
- // The angle difference between the direction of the touch points
- deltaAngle = fabs(touch->angle - prevTouch->angle);
- deltaAngle = fabs(((deltaAngle + 180) % 360) - 180);
-
- // PINCH or TWODRAG can be eliminated depending on the angle
- if (deltaAngle > GH_ANGLE_THRESHOLD)
- state &= ~GH_TWODRAG;
- else
- state &= ~GH_PINCH;
-
- if (twoTouchTimer.isStarted())
- twoTouchTimer.stop();
-
- } else if (!twoTouchTimer.isStarted()) {
- // We can't determine the gesture right now, let's
- // wait and see if more events are on their way
- twoTouchTimer.start(GH_TWOTOUCH_TIMEOUT);
- }
- }
-
- if (!hasDetectedGesture())
- return;
-
- pushEvent(GestureBegin);
- }
-
- pushEvent(GestureUpdate);
- }
-
- void GestureHandler::handleTouchEnd(int id)
- {
- std::map<int, GHTouch>::const_iterator iter;
-
- // Check if this is an ignored touch
- if (ignored.count(id)) {
- ignored.erase(id);
- if (ignored.empty() && tracked.empty()) {
- state = GH_INITSTATE;
- waitingRelease = false;
- }
- return;
- }
-
- // We got a TouchEnd before the timer triggered,
- // this cannot result in a gesture anymore.
- if (!hasDetectedGesture() && twoTouchTimer.isStarted()) {
- twoTouchTimer.stop();
- state = GH_NOGESTURE;
- }
-
- // Some gestures don't trigger until a touch is released
- if (!hasDetectedGesture()) {
- // Can't be a gesture that relies on movement
- state &= ~(GH_DRAG | GH_TWODRAG | GH_PINCH);
- // Or something that relies on more time
- state &= ~GH_LONGPRESS;
- longpressTimer.stop();
-
- if (!waitingRelease) {
- gettimeofday(&releaseStart, NULL);
- waitingRelease = true;
-
- // Can't be a tap that requires more touches than we current have
- switch (tracked.size()) {
- case 1:
- state &= ~(GH_TWOTAP | GH_THREETAP);
- break;
-
- case 2:
- state &= ~(GH_ONETAP | GH_THREETAP);
- break;
- }
- }
- }
-
- // Waiting for all touches to release? (i.e. some tap)
- if (waitingRelease) {
- // Were all touches released at roughly the same time?
- if (rfb::msSince(&releaseStart) > GH_MULTITOUCH_TIMEOUT)
- state = GH_NOGESTURE;
-
- // Did too long time pass between press and release?
- for (iter = tracked.begin(); iter != tracked.end(); ++iter) {
- if (rfb::msSince(&iter->second.started) > GH_TAP_TIMEOUT) {
- state = GH_NOGESTURE;
- break;
- }
- }
-
- tracked[id].active = false;
-
- // Are we still waiting for more releases?
- if (hasDetectedGesture()) {
- pushEvent(GestureBegin);
- } else {
- // Have we reached a dead end?
- if (state != GH_NOGESTURE)
- return;
- }
- }
-
- if (hasDetectedGesture())
- pushEvent(GestureEnd);
-
- // Ignore any remaining touches until they are ended
- for (iter = tracked.begin(); iter != tracked.end(); ++iter) {
- if (iter->second.active)
- ignored.insert(iter->first);
- }
- tracked.clear();
-
- state = GH_NOGESTURE;
-
- ignored.erase(id);
- if (ignored.empty()) {
- state = GH_INITSTATE;
- waitingRelease = false;
- }
- }
-
- bool GestureHandler::hasDetectedGesture()
- {
- if (state == GH_NOGESTURE)
- return false;
- // Check to see if the bitmask value is a power of 2
- // (i.e. only one bit set). If it is, we have a state.
- if (state & (state - 1))
- return false;
-
- // For taps we also need to have all touches released
- // before we've fully detected the gesture
- if (state & (GH_ONETAP | GH_TWOTAP | GH_THREETAP)) {
- std::map<int, GHTouch>::const_iterator iter;
-
- // Any touch still active/pressed?
- for (iter = tracked.begin(); iter != tracked.end(); ++iter) {
- if (iter->second.active)
- return false;
- }
- }
-
- return true;
- }
-
- bool GestureHandler::handleTimeout(rfb::Timer* t)
- {
- if (t == &longpressTimer)
- longpressTimeout();
- else if (t == &twoTouchTimer)
- twoTouchTimeout();
-
- return false;
- }
-
- void GestureHandler::longpressTimeout()
- {
- assert(!hasDetectedGesture());
-
- state = GH_LONGPRESS;
- pushEvent(GestureBegin);
- }
-
- void GestureHandler::twoTouchTimeout()
- {
- double avgMoveH, avgMoveV, fdx, fdy, ldx, ldy, deltaTouchDistance;
-
- assert(!tracked.empty());
-
- // How far each touch point has moved since start
- getAverageMovement(&avgMoveH, &avgMoveV);
- avgMoveH = fabs(avgMoveH);
- avgMoveV = fabs(avgMoveV);
-
- // The difference in the distance between where
- // the touch points started and where they are now
- getAverageDistance(&fdx, &fdy, &ldx, &ldy);
- deltaTouchDistance = fabs(hypot(fdx, fdy) - hypot(ldx, ldy));
-
- if ((avgMoveV < deltaTouchDistance) &&
- (avgMoveH < deltaTouchDistance))
- state = GH_PINCH;
- else
- state = GH_TWODRAG;
-
- pushEvent(GestureBegin);
- pushEvent(GestureUpdate);
- }
-
- void GestureHandler::pushEvent(GestureEventType t)
- {
- GestureEvent gev;
- double avgX, avgY;
-
- gev.type = t;
- gev.gesture = stateToGesture(state);
-
- // For most gesture events the current (average) position is the
- // most useful
- getPosition(NULL, NULL, &avgX, &avgY);
-
- // However we have a slight distance to detect gestures, so for the
- // first gesture event we want to use the first positions we saw
- if (t == GestureBegin)
- getPosition(&avgX, &avgY, NULL, NULL);
-
- // For these gestures, we always want the event coordinates
- // to be where the gesture began, not the current touch location.
- switch (state) {
- case GH_TWODRAG:
- case GH_PINCH:
- getPosition(&avgX, &avgY, NULL, NULL);
- break;
- }
-
- gev.eventX = avgX;
- gev.eventY = avgY;
-
- // Some gestures also have a magnitude
- if (state == GH_PINCH) {
- if (t == GestureBegin)
- getAverageDistance(&gev.magnitudeX, &gev.magnitudeY,
- NULL, NULL);
- else
- getAverageDistance(NULL, NULL,
- &gev.magnitudeX, &gev.magnitudeY);
- } else if (state == GH_TWODRAG) {
- if (t == GestureBegin)
- gev.magnitudeX = gev.magnitudeY = 0;
- else
- getAverageMovement(&gev.magnitudeX, &gev.magnitudeY);
- }
-
- handleGestureEvent(gev);
- }
-
- GestureEventGesture GestureHandler::stateToGesture(unsigned char state)
- {
- switch (state) {
- case GH_ONETAP:
- return GestureOneTap;
- case GH_TWOTAP:
- return GestureTwoTap;
- case GH_THREETAP:
- return GestureThreeTap;
- case GH_DRAG:
- return GestureDrag;
- case GH_LONGPRESS:
- return GestureLongPress;
- case GH_TWODRAG:
- return GestureTwoDrag;
- case GH_PINCH:
- return GesturePinch;
- }
-
- assert(false);
-
- return (GestureEventGesture)0;
- }
-
- void GestureHandler::getPosition(double *firstX, double *firstY,
- double *lastX, double *lastY)
- {
- size_t size;
- double fx = 0, fy = 0, lx = 0, ly = 0;
-
- assert(!tracked.empty());
-
- size = tracked.size();
-
- std::map<int, GHTouch>::const_iterator iter;
- for (iter = tracked.begin(); iter != tracked.end(); ++iter) {
- fx += iter->second.firstX;
- fy += iter->second.firstY;
- lx += iter->second.lastX;
- ly += iter->second.lastY;
- }
-
- if (firstX)
- *firstX = fx / size;
- if (firstY)
- *firstY = fy / size;
- if (lastX)
- *lastX = lx / size;
- if (lastY)
- *lastY = ly / size;
- }
-
- void GestureHandler::getAverageMovement(double *h, double *v)
- {
- double totalH, totalV;
- size_t size;
-
- assert(!tracked.empty());
-
- totalH = totalV = 0;
- size = tracked.size();
-
- std::map<int, GHTouch>::const_iterator iter;
- for (iter = tracked.begin(); iter != tracked.end(); ++iter) {
- totalH += iter->second.lastX - iter->second.firstX;
- totalV += iter->second.lastY - iter->second.firstY;
- }
-
- if (h)
- *h = totalH / size;
- if (v)
- *v = totalV / size;
- }
-
- void GestureHandler::getAverageDistance(double *firstX, double *firstY,
- double *lastX, double *lastY)
- {
- double dx, dy;
-
- assert(!tracked.empty());
-
- // Distance between the first and last tracked touches
-
- dx = fabs(tracked.rbegin()->second.firstX - tracked.begin()->second.firstX);
- dy = fabs(tracked.rbegin()->second.firstY - tracked.begin()->second.firstY);
-
- if (firstX)
- *firstX = dx;
- if (firstY)
- *firstY = dy;
-
- dx = fabs(tracked.rbegin()->second.lastX - tracked.begin()->second.lastX);
- dy = fabs(tracked.rbegin()->second.lastY - tracked.begin()->second.lastY);
-
- if (lastX)
- *lastX = dx;
- if (lastY)
- *lastY = dy;
- }
|