diff options
Diffstat (limited to 'src/quicktemplates/qquickscrollindicator.cpp')
-rw-r--r-- | src/quicktemplates/qquickscrollindicator.cpp | 648 |
1 files changed, 648 insertions, 0 deletions
diff --git a/src/quicktemplates/qquickscrollindicator.cpp b/src/quicktemplates/qquickscrollindicator.cpp new file mode 100644 index 0000000000..9928503e86 --- /dev/null +++ b/src/quicktemplates/qquickscrollindicator.cpp @@ -0,0 +1,648 @@ +// 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 "qquickscrollindicator_p.h" +#include "qquickcontrol_p_p.h" + +#include <QtQml/qqmlinfo.h> +#include <QtQuick/private/qquickflickable_p.h> +#include <QtQuick/private/qquickitemchangelistener_p.h> + +QT_BEGIN_NAMESPACE + +/*! + \qmltype ScrollIndicator + \inherits Control +//! \instantiates QQuickScrollIndicator + \inqmlmodule QtQuick.Controls + \since 5.7 + \ingroup qtquickcontrols-indicators + \brief Vertical or horizontal non-interactive scroll indicator. + + \image qtquickcontrols-scrollindicator.gif + + ScrollIndicator is a non-interactive indicator that indicates the current scroll + position. A scroll indicator can be either \l vertical or \l horizontal, and can + be attached to any \l Flickable, such as \l ListView and \l GridView. + + \code + Flickable { + // ... + ScrollIndicator.vertical: ScrollIndicator { } + } + \endcode + + \section1 Attaching ScrollIndicator to a Flickable + + \note When ScrollIndicator is attached \l {ScrollIndicator::vertical}{vertically} + or \l {ScrollIndicator::horizontal}{horizontally} to a Flickable, its geometry and + the following properties are automatically set and updated as appropriate: + + \list + \li \l orientation + \li \l position + \li \l size + \li \l active + \endlist + + An attached ScrollIndicator re-parents itself to the target Flickable. A vertically + attached ScrollIndicator resizes itself to the height of the Flickable, and positions + itself to either side of it based on the \l {Control::mirrored}{layout direction}. + A horizontally attached ScrollIndicator resizes itself to the width of the Flickable, + and positions itself to the bottom. The automatic geometry management can be disabled + by specifying another parent for the attached ScrollIndicator. This can be useful, for + example, if the ScrollIndicator should be placed outside a clipping Flickable. This is + demonstrated by the following example: + + \code + Flickable { + id: flickable + clip: true + // ... + ScrollIndicator.vertical: ScrollIndicator { + parent: flickable.parent + anchors.top: flickable.top + anchors.left: flickable.right + anchors.bottom: flickable.bottom + } + } + \endcode + + \section1 Binding the Active State of Horizontal and Vertical Scroll Indicators + + Horizontal and vertical scroll indicators do not share the \l active state with + each other by default. In order to keep both indicators visible whilst scrolling + to either direction, establish a two-way binding between the active states as + presented by the following example: + + \snippet qtquickcontrols-scrollindicator-active.qml 1 + + \section1 Non-attached Scroll Indicators + + It is possible to create an instance of ScrollIndicator without using the + attached property API. This is useful when the behavior of the attached + scoll indicator is not sufficient or a \l Flickable is not in use. In the + following example, horizontal and vertical scroll indicators are used to + indicate how far the user has scrolled over the text (using \l MouseArea + instead of \l Flickable): + + \snippet qtquickcontrols-scrollindicator-non-attached.qml 1 + + \image qtquickcontrols-scrollindicator-non-attached.png + + \sa ScrollBar, {Customizing ScrollIndicator}, {Indicator Controls} +*/ + +static const QQuickItemPrivate::ChangeTypes QsiChangeTypes = QQuickItemPrivate::Geometry | QQuickItemPrivate::Destroyed; +static const QQuickItemPrivate::ChangeTypes QsiHorizontalChangeTypes = QsiChangeTypes | QQuickItemPrivate::ImplicitHeight; +static const QQuickItemPrivate::ChangeTypes QsiVerticalChangeTypes = QsiChangeTypes | QQuickItemPrivate::ImplicitWidth; + +class QQuickScrollIndicatorPrivate : public QQuickControlPrivate +{ + Q_DECLARE_PUBLIC(QQuickScrollIndicator) + +public: + struct VisualArea + { + VisualArea(qreal pos, qreal sz) + : position(pos), size(sz) { } + qreal position = 0; + qreal size = 0; + }; + VisualArea visualArea() const; + void visualAreaChange(const VisualArea &newVisualArea, const VisualArea &oldVisualArea); + + void resizeContent() override; + + qreal size = 0; + qreal minimumSize = 0; + qreal position = 0; + bool active = false; + Qt::Orientation orientation = Qt::Vertical; +}; + +QQuickScrollIndicatorPrivate::VisualArea QQuickScrollIndicatorPrivate::visualArea() const +{ + qreal visualPos = position; + if (minimumSize > size) + visualPos = position / (1.0 - size) * (1.0 - minimumSize); + + qreal maximumSize = qMax<qreal>(0.0, 1.0 - visualPos); + qreal visualSize = qMax<qreal>(minimumSize, + qMin<qreal>(qMax(size, minimumSize) + qMin<qreal>(0, visualPos), + maximumSize)); + + visualPos = qMax<qreal>(0,qMin<qreal>(visualPos,qMax<qreal>(0, 1.0 - visualSize))); + + return VisualArea(visualPos, visualSize); +} + +void QQuickScrollIndicatorPrivate::visualAreaChange(const VisualArea &newVisualArea, const VisualArea &oldVisualArea) +{ + Q_Q(QQuickScrollIndicator); + if (!qFuzzyCompare(newVisualArea.size, oldVisualArea.size)) + emit q->visualSizeChanged(); + if (!qFuzzyCompare(newVisualArea.position, oldVisualArea.position)) + emit q->visualPositionChanged(); +} + +void QQuickScrollIndicatorPrivate::resizeContent() +{ + Q_Q(QQuickScrollIndicator); + if (!contentItem) + return; + + // - negative overshoot (pos < 0): clamp the pos to 0, and deduct the overshoot from the size + // - positive overshoot (pos + size > 1): clamp the size to 1-pos + const VisualArea visual = visualArea(); + + if (orientation == Qt::Horizontal) { + contentItem->setPosition(QPointF(q->leftPadding() + visual.position * q->availableWidth(), q->topPadding())); + contentItem->setSize(QSizeF(q->availableWidth() * visual.size, q->availableHeight())); + } else { + contentItem->setPosition(QPointF(q->leftPadding(), q->topPadding() + visual.position * q->availableHeight())); + contentItem->setSize(QSizeF(q->availableWidth(), q->availableHeight() * visual.size)); + } +} + +QQuickScrollIndicator::QQuickScrollIndicator(QQuickItem *parent) + : QQuickControl(*(new QQuickScrollIndicatorPrivate), parent) +{ + Q_D(QQuickScrollIndicator); + d->setSizePolicy(QLayoutPolicy::Preferred, QLayoutPolicy::Fixed); +} + +QQuickScrollIndicatorAttached *QQuickScrollIndicator::qmlAttachedProperties(QObject *object) +{ + return new QQuickScrollIndicatorAttached(object); +} + +/*! + \qmlproperty real QtQuick.Controls::ScrollIndicator::size + + This property holds the size of the indicator, scaled to \c {0.0 - 1.0}. + + \sa {Flickable::visibleArea.heightRatio}{Flickable::visibleArea} + + This property is automatically set when the scroll indicator is + \l {Attaching ScrollIndicator to a Flickable}{attached to a flickable}. + + \sa minimumSize, visualSize +*/ +qreal QQuickScrollIndicator::size() const +{ + Q_D(const QQuickScrollIndicator); + return d->size; +} + +void QQuickScrollIndicator::setSize(qreal size) +{ + Q_D(QQuickScrollIndicator); + if (qFuzzyCompare(d->size, size)) + return; + + auto oldVisualArea = d->visualArea(); + d->size = size; + if (d->size + d->position > 1.0) { + setPosition(1.0 - d->size); + oldVisualArea = d->visualArea(); + } + if (isComponentComplete()) + d->resizeContent(); + emit sizeChanged(); + d->visualAreaChange(d->visualArea(), oldVisualArea); +} + +/*! + \qmlproperty real QtQuick.Controls::ScrollIndicator::position + + This property holds the position of the indicator, scaled to \c {0.0 - 1.0}. + + This property is automatically set when the scroll indicator is + \l {Attaching ScrollIndicator to a Flickable}{attached to a flickable}. + + \sa {Flickable::visibleArea.yPosition}{Flickable::visibleArea}, visualPosition +*/ +qreal QQuickScrollIndicator::position() const +{ + Q_D(const QQuickScrollIndicator); + return d->position; +} + +void QQuickScrollIndicator::setPosition(qreal position) +{ + Q_D(QQuickScrollIndicator); + if (qFuzzyCompare(d->position, position)) + return; + + auto oldVisualArea = d->visualArea(); + d->position = position; + if (isComponentComplete()) + d->resizeContent(); + emit positionChanged(); + d->visualAreaChange(d->visualArea(), oldVisualArea); +} + +/*! + \qmlproperty bool QtQuick.Controls::ScrollIndicator::active + + This property holds whether the indicator is active, that is, when the + attached Flickable is \l {Flickable::moving}{moving}. + + It is possible to keep \l {Binding the Active State of Horizontal and Vertical Scroll Indicators} + {both horizontal and vertical indicators visible} while scrolling in either direction. + + This property is automatically set when the scroll indicator is + \l {Attaching ScrollIndicator to a Flickable}{attached to a flickable}. +*/ +bool QQuickScrollIndicator::isActive() const +{ + Q_D(const QQuickScrollIndicator); + return d->active; +} + +void QQuickScrollIndicator::setActive(bool active) +{ + Q_D(QQuickScrollIndicator); + if (d->active == active) + return; + + d->active = active; + emit activeChanged(); +} + +/*! + \qmlproperty enumeration QtQuick.Controls::ScrollIndicator::orientation + + This property holds the orientation of the indicator. + + Possible values: + \value Qt.Horizontal Horizontal + \value Qt.Vertical Vertical (default) + + This property is automatically set when the scroll indicator is + \l {Attaching ScrollIndicator to a Flickable}{attached to a flickable}. + + \sa horizontal, vertical +*/ +Qt::Orientation QQuickScrollIndicator::orientation() const +{ + Q_D(const QQuickScrollIndicator); + return d->orientation; +} + +void QQuickScrollIndicator::setOrientation(Qt::Orientation orientation) +{ + Q_D(QQuickScrollIndicator); + if (d->orientation == orientation) + return; + + if (orientation == Qt::Horizontal) + d->setSizePolicy(QLayoutPolicy::Preferred, QLayoutPolicy::Fixed); + else + d->setSizePolicy(QLayoutPolicy::Fixed, QLayoutPolicy::Preferred); + + d->orientation = orientation; + if (isComponentComplete()) + d->resizeContent(); + emit orientationChanged(); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlproperty bool QtQuick.Controls::ScrollIndicator::horizontal + \readonly + + This property holds whether the scroll indicator is horizontal. + + \sa orientation +*/ +bool QQuickScrollIndicator::isHorizontal() const +{ + Q_D(const QQuickScrollIndicator); + return d->orientation == Qt::Horizontal; +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlproperty bool QtQuick.Controls::ScrollIndicator::vertical + \readonly + + This property holds whether the scroll indicator is vertical. + + \sa orientation +*/ +bool QQuickScrollIndicator::isVertical() const +{ + Q_D(const QQuickScrollIndicator); + return d->orientation == Qt::Vertical; +} + +/*! + \since QtQuick.Controls 2.4 (Qt 5.11) + \qmlproperty real QtQuick.Controls::ScrollIndicator::minimumSize + + This property holds the minimum size of the indicator, scaled to \c {0.0 - 1.0}. + + \sa size, visualSize, visualPosition +*/ +qreal QQuickScrollIndicator::minimumSize() const +{ + Q_D(const QQuickScrollIndicator); + return d->minimumSize; +} + +void QQuickScrollIndicator::setMinimumSize(qreal minimumSize) +{ + Q_D(QQuickScrollIndicator); + if (qFuzzyCompare(d->minimumSize, minimumSize)) + return; + + auto oldVisualArea = d->visualArea(); + d->minimumSize = minimumSize; + if (isComponentComplete()) + d->resizeContent(); + emit minimumSizeChanged(); + d->visualAreaChange(d->visualArea(), oldVisualArea); +} + +/*! + \since QtQuick.Controls 2.4 (Qt 5.11) + \qmlproperty real QtQuick.Controls::ScrollIndicator::visualSize + + This property holds the effective visual size of the indicator, + which may be limited by the \l {minimumSize}{minimum size}. + + \sa size, minimumSize +*/ +qreal QQuickScrollIndicator::visualSize() const +{ + Q_D(const QQuickScrollIndicator); + return d->visualArea().size; +} + +/*! + \since QtQuick.Controls 2.4 (Qt 5.11) + \qmlproperty real QtQuick.Controls::ScrollIndicator::visualPosition + + This property holds the effective visual position of the indicator, + which may be limited by the \l {minimumSize}{minimum size}. + + \sa position, minimumSize +*/ +qreal QQuickScrollIndicator::visualPosition() const +{ + Q_D(const QQuickScrollIndicator); + return d->visualArea().position; +} + +class QQuickScrollIndicatorAttachedPrivate : public QObjectPrivate, public QQuickItemChangeListener +{ +public: + void activateHorizontal(); + void activateVertical(); + + void layoutHorizontal(bool move = true); + void layoutVertical(bool move = true); + + void itemGeometryChanged(QQuickItem *item, QQuickGeometryChange change, const QRectF &diff) override; + void itemImplicitWidthChanged(QQuickItem *item) override; + void itemImplicitHeightChanged(QQuickItem *item) override; + void itemDestroyed(QQuickItem *item) override; + + QQuickFlickable *flickable = nullptr; + QQuickScrollIndicator *horizontal = nullptr; + QQuickScrollIndicator *vertical = nullptr; +}; + +void QQuickScrollIndicatorAttachedPrivate::activateHorizontal() +{ + horizontal->setActive(flickable->isMovingHorizontally()); +} + +void QQuickScrollIndicatorAttachedPrivate::activateVertical() +{ + vertical->setActive(flickable->isMovingVertically()); +} + +void QQuickScrollIndicatorAttachedPrivate::layoutHorizontal(bool move) +{ + Q_ASSERT(horizontal && flickable); + if (horizontal->parentItem() != flickable) + return; + horizontal->setWidth(flickable->width()); + if (move) + horizontal->setY(flickable->height() - horizontal->height()); +} + +void QQuickScrollIndicatorAttachedPrivate::layoutVertical(bool move) +{ + Q_ASSERT(vertical && flickable); + if (vertical->parentItem() != flickable) + return; + vertical->setHeight(flickable->height()); + if (move && !QQuickItemPrivate::get(vertical)->isMirrored()) + vertical->setX(flickable->width() - vertical->width()); +} + +void QQuickScrollIndicatorAttachedPrivate::itemGeometryChanged(QQuickItem *item, QQuickGeometryChange change, const QRectF &diff) +{ + Q_UNUSED(item); + Q_UNUSED(change); + if (horizontal && horizontal->height() > 0) { +#ifdef QT_QUICK_NEW_GEOMETRY_CHANGED_HANDLING // TODO: correct/rename diff to oldGeometry + bool move = qFuzzyIsNull(horizontal->y()) || qFuzzyCompare(horizontal->y(), diff.height() - horizontal->height()); +#else + bool move = qFuzzyIsNull(horizontal->y()) || qFuzzyCompare(horizontal->y(), item->height() - diff.height() - horizontal->height()); +#endif + layoutHorizontal(move); + } + if (vertical && vertical->width() > 0) { +#ifdef QT_QUICK_NEW_GEOMETRY_CHANGED_HANDLING // TODO: correct/rename diff to oldGeometry + bool move = qFuzzyIsNull(vertical->x()) || qFuzzyCompare(vertical->x(), diff.width() - vertical->width()); +#else + bool move = qFuzzyIsNull(vertical->x()) || qFuzzyCompare(vertical->x(), item->width() - diff.width() - vertical->width()); +#endif + layoutVertical(move); + } +} + +void QQuickScrollIndicatorAttachedPrivate::itemImplicitWidthChanged(QQuickItem *item) +{ + if (item == vertical) + layoutVertical(true); +} + +void QQuickScrollIndicatorAttachedPrivate::itemImplicitHeightChanged(QQuickItem *item) +{ + if (item == horizontal) + layoutHorizontal(true); +} + +void QQuickScrollIndicatorAttachedPrivate::itemDestroyed(QQuickItem *item) +{ + if (item == horizontal) + horizontal = nullptr; + if (item == vertical) + vertical = nullptr; +} + +QQuickScrollIndicatorAttached::QQuickScrollIndicatorAttached(QObject *parent) + : QObject(*(new QQuickScrollIndicatorAttachedPrivate), parent) +{ + Q_D(QQuickScrollIndicatorAttached); + d->flickable = qobject_cast<QQuickFlickable *>(parent); + if (d->flickable) + QQuickItemPrivate::get(d->flickable)->updateOrAddGeometryChangeListener(d, QQuickGeometryChange::Size); + else if (parent) + qmlWarning(parent) << "ScrollIndicator must be attached to a Flickable"; +} + +QQuickScrollIndicatorAttached::~QQuickScrollIndicatorAttached() +{ + Q_D(QQuickScrollIndicatorAttached); + if (d->flickable) { + if (d->horizontal) + QQuickItemPrivate::get(d->horizontal)->removeItemChangeListener(d, QsiHorizontalChangeTypes); + if (d->vertical) + QQuickItemPrivate::get(d->vertical)->removeItemChangeListener(d, QsiVerticalChangeTypes); + // NOTE: Use removeItemChangeListener(Geometry) instead of updateOrRemoveGeometryChangeListener(Size). + // The latter doesn't remove the listener but only resets its types. Thus, it leaves behind a dangling + // pointer on destruction. + QQuickItemPrivate::get(d->flickable)->removeItemChangeListener(d, QQuickItemPrivate::Geometry); + } +} + +/*! + \qmlattachedproperty ScrollIndicator QtQuick.Controls::ScrollIndicator::horizontal + + This property attaches a horizontal scroll indicator to a \l Flickable. + + \code + Flickable { + contentWidth: 2000 + ScrollIndicator.horizontal: ScrollIndicator { } + } + \endcode + + \sa {Attaching ScrollIndicator to a Flickable} +*/ +QQuickScrollIndicator *QQuickScrollIndicatorAttached::horizontal() const +{ + Q_D(const QQuickScrollIndicatorAttached); + return d->horizontal; +} + +void QQuickScrollIndicatorAttached::setHorizontal(QQuickScrollIndicator *horizontal) +{ + Q_D(QQuickScrollIndicatorAttached); + if (d->horizontal == horizontal) + return; + + if (d->horizontal && d->flickable) { + QQuickItemPrivate::get(d->horizontal)->removeItemChangeListener(d, QsiHorizontalChangeTypes); + QObjectPrivate::disconnect(d->flickable, &QQuickFlickable::movingHorizontallyChanged, d, &QQuickScrollIndicatorAttachedPrivate::activateHorizontal); + + // TODO: export QQuickFlickableVisibleArea + QObject *area = d->flickable->property("visibleArea").value<QObject *>(); + disconnect(area, SIGNAL(widthRatioChanged(qreal)), d->horizontal, SLOT(setSize(qreal))); + disconnect(area, SIGNAL(xPositionChanged(qreal)), d->horizontal, SLOT(setPosition(qreal))); + } + + d->horizontal = horizontal; + + if (horizontal && d->flickable) { + if (!horizontal->parentItem()) + horizontal->setParentItem(d->flickable); + horizontal->setOrientation(Qt::Horizontal); + + QQuickItemPrivate::get(horizontal)->addItemChangeListener(d, QsiHorizontalChangeTypes); + QObjectPrivate::connect(d->flickable, &QQuickFlickable::movingHorizontallyChanged, d, &QQuickScrollIndicatorAttachedPrivate::activateHorizontal); + + // TODO: export QQuickFlickableVisibleArea + QObject *area = d->flickable->property("visibleArea").value<QObject *>(); + connect(area, SIGNAL(widthRatioChanged(qreal)), horizontal, SLOT(setSize(qreal))); + connect(area, SIGNAL(xPositionChanged(qreal)), horizontal, SLOT(setPosition(qreal))); + + d->layoutHorizontal(); + horizontal->setSize(area->property("widthRatio").toReal()); + horizontal->setPosition(area->property("xPosition").toReal()); + } + emit horizontalChanged(); +} + +/*! + \qmlattachedproperty ScrollIndicator QtQuick.Controls::ScrollIndicator::vertical + + This property attaches a vertical scroll indicator to a \l Flickable. + + \code + Flickable { + contentHeight: 2000 + ScrollIndicator.vertical: ScrollIndicator { } + } + \endcode + + \sa {Attaching ScrollIndicator to a Flickable} +*/ +QQuickScrollIndicator *QQuickScrollIndicatorAttached::vertical() const +{ + Q_D(const QQuickScrollIndicatorAttached); + return d->vertical; +} + +void QQuickScrollIndicatorAttached::setVertical(QQuickScrollIndicator *vertical) +{ + Q_D(QQuickScrollIndicatorAttached); + if (d->vertical == vertical) + return; + + if (d->vertical && d->flickable) { + QQuickItemPrivate::get(d->vertical)->removeItemChangeListener(d, QsiVerticalChangeTypes); + QObjectPrivate::disconnect(d->flickable, &QQuickFlickable::movingVerticallyChanged, d, &QQuickScrollIndicatorAttachedPrivate::activateVertical); + + // TODO: export QQuickFlickableVisibleArea + QObject *area = d->flickable->property("visibleArea").value<QObject *>(); + disconnect(area, SIGNAL(heightRatioChanged(qreal)), d->vertical, SLOT(setSize(qreal))); + disconnect(area, SIGNAL(yPositionChanged(qreal)), d->vertical, SLOT(setPosition(qreal))); + } + + d->vertical = vertical; + + if (vertical && d->flickable) { + if (!vertical->parentItem()) + vertical->setParentItem(d->flickable); + vertical->setOrientation(Qt::Vertical); + + QQuickItemPrivate::get(vertical)->addItemChangeListener(d, QsiVerticalChangeTypes); + QObjectPrivate::connect(d->flickable, &QQuickFlickable::movingVerticallyChanged, d, &QQuickScrollIndicatorAttachedPrivate::activateVertical); + + // TODO: export QQuickFlickableVisibleArea + QObject *area = d->flickable->property("visibleArea").value<QObject *>(); + connect(area, SIGNAL(heightRatioChanged(qreal)), vertical, SLOT(setSize(qreal))); + connect(area, SIGNAL(yPositionChanged(qreal)), vertical, SLOT(setPosition(qreal))); + + d->layoutVertical(); + vertical->setSize(area->property("heightRatio").toReal()); + vertical->setPosition(area->property("yPosition").toReal()); + } + emit verticalChanged(); +} + +#if QT_CONFIG(quicktemplates2_multitouch) +void QQuickScrollIndicator::touchEvent(QTouchEvent *event) +{ + event->ignore(); // QTBUG-61785 +} +#endif + +#if QT_CONFIG(accessibility) +QAccessible::Role QQuickScrollIndicator::accessibleRole() const +{ + return QAccessible::Indicator; +} +#endif + +QT_END_NAMESPACE + +#include "moc_qquickscrollindicator_p.cpp" |