QskGestureRecognizer using event filtering

This commit is contained in:
Uwe Rathmann 2023-10-05 08:59:30 +02:00
parent 267c559330
commit 067cffbd7c
16 changed files with 515 additions and 585 deletions

View File

@ -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

View File

@ -12,6 +12,7 @@
#include <QskEvent.h>
#include <QskLinearBox.h>
#include <QskStackBoxAnimator.h>
#include <QskPanGestureRecognizer.h>
#include <QQuickFramebufferObject>
#include <QGuiApplication>
@ -20,6 +21,23 @@
#include <QTimer>
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"

View File

@ -1,11 +1,8 @@
#pragma once
#include <QskControl.h>
#include <QskPanGestureRecognizer.h>
#include <QskStackBox.h>
#include <QQuickWindow>
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;
};

View File

@ -16,8 +16,10 @@
#include <QskObjectCounter.h>
#include <QskPushButton.h>
#include <QskScrollArea.h>
#include <QskSwipeView.h>
#include <QskQuick.h>
#include <QskWindow.h>
#include <QskRgbValue.h>
#include <QGuiApplication>
#include <QPainter>
@ -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();

View File

@ -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();

View File

@ -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;

View File

@ -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;

View File

@ -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;
};

View File

@ -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 <qbasictimer.h>
#include <qcoreapplication.h>
#include <qcoreevent.h>
#include <qquickitem.h>
#include <qquickwindow.h>
#include <qscopedpointer.h>
#include <qvector.h>
QSK_QT_PRIVATE_BEGIN
#include <private/qquickwindow_p.h>
#if QT_VERSION >= QT_VERSION_CHECK( 6, 3, 0 )
#include <private/qeventpoint_p.h>
#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 <private/qquickwindow_p.h>
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"

View File

@ -8,6 +8,7 @@
#include "QskGlobal.h"
#include <qobject.h>
#include <qnamespace.h>
#include <memory>
@ -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();

View File

@ -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,

View File

@ -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;

View File

@ -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();

View File

@ -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:

View File

@ -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() );

View File

@ -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: