1// Copyright (C) 2017 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qquicktumbler_p.h"
5
6#include <QtCore/qloggingcategory.h>
7#include <QtGui/qpa/qplatformtheme.h>
8#include <QtQml/qqmlinfo.h>
9#include <QtQuick/private/qquickflickable_p.h>
10#include <QtQuickTemplates2/private/qquickcontrol_p_p.h>
11#include <QtQuickTemplates2/private/qquicktumbler_p_p.h>
12#include <QtGui/private/qguiapplication_p.h>
13
14QT_BEGIN_NAMESPACE
15
16Q_STATIC_LOGGING_CATEGORY(lcTumbler, "qt.quick.controls.tumbler")
17
18/*!
19 \qmltype Tumbler
20 \inherits Control
21//! \nativetype QQuickTumbler
22 \inqmlmodule QtQuick.Controls
23 \since 5.7
24 \ingroup qtquickcontrols-input
25 \brief Spinnable wheel of items that can be selected.
26
27 \image qtquickcontrols-tumbler-wrap.gif
28
29 \code
30 Tumbler {
31 model: 5
32 // ...
33 }
34 \endcode
35
36 Tumbler allows the user to select an option from a spinnable \e "wheel" of
37 items. It is useful for when there are too many options to use, for
38 example, a RadioButton, and too few options to require the use of an
39 editable SpinBox. It is convenient in that it requires no keyboard usage
40 and wraps around at each end when there are a large number of items.
41
42 The API is similar to that of views like \l ListView and \l PathView; a
43 \l model and \l delegate can be set, and the \l count and \l currentItem
44 properties provide read-only access to information about the view. To
45 position the view at a certain index, use \l positionViewAtIndex().
46
47 Unlike views like \l PathView and \l ListView, however, there is always a
48 current item (when the model isn't empty). This means that when \l count is
49 equal to \c 0, \l currentIndex will be \c -1. In all other cases, it will
50 be greater than or equal to \c 0.
51
52 By default, Tumbler \l {wrap}{wraps} when it reaches the top and bottom, as
53 long as there are more items in the model than there are visible items;
54 that is, when \l count is greater than \l visibleItemCount:
55
56 \snippet qtquickcontrols-tumbler-timePicker.qml tumbler
57
58 \sa {Customizing Tumbler}, {Input Controls}
59*/
60
61namespace {
62 static inline qreal delegateHeight(const QQuickTumbler *tumbler)
63 {
64 return tumbler->availableHeight() / tumbler->visibleItemCount();
65 }
66
67 static qreal defaultFlickDeceleration(bool wrap)
68 {
69 if (wrap)
70 return 100;
71 return QGuiApplicationPrivate::platformTheme()->themeHint(hint: QPlatformTheme::FlickDeceleration).toReal();
72 }
73}
74
75/*
76 Finds the contentItem of the view that is a child of the control's \a contentItem.
77 The type is stored in \a type.
78*/
79QQuickItem *QQuickTumblerPrivate::determineViewType(QQuickItem *contentItem)
80{
81 if (!contentItem) {
82 resetViewData();
83 return nullptr;
84 }
85
86 if (contentItem->inherits(classname: "QQuickPathView")) {
87 view = contentItem;
88 viewContentItem = contentItem;
89 viewContentItemType = PathViewContentItem;
90 viewOffset = 0;
91
92 return contentItem;
93 } else if (contentItem->inherits(classname: "QQuickListView")) {
94 view = contentItem;
95 viewContentItem = qobject_cast<QQuickFlickable*>(object: contentItem)->contentItem();
96 viewContentItemType = ListViewContentItem;
97 viewContentY = 0;
98
99 return contentItem;
100 } else {
101 const auto childItems = contentItem->childItems();
102 for (QQuickItem *childItem : childItems) {
103 QQuickItem *item = determineViewType(contentItem: childItem);
104 if (item)
105 return item;
106 }
107 }
108
109 resetViewData();
110 viewContentItemType = UnsupportedContentItemType;
111 return nullptr;
112}
113
114void QQuickTumblerPrivate::resetViewData()
115{
116 view = nullptr;
117 viewContentItem = nullptr;
118 if (viewContentItemType == PathViewContentItem)
119 viewOffset = 0;
120 else if (viewContentItemType == ListViewContentItem)
121 viewContentY = 0;
122 viewContentItemType = NoContentItem;
123}
124
125QList<QQuickItem *> QQuickTumblerPrivate::viewContentItemChildItems() const
126{
127 if (!viewContentItem)
128 return QList<QQuickItem *>();
129
130 return viewContentItem->childItems();
131}
132
133QQuickTumblerPrivate *QQuickTumblerPrivate::get(QQuickTumbler *tumbler)
134{
135 return tumbler->d_func();
136}
137
138void QQuickTumblerPrivate::_q_updateItemHeights()
139{
140 if (ignoreSignals)
141 return;
142
143 // Can't use our own private padding members here, as the padding property might be set,
144 // which doesn't affect them, only their getters.
145 Q_Q(const QQuickTumbler);
146 const qreal itemHeight = delegateHeight(tumbler: q);
147 const auto items = viewContentItemChildItems();
148 for (QQuickItem *childItem : items)
149 childItem->setHeight(itemHeight);
150}
151
152void QQuickTumblerPrivate::_q_updateItemWidths()
153{
154 if (ignoreSignals)
155 return;
156
157 Q_Q(const QQuickTumbler);
158 const qreal availableWidth = q->availableWidth();
159 const auto items = viewContentItemChildItems();
160 for (QQuickItem *childItem : items)
161 childItem->setWidth(availableWidth);
162}
163
164void QQuickTumblerPrivate::_q_onViewCurrentIndexChanged()
165{
166 Q_Q(QQuickTumbler);
167 if (!view || ignoreCurrentIndexChanges || currentIndexSetDuringModelChange) {
168 // If the user set currentIndex in the onModelChanged handler,
169 // we have to respect that currentIndex by ignoring changes in the view
170 // until the model has finished being set.
171 qCDebug(lcTumbler).nospace() << "view currentIndex changed to "
172 << (view ? view->property(name: "currentIndex").toString() : QStringLiteral("unknown index (no view)"))
173 << ", but we're ignoring it because one or more of the following conditions are true:"
174 << "\n- !view: " << !view
175 << "\n- ignoreCurrentIndexChanges: " << ignoreCurrentIndexChanges
176 << "\n- currentIndexSetDuringModelChange: " << currentIndexSetDuringModelChange;
177 return;
178 }
179
180 const int oldCurrentIndex = currentIndex;
181 currentIndex = view->property(name: "currentIndex").toInt();
182
183 qCDebug(lcTumbler).nospace() << "view currentIndex changed to "
184 << (view ? view->property(name: "currentIndex").toString() : QStringLiteral("unknown index (no view)"))
185 << ", our old currentIndex was " << oldCurrentIndex;
186
187 if (oldCurrentIndex != currentIndex)
188 emit q->currentIndexChanged();
189}
190
191void QQuickTumblerPrivate::_q_onViewCountChanged()
192{
193 Q_Q(QQuickTumbler);
194 qCDebug(lcTumbler) << "view count changed - ignoring signals?" << ignoreSignals;
195 if (ignoreSignals)
196 return;
197
198 setCount(view->property(name: "count").toInt());
199
200 if (count > 0) {
201 if (pendingCurrentIndex != -1) {
202 // If there was an attempt to set currentIndex at creation, try to finish that attempt now.
203 // componentComplete() is too early, because the count might only be known sometime after completion.
204 setCurrentIndex(newCurrentIndex: pendingCurrentIndex);
205 // If we could successfully set the currentIndex, consider it done.
206 // Otherwise, we'll try again later in updatePolish().
207 if (currentIndex == pendingCurrentIndex)
208 setPendingCurrentIndex(-1);
209 else
210 q->polish();
211 } else if (currentIndex == -1) {
212 // If new items were added and our currentIndex was -1, we must
213 // enforce our rule of a non-negative currentIndex when count > 0.
214 setCurrentIndex(newCurrentIndex: 0);
215 }
216 } else {
217 setCurrentIndex(newCurrentIndex: -1);
218 }
219}
220
221void QQuickTumblerPrivate::_q_onViewOffsetChanged()
222{
223 viewOffset = view->property(name: "offset").toReal();
224 calculateDisplacements();
225}
226
227void QQuickTumblerPrivate::_q_onViewContentYChanged()
228{
229 viewContentY = view->property(name: "contentY").toReal();
230 calculateDisplacements();
231}
232
233void QQuickTumblerPrivate::calculateDisplacements()
234{
235 const auto items = viewContentItemChildItems();
236 for (QQuickItem *childItem : items) {
237 QQuickTumblerAttached *attached = qobject_cast<QQuickTumblerAttached *>(object: qmlAttachedPropertiesObject<QQuickTumbler>(obj: childItem, create: false));
238 if (attached)
239 QQuickTumblerAttachedPrivate::get(attached)->calculateDisplacement();
240 }
241}
242
243void QQuickTumblerPrivate::itemChildAdded(QQuickItem *, QQuickItem *)
244{
245 _q_updateItemWidths();
246 _q_updateItemHeights();
247}
248
249void QQuickTumblerPrivate::itemChildRemoved(QQuickItem *, QQuickItem *)
250{
251 _q_updateItemWidths();
252 _q_updateItemHeights();
253}
254
255void QQuickTumblerPrivate::itemGeometryChanged(QQuickItem *item, QQuickGeometryChange change, const QRectF &diff)
256{
257 QQuickControlPrivate::itemGeometryChanged(item, change, diff);
258 if (change.sizeChange())
259 calculateDisplacements();
260}
261
262QPalette QQuickTumblerPrivate::defaultPalette() const
263{
264 return QQuickTheme::palette(scope: QQuickTheme::Tumbler);
265}
266
267QQuickTumbler::QQuickTumbler(QQuickItem *parent)
268 : QQuickControl(*(new QQuickTumblerPrivate), parent)
269{
270 Q_D(QQuickTumbler);
271 d->setSizePolicy(horizontalPolicy: QLayoutPolicy::Preferred, verticalPolicy: QLayoutPolicy::Preferred);
272
273 setActiveFocusOnTab(true);
274
275 connect(sender: this, SIGNAL(leftPaddingChanged()), receiver: this, SLOT(_q_updateItemWidths()));
276 connect(sender: this, SIGNAL(rightPaddingChanged()), receiver: this, SLOT(_q_updateItemWidths()));
277 connect(sender: this, SIGNAL(topPaddingChanged()), receiver: this, SLOT(_q_updateItemHeights()));
278 connect(sender: this, SIGNAL(bottomPaddingChanged()), receiver: this, SLOT(_q_updateItemHeights()));
279}
280
281QQuickTumbler::~QQuickTumbler()
282{
283 Q_D(QQuickTumbler);
284 // Ensure that the item change listener is removed.
285 d->disconnectFromView();
286}
287
288/*!
289 \qmlproperty variant QtQuick.Controls::Tumbler::model
290
291 This property holds the model that provides data for this tumbler.
292*/
293QVariant QQuickTumbler::model() const
294{
295 Q_D(const QQuickTumbler);
296 return d->model;
297}
298
299void QQuickTumbler::setModel(const QVariant &model)
300{
301 Q_D(QQuickTumbler);
302 if (model == d->model)
303 return;
304
305 d->beginSetModel();
306
307 d->model = model;
308 emit modelChanged();
309
310 d->endSetModel();
311
312 if (d->view && d->currentIndexSetDuringModelChange) {
313 const int viewCurrentIndex = d->view->property(name: "currentIndex").toInt();
314 if (viewCurrentIndex != d->currentIndex)
315 d->view->setProperty(name: "currentIndex", value: d->currentIndex);
316 }
317
318 d->currentIndexSetDuringModelChange = false;
319
320 // Don't try to correct the currentIndex if count() isn't known yet.
321 // We can check in setupViewData() instead.
322 if (isComponentComplete() && d->view && count() == 0)
323 d->setCurrentIndex(newCurrentIndex: -1);
324}
325
326/*!
327 \qmlproperty int QtQuick.Controls::Tumbler::count
328 \readonly
329
330 This property holds the number of items in the model.
331*/
332int QQuickTumbler::count() const
333{
334 Q_D(const QQuickTumbler);
335 return d->count;
336}
337
338/*!
339 \qmlproperty int QtQuick.Controls::Tumbler::currentIndex
340
341 This property holds the index of the current item.
342
343 The value of this property is \c -1 when \l count is equal to \c 0. In all
344 other cases, it will be greater than or equal to \c 0.
345
346 \sa currentItem, positionViewAtIndex()
347*/
348int QQuickTumbler::currentIndex() const
349{
350 Q_D(const QQuickTumbler);
351 return d->currentIndex;
352}
353
354void QQuickTumbler::setCurrentIndex(int currentIndex)
355{
356 Q_D(QQuickTumbler);
357 if (d->modelBeingSet)
358 d->currentIndexSetDuringModelChange = true;
359 d->setCurrentIndex(newCurrentIndex: currentIndex, changeReason: QQuickTumblerPrivate::UserChange);
360}
361
362/*!
363 \qmlproperty Item QtQuick.Controls::Tumbler::currentItem
364 \readonly
365
366 This property holds the item at the current index.
367
368 \sa currentIndex, positionViewAtIndex()
369*/
370QQuickItem *QQuickTumbler::currentItem() const
371{
372 Q_D(const QQuickTumbler);
373 return d->view ? d->view->property(name: "currentItem").value<QQuickItem*>() : nullptr;
374}
375
376/*!
377 \qmlproperty Component QtQuick.Controls::Tumbler::delegate
378
379 This property holds the delegate used to display each item.
380*/
381QQmlComponent *QQuickTumbler::delegate() const
382{
383 Q_D(const QQuickTumbler);
384 return d->delegate;
385}
386
387void QQuickTumbler::setDelegate(QQmlComponent *delegate)
388{
389 Q_D(QQuickTumbler);
390 if (delegate == d->delegate)
391 return;
392
393 d->delegate = delegate;
394 emit delegateChanged();
395}
396
397/*!
398 \qmlproperty int QtQuick.Controls::Tumbler::visibleItemCount
399
400 This property holds the number of items visible in the tumbler. It must be
401 an odd number, as the current item is always vertically centered.
402*/
403int QQuickTumbler::visibleItemCount() const
404{
405 Q_D(const QQuickTumbler);
406 return d->visibleItemCount;
407}
408
409void QQuickTumbler::setVisibleItemCount(int visibleItemCount)
410{
411 Q_D(QQuickTumbler);
412 if (visibleItemCount == d->visibleItemCount)
413 return;
414
415 d->visibleItemCount = visibleItemCount;
416 d->_q_updateItemHeights();
417 emit visibleItemCountChanged();
418}
419
420QQuickTumblerAttached *QQuickTumbler::qmlAttachedProperties(QObject *object)
421{
422 return new QQuickTumblerAttached(object);
423}
424
425/*!
426 \qmlproperty bool QtQuick.Controls::Tumbler::wrap
427 \since QtQuick.Controls 2.1 (Qt 5.8)
428
429 This property determines whether or not the tumbler wraps around when it
430 reaches the top or bottom.
431
432 The default value is \c false when \l count is less than
433 \l visibleItemCount, as it is simpler to interact with a non-wrapping Tumbler
434 when there are only a few items. To override this behavior, explicitly set
435 the value of this property. To return to the default behavior, set this
436 property to \c undefined.
437*/
438bool QQuickTumbler::wrap() const
439{
440 Q_D(const QQuickTumbler);
441 return d->wrap;
442}
443
444void QQuickTumbler::setWrap(bool wrap)
445{
446 Q_D(QQuickTumbler);
447 d->setWrap(shouldWrap: wrap, propertyState: QQml::PropertyUtils::State::ExplicitlySet);
448}
449
450void QQuickTumbler::resetWrap()
451{
452 Q_D(QQuickTumbler);
453 d->explicitWrap = false;
454 d->setWrapBasedOnCount();
455}
456
457/*!
458 \qmlproperty bool QtQuick.Controls::Tumbler::moving
459 \since QtQuick.Controls 2.2 (Qt 5.9)
460
461 This property describes whether the tumbler is currently moving, due to
462 the user either dragging or flicking it.
463*/
464bool QQuickTumbler::isMoving() const
465{
466 Q_D(const QQuickTumbler);
467 return d->view && d->view->property(name: "moving").toBool();
468}
469
470/*!
471 \qmlmethod void QtQuick.Controls::Tumbler::positionViewAtIndex(int index, PositionMode mode)
472 \since QtQuick.Controls 2.5 (Qt 5.12)
473
474 Positions the view so that the \a index is at the position specified by \a mode.
475
476 For example:
477
478 \code
479 positionViewAtIndex(10, Tumbler.Center)
480 \endcode
481
482 If \l wrap is true (the default), the modes available to \l {PathView}'s
483 \l {PathView::}{positionViewAtIndex()} function
484 are available, otherwise the modes available to \l {ListView}'s
485 \l {ListView::}{positionViewAtIndex()} function
486 are available.
487
488 \note There is a known limitation that using \c Tumbler.Beginning when \l
489 wrap is \c true will result in the wrong item being positioned at the top
490 of view. As a workaround, pass \c {index - 1}.
491
492 \sa currentIndex
493*/
494void QQuickTumbler::positionViewAtIndex(int index, QQuickTumbler::PositionMode mode)
495{
496 Q_D(QQuickTumbler);
497 if (!d->view) {
498 d->warnAboutIncorrectContentItem();
499 return;
500 }
501
502 QMetaObject::invokeMethod(obj: d->view, member: "positionViewAtIndex", Q_ARG(int, index), Q_ARG(int, mode));
503}
504
505
506/*!
507 \qmlproperty int QtQuick.Controls::Tumbler::flickDeceleration
508
509 This property holds the rate at which a flick will decelerate:
510 the higher the number, the faster it slows down when the user stops
511 flicking via touch. For example, \c 0.0001 is nearly
512 "frictionless", and \c 10000 feels quite "sticky".
513
514 When \l wrap is true (the default), the default
515 \l flickDeceleration is \c 100. Otherwise, it is platform-dependent.
516 To override this behavior, explicitly set the value of this property.
517 To return to the default behavior, set this property to undefined.
518 Values of zero or less are not allowed.
519*/
520qreal QQuickTumbler::flickDeceleration() const
521{
522 Q_D(const QQuickTumbler);
523 return d->effectiveFlickDeceleration();
524}
525
526void QQuickTumbler::setFlickDeceleration(qreal flickDeceleration)
527{
528 Q_D(QQuickTumbler);
529 const qreal oldFlickDeceleration = d->effectiveFlickDeceleration();
530 flickDeceleration = qMax(a: 0.001, b: flickDeceleration);
531 d->flickDeceleration = flickDeceleration;
532 if (!qFuzzyCompare(p1: oldFlickDeceleration, p2: flickDeceleration))
533 emit flickDecelerationChanged();
534}
535
536void QQuickTumbler::resetFlickDeceleration()
537{
538 Q_D(QQuickTumbler);
539 const qreal oldFlickDeceleration = d->effectiveFlickDeceleration();
540 d->flickDeceleration = 0.0;
541 if (!qFuzzyCompare(p1: oldFlickDeceleration, p2: d->effectiveFlickDeceleration()))
542 emit flickDecelerationChanged();
543}
544
545void QQuickTumbler::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
546{
547 Q_D(QQuickTumbler);
548
549 QQuickControl::geometryChange(newGeometry, oldGeometry);
550
551 d->_q_updateItemHeights();
552
553 if (newGeometry.width() != oldGeometry.width())
554 d->_q_updateItemWidths();
555}
556
557void QQuickTumbler::componentComplete()
558{
559 Q_D(QQuickTumbler);
560 qCDebug(lcTumbler) << "componentComplete()";
561 QQuickControl::componentComplete();
562
563 if (!d->view) {
564 // Force the view to be created.
565 qCDebug(lcTumbler) << "emitting wrapChanged() to force view to be created";
566 emit wrapChanged();
567 // Determine the type of view for attached properties, etc.
568 d->setupViewData(d->contentItem);
569 }
570
571 // If there was no contentItem or it was of an unsupported type,
572 // we don't have anything else to do.
573 if (!d->view)
574 return;
575
576 // Update item heights after we've populated the model,
577 // otherwise ignoreSignals will cause these functions to return early.
578 d->_q_updateItemHeights();
579 d->_q_updateItemWidths();
580 d->_q_onViewCountChanged();
581
582 qCDebug(lcTumbler) << "componentComplete() is done";
583}
584
585void QQuickTumbler::contentItemChange(QQuickItem *newItem, QQuickItem *oldItem)
586{
587 Q_D(QQuickTumbler);
588
589 QQuickControl::contentItemChange(newItem, oldItem);
590
591 if (oldItem)
592 d->disconnectFromView();
593
594 if (newItem) {
595 // We wait until wrap is set to that we know which type of view to create.
596 // If we try to set up the view too early, we'll issue warnings about it not existing.
597 if (isComponentComplete()) {
598 // Make sure we use the new content item and not the current one, as that won't
599 // be changed until after contentItemChange() has finished.
600 d->setupViewData(newItem);
601 }
602 }
603}
604
605void QQuickTumblerPrivate::disconnectFromView()
606{
607 Q_Q(QQuickTumbler);
608 if (!view) {
609 // If a custom content item is declared, it can happen that
610 // the original contentItem exists without the view etc. having been
611 // determined yet, and then this is called when the custom content item
612 // is eventually set.
613 return;
614 }
615
616 QObject::disconnect(sender: view, SIGNAL(currentIndexChanged()), receiver: q, SLOT(_q_onViewCurrentIndexChanged()));
617 QObject::disconnect(sender: view, SIGNAL(currentItemChanged()), receiver: q, SIGNAL(currentItemChanged()));
618 QObject::disconnect(sender: view, SIGNAL(countChanged()), receiver: q, SLOT(_q_onViewCountChanged()));
619 QObject::disconnect(sender: view, SIGNAL(movingChanged()), receiver: q, SIGNAL(movingChanged()));
620
621 if (viewContentItemType == PathViewContentItem)
622 QObject::disconnect(sender: view, SIGNAL(offsetChanged()), receiver: q, SLOT(_q_onViewOffsetChanged()));
623 else
624 QObject::disconnect(sender: view, SIGNAL(contentYChanged()), receiver: q, SLOT(_q_onViewContentYChanged()));
625
626 QQuickItemPrivate *oldViewContentItemPrivate = QQuickItemPrivate::get(item: viewContentItem);
627 oldViewContentItemPrivate->removeItemChangeListener(this, types: QQuickItemPrivate::Children | QQuickItemPrivate::Geometry);
628
629 resetViewData();
630}
631
632void QQuickTumblerPrivate::setupViewData(QQuickItem *newControlContentItem)
633{
634 // Don't do anything if we've already set up.
635 if (view)
636 return;
637
638 determineViewType(contentItem: newControlContentItem);
639
640 if (viewContentItemType == QQuickTumblerPrivate::NoContentItem)
641 return;
642
643 if (viewContentItemType == QQuickTumblerPrivate::UnsupportedContentItemType) {
644 warnAboutIncorrectContentItem();
645 return;
646 }
647
648 Q_Q(QQuickTumbler);
649 QObject::connect(sender: view, SIGNAL(currentIndexChanged()), receiver: q, SLOT(_q_onViewCurrentIndexChanged()));
650 QObject::connect(sender: view, SIGNAL(currentItemChanged()), receiver: q, SIGNAL(currentItemChanged()));
651 QObject::connect(sender: view, SIGNAL(countChanged()), receiver: q, SLOT(_q_onViewCountChanged()));
652 QObject::connect(sender: view, SIGNAL(movingChanged()), receiver: q, SIGNAL(movingChanged()));
653
654 if (viewContentItemType == PathViewContentItem) {
655 QObject::connect(sender: view, SIGNAL(offsetChanged()), receiver: q, SLOT(_q_onViewOffsetChanged()));
656 _q_onViewOffsetChanged();
657 } else {
658 QObject::connect(sender: view, SIGNAL(contentYChanged()), receiver: q, SLOT(_q_onViewContentYChanged()));
659 _q_onViewContentYChanged();
660 }
661
662 QQuickItemPrivate *viewContentItemPrivate = QQuickItemPrivate::get(item: viewContentItem);
663 viewContentItemPrivate->addItemChangeListener(listener: this, types: QQuickItemPrivate::Children | QQuickItemPrivate::Geometry);
664
665 // Sync the view's currentIndex with ours.
666 syncCurrentIndex();
667
668 calculateDisplacements();
669
670 if (q->isComponentComplete()) {
671 _q_updateItemWidths();
672 _q_updateItemHeights();
673 }
674}
675
676void QQuickTumblerPrivate::warnAboutIncorrectContentItem()
677{
678 Q_Q(QQuickTumbler);
679 qmlWarning(me: q) << "Tumbler: contentItem must contain either a PathView or a ListView";
680}
681
682void QQuickTumblerPrivate::syncCurrentIndex()
683{
684 const int actualViewIndex = view->property(name: "currentIndex").toInt();
685 Q_Q(QQuickTumbler);
686
687 const bool isPendingCurrentIndex = pendingCurrentIndex != -1;
688 const int indexToSet = isPendingCurrentIndex ? pendingCurrentIndex : currentIndex;
689
690 // Nothing to do.
691 if (actualViewIndex == indexToSet) {
692 setPendingCurrentIndex(-1);
693 return;
694 }
695
696 // actualViewIndex might be 0 or -1 for PathView and ListView respectively,
697 // but we always use -1 for that.
698 if (q->count() == 0 && actualViewIndex <= 0)
699 return;
700
701 ignoreCurrentIndexChanges = true;
702 view->setProperty(name: "currentIndex", value: QVariant(indexToSet));
703 ignoreCurrentIndexChanges = false;
704
705 if (view->property(name: "currentIndex").toInt() == indexToSet)
706 setPendingCurrentIndex(-1);
707 else if (isPendingCurrentIndex)
708 q->polish();
709}
710
711void QQuickTumblerPrivate::setPendingCurrentIndex(int index)
712{
713 qCDebug(lcTumbler) << "setting pendingCurrentIndex to" << index;
714 pendingCurrentIndex = index;
715}
716
717QString QQuickTumblerPrivate::propertyChangeReasonToString(
718 QQuickTumblerPrivate::PropertyChangeReason changeReason)
719{
720 return changeReason == UserChange ? QStringLiteral("UserChange") : QStringLiteral("InternalChange");
721}
722
723void QQuickTumblerPrivate::setCurrentIndex(int newCurrentIndex,
724 QQuickTumblerPrivate::PropertyChangeReason changeReason)
725{
726 Q_Q(QQuickTumbler);
727 qCDebug(lcTumbler).nospace() << "setting currentIndex to " << newCurrentIndex
728 << ", old currentIndex was " << currentIndex
729 << ", changeReason is " << propertyChangeReasonToString(changeReason);
730 if (newCurrentIndex == currentIndex || newCurrentIndex < -1)
731 return;
732
733 if (!q->isComponentComplete()) {
734 // Views can't set currentIndex until they're ready.
735 qCDebug(lcTumbler) << "we're not complete; setting pendingCurrentIndex instead";
736 setPendingCurrentIndex(newCurrentIndex);
737 return;
738 }
739
740 if (modelBeingSet && changeReason == UserChange) {
741 // If modelBeingSet is true and the user set the currentIndex,
742 // the model is in the process of being set and the user has set
743 // the currentIndex in onModelChanged. We have to queue the currentIndex
744 // change until we're ready.
745 qCDebug(lcTumbler) << "a model is being set; setting pendingCurrentIndex instead";
746 setPendingCurrentIndex(newCurrentIndex);
747 return;
748 }
749
750 // -1 doesn't make sense for a non-empty Tumbler, because unlike
751 // e.g. ListView, there's always one item selected.
752 // Wait until the component has finished before enforcing this rule, though,
753 // because the count might not be known yet.
754 if ((count > 0 && newCurrentIndex == -1) || (newCurrentIndex >= count)) {
755 return;
756 }
757
758 // The view might not have been created yet, as is the case
759 // if you create a Tumbler component and pass e.g. { currentIndex: 2 }
760 // to createObject().
761 if (view) {
762 // Only actually set our currentIndex if the view was able to set theirs.
763 bool couldSet = false;
764 if (count == 0 && newCurrentIndex == -1) {
765 // PathView insists on using 0 as the currentIndex when there are no items.
766 couldSet = true;
767 } else {
768 ignoreCurrentIndexChanges = true;
769 ignoreSignals = true;
770 view->setProperty(name: "currentIndex", value: newCurrentIndex);
771 ignoreSignals = false;
772 ignoreCurrentIndexChanges = false;
773
774 couldSet = view->property(name: "currentIndex").toInt() == newCurrentIndex;
775 }
776
777 if (couldSet) {
778 // The view's currentIndex might not have actually changed, but ours has,
779 // and that's what user code sees.
780 currentIndex = newCurrentIndex;
781 emit q->currentIndexChanged();
782 }
783
784 qCDebug(lcTumbler) << "view's currentIndex is now" << view->property(name: "currentIndex").toInt()
785 << "and ours is" << currentIndex;
786 }
787}
788
789void QQuickTumblerPrivate::setCount(int newCount)
790{
791 qCDebug(lcTumbler).nospace() << "setting count to " << newCount
792 << ", old count was " << count;
793 if (newCount == count)
794 return;
795
796 count = newCount;
797
798 Q_Q(QQuickTumbler);
799 setWrapBasedOnCount();
800
801 emit q->countChanged();
802}
803
804void QQuickTumblerPrivate::setWrapBasedOnCount()
805{
806 if (count == 0 || explicitWrap || modelBeingSet)
807 return;
808
809 setWrap(shouldWrap: count >= visibleItemCount, propertyState: QQml::PropertyUtils::State::ImplicitlySet);
810}
811
812void QQuickTumblerPrivate::setWrap(bool shouldWrap, QQml::PropertyUtils::State propertyState)
813{
814 if (isExplicitlySet(propertyState))
815 explicitWrap = true;
816 qCDebug(lcTumbler) << "setting wrap to" << shouldWrap << "- explicit?" << explicitWrap;
817
818 Q_Q(QQuickTumbler);
819 if (q->isComponentComplete() && shouldWrap == wrap)
820 return;
821
822 // Since we use the currentIndex of the contentItem directly, we must
823 // ensure that we keep track of the currentIndex so it doesn't get lost
824 // between view changes.
825 const int oldCurrentIndex = currentIndex;
826
827 // changing wrap can change the implicit flickDeceleration
828 const qreal oldFlickDeceleration = effectiveFlickDeceleration();
829
830 disconnectFromView();
831
832 wrap = shouldWrap;
833
834 // New views will set their currentIndex upon creation, which we'd otherwise
835 // take as the correct one, so we must ignore them.
836 ignoreCurrentIndexChanges = true;
837
838 // This will cause the view to be created if our contentItem is a TumblerView.
839 emit q->wrapChanged();
840
841 ignoreCurrentIndexChanges = false;
842
843 // If isComponentComplete() is true, we require a contentItem. If it's not
844 // true, it might not have been created yet, so we wait until
845 // componentComplete() is called.
846 //
847 // When the contentItem (usually QQuickTumblerView) has been created, we
848 // can start determining its type, etc. If the delegates use attached
849 // properties, this will have already been called, in which case it will
850 // return early. If the delegate doesn't use attached properties, we need
851 // to call it here.
852 if (q->isComponentComplete() || contentItem)
853 setupViewData(contentItem);
854
855 setCurrentIndex(newCurrentIndex: oldCurrentIndex);
856
857 if (effectiveFlickDeceleration() != oldFlickDeceleration)
858 emit q->flickDecelerationChanged();
859}
860
861qreal QQuickTumblerPrivate::effectiveFlickDeceleration() const
862{
863 if (flickDeceleration == 0.0)
864 return defaultFlickDeceleration(wrap);
865 return flickDeceleration;
866}
867
868void QQuickTumblerPrivate::beginSetModel()
869{
870 modelBeingSet = true;
871}
872
873void QQuickTumblerPrivate::endSetModel()
874{
875 modelBeingSet = false;
876 setWrapBasedOnCount();
877}
878
879void QQuickTumbler::keyPressEvent(QKeyEvent *event)
880{
881 QQuickControl::keyPressEvent(event);
882
883 Q_D(QQuickTumbler);
884 if (event->isAutoRepeat() || !d->view)
885 return;
886
887 if (event->key() == Qt::Key_Up) {
888 QMetaObject::invokeMethod(obj: d->view, member: "decrementCurrentIndex");
889 } else if (event->key() == Qt::Key_Down) {
890 QMetaObject::invokeMethod(obj: d->view, member: "incrementCurrentIndex");
891 }
892}
893
894void QQuickTumbler::updatePolish()
895{
896 Q_D(QQuickTumbler);
897 if (d->pendingCurrentIndex != -1) {
898 // Update our count, as ignoreSignals might have been true
899 // when _q_onViewCountChanged() was last called.
900 d->setCount(d->view->property(name: "count").toInt());
901
902 // If the count is still 0, it's not going to happen.
903 if (d->count == 0) {
904 d->setPendingCurrentIndex(-1);
905 return;
906 }
907
908 // If there is a pending currentIndex at this stage, it means that
909 // the view wouldn't set our currentIndex in _q_onViewCountChanged
910 // because it wasn't ready. Try one last time here.
911 d->setCurrentIndex(newCurrentIndex: d->pendingCurrentIndex);
912
913 if (d->currentIndex != d->pendingCurrentIndex && d->currentIndex == -1) {
914 // If we *still* couldn't set it, it's probably invalid.
915 // See if we can at least enforce our rule of "non-negative currentIndex when count > 0" instead.
916 d->setCurrentIndex(newCurrentIndex: 0);
917 }
918
919 d->setPendingCurrentIndex(-1);
920 }
921}
922
923QFont QQuickTumbler::defaultFont() const
924{
925 return QQuickTheme::font(scope: QQuickTheme::Tumbler);
926}
927
928void QQuickTumblerAttachedPrivate::init(QQuickItem *delegateItem)
929{
930 Q_Q(QQuickTumblerAttached);
931 if (!delegateItem->parentItem()) {
932 qmlWarning(me: q) << "Tumbler: attached properties must be accessed through a delegate item that has a parent";
933 return;
934 }
935
936 QVariant indexContextProperty = qmlContext(delegateItem)->contextProperty(QStringLiteral("index"));
937 if (!indexContextProperty.isValid()) {
938 qmlWarning(me: q) << "Tumbler: attempting to access attached property on item without an \"index\" property";
939 return;
940 }
941
942 index = indexContextProperty.toInt();
943
944 QQuickItem *parentItem = delegateItem;
945 while ((parentItem = parentItem->parentItem())) {
946 if ((tumbler = qobject_cast<QQuickTumbler*>(object: parentItem)))
947 break;
948 }
949}
950
951void QQuickTumblerAttachedPrivate::calculateDisplacement()
952{
953 const qreal previousDisplacement = displacement;
954 displacement = 0;
955
956 if (!tumbler) {
957 // Can happen if the attached properties are accessed on the wrong type of item or the tumbler was destroyed.
958 // We don't want to emit the change signal though, as this could cause warnings about Tumbler.tumbler being null.
959 return;
960 }
961
962 // Can happen if there is no ListView or PathView within the contentItem.
963 QQuickTumblerPrivate *tumblerPrivate = QQuickTumblerPrivate::get(tumbler);
964 if (!tumblerPrivate->viewContentItem) {
965 emitIfDisplacementChanged(oldDisplacement: previousDisplacement, newDisplacement: displacement);
966 return;
967 }
968
969 // The attached property gets created before our count is updated, so just cheat here
970 // to avoid having to listen to count changes.
971 const int count = tumblerPrivate->view->property(name: "count").toInt();
972 // This can happen in tests, so it may happen in normal usage too.
973 if (count == 0) {
974 emitIfDisplacementChanged(oldDisplacement: previousDisplacement, newDisplacement: displacement);
975 return;
976 }
977
978 if (tumblerPrivate->viewContentItemType == QQuickTumblerPrivate::PathViewContentItem) {
979 const qreal offset = tumblerPrivate->viewOffset;
980
981 displacement = count > 1 ? count - index - offset : 0;
982 // Don't add 1 if count <= visibleItemCount
983 const int visibleItems = tumbler->visibleItemCount();
984 const int halfVisibleItems = visibleItems / 2 + (visibleItems < count ? 1 : 0);
985 if (displacement > halfVisibleItems)
986 displacement -= count;
987 else if (displacement < -halfVisibleItems)
988 displacement += count;
989 } else {
990 const qreal contentY = tumblerPrivate->viewContentY;
991 const qreal delegateH = delegateHeight(tumbler);
992 const qreal preferredHighlightBegin = tumblerPrivate->view->property(name: "preferredHighlightBegin").toReal();
993 const qreal itemY = qobject_cast<QQuickItem*>(o: parent)->y();
994 qreal currentItemY = 0;
995 auto currentItem = tumblerPrivate->view->property(name: "currentItem").value<QQuickItem*>();
996 if (currentItem)
997 currentItemY = currentItem->y();
998 // Start from the y position of the current item.
999 const qreal topOfCurrentItemInViewport = currentItemY - contentY;
1000 // Then, calculate the distance between it and the preferredHighlightBegin.
1001 const qreal relativePositionToPreferredHighlightBegin = topOfCurrentItemInViewport - preferredHighlightBegin;
1002 // Next, calculate the distance between us and the current item.
1003 const qreal distanceFromCurrentItem = currentItemY - itemY;
1004 const qreal displacementInPixels = distanceFromCurrentItem - relativePositionToPreferredHighlightBegin;
1005 // Convert it from pixels to a floating point index.
1006 displacement = displacementInPixels / delegateH;
1007 }
1008
1009 emitIfDisplacementChanged(oldDisplacement: previousDisplacement, newDisplacement: displacement);
1010}
1011
1012void QQuickTumblerAttachedPrivate::emitIfDisplacementChanged(qreal oldDisplacement, qreal newDisplacement)
1013{
1014 Q_Q(QQuickTumblerAttached);
1015 if (newDisplacement != oldDisplacement)
1016 emit q->displacementChanged();
1017}
1018
1019QQuickTumblerAttached::QQuickTumblerAttached(QObject *parent)
1020 : QObject(*(new QQuickTumblerAttachedPrivate), parent)
1021{
1022 Q_D(QQuickTumblerAttached);
1023 QQuickItem *delegateItem = qobject_cast<QQuickItem *>(o: parent);
1024 if (delegateItem)
1025 d->init(delegateItem);
1026 else if (parent)
1027 qmlWarning(me: parent) << "Tumbler: attached properties of Tumbler must be accessed through a delegate item";
1028
1029 if (d->tumbler) {
1030 // When the Tumbler is completed, wrapChanged() is emitted to let QQuickTumblerView
1031 // know that it can create the view. The view itself might instantiate delegates
1032 // that use attached properties. At this point, setupViewData() hasn't been called yet
1033 // (it's called on the next line in componentComplete()), so we call it here so that
1034 // we have access to the view.
1035 QQuickTumblerPrivate *tumblerPrivate = QQuickTumblerPrivate::get(tumbler: d->tumbler);
1036 tumblerPrivate->setupViewData(tumblerPrivate->contentItem);
1037
1038 if (delegateItem && delegateItem->parentItem() == tumblerPrivate->viewContentItem) {
1039 // This item belongs to the "new" view, meaning that the tumbler's contentItem
1040 // was probably assigned declaratively. If they're not equal, calling
1041 // calculateDisplacement() would use the old contentItem data, which is bad.
1042 d->calculateDisplacement();
1043 }
1044 }
1045}
1046
1047/*!
1048 \qmlattachedproperty Tumbler QtQuick.Controls::Tumbler::tumbler
1049 \readonly
1050
1051 This attached property holds the tumbler. The property can be attached to
1052 a tumbler delegate. The value is \c null if the item is not a tumbler delegate.
1053*/
1054QQuickTumbler *QQuickTumblerAttached::tumbler() const
1055{
1056 Q_D(const QQuickTumblerAttached);
1057 return d->tumbler;
1058}
1059
1060/*!
1061 \qmlattachedproperty real QtQuick.Controls::Tumbler::displacement
1062 \readonly
1063
1064 This attached property holds a value from \c {-visibleItemCount / 2} to
1065 \c {visibleItemCount / 2}, which represents how far away this item is from
1066 being the current item, with \c 0 being completely current.
1067
1068 For example, the item below will be 40% opaque when it is not the current item,
1069 and transition to 100% opacity when it becomes the current item:
1070
1071 \code
1072 delegate: Text {
1073 text: modelData
1074 opacity: 0.4 + Math.max(0, 1 - Math.abs(Tumbler.displacement)) * 0.6
1075 }
1076 \endcode
1077*/
1078qreal QQuickTumblerAttached::displacement() const
1079{
1080 Q_D(const QQuickTumblerAttached);
1081 return d->displacement;
1082}
1083
1084QT_END_NAMESPACE
1085
1086#include "moc_qquicktumbler_p.cpp"
1087

source code of qtdeclarative/src/quicktemplates/qquicktumbler.cpp