File indexing completed on 2025-01-19 03:57:35

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2014-02-01
0007  * Description : Kinetic Scroller for Thumbnail Bar
0008  *               based on Razvan Petru implementation.
0009  *
0010  * SPDX-FileCopyrightText: 2014 by Mohamed_Anwer <m_dot_anwer at gmx dot com>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "showfotokineticscroller.h"
0017 
0018 // Qt includes
0019 
0020 #include <QApplication>
0021 #include <QScrollBar>
0022 #include <QMouseEvent>
0023 #include <QEvent>
0024 #include <QTimer>
0025 
0026 namespace ShowFoto
0027 {
0028 
0029 /**
0030  *A number of mouse moves are ignored after a press to differentiate
0031  *it from a press & drag.
0032  */
0033 static const int gMaxIgnoredMouseMoves = 4;
0034 
0035 /**
0036  * The timer measures the drag speed & handles kinetic scrolling. Adjusting
0037  * the timer interval will change the scrolling speed and smoothness.
0038  */
0039 static const int gTimerInterval        = 30;
0040 
0041 /**
0042  * The speed measurement is imprecise, limit it so that the scrolling is not
0043  * too fast.
0044  */
0045 static const int gMaxDecelerationSpeed = 30;
0046 
0047 /**
0048  * influences how fast the scroller decelerates
0049  */
0050 static const int gFriction             = 1;
0051 
0052 class Q_DECL_HIDDEN ShowfotoKineticScroller::Private
0053 {
0054 public:
0055 
0056     explicit Private()
0057       : isPressed            (false),
0058         isMoving             (false),
0059         lastMouseYPos        (0),
0060         lastMouseXPos        (0),
0061         lastScrollBarPosition(0),
0062         velocity             (0),
0063         ignoredMouseMoves    (0),
0064         ignoredMouseActions  (0),
0065         scrollArea           (nullptr),
0066         scrollFlow           (QListView::TopToBottom)
0067     {
0068     }
0069 
0070     void stopMotion()
0071     {
0072         isMoving = false;
0073         velocity = 0;
0074         kineticTimer.stop();
0075     }
0076 
0077     bool                 isPressed;
0078     bool                 isMoving;
0079 
0080     int                  lastMouseYPos;
0081     int                  lastMouseXPos;
0082     int                  lastScrollBarPosition;
0083     int                  velocity;
0084     int                  ignoredMouseMoves;
0085     int                  ignoredMouseActions;
0086 
0087     QAbstractScrollArea* scrollArea;
0088     QPoint               lastPressPoint;
0089     QTimer               kineticTimer;
0090     QListView::Flow      scrollFlow;
0091 };
0092 
0093 ShowfotoKineticScroller::ShowfotoKineticScroller(QObject* const parent)
0094     : QObject(parent),
0095       d      (new Private())
0096 {
0097     connect(&d->kineticTimer, &QTimer::timeout,
0098             this, &ShowfotoKineticScroller::onKineticTimerElapsed);
0099 }
0100 
0101 ShowfotoKineticScroller::~ShowfotoKineticScroller()
0102 {
0103     delete d;
0104 }
0105 
0106 void ShowfotoKineticScroller::enableKineticScrollFor(QAbstractScrollArea* const scrollArea)
0107 {
0108     if (!scrollArea)
0109     {
0110         Q_ASSERT_X(0, "kinetic scroller", "missing scroll area");
0111         return;
0112     }
0113 
0114     // remove existing association
0115 
0116     if (d->scrollArea)
0117     {
0118         d->scrollArea->viewport()->removeEventFilter(this);
0119         d->scrollArea->removeEventFilter(this);
0120         d->scrollArea = nullptr;
0121     }
0122 
0123     // associate
0124 
0125     scrollArea->installEventFilter(this);
0126     scrollArea->viewport()->installEventFilter(this);
0127     d->scrollArea = scrollArea;
0128 }
0129 
0130 /// intercepts mouse events to make the scrolling work
0131 bool ShowfotoKineticScroller::eventFilter(QObject* object, QEvent* event)
0132 {
0133     const QEvent::Type eventType = event->type();
0134     const bool isMouseAction     = QEvent::MouseButtonPress == eventType || QEvent::MouseButtonRelease == eventType;
0135     const bool isMouseEvent      = isMouseAction || QEvent::MouseMove == eventType;
0136 
0137     if (!isMouseEvent || !d->scrollArea)
0138     {
0139         return false;
0140     }
0141 
0142     if (isMouseAction && (d->ignoredMouseActions-- > 0)) // don't filter simulated click
0143     {
0144         return false;
0145     }
0146 
0147     QMouseEvent* const mouseEvent = static_cast<QMouseEvent*>(event);
0148 
0149     switch (eventType)
0150     {
0151         case QEvent::MouseButtonPress:
0152         {
0153             d->isPressed      = true;
0154             d->lastPressPoint = mouseEvent->pos();
0155 
0156             if (d->scrollFlow == QListView::TopToBottom)
0157             {
0158                 d->lastScrollBarPosition = d->scrollArea->verticalScrollBar()->value();
0159             }
0160             else
0161             {
0162                 d->lastScrollBarPosition = d->scrollArea->horizontalScrollBar()->value();
0163             }
0164 
0165             if (d->isMoving)
0166             {
0167                 // press while kinetic scrolling, so stop
0168 
0169                 d->stopMotion();
0170             }
0171 
0172             break;
0173         }
0174 
0175         case QEvent::MouseMove:
0176         {
0177             if (!d->isMoving && d->isPressed)
0178             {
0179                 // A few move events are ignored as "click jitter", but after that we
0180                 // assume that the user is doing a click & drag
0181 
0182                 if      (d->ignoredMouseMoves < gMaxIgnoredMouseMoves)
0183                 {
0184                     ++d->ignoredMouseMoves;
0185                 }
0186                 else if (d->isPressed)
0187                 {
0188                     d->ignoredMouseMoves = 0;
0189                     d->isMoving          = true;
0190 
0191                     if (d->scrollFlow == QListView::TopToBottom)
0192                     {
0193                         d->lastMouseYPos = mouseEvent->pos().y();
0194                     }
0195                     else
0196                     {
0197                         d->lastMouseXPos = mouseEvent->pos().x();
0198                     }
0199 
0200                     if (!d->kineticTimer.isActive())
0201                     {
0202                         d->kineticTimer.start(gTimerInterval);
0203                     }
0204                 }
0205             }
0206             else if (d->isPressed)
0207             {
0208                 // manual scroll
0209 
0210                 if (d->scrollFlow == QListView::TopToBottom)
0211                 {
0212                     const int dragDistance = mouseEvent->pos().y() - d->lastPressPoint.y();
0213                     d->scrollArea->verticalScrollBar()->setValue(d->lastScrollBarPosition - dragDistance);
0214                 }
0215                 else
0216                 {
0217                     const int dragDistance = mouseEvent->pos().x() - d->lastPressPoint.x();
0218                     d->scrollArea->horizontalScrollBar()->setValue(d->lastScrollBarPosition - dragDistance);
0219                 }
0220             }
0221 
0222             break;
0223         }
0224 
0225         case QEvent::MouseButtonRelease:
0226         {
0227             d->isPressed = false;
0228 
0229             // Looks like the user wanted a single click. Simulate the click,
0230             // as the events were already consumed
0231 
0232             if (!d->isMoving)
0233             {
0234                 QMouseEvent* const mousePress   = new QMouseEvent(QEvent::MouseButtonPress,
0235                                                                   d->lastPressPoint, Qt::LeftButton,
0236                                                                   Qt::LeftButton, Qt::NoModifier);
0237                 QMouseEvent* const mouseRelease = new QMouseEvent(QEvent::MouseButtonRelease,
0238                                                                   d->lastPressPoint, Qt::LeftButton,
0239                                                                   Qt::LeftButton, Qt::NoModifier);
0240                 d->ignoredMouseActions          = 2;
0241 
0242                 QApplication::postEvent(object, mousePress);
0243                 QApplication::postEvent(object, mouseRelease);
0244             }
0245 
0246             break;
0247         }
0248 
0249         default:
0250         {
0251             // Nothing to do here.
0252             break;
0253         }
0254     }
0255 
0256     return true; // filter event
0257 }
0258 
0259 void ShowfotoKineticScroller::onKineticTimerElapsed()
0260 {
0261     if (d->isPressed && d->isMoving)
0262     {
0263         if (d->scrollFlow == QListView::TopToBottom)
0264         {
0265             // the speed is measured between two timer ticks
0266 
0267             const int cursorYPos = d->scrollArea->mapFromGlobal(QCursor::pos()).y();
0268             d->velocity          = cursorYPos - d->lastMouseYPos;
0269             d->lastMouseYPos     = cursorYPos;
0270         }
0271         else
0272         {
0273             const int cursorXPos = d->scrollArea->mapFromGlobal(QCursor::pos()).x();
0274             d->velocity          = cursorXPos - d->lastMouseXPos;
0275             d->lastMouseXPos     = cursorXPos;
0276         }
0277     }
0278     else if (!d->isPressed && d->isMoving)
0279     {
0280         // use the previously recorded speed and gradually decelerate
0281 
0282         d->velocity = qBound(-gMaxDecelerationSpeed, d->velocity, gMaxDecelerationSpeed);
0283 
0284         if      (d->velocity > 0)
0285         {
0286             d->velocity -= gFriction;
0287         }
0288         else if (d->velocity < 0)
0289         {
0290             d->velocity += gFriction;
0291         }
0292 
0293         if (qAbs(d->velocity) < qAbs(gFriction))
0294         {
0295             d->stopMotion();
0296         }
0297 
0298         if (d->scrollFlow == QListView::TopToBottom)
0299         {
0300             const int scrollBarYPos = d->scrollArea->verticalScrollBar()->value();
0301             d->scrollArea->verticalScrollBar()->setValue(scrollBarYPos - d->velocity);
0302         }
0303         else
0304         {
0305             const int scrollBarXPos = d->scrollArea->horizontalScrollBar()->value();
0306             d->scrollArea->horizontalScrollBar()->setValue(scrollBarXPos - d->velocity);
0307         }
0308     }
0309     else
0310     {
0311         d->stopMotion();
0312     }
0313 }
0314 
0315 void ShowfotoKineticScroller::setScrollFlow(QListView::Flow flow)
0316 {
0317     d->scrollFlow = flow;
0318 }
0319 
0320 } // namespace ShowFoto
0321 
0322 #include "moc_showfotokineticscroller.cpp"