diff options
Diffstat (limited to 'src/quicktemplates/qquickrangeslider.cpp')
-rw-r--r-- | src/quicktemplates/qquickrangeslider.cpp | 1340 |
1 files changed, 1340 insertions, 0 deletions
diff --git a/src/quicktemplates/qquickrangeslider.cpp b/src/quicktemplates/qquickrangeslider.cpp new file mode 100644 index 0000000000..d97dfdff90 --- /dev/null +++ b/src/quicktemplates/qquickrangeslider.cpp @@ -0,0 +1,1340 @@ +// 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 "qquickrangeslider_p.h" +#include "qquickcontrol_p_p.h" +#include "qquickdeferredexecute_p_p.h" + +#include <QtCore/qscopedpointer.h> +#include <QtQuick/private/qquickwindow_p.h> + +QT_BEGIN_NAMESPACE + +/*! + \qmltype RangeSlider + \inherits Control +//! \instantiates QQuickRangeSlider + \inqmlmodule QtQuick.Controls + \since 5.7 + \ingroup qtquickcontrols-input + \ingroup qtquickcontrols-focusscopes + \brief Used to select a range of values by sliding two handles along a track. + + \image qtquickcontrols-rangeslider.gif + + RangeSlider is used to select a range specified by two values, by sliding + each handle along a track. + + In the example below, custom \l from and \l to values are set, and the + initial positions of the \l first and \l second handles are set: + + \code + RangeSlider { + from: 1 + to: 100 + first.value: 25 + second.value: 75 + } + \endcode + + In order to perform an action when the value for a particular handle changes, + use the following syntax: + + \code + first.onMoved: console.log("first.value changed to " + first.value) + \endcode + + The \l {first.position} and \l {second.position} properties are expressed as + fractions of the control's size, in the range \c {0.0 - 1.0}. + The \l {first.visualPosition} and \l {second.visualPosition} properties are + the same, except that they are reversed in a + \l {Right-to-left User Interfaces}{right-to-left} application. + The \c visualPosition is useful for positioning the handles when styling + RangeSlider. In the example above, \l {first.visualPosition} will be \c 0.24 + in a left-to-right application, and \c 0.76 in a right-to-left application. + + For a slider that allows the user to select a single value, see \l Slider. + + \sa {Customizing RangeSlider}, {Input Controls}, + {Focus Management in Qt Quick Controls} +*/ + +class QQuickRangeSliderNodePrivate : public QObjectPrivate +{ + Q_DECLARE_PUBLIC(QQuickRangeSliderNode) +public: + QQuickRangeSliderNodePrivate(qreal value, QQuickRangeSlider *slider) + : value(value), + slider(slider) + { + } + + bool isFirst() const; + + void setPosition(qreal position, bool ignoreOtherPosition = false); + void updatePosition(bool ignoreOtherPosition = false); + + void cancelHandle(); + void executeHandle(bool complete = false); + + static QQuickRangeSliderNodePrivate *get(QQuickRangeSliderNode *node); + + qreal value = 0; + bool isPendingValue = false; + qreal pendingValue = 0; + qreal position = 0; + QQuickDeferredPointer<QQuickItem> handle; + QQuickRangeSlider *slider = nullptr; + bool pressed = false; + bool hovered = false; + int touchId = -1; +}; + +bool QQuickRangeSliderNodePrivate::isFirst() const +{ + return this == get(slider->first()); +} + +void QQuickRangeSliderNodePrivate::setPosition(qreal position, bool ignoreOtherPosition) +{ + Q_Q(QQuickRangeSliderNode); + + const qreal min = isFirst() || ignoreOtherPosition ? 0.0 : qMax<qreal>(0.0, slider->first()->position()); + const qreal max = !isFirst() || ignoreOtherPosition ? 1.0 : qMin<qreal>(1.0, slider->second()->position()); + position = qBound(min, position, max); + if (!qFuzzyCompare(this->position, position)) { + this->position = position; + emit q->positionChanged(); + emit q->visualPositionChanged(); + } +} + +void QQuickRangeSliderNodePrivate::updatePosition(bool ignoreOtherPosition) +{ + qreal pos = 0; + if (!qFuzzyCompare(slider->from(), slider->to())) + pos = (value - slider->from()) / (slider->to() - slider->from()); + setPosition(pos, ignoreOtherPosition); +} + +void QQuickRangeSliderNodePrivate::cancelHandle() +{ + Q_Q(QQuickRangeSliderNode); + quickCancelDeferred(q, handleName()); +} + +void QQuickRangeSliderNodePrivate::executeHandle(bool complete) +{ + Q_Q(QQuickRangeSliderNode); + if (handle.wasExecuted()) + return; + + if (!handle || complete) + quickBeginDeferred(q, handleName(), handle); + if (complete) + quickCompleteDeferred(q, handleName(), handle); +} + +QQuickRangeSliderNodePrivate *QQuickRangeSliderNodePrivate::get(QQuickRangeSliderNode *node) +{ + return node->d_func(); +} + +QQuickRangeSliderNode::QQuickRangeSliderNode(qreal value, QQuickRangeSlider *slider) + : QObject(*(new QQuickRangeSliderNodePrivate(value, slider)), slider) +{ +} + +QQuickRangeSliderNode::~QQuickRangeSliderNode() +{ +} + +qreal QQuickRangeSliderNode::value() const +{ + Q_D(const QQuickRangeSliderNode); + return d->value; +} + +void QQuickRangeSliderNode::setValue(qreal value) +{ + Q_D(QQuickRangeSliderNode); + if (!d->slider->isComponentComplete()) { + d->pendingValue = value; + d->isPendingValue = true; + return; + } + + // First, restrict the first value to be within to and from. + const qreal smaller = qMin(d->slider->to(), d->slider->from()); + const qreal larger = qMax(d->slider->to(), d->slider->from()); + value = qBound(smaller, value, larger); + + // Then, ensure that it doesn't go past the other value, + // a check that depends on whether or not the range is inverted. + const bool invertedRange = d->slider->from() > d->slider->to(); + if (d->isFirst()) { + if (invertedRange) { + if (value < d->slider->second()->value()) + value = d->slider->second()->value(); + } else { + if (value > d->slider->second()->value()) + value = d->slider->second()->value(); + } + } else { + if (invertedRange) { + if (value > d->slider->first()->value()) + value = d->slider->first()->value(); + } else { + if (value < d->slider->first()->value()) + value = d->slider->first()->value(); + } + } + + if (!qFuzzyCompare(d->value, value)) { + d->value = value; + d->updatePosition(); + emit valueChanged(); + } +} + +qreal QQuickRangeSliderNode::position() const +{ + Q_D(const QQuickRangeSliderNode); + return d->position; +} + +qreal QQuickRangeSliderNode::visualPosition() const +{ + Q_D(const QQuickRangeSliderNode); + if (d->slider->orientation() == Qt::Vertical || d->slider->isMirrored()) + return 1.0 - d->position; + return d->position; +} + +QQuickItem *QQuickRangeSliderNode::handle() const +{ + QQuickRangeSliderNodePrivate *d = const_cast<QQuickRangeSliderNodePrivate *>(d_func()); + if (!d->handle) + d->executeHandle(); + return d->handle; +} + +void QQuickRangeSliderNode::setHandle(QQuickItem *handle) +{ + Q_D(QQuickRangeSliderNode); + if (d->handle == handle) + return; + + QQuickControlPrivate::warnIfCustomizationNotSupported(d->slider, handle, QStringLiteral("handle")); + + if (!d->handle.isExecuting()) + d->cancelHandle(); + + const qreal oldImplicitHandleWidth = implicitHandleWidth(); + const qreal oldImplicitHandleHeight = implicitHandleHeight(); + + QQuickControlPrivate::get(d->slider)->removeImplicitSizeListener(d->handle); + QQuickControlPrivate::hideOldItem(d->handle); + d->handle = handle; + + if (handle) { + if (!handle->parentItem()) + handle->setParentItem(d->slider); + + QQuickItem *firstHandle = QQuickRangeSliderNodePrivate::get(d->slider->first())->handle; + QQuickItem *secondHandle = QQuickRangeSliderNodePrivate::get(d->slider->second())->handle; + if (firstHandle && secondHandle) { + // The order of property assignments in QML is undefined, + // but we need the first handle to be before the second due + // to focus order constraints, so check for that here. + const QList<QQuickItem *> childItems = d->slider->childItems(); + const int firstIndex = childItems.indexOf(firstHandle); + const int secondIndex = childItems.indexOf(secondHandle); + if (firstIndex != -1 && secondIndex != -1 && firstIndex > secondIndex) { + firstHandle->stackBefore(secondHandle); + // Ensure we have some way of knowing which handle is above + // the other when it comes to mouse presses, and also that + // they are rendered in the correct order. + secondHandle->setZ(secondHandle->z() + 1); + } + } + + handle->setActiveFocusOnTab(true); + QQuickControlPrivate::get(d->slider)->addImplicitSizeListener(handle); + } + + if (!qFuzzyCompare(oldImplicitHandleWidth, implicitHandleWidth())) + emit implicitHandleWidthChanged(); + if (!qFuzzyCompare(oldImplicitHandleHeight, implicitHandleHeight())) + emit implicitHandleHeightChanged(); + if (!d->handle.isExecuting()) + emit handleChanged(); +} + +bool QQuickRangeSliderNode::isPressed() const +{ + Q_D(const QQuickRangeSliderNode); + return d->pressed; +} + +void QQuickRangeSliderNode::setPressed(bool pressed) +{ + Q_D(QQuickRangeSliderNode); + if (d->pressed == pressed) + return; + + d->pressed = pressed; + d->slider->setAccessibleProperty("pressed", pressed || d->slider->second()->isPressed()); + emit pressedChanged(); +} + +bool QQuickRangeSliderNode::isHovered() const +{ + Q_D(const QQuickRangeSliderNode); + return d->hovered; +} + +void QQuickRangeSliderNode::setHovered(bool hovered) +{ + Q_D(QQuickRangeSliderNode); + if (d->hovered == hovered) + return; + + d->hovered = hovered; + emit hoveredChanged(); +} + +qreal QQuickRangeSliderNode::implicitHandleWidth() const +{ + Q_D(const QQuickRangeSliderNode); + if (!d->handle) + return 0; + return d->handle->implicitWidth(); +} + +qreal QQuickRangeSliderNode::implicitHandleHeight() const +{ + Q_D(const QQuickRangeSliderNode); + if (!d->handle) + return 0; + return d->handle->implicitHeight(); +} + +void QQuickRangeSliderNode::increase() +{ + Q_D(QQuickRangeSliderNode); + qreal step = qFuzzyIsNull(d->slider->stepSize()) ? 0.1 : d->slider->stepSize(); + setValue(d->value + step); +} + +void QQuickRangeSliderNode::decrease() +{ + Q_D(QQuickRangeSliderNode); + qreal step = qFuzzyIsNull(d->slider->stepSize()) ? 0.1 : d->slider->stepSize(); + setValue(d->value - step); +} + +static const qreal defaultFrom = 0.0; +static const qreal defaultTo = 1.0; + +class QQuickRangeSliderPrivate : public QQuickControlPrivate +{ + Q_DECLARE_PUBLIC(QQuickRangeSlider) + +public: + QQuickRangeSliderNode *pressedNode(int touchId = -1) const; + +#if QT_CONFIG(quicktemplates2_multitouch) + bool acceptTouch(const QTouchEvent::TouchPoint &point) override; +#endif + bool handlePress(const QPointF &point, ulong timestamp) override; + bool handleMove(const QPointF &point, ulong timestamp) override; + bool handleRelease(const QPointF &point, ulong timestamp) override; + void handleUngrab() override; + + void updateHover(const QPointF &pos); + + void itemImplicitWidthChanged(QQuickItem *item) override; + void itemImplicitHeightChanged(QQuickItem *item) override; + void itemDestroyed(QQuickItem *item) override; + + bool live = true; + qreal from = defaultFrom; + qreal to = defaultTo; + qreal stepSize = 0; + qreal touchDragThreshold = -1; + QQuickRangeSliderNode *first = nullptr; + QQuickRangeSliderNode *second = nullptr; + QPointF pressPoint; + Qt::Orientation orientation = Qt::Horizontal; + QQuickRangeSlider::SnapMode snapMode = QQuickRangeSlider::NoSnap; +}; + +static qreal valueAt(const QQuickRangeSlider *slider, qreal position) +{ + return slider->from() + (slider->to() - slider->from()) * position; +} + +static qreal snapPosition(const QQuickRangeSlider *slider, qreal position) +{ + const qreal range = slider->to() - slider->from(); + if (qFuzzyIsNull(range)) + return position; + + const qreal effectiveStep = slider->stepSize() / range; + if (qFuzzyIsNull(effectiveStep)) + return position; + + return qRound(position / effectiveStep) * effectiveStep; +} + +static qreal positionAt(const QQuickRangeSlider *slider, QQuickItem *handle, const QPointF &point) +{ + if (slider->orientation() == Qt::Horizontal) { + const qreal hw = handle ? handle->width() : 0; + const qreal offset = hw / 2; + const qreal extent = slider->availableWidth() - hw; + if (!qFuzzyIsNull(extent)) { + if (slider->isMirrored()) + return (slider->width() - point.x() - slider->rightPadding() - offset) / extent; + return (point.x() - slider->leftPadding() - offset) / extent; + } + } else { + const qreal hh = handle ? handle->height() : 0; + const qreal offset = hh / 2; + const qreal extent = slider->availableHeight() - hh; + if (!qFuzzyIsNull(extent)) + return (slider->height() - point.y() - slider->bottomPadding() - offset) / extent; + } + return 0; +} + +QQuickRangeSliderNode *QQuickRangeSliderPrivate::pressedNode(int touchId) const +{ + if (touchId == -1) + return first->isPressed() ? first : (second->isPressed() ? second : nullptr); + if (QQuickRangeSliderNodePrivate::get(first)->touchId == touchId) + return first; + if (QQuickRangeSliderNodePrivate::get(second)->touchId == touchId) + return second; + return nullptr; +} + +#if QT_CONFIG(quicktemplates2_multitouch) +bool QQuickRangeSliderPrivate::acceptTouch(const QTouchEvent::TouchPoint &point) +{ + int firstId = QQuickRangeSliderNodePrivate::get(first)->touchId; + int secondId = QQuickRangeSliderNodePrivate::get(second)->touchId; + + if (((firstId == -1 || secondId == -1) && point.state() == QEventPoint::Pressed) || point.id() == firstId || point.id() == secondId) { + touchId = point.id(); + return true; + } + + return false; +} +#endif + +bool QQuickRangeSliderPrivate::handlePress(const QPointF &point, ulong timestamp) +{ + Q_Q(QQuickRangeSlider); + QQuickControlPrivate::handlePress(point, timestamp); + pressPoint = point; + + QQuickItem *firstHandle = first->handle(); + QQuickItem *secondHandle = second->handle(); + const bool firstHit = firstHandle && !first->isPressed() && firstHandle->contains(q->mapToItem(firstHandle, point)); + const bool secondHit = secondHandle && !second->isPressed() && secondHandle->contains(q->mapToItem(secondHandle, point)); + QQuickRangeSliderNode *hitNode = nullptr; + QQuickRangeSliderNode *otherNode = nullptr; + + if (firstHit && secondHit) { + // choose highest + hitNode = firstHandle->z() > secondHandle->z() ? first : second; + otherNode = firstHandle->z() > secondHandle->z() ? second : first; + } else if (firstHit) { + hitNode = first; + otherNode = second; + } else if (secondHit) { + hitNode = second; + otherNode = first; + } else { + // find the nearest + const qreal firstPos = positionAt(q, firstHandle, point); + const qreal secondPos = positionAt(q, secondHandle, point); + const qreal firstDistance = qAbs(firstPos - first->position()); + const qreal secondDistance = qAbs(secondPos - second->position()); + + if (qFuzzyCompare(firstDistance, secondDistance)) { + // same distance => choose the one that can be moved towards the press position + const bool inverted = from > to; + if ((!inverted && firstPos < first->position()) || (inverted && firstPos > first->position())) { + hitNode = first; + otherNode = second; + } else { + hitNode = second; + otherNode = first; + } + } else if (firstDistance < secondDistance) { + hitNode = first; + otherNode = second; + } else { + hitNode = second; + otherNode = first; + } + } + + if (hitNode) { + hitNode->setPressed(true); + if (QQuickItem *handle = hitNode->handle()) { + handle->setZ(1); + + // A specific handle was hit, so it should get focus, rather than the default + // (first handle) that gets focus whenever the RangeSlider itself does - see focusInEvent(). + if (focusPolicy & Qt::ClickFocus) + handle->forceActiveFocus(Qt::MouseFocusReason); + } + QQuickRangeSliderNodePrivate::get(hitNode)->touchId = touchId; + } + if (otherNode) { + if (QQuickItem *handle = otherNode->handle()) + handle->setZ(0); + } + return true; +} + +bool QQuickRangeSliderPrivate::handleMove(const QPointF &point, ulong timestamp) +{ + Q_Q(QQuickRangeSlider); + QQuickControlPrivate::handleMove(point, timestamp); + QQuickRangeSliderNode *pressedNode = QQuickRangeSliderPrivate::pressedNode(touchId); + if (pressedNode) { + const qreal oldPos = pressedNode->position(); + qreal pos = positionAt(q, pressedNode->handle(), point); + if (snapMode == QQuickRangeSlider::SnapAlways) + pos = snapPosition(q, pos); + if (live) + pressedNode->setValue(valueAt(q, pos)); + else + QQuickRangeSliderNodePrivate::get(pressedNode)->setPosition(pos); + + if (!qFuzzyCompare(pressedNode->position(), oldPos)) + emit pressedNode->moved(); + } + return true; +} + +bool QQuickRangeSliderPrivate::handleRelease(const QPointF &point, ulong timestamp) +{ + Q_Q(QQuickRangeSlider); + QQuickControlPrivate::handleRelease(point, timestamp); + pressPoint = QPointF(); + + QQuickRangeSliderNode *pressedNode = QQuickRangeSliderPrivate::pressedNode(touchId); + if (!pressedNode) + return true; + QQuickRangeSliderNodePrivate *pressedNodePrivate = QQuickRangeSliderNodePrivate::get(pressedNode); + + const qreal oldPos = pressedNode->position(); + qreal pos = positionAt(q, pressedNode->handle(), point); + if (snapMode != QQuickRangeSlider::NoSnap) + pos = snapPosition(q, pos); + qreal val = valueAt(q, pos); + if (!qFuzzyCompare(val, pressedNode->value())) + pressedNode->setValue(val); + else if (snapMode != QQuickRangeSlider::NoSnap) + pressedNodePrivate->setPosition(pos); + q->setKeepMouseGrab(false); + q->setKeepTouchGrab(false); + + if (!qFuzzyCompare(pressedNode->position(), oldPos)) + emit pressedNode->moved(); + + pressedNode->setPressed(false); + pressedNodePrivate->touchId = -1; + return true; +} + +void QQuickRangeSliderPrivate::handleUngrab() +{ + QQuickControlPrivate::handleUngrab(); + pressPoint = QPointF(); + first->setPressed(false); + second->setPressed(false); + QQuickRangeSliderNodePrivate::get(first)->touchId = -1; + QQuickRangeSliderNodePrivate::get(second)->touchId = -1; +} + +void QQuickRangeSliderPrivate::updateHover(const QPointF &pos) +{ + Q_Q(QQuickRangeSlider); + QQuickItem *firstHandle = first->handle(); + QQuickItem *secondHandle = second->handle(); + bool firstHandleHovered = firstHandle && firstHandle->isEnabled() + && firstHandle->contains(q->mapToItem(firstHandle, pos)); + bool secondHandleHovered = secondHandle && secondHandle->isEnabled() + && secondHandle->contains(q->mapToItem(secondHandle, pos)); + + if (firstHandleHovered && secondHandleHovered) { + // Only hover the handle with the higher Z value. + const bool firstHandleHasHigherZ = firstHandle->z() > secondHandle->z(); + firstHandleHovered = firstHandleHasHigherZ; + secondHandleHovered = !firstHandleHasHigherZ; + } + first->setHovered(firstHandleHovered); + second->setHovered(secondHandleHovered); +} + +void QQuickRangeSliderPrivate::itemImplicitWidthChanged(QQuickItem *item) +{ + QQuickControlPrivate::itemImplicitWidthChanged(item); + if (item == first->handle()) + emit first->implicitHandleWidthChanged(); + else if (item == second->handle()) + emit second->implicitHandleWidthChanged(); +} + +void QQuickRangeSliderPrivate::itemImplicitHeightChanged(QQuickItem *item) +{ + QQuickControlPrivate::itemImplicitHeightChanged(item); + if (item == first->handle()) + emit first->implicitHandleHeightChanged(); + else if (item == second->handle()) + emit second->implicitHandleHeightChanged(); +} + +void QQuickRangeSliderPrivate::itemDestroyed(QQuickItem *item) +{ + QQuickControlPrivate::itemDestroyed(item); + if (item == first->handle()) + first->setHandle(nullptr); + else if (item == second->handle()) + second->setHandle(nullptr); +} + +QQuickRangeSlider::QQuickRangeSlider(QQuickItem *parent) + : QQuickControl(*(new QQuickRangeSliderPrivate), parent) +{ + Q_D(QQuickRangeSlider); + d->first = new QQuickRangeSliderNode(0.0, this); + d->second = new QQuickRangeSliderNode(1.0, this); + d->setSizePolicy(QLayoutPolicy::Preferred, QLayoutPolicy::Fixed); + + setFlag(QQuickItem::ItemIsFocusScope); +#ifdef Q_OS_MACOS + setFocusPolicy(Qt::TabFocus); +#else + setFocusPolicy(Qt::StrongFocus); +#endif + setAcceptedMouseButtons(Qt::LeftButton); +#if QT_CONFIG(quicktemplates2_multitouch) + setAcceptTouchEvents(true); +#endif +#if QT_CONFIG(cursor) + setCursor(Qt::ArrowCursor); +#endif +} + +QQuickRangeSlider::~QQuickRangeSlider() +{ + Q_D(QQuickRangeSlider); + d->removeImplicitSizeListener(d->first->handle()); + d->removeImplicitSizeListener(d->second->handle()); +} + +/*! + \qmlproperty real QtQuick.Controls::RangeSlider::from + + This property holds the starting value for the range. The default value is \c 0.0. + + \sa to, first.value, second.value +*/ +qreal QQuickRangeSlider::from() const +{ + Q_D(const QQuickRangeSlider); + return d->from; +} + +void QQuickRangeSlider::setFrom(qreal from) +{ + Q_D(QQuickRangeSlider); + if (qFuzzyCompare(d->from, from)) + return; + + d->from = from; + emit fromChanged(); + + if (isComponentComplete()) { + d->first->setValue(d->first->value()); + d->second->setValue(d->second->value()); + auto *firstPrivate = QQuickRangeSliderNodePrivate::get(d->first); + auto *secondPrivate = QQuickRangeSliderNodePrivate::get(d->second); + firstPrivate->updatePosition(true); + secondPrivate->updatePosition(); + } +} + +/*! + \qmlproperty real QtQuick.Controls::RangeSlider::to + + This property holds the end value for the range. The default value is \c 1.0. + + \sa from, first.value, second.value +*/ +qreal QQuickRangeSlider::to() const +{ + Q_D(const QQuickRangeSlider); + return d->to; +} + +void QQuickRangeSlider::setTo(qreal to) +{ + Q_D(QQuickRangeSlider); + if (qFuzzyCompare(d->to, to)) + return; + + d->to = to; + emit toChanged(); + + if (isComponentComplete()) { + d->first->setValue(d->first->value()); + d->second->setValue(d->second->value()); + auto *firstPrivate = QQuickRangeSliderNodePrivate::get(d->first); + auto *secondPrivate = QQuickRangeSliderNodePrivate::get(d->second); + firstPrivate->updatePosition(true); + secondPrivate->updatePosition(); + } +} + +/*! + \since QtQuick.Controls 2.5 (Qt 5.12) + \qmlproperty qreal QtQuick.Controls::RangeSlider::touchDragThreshold + + This property holds the threshold (in logical pixels) at which a touch drag event will be initiated. + The mouse drag threshold won't be affected. + The default value is \c Qt.styleHints.startDragDistance. + + \sa QStyleHints + +*/ +qreal QQuickRangeSlider::touchDragThreshold() const +{ + Q_D(const QQuickRangeSlider); + return d->touchDragThreshold; +} + +void QQuickRangeSlider::setTouchDragThreshold(qreal touchDragThreshold) +{ + Q_D(QQuickRangeSlider); + if (d->touchDragThreshold == touchDragThreshold) + return; + + d->touchDragThreshold = touchDragThreshold; + emit touchDragThresholdChanged(); +} + +void QQuickRangeSlider::resetTouchDragThreshold() +{ + setTouchDragThreshold(-1); +} + +/*! + \since QtQuick.Controls 2.5 (Qt 5.12) + \qmlmethod real QtQuick.Controls::RangeSlider::valueAt(real position) + + Returns the value for the given \a position. + + \sa first.value, second.value, first.position, second.position, live +*/ +qreal QQuickRangeSlider::valueAt(qreal position) const +{ + Q_D(const QQuickRangeSlider); + const qreal value = (d->to - d->from) * position; + if (qFuzzyIsNull(d->stepSize)) + return d->from + value; + return d->from + qRound(value / d->stepSize) * d->stepSize; +} + +/*! + \qmlproperty real QtQuick.Controls::RangeSlider::first.value + \qmlproperty real QtQuick.Controls::RangeSlider::first.position + \qmlproperty real QtQuick.Controls::RangeSlider::first.visualPosition + \qmlproperty Item QtQuick.Controls::RangeSlider::first.handle + \qmlproperty bool QtQuick.Controls::RangeSlider::first.pressed + \qmlproperty bool QtQuick.Controls::RangeSlider::first.hovered + \qmlproperty real QtQuick.Controls::RangeSlider::first.implicitHandleWidth + \qmlproperty real QtQuick.Controls::RangeSlider::first.implicitHandleHeight + + \table + \header + \li Property + \li Description + \row + \li value + \li This property holds the value of the first handle in the range + \c from - \c to. + + If \l from is greater than \l to, the value of the first handle + must be greater than the second, and vice versa. + + The default value is \c 0.0. + \row + \li handle + \li This property holds the first handle item. + \row + \li visualPosition + \li This property holds the visual position of the first handle. + + The position is expressed as a fraction of the control's size, in the range + \c {0.0 - 1.0}. When the control is \l {Control::mirrored}{mirrored}, the + value is equal to \c {1.0 - position}. This makes the value suitable for + visualizing the slider, taking right-to-left support into account. + \row + \li position + \li This property holds the logical position of the first handle. + + The position is expressed as a fraction of the control's size, in the range + \c {0.0 - 1.0}. For visualizing a slider, the right-to-left aware + \l {first.visualPosition}{visualPosition} should be used instead. + \row + \li pressed + \li This property holds whether the first handle is pressed by either touch, + mouse, or keys. + \row + \li hovered + \li This property holds whether the first handle is hovered. + This property was introduced in QtQuick.Controls 2.1. + \row + \li implicitHandleWidth + \li This property holds the implicit width of the first handle. + This property was introduced in QtQuick.Controls 2.5. + \row + \li implicitHandleHeight + \li This property holds the implicit height of the first handle. + This property was introduced in QtQuick.Controls 2.5. + \endtable + + \sa first.moved(), first.increase(), first.decrease() +*/ +QQuickRangeSliderNode *QQuickRangeSlider::first() const +{ + Q_D(const QQuickRangeSlider); + return d->first; +} + +/*! + \qmlsignal void QtQuick.Controls::RangeSlider::first.moved() + \qmlsignal void QtQuick.Controls::RangeSlider::second.moved() + \since QtQuick.Controls 2.5 + + This signal is emitted when either the first or second handle has been + interactively moved by the user by either touch, mouse, or keys. + + \sa first, second +*/ + +/*! + \qmlproperty real QtQuick.Controls::RangeSlider::second.value + \qmlproperty real QtQuick.Controls::RangeSlider::second.position + \qmlproperty real QtQuick.Controls::RangeSlider::second.visualPosition + \qmlproperty Item QtQuick.Controls::RangeSlider::second.handle + \qmlproperty bool QtQuick.Controls::RangeSlider::second.pressed + \qmlproperty bool QtQuick.Controls::RangeSlider::second.hovered + \qmlproperty real QtQuick.Controls::RangeSlider::second.implicitHandleWidth + \qmlproperty real QtQuick.Controls::RangeSlider::second.implicitHandleHeight + + \table + \header + \li Property + \li Description + \row + \li value + \li This property holds the value of the second handle in the range + \c from - \c to. + + If \l from is greater than \l to, the value of the first handle + must be greater than the second, and vice versa. + + The default value is \c 0.0. + \row + \li handle + \li This property holds the second handle item. + \row + \li visualPosition + \li This property holds the visual position of the second handle. + + The position is expressed as a fraction of the control's size, in the range + \c {0.0 - 1.0}. When the control is \l {Control::mirrored}{mirrored}, the + value is equal to \c {1.0 - position}. This makes the value suitable for + visualizing the slider, taking right-to-left support into account. + \row + \li position + \li This property holds the logical position of the second handle. + + The position is expressed as a fraction of the control's size, in the range + \c {0.0 - 1.0}. For visualizing a slider, the right-to-left aware + \l {second.visualPosition}{visualPosition} should be used instead. + \row + \li pressed + \li This property holds whether the second handle is pressed by either touch, + mouse, or keys. + \row + \li hovered + \li This property holds whether the second handle is hovered. + This property was introduced in QtQuick.Controls 2.1. + \row + \li implicitHandleWidth + \li This property holds the implicit width of the second handle. + This property was introduced in QtQuick.Controls 2.5. + \row + \li implicitHandleHeight + \li This property holds the implicit height of the second handle. + This property was introduced in QtQuick.Controls 2.5. + \endtable + + \sa second.moved(), second.increase(), second.decrease() +*/ +QQuickRangeSliderNode *QQuickRangeSlider::second() const +{ + Q_D(const QQuickRangeSlider); + return d->second; +} + +/*! + \qmlproperty real QtQuick.Controls::RangeSlider::stepSize + + This property holds the step size. The default value is \c 0.0. + + \sa snapMode, first.increase(), first.decrease() +*/ +qreal QQuickRangeSlider::stepSize() const +{ + Q_D(const QQuickRangeSlider); + return d->stepSize; +} + +void QQuickRangeSlider::setStepSize(qreal step) +{ + Q_D(QQuickRangeSlider); + if (qFuzzyCompare(d->stepSize, step)) + return; + + d->stepSize = step; + emit stepSizeChanged(); +} + +/*! + \qmlproperty enumeration QtQuick.Controls::RangeSlider::snapMode + + This property holds the snap mode. + + The snap mode determines how the slider handles behave with + regards to the \l stepSize. + + Possible values: + \value RangeSlider.NoSnap The slider does not snap (default). + \value RangeSlider.SnapAlways The slider snaps while the handle is dragged. + \value RangeSlider.SnapOnRelease The slider does not snap while being dragged, but only after the handle is released. + + For visual explanations of the various modes, see the + \l {Slider::}{snapMode} documentation of \l Slider. + + \sa stepSize +*/ +QQuickRangeSlider::SnapMode QQuickRangeSlider::snapMode() const +{ + Q_D(const QQuickRangeSlider); + return d->snapMode; +} + +void QQuickRangeSlider::setSnapMode(SnapMode mode) +{ + Q_D(QQuickRangeSlider); + if (d->snapMode == mode) + return; + + d->snapMode = mode; + emit snapModeChanged(); +} + +/*! + \qmlproperty enumeration QtQuick.Controls::RangeSlider::orientation + + This property holds the orientation. + + Possible values: + \value Qt.Horizontal Horizontal (default) + \value Qt.Vertical Vertical + + \sa horizontal, vertical +*/ +Qt::Orientation QQuickRangeSlider::orientation() const +{ + Q_D(const QQuickRangeSlider); + return d->orientation; +} + +void QQuickRangeSlider::setOrientation(Qt::Orientation orientation) +{ + Q_D(QQuickRangeSlider); + 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; + emit orientationChanged(); +} + +/*! + \qmlmethod void QtQuick.Controls::RangeSlider::setValues(real firstValue, real secondValue) + + Sets \l first.value and \l second.value with the given arguments. + + If \l to is larger than \l from and \a firstValue is larger than + \a secondValue, firstValue will be clamped to secondValue. + + If \l from is larger than \l to and secondValue is larger than + firstValue, secondValue will be clamped to firstValue. + + This function may be necessary to set the first and second values + after the control has been completed, as there is a circular + dependency between firstValue and secondValue which can cause + assigned values to be clamped to each other. + + \sa stepSize +*/ +void QQuickRangeSlider::setValues(qreal firstValue, qreal secondValue) +{ + Q_D(QQuickRangeSlider); + // Restrict the values to be within to and from. + const qreal smaller = qMin(d->to, d->from); + const qreal larger = qMax(d->to, d->from); + firstValue = qBound(smaller, firstValue, larger); + secondValue = qBound(smaller, secondValue, larger); + + if (d->from > d->to) { + // If the from and to values are reversed, the secondValue + // might be less than the first value, which is not allowed. + if (secondValue > firstValue) + secondValue = firstValue; + } else { + // Otherwise, clamp first to second if it's too large. + if (firstValue > secondValue) + firstValue = secondValue; + } + + // Then set both values. If they didn't change, no change signal will be emitted. + QQuickRangeSliderNodePrivate *firstPrivate = QQuickRangeSliderNodePrivate::get(d->first); + if (firstValue != firstPrivate->value) { + firstPrivate->value = firstValue; + emit d->first->valueChanged(); + } + + QQuickRangeSliderNodePrivate *secondPrivate = QQuickRangeSliderNodePrivate::get(d->second); + if (secondValue != secondPrivate->value) { + secondPrivate->value = secondValue; + emit d->second->valueChanged(); + } + + // After we've set both values, then we can update the positions. + // If we don't do this last, the positions may be incorrect. + firstPrivate->updatePosition(true); + secondPrivate->updatePosition(); +} + +/*! + \since QtQuick.Controls 2.2 (Qt 5.9) + \qmlproperty bool QtQuick.Controls::RangeSlider::live + + This property holds whether the slider provides live updates for the \l first.value + and \l second.value properties while the respective handles are dragged. + + The default value is \c true. + + \sa first.value, second.value +*/ +bool QQuickRangeSlider::live() const +{ + Q_D(const QQuickRangeSlider); + return d->live; +} + +void QQuickRangeSlider::setLive(bool live) +{ + Q_D(QQuickRangeSlider); + if (d->live == live) + return; + + d->live = live; + emit liveChanged(); +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlproperty bool QtQuick.Controls::RangeSlider::horizontal + \readonly + + This property holds whether the slider is horizontal. + + \sa orientation +*/ +bool QQuickRangeSlider::isHorizontal() const +{ + Q_D(const QQuickRangeSlider); + return d->orientation == Qt::Horizontal; +} + +/*! + \since QtQuick.Controls 2.3 (Qt 5.10) + \qmlproperty bool QtQuick.Controls::RangeSlider::vertical + \readonly + + This property holds whether the slider is vertical. + + \sa orientation +*/ +bool QQuickRangeSlider::isVertical() const +{ + Q_D(const QQuickRangeSlider); + return d->orientation == Qt::Vertical; +} + +void QQuickRangeSlider::focusInEvent(QFocusEvent *event) +{ + Q_D(QQuickRangeSlider); + QQuickControl::focusInEvent(event); + + // The active focus ends up to RangeSlider when using forceActiveFocus() + // or QML KeyNavigation. We must forward the focus to one of the handles, + // because RangeSlider handles key events for the focused handle. If + // neither handle has active focus, RangeSlider doesn't do anything. + QQuickItem *handle = nextItemInFocusChain(); + // QQuickItem::nextItemInFocusChain() only works as desired with + // Qt::TabFocusAllControls. otherwise pick the first handle + if (!handle || handle == this) + handle = d->first->handle(); + if (handle) + handle->forceActiveFocus(event->reason()); +} + +void QQuickRangeSlider::keyPressEvent(QKeyEvent *event) +{ + Q_D(QQuickRangeSlider); + QQuickControl::keyPressEvent(event); + + QQuickRangeSliderNode *focusNode = d->first->handle()->hasActiveFocus() + ? d->first : (d->second->handle()->hasActiveFocus() ? d->second : nullptr); + if (!focusNode) + return; + + const qreal oldValue = focusNode->value(); + if (d->orientation == Qt::Horizontal) { + if (event->key() == Qt::Key_Left) { + focusNode->setPressed(true); + if (isMirrored()) + focusNode->increase(); + else + focusNode->decrease(); + event->accept(); + } else if (event->key() == Qt::Key_Right) { + focusNode->setPressed(true); + if (isMirrored()) + focusNode->decrease(); + else + focusNode->increase(); + event->accept(); + } + } else { + if (event->key() == Qt::Key_Up) { + focusNode->setPressed(true); + focusNode->increase(); + event->accept(); + } else if (event->key() == Qt::Key_Down) { + focusNode->setPressed(true); + focusNode->decrease(); + event->accept(); + } + } + if (!qFuzzyCompare(focusNode->value(), oldValue)) + emit focusNode->moved(); +} + +void QQuickRangeSlider::hoverEnterEvent(QHoverEvent *event) +{ + Q_D(QQuickRangeSlider); + QQuickControl::hoverEnterEvent(event); + d->updateHover(event->position()); + event->ignore(); +} + +void QQuickRangeSlider::hoverMoveEvent(QHoverEvent *event) +{ + Q_D(QQuickRangeSlider); + QQuickControl::hoverMoveEvent(event); + d->updateHover(event->position()); + event->ignore(); +} + +void QQuickRangeSlider::hoverLeaveEvent(QHoverEvent *event) +{ + Q_D(QQuickRangeSlider); + QQuickControl::hoverLeaveEvent(event); + d->first->setHovered(false); + d->second->setHovered(false); + event->ignore(); +} + +void QQuickRangeSlider::keyReleaseEvent(QKeyEvent *event) +{ + Q_D(QQuickRangeSlider); + QQuickControl::keyReleaseEvent(event); + d->first->setPressed(false); + d->second->setPressed(false); +} + +void QQuickRangeSlider::mousePressEvent(QMouseEvent *event) +{ + Q_D(QQuickRangeSlider); + QQuickControl::mousePressEvent(event); + d->handleMove(event->position(), event->timestamp()); + setKeepMouseGrab(true); +} + +#if QT_CONFIG(quicktemplates2_multitouch) +void QQuickRangeSlider::touchEvent(QTouchEvent *event) +{ + Q_D(QQuickRangeSlider); + switch (event->type()) { + case QEvent::TouchUpdate: + for (const QTouchEvent::TouchPoint &point : event->points()) { + if (!d->acceptTouch(point)) + continue; + + switch (point.state()) { + case QEventPoint::Pressed: + d->handlePress(point.position(), event->timestamp()); + break; + case QEventPoint::Updated: + if (!keepTouchGrab()) { + if (d->orientation == Qt::Horizontal) + setKeepTouchGrab(QQuickWindowPrivate::dragOverThreshold(point.position().x() - point.pressPosition().x(), Qt::XAxis, &point, qRound(d->touchDragThreshold))); + else + setKeepTouchGrab(QQuickWindowPrivate::dragOverThreshold(point.position().y() - point.pressPosition().y(), Qt::YAxis, &point, qRound(d->touchDragThreshold))); + } + if (keepTouchGrab()) + d->handleMove(point.position(), event->timestamp()); + break; + case QEventPoint::Released: + d->handleRelease(point.position(), event->timestamp()); + break; + default: + break; + } + } + break; + + default: + QQuickControl::touchEvent(event); + break; + } +} +#endif + +void QQuickRangeSlider::mirrorChange() +{ + Q_D(QQuickRangeSlider); + QQuickControl::mirrorChange(); + emit d->first->visualPositionChanged(); + emit d->second->visualPositionChanged(); +} + +void QQuickRangeSlider::classBegin() +{ + Q_D(QQuickRangeSlider); + QQuickControl::classBegin(); + + QQmlContext *context = qmlContext(this); + if (context) { + QQmlEngine::setContextForObject(d->first, context); + QQmlEngine::setContextForObject(d->second, context); + } +} + +void QQuickRangeSlider::componentComplete() +{ + Q_D(QQuickRangeSlider); + QQuickRangeSliderNodePrivate *firstPrivate = QQuickRangeSliderNodePrivate::get(d->first); + QQuickRangeSliderNodePrivate *secondPrivate = QQuickRangeSliderNodePrivate::get(d->second); + firstPrivate->executeHandle(true); + secondPrivate->executeHandle(true); + + QQuickControl::componentComplete(); + + if (firstPrivate->isPendingValue || secondPrivate->isPendingValue + || !qFuzzyCompare(d->from, defaultFrom) || !qFuzzyCompare(d->to, defaultTo)) { + // Properties were set while we were loading. To avoid clamping issues that occur when setting the + // values of first and second overriding values set by the user, set them all at once at the end. + // Another reason that we must set these values here is that the from and to values might have made the old range invalid. + setValues(firstPrivate->isPendingValue ? firstPrivate->pendingValue : firstPrivate->value, + secondPrivate->isPendingValue ? secondPrivate->pendingValue : secondPrivate->value); + + firstPrivate->pendingValue = 0; + firstPrivate->isPendingValue = false; + secondPrivate->pendingValue = 0; + secondPrivate->isPendingValue = false; + } else { + // If there was no pending data, we must still update the positions, + // as first.setValue()/second.setValue() won't be called as part of default construction. + // Don't need to ignore the second position when updating the first position here, + // as our default values are guaranteed to be valid. + firstPrivate->updatePosition(); + secondPrivate->updatePosition(); + } +} + +/*! + \qmlmethod void QtQuick.Controls::RangeSlider::first.increase() + + Increases the value of the handle by stepSize, or \c 0.1 if stepSize is not defined. + + \sa first +*/ + +/*! + \qmlmethod void QtQuick.Controls::RangeSlider::first.decrease() + + Decreases the value of the handle by stepSize, or \c 0.1 if stepSize is not defined. + + \sa first +*/ + +/*! + \qmlmethod void QtQuick.Controls::RangeSlider::second.increase() + + Increases the value of the handle by stepSize, or \c 0.1 if stepSize is not defined. + + \sa second +*/ + +/*! + \qmlmethod void QtQuick.Controls::RangeSlider::second.decrease() + + Decreases the value of the handle by stepSize, or \c 0.1 if stepSize is not defined. + + \sa second +*/ + +#if QT_CONFIG(accessibility) +QAccessible::Role QQuickRangeSlider::accessibleRole() const +{ + return QAccessible::Slider; +} +#endif + +QT_END_NAMESPACE + +#include "moc_qquickrangeslider_p.cpp" |