blob: 16f38af9629b72767c28011af53481c8118c3986 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2020 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtQuick module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qquickwheelhandler_p.h"
#include "qquickwheelhandler_p_p.h"
#include <QLoggingCategory>
#include <QtMath>
QT_BEGIN_NAMESPACE
Q_LOGGING_CATEGORY(lcWheelHandler, "qt.quick.handler.wheel")
/*!
\qmltype WheelHandler
\instantiates 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.
WheelHandler handles only a rotating mouse wheel by default.
Optionally it can handle smooth-scrolling events from touchpad gestures,
by setting \l {QtQuick::PointerDeviceHandler::}{acceptedDevices} to
\c{PointerDevice.Mouse | PointerDevice.TouchPad}.
\note Some non-mouse hardware (such as a touch-sensitive Wacom tablet, or
a Linux laptop touchpad) generates real wheel events from gestures.
WheelHandler will respond to those events as wheel events regardless of the
setting of the \l {QtQuick::PointerDeviceHandler::}{acceptedDevices}
property.
\sa MouseArea, Flickable
*/
QQuickWheelHandler::QQuickWheelHandler(QQuickItem *parent)
: QQuickSinglePointHandler(*(new QQuickWheelHandlerPrivate), parent)
{
setAcceptedDevices(QQuickPointerDevice::Mouse);
}
/*!
\qmlproperty enum 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
QQuickPointerScrollEvent::inverted is 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 the
\l {QQuickPointerScrollEvent::phase}{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.
*/
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();
}
bool QQuickWheelHandler::wantsPointerEvent(QQuickPointerEvent *event)
{
if (!event)
return false;
QQuickPointerScrollEvent *scroll = event->asPointerScrollEvent();
if (!scroll)
return false;
if (!acceptedDevices().testFlag(QQuickPointerDevice::DeviceType::TouchPad)
&& scroll->synthSource() != Qt::MouseEventNotSynthesized)
return false;
if (!active()) {
switch (orientation()) {
case Qt::Horizontal:
if (qFuzzyIsNull(scroll->angleDelta().x()) && qFuzzyIsNull(scroll->pixelDelta().x()))
return false;
break;
case Qt::Vertical:
if (qFuzzyIsNull(scroll->angleDelta().y()) && qFuzzyIsNull(scroll->pixelDelta().y()))
return false;
break;
}
}
QQuickEventPoint *point = event->point(0);
if (QQuickPointerDeviceHandler::wantsPointerEvent(event) && wantsEventPoint(point) && parentContains(point)) {
setPointId(point->pointId());
return true;
}
return false;
}
void QQuickWheelHandler::handleEventPoint(QQuickEventPoint *point)
{
Q_D(QQuickWheelHandler);
QQuickPointerScrollEvent *event = point->pointerEvent()->asPointerScrollEvent();
setActive(true); // ScrollEnd will not happen unless it was already active (see setActive(false) below)
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();
emit wheel(event);
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(PointerScrollEvent event)
This signal is emitted every time this handler receives a \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;
}
QT_END_NAMESPACE