diff options
Diffstat (limited to 'src/quicktemplates2/qquicktumbler.cpp')
-rw-r--r-- | src/quicktemplates2/qquicktumbler.cpp | 603 |
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" |