From cad7ffa361381556620ab826dd60adf0b080367c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 3 Jan 2017 13:06:15 +0100 Subject: QQuickRangeSlider: handle touch events This makes it possible to interact with both handles and multiple sliders at the same time. Change-Id: Iba47b8ec31619b3dbec09dbc9ea176735f984e8b Reviewed-by: Mitch Curtis --- src/quicktemplates2/qquickrangeslider.cpp | 133 +++++++++++++---- src/quicktemplates2/qquickrangeslider_p.h | 2 + tests/auto/controls/data/tst_rangeslider.qml | 216 ++++++++++++++++++++++++++- 3 files changed, 325 insertions(+), 26 deletions(-) diff --git a/src/quicktemplates2/qquickrangeslider.cpp b/src/quicktemplates2/qquickrangeslider.cpp index 460bbffc..6a629d62 100644 --- a/src/quicktemplates2/qquickrangeslider.cpp +++ b/src/quicktemplates2/qquickrangeslider.cpp @@ -92,7 +92,8 @@ public: handle(nullptr), slider(slider), pressed(false), - hovered(false) + hovered(false), + touchId(-1) { } @@ -103,9 +104,6 @@ public: static QQuickRangeSliderNodePrivate *get(QQuickRangeSliderNode *node); -private: - friend class QQuickRangeSlider; - qreal value; bool isPendingValue; qreal pendingValue; @@ -114,6 +112,7 @@ private: QQuickRangeSlider *slider; bool pressed; bool hovered; + int touchId; }; bool QQuickRangeSliderNodePrivate::isFirst() const @@ -327,9 +326,10 @@ public: { } - void handlePress(const QPointF &point); - void handleMove(const QPointF &point); - void handleRelease(const QPointF &point); + QQuickRangeSliderNode *pressedNode(int touchId = -1) const; + void handlePress(const QPointF &point, int touchId = -1); + void handleMove(const QPointF &point, int touchId = -1); + void handleRelease(const QPointF &point, int touchId = -1); void handleUngrab(); void updateHover(const QPointF &pos); @@ -384,15 +384,26 @@ static qreal positionAt(const QQuickRangeSlider *slider, QQuickItem *handle, con return 0; } -void QQuickRangeSliderPrivate::handlePress(const QPointF &point) +QQuickRangeSliderNode *QQuickRangeSliderPrivate::pressedNode(int touchId) const +{ + if (touchId == -1) + return first->isPressed() ? first : (second->isPressed() ? second : nullptr); + if (QQuickRangeSliderNodePrivate::get(first)->touchId == touchId) + return first; + if (QQuickRangeSliderNodePrivate::get(second)->touchId == touchId) + return second; + return nullptr; +} + +void QQuickRangeSliderPrivate::handlePress(const QPointF &point, int touchId) { Q_Q(QQuickRangeSlider); pressPoint = point; QQuickItem *firstHandle = first->handle(); QQuickItem *secondHandle = second->handle(); - const bool firstHit = firstHandle && firstHandle->contains(q->mapToItem(firstHandle, point)); - const bool secondHit = secondHandle && secondHandle->contains(q->mapToItem(secondHandle, point)); + const bool firstHit = firstHandle && !first->isPressed() && firstHandle->contains(q->mapToItem(firstHandle, point)); + const bool secondHit = secondHandle && !second->isPressed() && secondHandle->contains(q->mapToItem(secondHandle, point)); QQuickRangeSliderNode *hitNode = nullptr; QQuickRangeSliderNode *otherNode = nullptr; @@ -436,17 +447,18 @@ void QQuickRangeSliderPrivate::handlePress(const QPointF &point) if (hitNode) { hitNode->setPressed(true); hitNode->handle()->setZ(1); + QQuickRangeSliderNodePrivate::get(hitNode)->touchId = touchId; } if (otherNode) otherNode->handle()->setZ(0); } -void QQuickRangeSliderPrivate::handleMove(const QPointF &point) +void QQuickRangeSliderPrivate::handleMove(const QPointF &point, int touchId) { Q_Q(QQuickRangeSlider); if (!q->keepMouseGrab()) return; - QQuickRangeSliderNode *pressedNode = first->isPressed() ? first : (second->isPressed() ? second : nullptr); + QQuickRangeSliderNode *pressedNode = QQuickRangeSliderPrivate::pressedNode(touchId); if (pressedNode) { qreal pos = positionAt(q, pressedNode->handle(), point); if (snapMode == QQuickRangeSlider::SnapAlways) @@ -458,27 +470,29 @@ void QQuickRangeSliderPrivate::handleMove(const QPointF &point) } } -void QQuickRangeSliderPrivate::handleRelease(const QPointF &point) +void QQuickRangeSliderPrivate::handleRelease(const QPointF &point, int touchId) { Q_Q(QQuickRangeSlider); pressPoint = QPointF(); - if (!q->keepMouseGrab()) - return; - QQuickRangeSliderNode *pressedNode = first->isPressed() ? first : (second->isPressed() ? second : nullptr); + QQuickRangeSliderNode *pressedNode = QQuickRangeSliderPrivate::pressedNode(touchId); if (!pressedNode) return; + QQuickRangeSliderNodePrivate *pressedNodePrivate = QQuickRangeSliderNodePrivate::get(pressedNode); - qreal pos = positionAt(q, pressedNode->handle(), point); - if (snapMode != QQuickRangeSlider::NoSnap) - pos = snapPosition(q, pos); - qreal val = valueAt(q, pos); - if (!qFuzzyCompare(val, pressedNode->value())) - pressedNode->setValue(val); - else if (snapMode != QQuickRangeSlider::NoSnap) - QQuickRangeSliderNodePrivate::get(pressedNode)->setPosition(pos); - q->setKeepMouseGrab(false); + if (q->keepMouseGrab()) { + qreal pos = positionAt(q, pressedNode->handle(), point); + if (snapMode != QQuickRangeSlider::NoSnap) + pos = snapPosition(q, pos); + qreal val = valueAt(q, pos); + if (!qFuzzyCompare(val, pressedNode->value())) + pressedNode->setValue(val); + else if (snapMode != QQuickRangeSlider::NoSnap) + pressedNodePrivate->setPosition(pos); + q->setKeepMouseGrab(false); + } pressedNode->setPressed(false); + pressedNodePrivate->touchId = -1; } void QQuickRangeSliderPrivate::handleUngrab() @@ -486,6 +500,8 @@ void QQuickRangeSliderPrivate::handleUngrab() pressPoint = QPointF(); first->setPressed(false); second->setPressed(false); + QQuickRangeSliderNodePrivate::get(first)->touchId = -1; + QQuickRangeSliderNodePrivate::get(second)->touchId = -1; } void QQuickRangeSliderPrivate::updateHover(const QPointF &pos) @@ -976,6 +992,73 @@ void QQuickRangeSlider::mouseUngrabEvent() d->handleUngrab(); } +void QQuickRangeSlider::touchEvent(QTouchEvent *event) +{ + Q_D(QQuickRangeSlider); + switch (event->type()) { + case QEvent::TouchBegin: + if (!d->first->isPressed() || !d->second->isPressed()) { + const QTouchEvent::TouchPoint point = event->touchPoints().first(); + d->handlePress(point.pos(), point.id()); + } else { + event->ignore(); + } + break; + + case QEvent::TouchUpdate: + for (const QTouchEvent::TouchPoint &point : event->touchPoints()) { + switch (point.state()) { + case Qt::TouchPointPressed: + if (!d->first->isPressed() || !d->second->isPressed()) + d->handlePress(point.pos(), point.id()); + break; + case Qt::TouchPointMoved: + if (!keepMouseGrab()) { + if (d->orientation == Qt::Horizontal) + setKeepMouseGrab(QQuickWindowPrivate::dragOverThreshold(point.pos().x() - point.startPos().x(), Qt::XAxis, &point)); + else + setKeepMouseGrab(QQuickWindowPrivate::dragOverThreshold(point.pos().y() - point.startPos().y(), Qt::YAxis, &point)); + } + if (point.id() == QQuickRangeSliderNodePrivate::get(d->first)->touchId + || point.id() == QQuickRangeSliderNodePrivate::get(d->second)->touchId) + d->handleMove(point.pos(), point.id()); + break; + case Qt::TouchPointReleased: + if (point.id() == QQuickRangeSliderNodePrivate::get(d->first)->touchId + || point.id() == QQuickRangeSliderNodePrivate::get(d->second)->touchId) + d->handleRelease(point.pos(), point.id()); + break; + default: + break; + } + } + break; + + case QEvent::TouchEnd: + for (const QTouchEvent::TouchPoint &point : event->touchPoints()) { + if (point.id() == QQuickRangeSliderNodePrivate::get(d->first)->touchId + || point.id() == QQuickRangeSliderNodePrivate::get(d->second)->touchId) + d->handleRelease(point.pos(), point.id()); + } + break; + + case QEvent::TouchCancel: + d->handleUngrab(); + break; + + default: + QQuickControl::touchEvent(event); + break; + } +} + +void QQuickRangeSlider::touchUngrabEvent() +{ + Q_D(QQuickRangeSlider); + QQuickControl::touchUngrabEvent(); + d->handleUngrab(); +} + void QQuickRangeSlider::mirrorChange() { Q_D(QQuickRangeSlider); diff --git a/src/quicktemplates2/qquickrangeslider_p.h b/src/quicktemplates2/qquickrangeslider_p.h index 08e68b0e..f5a01b5f 100644 --- a/src/quicktemplates2/qquickrangeslider_p.h +++ b/src/quicktemplates2/qquickrangeslider_p.h @@ -119,6 +119,8 @@ protected: void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void mouseUngrabEvent() override; + void touchEvent(QTouchEvent *event) override; + void touchUngrabEvent() override; void mirrorChange() override; void componentComplete() override; diff --git a/tests/auto/controls/data/tst_rangeslider.qml b/tests/auto/controls/data/tst_rangeslider.qml index fb57cffc..b7132475 100644 --- a/tests/auto/controls/data/tst_rangeslider.qml +++ b/tests/auto/controls/data/tst_rangeslider.qml @@ -375,6 +375,184 @@ TestCase { compare(control.second.visualPosition, horizontal ? 1.0 : 0.0) } + function test_touch_data() { + return [ + { tag: "horizontal", orientation: Qt.Horizontal, live: false }, + { tag: "vertical", orientation: Qt.Vertical, live: false }, + { tag: "horizontal:live", orientation: Qt.Horizontal, live: true }, + { tag: "vertical:live", orientation: Qt.Vertical, live: true } + ] + } + + function test_touch(data) { + var control = createTemporaryObject(sliderComponent, testCase, { orientation: data.orientation, live: data.live }) + verify(control) + + var firstPressedSpy = signalSpy.createObject(control, {target: control.first, signalName: "pressedChanged"}) + verify(firstPressedSpy.valid) + + var secondPressedSpy = signalSpy.createObject(control, {target: control.second, signalName: "pressedChanged"}) + verify(secondPressedSpy.valid) + + var touch = touchEvent(control) + touch.press(0, control, control.width * 0.25, control.height * 0.75).commit() + compare(firstPressedSpy.count, 1) + compare(secondPressedSpy.count, 0) + compare(control.first.pressed, true) + compare(control.first.value, 0.0) + compare(control.first.position, 0.0) + compare(control.second.pressed, false) + compare(control.second.value, 1.0) + compare(control.second.position, 1.0) + + touch.release(0, control, control.width * 0.25, control.height * 0.75).commit() + compare(firstPressedSpy.count, 2) + compare(secondPressedSpy.count, 0) + compare(control.first.pressed, false) + compare(control.first.value, 0.0) + compare(control.first.position, 0.0) + compare(control.second.pressed, false) + compare(control.second.value, 1.0) + compare(control.second.position, 1.0) + + touch.press(0, control, control.width * 0.75, control.height * 0.25).commit() + compare(firstPressedSpy.count, 2) + compare(secondPressedSpy.count, 1) + compare(control.first.pressed, false) + compare(control.first.value, 0.0) + compare(control.first.position, 0.0) + compare(control.second.pressed, true) + compare(control.second.value, 1.0) + compare(control.second.position, 1.0) + + touch.release(0, control, control.width * 0.75, control.height * 0.25).commit() + compare(firstPressedSpy.count, 2) + compare(secondPressedSpy.count, 2) + compare(control.first.pressed, false) + compare(control.first.value, 0.0) + compare(control.first.position, 0.0) + compare(control.second.pressed, false) + compare(control.second.value, 1.0) + compare(control.second.position, 1.0) + + touch.press(0, control, 0, control.height).commit() + compare(firstPressedSpy.count, 3) + compare(secondPressedSpy.count, 2) + compare(control.first.pressed, true) + compare(control.first.value, 0.0) + compare(control.first.position, 0.0) + compare(control.second.pressed, false) + compare(control.second.value, 1.0) + compare(control.second.position, 1.0) + + touch.release(0, control, 0, control.height).commit() + compare(firstPressedSpy.count, 4) + compare(secondPressedSpy.count, 2) + compare(control.first.pressed, false) + compare(control.first.value, 0.0) + compare(control.first.position, 0.0) + compare(control.second.pressed, false) + compare(control.second.value, 1.0) + compare(control.second.position, 1.0) + + touch.press(0, control, control.first.handle.x, control.first.handle.y).commit() + compare(firstPressedSpy.count, 5) + compare(secondPressedSpy.count, 2) + compare(control.first.pressed, true) + compare(control.first.value, 0.0) + compare(control.first.position, 0.0) + compare(control.second.pressed, false) + compare(control.second.value, 1.0) + compare(control.second.position, 1.0) + + var horizontal = control.orientation === Qt.Horizontal + var toX = horizontal ? control.width * 0.5 : control.first.handle.x + var toY = horizontal ? control.first.handle.y : control.height * 0.5 + touch.move(0, control, toX, toY).commit() + compare(firstPressedSpy.count, 5) + compare(secondPressedSpy.count, 2) + compare(control.first.pressed, true) + compare(control.first.value, data.live ? 0.5 : 0.0) + compare(control.first.position, 0.5) + compare(control.first.visualPosition, 0.5) + compare(control.second.pressed, false) + compare(control.second.value, 1.0) + compare(control.second.position, 1.0) + compare(control.second.visualPosition, horizontal ? 1.0 : 0.0) + + touch.release(0, control, toX, toY).commit() + compare(firstPressedSpy.count, 6) + compare(secondPressedSpy.count, 2) + compare(control.first.pressed, false) + compare(control.first.value, 0.5) + compare(control.first.position, 0.5) + compare(control.first.visualPosition, 0.5) + compare(control.second.pressed, false) + compare(control.second.value, 1.0) + compare(control.second.position, 1.0) + compare(control.second.visualPosition, horizontal ? 1.0 : 0.0) + } + + function test_multiTouch() { + var control1 = createTemporaryObject(sliderComponent, testCase) + verify(control1) + + // press and move the first handle of the first slider + var touch = touchEvent(control1) + touch.press(0, control1, 0, 0).commit().move(0, control1, control1.width / 2, control1.height / 2).commit() + compare(control1.first.pressed, true) + compare(control1.first.position, 0.5) + compare(control1.second.pressed, false) + compare(control1.second.position, 1.0) + + // press and move the second handle of the first slider + touch.stationary(0).press(1, control1, control1.width, control1.height).commit() + touch.stationary(0).move(1, control1, control1.width / 2, control1.height / 2).commit() + compare(control1.first.pressed, true) + compare(control1.first.position, 0.5) + compare(control1.second.pressed, true) + compare(control1.second.position, 0.5) + + var control2 = createTemporaryObject(sliderComponent, testCase, {y: control1.height}) + verify(control2) + waitForRendering(control2) + + // press and move the first handle of the second slider + touch.stationary(0).stationary(1).press(2, control2, 0, 0).commit() + touch.stationary(0).stationary(1).move(2, control2, control2.width / 2, control2.height / 2).commit() + compare(control1.first.pressed, true) + compare(control1.first.position, 0.5) + compare(control1.second.pressed, true) + compare(control1.second.position, 0.5) + compare(control2.first.pressed, true) + compare(control2.first.position, 0.5) + compare(control2.second.pressed, false) + compare(control2.second.position, 1.0) + + // press and move the second handle of the second slider + touch.stationary(0).stationary(1).stationary(2).press(3, control2, control2.width, control2.height).commit() + touch.stationary(0).stationary(1).stationary(2).move(3, control2, control2.width / 2, control2.height / 2).commit() + compare(control1.first.pressed, true) + compare(control1.first.position, 0.5) + compare(control1.second.pressed, true) + compare(control1.second.position, 0.5) + compare(control2.first.pressed, true) + compare(control2.first.position, 0.5) + compare(control2.second.pressed, true) + compare(control2.second.position, 0.5) + + // release the both handles of the both sliders + touch.release(0, control1).release(1, control1).release(2, control2).release(3, control2).commit() + compare(control1.first.pressed, false) + compare(control1.first.position, 0.5) + compare(control1.second.pressed, false) + compare(control1.second.position, 0.5) + compare(control2.first.pressed, false) + compare(control2.first.position, 0.5) + compare(control2.second.pressed, false) + compare(control2.second.position, 0.5) + } + function test_overlappingHandles() { var control = createTemporaryObject(sliderComponent, testCase, { orientation: data.orientation }) verify(control) @@ -593,7 +771,11 @@ TestCase { ] } - function test_snapMode(data) { + function test_snapMode_mouse_data() { + return test_snapMode_data() + } + + function test_snapMode_mouse(data) { var control = createTemporaryObject(sliderComponent, testCase, {snapMode: data.snapMode, from: data.from, to: data.to, stepSize: 0.2}) verify(control) @@ -620,6 +802,38 @@ TestCase { verify(sliderCompare(control.first.position, data.positions[2])) } + function test_snapMode_touch_data() { + return test_snapMode_data() + } + + function test_snapMode_touch(data) { + var control = createTemporaryObject(sliderComponent, testCase, {snapMode: data.snapMode, from: data.from, to: data.to, stepSize: 0.2}) + verify(control) + + control.first.value = 0 + control.second.value = data.to + + function sliderCompare(left, right) { + return Math.abs(left - right) < 0.05 + } + + var touch = touchEvent(control) + touch.press(0, control, control.first.handle.x, control.first.handle.y).commit() + compare(control.first.pressed, true) + compare(control.first.value, data.values[0]) + compare(control.first.position, data.positions[0]) + + touch.move(0, control, control.leftPadding + 0.15 * (control.availableWidth + control.first.handle.width / 2)).commit() + compare(control.first.pressed, true) + verify(sliderCompare(control.first.value, data.values[1])) + verify(sliderCompare(control.first.position, data.positions[1])) + + touch.release(0, control, control.leftPadding + 0.15 * (control.availableWidth + control.first.handle.width / 2)).commit() + compare(control.first.pressed, false) + verify(sliderCompare(control.first.value, data.values[2])) + verify(sliderCompare(control.first.position, data.positions[2])) + } + function test_focus() { var control = createTemporaryObject(sliderComponent, testCase) verify(control) -- cgit v1.2.3