File indexing completed on 2024-04-28 04:41:52

0001 /***************************************************************************
0002  *   Copyright (C) 2017 by Emmanuel Lepage Vallee                          *
0003  *   Author : Emmanuel Lepage Vallee <emmanuel.lepage@kde.org>             *
0004  *                                                                         *
0005  *   This program is free software; you can redistribute it and/or modify  *
0006  *   it under the terms of the GNU General Public License as published by  *
0007  *   the Free Software Foundation; either version 3 of the License, or     *
0008  *   (at your option) any later version.                                   *
0009  *                                                                         *
0010  *   This program is distributed in the hope that it will be useful,       *
0011  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0012  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0013  *   GNU General Public License for more details.                          *
0014  *                                                                         *
0015  *   You should have received a copy of the GNU General Public License     *
0016  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0017  **************************************************************************/
0018 #include "flickable.h"
0019 
0020 // Qt
0021 #include <QtCore/QAbstractItemModel>
0022 #include <QtCore/QEasingCurve>
0023 #include <QtCore/QTimer>
0024 #include <QtCore/QDateTime>
0025 #include <QQmlComponent>
0026 #include <QQmlEngine>
0027 #include <QQmlContext>
0028 
0029 // LibStdC++
0030 #include <cmath>
0031 
0032 class FlickablePrivate final : public QObject
0033 {
0034     Q_OBJECT
0035 public:
0036     typedef bool(FlickablePrivate::*StateF)(QMouseEvent*);
0037 
0038     /// The current status of the inertial viewport state machine
0039     enum class DragState {
0040         IDLE    , /*!< Nothing is happening */
0041         PRESSED , /*!< A potential drag     */
0042         EVAL    , /*!< Drag without lock    */
0043         DRAGGED , /*!< An in progress grag  */
0044         INERTIA , /*!< A leftover drag      */
0045     };
0046 
0047     /// Events affecting the behavior of the inertial viewport state machine
0048     enum class DragEvent {
0049         TIMEOUT , /*!< When inertia is exhausted       */
0050         PRESS   , /*!< When a mouse button is pressed  */
0051         RELEASE , /*!< When a mouse button is released */
0052         MOVE    , /*!< When the mouse moves            */
0053         TIMER   , /*!< 30 times per seconds            */
0054         OTHER   , /*!< Doesn't affect the state        */
0055         ACCEPT  , /*!< Accept the drag ownership       */
0056         REJECT  , /*!< Reject the drag ownership       */
0057     };
0058 
0059     // Actions
0060     bool nothing (QMouseEvent* e); /*!< No operations                */
0061     bool error   (QMouseEvent* e); /*!< Warn something went wrong    */
0062     bool start   (QMouseEvent* e); /*!< From idle to pre-drag        */
0063     bool stop    (QMouseEvent* e); /*!< Stop inertia                 */
0064     bool drag    (QMouseEvent* e); /*!< Move the viewport            */
0065     bool cancel  (QMouseEvent* e); /*!< Cancel potential drag        */
0066     bool inertia (QMouseEvent* e); /*!< Iterate on the inertia curve */
0067     bool release (QMouseEvent* e); /*!< Trigger the inertia          */
0068     bool eval    (QMouseEvent* e); /*!< Check for potential drag ops */
0069     bool lock    (QMouseEvent* e); /*!< Lock the input grabber       */
0070 
0071     // Attributes
0072     QQuickItem* m_pContainer {nullptr};
0073     QPointF     m_StartPoint {       };
0074     QPointF     m_DragPoint  {       };
0075     QTimer*     m_pTimer     {nullptr};
0076     qint64      m_StartTime  {   0   };
0077     int         m_LastDelta  {   0   };
0078     qreal       m_Velocity   {   0   };
0079     qreal       m_DecelRate  {  0.9  };
0080     bool        m_Interactive{ true  };
0081 
0082     mutable QQmlContext *m_pRootContext {nullptr};
0083 
0084     qreal m_MaxVelocity {std::numeric_limits<qreal>::max()};
0085 
0086     DragState m_State {DragState::IDLE};
0087 
0088     // Helpers
0089     void loadVisibleElements();
0090     bool applyEvent(DragEvent event, QMouseEvent* e);
0091     bool updateVelocity();
0092     DragEvent eventMapper(QEvent* e) const;
0093 
0094     // State machine
0095     static const StateF m_fStateMachine[5][8];
0096     static const DragState m_fStateMap [5][8];
0097 
0098     Flickable* q_ptr;
0099 
0100 public Q_SLOTS:
0101     void tick();
0102 };
0103 
0104 #define A &FlickablePrivate::           // Actions
0105 #define S FlickablePrivate::DragState:: // Next state
0106 /**
0107  * This is a Mealy machine, states callbacks are allowed to throw more events
0108  */
0109 const FlickablePrivate::DragState FlickablePrivate::m_fStateMap[5][8] {
0110 /*             TIMEOUT      PRESS      RELEASE     MOVE       TIMER      OTHER      ACCEPT    REJECT  */
0111 /* IDLE    */ {S IDLE   , S PRESSED, S IDLE    , S IDLE   , S IDLE   , S IDLE   , S IDLE   , S IDLE   },
0112 /* PRESSED */ {S PRESSED, S PRESSED, S IDLE    , S EVAL   , S PRESSED, S PRESSED, S PRESSED, S PRESSED},
0113 /* EVAL    */ {S IDLE   , S EVAL   , S IDLE    , S EVAL   , S EVAL   , S EVAL   , S DRAGGED, S IDLE   },
0114 /* DRAGGED */ {S DRAGGED, S DRAGGED, S INERTIA , S DRAGGED, S DRAGGED, S DRAGGED, S DRAGGED, S DRAGGED},
0115 /* INERTIA */ {S IDLE   , S IDLE   , S IDLE    , S DRAGGED, S INERTIA, S INERTIA, S INERTIA, S INERTIA}};
0116 const FlickablePrivate::StateF FlickablePrivate::m_fStateMachine[5][8] {
0117 /*             TIMEOUT      PRESS      RELEASE     MOVE       TIMER      OTHER     ACCEPT   REJECT  */
0118 /* IDLE    */ {A error  , A start  , A nothing , A nothing, A error  , A nothing, A error, A error  },
0119 /* PRESSED */ {A error  , A start  , A cancel  , A eval   , A error  , A nothing, A error, A error  },
0120 /* EVAL    */ {A error  , A nothing, A cancel  , A eval   , A error  , A nothing, A lock , A cancel },
0121 /* DRAGGED */ {A error  , A drag   , A release , A drag   , A error  , A nothing, A error, A error  },
0122 /* INERTIA */ {A stop   , A stop   , A stop    , A error  , A inertia, A nothing, A error, A error  }};
0123 #undef S
0124 #undef A
0125 
0126 Flickable::Flickable(QQuickItem* parent)
0127     : QQuickItem(parent), d_ptr(new FlickablePrivate)
0128 {
0129     d_ptr->q_ptr = this;
0130     setClip(true);
0131     setAcceptedMouseButtons(Qt::LeftButton);
0132     setFiltersChildMouseEvents(true);
0133 
0134     d_ptr->m_pTimer = new QTimer(this);
0135     d_ptr->m_pTimer->setInterval(1000/30);
0136     connect(d_ptr->m_pTimer, &QTimer::timeout, d_ptr, &FlickablePrivate::tick);
0137 }
0138 
0139 Flickable::~Flickable()
0140 {
0141     if (d_ptr->m_pContainer)
0142         delete d_ptr->m_pContainer;
0143     delete d_ptr;
0144 }
0145 
0146 QQuickItem* Flickable::contentItem()
0147 {
0148     if (!d_ptr->m_pContainer) {
0149         QQmlEngine *engine = QQmlEngine::contextForObject(this)->engine();
0150 
0151         // Can't be called too early as the engine wont be ready.
0152         Q_ASSERT(engine);
0153 
0154         QQmlComponent rect1(engine, this);
0155         rect1.setData("import QtQuick 2.4; Item {}", {});
0156         d_ptr->m_pContainer = qobject_cast<QQuickItem *>(rect1.create());
0157         d_ptr->m_pContainer->setHeight(height());
0158         d_ptr->m_pContainer->setWidth(width ());
0159         engine->setObjectOwnership(d_ptr->m_pContainer, QQmlEngine::CppOwnership);
0160         d_ptr->m_pContainer->setParentItem(this);
0161 
0162         emit contentHeightChanged(height());
0163     }
0164 
0165     return d_ptr->m_pContainer;
0166 }
0167 
0168 QRectF Flickable::viewport() const
0169 {
0170     return {
0171         0.0,
0172         contentY(),
0173         width(),
0174         height()
0175     };
0176 }
0177 
0178 qreal Flickable::contentY() const
0179 {
0180     if (!d_ptr->m_pContainer)
0181         return 0;
0182 
0183     return -d_ptr->m_pContainer->y();
0184 }
0185 
0186 void Flickable::setContentY(qreal y)
0187 {
0188     if (!d_ptr->m_pContainer)
0189         return;
0190 
0191     // Do not allow out of bound scroll
0192     y = std::fmax(y, 0);
0193 
0194     if (d_ptr->m_pContainer->height() >= height())
0195         y = std::fmin(y, d_ptr->m_pContainer->height() - height());
0196 
0197     if (d_ptr->m_pContainer->y() == -y)
0198         return;
0199 
0200     d_ptr->m_pContainer->setY(-y);
0201 
0202     emit contentYChanged(y);
0203     emit viewportChanged(viewport());
0204     emit percentageChanged(
0205         ((-d_ptr->m_pContainer->y()))/(d_ptr->m_pContainer->height()-height())
0206     );
0207 }
0208 
0209 qreal Flickable::contentHeight() const
0210 {
0211     if (!d_ptr->m_pContainer)
0212         return 0;
0213 
0214     return  d_ptr->m_pContainer->height();
0215 }
0216 
0217 /// Timer events
0218 void FlickablePrivate::tick()
0219 {
0220     applyEvent(DragEvent::TIMER, nullptr);
0221 }
0222 
0223 /**
0224  * Use the linear velocity. This class currently mostly ignore horizontal
0225  * movements, but nevertheless the intention is to keep the inertia factor
0226  * from its vector.
0227  *
0228  * @return If there is inertia
0229  */
0230 bool FlickablePrivate::updateVelocity()
0231 {
0232     const qreal dy = m_DragPoint.y() - m_StartPoint.y();
0233     const qreal dt = (QDateTime::currentMSecsSinceEpoch() - m_StartTime)/(1000.0/30.0);
0234 
0235     // Points per frame
0236     m_Velocity = (dy/dt);
0237 
0238     // Do not start for low velocity mouse release
0239     if (std::fabs(m_Velocity) < 40) //TODO C++17 use std::clamp
0240         m_Velocity = 0;
0241 
0242     if (std::fabs(m_Velocity) > std::fabs(m_MaxVelocity))
0243         m_Velocity = m_Velocity > 0 ? m_MaxVelocity : -m_MaxVelocity;
0244 
0245     return m_Velocity;
0246 }
0247 
0248 /**
0249  * Map qevent to DragEvent
0250  */
0251 FlickablePrivate::DragEvent FlickablePrivate::eventMapper(QEvent* event) const
0252 {
0253     auto e = FlickablePrivate::DragEvent::OTHER;
0254 
0255     #pragma GCC diagnostic ignored "-Wswitch-enum"
0256     switch(event->type()) {
0257         case QEvent::MouseMove:
0258             e = FlickablePrivate::DragEvent::MOVE;
0259             break;
0260         case QEvent::MouseButtonPress:
0261             e = FlickablePrivate::DragEvent::PRESS;
0262             break;
0263         case QEvent::MouseButtonRelease:
0264             e = FlickablePrivate::DragEvent::RELEASE;
0265             break;
0266         default:
0267             break;
0268     }
0269     #pragma GCC diagnostic pop
0270 
0271     return e;
0272 }
0273 
0274 /**
0275  * The tabs eat some mousePress events at random.
0276  *
0277  * Mitigate the issue by allowing the event series to begin later.
0278  */
0279 bool Flickable::childMouseEventFilter(QQuickItem* item, QEvent* event)
0280 {
0281     if (!d_ptr->m_Interactive)
0282         return false;
0283 
0284     const auto e = d_ptr->eventMapper(event);
0285 
0286     return e == FlickablePrivate::DragEvent::OTHER ?
0287         QQuickItem::childMouseEventFilter(item, event) :
0288         d_ptr->applyEvent(e, static_cast<QMouseEvent*>(event) );
0289 }
0290 
0291 bool Flickable::event(QEvent *event)
0292 {
0293     if (!d_ptr->m_Interactive)
0294         return false;
0295 
0296     const auto e = d_ptr->eventMapper(event);
0297 
0298     if (event->type() == QEvent::Wheel) {
0299         setContentY(contentY() - static_cast<QWheelEvent*>(event)->angleDelta().y());
0300         event->accept();
0301         return true;
0302     }
0303 
0304     return e == FlickablePrivate::DragEvent::OTHER ?
0305         QQuickItem::event(event) :
0306         d_ptr->applyEvent(e, static_cast<QMouseEvent*>(event) );
0307 }
0308 
0309 void Flickable::geometryChanged(const QRectF& newGeometry, const QRectF& oldGeometry)
0310 {
0311     if (d_ptr->m_pContainer) {
0312         d_ptr->m_pContainer->setWidth(std::max(newGeometry.width(), d_ptr->m_pContainer->width()));
0313         d_ptr->m_pContainer->setHeight(std::max(newGeometry.height(), d_ptr->m_pContainer->height()));
0314 
0315         emit contentHeightChanged(d_ptr->m_pContainer->height());
0316     }
0317 
0318     //TODO prevent out of scope
0319     QQuickItem::geometryChanged(newGeometry, oldGeometry);
0320 
0321     emit viewportChanged(viewport());
0322 }
0323 
0324 /// State functions ///
0325 
0326 /**
0327  * Make the Mealy machine move between the states
0328  */
0329 bool FlickablePrivate::applyEvent(DragEvent event, QMouseEvent* e)
0330 {
0331     if (!m_pContainer)
0332         return false;
0333 
0334     const bool wasDragging(q_ptr->isDragging()), wasMoving(q_ptr->isMoving());
0335 
0336     // Set the state before the callback so recursive events work
0337     const int s = (int)m_State;
0338     m_State     = m_fStateMap            [s][(int)event];
0339     bool ret    = (this->*m_fStateMachine[s][(int)event])(e);
0340 
0341     if (ret && e)
0342         e->accept();
0343 
0344     if (wasDragging != q_ptr->isDragging())
0345         emit q_ptr->draggingChanged(q_ptr->isDragging());
0346 
0347     if (wasMoving != q_ptr->isMoving())
0348         emit q_ptr->movingChanged(q_ptr->isMoving());
0349 
0350     return ret && e;
0351 }
0352 
0353 bool FlickablePrivate::nothing(QMouseEvent*)
0354 {
0355     return false;
0356 }
0357 
0358 #pragma GCC diagnostic push
0359 #pragma GCC diagnostic ignored "-Wsuggest-attribute=noreturn"
0360 bool FlickablePrivate::error(QMouseEvent*)
0361 {
0362     qWarning() << "simpleFlickable: Invalid state change";
0363 
0364     Q_ASSERT(false);
0365     return false;
0366 }
0367 #pragma GCC diagnostic pop
0368 
0369 bool FlickablePrivate::stop(QMouseEvent* event)
0370 {
0371     m_pTimer->stop();
0372     m_Velocity = 0;
0373     m_StartPoint = m_DragPoint  = {};
0374 
0375     // Resend for further processing
0376     if (event)
0377         applyEvent(FlickablePrivate::DragEvent::PRESS, event);
0378 
0379     return false;
0380 }
0381 
0382 bool FlickablePrivate::drag(QMouseEvent* e)
0383 {
0384     if (!m_pContainer)
0385         return false;
0386 
0387     const int dy(e->pos().y() - m_DragPoint.y());
0388     m_DragPoint = e->pos();
0389     q_ptr->setContentY(q_ptr->contentY() - dy);
0390 
0391     // Reset the inertia on the differential inflexion points
0392     if ((m_LastDelta >= 0) ^ (dy >= 0)) {
0393         m_StartPoint = e->pos();
0394         m_StartTime  = QDateTime::currentMSecsSinceEpoch();
0395     }
0396 
0397     m_LastDelta = dy;
0398 
0399     return true;
0400 }
0401 
0402 bool FlickablePrivate::start(QMouseEvent* e)
0403 {
0404     m_StartPoint = m_DragPoint = e->pos();
0405     m_StartTime  = QDateTime::currentMSecsSinceEpoch();
0406 
0407     q_ptr->setFocus(true, Qt::MouseFocusReason);
0408 
0409     // The event itself may be a normal click, let the children handle it too
0410     return false;
0411 }
0412 
0413 bool FlickablePrivate::cancel(QMouseEvent*)
0414 {
0415     m_StartPoint = m_DragPoint = {};
0416     q_ptr->setKeepMouseGrab(false);
0417 
0418     // Reject the event, let the click pass though
0419     return false;
0420 }
0421 
0422 bool FlickablePrivate::release(QMouseEvent*)
0423 {
0424     q_ptr->setKeepMouseGrab(false);
0425     q_ptr->ungrabMouse();
0426 
0427     if (updateVelocity())
0428         m_pTimer->start();
0429     else
0430         applyEvent(DragEvent::TIMEOUT, nullptr);
0431 
0432     m_DragPoint = {};
0433 
0434     return false;
0435 }
0436 
0437 bool FlickablePrivate::lock(QMouseEvent*)
0438 {
0439     q_ptr->setKeepMouseGrab(true);
0440     q_ptr->grabMouse();
0441 
0442     return true;
0443 }
0444 
0445 bool FlickablePrivate::eval(QMouseEvent* e)
0446 {
0447     // It might look like an oversimplification, but the math here is correct.
0448     // Think of the rectangle being at the origin of a radiant wheel. The
0449     // hypotenuse of the rectangle will point at an angle. This code is
0450     // equivalent to the range PI/2 <-> 3*(PI/2) U 5*(PI/2) <-> 7*(PI/2)
0451 
0452     static const constexpr uchar EVENT_THRESHOLD = 10;
0453 
0454     // Reject large horizontal swipe and allow large vertical ones
0455     if (std::fabs(m_StartPoint.x() - e->pos().x()) > EVENT_THRESHOLD) {
0456         applyEvent(DragEvent::REJECT, e);
0457         return false;
0458     }
0459     else if (std::fabs(m_StartPoint.y() - e->pos().y()) > EVENT_THRESHOLD)
0460         applyEvent(DragEvent::ACCEPT, e);
0461 
0462     return drag(e);
0463 }
0464 
0465 bool FlickablePrivate::inertia(QMouseEvent*)
0466 {
0467     m_Velocity *= m_DecelRate;
0468 
0469     q_ptr->setContentY(q_ptr->contentY() - m_Velocity);
0470 
0471     // Clamp the asymptotes to avoid an infinite loop, I chose a random value
0472     if (std::fabs(m_Velocity) < 0.05)
0473         applyEvent(DragEvent::TIMEOUT, nullptr);
0474 
0475     return true;
0476 }
0477 
0478 bool Flickable::isDragging() const
0479 {
0480     return d_ptr->m_State == FlickablePrivate::DragState::DRAGGED
0481         || d_ptr->m_State == FlickablePrivate::DragState::EVAL;
0482 }
0483 
0484 bool Flickable::isMoving() const
0485 {
0486     return isDragging()
0487         || d_ptr->m_State == FlickablePrivate::DragState::INERTIA;
0488 }
0489 
0490 qreal Flickable::flickDeceleration() const
0491 {
0492     return d_ptr->m_DecelRate;
0493 }
0494 
0495 void Flickable::setFlickDeceleration(qreal v)
0496 {
0497     d_ptr->m_DecelRate = v;
0498 }
0499 
0500 bool Flickable::isInteractive() const
0501 {
0502     return d_ptr->m_Interactive;
0503 }
0504 
0505 void Flickable::setInteractive(bool v)
0506 {
0507     d_ptr->m_Interactive = v;
0508 }
0509 
0510 qreal Flickable::maximumFlickVelocity() const
0511 {
0512     return d_ptr->m_MaxVelocity;
0513 }
0514 
0515 void Flickable::setMaximumFlickVelocity(qreal v)
0516 {
0517     d_ptr->m_MaxVelocity = v;
0518 }
0519 
0520 QQmlContext* Flickable::rootContext() const
0521 {
0522     if (!d_ptr->m_pRootContext)
0523         d_ptr->m_pRootContext = QQmlEngine::contextForObject(this);
0524 
0525     return d_ptr->m_pRootContext;
0526 }
0527 
0528 #include <flickable.moc>