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

source code of qtquickcontrols2/src/quicktemplates2/qquicktumbler.cpp