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