From 3d55d182704e2c2b01aa3e5fc159507e322dd281 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Wed, 13 Mar 2019 09:50:14 +0100 Subject: Add BoundaryRule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ChangeLog][Qt Labs Animation] Added the BoundaryRule QML type, a PropertyValueInterceptor that restricts the range of values a numeric property can have, applies "resistance" when the value is overshooting, and provides the ability to animate it back within range. Change-Id: I677b407a351c12b0c5b23c34a45933154310c2cd Reviewed-by: Jan Arve Sæther --- src/imports/imports.pro | 1 + src/imports/labsanimation/dependencies.json | 2 + src/imports/labsanimation/labsanimation.pro | 11 + src/imports/labsanimation/plugin.cpp | 81 +++ src/imports/labsanimation/plugins.qmltypes | 36 ++ src/imports/labsanimation/qmldir | 3 + src/quick/doc/snippets/qml/boundaryRule.qml | 74 +++ src/quick/util/qquickboundaryrule.cpp | 574 +++++++++++++++++++++ src/quick/util/qquickboundaryrule_p.h | 145 ++++++ src/quick/util/util.pri | 2 + .../quick/qquickboundaryrule/data/dragHandler.qml | 23 + .../qquickboundaryrule/qquickboundaryrule.pro | 12 + .../qquickboundaryrule/tst_qquickboundaryrule.cpp | 99 ++++ tests/auto/quick/quick.pro | 1 + 14 files changed, 1064 insertions(+) create mode 100644 src/imports/labsanimation/dependencies.json create mode 100644 src/imports/labsanimation/labsanimation.pro create mode 100644 src/imports/labsanimation/plugin.cpp create mode 100644 src/imports/labsanimation/plugins.qmltypes create mode 100644 src/imports/labsanimation/qmldir create mode 100644 src/quick/doc/snippets/qml/boundaryRule.qml create mode 100644 src/quick/util/qquickboundaryrule.cpp create mode 100644 src/quick/util/qquickboundaryrule_p.h create mode 100644 tests/auto/quick/qquickboundaryrule/data/dragHandler.qml create mode 100644 tests/auto/quick/qquickboundaryrule/qquickboundaryrule.pro create mode 100644 tests/auto/quick/qquickboundaryrule/tst_qquickboundaryrule.cpp diff --git a/src/imports/imports.pro b/src/imports/imports.pro index 24e93fec1c..9b1cfa6aa8 100644 --- a/src/imports/imports.pro +++ b/src/imports/imports.pro @@ -4,6 +4,7 @@ SUBDIRS += \ builtins \ qtqml \ models \ + labsanimation \ labsmodels qtConfig(thread): SUBDIRS += folderlistmodel diff --git a/src/imports/labsanimation/dependencies.json b/src/imports/labsanimation/dependencies.json new file mode 100644 index 0000000000..0d4f101c7a --- /dev/null +++ b/src/imports/labsanimation/dependencies.json @@ -0,0 +1,2 @@ +[ +] diff --git a/src/imports/labsanimation/labsanimation.pro b/src/imports/labsanimation/labsanimation.pro new file mode 100644 index 0000000000..64e076401f --- /dev/null +++ b/src/imports/labsanimation/labsanimation.pro @@ -0,0 +1,11 @@ +CXX_MODULE = qml +TARGET = labsanimationplugin +TARGETPATH = Qt/labs/animation +IMPORT_VERSION = 1.0 + +SOURCES += \ + plugin.cpp + +QT = qml-private quick-private + +load(qml_plugin) diff --git a/src/imports/labsanimation/plugin.cpp b/src/imports/labsanimation/plugin.cpp new file mode 100644 index 0000000000..d8c0c071ca --- /dev/null +++ b/src/imports/labsanimation/plugin.cpp @@ -0,0 +1,81 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the plugins of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include + +#include + +QT_BEGIN_NAMESPACE + +/*! + \qmlmodule Qt.labs.animation 1.0 + \title Qt Quick experimental animation types + \ingroup qmlmodules + \brief Provides QML experimental types for animation + \since 5.14 + + This QML module contains experimental QML types related to animation. + + To use the types in this module, import the module with the following line: + + \code + import Qt.labs.animation 1.0 + \endcode +*/ + +//![class decl] +class QtLabsAnimationPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid) +public: + QtLabsAnimationPlugin(QObject *parent = nullptr) : QQmlExtensionPlugin(parent) { } + void registerTypes(const char *uri) override + { + Q_ASSERT(QLatin1String(uri) == QLatin1String("Qt.labs.animation")); + qmlRegisterType(uri, 1, 0, "BoundaryRule"); + qmlRegisterModule(uri, 1, 0); + } +}; +//![class decl] + +QT_END_NAMESPACE + +#include "plugin.moc" diff --git a/src/imports/labsanimation/plugins.qmltypes b/src/imports/labsanimation/plugins.qmltypes new file mode 100644 index 0000000000..065e65ad7a --- /dev/null +++ b/src/imports/labsanimation/plugins.qmltypes @@ -0,0 +1,36 @@ +import QtQuick.tooling 1.2 + +// This file describes the plugin-supplied types contained in the library. +// It is used for QML tooling purposes only. +// +// This file was auto-generated by: +// 'qmlplugindump -nonrelocatable Qt.labs.animation 1.0' + +Module { + dependencies: ["QtQuick 2.0"] + Component { + name: "QQuickBoundaryRule" + prototype: "QObject" + exports: ["Qt.labs.animation/BoundaryRule 1.0"] + exportMetaObjectRevisions: [0] + Enum { + name: "OvershootFilter" + values: { + "None": 0, + "Peak": 1 + } + } + Property { name: "enabled"; type: "bool" } + Property { name: "minimum"; type: "double" } + Property { name: "minimumOvershoot"; type: "double" } + Property { name: "maximum"; type: "double" } + Property { name: "maximumOvershoot"; type: "double" } + Property { name: "overshootScale"; type: "double" } + Property { name: "currentOvershoot"; type: "double"; isReadonly: true } + Property { name: "peakOvershoot"; type: "double"; isReadonly: true } + Property { name: "overshootFilter"; type: "OvershootFilter" } + Property { name: "easing"; type: "QEasingCurve" } + Property { name: "returnDuration"; type: "int" } + Method { name: "returnToBounds"; type: "bool" } + } +} diff --git a/src/imports/labsanimation/qmldir b/src/imports/labsanimation/qmldir new file mode 100644 index 0000000000..b24fc98bfa --- /dev/null +++ b/src/imports/labsanimation/qmldir @@ -0,0 +1,3 @@ +module Qt.labs.animation +plugin labsanimationplugin +classname QtLabsAnimationPlugin diff --git a/src/quick/doc/snippets/qml/boundaryRule.qml b/src/quick/doc/snippets/qml/boundaryRule.qml new file mode 100644 index 0000000000..c010f5de5e --- /dev/null +++ b/src/quick/doc/snippets/qml/boundaryRule.qml @@ -0,0 +1,74 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +//![0] +import QtQuick 2.14 +import Qt.labs.animation 1.0 + +Rectangle { + id: root + width: 170; height: 120 + color: "green" + + DragHandler { + id: dragHandler + yAxis.minimum: -1000 + xAxis.minimum: -1000 + onActiveChanged: if (!active) xbr.returnToBounds(); + } + + BoundaryRule on x { + id: xbr + minimum: -50 + maximum: 100 + minimumOvershoot: 40 + maximumOvershoot: 40 + } +} +//![0] diff --git a/src/quick/util/qquickboundaryrule.cpp b/src/quick/util/qquickboundaryrule.cpp new file mode 100644 index 0000000000..3558c8bfa0 --- /dev/null +++ b/src/quick/util/qquickboundaryrule.cpp @@ -0,0 +1,574 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qquickboundaryrule_p.h" + +#include +#include +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcBR, "qt.quick.boundaryrule") + +class QQuickBoundaryReturnJob; +class QQuickBoundaryRulePrivate : public QObjectPrivate +{ + Q_DECLARE_PUBLIC(QQuickBoundaryRule) +public: + QQuickBoundaryRulePrivate() {} + + QQmlProperty property; + QEasingCurve easing = QEasingCurve(QEasingCurve::OutQuad); + QQuickBoundaryReturnJob *returnAnimationJob = nullptr; + // read-only properties, updated on each write() + qreal targetValue = 0; // after easing was applied + qreal peakOvershoot = 0; + qreal currentOvershoot = 0; + // settable properties + qreal minimum = 0; + qreal maximum = 0; + qreal minimumOvershoot = 0; + qreal maximumOvershoot = 0; + qreal overshootScale = 0.5; + int returnDuration = 100; + QQuickBoundaryRule::OvershootFilter overshootFilter = QQuickBoundaryRule::OvershootFilter::None; + bool enabled = true; + bool finalized = false; + + qreal easedOvershoot(qreal overshootingValue); + void resetOvershoot(); +}; + +class QQuickBoundaryReturnJob : public QAbstractAnimationJob +{ +public: + QQuickBoundaryReturnJob(QQuickBoundaryRulePrivate *br, qreal to) + : QAbstractAnimationJob() + , boundaryRule(br) + , fromValue(br->targetValue) + , toValue(to) {} + + int duration() const override { return boundaryRule->returnDuration; } + + void updateCurrentTime(int) override; + + void updateState(QAbstractAnimationJob::State newState, + QAbstractAnimationJob::State oldState) override; + + QQuickBoundaryRulePrivate *boundaryRule; + qreal fromValue; // snapshot of initial value from which we're returning + qreal toValue; // target property value to which we're returning +}; + +void QQuickBoundaryReturnJob::updateCurrentTime(int t) +{ + // The easing property tells how to behave when the property is being + // externally manipulated beyond the bounds. During returnToBounds() + // we run it in reverse, by reversing time. + qreal progress = (duration() - t) / qreal(duration()); + qreal easingValue = boundaryRule->easing.valueForProgress(progress); + qreal delta = qAbs(fromValue - toValue) * easingValue; + qreal value = (fromValue > toValue ? toValue + delta : toValue - delta); + qCDebug(lcBR) << t << "ms" << qRound(progress * 100) << "% easing" << easingValue << "->" << value; + QQmlPropertyPrivate::write(boundaryRule->property, value, + QQmlPropertyData::BypassInterceptor | QQmlPropertyData::DontRemoveBinding); +} + +void QQuickBoundaryReturnJob::updateState(QAbstractAnimationJob::State newState, QAbstractAnimationJob::State oldState) +{ + Q_UNUSED(oldState) + if (newState == QAbstractAnimationJob::Stopped) { + qCDebug(lcBR) << "return animation done"; + boundaryRule->resetOvershoot(); + boundaryRule->returnAnimationJob = nullptr; + delete this; + } +} + +/*! + \qmltype BoundaryRule + \instantiates QQuickBoundaryRule + \inqmlmodule Qt.labs.animation + \ingroup qtquick-transitions-animations + \ingroup qtquick-interceptors + \brief Defines a restriction on the range of values that can be set on a numeric property. + \since 5.14 + + A BoundaryRule defines the range of values that a particular property is + allowed to have. When an out-of-range value would otherwise be set, + it applies "resistance" via an easing curve. + + For example, the following BoundaryRule prevents DragHandler from dragging + the Rectangle too far: + + \snippet qml/boundaryRule.qml 0 + + Note that a property cannot have more than one assigned BoundaryRule. + + \sa {Animation and Transitions in Qt Quick}, {Qt Quick Examples - Animation#Behaviors}{Behavior example}, {Qt QML} +*/ + +QQuickBoundaryRule::QQuickBoundaryRule(QObject *parent) + : QObject(*(new QQuickBoundaryRulePrivate), parent) + , QQmlPropertyValueInterceptor() +{ +} + +QQuickBoundaryRule::~QQuickBoundaryRule() +{ + Q_D(QQuickBoundaryRule); + // stop any running animation and + // prevent QQuickBoundaryReturnJob::updateState() from accessing QQuickBoundaryRulePrivate + delete d->returnAnimationJob; +} + +/*! + \qmlproperty bool QtQuick::BoundaryRule::enabled + + This property holds whether the rule will be enforced when the tracked + property changes value. + + By default a BoundaryRule is enabled. +*/ +bool QQuickBoundaryRule::enabled() const +{ + Q_D(const QQuickBoundaryRule); + return d->enabled; +} + +void QQuickBoundaryRule::setEnabled(bool enabled) +{ + Q_D(QQuickBoundaryRule); + if (d->enabled == enabled) + return; + d->enabled = enabled; + emit enabledChanged(); +} + +/*! + \qmlproperty qreal QtQuick::BoundaryRule::minimum + + This property holds the smallest unconstrained value that the property is + allowed to have. If the property is set to a smaller value, it will be + constrained by \l easing and \l minimumOvershoot. + + The default is \c 0. +*/ +qreal QQuickBoundaryRule::minimum() const +{ + Q_D(const QQuickBoundaryRule); + return d->minimum; +} + +void QQuickBoundaryRule::setMinimum(qreal minimum) +{ + Q_D(QQuickBoundaryRule); + if (qFuzzyCompare(d->minimum, minimum)) + return; + d->minimum = minimum; + emit minimumChanged(); +} + +/*! + \qmlproperty qreal QtQuick::BoundaryRule::minimumOvershoot + + This property holds the amount that the property is allowed to be + less than \l minimum. Whenever the value is less than \l minimum + and greater than \c {minimum - minimumOvershoot}, it is constrained + by the \l easing curve. When the value attempts to go under + \c {minimum - minimumOvershoots} there is a hard stop. + + The default is \c 0. +*/ +qreal QQuickBoundaryRule::minimumOvershoot() const +{ + Q_D(const QQuickBoundaryRule); + return d->minimumOvershoot; +} + +void QQuickBoundaryRule::setMinimumOvershoot(qreal minimumOvershoot) +{ + Q_D(QQuickBoundaryRule); + if (qFuzzyCompare(d->minimumOvershoot, minimumOvershoot)) + return; + d->minimumOvershoot = minimumOvershoot; + emit minimumOvershootChanged(); +} + +/*! + \qmlproperty qreal QtQuick::BoundaryRule::maximum + + This property holds the largest unconstrained value that the property is + allowed to have. If the property is set to a larger value, it will be + constrained by \l easing and \l maximumOvershoot. + + The default is \c 1. +*/ +qreal QQuickBoundaryRule::maximum() const +{ + Q_D(const QQuickBoundaryRule); + return d->maximum; +} + +void QQuickBoundaryRule::setMaximum(qreal maximum) +{ + Q_D(QQuickBoundaryRule); + if (qFuzzyCompare(d->maximum, maximum)) + return; + d->maximum = maximum; + emit maximumChanged(); +} + +/*! + \qmlproperty qreal QtQuick::BoundaryRule::maximumOvershoot + + This property holds the amount that the property is allowed to be + more than \l maximum. Whenever the value is greater than \l maximum + and less than \c {maximum + maximumOvershoot}, it is constrained + by the \l easing curve. When the value attempts to exceed + \c {maximum + maximumOvershoot} there is a hard stop. + + The default is 0. +*/ +qreal QQuickBoundaryRule::maximumOvershoot() const +{ + Q_D(const QQuickBoundaryRule); + return d->maximumOvershoot; +} + +void QQuickBoundaryRule::setMaximumOvershoot(qreal maximumOvershoot) +{ + Q_D(QQuickBoundaryRule); + if (qFuzzyCompare(d->maximumOvershoot, maximumOvershoot)) + return; + d->maximumOvershoot = maximumOvershoot; + emit maximumOvershootChanged(); +} + +/*! + \qmlproperty qreal QtQuick::BoundaryRule::overshootScale + + This property holds the amount by which the \l easing is scaled during the + overshoot condition. For example if an Item is restricted from moving more + than 100 pixels beyond some limit, and the user (by means of some Input + Handler) is trying to drag it 100 pixels past the limit, if overshootScale + is set to 1, the user will succeed: the only effect of the easing curve is + to change the rate at which the item moves from overshoot 0 to overshoot + 100. But if it is set to 0.5, the BoundaryRule provides resistance such + that when the user tries to move 100 pixels, the Item will only move 50 + pixels; and the easing curve modulates the rate of movement such that it + may move in sync with the user's attempted movement at the beginning, and + then slows down, depending on the shape of the easing curve. + + The default is 0.5. +*/ +qreal QQuickBoundaryRule::overshootScale() const +{ + Q_D(const QQuickBoundaryRule); + return d->overshootScale; +} + +void QQuickBoundaryRule::setOvershootScale(qreal overshootScale) +{ + Q_D(QQuickBoundaryRule); + if (qFuzzyCompare(d->overshootScale, overshootScale)) + return; + d->overshootScale = overshootScale; + emit overshootScaleChanged(); +} + +/*! + \qmlproperty qreal QtQuick::BoundaryRule::currentOvershoot + + This property holds the amount by which the most recently set value of the + intercepted property exceeds \l maximum or is less than \l minimum. + + It is positive if the property value exceeds \l maximum, negative if the + property value is less than \l minimum, or 0 if the property value is + within both boundaries. +*/ +qreal QQuickBoundaryRule::currentOvershoot() const +{ + Q_D(const QQuickBoundaryRule); + return d->currentOvershoot; +} + +/*! + \qmlproperty qreal QtQuick::BoundaryRule::peakOvershoot + + This property holds the most-positive or most-negative value of + \l currentOvershoot that has been seen, until \l returnToBounds() is called. + + This can be useful when the intercepted property value is known to + fluctuate, and you want to find and react to the maximum amount of + overshoot rather than to the fluctuations. + + \sa overshootFilter +*/ +qreal QQuickBoundaryRule::peakOvershoot() const +{ + Q_D(const QQuickBoundaryRule); + return d->peakOvershoot; +} + +/*! + \qmlproperty enum QtQuick::BoundaryRule::overshootFilter + + This property specifies the aggregation function that will be applied to + the intercepted property value. + + If this is set to \c BoundaryRule.None (the default), the intercepted + property will hold a value whose overshoot is limited to \l currentOvershoot. + If this is set to \c BoundaryRule.Peak, the intercepted property will hold + a value whose overshoot is limited to \l peakOvershoot. +*/ +QQuickBoundaryRule::OvershootFilter QQuickBoundaryRule::overshootFilter() const +{ + Q_D(const QQuickBoundaryRule); + return d->overshootFilter; +} + +void QQuickBoundaryRule::setOvershootFilter(OvershootFilter overshootFilter) +{ + Q_D(QQuickBoundaryRule); + if (d->overshootFilter == overshootFilter) + return; + d->overshootFilter = overshootFilter; + emit overshootFilterChanged(); +} + +/*! + \qmlmethod bool QtQuick::BoundaryRule::returnToBounds + + Returns the intercepted property to a value between \l minimum and + \l maximum, such that \l currentOvershoot and \l peakOvershoot are both + zero. This will be animated if \l returnDuration is greater than zero. + + Returns true if the value needed to be adjusted, or false if it was already + within bounds. +*/ +bool QQuickBoundaryRule::returnToBounds() +{ + Q_D(QQuickBoundaryRule); + if (d->returnAnimationJob) { + qCDebug(lcBR) << "animation already in progress"; + return true; + } + if (currentOvershoot() > 0) { + if (d->returnDuration > 0) + d->returnAnimationJob = new QQuickBoundaryReturnJob(d, maximum()); + else + write(maximum()); + } else if (currentOvershoot() < 0) { + if (d->returnDuration > 0) + d->returnAnimationJob = new QQuickBoundaryReturnJob(d, minimum()); + else + write(minimum()); + } else { + return false; + } + if (d->returnAnimationJob) { + qCDebug(lcBR) << "animating from" << d->returnAnimationJob->fromValue << "to" << d->returnAnimationJob->toValue; + d->returnAnimationJob->start(); + } else { + d->resetOvershoot(); + qCDebug(lcBR) << "returned to" << d->property.read(); + } + return true; +} + +/*! + \qmlproperty qreal QtQuick::BoundaryRule::easing + + This property holds the easing curve to be applied in overshoot mode + (whenever the \l minimum or \l maximum constraint is violated, while + the value is still within the respective overshoot range). + + The default easing curve is \l QEasingCurve::OutQuad. +*/ +QEasingCurve QQuickBoundaryRule::easing() const +{ + Q_D(const QQuickBoundaryRule); + return d->easing; +} + +void QQuickBoundaryRule::setEasing(const QEasingCurve &easing) +{ + Q_D(QQuickBoundaryRule); + if (d->easing == easing) + return; + d->easing = easing; + emit easingChanged(); +} + +/*! + \qmlproperty int QtQuick::BoundaryRule::returnDuration + + This property holds the amount of time in milliseconds that + \l returnToBounds() will take to return the target property to the nearest bound. + If it is set to 0, returnToBounds() will set the property immediately + rather than creating an animation job. + + The default is 100 ms. +*/ +int QQuickBoundaryRule::returnDuration() const +{ + Q_D(const QQuickBoundaryRule); + return d->returnDuration; +} + +void QQuickBoundaryRule::setReturnDuration(int duration) +{ + Q_D(QQuickBoundaryRule); + if (d->returnDuration == duration) + return; + d->returnDuration = duration; + emit returnDurationChanged(); +} + +void QQuickBoundaryRule::write(const QVariant &value) +{ + bool conversionOk = false; + qreal rValue = value.toReal(&conversionOk); + if (!conversionOk) { + qWarning() << "BoundaryRule doesn't work with non-numeric values:" << value; + return; + } + Q_D(QQuickBoundaryRule); + bool bypass = !d->enabled || !d->finalized || QQmlEnginePrivate::designerMode(); + if (bypass) { + QQmlPropertyPrivate::write(d->property, value, + QQmlPropertyData::BypassInterceptor | QQmlPropertyData::DontRemoveBinding); + return; + } + + qmlExecuteDeferred(this); + d->targetValue = d->easedOvershoot(rValue); + QQmlPropertyPrivate::write(d->property, d->targetValue, + QQmlPropertyData::BypassInterceptor | QQmlPropertyData::DontRemoveBinding); +} + +void QQuickBoundaryRule::setTarget(const QQmlProperty &property) +{ + Q_D(QQuickBoundaryRule); + d->property = property; + + QQmlEnginePrivate *engPriv = QQmlEnginePrivate::get(qmlEngine(this)); + static int finalizedIdx = -1; + if (finalizedIdx < 0) + finalizedIdx = metaObject()->indexOfSlot("componentFinalized()"); + engPriv->registerFinalizeCallback(this, finalizedIdx); +} + +void QQuickBoundaryRule::componentFinalized() +{ + Q_D(QQuickBoundaryRule); + d->finalized = true; +} + +/*! + \internal + Given that something is trying to set the target property to \a value, + this function applies the easing curve and returns the value that the + property should actually get instead. +*/ +qreal QQuickBoundaryRulePrivate::easedOvershoot(qreal value) +{ + qreal ret = value; + Q_Q(QQuickBoundaryRule); + if (value > maximum) { + qreal overshootWas = currentOvershoot; + currentOvershoot = value - maximum; + if (!qFuzzyCompare(overshootWas, currentOvershoot)) + emit q->currentOvershootChanged(); + overshootWas = peakOvershoot; + peakOvershoot = qMax(currentOvershoot, peakOvershoot); + if (!qFuzzyCompare(overshootWas, peakOvershoot)) + emit q->peakOvershootChanged(); + ret = maximum + maximumOvershoot * easing.valueForProgress( + (overshootFilter == QQuickBoundaryRule::OvershootFilter::Peak ? peakOvershoot : currentOvershoot) + * overshootScale / maximumOvershoot); + qCDebug(lcBR).nospace() << value << " overshoots maximum " << maximum << " by " + << currentOvershoot << " (peak " << peakOvershoot << "): eased to " << ret; + } else if (value < minimum) { + qreal overshootWas = currentOvershoot; + currentOvershoot = value - minimum; + if (!qFuzzyCompare(overshootWas, currentOvershoot)) + emit q->currentOvershootChanged(); + overshootWas = peakOvershoot; + peakOvershoot = qMin(currentOvershoot, peakOvershoot); + if (!qFuzzyCompare(overshootWas, peakOvershoot)) + emit q->peakOvershootChanged(); + ret = minimum - minimumOvershoot * easing.valueForProgress( + -(overshootFilter == QQuickBoundaryRule::OvershootFilter::Peak ? peakOvershoot : currentOvershoot) + * overshootScale / minimumOvershoot); + qCDebug(lcBR).nospace() << value << " overshoots minimum " << minimum << " by " + << currentOvershoot << " (peak " << peakOvershoot << "): eased to " << ret; + } else { + resetOvershoot(); + } + return ret; +} + +/*! + \internal + Resets the currentOvershoot and peakOvershoot + properties to zero. +*/ +void QQuickBoundaryRulePrivate::resetOvershoot() +{ + Q_Q(QQuickBoundaryRule); + if (!qFuzzyCompare(peakOvershoot, 0)) { + peakOvershoot = 0; + emit q->peakOvershootChanged(); + } + if (!qFuzzyCompare(currentOvershoot, 0)) { + currentOvershoot = 0; + emit q->currentOvershootChanged(); + } +} + +QT_END_NAMESPACE + +#include "moc_qquickboundaryrule_p.cpp" diff --git a/src/quick/util/qquickboundaryrule_p.h b/src/quick/util/qquickboundaryrule_p.h new file mode 100644 index 0000000000..3325b675c5 --- /dev/null +++ b/src/quick/util/qquickboundaryrule_p.h @@ -0,0 +1,145 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQUICKBOUNDARYRULE_H +#define QQUICKBOUNDARYRULE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include + +#include +#include + +QT_BEGIN_NAMESPACE + +class QQuickAbstractAnimation; +class QQuickBoundaryRulePrivate; +class Q_QUICK_PRIVATE_EXPORT QQuickBoundaryRule : public QObject, public QQmlPropertyValueInterceptor +{ + Q_OBJECT + Q_DECLARE_PRIVATE(QQuickBoundaryRule) + + Q_INTERFACES(QQmlPropertyValueInterceptor) + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(qreal minimum READ minimum WRITE setMinimum NOTIFY minimumChanged) + Q_PROPERTY(qreal minimumOvershoot READ minimumOvershoot WRITE setMinimumOvershoot NOTIFY minimumOvershootChanged) + Q_PROPERTY(qreal maximum READ maximum WRITE setMaximum NOTIFY maximumChanged) + Q_PROPERTY(qreal maximumOvershoot READ maximumOvershoot WRITE setMaximumOvershoot NOTIFY maximumOvershootChanged) + Q_PROPERTY(qreal overshootScale READ overshootScale WRITE setOvershootScale NOTIFY overshootScaleChanged) + Q_PROPERTY(qreal currentOvershoot READ currentOvershoot NOTIFY currentOvershootChanged) + Q_PROPERTY(qreal peakOvershoot READ peakOvershoot NOTIFY peakOvershootChanged) + Q_PROPERTY(OvershootFilter overshootFilter READ overshootFilter WRITE setOvershootFilter NOTIFY overshootFilterChanged) + Q_PROPERTY(QEasingCurve easing READ easing WRITE setEasing NOTIFY easingChanged) + Q_PROPERTY(int returnDuration READ returnDuration WRITE setReturnDuration NOTIFY returnDurationChanged) + +public: + enum OvershootFilter { + None, + Peak + }; + Q_ENUM(OvershootFilter) + + QQuickBoundaryRule(QObject *parent=nullptr); + ~QQuickBoundaryRule(); + + void setTarget(const QQmlProperty &) override; + void write(const QVariant &value) override; + + bool enabled() const; + void setEnabled(bool enabled); + + qreal minimum() const; + void setMinimum(qreal minimum); + qreal minimumOvershoot() const; + void setMinimumOvershoot(qreal minimum); + + qreal maximum() const; + void setMaximum(qreal maximum); + qreal maximumOvershoot() const; + void setMaximumOvershoot(qreal maximum); + + qreal overshootScale() const; + void setOvershootScale(qreal scale); + + qreal currentOvershoot() const; + qreal peakOvershoot() const; + + OvershootFilter overshootFilter() const; + void setOvershootFilter(OvershootFilter overshootFilter); + + Q_INVOKABLE bool returnToBounds(); + + QEasingCurve easing() const; + void setEasing(const QEasingCurve &easing); + + int returnDuration() const; + void setReturnDuration(int duration); + +Q_SIGNALS: + void enabledChanged(); + void minimumChanged(); + void minimumOvershootChanged(); + void maximumChanged(); + void maximumOvershootChanged(); + void overshootScaleChanged(); + void currentOvershootChanged(); + void peakOvershootChanged(); + void overshootFilterChanged(); + void easingChanged(); + void returnDurationChanged(); + +private Q_SLOTS: + void componentFinalized(); +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickBoundaryRule) + +#endif // QQUICKBOUNDARYRULE_H diff --git a/src/quick/util/util.pri b/src/quick/util/util.pri index c51f082d03..63d995e34c 100644 --- a/src/quick/util/util.pri +++ b/src/quick/util/util.pri @@ -15,6 +15,7 @@ SOURCES += \ $$PWD/qquicktimeline.cpp \ $$PWD/qquickpixmapcache.cpp \ $$PWD/qquickbehavior.cpp \ + $$PWD/qquickboundaryrule.cpp \ $$PWD/qquickfontloader.cpp \ $$PWD/qquickstyledtext.cpp \ $$PWD/qquickimageprovider.cpp \ @@ -50,6 +51,7 @@ HEADERS += \ $$PWD/qquicktimeline_p_p.h \ $$PWD/qquickpixmapcache_p.h \ $$PWD/qquickbehavior_p.h \ + $$PWD/qquickboundaryrule_p.h \ $$PWD/qquickfontloader_p.h \ $$PWD/qquickstyledtext_p.h \ $$PWD/qquickimageprovider.h \ diff --git a/tests/auto/quick/qquickboundaryrule/data/dragHandler.qml b/tests/auto/quick/qquickboundaryrule/data/dragHandler.qml new file mode 100644 index 0000000000..c66fd76ff1 --- /dev/null +++ b/tests/auto/quick/qquickboundaryrule/data/dragHandler.qml @@ -0,0 +1,23 @@ +import QtQuick 2.14 +import Qt.labs.animation 1.0 + +Rectangle { + id: root + width: 240; height: 120 + color: "green" + + DragHandler { + id: dragHandler + yAxis.minimum: -1000 + xAxis.minimum: -1000 + onActiveChanged: if (!active) xbr.returnToBounds(); + } + + BoundaryRule on x { + id: xbr + minimum: -50 + maximum: 100 + minimumOvershoot: 40 + maximumOvershoot: 40 + } +} diff --git a/tests/auto/quick/qquickboundaryrule/qquickboundaryrule.pro b/tests/auto/quick/qquickboundaryrule/qquickboundaryrule.pro new file mode 100644 index 0000000000..ef43f4526a --- /dev/null +++ b/tests/auto/quick/qquickboundaryrule/qquickboundaryrule.pro @@ -0,0 +1,12 @@ +CONFIG += testcase +TARGET = tst_qquickboundaryrule +macx:CONFIG -= app_bundle + +SOURCES += tst_qquickboundaryrule.cpp + +include (../../shared/util.pri) +include (../shared/util.pri) + +TESTDATA = data/* + +QT += core-private gui-private qml-private quick-private testlib diff --git a/tests/auto/quick/qquickboundaryrule/tst_qquickboundaryrule.cpp b/tests/auto/quick/qquickboundaryrule/tst_qquickboundaryrule.cpp new file mode 100644 index 0000000000..44f1c9a2f9 --- /dev/null +++ b/tests/auto/quick/qquickboundaryrule/tst_qquickboundaryrule.cpp @@ -0,0 +1,99 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +#include +#include +#include +#include +#include +#include +#include +#include "../../shared/util.h" +#include "../shared/viewtestutil.h" + +class tst_qquickboundaryrule : public QQmlDataTest +{ + Q_OBJECT +public: + tst_qquickboundaryrule() {} + +private slots: + void init() { qApp->processEvents(); } //work around animation timer bug (QTBUG-22865) + void dragHandler(); +}; + +void tst_qquickboundaryrule::dragHandler() +{ + QQuickView window; + QByteArray errorMessage; + QVERIFY2(QQuickTest::initView(window, testFileUrl("dragHandler.qml"), true, &errorMessage), errorMessage.constData()); + window.show(); + QVERIFY(QTest::qWaitForWindowExposed(&window)); + QQuickItem *target = window.rootObject(); + QVERIFY(target); + QQuickDragHandler *dragHandler = target->findChild(); + QVERIFY(dragHandler); + QQuickBoundaryRule *boundaryRule = target->findChild(); + QVERIFY(boundaryRule); + QSignalSpy overshootChangedSpy(boundaryRule, SIGNAL(currentOvershootChanged())); + + QPoint p1(10, 10); + QTest::mousePress(&window, Qt::LeftButton, Qt::NoModifier, p1); + // unrestricted drag + p1 += QPoint(100, 0); + QTest::mouseMove(&window, p1); + QTRY_VERIFY(dragHandler->active()); + QCOMPARE(target->position().x(), 100); + QCOMPARE(boundaryRule->currentOvershoot(), 0); + QCOMPARE(boundaryRule->peakOvershoot(), 0); + QCOMPARE(overshootChangedSpy.count(), 0); + // restricted drag: halfway into overshoot + p1 += QPoint(20, 0); + QTest::mouseMove(&window, p1); + QCOMPARE(target->position().x(), 117.5); + QCOMPARE(boundaryRule->currentOvershoot(), 20); + QCOMPARE(boundaryRule->peakOvershoot(), 20); + QCOMPARE(overshootChangedSpy.count(), 1); + // restricted drag: maximum overshoot + p1 += QPoint(80, 0); + QTest::mouseMove(&window, p1); + QCOMPARE(target->position().x(), 140); + QCOMPARE(boundaryRule->currentOvershoot(), 100); + QCOMPARE(boundaryRule->peakOvershoot(), 100); + QCOMPARE(overshootChangedSpy.count(), 2); + // release and let it return to bounds + QTest::mouseRelease(&window, Qt::LeftButton, Qt::NoModifier, p1); + QTRY_COMPARE(dragHandler->active(), false); + QTRY_COMPARE(overshootChangedSpy.count(), 3); + QCOMPARE(boundaryRule->currentOvershoot(), 0); + QCOMPARE(boundaryRule->peakOvershoot(), 0); + QCOMPARE(target->position().x(), 100); +} + +QTEST_MAIN(tst_qquickboundaryrule) + +#include "tst_qquickboundaryrule.moc" diff --git a/tests/auto/quick/quick.pro b/tests/auto/quick/quick.pro index 7257a99d2a..b6ffdc0f7e 100644 --- a/tests/auto/quick/quick.pro +++ b/tests/auto/quick/quick.pro @@ -29,6 +29,7 @@ PRIVATETESTS += \ qquickanimations \ qquickapplication \ qquickbehaviors \ + qquickboundaryrule \ qquickfontloader \ qquickfontloader_static \ qquickfontmetrics \ -- cgit v1.2.3