qskinny/src/controls/QskScrollArea.cpp

551 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"
2018-08-03 08:15:28 +02:00
#include "QskEvent.h"
2018-05-01 12:41:20 +02:00
#include "QskQuick.h"
2017-07-21 18:21:34 +02:00
#include "QskScrollViewSkinlet.h"
#include "QskBoxBorderMetrics.h"
2017-07-21 18:21:34 +02:00
QSK_QT_PRIVATE_BEGIN
#include <private/qquickclipnode_p.h>
2018-08-03 08:15:28 +02:00
#include <private/qquickitem_p.h>
2017-07-21 18:21:34 +02:00
#include <private/qquickitemchangelistener_p.h>
2018-08-03 08:15:28 +02:00
#include <private/qquickwindow_p.h>
2017-07-21 18:21:34 +02:00
QSK_QT_PRIVATE_END
static inline bool qskNeedsScrollBars(
qreal available, qreal required, Qt::ScrollBarPolicy policy )
{
if ( policy == Qt::ScrollBarAsNeeded )
return required > available;
else
return policy == Qt::ScrollBarAlwaysOn;
}
static inline QSizeF qskPanelInnerSize( const QskScrollView* scrollView )
{
auto size = scrollView->subControlRect( QskScrollView::Panel ).size();
const auto borderMetrics = scrollView->boxBorderMetricsHint( QskScrollView::Viewport );
const qreal bw = 2 * borderMetrics.widthAt( Qt::TopEdge );
size.setWidth( qMax( size.width() - bw, 0.0 ) );
size.setHeight( qMax( size.height() - bw, 0.0 ) );
return size;
}
static inline QSizeF qskScrolledItemSize( const QskScrollView* scrollView,
const QQuickItem* item, const QSizeF& boundingSize )
{
QSizeF outerSize = boundingSize;
const qreal spacing = scrollView->metric( QskScrollView::Panel | QskAspect::Spacing );
const auto sbV = scrollView->metric( QskScrollView::VerticalScrollBar | QskAspect::Size );
const auto sbH = scrollView->metric( QskScrollView::HorizontalScrollBar | QskAspect::Size );
const auto policyH = scrollView->horizontalScrollBarPolicy();
const auto policyV = scrollView->verticalScrollBarPolicy();
auto itemSize = qskConstrainedItemSize( item, outerSize );
bool needScrollBarV = qskNeedsScrollBars( outerSize.height(), itemSize.height(), policyV );
bool needScrollBarH = qskNeedsScrollBars( outerSize.width(), itemSize.width(), policyH );
bool hasScrollBarV = needScrollBarV;
// Vertical/Horizonal scroll bars might depend on each other
if ( needScrollBarV )
{
outerSize.rwidth() -= sbV + spacing;
itemSize = qskConstrainedItemSize( item, outerSize );
if ( !needScrollBarH )
{
needScrollBarH = qskNeedsScrollBars(
outerSize.width(), itemSize.width(), policyH );
}
}
if ( needScrollBarH )
{
outerSize.rheight() -= sbH + spacing;
itemSize = qskConstrainedItemSize( item, outerSize );
if ( !hasScrollBarV )
{
needScrollBarV = qskNeedsScrollBars(
outerSize.height(), itemSize.height(), policyV );
}
}
if ( needScrollBarV )
{
outerSize.rwidth() -= sbV + spacing;
itemSize = qskConstrainedItemSize( item, outerSize );
}
return itemSize;
}
2017-07-21 18:21:34 +02:00
namespace
{
class ViewportClipNode final : public QQuickDefaultClipNode
{
2018-08-03 08:15:28 +02:00
public:
ViewportClipNode()
: QQuickDefaultClipNode( QRectF() )
2017-07-21 18:21:34 +02:00
{
setGeometry( nullptr );
// 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 )
2017-07-21 18:21:34 +02:00
{
if ( other == nullptr )
{
if ( !( isRectangular() && clipRect().isEmpty() ) )
2017-07-21 18:21:34 +02:00
{
setIsRectangular( true );
setClipRect( QRectF() );
2017-07-21 18:21:34 +02:00
setGeometry( nullptr );
markDirty( QSGNode::DirtyGeometry );
2017-07-21 18:21:34 +02:00
}
return;
2017-07-21 18:21:34 +02:00
}
bool isDirty = false;
if ( clipRect() != other->clipRect() )
2017-07-21 18:21:34 +02:00
{
setClipRect( other->clipRect() );
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 );
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() != other->geometry() )
2017-07-21 18:21:34 +02:00
{
// both nodes share the same geometry
setGeometry( const_cast< QSGGeometry* >( other->geometry() ) );
isDirty = true;
2017-07-21 18:21:34 +02:00
}
}
if ( isDirty )
markDirty( QSGNode::DirtyGeometry );
}
2018-07-31 17:32:25 +02:00
void update() override
2017-07-21 18:21:34 +02:00
{
/*
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.
*/
}
};
}
2020-10-25 17:40:29 +01:00
namespace
2017-07-21 18:21:34 +02:00
{
2020-10-25 17:40:29 +01:00
class ClipItem final : public QskControl, public QQuickItemChangeListener
2017-07-21 18:21:34 +02:00
{
2020-10-25 17:40:29 +01:00
// when inheriting from QskControl we participate in node cleanups
using Inherited = QskControl;
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
public:
ClipItem( QskScrollArea* );
virtual ~ClipItem();
2020-10-25 17:40:29 +01:00
void enableGeometryListener( bool on );
2020-10-25 17:40:29 +01:00
QQuickItem* scrolledItem() const
{
auto children = childItems();
return children.isEmpty() ? nullptr : children.first();
}
2020-10-25 17:40:29 +01:00
bool contains( const QPointF& pos ) const override
{
return clipRect().contains( pos );
}
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
QRectF clipRect() const override
{
return scrollArea()->subControlRect( QskScrollView::Viewport );
}
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
inline void setItemSizeChangedEnabled( bool on )
{
m_isSizeChangedEnabled = on;
}
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
protected:
bool event( QEvent* event ) override;
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
void itemChange( ItemChange, const ItemChangeData& ) override;
2017-07-21 18:21:34 +02:00
2020-10-25 17:53:22 +01:00
#if QT_VERSION >= QT_VERSION_CHECK( 5, 8, 0 )
2020-10-25 17:40:29 +01:00
void itemGeometryChanged( QQuickItem*,
QQuickGeometryChange change, const QRectF& ) override
{
if ( m_isSizeChangedEnabled && change.sizeChange() )
scrollArea()->polish();
}
2017-07-21 18:21:34 +02:00
2020-10-25 17:53:22 +01:00
#else
2020-10-25 17:40:29 +01:00
void itemGeometryChanged( QQuickItem*,
const QRectF& newRect, const QRectF& oldRect ) override
{
if ( m_isSizeChangedEnabled && ( oldRect.size() != newRect.size() ) )
scrollArea()->polish();
}
2020-10-25 17:53:22 +01:00
#endif
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
void updateNode( QSGNode* ) override;
2020-10-25 17:40:29 +01:00
private:
inline QskScrollArea* scrollArea()
{
return static_cast< QskScrollArea* >( parentItem() );
}
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
inline const QskScrollArea* scrollArea() const
{
return static_cast< const QskScrollArea* >( parentItem() );
}
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
const QSGClipNode* viewPortClipNode() const;
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
bool m_isSizeChangedEnabled = true;
};
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
ClipItem::ClipItem( QskScrollArea* scrollArea )
: Inherited( scrollArea )
2017-07-21 18:21:34 +02:00
{
2020-10-25 17:40:29 +01:00
setObjectName( QStringLiteral( "QskScrollAreaClipItem" ) );
setClip( true );
2017-07-21 18:21:34 +02:00
}
2020-10-25 17:40:29 +01:00
ClipItem::~ClipItem()
{
enableGeometryListener( false );
}
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
void ClipItem::updateNode( QSGNode* )
2017-07-21 18:21:34 +02:00
{
2020-10-25 17:40:29 +01:00
auto* d = QQuickItemPrivate::get( this );
2020-10-25 17:40:29 +01:00
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 );
}
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
// in the next cycle we will find a valid clip
update();
return;
}
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
auto clipNode = d->clipNode();
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
if ( clipNode && !( clipNode->flags() & QSGNode::OwnsMaterial ) )
{
// Replace the clip node being inserted from QQuickWindow
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
auto parentNode = clipNode->parent();
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
auto node = new ViewportClipNode();
parentNode->appendChildNode( node );
clipNode->reparentChildNodesTo( node );
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
parentNode->removeChildNode( clipNode );
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
if ( clipNode->flags() & QSGNode::OwnedByParent )
delete clipNode;
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
d->extra->clipNode = clipNode = node;
Q_ASSERT( clipNode == QQuickItemPrivate::get( this )->clipNode() );
}
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
if ( clipNode )
{
/*
Update the clip node with the geometry of the clip node
of the viewport of the scrollview.
*/
auto viewClipNode = static_cast< ViewportClipNode* >( clipNode );
viewClipNode->copyFrom( viewPortClipNode() );
}
}
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
const QSGClipNode* ClipItem::viewPortClipNode() const
2017-07-21 18:21:34 +02:00
{
2020-10-25 17:40:29 +01:00
auto node = const_cast< QSGNode* >( qskPaintNode( scrollArea() ) );
if ( node )
node = QskSkinlet::findNodeByRole( node, QskScrollViewSkinlet::ContentsRootRole );
if ( node && node->type() == QSGNode::ClipNodeType )
return static_cast< QSGClipNode* >( node );
return nullptr;
2017-07-21 18:21:34 +02:00
}
2020-10-25 17:40:29 +01:00
void ClipItem::itemChange(
QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& value )
{
2020-10-25 17:40:29 +01:00
if ( change == QQuickItem::ItemChildAddedChange )
{
enableGeometryListener( true );
}
else if ( change == QQuickItem::ItemChildRemovedChange )
{
enableGeometryListener( false );
}
2018-01-16 12:13:38 +01:00
2020-10-25 17:40:29 +01:00
Inherited::itemChange( change, value );
}
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
void ClipItem::enableGeometryListener( bool on )
2017-07-21 18:21:34 +02:00
{
2020-10-25 17:40:29 +01:00
auto item = scrolledItem();
if ( item )
{
// we might also be interested in ImplicitWidth/ImplicitHeight
const QQuickItemPrivate::ChangeTypes types = QQuickItemPrivate::Geometry;
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
QQuickItemPrivate* p = QQuickItemPrivate::get( item );
if ( on )
p->addItemChangeListener( this, types );
else
p->removeItemChangeListener( this, types );
}
2017-07-21 18:21:34 +02:00
}
2020-10-25 17:40:29 +01:00
bool ClipItem::event( QEvent* event )
2017-07-21 18:21:34 +02:00
{
2020-10-25 17:53:22 +01:00
const int eventType = event->type();
if ( eventType == QEvent::LayoutRequest )
2020-10-25 17:40:29 +01:00
{
if ( scrollArea()->isItemResizable() )
scrollArea()->polish();
}
2020-10-25 17:53:22 +01:00
else if ( eventType == QskEvent::GeometryChange )
{
auto geometryEvent = static_cast< const QskGeometryChangeEvent* >( event );
if ( geometryEvent->isResized() )
{
// we need to restore the clip node
update();
}
}
2017-07-21 18:21:34 +02:00
2020-10-25 17:40:29 +01:00
return Inherited::event( event );
}
2017-07-21 18:21:34 +02:00
}
class QskScrollArea::PrivateData
{
2018-08-03 08:15:28 +02:00
public:
PrivateData()
: isItemResizable( true )
2017-07-21 18:21:34 +02:00
{
}
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 );
}
}
2020-10-25 17:40:29 +01:00
ClipItem* clipItem = nullptr;
2018-01-16 12:13:38 +01:00
2017-07-21 18:21:34 +02:00
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 a fundamental limitation
of the Qt/Quick design: node subtrees for the children have to be in parallel to
the paint node.
We work around this problem, by inserting an extra item between the scroll area
and the scrollable item. This item replaces its default clip node by its own node,
that references the geometry of the viewport clip node.
*/
2018-08-03 08:15:28 +02:00
QskScrollArea::QskScrollArea( QQuickItem* parentItem )
: Inherited( parentItem )
, m_data( new PrivateData() )
2017-07-21 18:21:34 +02:00
{
setPolishOnResize( true );
2020-10-25 17:40:29 +01:00
m_data->clipItem = new ClipItem( this );
2017-07-21 18:21:34 +02:00
m_data->enableAutoTranslation( this, true );
}
QskScrollArea::~QskScrollArea()
{
delete m_data->clipItem;
2017-07-21 18:21:34 +02:00
}
void QskScrollArea::updateLayout()
{
Inherited::updateLayout();
// the clipItem always has the same geometry as the scroll area
m_data->clipItem->setSize( size() );
2017-07-21 18:21:34 +02:00
adjustItem();
}
void QskScrollArea::adjustItem()
{
auto item = m_data->clipItem->scrolledItem();
2017-07-21 18:21:34 +02:00
if ( item == nullptr )
{
setScrollableSize( QSizeF() );
setScrollPos( QPointF() );
return;
2017-07-21 18:21:34 +02:00
}
if ( m_data->isItemResizable )
2017-07-21 18:21:34 +02:00
{
QSizeF itemSize;
const auto viewSize = qskPanelInnerSize( this );
if ( !viewSize.isEmpty() )
2017-07-21 18:21:34 +02:00
{
// we have to anticipate the scrollbars
itemSize = qskScrolledItemSize( this, item, viewSize );
}
2020-06-09 06:56:20 +02:00
if ( itemSize.isEmpty() )
itemSize = QSizeF( 0.0, 0.0 );
2017-07-21 18:21:34 +02:00
m_data->clipItem->setItemSizeChangedEnabled( false );
item->setSize( itemSize );
m_data->clipItem->setItemSizeChangedEnabled( true );
}
2017-07-21 18:21:34 +02:00
m_data->enableAutoTranslation( this, false );
2017-07-21 18:21:34 +02:00
setScrollableSize( QSizeF( item->width(), item->height() ) );
setScrollPos( scrollPos() );
2017-07-21 18:21:34 +02:00
m_data->enableAutoTranslation( this, true );
translateItem();
2017-07-21 18:21:34 +02:00
}
void QskScrollArea::setItemResizable( bool on )
{
if ( on != m_data->isItemResizable )
{
m_data->isItemResizable = on;
2020-03-13 07:39:31 +01:00
Q_EMIT itemResizableChanged( on );
2017-07-21 18:21:34 +02:00
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()
{
2020-03-13 07:39:31 +01:00
if ( auto item = m_data->clipItem->scrolledItem() )
{
const QPointF pos = viewContentsRect().topLeft() - scrollPos();
item->setPosition( pos );
}
2017-07-21 18:21:34 +02:00
}
#include "moc_QskScrollArea.cpp"