1/****************************************************************************
2**
3** Copyright (C) 2018 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 "qquickdraghandler_p.h"
41#include <private/qquickwindow_p.h>
42#include <private/qquickmultipointhandler_p_p.h>
43#include <QDebug>
44
45QT_BEGIN_NAMESPACE
46
47static const qreal DragAngleToleranceDegrees = 10;
48
49Q_LOGGING_CATEGORY(lcDragHandler, "qt.quick.handler.drag")
50
51/*!
52 \qmltype DragHandler
53 \instantiates QQuickDragHandler
54 \inherits MultiPointHandler
55 \inqmlmodule QtQuick
56 \ingroup qtquick-input-handlers
57 \brief Handler for dragging.
58
59 DragHandler is a handler that is used to interactively move an Item.
60 Like other Input Handlers, by default it is fully functional, and
61 manipulates its \l {PointerHandler::target} {target}.
62
63 \snippet pointerHandlers/dragHandler.qml 0
64
65 It has properties to restrict the range of dragging.
66
67 If it is declared within one Item but is assigned a different
68 \l {PointerHandler::target} {target}, then it handles events within the
69 bounds of the \l {PointerHandler::parent} {parent} Item but
70 manipulates the \c target Item instead:
71
72 \snippet pointerHandlers/dragHandlerDifferentTarget.qml 0
73
74 A third way to use it is to set \l {PointerHandler::target} {target} to
75 \c null and react to property changes in some other way:
76
77 \snippet pointerHandlers/dragHandlerNullTarget.qml 0
78
79 If minimumPointCount and maximumPointCount are set to values larger than 1,
80 the user will need to drag that many fingers in the same direction to start
81 dragging. A multi-finger drag gesture can be detected independently of both
82 a (default) single-finger DragHandler and a PinchHandler on the same Item,
83 and thus can be used to adjust some other feature independently of the
84 usual pinch behavior: for example adjust a tilt transformation, or adjust
85 some other numeric value, if the \c target is set to null. But if the
86 \c target is an Item, \c centroid is the point at which the drag begins and
87 to which the \c target will be moved (subject to constraints).
88
89 At this time, drag-and-drop is not yet supported.
90
91 \sa Drag, MouseArea
92*/
93
94QQuickDragHandler::QQuickDragHandler(QQuickItem *parent)
95 : QQuickMultiPointHandler(parent, 1, 1)
96{
97}
98
99bool QQuickDragHandler::targetContainsCentroid()
100{
101 Q_ASSERT(parentItem() && target());
102 return target()->contains(point: targetCentroidPosition());
103}
104
105QPointF QQuickDragHandler::targetCentroidPosition()
106{
107 QPointF pos = centroid().position();
108 if (target() != parentItem())
109 pos = parentItem()->mapToItem(item: target(), point: pos);
110 return pos;
111}
112
113void QQuickDragHandler::onGrabChanged(QQuickPointerHandler *grabber, QQuickEventPoint::GrabTransition transition, QQuickEventPoint *point)
114{
115 QQuickMultiPointHandler::onGrabChanged(grabber, transition, point);
116 if (grabber == this && transition == QQuickEventPoint::GrabExclusive && target()) {
117 // In case the grab got handed over from another grabber, we might not get the Press.
118
119 auto isDescendant = [](QQuickItem *parent, QQuickItem *target) {
120 return (target != parent) && !target->isAncestorOf(child: parent);
121 };
122 if (m_snapMode == SnapAlways
123 || (m_snapMode == SnapIfPressedOutsideTarget && !m_pressedInsideTarget)
124 || (m_snapMode == SnapAuto && !m_pressedInsideTarget && isDescendant(parentItem(), target()))
125 ) {
126 m_pressTargetPos = QPointF(target()->width(), target()->height()) / 2;
127 } else if (m_pressTargetPos.isNull()) {
128 m_pressTargetPos = targetCentroidPosition();
129 }
130 }
131}
132
133/*!
134 \qmlproperty enumeration QtQuick.DragHandler::snapMode
135
136 This property holds the snap mode.
137
138 The snap mode configures snapping of the \l target item's center to the event point.
139
140 Possible values:
141 \value DragHandler.SnapNever Never snap
142 \value DragHandler.SnapAuto The \l target snaps if the event point was pressed outside of the \l target
143 item \e and the \l target is a descendant of \l parentItem (default)
144 \value DragHandler.SnapWhenPressedOutsideTarget The \l target snaps if the event point was pressed outside of the \l target
145 \value DragHandler.SnapAlways Always snap
146*/
147QQuickDragHandler::SnapMode QQuickDragHandler::snapMode() const
148{
149 return m_snapMode;
150}
151
152void QQuickDragHandler::setSnapMode(QQuickDragHandler::SnapMode mode)
153{
154 if (mode == m_snapMode)
155 return;
156 m_snapMode = mode;
157 emit snapModeChanged();
158}
159
160void QQuickDragHandler::onActiveChanged()
161{
162 QQuickMultiPointHandler::onActiveChanged();
163 if (active()) {
164 if (auto parent = parentItem()) {
165 if (currentEvent()->asPointerTouchEvent())
166 parent->setKeepTouchGrab(true);
167 // tablet and mouse are treated the same by Item's legacy event handling, and
168 // touch becomes synth-mouse for Flickable, so we need to prevent stealing
169 // mouse grab too, whenever dragging occurs in an enabled direction
170 parent->setKeepMouseGrab(true);
171 }
172 } else {
173 m_pressTargetPos = QPointF();
174 m_pressedInsideTarget = false;
175 if (auto parent = parentItem()) {
176 parent->setKeepTouchGrab(false);
177 parent->setKeepMouseGrab(false);
178 }
179 }
180}
181
182void QQuickDragHandler::handlePointerEventImpl(QQuickPointerEvent *event)
183{
184 QQuickMultiPointHandler::handlePointerEventImpl(event);
185 event->setAccepted(true);
186
187 if (active()) {
188 // Calculate drag delta, taking into account the axis enabled constraint
189 // i.e. if xAxis is not enabled, then ignore the horizontal component of the actual movement
190 QVector2D accumulatedDragDelta = QVector2D(centroid().scenePosition() - centroid().scenePressPosition());
191 if (!m_xAxis.enabled())
192 accumulatedDragDelta.setX(0);
193 if (!m_yAxis.enabled())
194 accumulatedDragDelta.setY(0);
195 setTranslation(accumulatedDragDelta);
196 } else {
197 // Check that all points have been dragged past the drag threshold,
198 // to the extent that the constraints allow,
199 // and in approximately the same direction
200 qreal minAngle = 361;
201 qreal maxAngle = -361;
202 bool allOverThreshold = !event->isReleaseEvent();
203 QVector <QQuickEventPoint *> chosenPoints;
204
205 if (event->isPressEvent())
206 m_pressedInsideTarget = target() && currentPoints().count() > 0;
207
208 for (const QQuickHandlerPoint &p : currentPoints()) {
209 if (!allOverThreshold)
210 break;
211 QQuickEventPoint *point = event->pointById(pointId: p.id());
212 chosenPoints << point;
213 setPassiveGrab(point);
214 // Calculate drag delta, taking into account the axis enabled constraint
215 // i.e. if xAxis is not enabled, then ignore the horizontal component of the actual movement
216 QVector2D accumulatedDragDelta = QVector2D(point->scenePosition() - point->scenePressPosition());
217 if (!m_xAxis.enabled()) {
218 // If horizontal dragging is disallowed, but the user is dragging
219 // mostly horizontally, then don't activate.
220 if (qAbs(t: accumulatedDragDelta.x()) > qAbs(t: accumulatedDragDelta.y()))
221 accumulatedDragDelta.setY(0);
222 accumulatedDragDelta.setX(0);
223 }
224 if (!m_yAxis.enabled()) {
225 // If vertical dragging is disallowed, but the user is dragging
226 // mostly vertically, then don't activate.
227 if (qAbs(t: accumulatedDragDelta.y()) > qAbs(t: accumulatedDragDelta.x()))
228 accumulatedDragDelta.setX(0);
229 accumulatedDragDelta.setY(0);
230 }
231 qreal angle = std::atan2(y: accumulatedDragDelta.y(), x: accumulatedDragDelta.x()) * 180 / M_PI;
232 bool overThreshold = d_func()->dragOverThreshold(delta: accumulatedDragDelta);
233 qCDebug(lcDragHandler) << "movement" << accumulatedDragDelta << "angle" << angle << "of point" << point
234 << "pressed @" << point->scenePressPosition() << "over threshold?" << overThreshold;
235 minAngle = qMin(a: angle, b: minAngle);
236 maxAngle = qMax(a: angle, b: maxAngle);
237 if (allOverThreshold && !overThreshold)
238 allOverThreshold = false;
239
240 if (event->isPressEvent()) {
241 // m_pressedInsideTarget should stay true iff ALL points in which DragHandler is interested
242 // have been pressed inside the target() Item. (E.g. in a Slider the parent might be the
243 // whole control while the target is just the knob.)
244 if (target()) {
245 const QPointF localPressPos = target()->mapFromScene(point: point->scenePressPosition());
246 m_pressedInsideTarget &= target()->contains(point: localPressPos);
247 m_pressTargetPos = targetCentroidPosition();
248 }
249 // QQuickWindowPrivate::deliverToPassiveGrabbers() skips subsequent delivery if the event is filtered.
250 // (That affects behavior for mouse but not for touch, because Flickable only handles mouse.)
251 // So we have to compensate by accepting the event here to avoid any parent Flickable from
252 // getting the event via direct delivery and grabbing too soon.
253 point->setAccepted(event->asPointerMouseEvent()); // stop propagation iff it's a mouse event
254 }
255 }
256 if (allOverThreshold) {
257 qreal angleDiff = maxAngle - minAngle;
258 if (angleDiff > 180)
259 angleDiff = 360 - angleDiff;
260 qCDebug(lcDragHandler) << "angle min" << minAngle << "max" << maxAngle << "range" << angleDiff;
261 if (angleDiff < DragAngleToleranceDegrees && grabPoints(points: chosenPoints))
262 setActive(true);
263 }
264 }
265 if (active() && target() && target()->parentItem()) {
266 const QPointF newTargetTopLeft = targetCentroidPosition() - m_pressTargetPos;
267 const QPointF xformOrigin = target()->transformOriginPoint();
268 const QPointF targetXformOrigin = newTargetTopLeft + xformOrigin;
269 QPointF pos = target()->parentItem()->mapFromItem(item: target(), point: targetXformOrigin);
270 pos -= xformOrigin;
271 QPointF targetItemPos = target()->position();
272 if (!m_xAxis.enabled())
273 pos.setX(targetItemPos.x());
274 if (!m_yAxis.enabled())
275 pos.setY(targetItemPos.y());
276 enforceAxisConstraints(localPos: &pos);
277 moveTarget(pos);
278 }
279}
280
281void QQuickDragHandler::enforceConstraints()
282{
283 if (!target() || !target()->parentItem())
284 return;
285 QPointF pos = target()->position();
286 QPointF copy(pos);
287 enforceAxisConstraints(localPos: &pos);
288 if (pos != copy)
289 target()->setPosition(pos);
290}
291
292void QQuickDragHandler::enforceAxisConstraints(QPointF *localPos)
293{
294 if (m_xAxis.enabled())
295 localPos->setX(qBound(min: m_xAxis.minimum(), val: localPos->x(), max: m_xAxis.maximum()));
296 if (m_yAxis.enabled())
297 localPos->setY(qBound(min: m_yAxis.minimum(), val: localPos->y(), max: m_yAxis.maximum()));
298}
299
300void QQuickDragHandler::setTranslation(const QVector2D &trans)
301{
302 if (trans == m_translation) // fuzzy compare?
303 return;
304 m_translation = trans;
305 emit translationChanged();
306}
307
308/*!
309 \qmlpropertygroup QtQuick::DragHandler::xAxis
310 \qmlproperty real QtQuick::DragHandler::xAxis.minimum
311 \qmlproperty real QtQuick::DragHandler::xAxis.maximum
312 \qmlproperty bool QtQuick::DragHandler::xAxis.enabled
313
314 \c xAxis controls the constraints for horizontal dragging.
315
316 \c minimum is the minimum acceptable value of \l {Item::x}{x} to be
317 applied to the \l {PointerHandler::target} {target}.
318 \c maximum is the maximum acceptable value of \l {Item::x}{x} to be
319 applied to the \l {PointerHandler::target} {target}.
320 If \c enabled is true, horizontal dragging is allowed.
321 */
322
323/*!
324 \qmlpropertygroup QtQuick::DragHandler::yAxis
325 \qmlproperty real QtQuick::DragHandler::yAxis.minimum
326 \qmlproperty real QtQuick::DragHandler::yAxis.maximum
327 \qmlproperty bool QtQuick::DragHandler::yAxis.enabled
328
329 \c yAxis controls the constraints for vertical dragging.
330
331 \c minimum is the minimum acceptable value of \l {Item::y}{y} to be
332 applied to the \l {PointerHandler::target} {target}.
333 \c maximum is the maximum acceptable value of \l {Item::y}{y} to be
334 applied to the \l {PointerHandler::target} {target}.
335 If \c enabled is true, vertical dragging is allowed.
336 */
337
338/*!
339 \readonly
340 \qmlproperty QVector2D QtQuick::DragHandler::translation
341
342 The translation since the gesture began.
343*/
344
345QT_END_NAMESPACE
346

source code of qtdeclarative/src/quick/handlers/qquickdraghandler.cpp