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

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