1/****************************************************************************
2**
3** Copyright (C) 2019 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the QtQuick module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "qquickboundaryrule_p.h"
41
42#include <qqmlcontext.h>
43#include <qqmlinfo.h>
44#include <private/qqmlproperty_p.h>
45#include <private/qqmlengine_p.h>
46#include <private/qobject_p.h>
47#include <private/qquickanimation_p_p.h>
48#include <QtCore/qloggingcategory.h>
49
50QT_BEGIN_NAMESPACE
51
52Q_LOGGING_CATEGORY(lcBR, "qt.quick.boundaryrule")
53
54class QQuickBoundaryReturnJob;
55class QQuickBoundaryRulePrivate : public QObjectPrivate
56{
57 Q_DECLARE_PUBLIC(QQuickBoundaryRule)
58public:
59 QQuickBoundaryRulePrivate() {}
60
61 QQmlProperty property;
62 QEasingCurve easing = QEasingCurve(QEasingCurve::OutQuad);
63 QQuickBoundaryReturnJob *returnAnimationJob = nullptr;
64 // read-only properties, updated on each write()
65 qreal targetValue = 0; // after easing was applied
66 qreal peakOvershoot = 0;
67 qreal currentOvershoot = 0;
68 // settable properties
69 qreal minimum = 0;
70 qreal maximum = 0;
71 qreal minimumOvershoot = 0;
72 qreal maximumOvershoot = 0;
73 qreal overshootScale = 0.5;
74 int returnDuration = 100;
75 QQuickBoundaryRule::OvershootFilter overshootFilter = QQuickBoundaryRule::OvershootFilter::None;
76 bool enabled = true;
77 bool finalized = false;
78
79 qreal easedOvershoot(qreal overshootingValue);
80 void resetOvershoot();
81};
82
83class QQuickBoundaryReturnJob : public QAbstractAnimationJob
84{
85public:
86 QQuickBoundaryReturnJob(QQuickBoundaryRulePrivate *br, qreal to)
87 : QAbstractAnimationJob()
88 , boundaryRule(br)
89 , fromValue(br->targetValue)
90 , toValue(to) {}
91
92 int duration() const override { return boundaryRule->returnDuration; }
93
94 void updateCurrentTime(int) override;
95
96 void updateState(QAbstractAnimationJob::State newState,
97 QAbstractAnimationJob::State oldState) override;
98
99 QQuickBoundaryRulePrivate *boundaryRule;
100 qreal fromValue; // snapshot of initial value from which we're returning
101 qreal toValue; // target property value to which we're returning
102};
103
104void QQuickBoundaryReturnJob::updateCurrentTime(int t)
105{
106 // The easing property tells how to behave when the property is being
107 // externally manipulated beyond the bounds. During returnToBounds()
108 // we run it in reverse, by reversing time.
109 qreal progress = (duration() - t) / qreal(duration());
110 qreal easingValue = boundaryRule->easing.valueForProgress(progress);
111 qreal delta = qAbs(fromValue - toValue) * easingValue;
112 qreal value = (fromValue > toValue ? toValue + delta : toValue - delta);
113 qCDebug(lcBR) << t << "ms" << qRound(progress * 100) << "% easing" << easingValue << "->" << value;
114 QQmlPropertyPrivate::write(boundaryRule->property, value,
115 QQmlPropertyData::BypassInterceptor | QQmlPropertyData::DontRemoveBinding);
116}
117
118void QQuickBoundaryReturnJob::updateState(QAbstractAnimationJob::State newState, QAbstractAnimationJob::State oldState)
119{
120 Q_UNUSED(oldState)
121 if (newState == QAbstractAnimationJob::Stopped) {
122 qCDebug(lcBR) << "return animation done";
123 boundaryRule->resetOvershoot();
124 boundaryRule->returnAnimationJob = nullptr;
125 delete this;
126 }
127}
128
129/*!
130 \qmltype BoundaryRule
131 \instantiates QQuickBoundaryRule
132 \inqmlmodule Qt.labs.animation
133 \ingroup qtquick-transitions-animations
134 \ingroup qtquick-interceptors
135 \brief Defines a restriction on the range of values that can be set on a numeric property.
136 \since 5.14
137
138 A BoundaryRule defines the range of values that a particular property is
139 allowed to have. When an out-of-range value would otherwise be set,
140 it applies "resistance" via an easing curve.
141
142 For example, the following BoundaryRule prevents DragHandler from dragging
143 the Rectangle too far:
144
145 \snippet qml/boundaryRule.qml 0
146
147 Note that a property cannot have more than one assigned BoundaryRule.
148
149 \sa {Animation and Transitions in Qt Quick}, {Qt Quick Examples - Animation#Behaviors}{Behavior example}, {Qt QML}
150*/
151
152QQuickBoundaryRule::QQuickBoundaryRule(QObject *parent)
153 : QObject(*(new QQuickBoundaryRulePrivate), parent)
154 , QQmlPropertyValueInterceptor()
155{
156}
157
158QQuickBoundaryRule::~QQuickBoundaryRule()
159{
160 Q_D(QQuickBoundaryRule);
161 // stop any running animation and
162 // prevent QQuickBoundaryReturnJob::updateState() from accessing QQuickBoundaryRulePrivate
163 delete d->returnAnimationJob;
164}
165
166/*!
167 \qmlproperty bool QtQuick::BoundaryRule::enabled
168
169 This property holds whether the rule will be enforced when the tracked
170 property changes value.
171
172 By default a BoundaryRule is enabled.
173*/
174bool QQuickBoundaryRule::enabled() const
175{
176 Q_D(const QQuickBoundaryRule);
177 return d->enabled;
178}
179
180void QQuickBoundaryRule::setEnabled(bool enabled)
181{
182 Q_D(QQuickBoundaryRule);
183 if (d->enabled == enabled)
184 return;
185 d->enabled = enabled;
186 emit enabledChanged();
187}
188
189/*!
190 \qmlproperty qreal QtQuick::BoundaryRule::minimum
191
192 This property holds the smallest unconstrained value that the property is
193 allowed to have. If the property is set to a smaller value, it will be
194 constrained by \l easing and \l minimumOvershoot.
195
196 The default is \c 0.
197*/
198qreal QQuickBoundaryRule::minimum() const
199{
200 Q_D(const QQuickBoundaryRule);
201 return d->minimum;
202}
203
204void QQuickBoundaryRule::setMinimum(qreal minimum)
205{
206 Q_D(QQuickBoundaryRule);
207 if (qFuzzyCompare(d->minimum, minimum))
208 return;
209 d->minimum = minimum;
210 emit minimumChanged();
211}
212
213/*!
214 \qmlproperty qreal QtQuick::BoundaryRule::minimumOvershoot
215
216 This property holds the amount that the property is allowed to be
217 less than \l minimum. Whenever the value is less than \l minimum
218 and greater than \c {minimum - minimumOvershoot}, it is constrained
219 by the \l easing curve. When the value attempts to go under
220 \c {minimum - minimumOvershoots} there is a hard stop.
221
222 The default is \c 0.
223*/
224qreal QQuickBoundaryRule::minimumOvershoot() const
225{
226 Q_D(const QQuickBoundaryRule);
227 return d->minimumOvershoot;
228}
229
230void QQuickBoundaryRule::setMinimumOvershoot(qreal minimumOvershoot)
231{
232 Q_D(QQuickBoundaryRule);
233 if (qFuzzyCompare(d->minimumOvershoot, minimumOvershoot))
234 return;
235 d->minimumOvershoot = minimumOvershoot;
236 emit minimumOvershootChanged();
237}
238
239/*!
240 \qmlproperty qreal QtQuick::BoundaryRule::maximum
241
242 This property holds the largest unconstrained value that the property is
243 allowed to have. If the property is set to a larger value, it will be
244 constrained by \l easing and \l maximumOvershoot.
245
246 The default is \c 1.
247*/
248qreal QQuickBoundaryRule::maximum() const
249{
250 Q_D(const QQuickBoundaryRule);
251 return d->maximum;
252}
253
254void QQuickBoundaryRule::setMaximum(qreal maximum)
255{
256 Q_D(QQuickBoundaryRule);
257 if (qFuzzyCompare(d->maximum, maximum))
258 return;
259 d->maximum = maximum;
260 emit maximumChanged();
261}
262
263/*!
264 \qmlproperty qreal QtQuick::BoundaryRule::maximumOvershoot
265
266 This property holds the amount that the property is allowed to be
267 more than \l maximum. Whenever the value is greater than \l maximum
268 and less than \c {maximum + maximumOvershoot}, it is constrained
269 by the \l easing curve. When the value attempts to exceed
270 \c {maximum + maximumOvershoot} there is a hard stop.
271
272 The default is 0.
273*/
274qreal QQuickBoundaryRule::maximumOvershoot() const
275{
276 Q_D(const QQuickBoundaryRule);
277 return d->maximumOvershoot;
278}
279
280void QQuickBoundaryRule::setMaximumOvershoot(qreal maximumOvershoot)
281{
282 Q_D(QQuickBoundaryRule);
283 if (qFuzzyCompare(d->maximumOvershoot, maximumOvershoot))
284 return;
285 d->maximumOvershoot = maximumOvershoot;
286 emit maximumOvershootChanged();
287}
288
289/*!
290 \qmlproperty qreal QtQuick::BoundaryRule::overshootScale
291
292 This property holds the amount by which the \l easing is scaled during the
293 overshoot condition. For example if an Item is restricted from moving more
294 than 100 pixels beyond some limit, and the user (by means of some Input
295 Handler) is trying to drag it 100 pixels past the limit, if overshootScale
296 is set to 1, the user will succeed: the only effect of the easing curve is
297 to change the rate at which the item moves from overshoot 0 to overshoot
298 100. But if it is set to 0.5, the BoundaryRule provides resistance such
299 that when the user tries to move 100 pixels, the Item will only move 50
300 pixels; and the easing curve modulates the rate of movement such that it
301 may move in sync with the user's attempted movement at the beginning, and
302 then slows down, depending on the shape of the easing curve.
303
304 The default is 0.5.
305*/
306qreal QQuickBoundaryRule::overshootScale() const
307{
308 Q_D(const QQuickBoundaryRule);
309 return d->overshootScale;
310}
311
312void QQuickBoundaryRule::setOvershootScale(qreal overshootScale)
313{
314 Q_D(QQuickBoundaryRule);
315 if (qFuzzyCompare(d->overshootScale, overshootScale))
316 return;
317 d->overshootScale = overshootScale;
318 emit overshootScaleChanged();
319}
320
321/*!
322 \qmlproperty qreal QtQuick::BoundaryRule::currentOvershoot
323
324 This property holds the amount by which the most recently set value of the
325 intercepted property exceeds \l maximum or is less than \l minimum.
326
327 It is positive if the property value exceeds \l maximum, negative if the
328 property value is less than \l minimum, or 0 if the property value is
329 within both boundaries.
330*/
331qreal QQuickBoundaryRule::currentOvershoot() const
332{
333 Q_D(const QQuickBoundaryRule);
334 return d->currentOvershoot;
335}
336
337/*!
338 \qmlproperty qreal QtQuick::BoundaryRule::peakOvershoot
339
340 This property holds the most-positive or most-negative value of
341 \l currentOvershoot that has been seen, until \l returnToBounds() is called.
342
343 This can be useful when the intercepted property value is known to
344 fluctuate, and you want to find and react to the maximum amount of
345 overshoot rather than to the fluctuations.
346
347 \sa overshootFilter
348*/
349qreal QQuickBoundaryRule::peakOvershoot() const
350{
351 Q_D(const QQuickBoundaryRule);
352 return d->peakOvershoot;
353}
354
355/*!
356 \qmlproperty enum QtQuick::BoundaryRule::overshootFilter
357
358 This property specifies the aggregation function that will be applied to
359 the intercepted property value.
360
361 If this is set to \c BoundaryRule.None (the default), the intercepted
362 property will hold a value whose overshoot is limited to \l currentOvershoot.
363 If this is set to \c BoundaryRule.Peak, the intercepted property will hold
364 a value whose overshoot is limited to \l peakOvershoot.
365*/
366QQuickBoundaryRule::OvershootFilter QQuickBoundaryRule::overshootFilter() const
367{
368 Q_D(const QQuickBoundaryRule);
369 return d->overshootFilter;
370}
371
372void QQuickBoundaryRule::setOvershootFilter(OvershootFilter overshootFilter)
373{
374 Q_D(QQuickBoundaryRule);
375 if (d->overshootFilter == overshootFilter)
376 return;
377 d->overshootFilter = overshootFilter;
378 emit overshootFilterChanged();
379}
380
381/*!
382 \qmlmethod bool QtQuick::BoundaryRule::returnToBounds
383
384 Returns the intercepted property to a value between \l minimum and
385 \l maximum, such that \l currentOvershoot and \l peakOvershoot are both
386 zero. This will be animated if \l returnDuration is greater than zero.
387
388 Returns true if the value needed to be adjusted, or false if it was already
389 within bounds.
390*/
391bool QQuickBoundaryRule::returnToBounds()
392{
393 Q_D(QQuickBoundaryRule);
394 if (d->returnAnimationJob) {
395 qCDebug(lcBR) << "animation already in progress";
396 return true;
397 }
398 if (currentOvershoot() > 0) {
399 if (d->returnDuration > 0)
400 d->returnAnimationJob = new QQuickBoundaryReturnJob(d, maximum());
401 else
402 write(maximum());
403 } else if (currentOvershoot() < 0) {
404 if (d->returnDuration > 0)
405 d->returnAnimationJob = new QQuickBoundaryReturnJob(d, minimum());
406 else
407 write(minimum());
408 } else {
409 return false;
410 }
411 if (d->returnAnimationJob) {
412 qCDebug(lcBR) << "animating from" << d->returnAnimationJob->fromValue << "to" << d->returnAnimationJob->toValue;
413 d->returnAnimationJob->start();
414 } else {
415 d->resetOvershoot();
416 qCDebug(lcBR) << "returned to" << d->property.read();
417 }
418 return true;
419}
420
421/*!
422 \qmlproperty qreal QtQuick::BoundaryRule::easing
423
424 This property holds the easing curve to be applied in overshoot mode
425 (whenever the \l minimum or \l maximum constraint is violated, while
426 the value is still within the respective overshoot range).
427
428 The default easing curve is \l QEasingCurve::OutQuad.
429*/
430QEasingCurve QQuickBoundaryRule::easing() const
431{
432 Q_D(const QQuickBoundaryRule);
433 return d->easing;
434}
435
436void QQuickBoundaryRule::setEasing(const QEasingCurve &easing)
437{
438 Q_D(QQuickBoundaryRule);
439 if (d->easing == easing)
440 return;
441 d->easing = easing;
442 emit easingChanged();
443}
444
445/*!
446 \qmlproperty int QtQuick::BoundaryRule::returnDuration
447
448 This property holds the amount of time in milliseconds that
449 \l returnToBounds() will take to return the target property to the nearest bound.
450 If it is set to 0, returnToBounds() will set the property immediately
451 rather than creating an animation job.
452
453 The default is 100 ms.
454*/
455int QQuickBoundaryRule::returnDuration() const
456{
457 Q_D(const QQuickBoundaryRule);
458 return d->returnDuration;
459}
460
461void QQuickBoundaryRule::setReturnDuration(int duration)
462{
463 Q_D(QQuickBoundaryRule);
464 if (d->returnDuration == duration)
465 return;
466 d->returnDuration = duration;
467 emit returnDurationChanged();
468}
469
470void QQuickBoundaryRule::write(const QVariant &value)
471{
472 bool conversionOk = false;
473 qreal rValue = value.toReal(&conversionOk);
474 if (!conversionOk) {
475 qWarning() << "BoundaryRule doesn't work with non-numeric values:" << value;
476 return;
477 }
478 Q_D(QQuickBoundaryRule);
479 bool bypass = !d->enabled || !d->finalized || QQmlEnginePrivate::designerMode();
480 if (bypass) {
481 QQmlPropertyPrivate::write(d->property, value,
482 QQmlPropertyData::BypassInterceptor | QQmlPropertyData::DontRemoveBinding);
483 return;
484 }
485
486 qmlExecuteDeferred(this);
487 d->targetValue = d->easedOvershoot(rValue);
488 QQmlPropertyPrivate::write(d->property, d->targetValue,
489 QQmlPropertyData::BypassInterceptor | QQmlPropertyData::DontRemoveBinding);
490}
491
492void QQuickBoundaryRule::setTarget(const QQmlProperty &property)
493{
494 Q_D(QQuickBoundaryRule);
495 d->property = property;
496
497 QQmlEnginePrivate *engPriv = QQmlEnginePrivate::get(qmlEngine(this));
498 static int finalizedIdx = -1;
499 if (finalizedIdx < 0)
500 finalizedIdx = metaObject()->indexOfSlot("componentFinalized()");
501 engPriv->registerFinalizeCallback(this, finalizedIdx);
502}
503
504void QQuickBoundaryRule::componentFinalized()
505{
506 Q_D(QQuickBoundaryRule);
507 d->finalized = true;
508}
509
510/*!
511 \internal
512 Given that something is trying to set the target property to \a value,
513 this function applies the easing curve and returns the value that the
514 property should actually get instead.
515*/
516qreal QQuickBoundaryRulePrivate::easedOvershoot(qreal value)
517{
518 qreal ret = value;
519 Q_Q(QQuickBoundaryRule);
520 if (value > maximum) {
521 qreal overshootWas = currentOvershoot;
522 currentOvershoot = value - maximum;
523 if (!qFuzzyCompare(overshootWas, currentOvershoot))
524 emit q->currentOvershootChanged();
525 overshootWas = peakOvershoot;
526 peakOvershoot = qMax(currentOvershoot, peakOvershoot);
527 if (!qFuzzyCompare(overshootWas, peakOvershoot))
528 emit q->peakOvershootChanged();
529 ret = maximum + maximumOvershoot * easing.valueForProgress(
530 (overshootFilter == QQuickBoundaryRule::OvershootFilter::Peak ? peakOvershoot : currentOvershoot)
531 * overshootScale / maximumOvershoot);
532 qCDebug(lcBR).nospace() << value << " overshoots maximum " << maximum << " by "
533 << currentOvershoot << " (peak " << peakOvershoot << "): eased to " << ret;
534 } else if (value < minimum) {
535 qreal overshootWas = currentOvershoot;
536 currentOvershoot = value - minimum;
537 if (!qFuzzyCompare(overshootWas, currentOvershoot))
538 emit q->currentOvershootChanged();
539 overshootWas = peakOvershoot;
540 peakOvershoot = qMin(currentOvershoot, peakOvershoot);
541 if (!qFuzzyCompare(overshootWas, peakOvershoot))
542 emit q->peakOvershootChanged();
543 ret = minimum - minimumOvershoot * easing.valueForProgress(
544 -(overshootFilter == QQuickBoundaryRule::OvershootFilter::Peak ? peakOvershoot : currentOvershoot)
545 * overshootScale / minimumOvershoot);
546 qCDebug(lcBR).nospace() << value << " overshoots minimum " << minimum << " by "
547 << currentOvershoot << " (peak " << peakOvershoot << "): eased to " << ret;
548 } else {
549 resetOvershoot();
550 }
551 return ret;
552}
553
554/*!
555 \internal
556 Resets the currentOvershoot and peakOvershoot
557 properties to zero.
558*/
559void QQuickBoundaryRulePrivate::resetOvershoot()
560{
561 Q_Q(QQuickBoundaryRule);
562 if (!qFuzzyCompare(peakOvershoot, 0)) {
563 peakOvershoot = 0;
564 emit q->peakOvershootChanged();
565 }
566 if (!qFuzzyCompare(currentOvershoot, 0)) {
567 currentOvershoot = 0;
568 emit q->currentOvershootChanged();
569 }
570}
571
572QT_END_NAMESPACE
573
574#include "moc_qquickboundaryrule_p.cpp"
575