QskScrollArea reimplemented to have a proper filtering of child events
This commit is contained in:
parent
ebe332c14e
commit
b1c3acde8e
|
@ -8,13 +8,14 @@
|
|||
|
||||
#include <QskScrollArea.h>
|
||||
#include <QskWindow.h>
|
||||
#include <QskGraphicLabel.h>
|
||||
#include <QskPushButton.h>
|
||||
#include <QskLinearBox.h>
|
||||
#include <QskObjectCounter.h>
|
||||
#include <QskGraphic.h>
|
||||
#include <QskBoxBorderMetrics.h>
|
||||
#include <QskBoxShapeMetrics.h>
|
||||
#include <QskAspect.h>
|
||||
#include <QskFocusIndicator.h>
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QPainter>
|
||||
|
@ -57,11 +58,11 @@ static int randomShape()
|
|||
return qrand() % SkinnyShapeFactory::ShapeCount;
|
||||
}
|
||||
|
||||
class Thumbnail : public QskGraphicLabel
|
||||
class Thumbnail : public QskPushButton
|
||||
{
|
||||
public:
|
||||
Thumbnail( const QColor& color, int shape, QQuickItem* parentItem ):
|
||||
QskGraphicLabel( parentItem )
|
||||
QskPushButton( parentItem )
|
||||
{
|
||||
using namespace SkinnyShapeFactory;
|
||||
|
||||
|
@ -83,6 +84,8 @@ public:
|
|||
|
||||
setGraphic( graphic );
|
||||
setFixedSize( size );
|
||||
|
||||
setFlat( true );
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -141,6 +144,9 @@ int main( int argc, char* argv[] )
|
|||
with QskScrollView using scene graph node composition - like done
|
||||
with QskListView.
|
||||
|
||||
The thumbnails are implemented as buttons, so that we can see if the gesture
|
||||
recognition for the flicking works without stopping the buttons from being functional.
|
||||
|
||||
This example also shows, that blocking of the scene graph node creation
|
||||
( QskControl::DeferredUpdate + QskControl::CleanupOnVisibility )
|
||||
could be improved to also respect being inside the window or a clip region.
|
||||
|
@ -156,6 +162,7 @@ int main( int argc, char* argv[] )
|
|||
window.resize( 600, 600 );
|
||||
window.setColor( "SteelBlue" );
|
||||
window.addItem( scrollArea );
|
||||
window.addItem( new QskFocusIndicator() );
|
||||
|
||||
window.show();
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include "QskScrollArea.h"
|
||||
#include "QskScrollViewSkinlet.h"
|
||||
#include "QskLayoutConstraint.h"
|
||||
#include "QskBoxClipNode.h"
|
||||
|
||||
QSK_QT_PRIVATE_BEGIN
|
||||
#include <private/qquickitem_p.h>
|
||||
|
@ -61,9 +62,11 @@ namespace
|
|||
{
|
||||
public:
|
||||
ViewportClipNode():
|
||||
QQuickDefaultClipNode( QRectF() )
|
||||
QQuickDefaultClipNode( QRectF() ),
|
||||
m_otherGeometry( nullptr )
|
||||
{
|
||||
setGeometry( nullptr );
|
||||
setFlag( QSGNode::OwnsGeometry, true );
|
||||
|
||||
// clip nodes have no material, so this flag
|
||||
// is available to indicate our replaced clip node
|
||||
|
@ -71,52 +74,63 @@ namespace
|
|||
setFlag( QSGNode::OwnsMaterial, true );
|
||||
}
|
||||
|
||||
void copyFrom( const QSGClipNode* other )
|
||||
void copyFrom( const QSGClipNode* other, const QPointF& offset )
|
||||
{
|
||||
bool isDirty = false;
|
||||
|
||||
if ( other == nullptr )
|
||||
{
|
||||
if ( !isRectangular() && clipRect().isEmpty() )
|
||||
if ( !( clipRect().isEmpty() && isRectangular() ) )
|
||||
{
|
||||
setClipRect( QRectF() );
|
||||
setIsRectangular( true );
|
||||
|
||||
setGeometry( nullptr );
|
||||
m_otherGeometry = nullptr;
|
||||
|
||||
markDirty( QSGNode::DirtyGeometry );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bool isDirty = false;
|
||||
|
||||
const auto newClipRect = other->clipRect().translated( offset );
|
||||
|
||||
if ( clipRect() != newClipRect )
|
||||
{
|
||||
setClipRect( newClipRect );
|
||||
isDirty = true;
|
||||
}
|
||||
|
||||
if ( other->isRectangular() )
|
||||
{
|
||||
if ( !isRectangular() )
|
||||
{
|
||||
setIsRectangular( true );
|
||||
setClipRect( QRectF() );
|
||||
|
||||
setGeometry( nullptr );
|
||||
m_otherGeometry = nullptr;
|
||||
|
||||
isDirty = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const bool isRect = other->isRectangular();
|
||||
|
||||
if ( isRect != isRectangular() )
|
||||
if ( isRectangular() )
|
||||
{
|
||||
setIsRectangular( isRect );
|
||||
setIsRectangular( false );
|
||||
isDirty = true;
|
||||
}
|
||||
|
||||
if ( clipRect() != other->clipRect() )
|
||||
if ( ( geometry() == nullptr )
|
||||
|| ( geometry()->vertexCount() != other->geometry()->vertexCount() )
|
||||
|| ( other->geometry() != m_otherGeometry ) )
|
||||
{
|
||||
setClipRect( other->clipRect() );
|
||||
isDirty = true;
|
||||
}
|
||||
setGeometry( QskBoxClipNode::translatedGeometry(
|
||||
other->geometry(), offset ) );
|
||||
|
||||
if ( isRect )
|
||||
{
|
||||
setGeometry( nullptr );
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( geometry() != other->geometry() )
|
||||
{
|
||||
/*
|
||||
A deep copy would be less efficient, but avoids
|
||||
any further problems - let's see how stable it is.
|
||||
*/
|
||||
setGeometry( const_cast< QSGGeometry* >( other->geometry() ) );
|
||||
isDirty = true;
|
||||
}
|
||||
m_otherGeometry = other->geometry();
|
||||
isDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,6 +147,8 @@ namespace
|
|||
into nops.
|
||||
*/
|
||||
}
|
||||
|
||||
const QSGGeometry* m_otherGeometry;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -155,7 +171,6 @@ public:
|
|||
|
||||
protected:
|
||||
virtual bool event( QEvent* event ) override final;
|
||||
virtual bool childMouseEventFilter( QQuickItem*, QEvent* ) override final;
|
||||
|
||||
virtual void itemChange( ItemChange, const ItemChangeData& ) override final;
|
||||
virtual void geometryChanged( const QRectF&, const QRectF& ) override final;
|
||||
|
@ -169,7 +184,7 @@ protected:
|
|||
}
|
||||
|
||||
#else
|
||||
virtual void itemGeometryChanged( QQuickItem *,
|
||||
virtual void itemGeometryChanged( QQuickItem*,
|
||||
const QRectF& newRect, const QRectF& oldRect ) override final
|
||||
{
|
||||
if ( oldRect.size() != newRect.size() )
|
||||
|
@ -197,9 +212,7 @@ QskScrollAreaClipItem::QskScrollAreaClipItem( QskScrollArea* scrollArea ):
|
|||
Inherited( scrollArea )
|
||||
{
|
||||
setObjectName( QStringLiteral( "QskScrollAreaClipItem" ) );
|
||||
|
||||
setClip( true );
|
||||
setFiltersChildMouseEvents( true );
|
||||
}
|
||||
|
||||
QskScrollAreaClipItem::~QskScrollAreaClipItem()
|
||||
|
@ -236,6 +249,8 @@ void QskScrollAreaClipItem::updateNode( QSGNode* )
|
|||
|
||||
if ( clipNode && !( clipNode->flags() & QSGNode::OwnsMaterial ) )
|
||||
{
|
||||
// Replace the clip node being inserted from QQuickWindow
|
||||
|
||||
auto parentNode = clipNode->parent();
|
||||
|
||||
auto node = new ViewportClipNode();
|
||||
|
@ -253,8 +268,17 @@ void QskScrollAreaClipItem::updateNode( QSGNode* )
|
|||
|
||||
if ( clipNode )
|
||||
{
|
||||
/*
|
||||
Update the clip node with the geometry of the clip node
|
||||
of the viewport of the scrollview.
|
||||
|
||||
Maybe it would be better to ask the skinlet for translated clip node
|
||||
but we would have a dependency for QskScrollViewSkinlet then.
|
||||
*/
|
||||
auto viewClipNode = static_cast< ViewportClipNode* >( clipNode );
|
||||
viewClipNode->copyFrom( viewPortClipNode() );
|
||||
viewClipNode->copyFrom( viewPortClipNode(), -position() );
|
||||
|
||||
Q_ASSERT( viewClipNode->isRectangular() || viewClipNode->geometry() );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -285,27 +309,15 @@ void QskScrollAreaClipItem::geometryChanged(
|
|||
void QskScrollAreaClipItem::itemChange(
|
||||
QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& value )
|
||||
{
|
||||
/*
|
||||
Unfortunately QT/Quick developers didn't use the well established
|
||||
concept of events and introduced a proprietory and less powerful
|
||||
notification system of hooks and optional listeners.
|
||||
As we like to support items not being derived from QskControl we have
|
||||
to use them.
|
||||
*/
|
||||
|
||||
if ( ( change == QQuickItem::ItemChildAddedChange ) ||
|
||||
( change == QQuickItem::ItemChildRemovedChange ) )
|
||||
if ( change == QQuickItem::ItemChildAddedChange )
|
||||
{
|
||||
/*
|
||||
In case we are not adjusting the geometry of the scrolled item
|
||||
we have to adjust the scrollview to the geometry changes indicated
|
||||
from the scrolled item.
|
||||
*/
|
||||
|
||||
if ( !scrollArea()->isItemResizable() )
|
||||
enableGeometryListener( change == QQuickItem::ItemChildAddedChange );
|
||||
enableGeometryListener( true );
|
||||
}
|
||||
|
||||
else if ( change == QQuickItem::ItemChildRemovedChange )
|
||||
{
|
||||
enableGeometryListener( false );
|
||||
}
|
||||
|
||||
Inherited::itemChange( change, value );
|
||||
}
|
||||
|
||||
|
@ -336,35 +348,6 @@ bool QskScrollAreaClipItem::event( QEvent* event )
|
|||
return Inherited::event( event );
|
||||
}
|
||||
|
||||
bool QskScrollAreaClipItem::childMouseEventFilter( QQuickItem* item, QEvent* event )
|
||||
{
|
||||
if ( ( event->type() == QEvent::MouseButtonPress )
|
||||
|| ( event->type() == QEvent::MouseMove ) )
|
||||
{
|
||||
const auto mouseEvent = static_cast< const QMouseEvent* >( event );
|
||||
const QPointF pos = mapFromScene( mouseEvent->windowPos() );
|
||||
|
||||
auto clipNode = QQuickItemPrivate::get( this )->clipNode();
|
||||
if ( clipNode && !clipNode->clipRect().contains( pos ) )
|
||||
{
|
||||
if ( event->type() == QEvent::MouseButtonPress )
|
||||
{
|
||||
QCoreApplication::sendEvent( scrollArea(), event );
|
||||
return true;
|
||||
}
|
||||
else if ( event->type() == QEvent::MouseMove )
|
||||
{
|
||||
if ( item == window()->mouseGrabberItem() )
|
||||
item->ungrabMouse();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
class QskScrollArea::PrivateData
|
||||
{
|
||||
public:
|
||||
|
@ -391,12 +374,46 @@ public:
|
|||
bool isItemResizable : 1;
|
||||
};
|
||||
|
||||
/*
|
||||
When doing scene graph composition it is quite easy to insert a clip node
|
||||
somewhere below the paint node to have all items on the viewport being clipped.
|
||||
This is how it is done f.e. for the list boxes.
|
||||
|
||||
But when having QQuickItems on the viewport we run into 2 fundamental
|
||||
limitations of the Qt/Quick design.
|
||||
|
||||
a) The node subtrees for the children are in parallel to the paint node.
|
||||
b) The default clipNode() is always rectangular and only for the
|
||||
complete boundingRect() of the item.
|
||||
|
||||
Both limitations are hardcoded in QQuickWindow without offering ways
|
||||
to customize the operations. Even worse: obviously the code was once started
|
||||
with having more flexible APIs in mind, but for some reasons it was never
|
||||
finalized and not even the existing APIs are internally used properly.
|
||||
|
||||
( F.e there would be a virtual method QQuickItem::clipRect(), but QQuickWindow
|
||||
uses erroneously QQuickItem::contains() to filter events - grmpf. )
|
||||
|
||||
--
|
||||
|
||||
This class works around those limitations, by inserting a clip item
|
||||
that replaces its default clip node by copying out the geometry of clip node
|
||||
for view port.
|
||||
|
||||
This clip item needs to have exactly the same position + size as the
|
||||
viewport, so that clipping of the mouse/touch/hover/wheel in QQuickWindow
|
||||
works properly. Unfortunately we then have to copy + translate the geometry of
|
||||
the view port instead of simply sharing it between the 2 clip nodes.
|
||||
|
||||
But even then, filtering of events does not yet work perfect for non rectangular
|
||||
clip regions. Maybe it could be done by adding a childMouseEventFilter(). TODO ...
|
||||
*/
|
||||
|
||||
QskScrollArea::QskScrollArea( QQuickItem* parentItem ):
|
||||
Inherited( parentItem ),
|
||||
m_data( new PrivateData() )
|
||||
{
|
||||
setPolishOnResize( true );
|
||||
setFiltersChildMouseEvents( true );
|
||||
|
||||
m_data->clipItem = new QskScrollAreaClipItem( this );
|
||||
m_data->enableAutoTranslation( this, true );
|
||||
|
@ -411,14 +428,14 @@ void QskScrollArea::updateLayout()
|
|||
{
|
||||
Inherited::updateLayout();
|
||||
|
||||
// the clipItem always has the same geometry as the scroll area
|
||||
m_data->clipItem->setSize( size() );
|
||||
m_data->clipItem->setGeometry( viewContentsRect() );
|
||||
adjustItem();
|
||||
}
|
||||
|
||||
void QskScrollArea::adjustItem()
|
||||
{
|
||||
QQuickItem* item = m_data->clipItem->scrolledItem();
|
||||
|
||||
if ( item == nullptr )
|
||||
{
|
||||
setScrollableSize( QSizeF() );
|
||||
|
@ -457,8 +474,6 @@ void QskScrollArea::setItemResizable( bool on )
|
|||
if ( on != m_data->isItemResizable )
|
||||
{
|
||||
m_data->isItemResizable = on;
|
||||
m_data->clipItem->enableGeometryListener( !on );
|
||||
|
||||
Q_EMIT itemResizableChanged();
|
||||
|
||||
if ( m_data->isItemResizable )
|
||||
|
@ -503,12 +518,9 @@ QQuickItem* QskScrollArea::scrolledItem() const
|
|||
|
||||
void QskScrollArea::translateItem()
|
||||
{
|
||||
auto item = m_data->clipItem->scrolledItem();
|
||||
auto item = scrolledItem();
|
||||
if ( item )
|
||||
{
|
||||
const QPointF pos = viewContentsRect().topLeft() - scrollPos();
|
||||
item->setPosition( pos );
|
||||
}
|
||||
item->setPosition( -scrollPos() );
|
||||
}
|
||||
|
||||
#include "moc_QskScrollArea.cpp"
|
||||
|
|
|
@ -85,9 +85,10 @@ QskScrollView::QskScrollView( QQuickItem* parent ):
|
|||
|
||||
m_data->panRecognizer.setWatchedItem( this );
|
||||
m_data->panRecognizer.setOrientations( Qt::Horizontal | Qt::Vertical );
|
||||
m_data->panRecognizer.setTimeout( 200 );
|
||||
|
||||
setAcceptedMouseButtons( Qt::LeftButton );
|
||||
setFiltersChildMouseEvents( true );
|
||||
|
||||
setWheelEnabled( true );
|
||||
setFocusPolicy( Qt::StrongFocus );
|
||||
}
|
||||
|
|
|
@ -45,7 +45,6 @@ void QskBoxClipNode::setBox( const QRectF& rect,
|
|||
m_geometry.allocate( 0 );
|
||||
|
||||
setIsRectangular( true );
|
||||
setClipRect( qskValidOrEmptyInnerRect( rect, border.widths() ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -53,5 +52,62 @@ void QskBoxClipNode::setBox( const QRectF& rect,
|
|||
QskBoxRenderer().renderFill( rect, shape, border, m_geometry );
|
||||
}
|
||||
|
||||
/*
|
||||
Even in situations, where the clipping is not rectangular, it is
|
||||
useful to know its bounding rectangle
|
||||
*/
|
||||
setClipRect( qskValidOrEmptyInnerRect( rect, border.widths() ) );
|
||||
|
||||
markDirty( QSGNode::DirtyGeometry );
|
||||
}
|
||||
|
||||
template< class Point >
|
||||
static inline void qskCopyPoints( const Point* from, Point* to,
|
||||
int numPoints, const QPointF& offset )
|
||||
{
|
||||
if ( offset.isNull() )
|
||||
{
|
||||
memcpy( to, from, numPoints * sizeof( Point ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
const float dx = offset.x();
|
||||
const float dy = offset.y();
|
||||
|
||||
for ( int i = 0; i < numPoints; i++ )
|
||||
{
|
||||
to[i].x = from[i].x + dx;
|
||||
to[i].y = from[i].y + dy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QSGGeometry* QskBoxClipNode::translatedGeometry(
|
||||
const QSGGeometry* geometry, const QPointF& offset )
|
||||
{
|
||||
if ( geometry == nullptr || geometry->vertexCount() == 0 )
|
||||
return nullptr;
|
||||
|
||||
QSGGeometry* g = nullptr;
|
||||
|
||||
if ( geometry->sizeOfVertex() == sizeof( QSGGeometry::Point2D ) )
|
||||
{
|
||||
g = new QSGGeometry(
|
||||
QSGGeometry::defaultAttributes_Point2D(), geometry->vertexCount(),
|
||||
geometry->indexCount(), geometry->indexType() );
|
||||
|
||||
qskCopyPoints( geometry->vertexDataAsPoint2D(),
|
||||
g->vertexDataAsPoint2D(), g->vertexCount(), offset );
|
||||
}
|
||||
else if ( geometry->sizeOfVertex() == sizeof( QSGGeometry::ColoredPoint2D ) )
|
||||
{
|
||||
g = new QSGGeometry(
|
||||
QSGGeometry::defaultAttributes_ColoredPoint2D(), geometry->vertexCount(),
|
||||
geometry->indexCount(), geometry->indexType() );
|
||||
|
||||
qskCopyPoints( geometry->vertexDataAsColoredPoint2D(),
|
||||
g->vertexDataAsColoredPoint2D(), g->vertexCount(), offset );
|
||||
}
|
||||
|
||||
return g;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ public:
|
|||
void setBox( const QRectF&,
|
||||
const QskBoxShapeMetrics&, const QskBoxBorderMetrics& );
|
||||
|
||||
static QSGGeometry* translatedGeometry( const QSGGeometry*, const QPointF& );
|
||||
|
||||
private:
|
||||
uint m_hash;
|
||||
QRectF m_rect;
|
||||
|
|
Loading…
Reference in New Issue