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