Fix Symbian bugs.
[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 #ifdef Q_OS_SYMBIAN\r
61 const int maxSpeed = 2000;\r
62 const int maxSpeedAutoScroll = 1250;\r
63 #else\r
64 const int maxSpeed = 4000;\r
65 const int maxSpeedAutoScroll = 2500;\r
66 #endif\r
67 \r
68 struct FlickData {\r
69     typedef enum {\r
70         Steady, // Interaction without scrolling\r
71         ManualScroll, // Scrolling manually with the finger on the screen\r
72         AutoScroll, // Scrolling automatically\r
73         AutoScrollAcceleration // Scrolling automatically but a finger is on the screen\r
74     } State;\r
75     State state;\r
76     QWidget *widget;\r
77     QPoint pressPos;\r
78     QPoint lastPos;\r
79     QPoint speed;\r
80     QTime speedTimer;\r
81     QList<QEvent*> ignored;\r
82     QTime accelerationTimer;\r
83     bool lastPosValid:1;\r
84     bool waitingAcceleration:1;\r
85 \r
86     FlickData()\r
87         : lastPosValid(false)\r
88         , waitingAcceleration(false)\r
89     {}\r
90 \r
91     void resetSpeed()\r
92     {\r
93         speed = QPoint();\r
94         lastPosValid = false;\r
95     }\r
96     void updateSpeed(const QPoint &newPosition)\r
97     {\r
98         if (lastPosValid) {\r
99             const int timeElapsed = speedTimer.elapsed();\r
100             if (timeElapsed) {\r
101                 const QPoint newPixelDiff = (newPosition - lastPos);\r
102                 const QPoint pixelsPerSecond = newPixelDiff * (1000 / timeElapsed);\r
103                 // fingers are inacurates, we ignore small changes to avoid stopping the autoscroll because\r
104                 // of a small horizontal offset when scrolling vertically\r
105                 const int newSpeedY = (qAbs(pixelsPerSecond.y()) > fingerAccuracyThreshold) ? pixelsPerSecond.y() : 0;\r
106                 const int newSpeedX = (qAbs(pixelsPerSecond.x()) > fingerAccuracyThreshold) ? pixelsPerSecond.x() : 0;\r
107                 if (state == AutoScrollAcceleration) {\r
108                     const int max = maxSpeedAutoScroll; // px by seconds\r
109                     const int oldSpeedY = speed.y();\r
110                     const int oldSpeedX = speed.x();\r
111                     if ((oldSpeedY <= 0 && newSpeedY <= 0) ||  (oldSpeedY >= 0 && newSpeedY >= 0)\r
112                         && (oldSpeedX <= 0 && newSpeedX <= 0) ||  (oldSpeedX >= 0 && newSpeedX >= 0)) {\r
113                         speed.setY(qBound(-max, (oldSpeedY + (newSpeedY / 4)), max));\r
114                         speed.setX(qBound(-max, (oldSpeedX + (newSpeedX / 4)), max));\r
115                     } else {\r
116                         speed = QPoint();\r
117                     }\r
118                 } else {\r
119                     const int max = maxSpeed; // px by seconds\r
120                     // we average the speed to avoid strange effects with the last delta\r
121                     if (!speed.isNull()) {\r
122                         speed.setX(qBound(-max, (speed.x() / 4) + (newSpeedX * 3 / 4), max));\r
123                         speed.setY(qBound(-max, (speed.y() / 4) + (newSpeedY * 3 / 4), max));\r
124                     } else {\r
125                         speed = QPoint(newSpeedX, newSpeedY);\r
126                     }\r
127                 }\r
128             }\r
129         } else {\r
130             lastPosValid = true;\r
131         }\r
132         speedTimer.start();\r
133         lastPos = newPosition;\r
134     }\r
135 \r
136     // scroll by dx, dy\r
137     // return true if the widget was scrolled\r
138     bool scrollWidget(const int dx, const int dy)\r
139     {\r
140         QAbstractScrollArea *scrollArea = qobject_cast<QAbstractScrollArea*>(widget);\r
141         if (scrollArea) {\r
142             const int x = scrollArea->horizontalScrollBar()->value();\r
143             const int y = scrollArea->verticalScrollBar()->value();\r
144             scrollArea->horizontalScrollBar()->setValue(x - dx);\r
145             scrollArea->verticalScrollBar()->setValue(y - dy);\r
146             return (scrollArea->horizontalScrollBar()->value() != x\r
147                     || scrollArea->verticalScrollBar()->value() != y);\r
148         }\r
149 \r
150         QWebView *webView = qobject_cast<QWebView*>(widget);\r
151         if (webView) {\r
152             QWebFrame *frame = webView->page()->mainFrame();\r
153             const QPoint position = frame->scrollPosition();\r
154             frame->setScrollPosition(position - QPoint(dx, dy));\r
155             return frame->scrollPosition() != position;\r
156         }\r
157         return false;\r
158     }\r
159 \r
160     bool scrollTo(const QPoint &newPosition)\r
161     {\r
162         const QPoint delta = newPosition - lastPos;\r
163         updateSpeed(newPosition);\r
164         return scrollWidget(delta.x(), delta.y());\r
165     }\r
166 };\r
167 \r
168 class FlickCharmPrivate\r
169 {\r
170 public:\r
171     QHash<QWidget*, FlickData*> flickData;\r
172     QBasicTimer ticker;\r
173     QTime timeCounter;\r
174     void startTicker(QObject *object)\r
175     {\r
176         if (!ticker.isActive())\r
177             ticker.start(15, object);\r
178         timeCounter.start();\r
179     }\r
180 };\r
181 \r
182 FlickCharm::FlickCharm(QObject *parent): QObject(parent)\r
183 {\r
184     d = new FlickCharmPrivate;\r
185 }\r
186 \r
187 FlickCharm::~FlickCharm()\r
188 {\r
189     delete d;\r
190 }\r
191 \r
192 void FlickCharm::activateOn(QWidget *widget)\r
193 {\r
194     QAbstractScrollArea *scrollArea = qobject_cast<QAbstractScrollArea*>(widget);\r
195     if (scrollArea) {\r
196         scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);\r
197         scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);\r
198 \r
199         QWidget *viewport = scrollArea->viewport();\r
200 \r
201         viewport->installEventFilter(this);\r
202         scrollArea->installEventFilter(this);\r
203 \r
204         d->flickData.remove(viewport);\r
205         d->flickData[viewport] = new FlickData;\r
206         d->flickData[viewport]->widget = widget;\r
207         d->flickData[viewport]->state = FlickData::Steady;\r
208 \r
209         return;\r
210     }\r
211 \r
212     QWebView *webView = qobject_cast<QWebView*>(widget);\r
213     if (webView) {\r
214         QWebFrame *frame = webView->page()->mainFrame();\r
215         frame->setScrollBarPolicy(Qt::Vertical, Qt::ScrollBarAlwaysOff);\r
216         frame->setScrollBarPolicy(Qt::Horizontal, Qt::ScrollBarAlwaysOff);\r
217 \r
218         webView->installEventFilter(this);\r
219 \r
220         d->flickData.remove(webView);\r
221         d->flickData[webView] = new FlickData;\r
222         d->flickData[webView]->widget = webView;\r
223         d->flickData[webView]->state = FlickData::Steady;\r
224 \r
225         return;\r
226     }\r
227 \r
228     qWarning() << "FlickCharm only works on QAbstractScrollArea (and derived classes)";\r
229     qWarning() << "or QWebView (and derived classes)";\r
230 }\r
231 \r
232 void FlickCharm::deactivateFrom(QWidget *widget)\r
233 {\r
234     QAbstractScrollArea *scrollArea = qobject_cast<QAbstractScrollArea*>(widget);\r
235     if (scrollArea) {\r
236         QWidget *viewport = scrollArea->viewport();\r
237 \r
238         viewport->removeEventFilter(this);\r
239         scrollArea->removeEventFilter(this);\r
240 \r
241         delete d->flickData[viewport];\r
242         d->flickData.remove(viewport);\r
243 \r
244         return;\r
245     }\r
246 \r
247     QWebView *webView = qobject_cast<QWebView*>(widget);\r
248     if (webView) {\r
249         webView->removeEventFilter(this);\r
250 \r
251         delete d->flickData[webView];\r
252         d->flickData.remove(webView);\r
253 \r
254         return;\r
255     }\r
256 }\r
257 \r
258 static QPoint deaccelerate(const QPoint &speed, const int deltatime)\r
259 {\r
260     const int deltaSpeed = deltatime;\r
261 \r
262     int x = speed.x();\r
263     int y = speed.y();\r
264     x = (x == 0) ? x : (x > 0) ? qMax(0, x - deltaSpeed) : qMin(0, x + deltaSpeed);\r
265     y = (y == 0) ? y : (y > 0) ? qMax(0, y - deltaSpeed) : qMin(0, y + deltaSpeed);\r
266     return QPoint(x, y);\r
267 }\r
268 \r
269 bool FlickCharm::eventFilter(QObject *object, QEvent *event)\r
270 {\r
271     if (!object->isWidgetType())\r
272         return false;\r
273 \r
274     const QEvent::Type type = event->type();\r
275 \r
276     switch (type) {\r
277     case QEvent::MouseButtonPress:\r
278     case QEvent::MouseMove:\r
279     case QEvent::MouseButtonRelease:\r
280         break;\r
281     case QEvent::MouseButtonDblClick: // skip double click\r
282         return true;\r
283     default:\r
284         return false;\r
285     }\r
286 \r
287     QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);\r
288     if (type == QEvent::MouseMove && mouseEvent->buttons() != Qt::LeftButton)\r
289         return false;\r
290 \r
291     if (mouseEvent->modifiers() != Qt::NoModifier)\r
292         return false;\r
293 \r
294     QWidget *viewport = qobject_cast<QWidget*>(object);\r
295     FlickData *data = d->flickData.value(viewport);\r
296     if (!viewport || !data || data->ignored.removeAll(event))\r
297         return false;\r
298 \r
299     const QPoint mousePos = mouseEvent->pos();\r
300     bool consumed = false;\r
301     switch (data->state) {\r
302 \r
303     case FlickData::Steady:\r
304         if (type == QEvent::MouseButtonPress) {\r
305             consumed = true;\r
306             data->pressPos = mousePos;\r
307         } else if (type == QEvent::MouseButtonRelease) {\r
308             consumed = true;\r
309             QMouseEvent *event1 = new QMouseEvent(QEvent::MouseButtonPress,\r
310                                                   data->pressPos, Qt::LeftButton,\r
311                                                   Qt::LeftButton, Qt::NoModifier);\r
312             QMouseEvent *event2 = new QMouseEvent(QEvent::MouseButtonRelease,\r
313                                                   data->pressPos, Qt::LeftButton,\r
314                                                   Qt::LeftButton, Qt::NoModifier);\r
315 \r
316             data->ignored << event1;\r
317             data->ignored << event2;\r
318             QApplication::postEvent(object, event1);\r
319             QApplication::postEvent(object, event2);\r
320         } else if (type == QEvent::MouseMove) {\r
321             consumed = true;\r
322             data->scrollTo(mousePos);\r
323 \r
324             const QPoint delta = mousePos - data->pressPos;\r
325             if (delta.x() > fingerAccuracyThreshold || delta.y() > fingerAccuracyThreshold)\r
326                 data->state = FlickData::ManualScroll;\r
327         }\r
328         break;\r
329 \r
330     case FlickData::ManualScroll:\r
331         if (type == QEvent::MouseMove) {\r
332             consumed = true;\r
333             data->scrollTo(mousePos);\r
334         } else if (type == QEvent::MouseButtonRelease) {\r
335             consumed = true;\r
336             data->state = FlickData::AutoScroll;\r
337             data->lastPosValid = false;\r
338             d->startTicker(this);\r
339         }\r
340         break;\r
341 \r
342     case FlickData::AutoScroll:\r
343         if (type == QEvent::MouseButtonPress) {\r
344             consumed = true;\r
345             data->state = FlickData::AutoScrollAcceleration;\r
346             data->waitingAcceleration = true;\r
347             data->accelerationTimer.start();\r
348             data->updateSpeed(mousePos);\r
349             data->pressPos = mousePos;\r
350         } else if (type == QEvent::MouseButtonRelease) {\r
351             consumed = true;\r
352             data->state = FlickData::Steady;\r
353             data->resetSpeed();\r
354         }\r
355         break;\r
356 \r
357     case FlickData::AutoScrollAcceleration:\r
358         if (type == QEvent::MouseMove) {\r
359             consumed = true;\r
360             data->updateSpeed(mousePos);\r
361             data->accelerationTimer.start();\r
362             if (data->speed.isNull())\r
363                 data->state = FlickData::ManualScroll;\r
364         } else if (type == QEvent::MouseButtonRelease) {\r
365             consumed = true;\r
366             data->state = FlickData::AutoScroll;\r
367             data->waitingAcceleration = false;\r
368             data->lastPosValid = false;\r
369         }\r
370         break;\r
371     default:\r
372         break;\r
373     }\r
374     data->lastPos = mousePos;\r
375     return true;\r
376 }\r
377 \r
378 void FlickCharm::timerEvent(QTimerEvent *event)\r
379 {\r
380     int count = 0;\r
381     QHashIterator<QWidget*, FlickData*> item(d->flickData);\r
382     while (item.hasNext()) {\r
383         item.next();\r
384         FlickData *data = item.value();\r
385         if (data->state == FlickData::AutoScrollAcceleration\r
386             && data->waitingAcceleration\r
387             && data->accelerationTimer.elapsed() > 40) {\r
388             data->state = FlickData::ManualScroll;\r
389             data->resetSpeed();\r
390         }\r
391         if (data->state == FlickData::AutoScroll || data->state == FlickData::AutoScrollAcceleration) {\r
392             const int timeElapsed = d->timeCounter.elapsed();\r
393             const QPoint delta = (data->speed) * timeElapsed / 1000;\r
394             bool hasScrolled = data->scrollWidget(delta.x(), delta.y());\r
395 \r
396             if (data->speed.isNull() || !hasScrolled)\r
397                 data->state = FlickData::Steady;\r
398             else\r
399                 count++;\r
400             data->speed = deaccelerate(data->speed, timeElapsed);\r
401         }\r
402     }\r
403 \r
404     if (!count)\r
405         d->ticker.stop();\r
406     else\r
407         d->timeCounter.start();\r
408 \r
409     QObject::timerEvent(event);\r
410 }\r