diff options
author | Mitch Curtis <mitch.curtis@qt.io> | 2016-06-27 13:08:42 +0200 |
---|---|---|
committer | Mitch Curtis <mitch.curtis@qt.io> | 2016-07-01 06:19:11 +0000 |
commit | 2c4b2d488291e83bf1a6aac1d59351c1a6e901a3 (patch) | |
tree | 94743b9866e7c6e0a7d20827ff104d0c979eeea2 | |
parent | a5df6b69672afd780433ee8f43d343d1e2251fd4 (diff) |
Tumbler: make wrap property depend on count by default
[ChangeLog][Important Behavior Changes][Tumbler] Changed the default
value of wrap to be false when count is less than visibleItemCount.
Explicitly setting wrap overrides this behavior.
Change-Id: I0089f517a25a606625c245df52b0db5fd859ffc0
Task-number: QTBUG-53587
Reviewed-by: J-P Nurmi <jpnurmi@qt.io>
-rw-r--r-- | src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-timePicker.qml | 1 | ||||
-rw-r--r-- | src/quickcontrols2/qquicktumblerview.cpp | 51 | ||||
-rw-r--r-- | src/quickcontrols2/qquicktumblerview_p.h | 1 | ||||
-rw-r--r-- | src/quicktemplates2/qquicktumbler.cpp | 311 | ||||
-rw-r--r-- | src/quicktemplates2/qquicktumbler_p.h | 4 | ||||
-rw-r--r-- | src/quicktemplates2/qquicktumbler_p_p.h | 113 | ||||
-rw-r--r-- | src/quicktemplates2/quicktemplates2.pri | 1 | ||||
-rw-r--r-- | tests/auto/controls/data/tst_tumbler.qml | 114 |
8 files changed, 461 insertions, 135 deletions
diff --git a/src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-timePicker.qml b/src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-timePicker.qml index 52560bfa..86276266 100644 --- a/src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-timePicker.qml +++ b/src/imports/controls/doc/snippets/qtquickcontrols2-tumbler-timePicker.qml @@ -90,7 +90,6 @@ Rectangle { Tumbler { id: amPmTumbler - wrap: false model: ["AM", "PM"] delegate: delegateComponent } diff --git a/src/quickcontrols2/qquicktumblerview.cpp b/src/quickcontrols2/qquicktumblerview.cpp index 93906408..540a8dd1 100644 --- a/src/quickcontrols2/qquicktumblerview.cpp +++ b/src/quickcontrols2/qquicktumblerview.cpp @@ -42,6 +42,7 @@ #include <QtQuick/private/qquickpathview_p.h> #include <QtQuickTemplates2/private/qquicktumbler_p.h> +#include <QtQuickTemplates2/private/qquicktumbler_p_p.h> QT_BEGIN_NAMESPACE @@ -121,6 +122,8 @@ void QQuickTumblerView::createView() { Q_ASSERT(m_tumbler); + // We create a view regardless of whether or not we know + // the count yet, because we rely on the view to tell us the count. if (m_tumbler->wrap()) { if (m_listView) { delete m_listView; @@ -131,12 +134,7 @@ void QQuickTumblerView::createView() m_pathView = new QQuickPathView; QQmlEngine::setContextForObject(m_pathView, qmlContext(this)); QQml_setParent_noEvent(m_pathView, this); - // QQuickPathView::setPathItemCount() resets the offset animation, - // so we just skip the animation while constructing the view. - const int oldHighlightMoveDuration = m_pathView->highlightMoveDuration(); - m_pathView->setHighlightMoveDuration(0); m_pathView->setParentItem(this); - m_pathView->setModel(m_model); m_pathView->setPath(m_path); m_pathView->setDelegate(m_delegate); m_pathView->setPreferredHighlightBegin(0.5); @@ -145,8 +143,8 @@ void QQuickTumblerView::createView() // Give the view a size. updateView(); - - m_pathView->setHighlightMoveDuration(oldHighlightMoveDuration); + // Ensure that the model is set eventually. + polish(); } } else { if (m_pathView) { @@ -162,11 +160,12 @@ void QQuickTumblerView::createView() m_listView->setSnapMode(QQuickListView::SnapToItem); m_listView->setHighlightRangeMode(QQuickListView::StrictlyEnforceRange); m_listView->setClip(true); - m_listView->setModel(m_model); m_listView->setDelegate(m_delegate); // Give the view a size. updateView(); + // Ensure that the model is set eventually. + polish(); } } } @@ -224,6 +223,42 @@ void QQuickTumblerView::itemChange(QQuickItem::ItemChange change, const QQuickIt } } +void QQuickTumblerView::updatePolish() +{ + // There are certain cases where model count changes can potentially cause problems. + // An example of this is a ListModel that appends items in a for loop in Component.onCompleted. + // If we didn't delay assignment of the model, the PathView/ListView would be deleted in + // response to it emitting countChanged(), causing a crash. To avoid this issue, + // and to avoid the overhead of count affecting the wrap property, which in turn may + // unnecessarily create delegates that are never seen, we delay setting the model. This ensures that + // Component.onCompleted would have been finished, for example. + if (m_pathView && !m_pathView->model().isValid() && m_model.isValid()) { + // QQuickPathView::setPathItemCount() resets the offset animation, + // so we just skip the animation while constructing the view. + const int oldHighlightMoveDuration = m_pathView->highlightMoveDuration(); + m_pathView->setHighlightMoveDuration(0); + + // Setting model can change the count, which can affect the wrap, which can cause + // the current view to be deleted before setModel() is finished, which causes a crash. + // Since QQuickTumbler can't know about QQuickTumblerView, we use its private API to + // inform it that it should delay setting wrap. + QQuickTumblerPrivate *tumblerPrivate = QQuickTumblerPrivate::get(m_tumbler); + tumblerPrivate->lockWrap(); + m_pathView->setModel(m_model); + tumblerPrivate->unlockWrap(); + + // The count-depends-on-wrap behavior could cause wrap to change after + // the call above, so we must check that we're still using a PathView. + if (m_pathView) + m_pathView->setHighlightMoveDuration(oldHighlightMoveDuration); + } else if (m_listView && !m_listView->model().isValid() && m_model.isValid()) { + // Usually we'd do this in QQuickTumbler::setWrap(), but that will be too early for polishes. + const int currentIndex = m_tumbler->currentIndex(); + m_listView->setModel(m_model); + m_listView->setCurrentIndex(currentIndex); + } +} + QQuickItem *QQuickTumblerView::view() { if (!m_tumbler) diff --git a/src/quickcontrols2/qquicktumblerview_p.h b/src/quickcontrols2/qquicktumblerview_p.h index dad913da..e82f7f56 100644 --- a/src/quickcontrols2/qquicktumblerview_p.h +++ b/src/quickcontrols2/qquicktumblerview_p.h @@ -87,6 +87,7 @@ protected: void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; void componentComplete() override; void itemChange(ItemChange change, const ItemChangeData &data) override; + void updatePolish(); private: QQuickItem *view(); diff --git a/src/quicktemplates2/qquicktumbler.cpp b/src/quicktemplates2/qquicktumbler.cpp index 1d3c3240..49f3fa42 100644 --- a/src/quicktemplates2/qquicktumbler.cpp +++ b/src/quicktemplates2/qquicktumbler.cpp @@ -38,6 +38,7 @@ #include <QtQuick/private/qquickflickable_p.h> #include <QtQuickTemplates2/private/qquickcontrol_p_p.h> +#include <QtQuickTemplates2/private/qquicktumbler_p_p.h> QT_BEGIN_NAMESPACE @@ -61,8 +62,7 @@ QT_BEGIN_NAMESPACE items. It is useful for when there are too many options to use, for example, a RadioButton, and too few options to require the use of an editable SpinBox. It is convenient in that it requires no keyboard usage - and can be made to wrap around at each end when there are a large number of - items. + and wraps around at each end when there are a large number of items. The API is similar to that of views like \l ListView and \l PathView; a \l model and \l delegate can be set, and the \l count and \l currentItem @@ -73,8 +73,9 @@ QT_BEGIN_NAMESPACE equal to \c 0, \l currentIndex will be \c -1. In all other cases, it will be greater than or equal to \c 0. - By default, Tumbler wraps when it reaches the top and bottom. To achieve a - non-wrapping Tumbler, set the \l wrap property to \c false: + By default, Tumbler \l {wrap}{wraps} when it reaches the top and bottom, as + long as there are more items in the model than there are visible items; + that is, when \l count is greater than \l visibleItemCount: \snippet qtquickcontrols2-tumbler-timePicker.qml tumbler @@ -83,64 +84,25 @@ QT_BEGIN_NAMESPACE \sa {Customizing Tumbler}, {Input Controls} */ -class QQuickTumblerPrivate : public QQuickControlPrivate, public QQuickItemChangeListener +QQuickTumblerPrivate::QQuickTumblerPrivate() : + delegate(nullptr), + visibleItemCount(5), + wrap(true), + explicitWrap(false), + ignoreWrapChanges(false), + view(nullptr), + viewContentItem(nullptr), + viewContentItemType(UnsupportedContentItemType), + currentIndex(-1), + pendingCurrentIndex(-1), + ignoreCurrentIndexChanges(false), + count(0) { - Q_DECLARE_PUBLIC(QQuickTumbler) - -public: - QQuickTumblerPrivate() : - delegate(nullptr), - visibleItemCount(5), - wrap(true), - view(nullptr), - viewContentItem(nullptr), - viewContentItemType(UnsupportedContentItemType), - currentIndex(-1), - ignoreCurrentIndexChanges(false) - { - } - - ~QQuickTumblerPrivate() - { - } - - enum ContentItemType { - UnsupportedContentItemType, - PathViewContentItem, - ListViewContentItem - }; - - QQuickItem *determineViewType(QQuickItem *contentItem); - void resetViewData(); - QList<QQuickItem *> viewContentItemChildItems() const; - - static QQuickTumblerPrivate *get(QQuickTumbler *tumbler) - { - return tumbler->d_func(); - } - - QVariant model; - QQmlComponent *delegate; - int visibleItemCount; - bool wrap; - QQuickItem *view; - QQuickItem *viewContentItem; - ContentItemType viewContentItemType; - int currentIndex; - bool ignoreCurrentIndexChanges; - - void _q_updateItemHeights(); - void _q_updateItemWidths(); - void _q_onViewCurrentIndexChanged(); - void _q_onViewCountChanged(); - - void disconnectFromView(); - void setupViewData(QQuickItem *newControlContentItem); - void syncCurrentIndex(); +} - void itemChildAdded(QQuickItem *, QQuickItem *) override; - void itemChildRemoved(QQuickItem *, QQuickItem *) override; -}; +QQuickTumblerPrivate::~QQuickTumblerPrivate() +{ +} namespace { static inline qreal delegateHeight(const QQuickTumbler *tumbler) @@ -193,6 +155,11 @@ QList<QQuickItem *> QQuickTumblerPrivate::viewContentItemChildItems() const return viewContentItem->childItems(); } +QQuickTumblerPrivate *QQuickTumblerPrivate::get(QQuickTumbler *tumbler) +{ + return tumbler->d_func(); +} + void QQuickTumblerPrivate::_q_updateItemHeights() { // Can't use our own private padding members here, as the padding property might be set, @@ -219,9 +186,7 @@ void QQuickTumblerPrivate::_q_onViewCurrentIndexChanged() if (!ignoreCurrentIndexChanges) { Q_ASSERT(view); const int oldCurrentIndex = currentIndex; - currentIndex = view->property("currentIndex").toInt(); - if (oldCurrentIndex != currentIndex) emit q->currentIndexChanged(); } @@ -230,14 +195,28 @@ void QQuickTumblerPrivate::_q_onViewCurrentIndexChanged() void QQuickTumblerPrivate::_q_onViewCountChanged() { Q_Q(QQuickTumbler); - // If new items were added and our currentIndex was -1, we must - // enforce our rule of a non-negative currentIndex when count > 0. - if (q->count() > 0 && currentIndex == -1) - q->setCurrentIndex(0); - else - syncCurrentIndex(); - emit q->countChanged(); + setCount(view->property("count").toInt()); + + if (count > 0) { + if (pendingCurrentIndex != -1) { + // If there was an attempt to set currentIndex at creation, try to finish that attempt now. + // componentComplete() is too early, because the count might only be known sometime after completion. + q->setCurrentIndex(pendingCurrentIndex); + // If we could successfully set the currentIndex, consider it done. + // Otherwise, we'll try again later in updatePolish(). + if (currentIndex == pendingCurrentIndex) + pendingCurrentIndex = -1; + else + q->polish(); + } else if (currentIndex == -1) { + // If new items were added and our currentIndex was -1, we must + // enforce our rule of a non-negative currentIndex when count > 0. + q->setCurrentIndex(0); + } + } else { + q->setCurrentIndex(-1); + } } void QQuickTumblerPrivate::itemChildAdded(QQuickItem *, QQuickItem *) @@ -287,9 +266,13 @@ void QQuickTumbler::setModel(const QVariant &model) if (model == d->model) return; + d->lockWrap(); + d->model = model; emit modelChanged(); + d->unlockWrap(); + // Don't try to correct the currentIndex if count() isn't known yet. // We can check in setupViewData() instead. if (isComponentComplete() && d->view && count() == 0) @@ -305,7 +288,7 @@ void QQuickTumbler::setModel(const QVariant &model) int QQuickTumbler::count() const { Q_D(const QQuickTumbler); - return d->view ? d->view->property("count").toInt() : 0; + return d->count; } /*! @@ -325,25 +308,47 @@ int QQuickTumbler::currentIndex() const void QQuickTumbler::setCurrentIndex(int currentIndex) { Q_D(QQuickTumbler); + if (currentIndex == d->currentIndex || currentIndex < -1) + return; + + if (!isComponentComplete()) { + // Views can't set currentIndex until they're ready. + d->pendingCurrentIndex = currentIndex; + return; + } + // -1 doesn't make sense for a non-empty Tumbler, because unlike // e.g. ListView, there's always one item selected. // Wait until the component has finished before enforcing this rule, though, // because the count might not be known yet. - if (currentIndex == d->currentIndex || (isComponentComplete() && currentIndex == -1 && count() > 0)) + if ((d->count > 0 && currentIndex == -1) || (currentIndex >= d->count)) { return; + } - d->currentIndex = currentIndex; - + // The view might not have been created yet, as is the case + // if you create a Tumbler component and pass e.g. { currentIndex: 2 } + // to createObject(). if (d->view) { - // The view might not have been created yet, as is the case - // if you create a Tumbler component and pass e.g. { currentIndex: 2 } - // to createObject(). - d->ignoreCurrentIndexChanges = true; - d->view->setProperty("currentIndex", currentIndex); - d->ignoreCurrentIndexChanges = false; - } + // Only actually set our currentIndex if the view was able to set theirs. + bool couldSet = false; + if (d->count == 0 && currentIndex == -1) { + // PathView insists on using 0 as the currentIndex when there are no items. + couldSet = true; + } else { + d->ignoreCurrentIndexChanges = true; + d->view->setProperty("currentIndex", currentIndex); + d->ignoreCurrentIndexChanges = false; + + couldSet = d->view->property("currentIndex").toInt() == currentIndex; + } - emit currentIndexChanged(); + if (couldSet) { + // The view's currentIndex might not have actually changed, but ours has, + // and that's what user code sees. + d->currentIndex = currentIndex; + emit currentIndexChanged(); + } + } } /*! @@ -409,9 +414,11 @@ void QQuickTumbler::setVisibleItemCount(int visibleItemCount) This property determines whether or not the tumbler wraps around when it reaches the top or bottom. - It is recommended to set this property to \c false when \l count is less than + The default value is \c false when \l count is less than \l visibleItemCount, as it is simpler to interact with a non-wrapping Tumbler - when there are only a few items. + when there are only a few items. To override this behavior, explicitly set + the value of this property. To return to the default behavior, set this + property to \c undefined. */ bool QQuickTumbler::wrap() const { @@ -422,34 +429,14 @@ bool QQuickTumbler::wrap() const void QQuickTumbler::setWrap(bool wrap) { Q_D(QQuickTumbler); - if (isComponentComplete() && wrap == d->wrap) - return; - - // Since we use the currentIndex of the contentItem directly, we must - // ensure that we keep track of the currentIndex so it doesn't get lost - // between view changes. - const int oldCurrentIndex = currentIndex(); - - d->disconnectFromView(); - - d->wrap = wrap; - - // New views will set their currentIndex upon creation, which we'd otherwise - // take as the correct one, so we must ignore them. - d->ignoreCurrentIndexChanges = true; - - // This will cause the view to be created if our contentItem is a TumblerView. - emit wrapChanged(); - - d->ignoreCurrentIndexChanges = false; - - // The view should have been created now, so we can start determining its type, etc. - // If the delegates use attached properties, this will have already been called, - // in which case it will return early. If the delegate doesn't use attached properties, - // we need to call it here. - d->setupViewData(d->contentItem); + d->setWrap(wrap, true); +} - setCurrentIndex(oldCurrentIndex); +void QQuickTumbler::resetWrap() +{ + Q_D(QQuickTumbler); + d->explicitWrap = false; + d->setWrapBasedOnCount(); } QQuickTumblerAttached *QQuickTumbler::qmlAttachedProperties(QObject *object) @@ -483,12 +470,9 @@ void QQuickTumbler::componentComplete() d->_q_updateItemWidths(); if (!d->view) { - // We don't want to create a PathView or ListView until we're certain - // which one we need, and if wrap is not set, it will be the default. - // We can only know the final value of wrap when componentComplete() is called, - // so, if the view hasn't already been created, we cause it to be created here. + // Force the view to be created. emit wrapChanged(); - // Then, we determine the type of view for attached properties, etc. + // Determine the type of view for attached properties, etc. d->setupViewData(d->contentItem); } } @@ -578,6 +562,74 @@ void QQuickTumblerPrivate::syncCurrentIndex() ignoreCurrentIndexChanges = false; } +void QQuickTumblerPrivate::setCount(int newCount) +{ + if (newCount == count) + return; + + count = newCount; + + Q_Q(QQuickTumbler); + setWrapBasedOnCount(); + + emit q->countChanged(); +} + +void QQuickTumblerPrivate::setWrapBasedOnCount() +{ + if (count == 0 || explicitWrap || ignoreWrapChanges) + return; + + setWrap(count >= visibleItemCount, false); +} + +void QQuickTumblerPrivate::setWrap(bool shouldWrap, bool isExplicit) +{ + if (isExplicit) + explicitWrap = true; + + Q_Q(QQuickTumbler); + if (q->isComponentComplete() && shouldWrap == wrap) + return; + + // Since we use the currentIndex of the contentItem directly, we must + // ensure that we keep track of the currentIndex so it doesn't get lost + // between view changes. + const int oldCurrentIndex = currentIndex; + + disconnectFromView(); + + wrap = shouldWrap; + + // New views will set their currentIndex upon creation, which we'd otherwise + // take as the correct one, so we must ignore them. + ignoreCurrentIndexChanges = true; + + // This will cause the view to be created if our contentItem is a TumblerView. + emit q->wrapChanged(); + + ignoreCurrentIndexChanges = false; + + // The view should have been created now, so we can start determining its type, etc. + // If the delegates use attached properties, this will have already been called, + // in which case it will return early. If the delegate doesn't use attached properties, + // we need to call it here. + setupViewData(contentItem); + + q->setCurrentIndex(oldCurrentIndex); +} + +void QQuickTumblerPrivate::lockWrap() +{ + ignoreWrapChanges = true; +} + +void QQuickTumblerPrivate::unlockWrap() +{ + ignoreWrapChanges = false; + setWrapBasedOnCount(); +} + void QQuickTumbler::keyPressEvent(QKeyEvent *event) { QQuickControl::keyPressEvent(event); @@ -593,6 +645,31 @@ void QQuickTumbler::keyPressEvent(QKeyEvent *event) } } +void QQuickTumbler::updatePolish() +{ + Q_D(QQuickTumbler); + if (d->pendingCurrentIndex != -1) { + // If the count is still 0, it's not going to happen. + if (d->count == 0) { + d->pendingCurrentIndex = -1; + return; + } + + // If there is a pending currentIndex at this stage, it means that + // the view wouldn't set our currentIndex in _q_onViewCountChanged + // because it wasn't ready. Try one last time here. + setCurrentIndex(d->pendingCurrentIndex); + + if (d->currentIndex != d->pendingCurrentIndex && d->currentIndex == -1) { + // If we *still* couldn't set it, it's probably invalid. + // See if we can at least enforce our rule of "non-negative currentIndex when count > 0" instead. + setCurrentIndex(0); + } + + d->pendingCurrentIndex = -1; + } +} + class QQuickTumblerAttachedPrivate : public QObjectPrivate, public QQuickItemChangeListener { Q_DECLARE_PUBLIC(QQuickTumblerAttached) @@ -679,7 +756,9 @@ void QQuickTumblerAttachedPrivate::_q_calculateDisplacement() if (!tumblerPrivate->viewContentItem) return; - const int count = tumbler->count(); + // The attached property gets created before our count is updated, so just cheat here + // to avoid having to listen to count changes. + const int count = tumblerPrivate->view->property("count").toInt(); // This can happen in tests, so it may happen in normal usage too. if (count == 0) return; diff --git a/src/quicktemplates2/qquicktumbler_p.h b/src/quicktemplates2/qquicktumbler_p.h index 698df84c..d047aa96 100644 --- a/src/quicktemplates2/qquicktumbler_p.h +++ b/src/quicktemplates2/qquicktumbler_p.h @@ -66,7 +66,7 @@ class Q_QUICKTEMPLATES2_PRIVATE_EXPORT QQuickTumbler : public QQuickControl Q_PROPERTY(QQuickItem *currentItem READ currentItem NOTIFY currentItemChanged FINAL) Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged FINAL) Q_PROPERTY(int visibleItemCount READ visibleItemCount WRITE setVisibleItemCount NOTIFY visibleItemCountChanged FINAL) - Q_PROPERTY(bool wrap READ wrap WRITE setWrap NOTIFY wrapChanged FINAL REVISION 1) + Q_PROPERTY(bool wrap READ wrap WRITE setWrap RESET resetWrap NOTIFY wrapChanged FINAL REVISION 1) public: explicit QQuickTumbler(QQuickItem *parent = nullptr); @@ -89,6 +89,7 @@ public: bool wrap() const; void setWrap(bool wrap); + void resetWrap(); static QQuickTumblerAttached *qmlAttachedProperties(QObject *object); @@ -106,6 +107,7 @@ protected: void componentComplete() override; void contentItemChange(QQuickItem *newItem, QQuickItem *oldItem) override; void keyPressEvent(QKeyEvent *event) override; + void updatePolish() override; private: Q_DISABLE_COPY(QQuickTumbler) diff --git a/src/quicktemplates2/qquicktumbler_p_p.h b/src/quicktemplates2/qquicktumbler_p_p.h new file mode 100644 index 00000000..daced3a3 --- /dev/null +++ b/src/quicktemplates2/qquicktumbler_p_p.h @@ -0,0 +1,113 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Quick Templates 2 module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQUICKTUMBLER_P_P_H +#define QQUICKTUMBLER_P_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtQuick/private/qquickitemchangelistener_p.h> +#include <QtQuickTemplates2/private/qquickcontrol_p_p.h> + +QT_BEGIN_NAMESPACE + +class QQuickTumbler; + +class Q_QUICKTEMPLATES2_PRIVATE_EXPORT QQuickTumblerPrivate : public QQuickControlPrivate, public QQuickItemChangeListener +{ + Q_DECLARE_PUBLIC(QQuickTumbler) + +public: + QQuickTumblerPrivate(); + ~QQuickTumblerPrivate(); + + enum ContentItemType { + UnsupportedContentItemType, + PathViewContentItem, + ListViewContentItem + }; + + QQuickItem *determineViewType(QQuickItem *contentItem); + void resetViewData(); + QList<QQuickItem *> viewContentItemChildItems() const; + + static QQuickTumblerPrivate *get(QQuickTumbler *tumbler); + + QVariant model; + QQmlComponent *delegate; + int visibleItemCount; + bool wrap; + bool explicitWrap; + bool ignoreWrapChanges; + QQuickItem *view; + QQuickItem *viewContentItem; + ContentItemType viewContentItemType; + int currentIndex; + int pendingCurrentIndex; + bool ignoreCurrentIndexChanges; + int count; + + void _q_updateItemHeights(); + void _q_updateItemWidths(); + void _q_onViewCurrentIndexChanged(); + void _q_onViewCountChanged(); + + void disconnectFromView(); + void setupViewData(QQuickItem *newControlContentItem); + void syncCurrentIndex(); + + void setCount(int newCount); + void setWrapBasedOnCount(); + void setWrap(bool shouldWrap, bool isExplicit); + void lockWrap(); + void unlockWrap(); + + void itemChildAdded(QQuickItem *, QQuickItem *) override; + void itemChildRemoved(QQuickItem *, QQuickItem *) override; +}; + +QT_END_NAMESPACE + +#endif // QQUICKTUMBLER_P_P_H diff --git a/src/quicktemplates2/quicktemplates2.pri b/src/quicktemplates2/quicktemplates2.pri index 4f3f6af0..94d91e68 100644 --- a/src/quicktemplates2/quicktemplates2.pri +++ b/src/quicktemplates2/quicktemplates2.pri @@ -61,6 +61,7 @@ HEADERS += \ $$PWD/qquicktoolbutton_p.h \ $$PWD/qquicktooltip_p.h \ $$PWD/qquicktumbler_p.h \ + $$PWD/qquicktumbler_p_p.h \ $$PWD/qquickvelocitycalculator_p_p.h SOURCES += \ diff --git a/tests/auto/controls/data/tst_tumbler.qml b/tests/auto/controls/data/tst_tumbler.qml index ec13665e..b53c3fe7 100644 --- a/tests/auto/controls/data/tst_tumbler.qml +++ b/tests/auto/controls/data/tst_tumbler.qml @@ -156,6 +156,8 @@ TestCase { tumbler.model = null; tryCompare(tumbler, "currentIndex", -1); + // PathView will only use 0 as the currentIndex when there are no items. + compare(tumblerView.currentIndex, 0); tumbler.model = ["A", "B", "C"]; tryCompare(tumbler, "currentIndex", 0); @@ -167,6 +169,21 @@ TestCase { tumbler.model = 1; compare(tumbler.currentIndex, 0); + tumbler.model = 5; + compare(tumbler.count, 5); + tumblerView = findView(tumbler); + tryCompare(tumblerView, "count", 5); + tumbler.currentIndex = 4; + compare(tumbler.currentIndex, 4); + compare(tumblerView.currentIndex, 4); + + --tumbler.model; + compare(tumbler.count, 4); + compare(tumblerView.count, 4); + // Removing an item from an integer-based model will cause views to reset their currentIndex to 0. + compare(tumbler.currentIndex, 0); + compare(tumblerView.currentIndex, 0); + tumbler.model = 0; compare(tumbler.currentIndex, -1); } @@ -214,14 +231,26 @@ TestCase { } } + Component { + id: currentIndexTooLargeTumbler + + Tumbler { + objectName: "currentIndexTooLargeTumbler" + model: 10 + currentIndex: 10 + } + } + + function test_currentIndexAtCreation_data() { return [ - { tag: "wrap: true, currentIndex: 2", currentIndex: 2, wrap: true, component: currentIndexTumbler }, - { tag: "wrap: false, currentIndex: 2", currentIndex: 2, wrap: false, component: currentIndexTumblerNoWrap }, + { tag: "wrap: implicit, expected currentIndex: 2", currentIndex: 2, wrap: true, component: currentIndexTumbler }, + { tag: "wrap: false, expected currentIndex: 2", currentIndex: 2, wrap: false, component: currentIndexTumblerNoWrap }, // Order of property assignments shouldn't matter - { tag: "wrap: false, currentIndex: 2, reversed property assignment order", + { tag: "wrap: false, expected currentIndex: 2, reversed property assignment order", currentIndex: 2, wrap: false, component: currentIndexTumblerNoWrapReversedOrder }, - { tag: "wrap: false, currentIndex: -1", currentIndex: 0, wrap: false, component: negativeCurrentIndexTumblerNoWrap } + { tag: "wrap: false, expected currentIndex: 0", currentIndex: 0, wrap: false, component: negativeCurrentIndexTumblerNoWrap }, + { tag: "wrap: implicit, expected currentIndex: 0", currentIndex: 0, wrap: true, component: currentIndexTooLargeTumbler } ] } @@ -229,7 +258,9 @@ TestCase { // Test setting currentIndex at creation time var tumbler = data.component.createObject(testCase); verify(tumbler); - compare(tumbler.currentIndex, data.currentIndex); + // A "statically declared" currentIndex will be pending until the count has changed, + // which happens when the model is set, which happens on the TumblerView's next polish. + tryCompare(tumbler, "currentIndex", data.currentIndex); tumblerView = findView(tumbler); // TODO: replace once QTBUG-19708 is fixed. @@ -242,10 +273,11 @@ TestCase { compare(tumblerView.currentIndex, data.currentIndex); compare(tumblerView.currentItem.text, data.currentIndex.toString()); + var fuzz = 1; if (data.wrap) { - compare(tumblerView.offset, tumblerView.count - data.currentIndex); + fuzzyCompare(tumblerView.offset, data.currentIndex > 0 ? tumblerView.count - data.currentIndex : 0, fuzz); } else { - compare(tumblerView.contentY, tumblerDelegateHeight * data.currentIndex - tumblerView.preferredHighlightBegin); + fuzzyCompare(tumblerView.contentY, tumblerDelegateHeight * data.currentIndex - tumblerView.preferredHighlightBegin, fuzz); } tumbler.destroy(); @@ -337,7 +369,7 @@ TestCase { tumbler = component.createObject(testCase); // Should not be any warnings. - compare(tumbler.dayTumbler.currentIndex, 0); + tryCompare(tumbler.dayTumbler, "currentIndex", 0); compare(tumbler.dayTumbler.count, 31); compare(tumbler.monthTumbler.currentIndex, 0); compare(tumbler.monthTumbler.count, 12); @@ -421,6 +453,7 @@ TestCase { function test_displacement(data) { // TODO: test setting these in the opposite order (delegate after model // doesn't seem to cause a change in delegates in PathView) + tumbler.wrap = true; tumbler.delegate = displacementDelegate; tumbler.model = data.count; compare(tumbler.count, data.count); @@ -445,11 +478,74 @@ TestCase { tumblerView = findView(tumbler); compare(tumbler.count, 5); compare(tumbler.currentIndex, 2); - compare(tumblerView.count, 5); + // Tumbler's count hasn't changed (the model hasn't changed), + // but the new view needs time to instantiate its items. + tryCompare(tumblerView, "count", 5); compare(tumblerView.currentIndex, 2); } Component { + id: twoItemTumbler + + Tumbler { + model: 2 + } + } + + Component { + id: tenItemTumbler + + Tumbler { + model: 10 + } + } + + function test_countWrap() { + // Check that a count that is less than visibleItemCount results in wrap being set to false. + verify(2 < tumbler.visibleItemCount); + tumbler.model = 2; + compare(tumbler.count, 2); + compare(tumbler.wrap, false); + } + + function test_explicitlyNonwrapping() { + // Check that explicitly setting wrap to false works even when it was implicitly false. + var explicitlyNonWrapping = twoItemTumbler.createObject(testCase); + verify(explicitlyNonWrapping); + tryCompare(explicitlyNonWrapping, "wrap", false); + + explicitlyNonWrapping.wrap = false; + // wrap shouldn't be set to true now that there are more items than there are visible ones. + verify(10 > explicitlyNonWrapping.visibleItemCount); + explicitlyNonWrapping.model = 10; + compare(explicitlyNonWrapping.wrap, false); + + // Test resetting wrap back to the default behavior. + explicitlyNonWrapping.wrap = undefined; + compare(explicitlyNonWrapping.wrap, true); + + explicitlyNonWrapping.destroy(); + } + + function test_explicitlyWrapping() { + // Check that explicitly setting wrap to true works even when it was implicitly true. + var explicitlyWrapping = tenItemTumbler.createObject(testCase); + verify(explicitlyWrapping); + compare(explicitlyWrapping.wrap, true); + + explicitlyWrapping.wrap = true; + // wrap shouldn't be set to false now that there are more items than there are visible ones. + explicitlyWrapping.model = 2; + compare(explicitlyWrapping.wrap, true); + + // Test resetting wrap back to the default behavior. + explicitlyWrapping.wrap = undefined; + compare(explicitlyWrapping.wrap, false); + + explicitlyWrapping.destroy(); + } + + Component { id: customListViewTumblerComponent Tumbler { |