/**************************************************************************** ** ** 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 #include 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 contentItemChildItems(QQuickItem *contentItem) { if (!contentItem) return QList(); // PathView has no contentItem property, but ListView does. QQuickFlickable *flickable = qobject_cast(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(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() : 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(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(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 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"