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