Calculate velocity in QMutEventPoint::setTimestamp() with Kalman filter

This functionality was only in Qt Quick in Qt 5.  Now we move it up to QtGui
so that every QEventPoint will have a valid velocity() before being delivered
anywhere.

[ChangeLog][QtGui][QPointerEvent] Every QEventPoint should now carry a valid
velocity(): if the operating system doesn't provide it, Qt will calculate it,
using a simple Kalman filter to provide a weighted average over time.

Fixes: QTBUG-33891
Change-Id: I40352f717f0ad6edd87cf71ef55e955a591eeea1
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
This commit is contained in:
Shawn Rutledge 2020-08-19 22:45:17 +02:00
parent 2692237bb1
commit 1fdbbb49d9
5 changed files with 167 additions and 6 deletions

View File

@ -62,6 +62,7 @@
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
Q_LOGGING_CATEGORY(lcPointerGrab, "qt.pointer.grab") Q_LOGGING_CATEGORY(lcPointerGrab, "qt.pointer.grab")
Q_LOGGING_CATEGORY(lcPointerVel, "qt.pointer.velocity")
Q_LOGGING_CATEGORY(lcEPDetach, "qt.pointer.eventpoint.detach") Q_LOGGING_CATEGORY(lcEPDetach, "qt.pointer.eventpoint.detach")
/*! /*!
@ -335,6 +336,14 @@ QSizeF QEventPoint::ellipseDiameters() const
bool QEventPoint::isAccepted() const bool QEventPoint::isAccepted() const
{ return d->accept; } { return d->accept; }
/*!
Returns the time from the previous QPointerEvent that contained this point.
\sa globalLastPosition()
*/
ulong QEventPoint::lastTimestamp() const
{ return d->lastTimestamp; }
/*! /*!
Sets the accepted state of the point. Sets the accepted state of the point.
@ -487,14 +496,30 @@ void QMutableEventPoint::updateFrom(const QEventPoint &other)
} }
/*! \internal /*! \internal
Set the timestamp from the event that updated this point's positions. Set the timestamp from the event that updated this point's positions,
and calculate a new value for velocity().
The velocity calculation is done here because none of the QPointerEvent
subclass constructors take the timestamp directly, and because
QGuiApplication traditionally constructs an event first and then sets its
timestamp (see for example QGuiApplicationPrivate::processMouseEvent()).
This function looks up the corresponding instance in QPointingDevicePrivate::activePoints,
and assumes that its timestamp() still holds the previous time when this point
was updated, its velocity() holds this point's last-known velocity, and
its globalPosition() and globalLastPosition() hold this point's current
and previous positions, respectively. We assume timestamps are in milliseconds.
The velocity calculation is skipped if the platform has promised to
provide velocities already by setting the QInputDevice::Velocity capability.
*/ */
void QMutableEventPoint::setTimestamp(const ulong t) void QMutableEventPoint::setTimestamp(const ulong t)
{ {
// On mouse press, if the mouse has moved from its last-known location, // On mouse press, if the mouse has moved from its last-known location,
// QGuiApplicationPrivate::processMouseEvent() sends first a mouse move and // QGuiApplicationPrivate::processMouseEvent() sends first a mouse move and
// then a press. Both events will get the same timestamp. So we need to set // then a press. Both events will get the same timestamp. So we need to set
// the press timestamp and position even when the timestamp isn't advancing. // the press timestamp and position even when the timestamp isn't advancing,
// but skip setting lastTimestamp and velocity because those need a time delta.
if (state() == QEventPoint::State::Pressed) { if (state() == QEventPoint::State::Pressed) {
d->pressTimestamp = t; d->pressTimestamp = t;
d->globalPressPos = d->globalPos; d->globalPressPos = d->globalPos;
@ -502,6 +527,33 @@ void QMutableEventPoint::setTimestamp(const ulong t)
if (d->timestamp == t) if (d->timestamp == t)
return; return;
detach(); detach();
if (device()) {
// get the persistent instance out of QPointingDevicePrivate::activePoints
// (which sometimes might be the same as this instance)
QEventPointPrivate *pd = QPointingDevicePrivate::get(
const_cast<QPointingDevice *>(d->device))->pointById(id())->eventPoint.d;
if (t > pd->timestamp) {
pd->lastTimestamp = pd->timestamp;
pd->timestamp = t;
if (state() == QEventPoint::State::Pressed)
pd->pressTimestamp = t;
if (pd->lastTimestamp > 0 && !device()->capabilities().testFlag(QInputDevice::Capability::Velocity)) {
// calculate instantaneous velocity according to time and distance moved since the previous point
QVector2D newVelocity = QVector2D(pd->globalPos - pd->globalLastPos) / (t - pd->lastTimestamp) * 1000;
// VERY simple kalman filter: does a weighted average
// where the older velocities get less and less significant
static const float KalmanGain = 0.7f;
pd->velocity = newVelocity * KalmanGain + pd->velocity * (1.0f - KalmanGain);
qCDebug(lcPointerVel) << "velocity" << newVelocity << "filtered" << pd->velocity <<
"based on movement" << pd->globalLastPos << "->" << pd->globalPos <<
"over time" << pd->lastTimestamp << "->" << pd->timestamp;
}
if (d != pd) {
d->lastTimestamp = pd->lastTimestamp;
d->velocity = pd->velocity;
}
}
}
d->timestamp = t; d->timestamp = t;
} }
@ -4820,10 +4872,17 @@ bool QTouchEvent::isReleaseEvent() const
/*! /*!
\fn QVector2D QEventPoint::velocity() const \fn QVector2D QEventPoint::velocity() const
Returns a velocity vector for this point. Returns a velocity vector, in units of pixels per second, in the coordinate
The vector is in the screen's coordinate system, using pixels per seconds for the magnitude. system of the screen or desktop.
\note The returned vector is only valid if the device's capabilities include QInputDevice::Velocity. \note If the device's capabilities include QInputDevice::Velocity, it means
velocity comes from the operating system (perhaps the touch hardware or
driver provides it). But usually the \c Velocity capability is not set,
indicating that the velocity is calculated by Qt, using a simple Kalman
filter to provide a smoothed average velocity rather than an instantaneous
value. Effectively it tells how fast and in what direction the user has
been dragging this point over the last few events, with the most recent
event having the strongest influence.
\sa QInputDevice::capabilities(), QInputEvent::device() \sa QInputDevice::capabilities(), QInputEvent::device()
*/ */

View File

@ -157,6 +157,7 @@ public:
int id() const; int id() const;
QPointingDeviceUniqueId uniqueId() const; QPointingDeviceUniqueId uniqueId() const;
ulong timestamp() const; ulong timestamp() const;
ulong lastTimestamp() const;
ulong pressTimestamp() const; ulong pressTimestamp() const;
qreal timeHeld() const; qreal timeHeld() const;
qreal pressure() const; qreal pressure() const;

View File

@ -81,6 +81,7 @@ struct QEventPointPrivate {
QSizeF ellipseDiameters = QSizeF(0, 0); QSizeF ellipseDiameters = QSizeF(0, 0);
QVector2D velocity; QVector2D velocity;
ulong timestamp = 0; ulong timestamp = 0;
ulong lastTimestamp = 0;
ulong pressTimestamp = 0; ulong pressTimestamp = 0;
QPointingDeviceUniqueId uniqueId; QPointingDeviceUniqueId uniqueId;
int refCount = 1; int refCount = 1;

View File

@ -1,6 +1,6 @@
/**************************************************************************** /****************************************************************************
** **
** Copyright (C) 2016 The Qt Company Ltd. ** Copyright (C) 2020 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/ ** Contact: https://www.qt.io/licensing/
** **
** This file is part of the test suite of the Qt Toolkit. ** This file is part of the test suite of the Qt Toolkit.
@ -51,11 +51,17 @@ public:
int mouseReleaseButton; int mouseReleaseButton;
int mouseReleaseButtons; int mouseReleaseButtons;
int mouseReleaseModifiers; int mouseReleaseModifiers;
ulong timestamp;
ulong pressTimestamp; ulong pressTimestamp;
ulong lastTimestamp;
QVector2D velocity;
protected: protected:
void mousePressEvent(QMouseEvent *e) override void mousePressEvent(QMouseEvent *e) override
{ {
const auto &firstPoint = e->point(0); const auto &firstPoint = e->point(0);
qCDebug(lcTests) << e << firstPoint;
timestamp = firstPoint.timestamp();
lastTimestamp = firstPoint.lastTimestamp();
if (e->type() == QEvent::MouseButtonPress) { if (e->type() == QEvent::MouseButtonPress) {
auto firstPoint = e->points().first(); auto firstPoint = e->points().first();
QCOMPARE(e->exclusiveGrabber(firstPoint), nullptr); QCOMPARE(e->exclusiveGrabber(firstPoint), nullptr);
@ -77,12 +83,23 @@ protected:
if (grabPassive) if (grabPassive)
e->addPassiveGrabber(firstPoint, this); e->addPassiveGrabber(firstPoint, this);
} }
void mouseMoveEvent(QMouseEvent *e) override
{
qCDebug(lcTests) << e << e->points().first();
timestamp = e->points().first().timestamp();
lastTimestamp = e->points().first().lastTimestamp();
velocity = e->points().first().velocity();
}
void mouseReleaseEvent(QMouseEvent *e) override void mouseReleaseEvent(QMouseEvent *e) override
{ {
qCDebug(lcTests) << e << e->points().first();
QWindow::mouseReleaseEvent(e); QWindow::mouseReleaseEvent(e);
mouseReleaseButton = e->button(); mouseReleaseButton = e->button();
mouseReleaseButtons = e->buttons(); mouseReleaseButtons = e->buttons();
mouseReleaseModifiers = e->modifiers(); mouseReleaseModifiers = e->modifiers();
timestamp = e->points().first().timestamp();
lastTimestamp = e->points().first().lastTimestamp();
velocity = e->points().first().velocity();
mouseReleaseEventRecieved = true; mouseReleaseEventRecieved = true;
e->accept(); e->accept();
} }
@ -104,6 +121,7 @@ private slots:
void checkMouseReleaseEvent(); void checkMouseReleaseEvent();
void grabbers_data(); void grabbers_data();
void grabbers(); void grabbers();
void velocity();
private: private:
MouseEventWidget* testMouseWidget; MouseEventWidget* testMouseWidget;
@ -279,5 +297,41 @@ void tst_QMouseEvent::grabbers()
QCOMPARE(firstEPD->passiveGrabbers.count(), 0); QCOMPARE(firstEPD->passiveGrabbers.count(), 0);
} }
void tst_QMouseEvent::velocity()
{
testMouseWidget->grabExclusive = true;
auto devPriv = QPointingDevicePrivate::get(const_cast<QPointingDevice *>(QPointingDevice::primaryPointingDevice()));
devPriv->activePoints.clear();
qCDebug(lcTests) << "sending mouse press event";
QPoint pos(10, 10);
QTest::mousePress(testMouseWidget, Qt::LeftButton, Qt::KeyboardModifiers(), pos);
QCOMPARE(devPriv->activePoints.count(), 1);
QVERIFY(devPriv->activePoints.count() <= 2);
const auto &firstPoint = devPriv->pointById(0)->eventPoint;
QVERIFY(firstPoint.timestamp() > 0);
QCOMPARE(firstPoint.state(), QEventPoint::State::Pressed);
ulong timestamp = firstPoint.timestamp();
for (int i = 1; i < 4; ++i) {
qCDebug(lcTests) << "sending mouse move event" << i;
pos += {10, 10};
QTest::mouseMove(testMouseWidget, pos, 1);
qApp->processEvents();
qCDebug(lcTests) << firstPoint;
// currently we expect it to be updated in-place in devPriv->activePoints
QVERIFY(firstPoint.timestamp() > timestamp);
QVERIFY(testMouseWidget->timestamp > testMouseWidget->lastTimestamp);
QCOMPARE(testMouseWidget->timestamp, firstPoint.timestamp());
timestamp = firstPoint.timestamp();
QVERIFY(testMouseWidget->velocity.x() > 0);
QVERIFY(testMouseWidget->velocity.y() > 0);
}
QTest::mouseRelease(testMouseWidget, Qt::LeftButton, Qt::KeyboardModifiers(), pos, 1);
qCDebug(lcTests) << firstPoint;
QVERIFY(testMouseWidget->velocity.x() > 0);
QVERIFY(testMouseWidget->velocity.y() > 0);
}
QTEST_MAIN(tst_QMouseEvent) QTEST_MAIN(tst_QMouseEvent)
#include "tst_qmouseevent.moc" #include "tst_qmouseevent.moc"

View File

@ -215,10 +215,16 @@ struct GrabberWindow : public QWindow
{ {
bool grabExclusive = false; bool grabExclusive = false;
bool grabPassive = false; bool grabPassive = false;
QVector2D velocity;
ulong timestamp;
ulong lastTimestamp;
void touchEvent(QTouchEvent *ev) override { void touchEvent(QTouchEvent *ev) override {
qCDebug(lcTests) << ev; qCDebug(lcTests) << ev;
const auto &firstPoint = ev->point(0); const auto &firstPoint = ev->point(0);
velocity = firstPoint.velocity();
timestamp = firstPoint.timestamp();
lastTimestamp = firstPoint.lastTimestamp();
switch (ev->type()) { switch (ev->type()) {
case QEvent::TouchBegin: { case QEvent::TouchBegin: {
QCOMPARE(ev->exclusiveGrabber(firstPoint), nullptr); QCOMPARE(ev->exclusiveGrabber(firstPoint), nullptr);
@ -264,6 +270,7 @@ private slots:
void testMultiDevice(); void testMultiDevice();
void grabbers_data(); void grabbers_data();
void grabbers(); void grabbers();
void velocity();
private: private:
QPointingDevice *touchScreenDevice; QPointingDevice *touchScreenDevice;
@ -1942,6 +1949,45 @@ void tst_QTouchEvent::grabbers()
QTRY_COMPARE(devPriv->activePoints.count(), 0); QTRY_COMPARE(devPriv->activePoints.count(), 0);
} }
void tst_QTouchEvent::velocity()
{
GrabberWindow w;
w.grabExclusive = true;
w.setGeometry(100, 100, 100, 100);
w.show();
QVERIFY(QTest::qWaitForWindowExposed(&w));
auto devPriv = QPointingDevicePrivate::get(touchScreenDevice);
devPriv->activePoints.clear();
QPoint pos(10, 10);
QTest::touchEvent(&w, touchScreenDevice).press(0, pos, &w);
QCOMPARE(devPriv->activePoints.count(), 1);
const auto &firstPoint = devPriv->pointById(0)->eventPoint;
qCDebug(lcTests) << "persistent active point after press" << firstPoint;
QCOMPARE(firstPoint.velocity(), QVector2D());
QCOMPARE(firstPoint.pressTimestamp(), firstPoint.timestamp());
QVERIFY(firstPoint.timestamp() > 0);
QCOMPARE(firstPoint.state(), QEventPoint::State::Pressed);
ulong timestamp = firstPoint.timestamp();
for (int i = 1; i < 4; ++i) {
qCDebug(lcTests) << "sending touch move event" << i;
pos += {10, 10};
QTest::touchEvent(&w, touchScreenDevice).move(0, pos, &w);
qCDebug(lcTests) << firstPoint;
QVERIFY(firstPoint.timestamp() > timestamp);
QVERIFY(w.timestamp > w.lastTimestamp);
QCOMPARE(w.timestamp, firstPoint.timestamp());
timestamp = firstPoint.timestamp();
QVERIFY(w.velocity.x() > 0);
QVERIFY(w.velocity.y() > 0);
}
QTest::touchEvent(&w, touchScreenDevice).release(0, pos, &w);
QVERIFY(w.velocity.x() > 0);
QVERIFY(w.velocity.y() > 0);
}
QTEST_MAIN(tst_QTouchEvent) QTEST_MAIN(tst_QTouchEvent)
#include "tst_qtouchevent.moc" #include "tst_qtouchevent.moc"