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_STATIC_LOGGING_CATEGORY(lcDragHandler, "qt.quick.handler.drag")
14
15/*!
16 \qmltype DragHandler
17 \nativetype 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 DragHandler can be used together with the \l Drag attached property to
54 implement drag-and-drop.
55
56 \sa Drag, MouseArea, {Qt Quick Examples - Pointer Handlers}
57*/
58
59QQuickDragHandler::QQuickDragHandler(QQuickItem *parent)
60 : QQuickMultiPointHandler(parent, 1, 1)
61{
62}
63
64QPointF QQuickDragHandler::targetCentroidPosition()
65{
66 QPointF pos = centroid().position();
67 if (auto par = parentItem()) {
68 if (target() != par)
69 pos = par->mapToItem(item: target(), point: pos);
70 }
71 return pos;
72}
73
74void QQuickDragHandler::onGrabChanged(QQuickPointerHandler *grabber, QPointingDevice::GrabTransition transition, QPointerEvent *event, QEventPoint &point)
75{
76 QQuickMultiPointHandler::onGrabChanged(grabber, transition, event, point);
77 if (grabber == this && transition == QPointingDevice::GrabExclusive && target()) {
78 // In case the grab got handed over from another grabber, we might not get the Press.
79
80 auto isDescendant = [](QQuickItem *parent, QQuickItem *target) {
81 return parent && (target != parent) && !target->isAncestorOf(child: parent);
82 };
83 if (m_snapMode == SnapAlways
84 || (m_snapMode == SnapIfPressedOutsideTarget && !m_pressedInsideTarget)
85 || (m_snapMode == SnapAuto && !m_pressedInsideTarget && isDescendant(parentItem(), target()))
86 ) {
87 m_pressTargetPos = QPointF(target()->width(), target()->height()) / 2;
88 } else if (m_pressTargetPos.isNull()) {
89 m_pressTargetPos = targetCentroidPosition();
90 }
91 }
92}
93
94/*!
95 \qmlproperty enumeration QtQuick::DragHandler::snapMode
96
97 This property holds the snap mode.
98
99 The snap mode configures snapping of the \l target item's center to the \l eventPoint.
100
101 Possible values:
102 \value DragHandler.NoSnap Never snap
103 \value DragHandler.SnapAuto The \l target snaps if the \l eventPoint was pressed outside of the \l target
104 item \e and the \l target is a descendant of \l {PointerHandler::}{parent} item (default)
105 \value DragHandler.SnapWhenPressedOutsideTarget The \l target snaps if the \l eventPoint was pressed outside of the \l target
106 \value DragHandler.SnapAlways Always snap
107*/
108QQuickDragHandler::SnapMode QQuickDragHandler::snapMode() const
109{
110 return m_snapMode;
111}
112
113void QQuickDragHandler::setSnapMode(QQuickDragHandler::SnapMode mode)
114{
115 if (mode == m_snapMode)
116 return;
117 m_snapMode = mode;
118 emit snapModeChanged();
119}
120
121void QQuickDragHandler::onActiveChanged()
122{
123 QQuickMultiPointHandler::onActiveChanged();
124 const bool curActive = active();
125 m_xAxis.onActiveChanged(active: curActive, initActiveValue: 0);
126 m_yAxis.onActiveChanged(active: curActive, initActiveValue: 0);
127 if (curActive) {
128 if (auto parent = parentItem()) {
129 if (QQuickDeliveryAgentPrivate::isTouchEvent(ev: currentEvent()))
130 parent->setKeepTouchGrab(true);
131 // tablet and mouse are treated the same by Item's legacy event handling, and
132 // touch becomes synth-mouse for Flickable, so we need to prevent stealing
133 // mouse grab too, whenever dragging occurs in an enabled direction
134 parent->setKeepMouseGrab(true);
135 }
136 } else {
137 m_pressTargetPos = QPointF();
138 m_pressedInsideTarget = false;
139 m_pressedInsideParent = false;
140 if (auto parent = parentItem()) {
141 parent->setKeepTouchGrab(false);
142 parent->setKeepMouseGrab(false);
143 }
144 }
145}
146
147bool QQuickDragHandler::wantsPointerEvent(QPointerEvent *event)
148{
149 if (!QQuickMultiPointHandler::wantsPointerEvent(event))
150 /* Do handle other events than we would normally care about
151 while we are still doing a drag; otherwise we would suddenly
152 become inactive when a wheel event arrives during dragging.
153 This extra condition needs to be kept in sync with
154 handlePointerEventImpl */
155 if (!active())
156 return false;
157
158#if QT_CONFIG(gestures)
159 if (event->type() == QEvent::NativeGesture)
160 return false;
161#endif
162
163 if (event->isBeginEvent()) {
164 // At least one point must be pressed in the parent item
165 if (event->isSinglePointEvent()) {
166 m_pressedInsideParent = parentContains(point: event->points().first());
167 } else {
168 for (int i = 0; !m_pressedInsideParent && i < event->pointCount(); ++i) {
169 auto &p = event->point(i);
170 if (p.state() == QEventPoint::Pressed && parentContains(point: p))
171 m_pressedInsideParent = true;
172 }
173 }
174 }
175
176 if (!m_pressedInsideParent)
177 return false;
178
179 return true;
180}
181
182void QQuickDragHandler::handlePointerEventImpl(QPointerEvent *event)
183{
184 if (active() && !QQuickMultiPointHandler::wantsPointerEvent(event))
185 return; // see QQuickDragHandler::wantsPointerEvent; we don't want to handle those events
186
187 QQuickMultiPointHandler::handlePointerEventImpl(event);
188 event->accept(); // just the event, not the points
189
190 if (active()) {
191 // Calculate drag delta, taking into account the axis enabled constraint
192 // i.e. if xAxis is not enabled, then ignore the horizontal component of the actual movement
193 QVector2D accumulatedDragDelta = QVector2D(centroid().scenePosition() - centroid().scenePressPosition());
194 if (!m_xAxis.enabled())
195 accumulatedDragDelta.setX(0);
196 if (!m_yAxis.enabled())
197 accumulatedDragDelta.setY(0);
198 setActiveTranslation(accumulatedDragDelta);
199 } else {
200 // Check that all points have been dragged past the drag threshold,
201 // to the extent that the constraints allow,
202 // and in approximately the same direction
203 qreal minAngle = 361;
204 qreal maxAngle = -361;
205 bool allOverThreshold = QQuickDeliveryAgentPrivate::isTouchEvent(ev: event) ?
206 static_cast<QTouchEvent *>(event)->touchPointStates() != QEventPoint::Released :
207 !event->isEndEvent();
208 QVector<QEventPoint> chosenPoints;
209
210 if (event->isBeginEvent())
211 m_pressedInsideTarget = target() && currentPoints().size() > 0;
212
213 for (const QQuickHandlerPoint &p : std::as_const(t&: currentPoints())) {
214 if (!allOverThreshold)
215 break;
216 auto point = event->pointById(id: p.id());
217 Q_ASSERT(point);
218 chosenPoints << *point;
219 setPassiveGrab(event, point: *point);
220 // Calculate drag delta, taking into account the axis enabled constraint
221 // i.e. if xAxis is not enabled, then ignore the horizontal component of the actual movement
222 auto const mapFromScene = [this](auto const &scenePos) {
223 return target() ? target()->mapFromScene(point: scenePos) : scenePos;
224 };
225 QVector2D accumulatedDragDelta = QVector2D(mapFromScene(point->scenePosition())
226 - mapFromScene(point->scenePressPosition()));
227 if (!m_xAxis.enabled()) {
228 // If horizontal dragging is disallowed, but the user is dragging
229 // mostly horizontally, then don't activate.
230 if (qAbs(t: accumulatedDragDelta.x()) > qAbs(t: accumulatedDragDelta.y()))
231 accumulatedDragDelta.setY(0);
232 accumulatedDragDelta.setX(0);
233 }
234 if (!m_yAxis.enabled()) {
235 // If vertical dragging is disallowed, but the user is dragging
236 // mostly vertically, then don't activate.
237 if (qAbs(t: accumulatedDragDelta.y()) > qAbs(t: accumulatedDragDelta.x()))
238 accumulatedDragDelta.setX(0);
239 accumulatedDragDelta.setY(0);
240 }
241 qreal angle = std::atan2(y: accumulatedDragDelta.y(), x: accumulatedDragDelta.x()) * 180 / M_PI;
242 bool overThreshold = d_func()->dragOverThreshold(delta: accumulatedDragDelta);
243 qCDebug(lcDragHandler) << "movement" << accumulatedDragDelta << "angle" << angle << "of point" << point
244 << "pressed @" << point->scenePressPosition() << "over threshold?" << overThreshold;
245 minAngle = qMin(a: angle, b: minAngle);
246 maxAngle = qMax(a: angle, b: maxAngle);
247 if (allOverThreshold && !overThreshold)
248 allOverThreshold = false;
249
250 if (event->isBeginEvent()) {
251 // m_pressedInsideTarget should stay true iff ALL points in which DragHandler is interested
252 // have been pressed inside the target() Item. (E.g. in a Slider the parent might be the
253 // whole control while the target is just the knob.)
254 if (target()) {
255 const QPointF localPressPos = target()->mapFromScene(point: point->scenePressPosition());
256 m_pressedInsideTarget &= target()->contains(point: localPressPos);
257 m_pressTargetPos = targetCentroidPosition();
258 }
259 // QQuickDeliveryAgentPrivate::deliverToPassiveGrabbers() skips subsequent delivery if the event is filtered.
260 // That affects behavior for mouse but not for touch, because Flickable behaves differently in the mouse case.
261 // So we have to compensate by accepting the event here to avoid any parent Flickable from
262 // getting the event via direct delivery and grabbing too soon.
263 if (QQuickDeliveryAgentPrivate::isMouseEvent(ev: event))
264 point->setAccepted(true); // stop propagation iff it's a mouse event
265 }
266 }
267 if (allOverThreshold) {
268 qreal angleDiff = maxAngle - minAngle;
269 if (angleDiff > 180)
270 angleDiff = 360 - angleDiff;
271 qCDebug(lcDragHandler) << "angle min" << minAngle << "max" << maxAngle << "range" << angleDiff;
272 if (angleDiff < DragAngleToleranceDegrees && grabPoints(event, points: chosenPoints))
273 setActive(true);
274 }
275 }
276 if (active() && target() && target()->parentItem()) {
277 const QPointF newTargetTopLeft = targetCentroidPosition() - m_pressTargetPos;
278 const QPointF xformOrigin = target()->transformOriginPoint();
279 const QPointF targetXformOrigin = newTargetTopLeft + xformOrigin;
280 QPointF pos = target()->parentItem()->mapFromItem(item: target(), point: targetXformOrigin);
281 pos -= xformOrigin;
282 QPointF targetItemPos = target()->position();
283 if (!m_xAxis.enabled())
284 pos.setX(targetItemPos.x());
285 if (!m_yAxis.enabled())
286 pos.setY(targetItemPos.y());
287 enforceAxisConstraints(localPos: &pos);
288 moveTarget(pos);
289 }
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::setPersistentTranslation(const QVector2D &trans)
301{
302 if (trans == persistentTranslation())
303 return;
304
305 m_xAxis.updateValue(activeValue: m_xAxis.activeValue(), accumulatedValue: trans.x());
306 m_yAxis.updateValue(activeValue: m_yAxis.activeValue(), accumulatedValue: trans.y());
307 emit translationChanged(delta: {});
308}
309
310void QQuickDragHandler::setActiveTranslation(const QVector2D &trans)
311{
312 if (trans == activeTranslation())
313 return;
314
315 const QVector2D delta = trans - activeTranslation();
316 m_xAxis.updateValue(activeValue: trans.x(), accumulatedValue: m_xAxis.persistentValue() + delta.x(), delta: delta.x());
317 m_yAxis.updateValue(activeValue: trans.y(), accumulatedValue: m_yAxis.persistentValue() + delta.y(), delta: delta.y());
318
319 qCDebug(lcDragHandler) << "translation: delta" << delta
320 << "active" << trans << "accumulated" << persistentTranslation();
321 emit translationChanged(delta);
322}
323
324/*!
325 \qmlpropertygroup QtQuick::DragHandler::xAxis
326 \qmlproperty real QtQuick::DragHandler::xAxis.minimum
327 \qmlproperty real QtQuick::DragHandler::xAxis.maximum
328 \qmlproperty bool QtQuick::DragHandler::xAxis.enabled
329 \qmlproperty real QtQuick::DragHandler::xAxis.activeValue
330
331 \c xAxis controls the constraints for horizontal dragging.
332
333 \c minimum is the minimum acceptable value of \l {Item::x}{x} to be
334 applied to the \l {PointerHandler::target} {target}.
335 \c maximum is the maximum acceptable value of \l {Item::x}{x} to be
336 applied to the \l {PointerHandler::target} {target}.
337 If \c enabled is true, horizontal dragging is allowed.
338 \c activeValue is the same as \l {QtQuick::DragHandler::activeTranslation}{activeTranslation.x}.
339
340 The \c activeValueChanged signal is emitted when \c activeValue changes, to
341 provide the increment by which it changed.
342 This is intended for incrementally adjusting one property via multiple handlers.
343*/
344
345/*!
346 \qmlpropertygroup QtQuick::DragHandler::yAxis
347 \qmlproperty real QtQuick::DragHandler::yAxis.minimum
348 \qmlproperty real QtQuick::DragHandler::yAxis.maximum
349 \qmlproperty bool QtQuick::DragHandler::yAxis.enabled
350 \qmlproperty real QtQuick::DragHandler::yAxis.activeValue
351
352 \c yAxis controls the constraints for vertical dragging.
353
354 \c minimum is the minimum acceptable value of \l {Item::y}{y} to be
355 applied to the \l {PointerHandler::target} {target}.
356 \c maximum is the maximum acceptable value of \l {Item::y}{y} to be
357 applied to the \l {PointerHandler::target} {target}.
358 If \c enabled is true, vertical dragging is allowed.
359 \c activeValue is the same as \l {QtQuick::DragHandler::activeTranslation}{activeTranslation.y}.
360
361 The \c activeValueChanged signal is emitted when \c activeValue changes, to
362 provide the increment by which it changed.
363 This is intended for incrementally adjusting one property via multiple handlers:
364
365 \snippet pointerHandlers/rotateViaWheelOrDrag.qml 0
366*/
367
368/*!
369 \readonly
370 \qmlproperty vector2d QtQuick::DragHandler::translation
371 \deprecated [6.2] Use activeTranslation
372*/
373
374/*!
375 \qmlproperty vector2d QtQuick::DragHandler::persistentTranslation
376
377 The translation to be applied to the \l target if it is not \c null.
378 Otherwise, bindings can be used to do arbitrary things with this value.
379 While the drag gesture is being performed, \l activeTranslation is
380 continuously added to it; after the gesture ends, it stays the same.
381*/
382
383/*!
384 \readonly
385 \qmlproperty vector2d QtQuick::DragHandler::activeTranslation
386
387 The translation while the drag gesture is being performed.
388 It is \c {0, 0} when the gesture begins, and increases as the event
389 point(s) are dragged downward and to the right. After the gesture ends, it
390 stays the same; and when the next drag gesture begins, it is reset to
391 \c {0, 0} again.
392*/
393
394/*!
395 \qmlproperty flags QtQuick::DragHandler::acceptedButtons
396
397 The mouse buttons that can activate this DragHandler.
398
399 By default, this property is set to
400 \l {QtQuick::MouseEvent::button} {Qt.LeftButton}.
401 It can be set to an OR combination of mouse buttons, and will ignore events
402 from other buttons.
403
404 For example, if a component (such as TextEdit) already handles
405 left-button drags in its own way, it can be augmented with a
406 DragHandler that does something different when dragged via the
407 right button:
408
409 \snippet pointerHandlers/dragHandlerAcceptedButtons.qml 0
410*/
411
412/*!
413 \qmlproperty flags DragHandler::acceptedDevices
414
415 The types of pointing devices that can activate this DragHandler.
416
417 By default, this property is set to
418 \l{QInputDevice::DeviceType}{PointerDevice.AllDevices}.
419 If you set it to an OR combination of device types, it will ignore events
420 from non-matching devices.
421
422 \note Not all platforms are yet able to distinguish mouse and touchpad; and
423 on those that do, you often want to make mouse and touchpad behavior the same.
424*/
425
426/*!
427 \qmlproperty flags DragHandler::acceptedModifiers
428
429 If this property is set, it will require the given keyboard modifiers to
430 be pressed in order to react to pointer events, and otherwise ignore them.
431
432 For example, two DragHandlers can perform two different drag-and-drop
433 operations, depending on whether the \c Control modifier is pressed:
434
435 \snippet pointerHandlers/draggableGridView.qml entire
436
437 If this property is set to \c Qt.KeyboardModifierMask (the default value),
438 then the DragHandler ignores the modifier keys.
439
440 If you set \c acceptedModifiers to an OR combination of modifier keys,
441 it means \e all of those modifiers must be pressed to activate the handler.
442
443 The available modifiers are as follows:
444
445 \value NoModifier No modifier key is allowed.
446 \value ShiftModifier A Shift key on the keyboard must be pressed.
447 \value ControlModifier A Ctrl key on the keyboard must be pressed.
448 \value AltModifier An Alt key on the keyboard must be pressed.
449 \value MetaModifier A Meta key on the keyboard must be pressed.
450 \value KeypadModifier A keypad button must be pressed.
451 \value GroupSwitchModifier X11 only (unless activated on Windows by a command line argument).
452 A Mode_switch key on the keyboard must be pressed.
453 \value KeyboardModifierMask The handler does not care which modifiers are pressed.
454
455 \sa Qt::KeyboardModifier
456*/
457
458/*!
459 \qmlproperty flags DragHandler::acceptedPointerTypes
460
461 The types of pointing instruments (finger, stylus, eraser, etc.)
462 that can activate this DragHandler.
463
464 By default, this property is set to
465 \l {QPointingDevice::PointerType} {PointerDevice.AllPointerTypes}.
466 If you set it to an OR combination of device types, it will ignore events
467 from non-matching \l {PointerDevice}{devices}.
468*/
469
470/*!
471 \qmlproperty real DragHandler::margin
472
473 The margin beyond the bounds of the \l {PointerHandler::parent}{parent}
474 item within which an \l eventPoint can activate this handler. For example,
475 you can make it easier to drag small items by allowing the user to drag
476 from a position nearby:
477
478 \snippet pointerHandlers/dragHandlerMargin.qml draggable
479*/
480
481QT_END_NAMESPACE
482
483#include "moc_qquickdraghandler_p.cpp"
484

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