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.cpp594
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);
}
/*!