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//! \nativetype 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 Q_D(QQuickTumbler);
263 d->setSizePolicy(horizontalPolicy: QLayoutPolicy::Preferred, verticalPolicy: QLayoutPolicy::Preferred);
264
265 setActiveFocusOnTab(true);
266
267 connect(sender: this, SIGNAL(leftPaddingChanged()), receiver: this, SLOT(_q_updateItemWidths()));
268 connect(sender: this, SIGNAL(rightPaddingChanged()), receiver: this, SLOT(_q_updateItemWidths()));
269 connect(sender: this, SIGNAL(topPaddingChanged()), receiver: this, SLOT(_q_updateItemHeights()));
270 connect(sender: this, SIGNAL(bottomPaddingChanged()), receiver: this, SLOT(_q_updateItemHeights()));
271}
272
273QQuickTumbler::~QQuickTumbler()
274{
275 Q_D(QQuickTumbler);
276 // Ensure that the item change listener is removed.
277 d->disconnectFromView();
278}
279
280/*!
281 \qmlproperty variant QtQuick.Controls::Tumbler::model
282
283 This property holds the model that provides data for this tumbler.
284*/
285QVariant QQuickTumbler::model() const
286{
287 Q_D(const QQuickTumbler);
288 return d->model;
289}
290
291void QQuickTumbler::setModel(const QVariant &model)
292{
293 Q_D(QQuickTumbler);
294 if (model == d->model)
295 return;
296
297 d->beginSetModel();
298
299 d->model = model;
300 emit modelChanged();
301
302 d->endSetModel();
303
304 if (d->view && d->currentIndexSetDuringModelChange) {
305 const int viewCurrentIndex = d->view->property(name: "currentIndex").toInt();
306 if (viewCurrentIndex != d->currentIndex)
307 d->view->setProperty(name: "currentIndex", value: d->currentIndex);
308 }
309
310 d->currentIndexSetDuringModelChange = false;
311
312 // Don't try to correct the currentIndex if count() isn't known yet.
313 // We can check in setupViewData() instead.
314 if (isComponentComplete() && d->view && count() == 0)
315 d->setCurrentIndex(newCurrentIndex: -1);
316}
317
318/*!
319 \qmlproperty int QtQuick.Controls::Tumbler::count
320 \readonly
321
322 This property holds the number of items in the model.
323*/
324int QQuickTumbler::count() const
325{
326 Q_D(const QQuickTumbler);
327 return d->count;
328}
329
330/*!
331 \qmlproperty int QtQuick.Controls::Tumbler::currentIndex
332
333 This property holds the index of the current item.
334
335 The value of this property is \c -1 when \l count is equal to \c 0. In all
336 other cases, it will be greater than or equal to \c 0.
337
338 \sa currentItem, positionViewAtIndex()
339*/
340int QQuickTumbler::currentIndex() const
341{
342 Q_D(const QQuickTumbler);
343 return d->currentIndex;
344}
345
346void QQuickTumbler::setCurrentIndex(int currentIndex)
347{
348 Q_D(QQuickTumbler);
349 if (d->modelBeingSet)
350 d->currentIndexSetDuringModelChange = true;
351 d->setCurrentIndex(newCurrentIndex: currentIndex, changeReason: QQuickTumblerPrivate::UserChange);
352}
353
354/*!
355 \qmlproperty Item QtQuick.Controls::Tumbler::currentItem
356 \readonly
357
358 This property holds the item at the current index.
359
360 \sa currentIndex, positionViewAtIndex()
361*/
362QQuickItem *QQuickTumbler::currentItem() const
363{
364 Q_D(const QQuickTumbler);
365 return d->view ? d->view->property(name: "currentItem").value<QQuickItem*>() : nullptr;
366}
367
368/*!
369 \qmlproperty Component QtQuick.Controls::Tumbler::delegate
370
371 This property holds the delegate used to display each item.
372*/
373QQmlComponent *QQuickTumbler::delegate() const
374{
375 Q_D(const QQuickTumbler);
376 return d->delegate;
377}
378
379void QQuickTumbler::setDelegate(QQmlComponent *delegate)
380{
381 Q_D(QQuickTumbler);
382 if (delegate == d->delegate)
383 return;
384
385 d->delegate = delegate;
386 emit delegateChanged();
387}
388
389/*!
390 \qmlproperty int QtQuick.Controls::Tumbler::visibleItemCount
391
392 This property holds the number of items visible in the tumbler. It must be
393 an odd number, as the current item is always vertically centered.
394*/
395int QQuickTumbler::visibleItemCount() const
396{
397 Q_D(const QQuickTumbler);
398 return d->visibleItemCount;
399}
400
401void QQuickTumbler::setVisibleItemCount(int visibleItemCount)
402{
403 Q_D(QQuickTumbler);
404 if (visibleItemCount == d->visibleItemCount)
405 return;
406
407 d->visibleItemCount = visibleItemCount;
408 d->_q_updateItemHeights();
409 emit visibleItemCountChanged();
410}
411
412QQuickTumblerAttached *QQuickTumbler::qmlAttachedProperties(QObject *object)
413{
414 return new QQuickTumblerAttached(object);
415}
416
417/*!
418 \qmlproperty bool QtQuick.Controls::Tumbler::wrap
419 \since QtQuick.Controls 2.1 (Qt 5.8)
420
421 This property determines whether or not the tumbler wraps around when it
422 reaches the top or bottom.
423
424 The default value is \c false when \l count is less than
425 \l visibleItemCount, as it is simpler to interact with a non-wrapping Tumbler
426 when there are only a few items. To override this behavior, explicitly set
427 the value of this property. To return to the default behavior, set this
428 property to \c undefined.
429*/
430bool QQuickTumbler::wrap() const
431{
432 Q_D(const QQuickTumbler);
433 return d->wrap;
434}
435
436void QQuickTumbler::setWrap(bool wrap)
437{
438 Q_D(QQuickTumbler);
439 d->setWrap(shouldWrap: wrap, isExplicit: true);
440}
441
442void QQuickTumbler::resetWrap()
443{
444 Q_D(QQuickTumbler);
445 d->explicitWrap = false;
446 d->setWrapBasedOnCount();
447}
448
449/*!
450 \qmlproperty bool QtQuick.Controls::Tumbler::moving
451 \since QtQuick.Controls 2.2 (Qt 5.9)
452
453 This property describes whether the tumbler is currently moving, due to
454 the user either dragging or flicking it.
455*/
456bool QQuickTumbler::isMoving() const
457{
458 Q_D(const QQuickTumbler);
459 return d->view && d->view->property(name: "moving").toBool();
460}
461
462/*!
463 \qmlmethod void QtQuick.Controls::Tumbler::positionViewAtIndex(int index, PositionMode mode)
464 \since QtQuick.Controls 2.5 (Qt 5.12)
465
466 Positions the view so that the \a index is at the position specified by \a mode.
467
468 For example:
469
470 \code
471 positionViewAtIndex(10, Tumbler.Center)
472 \endcode
473
474 If \l wrap is true (the default), the modes available to \l {PathView}'s
475 \l {PathView::}{positionViewAtIndex()} function
476 are available, otherwise the modes available to \l {ListView}'s
477 \l {ListView::}{positionViewAtIndex()} function
478 are available.
479
480 \note There is a known limitation that using \c Tumbler.Beginning when \l
481 wrap is \c true will result in the wrong item being positioned at the top
482 of view. As a workaround, pass \c {index - 1}.
483
484 \sa currentIndex
485*/
486void QQuickTumbler::positionViewAtIndex(int index, QQuickTumbler::PositionMode mode)
487{
488 Q_D(QQuickTumbler);
489 if (!d->view) {
490 d->warnAboutIncorrectContentItem();
491 return;
492 }
493
494 QMetaObject::invokeMethod(obj: d->view, member: "positionViewAtIndex", Q_ARG(int, index), Q_ARG(int, mode));
495}
496
497void QQuickTumbler::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
498{
499 Q_D(QQuickTumbler);
500
501 QQuickControl::geometryChange(newGeometry, oldGeometry);
502
503 d->_q_updateItemHeights();
504
505 if (newGeometry.width() != oldGeometry.width())
506 d->_q_updateItemWidths();
507}
508
509void QQuickTumbler::componentComplete()
510{
511 Q_D(QQuickTumbler);
512 qCDebug(lcTumbler) << "componentComplete()";
513 QQuickControl::componentComplete();
514
515 if (!d->view) {
516 // Force the view to be created.
517 qCDebug(lcTumbler) << "emitting wrapChanged() to force view to be created";
518 emit wrapChanged();
519 // Determine the type of view for attached properties, etc.
520 d->setupViewData(d->contentItem);
521 }
522
523 // If there was no contentItem or it was of an unsupported type,
524 // we don't have anything else to do.
525 if (!d->view)
526 return;
527
528 // Update item heights after we've populated the model,
529 // otherwise ignoreSignals will cause these functions to return early.
530 d->_q_updateItemHeights();
531 d->_q_updateItemWidths();
532 d->_q_onViewCountChanged();
533
534 qCDebug(lcTumbler) << "componentComplete() is done";
535}
536
537void QQuickTumbler::contentItemChange(QQuickItem *newItem, QQuickItem *oldItem)
538{
539 Q_D(QQuickTumbler);
540
541 QQuickControl::contentItemChange(newItem, oldItem);
542
543 if (oldItem)
544 d->disconnectFromView();
545
546 if (newItem) {
547 // We wait until wrap is set to that we know which type of view to create.
548 // If we try to set up the view too early, we'll issue warnings about it not existing.
549 if (isComponentComplete()) {
550 // Make sure we use the new content item and not the current one, as that won't
551 // be changed until after contentItemChange() has finished.
552 d->setupViewData(newItem);
553
554 d->_q_updateItemHeights();
555 d->_q_updateItemWidths();
556 }
557 }
558}
559
560void QQuickTumblerPrivate::disconnectFromView()
561{
562 Q_Q(QQuickTumbler);
563 if (!view) {
564 // If a custom content item is declared, it can happen that
565 // the original contentItem exists without the view etc. having been
566 // determined yet, and then this is called when the custom content item
567 // is eventually set.
568 return;
569 }
570
571 QObject::disconnect(sender: view, SIGNAL(currentIndexChanged()), receiver: q, SLOT(_q_onViewCurrentIndexChanged()));
572 QObject::disconnect(sender: view, SIGNAL(currentItemChanged()), receiver: q, SIGNAL(currentItemChanged()));
573 QObject::disconnect(sender: view, SIGNAL(countChanged()), receiver: q, SLOT(_q_onViewCountChanged()));
574 QObject::disconnect(sender: view, SIGNAL(movingChanged()), receiver: q, SIGNAL(movingChanged()));
575
576 if (viewContentItemType == PathViewContentItem)
577 QObject::disconnect(sender: view, SIGNAL(offsetChanged()), receiver: q, SLOT(_q_onViewOffsetChanged()));
578 else
579 QObject::disconnect(sender: view, SIGNAL(contentYChanged()), receiver: q, SLOT(_q_onViewContentYChanged()));
580
581 QQuickItemPrivate *oldViewContentItemPrivate = QQuickItemPrivate::get(item: viewContentItem);
582 oldViewContentItemPrivate->removeItemChangeListener(this, types: QQuickItemPrivate::Children | QQuickItemPrivate::Geometry);
583
584 resetViewData();
585}
586
587void QQuickTumblerPrivate::setupViewData(QQuickItem *newControlContentItem)
588{
589 // Don't do anything if we've already set up.
590 if (view)
591 return;
592
593 determineViewType(contentItem: newControlContentItem);
594
595 if (viewContentItemType == QQuickTumblerPrivate::NoContentItem)
596 return;
597
598 if (viewContentItemType == QQuickTumblerPrivate::UnsupportedContentItemType) {
599 warnAboutIncorrectContentItem();
600 return;
601 }
602
603 Q_Q(QQuickTumbler);
604 QObject::connect(sender: view, SIGNAL(currentIndexChanged()), receiver: q, SLOT(_q_onViewCurrentIndexChanged()));
605 QObject::connect(sender: view, SIGNAL(currentItemChanged()), receiver: q, SIGNAL(currentItemChanged()));
606 QObject::connect(sender: view, SIGNAL(countChanged()), receiver: q, SLOT(_q_onViewCountChanged()));
607 QObject::connect(sender: view, SIGNAL(movingChanged()), receiver: q, SIGNAL(movingChanged()));
608
609 if (viewContentItemType == PathViewContentItem) {
610 QObject::connect(sender: view, SIGNAL(offsetChanged()), receiver: q, SLOT(_q_onViewOffsetChanged()));
611 _q_onViewOffsetChanged();
612 } else {
613 QObject::connect(sender: view, SIGNAL(contentYChanged()), receiver: q, SLOT(_q_onViewContentYChanged()));
614 _q_onViewContentYChanged();
615 }
616
617 QQuickItemPrivate *viewContentItemPrivate = QQuickItemPrivate::get(item: viewContentItem);
618 viewContentItemPrivate->addItemChangeListener(listener: this, types: QQuickItemPrivate::Children | QQuickItemPrivate::Geometry);
619
620 // Sync the view's currentIndex with ours.
621 syncCurrentIndex();
622
623 calculateDisplacements();
624}
625
626void QQuickTumblerPrivate::warnAboutIncorrectContentItem()
627{
628 Q_Q(QQuickTumbler);
629 qmlWarning(me: q) << "Tumbler: contentItem must contain either a PathView or a ListView";
630}
631
632void QQuickTumblerPrivate::syncCurrentIndex()
633{
634 const int actualViewIndex = view->property(name: "currentIndex").toInt();
635 Q_Q(QQuickTumbler);
636
637 const bool isPendingCurrentIndex = pendingCurrentIndex != -1;
638 const int indexToSet = isPendingCurrentIndex ? pendingCurrentIndex : currentIndex;
639
640 // Nothing to do.
641 if (actualViewIndex == indexToSet) {
642 setPendingCurrentIndex(-1);
643 return;
644 }
645
646 // actualViewIndex might be 0 or -1 for PathView and ListView respectively,
647 // but we always use -1 for that.
648 if (q->count() == 0 && actualViewIndex <= 0)
649 return;
650
651 ignoreCurrentIndexChanges = true;
652 view->setProperty(name: "currentIndex", value: QVariant(indexToSet));
653 ignoreCurrentIndexChanges = false;
654
655 if (view->property(name: "currentIndex").toInt() == indexToSet)
656 setPendingCurrentIndex(-1);
657 else if (isPendingCurrentIndex)
658 q->polish();
659}
660
661void QQuickTumblerPrivate::setPendingCurrentIndex(int index)
662{
663 qCDebug(lcTumbler) << "setting pendingCurrentIndex to" << index;
664 pendingCurrentIndex = index;
665}
666
667QString QQuickTumblerPrivate::propertyChangeReasonToString(
668 QQuickTumblerPrivate::PropertyChangeReason changeReason)
669{
670 return changeReason == UserChange ? QStringLiteral("UserChange") : QStringLiteral("InternalChange");
671}
672
673void QQuickTumblerPrivate::setCurrentIndex(int newCurrentIndex,
674 QQuickTumblerPrivate::PropertyChangeReason changeReason)
675{
676 Q_Q(QQuickTumbler);
677 qCDebug(lcTumbler).nospace() << "setting currentIndex to " << newCurrentIndex
678 << ", old currentIndex was " << currentIndex
679 << ", changeReason is " << propertyChangeReasonToString(changeReason);
680 if (newCurrentIndex == currentIndex || newCurrentIndex < -1)
681 return;
682
683 if (!q->isComponentComplete()) {
684 // Views can't set currentIndex until they're ready.
685 qCDebug(lcTumbler) << "we're not complete; setting pendingCurrentIndex instead";
686 setPendingCurrentIndex(newCurrentIndex);
687 return;
688 }
689
690 if (modelBeingSet && changeReason == UserChange) {
691 // If modelBeingSet is true and the user set the currentIndex,
692 // the model is in the process of being set and the user has set
693 // the currentIndex in onModelChanged. We have to queue the currentIndex
694 // change until we're ready.
695 qCDebug(lcTumbler) << "a model is being set; setting pendingCurrentIndex instead";
696 setPendingCurrentIndex(newCurrentIndex);
697 return;
698 }
699
700 // -1 doesn't make sense for a non-empty Tumbler, because unlike
701 // e.g. ListView, there's always one item selected.
702 // Wait until the component has finished before enforcing this rule, though,
703 // because the count might not be known yet.
704 if ((count > 0 && newCurrentIndex == -1) || (newCurrentIndex >= count)) {
705 return;
706 }
707
708 // The view might not have been created yet, as is the case
709 // if you create a Tumbler component and pass e.g. { currentIndex: 2 }
710 // to createObject().
711 if (view) {
712 // Only actually set our currentIndex if the view was able to set theirs.
713 bool couldSet = false;
714 if (count == 0 && newCurrentIndex == -1) {
715 // PathView insists on using 0 as the currentIndex when there are no items.
716 couldSet = true;
717 } else {
718 ignoreCurrentIndexChanges = true;
719 ignoreSignals = true;
720 view->setProperty(name: "currentIndex", value: newCurrentIndex);
721 ignoreSignals = false;
722 ignoreCurrentIndexChanges = false;
723
724 couldSet = view->property(name: "currentIndex").toInt() == newCurrentIndex;
725 }
726
727 if (couldSet) {
728 // The view's currentIndex might not have actually changed, but ours has,
729 // and that's what user code sees.
730 currentIndex = newCurrentIndex;
731 emit q->currentIndexChanged();
732 }
733
734 qCDebug(lcTumbler) << "view's currentIndex is now" << view->property(name: "currentIndex").toInt()
735 << "and ours is" << currentIndex;
736 }
737}
738
739void QQuickTumblerPrivate::setCount(int newCount)
740{
741 qCDebug(lcTumbler).nospace() << "setting count to " << newCount
742 << ", old count was " << count;
743 if (newCount == count)
744 return;
745
746 count = newCount;
747
748 Q_Q(QQuickTumbler);
749 setWrapBasedOnCount();
750
751 emit q->countChanged();
752}
753
754void QQuickTumblerPrivate::setWrapBasedOnCount()
755{
756 if (count == 0 || explicitWrap || modelBeingSet)
757 return;
758
759 setWrap(shouldWrap: count >= visibleItemCount, isExplicit: false);
760}
761
762void QQuickTumblerPrivate::setWrap(bool shouldWrap, bool isExplicit)
763{
764 qCDebug(lcTumbler) << "setting wrap to" << shouldWrap << "- explicit?" << isExplicit;
765 if (isExplicit)
766 explicitWrap = true;
767
768 Q_Q(QQuickTumbler);
769 if (q->isComponentComplete() && shouldWrap == wrap)
770 return;
771
772 // Since we use the currentIndex of the contentItem directly, we must
773 // ensure that we keep track of the currentIndex so it doesn't get lost
774 // between view changes.
775 const int oldCurrentIndex = currentIndex;
776
777 disconnectFromView();
778
779 wrap = shouldWrap;
780
781 // New views will set their currentIndex upon creation, which we'd otherwise
782 // take as the correct one, so we must ignore them.
783 ignoreCurrentIndexChanges = true;
784
785 // This will cause the view to be created if our contentItem is a TumblerView.
786 emit q->wrapChanged();
787
788 ignoreCurrentIndexChanges = false;
789
790 // If isComponentComplete() is true, we require a contentItem. If it's not
791 // true, it might not have been created yet, so we wait until
792 // componentComplete() is called.
793 //
794 // When the contentItem (usually QQuickTumblerView) has been created, we
795 // can start determining its type, etc. If the delegates use attached
796 // properties, this will have already been called, in which case it will
797 // return early. If the delegate doesn't use attached properties, we need
798 // to call it here.
799 if (q->isComponentComplete() || contentItem)
800 setupViewData(contentItem);
801
802 setCurrentIndex(newCurrentIndex: oldCurrentIndex);
803}
804
805void QQuickTumblerPrivate::beginSetModel()
806{
807 modelBeingSet = true;
808}
809
810void QQuickTumblerPrivate::endSetModel()
811{
812 modelBeingSet = false;
813 setWrapBasedOnCount();
814}
815
816void QQuickTumbler::keyPressEvent(QKeyEvent *event)
817{
818 QQuickControl::keyPressEvent(event);
819
820 Q_D(QQuickTumbler);
821 if (event->isAutoRepeat() || !d->view)
822 return;
823
824 if (event->key() == Qt::Key_Up) {
825 QMetaObject::invokeMethod(obj: d->view, member: "decrementCurrentIndex");
826 } else if (event->key() == Qt::Key_Down) {
827 QMetaObject::invokeMethod(obj: d->view, member: "incrementCurrentIndex");
828 }
829}
830
831void QQuickTumbler::updatePolish()
832{
833 Q_D(QQuickTumbler);
834 if (d->pendingCurrentIndex != -1) {
835 // Update our count, as ignoreSignals might have been true
836 // when _q_onViewCountChanged() was last called.
837 d->setCount(d->view->property(name: "count").toInt());
838
839 // If the count is still 0, it's not going to happen.
840 if (d->count == 0) {
841 d->setPendingCurrentIndex(-1);
842 return;
843 }
844
845 // If there is a pending currentIndex at this stage, it means that
846 // the view wouldn't set our currentIndex in _q_onViewCountChanged
847 // because it wasn't ready. Try one last time here.
848 d->setCurrentIndex(newCurrentIndex: d->pendingCurrentIndex);
849
850 if (d->currentIndex != d->pendingCurrentIndex && d->currentIndex == -1) {
851 // If we *still* couldn't set it, it's probably invalid.
852 // See if we can at least enforce our rule of "non-negative currentIndex when count > 0" instead.
853 d->setCurrentIndex(newCurrentIndex: 0);
854 }
855
856 d->setPendingCurrentIndex(-1);
857 }
858}
859
860QFont QQuickTumbler::defaultFont() const
861{
862 return QQuickTheme::font(scope: QQuickTheme::Tumbler);
863}
864
865void QQuickTumblerAttachedPrivate::init(QQuickItem *delegateItem)
866{
867 Q_Q(QQuickTumblerAttached);
868 if (!delegateItem->parentItem()) {
869 qmlWarning(me: q) << "Tumbler: attached properties must be accessed through a delegate item that has a parent";
870 return;
871 }
872
873 QVariant indexContextProperty = qmlContext(delegateItem)->contextProperty(QStringLiteral("index"));
874 if (!indexContextProperty.isValid()) {
875 qmlWarning(me: q) << "Tumbler: attempting to access attached property on item without an \"index\" property";
876 return;
877 }
878
879 index = indexContextProperty.toInt();
880
881 QQuickItem *parentItem = delegateItem;
882 while ((parentItem = parentItem->parentItem())) {
883 if ((tumbler = qobject_cast<QQuickTumbler*>(object: parentItem)))
884 break;
885 }
886}
887
888void QQuickTumblerAttachedPrivate::calculateDisplacement()
889{
890 const qreal previousDisplacement = displacement;
891 displacement = 0;
892
893 if (!tumbler) {
894 // Can happen if the attached properties are accessed on the wrong type of item or the tumbler was destroyed.
895 // We don't want to emit the change signal though, as this could cause warnings about Tumbler.tumbler being null.
896 return;
897 }
898
899 // Can happen if there is no ListView or PathView within the contentItem.
900 QQuickTumblerPrivate *tumblerPrivate = QQuickTumblerPrivate::get(tumbler);
901 if (!tumblerPrivate->viewContentItem) {
902 emitIfDisplacementChanged(oldDisplacement: previousDisplacement, newDisplacement: displacement);
903 return;
904 }
905
906 // The attached property gets created before our count is updated, so just cheat here
907 // to avoid having to listen to count changes.
908 const int count = tumblerPrivate->view->property(name: "count").toInt();
909 // This can happen in tests, so it may happen in normal usage too.
910 if (count == 0) {
911 emitIfDisplacementChanged(oldDisplacement: previousDisplacement, newDisplacement: displacement);
912 return;
913 }
914
915 if (tumblerPrivate->viewContentItemType == QQuickTumblerPrivate::PathViewContentItem) {
916 const qreal offset = tumblerPrivate->viewOffset;
917
918 displacement = count > 1 ? count - index - offset : 0;
919 // Don't add 1 if count <= visibleItemCount
920 const int visibleItems = tumbler->visibleItemCount();
921 const int halfVisibleItems = visibleItems / 2 + (visibleItems < count ? 1 : 0);
922 if (displacement > halfVisibleItems)
923 displacement -= count;
924 else if (displacement < -halfVisibleItems)
925 displacement += count;
926 } else {
927 const qreal contentY = tumblerPrivate->viewContentY;
928 const qreal delegateH = delegateHeight(tumbler);
929 const qreal preferredHighlightBegin = tumblerPrivate->view->property(name: "preferredHighlightBegin").toReal();
930 const qreal itemY = qobject_cast<QQuickItem*>(o: parent)->y();
931 qreal currentItemY = 0;
932 auto currentItem = tumblerPrivate->view->property(name: "currentItem").value<QQuickItem*>();
933 if (currentItem)
934 currentItemY = currentItem->y();
935 // Start from the y position of the current item.
936 const qreal topOfCurrentItemInViewport = currentItemY - contentY;
937 // Then, calculate the distance between it and the preferredHighlightBegin.
938 const qreal relativePositionToPreferredHighlightBegin = topOfCurrentItemInViewport - preferredHighlightBegin;
939 // Next, calculate the distance between us and the current item.
940 const qreal distanceFromCurrentItem = currentItemY - itemY;
941 const qreal displacementInPixels = distanceFromCurrentItem - relativePositionToPreferredHighlightBegin;
942 // Convert it from pixels to a floating point index.
943 displacement = displacementInPixels / delegateH;
944 }
945
946 emitIfDisplacementChanged(oldDisplacement: previousDisplacement, newDisplacement: displacement);
947}
948
949void QQuickTumblerAttachedPrivate::emitIfDisplacementChanged(qreal oldDisplacement, qreal newDisplacement)
950{
951 Q_Q(QQuickTumblerAttached);
952 if (newDisplacement != oldDisplacement)
953 emit q->displacementChanged();
954}
955
956QQuickTumblerAttached::QQuickTumblerAttached(QObject *parent)
957 : QObject(*(new QQuickTumblerAttachedPrivate), parent)
958{
959 Q_D(QQuickTumblerAttached);
960 QQuickItem *delegateItem = qobject_cast<QQuickItem *>(o: parent);
961 if (delegateItem)
962 d->init(delegateItem);
963 else if (parent)
964 qmlWarning(me: parent) << "Tumbler: attached properties of Tumbler must be accessed through a delegate item";
965
966 if (d->tumbler) {
967 // When the Tumbler is completed, wrapChanged() is emitted to let QQuickTumblerView
968 // know that it can create the view. The view itself might instantiate delegates
969 // that use attached properties. At this point, setupViewData() hasn't been called yet
970 // (it's called on the next line in componentComplete()), so we call it here so that
971 // we have access to the view.
972 QQuickTumblerPrivate *tumblerPrivate = QQuickTumblerPrivate::get(tumbler: d->tumbler);
973 tumblerPrivate->setupViewData(tumblerPrivate->contentItem);
974
975 if (delegateItem && delegateItem->parentItem() == tumblerPrivate->viewContentItem) {
976 // This item belongs to the "new" view, meaning that the tumbler's contentItem
977 // was probably assigned declaratively. If they're not equal, calling
978 // calculateDisplacement() would use the old contentItem data, which is bad.
979 d->calculateDisplacement();
980 }
981 }
982}
983
984/*!
985 \qmlattachedproperty Tumbler QtQuick.Controls::Tumbler::tumbler
986 \readonly
987
988 This attached property holds the tumbler. The property can be attached to
989 a tumbler delegate. The value is \c null if the item is not a tumbler delegate.
990*/
991QQuickTumbler *QQuickTumblerAttached::tumbler() const
992{
993 Q_D(const QQuickTumblerAttached);
994 return d->tumbler;
995}
996
997/*!
998 \qmlattachedproperty real QtQuick.Controls::Tumbler::displacement
999 \readonly
1000
1001 This attached property holds a value from \c {-visibleItemCount / 2} to
1002 \c {visibleItemCount / 2}, which represents how far away this item is from
1003 being the current item, with \c 0 being completely current.
1004
1005 For example, the item below will be 40% opaque when it is not the current item,
1006 and transition to 100% opacity when it becomes the current item:
1007
1008 \code
1009 delegate: Text {
1010 text: modelData
1011 opacity: 0.4 + Math.max(0, 1 - Math.abs(Tumbler.displacement)) * 0.6
1012 }
1013 \endcode
1014*/
1015qreal QQuickTumblerAttached::displacement() const
1016{
1017 Q_D(const QQuickTumblerAttached);
1018 return d->displacement;
1019}
1020
1021QT_END_NAMESPACE
1022
1023#include "moc_qquicktumbler_p.cpp"
1024

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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