1/****************************************************************************
2**
3** Copyright (C) 2017 The Qt Company Ltd.
4** Contact: http://www.qt.io/licensing/
5**
6** This file is part of the Qt Quick Templates 2 module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL3$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see http://www.qt.io/terms-conditions. For further
15** information use the contact form at http://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPLv3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or later as published by the Free
28** Software Foundation and appearing in the file LICENSE.GPL included in
29** the packaging of this file. Please review the following information to
30** ensure the GNU General Public License version 2.0 requirements will be
31** met: http://www.gnu.org/licenses/gpl-2.0.html.
32**
33** $QT_END_LICENSE$
34**
35****************************************************************************/
36
37#include "qquickscrollview_p.h"
38#include "qquickpane_p_p.h"
39#include "qquickscrollbar_p_p.h"
40
41#include <QtQml/qqmlinfo.h>
42#include <QtQuick/private/qquickflickable_p.h>
43
44QT_BEGIN_NAMESPACE
45
46/*!
47 \qmltype ScrollView
48 \inherits Pane
49//! \instantiates QQuickScrollView
50 \inqmlmodule QtQuick.Controls
51 \since 5.9
52 \ingroup qtquickcontrols2-containers
53 \ingroup qtquickcontrols2-focusscopes
54 \brief Scrollable view.
55
56 ScrollView provides scrolling for user-defined content. It can be used to
57 either replace a \l Flickable, or to decorate an existing one.
58
59 \image qtquickcontrols2-scrollview.png
60
61 The first example demonstrates the simplest usage of ScrollView.
62
63 \snippet qtquickcontrols2-scrollview.qml file
64
65 \note ScrollView does not automatically clip its contents. If it is not used as
66 a full-screen item, you should consider setting the \l {Item::}{clip} property
67 to \c true, as shown above.
68
69 The second example illustrates using an existing \l Flickable, that is,
70 a \l ListView.
71
72 \snippet qtquickcontrols2-scrollview-listview.qml file
73
74 \section2 Sizing
75
76 As with Flickable, there are several things to keep in mind when using
77 ScrollView:
78 \list
79 \li If only a single item is used within a ScrollView, the content size is
80 automatically calculated based on the implicit size of its contained item.
81 However, if more than one item is used (or an implicit size is not
82 provided), the \l {QtQuick.Controls::Pane::}{contentWidth} and
83 \l {QtQuick.Controls::Pane::}{contentHeight} properties must
84 be set to the combined size of its contained items.
85 \li If the content size is less than or equal to the size of the ScrollView,
86 it will not be scrollable.
87 \li If you want the ScrollView to only scroll vertically, you can bind
88 \l {QtQuick.Controls::Pane::}{contentWidth} to
89 \l {QtQuick.Controls::Control::}{availableWidth}
90 (and vice versa for contentHeight). This will let the contents fill
91 out all the available space horizontally inside the ScrollView, taking
92 any padding or scroll bars into account.
93 \endlist
94
95 \section2 Scroll Bars
96
97 The horizontal and vertical scroll bars can be accessed and customized using
98 the \l {ScrollBar::horizontal}{ScrollBar.horizontal} and \l {ScrollBar::vertical}
99 {ScrollBar.vertical} attached properties. The following example adjusts the scroll
100 bar policies so that the horizontal scroll bar is always off, and the vertical
101 scroll bar is always on.
102
103 \snippet qtquickcontrols2-scrollview-policy.qml file
104
105 \section2 Touch vs. Mouse Interaction
106
107 On touch, ScrollView enables flicking and makes the scroll bars non-interactive.
108
109 \image qtquickcontrols2-scrollindicator.gif
110
111 When interacted with a mouse device, flicking is disabled and the scroll bars
112 are interactive.
113
114 \image qtquickcontrols2-scrollbar.gif
115
116 Scroll bars can be made interactive on touch, or non-interactive when interacted
117 with a mouse device, by setting the \l {ScrollBar::}{interactive} property explicitly
118 to \c true or \c false, respectively.
119
120 \snippet qtquickcontrols2-scrollview-interactive.qml file
121
122 \sa ScrollBar, ScrollIndicator, {Customizing ScrollView}, {Container Controls},
123 {Focus Management in Qt Quick Controls}
124*/
125
126class QQuickScrollViewPrivate : public QQuickPanePrivate
127{
128 Q_DECLARE_PUBLIC(QQuickScrollView)
129
130public:
131 QQmlListProperty<QObject> contentData() override;
132 QQmlListProperty<QQuickItem> contentChildren() override;
133 QList<QQuickItem *> contentChildItems() const override;
134
135 QQuickItem *getContentItem() override;
136
137 QQuickFlickable *ensureFlickable(bool content);
138 bool setFlickable(QQuickFlickable *flickable, bool content);
139
140 void flickableContentWidthChanged();
141 void flickableContentHeightChanged();
142
143 qreal getContentWidth() const override;
144 qreal getContentHeight() const override;
145
146 QQuickScrollBar *verticalScrollBar() const;
147 QQuickScrollBar *horizontalScrollBar() const;
148
149 void setScrollBarsInteractive(bool interactive);
150
151 static void contentData_append(QQmlListProperty<QObject> *prop, QObject *obj);
152 static int contentData_count(QQmlListProperty<QObject> *prop);
153 static QObject *contentData_at(QQmlListProperty<QObject> *prop, int index);
154 static void contentData_clear(QQmlListProperty<QObject> *prop);
155
156 static void contentChildren_append(QQmlListProperty<QQuickItem> *prop, QQuickItem *obj);
157 static int contentChildren_count(QQmlListProperty<QQuickItem> *prop);
158 static QQuickItem *contentChildren_at(QQmlListProperty<QQuickItem> *prop, int index);
159 static void contentChildren_clear(QQmlListProperty<QQuickItem> *prop);
160
161 void itemImplicitWidthChanged(QQuickItem *item) override;
162
163 bool wasTouched = false;
164 QQuickFlickable *flickable = nullptr;
165 bool flickableHasExplicitContentWidth = true;
166 bool flickableHasExplicitContentHeight = true;
167};
168
169QList<QQuickItem *> QQuickScrollViewPrivate::contentChildItems() const
170{
171 if (!flickable)
172 return QList<QQuickItem *>();
173
174 return flickable->contentItem()->childItems();
175}
176
177QQuickItem *QQuickScrollViewPrivate::getContentItem()
178{
179 if (!contentItem)
180 executeContentItem();
181 return ensureFlickable(content: false);
182}
183
184QQuickFlickable *QQuickScrollViewPrivate::ensureFlickable(bool content)
185{
186 Q_Q(QQuickScrollView);
187 if (!flickable) {
188 flickableHasExplicitContentWidth = false;
189 flickableHasExplicitContentHeight = false;
190 setFlickable(flickable: new QQuickFlickable(q), content);
191 }
192 return flickable;
193}
194
195bool QQuickScrollViewPrivate::setFlickable(QQuickFlickable *item, bool content)
196{
197 Q_Q(QQuickScrollView);
198 if (item == flickable)
199 return false;
200
201 QQuickScrollBarAttached *attached = qobject_cast<QQuickScrollBarAttached *>(object: qmlAttachedPropertiesObject<QQuickScrollBar>(obj: q, create: false));
202
203 if (flickable) {
204 flickable->removeEventFilter(obj: q);
205
206 if (attached)
207 QQuickScrollBarAttachedPrivate::get(attached)->setFlickable(nullptr);
208
209 QObjectPrivate::disconnect(sender: flickable->contentItem(), signal: &QQuickItem::childrenChanged, receiverPrivate: this, slot: &QQuickPanePrivate::contentChildrenChange);
210 QObjectPrivate::disconnect(sender: flickable, signal: &QQuickFlickable::contentWidthChanged, receiverPrivate: this, slot: &QQuickScrollViewPrivate::flickableContentWidthChanged);
211 QObjectPrivate::disconnect(sender: flickable, signal: &QQuickFlickable::contentHeightChanged, receiverPrivate: this, slot: &QQuickScrollViewPrivate::flickableContentHeightChanged);
212 }
213
214 flickable = item;
215 if (content)
216 q->setContentItem(flickable);
217
218 if (flickable) {
219 flickable->installEventFilter(filterObj: q);
220 if (hasContentWidth)
221 flickable->setContentWidth(contentWidth);
222 else
223 flickableContentWidthChanged();
224 if (hasContentHeight)
225 flickable->setContentHeight(contentHeight);
226 else
227 flickableContentHeightChanged();
228
229 if (attached)
230 QQuickScrollBarAttachedPrivate::get(attached)->setFlickable(flickable);
231
232 QObjectPrivate::connect(sender: flickable->contentItem(), signal: &QQuickItem::childrenChanged, receiverPrivate: this, slot: &QQuickPanePrivate::contentChildrenChange);
233 QObjectPrivate::connect(sender: flickable, signal: &QQuickFlickable::contentWidthChanged, receiverPrivate: this, slot: &QQuickScrollViewPrivate::flickableContentWidthChanged);
234 QObjectPrivate::connect(sender: flickable, signal: &QQuickFlickable::contentHeightChanged, receiverPrivate: this, slot: &QQuickScrollViewPrivate::flickableContentHeightChanged);
235 }
236
237 return true;
238}
239
240void QQuickScrollViewPrivate::flickableContentWidthChanged()
241{
242 Q_Q(QQuickScrollView);
243 if (!flickable || !componentComplete)
244 return;
245
246 const qreal cw = flickable->contentWidth();
247 if (qFuzzyCompare(p1: cw, p2: implicitContentWidth))
248 return;
249
250 flickableHasExplicitContentWidth = true;
251 implicitContentWidth = cw;
252 emit q->implicitContentWidthChanged();
253}
254
255void QQuickScrollViewPrivate::flickableContentHeightChanged()
256{
257 Q_Q(QQuickScrollView);
258 if (!flickable || !componentComplete)
259 return;
260
261 const qreal ch = flickable->contentHeight();
262 if (qFuzzyCompare(p1: ch, p2: implicitContentHeight))
263 return;
264
265 flickableHasExplicitContentHeight = true;
266 implicitContentHeight = ch;
267 emit q->implicitContentHeightChanged();
268}
269
270qreal QQuickScrollViewPrivate::getContentWidth() const
271{
272 if (flickable && flickableHasExplicitContentWidth)
273 return flickable->contentWidth();
274
275 // The scrollview wraps a flickable created by us, and nobody searched for it and
276 // modified its contentWidth. In that case, since the application does not control
277 // this flickable, we fall back to calculate the content width based on the child
278 // items inside it.
279 return QQuickPanePrivate::getContentWidth();
280}
281
282qreal QQuickScrollViewPrivate::getContentHeight() const
283{
284 if (flickable && flickableHasExplicitContentHeight)
285 return flickable->contentHeight();
286
287 // The scrollview wraps a flickable created by us, and nobody searched for it and
288 // modified its contentHeight. In that case, since the application does not control
289 // this flickable, we fall back to calculate the content height based on the child
290 // items inside it.
291 return QQuickPanePrivate::getContentHeight();
292}
293
294QQuickScrollBar *QQuickScrollViewPrivate::verticalScrollBar() const
295{
296 Q_Q(const QQuickScrollView);
297 QQuickScrollBarAttached *attached = qobject_cast<QQuickScrollBarAttached *>(object: qmlAttachedPropertiesObject<QQuickScrollBar>(obj: q, create: false));
298 if (!attached)
299 return nullptr;
300 return attached->vertical();
301}
302
303QQuickScrollBar *QQuickScrollViewPrivate::horizontalScrollBar() const
304{
305 Q_Q(const QQuickScrollView);
306 QQuickScrollBarAttached *attached = qobject_cast<QQuickScrollBarAttached *>(object: qmlAttachedPropertiesObject<QQuickScrollBar>(obj: q, create: false));
307 if (!attached)
308 return nullptr;
309 return attached->horizontal();
310}
311
312void QQuickScrollViewPrivate::setScrollBarsInteractive(bool interactive)
313{
314 QQuickScrollBar *hbar = horizontalScrollBar();
315 if (hbar) {
316 QQuickScrollBarPrivate *p = QQuickScrollBarPrivate::get(bar: hbar);
317 if (!p->explicitInteractive)
318 p->setInteractive(interactive);
319 }
320
321 QQuickScrollBar *vbar = verticalScrollBar();
322 if (vbar) {
323 QQuickScrollBarPrivate *p = QQuickScrollBarPrivate::get(bar: vbar);
324 if (!p->explicitInteractive)
325 p->setInteractive(interactive);
326 }
327}
328
329void QQuickScrollViewPrivate::contentData_append(QQmlListProperty<QObject> *prop, QObject *obj)
330{
331 QQuickScrollViewPrivate *p = static_cast<QQuickScrollViewPrivate *>(prop->data);
332 if (!p->flickable && p->setFlickable(item: qobject_cast<QQuickFlickable *>(object: obj), content: true))
333 return;
334
335 QQuickFlickable *flickable = p->ensureFlickable(content: true);
336 Q_ASSERT(flickable);
337 QQmlListProperty<QObject> data = flickable->flickableData();
338 data.append(&data, obj);
339}
340
341int QQuickScrollViewPrivate::contentData_count(QQmlListProperty<QObject> *prop)
342{
343 QQuickScrollViewPrivate *p = static_cast<QQuickScrollViewPrivate *>(prop->data);
344 if (!p->flickable)
345 return 0;
346
347 QQmlListProperty<QObject> data = p->flickable->flickableData();
348 return data.count(&data);
349}
350
351QObject *QQuickScrollViewPrivate::contentData_at(QQmlListProperty<QObject> *prop, int index)
352{
353 QQuickScrollViewPrivate *p = static_cast<QQuickScrollViewPrivate *>(prop->data);
354 if (!p->flickable)
355 return nullptr;
356
357 QQmlListProperty<QObject> data = p->flickable->flickableData();
358 return data.at(&data, index);
359}
360
361void QQuickScrollViewPrivate::contentData_clear(QQmlListProperty<QObject> *prop)
362{
363 QQuickScrollViewPrivate *p = static_cast<QQuickScrollViewPrivate *>(prop->data);
364 if (!p->flickable)
365 return;
366
367 QQmlListProperty<QObject> data = p->flickable->flickableData();
368 return data.clear(&data);
369}
370
371void QQuickScrollViewPrivate::contentChildren_append(QQmlListProperty<QQuickItem> *prop, QQuickItem *item)
372{
373 QQuickScrollViewPrivate *p = static_cast<QQuickScrollViewPrivate *>(prop->data);
374 if (!p->flickable)
375 p->setFlickable(item: qobject_cast<QQuickFlickable *>(object: item), content: true);
376
377 QQuickFlickable *flickable = p->ensureFlickable(content: true);
378 Q_ASSERT(flickable);
379 QQmlListProperty<QQuickItem> children = flickable->flickableChildren();
380 children.append(&children, item);
381}
382
383int QQuickScrollViewPrivate::contentChildren_count(QQmlListProperty<QQuickItem> *prop)
384{
385 QQuickScrollViewPrivate *p = static_cast<QQuickScrollViewPrivate *>(prop->data);
386 if (!p->flickable)
387 return 0;
388
389 QQmlListProperty<QQuickItem> children = p->flickable->flickableChildren();
390 return children.count(&children);
391}
392
393QQuickItem *QQuickScrollViewPrivate::contentChildren_at(QQmlListProperty<QQuickItem> *prop, int index)
394{
395 QQuickScrollViewPrivate *p = static_cast<QQuickScrollViewPrivate *>(prop->data);
396 if (!p->flickable)
397 return nullptr;
398
399 QQmlListProperty<QQuickItem> children = p->flickable->flickableChildren();
400 return children.at(&children, index);
401}
402
403void QQuickScrollViewPrivate::contentChildren_clear(QQmlListProperty<QQuickItem> *prop)
404{
405 QQuickScrollViewPrivate *p = static_cast<QQuickScrollViewPrivate *>(prop->data);
406 if (!p->flickable)
407 return;
408
409 QQmlListProperty<QQuickItem> children = p->flickable->flickableChildren();
410 children.clear(&children);
411}
412
413void QQuickScrollViewPrivate::itemImplicitWidthChanged(QQuickItem *item)
414{
415 // a special case for width<->height dependent content (wrapping text) in ScrollView
416 if (contentWidth < 0 && !componentComplete)
417 return;
418
419 QQuickPanePrivate::itemImplicitWidthChanged(item);
420}
421
422QQuickScrollView::QQuickScrollView(QQuickItem *parent)
423 : QQuickPane(*(new QQuickScrollViewPrivate), parent)
424{
425 Q_D(QQuickScrollView);
426 d->contentWidth = -1;
427 d->contentHeight = -1;
428
429 setFiltersChildMouseEvents(true);
430 setWheelEnabled(true);
431}
432
433/*!
434 \qmlproperty list<Object> QtQuick.Controls::ScrollView::contentData
435 \default
436
437 This property holds the list of content data.
438
439 The list contains all objects that have been declared in QML as children of the view.
440
441 \note Unlike \c contentChildren, \c contentData does include non-visual QML objects.
442
443 \sa Item::data, contentChildren
444*/
445QQmlListProperty<QObject> QQuickScrollViewPrivate::contentData()
446{
447 Q_Q(QQuickScrollView);
448 return QQmlListProperty<QObject>(q, this,
449 QQuickScrollViewPrivate::contentData_append,
450 QQuickScrollViewPrivate::contentData_count,
451 QQuickScrollViewPrivate::contentData_at,
452 QQuickScrollViewPrivate::contentData_clear);
453}
454
455/*!
456 \qmlproperty list<Item> QtQuick.Controls::ScrollView::contentChildren
457
458 This property holds the list of content children.
459
460 The list contains all items that have been declared in QML as children of the view.
461
462 \note Unlike \c contentData, \c contentChildren does not include non-visual QML objects.
463
464 \sa Item::children, contentData
465*/
466QQmlListProperty<QQuickItem> QQuickScrollViewPrivate::contentChildren()
467{
468 Q_Q(QQuickScrollView);
469 return QQmlListProperty<QQuickItem>(q, this,
470 QQuickScrollViewPrivate::contentChildren_append,
471 QQuickScrollViewPrivate::contentChildren_count,
472 QQuickScrollViewPrivate::contentChildren_at,
473 QQuickScrollViewPrivate::contentChildren_clear);
474}
475
476bool QQuickScrollView::childMouseEventFilter(QQuickItem *item, QEvent *event)
477{
478 Q_D(QQuickScrollView);
479 switch (event->type()) {
480 case QEvent::TouchBegin:
481 d->wasTouched = true;
482 d->setScrollBarsInteractive(false);
483 return false;
484
485 case QEvent::TouchEnd:
486 d->wasTouched = false;
487 return false;
488
489 case QEvent::MouseButtonPress:
490 // NOTE: Flickable does not handle touch events, only synthesized mouse events
491 if (static_cast<QMouseEvent *>(event)->source() == Qt::MouseEventNotSynthesized) {
492 d->wasTouched = false;
493 d->setScrollBarsInteractive(true);
494 return false;
495 }
496 return !d->wasTouched && item == d->flickable;
497
498 case QEvent::MouseMove:
499 case QEvent::MouseButtonRelease:
500 if (static_cast<QMouseEvent *>(event)->source() == Qt::MouseEventNotSynthesized)
501 return item == d->flickable;
502 break;
503
504 case QEvent::HoverEnter:
505 case QEvent::HoverMove:
506 if (d->wasTouched && (item == d->verticalScrollBar() || item == d->horizontalScrollBar()))
507 d->setScrollBarsInteractive(true);
508 break;
509
510 default:
511 break;
512 }
513
514 return false;
515}
516
517bool QQuickScrollView::eventFilter(QObject *object, QEvent *event)
518{
519 Q_D(QQuickScrollView);
520 if (event->type() == QEvent::Wheel) {
521 d->setScrollBarsInteractive(true);
522 if (!d->wheelEnabled)
523 return true;
524 }
525 return QQuickPane::eventFilter(watched: object, event);
526}
527
528void QQuickScrollView::keyPressEvent(QKeyEvent *event)
529{
530 Q_D(QQuickScrollView);
531 QQuickPane::keyPressEvent(event);
532 switch (event->key()) {
533 case Qt::Key_Up:
534 if (QQuickScrollBar *vbar = d->verticalScrollBar()) {
535 vbar->decrease();
536 event->accept();
537 }
538 break;
539 case Qt::Key_Down:
540 if (QQuickScrollBar *vbar = d->verticalScrollBar()) {
541 vbar->increase();
542 event->accept();
543 }
544 break;
545 case Qt::Key_Left:
546 if (QQuickScrollBar *hbar = d->horizontalScrollBar()) {
547 hbar->decrease();
548 event->accept();
549 }
550 break;
551 case Qt::Key_Right:
552 if (QQuickScrollBar *hbar = d->horizontalScrollBar()) {
553 hbar->increase();
554 event->accept();
555 }
556 break;
557 default:
558 event->ignore();
559 break;
560 }
561}
562
563void QQuickScrollView::componentComplete()
564{
565 Q_D(QQuickScrollView);
566 QQuickPane::componentComplete();
567 if (!d->contentItem)
568 d->ensureFlickable(content: true);
569}
570
571void QQuickScrollView::contentItemChange(QQuickItem *newItem, QQuickItem *oldItem)
572{
573 Q_D(QQuickScrollView);
574 if (newItem != d->flickable) {
575 // The new flickable was not created by us. In that case, we always
576 // assume/require that it has an explicit content size assigned.
577 d->flickableHasExplicitContentWidth = true;
578 d->flickableHasExplicitContentHeight = true;
579 auto newItemAsFlickable = qobject_cast<QQuickFlickable *>(object: newItem);
580 if (newItem && !newItemAsFlickable)
581 qmlWarning(me: this) << "ScrollView only supports Flickable types as its contentItem";
582 d->setFlickable(item: newItemAsFlickable, content: false);
583 }
584 QQuickPane::contentItemChange(newItem, oldItem);
585}
586
587void QQuickScrollView::contentSizeChange(const QSizeF &newSize, const QSizeF &oldSize)
588{
589 Q_D(QQuickScrollView);
590 QQuickPane::contentSizeChange(newSize, oldSize);
591 if (d->flickable) {
592 // Only set the content size on the flickable if the flickable doesn't
593 // have an explicit assignment from before. Otherwise we can end up overwriting
594 // assignments done to those properties by the application. The
595 // exception is if the application has assigned a content size
596 // directly to the scrollview, which will then win even if the
597 // application has assigned something else to the flickable.
598 if (d->hasContentWidth || !d->flickableHasExplicitContentWidth)
599 d->flickable->setContentWidth(newSize.width());
600 if (d->hasContentHeight || !d->flickableHasExplicitContentHeight)
601 d->flickable->setContentHeight(newSize.height());
602 }
603}
604
605#if QT_CONFIG(accessibility)
606QAccessible::Role QQuickScrollView::accessibleRole() const
607{
608 return QAccessible::Pane;
609}
610#endif
611
612QT_END_NAMESPACE
613
614#include "moc_qquickscrollview_p.cpp"
615

source code of qtquickcontrols2/src/quicktemplates2/qquickscrollview.cpp