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