1// Copyright (C) 2018 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qquickdraghandler_p.h"
5#include <private/qquickwindow_p.h>
6#include <private/qquickmultipointhandler_p_p.h>
7#include <QDebug>
8
9QT_BEGIN_NAMESPACE
10
11static const qreal DragAngleToleranceDegrees = 10;
12
13Q_LOGGING_CATEGORY(lcDragHandler, "qt.quick.handler.drag")
14
15/*!
16 \qmltype DragHandler
17 \instantiates QQuickDragHandler
18 \inherits MultiPointHandler
19 \inqmlmodule QtQuick
20 \ingroup qtquick-input-handlers
21 \brief Handler for dragging.
22
23 DragHandler is a handler that is used to interactively move an Item.
24 Like other Input Handlers, by default it is fully functional, and
25 manipulates its \l {PointerHandler::target} {target}.
26
27 \snippet pointerHandlers/dragHandler.qml 0
28
29 It has properties to restrict the range of dragging.
30
31 If it is declared within one Item but is assigned a different
32 \l {PointerHandler::target} {target}, then it handles events within the
33 bounds of the \l {PointerHandler::parent} {parent} Item but
34 manipulates the \c target Item instead:
35
36 \snippet pointerHandlers/dragHandlerDifferentTarget.qml 0
37
38 A third way to use it is to set \l {PointerHandler::target} {target} to
39 \c null and react to property changes in some other way:
40
41 \snippet pointerHandlers/dragHandlerNullTarget.qml 0
42
43 If minimumPointCount and maximumPointCount are set to values larger than 1,
44 the user will need to drag that many fingers in the same direction to start
45 dragging. A multi-finger drag gesture can be detected independently of both
46 a (default) single-finger DragHandler and a PinchHandler on the same Item,
47 and thus can be used to adjust some other feature independently of the
48 usual pinch behavior: for example adjust a tilt transformation, or adjust
49 some other numeric value, if the \c target is set to null. But if the
50 \c target is an Item, \c centroid is the point at which the drag begins and
51 to which the \c target will be moved (subject to constraints).
52
53 At this time, drag-and-drop is not yet supported.
54
55 \sa Drag, MouseArea, {Qt Quick Examples - Pointer Handlers}
56*/
57
58QQuickDragHandler::QQuickDragHandler(QQuickItem *parent)
59 : QQuickMultiPointHandler(parent, 1, 1)
60{
61}
62
63QPointF QQuickDragHandler::targetCentroidPosition()
64{
65 QPointF pos = centroid().position();
66 if (auto par = parentItem()) {
67 if (target() != par)
68 pos = par->mapToItem(item: target(), point: pos);
69 }
70 return pos;
71}
72
73void QQuickDragHandler::onGrabChanged(QQuickPointerHandler *grabber, QPointingDevice::GrabTransition transition, QPointerEvent *event, QEventPoint &point)
74{
75 QQuickMultiPointHandler::onGrabChanged(grabber, transition, event, point);
76 if (grabber == this && transition == QPointingDevice::GrabExclusive && target()) {
77 // In case the grab got handed over from another grabber, we might not get the Press.
78
79 auto isDescendant = [](QQuickItem *parent, QQuickItem *target) {
80 return parent && (target != parent) && !target->isAncestorOf(child: parent);
81 };
82 if (m_snapMode == SnapAlways
83 || (m_snapMode == SnapIfPressedOutsideTarget && !m_pressedInsideTarget)
84 || (m_snapMode == SnapAuto && !m_pressedInsideTarget && isDescendant(parentItem(), target()))
85 ) {
86 m_pressTargetPos = QPointF(target()->width(), target()->height()) / 2;
87 } else if (m_pressTargetPos.isNull()) {
88 m_pressTargetPos = targetCentroidPosition();
89 }
90 }
91}
92
93/*!
94 \qmlproperty enumeration QtQuick::DragHandler::snapMode
95
96 This property holds the snap mode.
97
98 The snap mode configures snapping of the \l target item's center to the \l eventPoint.
99
100 Possible values:
101 \value DragHandler.SnapNever Never snap
102 \value DragHandler.SnapAuto The \l target snaps if the \l eventPoint was pressed outside of the \l target
103 item \e and the \l target is a descendant of \l {PointerHandler::}{parent} item (default)
104 \value DragHandler.SnapWhenPressedOutsideTarget The \l target snaps if the \l eventPoint was pressed outside of the \l target
105 \value DragHandler.SnapAlways Always snap
106*/
107QQuickDragHandler::SnapMode QQuickDragHandler::snapMode() const
108{
109 return m_snapMode;
110}
111
112void QQuickDragHandler::setSnapMode(QQuickDragHandler::SnapMode mode)
113{
114 if (mode == m_snapMode)
115 return;
116 m_snapMode = mode;
117 emit snapModeChanged();
118}
119
120void QQuickDragHandler::onActiveChanged()
121{
122 QQuickMultiPointHandler::onActiveChanged();
123 const bool curActive = active();
124 m_xAxis.onActiveChanged(active: curActive, initActiveValue: 0);
125 m_yAxis.onActiveChanged(active: curActive, initActiveValue: 0);
126 if (curActive) {
127 if (auto parent = parentItem()) {
128 if (QQuickDeliveryAgentPrivate::isTouchEvent(ev: currentEvent()))
129 parent->setKeepTouchGrab(true);
130 // tablet and mouse are treated the same by Item's legacy event handling, and
131 // touch becomes synth-mouse for Flickable, so we need to prevent stealing
132 // mouse grab too, whenever dragging occurs in an enabled direction
133 parent->setKeepMouseGrab(true);
134 }
135 } else {
136 m_pressTargetPos = QPointF();
137 m_pressedInsideTarget = false;
138 if (auto parent = parentItem()) {
139 parent->setKeepTouchGrab(false);
140 parent->setKeepMouseGrab(false);
141 }
142 }
143}
144
145bool QQuickDragHandler::wantsPointerEvent(QPointerEvent *event)
146{
147 if (!QQuickMultiPointHandler::wantsPointerEvent(event))
148 /* Do handle other events than we would normally care about
149 while we are still doing a drag; otherwise we would suddenly
150 become inactive when a wheel event arrives during dragging.
151 This extra condition needs to be kept in sync with
152 handlePointerEventImpl */
153 if (!active())
154 return false;
155
156#if QT_CONFIG(gestures)
157 if (event->type() == QEvent::NativeGesture)
158 return false;
159#endif
160
161 return true;
162}
163
164void QQuickDragHandler::handlePointerEventImpl(QPointerEvent *event)
165{
166 if (active() && !QQuickMultiPointHandler::wantsPointerEvent(event))
167 return; // see QQuickDragHandler::wantsPointerEvent; we don't want to handle those events
168
169 QQuickMultiPointHandler::handlePointerEventImpl(event);
170 event->setAccepted(true);
171
172 if (active()) {
173 // Calculate drag delta, taking into account the axis enabled constraint
174 // i.e. if xAxis is not enabled, then ignore the horizontal component of the actual movement
175 QVector2D accumulatedDragDelta = QVector2D(centroid().scenePosition() - centroid().scenePressPosition());
176 if (!m_xAxis.enabled())
177 accumulatedDragDelta.setX(0);
178 if (!m_yAxis.enabled())
179 accumulatedDragDelta.setY(0);
180 setActiveTranslation(accumulatedDragDelta);
181 } else {
182 // Check that all points have been dragged past the drag threshold,
183 // to the extent that the constraints allow,
184 // and in approximately the same direction
185 qreal minAngle = 361;
186 qreal maxAngle = -361;
187 bool allOverThreshold = !event->isEndEvent();
188 QVector<QEventPoint> chosenPoints;
189
190 if (event->isBeginEvent())
191 m_pressedInsideTarget = target() && currentPoints().size() > 0;
192
193 for (const QQuickHandlerPoint &p : currentPoints()) {
194 if (!allOverThreshold)
195 break;
196 auto point = event->pointById(id: p.id());
197 Q_ASSERT(point);
198 chosenPoints << *point;
199 setPassiveGrab(event, point: *point);
200 // Calculate drag delta, taking into account the axis enabled constraint
201 // i.e. if xAxis is not enabled, then ignore the horizontal component of the actual movement
202 QVector2D accumulatedDragDelta = QVector2D(point->scenePosition() - point->scenePressPosition());
203 if (!m_xAxis.enabled()) {
204 // If horizontal dragging is disallowed, but the user is dragging
205 // mostly horizontally, then don't activate.
206 if (qAbs(t: accumulatedDragDelta.x()) > qAbs(t: accumulatedDragDelta.y()))
207 accumulatedDragDelta.setY(0);
208 accumulatedDragDelta.setX(0);
209 }
210 if (!m_yAxis.enabled()) {
211 // If vertical dragging is disallowed, but the user is dragging
212 // mostly vertically, then don't activate.
213 if (qAbs(t: accumulatedDragDelta.y()) > qAbs(t: accumulatedDragDelta.x()))
214 accumulatedDragDelta.setX(0);
215 accumulatedDragDelta.setY(0);
216 }
217 qreal angle = std::atan2(y: accumulatedDragDelta.y(), x: accumulatedDragDelta.x()) * 180 / M_PI;
218 bool overThreshold = d_func()->dragOverThreshold(delta: accumulatedDragDelta);
219 qCDebug(lcDragHandler) << "movement" << accumulatedDragDelta << "angle" << angle << "of point" << point
220 << "pressed @" << point->scenePressPosition() << "over threshold?" << overThreshold;
221 minAngle = qMin(a: angle, b: minAngle);
222 maxAngle = qMax(a: angle, b: maxAngle);
223 if (allOverThreshold && !overThreshold)
224 allOverThreshold = false;
225
226 if (event->isBeginEvent()) {
227 // m_pressedInsideTarget should stay true iff ALL points in which DragHandler is interested
228 // have been pressed inside the target() Item. (E.g. in a Slider the parent might be the
229 // whole control while the target is just the knob.)
230 if (target()) {
231 const QPointF localPressPos = target()->mapFromScene(point: point->scenePressPosition());
232 m_pressedInsideTarget &= target()->contains(point: localPressPos);
233 m_pressTargetPos = targetCentroidPosition();
234 }
235 // QQuickDeliveryAgentPrivate::deliverToPassiveGrabbers() skips subsequent delivery if the event is filtered.
236 // (That affects behavior for mouse but not for touch, because Flickable only handles mouse.)
237 // So we have to compensate by accepting the event here to avoid any parent Flickable from
238 // getting the event via direct delivery and grabbing too soon.
239 point->setAccepted(QQuickDeliveryAgentPrivate::isMouseEvent(ev: event)); // stop propagation iff it's a mouse event
240 }
241 }
242 if (allOverThreshold) {
243 qreal angleDiff = maxAngle - minAngle;
244 if (angleDiff > 180)
245 angleDiff = 360 - angleDiff;
246 qCDebug(lcDragHandler) << "angle min" << minAngle << "max" << maxAngle << "range" << angleDiff;
247 if (angleDiff < DragAngleToleranceDegrees && grabPoints(event, points: chosenPoints))
248 setActive(true);
249 }
250 }
251 if (active() && target() && target()->parentItem()) {
252 const QPointF newTargetTopLeft = targetCentroidPosition() - m_pressTargetPos;
253 const QPointF xformOrigin = target()->transformOriginPoint();
254 const QPointF targetXformOrigin = newTargetTopLeft + xformOrigin;
255 QPointF pos = target()->parentItem()->mapFromItem(item: target(), point: targetXformOrigin);
256 pos -= xformOrigin;
257 QPointF targetItemPos = target()->position();
258 if (!m_xAxis.enabled())
259 pos.setX(targetItemPos.x());
260 if (!m_yAxis.enabled())
261 pos.setY(targetItemPos.y());
262 enforceAxisConstraints(localPos: &pos);
263 moveTarget(pos);
264 }
265}
266
267void QQuickDragHandler::enforceAxisConstraints(QPointF *localPos)
268{
269 if (m_xAxis.enabled())
270 localPos->setX(qBound(min: m_xAxis.minimum(), val: localPos->x(), max: m_xAxis.maximum()));
271 if (m_yAxis.enabled())
272 localPos->setY(qBound(min: m_yAxis.minimum(), val: localPos->y(), max: m_yAxis.maximum()));
273}
274
275void QQuickDragHandler::setPersistentTranslation(const QVector2D &trans)
276{
277 if (trans == persistentTranslation())
278 return;
279
280 m_xAxis.updateValue(activeValue: m_xAxis.activeValue(), accumulatedValue: trans.x());
281 m_yAxis.updateValue(activeValue: m_yAxis.activeValue(), accumulatedValue: trans.y());
282 emit translationChanged(delta: {});
283}
284
285void QQuickDragHandler::setActiveTranslation(const QVector2D &trans)
286{
287 if (trans == activeTranslation())
288 return;
289
290 const QVector2D delta = trans - activeTranslation();
291 m_xAxis.updateValue(activeValue: trans.x(), accumulatedValue: m_xAxis.persistentValue() + delta.x(), delta: delta.x());
292 m_yAxis.updateValue(activeValue: trans.y(), accumulatedValue: m_yAxis.persistentValue() + delta.y(), delta: delta.y());
293
294 qCDebug(lcDragHandler) << "translation: delta" << delta
295 << "active" << trans << "accumulated" << persistentTranslation();
296 emit translationChanged(delta);
297}
298
299/*!
300 \qmlpropertygroup QtQuick::DragHandler::xAxis
301 \qmlproperty real QtQuick::DragHandler::xAxis.minimum
302 \qmlproperty real QtQuick::DragHandler::xAxis.maximum
303 \qmlproperty bool QtQuick::DragHandler::xAxis.enabled
304 \qmlproperty real QtQuick::DragHandler::xAxis.activeValue
305
306 \c xAxis controls the constraints for horizontal dragging.
307
308 \c minimum is the minimum acceptable value of \l {Item::x}{x} to be
309 applied to the \l {PointerHandler::target} {target}.
310 \c maximum is the maximum acceptable value of \l {Item::x}{x} to be
311 applied to the \l {PointerHandler::target} {target}.
312 If \c enabled is true, horizontal dragging is allowed.
313 \c activeValue is the same as \l {QtQuick::DragHandler::activeTranslation}{activeTranslation.x}.
314
315 The \c activeValueChanged signal is emitted when \c activeValue changes, to
316 provide the increment by which it changed.
317 This is intended for incrementally adjusting one property via multiple handlers.
318*/
319
320/*!
321 \qmlpropertygroup QtQuick::DragHandler::yAxis
322 \qmlproperty real QtQuick::DragHandler::yAxis.minimum
323 \qmlproperty real QtQuick::DragHandler::yAxis.maximum
324 \qmlproperty bool QtQuick::DragHandler::yAxis.enabled
325 \qmlproperty real QtQuick::DragHandler::yAxis.activeValue
326
327 \c yAxis controls the constraints for vertical dragging.
328
329 \c minimum is the minimum acceptable value of \l {Item::y}{y} to be
330 applied to the \l {PointerHandler::target} {target}.
331 \c maximum is the maximum acceptable value of \l {Item::y}{y} to be
332 applied to the \l {PointerHandler::target} {target}.
333 If \c enabled is true, vertical dragging is allowed.
334 \c activeValue is the same as \l {QtQuick::DragHandler::activeTranslation}{activeTranslation.y}.
335
336 The \c activeValueChanged signal is emitted when \c activeValue changes, to
337 provide the increment by which it changed.
338 This is intended for incrementally adjusting one property via multiple handlers:
339
340 \snippet pointerHandlers/rotateViaWheelOrDrag.qml 0
341*/
342
343/*!
344 \readonly
345 \qmlproperty QVector2D QtQuick::DragHandler::translation
346 \deprecated [6.2] Use activeTranslation
347*/
348
349/*!
350 \qmlproperty QVector2D QtQuick::DragHandler::persistentTranslation
351
352 The translation to be applied to the \l target if it is not \c null.
353 Otherwise, bindings can be used to do arbitrary things with this value.
354 While the drag gesture is being performed, \l activeTranslation is
355 continuously added to it; after the gesture ends, it stays the same.
356*/
357
358/*!
359 \readonly
360 \qmlproperty QVector2D QtQuick::DragHandler::activeTranslation
361
362 The translation while the drag gesture is being performed.
363 It is \c {0, 0} when the gesture begins, and increases as the event
364 point(s) are dragged downward and to the right. After the gesture ends, it
365 stays the same; and when the next drag gesture begins, it is reset to
366 \c {0, 0} again.
367*/
368
369QT_END_NAMESPACE
370
371#include "moc_qquickdraghandler_p.cpp"
372

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