00001 // Copyright (c) 2010, Razvan Petru 00002 // All rights reserved. 00003 00004 // Redistribution and use in source and binary forms, with or without modification, 00005 // are permitted provided that the following conditions are met: 00006 00007 // * Redistributions of source code must retain the above copyright notice, this 00008 // list of conditions and the following disclaimer. 00009 // * Redistributions in binary form must reproduce the above copyright notice, this 00010 // list of conditions and the following disclaimer in the documentation and/or other 00011 // materials provided with the distribution. 00012 // * The name of the contributors may not be used to endorse or promote products 00013 // derived from this software without specific prior written permission. 00014 00015 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 00016 // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 00017 // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 00018 // IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 00019 // INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 00020 // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 00021 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 00022 // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 00023 // OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 00024 // OF THE POSSIBILITY OF SUCH DAMAGE. 00025 00026 // References: 00027 // http://blog.codeimproved.net/2010/12/kinetic-scrolling-with-qt-the-what-and-the-how/ 00028 00029 #include "QsKineticScroller.hpp" 00030 #include <QApplication> 00031 #include <QScrollBar> 00032 #include <QAbstractScrollArea> 00033 #include <QMouseEvent> 00034 #include <QEvent> 00035 #include <QTimer> 00036 #include <cstddef> // for NULL 00037 00038 // A number of mouse moves are ignored after a press to differentiate 00039 // it from a press & drag. 00040 static const int gMaxIgnoredMouseMoves = 4; 00041 // The timer measures the drag speed & handles kinetic scrolling. Adjusting 00042 // the timer interval will change the scrolling speed and smoothness. 00043 static const int gTimerInterval = 30; 00044 // The speed measurement is imprecise, limit it so that the scrolling is not 00045 // too fast. 00046 static const int gMaxDecelerationSpeed = 30; 00047 // influences how fast the scroller decelerates 00048 static const int gFriction = 1; 00049 00050 class QsKineticScrollerImpl 00051 { 00052 public: 00053 QsKineticScrollerImpl() 00054 : scrollArea(NULL) 00055 , isPressed(false) 00056 , isMoving(false) 00057 , lastMouseYPos(0) 00058 , lastScrollBarPosition(0) 00059 , velocity(0) 00060 , ignoredMouseMoves(0) 00061 , ignoredMouseActions(0) {} 00062 00063 void stopMotion() 00064 { 00065 isMoving = false; 00066 velocity = 0; 00067 kineticTimer.stop(); 00068 } 00069 00070 QAbstractScrollArea* scrollArea; 00071 bool isPressed; 00072 bool isMoving; 00073 QPoint lastPressPoint; 00074 int lastMouseYPos; 00075 int lastScrollBarPosition; 00076 int velocity; 00077 int ignoredMouseMoves; 00078 int ignoredMouseActions; 00079 QTimer kineticTimer; 00080 }; 00081 00082 QsKineticScroller::QsKineticScroller(QObject *parent) 00083 : QObject(parent) 00084 , d(new QsKineticScrollerImpl) 00085 { 00086 connect(&d->kineticTimer, SIGNAL(timeout()), SLOT(onKineticTimerElapsed())); 00087 } 00088 00089 // needed by smart pointer 00090 QsKineticScroller::~QsKineticScroller() 00091 { 00092 } 00093 00094 void QsKineticScroller::enableKineticScrollFor(QAbstractScrollArea* scrollArea) 00095 { 00096 if( !scrollArea ) 00097 { 00098 Q_ASSERT_X(0, "kinetic scroller", "missing scroll area"); 00099 return; 00100 } 00101 00102 // remove existing association 00103 if( d->scrollArea ) 00104 { 00105 d->scrollArea->viewport()->removeEventFilter(this); 00106 d->scrollArea->removeEventFilter(this); 00107 d->scrollArea = NULL; 00108 } 00109 00110 // associate 00111 scrollArea->installEventFilter(this); 00112 scrollArea->viewport()->installEventFilter(this); 00113 d->scrollArea = scrollArea; 00114 } 00115 00116 //! intercepts mouse events to make the scrolling work 00117 bool QsKineticScroller::eventFilter(QObject* object, QEvent* event) 00118 { 00119 const QEvent::Type eventType = event->type(); 00120 const bool isMouseAction = QEvent::MouseButtonPress == eventType 00121 || QEvent::MouseButtonRelease == eventType; 00122 const bool isMouseEvent = isMouseAction || QEvent::MouseMove == eventType; 00123 if( !isMouseEvent || !d->scrollArea ) 00124 return false; 00125 if( isMouseAction && d->ignoredMouseActions-- > 0 ) // don't filter simulated click 00126 return false; 00127 00128 QMouseEvent* const mouseEvent = static_cast<QMouseEvent*>(event); 00129 switch( eventType ) 00130 { 00131 case QEvent::MouseButtonPress: 00132 { 00133 d->isPressed = true; 00134 d->lastPressPoint = mouseEvent->pos(); 00135 d->lastScrollBarPosition = d->scrollArea->verticalScrollBar()->value(); 00136 if( d->isMoving ) // press while kinetic scrolling, so stop 00137 d->stopMotion(); 00138 } 00139 break; 00140 case QEvent::MouseMove: 00141 { 00142 if( !d->isMoving ) 00143 { 00144 // A few move events are ignored as "click jitter", but after that we 00145 // assume that the user is doing a click & drag 00146 if( d->ignoredMouseMoves < gMaxIgnoredMouseMoves ) 00147 ++d->ignoredMouseMoves; 00148 else 00149 { 00150 d->ignoredMouseMoves = 0; 00151 d->isMoving = true; 00152 d->lastMouseYPos = mouseEvent->pos().y(); 00153 if( !d->kineticTimer.isActive() ) 00154 d->kineticTimer.start(gTimerInterval); 00155 } 00156 } 00157 else 00158 { 00159 // manual scroll 00160 const int dragDistance = mouseEvent->pos().y() - d->lastPressPoint.y(); 00161 d->scrollArea->verticalScrollBar()->setValue( 00162 d->lastScrollBarPosition - dragDistance); 00163 } 00164 } 00165 break; 00166 case QEvent::MouseButtonRelease: 00167 { 00168 d->isPressed = false; 00169 d->ignoredMouseMoves = 0; 00170 // Looks like the user wanted a single click. Simulate the click, 00171 // as the events were already consumed 00172 if( !d->isMoving ) 00173 { 00174 QMouseEvent* mousePress = new QMouseEvent(QEvent::MouseButtonPress, 00175 d->lastPressPoint, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); 00176 QMouseEvent* mouseRelease = new QMouseEvent(QEvent::MouseButtonRelease, 00177 d->lastPressPoint, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); 00178 00179 d->ignoredMouseActions = 2; 00180 QApplication::postEvent(object, mousePress); 00181 QApplication::postEvent(object, mouseRelease); 00182 } 00183 } 00184 break; 00185 default: 00186 break; 00187 } 00188 00189 return true; // filter event 00190 } 00191 00192 void QsKineticScroller::onKineticTimerElapsed() 00193 { 00194 if( d->isPressed && d->isMoving ) 00195 { 00196 // the speed is measured between two timer ticks 00197 const int cursorYPos = d->scrollArea->mapFromGlobal(QCursor::pos()).y(); 00198 d->velocity = cursorYPos - d->lastMouseYPos; 00199 d->lastMouseYPos = cursorYPos; 00200 } 00201 else if( !d->isPressed && d->isMoving ) 00202 { 00203 // use the previously recorded speed and gradually decelerate 00204 d->velocity = qBound(-gMaxDecelerationSpeed, d->velocity, gMaxDecelerationSpeed); 00205 if( d->velocity > 0 ) 00206 d->velocity -= gFriction; 00207 else if( d->velocity < 0 ) 00208 d->velocity += gFriction; 00209 if( qAbs(d->velocity) < qAbs(gFriction) ) 00210 d->stopMotion(); 00211 00212 const int scrollBarYPos = d->scrollArea->verticalScrollBar()->value(); 00213 d->scrollArea->verticalScrollBar()->setValue(scrollBarYPos - d->velocity); 00214 } 00215 else 00216 d->stopMotion(); 00217 }
ContextLogger2—ContextLogger2 Logger Daemon Internals—Generated on Mon May 2 13:49:56 2011 by Doxygen 1.6.1