qskinny/src/controls/QskScrollArea.cpp

527 lines
15 KiB
C++
Raw Normal View History

2017-07-21 18:21:34 +02:00
/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the QSkinny License, Version 1.0
*****************************************************************************/
#include "QskScrollArea.h"
#include "QskScrollViewSkinlet.h"
#include "QskLayoutConstraint.h"
#include "QskBoxClipNode.h"
2017-07-21 18:21:34 +02:00
QSK_QT_PRIVATE_BEGIN
#include <private/qquickitem_p.h>
#include <private/qquickclipnode_p.h>
#include <private/qquickitemchangelistener_p.h>
QSK_QT_PRIVATE_END
static QSizeF qskAdjustedSize( const QQuickItem* item, const QSizeF& targetSize )
{
using namespace QskLayoutConstraint;
QSizeF sz = effectiveConstraint( item, Qt::PreferredSize );
qreal w = sz.width();
qreal h = sz.height();
if ( targetSize != sz )
{
const QSizeF minSize = effectiveConstraint( item, Qt::MinimumSize );
const QSizeF maxSize = effectiveConstraint( item, Qt::MaximumSize );
const auto policy = sizePolicy( item );
if ( targetSize.width() > w )
{
if ( policy.horizontalPolicy() & QskSizePolicy::GrowFlag )
w = qMin( maxSize.width(), targetSize.width() );
}
else if ( targetSize.width() < w )
{
if ( policy.horizontalPolicy() & QskSizePolicy::ShrinkFlag )
w = qMax( minSize.width(), w );
}
if ( targetSize.height() > h )
{
if ( policy.verticalPolicy() & QskSizePolicy::GrowFlag )
h = qMin( maxSize.height(), targetSize.height() );
}
else if ( targetSize.height() < h )
{
if ( policy.verticalPolicy() & QskSizePolicy::ShrinkFlag )
h = qMax( minSize.height(), h );
}
}
return QSizeF( w, h );
}
namespace
{
class ViewportClipNode final : public QQuickDefaultClipNode
{
public:
ViewportClipNode():
QQuickDefaultClipNode( QRectF() ),
m_otherGeometry( nullptr )
2017-07-21 18:21:34 +02:00
{
setGeometry( nullptr );
setFlag( QSGNode::OwnsGeometry, true );
2017-07-21 18:21:34 +02:00
// clip nodes have no material, so this flag
// is available to indicate our replaced clip node
setFlag( QSGNode::OwnsMaterial, true );
}
void copyFrom( const QSGClipNode* other, const QPointF& offset )
2017-07-21 18:21:34 +02:00
{
if ( other == nullptr )
{
if ( !( clipRect().isEmpty() && isRectangular() ) )
2017-07-21 18:21:34 +02:00
{
setClipRect( QRectF() );
setIsRectangular( true );
2017-07-21 18:21:34 +02:00
setGeometry( nullptr );
m_otherGeometry = nullptr;
2017-07-21 18:21:34 +02:00
markDirty( QSGNode::DirtyGeometry );
2017-07-21 18:21:34 +02:00
}
return;
2017-07-21 18:21:34 +02:00
}
bool isDirty = false;
const auto newClipRect = other->clipRect().translated( offset );
if ( clipRect() != newClipRect )
2017-07-21 18:21:34 +02:00
{
setClipRect( newClipRect );
isDirty = true;
}
2017-07-21 18:21:34 +02:00
if ( other->isRectangular() )
{
if ( !isRectangular() )
2017-07-21 18:21:34 +02:00
{
setIsRectangular( true );
setGeometry( nullptr );
m_otherGeometry = nullptr;
2017-07-21 18:21:34 +02:00
isDirty = true;
}
}
else
{
if ( isRectangular() )
2017-07-21 18:21:34 +02:00
{
setIsRectangular( false );
2017-07-21 18:21:34 +02:00
isDirty = true;
}
if ( ( geometry() == nullptr )
|| ( geometry()->vertexCount() != other->geometry()->vertexCount() )
|| ( other->geometry() != m_otherGeometry ) )
2017-07-21 18:21:34 +02:00
{
setGeometry( QskBoxClipNode::translatedGeometry(
other->geometry(), offset ) );
m_otherGeometry = other->geometry();
isDirty = true;
2017-07-21 18:21:34 +02:00
}
}
if ( isDirty )
markDirty( QSGNode::DirtyGeometry );
}
virtual void update() override final
{
/*
The Qt-Quick framework is limited to setting clipNodes from
the bounding rectangle. As we need a different clipping
we turn any updates of the clip done by QQuickWindow
into nops.
*/
}
const QSGGeometry* m_otherGeometry;
2017-07-21 18:21:34 +02:00
};
}
class QskScrollAreaClipItem final : public QskControl, public QQuickItemChangeListener
{
// when inheriting from QskControl we participate in node cleanups
using Inherited = QskControl;
public:
QskScrollAreaClipItem( QskScrollArea* );
virtual ~QskScrollAreaClipItem();
void enableGeometryListener( bool on );
QQuickItem* scrolledItem() const
{
auto children = childItems();
return children.isEmpty() ? nullptr : children.first();
}
protected:
virtual bool event( QEvent* event ) override final;
virtual void itemChange( ItemChange, const ItemChangeData& ) override final;
virtual void geometryChanged( const QRectF&, const QRectF& ) override final;
#if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)
virtual void itemGeometryChanged( QQuickItem*,
QQuickGeometryChange change, const QRectF& ) override final
{
if ( change.sizeChange() )
scrollArea()->polish();
}
#else
virtual void itemGeometryChanged( QQuickItem*,
2017-07-21 18:21:34 +02:00
const QRectF& newRect, const QRectF& oldRect ) override final
{
if ( oldRect.size() != newRect.size() )
scrollArea()->polish();
}
#endif
virtual void updateNode( QSGNode* ) override final;
private:
inline QskScrollArea* scrollArea()
{
return static_cast< QskScrollArea* >( parentItem() );
}
inline const QskScrollArea* scrollArea() const
{
return static_cast< const QskScrollArea* >( parentItem() );
}
const QSGClipNode* viewPortClipNode() const;
};
QskScrollAreaClipItem::QskScrollAreaClipItem( QskScrollArea* scrollArea ):
Inherited( scrollArea )
{
setObjectName( QStringLiteral( "QskScrollAreaClipItem" ) );
setClip( true );
}
QskScrollAreaClipItem::~QskScrollAreaClipItem()
{
enableGeometryListener( false );
}
void QskScrollAreaClipItem::updateNode( QSGNode* )
{
auto* d = QQuickItemPrivate::get( this );
if ( QQuickItemPrivate::get( scrollArea() )->dirtyAttributes
& QQuickItemPrivate::ContentUpdateMask )
{
/*
The update order depends on who calls update first and we
have to handle being called before a new clip node has
been created by the scrollview.
But better invalidate the unguarded clip geometry until then ...
*/
auto clipNode = d->clipNode();
if ( clipNode && !clipNode->isRectangular() )
{
clipNode->setIsRectangular( true );
clipNode->setGeometry( nullptr );
}
// in the next cycle we will find a valid clip
update();
return;
}
auto clipNode = d->clipNode();
if ( clipNode && !( clipNode->flags() & QSGNode::OwnsMaterial ) )
{
// Replace the clip node being inserted from QQuickWindow
2017-07-21 18:21:34 +02:00
auto parentNode = clipNode->parent();
auto node = new ViewportClipNode();
parentNode->appendChildNode( node );
clipNode->reparentChildNodesTo( node );
parentNode->removeChildNode( clipNode );
if ( clipNode->flags() & QSGNode::OwnedByParent )
delete clipNode;
d->extra->clipNode = clipNode = node;
Q_ASSERT( clipNode == QQuickItemPrivate::get( this )->clipNode() );
}
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.
*/
2017-07-21 18:21:34 +02:00
auto viewClipNode = static_cast< ViewportClipNode* >( clipNode );
viewClipNode->copyFrom( viewPortClipNode(), -position() );
Q_ASSERT( viewClipNode->isRectangular() || viewClipNode->geometry() );
2017-07-21 18:21:34 +02:00
}
}
const QSGClipNode* QskScrollAreaClipItem::viewPortClipNode() const
{
auto node = const_cast< QSGNode* >( QskControl::paintNode( scrollArea() ) );
if ( node )
node = QskSkinlet::findNodeByRole( node, QskScrollViewSkinlet::ContentsRootRole );
if ( node && node->type() == QSGNode::ClipNodeType )
return static_cast< QSGClipNode* >( node );
return nullptr;
}
void QskScrollAreaClipItem::geometryChanged(
const QRectF& newRect, const QRectF& oldRect )
{
Inherited::geometryChanged( newRect, oldRect );
if ( newRect.size() != oldRect.size() )
{
// we need to restore the clip node
update();
}
}
void QskScrollAreaClipItem::itemChange(
QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& value )
{
if ( change == QQuickItem::ItemChildAddedChange )
2017-07-21 18:21:34 +02:00
{
enableGeometryListener( true );
2017-07-21 18:21:34 +02:00
}
else if ( change == QQuickItem::ItemChildRemovedChange )
{
enableGeometryListener( false );
}
2017-07-21 18:21:34 +02:00
Inherited::itemChange( change, value );
}
void QskScrollAreaClipItem::enableGeometryListener( bool on )
{
auto item = scrolledItem();
if ( item )
{
// we might also be interested in ImplicitWidth/ImplicitHeight
const QQuickItemPrivate::ChangeTypes types = QQuickItemPrivate::Geometry;
QQuickItemPrivate* p = QQuickItemPrivate::get( item );
if ( on )
p->addItemChangeListener( this, types );
else
p->removeItemChangeListener( this, types );
}
}
bool QskScrollAreaClipItem::event( QEvent* event )
{
if( event->type() == QEvent::LayoutRequest )
{
if ( scrollArea()->isItemResizable() )
scrollArea()->polish();
}
return Inherited::event( event );
}
class QskScrollArea::PrivateData
{
public:
PrivateData():
isItemResizable( true )
{
}
void enableAutoTranslation( QskScrollArea* scrollArea, bool on )
{
if ( on )
{
QObject::connect( scrollArea, &QskScrollView::scrollPosChanged,
scrollArea, &QskScrollArea::translateItem );
}
else
{
QObject::disconnect( scrollArea, &QskScrollView::scrollPosChanged,
scrollArea, &QskScrollArea::translateItem );
}
}
QskScrollAreaClipItem* clipItem;
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 ...
*/
2017-07-21 18:21:34 +02:00
QskScrollArea::QskScrollArea( QQuickItem* parentItem ):
Inherited( parentItem ),
m_data( new PrivateData() )
{
setPolishOnResize( true );
m_data->clipItem = new QskScrollAreaClipItem( this );
m_data->enableAutoTranslation( this, true );
}
QskScrollArea::~QskScrollArea()
{
delete m_data->clipItem;
2017-07-21 18:21:34 +02:00
}
void QskScrollArea::updateLayout()
{
Inherited::updateLayout();
m_data->clipItem->setGeometry( viewContentsRect() );
2017-07-21 18:21:34 +02:00
adjustItem();
}
void QskScrollArea::adjustItem()
{
QQuickItem* item = m_data->clipItem->scrolledItem();
2017-07-21 18:21:34 +02:00
if ( item == nullptr )
{
setScrollableSize( QSizeF() );
setScrollPos( QPointF() );
}
else
{
if ( m_data->isItemResizable )
{
const QRectF rect = viewContentsRect();
#if 0
/*
For optional scrollbars the available space also depends
on wether the adjustedSize results in scroll bars. For the
moment we ignore this and start with a simplified code.
*/
#endif
auto newSize = qskAdjustedSize( item, rect.size() );
item->setSize( newSize );
}
m_data->enableAutoTranslation( this, false );
setScrollableSize( QSizeF( item->width(), item->height() ) );
setScrollPos( scrollPos() );
m_data->enableAutoTranslation( this, true );
translateItem();
}
}
void QskScrollArea::setItemResizable( bool on )
{
if ( on != m_data->isItemResizable )
{
m_data->isItemResizable = on;
Q_EMIT itemResizableChanged();
if ( m_data->isItemResizable )
polish();
}
}
bool QskScrollArea::isItemResizable() const
{
return m_data->isItemResizable;
}
void QskScrollArea::setScrolledItem( QQuickItem* item )
{
auto oldItem = m_data->clipItem->scrolledItem();
if ( item == oldItem )
return;
if ( oldItem )
{
if ( oldItem->parent() == this )
delete oldItem;
else
oldItem->setParentItem( nullptr );
}
if ( item )
{
item->setParentItem( m_data->clipItem );
if ( item->parent() == nullptr )
item->setParent( m_data->clipItem );
}
polish();
Q_EMIT scrolledItemChanged();
}
QQuickItem* QskScrollArea::scrolledItem() const
{
return m_data->clipItem->scrolledItem();
}
void QskScrollArea::translateItem()
{
auto item = scrolledItem();
2017-07-21 18:21:34 +02:00
if ( item )
item->setPosition( -scrollPos() );
2017-07-21 18:21:34 +02:00
}
#include "moc_QskScrollArea.cpp"