diff options
Diffstat (limited to 'src/quicktemplates2/qquicktumbler.cpp')
-rw-r--r-- | src/quicktemplates2/qquicktumbler.cpp | 594 |
1 files changed, 437 insertions, 157 deletions
diff --git a/src/quicktemplates2/qquicktumbler.cpp b/src/quicktemplates2/qquicktumbler.cpp index 9f19f661..cf5df27a 100644 --- a/src/quicktemplates2/qquicktumbler.cpp +++ b/src/quicktemplates2/qquicktumbler.cpp @@ -36,8 +36,10 @@ #include "qquicktumbler_p.h" +#include <QtQml/qqmlinfo.h> #include <QtQuick/private/qquickflickable_p.h> #include <QtQuickTemplates2/private/qquickcontrol_p_p.h> +#include <QtQuickTemplates2/private/qquicktumbler_p_p.h> QT_BEGIN_NAMESPACE @@ -59,51 +61,48 @@ QT_BEGIN_NAMESPACE } \endcode - \section1 Non-wrapping Tumbler + Tumbler allows the user to select an option from a spinnable \e "wheel" of + items. It is useful for when there are too many options to use, for + example, a RadioButton, and too few options to require the use of an + editable SpinBox. It is convenient in that it requires no keyboard usage + and wraps around at each end when there are a large number of items. - 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: + The API is similar to that of views like \l ListView and \l PathView; a + \l model and \l delegate can be set, and the \l count and \l currentItem + properties provide read-only access to information about the view. - \snippet tst_tumbler.qml contentItem + Unlike views like \l PathView and \l ListView, however, there is always a + current item (when the model isn't empty). This means that when \l count is + equal to \c 0, \l currentIndex will be \c -1. In all other cases, it will + be greater than or equal to \c 0. + + By default, Tumbler \l {wrap}{wraps} when it reaches the top and bottom, as + long as there are more items in the model than there are visible items; + that is, when \l count is greater than \l visibleItemCount: + + \snippet qtquickcontrols2-tumbler-timePicker.qml tumbler \sa {Customizing Tumbler}, {Input Controls} */ -class QQuickTumblerPrivate : public QQuickControlPrivate, public QQuickItemChangeListener +QQuickTumblerPrivate::QQuickTumblerPrivate() : + delegate(nullptr), + visibleItemCount(5), + wrap(true), + explicitWrap(false), + ignoreWrapChanges(false), + view(nullptr), + viewContentItem(nullptr), + viewContentItemType(UnsupportedContentItemType), + currentIndex(-1), + pendingCurrentIndex(-1), + ignoreCurrentIndexChanges(false), + count(0) { - 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) +QQuickTumblerPrivate::~QQuickTumblerPrivate() { - 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 { @@ -111,44 +110,55 @@ namespace { { 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; +/* + Finds the contentItem of the view that is a child of the control's \a contentItem. + The type is stored in \a type. +*/ +QQuickItem *QQuickTumblerPrivate::determineViewType(QQuickItem *contentItem) +{ + if (contentItem->inherits("QQuickPathView")) { + view = contentItem; + viewContentItem = contentItem; + viewContentItemType = PathViewContentItem; + return contentItem; + } else if (contentItem->inherits("QQuickListView")) { + view = contentItem; + viewContentItem = qobject_cast<QQuickFlickable*>(contentItem)->contentItem(); + viewContentItemType = ListViewContentItem; + return contentItem; + } else { + const auto childItems = contentItem->childItems(); + for (QQuickItem *childItem : childItems) { + QQuickItem *item = determineViewType(childItem); + if (item) + return item; + } } - static inline ContentItemType contentItemType(QQuickItem *rootContentItem) - { - if (rootContentItem->inherits("QQuickPathView")) - return PathViewContentItem; - else if (rootContentItem->inherits("QQuickListView")) - return ListViewContentItem; + resetViewData(); + return nullptr; +} - return UnsupportedContentItemType; - } +void QQuickTumblerPrivate::resetViewData() +{ + view = nullptr; + viewContentItem = nullptr; + viewContentItemType = 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; - } +QList<QQuickItem *> QQuickTumblerPrivate::viewContentItemChildItems() const +{ + if (!viewContentItem) + return QList<QQuickItem *>(); - return UnsupportedContentItemType; - } + return viewContentItem->childItems(); +} + +QQuickTumblerPrivate *QQuickTumblerPrivate::get(QQuickTumbler *tumbler) +{ + return tumbler->d_func(); } void QQuickTumblerPrivate::_q_updateItemHeights() @@ -157,7 +167,7 @@ void QQuickTumblerPrivate::_q_updateItemHeights() // which doesn't affect them, only their getters. Q_Q(const QQuickTumbler); const qreal itemHeight = delegateHeight(q); - const auto items = contentItemChildItems(contentItem); + const auto items = viewContentItemChildItems(); for (QQuickItem *childItem : items) childItem->setHeight(itemHeight); } @@ -166,11 +176,49 @@ void QQuickTumblerPrivate::_q_updateItemWidths() { Q_Q(const QQuickTumbler); const qreal availableWidth = q->availableWidth(); - const auto items = contentItemChildItems(contentItem); + const auto items = viewContentItemChildItems(); for (QQuickItem *childItem : items) childItem->setWidth(availableWidth); } +void QQuickTumblerPrivate::_q_onViewCurrentIndexChanged() +{ + Q_Q(QQuickTumbler); + if (view && !ignoreCurrentIndexChanges) { + const int oldCurrentIndex = currentIndex; + currentIndex = view->property("currentIndex").toInt(); + if (oldCurrentIndex != currentIndex) + emit q->currentIndexChanged(); + } +} + +void QQuickTumblerPrivate::_q_onViewCountChanged() +{ + Q_Q(QQuickTumbler); + + setCount(view->property("count").toInt()); + + if (count > 0) { + if (pendingCurrentIndex != -1) { + // If there was an attempt to set currentIndex at creation, try to finish that attempt now. + // componentComplete() is too early, because the count might only be known sometime after completion. + q->setCurrentIndex(pendingCurrentIndex); + // If we could successfully set the currentIndex, consider it done. + // Otherwise, we'll try again later in updatePolish(). + if (currentIndex == pendingCurrentIndex) + pendingCurrentIndex = -1; + else + q->polish(); + } else if (currentIndex == -1) { + // If new items were added and our currentIndex was -1, we must + // enforce our rule of a non-negative currentIndex when count > 0. + q->setCurrentIndex(0); + } + } else { + q->setCurrentIndex(-1); + } +} + void QQuickTumblerPrivate::itemChildAdded(QQuickItem *, QQuickItem *) { _q_updateItemWidths(); @@ -196,6 +244,9 @@ QQuickTumbler::QQuickTumbler(QQuickItem *parent) : QQuickTumbler::~QQuickTumbler() { + Q_D(QQuickTumbler); + // Ensure that the item change listener is removed. + d->disconnectFromView(); } /*! @@ -215,8 +266,17 @@ void QQuickTumbler::setModel(const QVariant &model) if (model == d->model) return; + d->lockWrap(); + d->model = model; emit modelChanged(); + + d->unlockWrap(); + + // Don't try to correct the currentIndex if count() isn't known yet. + // We can check in setupViewData() instead. + if (isComponentComplete() && d->view && count() == 0) + setCurrentIndex(-1); } /*! @@ -228,24 +288,67 @@ void QQuickTumbler::setModel(const QVariant &model) int QQuickTumbler::count() const { Q_D(const QQuickTumbler); - return d->contentItem->property("count").toInt(); + return d->count; } /*! \qmlproperty int QtQuick.Controls::Tumbler::currentIndex This property holds the index of the current item. + + The value of this property is \c -1 when \l count is equal to \c 0. In all + other cases, it will be greater than or equal to \c 0. */ int QQuickTumbler::currentIndex() const { Q_D(const QQuickTumbler); - return d->contentItem ? d->contentItem->property("currentIndex").toInt() : -1; + return d->currentIndex; } void QQuickTumbler::setCurrentIndex(int currentIndex) { Q_D(QQuickTumbler); - d->contentItem->setProperty("currentIndex", currentIndex); + if (currentIndex == d->currentIndex || currentIndex < -1) + return; + + if (!isComponentComplete()) { + // Views can't set currentIndex until they're ready. + d->pendingCurrentIndex = currentIndex; + return; + } + + // -1 doesn't make sense for a non-empty Tumbler, because unlike + // e.g. ListView, there's always one item selected. + // Wait until the component has finished before enforcing this rule, though, + // because the count might not be known yet. + if ((d->count > 0 && currentIndex == -1) || (currentIndex >= d->count)) { + return; + } + + // The view might not have been created yet, as is the case + // if you create a Tumbler component and pass e.g. { currentIndex: 2 } + // to createObject(). + if (d->view) { + // Only actually set our currentIndex if the view was able to set theirs. + bool couldSet = false; + if (d->count == 0 && currentIndex == -1) { + // PathView insists on using 0 as the currentIndex when there are no items. + couldSet = true; + } else { + d->ignoreCurrentIndexChanges = true; + d->view->setProperty("currentIndex", currentIndex); + d->ignoreCurrentIndexChanges = false; + + couldSet = d->view->property("currentIndex").toInt() == currentIndex; + } + + if (couldSet) { + // The view's currentIndex might not have actually changed, but ours has, + // and that's what user code sees. + d->currentIndex = currentIndex; + emit currentIndexChanged(); + } + } } /*! @@ -257,7 +360,7 @@ void QQuickTumbler::setCurrentIndex(int currentIndex) QQuickItem *QQuickTumbler::currentItem() const { Q_D(const QQuickTumbler); - return d->contentItem ? d->contentItem->property("currentItem").value<QQuickItem*>() : nullptr; + return d->view ? d->view->property("currentItem").value<QQuickItem*>() : nullptr; } /*! @@ -304,15 +407,41 @@ void QQuickTumbler::setVisibleItemCount(int visibleItemCount) emit visibleItemCountChanged(); } -QQuickTumblerAttached *QQuickTumbler::qmlAttachedProperties(QObject *object) +/*! + \qmlproperty bool QtQuick.Controls::Tumbler::wrap + \since QtQuick.Controls 2.1 + + This property determines whether or not the tumbler wraps around when it + reaches the top or bottom. + + The default value is \c false when \l count is less than + \l visibleItemCount, as it is simpler to interact with a non-wrapping Tumbler + when there are only a few items. To override this behavior, explicitly set + the value of this property. To return to the default behavior, set this + property to \c undefined. +*/ +bool QQuickTumbler::wrap() const { - QQuickItem *delegateItem = qobject_cast<QQuickItem *>(object); - if (!delegateItem) { - qWarning() << "Tumbler: attached properties of Tumbler must be accessed from within a delegate item"; - return nullptr; - } + Q_D(const QQuickTumbler); + return d->wrap; +} + +void QQuickTumbler::setWrap(bool wrap) +{ + Q_D(QQuickTumbler); + d->setWrap(wrap, true); +} - return new QQuickTumblerAttached(delegateItem); +void QQuickTumbler::resetWrap() +{ + Q_D(QQuickTumbler); + d->explicitWrap = false; + d->setWrapBasedOnCount(); +} + +QQuickTumblerAttached *QQuickTumbler::qmlAttachedProperties(QObject *object) +{ + return new QQuickTumblerAttached(object); } void QQuickTumbler::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) @@ -333,6 +462,13 @@ void QQuickTumbler::componentComplete() QQuickControl::componentComplete(); d->_q_updateItemHeights(); d->_q_updateItemWidths(); + + if (!d->view) { + // Force the view to be created. + emit wrapChanged(); + // Determine the type of view for attached properties, etc. + d->setupViewData(d->contentItem); + } } void QQuickTumbler::contentItemChange(QQuickItem *newItem, QQuickItem *oldItem) @@ -341,59 +477,189 @@ void QQuickTumbler::contentItemChange(QQuickItem *newItem, QQuickItem *oldItem) 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 (oldItem) + d->disconnectFromView(); if (newItem) { - ContentItemType contentType = contentItemType(newItem); - if (contentType == UnsupportedContentItemType) { - qWarning() << "Tumbler: contentItems other than PathView and ListView are not supported"; - return; + // We wait until wrap is set to that we know which type of view to create. + // If we try to set up the view too early, we'll issue warnings about it not existing. + if (isComponentComplete()) { + // Make sure we use the new content item and not the current one, as that won't + // be changed until after contentItemChange() has finished. + d->setupViewData(newItem); } + } +} + +void QQuickTumblerPrivate::disconnectFromView() +{ + Q_Q(QQuickTumbler); + if (!view) { + // If a custom content item is declared, it can happen that + // the original contentItem exists without the view etc. having been + // determined yet, and then this is called when the custom content item + // is eventually set. + return; + } - connect(newItem, SIGNAL(currentIndexChanged()), this, SIGNAL(currentIndexChanged())); - connect(newItem, SIGNAL(currentItemChanged()), this, SIGNAL(currentItemChanged())); - connect(newItem, SIGNAL(countChanged()), this, SIGNAL(countChanged())); + QObject::disconnect(view, SIGNAL(currentIndexChanged()), q, SLOT(_q_onViewCurrentIndexChanged())); + QObject::disconnect(view, SIGNAL(currentItemChanged()), q, SIGNAL(currentItemChanged())); + QObject::disconnect(view, SIGNAL(countChanged()), q, SLOT(_q_onViewCountChanged())); - QQuickItem *actualNewContentItem = actualContentItem(newItem, contentType); - QQuickItemPrivate *actualContentItemPrivate = QQuickItemPrivate::get(actualNewContentItem); - actualContentItemPrivate->addItemChangeListener(d, QQuickItemPrivate::Children); + QQuickItemPrivate *oldViewContentItemPrivate = QQuickItemPrivate::get(viewContentItem); + oldViewContentItemPrivate->removeItemChangeListener(this, 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); - } + resetViewData(); +} + +void QQuickTumblerPrivate::setupViewData(QQuickItem *newControlContentItem) +{ + // Don't do anything if we've already set up. + if (view) + return; + + determineViewType(newControlContentItem); + + if (viewContentItemType == QQuickTumblerPrivate::UnsupportedContentItemType) { + qWarning() << "Tumbler: contentItem must contain either a PathView or a ListView"; + return; } + + Q_Q(QQuickTumbler); + QObject::connect(view, SIGNAL(currentIndexChanged()), q, SLOT(_q_onViewCurrentIndexChanged())); + QObject::connect(view, SIGNAL(currentItemChanged()), q, SIGNAL(currentItemChanged())); + QObject::connect(view, SIGNAL(countChanged()), q, SLOT(_q_onViewCountChanged())); + + QQuickItemPrivate *viewContentItemPrivate = QQuickItemPrivate::get(viewContentItem); + viewContentItemPrivate->addItemChangeListener(this, QQuickItemPrivate::Children); + + // Sync the view's currentIndex with ours. + syncCurrentIndex(); } -void QQuickTumbler::keyPressEvent(QKeyEvent *event) +void QQuickTumblerPrivate::syncCurrentIndex() { - Q_D(QQuickTumbler); + const int actualViewIndex = view->property("currentIndex").toInt(); + Q_Q(QQuickTumbler); + + // Nothing to do. + if (actualViewIndex == currentIndex) + return; + // PathView likes to use 0 as currentIndex for empty models, but we use -1 for that. + if (q->count() == 0 && actualViewIndex == 0) + return; + + ignoreCurrentIndexChanges = true; + view->setProperty("currentIndex", currentIndex); + ignoreCurrentIndexChanges = false; +} + +void QQuickTumblerPrivate::setCount(int newCount) +{ + if (newCount == count) + return; + + count = newCount; + + Q_Q(QQuickTumbler); + setWrapBasedOnCount(); + + emit q->countChanged(); +} + +void QQuickTumblerPrivate::setWrapBasedOnCount() +{ + if (count == 0 || explicitWrap || ignoreWrapChanges) + return; + + setWrap(count >= visibleItemCount, false); +} + +void QQuickTumblerPrivate::setWrap(bool shouldWrap, bool isExplicit) +{ + if (isExplicit) + explicitWrap = true; + + Q_Q(QQuickTumbler); + if (q->isComponentComplete() && shouldWrap == wrap) + return; + + // 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 view changes. + const int oldCurrentIndex = currentIndex; + + disconnectFromView(); + + wrap = shouldWrap; + + // New views will set their currentIndex upon creation, which we'd otherwise + // take as the correct one, so we must ignore them. + ignoreCurrentIndexChanges = true; + + // This will cause the view to be created if our contentItem is a TumblerView. + emit q->wrapChanged(); + + ignoreCurrentIndexChanges = false; + + // The view should have been created now, so we can start determining its type, etc. + // If the delegates use attached properties, this will have already been called, + // in which case it will return early. If the delegate doesn't use attached properties, + // we need to call it here. + setupViewData(contentItem); + + q->setCurrentIndex(oldCurrentIndex); +} + +void QQuickTumblerPrivate::lockWrap() +{ + ignoreWrapChanges = true; +} + +void QQuickTumblerPrivate::unlockWrap() +{ + ignoreWrapChanges = false; + setWrapBasedOnCount(); +} + +void QQuickTumbler::keyPressEvent(QKeyEvent *event) +{ QQuickControl::keyPressEvent(event); - if (event->isAutoRepeat()) + Q_D(QQuickTumbler); + if (event->isAutoRepeat() || !d->view) return; if (event->key() == Qt::Key_Up) { - QMetaObject::invokeMethod(d->contentItem, "decrementCurrentIndex"); + QMetaObject::invokeMethod(d->view, "decrementCurrentIndex"); } else if (event->key() == Qt::Key_Down) { - QMetaObject::invokeMethod(d->contentItem, "incrementCurrentIndex"); + QMetaObject::invokeMethod(d->view, "incrementCurrentIndex"); + } +} + +void QQuickTumbler::updatePolish() +{ + Q_D(QQuickTumbler); + if (d->pendingCurrentIndex != -1) { + // If the count is still 0, it's not going to happen. + if (d->count == 0) { + d->pendingCurrentIndex = -1; + return; + } + + // If there is a pending currentIndex at this stage, it means that + // the view wouldn't set our currentIndex in _q_onViewCountChanged + // because it wasn't ready. Try one last time here. + setCurrentIndex(d->pendingCurrentIndex); + + if (d->currentIndex != d->pendingCurrentIndex && d->currentIndex == -1) { + // If we *still* couldn't set it, it's probably invalid. + // See if we can at least enforce our rule of "non-negative currentIndex when count > 0" instead. + setCurrentIndex(0); + } + + d->pendingCurrentIndex = -1; } } @@ -401,13 +667,17 @@ class QQuickTumblerAttachedPrivate : public QObjectPrivate, public QQuickItemCha { Q_DECLARE_PUBLIC(QQuickTumblerAttached) public: - QQuickTumblerAttachedPrivate(QQuickItem *delegateItem) : + QQuickTumblerAttachedPrivate() : tumbler(nullptr), index(-1), displacement(0) { + } + + void init(QQuickItem *delegateItem) + { if (!delegateItem->parentItem()) { - qWarning() << "Tumbler: attached properties must be accessed from within a delegate item that has a parent"; + qWarning() << "Tumbler: attached properties must be accessed through a delegate item that has a parent"; return; } @@ -418,20 +688,15 @@ public: } 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() { + QQuickItem *parentItem = delegateItem; + while ((parentItem = parentItem->parentItem())) { + if ((tumbler = qobject_cast<QQuickTumbler*>(parentItem))) + break; + } } - void itemGeometryChanged(QQuickItem *item, const QRectF &newGeometry, const QRectF &oldGeometry) override; + void itemGeometryChanged(QQuickItem *item, QQuickGeometryChange change, const QRectF &diff) override; void itemChildAdded(QQuickItem *, QQuickItem *) override; void itemChildRemoved(QQuickItem *, QQuickItem *) override; @@ -446,7 +711,7 @@ public: qreal displacement; }; -void QQuickTumblerAttachedPrivate::itemGeometryChanged(QQuickItem *, const QRectF &, const QRectF &) +void QQuickTumblerAttachedPrivate::itemGeometryChanged(QQuickItem *, QQuickGeometryChange, const QRectF &) { _q_calculateDisplacement(); } @@ -477,41 +742,43 @@ void QQuickTumblerAttachedPrivate::_q_calculateDisplacement() const int previousDisplacement = displacement; displacement = 0; + // Can happen if the attached properties are accessed on the wrong type of item or the tumbler was destroyed. 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) { + // Can happen if there is no ListView or PathView within the contentItem. + QQuickTumblerPrivate *tumblerPrivate = QQuickTumblerPrivate::get(tumbler); + if (!tumblerPrivate->viewContentItem) { emitIfDisplacementChanged(previousDisplacement, displacement); return; } - ContentItemType contentType = contentItemType(tumbler->contentItem()); - if (contentType == UnsupportedContentItemType) { + // The attached property gets created before our count is updated, so just cheat here + // to avoid having to listen to count changes. + const int count = tumblerPrivate->view->property("count").toInt(); + // This can happen in tests, so it may happen in normal usage too. + if (count == 0) { emitIfDisplacementChanged(previousDisplacement, displacement); return; } - qreal offset = 0; - - if (contentType == PathViewContentItem) { - offset = tumbler->contentItem()->property("offset").toReal(); + if (tumblerPrivate->viewContentItemType == QQuickTumblerPrivate::PathViewContentItem) { + const qreal offset = tumblerPrivate->view->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); + const 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 contentY = tumblerPrivate->view->property("contentY").toReal(); const qreal delegateH = delegateHeight(tumbler); - const qreal preferredHighlightBegin = tumbler->contentItem()->property("preferredHighlightBegin").toReal(); + const qreal preferredHighlightBegin = tumblerPrivate->view->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; @@ -527,19 +794,34 @@ void QQuickTumblerAttachedPrivate::emitIfDisplacementChanged(qreal oldDisplaceme emit q->displacementChanged(); } -QQuickTumblerAttached::QQuickTumblerAttached(QQuickItem *delegateItem) : - QObject(*(new QQuickTumblerAttachedPrivate(delegateItem)), delegateItem) +QQuickTumblerAttached::QQuickTumblerAttached(QObject *parent) : + QObject(*(new QQuickTumblerAttachedPrivate), parent) { Q_D(QQuickTumblerAttached); + QQuickItem *delegateItem = qobject_cast<QQuickItem *>(parent); + if (delegateItem) + d->init(delegateItem); + else if (parent) + qmlInfo(parent) << "Tumbler: attached properties of Tumbler must be accessed through a delegate item"; + if (d->tumbler) { - QQuickItem *rootContentItem = d->tumbler->contentItem(); - const ContentItemType contentType = contentItemType(rootContentItem); - QQuickItemPrivate *p = QQuickItemPrivate::get(actualContentItem(rootContentItem, contentType)); + // When the Tumbler is completed, wrapChanged() is emitted to let QQuickTumblerView + // know that it can create the view. The view itself might instantiate delegates + // that use attached properties. At this point, setupViewData() hasn't been called yet + // (it's called on the next line in componentComplete()), so we call it here so that + // we have access to the view. + QQuickTumblerPrivate *tumblerPrivate = QQuickTumblerPrivate::get(d->tumbler); + tumblerPrivate->setupViewData(tumblerPrivate->contentItem); + + if (!tumblerPrivate->viewContentItem) + return; + + QQuickItemPrivate *p = QQuickItemPrivate::get(tumblerPrivate->viewContentItem); p->addItemChangeListener(d, QQuickItemPrivate::Geometry | QQuickItemPrivate::Children); - const char *contentItemSignal = contentType == PathViewContentItem + const char *contentItemSignal = tumblerPrivate->viewContentItemType == QQuickTumblerPrivate::PathViewContentItem ? SIGNAL(offsetChanged()) : SIGNAL(contentYChanged()); - connect(d->tumbler->contentItem(), contentItemSignal, this, SLOT(_q_calculateDisplacement())); + connect(tumblerPrivate->view, contentItemSignal, this, SLOT(_q_calculateDisplacement())); d->_q_calculateDisplacement(); } @@ -548,17 +830,15 @@ QQuickTumblerAttached::QQuickTumblerAttached(QQuickItem *delegateItem) : QQuickTumblerAttached::~QQuickTumblerAttached() { Q_D(QQuickTumblerAttached); - if (!d->tumbler || !d->tumbler->contentItem()) + if (!d->tumbler) return; - QQuickItem *rootContentItem = d->tumbler->contentItem(); - const ContentItemType contentType = contentItemType(rootContentItem); - QQuickItem *actualItem = actualContentItem(rootContentItem, contentType); - if (!actualItem) + QQuickTumblerPrivate *tumblerPrivate = QQuickTumblerPrivate::get(d->tumbler); + if (!tumblerPrivate->viewContentItem) return; - QQuickItemPrivate *p = QQuickItemPrivate::get(actualItem); - p->removeItemChangeListener(d, QQuickItemPrivate::Geometry | QQuickItemPrivate::Children); + QQuickItemPrivate *viewContentItemPrivate = QQuickItemPrivate::get(tumblerPrivate->viewContentItem); + viewContentItemPrivate->removeItemChangeListener(d, QQuickItemPrivate::Geometry | QQuickItemPrivate::Children); } /*! |