aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/extras/extras.pri6
-rw-r--r--src/extras/qquicktumbler.cpp416
-rw-r--r--src/extras/qquicktumbler_p.h136
-rw-r--r--src/imports/extras/Tumbler.qml76
-rw-r--r--src/imports/extras/extras.pro5
-rw-r--r--src/imports/extras/qmldir1
-rw-r--r--src/imports/extras/qtquickextras2plugin.cpp3
-rw-r--r--tests/auto/extras/data/TumblerDatePicker.qml88
-rw-r--r--tests/auto/extras/data/tst_tumbler.qml377
9 files changed, 1105 insertions, 3 deletions
diff --git a/src/extras/extras.pri b/src/extras/extras.pri
index 63c10268..edbc754a 100644
--- a/src/extras/extras.pri
+++ b/src/extras/extras.pri
@@ -2,8 +2,10 @@ INCLUDEPATH += $$PWD
HEADERS += \
$$PWD/qquickdrawer_p.h \
- $$PWD/qquickswipeview_p.h
+ $$PWD/qquickswipeview_p.h \
+ $$PWD/qquicktumbler_p.h
SOURCES += \
$$PWD/qquickdrawer.cpp \
- $$PWD/qquickswipeview.cpp
+ $$PWD/qquickswipeview.cpp \
+ $$PWD/qquicktumbler.cpp
diff --git a/src/extras/qquicktumbler.cpp b/src/extras/qquicktumbler.cpp
new file mode 100644
index 00000000..7216d4ea
--- /dev/null
+++ b/src/extras/qquicktumbler.cpp
@@ -0,0 +1,416 @@
+/****************************************************************************
+**
+** Copyright (C) 2015 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the Qt Quick Extras 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$
+**
+****************************************************************************/
+
+#include "qquicktumbler_p.h"
+
+#include <QtQuick/private/qquickflickable_p.h>
+#include <QtQuickControls/private/qquickcontrol_p_p.h>
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \qmltype Tumbler
+ \inherits Container
+ \instantiates QQuickTumbler
+ \inqmlmodule QtQuick.Extras
+ \ingroup containers
+ \brief A spinnable wheel of items that can be selected.
+
+ TODO
+*/
+
+class QQuickTumblerPrivate : public QQuickControlPrivate, public QQuickItemChangeListener
+{
+ Q_DECLARE_PUBLIC(QQuickTumbler)
+
+public:
+ QQuickTumblerPrivate() :
+ delegate(Q_NULLPTR),
+ visibleItemCount(3)
+ {
+ }
+
+ QVariant model;
+ QQmlComponent *delegate;
+ int visibleItemCount;
+
+ void updateItemHeights();
+ void updateItemWidths();
+
+ void itemChildAdded(QQuickItem *, QQuickItem *);
+ void itemChildRemoved(QQuickItem *, QQuickItem *);
+};
+
+static QList<QQuickItem *> contentItemChildItems(QQuickItem *contentItem)
+{
+ if (!contentItem)
+ return QList<QQuickItem *>();
+
+ // PathView has no contentItem property, but ListView does.
+ QQuickFlickable *flickable = qobject_cast<QQuickFlickable *>(contentItem);
+ return flickable ? flickable->contentItem()->childItems() : contentItem->childItems();
+}
+
+void QQuickTumblerPrivate::updateItemHeights()
+{
+ // TODO: can we/do we want to support spacing?
+ const qreal itemHeight = (contentItem->height()/* - qMax(0, itemCount - 1) * spacing*/
+ - topPadding - bottomPadding) / visibleItemCount;
+ foreach (QQuickItem *childItem, contentItemChildItems(contentItem))
+ childItem->setHeight(itemHeight);
+}
+
+void QQuickTumblerPrivate::updateItemWidths()
+{
+ foreach (QQuickItem *childItem, contentItemChildItems(contentItem))
+ childItem->setWidth(width);
+}
+
+void QQuickTumblerPrivate::itemChildAdded(QQuickItem *, QQuickItem *)
+{
+ updateItemWidths();
+ updateItemHeights();
+}
+
+void QQuickTumblerPrivate::itemChildRemoved(QQuickItem *, QQuickItem *)
+{
+ updateItemWidths();
+ updateItemHeights();
+}
+
+QQuickTumbler::QQuickTumbler(QQuickItem *parent) :
+ QQuickControl(*(new QQuickTumblerPrivate), parent)
+{
+}
+
+/*!
+ \qmlproperty variant QtQuickExtras2::Tumbler::model
+
+ This property holds the model that provides data for this tumbler.
+*/
+QVariant QQuickTumbler::model() const
+{
+ Q_D(const QQuickTumbler);
+ return d->model;
+}
+
+void QQuickTumbler::setModel(const QVariant &model)
+{
+ Q_D(QQuickTumbler);
+ if (model != d->model) {
+ d->model = model;
+ emit modelChanged();
+ }
+}
+
+/*!
+ \qmlproperty int QtQuickExtras2::Tumbler::count
+
+ This property holds the number of items in the model.
+*/
+int QQuickTumbler::count() const
+{
+ Q_D(const QQuickTumbler);
+ return d->contentItem->property("count").toInt();
+}
+
+/*!
+ \qmlproperty int QtQuickExtras2::Tumbler::currentIndex
+
+ This property holds the index of the current item.
+*/
+int QQuickTumbler::currentIndex() const
+{
+ Q_D(const QQuickTumbler);
+ return d->contentItem ? d->contentItem->property("currentIndex").toInt() : -1;
+}
+
+void QQuickTumbler::setCurrentIndex(int currentIndex)
+{
+ Q_D(QQuickTumbler);
+ d->contentItem->setProperty("currentIndex", currentIndex);
+}
+
+/*!
+ \qmlproperty Item QtQuickExtras2::Tumbler::currentItem
+
+ This property holds the item at the current index.
+*/
+QQuickItem *QQuickTumbler::currentItem() const
+{
+ Q_D(const QQuickTumbler);
+ return d->contentItem ? d->contentItem->property("currentItem").value<QQuickItem*>() : Q_NULLPTR;
+}
+
+/*!
+ \qmlproperty component QtQuickExtras2::Tumbler::delegate
+
+ This property holds the delegate used to display each item.
+*/
+QQmlComponent *QQuickTumbler::delegate() const
+{
+ Q_D(const QQuickTumbler);
+ return d->delegate;
+}
+
+void QQuickTumbler::setDelegate(QQmlComponent *delegate)
+{
+ Q_D(QQuickTumbler);
+ if (delegate != d->delegate) {
+ d->delegate = delegate;
+ emit delegateChanged();
+ }
+}
+
+/*!
+ \qmlproperty int QtQuickExtras2::Tumbler::visibleItemCount
+
+ This property holds the number of items visible in the tumbler. It must be
+ an odd number.
+*/
+int QQuickTumbler::visibleItemCount() const
+{
+ Q_D(const QQuickTumbler);
+ return d->visibleItemCount;
+}
+
+void QQuickTumbler::setVisibleItemCount(int visibleItemCount)
+{
+ Q_D(QQuickTumbler);
+ if (visibleItemCount != d->visibleItemCount) {
+ d->visibleItemCount = visibleItemCount;
+ d->updateItemHeights();
+ emit visibleItemCountChanged();
+ }
+}
+
+QQuickTumblerAttached *QQuickTumbler::qmlAttachedProperties(QObject *object)
+{
+ QQuickItem *delegateItem = qobject_cast<QQuickItem *>(object);
+ if (!delegateItem) {
+ qWarning() << "Attached properties of Tumbler must be accessed from within a delegate item";
+ return Q_NULLPTR;
+ }
+
+ return new QQuickTumblerAttached(delegateItem);
+}
+
+void QQuickTumbler::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
+{
+ Q_D(QQuickTumbler);
+
+ QQuickControl::geometryChanged(newGeometry, oldGeometry);
+
+ d->updateItemHeights();
+
+ if (newGeometry.width() != oldGeometry.width())
+ d->updateItemWidths();
+}
+
+void QQuickTumbler::componentComplete()
+{
+ Q_D(QQuickTumbler);
+ QQuickControl::componentComplete();
+ d->updateItemHeights();
+ d->updateItemWidths();
+}
+
+void QQuickTumbler::contentItemChange(QQuickItem *newItem, QQuickItem *oldItem)
+{
+ Q_D(QQuickTumbler);
+
+ QQuickControl::contentItemChange(newItem, oldItem);
+
+ // 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 contentItem changes.
+ const int previousCurrentIndex = currentIndex();
+
+ if (oldItem) {
+ disconnect(oldItem, SIGNAL(currentIndexChanged()), this, SIGNAL(currentIndexChanged()));
+ disconnect(oldItem, SIGNAL(currentItemChanged()), this, SIGNAL(currentItemChanged()));
+ disconnect(oldItem, SIGNAL(countChanged()), this, SIGNAL(countChanged()));
+
+ QQuickItemPrivate *oldItemPrivate = QQuickItemPrivate::get(oldItem);
+ oldItemPrivate->removeItemChangeListener(d, QQuickItemPrivate::Children);
+ }
+
+ if (newItem) {
+ connect(newItem, SIGNAL(currentIndexChanged()), this, SIGNAL(currentIndexChanged()));
+ connect(newItem, SIGNAL(currentItemChanged()), this, SIGNAL(currentItemChanged()));
+ connect(newItem, SIGNAL(countChanged()), this, SIGNAL(countChanged()));
+
+ QQuickItemPrivate *newItemPrivate = QQuickItemPrivate::get(newItem);
+ newItemPrivate->addItemChangeListener(d, QQuickItemPrivate::Children);
+
+ // If the previous currentIndex is -1, it means we had no contentItem previously.
+ if (previousCurrentIndex != -1) {
+ // Can't call setCurrentIndex here, as contentItemChange() is
+ // called *before* the contentItem is set.
+ newItem->setProperty("currentIndex", previousCurrentIndex);
+ }
+ }
+}
+
+void QQuickTumbler::keyPressEvent(QKeyEvent *event)
+{
+ Q_D(QQuickTumbler);
+
+ QQuickControl::keyPressEvent(event);
+
+ if (event->isAutoRepeat())
+ return;
+
+ if (event->key() == Qt::Key_Up) {
+ QMetaObject::invokeMethod(d->contentItem, "decrementCurrentIndex");
+ } else if (event->key() == Qt::Key_Down) {
+ QMetaObject::invokeMethod(d->contentItem, "incrementCurrentIndex");
+ }
+}
+
+class QQuickTumblerAttachedPrivate : public QObjectPrivate, public QQuickItemChangeListener
+{
+ Q_DECLARE_PUBLIC(QQuickTumblerAttached)
+public:
+ QQuickTumblerAttachedPrivate(QQuickItem *delegateItem) :
+ tumbler(Q_NULLPTR),
+ index(-1),
+ displacement(1)
+ {
+ if (!delegateItem->parentItem()) {
+ qWarning() << "Attached properties of Tumbler must be accessed from within a delegate item that has a parent";
+ return;
+ }
+
+ QVariant indexContextProperty = qmlContext(delegateItem)->contextProperty(QStringLiteral("index"));
+ if (!indexContextProperty.isValid()) {
+ qWarning() << "Attempting to access attached property on item without an \"index\" property";
+ return;
+ }
+
+ index = indexContextProperty.toInt();
+ if (!delegateItem->parentItem()->inherits("QQuickPathView")) {
+ qWarning() << "contentItems other than PathView are not currently supported";
+ return;
+ }
+
+ tumbler = qobject_cast<QQuickTumbler* >(delegateItem->parentItem()->parentItem());
+ }
+
+ void itemGeometryChanged(QQuickItem *item, const QRectF &newGeometry, const QRectF &oldGeometry) Q_DECL_OVERRIDE;
+ void itemChildAdded(QQuickItem *, QQuickItem *);
+ void itemChildRemoved(QQuickItem *, QQuickItem *);
+
+ void _q_calculateDisplacement();
+
+ // The Tumbler that contains the delegate. Required to calculated the displacement.
+ QQuickTumbler *tumbler;
+ // The index of the delegate. Used to calculate the displacement.
+ int index;
+ // The displacement for our delegate.
+ qreal displacement;
+};
+
+void QQuickTumblerAttachedPrivate::itemGeometryChanged(QQuickItem *, const QRectF &, const QRectF &)
+{
+ _q_calculateDisplacement();
+}
+
+void QQuickTumblerAttachedPrivate::itemChildAdded(QQuickItem *, QQuickItem *)
+{
+ _q_calculateDisplacement();
+}
+
+void QQuickTumblerAttachedPrivate::itemChildRemoved(QQuickItem *, QQuickItem *child)
+{
+ _q_calculateDisplacement();
+
+ if (parent == child) {
+ // The child that was removed from the contentItem was the delegate
+ // that our properties are attached to. If we don't remove the change
+ // listener, the contentItem will attempt to notify a destroyed
+ // listener, causing a crash.
+ QQuickItemPrivate *p = QQuickItemPrivate::get(tumbler->contentItem());
+ p->removeItemChangeListener(this, QQuickItemPrivate::Geometry | QQuickItemPrivate::Children);
+ }
+}
+
+void QQuickTumblerAttachedPrivate::_q_calculateDisplacement()
+{
+ const int previousDisplacement = displacement;
+
+ displacement = 1;
+
+ // TODO: ListView has no offset property, need to try using contentY instead.
+ if (tumbler && tumbler->contentItem()->inherits("QQuickListView"))
+ return;
+
+ qreal offset = tumbler->contentItem()->property("offset").toReal();
+ displacement = tumbler->count() - index - offset;
+ int halfVisibleItems = tumbler->visibleItemCount() / 2 + 1;
+ if (displacement > halfVisibleItems)
+ displacement -= tumbler->count();
+ else if (displacement < -halfVisibleItems)
+ displacement += tumbler->count();
+
+ Q_Q(QQuickTumblerAttached);
+ if (displacement != previousDisplacement)
+ emit q->displacementChanged();
+}
+
+QQuickTumblerAttached::QQuickTumblerAttached(QQuickItem *delegateItem) :
+ QObject(*(new QQuickTumblerAttachedPrivate(delegateItem)), delegateItem)
+{
+ Q_D(QQuickTumblerAttached);
+ if (d->tumbler) {
+ // TODO: in case of listview, listen to contentItem of contentItem
+ QQuickItemPrivate *p = QQuickItemPrivate::get(d->tumbler->contentItem());
+ p->addItemChangeListener(d, QQuickItemPrivate::Geometry | QQuickItemPrivate::Children);
+
+ // TODO: in case of ListView, listen to contentY changed
+ connect(d->tumbler->contentItem(), SIGNAL(offsetChanged()), this, SLOT(_q_calculateDisplacement()));
+ }
+}
+
+qreal QQuickTumblerAttached::displacement() const
+{
+ Q_D(const QQuickTumblerAttached);
+ return d->displacement;
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qquicktumbler_p.cpp"
diff --git a/src/extras/qquicktumbler_p.h b/src/extras/qquicktumbler_p.h
new file mode 100644
index 00000000..d8f54972
--- /dev/null
+++ b/src/extras/qquicktumbler_p.h
@@ -0,0 +1,136 @@
+/****************************************************************************
+**
+** Copyright (C) 2015 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the Qt Quick Extras 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_H
+#define QQUICKTUMBLER_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 <QtCore/qvariant.h>
+#include <QtQml/qqmlcomponent.h>
+#include <QtQuickControls/private/qquickcontrol_p.h>
+#include <QtQuickExtras/private/qtquickextrasglobal_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QQuickTumblerAttached;
+class QQuickTumblerPrivate;
+
+class Q_QUICKEXTRAS_EXPORT QQuickTumbler : public QQuickControl
+{
+ Q_OBJECT
+ Q_PROPERTY(QVariant model READ model WRITE setModel NOTIFY modelChanged FINAL)
+ Q_PROPERTY(int count READ count NOTIFY countChanged FINAL)
+ Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged FINAL)
+ 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)
+
+public:
+ explicit QQuickTumbler(QQuickItem *parent = Q_NULLPTR);
+
+ QVariant model() const;
+ void setModel(const QVariant &model);
+
+ int count() const;
+
+ int currentIndex() const;
+ void setCurrentIndex(int currentIndex);
+ QQuickItem *currentItem() const;
+
+ QQmlComponent *delegate() const;
+ void setDelegate(QQmlComponent *delegate);
+
+ int visibleItemCount() const;
+ void setVisibleItemCount(int visibleItemCount);
+
+ static QQuickTumblerAttached *qmlAttachedProperties(QObject *object);
+
+Q_SIGNALS:
+ void modelChanged();
+ void countChanged();
+ void currentIndexChanged();
+ void currentItemChanged();
+ void delegateChanged();
+ void visibleItemCountChanged();
+
+protected:
+ void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) Q_DECL_OVERRIDE;
+ void componentComplete() Q_DECL_OVERRIDE;
+ void contentItemChange(QQuickItem *newItem, QQuickItem *oldItem) Q_DECL_OVERRIDE;
+ void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE;
+
+private:
+ Q_DISABLE_COPY(QQuickTumbler)
+ Q_DECLARE_PRIVATE(QQuickTumbler)
+};
+
+class QQuickTumblerAttachedPrivate;
+
+class Q_QUICKEXTRAS_EXPORT QQuickTumblerAttached : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(qreal displacement READ displacement NOTIFY displacementChanged FINAL)
+
+public:
+ explicit QQuickTumblerAttached(QQuickItem *delegateItem);
+
+ qreal displacement() const;
+
+Q_SIGNALS:
+ void displacementChanged();
+
+private:
+ Q_DISABLE_COPY(QQuickTumblerAttached)
+ Q_DECLARE_PRIVATE(QQuickTumblerAttached)
+
+ Q_PRIVATE_SLOT(d_func(), void _q_calculateDisplacement())
+};
+
+QT_END_NAMESPACE
+
+QML_DECLARE_TYPEINFO(QQuickTumbler, QML_HAS_ATTACHED_PROPERTIES)
+
+#endif // QQUICKTUMBLER_H
diff --git a/src/imports/extras/Tumbler.qml b/src/imports/extras/Tumbler.qml
new file mode 100644
index 00000000..bae6688a
--- /dev/null
+++ b/src/imports/extras/Tumbler.qml
@@ -0,0 +1,76 @@
+/****************************************************************************
+**
+** Copyright (C) 2015 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the Qt Quick Extras 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$
+**
+****************************************************************************/
+
+import QtQuick 2.6
+import QtQuick.Controls 2.0
+import QtQuick.Extras 2.0
+
+AbstractTumbler {
+ id: control
+ width: 60
+ height: 200
+
+ delegate: Text {
+ id: label
+ text: modelData
+ color: "#666666"
+ opacity: 0.4 + Math.max(0, 1 - Math.abs(AbstractTumbler.displacement)) * 0.6
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ contentItem: PathView {
+ id: pathView
+ model: control.model
+ delegate: control.delegate
+ clip: true
+ pathItemCount: control.visibleItemCount + 1
+ preferredHighlightBegin: 0.5
+ preferredHighlightEnd: 0.5
+ dragMargin: width / 2
+
+ path: Path {
+ startX: pathView.width / 2
+ startY: -pathView.delegateHeight / 2
+ PathLine {
+ x: pathView.width / 2
+ y: pathView.pathItemCount * pathView.delegateHeight - pathView.delegateHeight / 2
+ }
+ }
+
+ property real delegateHeight: (control.height - control.topPadding - control.bottomPadding) / control.visibleItemCount
+ }
+}
diff --git a/src/imports/extras/extras.pro b/src/imports/extras/extras.pro
index 675a058f..79e9bbfe 100644
--- a/src/imports/extras/extras.pro
+++ b/src/imports/extras/extras.pro
@@ -10,10 +10,13 @@ OTHER_FILES += \
QML_FILES = \
Drawer.qml \
- SwipeView.qml
+ SwipeView.qml \
+ Tumbler.qml
SOURCES += \
$$PWD/qtquickextras2plugin.cpp
CONFIG += no_cxx_module
load(qml_plugin)
+
+DISTFILES +=
diff --git a/src/imports/extras/qmldir b/src/imports/extras/qmldir
index d21f666e..4ba7c40b 100644
--- a/src/imports/extras/qmldir
+++ b/src/imports/extras/qmldir
@@ -3,3 +3,4 @@ plugin qtquickextras2plugin
classname QtQuickExtras2Plugin
Drawer 2.0 Drawer.qml
SwipeView 2.0 SwipeView.qml
+Tumbler 2.0 Tumbler.qml
diff --git a/src/imports/extras/qtquickextras2plugin.cpp b/src/imports/extras/qtquickextras2plugin.cpp
index 2a099ae2..2a930ffd 100644
--- a/src/imports/extras/qtquickextras2plugin.cpp
+++ b/src/imports/extras/qtquickextras2plugin.cpp
@@ -38,6 +38,7 @@
#include <QtQuickExtras/private/qquickdrawer_p.h>
#include <QtQuickExtras/private/qquickswipeview_p.h>
+#include <QtQuickExtras/private/qquicktumbler_p.h>
QT_BEGIN_NAMESPACE
@@ -54,6 +55,8 @@ void QtQuickExtras2Plugin::registerTypes(const char *uri)
{
qmlRegisterType<QQuickDrawer>(uri, 2, 0, "AbstractDrawer");
qmlRegisterType<QQuickSwipeView>(uri, 2, 0, "AbstractSwipeView");
+ qmlRegisterType<QQuickTumbler>(uri, 2, 0, "AbstractTumbler");
+ qmlRegisterType<QQuickTumblerAttached>();
}
QT_END_NAMESPACE
diff --git a/tests/auto/extras/data/TumblerDatePicker.qml b/tests/auto/extras/data/TumblerDatePicker.qml
new file mode 100644
index 00000000..673ed142
--- /dev/null
+++ b/tests/auto/extras/data/TumblerDatePicker.qml
@@ -0,0 +1,88 @@
+/****************************************************************************
+**
+** Copyright (C) 2015 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the test suite of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** You may use this file under the terms of the BSD license as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.6
+import QtQuick.Controls 2.0
+import QtQuick.Extras 2.0
+
+Row {
+ id: datePicker
+
+ readonly property var days: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
+
+ property alias dayTumbler: dayTumbler
+ property alias monthTumbler: monthTumbler
+ property alias yearTumbler: yearTumbler
+
+ Tumbler {
+ id: dayTumbler
+ objectName: "dayTumbler"
+
+ Component.onCompleted: updateModel()
+
+ function updateModel() {
+ var previousIndex = dayTumbler.currentIndex;
+ var array = [];
+ var newDays = datePicker.days[monthTumbler.currentIndex];
+ for (var i = 0; i < newDays; ++i) {
+ array.push(i + 1);
+ }
+ dayTumbler.model = array;
+ dayTumbler.currentIndex = Math.min(newDays - 1, previousIndex);
+ }
+ }
+ Tumbler {
+ id: monthTumbler
+ objectName: "monthTumbler"
+ model: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
+ onCurrentIndexChanged: dayTumbler.updateModel()
+ }
+ Tumbler {
+ id: yearTumbler
+ objectName: "yearTumbler"
+ model: ListModel {
+ Component.onCompleted: {
+ for (var i = 2000; i < 2100; ++i) {
+ append({value: i.toString()});
+ }
+ }
+ }
+ }
+}
diff --git a/tests/auto/extras/data/tst_tumbler.qml b/tests/auto/extras/data/tst_tumbler.qml
new file mode 100644
index 00000000..4618bc8a
--- /dev/null
+++ b/tests/auto/extras/data/tst_tumbler.qml
@@ -0,0 +1,377 @@
+/****************************************************************************
+**
+** Copyright (C) 2015 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the test suite of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** You may use this file under the terms of the BSD license as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.2
+import QtTest 1.0
+import QtQuick.Extras 2.0
+
+TestCase {
+ id: testCase
+ width: 200
+ height: 200
+ visible: true
+ when: windowShown
+ name: "Tumbler"
+
+ property var tumbler: null
+
+ function init() {
+ tumbler = Qt.createQmlObject("import QtQuick.Extras 2.0; Tumbler { }", testCase, "");
+ verify(tumbler, "Tumbler: failed to create an instance");
+ }
+
+ function cleanup() {
+ tumbler.destroy();
+ }
+
+ function tumblerXCenter() {
+ return tumbler.leftPadding + tumbler.width / 2;
+ }
+
+ // visualItemIndex is from 0 to the amount of visible items.
+ function itemCenterPos(visualItemIndex) {
+ var halfDelegateHeight = tumbler.contentItem.delegateHeight / 2;
+ var yCenter = tumbler.y + tumbler.topPadding + halfDelegateHeight
+ + (tumbler.contentItem.delegateHeight * visualItemIndex);
+ return Qt.point(tumblerXCenter(), yCenter);
+ }
+
+ function checkItemSizes() {
+ var contentChildren = tumbler.contentItem.hasOwnProperty("contentItem")
+ ? tumbler.contentItem.contentItem.children : tumbler.contentItem.children;
+ verify(contentChildren.length >= tumbler.count);
+ for (var i = 0; i < contentChildren.length; ++i) {
+ compare(contentChildren[i].width, tumbler.width);
+ compare(contentChildren[i].height, tumbler.contentItem.delegateHeight);
+ }
+ }
+
+ function tst_dynamicContentItemChange() {
+ // test that currentIndex is maintained between contentItem changes...
+ }
+
+ function test_currentIndex() {
+ tumbler.model = 5;
+
+ compare(tumbler.currentIndex, 0);
+ waitForRendering(tumbler);
+
+ // Set it through user interaction.
+ var pos = Qt.point(tumblerXCenter(), tumbler.height / 2);
+ mouseDrag(tumbler, pos.x, pos.y, 0, -tumbler.contentItem.delegateHeight / 2, Qt.LeftButton, Qt.NoModifier, 200);
+ compare(tumbler.currentIndex, 1);
+ compare(tumbler.contentItem.currentIndex, 1);
+
+ // Set it manually.
+ tumbler.currentIndex = 2;
+ tryCompare(tumbler, "currentIndex", 2);
+ compare(tumbler.contentItem.currentIndex, 2);
+
+ // PathView has 0 as its currentIndex in this case for some reason.
+ tumbler.model = null;
+ tryCompare(tumbler, "currentIndex", 0);
+
+ tumbler.model = ["A", "B", "C"];
+ tryCompare(tumbler, "currentIndex", 0);
+ }
+
+ function test_keyboardNavigation() {
+ tumbler.model = 5;
+ tumbler.forceActiveFocus();
+ var keyClickDelay = 100;
+
+ // Navigate upwards through entire wheel.
+ for (var j = 0; j < tumbler.count - 1; ++j) {
+ keyClick(Qt.Key_Up, Qt.NoModifier, keyClickDelay);
+ tryCompare(tumbler.contentItem, "offset", j + 1);
+ compare(tumbler.currentIndex, tumbler.count - 1 - j);
+ }
+
+ keyClick(Qt.Key_Up, Qt.NoModifier, keyClickDelay);
+ tryCompare(tumbler.contentItem, "offset", 0);
+ compare(tumbler.currentIndex, 0);
+
+ // Navigate downwards through entire wheel.
+ for (j = 0; j < tumbler.count - 1; ++j) {
+ keyClick(Qt.Key_Down, Qt.NoModifier, keyClickDelay);
+ tryCompare(tumbler.contentItem, "offset", tumbler.count - 1 - j);
+ compare(tumbler.currentIndex, j + 1);
+ }
+
+ keyClick(Qt.Key_Down, Qt.NoModifier, keyClickDelay);
+ tryCompare(tumbler.contentItem, "offset", 0);
+ compare(tumbler.currentIndex, 0);
+ }
+
+ function test_itemsCorrectlyPositioned() {
+ tumbler.model = 4;
+ tumbler.height = 120;
+ compare(tumbler.contentItem.delegateHeight, 40);
+ checkItemSizes();
+
+ wait(tumbler.contentItem.highlightMoveDuration);
+ var firstItemCenterPos = itemCenterPos(1);
+ var firstItem = tumbler.contentItem.itemAt(firstItemCenterPos.x, firstItemCenterPos.y);
+ var actualPos = testCase.mapFromItem(firstItem, 0, 0);
+ compare(actualPos.x, tumbler.leftPadding);
+ compare(actualPos.y, tumbler.topPadding + 40);
+
+ tumbler.forceActiveFocus();
+ keyClick(Qt.Key_Down);
+ tryCompare(tumbler.contentItem, "offset", 3.0);
+ firstItemCenterPos = itemCenterPos(0);
+ firstItem = tumbler.contentItem.itemAt(firstItemCenterPos.x, firstItemCenterPos.y);
+ verify(firstItem);
+ // Test QTBUG-40298.
+ actualPos = testCase.mapFromItem(firstItem, 0, 0);
+ compare(actualPos.x, tumbler.leftPadding);
+ compare(actualPos.y, tumbler.topPadding);
+
+ var secondItemCenterPos = itemCenterPos(1);
+ var secondItem = tumbler.contentItem.itemAt(secondItemCenterPos.x, secondItemCenterPos.y);
+ verify(secondItem);
+ verify(firstItem.y < secondItem.y);
+
+ var thirdItemCenterPos = itemCenterPos(2);
+ var thirdItem = tumbler.contentItem.itemAt(thirdItemCenterPos.x, thirdItemCenterPos.y);
+ verify(thirdItem);
+ verify(firstItem.y < thirdItem.y);
+ verify(secondItem.y < thirdItem.y);
+ }
+
+ function test_resizeAfterFlicking() {
+ // Test QTBUG-40367 (which is actually invalid because it was my fault :)).
+ tumbler.model = 100;
+
+ // Flick in some direction.
+ var pos = Qt.point(tumblerXCenter(), tumbler.topPadding);
+ mouseDrag(tumbler, pos.x, pos.y, 0, tumbler.height - tumbler.bottomPadding,
+ Qt.LeftButton, Qt.NoModifier, 300);
+ tryCompare(tumbler.contentItem, "offset", 4.0);
+
+ tumbler.height += 100;
+ compare(tumbler.contentItem.delegateHeight,
+ (tumbler.height - tumbler.topPadding - tumbler.bottomPadding) / tumbler.visibleItemCount);
+ waitForRendering(tumbler);
+ pos = itemCenterPos(1);
+ var ninetyEighthItem = tumbler.contentItem.itemAt(pos.x, pos.y);
+ verify(ninetyEighthItem);
+ }
+
+ function test_focusPastTumbler() {
+ var mouseArea = Qt.createQmlObject(
+ "import QtQuick 2.2; TextInput { activeFocusOnTab: true; width: 50; height: 50 }", testCase, "");
+
+ tumbler.forceActiveFocus();
+ verify(tumbler.activeFocus);
+
+ keyClick(Qt.Key_Tab);
+ verify(!tumbler.activeFocus);
+ verify(mouseArea.activeFocus);
+
+ mouseArea.destroy();
+ }
+
+ function test_datePicker() {
+ tumbler.destroy();
+
+ var component = Qt.createComponent("TumblerDatePicker.qml");
+ compare(component.status, Component.Ready, component.errorString());
+ tumbler = component.createObject(testCase);
+ // Should not be any warnings.
+
+ compare(tumbler.dayTumbler.currentIndex, 0);
+ compare(tumbler.dayTumbler.count, 31);
+ compare(tumbler.monthTumbler.currentIndex, 0);
+ compare(tumbler.monthTumbler.count, 12);
+ compare(tumbler.yearTumbler.currentIndex, 0);
+ compare(tumbler.yearTumbler.count, 100);
+
+ verify(tumbler.dayTumbler.contentItem.children.length >= tumbler.dayTumbler.visibleItemCount);
+ verify(tumbler.monthTumbler.contentItem.children.length >= tumbler.monthTumbler.visibleItemCount);
+ // TODO: do this properly somehow
+ wait(100);
+ verify(tumbler.yearTumbler.contentItem.children.length >= tumbler.yearTumbler.visibleItemCount);
+
+ // March.
+ tumbler.monthTumbler.currentIndex = 2;
+ tryCompare(tumbler.monthTumbler, "currentIndex", 2);
+
+ // 30th of March.
+ tumbler.dayTumbler.currentIndex = 29;
+ tryCompare(tumbler.dayTumbler, "currentIndex", 29);
+
+ // February.
+ tumbler.monthTumbler.currentIndex = 1;
+ tryCompare(tumbler.monthTumbler, "currentIndex", 1);
+ tryCompare(tumbler.dayTumbler, "currentIndex", 27);
+ }
+
+ function test_displacement_data() {
+ var data = [
+ // At 0 offset, the first item is current.
+ { index: 0, offset: 0, expectedDisplacement: 0 },
+ { index: 1, offset: 0, expectedDisplacement: -1 },
+ { index: 5, offset: 0, expectedDisplacement: 1 },
+ // When we start to move the first item down, the second item above it starts to become current.
+ { index: 0, offset: 0.25, expectedDisplacement: -0.25 },
+ { index: 1, offset: 0.25, expectedDisplacement: -1.25 },
+ { index: 5, offset: 0.25, expectedDisplacement: 0.75 },
+ { index: 0, offset: 0.5, expectedDisplacement: -0.5 },
+ { index: 1, offset: 0.5, expectedDisplacement: -1.5 },
+ { index: 5, offset: 0.5, expectedDisplacement: 0.5 },
+ // By this stage, the delegate at index 1 is destroyed, so we can't test its displacement.
+ { index: 0, offset: 0.75, expectedDisplacement: -0.75 },
+ { index: 5, offset: 0.75, expectedDisplacement: 0.25 },
+ { index: 0, offset: 4.75, expectedDisplacement: 1.25 },
+ { index: 1, offset: 4.75, expectedDisplacement: 0.25 },
+ { index: 0, offset: 4.5, expectedDisplacement: 1.5 },
+ { index: 1, offset: 4.5, expectedDisplacement: 0.5 },
+ { index: 0, offset: 4.25, expectedDisplacement: 1.75 },
+ { index: 1, offset: 4.25, expectedDisplacement: 0.75 }
+ ];
+ for (var i = 0; i < data.length; ++i) {
+ var row = data[i];
+ row.tag = "delegate" + row.index + " offset=" + row.offset + " expectedDisplacement=" + row.expectedDisplacement;
+ }
+ return data;
+ }
+
+ property Component displacementDelegate: Text {
+ objectName: "delegate" + index
+ text: modelData
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+
+ property real displacement: AbstractTumbler.displacement
+ }
+
+ 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.delegate = displacementDelegate;
+ tumbler.model = 6;
+ compare(tumbler.count, 6);
+
+ var delegate = findChild(tumbler.contentItem, "delegate" + data.index);
+ verify(delegate);
+
+ tumbler.contentItem.offset = data.offset;
+ compare(delegate.displacement, data.expectedDisplacement);
+
+ // test displacement after adding and removing items
+ }
+
+ property Component objectNameDelegate: Text {
+ objectName: "delegate" + index
+ text: modelData
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ function test_visibleItemCount_data() {
+ var data = [
+ // e.g. {0: 2} = {delegate index: y pos / delegate height}
+ // Skip item at index 3, because it's out of view.
+ { model: 6, visibleItemCount: 5, expectedYPositions: {0: 2, 1: 3, 2: 4, 4: 0} },
+ { model: 5, visibleItemCount: 3, expectedYPositions: {0: 1, 1: 2, 4: 0} },
+ // Takes up the whole view.
+ { model: 2, visibleItemCount: 1, expectedYPositions: {0: 0} },
+ ];
+
+ for (var i = 0; i < data.length; ++i) {
+ data[i].tag = "items=" + data[i].model + ", visibleItemCount=" + data[i].visibleItemCount;
+ }
+ return data;
+ }
+
+ function test_visibleItemCount(data) {
+ tumbler.delegate = objectNameDelegate;
+ tumbler.visibleItemCount = data.visibleItemCount;
+
+ tumbler.model = data.model;
+ compare(tumbler.count, data.model);
+
+ for (var delegateIndex = 0; delegateIndex < data.visibleItemCount; ++delegateIndex) {
+ if (data.expectedYPositions.hasOwnProperty(delegateIndex)) {
+ var delegate = findChild(tumbler.contentItem, "delegate" + delegateIndex);
+ verify(delegate, "Delegate found at index " + delegateIndex);
+ var expectedYPos = data.expectedYPositions[delegateIndex] * tumbler.contentItem.delegateHeight;
+ compare(delegate.mapToItem(tumbler.contentItem, 0, 0).y, expectedYPos);
+ }
+ }
+ }
+
+ property Component wrongDelegateTypeComponent: QtObject {
+ property real displacement: AbstractTumbler.displacement
+ }
+
+ property Component noParentDelegateComponent: Item {
+ property real displacement: AbstractTumbler.displacement
+ }
+
+ property Component listViewComponent: ListView {}
+ property Component simpleDisplacementDelegate: Text {
+ property real displacement: AbstractTumbler.displacement
+ property int index: -1
+ }
+
+ function test_attachedProperties() {
+ // TODO: crashes somewhere in QML's guts
+// tumbler.model = 5;
+// tumbler.delegate = wrongDelegateTypeComponent;
+// ignoreWarning("Attached properties of Tumbler must be accessed from within a delegate item");
+// // Cause displacement to be changed. The warning isn't triggered if we don't do this.
+// tumbler.contentItem.offset += 1;
+
+ ignoreWarning("Attached properties of Tumbler must be accessed from within a delegate item that has a parent");
+ noParentDelegateComponent.createObject(null);
+
+ ignoreWarning("Attempting to access attached property on item without an \"index\" property");
+ var object = noParentDelegateComponent.createObject(testCase);
+ object.destroy();
+
+ var listView = listViewComponent.createObject(testCase);
+ ignoreWarning("contentItems other than PathView are not currently supported");
+ object = simpleDisplacementDelegate.createObject(listView);
+ object.destroy();
+ listView.destroy();
+ }
+}