| /**************************************************************************** |
| ** |
| ** Copyright (C) 2017 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 <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 qtquickcontrols2-input |
| \brief Spinnable wheel of items that can be selected. |
| |
| \image qtquickcontrols2-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 qtquickcontrols2-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(); |
| } |
| |
| 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() |
| { |
| 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::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); |
| 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; |
| } |
| |
| // 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", 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 << "- exlicit?" << 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); |
| } |
| |
| QPalette QQuickTumbler::defaultPalette() const |
| { |
| return QQuickTheme::palette(QQuickTheme::Tumbler); |
| } |
| |
| void QQuickTumblerAttachedPrivate::init(QQuickItem *delegateItem) |
| { |
| if (!delegateItem->parentItem()) { |
| qWarning() << "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()) { |
| qWarning() << "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->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" |