aboutsummaryrefslogtreecommitdiffstats
path: root/src/quicktemplates2/qquicktumbler.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/quicktemplates2/qquicktumbler.cpp')
-rw-r--r--src/quicktemplates2/qquicktumbler.cpp603
1 files changed, 603 insertions, 0 deletions
diff --git a/src/quicktemplates2/qquicktumbler.cpp b/src/quicktemplates2/qquicktumbler.cpp
new file mode 100644
index 00000000..9f19f661
--- /dev/null
+++ b/src/quicktemplates2/qquicktumbler.cpp
@@ -0,0 +1,603 @@
+/****************************************************************************
+**
+** 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$
+**
+****************************************************************************/
+
+#include "qquicktumbler_p.h"
+
+#include <QtQuick/private/qquickflickable_p.h>
+#include <QtQuickTemplates2/private/qquickcontrol_p_p.h>
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \qmltype Tumbler
+ \inherits Control
+ \instantiates QQuickTumbler
+ \inqmlmodule QtQuick.Controls
+ \since 5.7
+ \ingroup qtquickcontrols2-input
+ \brief Spinnable wheel of items that can be selected.
+
+ \image qtquickcontrols2-tumbler-wrap.gif
+
+ \code
+ Tumbler {
+ model: 5
+ // ...
+ }
+ \endcode
+
+ \section1 Non-wrapping Tumbler
+
+ The default contentItem of Tumbler is a \l PathView, which wraps when it
+ reaches the top and bottom. To achieve a non-wrapping Tumbler, use ListView
+ as the contentItem:
+
+ \snippet tst_tumbler.qml contentItem
+
+ \sa {Customizing Tumbler}, {Input Controls}
+*/
+
+class QQuickTumblerPrivate : public QQuickControlPrivate, public QQuickItemChangeListener
+{
+ Q_DECLARE_PUBLIC(QQuickTumbler)
+
+public:
+ QQuickTumblerPrivate() :
+ delegate(nullptr),
+ visibleItemCount(3)
+ {
+ }
+
+ ~QQuickTumblerPrivate()
+ {
+ }
+
+ QVariant model;
+ QQmlComponent *delegate;
+ int visibleItemCount;
+
+ void _q_updateItemHeights();
+ void _q_updateItemWidths();
+
+ void itemChildAdded(QQuickItem *, QQuickItem *) override;
+ void itemChildRemoved(QQuickItem *, QQuickItem *) override;
+};
+
+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();
+}
+
+namespace {
+ static inline qreal delegateHeight(const QQuickTumbler *tumbler)
+ {
+ return tumbler->availableHeight() / tumbler->visibleItemCount();
+ }
+
+ enum ContentItemType {
+ UnsupportedContentItemType,
+ PathViewContentItem,
+ ListViewContentItem
+ };
+
+ static inline QQuickItem *actualContentItem(QQuickItem *rootContentItem, ContentItemType contentType)
+ {
+ if (contentType == PathViewContentItem)
+ return rootContentItem;
+ else if (contentType == ListViewContentItem)
+ return qobject_cast<QQuickFlickable*>(rootContentItem)->contentItem();
+
+ return nullptr;
+ }
+
+ static inline ContentItemType contentItemType(QQuickItem *rootContentItem)
+ {
+ if (rootContentItem->inherits("QQuickPathView"))
+ return PathViewContentItem;
+ else if (rootContentItem->inherits("QQuickListView"))
+ return ListViewContentItem;
+
+ return UnsupportedContentItemType;
+ }
+
+ static inline ContentItemType contentItemTypeFromDelegate(QQuickItem *delegateItem)
+ {
+ if (delegateItem->parentItem()->inherits("QQuickPathView")) {
+ return PathViewContentItem;
+ } else if (delegateItem->parentItem()->parentItem()
+ && delegateItem->parentItem()->parentItem()->inherits("QQuickListView")) {
+ return ListViewContentItem;
+ }
+
+ return UnsupportedContentItemType;
+ }
+}
+
+void QQuickTumblerPrivate::_q_updateItemHeights()
+{
+ // Can't use our own private padding members here, as the padding property might be set,
+ // which doesn't affect them, only their getters.
+ Q_Q(const QQuickTumbler);
+ const qreal itemHeight = delegateHeight(q);
+ const auto items = contentItemChildItems(contentItem);
+ for (QQuickItem *childItem : items)
+ childItem->setHeight(itemHeight);
+}
+
+void QQuickTumblerPrivate::_q_updateItemWidths()
+{
+ Q_Q(const QQuickTumbler);
+ const qreal availableWidth = q->availableWidth();
+ const auto items = contentItemChildItems(contentItem);
+ for (QQuickItem *childItem : items)
+ childItem->setWidth(availableWidth);
+}
+
+void QQuickTumblerPrivate::itemChildAdded(QQuickItem *, QQuickItem *)
+{
+ _q_updateItemWidths();
+ _q_updateItemHeights();
+}
+
+void QQuickTumblerPrivate::itemChildRemoved(QQuickItem *, QQuickItem *)
+{
+ _q_updateItemWidths();
+ _q_updateItemHeights();
+}
+
+QQuickTumbler::QQuickTumbler(QQuickItem *parent) :
+ QQuickControl(*(new QQuickTumblerPrivate), parent)
+{
+ setActiveFocusOnTab(true);
+
+ connect(this, SIGNAL(leftPaddingChanged()), this, SLOT(_q_updateItemWidths()));
+ connect(this, SIGNAL(rightPaddingChanged()), this, SLOT(_q_updateItemWidths()));
+ connect(this, SIGNAL(topPaddingChanged()), this, SLOT(_q_updateItemHeights()));
+ connect(this, SIGNAL(bottomPaddingChanged()), this, SLOT(_q_updateItemHeights()));
+}
+
+QQuickTumbler::~QQuickTumbler()
+{
+}
+
+/*!
+ \qmlproperty variant QtQuick.Controls::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)
+ return;
+
+ d->model = model;
+ emit modelChanged();
+}
+
+/*!
+ \qmlproperty int QtQuick.Controls::Tumbler::count
+ \readonly
+
+ 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 QtQuick.Controls::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 QtQuick.Controls::Tumbler::currentItem
+ \readonly
+
+ 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*>() : nullptr;
+}
+
+/*!
+ \qmlproperty Component QtQuick.Controls::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)
+ return;
+
+ d->delegate = delegate;
+ emit delegateChanged();
+}
+
+/*!
+ \qmlproperty int QtQuick.Controls::Tumbler::visibleItemCount
+
+ This property holds the number of items visible in the tumbler. It must be
+ an odd number, as the current item is always vertically centered.
+*/
+int QQuickTumbler::visibleItemCount() const
+{
+ Q_D(const QQuickTumbler);
+ return d->visibleItemCount;
+}
+
+void QQuickTumbler::setVisibleItemCount(int visibleItemCount)
+{
+ Q_D(QQuickTumbler);
+ if (visibleItemCount == d->visibleItemCount)
+ return;
+
+ d->visibleItemCount = visibleItemCount;
+ d->_q_updateItemHeights();
+ emit visibleItemCountChanged();
+}
+
+QQuickTumblerAttached *QQuickTumbler::qmlAttachedProperties(QObject *object)
+{
+ QQuickItem *delegateItem = qobject_cast<QQuickItem *>(object);
+ if (!delegateItem) {
+ qWarning() << "Tumbler: attached properties of Tumbler must be accessed from within a delegate item";
+ return nullptr;
+ }
+
+ return new QQuickTumblerAttached(delegateItem);
+}
+
+void QQuickTumbler::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
+{
+ Q_D(QQuickTumbler);
+
+ QQuickControl::geometryChanged(newGeometry, oldGeometry);
+
+ d->_q_updateItemHeights();
+
+ if (newGeometry.width() != oldGeometry.width())
+ d->_q_updateItemWidths();
+}
+
+void QQuickTumbler::componentComplete()
+{
+ Q_D(QQuickTumbler);
+ QQuickControl::componentComplete();
+ d->_q_updateItemHeights();
+ d->_q_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()));
+
+ ContentItemType oldContentItemType = contentItemType(oldItem);
+ QQuickItem *actualOldContentItem = actualContentItem(oldItem, oldContentItemType);
+ QQuickItemPrivate *actualContentItemPrivate = QQuickItemPrivate::get(actualOldContentItem);
+ actualContentItemPrivate->removeItemChangeListener(d, QQuickItemPrivate::Children);
+ }
+
+ if (newItem) {
+ ContentItemType contentType = contentItemType(newItem);
+ if (contentType == UnsupportedContentItemType) {
+ qWarning() << "Tumbler: contentItems other than PathView and ListView are not supported";
+ return;
+ }
+
+ connect(newItem, SIGNAL(currentIndexChanged()), this, SIGNAL(currentIndexChanged()));
+ connect(newItem, SIGNAL(currentItemChanged()), this, SIGNAL(currentItemChanged()));
+ connect(newItem, SIGNAL(countChanged()), this, SIGNAL(countChanged()));
+
+ QQuickItem *actualNewContentItem = actualContentItem(newItem, contentType);
+ QQuickItemPrivate *actualContentItemPrivate = QQuickItemPrivate::get(actualNewContentItem);
+ actualContentItemPrivate->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(nullptr),
+ index(-1),
+ displacement(0)
+ {
+ if (!delegateItem->parentItem()) {
+ qWarning() << "Tumbler: attached properties must be accessed from within a delegate item that has a parent";
+ return;
+ }
+
+ QVariant indexContextProperty = qmlContext(delegateItem)->contextProperty(QStringLiteral("index"));
+ if (!indexContextProperty.isValid()) {
+ qWarning() << "Tumbler: attempting to access attached property on item without an \"index\" property";
+ return;
+ }
+
+ index = indexContextProperty.toInt();
+ const ContentItemType contentItemType = contentItemTypeFromDelegate(delegateItem);
+ if (contentItemType == UnsupportedContentItemType)
+ return;
+
+ // ListView has an "additional" content item.
+ tumbler = qobject_cast<QQuickTumbler* >(contentItemType == PathViewContentItem
+ ? delegateItem->parentItem()->parentItem() : delegateItem->parentItem()->parentItem()->parentItem());
+ Q_ASSERT(tumbler);
+ }
+
+ ~QQuickTumblerAttachedPrivate() {
+ }
+
+ void itemGeometryChanged(QQuickItem *item, const QRectF &newGeometry, const QRectF &oldGeometry) override;
+ void itemChildAdded(QQuickItem *, QQuickItem *) override;
+ void itemChildRemoved(QQuickItem *, QQuickItem *) override;
+
+ void _q_calculateDisplacement();
+ void emitIfDisplacementChanged(qreal oldDisplacement, qreal newDisplacement);
+
+ // The Tumbler that contains the delegate. Required to calculated the displacement.
+ QPointer<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 *item, 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.
+
+ // item is the "actual content item" of Tumbler's contentItem, i.e. a PathView or ListView.contentItem
+ QQuickItemPrivate *p = QQuickItemPrivate::get(item);
+ p->removeItemChangeListener(this, QQuickItemPrivate::Geometry | QQuickItemPrivate::Children);
+ }
+}
+
+void QQuickTumblerAttachedPrivate::_q_calculateDisplacement()
+{
+ const int previousDisplacement = displacement;
+ displacement = 0;
+
+ if (!tumbler) {
+ emitIfDisplacementChanged(previousDisplacement, displacement);
+ return;
+ }
+
+ const int count = tumbler->count();
+ // This can happen in tests, so it may happen in normal usage too.
+ if (count == 0) {
+ emitIfDisplacementChanged(previousDisplacement, displacement);
+ return;
+ }
+
+ ContentItemType contentType = contentItemType(tumbler->contentItem());
+ if (contentType == UnsupportedContentItemType) {
+ emitIfDisplacementChanged(previousDisplacement, displacement);
+ return;
+ }
+
+ qreal offset = 0;
+
+ if (contentType == PathViewContentItem) {
+ offset = tumbler->contentItem()->property("offset").toReal();
+
+ displacement = count > 1 ? count - index - offset : 0;
+ // Don't add 1 if count <= visibleItemCount
+ const int visibleItems = tumbler->visibleItemCount();
+ int halfVisibleItems = visibleItems / 2 + (visibleItems < count ? 1 : 0);
+ if (displacement > halfVisibleItems)
+ displacement -= count;
+ else if (displacement < -halfVisibleItems)
+ displacement += count;
+ } else {
+ const qreal contentY = tumbler->contentItem()->property("contentY").toReal();
+ const qreal delegateH = delegateHeight(tumbler);
+ const qreal preferredHighlightBegin = tumbler->contentItem()->property("preferredHighlightBegin").toReal();
+ // Tumbler's displacement goes from negative at the top to positive towards the bottom, so we must switch this around.
+ const qreal reverseDisplacement = (contentY + preferredHighlightBegin) / delegateH;
+ displacement = reverseDisplacement - index;
+ }
+
+ emitIfDisplacementChanged(previousDisplacement, displacement);
+}
+
+void QQuickTumblerAttachedPrivate::emitIfDisplacementChanged(qreal oldDisplacement, qreal newDisplacement)
+{
+ Q_Q(QQuickTumblerAttached);
+ if (newDisplacement != oldDisplacement)
+ emit q->displacementChanged();
+}
+
+QQuickTumblerAttached::QQuickTumblerAttached(QQuickItem *delegateItem) :
+ QObject(*(new QQuickTumblerAttachedPrivate(delegateItem)), delegateItem)
+{
+ Q_D(QQuickTumblerAttached);
+ if (d->tumbler) {
+ QQuickItem *rootContentItem = d->tumbler->contentItem();
+ const ContentItemType contentType = contentItemType(rootContentItem);
+ QQuickItemPrivate *p = QQuickItemPrivate::get(actualContentItem(rootContentItem, contentType));
+ p->addItemChangeListener(d, QQuickItemPrivate::Geometry | QQuickItemPrivate::Children);
+
+ const char *contentItemSignal = contentType == PathViewContentItem
+ ? SIGNAL(offsetChanged()) : SIGNAL(contentYChanged());
+ connect(d->tumbler->contentItem(), contentItemSignal, this, SLOT(_q_calculateDisplacement()));
+
+ d->_q_calculateDisplacement();
+ }
+}
+
+QQuickTumblerAttached::~QQuickTumblerAttached()
+{
+ Q_D(QQuickTumblerAttached);
+ if (!d->tumbler || !d->tumbler->contentItem())
+ return;
+
+ QQuickItem *rootContentItem = d->tumbler->contentItem();
+ const ContentItemType contentType = contentItemType(rootContentItem);
+ QQuickItem *actualItem = actualContentItem(rootContentItem, contentType);
+ if (!actualItem)
+ return;
+
+ QQuickItemPrivate *p = QQuickItemPrivate::get(actualItem);
+ p->removeItemChangeListener(d, QQuickItemPrivate::Geometry | QQuickItemPrivate::Children);
+}
+
+/*!
+ \qmlattachedproperty Tumbler QtQuick.Controls::Tumbler::tumbler
+ \readonly
+
+ This attached property holds the tumbler. The property can be attached to
+ a tumbler delegate. The value is \c null if the item is not a tumbler delegate.
+*/
+QQuickTumbler *QQuickTumblerAttached::tumbler() const
+{
+ Q_D(const QQuickTumblerAttached);
+ return d->tumbler;
+}
+
+/*!
+ \qmlattachedproperty real QtQuick.Controls::Tumbler::displacement
+ \readonly
+
+ This attached property holds a value from \c {-visibleItemCount / 2} to
+ \c {visibleItemCount / 2}, which represents how far away this item is from
+ being the current item, with \c 0 being completely current.
+
+ For example, the item below will be 40% opaque when it is not the current item,
+ and transition to 100% opacity when it becomes the current item:
+
+ \code
+ delegate: Text {
+ text: modelData
+ opacity: 0.4 + Math.max(0, 1 - Math.abs(Tumbler.displacement)) * 0.6
+ }
+ \endcode
+*/
+qreal QQuickTumblerAttached::displacement() const
+{
+ Q_D(const QQuickTumblerAttached);
+ return d->displacement;
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qquicktumbler_p.cpp"