| /**************************************************************************** |
| ** |
| ** Copyright (C) 2018 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 "qquickdraghandler_p.h" |
| #include <private/qquickwindow_p.h> |
| #include <QDebug> |
| |
| QT_BEGIN_NAMESPACE |
| |
| static const qreal DragAngleToleranceDegrees = 10; |
| |
| Q_LOGGING_CATEGORY(lcDragHandler, "qt.quick.handler.drag") |
| |
| /*! |
| \qmltype DragHandler |
| \instantiates QQuickDragHandler |
| \inherits MultiPointHandler |
| \inqmlmodule QtQuick |
| \ingroup qtquick-input-handlers |
| \brief Handler for dragging. |
| |
| DragHandler is a handler that is used to interactively move an Item. |
| Like other Input Handlers, by default it is fully functional, and |
| manipulates its \l {PointerHandler::target} {target}. |
| |
| \snippet pointerHandlers/dragHandler.qml 0 |
| |
| It has properties to restrict the range of dragging. |
| |
| If it is declared within one Item but is assigned a different |
| \l {PointerHandler::target} {target}, then it handles events within the |
| bounds of the \l {PointerHandler::parent} {parent} Item but |
| manipulates the \c target Item instead: |
| |
| \snippet pointerHandlers/dragHandlerDifferentTarget.qml 0 |
| |
| A third way to use it is to set \l {PointerHandler::target} {target} to |
| \c null and react to property changes in some other way: |
| |
| \snippet pointerHandlers/dragHandlerNullTarget.qml 0 |
| |
| If minimumPointCount and maximumPointCount are set to values larger than 1, |
| the user will need to drag that many fingers in the same direction to start |
| dragging. A multi-finger drag gesture can be detected independently of both |
| a (default) single-finger DragHandler and a PinchHandler on the same Item, |
| and thus can be used to adjust some other feature independently of the |
| usual pinch behavior: for example adjust a tilt transformation, or adjust |
| some other numeric value, if the \c target is set to null. But if the |
| \c target is an Item, \c centroid is the point at which the drag begins and |
| to which the \c target will be moved (subject to constraints). |
| |
| At this time, drag-and-drop is not yet supported. |
| |
| \sa Drag, MouseArea |
| */ |
| |
| QQuickDragHandler::QQuickDragHandler(QQuickItem *parent) |
| : QQuickMultiPointHandler(parent, 1, 1) |
| { |
| } |
| |
| bool QQuickDragHandler::targetContainsCentroid() |
| { |
| Q_ASSERT(parentItem() && target()); |
| return target()->contains(targetCentroidPosition()); |
| } |
| |
| QPointF QQuickDragHandler::targetCentroidPosition() |
| { |
| QPointF pos = centroid().position(); |
| if (target() != parentItem()) |
| pos = parentItem()->mapToItem(target(), pos); |
| return pos; |
| } |
| |
| void QQuickDragHandler::onGrabChanged(QQuickPointerHandler *grabber, QQuickEventPoint::GrabTransition transition, QQuickEventPoint *point) |
| { |
| QQuickMultiPointHandler::onGrabChanged(grabber, transition, point); |
| if (grabber == this && transition == QQuickEventPoint::GrabExclusive && target()) { |
| // In case the grab got handed over from another grabber, we might not get the Press. |
| |
| auto isDescendant = [](QQuickItem *parent, QQuickItem *target) { |
| return (target != parent) && !target->isAncestorOf(parent); |
| }; |
| if (m_snapMode == SnapAlways |
| || (m_snapMode == SnapIfPressedOutsideTarget && !m_pressedInsideTarget) |
| || (m_snapMode == SnapAuto && !m_pressedInsideTarget && isDescendant(parentItem(), target())) |
| ) { |
| m_pressTargetPos = QPointF(target()->width(), target()->height()) / 2; |
| } else if (m_pressTargetPos.isNull()) { |
| m_pressTargetPos = targetCentroidPosition(); |
| } |
| } |
| } |
| |
| /*! |
| \qmlproperty enumeration QtQuick.DragHandler::snapMode |
| |
| This property holds the snap mode. |
| |
| The snap mode configures snapping of the \l target item's center to the event point. |
| |
| Possible values: |
| \value DragHandler.SnapNever Never snap |
| \value DragHandler.SnapAuto The \l target snaps if the event point was pressed outside of the \l target |
| item \e and the \l target is a descendant of \l parentItem (default) |
| \value DragHandler.SnapWhenPressedOutsideTarget The \l target snaps if the event point was pressed outside of the \l target |
| \value DragHandler.SnapAlways Always snap |
| */ |
| QQuickDragHandler::SnapMode QQuickDragHandler::snapMode() const |
| { |
| return m_snapMode; |
| } |
| |
| void QQuickDragHandler::setSnapMode(QQuickDragHandler::SnapMode mode) |
| { |
| if (mode == m_snapMode) |
| return; |
| m_snapMode = mode; |
| emit snapModeChanged(); |
| } |
| |
| void QQuickDragHandler::onActiveChanged() |
| { |
| QQuickMultiPointHandler::onActiveChanged(); |
| if (active()) { |
| if (auto parent = parentItem()) { |
| if (currentEvent()->asPointerTouchEvent()) |
| parent->setKeepTouchGrab(true); |
| // tablet and mouse are treated the same by Item's legacy event handling, and |
| // touch becomes synth-mouse for Flickable, so we need to prevent stealing |
| // mouse grab too, whenever dragging occurs in an enabled direction |
| parent->setKeepMouseGrab(true); |
| } |
| } else { |
| m_pressTargetPos = QPointF(); |
| m_pressedInsideTarget = false; |
| if (auto parent = parentItem()) { |
| parent->setKeepTouchGrab(false); |
| parent->setKeepMouseGrab(false); |
| } |
| } |
| } |
| |
| void QQuickDragHandler::handlePointerEventImpl(QQuickPointerEvent *event) |
| { |
| QQuickMultiPointHandler::handlePointerEventImpl(event); |
| event->setAccepted(true); |
| |
| if (active()) { |
| // Calculate drag delta, taking into account the axis enabled constraint |
| // i.e. if xAxis is not enabled, then ignore the horizontal component of the actual movement |
| QVector2D accumulatedDragDelta = QVector2D(centroid().scenePosition() - centroid().scenePressPosition()); |
| if (!m_xAxis.enabled()) |
| accumulatedDragDelta.setX(0); |
| if (!m_yAxis.enabled()) |
| accumulatedDragDelta.setY(0); |
| setTranslation(accumulatedDragDelta); |
| } else { |
| // Check that all points have been dragged past the drag threshold, |
| // to the extent that the constraints allow, |
| // and in approximately the same direction |
| qreal minAngle = 361; |
| qreal maxAngle = -361; |
| bool allOverThreshold = !event->isReleaseEvent(); |
| QVector <QQuickEventPoint *> chosenPoints; |
| |
| if (event->isPressEvent()) |
| m_pressedInsideTarget = target() && currentPoints().count() > 0; |
| |
| for (const QQuickHandlerPoint &p : currentPoints()) { |
| if (!allOverThreshold) |
| break; |
| QQuickEventPoint *point = event->pointById(p.id()); |
| chosenPoints << point; |
| setPassiveGrab(point); |
| // Calculate drag delta, taking into account the axis enabled constraint |
| // i.e. if xAxis is not enabled, then ignore the horizontal component of the actual movement |
| QVector2D accumulatedDragDelta = QVector2D(point->scenePosition() - point->scenePressPosition()); |
| if (!m_xAxis.enabled()) { |
| // If horizontal dragging is disallowed, but the user is dragging |
| // mostly horizontally, then don't activate. |
| if (qAbs(accumulatedDragDelta.x()) > qAbs(accumulatedDragDelta.y())) |
| accumulatedDragDelta.setY(0); |
| accumulatedDragDelta.setX(0); |
| } |
| if (!m_yAxis.enabled()) { |
| // If vertical dragging is disallowed, but the user is dragging |
| // mostly vertically, then don't activate. |
| if (qAbs(accumulatedDragDelta.y()) > qAbs(accumulatedDragDelta.x())) |
| accumulatedDragDelta.setX(0); |
| accumulatedDragDelta.setY(0); |
| } |
| qreal angle = std::atan2(accumulatedDragDelta.y(), accumulatedDragDelta.x()) * 180 / M_PI; |
| bool overThreshold = QQuickWindowPrivate::dragOverThreshold(accumulatedDragDelta); |
| qCDebug(lcDragHandler) << "movement" << accumulatedDragDelta << "angle" << angle << "of point" << point |
| << "pressed @" << point->scenePressPosition() << "over threshold?" << overThreshold; |
| minAngle = qMin(angle, minAngle); |
| maxAngle = qMax(angle, maxAngle); |
| if (allOverThreshold && !overThreshold) |
| allOverThreshold = false; |
| |
| if (event->isPressEvent()) { |
| // m_pressedInsideTarget should stay true iff ALL points in which DragHandler is interested |
| // have been pressed inside the target() Item. (E.g. in a Slider the parent might be the |
| // whole control while the target is just the knob.) |
| if (target()) { |
| const QPointF localPressPos = target()->mapFromScene(point->scenePressPosition()); |
| m_pressedInsideTarget &= target()->contains(localPressPos); |
| m_pressTargetPos = targetCentroidPosition(); |
| } |
| // QQuickWindowPrivate::deliverToPassiveGrabbers() skips subsequent delivery if the event is filtered. |
| // (That affects behavior for mouse but not for touch, because Flickable only handles mouse.) |
| // So we have to compensate by accepting the event here to avoid any parent Flickable from |
| // getting the event via direct delivery and grabbing too soon. |
| point->setAccepted(event->asPointerMouseEvent()); // stop propagation iff it's a mouse event |
| } |
| } |
| if (allOverThreshold) { |
| qreal angleDiff = maxAngle - minAngle; |
| if (angleDiff > 180) |
| angleDiff = 360 - angleDiff; |
| qCDebug(lcDragHandler) << "angle min" << minAngle << "max" << maxAngle << "range" << angleDiff; |
| if (angleDiff < DragAngleToleranceDegrees && grabPoints(chosenPoints)) |
| setActive(true); |
| } |
| } |
| if (active() && target() && target()->parentItem()) { |
| const QPointF newTargetTopLeft = targetCentroidPosition() - m_pressTargetPos; |
| const QPointF xformOrigin = target()->transformOriginPoint(); |
| const QPointF targetXformOrigin = newTargetTopLeft + xformOrigin; |
| QPointF pos = target()->parentItem()->mapFromItem(target(), targetXformOrigin); |
| pos -= xformOrigin; |
| QPointF targetItemPos = target()->position(); |
| if (!m_xAxis.enabled()) |
| pos.setX(targetItemPos.x()); |
| if (!m_yAxis.enabled()) |
| pos.setY(targetItemPos.y()); |
| enforceAxisConstraints(&pos); |
| moveTarget(pos); |
| } |
| } |
| |
| void QQuickDragHandler::enforceConstraints() |
| { |
| if (!target() || !target()->parentItem()) |
| return; |
| QPointF pos = target()->position(); |
| QPointF copy(pos); |
| enforceAxisConstraints(&pos); |
| if (pos != copy) |
| target()->setPosition(pos); |
| } |
| |
| void QQuickDragHandler::enforceAxisConstraints(QPointF *localPos) |
| { |
| if (m_xAxis.enabled()) |
| localPos->setX(qBound(m_xAxis.minimum(), localPos->x(), m_xAxis.maximum())); |
| if (m_yAxis.enabled()) |
| localPos->setY(qBound(m_yAxis.minimum(), localPos->y(), m_yAxis.maximum())); |
| } |
| |
| void QQuickDragHandler::setTranslation(const QVector2D &trans) |
| { |
| if (trans == m_translation) // fuzzy compare? |
| return; |
| m_translation = trans; |
| emit translationChanged(); |
| } |
| |
| /*! |
| \qmlpropertygroup QtQuick::DragHandler::xAxis |
| \qmlproperty real QtQuick::DragHandler::xAxis.minimum |
| \qmlproperty real QtQuick::DragHandler::xAxis.maximum |
| \qmlproperty bool QtQuick::DragHandler::xAxis.enabled |
| |
| \c xAxis controls the constraints for horizontal dragging. |
| |
| \c minimum is the minimum acceptable value of \l {Item::x}{x} to be |
| applied to the \l {PointerHandler::target} {target}. |
| \c maximum is the maximum acceptable value of \l {Item::x}{x} to be |
| applied to the \l {PointerHandler::target} {target}. |
| If \c enabled is true, horizontal dragging is allowed. |
| */ |
| |
| /*! |
| \qmlpropertygroup QtQuick::DragHandler::yAxis |
| \qmlproperty real QtQuick::DragHandler::yAxis.minimum |
| \qmlproperty real QtQuick::DragHandler::yAxis.maximum |
| \qmlproperty bool QtQuick::DragHandler::yAxis.enabled |
| |
| \c yAxis controls the constraints for vertical dragging. |
| |
| \c minimum is the minimum acceptable value of \l {Item::y}{y} to be |
| applied to the \l {PointerHandler::target} {target}. |
| \c maximum is the maximum acceptable value of \l {Item::y}{y} to be |
| applied to the \l {PointerHandler::target} {target}. |
| If \c enabled is true, vertical dragging is allowed. |
| */ |
| |
| /*! |
| \readonly |
| \qmlproperty QVector2D QtQuick::DragHandler::translation |
| |
| The translation since the gesture began. |
| */ |
| |
| QT_END_NAMESPACE |