Unsolved How to add swipe control on top of GridView
-
I have a StackView, which loads different GridView's. When a new GridView is pushed, the default animation slides the new page from the left to the right. The natural gesture to pop would hence be a right swipe. However I cannot find how to do that.
Some threads show ho to "manually" analyze a mouse area to detect a swipe, but it seems hard to believe that Qt won't let us implement a simple swipe gesture.
Others mention using a SwipeView. But I want to keep the StackView/GridView combination bahaviour (the use case is more complex, with a collection of button navigating down a menu tree). Users can only swipe to the right, to go back one level.
Here is a simplified code:
StackView { id: testStack anchors.fill: parent Component.onCompleted: { push(testGridView); } Component { id: testGridView Rectangle { GridView { model:testModel anchors.fill: parent cellWidth: width / 2 delegate: Rectangle { color: modelColor width: testStack.width * .4 height: width MouseArea { anchors.fill: parent onClicked: { if (add) testStack.push(testGridView); else testStack.pop(); } } } } } } MouseArea { id: testMouseArea anchors.fill: parent propagateComposedEvents: true onClicked: { mouse.accepted = false } // Ideal case: // onRightSwipe() { // testStack.pop() } } } ListModel { id: testModel ListElement {modelColor: "green"; add: true} ListElement {modelColor: "red"; add: false} }
At least my MouseArea doesn't mess the GridView up/down swipes, and clicks. But it doesn't handle swipes.
I've tried to add other elements instead of the MouseArea, but these typically don't have a propagateComposedEvents equivalebt, and will capture all interactions.
How to handle this? Do I really have to re-do from scratch what a SwipeView does just fine? Thanks!
-
I am starting to wonder if this is possible at all. I created a new object type, based on QQuickItem, and layed it on top of my StackView.
Unfortunately it looks like only one object can grab the mouse events at a given time.
Am I getting this right? Is there a way for my object to analyze the events, but pass them on to the underlying objects? -
@gomgom You can propagate the mouse events by emitting a signal from your signal handler.
-
@tom_h Thanks for the answer, do you know what would be the receiving slot?
-
@gomgom You just call the signal as a function. e.g.
MouseArea { onClicked: { targetMouseArea.clicked() } }
I would be interested to see what you implemented in your custom QQuickItem. Did you get swipe to work with it? If not, I think you'll have to roll your own swipe gesture in your MouseArea. Not hard, but I agree with you -- Qt should include a built in swipe signal.
-
@tom_h Thanks for the suggestion. I don't really see how to do this in C++ though, as the different calls (mouseMoveEvent etc...) are protected. I want to do this on the C++ side, to have access to a timer.
I am almost there though, I did end up doing my own swipe detection, looking at qquickflickable.cpp
I think that the only way to get access to all mouse events without affecting children elements is to implement a filter, which is what I've done. The following works well under Windows.
FlickableMouseArea.h :
#ifndef FLICKABLEMOUSEAREA_H #define FLICKABLEMOUSEAREA_H #include <QQuickItem> #include <QElapsedTimer> #include <QMouseEvent> #define MAX_FLICK_ANGLE_RATIO 0.3 #define MAX_FLICK_TIME_MS 150 #define MIN_FLICK_LENGTH_INCH 0.4 class FlickableMouseArea : public QQuickItem { Q_OBJECT public: FlickableMouseArea(); Q_PROPERTY(int physicalDpi READ physicalDpi WRITE setPhysicalDpi NOTIFY physicalDpiChanged) int physicalDpi() {return _physicalDpi;} void setPhysicalDpi(int newPhysicalDpi); signals: //void PropagateMouseToChanged(); void leftFlick(); void rightFlick(); void log(QString message); void physicalDpiChanged(); protected: bool childMouseEventFilter(QQuickItem *item, QEvent *event) override; void handleMousePressEvent(QMouseEvent *event); bool handleMouseMoveEvent(QMouseEvent *event); bool handleMouseReleaseEvent(QMouseEvent *event); private: QPoint _startingPosition; QElapsedTimer _timer; int _physicalDpi; bool _detectFlick(QMouseEvent *event, const bool notify); void _filterSlowOrAngled(QMouseEvent *event); }; #endif // FLICKABLEMOUSEAREA_H
FlickableMouseArea.cpp:
#include "flickablemousearea.h" FlickableMouseArea::FlickableMouseArea(): _physicalDpi(157) { setAcceptedMouseButtons(Qt::LeftButton); setFiltersChildMouseEvents(true); setAcceptTouchEvents(false); // rely on mouse events synthesized from touch } void FlickableMouseArea::setPhysicalDpi(int newPhysicalDpi) { if (_physicalDpi != newPhysicalDpi) { _physicalDpi = newPhysicalDpi; emit physicalDpiChanged(); log("*** New physical DPI: " + QString(_physicalDpi) + ", flick detection over " + QString(int(MIN_FLICK_LENGTH_INCH * _physicalDpi)) + ""); } } bool FlickableMouseArea::childMouseEventFilter(QQuickItem *item, QEvent *event) { Q_UNUSED(item) QMouseEvent* mouseEvent; switch (event->type()) { case QEvent::MouseButtonPress: mouseEvent = static_cast<QMouseEvent *>(event); handleMousePressEvent(mouseEvent); break; case QEvent::MouseMove: mouseEvent = static_cast<QMouseEvent *>(event); return handleMouseMoveEvent(mouseEvent); case QEvent::MouseButtonRelease: mouseEvent = static_cast<QMouseEvent *>(event); return handleMouseReleaseEvent(mouseEvent); default: break; } return false; } void FlickableMouseArea::handleMousePressEvent(QMouseEvent *event) { _timer.start(); _startingPosition = event->pos(); } bool FlickableMouseArea::handleMouseMoveEvent(QMouseEvent *event) { _filterSlowOrAngled(event); return _detectFlick(event, false); } bool FlickableMouseArea::handleMouseReleaseEvent(QMouseEvent *event) { _filterSlowOrAngled(event); bool detected = _detectFlick(event, true); _timer.invalidate(); return detected; } void FlickableMouseArea::_filterSlowOrAngled(QMouseEvent *event) { if (_timer.isValid()) { if (_timer.elapsed()) { // Don't analyze 2 events too close in time int xDelta = event->pos().x() - _startingPosition.x(); int yDelta = event->pos().y() - _startingPosition.y(); if (abs(yDelta) > (MAX_FLICK_ANGLE_RATIO * abs(xDelta))) { log("*** FlickableMouseArea - move too angled - invalidate detection ***"); _timer.invalidate(); } else { if (_timer.elapsed() > MAX_FLICK_TIME_MS) { // Moving too slowly log("*** FlickableMouseArea - move too slow - invalidate detection ***"); _timer.invalidate(); } } } } if (!_timer.isValid()) { // Restarts detection after timer becomes invalid _timer.start(); _startingPosition = event->pos(); } } bool FlickableMouseArea::_detectFlick(QMouseEvent *event, const bool notify) { if (_timer.isValid()) { if (_timer.elapsed()) { // Don't analyze 2 events too close in time int xDelta = event->pos().x() - _startingPosition.x(); int yDelta = event->pos().y() - _startingPosition.y(); if (abs(xDelta) > (int(MIN_FLICK_LENGTH_INCH * _physicalDpi))) { log("*** FlickableMouseArea flick delta: x=" + QString::number(xDelta) + " y=" + QString::number(yDelta) + ", start x=" + QString::number(_startingPosition.x()) + ", y=" + QString::number(_startingPosition.y()) + ", t=" + QString::number(_timer.elapsed()) + "ms, edge=" + QString::number(int(MIN_FLICK_LENGTH_INCH * _physicalDpi)) + "" + ((notify)? " - final" : " - ongoing") ); if (notify) { if (xDelta > 0) emit rightFlick(); else emit leftFlick(); } // This qualifies as a flick, filter it return true; } } } return false; }
But... on Android, it looks like I never get the MouseButtonRelease event. Still need to dig to see what is going on!
-
By the way, this item is meant to encapsulate the gridView (or Flickable, or MouseArea) for which we want to snoop the mouse's moves, and add left/right flicks...