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