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