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

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