1 | /****************************************************************************
|
---|
2 | **
|
---|
3 | ** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
|
---|
4 | ** All rights reserved.
|
---|
5 | ** Contact: Nokia Corporation ([email protected])
|
---|
6 | **
|
---|
7 | ** This file is part of the demos of the Qt Toolkit.
|
---|
8 | **
|
---|
9 | ** $QT_BEGIN_LICENSE:LGPL$
|
---|
10 | ** Commercial Usage
|
---|
11 | ** Licensees holding valid Qt Commercial licenses may use this file in
|
---|
12 | ** accordance with the Qt Commercial License Agreement provided with the
|
---|
13 | ** Software or, alternatively, in accordance with the terms contained in
|
---|
14 | ** a written agreement between you and Nokia.
|
---|
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 | ** GNU General Public License Usage
|
---|
29 | ** Alternatively, this file may be used under the terms of the GNU
|
---|
30 | ** General Public License version 3.0 as published by the Free Software
|
---|
31 | ** Foundation and appearing in the file LICENSE.GPL included in the
|
---|
32 | ** packaging of this file. Please review the following information to
|
---|
33 | ** ensure the GNU General Public License version 3.0 requirements will be
|
---|
34 | ** met: http://www.gnu.org/copyleft/gpl.html.
|
---|
35 | **
|
---|
36 | ** If you have questions regarding the use of this file, please contact
|
---|
37 | ** Nokia at [email protected].
|
---|
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 | }
|
---|