1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qgesture.h"
5#include "qapplication.h"
6#include "qevent.h"
7#include "qwidget.h"
8#if QT_CONFIG(graphicsview)
9#include "qgraphicsitem.h"
10#include "qgraphicsscene.h"
11#include "qgraphicssceneevent.h"
12#include "qgraphicsview.h"
13#endif
14#include "qscroller.h"
15#include <QtGui/qpointingdevice.h>
16#include "private/qapplication_p.h"
17#include "private/qevent_p.h"
18#include "private/qflickgesture_p.h"
19#include "qbasictimer.h"
20#include "qdebug.h"
21
22#ifndef QT_NO_GESTURES
23
24QT_BEGIN_NAMESPACE
25
26//#define QFLICKGESTURE_DEBUG
27
28#ifdef QFLICKGESTURE_DEBUG
29# define qFGDebug qDebug
30#else
31# define qFGDebug while (false) qDebug
32#endif
33
34extern bool qt_sendSpontaneousEvent(QObject *receiver, QEvent *event);
35
36static QMouseEvent *copyMouseEvent(QEvent *e)
37{
38 switch (e->type()) {
39 case QEvent::MouseButtonPress:
40 case QEvent::MouseButtonRelease:
41 case QEvent::MouseMove: {
42 return static_cast<QMouseEvent *>(e->clone());
43 }
44#if QT_CONFIG(graphicsview)
45 case QEvent::GraphicsSceneMousePress:
46 case QEvent::GraphicsSceneMouseRelease:
47 case QEvent::GraphicsSceneMouseMove: {
48 QGraphicsSceneMouseEvent *me = static_cast<QGraphicsSceneMouseEvent *>(e);
49 QEvent::Type met = me->type() == QEvent::GraphicsSceneMousePress ? QEvent::MouseButtonPress :
50 (me->type() == QEvent::GraphicsSceneMouseRelease ? QEvent::MouseButtonRelease : QEvent::MouseMove);
51 QMouseEvent *cme = new QMouseEvent(met, QPoint(0, 0), QPoint(0, 0), me->screenPos(),
52 me->button(), me->buttons(), me->modifiers(), me->source());
53 cme->setTimestamp(me->timestamp());
54 return cme;
55 }
56#endif // QT_CONFIG(graphicsview)
57 default:
58 return nullptr;
59 }
60}
61
62class PressDelayHandler : public QObject
63{
64private:
65 PressDelayHandler(QObject *parent = nullptr)
66 : QObject(parent)
67 , sendingEvent(false)
68 , mouseButton(Qt::NoButton)
69 , mouseTarget(nullptr)
70 , mouseEventSource(Qt::MouseEventNotSynthesized)
71 { }
72
73public:
74 enum {
75 UngrabMouseBefore = 1,
76 RegrabMouseAfterwards = 2
77 };
78
79 static PressDelayHandler *instance()
80 {
81 static PressDelayHandler *inst = nullptr;
82 if (!inst)
83 inst = new PressDelayHandler(QCoreApplication::instance());
84 return inst;
85 }
86
87 bool shouldEventBeIgnored(QEvent *) const
88 {
89 return sendingEvent;
90 }
91
92 bool isDelaying() const
93 {
94 return !pressDelayEvent.isNull();
95 }
96
97 void pressed(QEvent *e, int delay)
98 {
99 if (!pressDelayEvent) {
100 pressDelayEvent.reset(other: copyMouseEvent(e));
101 using namespace std::chrono_literals;
102 pressDelayTimer.start(duration: delay * 1ms, obj: this);
103 mouseTarget = QApplication::widgetAt(p: pressDelayEvent->globalPosition().toPoint());
104 mouseButton = pressDelayEvent->button();
105 mouseEventSource = pressDelayEvent->source();
106 qFGDebug(msg: "QFG: consuming/delaying mouse press");
107 } else {
108 qFGDebug(msg: "QFG: NOT consuming/delaying mouse press");
109 }
110 e->setAccepted(true);
111 }
112
113 bool released(QEvent *e, bool scrollerWasActive, bool scrollerIsActive)
114 {
115 // consume this event if the scroller was or is active
116 bool result = scrollerWasActive || scrollerIsActive;
117
118 // stop the timer
119 if (pressDelayTimer.isActive())
120 pressDelayTimer.stop();
121
122 // we still haven't even sent the press, so do it now
123 if (pressDelayEvent && mouseTarget && !scrollerIsActive) {
124 QScopedPointer<QMouseEvent> releaseEvent(copyMouseEvent(e));
125
126 qFGDebug() << "QFG: re-sending mouse press (due to release) for " << mouseTarget;
127 sendMouseEvent(me: pressDelayEvent.data(), flags: UngrabMouseBefore);
128
129 qFGDebug() << "QFG: faking mouse release (due to release) for " << mouseTarget;
130 sendMouseEvent(me: releaseEvent.data());
131
132 result = true; // consume this event
133 } else if (mouseTarget && scrollerIsActive) {
134 // we grabbed the mouse explicitly when the scroller became active, so undo that now
135 sendMouseEvent(me: nullptr, flags: UngrabMouseBefore);
136 }
137 pressDelayEvent.reset(other: nullptr);
138 mouseTarget = nullptr;
139 return result;
140 }
141
142 void scrollerWasIntercepted()
143 {
144 qFGDebug(msg: "QFG: deleting delayed mouse press, since scroller was only intercepted");
145 if (pressDelayEvent) {
146 // we still haven't even sent the press, so just throw it away now
147 if (pressDelayTimer.isActive())
148 pressDelayTimer.stop();
149 pressDelayEvent.reset(other: nullptr);
150 }
151 mouseTarget = nullptr;
152 }
153
154 void scrollerBecameActive(Qt::KeyboardModifiers eventModifiers, Qt::MouseButtons eventButtons)
155 {
156 if (pressDelayEvent) {
157 // we still haven't even sent the press, so just throw it away now
158 qFGDebug(msg: "QFG: deleting delayed mouse press, since scroller is active now");
159 if (pressDelayTimer.isActive())
160 pressDelayTimer.stop();
161 pressDelayEvent.reset(other: nullptr);
162 mouseTarget = nullptr;
163 } else if (mouseTarget) {
164 // we did send a press, so we need to fake a release now
165 QPoint farFarAway(-QWIDGETSIZE_MAX, -QWIDGETSIZE_MAX);
166
167 qFGDebug() << "QFG: sending a fake mouse release at far-far-away to " << mouseTarget;
168 QMouseEvent re(QEvent::MouseButtonRelease, QPoint(), farFarAway, farFarAway,
169 mouseButton, eventButtons & ~mouseButton,
170 eventModifiers, mouseEventSource);
171 sendMouseEvent(me: &re, flags: RegrabMouseAfterwards);
172 // don't clear the mouseTarget just yet, since we need to explicitly ungrab the mouse on release!
173 }
174 }
175
176protected:
177 void timerEvent(QTimerEvent *e) override
178 {
179 if (e->id() == pressDelayTimer.id()) {
180 if (pressDelayEvent && mouseTarget) {
181 qFGDebug() << "QFG: timer event: re-sending mouse press to " << mouseTarget;
182 sendMouseEvent(me: pressDelayEvent.data(), flags: UngrabMouseBefore);
183 }
184 pressDelayEvent.reset(other: nullptr);
185
186 if (pressDelayTimer.isActive())
187 pressDelayTimer.stop();
188 }
189 }
190
191 void sendMouseEvent(QMouseEvent *me, int flags = 0)
192 {
193 if (mouseTarget) {
194 sendingEvent = true;
195
196#if QT_CONFIG(graphicsview)
197 QGraphicsItem *grabber = nullptr;
198 if (mouseTarget->parentWidget()) {
199 if (QGraphicsView *gv = qobject_cast<QGraphicsView *>(object: mouseTarget->parentWidget())) {
200 if (gv->scene())
201 grabber = gv->scene()->mouseGrabberItem();
202 }
203 }
204
205 if (grabber && (flags & UngrabMouseBefore)) {
206 // GraphicsView Mouse Handling Workaround #1:
207 // we need to ungrab the mouse before re-sending the press,
208 // since the scene had already set the mouse grabber to the
209 // original (and consumed) event's receiver
210 qFGDebug() << "QFG: ungrabbing" << grabber;
211 grabber->ungrabMouse();
212 }
213#else
214 Q_UNUSED(flags);
215#endif // QT_CONFIG(graphicsview)
216
217 if (me) {
218 QMouseEvent copy(me->type(), mouseTarget->mapFromGlobal(me->globalPosition()),
219 mouseTarget->topLevelWidget()->mapFromGlobal(me->globalPosition()), me->globalPosition(),
220 me->button(), me->buttons(), me->modifiers(),
221 me->source(), me->pointingDevice());
222 copy.setTimestamp(me->timestamp());
223 qt_sendSpontaneousEvent(receiver: mouseTarget, event: &copy);
224 }
225
226#if QT_CONFIG(graphicsview)
227 if (grabber && (flags & RegrabMouseAfterwards)) {
228 // GraphicsView Mouse Handling Workaround #2:
229 // we need to re-grab the mouse after sending a faked mouse
230 // release, since we still need the mouse moves for the gesture
231 // (the scene will clear the item's mouse grabber status on
232 // release).
233 qFGDebug() << "QFG: re-grabbing" << grabber;
234 grabber->grabMouse();
235 }
236#endif
237 sendingEvent = false;
238 }
239 }
240
241
242private:
243 QBasicTimer pressDelayTimer;
244 QScopedPointer<QMouseEvent> pressDelayEvent;
245 bool sendingEvent;
246 Qt::MouseButton mouseButton;
247 QPointer<QWidget> mouseTarget;
248 Qt::MouseEventSource mouseEventSource;
249};
250
251
252/*!
253 \internal
254 \class QFlickGesture
255 \since 4.8
256 \brief The QFlickGesture class describes a flicking gesture made by the user.
257 \ingroup gestures
258 The QFlickGesture is more complex than the QPanGesture that uses QScroller and QScrollerProperties
259 to decide if it is triggered.
260 This gesture is reacting on touch event as compared to the QMouseFlickGesture.
261
262 \sa {Gestures in Widgets and Graphics View}, QScroller, QScrollerProperties, QMouseFlickGesture
263*/
264
265/*!
266 \internal
267*/
268QFlickGesture::QFlickGesture(QObject *receiver, Qt::MouseButton button, QObject *parent)
269 : QGesture(*new QFlickGesturePrivate, parent)
270{
271 d_func()->q_ptr = this;
272 d_func()->receiver = receiver;
273 d_func()->receiverScroller = (receiver && QScroller::hasScroller(target: receiver)) ? QScroller::scroller(target: receiver) : nullptr;
274 d_func()->button = button;
275}
276
277QFlickGesture::~QFlickGesture()
278{ }
279
280QFlickGesturePrivate::QFlickGesturePrivate()
281 : receiverScroller(nullptr), button(Qt::NoButton), macIgnoreWheel(false)
282{ }
283
284
285//
286// QFlickGestureRecognizer
287//
288
289
290QFlickGestureRecognizer::QFlickGestureRecognizer(Qt::MouseButton button)
291{
292 this->button = button;
293}
294
295/*! \reimp
296 */
297QGesture *QFlickGestureRecognizer::create(QObject *target)
298{
299#if QT_CONFIG(graphicsview)
300 QGraphicsObject *go = qobject_cast<QGraphicsObject*>(object: target);
301 if (go && button == Qt::NoButton) {
302 go->setAcceptTouchEvents(true);
303 }
304#endif
305 return new QFlickGesture(target, button);
306}
307
308/*! \internal
309 The recognize function detects a touch event suitable to start the attached QScroller.
310 The QFlickGesture will be triggered as soon as the scroller is no longer in the state
311 QScroller::Inactive or QScroller::Pressed. It will be finished or canceled
312 at the next QEvent::TouchEnd.
313 Note that the QScroller might continue scrolling (kinetically) at this point.
314 */
315QGestureRecognizer::Result QFlickGestureRecognizer::recognize(QGesture *state,
316 QObject *watched,
317 QEvent *event)
318{
319 Q_UNUSED(watched);
320
321 Q_CONSTINIT static QElapsedTimer monotonicTimer;
322 if (!monotonicTimer.isValid())
323 monotonicTimer.start();
324
325 QFlickGesture *q = static_cast<QFlickGesture *>(state);
326 QFlickGesturePrivate *d = q->d_func();
327
328 QScroller *scroller = d->receiverScroller;
329 if (!scroller)
330 return Ignore; // nothing to do without a scroller?
331
332 QWidget *receiverWidget = qobject_cast<QWidget *>(o: d->receiver);
333#if QT_CONFIG(graphicsview)
334 QGraphicsObject *receiverGraphicsObject = qobject_cast<QGraphicsObject *>(object: d->receiver);
335#endif
336
337 // this is only set for events that we inject into the event loop via sendEvent()
338 if (PressDelayHandler::instance()->shouldEventBeIgnored(event)) {
339 //qFGDebug() << state << "QFG: ignored event: " << event->type();
340 return Ignore;
341 }
342
343 const QMouseEvent *me = nullptr;
344#if QT_CONFIG(graphicsview)
345 const QGraphicsSceneMouseEvent *gsme = nullptr;
346#endif
347 const QTouchEvent *te = nullptr;
348 QPoint globalPos;
349
350 // qFGDebug() << "FlickGesture "<<state<<"watched:"<<watched<<"receiver"<<d->receiver<<"event"<<event->type()<<"button"<<button;
351
352 Qt::KeyboardModifiers keyboardModifiers = Qt::NoModifier;
353 Qt::MouseButtons mouseButtons = Qt::NoButton;
354 switch (event->type()) {
355 case QEvent::MouseButtonPress:
356 case QEvent::MouseButtonRelease:
357 case QEvent::MouseMove:
358 if (!receiverWidget)
359 return Ignore;
360 if (button != Qt::NoButton) {
361 me = static_cast<const QMouseEvent *>(event);
362 keyboardModifiers = me->modifiers();
363 mouseButtons = me->buttons();
364 globalPos = me->globalPosition().toPoint();
365 }
366 break;
367#if QT_CONFIG(graphicsview)
368 case QEvent::GraphicsSceneMousePress:
369 case QEvent::GraphicsSceneMouseRelease:
370 case QEvent::GraphicsSceneMouseMove:
371 if (!receiverGraphicsObject)
372 return Ignore;
373 if (button != Qt::NoButton) {
374 gsme = static_cast<const QGraphicsSceneMouseEvent *>(event);
375 keyboardModifiers = gsme->modifiers();
376 mouseButtons = gsme->buttons();
377 globalPos = gsme->screenPos();
378 }
379 break;
380#endif
381 case QEvent::TouchBegin:
382 case QEvent::TouchEnd:
383 case QEvent::TouchUpdate:
384 if (button == Qt::NoButton) {
385 te = static_cast<const QTouchEvent *>(event);
386 keyboardModifiers = te->modifiers();
387 if (!te->points().isEmpty())
388 globalPos = te->points().at(i: 0).globalPosition().toPoint();
389 }
390 break;
391
392 // consume all wheel events if the scroller is active
393 case QEvent::Wheel:
394 if (d->macIgnoreWheel || (scroller->state() != QScroller::Inactive))
395 return Ignore | ConsumeEventHint;
396 break;
397
398 // consume all dbl click events if the scroller is active
399 case QEvent::MouseButtonDblClick:
400 if (scroller->state() != QScroller::Inactive)
401 return Ignore | ConsumeEventHint;
402 break;
403
404 default:
405 break;
406 }
407
408 if (!me
409#if QT_CONFIG(graphicsview)
410 && !gsme
411#endif
412 && !te) // Neither mouse nor touch
413 return Ignore;
414
415 // get the current pointer position in local coordinates.
416 QPointF point;
417 QScroller::Input inputType = (QScroller::Input) 0;
418
419 switch (event->type()) {
420 case QEvent::MouseButtonPress:
421 if (me && me->button() == button && me->buttons() == button) {
422 point = me->globalPosition().toPoint();
423 inputType = QScroller::InputPress;
424 } else if (me) {
425 scroller->stop();
426 return CancelGesture;
427 }
428 break;
429 case QEvent::MouseButtonRelease:
430 if (me && me->button() == button) {
431 point = me->globalPosition().toPoint();
432 inputType = QScroller::InputRelease;
433 }
434 break;
435 case QEvent::MouseMove:
436 if (me && me->buttons() == button) {
437 point = me->globalPosition().toPoint();
438 inputType = QScroller::InputMove;
439 }
440 break;
441
442#if QT_CONFIG(graphicsview)
443 case QEvent::GraphicsSceneMousePress:
444 if (gsme && gsme->button() == button && gsme->buttons() == button) {
445 point = gsme->scenePos();
446 inputType = QScroller::InputPress;
447 } else if (gsme) {
448 scroller->stop();
449 return CancelGesture;
450 }
451 break;
452 case QEvent::GraphicsSceneMouseRelease:
453 if (gsme && gsme->button() == button) {
454 point = gsme->scenePos();
455 inputType = QScroller::InputRelease;
456 }
457 break;
458 case QEvent::GraphicsSceneMouseMove:
459 if (gsme && gsme->buttons() == button) {
460 point = gsme->scenePos();
461 inputType = QScroller::InputMove;
462 }
463 break;
464#endif
465
466 case QEvent::TouchBegin:
467 inputType = QScroller::InputPress;
468 Q_FALLTHROUGH();
469 case QEvent::TouchEnd:
470 if (!inputType)
471 inputType = QScroller::InputRelease;
472 Q_FALLTHROUGH();
473 case QEvent::TouchUpdate:
474 if (!inputType)
475 inputType = QScroller::InputMove;
476
477 if (te->pointingDevice()->type() == QInputDevice::DeviceType::TouchPad) {
478 if (te->points().size() != 2) // 2 fingers on pad
479 return Ignore;
480
481 point = te->points().at(i: 0).scenePressPosition() +
482 ((te->points().at(i: 0).scenePosition() - te->points().at(i: 0).scenePressPosition()) +
483 (te->points().at(i: 1).scenePosition() - te->points().at(i: 1).scenePressPosition())) / 2;
484 } else { // TouchScreen
485 if (te->points().size() != 1) // 1 finger on screen
486 return Ignore;
487
488 point = te->points().at(i: 0).scenePosition();
489 }
490 break;
491
492 default:
493 break;
494 }
495
496 // Check for an active scroller at globalPos
497 if (inputType == QScroller::InputPress) {
498 const auto activeScrollers = QScroller::activeScrollers();
499 for (QScroller *as : activeScrollers) {
500 if (as != scroller) {
501 QRegion scrollerRegion;
502
503 if (QWidget *w = qobject_cast<QWidget *>(o: as->target())) {
504 scrollerRegion = QRect(w->mapToGlobal(QPoint(0, 0)), w->size());
505#if QT_CONFIG(graphicsview)
506 } else if (QGraphicsObject *go = qobject_cast<QGraphicsObject *>(object: as->target())) {
507 if (const auto *scene = go->scene()) {
508 const auto goBoundingRectMappedToScene = go->mapToScene(rect: go->boundingRect());
509 const auto views = scene->views();
510 for (QGraphicsView *gv : views) {
511 scrollerRegion |= gv->mapFromScene(polygon: goBoundingRectMappedToScene)
512 .translated(offset: gv->mapToGlobal(QPoint(0, 0)));
513 }
514 }
515#endif
516 }
517 // active scrollers always have priority
518 if (scrollerRegion.contains(p: globalPos))
519 return Ignore;
520 }
521 }
522 }
523
524 bool scrollerWasDragging = (scroller->state() == QScroller::Dragging);
525 bool scrollerWasScrolling = (scroller->state() == QScroller::Scrolling);
526
527 if (inputType) {
528 if (QWidget *w = qobject_cast<QWidget *>(o: d->receiver))
529 point = w->mapFromGlobal(point.toPoint());
530#if QT_CONFIG(graphicsview)
531 else if (QGraphicsObject *go = qobject_cast<QGraphicsObject *>(object: d->receiver))
532 point = go->mapFromScene(point);
533#endif
534
535 // inform the scroller about the new event
536 scroller->handleInput(input: inputType, position: point, timestamp: monotonicTimer.elapsed());
537 }
538
539 // depending on the scroller state return the gesture state
540 Result result;
541 bool scrollerIsActive = (scroller->state() == QScroller::Dragging ||
542 scroller->state() == QScroller::Scrolling);
543
544 // Consume all mouse events while dragging or scrolling to avoid nasty
545 // side effects with Qt's standard widgets.
546 if ((me
547#if QT_CONFIG(graphicsview)
548 || gsme
549#endif
550 ) && scrollerIsActive)
551 result |= ConsumeEventHint;
552
553 // The only problem with this approach is that we consume the
554 // MouseRelease when we start the scrolling with a flick gesture, so we
555 // have to fake a MouseRelease "somewhere" to not mess with the internal
556 // states of Qt's widgets (a QPushButton would stay in 'pressed' state
557 // forever, if it doesn't receive a MouseRelease).
558 if (me
559#if QT_CONFIG(graphicsview)
560 || gsme
561#endif
562 ) {
563 if (!scrollerWasDragging && !scrollerWasScrolling && scrollerIsActive)
564 PressDelayHandler::instance()->scrollerBecameActive(eventModifiers: keyboardModifiers, eventButtons: mouseButtons);
565 else if (scrollerWasScrolling && (scroller->state() == QScroller::Dragging || scroller->state() == QScroller::Inactive))
566 PressDelayHandler::instance()->scrollerWasIntercepted();
567 }
568
569 if (!inputType) {
570 result |= Ignore;
571 } else {
572 switch (event->type()) {
573 case QEvent::MouseButtonPress:
574#if QT_CONFIG(graphicsview)
575 case QEvent::GraphicsSceneMousePress:
576#endif
577 if (scroller->state() == QScroller::Pressed) {
578 int pressDelay = int(1000 * scroller->scrollerProperties().scrollMetric(metric: QScrollerProperties::MousePressEventDelay).toReal());
579 if (pressDelay > 0) {
580 result |= ConsumeEventHint;
581
582 PressDelayHandler::instance()->pressed(e: event, delay: pressDelay);
583 event->accept();
584 }
585 }
586 Q_FALLTHROUGH();
587 case QEvent::TouchBegin:
588 q->setHotSpot(globalPos);
589 result |= scrollerIsActive ? TriggerGesture : MayBeGesture;
590 break;
591
592 case QEvent::MouseMove:
593#if QT_CONFIG(graphicsview)
594 case QEvent::GraphicsSceneMouseMove:
595#endif
596 if (PressDelayHandler::instance()->isDelaying())
597 result |= ConsumeEventHint;
598 Q_FALLTHROUGH();
599 case QEvent::TouchUpdate:
600 result |= scrollerIsActive ? TriggerGesture : Ignore;
601 break;
602
603#if QT_CONFIG(graphicsview)
604 case QEvent::GraphicsSceneMouseRelease:
605#endif
606 case QEvent::MouseButtonRelease:
607 if (PressDelayHandler::instance()->released(e: event, scrollerWasActive: scrollerWasDragging || scrollerWasScrolling, scrollerIsActive))
608 result |= ConsumeEventHint;
609 Q_FALLTHROUGH();
610 case QEvent::TouchEnd:
611 result |= scrollerIsActive ? FinishGesture : CancelGesture;
612 break;
613
614 default:
615 result |= Ignore;
616 break;
617 }
618 }
619 return result;
620}
621
622
623/*! \reimp
624 */
625void QFlickGestureRecognizer::reset(QGesture *state)
626{
627 QGestureRecognizer::reset(state);
628}
629
630QT_END_NAMESPACE
631
632#include "moc_qflickgesture_p.cpp"
633
634#endif // QT_NO_GESTURES
635

source code of qtbase/src/widgets/util/qflickgesture.cpp