From 067cffbd7c06c5a6847067972c1e94f3995fef77 Mon Sep 17 00:00:00 2001 From: Uwe Rathmann Date: Thu, 5 Oct 2023 08:59:30 +0200 Subject: [PATCH] QskGestureRecognizer using event filtering --- doc/classes/QskControl.dox | 13 - examples/iotdashboard/MainItem.cpp | 41 +- examples/iotdashboard/MainItem.h | 8 +- examples/thumbnails/main.cpp | 39 +- src/controls/QskControl.cpp | 89 ++-- src/controls/QskControl.h | 2 - src/controls/QskGesture.cpp | 6 - src/controls/QskGesture.h | 6 +- src/controls/QskGestureRecognizer.cpp | 608 +++++++++++------------ src/controls/QskGestureRecognizer.h | 53 +- src/controls/QskPanGestureRecognizer.cpp | 53 +- src/controls/QskPanGestureRecognizer.h | 8 +- src/controls/QskScrollBox.cpp | 153 ++---- src/controls/QskScrollBox.h | 3 - src/controls/QskSwipeView.cpp | 17 - src/controls/QskSwipeView.h | 1 - 16 files changed, 515 insertions(+), 585 deletions(-) diff --git a/doc/classes/QskControl.dox b/doc/classes/QskControl.dox index 04c57754..f8a39834 100644 --- a/doc/classes/QskControl.dox +++ b/doc/classes/QskControl.dox @@ -345,15 +345,6 @@ \return Area, where to lay out the child items */ -/*! - \fn QskControl::gestureRect - - Returns the area where to accept gestures. - The default implementation returns QskQuickItem::rect(). - - \sa gestureFilter(), gestureEvent() -*/ - /*! \fn QskControl::focusIndicatorRect @@ -896,10 +887,6 @@ */ -/*! - \fn QskControl::gestureFilter -*/ - /*! \fn void QskControl::gestureEvent diff --git a/examples/iotdashboard/MainItem.cpp b/examples/iotdashboard/MainItem.cpp index d0c5ed29..d8b7c46f 100644 --- a/examples/iotdashboard/MainItem.cpp +++ b/examples/iotdashboard/MainItem.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -20,6 +21,23 @@ #include +namespace +{ + class PanRecognizer final : public QskPanGestureRecognizer + { + public: + PanRecognizer( MainItem* mainItem ) + : QskPanGestureRecognizer( mainItem ) + { + setOrientations( Qt::Horizontal | Qt::Vertical ); + setMinDistance( 50 ); + setTimeout( 100 ); + + setWatchedItem( mainItem ); + } + }; +} + QPair< Cube::Position, Cube::Edge > Cube::s_neighbors[ Cube::NumPositions ][ Cube::NumEdges ] = { // neighbors of Left side: @@ -245,9 +263,7 @@ MainItem::MainItem( QQuickItem* parent ) setAcceptedMouseButtons( Qt::LeftButton ); setFiltersChildMouseEvents( true ); - m_panRecognizer.setOrientations( Qt::Horizontal | Qt::Vertical ); - m_panRecognizer.setMinDistance( 50 ); - m_panRecognizer.setWatchedItem( this ); + (void) new PanRecognizer( this ); m_mainLayout->setSpacing( 0 ); @@ -331,24 +347,5 @@ void MainItem::keyPressEvent( QKeyEvent* event ) m_cube->switchPosition( direction ); } -bool MainItem::gestureFilter( const QQuickItem* item, const QEvent* event ) -{ - auto& recognizer = m_panRecognizer; - - if( event->type() == QEvent::MouseButtonPress ) - { - auto mouseEvent = static_cast< const QMouseEvent* >( event ); - - if( ( item != this ) || ( recognizer.timeout() < 0 ) ) - { - if( recognizer.hasProcessedBefore( mouseEvent ) ) - return false; - } - - recognizer.setTimeout( ( item == this ) ? -1 : 100 ); - } - - return recognizer.processEvent( item, event, false ); -} #include "moc_MainItem.cpp" diff --git a/examples/iotdashboard/MainItem.h b/examples/iotdashboard/MainItem.h index daf2b2e5..feb6ef5d 100644 --- a/examples/iotdashboard/MainItem.h +++ b/examples/iotdashboard/MainItem.h @@ -1,11 +1,8 @@ #pragma once #include -#include #include -#include - class MenuBar; class QskBox; class QskLinearBox; @@ -66,18 +63,17 @@ class MainItem : public QskControl { Q_OBJECT + using Inherited = QskControl; + public: MainItem( QQuickItem* parent = nullptr ); protected: void keyPressEvent( QKeyEvent* ) override final; - - bool gestureFilter( const QQuickItem*, const QEvent* ) override final; void gestureEvent( QskGestureEvent* ) override final; private: QskLinearBox* m_mainLayout; MenuBar* m_menuBar; Cube* m_cube; - QskPanGestureRecognizer m_panRecognizer; }; diff --git a/examples/thumbnails/main.cpp b/examples/thumbnails/main.cpp index ff1f092c..4c577de0 100644 --- a/examples/thumbnails/main.cpp +++ b/examples/thumbnails/main.cpp @@ -16,8 +16,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -83,8 +85,9 @@ class Thumbnail : public QskPushButton void mousePressEvent( QMouseEvent* event ) override { /* - rgnore events: to check if the pae gesture recoognizer of the scroll - area becomes active without timeout ( see QskScrollBox::mousePressEvent ) + ignore events: to check if the pan gesture recoognizer of the scroll + area works, when the event arrives as regular event + ( not via childMouseEventFilter ) */ event->setAccepted( false ); } @@ -198,6 +201,8 @@ class ScrollArea : public QskScrollArea ScrollArea( QQuickItem* parentItem = nullptr ) : QskScrollArea( parentItem ) { + setMargins( QMarginsF( 25, 25, 5, 5 ) ); + // settings usually done in the skins setBoxBorderMetricsHint( Viewport, 2 ); setBoxBorderColorsHint( Viewport, Qt::gray ); // works with most color schemes @@ -282,18 +287,42 @@ int main( int argc, char* argv[] ) iconGrid->setSizePolicy( QskSizePolicy::MinimumExpanding, QskSizePolicy::MinimumExpanding ); - auto scrollArea = new ScrollArea( box ); - scrollArea->setMargins( QMarginsF( 25, 25, 5, 5 ) ); + auto scrollArea = new ScrollArea(); scrollArea->setScrolledItem( iconGrid ); +#if 0 + // for testing nested gestures + auto swipeView = new QskSwipeView(); + + swipeView->addItem( scrollArea ); + + for ( int i = 0; i < 1; i++ ) + { + using namespace QskRgb; + + const QRgb colors[] = { FireBrick, DodgerBlue, OliveDrab, Gold, Wheat }; + + auto page = new QskControl(); + + const auto index = i % ( sizeof( colors ) / sizeof( colors[0] ) ); + page->setBackgroundColor( colors[ index ] ); + + swipeView->addItem( page ); + } + + box->addItem( swipeView ); +#else + box->addItem( scrollArea ); +#endif + auto focusIndicator = new QskFocusIndicator(); focusIndicator->setBoxBorderColorsHint( QskFocusIndicator::Panel, Qt::darkRed ); QskWindow window; - window.resize( 600, 600 ); window.addItem( box ); window.addItem( focusIndicator ); + window.resize( 600, 600 ); window.show(); return app.exec(); diff --git a/src/controls/QskControl.cpp b/src/controls/QskControl.cpp index 7aa7b64c..a293882b 100644 --- a/src/controls/QskControl.cpp +++ b/src/controls/QskControl.cpp @@ -805,38 +805,45 @@ bool QskControl::event( QEvent* event ) break; } - case QskEvent::GestureFilter: - { - /* - qskMaybeGesture is sending an event, so that it can be manipulated - by event filters. F.e QskDrawer wants to add a gesture to - some other control to initiate its appearance. - */ - - auto ev = static_cast< QskGestureFilterEvent* >( event ); - - if ( ev->event()->type() == QEvent::MouseButtonPress ) - { - auto mouseEvent = static_cast< const QMouseEvent* >( ev->event() ); - const auto pos = - mapFromItem( ev->item(), qskMousePosition( mouseEvent ) ); - - if ( !gestureRect().contains( pos ) ) - { - ev->setMaybeGesture( false ); - break; - } - } - - ev->setMaybeGesture( gestureFilter( ev->item(), ev->event() ) ); - - break; - } case QskEvent::Gesture: { gestureEvent( static_cast< QskGestureEvent* >( event ) ); return true; } + case QEvent::MouseButtonPress: + { + /* + We need to do a gesture detection with abort criterions + first. When it fails the input events will be processed + in ascending order along the item tree until an item accepts + the event. + + This detection can be done in childMouseEventFilter() for all + children - but not for the filtering control ( = this ) itself. + So we need to do this here. + */ + if ( qskMaybeGesture( this, this, event ) ) + return true; + + const bool ok = Inherited::event( event ); + + if ( !event->isAccepted() ) + { + /* + When the initial gesture detection failed and the event + has not been handled here we do a second gesture detection + without passing a child. It can be used for detections + without abort criterions. + + An example is the pan gesture detection, that can be started without + any timeouts now. + */ + if ( qskMaybeGesture( this, nullptr, event ) ) + return true; + } + + return ok; + } } if ( qskMaybeGesture( this, this, event ) ) @@ -847,12 +854,25 @@ bool QskControl::event( QEvent* event ) bool QskControl::childMouseEventFilter( QQuickItem* child, QEvent* event ) { - return qskMaybeGesture( this, child, event ); -} + /* + The strategy implemented in many classes of the Qt development is + to analyze the events without blocking the handling of the child. + Once a gesture is detected the gesture handling trys to steal the + mouse grab hoping for the child to abort its operation. -bool QskControl::gestureFilter( const QQuickItem*, const QEvent* ) -{ - return false; + This approach has obvious problems: + + - operations already started on press can't be aborted anymore + - the child needs to agree on losing the grab ( setKeepMouseGrab( false ) ) + - ... + + We implement a different strategy: processing of the events + by the children is blocked until the gesture detection has accepted + or rejected. In case of a rejection the events will be replayed. + ( see QskGestureRecognizer ) + */ + + return qskMaybeGesture( this, child, event ); } void QskControl::gestureEvent( QskGestureEvent* ) @@ -978,11 +998,6 @@ QRectF QskControl::layoutRectForSize( const QSizeF& size ) const return qskValidOrEmptyInnerRect( r, margins() ); } -QRectF QskControl::gestureRect() const -{ - return rect(); -} - QRectF QskControl::focusIndicatorRect() const { return contentsRect(); diff --git a/src/controls/QskControl.h b/src/controls/QskControl.h index dd8ff9ed..ac04e197 100644 --- a/src/controls/QskControl.h +++ b/src/controls/QskControl.h @@ -80,7 +80,6 @@ class QSK_EXPORT QskControl : public QskQuickItem, public QskSkinnable QRectF layoutRect() const; virtual QRectF layoutRectForSize( const QSizeF& ) const; - virtual QRectF gestureRect() const; virtual QRectF focusIndicatorRect() const; virtual QRectF focusIndicatorClipRect() const; @@ -196,7 +195,6 @@ class QSK_EXPORT QskControl : public QskQuickItem, public QskSkinnable void hoverLeaveEvent( QHoverEvent* ) override; bool childMouseEventFilter( QQuickItem*, QEvent* ) override; - virtual bool gestureFilter( const QQuickItem*, const QEvent* ); void itemChange( ItemChange, const ItemChangeData& ) override; void geometryChange( const QRectF&, const QRectF& ) override; diff --git a/src/controls/QskGesture.cpp b/src/controls/QskGesture.cpp index f32c40fd..ef4b177a 100644 --- a/src/controls/QskGesture.cpp +++ b/src/controls/QskGesture.cpp @@ -92,7 +92,6 @@ void QskPanGesture::setPosition( const QPointF& pos ) QskSwipeGesture::QskSwipeGesture() : QskGesture( Swipe ) - , m_velocity( 0.0 ) , m_angle( 0.0 ) { } @@ -101,11 +100,6 @@ QskSwipeGesture::~QskSwipeGesture() { } -void QskSwipeGesture::setVelocity( qreal velocity ) -{ - m_velocity = velocity; -} - void QskSwipeGesture::setAngle( qreal angle ) { m_angle = angle; diff --git a/src/controls/QskGesture.h b/src/controls/QskGesture.h index bde70172..7432c6b4 100644 --- a/src/controls/QskGesture.h +++ b/src/controls/QskGesture.h @@ -136,14 +136,10 @@ class QSK_EXPORT QskSwipeGesture : public QskGesture QskSwipeGesture(); ~QskSwipeGesture() override; - void setVelocity( qreal velocity ); - inline qreal velocity() const { return m_velocity; } - void setAngle( qreal angle ); - inline qreal angle() const; + inline qreal angle() const { return m_angle; }; private: - qreal m_velocity; qreal m_angle; }; diff --git a/src/controls/QskGestureRecognizer.cpp b/src/controls/QskGestureRecognizer.cpp index d5262fa7..07a44f2d 100644 --- a/src/controls/QskGestureRecognizer.cpp +++ b/src/controls/QskGestureRecognizer.cpp @@ -1,241 +1,119 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + #include "QskGestureRecognizer.h" #include "QskEvent.h" #include "QskQuick.h" -#include #include -#include #include #include -#include #include -QSK_QT_PRIVATE_BEGIN - -#include - -#if QT_VERSION >= QT_VERSION_CHECK( 6, 3, 0 ) -#include -#endif - -QSK_QT_PRIVATE_END - -static QMouseEvent* qskClonedMouseEventAt( - const QMouseEvent* event, QPointF* localPos ) -{ #if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 ) - - auto clonedEvent = QQuickWindowPrivate::cloneMouseEvent( - const_cast< QMouseEvent* >( event ), localPos ); - -#else - auto clonedEvent = event->clone(); - - if ( localPos ) - { -#if QT_VERSION < QT_VERSION_CHECK( 6, 3, 0 ) - auto& point = QMutableEventPoint::from( clonedEvent->point( 0 ) ); - - point.detach(); - point.setPosition( *localPos ); -#else - auto& point = clonedEvent->point( 0 ); - - QMutableEventPoint::detach( point ); - QMutableEventPoint::setPosition( point, *localPos ); -#endif - } + QSK_QT_PRIVATE_BEGIN + #include + QSK_QT_PRIVATE_END #endif - Q_ASSERT( event->timestamp() == clonedEvent->timestamp() ); - - return clonedEvent; -} - -static inline QMouseEvent* qskClonedMouseEvent( - const QMouseEvent* mouseEvent, const QQuickItem* item = nullptr ) +static QMouseEvent* qskClonedMouseEvent( const QMouseEvent* event ) { QMouseEvent* clonedEvent; - auto event = const_cast< QMouseEvent* >( mouseEvent ); - - if ( item ) - { - auto localPos = item->mapFromScene( qskMouseScenePosition( event ) ); - clonedEvent = qskClonedMouseEventAt( event, &localPos ); - } - else - { - clonedEvent = qskClonedMouseEventAt( event, nullptr ); - } +#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 ) + clonedEvent = QQuickWindowPrivate::cloneMouseEvent( + const_cast< QMouseEvent* >( event ), nullptr ); +#else + clonedEvent = event->clone(); +#endif clonedEvent->setAccepted( false ); + return clonedEvent; } -namespace -{ - /* - As we don't want QskGestureRecognizer being a QObject - we need some extra timers - usually one per screen. - */ - - class Timer final : public QObject - { - public: - void start( int ms, QskGestureRecognizer* recognizer ) - { - if ( m_timer.isActive() ) - qWarning() << "QskGestureRecognizer: resetting an active timer"; - - m_recognizer = recognizer; - m_timer.start( ms, this ); - } - - void stop() - { - m_timer.stop(); - m_recognizer = nullptr; - } - - const QskGestureRecognizer* recognizer() const - { - return m_recognizer; - } - - protected: - void timerEvent( QTimerEvent* ) override - { - m_timer.stop(); - - if ( m_recognizer ) - { - auto recognizer = m_recognizer; - m_recognizer = nullptr; - - recognizer->reject(); - } - } - - QBasicTimer m_timer; - QskGestureRecognizer* m_recognizer = nullptr; - }; - - class TimerTable - { - public: - ~TimerTable() - { - qDeleteAll( m_table ); - } - - void startTimer( int ms, QskGestureRecognizer* recognizer ) - { - Timer* timer = nullptr; - - for ( auto t : std::as_const( m_table ) ) - { - if ( t->recognizer() == nullptr || - t->recognizer() == recognizer ) - { - timer = t; - break; - } - } - - if ( timer == nullptr ) - { - timer = new Timer(); - m_table += timer; - } - - timer->start( ms, recognizer ); - } - - void stopTimer( const QskGestureRecognizer* recognizer ) - { - for ( auto timer : std::as_const( m_table ) ) - { - if ( timer->recognizer() == recognizer ) - { - // we keep the timer to be used later again - timer->stop(); - return; - } - } - } - - private: - /* - Usually we have not more than one entry. - Only when having more than one screen we - might have mouse events to be processed - simultaneously. - */ - QVector< Timer* > m_table; - }; - - class PendingEvents : public QVector< QMouseEvent* > - { - public: - ~PendingEvents() - { - qDeleteAll( *this ); - } - - void reset() - { - qDeleteAll( *this ); - clear(); - } - }; -} - -Q_GLOBAL_STATIC( TimerTable, qskTimerTable ) - class QskGestureRecognizer::PrivateData { public: - PrivateData() - : watchedItem( nullptr ) - , timestamp( 0 ) - , timestampProcessed( 0 ) - , timeout( -1 ) - , buttons( Qt::NoButton ) - , state( QskGestureRecognizer::Idle ) - , isReplayingEvents( false ) + void startTimer( QskGestureRecognizer* recognizer ) { + if ( timeoutId >= 0 ) + { + // warning + } + + if ( timeout <= 0 ) + { + // warning + } + + timeoutId = recognizer->startTimer( timeout ); } - QQuickItem* watchedItem; + void stopTimer( QskGestureRecognizer* recognizer ) + { + if ( timeoutId >= 0 ) + { + recognizer->killTimer( timeoutId ); + timeoutId = -1; + } + } - PendingEvents pendingEvents; - ulong timestamp; - ulong timestampProcessed; + inline Qt::MouseButtons effectiveMouseButtons() const + { + if ( buttons != Qt::NoButton ) + return buttons; - int timeout; // ms + return watchedItem->acceptedMouseButtons(); + } - Qt::MouseButtons buttons; + QQuickItem* watchedItem = nullptr; - int state : 4; - bool isReplayingEvents : 1; // not exception safe !!! + QVector< QMouseEvent* > pendingEvents; + + quint64 timestampStarted = 0; + quint64 timestampProcessed = 0; + + int timeoutId = -1; + int timeout = -1; // ms + + Qt::MouseButtons buttons = Qt::NoButton; + + unsigned char state = QskGestureRecognizer::Idle; + bool rejectOnTimeout = true; + bool expired = false; }; -QskGestureRecognizer::QskGestureRecognizer() - : m_data( new PrivateData() ) +QskGestureRecognizer::QskGestureRecognizer( QObject* parent ) + : QObject( parent ) + , m_data( new PrivateData() ) { } QskGestureRecognizer::~QskGestureRecognizer() { - qskTimerTable->stopTimer( this ); + qDeleteAll( m_data->pendingEvents ); } void QskGestureRecognizer::setWatchedItem( QQuickItem* item ) { if ( m_data->watchedItem ) + { + m_data->watchedItem->removeEventFilter( this ); reset(); + } m_data->watchedItem = item; + + if ( m_data->watchedItem ) + m_data->watchedItem->installEventFilter( this ); + + /* + // doing those here: ??? + m_data->watchedItem->setFiltersChildMouseEvents(); + m_data->watchedItem->setAcceptedMouseButtons(); + */ } QQuickItem* QskGestureRecognizer::watchedItem() const @@ -253,6 +131,24 @@ Qt::MouseButtons QskGestureRecognizer::acceptedMouseButtons() const return m_data->buttons; } +QRectF QskGestureRecognizer::gestureRect() const +{ + if ( m_data->watchedItem ) + return qskItemRect( m_data->watchedItem ); + + return QRectF( 0.0, 0.0, -1.0, -1.0 ); +} + +void QskGestureRecognizer::setRejectOnTimeout( bool on ) +{ + m_data->rejectOnTimeout = on; +} + +bool QskGestureRecognizer::rejectOnTimeout() const +{ + return m_data->rejectOnTimeout; +} + void QskGestureRecognizer::setTimeout( int ms ) { m_data->timeout = ms; @@ -263,29 +159,37 @@ int QskGestureRecognizer::timeout() const return m_data->timeout; } -ulong QskGestureRecognizer::timestamp() const +void QskGestureRecognizer::timerEvent( QTimerEvent* event ) { - return m_data->timestamp; + if ( event->timerId() == m_data->timeoutId ) + { + m_data->stopTimer( this ); + + if ( m_data->rejectOnTimeout ) + { + reject(); + m_data->expired = true; + } + + return; + } + + Inherited::timerEvent( event ); } -bool QskGestureRecognizer::hasProcessedBefore( const QMouseEvent* event ) const +quint64 QskGestureRecognizer::timestampStarted() const { - return event && ( event->timestamp() <= m_data->timestampProcessed ); -} - -bool QskGestureRecognizer::isReplaying() const -{ - return m_data->isReplayingEvents; + return m_data->timestampStarted; } void QskGestureRecognizer::setState( State state ) { if ( state != m_data->state ) { - const State oldState = static_cast< QskGestureRecognizer::State >( m_data->state ); + const auto oldState = static_cast< State >( m_data->state ); m_data->state = state; - stateChanged( oldState, state ); + Q_EMIT stateChanged( oldState, state ); } } @@ -294,31 +198,113 @@ QskGestureRecognizer::State QskGestureRecognizer::state() const return static_cast< QskGestureRecognizer::State >( m_data->state ); } -bool QskGestureRecognizer::processEvent( - const QQuickItem* item, const QEvent* event, bool blockReplayedEvents ) +bool QskGestureRecognizer::eventFilter( QObject* object, QEvent* event) { - if ( m_data->isReplayingEvents && blockReplayedEvents ) - { - /* - This one is a replayed event after we had decided - that this interaction is to be ignored - */ - return false; - } - auto& watchedItem = m_data->watchedItem; - if ( watchedItem == nullptr || !watchedItem->isEnabled() || - !watchedItem->isVisible() || watchedItem->window() == nullptr ) + if ( ( object == watchedItem ) && + ( static_cast< int >( event->type() ) == QskEvent::GestureFilter ) ) { - reset(); - return false; + if ( !watchedItem->isEnabled() || !watchedItem->isVisible() + || watchedItem->window() == nullptr ) + { + reset(); + return false; + } + + auto ev = static_cast< QskGestureFilterEvent* >( event ); + ev->setMaybeGesture( maybeGesture( ev->item(), ev->event() ) ); + + return ev->maybeGesture(); } - QScopedPointer< QMouseEvent > clonedPress; + return false; +} + +bool QskGestureRecognizer::maybeGesture( + const QQuickItem* item, const QEvent* event ) +{ + switch( static_cast< int >( event->type() ) ) + { + case QEvent::UngrabMouse: + { + if ( m_data->state != Idle ) + { + // someone took our grab away, we have to give up + reset(); + return true; + } + + return false; + } + case QEvent::MouseButtonPress: + { + if ( state() != Idle ) + { + qWarning() << "QskGestureRecognizer: pressed, while not being idle"; + return false; + } + + const auto mouseEvent = static_cast< const QMouseEvent* >( event ); + + if ( mouseEvent->timestamp() > m_data->timestampProcessed ) + { + m_data->timestampProcessed = mouseEvent->timestamp(); + } + else + { + /* + A mouse event might appear several times: + + - we ran into a timeout and reposted the events + + - we rejected because the event sequence does + not match the acceptance criterions and reposted + the events + + - another gesture recognizer for a child item + has reposted the events because of the reasons above + + For most situations we can simply skip processing already + processed events with the exception of a timeout. Here we might + have to retry without timeout, when none of the items was + accepting the events. This specific situation is indicated by + item set to null. + */ + + if ( item || !m_data->expired ) + return false; + } + + return processMouseEvent( item, mouseEvent ); + } + case QEvent::MouseMove: + case QEvent::MouseButtonRelease: + { + if ( state() <= Idle ) + return false; + + const auto mouseEvent = static_cast< const QMouseEvent* >( event ); + return processMouseEvent( item, mouseEvent ); + } + } + + return false; +} + +bool QskGestureRecognizer::processMouseEvent( + const QQuickItem* item, const QMouseEvent* event ) +{ + auto& watchedItem = m_data->watchedItem; + + const auto pos = watchedItem->mapFromScene( qskMouseScenePosition( event ) ); + const auto timestamp = event->timestamp(); if ( event->type() == QEvent::MouseButtonPress ) { + if ( !gestureRect().contains( pos ) ) + return false; + if ( m_data->state != Idle ) { // should not happen, when using the recognizer correctly @@ -327,12 +313,7 @@ bool QskGestureRecognizer::processEvent( return false; } - Qt::MouseButtons buttons = m_data->buttons; - if ( buttons == Qt::NoButton ) - buttons = watchedItem->acceptedMouseButtons(); - - auto mouseEvent = static_cast< const QMouseEvent* >( event ); - if ( !( buttons & mouseEvent->button() ) ) + if ( !( m_data->effectiveMouseButtons() & event->button() ) ) return false; /* @@ -342,22 +323,7 @@ bool QskGestureRecognizer::processEvent( if ( !qskGrabMouse( watchedItem ) ) return false; - m_data->timestamp = mouseEvent->timestamp(); - - if ( item != watchedItem ) - { - /* - The first press happens before having the mouse grab and might - have been for a child of watchedItem. Then we create a clone - of the event with positions translated into the coordinate system - of watchedItem. - */ - - clonedPress.reset( qskClonedMouseEvent( mouseEvent, watchedItem ) ); - - item = watchedItem; - event = clonedPress.data(); - } + m_data->timestampStarted = timestamp; if ( m_data->timeout != 0 ) { @@ -366,134 +332,118 @@ bool QskGestureRecognizer::processEvent( out, that we don't want to handle the mouse event sequence, */ + m_data->stopTimer( this ); + if ( m_data->timeout > 0 ) - qskTimerTable->startTimer( m_data->timeout, this ); + m_data->startTimer( this ); setState( Pending ); } else { - setState( Accepted ); + accept(); } } - if ( ( item == watchedItem ) && ( m_data->state > Idle ) ) + if ( m_data->state <= Idle ) + return false; + + if ( m_data->state == Pending ) + m_data->pendingEvents += qskClonedMouseEvent( event ); + + switch( static_cast< int >( event->type() ) ) { - switch ( event->type() ) + case QEvent::MouseButtonPress: + processPress( pos, timestamp, item == nullptr ); + break; + + case QEvent::MouseMove: + processMove( pos, timestamp ); + break; + + case QEvent::MouseButtonRelease: { - case QEvent::MouseButtonPress: + if ( m_data->state == Pending ) { - auto mouseEvent = static_cast< const QMouseEvent* >( event ); - m_data->pendingEvents += qskClonedMouseEvent( mouseEvent ); - - pressEvent( mouseEvent ); - return true; + reject(); } - - case QEvent::MouseMove: + else { - auto mouseEvent = static_cast< const QMouseEvent* >( event ); - m_data->pendingEvents += qskClonedMouseEvent( mouseEvent ); - - moveEvent( mouseEvent ); - return true; - } - - case QEvent::MouseButtonRelease: - { - auto mouseEvent = static_cast< const QMouseEvent* >( event ); - m_data->pendingEvents += qskClonedMouseEvent( mouseEvent ); - - if ( m_data->state == Pending ) - { - reject(); - } - else - { - releaseEvent( mouseEvent ); - reset(); - } - - return true; - } - - case QEvent::UngrabMouse: - { - // someone took away our grab, we have to give up + processRelease( pos, timestamp ); reset(); - break; } - default: - break; + break; } } - return false; + return true; } -void QskGestureRecognizer::pressEvent( const QMouseEvent* ) +void QskGestureRecognizer::processPress( + const QPointF& pos, quint64 timestamp, bool isFinal ) { + Q_UNUSED( pos ); + Q_UNUSED( timestamp ); + Q_UNUSED( isFinal ); } -void QskGestureRecognizer::moveEvent( const QMouseEvent* ) +void QskGestureRecognizer::processMove( const QPointF& pos, quint64 timestamp ) { + Q_UNUSED( pos ); + Q_UNUSED( timestamp ); } -void QskGestureRecognizer::releaseEvent( const QMouseEvent* ) +void QskGestureRecognizer::processRelease( const QPointF& pos, quint64 timestamp ) { -} - -void QskGestureRecognizer::stateChanged( State from, State to ) -{ - Q_UNUSED( from ) - Q_UNUSED( to ) + Q_UNUSED( pos ); + Q_UNUSED( timestamp ); } void QskGestureRecognizer::accept() { - qskTimerTable->stopTimer( this ); - m_data->pendingEvents.reset(); + m_data->stopTimer( this ); + + qDeleteAll( m_data->pendingEvents ); + m_data->pendingEvents.clear(); setState( Accepted ); } void QskGestureRecognizer::reject() { + /* + Moving the events to a local buffer, so that we can clear + m_data->pendingEvents before replaying them. + */ const auto events = m_data->pendingEvents; m_data->pendingEvents.clear(); reset(); - auto watchedItem = m_data->watchedItem; - if ( watchedItem == nullptr ) - return; + /* + Not 100% sure if we should send or post the events - const auto window = watchedItem->window(); - if ( window == nullptr ) - return; + Posting the events adds an extra round trip but we avoid + recursive event handler calls, that might not be expected + from the implementation of a control. + */ - m_data->isReplayingEvents = true; - - qskUngrabMouse( watchedItem ); - - if ( !events.isEmpty() && - ( events[ 0 ]->type() == QEvent::MouseButtonPress ) ) + if ( !events.isEmpty() ) { - /* - In a situation of several recognizers ( f.e a vertical - scroll view inside a horizontal swipe view ), we might receive - cloned events from another recognizer. - To avoid to process them twice we store the most recent timestamp - of the cloned events we have already processed, but reposted. - */ + if ( auto watchedItem = m_data->watchedItem ) + { + if ( const auto window = watchedItem->window() ) + { + for ( auto event : events ) + QCoreApplication::postEvent( window, event ); + } + } - m_data->timestampProcessed = events.last()->timestamp(); - - for ( auto event : events ) - QCoreApplication::sendEvent( window, event ); +#if 0 + // when using QCoreApplication::sendEvent above + qDeleteAll( events ); +#endif } - - m_data->isReplayingEvents = false; } void QskGestureRecognizer::abort() @@ -503,12 +453,16 @@ void QskGestureRecognizer::abort() void QskGestureRecognizer::reset() { - qskTimerTable->stopTimer( this ); - + m_data->stopTimer( this ); qskUngrabMouse( m_data->watchedItem ); - m_data->pendingEvents.reset(); - m_data->timestamp = 0; + qDeleteAll( m_data->pendingEvents ); + m_data->pendingEvents.clear(); + + m_data->timestampStarted = 0; + m_data->expired = false; setState( Idle ); } + +#include "moc_QskGestureRecognizer.cpp" diff --git a/src/controls/QskGestureRecognizer.h b/src/controls/QskGestureRecognizer.h index 7ed710be..012aa839 100644 --- a/src/controls/QskGestureRecognizer.h +++ b/src/controls/QskGestureRecognizer.h @@ -8,6 +8,7 @@ #include "QskGlobal.h" +#include #include #include @@ -15,8 +16,20 @@ class QQuickItem; class QEvent; class QMouseEvent; -class QSK_EXPORT QskGestureRecognizer +class QSK_EXPORT QskGestureRecognizer : public QObject { + Q_OBJECT + + Q_PROPERTY( State state READ state NOTIFY stateChanged ) + Q_PROPERTY( QQuickItem* watchedItem READ watchedItem WRITE setWatchedItem ) + + Q_PROPERTY( Qt::MouseButtons acceptedMouseButtons + READ acceptedMouseButtons WRITE setAcceptedMouseButtons ) + + Q_PROPERTY( int timeout READ timeout WRITE setTimeout ) + + using Inherited = QObject; + public: enum State { @@ -25,8 +38,12 @@ class QSK_EXPORT QskGestureRecognizer Accepted }; - QskGestureRecognizer(); - virtual ~QskGestureRecognizer(); + Q_ENUM( State ) + + QskGestureRecognizer( QObject* parent = nullptr ); + ~QskGestureRecognizer() override; + + bool eventFilter( QObject* object, QEvent* event) override; void setWatchedItem( QQuickItem* ); QQuickItem* watchedItem() const; @@ -35,13 +52,14 @@ class QSK_EXPORT QskGestureRecognizer void setAcceptedMouseButtons( Qt::MouseButtons ); Qt::MouseButtons acceptedMouseButtons() const; + void setRejectOnTimeout( bool ); + bool rejectOnTimeout() const; + void setTimeout( int ); int timeout() const; // timestamp, when the Idle state had been left - ulong timestamp() const; - - bool processEvent( const QQuickItem*, const QEvent*, bool blockReplayedEvents = true ); + quint64 timestampStarted() const; void reject(); void accept(); @@ -49,17 +67,28 @@ class QSK_EXPORT QskGestureRecognizer State state() const; - bool isReplaying() const; - bool hasProcessedBefore( const QMouseEvent* ) const; + virtual QRectF gestureRect() const; + + Q_SIGNALS: + void stateChanged( State from, State to ); protected: - virtual void pressEvent( const QMouseEvent* ); - virtual void moveEvent( const QMouseEvent* ); - virtual void releaseEvent( const QMouseEvent* ); + void timerEvent( QTimerEvent* ) override; - virtual void stateChanged( State from, State to ); + /* + This API works for single-touch gestures and multi-touch gestures, + that can be translated to single positions ( f.e 2 finger swipes ). + Once we support more complex inputs ( f.e pinch gesture ) we need to + make substantial adjustments here. + */ + virtual void processPress( const QPointF&, quint64 timestamp, bool isFinal ); + virtual void processMove( const QPointF&, quint64 timestamp ); + virtual void processRelease( const QPointF&, quint64 timestamp ); private: + bool maybeGesture( const QQuickItem*, const QEvent* ); + bool processMouseEvent( const QQuickItem*, const QMouseEvent* ); + void setState( State ); void reset(); diff --git a/src/controls/QskPanGestureRecognizer.cpp b/src/controls/QskPanGestureRecognizer.cpp index 6a2ce28d..f7db9f5a 100644 --- a/src/controls/QskPanGestureRecognizer.cpp +++ b/src/controls/QskPanGestureRecognizer.cpp @@ -1,3 +1,8 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + #include "QskPanGestureRecognizer.h" #include "QskEvent.h" #include "QskGesture.h" @@ -139,20 +144,12 @@ namespace class QskPanGestureRecognizer::PrivateData { public: - PrivateData() - : orientations( Qt::Horizontal | Qt::Vertical ) - , minDistance( 15 ) - , timestamp( 0.0 ) - , angle( 0.0 ) - { - } + Qt::Orientations orientations = Qt::Horizontal | Qt::Vertical; - Qt::Orientations orientations; + int minDistance = 15; - int minDistance; - - ulong timestamp; // timestamp of the last mouse event - qreal angle; + quint64 timestampVelocity = 0.0; // timestamp of the last mouse event + qreal angle = 0.0; QPointF origin; QPointF pos; // position of the last mouse event @@ -160,9 +157,11 @@ class QskPanGestureRecognizer::PrivateData VelocityTracker velocityTracker; }; -QskPanGestureRecognizer::QskPanGestureRecognizer() - : m_data( new PrivateData() ) +QskPanGestureRecognizer::QskPanGestureRecognizer( QObject* parent ) + : QskGestureRecognizer( parent ) + , m_data( new PrivateData() ) { + setTimeout( 100 ); // value from the platform ??? } QskPanGestureRecognizer::~QskPanGestureRecognizer() @@ -189,23 +188,29 @@ int QskPanGestureRecognizer::minDistance() const return m_data->minDistance; } -void QskPanGestureRecognizer::pressEvent( const QMouseEvent* event ) +void QskPanGestureRecognizer::processPress( const QPointF& pos, quint64, bool isFinal ) { - m_data->origin = m_data->pos = qskMousePosition( event ); - m_data->timestamp = timestamp(); + /* + When nobody was interested in the press we can disable the timeout and let + the distance of the mouse moves be the only criterion. + */ + setRejectOnTimeout( !isFinal ); + + m_data->origin = m_data->pos = pos; + m_data->timestampVelocity = timestampStarted(); m_data->velocityTracker.reset(); } -void QskPanGestureRecognizer::moveEvent( const QMouseEvent* event ) +void QskPanGestureRecognizer::processMove( const QPointF& pos, quint64 timestamp ) { - const ulong elapsed = event->timestamp() - m_data->timestamp; - const ulong elapsedTotal = event->timestamp() - timestamp(); + const ulong elapsed = timestamp - m_data->timestampVelocity; + const ulong elapsedTotal = timestamp - timestampStarted(); const QPointF oldPos = m_data->pos; - m_data->timestamp = event->timestamp(); - m_data->pos = qskMousePosition( event ); + m_data->timestampVelocity = timestamp; + m_data->pos = pos; if ( elapsedTotal > 0 ) // ??? { @@ -249,11 +254,11 @@ void QskPanGestureRecognizer::moveEvent( const QMouseEvent* event ) } } -void QskPanGestureRecognizer::releaseEvent( const QMouseEvent* event ) +void QskPanGestureRecognizer::processRelease( const QPointF&, quint64 timestamp ) { if ( state() == QskGestureRecognizer::Accepted ) { - const ulong elapsedTotal = event->timestamp() - timestamp(); + const ulong elapsedTotal = timestamp - timestampStarted(); const qreal velocity = m_data->velocityTracker.velocity( elapsedTotal ); qskSendPanGestureEvent( watchedItem(), QskGesture::Finished, diff --git a/src/controls/QskPanGestureRecognizer.h b/src/controls/QskPanGestureRecognizer.h index 4e9960ad..26a72576 100644 --- a/src/controls/QskPanGestureRecognizer.h +++ b/src/controls/QskPanGestureRecognizer.h @@ -14,7 +14,7 @@ class QSK_EXPORT QskPanGestureRecognizer : public QskGestureRecognizer using Inherited = QskGestureRecognizer; public: - QskPanGestureRecognizer(); + QskPanGestureRecognizer( QObject* = nullptr ); ~QskPanGestureRecognizer() override; void setMinDistance( int pixels ); @@ -24,9 +24,9 @@ class QSK_EXPORT QskPanGestureRecognizer : public QskGestureRecognizer Qt::Orientations orientations() const; private: - void pressEvent( const QMouseEvent* ) override; - void moveEvent( const QMouseEvent* ) override; - void releaseEvent( const QMouseEvent* ) override; + void processPress( const QPointF&, quint64 timestamp, bool isFinal ) override; + void processMove( const QPointF&, quint64 timestamp ) override; + void processRelease( const QPointF&, quint64 timestamp ) override; class PrivateData; std::unique_ptr< PrivateData > m_data; diff --git a/src/controls/QskScrollBox.cpp b/src/controls/QskScrollBox.cpp index 842a2dd5..a2304842 100644 --- a/src/controls/QskScrollBox.cpp +++ b/src/controls/QskScrollBox.cpp @@ -20,6 +20,30 @@ static inline constexpr qreal qskViewportPadding() return 10.0; // should be from the skin, TODO ... } +static inline bool qskIsScrollable( + const QskScrollBox* scrollBox, Qt::Orientations orientations ) +{ + if ( orientations ) + { + const QSizeF viewSize = scrollBox->viewContentsRect().size(); + const QSizeF& scrollableSize = scrollBox->scrollableSize(); + + if ( orientations & Qt::Vertical ) + { + if ( viewSize.height() < scrollableSize.height() ) + return true; + } + + if ( orientations & Qt::Horizontal ) + { + if ( viewSize.width() < scrollableSize.width() ) + return true; + } + } + + return false; +} + namespace { class FlickAnimator final : public QskFlickAnimator @@ -100,6 +124,29 @@ namespace QPointF m_from; QPointF m_to; }; + + class PanRecognizer final : public QskPanGestureRecognizer + { + using Inherited = QskPanGestureRecognizer; + + public: + PanRecognizer( QObject* parent = nullptr ) + : QskPanGestureRecognizer( parent ) + { + setOrientations( Qt::Horizontal | Qt::Vertical ); + } + + QRectF gestureRect() const override + { + if ( auto scrollBox = qobject_cast< const QskScrollBox* >( watchedItem() ) ) + { + if ( qskIsScrollable( scrollBox, orientations() ) ) + return scrollBox->viewContentsRect(); + } + + return QRectF( 0.0, 0.0, -1.0, -1.0 ); // empty + } + }; } class QskScrollBox::PrivateData @@ -108,8 +155,7 @@ class QskScrollBox::PrivateData QPointF scrollPos; QSizeF scrollableSize = QSize( 0.0, 0.0 ); - QskPanGestureRecognizer panRecognizer; - int panRecognizerTimeout = 100; // value coming from the platform ??? + PanRecognizer panRecognizer; FlickAnimator flicker; ScrollAnimator scroller; @@ -125,13 +171,11 @@ QskScrollBox::QskScrollBox( QQuickItem* parent ) m_data->scroller.setScrollBox( this ); m_data->panRecognizer.setWatchedItem( this ); - m_data->panRecognizer.setOrientations( Qt::Horizontal | Qt::Vertical ); - - setFiltersChildMouseEvents( true ); setAcceptedMouseButtons( Qt::LeftButton ); - setWheelEnabled( true ); + setFiltersChildMouseEvents( true ); + setWheelEnabled( true ); setFocusPolicy( Qt::StrongFocus ); connectWindow( window(), true ); @@ -185,12 +229,12 @@ void QskScrollBox::setFlickRecognizerTimeout( int timeout ) if ( timeout < 0 ) timeout = -1; - m_data->panRecognizerTimeout = timeout; + m_data->panRecognizer.setTimeout( timeout ); } int QskScrollBox::flickRecognizerTimeout() const { - return m_data->panRecognizerTimeout; + return m_data->panRecognizer.timeout(); } void QskScrollBox::setFlickableOrientations( Qt::Orientations orientations ) @@ -250,11 +294,6 @@ QSizeF QskScrollBox::scrollableSize() const return m_data->scrollableSize; } -QRectF QskScrollBox::gestureRect() const -{ - return viewContentsRect(); -} - void QskScrollBox::ensureItemVisible( const QQuickItem* item ) { if ( qskIsAncestorOf( this, item ) ) @@ -362,25 +401,6 @@ void QskScrollBox::geometryChangeEvent( QskGeometryChangeEvent* event ) Inherited::geometryChangeEvent( event ); } -void QskScrollBox::mousePressEvent( QMouseEvent* event ) -{ - auto& recognizer = m_data->panRecognizer; - if ( recognizer.hasProcessedBefore( event ) ) - { - if ( m_data->panRecognizerTimeout != 0 ) - { - recognizer.abort(); - recognizer.setTimeout( -1 ); - - recognizer.processEvent( this, event, false ); - } - - return; - } - - return Inherited::mousePressEvent( event ); -} - void QskScrollBox::gestureEvent( QskGestureEvent* event ) { if ( event->gesture()->type() == QskGesture::Pan ) @@ -446,75 +466,6 @@ void QskScrollBox::wheelEvent( QWheelEvent* event ) #endif -bool QskScrollBox::gestureFilter( const QQuickItem* item, const QEvent* event ) -{ - if ( event->type() == QEvent::MouseButtonPress ) - { - // Checking first if panning is possible at all - - bool maybeGesture = false; - - const auto orientations = m_data->panRecognizer.orientations(); - if ( orientations ) - { - const QSizeF viewSize = viewContentsRect().size(); - const QSizeF& scrollableSize = m_data->scrollableSize; - - if ( orientations & Qt::Vertical ) - { - if ( viewSize.height() < scrollableSize.height() ) - maybeGesture = true; - } - - if ( orientations & Qt::Horizontal ) - { - if ( viewSize.width() < scrollableSize.width() ) - maybeGesture = true; - } - } - - if ( !maybeGesture ) - return false; - } - - - auto& recognizer = m_data->panRecognizer; - recognizer.setTimeout( m_data->panRecognizerTimeout ); - - if ( event->type() == QEvent::MouseButtonPress ) - { - /* - This code is a bit tricky as the filter is called in different situations: - - a) The press was on a child of the view - b) The press was on the view - - In case of b) things are simple and we can let the recognizer - decide without timeout if it is was a gesture or not. - - In case of a) we give the recognizer some time to decide - usually - based on the distances of the following mouse events. If no decision - could be made the recognizer aborts and replays the mouse events, so - that the children can process them. - - But if a child does not accept the mouse event it will be sent to - its parent, finally ending up here for a second time. - */ - - auto mouseEvent = static_cast< const QMouseEvent* >( event ); - if ( recognizer.hasProcessedBefore( mouseEvent ) ) - { - /* - Note that the recognizer will be restarted without timout if the - event ends up in in mousePressEvent ( = nobody else was interested ) - */ - return false; - } - } - - return recognizer.processEvent( item, event ); -} - QPointF QskScrollBox::boundedScrollPos( const QPointF& pos ) const { const QRectF vr = viewContentsRect(); diff --git a/src/controls/QskScrollBox.h b/src/controls/QskScrollBox.h index 889a1809..c7a64ffd 100644 --- a/src/controls/QskScrollBox.h +++ b/src/controls/QskScrollBox.h @@ -42,7 +42,6 @@ class QSK_EXPORT QskScrollBox : public QskControl QSizeF scrollableSize() const; virtual QRectF viewContentsRect() const = 0; - QRectF gestureRect() const override; Q_SIGNALS: void scrolledTo( const QPointF& ); @@ -65,7 +64,6 @@ class QSK_EXPORT QskScrollBox : public QskControl void geometryChangeEvent( QskGeometryChangeEvent* ) override; void windowChangeEvent( QskWindowChangeEvent* ) override; - void mousePressEvent( QMouseEvent* ) override; void gestureEvent( QskGestureEvent* ) override; #ifndef QT_NO_WHEELEVENT @@ -73,7 +71,6 @@ class QSK_EXPORT QskScrollBox : public QskControl virtual QPointF scrollOffset( const QWheelEvent* ) const; #endif - bool gestureFilter( const QQuickItem*, const QEvent* ) override; void setScrollableSize( const QSizeF& ); private: diff --git a/src/controls/QskSwipeView.cpp b/src/controls/QskSwipeView.cpp index ebe702e7..545c416f 100644 --- a/src/controls/QskSwipeView.cpp +++ b/src/controls/QskSwipeView.cpp @@ -96,23 +96,6 @@ void QskSwipeView::resetDuration() setDuration( 500 ); } -bool QskSwipeView::gestureFilter( const QQuickItem* item, const QEvent* event ) -{ - // see QskScrollBox.cpp - - auto& recognizer = m_data->panRecognizer; - - if ( event->type() == QEvent::MouseButtonPress ) - { - auto mouseEvent = static_cast< const QMouseEvent* >( event ); - if ( recognizer.hasProcessedBefore( mouseEvent ) ) - return false; - } - - return recognizer.processEvent( item, event ); - -} - void QskSwipeView::gestureEvent( QskGestureEvent* event ) { const auto gesture = static_cast< const QskPanGesture* >( event->gesture().get() ); diff --git a/src/controls/QskSwipeView.h b/src/controls/QskSwipeView.h index 868a6d4b..390634e0 100644 --- a/src/controls/QskSwipeView.h +++ b/src/controls/QskSwipeView.h @@ -57,7 +57,6 @@ class QSK_EXPORT QskSwipeView : public QskStackBox void swipeDistanceChanged( int ); protected: - bool gestureFilter( const QQuickItem*, const QEvent* ) override; void gestureEvent( QskGestureEvent* ) override; private: