diff options
-rw-r--r-- | src/extras/extras.pri | 6 | ||||
-rw-r--r-- | src/extras/qquicktumbler.cpp | 416 | ||||
-rw-r--r-- | src/extras/qquicktumbler_p.h | 136 | ||||
-rw-r--r-- | src/imports/extras/Tumbler.qml | 76 | ||||
-rw-r--r-- | src/imports/extras/extras.pro | 5 | ||||
-rw-r--r-- | src/imports/extras/qmldir | 1 | ||||
-rw-r--r-- | src/imports/extras/qtquickextras2plugin.cpp | 3 | ||||
-rw-r--r-- | tests/auto/extras/data/TumblerDatePicker.qml | 88 | ||||
-rw-r--r-- | tests/auto/extras/data/tst_tumbler.qml | 377 |
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(); + } +} |