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