aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMitch Curtis <mitch.curtis@qt.io>2016-06-27 13:08:42 +0200
committerMitch Curtis <mitch.curtis@qt.io>2016-07-01 06:19:11 +0000
commit2c4b2d488291e83bf1a6aac1d59351c1a6e901a3 (patch)
tree94743b9866e7c6e0a7d20827ff104d0c979eeea2
parenta5df6b69672afd780433ee8f43d343d1e2251fd4 (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.qml1
-rw-r--r--src/quickcontrols2/qquicktumblerview.cpp51
-rw-r--r--src/quickcontrols2/qquicktumblerview_p.h1
-rw-r--r--src/quicktemplates2/qquicktumbler.cpp311
-rw-r--r--src/quicktemplates2/qquicktumbler_p.h4
-rw-r--r--src/quicktemplates2/qquicktumbler_p_p.h113
-rw-r--r--src/quicktemplates2/quicktemplates2.pri1
-rw-r--r--tests/auto/controls/data/tst_tumbler.qml114
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 {