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

source code of qtdeclarative/src/labs/animation/qquickboundaryrule.cpp