QSkinny 0.8.0
C++/Qt UI toolkit
All Classes Namespaces Functions Variables Enumerations Enumerator Properties Pages
QskScrollArea.cpp
1/******************************************************************************
2 * QSkinny - Copyright (C) The authors
3 * SPDX-License-Identifier: BSD-3-Clause
4 *****************************************************************************/
5
6#include "QskScrollArea.h"
7#include "QskEvent.h"
8#include "QskQuick.h"
9#include "QskScrollViewSkinlet.h"
10#include "QskBoxBorderMetrics.h"
11#include "QskSGNode.h"
12#include "QskInternalMacros.h"
13
14QSK_QT_PRIVATE_BEGIN
15#include <private/qquickitem_p.h>
16#include <private/qquickitemchangelistener_p.h>
17QSK_QT_PRIVATE_END
18
19static inline bool qskNeedsScrollBars(
20 qreal available, qreal required, Qt::ScrollBarPolicy policy )
21{
22 if ( policy == Qt::ScrollBarAsNeeded )
23 return required > available;
24 else
25 return policy == Qt::ScrollBarAlwaysOn;
26}
27
28static inline QSizeF qskPanelInnerSize( const QskScrollView* scrollView )
29{
30 auto size = scrollView->subControlRect( QskScrollView::Panel ).size();
31
32 const auto borderMetrics = scrollView->boxBorderMetricsHint( QskScrollView::Viewport );
33 const qreal bw = 2 * borderMetrics.widthAt( Qt::TopEdge );
34
35 size.setWidth( qMax( size.width() - bw, 0.0 ) );
36 size.setHeight( qMax( size.height() - bw, 0.0 ) );
37
38 return size;
39}
40
41static inline QSizeF qskScrolledItemSize( const QskScrollView* scrollView,
42 const QQuickItem* item, const QSizeF& boundingSize )
43{
44 using Q = QskScrollView;
45
46 QSizeF outerSize = boundingSize;
47
48 const qreal spacing = scrollView->spacingHint( Q::Panel );
49
50 const auto sbV = scrollView->metric( Q::VerticalScrollBar | QskAspect::Size );
51 const auto sbH = scrollView->metric( Q::HorizontalScrollBar | QskAspect::Size );
52
53 const auto policyH = scrollView->horizontalScrollBarPolicy();
54 const auto policyV = scrollView->verticalScrollBarPolicy();
55
56 auto itemSize = qskConstrainedItemSize( item, outerSize );
57
58 bool needScrollBarV = qskNeedsScrollBars( outerSize.height(), itemSize.height(), policyV );
59 bool needScrollBarH = qskNeedsScrollBars( outerSize.width(), itemSize.width(), policyH );
60
61 bool hasScrollBarV = needScrollBarV;
62
63 // Vertical/Horizonal scroll bars might depend on each other
64
65 if ( needScrollBarV )
66 {
67 outerSize.rwidth() -= sbV + spacing;
68 itemSize = qskConstrainedItemSize( item, outerSize );
69
70 if ( !needScrollBarH )
71 {
72 needScrollBarH = qskNeedsScrollBars(
73 outerSize.width(), itemSize.width(), policyH );
74 }
75 }
76
77 if ( needScrollBarH )
78 {
79 outerSize.rheight() -= sbH + spacing;
80 itemSize = qskConstrainedItemSize( item, outerSize );
81
82 if ( !hasScrollBarV )
83 {
84 needScrollBarV = qskNeedsScrollBars(
85 outerSize.height(), itemSize.height(), policyV );
86 }
87 }
88
89 if ( needScrollBarV )
90 {
91 outerSize.rwidth() -= sbV + spacing;
92 itemSize = qskConstrainedItemSize( item, outerSize );
93 }
94
95 return itemSize;
96}
97
98namespace
99{
100 class ViewportClipNode final : public QQuickDefaultClipNode
101 {
102 public:
103 ViewportClipNode()
104 : QQuickDefaultClipNode( QRectF() )
105 {
106 setGeometry( nullptr );
107
108 // clip nodes have no material, so this flag
109 // is available to indicate our replaced clip node
110
111 setFlag( QSGNode::OwnsMaterial, true );
112 }
113
114 void copyFrom( const QSGClipNode* other )
115 {
116 if ( other == nullptr )
117 {
118 if ( !( isRectangular() && clipRect().isEmpty() ) )
119 {
120 setIsRectangular( true );
121 setClipRect( QRectF() );
122 setGeometry( nullptr );
123
124 markDirty( QSGNode::DirtyGeometry );
125 }
126
127 return;
128 }
129
130 bool isDirty = false;
131
132 if ( clipRect() != other->clipRect() )
133 {
134 setClipRect( other->clipRect() );
135 isDirty = true;
136 }
137
138 if ( other->isRectangular() )
139 {
140 if ( !isRectangular() )
141 {
142 setIsRectangular( true );
143 setGeometry( nullptr );
144
145 isDirty = true;
146 }
147 }
148 else
149 {
150 if ( isRectangular() )
151 {
152 setIsRectangular( false );
153 isDirty = true;
154 }
155
156 if ( geometry() != other->geometry() )
157 {
158 // both nodes share the same geometry
159 setGeometry( const_cast< QSGGeometry* >( other->geometry() ) );
160 isDirty = true;
161 }
162 }
163
164 if ( isDirty )
165 markDirty( QSGNode::DirtyGeometry );
166 }
167
168 void update() override
169 {
170 /*
171 The Qt-Quick framework is limited to setting clipNodes from
172 the bounding rectangle. As we need a different clipping
173 we turn any updates of the clip done by QQuickWindow
174 into nops.
175 */
176 }
177 };
178}
179
180namespace
181{
182 /*
183 When doing scene graph composition it is easy to insert a clip node
184 somewhere below the paint node to have all items on the viewport being clipped.
185 This is how it is done f.e. for the list boxes.
186
187 But when having QQuickItems on the viewport we run into a fundamental limitation
188 of the Qt/Quick design: node subtrees for the children have to be in parallel to
189 the paint node.
190
191 We work around this problem, by inserting an extra item between the scroll area
192 and the scrollable item. This item replaces its default clip node by its own node,
193 that references the geometry of the viewport clip node.
194 */
195
196 class ClipItem final : public QskControl, public QQuickItemChangeListener
197 {
198 // when inheriting from QskControl we participate in node cleanups
199 using Inherited = QskControl;
200
201 public:
202 ClipItem( QskScrollArea* );
203 ~ClipItem() override;
204
205 void enableGeometryListener( bool on );
206
207 QQuickItem* scrolledItem() const
208 {
209 auto children = childItems();
210 return children.isEmpty() ? nullptr : children.first();
211 }
212
213 bool contains( const QPointF& pos ) const override
214 {
215 return clipRect().contains( pos );
216 }
217
218 QRectF clipRect() const override
219 {
220 return scrollArea()->subControlRect( QskScrollView::Viewport );
221 }
222
223 inline void setItemSizeChangedEnabled( bool on )
224 {
225 m_isSizeChangedEnabled = on;
226 }
227
228 QRectF focusIndicatorClipRect() const override
229 {
230 if( scrollArea()->hasItemFocusClipping() )
231 {
233 }
234
235 return QRectF();
236 }
237
238 protected:
239 bool event( QEvent* event ) override;
240
241 void itemChange( ItemChange, const ItemChangeData& ) override;
242
243 void itemGeometryChanged( QQuickItem*,
244 QQuickGeometryChange change, const QRectF& ) override
245 {
246 if ( change.sizeChange() )
247 scrolledItemGeometryChange();
248
249 viewportChanged();
250 }
251
252 void updateNode( QSGNode* ) override;
253
254 private:
255 inline QskScrollArea* scrollArea()
256 {
257 return static_cast< QskScrollArea* >( parentItem() );
258 }
259
260 inline const QskScrollArea* scrollArea() const
261 {
262 return static_cast< const QskScrollArea* >( parentItem() );
263 }
264
265 void maybeUpdate()
266 {
267 if ( auto node = QQuickItemPrivate::get( this )->clipNode() )
268 {
269 if ( clipRect() != node->clipRect() )
270 update();
271 }
272 }
273
274 inline void scrolledItemGeometryChange()
275 {
276 if ( m_isSizeChangedEnabled )
277 {
278 auto area = scrollArea();
279
280 area->polish();
281
282 if ( !area->isItemResizable() )
283 {
284 // in this mode the size hint depends on it
285 area->resetImplicitSize();
286 }
287 }
288 }
289
290 const QSGClipNode* viewPortClipNode() const;
291
292 void viewportChanged()
293 {
294#if QT_VERSION < QT_VERSION_CHECK( 6, 3, 0 )
295 if ( auto item = scrollArea()->scrolledItem() )
296 {
297 QskViewportChangeEvent ev( this );
298 QCoreApplication::sendEvent( item, &ev );
299 }
300#endif
301 }
302
303 bool m_isSizeChangedEnabled = true;
304 };
305
306 ClipItem::ClipItem( QskScrollArea* scrollArea )
307 : Inherited( scrollArea )
308 {
309 setObjectName( QStringLiteral( "QskScrollAreaClipItem" ) );
310 setClip( true );
311
312 // scrollbars might (dis)appear
313 connect( scrollArea, &QskScrollBox::scrollableSizeChanged,
314 this, &ClipItem::maybeUpdate );
315 }
316
317 ClipItem::~ClipItem()
318 {
319 enableGeometryListener( false );
320 }
321
322 void ClipItem::updateNode( QSGNode* )
323 {
324 auto d = QQuickItemPrivate::get( this );
325
326 if ( QQuickItemPrivate::get( scrollArea() )->dirtyAttributes &
327 QQuickItemPrivate::ContentUpdateMask )
328 {
329 /*
330 The update order depends on who calls update first and we
331 have to handle being called before a new clip node has
332 been created by the scrollview.
333 But better invalidate the unguarded clip geometry until then ...
334 */
335 auto clipNode = d->clipNode();
336 if ( clipNode && !clipNode->isRectangular() )
337 {
338 clipNode->setIsRectangular( true );
339 clipNode->setGeometry( nullptr );
340 }
341
342 // in the next cycle we will find a valid clip
343 update();
344 return;
345 }
346
347 auto clipNode = d->clipNode();
348
349 if ( clipNode && !( clipNode->flags() & QSGNode::OwnsMaterial ) )
350 {
351 // Replace the clip node being inserted from QQuickWindow
352
353 auto parentNode = clipNode->parent();
354
355 auto node = new ViewportClipNode();
356 parentNode->appendChildNode( node );
357 clipNode->reparentChildNodesTo( node );
358
359 parentNode->removeChildNode( clipNode );
360
361 if ( clipNode->flags() & QSGNode::OwnedByParent )
362 delete clipNode;
363
364 d->extra->clipNode = clipNode = node;
365 Q_ASSERT( clipNode == QQuickItemPrivate::get( this )->clipNode() );
366 }
367
368 if ( clipNode )
369 {
370 /*
371 Update the clip node with the geometry of the clip node
372 of the viewport of the scrollview.
373 */
374 auto viewClipNode = static_cast< ViewportClipNode* >( clipNode );
375 viewClipNode->copyFrom( viewPortClipNode() );
376 }
377 }
378
379 const QSGClipNode* ClipItem::viewPortClipNode() const
380 {
381 auto node = const_cast< QSGNode* >( qskPaintNode( scrollArea() ) );
382 if ( node )
383 node = QskSGNode::findChildNode( node, QskScrollViewSkinlet::ContentsRootRole );
384
385 if ( node && node->type() == QSGNode::ClipNodeType )
386 return static_cast< QSGClipNode* >( node );
387
388 return nullptr;
389 }
390
391 void ClipItem::itemChange(
392 QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& value )
393 {
394 if ( change == QQuickItem::ItemChildAddedChange )
395 {
396 enableGeometryListener( true );
397 }
398 else if ( change == QQuickItem::ItemChildRemovedChange )
399 {
400 enableGeometryListener( false );
401 }
402
403 Inherited::itemChange( change, value );
404 }
405
406 void ClipItem::enableGeometryListener( bool on )
407 {
408 auto item = scrolledItem();
409 if ( item )
410 {
411 // we might also be interested in ImplicitWidth/ImplicitHeight
412 const QQuickItemPrivate::ChangeTypes types = QQuickItemPrivate::Geometry;
413
414 QQuickItemPrivate* p = QQuickItemPrivate::get( item );
415 if ( on )
416 p->addItemChangeListener( this, types );
417 else
418 p->removeItemChangeListener( this, types );
419 }
420 }
421
422 bool ClipItem::event( QEvent* event )
423 {
424 const int eventType = event->type();
425
426 if ( eventType == QEvent::LayoutRequest )
427 {
428 if ( scrollArea()->isItemResizable() )
429 scrollArea()->polish();
430 }
431 else if ( eventType == QskEvent::GeometryChange )
432 {
433 auto geometryEvent = static_cast< const QskGeometryChangeEvent* >( event );
434 if ( geometryEvent->isResized() )
435 {
436 // we need to restore the clip node
437 update();
438 viewportChanged();
439 }
440 }
441
442 return Inherited::event( event );
443 }
444}
445
446class QskScrollArea::PrivateData
447{
448 public:
449 PrivateData()
450 : isItemResizable( true )
451 , isItemFocusClipping( true )
452 {
453 }
454
455 void enableAutoTranslation( QskScrollArea* scrollArea, bool on )
456 {
457 if ( on )
458 {
459 QObject::connect( scrollArea, &QskScrollView::scrollPosChanged,
460 scrollArea, &QskScrollArea::translateItem );
461 }
462 else
463 {
464 QObject::disconnect( scrollArea, &QskScrollView::scrollPosChanged,
465 scrollArea, &QskScrollArea::translateItem );
466 }
467 }
468
469 ClipItem* clipItem = nullptr;
470
471 bool isItemResizable : 1;
472 bool isItemFocusClipping : 1;
473};
474
475
476QskScrollArea::QskScrollArea( QQuickItem* parentItem )
477 : Inherited( parentItem )
478 , m_data( new PrivateData() )
479{
480 setPolishOnResize( true );
481
482 m_data->clipItem = new ClipItem( this );
483 m_data->enableAutoTranslation( this, true );
484
485 initSizePolicy( QskSizePolicy::Ignored, QskSizePolicy::Ignored );
486}
487
488QskScrollArea::~QskScrollArea()
489{
490 delete m_data->clipItem;
491}
492
493void QskScrollArea::updateLayout()
494{
495 Inherited::updateLayout();
496
497 // the clipItem always has the same geometry as the scroll area
498 m_data->clipItem->setSize( size() );
499 adjustItem();
500}
501
502QSizeF QskScrollArea::layoutSizeHint( Qt::SizeHint which, const QSizeF& constraint ) const
503{
504 if ( which == Qt::PreferredSize )
505 {
506 if ( const auto contentItem = scrolledItem() )
507 {
508 QSizeF hint;
509
510 if ( m_data->isItemResizable )
511 {
512 hint = qskSizeConstraint( contentItem, which, constraint );
513 }
514 else
515 {
516 hint = contentItem->size();
517 }
518
519 if ( verticalScrollBarPolicy() != Qt::ScrollBarAlwaysOff )
520 {
521 hint.rwidth() += metric( VerticalScrollBar | QskAspect::Size );
522 hint.rwidth() += metric( Panel | QskAspect::Spacing );
523 }
524
525 if ( horizontalScrollBarPolicy() != Qt::ScrollBarAlwaysOff )
526 {
527 hint.rheight() += metric( HorizontalScrollBar | QskAspect::Size );
528 hint.rheight() += metric( Panel | QskAspect::Spacing );
529 }
530
531 return hint;
532 }
533 }
534
535 return Inherited::layoutSizeHint( which, constraint );
536}
537
538void QskScrollArea::adjustItem()
539{
540 auto item = m_data->clipItem->scrolledItem();
541
542 if ( item == nullptr )
543 {
544 setScrollableSize( QSizeF() );
545 setScrollPos( QPointF() );
546
547 return;
548 }
549
550 if ( m_data->isItemResizable )
551 {
552 QSizeF itemSize;
553
554 const auto viewSize = qskPanelInnerSize( this );
555 if ( !viewSize.isEmpty() )
556 {
557 // we have to anticipate the scrollbars
558 itemSize = qskScrolledItemSize( this, item, viewSize );
559 }
560
561 if ( itemSize.isEmpty() )
562 itemSize = QSizeF( 0.0, 0.0 );
563
564
565 m_data->clipItem->setItemSizeChangedEnabled( false );
566 item->setSize( itemSize );
567 m_data->clipItem->setItemSizeChangedEnabled( true );
568 }
569
570 m_data->enableAutoTranslation( this, false );
571
572 setScrollableSize( QSizeF( item->width(), item->height() ) );
573 setScrollPos( scrollPos() );
574
575 m_data->enableAutoTranslation( this, true );
576
577 translateItem();
578}
579
580void QskScrollArea::setItemResizable( bool on )
581{
582 if ( on != m_data->isItemResizable )
583 {
584 m_data->isItemResizable = on;
585 Q_EMIT itemResizableChanged( on );
586
587 if ( m_data->isItemResizable )
588 polish();
589 }
590}
591
592bool QskScrollArea::isItemResizable() const
593{
594 return m_data->isItemResizable;
595}
596
597void QskScrollArea::setItemFocusClipping( bool on )
598{
599 if( m_data->isItemFocusClipping != on )
600 {
601 m_data->isItemFocusClipping = on;
603 }
604}
605
606bool QskScrollArea::hasItemFocusClipping() const
607{
608 return m_data->isItemFocusClipping;
609}
610
611void QskScrollArea::setScrolledItem( QQuickItem* item )
612{
613 auto oldItem = m_data->clipItem->scrolledItem();
614 if ( item == oldItem )
615 return;
616
617 if ( oldItem )
618 {
619 if ( oldItem->parent() == this )
620 delete oldItem;
621 else
622 oldItem->setParentItem( nullptr );
623 }
624
625 if ( item )
626 {
627 item->setParentItem( m_data->clipItem );
628 if ( item->parent() == nullptr )
629 item->setParent( m_data->clipItem );
630 }
631
632 polish();
633 Q_EMIT scrolledItemChanged();
634}
635
636QQuickItem* QskScrollArea::scrolledItem() const
637{
638 return m_data->clipItem->scrolledItem();
639}
640
641void QskScrollArea::translateItem()
642{
643 if ( auto item = m_data->clipItem->scrolledItem() )
644 {
645 const QPointF pos = viewContentsRect().topLeft() - scrollPos();
646 item->setPosition( pos );
647 }
648}
649
650#ifndef QT_NO_WHEELEVENT
651
652QPointF QskScrollArea::scrollOffset( const QWheelEvent* event ) const
653{
654 // TODO: what to do here ???
655 return Inherited::scrollOffset( event );
656}
657
658#endif
659
660#include "moc_QskScrollArea.cpp"
Base class of all controls.
Definition QskControl.h:23
QRectF subControlRect(QskAspect::Subcontrol) const
void focusIndicatorRectChanged()
virtual QRectF focusIndicatorClipRect() const
qreal spacingHint(QskAspect, QskSkinHintStatus *=nullptr) const
Retrieves a spacing hint.
QskBoxBorderMetrics boxBorderMetricsHint(QskAspect, QskSkinHintStatus *=nullptr) const
Retrieves a border hint.