Fix kinetic scrolling on Symbian. Fix timer handling.
[dorian] / widgets / flickcharm.cpp
1 /****************************************************************************\r
2 **\r
3 ** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).\r
4 ** All rights reserved.\r
5 ** Contact: Nokia Corporation (qt-info@nokia.com)\r
6 **\r
7 ** This file is part of the demos of the Qt Toolkit.\r
8 **\r
9 ** $QT_BEGIN_LICENSE:LGPL$\r
10 ** Commercial Usage\r
11 ** Licensees holding valid Qt Commercial licenses may use this file in\r
12 ** accordance with the Qt Commercial License Agreement provided with the\r
13 ** Software or, alternatively, in accordance with the terms contained in\r
14 ** a written agreement between you and Nokia.\r
15 **\r
16 ** GNU Lesser General Public License Usage\r
17 ** Alternatively, this file may be used under the terms of the GNU Lesser\r
18 ** General Public License version 2.1 as published by the Free Software\r
19 ** Foundation and appearing in the file LICENSE.LGPL included in the\r
20 ** packaging of this file.  Please review the following information to\r
21 ** ensure the GNU Lesser General Public License version 2.1 requirements\r
22 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.\r
23 **\r
24 ** In addition, as a special exception, Nokia gives you certain additional\r
25 ** rights.  These rights are described in the Nokia Qt LGPL Exception\r
26 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.\r
27 **\r
28 ** GNU General Public License Usage\r
29 ** Alternatively, this file may be used under the terms of the GNU\r
30 ** General Public License version 3.0 as published by the Free Software\r
31 ** Foundation and appearing in the file LICENSE.GPL included in the\r
32 ** packaging of this file.  Please review the following information to\r
33 ** ensure the GNU General Public License version 3.0 requirements will be\r
34 ** met: http://www.gnu.org/copyleft/gpl.html.\r
35 **\r
36 ** If you have questions regarding the use of this file, please contact\r
37 ** Nokia at qt-info@nokia.com.\r
38 ** $QT_END_LICENSE$\r
39 **\r
40 ****************************************************************************/\r
41 \r
42 #include "flickcharm.h"\r
43 \r
44 #include <QAbstractScrollArea>\r
45 #include <QApplication>\r
46 #include <QBasicTimer>\r
47 #include <QEvent>\r
48 #include <QHash>\r
49 #include <QList>\r
50 #include <QMouseEvent>\r
51 #include <QScrollBar>\r
52 #include <QTime>\r
53 #include <QWebFrame>\r
54 #include <QWebView>\r
55 \r
56 #include <QDebug>\r
57 \r
58 const int fingerAccuracyThreshold = 3;\r
59 \r
60 struct FlickData {\r
61     typedef enum {\r
62         Steady, // Interaction without scrolling\r
63         ManualScroll, // Scrolling manually with the finger on the screen\r
64         AutoScroll, // Scrolling automatically\r
65         AutoScrollAcceleration // Scrolling automatically but a finger is on the screen\r
66     } State;\r
67     State state;\r
68     QWidget *widget;\r
69     QPoint pressPos;\r
70     QPoint lastPos;\r
71     QPoint speed;\r
72     QTime speedTimer;\r
73     QList<QEvent*> ignored;\r
74     QTime accelerationTimer;\r
75     bool lastPosValid:1;\r
76     bool waitingAcceleration:1;\r
77 \r
78     FlickData()\r
79         : lastPosValid(false)\r
80         , waitingAcceleration(false)\r
81     {}\r
82 \r
83     void resetSpeed()\r
84     {\r
85         speed = QPoint();\r
86         lastPosValid = false;\r
87     }\r
88     void updateSpeed(const QPoint &newPosition)\r
89     {\r
90         if (lastPosValid) {\r
91             const int timeElapsed = speedTimer.elapsed();\r
92             if (timeElapsed) {\r
93                 const QPoint newPixelDiff = (newPosition - lastPos);\r
94                 const QPoint pixelsPerSecond = newPixelDiff * (1000 / timeElapsed);\r
95                 // fingers are inacurates, we ignore small changes to avoid stopping the autoscroll because\r
96                 // of a small horizontal offset when scrolling vertically\r
97                 const int newSpeedY = (qAbs(pixelsPerSecond.y()) > fingerAccuracyThreshold) ? pixelsPerSecond.y() : 0;\r
98                 const int newSpeedX = (qAbs(pixelsPerSecond.x()) > fingerAccuracyThreshold) ? pixelsPerSecond.x() : 0;\r
99                 if (state == AutoScrollAcceleration) {\r
100                     const int max = 4000; // px by seconds\r
101                     const int oldSpeedY = speed.y();\r
102                     const int oldSpeedX = speed.x();\r
103                     if ((oldSpeedY <= 0 && newSpeedY <= 0) ||  (oldSpeedY >= 0 && newSpeedY >= 0)\r
104                         && (oldSpeedX <= 0 && newSpeedX <= 0) ||  (oldSpeedX >= 0 && newSpeedX >= 0)) {\r
105                         speed.setY(qBound(-max, (oldSpeedY + (newSpeedY / 4)), max));\r
106                         speed.setX(qBound(-max, (oldSpeedX + (newSpeedX / 4)), max));\r
107                     } else {\r
108                         speed = QPoint();\r
109                     }\r
110                 } else {\r
111                     const int max = 2500; // px by seconds\r
112                     // we average the speed to avoid strange effects with the last delta\r
113                     if (!speed.isNull()) {\r
114                         speed.setX(qBound(-max, (speed.x() / 4) + (newSpeedX * 3 / 4), max));\r
115                         speed.setY(qBound(-max, (speed.y() / 4) + (newSpeedY * 3 / 4), max));\r
116                     } else {\r
117                         speed = QPoint(newSpeedX, newSpeedY);\r
118                     }\r
119                 }\r
120             }\r
121         } else {\r
122             lastPosValid = true;\r
123         }\r
124         speedTimer.start();\r
125         lastPos = newPosition;\r
126     }\r
127 \r
128     // scroll by dx, dy\r
129     // return true if the widget was scrolled\r
130     bool scrollWidget(const int dx, const int dy)\r
131     {\r
132         QAbstractScrollArea *scrollArea = qobject_cast<QAbstractScrollArea*>(widget);\r
133         if (scrollArea) {\r
134             const int x = scrollArea->horizontalScrollBar()->value();\r
135             const int y = scrollArea->verticalScrollBar()->value();\r
136             scrollArea->horizontalScrollBar()->setValue(x - dx);\r
137             scrollArea->verticalScrollBar()->setValue(y - dy);\r
138             return (scrollArea->horizontalScrollBar()->value() != x\r
139                     || scrollArea->verticalScrollBar()->value() != y);\r
140         }\r
141 \r
142         QWebView *webView = qobject_cast<QWebView*>(widget);\r
143         if (webView) {\r
144             QWebFrame *frame = webView->page()->mainFrame();\r
145             const QPoint position = frame->scrollPosition();\r
146             frame->setScrollPosition(position - QPoint(dx, dy));\r
147             return frame->scrollPosition() != position;\r
148         }\r
149         return false;\r
150     }\r
151 \r
152     bool scrollTo(const QPoint &newPosition)\r
153     {\r
154         const QPoint delta = newPosition - lastPos;\r
155         updateSpeed(newPosition);\r
156         return scrollWidget(delta.x(), delta.y());\r
157     }\r
158 };\r
159 \r
160 class FlickCharmPrivate\r
161 {\r
162 public:\r
163     QHash<QWidget*, FlickData*> flickData;\r
164     QBasicTimer ticker;\r
165     QTime timeCounter;\r
166     void startTicker(QObject *object)\r
167     {\r
168         if (!ticker.isActive())\r
169             ticker.start(15, object);\r
170         timeCounter.start();\r
171     }\r
172 };\r
173 \r
174 FlickCharm::FlickCharm(QObject *parent): QObject(parent)\r
175 {\r
176     d = new FlickCharmPrivate;\r
177 }\r
178 \r
179 FlickCharm::~FlickCharm()\r
180 {\r
181     delete d;\r
182 }\r
183 \r
184 void FlickCharm::activateOn(QWidget *widget)\r
185 {\r
186     QAbstractScrollArea *scrollArea = qobject_cast<QAbstractScrollArea*>(widget);\r
187     if (scrollArea) {\r
188         scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);\r
189         scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);\r
190 \r
191         QWidget *viewport = scrollArea->viewport();\r
192 \r
193         viewport->installEventFilter(this);\r
194         scrollArea->installEventFilter(this);\r
195 \r
196         d->flickData.remove(viewport);\r
197         d->flickData[viewport] = new FlickData;\r
198         d->flickData[viewport]->widget = widget;\r
199         d->flickData[viewport]->state = FlickData::Steady;\r
200 \r
201         return;\r
202     }\r
203 \r
204     QWebView *webView = qobject_cast<QWebView*>(widget);\r
205     if (webView) {\r
206         QWebFrame *frame = webView->page()->mainFrame();\r
207         frame->setScrollBarPolicy(Qt::Vertical, Qt::ScrollBarAlwaysOff);\r
208         frame->setScrollBarPolicy(Qt::Horizontal, Qt::ScrollBarAlwaysOff);\r
209 \r
210         webView->installEventFilter(this);\r
211 \r
212         d->flickData.remove(webView);\r
213         d->flickData[webView] = new FlickData;\r
214         d->flickData[webView]->widget = webView;\r
215         d->flickData[webView]->state = FlickData::Steady;\r
216 \r
217         return;\r
218     }\r
219 \r
220     qWarning() << "FlickCharm only works on QAbstractScrollArea (and derived classes)";\r
221     qWarning() << "or QWebView (and derived classes)";\r
222 }\r
223 \r
224 void FlickCharm::deactivateFrom(QWidget *widget)\r
225 {\r
226     QAbstractScrollArea *scrollArea = qobject_cast<QAbstractScrollArea*>(widget);\r
227     if (scrollArea) {\r
228         QWidget *viewport = scrollArea->viewport();\r
229 \r
230         viewport->removeEventFilter(this);\r
231         scrollArea->removeEventFilter(this);\r
232 \r
233         delete d->flickData[viewport];\r
234         d->flickData.remove(viewport);\r
235 \r
236         return;\r
237     }\r
238 \r
239     QWebView *webView = qobject_cast<QWebView*>(widget);\r
240     if (webView) {\r
241         webView->removeEventFilter(this);\r
242 \r
243         delete d->flickData[webView];\r
244         d->flickData.remove(webView);\r
245 \r
246         return;\r
247     }\r
248 }\r
249 \r
250 static QPoint deaccelerate(const QPoint &speed, const int deltatime)\r
251 {\r
252     const int deltaSpeed = deltatime;\r
253 \r
254     int x = speed.x();\r
255     int y = speed.y();\r
256     x = (x == 0) ? x : (x > 0) ? qMax(0, x - deltaSpeed) : qMin(0, x + deltaSpeed);\r
257     y = (y == 0) ? y : (y > 0) ? qMax(0, y - deltaSpeed) : qMin(0, y + deltaSpeed);\r
258     return QPoint(x, y);\r
259 }\r
260 \r
261 bool FlickCharm::eventFilter(QObject *object, QEvent *event)\r
262 {\r
263     if (!object->isWidgetType())\r
264         return false;\r
265 \r
266     const QEvent::Type type = event->type();\r
267 \r
268     switch (type) {\r
269     case QEvent::MouseButtonPress:\r
270     case QEvent::MouseMove:\r
271     case QEvent::MouseButtonRelease:\r
272         break;\r
273     case QEvent::MouseButtonDblClick: // skip double click\r
274         return true;\r
275     default:\r
276         return false;\r
277     }\r
278 \r
279     QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);\r
280     if (type == QEvent::MouseMove && mouseEvent->buttons() != Qt::LeftButton)\r
281         return false;\r
282 \r
283     if (mouseEvent->modifiers() != Qt::NoModifier)\r
284         return false;\r
285 \r
286     QWidget *viewport = qobject_cast<QWidget*>(object);\r
287     FlickData *data = d->flickData.value(viewport);\r
288     if (!viewport || !data || data->ignored.removeAll(event))\r
289         return false;\r
290 \r
291     const QPoint mousePos = mouseEvent->pos();\r
292     bool consumed = false;\r
293     switch (data->state) {\r
294 \r
295     case FlickData::Steady:\r
296         if (type == QEvent::MouseButtonPress) {\r
297             consumed = true;\r
298             data->pressPos = mousePos;\r
299         } else if (type == QEvent::MouseButtonRelease) {\r
300             consumed = true;\r
301             QMouseEvent *event1 = new QMouseEvent(QEvent::MouseButtonPress,\r
302                                                   data->pressPos, Qt::LeftButton,\r
303                                                   Qt::LeftButton, Qt::NoModifier);\r
304             QMouseEvent *event2 = new QMouseEvent(QEvent::MouseButtonRelease,\r
305                                                   data->pressPos, Qt::LeftButton,\r
306                                                   Qt::LeftButton, Qt::NoModifier);\r
307 \r
308             data->ignored << event1;\r
309             data->ignored << event2;\r
310             QApplication::postEvent(object, event1);\r
311             QApplication::postEvent(object, event2);\r
312         } else if (type == QEvent::MouseMove) {\r
313             consumed = true;\r
314             data->scrollTo(mousePos);\r
315 \r
316             const QPoint delta = mousePos - data->pressPos;\r
317             if (delta.x() > fingerAccuracyThreshold || delta.y() > fingerAccuracyThreshold)\r
318                 data->state = FlickData::ManualScroll;\r
319         }\r
320         break;\r
321 \r
322     case FlickData::ManualScroll:\r
323         if (type == QEvent::MouseMove) {\r
324             consumed = true;\r
325             data->scrollTo(mousePos);\r
326         } else if (type == QEvent::MouseButtonRelease) {\r
327             consumed = true;\r
328             data->state = FlickData::AutoScroll;\r
329             data->lastPosValid = false;\r
330             d->startTicker(this);\r
331         }\r
332         break;\r
333 \r
334     case FlickData::AutoScroll:\r
335         if (type == QEvent::MouseButtonPress) {\r
336             consumed = true;\r
337             data->state = FlickData::AutoScrollAcceleration;\r
338             data->waitingAcceleration = true;\r
339             data->accelerationTimer.start();\r
340             data->updateSpeed(mousePos);\r
341             data->pressPos = mousePos;\r
342         } else if (type == QEvent::MouseButtonRelease) {\r
343             consumed = true;\r
344             data->state = FlickData::Steady;\r
345             data->resetSpeed();\r
346         }\r
347         break;\r
348 \r
349     case FlickData::AutoScrollAcceleration:\r
350         if (type == QEvent::MouseMove) {\r
351             consumed = true;\r
352             data->updateSpeed(mousePos);\r
353             data->accelerationTimer.start();\r
354             if (data->speed.isNull())\r
355                 data->state = FlickData::ManualScroll;\r
356         } else if (type == QEvent::MouseButtonRelease) {\r
357             consumed = true;\r
358             data->state = FlickData::AutoScroll;\r
359             data->waitingAcceleration = false;\r
360             data->lastPosValid = false;\r
361         }\r
362         break;\r
363     default:\r
364         break;\r
365     }\r
366     data->lastPos = mousePos;\r
367     return true;\r
368 }\r
369 \r
370 void FlickCharm::timerEvent(QTimerEvent *event)\r
371 {\r
372     int count = 0;\r
373     QHashIterator<QWidget*, FlickData*> item(d->flickData);\r
374     while (item.hasNext()) {\r
375         item.next();\r
376         FlickData *data = item.value();\r
377         if (data->state == FlickData::AutoScrollAcceleration\r
378             && data->waitingAcceleration\r
379             && data->accelerationTimer.elapsed() > 40) {\r
380             data->state = FlickData::ManualScroll;\r
381             data->resetSpeed();\r
382         }\r
383         if (data->state == FlickData::AutoScroll || data->state == FlickData::AutoScrollAcceleration) {\r
384             const int timeElapsed = d->timeCounter.elapsed();\r
385             const QPoint delta = (data->speed) * timeElapsed / 1000;\r
386             bool hasScrolled = data->scrollWidget(delta.x(), delta.y());\r
387 \r
388             if (data->speed.isNull() || !hasScrolled)\r
389                 data->state = FlickData::Steady;\r
390             else\r
391                 count++;\r
392             data->speed = deaccelerate(data->speed, timeElapsed);\r
393         }\r
394     }\r
395 \r
396     if (!count)\r
397         d->ticker.stop();\r
398     else\r
399         d->timeCounter.start();\r
400 \r
401     QObject::timerEvent(event);\r
402 }\r