diff options
Diffstat (limited to 'src/quicktemplates/qquickscrollbar.cpp')
-rw-r--r-- | src/quicktemplates/qquickscrollbar.cpp | 1295 |
1 files changed, 1295 insertions, 0 deletions
diff --git a/src/quicktemplates/qquickscrollbar.cpp b/src/quicktemplates/qquickscrollbar.cpp new file mode 100644 index 0000000000..75de0363e9 --- /dev/null +++ b/src/quicktemplates/qquickscrollbar.cpp @@ -0,0 +1,1295 @@ +// 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 "qquickscrollbar_p.h" +#include "qquickscrollbar_p_p.h" +#include "qquickscrollview_p.h" + +#include <QtQml/qqmlinfo.h> +#include <QtQuick/private/qquickflickable_p.h> +#if QT_CONFIG(accessibility) +#include <QtQuick/private/qquickaccessibleattached_p.h> +#endif + +QT_BEGIN_NAMESPACE + +/*! + \qmltype ScrollBar + \inherits Control +//! \instantiates QQuickScrollBar + \inqmlmodule QtQuick.Controls + \since 5.7 + \ingroup qtquickcontrols-indicators + \brief Vertical or horizontal interactive scroll bar. + + \image qtquickcontrols-scrollbar.gif + + ScrollBar is an interactive bar that can be used to scroll to a specific + position. A scroll bar can be either \l vertical or \l horizontal, and can + be attached to any \l Flickable, such as \l ListView and \l GridView. + It can also be used with \l ScrollView. + + \code + Flickable { + // ... + ScrollBar.vertical: ScrollBar { } + } + \endcode + + \section1 Attaching ScrollBar to a Flickable + + When ScrollBar is attached \l {ScrollBar::vertical}{vertically} or + \l {ScrollBar::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 {ScrollBar::} {size} + \li \l active + \endlist + + An attached ScrollBar re-parents itself to the target Flickable. A vertically + attached ScrollBar 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 ScrollBar 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 ScrollBar. This can be useful, for + example, if the ScrollBar should be placed outside a clipping Flickable. This is + demonstrated by the following example: + + \code + Flickable { + id: flickable + clip: true + // ... + ScrollBar.vertical: ScrollBar { + parent: flickable.parent + anchors.top: flickable.top + anchors.left: flickable.right + anchors.bottom: flickable.bottom + } + } + \endcode + + Notice that ScrollBar does not filter key events of the Flickable it is + attached to. The following example illustrates how to implement scrolling + with up and down keys: + + \code + Flickable { + focus: true + + Keys.onUpPressed: scrollBar.decrease() + Keys.onDownPressed: scrollBar.increase() + + ScrollBar.vertical: ScrollBar { id: scrollBar } + } + \endcode + + \section1 Binding the Active State of Horizontal and Vertical Scroll Bars + + Horizontal and vertical scroll bars do not share the \l active state with + each other by default. In order to keep both bars visible whilst scrolling + to either direction, establish a two-way binding between the active states + as presented by the following example: + + \snippet qtquickcontrols-scrollbar-active.qml 1 + + \section1 Non-attached Scroll Bars + + It is possible to create an instance of ScrollBar without using the + attached property API. This is useful when the behavior of the attached + scroll bar is not sufficient or a \l Flickable is not in use. In the + following example, horizontal and vertical scroll bars are used to + scroll over the text without using \l Flickable: + + \snippet qtquickcontrols-scrollbar-non-attached.qml 1 + + \image qtquickcontrols-scrollbar-non-attached.png + + When using a non-attached ScrollBar, the following must be done manually: + + \list + \li Layout the scroll bar (with the \l {Item::}{x} and \l {Item::}{y} or + \l {Item::}{anchors} property, for example). + \li Set the \l size and \l position properties to determine the size and position + of the scroll bar in relation to the scrolled item. + \li Set the \l active property to determine when the scroll bar will be + visible. + \endlist + + \sa ScrollIndicator, ScrollView, {Customizing ScrollBar}, {Indicator Controls} +*/ + +static const QQuickItemPrivate::ChangeTypes QsbChangeTypes = QQuickItemPrivate::Geometry | QQuickItemPrivate::Destroyed; +static const QQuickItemPrivate::ChangeTypes QsbHorizontalChangeTypes = QsbChangeTypes | QQuickItemPrivate::ImplicitHeight; +static const QQuickItemPrivate::ChangeTypes QsbVerticalChangeTypes = QsbChangeTypes | QQuickItemPrivate::ImplicitWidth; + +QQuickScrollBarPrivate::VisualArea QQuickScrollBarPrivate::visualArea() const +{ + qreal visualPos = position; + + if (minimumSize > size && size != 1.0) + 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); +} + +qreal QQuickScrollBarPrivate::logicalPosition(qreal position) const +{ + if (minimumSize > size && minimumSize != 1.0) + return position * (1.0 - size) / (1.0 - minimumSize); + return position; +} + +qreal QQuickScrollBarPrivate::snapPosition(qreal position) const +{ + const qreal effectiveStep = stepSize * (1.0 - size); + if (qFuzzyIsNull(effectiveStep)) + return position; + + return qRound(position / effectiveStep) * effectiveStep; +} + +qreal QQuickScrollBarPrivate::positionAt(const QPointF &point) const +{ + Q_Q(const QQuickScrollBar); + if (orientation == Qt::Horizontal) + return logicalPosition(point.x() - q->leftPadding()) / q->availableWidth(); + else + return logicalPosition(point.y() - q->topPadding()) / q->availableHeight(); +} + +void QQuickScrollBarPrivate::setInteractive(bool enabled) +{ + Q_Q(QQuickScrollBar); + if (interactive == enabled) + return; + + interactive = enabled; + if (interactive) { + q->setAcceptedMouseButtons(Qt::LeftButton); +#if QT_CONFIG(quicktemplates2_multitouch) + q->setAcceptTouchEvents(true); +#endif +#if QT_CONFIG(cursor) + q->setCursor(Qt::ArrowCursor); +#endif + } else { + q->setAcceptedMouseButtons(Qt::NoButton); +#if QT_CONFIG(quicktemplates2_multitouch) + q->setAcceptTouchEvents(false); +#endif +#if QT_CONFIG(cursor) + q->unsetCursor(); +#endif + q->ungrabMouse(); + } + emit q->interactiveChanged(); +} + +void QQuickScrollBarPrivate::updateActive() +{ + Q_Q(QQuickScrollBar); +#if QT_CONFIG(quicktemplates2_hover) + bool hover = hovered; +#else + bool hover = false; +#endif + q->setActive(moving || (interactive && (pressed || hover))); +} + +void QQuickScrollBarPrivate::resizeContent() +{ + Q_Q(QQuickScrollBar); + 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)); + } +} + +void QQuickScrollBarPrivate::itemImplicitWidthChanged(QQuickItem *item) +{ + Q_Q(QQuickScrollBar); + QQuickControlPrivate::itemImplicitWidthChanged(item); + QQuickIndicatorButton *indicatorButton = q->decreaseVisual(); + if (!indicatorButton || item != indicatorButton->indicator()) { + indicatorButton = q->increaseVisual(); + if (!indicatorButton || item != indicatorButton->indicator()) + return; + } + if (indicatorButton) + emit indicatorButton->implicitIndicatorWidthChanged(); +} + +void QQuickScrollBarPrivate::itemImplicitHeightChanged(QQuickItem *item) +{ + Q_Q(QQuickScrollBar); + QQuickControlPrivate::itemImplicitHeightChanged(item); + QQuickIndicatorButton *indicatorButton = q->decreaseVisual(); + if (!indicatorButton || item != indicatorButton->indicator()) { + indicatorButton = q->increaseVisual(); + if (!indicatorButton || item != indicatorButton->indicator()) + return; + } + if (indicatorButton) + emit indicatorButton->implicitIndicatorHeightChanged(); +} + +bool QQuickScrollBarPrivate::handlePress(const QPointF &point, ulong timestamp) +{ + Q_Q(QQuickScrollBar); + QQuickControlPrivate::handlePress(point, timestamp); + if (QQuickIndicatorButton *indicatorButton = q->decreaseVisual()) { + QQuickItem *decreaseArrow = indicatorButton->indicator(); + if (decreaseArrow && decreaseArrow->contains(q->mapToItem(decreaseArrow, point + QPointF(0.5, 0.5)))) { + indicatorButton->setPressed(true); + q->decrease(); + return true; + } + } + + if (QQuickIndicatorButton *increaseObject = q->increaseVisual()) { + QQuickItem *increaseArrow = increaseObject->indicator(); + if (increaseArrow && increaseArrow->contains(q->mapToItem(increaseArrow, point + QPointF(0.5, 0.5)))) { + increaseObject->setPressed(true); + q->increase(); + return true; + } + } + + offset = positionAt(point) - position; + qreal sz = qMax(size, logicalPosition(minimumSize)); + if (offset < 0 || offset > sz) + offset = sz / 2; + q->setPressed(true); + return true; +} + +bool QQuickScrollBarPrivate::handleMove(const QPointF &point, ulong timestamp) +{ + Q_Q(QQuickScrollBar); + QQuickControlPrivate::handleMove(point, timestamp); + + /* + * handleMove() will be called as soon as you hold the mouse button down *anywhere* on the + * ScrollBar, including the increase/decrease button indicator areas. So without the following + * early return, it would move the scrollbar handle to one of its extremeties. That would + * ruin the behavior we would like when clicking e.g. the "increase button": To step the + * scrollbar gently. + */ + if (!pressed) + return true; + + qreal pos = qMax<qreal>(0.0, qMin<qreal>(positionAt(point) - offset, 1.0 - size)); + if (snapMode == QQuickScrollBar::SnapAlways) + pos = snapPosition(pos); + q->setPosition(pos); + return true; +} + +bool QQuickScrollBarPrivate::handleRelease(const QPointF &point, ulong timestamp) +{ + Q_Q(QQuickScrollBar); + QQuickControlPrivate::handleRelease(point, timestamp); + + if (orientation == Qt::Vertical) { + if (point.y() < q->topPadding() || point.y() >= (q->height() - q->bottomPadding())) + return true; + } else /* orientation == Qt::Horizontal */{ + if (point.x() < q->leftPadding() || point.x() >= (q->width() - q->rightPadding())) + return true; + } + + qreal pos = qMax<qreal>(0.0, qMin<qreal>(positionAt(point) - offset, 1.0 - size)); + if (snapMode != QQuickScrollBar::NoSnap) + pos = snapPosition(pos); + q->setPosition(pos); + offset = 0.0; + q->setPressed(false); + return true; +} + +void QQuickScrollBarPrivate::handleUngrab() +{ + Q_Q(QQuickScrollBar); + QQuickControlPrivate::handleUngrab(); + offset = 0.0; + q->setPressed(false); +} + +void QQuickScrollBarPrivate::visualAreaChange(const VisualArea &newVisualArea, const VisualArea &oldVisualArea) +{ + Q_Q(QQuickScrollBar); + if (!qFuzzyCompare(newVisualArea.size, oldVisualArea.size)) + emit q->visualSizeChanged(); + if (!qFuzzyCompare(newVisualArea.position, oldVisualArea.position)) + emit q->visualPositionChanged(); +} + +void QQuickScrollBarPrivate::updateHover(const QPointF &pos, std::optional<bool> newHoverState) +{ + Q_Q(QQuickScrollBar); + auto updateHoverOnButton = [&](QQuickIndicatorButton *sbButton) { + if (sbButton) { + bool hovered = newHoverState.value_or(false); + if (!newHoverState.has_value()) { + if (QQuickItem *indicator = sbButton->indicator()) + hovered = indicator->contains(q->mapToItem(indicator, pos)); + } + sbButton->setHovered(hovered); + } + }; + updateHoverOnButton(q->decreaseVisual()); + updateHoverOnButton(q->increaseVisual()); +} + +QQuickScrollBar::QQuickScrollBar(QQuickItem *parent) + : QQuickControl(*(new QQuickScrollBarPrivate), parent) +{ + Q_D(QQuickScrollBar); + d->decreaseVisual = new QQuickIndicatorButton(this); + d->increaseVisual = new QQuickIndicatorButton(this); + d->setSizePolicy(QLayoutPolicy::Preferred, QLayoutPolicy::Fixed); + setKeepMouseGrab(true); + setAcceptedMouseButtons(Qt::LeftButton); +#if QT_CONFIG(quicktemplates2_multitouch) + setAcceptTouchEvents(true); +#endif +#if QT_CONFIG(cursor) + setCursor(Qt::ArrowCursor); +#endif +} + +QQuickScrollBarAttached *QQuickScrollBar::qmlAttachedProperties(QObject *object) +{ + return new QQuickScrollBarAttached(object); +} + +/*! + \qmlproperty real QtQuick.Controls::ScrollBar::size + + This property holds the size of the scroll bar, scaled to \c {0.0 - 1.0}. + + \sa {Flickable::visibleArea.heightRatio}{Flickable::visibleArea} + + This property is automatically set when the scroll bar is + \l {Attaching ScrollBar to a Flickable}{attached to a flickable}. + + \sa minimumSize, visualSize +*/ +qreal QQuickScrollBar::size() const +{ + Q_D(const QQuickScrollBar); + return d->size; +} + +void QQuickScrollBar::setSize(qreal size) +{ + Q_D(QQuickScrollBar); + if (!qt_is_finite(size)) + return; + size = qBound(0.0, size, 1.0); + if (qFuzzyCompare(d->size, size)) + return; + + const auto oldVisualArea = d->visualArea(); + d->size = size; + if (d->size + d->position > 1.0) { + d->setPosition(1.0 - d->size, false); + } + + if (isComponentComplete()) + d->resizeContent(); + emit sizeChanged(); + d->visualAreaChange(d->visualArea(), oldVisualArea); +} + +/*! + \qmlproperty real QtQuick.Controls::ScrollBar::position + + This property holds the position of the scroll bar, scaled to \c {0.0 - 1.0}. + + The largest valid scrollbar position is \c {(1.0 - size)}. This gives + correct behavior for the most used case where moving the scrollbar + to the end will put the end of the document at the lower end of the + visible area of the connected Flickable. + + \sa {Flickable::visibleArea.yPosition}{Flickable::visibleArea} + + This property is automatically set when the scroll bar is + \l {Attaching ScrollBar to a Flickable}{attached to a flickable}. + + \sa visualPosition +*/ +qreal QQuickScrollBar::position() const +{ + Q_D(const QQuickScrollBar); + return d->position; +} + +void QQuickScrollBar::setPosition(qreal position) +{ + Q_D(QQuickScrollBar); + d->setPosition(position); +} + +void QQuickScrollBarPrivate::setPosition(qreal newPosition, bool notifyVisualChange) +{ + Q_Q(QQuickScrollBar); + if (!qt_is_finite(newPosition) || qFuzzyCompare(position, newPosition)) + return; + + auto oldVisualArea = visualArea(); + position = newPosition; + if (q->isComponentComplete()) + resizeContent(); + emit q->positionChanged(); + if (notifyVisualChange) + visualAreaChange(visualArea(), oldVisualArea); +} + +/*! + \qmlproperty real QtQuick.Controls::ScrollBar::stepSize + + This property holds the step size. The default value is \c 0.0. + + \sa snapMode, increase(), decrease() +*/ +qreal QQuickScrollBar::stepSize() const +{ + Q_D(const QQuickScrollBar); + return d->stepSize; +} + +void QQuickScrollBar::setStepSize(qreal step) +{ + Q_D(QQuickScrollBar); + if (!qt_is_finite(step) || qFuzzyCompare(d->stepSize, step)) + return; + + d->stepSize = step; + emit stepSizeChanged(); +} + +/*! + \qmlproperty bool QtQuick.Controls::ScrollBar::active + + This property holds whether the scroll bar is active, i.e. when it's \l pressed + or the attached Flickable is \l {Flickable::moving}{moving}. + + It is possible to keep \l {Binding the Active State of Horizontal and Vertical Scroll Bars} + {both horizontal and vertical bars visible} while scrolling in either direction. + + This property is automatically set when the scroll bar is + \l {Attaching ScrollBar to a Flickable}{attached to a flickable}. +*/ +bool QQuickScrollBar::isActive() const +{ + Q_D(const QQuickScrollBar); + return d->active; +} + +void QQuickScrollBar::setActive(bool active) +{ + Q_D(QQuickScrollBar); + if (d->active == active) + return; + + d->active = active; + emit activeChanged(); +} + +/*! + \qmlproperty bool QtQuick.Controls::ScrollBar::pressed + + This property holds whether the scroll bar is pressed. +*/ +bool QQuickScrollBar::isPressed() const +{ + Q_D(const QQuickScrollBar); + return d->pressed; +} + +void QQuickScrollBar::setPressed(bool pressed) +{ + Q_D(QQuickScrollBar); + if (!pressed) { + if (QQuickIndicatorButton *button = decreaseVisual()) + button->setPressed(false); + if (QQuickIndicatorButton *button = increaseVisual()) + button->setPressed(false); + } + if (d->pressed == pressed) + return; + + d->pressed = pressed; + setAccessibleProperty("pressed", pressed); + d->updateActive(); + emit pressedChanged(); +} + +/*! + \qmlproperty enumeration QtQuick.Controls::ScrollBar::orientation + + This property holds the orientation of the scroll bar. + + Possible values: + \value Qt.Horizontal Horizontal + \value Qt.Vertical Vertical (default) + + This property is automatically set when the scroll bar is + \l {Attaching ScrollBar to a Flickable}{attached to a flickable}. + + \sa horizontal, vertical +*/ +Qt::Orientation QQuickScrollBar::orientation() const +{ + Q_D(const QQuickScrollBar); + return d->orientation; +} + +void QQuickScrollBar::setOrientation(Qt::Orientation orientation) +{ + Q_D(QQuickScrollBar); + 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.2 (Qt 5.9) + \qmlproperty enumeration QtQuick.Controls::ScrollBar::snapMode + + This property holds the snap mode. + + Possible values: + \value ScrollBar.NoSnap The scrollbar does not snap (default). + \value ScrollBar.SnapAlways The scrollbar snaps while dragged. + \value ScrollBar.SnapOnRelease The scrollbar does not snap while being dragged, but only after released. + + In the following table, the various modes are illustrated with animations. + The movement and the \l stepSize (\c 0.25) are identical in each animation. + + \table + \header + \row \li \b Value \li \b Example + \row \li \c ScrollBar.NoSnap \li \image qtquickcontrols-scrollbar-nosnap.gif + \row \li \c ScrollBar.SnapAlways \li \image qtquickcontrols-scrollbar-snapalways.gif + \row \li \c ScrollBar.SnapOnRelease \li \image qtquickcontrols-scrollbar-snaponrelease.gif + \endtable + + \sa stepSize +*/ +QQuickScrollBar::SnapMode QQuickScrollBar::snapMode() const +{ + Q_D(const QQuickScrollBar); + return d->snapMode; +} + +void QQuickScrollBar::setSnapMode(SnapMode mode) +{ + Q_D(QQuickScrollBar); + if (d->snapMode == mode) + return; + + d->snapMode = mode; + emit snapModeChanged(); +} + +/*! + \since QtQuick.Controls 2.2 (Qt 5.9) + \qmlproperty bool QtQuick.Controls::ScrollBar::interactive + + This property holds whether the scroll bar is interactive. The default value is \c true. + + A non-interactive scroll bar is visually and behaviorally similar to \l ScrollIndicator. + This property is useful for switching between typical mouse- and touch-orientated UIs + with interactive and non-interactive scroll bars, respectively. +*/ +bool QQuickScrollBar::isInteractive() const +{ + Q_D(const QQuickScrollBar); + return d->interactive; +} + +void QQuickScrollBar::setInteractive(bool interactive) +{ + Q_D(QQuickScrollBar); + d->explicitInteractive = true; + d->setInteractive(interactive); +} + +void QQuickScrollBar::resetInteractive() +{ + Q_D(QQuickScrollBar); + d->explicitInteractive = false; + d->setInteractive(true); +} + +/*! + \since QtQuick.Controls 2.2 (Qt 5.9) + \qmlproperty enumeration QtQuick.Controls::ScrollBar::policy + + This property holds the policy of the scroll bar. The default policy is \c ScrollBar.AsNeeded. + + Possible values: + \value ScrollBar.AsNeeded The scroll bar is only shown when the content is too large to fit. + \value ScrollBar.AlwaysOff The scroll bar is never shown. + \value ScrollBar.AlwaysOn The scroll bar is always shown. + + The following example keeps the vertical scroll bar always visible: + + \snippet qtquickcontrols-scrollbar-policy-alwayson.qml 1 + + Styles may use this property in combination with the \l active property + in order to implement transient scroll bars. Transient scroll bars are + hidden shortly after the last interaction event (hover or press). This + is typically done by animating the opacity of the scroll bar. To override + this behavior, set the policy to \c ScrollBar.AlwaysOn or + \c ScrollBar.AlwaysOff, depending on the size of the content compared to + its view. For example, for a vertical \l ListView: + + \snippet qtquickcontrols-scrollbar-policy-alwayson-when-needed.qml 1 +*/ +QQuickScrollBar::Policy QQuickScrollBar::policy() const +{ + Q_D(const QQuickScrollBar); + return d->policy; +} + +void QQuickScrollBar::setPolicy(Policy policy) +{ + Q_D(QQuickScrollBar); + if (d->policy == policy) + return; + + d->policy = policy; + emit policyChanged(); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlproperty bool QtQuick.Controls::ScrollBar::horizontal + \readonly + + This property holds whether the scroll bar is horizontal. + + \sa orientation +*/ +bool QQuickScrollBar::isHorizontal() const +{ + Q_D(const QQuickScrollBar); + return d->orientation == Qt::Horizontal; +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlproperty bool QtQuick.Controls::ScrollBar::vertical + \readonly + + This property holds whether the scroll bar is vertical. + + \sa orientation +*/ +bool QQuickScrollBar::isVertical() const +{ + Q_D(const QQuickScrollBar); + return d->orientation == Qt::Vertical; +} + +/*! + \since QtQuick.Controls 2.4 (Qt 5.11) + \qmlproperty real QtQuick.Controls::ScrollBar::minimumSize + + This property holds the minimum size of the scroll bar, scaled to \c {0.0 - 1.0}. + + \sa size, visualSize, visualPosition +*/ +qreal QQuickScrollBar::minimumSize() const +{ + Q_D(const QQuickScrollBar); + return d->minimumSize; +} + +void QQuickScrollBar::setMinimumSize(qreal minimumSize) +{ + Q_D(QQuickScrollBar); + if (!qt_is_finite(minimumSize) || qFuzzyCompare(d->minimumSize, minimumSize)) + return; + + auto oldVisualArea = d->visualArea(); + d->minimumSize = qBound(0.0, minimumSize, 1.0); + if (isComponentComplete()) + d->resizeContent(); + emit minimumSizeChanged(); + d->visualAreaChange(d->visualArea(), oldVisualArea); +} + +/*! + \since QtQuick.Controls 2.4 (Qt 5.11) + \qmlproperty real QtQuick.Controls::ScrollBar::visualSize + \readonly + + This property holds the effective visual size of the scroll bar, + which may be limited by the \l {minimumSize}{minimum size}. + + \sa size, minimumSize +*/ +qreal QQuickScrollBar::visualSize() const +{ + Q_D(const QQuickScrollBar); + return d->visualArea().size; +} + +/*! + \since QtQuick.Controls 2.4 (Qt 5.11) + \qmlproperty real QtQuick.Controls::ScrollBar::visualPosition + \readonly + + This property holds the effective visual position of the scroll bar, + which may be limited by the \l {minimumSize}{minimum size}. + + \sa position, minimumSize +*/ +qreal QQuickScrollBar::visualPosition() const +{ + Q_D(const QQuickScrollBar); + return d->visualArea().position; +} + +QQuickIndicatorButton *QQuickScrollBar::decreaseVisual() +{ + Q_D(QQuickScrollBar); + return d->decreaseVisual; +} + +QQuickIndicatorButton *QQuickScrollBar::increaseVisual() +{ + Q_D(QQuickScrollBar); + return d->increaseVisual; +} + +/*! + \qmlmethod void QtQuick.Controls::ScrollBar::increase() + + Increases the position by \l stepSize or \c 0.1 if stepSize is \c 0.0. + + \sa stepSize +*/ +void QQuickScrollBar::increase() +{ + Q_D(QQuickScrollBar); + qreal step = qFuzzyIsNull(d->stepSize) ? 0.1 : d->stepSize; + bool wasActive = d->active; + setActive(true); + setPosition(qMin<qreal>(1.0 - d->size, d->position + step)); + setActive(wasActive); +} + +/*! + \qmlmethod void QtQuick.Controls::ScrollBar::decrease() + + Decreases the position by \l stepSize or \c 0.1 if stepSize is \c 0.0. + + \sa stepSize +*/ +void QQuickScrollBar::decrease() +{ + Q_D(QQuickScrollBar); + qreal step = qFuzzyIsNull(d->stepSize) ? 0.1 : d->stepSize; + bool wasActive = d->active; + setActive(true); + setPosition(qMax<qreal>(0.0, d->position - step)); + setActive(wasActive); +} + +void QQuickScrollBar::mousePressEvent(QMouseEvent *event) +{ + Q_D(QQuickScrollBar); + QQuickControl::mousePressEvent(event); + d->handleMove(event->position(), event->timestamp()); +} + +#if QT_CONFIG(quicktemplates2_hover) +void QQuickScrollBar::hoverChange() +{ + Q_D(QQuickScrollBar); + d->updateActive(); +} + +void QQuickScrollBar::hoverEnterEvent(QHoverEvent *event) +{ + Q_D(QQuickScrollBar); + QQuickControl::hoverEnterEvent(event); + d->updateHover(event->position()); + event->ignore(); +} + +void QQuickScrollBar::hoverMoveEvent(QHoverEvent *event) +{ + Q_D(QQuickScrollBar); + QQuickControl::hoverMoveEvent(event); + d->updateHover(event->position()); + event->ignore(); +} + +void QQuickScrollBar::hoverLeaveEvent(QHoverEvent *event) +{ + Q_D(QQuickScrollBar); + QQuickControl::hoverLeaveEvent(event); + + d->updateHover(QPoint(), false); //position is not needed when we force it to unhover + event->ignore(); +} +#endif + +void QQuickScrollBar::classBegin() +{ + Q_D(QQuickScrollBar); + QQuickControl::classBegin(); + + QQmlContext *context = qmlContext(this); + if (context) { + QQmlEngine::setContextForObject(d->decreaseVisual, context); + QQmlEngine::setContextForObject(d->increaseVisual, context); + } +} + +void QQuickScrollBar::componentComplete() +{ + Q_D(QQuickScrollBar); + QQuickIndicatorButtonPrivate::get(d->decreaseVisual)->executeIndicator(true); + QQuickIndicatorButtonPrivate::get(d->increaseVisual)->executeIndicator(true); + + QQuickControl::componentComplete(); +} + +#if QT_CONFIG(accessibility) +void QQuickScrollBar::accessibilityActiveChanged(bool active) +{ + QQuickControl::accessibilityActiveChanged(active); + + Q_D(QQuickScrollBar); + if (active) { + setAccessibleProperty("pressed", d->pressed); + + if (QQuickAccessibleAttached *accessibleAttached = QQuickControlPrivate::accessibleAttached(this)) { + connect(accessibleAttached, &QQuickAccessibleAttached::increaseAction, this, &QQuickScrollBar::increase); + connect(accessibleAttached, &QQuickAccessibleAttached::decreaseAction, this, &QQuickScrollBar::decrease); + } + } else { + if (QQuickAccessibleAttached *accessibleAttached = QQuickControlPrivate::accessibleAttached(this)) { + disconnect(accessibleAttached, &QQuickAccessibleAttached::increaseAction, this, &QQuickScrollBar::increase); + disconnect(accessibleAttached, &QQuickAccessibleAttached::decreaseAction, this, &QQuickScrollBar::decrease); + } + } +} + +QAccessible::Role QQuickScrollBar::accessibleRole() const +{ + return QAccessible::ScrollBar; +} +#endif + +void QQuickScrollBarAttachedPrivate::setFlickable(QQuickFlickable *item) +{ + if (flickable) { + // 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(flickable)->removeItemChangeListener(this, QQuickItemPrivate::Geometry); + QQuickItemPrivate::get(flickable)->removeItemChangeListener(this, QQuickItemPrivate::Destroyed); + if (horizontal) + cleanupHorizontal(); + if (vertical) + cleanupVertical(); + } + + flickable = item; + + if (item) { + // Don't know how to combine these calls into one, and as long as they're separate calls, + // the remove* calls above need to be separate too, otherwise they will have no effect. + QQuickItemPrivate::get(item)->updateOrAddGeometryChangeListener(this, QQuickGeometryChange::Size); + QQuickItemPrivate::get(item)->updateOrAddItemChangeListener(this, QQuickItemPrivate::Destroyed); + if (horizontal) + initHorizontal(); + if (vertical) + initVertical(); + } +} + +void QQuickScrollBarAttachedPrivate::initHorizontal() +{ + Q_ASSERT(flickable && horizontal); + + connect(flickable, &QQuickFlickable::movingHorizontallyChanged, this, &QQuickScrollBarAttachedPrivate::activateHorizontal); + + // TODO: export QQuickFlickableVisibleArea + QObject *area = flickable->property("visibleArea").value<QObject *>(); + QObject::connect(area, SIGNAL(widthRatioChanged(qreal)), horizontal, SLOT(setSize(qreal))); + QObject::connect(area, SIGNAL(xPositionChanged(qreal)), horizontal, SLOT(setPosition(qreal))); + + // ensure that the ScrollBar is stacked above the Flickable in a ScrollView + QQuickItem *parent = horizontal->parentItem(); + if (parent && parent == flickable->parentItem()) + horizontal->stackAfter(flickable); + + // If a scroll bar was previously hidden (due to e.g. setting a new contentItem + // on a ScrollView), we need to make sure that we un-hide it. + if (auto control = qobject_cast<QQuickControl*>(q_func()->parent())) { + const auto visibility = horizontal->policy() != QQuickScrollBar::AlwaysOff + ? QQuickControlPrivate::UnhideVisibility::Show : QQuickControlPrivate::UnhideVisibility::Hide; + QQuickControlPrivate::unhideOldItem(control, horizontal, visibility); + } + + layoutHorizontal(); + horizontal->setSize(area->property("widthRatio").toReal()); + horizontal->setPosition(area->property("xPosition").toReal()); +} + +void QQuickScrollBarAttachedPrivate::initVertical() +{ + Q_ASSERT(flickable && vertical); + + connect(flickable, &QQuickFlickable::movingVerticallyChanged, this, &QQuickScrollBarAttachedPrivate::activateVertical); + + // TODO: export QQuickFlickableVisibleArea + QObject *area = flickable->property("visibleArea").value<QObject *>(); + QObject::connect(area, SIGNAL(heightRatioChanged(qreal)), vertical, SLOT(setSize(qreal))); + QObject::connect(area, SIGNAL(yPositionChanged(qreal)), vertical, SLOT(setPosition(qreal))); + + // ensure that the ScrollBar is stacked above the Flickable in a ScrollView + QQuickItem *parent = vertical->parentItem(); + if (parent && parent == flickable->parentItem()) + vertical->stackAfter(flickable); + + if (auto control = qobject_cast<QQuickControl*>(q_func()->parent())) { + const auto visibility = vertical->policy() != QQuickScrollBar::AlwaysOff + ? QQuickControlPrivate::UnhideVisibility::Show : QQuickControlPrivate::UnhideVisibility::Hide; + QQuickControlPrivate::unhideOldItem(control, vertical, visibility); + } + + layoutVertical(); + vertical->setSize(area->property("heightRatio").toReal()); + vertical->setPosition(area->property("yPosition").toReal()); +} + +void QQuickScrollBarAttachedPrivate::cleanupHorizontal() +{ + Q_ASSERT(flickable && horizontal); + + QQuickControlPrivate::hideOldItem(horizontal); + // ScrollBar.qml has a binding to visible and ScrollView.qml has a binding to parent. + // If we just set visible to false and parent to null, these bindings will overwrite + // them upon component completion as part of the binding evaluation. + // That's why we remove the binding completely. + const QQmlProperty visibleProperty(horizontal, QStringLiteral("visible")); + const QQmlProperty parentProperty(horizontal, QStringLiteral("parent")); + QQmlPropertyPrivate::removeBinding(visibleProperty); + QQmlPropertyPrivate::removeBinding(parentProperty); + + disconnect(flickable, &QQuickFlickable::movingHorizontallyChanged, this, &QQuickScrollBarAttachedPrivate::activateHorizontal); + + // TODO: export QQuickFlickableVisibleArea + QObject *area = flickable->property("visibleArea").value<QObject *>(); + QObject::disconnect(area, SIGNAL(widthRatioChanged(qreal)), horizontal, SLOT(setSize(qreal))); + QObject::disconnect(area, SIGNAL(xPositionChanged(qreal)), horizontal, SLOT(setPosition(qreal))); +} + +void QQuickScrollBarAttachedPrivate::cleanupVertical() +{ + Q_ASSERT(flickable && vertical); + + QQuickControlPrivate::hideOldItem(vertical); + const QQmlProperty visibleProperty(vertical, QStringLiteral("visible")); + const QQmlProperty parentProperty(vertical, QStringLiteral("parent")); + QQmlPropertyPrivate::removeBinding(visibleProperty); + QQmlPropertyPrivate::removeBinding(parentProperty); + + disconnect(flickable, &QQuickFlickable::movingVerticallyChanged, this, &QQuickScrollBarAttachedPrivate::activateVertical); + + // TODO: export QQuickFlickableVisibleArea + QObject *area = flickable->property("visibleArea").value<QObject *>(); + QObject::disconnect(area, SIGNAL(heightRatioChanged(qreal)), vertical, SLOT(setSize(qreal))); + QObject::disconnect(area, SIGNAL(yPositionChanged(qreal)), vertical, SLOT(setPosition(qreal))); +} + +void QQuickScrollBarAttachedPrivate::activateHorizontal() +{ + QQuickScrollBarPrivate *p = QQuickScrollBarPrivate::get(horizontal); + p->moving = flickable->isMovingHorizontally(); + p->updateActive(); +} + +void QQuickScrollBarAttachedPrivate::activateVertical() +{ + QQuickScrollBarPrivate *p = QQuickScrollBarPrivate::get(vertical); + p->moving = flickable->isMovingVertically(); + p->updateActive(); +} + +// TODO: QQuickFlickable::maxXYExtent() +class QQuickFriendlyFlickable : public QQuickFlickable +{ + friend class QQuickScrollBarAttachedPrivate; +}; + +void QQuickScrollBarAttachedPrivate::scrollHorizontal() +{ + if (!flickable) + return; + + QQuickFriendlyFlickable *f = reinterpret_cast<QQuickFriendlyFlickable *>(flickable); + + const qreal viewwidth = f->width(); + const qreal maxxextent = -f->maxXExtent() + f->minXExtent(); + const qreal cx = horizontal->position() * (maxxextent + viewwidth) - f->minXExtent(); + + if (!qIsNaN(cx) && !qFuzzyCompare(cx, flickable->contentX())) + flickable->setContentX(cx); +} + +void QQuickScrollBarAttachedPrivate::scrollVertical() +{ + if (!flickable) + return; + + QQuickFriendlyFlickable *f = reinterpret_cast<QQuickFriendlyFlickable *>(flickable); + + const qreal viewheight = f->height(); + const qreal maxyextent = -f->maxYExtent() + f->minYExtent(); + const qreal cy = vertical->position() * (maxyextent + viewheight) - f->minYExtent(); + + if (!qIsNaN(cy) && !qFuzzyCompare(cy, flickable->contentY())) + flickable->setContentY(cy); +} + +void QQuickScrollBarAttachedPrivate::mirrorVertical() +{ + layoutVertical(true); +} + +void QQuickScrollBarAttachedPrivate::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 QQuickScrollBarAttachedPrivate::layoutVertical(bool move) +{ + Q_ASSERT(vertical && flickable); + if (vertical->parentItem() != flickable) + return; + vertical->setHeight(flickable->height()); + if (move) + vertical->setX(vertical->isMirrored() ? 0 : flickable->width() - vertical->width()); +} + +void QQuickScrollBarAttachedPrivate::itemGeometryChanged(QQuickItem *item, const 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 + if (flickable) + 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 + if (flickable) + layoutVertical(move); + } +} + +void QQuickScrollBarAttachedPrivate::itemImplicitWidthChanged(QQuickItem *item) +{ + if (item == vertical && flickable) + layoutVertical(true); +} + +void QQuickScrollBarAttachedPrivate::itemImplicitHeightChanged(QQuickItem *item) +{ + if (item == horizontal && flickable) + layoutHorizontal(true); +} + +void QQuickScrollBarAttachedPrivate::itemDestroyed(QQuickItem *item) +{ + if (item == flickable) + flickable = nullptr; + if (item == horizontal) + horizontal = nullptr; + if (item == vertical) + vertical = nullptr; +} + +QQuickScrollBarAttached::QQuickScrollBarAttached(QObject *parent) + : QObject(*(new QQuickScrollBarAttachedPrivate), parent) +{ + Q_D(QQuickScrollBarAttached); + d->setFlickable(qobject_cast<QQuickFlickable *>(parent)); + + if (parent && !d->flickable && !qobject_cast<QQuickScrollView *>(parent)) + qmlWarning(parent) << "ScrollBar must be attached to a Flickable or ScrollView"; +} + +QQuickScrollBarAttached::~QQuickScrollBarAttached() +{ + Q_D(QQuickScrollBarAttached); + if (d->horizontal) { + QQuickItemPrivate::get(d->horizontal)->removeItemChangeListener(d, QsbHorizontalChangeTypes); + d->horizontal = nullptr; + } + if (d->vertical) { + QQuickItemPrivate::get(d->vertical)->removeItemChangeListener(d, QsbVerticalChangeTypes); + d->vertical = nullptr; + } + d->setFlickable(nullptr); +} + +/*! + \qmlattachedproperty ScrollBar QtQuick.Controls::ScrollBar::horizontal + + This property attaches a horizontal scroll bar to a \l Flickable. + + \code + Flickable { + contentWidth: 2000 + ScrollBar.horizontal: ScrollBar { } + } + \endcode + + \sa {Attaching ScrollBar to a Flickable} +*/ +QQuickScrollBar *QQuickScrollBarAttached::horizontal() const +{ + Q_D(const QQuickScrollBarAttached); + return d->horizontal; +} + +void QQuickScrollBarAttached::setHorizontal(QQuickScrollBar *horizontal) +{ + Q_D(QQuickScrollBarAttached); + if (d->horizontal == horizontal) + return; + + if (d->horizontal) { + QQuickItemPrivate::get(d->horizontal)->removeItemChangeListener(d, QsbHorizontalChangeTypes); + QObjectPrivate::disconnect(d->horizontal, &QQuickScrollBar::positionChanged, d, &QQuickScrollBarAttachedPrivate::scrollHorizontal); + + if (d->flickable) + d->cleanupHorizontal(); + } + + d->horizontal = horizontal; + + if (horizontal) { + if (!horizontal->parentItem()) + horizontal->setParentItem(qobject_cast<QQuickItem *>(parent())); + horizontal->setOrientation(Qt::Horizontal); + + QQuickItemPrivate::get(horizontal)->addItemChangeListener(d, QsbHorizontalChangeTypes); + QObjectPrivate::connect(horizontal, &QQuickScrollBar::positionChanged, d, &QQuickScrollBarAttachedPrivate::scrollHorizontal); + + if (d->flickable) + d->initHorizontal(); + } + emit horizontalChanged(); +} + +/*! + \qmlattachedproperty ScrollBar QtQuick.Controls::ScrollBar::vertical + + This property attaches a vertical scroll bar to a \l Flickable. + + \code + Flickable { + contentHeight: 2000 + ScrollBar.vertical: ScrollBar { } + } + \endcode + + \sa {Attaching ScrollBar to a Flickable} +*/ +QQuickScrollBar *QQuickScrollBarAttached::vertical() const +{ + Q_D(const QQuickScrollBarAttached); + return d->vertical; +} + +void QQuickScrollBarAttached::setVertical(QQuickScrollBar *vertical) +{ + Q_D(QQuickScrollBarAttached); + if (d->vertical == vertical) + return; + + if (d->vertical) { + QQuickItemPrivate::get(d->vertical)->removeItemChangeListener(d, QsbVerticalChangeTypes); + QObjectPrivate::disconnect(d->vertical, &QQuickScrollBar::mirroredChanged, d, &QQuickScrollBarAttachedPrivate::mirrorVertical); + QObjectPrivate::disconnect(d->vertical, &QQuickScrollBar::positionChanged, d, &QQuickScrollBarAttachedPrivate::scrollVertical); + + if (d->flickable) + d->cleanupVertical(); + } + + d->vertical = vertical; + + if (vertical) { + if (!vertical->parentItem()) + vertical->setParentItem(qobject_cast<QQuickItem *>(parent())); + vertical->setOrientation(Qt::Vertical); + + QQuickItemPrivate::get(vertical)->addItemChangeListener(d, QsbVerticalChangeTypes); + QObjectPrivate::connect(vertical, &QQuickScrollBar::mirroredChanged, d, &QQuickScrollBarAttachedPrivate::mirrorVertical); + QObjectPrivate::connect(vertical, &QQuickScrollBar::positionChanged, d, &QQuickScrollBarAttachedPrivate::scrollVertical); + + if (d->flickable) + d->initVertical(); + } + emit verticalChanged(); +} + +QT_END_NAMESPACE + +#include "moc_qquickscrollbar_p.cpp" |