Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

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...


Log in to reply