1// Copyright (C) 2020 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 "qquickwheelhandler_p.h"
5#include "qquickwheelhandler_p_p.h"
6#include <QtQuick/private/qquickitem_p.h>
7#include <QLoggingCategory>
8#include <QtMath>
9
10QT_BEGIN_NAMESPACE
11
12Q_LOGGING_CATEGORY(lcWheelHandler, "qt.quick.handler.wheel")
13
14/*!
15 \qmltype WheelHandler
16 \instantiates QQuickWheelHandler
17 \inherits SinglePointHandler
18 \inqmlmodule QtQuick
19 \ingroup qtquick-input-handlers
20 \brief Handler for the mouse wheel.
21
22 WheelHandler is a handler that is used to interactively manipulate some
23 numeric property of an Item as the user rotates the mouse wheel. Like other
24 Input Handlers, by default it manipulates its \l {PointerHandler::target}
25 {target}. Declare \l property to control which target property will be
26 manipulated:
27
28 \snippet pointerHandlers/wheelHandler.qml 0
29
30 \l BoundaryRule is quite useful in combination with WheelHandler (as well
31 as with other Input Handlers) to declare the allowed range of values that
32 the target property can have. For example it is possible to implement
33 scrolling using a combination of WheelHandler and \l DragHandler to
34 manipulate the scrollable Item's \l{QQuickItem::y}{y} property when the
35 user rotates the wheel or drags the item on a touchscreen, and
36 \l BoundaryRule to limit the range of motion from the top to the bottom:
37
38 \snippet pointerHandlers/handlerFlick.qml 0
39
40 Alternatively, if \l property is not set or \l target is null,
41 WheelHandler will not automatically manipulate anything; but the
42 \l rotation property can be used in a binding to manipulate another
43 property, or you can implement \c onWheel and handle the wheel event
44 directly.
45
46 WheelHandler handles only a rotating mouse wheel by default; this
47 can be changed by setting acceptedDevices.
48
49 \sa MouseArea, Flickable, {Qt Quick Examples - Pointer Handlers}
50*/
51
52QQuickWheelHandler::QQuickWheelHandler(QQuickItem *parent)
53 : QQuickSinglePointHandler(*(new QQuickWheelHandlerPrivate), parent)
54{
55 setAcceptedDevices(QInputDevice::DeviceType::Mouse);
56}
57
58/*!
59 \qmlproperty enumeration QtQuick::WheelHandler::orientation
60
61 Which wheel to react to. The default is \c Qt.Vertical.
62
63 Not every mouse has a \c Horizontal wheel; sometimes it is emulated by
64 tilting the wheel sideways. A touchpad can usually generate both vertical
65 and horizontal wheel events.
66*/
67Qt::Orientation QQuickWheelHandler::orientation() const
68{
69 Q_D(const QQuickWheelHandler);
70 return d->orientation;
71}
72
73void QQuickWheelHandler::setOrientation(Qt::Orientation orientation)
74{
75 Q_D(QQuickWheelHandler);
76 if (d->orientation == orientation)
77 return;
78
79 d->orientation = orientation;
80 emit orientationChanged();
81}
82
83/*!
84 \qmlproperty bool QtQuick::WheelHandler::invertible
85
86 Whether or not to reverse the direction of property change if
87 \l QWheelEvent::inverted is \c true. The default is \c true.
88
89 If the operating system has a "natural scrolling" setting that causes
90 scrolling to be in the same direction as the finger movement, then if this
91 property is set to \c true, and WheelHandler is directly setting a property
92 on \l target, the direction of movement will correspond to the system setting.
93 If this property is set to \c false, it will invert the \l rotation so that
94 the direction of motion is always the same as the direction of finger movement.
95*/
96bool QQuickWheelHandler::isInvertible() const
97{
98 Q_D(const QQuickWheelHandler);
99 return d->invertible;
100}
101
102void QQuickWheelHandler::setInvertible(bool invertible)
103{
104 Q_D(QQuickWheelHandler);
105 if (d->invertible == invertible)
106 return;
107
108 d->invertible = invertible;
109 emit invertibleChanged();
110}
111
112/*!
113 \qmlproperty real QtQuick::WheelHandler::activeTimeout
114
115 The amount of time in seconds after which the \l active property will
116 revert to \c false if no more wheel events are received. The default is
117 \c 0.1 (100 ms).
118
119 When WheelHandler handles events that contain
120 \l {Qt::ScrollPhase}{scroll phase} information, such as events from some
121 touchpads, the \l active property will become \c false as soon as an event
122 with phase \l Qt::ScrollEnd is received; in that case the timeout is not
123 necessary. But a conventional mouse with a wheel does not provide a scroll
124 phase: the mouse cannot detect when the user has decided to stop
125 scrolling, so the \l active property transitions to \c false after this
126 much time has elapsed.
127
128 \sa QWheelEvent::phase()
129*/
130qreal QQuickWheelHandler::activeTimeout() const
131{
132 Q_D(const QQuickWheelHandler);
133 return d->activeTimeout;
134}
135
136void QQuickWheelHandler::setActiveTimeout(qreal timeout)
137{
138 Q_D(QQuickWheelHandler);
139 if (qFuzzyCompare(p1: d->activeTimeout, p2: timeout))
140 return;
141
142 if (timeout < 0) {
143 qWarning(msg: "activeTimeout must be positive");
144 return;
145 }
146
147 d->activeTimeout = timeout;
148 emit activeTimeoutChanged();
149}
150
151/*!
152 \qmlproperty real QtQuick::WheelHandler::rotation
153
154 The angle through which the mouse wheel has been rotated since the last
155 time this property was set, in wheel degrees.
156
157 A positive value indicates that the wheel was rotated up/right;
158 a negative value indicates that the wheel was rotated down/left.
159
160 A basic mouse click-wheel works in steps of 15 degrees.
161
162 The default is \c 0 at startup. It can be programmatically set to any value
163 at any time. The value will be adjusted from there as the user rotates the
164 mouse wheel.
165
166 \sa orientation
167*/
168qreal QQuickWheelHandler::rotation() const
169{
170 Q_D(const QQuickWheelHandler);
171 return d->rotation * d->rotationScale;
172}
173
174void QQuickWheelHandler::setRotation(qreal rotation)
175{
176 Q_D(QQuickWheelHandler);
177 if (qFuzzyCompare(p1: d->rotation, p2: rotation / d->rotationScale))
178 return;
179
180 d->rotation = rotation / d->rotationScale;
181 emit rotationChanged();
182}
183
184/*!
185 \qmlproperty real QtQuick::WheelHandler::rotationScale
186
187 The scaling to be applied to the \l rotation property, and to the
188 \l property on the \l target item, if any. The default is 1, such that
189 \l rotation will be in units of degrees of rotation. It can be set to a
190 negative number to invert the effect of the direction of mouse wheel
191 rotation.
192*/
193qreal QQuickWheelHandler::rotationScale() const
194{
195 Q_D(const QQuickWheelHandler);
196 return d->rotationScale;
197}
198
199void QQuickWheelHandler::setRotationScale(qreal rotationScale)
200{
201 Q_D(QQuickWheelHandler);
202 if (qFuzzyCompare(p1: d->rotationScale, p2: rotationScale))
203 return;
204 if (qFuzzyIsNull(d: rotationScale)) {
205 qWarning(msg: "rotationScale cannot be set to zero");
206 return;
207 }
208
209 d->rotationScale = rotationScale;
210 emit rotationScaleChanged();
211}
212
213/*!
214 \qmlproperty string QtQuick::WheelHandler::property
215
216 The property to be modified on the \l target when the mouse wheel is rotated.
217
218 The default is no property (empty string). When no target property is being
219 automatically modified, you can use bindings to react to mouse wheel
220 rotation in arbitrary ways.
221
222 You can use the mouse wheel to adjust any numeric property. For example if
223 \c property is set to \c x, the \l target will move horizontally as the
224 wheel is rotated. The following properties have special behavior:
225
226 \value scale
227 \l{QQuickItem::scale}{scale} will be modified in a non-linear fashion
228 as described under \l targetScaleMultiplier. If
229 \l targetTransformAroundCursor is \c true, the \l{QQuickItem::x}{x} and
230 \l{QQuickItem::y}{y} properties will be simultaneously adjusted so that
231 the user will effectively zoom into or out of the point under the mouse
232 cursor.
233 \value rotation
234 \l{QQuickItem::rotation}{rotation} will be set to \l rotation. If
235 \l targetTransformAroundCursor is \c true, the l{QQuickItem::x}{x} and
236 \l{QQuickItem::y}{y} properties will be simultaneously adjusted so
237 that the user will effectively rotate the item around the point under
238 the mouse cursor.
239
240 The adjustment of the given target property is always scaled by \l rotationScale.
241*/
242QString QQuickWheelHandler::property() const
243{
244 Q_D(const QQuickWheelHandler);
245 return d->propertyName;
246}
247
248void QQuickWheelHandler::setProperty(const QString &propertyName)
249{
250 Q_D(QQuickWheelHandler);
251 if (d->propertyName == propertyName)
252 return;
253
254 d->propertyName = propertyName;
255 d->metaPropertyDirty = true;
256 emit propertyChanged();
257}
258
259/*!
260 \qmlproperty real QtQuick::WheelHandler::targetScaleMultiplier
261
262 The amount by which the \l target \l{QQuickItem::scale}{scale} is to be
263 multiplied whenever the \l rotation changes by 15 degrees. This
264 is relevant only when \l property is \c "scale".
265
266 The \c scale will be multiplied by
267 \c targetScaleMultiplier \sup {angleDelta * rotationScale / 15}.
268 The default is \c 2 \sup {1/3}, which means that if \l rotationScale is left
269 at its default value, and the mouse wheel is rotated by one "click"
270 (15 degrees), the \l target will be scaled by approximately 1.25; after
271 three "clicks" its size will be doubled or halved, depending on the
272 direction that the wheel is rotated. If you want to make it double or halve
273 with every 2 clicks of the wheel, set this to \c 2 \sup {1/2} (1.4142).
274 If you want to make it scale the opposite way as the wheel is rotated,
275 set \c rotationScale to a negative value.
276*/
277qreal QQuickWheelHandler::targetScaleMultiplier() const
278{
279 Q_D(const QQuickWheelHandler);
280 return d->targetScaleMultiplier;
281}
282
283void QQuickWheelHandler::setTargetScaleMultiplier(qreal targetScaleMultiplier)
284{
285 Q_D(QQuickWheelHandler);
286 if (qFuzzyCompare(p1: d->targetScaleMultiplier, p2: targetScaleMultiplier))
287 return;
288
289 d->targetScaleMultiplier = targetScaleMultiplier;
290 emit targetScaleMultiplierChanged();
291}
292
293/*!
294 \qmlproperty bool QtQuick::WheelHandler::targetTransformAroundCursor
295
296 Whether the \l target should automatically be repositioned in such a way
297 that it is transformed around the mouse cursor position while the
298 \l property is adjusted. The default is \c true.
299
300 If \l property is set to \c "rotation" and \l targetTransformAroundCursor
301 is \c true, then as the wheel is rotated, the \l target item will rotate in
302 place around the mouse cursor position. If \c targetTransformAroundCursor
303 is \c false, it will rotate around its
304 \l{QQuickItem::transformOrigin}{transformOrigin} instead.
305*/
306bool QQuickWheelHandler::isTargetTransformAroundCursor() const
307{
308 Q_D(const QQuickWheelHandler);
309 return d->targetTransformAroundCursor;
310}
311
312void QQuickWheelHandler::setTargetTransformAroundCursor(bool ttac)
313{
314 Q_D(QQuickWheelHandler);
315 if (d->targetTransformAroundCursor == ttac)
316 return;
317
318 d->targetTransformAroundCursor = ttac;
319 emit targetTransformAroundCursorChanged();
320}
321
322/*!
323 \qmlproperty bool QtQuick::WheelHandler::blocking
324 \since 6.3
325
326 Whether this handler prevents other items or handlers behind it from
327 handling the same wheel event. This property is \c true by default.
328*/
329bool QQuickWheelHandler::isBlocking() const
330{
331 Q_D(const QQuickWheelHandler);
332 return d->blocking;
333}
334
335void QQuickWheelHandler::setBlocking(bool blocking)
336{
337 Q_D(QQuickWheelHandler);
338 if (d->blocking == blocking)
339 return;
340
341 d->blocking = blocking;
342 emit blockingChanged();
343}
344
345bool QQuickWheelHandler::wantsPointerEvent(QPointerEvent *event)
346{
347 if (!event)
348 return false;
349 if (event->type() != QEvent::Wheel)
350 return false;
351 QWheelEvent *we = static_cast<QWheelEvent *>(event);
352 if (!acceptedDevices().testFlag(flag: QPointingDevice::DeviceType::TouchPad)
353 && we->source() != Qt::MouseEventNotSynthesized)
354 return false;
355 if (!active()) {
356 switch (orientation()) {
357 case Qt::Horizontal:
358 if (!(we->angleDelta().x()) && !(we->pixelDelta().x()))
359 return false;
360 break;
361 case Qt::Vertical:
362 if (!(we->angleDelta().y()) && !(we->pixelDelta().y()))
363 return false;
364 break;
365 }
366 }
367 auto &point = event->point(i: 0);
368 if (QQuickPointerDeviceHandler::wantsPointerEvent(event) && wantsEventPoint(event, point) && parentContains(point)) {
369 setPointId(point.id());
370 return true;
371 }
372 return false;
373}
374
375void QQuickWheelHandler::handleEventPoint(QPointerEvent *ev, QEventPoint &point)
376{
377 Q_D(QQuickWheelHandler);
378 QQuickSinglePointHandler::handleEventPoint(event: ev, point);
379
380 if (ev->type() != QEvent::Wheel)
381 return;
382 const QWheelEvent *event = static_cast<const QWheelEvent *>(ev);
383 setActive(true); // ScrollEnd will not happen unless it was already active (see setActive(false) below)
384 if (d->blocking)
385 point.setAccepted();
386 qreal inversion = !d->invertible && event->isInverted() ? -1 : 1;
387 qreal angleDelta = inversion * qreal(orientation() == Qt::Horizontal ? event->angleDelta().x() :
388 event->angleDelta().y()) / 8;
389 d->rotation += angleDelta;
390 emit rotationChanged();
391
392 d->wheelEvent.reset(event);
393 emit wheel(event: &d->wheelEvent);
394 if (!d->propertyName.isEmpty() && target()) {
395 QQuickItem *t = target();
396 // writing target()'s property is done via QMetaProperty::write() so that any registered interceptors can react.
397 if (d->propertyName == QLatin1String("scale")) {
398 qreal multiplier = qPow(x: d->targetScaleMultiplier, y: angleDelta * d->rotationScale / 15); // wheel "clicks"
399 const QPointF centroidParentPos = t->parentItem()->mapFromScene(point: point.scenePosition());
400 const QPointF positionWas = t->position();
401 const qreal scaleWas = t->scale();
402 const qreal activePropertyValue = scaleWas * multiplier;
403 qCDebug(lcWheelHandler) << objectName() << "angle delta" << event->angleDelta() << "pixel delta" << event->pixelDelta()
404 << "@" << point.position() << "in parent" << centroidParentPos
405 << "in scene" << point.scenePosition()
406 << "multiplier" << multiplier << "scale" << scaleWas
407 << "->" << activePropertyValue;
408 d->targetMetaProperty().write(obj: t, value: activePropertyValue);
409 if (d->targetTransformAroundCursor) {
410 // If an interceptor intervened, scale may now be different than we asked for. Adjust accordingly.
411 multiplier = t->scale() / scaleWas;
412 const QPointF adjPos = QQuickItemPrivate::get(item: t)->adjustedPosForTransform(
413 centroid: centroidParentPos, startPos: positionWas, activeTranslatation: QVector2D(), startScale: scaleWas, activeScale: multiplier, startRotation: t->rotation(), activeRotation: 0);
414 qCDebug(lcWheelHandler) << "adjusting item pos" << adjPos << "in scene" << t->parentItem()->mapToScene(point: adjPos);
415 t->setPosition(adjPos);
416 }
417 } else if (d->propertyName == QLatin1String("rotation")) {
418 const QPointF positionWas = t->position();
419 const qreal rotationWas = t->rotation();
420 const qreal activePropertyValue = rotationWas + angleDelta * d->rotationScale;
421 const QPointF centroidParentPos = t->parentItem()->mapFromScene(point: point.scenePosition());
422 qCDebug(lcWheelHandler) << objectName() << "angle delta" << event->angleDelta() << "pixel delta" << event->pixelDelta()
423 << "@" << point.position() << "in parent" << centroidParentPos
424 << "in scene" << point.scenePosition() << "rotation" << t->rotation()
425 << "->" << activePropertyValue;
426 d->targetMetaProperty().write(obj: t, value: activePropertyValue);
427 if (d->targetTransformAroundCursor) {
428 // If an interceptor intervened, rotation may now be different than we asked for. Adjust accordingly.
429 const QPointF adjPos = QQuickItemPrivate::get(item: t)->adjustedPosForTransform(
430 centroid: centroidParentPos, startPos: positionWas, activeTranslatation: QVector2D(),
431 startScale: t->scale(), activeScale: 1, startRotation: rotationWas, activeRotation: t->rotation() - rotationWas);
432 qCDebug(lcWheelHandler) << "adjusting item pos" << adjPos << "in scene" << t->parentItem()->mapToScene(point: adjPos);
433 t->setPosition(adjPos);
434 }
435 } else {
436 qCDebug(lcWheelHandler) << objectName() << "angle delta" << event->angleDelta() << "scaled" << angleDelta
437 << "total" << d->rotation << "pixel delta" << event->pixelDelta()
438 << "@" << point.position() << "in scene" << point.scenePosition() << "rotation" << t->rotation();
439 qreal delta = 0;
440 if (event->hasPixelDelta()) {
441 delta = inversion * d->rotationScale * qreal(orientation() == Qt::Horizontal ? event->pixelDelta().x() : event->pixelDelta().y());
442 qCDebug(lcWheelHandler) << "changing target" << d->propertyName << "by pixel delta" << delta << "from" << event;
443 } else {
444 delta = angleDelta * d->rotationScale;
445 qCDebug(lcWheelHandler) << "changing target" << d->propertyName << "by scaled angle delta" << delta << "from" << event;
446 }
447 bool ok = false;
448 qreal value = d->targetMetaProperty().read(obj: t).toReal(ok: &ok);
449 if (ok)
450 d->targetMetaProperty().write(obj: t, value: value + qreal(delta));
451 else
452 qWarning() << "failed to read property" << d->propertyName << "of" << t;
453 }
454 }
455 switch (event->phase()) {
456 case Qt::ScrollEnd:
457 qCDebug(lcWheelHandler) << objectName() << "deactivating due to ScrollEnd phase";
458 setActive(false);
459 break;
460 case Qt::NoScrollPhase:
461 d->deactivationTimer.start(msec: qRound(d: d->activeTimeout * 1000), obj: this);
462 break;
463 case Qt::ScrollBegin:
464 case Qt::ScrollUpdate:
465 case Qt::ScrollMomentum:
466 break;
467 }
468}
469
470void QQuickWheelHandler::onTargetChanged(QQuickItem *oldTarget)
471{
472 Q_UNUSED(oldTarget);
473 Q_D(QQuickWheelHandler);
474 d->metaPropertyDirty = true;
475}
476
477void QQuickWheelHandler::onActiveChanged()
478{
479 Q_D(QQuickWheelHandler);
480 if (!active())
481 d->deactivationTimer.stop();
482}
483
484void QQuickWheelHandler::timerEvent(QTimerEvent *event)
485{
486 Q_D(const QQuickWheelHandler);
487 if (event->timerId() == d->deactivationTimer.timerId()) {
488 qCDebug(lcWheelHandler) << objectName() << "deactivating due to timeout";
489 setActive(false);
490 }
491}
492
493/*!
494 \qmlsignal QtQuick::WheelHandler::wheel(WheelEvent event)
495
496 This signal is emitted every time this handler receives an \a event
497 of type \l QWheelEvent: that is, every time the wheel is moved or the
498 scrolling gesture is updated.
499*/
500
501QQuickWheelHandlerPrivate::QQuickWheelHandlerPrivate()
502 : QQuickSinglePointHandlerPrivate()
503{
504}
505
506QMetaProperty &QQuickWheelHandlerPrivate::targetMetaProperty() const
507{
508 Q_Q(const QQuickWheelHandler);
509 if (metaPropertyDirty && q->target()) {
510 if (!propertyName.isEmpty()) {
511 const QMetaObject *targetMeta = q->target()->metaObject();
512 metaProperty = targetMeta->property(
513 index: targetMeta->indexOfProperty(name: propertyName.toLocal8Bit().constData()));
514 }
515 metaPropertyDirty = false;
516 }
517 return metaProperty;
518}
519
520/*!
521 \qmlproperty flags WheelHandler::acceptedDevices
522
523 The types of pointing devices that can activate this handler.
524
525 By default, this property is set to
526 \l{QInputDevice::DeviceType}{PointerDevice.Mouse}, so as to react only to
527 events from an actual mouse wheel.
528
529 WheelHandler can be made to respond to both mouse wheel and touchpad
530 scrolling by setting acceptedDevices to
531 \c{PointerDevice.Mouse | PointerDevice.TouchPad}.
532
533 \note Some non-mouse hardware (such as a touch-sensitive Wacom tablet, or a
534 Linux laptop touchpad) generates real wheel events from gestures.
535 WheelHandler will respond to those events as wheel events even if
536 \c acceptedDevices remains set to its default value.
537*/
538
539QT_END_NAMESPACE
540
541#include "moc_qquickwheelhandler_p.cpp"
542

source code of qtdeclarative/src/quick/handlers/qquickwheelhandler.cpp