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>