diff options
Diffstat (limited to 'src/quicktemplates/qquicktumbler.cpp')
-rw-r--r-- | src/quicktemplates/qquicktumbler.cpp | 1017 |
1 files changed, 1017 insertions, 0 deletions
diff --git a/src/quicktemplates/qquicktumbler.cpp b/src/quicktemplates/qquicktumbler.cpp new file mode 100644 index 0000000000..71766cc698 --- /dev/null +++ b/src/quicktemplates/qquicktumbler.cpp @@ -0,0 +1,1017 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qquicktumbler_p.h" + +#include <QtCore/qloggingcategory.h> +#include <QtGui/qpa/qplatformtheme.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 + +Q_LOGGING_CATEGORY(lcTumbler, "qt.quick.controls.tumbler") + +/*! + \qmltype Tumbler + \inherits Control +//! \instantiates QQuickTumbler + \inqmlmodule QtQuick.Controls + \since 5.7 + \ingroup qtquickcontrols-input + \brief Spinnable wheel of items that can be selected. + + \image qtquickcontrols-tumbler-wrap.gif + + \code + Tumbler { + model: 5 + // ... + } + \endcode + + 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 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. To + position the view at a certain index, use \l positionViewAtIndex(). + + 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 qtquickcontrols-tumbler-timePicker.qml tumbler + + \sa {Customizing Tumbler}, {Input Controls} +*/ + +namespace { + static inline qreal delegateHeight(const QQuickTumbler *tumbler) + { + return tumbler->availableHeight() / tumbler->visibleItemCount(); + } +} + +/* + 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) { + resetViewData(); + return nullptr; + } + + if (contentItem->inherits("QQuickPathView")) { + view = contentItem; + viewContentItem = contentItem; + viewContentItemType = PathViewContentItem; + viewOffset = 0; + + return contentItem; + } else if (contentItem->inherits("QQuickListView")) { + view = contentItem; + viewContentItem = qobject_cast<QQuickFlickable*>(contentItem)->contentItem(); + viewContentItemType = ListViewContentItem; + viewContentY = 0; + + return contentItem; + } else { + const auto childItems = contentItem->childItems(); + for (QQuickItem *childItem : childItems) { + QQuickItem *item = determineViewType(childItem); + if (item) + return item; + } + } + + resetViewData(); + viewContentItemType = UnsupportedContentItemType; + return nullptr; +} + +void QQuickTumblerPrivate::resetViewData() +{ + view = nullptr; + viewContentItem = nullptr; + if (viewContentItemType == PathViewContentItem) + viewOffset = 0; + else if (viewContentItemType == ListViewContentItem) + viewContentY = 0; + viewContentItemType = NoContentItem; +} + +QList<QQuickItem *> QQuickTumblerPrivate::viewContentItemChildItems() const +{ + if (!viewContentItem) + return QList<QQuickItem *>(); + + return viewContentItem->childItems(); +} + +QQuickTumblerPrivate *QQuickTumblerPrivate::get(QQuickTumbler *tumbler) +{ + return tumbler->d_func(); +} + +void QQuickTumblerPrivate::_q_updateItemHeights() +{ + if (ignoreSignals) + return; + + // 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 = viewContentItemChildItems(); + for (QQuickItem *childItem : items) + childItem->setHeight(itemHeight); +} + +void QQuickTumblerPrivate::_q_updateItemWidths() +{ + if (ignoreSignals) + return; + + Q_Q(const QQuickTumbler); + const qreal availableWidth = q->availableWidth(); + const auto items = viewContentItemChildItems(); + for (QQuickItem *childItem : items) + childItem->setWidth(availableWidth); +} + +void QQuickTumblerPrivate::_q_onViewCurrentIndexChanged() +{ + Q_Q(QQuickTumbler); + if (!view || ignoreCurrentIndexChanges || currentIndexSetDuringModelChange) { + // If the user set currentIndex in the onModelChanged handler, + // we have to respect that currentIndex by ignoring changes in the view + // until the model has finished being set. + qCDebug(lcTumbler).nospace() << "view currentIndex changed to " + << (view ? view->property("currentIndex").toString() : QStringLiteral("unknown index (no view)")) + << ", but we're ignoring it because one or more of the following conditions are true:" + << "\n- !view: " << !view + << "\n- ignoreCurrentIndexChanges: " << ignoreCurrentIndexChanges + << "\n- currentIndexSetDuringModelChange: " << currentIndexSetDuringModelChange; + return; + } + + const int oldCurrentIndex = currentIndex; + currentIndex = view->property("currentIndex").toInt(); + + qCDebug(lcTumbler).nospace() << "view currentIndex changed to " + << (view ? view->property("currentIndex").toString() : QStringLiteral("unknown index (no view)")) + << ", our old currentIndex was " << oldCurrentIndex; + + if (oldCurrentIndex != currentIndex) + emit q->currentIndexChanged(); +} + +void QQuickTumblerPrivate::_q_onViewCountChanged() +{ + Q_Q(QQuickTumbler); + qCDebug(lcTumbler) << "view count changed - ignoring signals?" << ignoreSignals; + if (ignoreSignals) + return; + + 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. + setCurrentIndex(pendingCurrentIndex); + // If we could successfully set the currentIndex, consider it done. + // Otherwise, we'll try again later in updatePolish(). + if (currentIndex == pendingCurrentIndex) + setPendingCurrentIndex(-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. + setCurrentIndex(0); + } + } else { + setCurrentIndex(-1); + } +} + +void QQuickTumblerPrivate::_q_onViewOffsetChanged() +{ + viewOffset = view->property("offset").toReal(); + calculateDisplacements(); +} + +void QQuickTumblerPrivate::_q_onViewContentYChanged() +{ + viewContentY = view->property("contentY").toReal(); + calculateDisplacements(); +} + +void QQuickTumblerPrivate::calculateDisplacements() +{ + const auto items = viewContentItemChildItems(); + for (QQuickItem *childItem : items) { + QQuickTumblerAttached *attached = qobject_cast<QQuickTumblerAttached *>(qmlAttachedPropertiesObject<QQuickTumbler>(childItem, false)); + if (attached) + QQuickTumblerAttachedPrivate::get(attached)->calculateDisplacement(); + } +} + +void QQuickTumblerPrivate::itemChildAdded(QQuickItem *, QQuickItem *) +{ + _q_updateItemWidths(); + _q_updateItemHeights(); +} + +void QQuickTumblerPrivate::itemChildRemoved(QQuickItem *, QQuickItem *) +{ + _q_updateItemWidths(); + _q_updateItemHeights(); +} + +void QQuickTumblerPrivate::itemGeometryChanged(QQuickItem *item, QQuickGeometryChange change, const QRectF &diff) +{ + QQuickControlPrivate::itemGeometryChanged(item, change, diff); + if (change.sizeChange()) + calculateDisplacements(); +} + +QPalette QQuickTumblerPrivate::defaultPalette() const +{ + return QQuickTheme::palette(QQuickTheme::Tumbler); +} + +QQuickTumbler::QQuickTumbler(QQuickItem *parent) + : QQuickControl(*(new QQuickTumblerPrivate), parent) +{ + Q_D(QQuickTumbler); + d->setSizePolicy(QLayoutPolicy::Preferred, QLayoutPolicy::Preferred); + + 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() +{ + Q_D(QQuickTumbler); + // Ensure that the item change listener is removed. + d->disconnectFromView(); +} + +/*! + \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->beginSetModel(); + + d->model = model; + emit modelChanged(); + + d->endSetModel(); + + d->currentIndexSetDuringModelChange = false; + + // 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) + d->setCurrentIndex(-1); +} + +/*! + \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->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. + + \sa currentItem, positionViewAtIndex() +*/ +int QQuickTumbler::currentIndex() const +{ + Q_D(const QQuickTumbler); + return d->currentIndex; +} + +void QQuickTumbler::setCurrentIndex(int currentIndex) +{ + Q_D(QQuickTumbler); + if (d->modelBeingSet) + d->currentIndexSetDuringModelChange = true; + d->setCurrentIndex(currentIndex, QQuickTumblerPrivate::UserChange); +} + +/*! + \qmlproperty Item QtQuick.Controls::Tumbler::currentItem + \readonly + + This property holds the item at the current index. + + \sa currentIndex, positionViewAtIndex() +*/ +QQuickItem *QQuickTumbler::currentItem() const +{ + Q_D(const QQuickTumbler); + return d->view ? d->view->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) +{ + return new QQuickTumblerAttached(object); +} + +/*! + \qmlproperty bool QtQuick.Controls::Tumbler::wrap + \since QtQuick.Controls 2.1 (Qt 5.8) + + 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 +{ + Q_D(const QQuickTumbler); + return d->wrap; +} + +void QQuickTumbler::setWrap(bool wrap) +{ + Q_D(QQuickTumbler); + d->setWrap(wrap, true); +} + +void QQuickTumbler::resetWrap() +{ + Q_D(QQuickTumbler); + d->explicitWrap = false; + d->setWrapBasedOnCount(); +} + +/*! + \qmlproperty bool QtQuick.Controls::Tumbler::moving + \since QtQuick.Controls 2.2 (Qt 5.9) + + This property describes whether the tumbler is currently moving, due to + the user either dragging or flicking it. +*/ +bool QQuickTumbler::isMoving() const +{ + Q_D(const QQuickTumbler); + return d->view && d->view->property("moving").toBool(); +} + +/*! + \qmlmethod void QtQuick.Controls::Tumbler::positionViewAtIndex(int index, PositionMode mode) + \since QtQuick.Controls 2.5 (Qt 5.12) + + Positions the view so that the \a index is at the position specified by \a mode. + + For example: + + \code + positionViewAtIndex(10, Tumbler.Center) + \endcode + + If \l wrap is true (the default), the modes available to \l {PathView}'s + \l {PathView::}{positionViewAtIndex()} function + are available, otherwise the modes available to \l {ListView}'s + \l {ListView::}{positionViewAtIndex()} function + are available. + + \note There is a known limitation that using \c Tumbler.Beginning when \l + wrap is \c true will result in the wrong item being positioned at the top + of view. As a workaround, pass \c {index - 1}. + + \sa currentIndex +*/ +void QQuickTumbler::positionViewAtIndex(int index, QQuickTumbler::PositionMode mode) +{ + Q_D(QQuickTumbler); + if (!d->view) { + d->warnAboutIncorrectContentItem(); + return; + } + + QMetaObject::invokeMethod(d->view, "positionViewAtIndex", Q_ARG(int, index), Q_ARG(int, mode)); +} + +void QQuickTumbler::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + Q_D(QQuickTumbler); + + QQuickControl::geometryChange(newGeometry, oldGeometry); + + d->_q_updateItemHeights(); + + if (newGeometry.width() != oldGeometry.width()) + d->_q_updateItemWidths(); +} + +void QQuickTumbler::componentComplete() +{ + Q_D(QQuickTumbler); + qCDebug(lcTumbler) << "componentComplete()"; + QQuickControl::componentComplete(); + + if (!d->view) { + // Force the view to be created. + qCDebug(lcTumbler) << "emitting wrapChanged() to force view to be created"; + emit wrapChanged(); + // Determine the type of view for attached properties, etc. + d->setupViewData(d->contentItem); + } + + // If there was no contentItem or it was of an unsupported type, + // we don't have anything else to do. + if (!d->view) + return; + + // Update item heights after we've populated the model, + // otherwise ignoreSignals will cause these functions to return early. + d->_q_updateItemHeights(); + d->_q_updateItemWidths(); + d->_q_onViewCountChanged(); + + qCDebug(lcTumbler) << "componentComplete() is done"; +} + +void QQuickTumbler::contentItemChange(QQuickItem *newItem, QQuickItem *oldItem) +{ + Q_D(QQuickTumbler); + + QQuickControl::contentItemChange(newItem, oldItem); + + if (oldItem) + d->disconnectFromView(); + + if (newItem) { + // 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); + + d->_q_updateItemHeights(); + d->_q_updateItemWidths(); + } + } +} + +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; + } + + 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())); + QObject::disconnect(view, SIGNAL(movingChanged()), q, SIGNAL(movingChanged())); + + if (viewContentItemType == PathViewContentItem) + QObject::disconnect(view, SIGNAL(offsetChanged()), q, SLOT(_q_onViewOffsetChanged())); + else + QObject::disconnect(view, SIGNAL(contentYChanged()), q, SLOT(_q_onViewContentYChanged())); + + QQuickItemPrivate *oldViewContentItemPrivate = QQuickItemPrivate::get(viewContentItem); + oldViewContentItemPrivate->removeItemChangeListener(this, QQuickItemPrivate::Children | QQuickItemPrivate::Geometry); + + resetViewData(); +} + +void QQuickTumblerPrivate::setupViewData(QQuickItem *newControlContentItem) +{ + // Don't do anything if we've already set up. + if (view) + return; + + determineViewType(newControlContentItem); + + if (viewContentItemType == QQuickTumblerPrivate::NoContentItem) + return; + + if (viewContentItemType == QQuickTumblerPrivate::UnsupportedContentItemType) { + warnAboutIncorrectContentItem(); + 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())); + QObject::connect(view, SIGNAL(movingChanged()), q, SIGNAL(movingChanged())); + + if (viewContentItemType == PathViewContentItem) { + QObject::connect(view, SIGNAL(offsetChanged()), q, SLOT(_q_onViewOffsetChanged())); + _q_onViewOffsetChanged(); + } else { + QObject::connect(view, SIGNAL(contentYChanged()), q, SLOT(_q_onViewContentYChanged())); + _q_onViewContentYChanged(); + } + + QQuickItemPrivate *viewContentItemPrivate = QQuickItemPrivate::get(viewContentItem); + viewContentItemPrivate->addItemChangeListener(this, QQuickItemPrivate::Children | QQuickItemPrivate::Geometry); + + // Sync the view's currentIndex with ours. + syncCurrentIndex(); + + calculateDisplacements(); +} + +void QQuickTumblerPrivate::warnAboutIncorrectContentItem() +{ + Q_Q(QQuickTumbler); + qmlWarning(q) << "Tumbler: contentItem must contain either a PathView or a ListView"; +} + +void QQuickTumblerPrivate::syncCurrentIndex() +{ + const int actualViewIndex = view->property("currentIndex").toInt(); + Q_Q(QQuickTumbler); + + const bool isPendingCurrentIndex = pendingCurrentIndex != -1; + const int indexToSet = isPendingCurrentIndex ? pendingCurrentIndex : currentIndex; + + // Nothing to do. + if (actualViewIndex == indexToSet) { + setPendingCurrentIndex(-1); + return; + } + + // actualViewIndex might be 0 or -1 for PathView and ListView respectively, + // but we always use -1 for that. + if (q->count() == 0 && actualViewIndex <= 0) + return; + + ignoreCurrentIndexChanges = true; + view->setProperty("currentIndex", QVariant(indexToSet)); + ignoreCurrentIndexChanges = false; + + if (view->property("currentIndex").toInt() == indexToSet) + setPendingCurrentIndex(-1); + else if (isPendingCurrentIndex) + q->polish(); +} + +void QQuickTumblerPrivate::setPendingCurrentIndex(int index) +{ + qCDebug(lcTumbler) << "setting pendingCurrentIndex to" << index; + pendingCurrentIndex = index; +} + +QString QQuickTumblerPrivate::propertyChangeReasonToString( + QQuickTumblerPrivate::PropertyChangeReason changeReason) +{ + return changeReason == UserChange ? QStringLiteral("UserChange") : QStringLiteral("InternalChange"); +} + +void QQuickTumblerPrivate::setCurrentIndex(int newCurrentIndex, + QQuickTumblerPrivate::PropertyChangeReason changeReason) +{ + Q_Q(QQuickTumbler); + qCDebug(lcTumbler).nospace() << "setting currentIndex to " << newCurrentIndex + << ", old currentIndex was " << currentIndex + << ", changeReason is " << propertyChangeReasonToString(changeReason); + if (newCurrentIndex == currentIndex || newCurrentIndex < -1) + return; + + if (!q->isComponentComplete()) { + // Views can't set currentIndex until they're ready. + qCDebug(lcTumbler) << "we're not complete; setting pendingCurrentIndex instead"; + setPendingCurrentIndex(newCurrentIndex); + return; + } + + if (modelBeingSet && changeReason == UserChange) { + // If modelBeingSet is true and the user set the currentIndex, + // the model is in the process of being set and the user has set + // the currentIndex in onModelChanged. We have to queue the currentIndex + // change until we're ready. + qCDebug(lcTumbler) << "a model is being set; setting pendingCurrentIndex instead"; + setPendingCurrentIndex(newCurrentIndex); + 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 ((count > 0 && newCurrentIndex == -1) || (newCurrentIndex >= 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 (view) { + // Only actually set our currentIndex if the view was able to set theirs. + bool couldSet = false; + if (count == 0 && newCurrentIndex == -1) { + // PathView insists on using 0 as the currentIndex when there are no items. + couldSet = true; + } else { + ignoreCurrentIndexChanges = true; + ignoreSignals = true; + view->setProperty("currentIndex", newCurrentIndex); + ignoreSignals = false; + ignoreCurrentIndexChanges = false; + + couldSet = view->property("currentIndex").toInt() == newCurrentIndex; + } + + if (couldSet) { + // The view's currentIndex might not have actually changed, but ours has, + // and that's what user code sees. + currentIndex = newCurrentIndex; + emit q->currentIndexChanged(); + } + + qCDebug(lcTumbler) << "view's currentIndex is now" << view->property("currentIndex").toInt() + << "and ours is" << currentIndex; + } +} + +void QQuickTumblerPrivate::setCount(int newCount) +{ + qCDebug(lcTumbler).nospace() << "setting count to " << newCount + << ", old count was " << count; + if (newCount == count) + return; + + count = newCount; + + Q_Q(QQuickTumbler); + setWrapBasedOnCount(); + + emit q->countChanged(); +} + +void QQuickTumblerPrivate::setWrapBasedOnCount() +{ + if (count == 0 || explicitWrap || modelBeingSet) + return; + + setWrap(count >= visibleItemCount, false); +} + +void QQuickTumblerPrivate::setWrap(bool shouldWrap, bool isExplicit) +{ + qCDebug(lcTumbler) << "setting wrap to" << shouldWrap << "- explicit?" << 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; + + // If isComponentComplete() is true, we require a contentItem. If it's not + // true, it might not have been created yet, so we wait until + // componentComplete() is called. + // + // When the contentItem (usually QQuickTumblerView) has been created, 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. + if (q->isComponentComplete() || contentItem) + setupViewData(contentItem); + + setCurrentIndex(oldCurrentIndex); +} + +void QQuickTumblerPrivate::beginSetModel() +{ + modelBeingSet = true; +} + +void QQuickTumblerPrivate::endSetModel() +{ + modelBeingSet = false; + setWrapBasedOnCount(); +} + +void QQuickTumbler::keyPressEvent(QKeyEvent *event) +{ + QQuickControl::keyPressEvent(event); + + Q_D(QQuickTumbler); + if (event->isAutoRepeat() || !d->view) + return; + + if (event->key() == Qt::Key_Up) { + QMetaObject::invokeMethod(d->view, "decrementCurrentIndex"); + } else if (event->key() == Qt::Key_Down) { + QMetaObject::invokeMethod(d->view, "incrementCurrentIndex"); + } +} + +void QQuickTumbler::updatePolish() +{ + Q_D(QQuickTumbler); + if (d->pendingCurrentIndex != -1) { + // Update our count, as ignoreSignals might have been true + // when _q_onViewCountChanged() was last called. + d->setCount(d->view->property("count").toInt()); + + // If the count is still 0, it's not going to happen. + if (d->count == 0) { + d->setPendingCurrentIndex(-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. + d->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. + d->setCurrentIndex(0); + } + + d->setPendingCurrentIndex(-1); + } +} + +QFont QQuickTumbler::defaultFont() const +{ + return QQuickTheme::font(QQuickTheme::Tumbler); +} + +void QQuickTumblerAttachedPrivate::init(QQuickItem *delegateItem) +{ + Q_Q(QQuickTumblerAttached); + if (!delegateItem->parentItem()) { + qmlWarning(q) << "Tumbler: attached properties must be accessed through a delegate item that has a parent"; + return; + } + + QVariant indexContextProperty = qmlContext(delegateItem)->contextProperty(QStringLiteral("index")); + if (!indexContextProperty.isValid()) { + qmlWarning(q) << "Tumbler: attempting to access attached property on item without an \"index\" property"; + return; + } + + index = indexContextProperty.toInt(); + + QQuickItem *parentItem = delegateItem; + while ((parentItem = parentItem->parentItem())) { + if ((tumbler = qobject_cast<QQuickTumbler*>(parentItem))) + break; + } +} + +void QQuickTumblerAttachedPrivate::calculateDisplacement() +{ + const qreal previousDisplacement = displacement; + displacement = 0; + + if (!tumbler) { + // Can happen if the attached properties are accessed on the wrong type of item or the tumbler was destroyed. + // We don't want to emit the change signal though, as this could cause warnings about Tumbler.tumbler being null. + return; + } + + // Can happen if there is no ListView or PathView within the contentItem. + QQuickTumblerPrivate *tumblerPrivate = QQuickTumblerPrivate::get(tumbler); + if (!tumblerPrivate->viewContentItem) { + emitIfDisplacementChanged(previousDisplacement, displacement); + return; + } + + // 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; + } + + if (tumblerPrivate->viewContentItemType == QQuickTumblerPrivate::PathViewContentItem) { + const qreal offset = tumblerPrivate->viewOffset; + + displacement = count > 1 ? count - index - offset : 0; + // Don't add 1 if count <= visibleItemCount + const int visibleItems = tumbler->visibleItemCount(); + const int halfVisibleItems = visibleItems / 2 + (visibleItems < count ? 1 : 0); + if (displacement > halfVisibleItems) + displacement -= count; + else if (displacement < -halfVisibleItems) + displacement += count; + } else { + const qreal contentY = tumblerPrivate->viewContentY; + const qreal delegateH = delegateHeight(tumbler); + const qreal preferredHighlightBegin = tumblerPrivate->view->property("preferredHighlightBegin").toReal(); + const qreal itemY = qobject_cast<QQuickItem*>(parent)->y(); + qreal currentItemY = 0; + auto currentItem = tumblerPrivate->view->property("currentItem").value<QQuickItem*>(); + if (currentItem) + currentItemY = currentItem->y(); + // Start from the y position of the current item. + const qreal topOfCurrentItemInViewport = currentItemY - contentY; + // Then, calculate the distance between it and the preferredHighlightBegin. + const qreal relativePositionToPreferredHighlightBegin = topOfCurrentItemInViewport - preferredHighlightBegin; + // Next, calculate the distance between us and the current item. + const qreal distanceFromCurrentItem = currentItemY - itemY; + const qreal displacementInPixels = distanceFromCurrentItem - relativePositionToPreferredHighlightBegin; + // Convert it from pixels to a floating point index. + displacement = displacementInPixels / delegateH; + } + + emitIfDisplacementChanged(previousDisplacement, displacement); +} + +void QQuickTumblerAttachedPrivate::emitIfDisplacementChanged(qreal oldDisplacement, qreal newDisplacement) +{ + Q_Q(QQuickTumblerAttached); + if (newDisplacement != oldDisplacement) + emit q->displacementChanged(); +} + +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) + qmlWarning(parent) << "Tumbler: attached properties of Tumbler must be accessed through a delegate item"; + + if (d->tumbler) { + // 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 (delegateItem && delegateItem->parentItem() == tumblerPrivate->viewContentItem) { + // This item belongs to the "new" view, meaning that the tumbler's contentItem + // was probably assigned declaratively. If they're not equal, calling + // calculateDisplacement() would use the old contentItem data, which is bad. + d->calculateDisplacement(); + } + } +} + +/*! + \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" |