47 #include <QEvent> |
47 #include <QEvent> |
48 #include <QHash> |
48 #include <QHash> |
49 #include <QList> |
49 #include <QList> |
50 #include <QMouseEvent> |
50 #include <QMouseEvent> |
51 #include <QScrollBar> |
51 #include <QScrollBar> |
|
52 #include <QTime> |
52 #include <QWebFrame> |
53 #include <QWebFrame> |
53 #include <QWebView> |
54 #include <QWebView> |
54 |
55 |
55 #include <QDebug> |
56 #include <QDebug> |
56 |
57 |
|
58 const int fingerAccuracyThreshold = 3; |
|
59 |
57 struct FlickData { |
60 struct FlickData { |
58 typedef enum { Steady, Pressed, ManualScroll, AutoScroll, Stop } State; |
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; |
59 State state; |
67 State state; |
60 QWidget *widget; |
68 QWidget *widget; |
61 QPoint pressPos; |
69 QPoint pressPos; |
62 QPoint offset; |
70 QPoint lastPos; |
63 QPoint dragPos; |
|
64 QPoint speed; |
71 QPoint speed; |
|
72 QTime speedTimer; |
65 QList<QEvent*> ignored; |
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 } |
66 }; |
158 }; |
67 |
159 |
68 class FlickCharmPrivate |
160 class FlickCharmPrivate |
69 { |
161 { |
70 public: |
162 public: |
71 QHash<QWidget*, FlickData*> flickData; |
163 QHash<QWidget*, FlickData*> flickData; |
72 QBasicTimer ticker; |
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 } |
73 }; |
172 }; |
74 |
173 |
75 FlickCharm::FlickCharm(QObject *parent): QObject(parent) |
174 FlickCharm::FlickCharm(QObject *parent): QObject(parent) |
76 { |
175 { |
77 d = new FlickCharmPrivate; |
176 d = new FlickCharmPrivate; |
146 |
245 |
147 return; |
246 return; |
148 } |
247 } |
149 } |
248 } |
150 |
249 |
151 static QPoint scrollOffset(QWidget *widget) |
250 static QPoint deaccelerate(const QPoint &speed, const int deltatime) |
152 { |
251 { |
153 int x = 0, y = 0; |
252 const int deltaSpeed = deltatime; |
154 |
253 |
155 QAbstractScrollArea *scrollArea = qobject_cast<QAbstractScrollArea*>(widget); |
254 int x = speed.x(); |
156 if (scrollArea) { |
255 int y = speed.y(); |
157 x = scrollArea->horizontalScrollBar()->value(); |
256 x = (x == 0) ? x : (x > 0) ? qMax(0, x - deltaSpeed) : qMin(0, x + deltaSpeed); |
158 y = scrollArea->verticalScrollBar()->value(); |
257 y = (y == 0) ? y : (y > 0) ? qMax(0, y - deltaSpeed) : qMin(0, y + deltaSpeed); |
159 } |
|
160 |
|
161 QWebView *webView = qobject_cast<QWebView*>(widget); |
|
162 if (webView) { |
|
163 QWebFrame *frame = webView->page()->mainFrame(); |
|
164 x = frame->evaluateJavaScript("window.scrollX").toInt(); |
|
165 y = frame->evaluateJavaScript("window.scrollY").toInt(); |
|
166 } |
|
167 |
|
168 return QPoint(x, y); |
258 return QPoint(x, y); |
169 } |
259 } |
170 |
260 |
171 static void setScrollOffset(QWidget *widget, const QPoint &p) |
|
172 { |
|
173 QAbstractScrollArea *scrollArea = qobject_cast<QAbstractScrollArea*>(widget); |
|
174 if (scrollArea) { |
|
175 scrollArea->horizontalScrollBar()->setValue(p.x()); |
|
176 scrollArea->verticalScrollBar()->setValue(p.y()); |
|
177 } |
|
178 |
|
179 QWebView *webView = qobject_cast<QWebView*>(widget); |
|
180 QWebFrame *frame = webView ? webView->page()->mainFrame() : 0; |
|
181 if (frame) |
|
182 frame->evaluateJavaScript(QString("window.scrollTo(%1,%2);").arg(p.x()).arg(p.y())); |
|
183 } |
|
184 |
|
185 static QPoint deaccelerate(const QPoint &speed, int a = 1, int max = 64) |
|
186 { |
|
187 int x = qBound(-max, speed.x(), max); |
|
188 int y = qBound(-max, speed.y(), max); |
|
189 x = (x == 0) ? x : (x > 0) ? qMax(0, x - a) : qMin(0, x + a); |
|
190 y = (y == 0) ? y : (y > 0) ? qMax(0, y - a) : qMin(0, y + a); |
|
191 return QPoint(x, y); |
|
192 } |
|
193 |
|
194 bool FlickCharm::eventFilter(QObject *object, QEvent *event) |
261 bool FlickCharm::eventFilter(QObject *object, QEvent *event) |
195 { |
262 { |
196 if (!object->isWidgetType()) |
263 if (!object->isWidgetType()) |
197 return false; |
264 return false; |
198 |
265 |
199 QEvent::Type type = event->type(); |
266 const QEvent::Type type = event->type(); |
200 if (type != QEvent::MouseButtonPress && |
267 |
201 type != QEvent::MouseButtonRelease && |
268 switch (type) { |
202 type != QEvent::MouseMove) |
269 case QEvent::MouseButtonPress: |
203 return false; |
270 case QEvent::MouseMove: |
204 |
271 case QEvent::MouseButtonRelease: |
205 QMouseEvent *mouseEvent = 0; |
272 break; |
206 switch (event->type()) { |
273 case QEvent::MouseButtonDblClick: // skip double click |
207 case QEvent::MouseButtonPress: |
274 return true; |
208 case QEvent::MouseButtonRelease: |
275 default: |
209 case QEvent::MouseMove: |
276 return false; |
210 mouseEvent = static_cast<QMouseEvent*>(event); |
277 } |
211 break; |
278 |
212 } |
279 QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event); |
213 |
280 if (type == QEvent::MouseMove && mouseEvent->buttons() != Qt::LeftButton) |
214 if (!mouseEvent || mouseEvent->modifiers() != Qt::NoModifier) |
281 return false; |
|
282 |
|
283 if (mouseEvent->modifiers() != Qt::NoModifier) |
215 return false; |
284 return false; |
216 |
285 |
217 QWidget *viewport = qobject_cast<QWidget*>(object); |
286 QWidget *viewport = qobject_cast<QWidget*>(object); |
218 FlickData *data = d->flickData.value(viewport); |
287 FlickData *data = d->flickData.value(viewport); |
219 if (!viewport || !data || data->ignored.removeAll(event)) |
288 if (!viewport || !data || data->ignored.removeAll(event)) |
220 return false; |
289 return false; |
221 |
290 |
|
291 const QPoint mousePos = mouseEvent->pos(); |
222 bool consumed = false; |
292 bool consumed = false; |
223 switch (data->state) { |
293 switch (data->state) { |
224 |
294 |
225 case FlickData::Steady: |
295 case FlickData::Steady: |
226 if (mouseEvent->type() == QEvent::MouseButtonPress) |
296 if (type == QEvent::MouseButtonPress) { |
227 if (mouseEvent->buttons() == Qt::LeftButton) { |
297 consumed = true; |
228 consumed = true; |
298 data->pressPos = mousePos; |
229 data->state = FlickData::Pressed; |
299 } else if (type == QEvent::MouseButtonRelease) { |
230 data->pressPos = mouseEvent->pos(); |
300 consumed = true; |
231 data->offset = scrollOffset(data->widget); |
|
232 } |
|
233 break; |
|
234 |
|
235 case FlickData::Pressed: |
|
236 if (mouseEvent->type() == QEvent::MouseButtonRelease) { |
|
237 consumed = true; |
|
238 data->state = FlickData::Steady; |
|
239 |
|
240 QMouseEvent *event1 = new QMouseEvent(QEvent::MouseButtonPress, |
301 QMouseEvent *event1 = new QMouseEvent(QEvent::MouseButtonPress, |
241 data->pressPos, Qt::LeftButton, |
302 data->pressPos, Qt::LeftButton, |
242 Qt::LeftButton, Qt::NoModifier); |
303 Qt::LeftButton, Qt::NoModifier); |
243 QMouseEvent *event2 = new QMouseEvent(*mouseEvent); |
304 QMouseEvent *event2 = new QMouseEvent(QEvent::MouseButtonRelease, |
|
305 data->pressPos, Qt::LeftButton, |
|
306 Qt::LeftButton, Qt::NoModifier); |
244 |
307 |
245 data->ignored << event1; |
308 data->ignored << event1; |
246 data->ignored << event2; |
309 data->ignored << event2; |
247 QApplication::postEvent(object, event1); |
310 QApplication::postEvent(object, event1); |
248 QApplication::postEvent(object, event2); |
311 QApplication::postEvent(object, event2); |
249 } |
312 } else if (type == QEvent::MouseMove) { |
250 if (mouseEvent->type() == QEvent::MouseMove) { |
313 consumed = true; |
251 consumed = true; |
314 data->scrollTo(mousePos); |
252 data->state = FlickData::ManualScroll; |
315 |
253 data->dragPos = QCursor::pos(); |
316 const QPoint delta = mousePos - data->pressPos; |
254 if (!d->ticker.isActive()) |
317 if (delta.x() > fingerAccuracyThreshold || delta.y() > fingerAccuracyThreshold) |
255 d->ticker.start(20, this); |
318 data->state = FlickData::ManualScroll; |
256 } |
319 } |
257 break; |
320 break; |
258 |
321 |
259 case FlickData::ManualScroll: |
322 case FlickData::ManualScroll: |
260 if (mouseEvent->type() == QEvent::MouseMove) { |
323 if (type == QEvent::MouseMove) { |
261 consumed = true; |
324 consumed = true; |
262 QPoint delta = mouseEvent->pos() - data->pressPos; |
325 data->scrollTo(mousePos); |
263 setScrollOffset(data->widget, data->offset - delta); |
326 } else if (type == QEvent::MouseButtonRelease) { |
264 } |
|
265 if (mouseEvent->type() == QEvent::MouseButtonRelease) { |
|
266 consumed = true; |
327 consumed = true; |
267 data->state = FlickData::AutoScroll; |
328 data->state = FlickData::AutoScroll; |
|
329 data->lastPosValid = false; |
|
330 d->startTicker(this); |
268 } |
331 } |
269 break; |
332 break; |
270 |
333 |
271 case FlickData::AutoScroll: |
334 case FlickData::AutoScroll: |
272 if (mouseEvent->type() == QEvent::MouseButtonPress) { |
335 if (type == QEvent::MouseButtonPress) { |
273 consumed = true; |
336 consumed = true; |
274 data->state = FlickData::Stop; |
337 data->state = FlickData::AutoScrollAcceleration; |
275 data->speed = QPoint(0, 0); |
338 data->waitingAcceleration = true; |
276 data->pressPos = mouseEvent->pos(); |
339 data->accelerationTimer.start(); |
277 data->offset = scrollOffset(data->widget); |
340 data->updateSpeed(mousePos); |
278 } |
341 data->pressPos = mousePos; |
279 if (mouseEvent->type() == QEvent::MouseButtonRelease) { |
342 } else if (type == QEvent::MouseButtonRelease) { |
280 consumed = true; |
343 consumed = true; |
281 data->state = FlickData::Steady; |
344 data->state = FlickData::Steady; |
282 data->speed = QPoint(0, 0); |
345 data->resetSpeed(); |
283 } |
346 } |
284 break; |
347 break; |
285 |
348 |
286 case FlickData::Stop: |
349 case FlickData::AutoScrollAcceleration: |
287 if (mouseEvent->type() == QEvent::MouseButtonRelease) { |
350 if (type == QEvent::MouseMove) { |
288 consumed = true; |
351 consumed = true; |
289 data->state = FlickData::Steady; |
352 data->updateSpeed(mousePos); |
290 } |
353 data->accelerationTimer.start(); |
291 if (mouseEvent->type() == QEvent::MouseMove) { |
354 if (data->speed.isNull()) |
292 consumed = true; |
355 data->state = FlickData::ManualScroll; |
293 data->state = FlickData::ManualScroll; |
356 } else if (type == QEvent::MouseButtonRelease) { |
294 data->dragPos = QCursor::pos(); |
357 consumed = true; |
295 if (!d->ticker.isActive()) |
358 data->state = FlickData::AutoScroll; |
296 d->ticker.start(20, this); |
359 data->waitingAcceleration = false; |
297 } |
360 data->lastPosValid = false; |
298 break; |
361 } |
299 |
362 break; |
300 default: |
363 default: |
301 break; |
364 break; |
302 } |
365 } |
303 |
366 data->lastPos = mousePos; |
304 return consumed; |
367 return true; |
305 } |
368 } |
306 |
369 |
307 void FlickCharm::timerEvent(QTimerEvent *event) |
370 void FlickCharm::timerEvent(QTimerEvent *event) |
308 { |
371 { |
309 int count = 0; |
372 int count = 0; |
310 QHashIterator<QWidget*, FlickData*> item(d->flickData); |
373 QHashIterator<QWidget*, FlickData*> item(d->flickData); |
311 while (item.hasNext()) { |
374 while (item.hasNext()) { |
312 item.next(); |
375 item.next(); |
313 FlickData *data = item.value(); |
376 FlickData *data = item.value(); |
314 |
377 if (data->state == FlickData::AutoScrollAcceleration |
315 if (data->state == FlickData::ManualScroll) { |
378 && data->waitingAcceleration |
316 count++; |
379 && data->accelerationTimer.elapsed() > 40) { |
317 data->speed = QCursor::pos() - data->dragPos; |
380 data->state = FlickData::ManualScroll; |
318 data->dragPos = QCursor::pos(); |
381 data->resetSpeed(); |
319 } |
382 } |
320 |
383 if (data->state == FlickData::AutoScroll || data->state == FlickData::AutoScrollAcceleration) { |
321 if (data->state == FlickData::AutoScroll) { |
384 const int timeElapsed = d->timeCounter.elapsed(); |
322 count++; |
385 const QPoint delta = (data->speed) * timeElapsed / 1000; |
323 data->speed = deaccelerate(data->speed); |
386 bool hasScrolled = data->scrollWidget(delta.x(), delta.y()); |
324 QPoint p = scrollOffset(data->widget); |
387 |
325 setScrollOffset(data->widget, p - data->speed); |
388 if (data->speed.isNull() || !hasScrolled) |
326 if (data->speed == QPoint(0, 0)) |
|
327 data->state = FlickData::Steady; |
389 data->state = FlickData::Steady; |
|
390 else |
|
391 count++; |
|
392 data->speed = deaccelerate(data->speed, timeElapsed); |
328 } |
393 } |
329 } |
394 } |
330 |
395 |
331 if (!count) |
396 if (!count) |
332 d->ticker.stop(); |
397 d->ticker.stop(); |
|
398 else |
|
399 d->timeCounter.start(); |
333 |
400 |
334 QObject::timerEvent(event); |
401 QObject::timerEvent(event); |
335 } |
402 } |