Revision control
Copy as Markdown
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qquickwheelhandler_p.h"
#include "qquickwheelhandler_p_p.h"
#include <QtQuick/private/qquickitem_p.h>
#include <QLoggingCategory>
#include <QtMath>
QT_BEGIN_NAMESPACE
Q_STATIC_LOGGING_CATEGORY(lcWheelHandler, "qt.quick.handler.wheel")
/*!
\qmltype WheelHandler
\nativetype QQuickWheelHandler
\inherits SinglePointHandler
\inqmlmodule QtQuick
\ingroup qtquick-input-handlers
\brief Handler for the mouse wheel.
WheelHandler is a handler that is used to interactively manipulate some
numeric property of an Item as the user rotates the mouse wheel. Like other
Input Handlers, by default it manipulates its \l {PointerHandler::target}
{target}. Declare \l property to control which target property will be
manipulated:
\snippet pointerHandlers/wheelHandler.qml 0
\l BoundaryRule is quite useful in combination with WheelHandler (as well
as with other Input Handlers) to declare the allowed range of values that
the target property can have. For example it is possible to implement
scrolling using a combination of WheelHandler and \l DragHandler to
manipulate the scrollable Item's \l{QQuickItem::y}{y} property when the
user rotates the wheel or drags the item on a touchscreen, and
\l BoundaryRule to limit the range of motion from the top to the bottom:
\snippet pointerHandlers/handlerFlick.qml 0
Alternatively, if \l property is not set or \l target is null,
WheelHandler will not automatically manipulate anything; but the
\l rotation property can be used in a binding to manipulate another
property, or you can implement \c onWheel and handle the wheel event
directly.
By default, WheelHandler handles \l {QWheelEvent}{wheel events}
from a rotating mouse wheel, or virtual "wheel" events from a touchpad.
This can be changed by setting \l acceptedDevices.
\sa MouseArea, Flickable, {Qt Quick Examples - Pointer Handlers}
*/
QQuickWheelHandler::QQuickWheelHandler(QQuickItem *parent)
: QQuickSinglePointHandler(*(new QQuickWheelHandlerPrivate), parent)
{
setAcceptedDevices(QInputDevice::DeviceType::Mouse | QInputDevice::DeviceType::TouchPad);
}
/*!
\qmlproperty enumeration QtQuick::WheelHandler::orientation
Which wheel to react to. The default is \c Qt.Vertical.
Not every mouse has a \c Horizontal wheel; sometimes it is emulated by
tilting the wheel sideways. A touchpad can usually generate both vertical
and horizontal wheel events.
*/
Qt::Orientation QQuickWheelHandler::orientation() const
{
Q_D(const QQuickWheelHandler);
return d->orientation;
}
void QQuickWheelHandler::setOrientation(Qt::Orientation orientation)
{
Q_D(QQuickWheelHandler);
if (d->orientation == orientation)
return;
d->orientation = orientation;
emit orientationChanged();
}
/*!
\qmlproperty bool QtQuick::WheelHandler::invertible
Whether or not to reverse the direction of property change if
\l QWheelEvent::inverted is \c true. The default is \c true.
If the operating system has a "natural scrolling" setting that causes
scrolling to be in the same direction as the finger movement, then if this
property is set to \c true, and WheelHandler is directly setting a property
on \l target, the direction of movement will correspond to the system setting.
If this property is set to \c false, it will invert the \l rotation so that
the direction of motion is always the same as the direction of finger movement.
*/
bool QQuickWheelHandler::isInvertible() const
{
Q_D(const QQuickWheelHandler);
return d->invertible;
}
void QQuickWheelHandler::setInvertible(bool invertible)
{
Q_D(QQuickWheelHandler);
if (d->invertible == invertible)
return;
d->invertible = invertible;
emit invertibleChanged();
}
/*!
\qmlproperty real QtQuick::WheelHandler::activeTimeout
The amount of time in seconds after which the \l active property will
revert to \c false if no more wheel events are received. The default is
\c 0.1 (100 ms).
When WheelHandler handles events that contain
\l {Qt::ScrollPhase}{scroll phase} information, such as events from some
touchpads, the \l active property will become \c false as soon as an event
with phase \l Qt::ScrollEnd is received; in that case the timeout is not
necessary. But a conventional mouse with a wheel does not provide a scroll
phase: the mouse cannot detect when the user has decided to stop
scrolling, so the \l active property transitions to \c false after this
much time has elapsed.
\sa QWheelEvent::phase()
*/
qreal QQuickWheelHandler::activeTimeout() const
{
Q_D(const QQuickWheelHandler);
return d->activeTimeout;
}
void QQuickWheelHandler::setActiveTimeout(qreal timeout)
{
Q_D(QQuickWheelHandler);
if (qFuzzyCompare(d->activeTimeout, timeout))
return;
if (timeout < 0) {
qWarning("activeTimeout must be positive");
return;
}
d->activeTimeout = timeout;
emit activeTimeoutChanged();
}
/*!
\qmlproperty real QtQuick::WheelHandler::rotation
The angle through which the mouse wheel has been rotated since the last
time this property was set, in wheel degrees.
A positive value indicates that the wheel was rotated up/right;
a negative value indicates that the wheel was rotated down/left.
A basic mouse click-wheel works in steps of 15 degrees.
The default is \c 0 at startup. It can be programmatically set to any value
at any time. The value will be adjusted from there as the user rotates the
mouse wheel.
\sa orientation
*/
qreal QQuickWheelHandler::rotation() const
{
Q_D(const QQuickWheelHandler);
return d->rotation * d->rotationScale;
}
void QQuickWheelHandler::setRotation(qreal rotation)
{
Q_D(QQuickWheelHandler);
if (qFuzzyCompare(d->rotation, rotation / d->rotationScale))
return;
d->rotation = rotation / d->rotationScale;
emit rotationChanged();
}
/*!
\qmlproperty real QtQuick::WheelHandler::rotationScale
The scaling to be applied to the \l rotation property, and to the
\l property on the \l target item, if any. The default is 1, such that
\l rotation will be in units of degrees of rotation. It can be set to a
negative number to invert the effect of the direction of mouse wheel
rotation.
*/
qreal QQuickWheelHandler::rotationScale() const
{
Q_D(const QQuickWheelHandler);
return d->rotationScale;
}
void QQuickWheelHandler::setRotationScale(qreal rotationScale)
{
Q_D(QQuickWheelHandler);
if (qFuzzyCompare(d->rotationScale, rotationScale))
return;
if (qFuzzyIsNull(rotationScale)) {
qWarning("rotationScale cannot be set to zero");
return;
}
d->rotationScale = rotationScale;
emit rotationScaleChanged();
}
/*!
\qmlproperty string QtQuick::WheelHandler::property
The property to be modified on the \l target when the mouse wheel is rotated.
The default is no property (empty string). When no target property is being
automatically modified, you can use bindings to react to mouse wheel
rotation in arbitrary ways.
You can use the mouse wheel to adjust any numeric property. For example if
\c property is set to \c x, the \l target will move horizontally as the
wheel is rotated. The following properties have special behavior:
\value scale
\l{QQuickItem::scale}{scale} will be modified in a non-linear fashion
as described under \l targetScaleMultiplier. If
\l targetTransformAroundCursor is \c true, the \l{QQuickItem::x}{x} and
\l{QQuickItem::y}{y} properties will be simultaneously adjusted so that
the user will effectively zoom into or out of the point under the mouse
cursor.
\value rotation
\l{QQuickItem::rotation}{rotation} will be set to \l rotation. If
\l targetTransformAroundCursor is \c true, the l{QQuickItem::x}{x} and
\l{QQuickItem::y}{y} properties will be simultaneously adjusted so
that the user will effectively rotate the item around the point under
the mouse cursor.
The adjustment of the given target property is always scaled by \l rotationScale.
*/
QString QQuickWheelHandler::property() const
{
Q_D(const QQuickWheelHandler);
return d->propertyName;
}
void QQuickWheelHandler::setProperty(const QString &propertyName)
{
Q_D(QQuickWheelHandler);
if (d->propertyName == propertyName)
return;
d->propertyName = propertyName;
d->metaPropertyDirty = true;
emit propertyChanged();
}
/*!
\qmlproperty real QtQuick::WheelHandler::targetScaleMultiplier
The amount by which the \l target \l{QQuickItem::scale}{scale} is to be
multiplied whenever the \l rotation changes by 15 degrees. This
is relevant only when \l property is \c "scale".
The \c scale will be multiplied by
\c targetScaleMultiplier \sup {angleDelta * rotationScale / 15}.
The default is \c 2 \sup {1/3}, which means that if \l rotationScale is left
at its default value, and the mouse wheel is rotated by one "click"
(15 degrees), the \l target will be scaled by approximately 1.25; after
three "clicks" its size will be doubled or halved, depending on the
direction that the wheel is rotated. If you want to make it double or halve
with every 2 clicks of the wheel, set this to \c 2 \sup {1/2} (1.4142).
If you want to make it scale the opposite way as the wheel is rotated,
set \c rotationScale to a negative value.
*/
qreal QQuickWheelHandler::targetScaleMultiplier() const
{
Q_D(const QQuickWheelHandler);
return d->targetScaleMultiplier;
}
void QQuickWheelHandler::setTargetScaleMultiplier(qreal targetScaleMultiplier)
{
Q_D(QQuickWheelHandler);
if (qFuzzyCompare(d->targetScaleMultiplier, targetScaleMultiplier))
return;
d->targetScaleMultiplier = targetScaleMultiplier;
emit targetScaleMultiplierChanged();
}
/*!
\qmlproperty bool QtQuick::WheelHandler::targetTransformAroundCursor
Whether the \l target should automatically be repositioned in such a way
that it is transformed around the mouse cursor position while the
\l property is adjusted. The default is \c true.
If \l property is set to \c "rotation" and \l targetTransformAroundCursor
is \c true, then as the wheel is rotated, the \l target item will rotate in
place around the mouse cursor position. If \c targetTransformAroundCursor
is \c false, it will rotate around its
\l{QQuickItem::transformOrigin}{transformOrigin} instead.
*/
bool QQuickWheelHandler::isTargetTransformAroundCursor() const
{
Q_D(const QQuickWheelHandler);
return d->targetTransformAroundCursor;
}
void QQuickWheelHandler::setTargetTransformAroundCursor(bool ttac)
{
Q_D(QQuickWheelHandler);
if (d->targetTransformAroundCursor == ttac)
return;
d->targetTransformAroundCursor = ttac;
emit targetTransformAroundCursorChanged();
}
/*!
\qmlproperty bool QtQuick::WheelHandler::blocking
\since 6.3
Whether this handler prevents other items or handlers behind it from
handling the same wheel event. This property is \c true by default.
*/
bool QQuickWheelHandler::isBlocking() const
{
Q_D(const QQuickWheelHandler);
return d->blocking;
}
void QQuickWheelHandler::setBlocking(bool blocking)
{
Q_D(QQuickWheelHandler);
if (d->blocking == blocking)
return;
d->blocking = blocking;
emit blockingChanged();
}
bool QQuickWheelHandler::wantsPointerEvent(QPointerEvent *event)
{
if (!event)
return false;
if (event->type() != QEvent::Wheel)
return false;
QWheelEvent *we = static_cast<QWheelEvent *>(event);
if (!acceptedDevices().testFlag(QPointingDevice::DeviceType::TouchPad)
&& we->source() != Qt::MouseEventNotSynthesized)
return false;
if (!active()) {
switch (orientation()) {
case Qt::Horizontal:
if (!(we->angleDelta().x()) && !(we->pixelDelta().x()))
return false;
break;
case Qt::Vertical:
if (!(we->angleDelta().y()) && !(we->pixelDelta().y()))
return false;
break;
}
}
auto &point = event->point(0);
if (QQuickPointerDeviceHandler::wantsPointerEvent(event) && wantsEventPoint(event, point) && parentContains(point)) {
setPointId(point.id());
return true;
}
return false;
}
void QQuickWheelHandler::handleEventPoint(QPointerEvent *ev, QEventPoint &point)
{
Q_D(QQuickWheelHandler);
QQuickSinglePointHandler::handleEventPoint(ev, point);
if (ev->type() != QEvent::Wheel)
return;
const QWheelEvent *event = static_cast<const QWheelEvent *>(ev);
setActive(true); // ScrollEnd will not happen unless it was already active (see setActive(false) below)
if (d->blocking)
point.setAccepted();
qreal inversion = !d->invertible && event->isInverted() ? -1 : 1;
qreal angleDelta = inversion * qreal(orientation() == Qt::Horizontal ? event->angleDelta().x() :
event->angleDelta().y()) / 8;
d->rotation += angleDelta;
emit rotationChanged();
d->wheelEvent.reset(event);
emit wheel(&d->wheelEvent);
if (!d->propertyName.isEmpty() && target()) {
QQuickItem *t = target();
// writing target()'s property is done via QMetaProperty::write() so that any registered interceptors can react.
if (d->propertyName == QLatin1String("scale")) {
qreal multiplier = qPow(d->targetScaleMultiplier, angleDelta * d->rotationScale / 15); // wheel "clicks"
const QPointF centroidParentPos = t->parentItem()->mapFromScene(point.scenePosition());
const QPointF positionWas = t->position();
const qreal scaleWas = t->scale();
const qreal activePropertyValue = scaleWas * multiplier;
qCDebug(lcWheelHandler) << objectName() << "angle delta" << event->angleDelta() << "pixel delta" << event->pixelDelta()
<< "@" << point.position() << "in parent" << centroidParentPos
<< "in scene" << point.scenePosition()
<< "multiplier" << multiplier << "scale" << scaleWas
<< "->" << activePropertyValue;
d->targetMetaProperty().write(t, activePropertyValue);
if (d->targetTransformAroundCursor) {
// If an interceptor intervened, scale may now be different than we asked for. Adjust accordingly.
multiplier = t->scale() / scaleWas;
const QPointF adjPos = QQuickItemPrivate::get(t)->adjustedPosForTransform(
centroidParentPos, positionWas, QVector2D(), scaleWas, multiplier, t->rotation(), 0);
qCDebug(lcWheelHandler) << "adjusting item pos" << adjPos << "in scene" << t->parentItem()->mapToScene(adjPos);
t->setPosition(adjPos);
}
} else if (d->propertyName == QLatin1String("rotation")) {
const QPointF positionWas = t->position();
const qreal rotationWas = t->rotation();
const qreal activePropertyValue = rotationWas + angleDelta * d->rotationScale;
const QPointF centroidParentPos = t->parentItem()->mapFromScene(point.scenePosition());
qCDebug(lcWheelHandler) << objectName() << "angle delta" << event->angleDelta() << "pixel delta" << event->pixelDelta()
<< "@" << point.position() << "in parent" << centroidParentPos
<< "in scene" << point.scenePosition() << "rotation" << t->rotation()
<< "->" << activePropertyValue;
d->targetMetaProperty().write(t, activePropertyValue);
if (d->targetTransformAroundCursor) {
// If an interceptor intervened, rotation may now be different than we asked for. Adjust accordingly.
const QPointF adjPos = QQuickItemPrivate::get(t)->adjustedPosForTransform(
centroidParentPos, positionWas, QVector2D(),
t->scale(), 1, rotationWas, t->rotation() - rotationWas);
qCDebug(lcWheelHandler) << "adjusting item pos" << adjPos << "in scene" << t->parentItem()->mapToScene(adjPos);
t->setPosition(adjPos);
}
} else {
qCDebug(lcWheelHandler) << objectName() << "angle delta" << event->angleDelta() << "scaled" << angleDelta
<< "total" << d->rotation << "pixel delta" << event->pixelDelta()
<< "@" << point.position() << "in scene" << point.scenePosition() << "rotation" << t->rotation();
qreal delta = 0;
if (event->hasPixelDelta()) {
delta = inversion * d->rotationScale * qreal(orientation() == Qt::Horizontal ? event->pixelDelta().x() : event->pixelDelta().y());
qCDebug(lcWheelHandler) << "changing target" << d->propertyName << "by pixel delta" << delta << "from" << event;
} else {
delta = angleDelta * d->rotationScale;
qCDebug(lcWheelHandler) << "changing target" << d->propertyName << "by scaled angle delta" << delta << "from" << event;
}
bool ok = false;
qreal value = d->targetMetaProperty().read(t).toReal(&ok);
if (ok)
d->targetMetaProperty().write(t, value + qreal(delta));
else
qWarning() << "failed to read property" << d->propertyName << "of" << t;
}
}
switch (event->phase()) {
case Qt::ScrollEnd:
qCDebug(lcWheelHandler) << objectName() << "deactivating due to ScrollEnd phase";
setActive(false);
break;
case Qt::NoScrollPhase:
d->deactivationTimer.start(qRound(d->activeTimeout * 1000), this);
break;
case Qt::ScrollBegin:
case Qt::ScrollUpdate:
case Qt::ScrollMomentum:
break;
}
}
void QQuickWheelHandler::onTargetChanged(QQuickItem *oldTarget)
{
Q_UNUSED(oldTarget);
Q_D(QQuickWheelHandler);
d->metaPropertyDirty = true;
}
void QQuickWheelHandler::onActiveChanged()
{
Q_D(QQuickWheelHandler);
if (!active())
d->deactivationTimer.stop();
}
void QQuickWheelHandler::timerEvent(QTimerEvent *event)
{
Q_D(const QQuickWheelHandler);
if (event->timerId() == d->deactivationTimer.timerId()) {
qCDebug(lcWheelHandler) << objectName() << "deactivating due to timeout";
setActive(false);
}
}
/*!
\qmlsignal QtQuick::WheelHandler::wheel(WheelEvent event)
This signal is emitted every time this handler receives an \a event
of type \l QWheelEvent: that is, every time the wheel is moved or the
scrolling gesture is updated.
*/
QQuickWheelHandlerPrivate::QQuickWheelHandlerPrivate()
: QQuickSinglePointHandlerPrivate()
{
}
QMetaProperty &QQuickWheelHandlerPrivate::targetMetaProperty() const
{
Q_Q(const QQuickWheelHandler);
if (metaPropertyDirty && q->target()) {
if (!propertyName.isEmpty()) {
const QMetaObject *targetMeta = q->target()->metaObject();
metaProperty = targetMeta->property(
targetMeta->indexOfProperty(propertyName.toLocal8Bit().constData()));
}
metaPropertyDirty = false;
}
return metaProperty;
}
/*! \internal
\qmlproperty flags QtQuick::WheelHandler::acceptedButtons
This overrides QtQuick::PointerDeviceHandler::acceptedButtons
and hides it from the documentation as the property is not relevant for
WheelHandler.
*/
/*!
\qmlproperty flags QtQuick::WheelHandler::acceptedDevices
The types of pointing devices that can activate this handler.
By default, since 6.13 this property is set to
\l{QInputDevice::DeviceType}{PointerDevice.Mouse | PointerDevice.TouchPad}.
\note In prior versions of Qt, the default was
\l{QInputDevice::DeviceType}{PointerDevice.Mouse}, so as to react only to
events from an actual mouse wheel. Old behavior can be restored by
setting it that way again.
*/
/*!
\qmlproperty flags QtQuick::WheelHandler::acceptedModifiers
If this property is set, it will require the given keyboard modifiers to
be pressed in order to react to wheel events, and otherwise ignore them.
If this property is set to \c Qt.KeyboardModifierMask (the default value),
the WheelHandler ignores the modifier keys.
For example, an \l [QML] Item could have two handlers, one of which is
enabled only if the required keyboard modifier is pressed, while the other
ignores events if any modifier is pressed:
\snippet pointerHandlers/wheelHandlerAcceptedModifiers.qml entire
The available modifiers are as follows:
\value NoModifier No modifier key is allowed.
\value ShiftModifier A Shift key on the keyboard must be pressed.
\value ControlModifier A Ctrl key on the keyboard must be pressed.
\value AltModifier An Alt key on the keyboard must be pressed.
\value MetaModifier A Meta key on the keyboard must be pressed.
\value KeypadModifier A keypad button must be pressed.
\value GroupSwitchModifier X11 only (unless activated on Windows by a command line argument).
A Mode_switch key on the keyboard must be pressed.
\value KeyboardModifierMask The handler does not care which modifiers are pressed.
\sa Qt::KeyboardModifier
*/
/*! \internal
\qmlproperty flags QtQuick::WheelHandler::acceptedPointerTypes
This overrides QtQuick::PointerDeviceHandler::acceptedPointerTypes
and hides it from the documentation as the property is not relevant for
WheelHandler.
*/
/*!
\readonly
\qmlproperty bool QtQuick::WheelHandler::active
This holds \c true whenever the WheelHandler has recently seen a
QWheelEvent, is keeping its properties up-to-date, and actively manipulating
its \l target (if any).
\sa activeTimeout
*/
/*! \internal
\qmlproperty flags QtQuick::WheelHandler::cursorShape
This overrides QtQuick::PointerHandler::cursorShape
and hides it from the documentation as the property is not relevant for
WheelHandler.
*/
/*! \internal
\qmlproperty flags QtQuick::WheelHandler::dragThreshold
This overrides QtQuick::PointerHandler::dragThreshold
and hides it from the documentation as the property is not relevant for
WheelHandler.
*/
/*! \internal
\qmlproperty flags QtQuick::WheelHandler::grabPermissions
This overrides QtQuick::PointerHandler::grabPermissions
and hides it from the documentation as the property is not relevant for
WheelHandler.
*/
/*!
\qmlproperty real QtQuick::WheelHandler::margin
The margin beyond the bounds of the \l {PointerHandler::parent}{parent}
item within which the WheelHandler can react. For example if \c margin
is set to \c 10, you could place the cursor up to 10 pixels outside the
visible edge of the item, and it will still react to the wheel:
\snippet pointerHandlers/wheelHandlerMargin.qml entire
The default value is \c 0.
*/
/*! \internal
\qmlsignal QtQuick::WheelHandler::grabChanged(PointerDevice::GrabTransition transition, eventPoint point)
This overrides QtQuick::PointerHandler::grabChanged
and hides it from the documentation as the signal is not relevant for
WheelHandler.
*/
/*! \internal
\qmlsignal QtQuick::WheelHandler::canceled(eventPoint point)
This overrides QtQuick::PointerHandler::canceled
and hides it from the documentation as the signal is not relevant for
WheelHandler.
*/
QT_END_NAMESPACE
#include "moc_qquickwheelhandler_p.cpp"