/**************************************************************************** ** ** Copyright (C) 2019 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 #include QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(lcWheelHandler, "qt.quick.handler.wheel") /*! \qmltype WheelHandler \instantiates QQuickWheelHandler \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 targetProperty 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 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 acceptedDevices property. \sa MouseArea \sa 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 \l 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 \l 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); } } 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