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