1/****************************************************************************
2**
3** Copyright (C) 2016 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 "qquickpinchhandler_p.h"
41#include <QtQml/qqmlinfo.h>
42#include <QtQuick/qquickwindow.h>
43#include <private/qsgadaptationlayer_p.h>
44#include <private/qquickitem_p.h>
45#include <private/qguiapplication_p.h>
46#include <private/qquickwindow_p.h>
47#include <QEvent>
48#include <QMouseEvent>
49#include <QDebug>
50#include <qpa/qplatformnativeinterface.h>
51#include <math.h>
52
53QT_BEGIN_NAMESPACE
54
55Q_LOGGING_CATEGORY(lcPinchHandler, "qt.quick.handler.pinch")
56
57/*!
58 \qmltype PinchHandler
59 \instantiates QQuickPinchHandler
60 \inherits MultiPointHandler
61 \inqmlmodule QtQuick
62 \ingroup qtquick-input-handlers
63 \brief Handler for pinch gestures.
64
65 PinchHandler is a handler that interprets a multi-finger gesture to
66 interactively rotate, zoom, and drag an Item. Like other Input Handlers,
67 by default it is fully functional, and manipulates its \l target,
68 which is the Item within which it is declared.
69
70 \snippet pointerHandlers/pinchHandler.qml 0
71
72 It has properties to restrict the range of dragging, rotation, and zoom.
73
74 If it is declared within one Item but is assigned a different \l target, it
75 handles events within the bounds of the outer Item but manipulates the
76 \c target Item instead:
77
78 \snippet pointerHandlers/pinchHandlerDifferentTarget.qml 0
79
80 A third way to use it is to set \l target to \c null and react to property
81 changes in some other way:
82
83 \snippet pointerHandlers/pinchHandlerNullTarget.qml 0
84
85 \image touchpoints-pinchhandler.png
86
87 \sa PinchArea
88*/
89
90QQuickPinchHandler::QQuickPinchHandler(QQuickItem *parent)
91 : QQuickMultiPointHandler(parent, 2)
92{
93}
94
95/*!
96 \qmlproperty real QtQuick::PinchHandler::minimumScale
97
98 The minimum acceptable \l {Item::scale}{scale} to be applied
99 to the \l target.
100*/
101void QQuickPinchHandler::setMinimumScale(qreal minimumScale)
102{
103 if (qFuzzyCompare(m_minimumScale, minimumScale))
104 return;
105
106 m_minimumScale = minimumScale;
107 emit minimumScaleChanged();
108}
109
110/*!
111 \qmlproperty real QtQuick::PinchHandler::maximumScale
112
113 The maximum acceptable \l {Item::scale}{scale} to be applied
114 to the \l target.
115*/
116void QQuickPinchHandler::setMaximumScale(qreal maximumScale)
117{
118 if (qFuzzyCompare(m_maximumScale, maximumScale))
119 return;
120
121 m_maximumScale = maximumScale;
122 emit maximumScaleChanged();
123}
124
125/*!
126 \qmlproperty real QtQuick::PinchHandler::minimumRotation
127
128 The minimum acceptable \l {Item::rotation}{rotation} to be applied
129 to the \l target.
130*/
131void QQuickPinchHandler::setMinimumRotation(qreal minimumRotation)
132{
133 if (qFuzzyCompare(m_minimumRotation, minimumRotation))
134 return;
135
136 m_minimumRotation = minimumRotation;
137 emit minimumRotationChanged();
138}
139
140/*!
141 \qmlproperty real QtQuick::PinchHandler::maximumRotation
142
143 The maximum acceptable \l {Item::rotation}{rotation} to be applied
144 to the \l target.
145*/
146void QQuickPinchHandler::setMaximumRotation(qreal maximumRotation)
147{
148 if (qFuzzyCompare(m_maximumRotation, maximumRotation))
149 return;
150
151 m_maximumRotation = maximumRotation;
152 emit maximumRotationChanged();
153}
154
155#if QT_DEPRECATED_SINCE(5, 12)
156void QQuickPinchHandler::warnAboutMinMaxDeprecated() const
157{
158 qmlWarning(this) << "min and max constraints are now part of the xAxis and yAxis properties";
159}
160
161void QQuickPinchHandler::setMinimumX(qreal minX)
162{
163 warnAboutMinMaxDeprecated();
164 if (qFuzzyCompare(m_minimumX, minX))
165 return;
166 m_minimumX = minX;
167 emit minimumXChanged();
168}
169
170void QQuickPinchHandler::setMaximumX(qreal maxX)
171{
172 warnAboutMinMaxDeprecated();
173 if (qFuzzyCompare(m_maximumX, maxX))
174 return;
175 m_maximumX = maxX;
176 emit maximumXChanged();
177}
178
179void QQuickPinchHandler::setMinimumY(qreal minY)
180{
181 warnAboutMinMaxDeprecated();
182 if (qFuzzyCompare(m_minimumY, minY))
183 return;
184 m_minimumY = minY;
185 emit minimumYChanged();
186}
187
188void QQuickPinchHandler::setMaximumY(qreal maxY)
189{
190 warnAboutMinMaxDeprecated();
191 if (qFuzzyCompare(m_maximumY, maxY))
192 return;
193 m_maximumY = maxY;
194 emit maximumYChanged();
195}
196#endif
197
198bool QQuickPinchHandler::wantsPointerEvent(QQuickPointerEvent *event)
199{
200 if (!QQuickMultiPointHandler::wantsPointerEvent(event))
201 return false;
202
203#if QT_CONFIG(gestures)
204 if (const auto gesture = event->asPointerNativeGestureEvent()) {
205 if (minimumPointCount() == 2) {
206 switch (gesture->type()) {
207 case Qt::BeginNativeGesture:
208 case Qt::EndNativeGesture:
209 case Qt::ZoomNativeGesture:
210 case Qt::RotateNativeGesture:
211 return parentContains(event->point(0));
212 default:
213 return false;
214 }
215 } else {
216 return false;
217 }
218 }
219#endif
220
221 return true;
222}
223
224/*!
225 \qmlpropertygroup QtQuick::PinchHandler::xAxis
226 \qmlproperty real QtQuick::PinchHandler::xAxis.minimum
227 \qmlproperty real QtQuick::PinchHandler::xAxis.maximum
228 \qmlproperty bool QtQuick::PinchHandler::xAxis.enabled
229
230 \c xAxis controls the constraints for horizontal translation of the \l target item.
231
232 \c minimum is the minimum acceptable x coordinate of the translation.
233 \c maximum is the maximum acceptable x coordinate of the translation.
234 If \c enabled is true, horizontal dragging is allowed.
235 */
236
237/*!
238 \qmlpropertygroup QtQuick::PinchHandler::yAxis
239 \qmlproperty real QtQuick::PinchHandler::yAxis.minimum
240 \qmlproperty real QtQuick::PinchHandler::yAxis.maximum
241 \qmlproperty bool QtQuick::PinchHandler::yAxis.enabled
242
243 \c yAxis controls the constraints for vertical translation of the \l target item.
244
245 \c minimum is the minimum acceptable y coordinate of the translation.
246 \c maximum is the maximum acceptable y coordinate of the translation.
247 If \c enabled is true, vertical dragging is allowed.
248 */
249
250/*!
251 \qmlproperty int QtQuick::PinchHandler::minimumTouchPoints
252
253 The pinch begins when this number of fingers are pressed.
254 Until then, PinchHandler tracks the positions of any pressed fingers,
255 but if it's an insufficient number, it does not scale or rotate
256 its \l target, and the \l active property will remain false.
257*/
258
259/*!
260 \qmlproperty bool QtQuick::PinchHandler::active
261
262 This property is true when all the constraints (epecially \l minimumTouchPoints)
263 are satisfied and the \l target, if any, is being manipulated.
264*/
265
266void QQuickPinchHandler::onActiveChanged()
267{
268 QQuickMultiPointHandler::onActiveChanged();
269 if (active()) {
270 m_startAngles = angles(centroid().sceneGrabPosition());
271 m_startDistance = averageTouchPointDistance(centroid().sceneGrabPosition());
272 m_activeRotation = 0;
273 m_activeTranslation = QVector2D();
274 if (const QQuickItem *t = target()) {
275 m_startScale = t->scale(); // TODO incompatible with independent x/y scaling
276 m_startRotation = t->rotation();
277 m_startPos = t->position();
278 } else {
279 m_startScale = m_accumulatedScale;
280 m_startRotation = 0;
281 }
282 qCDebug(lcPinchHandler) << "activated with starting scale" << m_startScale << "rotation" << m_startRotation;
283 } else {
284 qCDebug(lcPinchHandler) << "deactivated with scale" << m_activeScale << "rotation" << m_activeRotation;
285 }
286}
287
288void QQuickPinchHandler::handlePointerEventImpl(QQuickPointerEvent *event)
289{
290 if (Q_UNLIKELY(lcPinchHandler().isDebugEnabled())) {
291 for (const QQuickHandlerPoint &p : currentPoints())
292 qCDebug(lcPinchHandler) << hex << p.id() << p.sceneGrabPosition() << "->" << p.scenePosition();
293 }
294 QQuickMultiPointHandler::handlePointerEventImpl(event);
295
296 qreal dist = 0;
297#if QT_CONFIG(gestures)
298 if (const auto gesture = event->asPointerNativeGestureEvent()) {
299 mutableCentroid().reset(event->point(0));
300 switch (gesture->type()) {
301 case Qt::EndNativeGesture:
302 m_activeScale = 1;
303 m_activeRotation = 0;
304 m_activeTranslation = QVector2D();
305 mutableCentroid().reset();
306 setActive(false);
307 emit updated();
308 return;
309 case Qt::ZoomNativeGesture:
310 m_activeScale *= 1 + gesture->value();
311 break;
312 case Qt::RotateNativeGesture:
313 m_activeRotation += gesture->value();
314 break;
315 default:
316 // Nothing of interest (which is unexpected, because wantsPointerEvent() should have returned false)
317 return;
318 }
319 if (!active()) {
320 setActive(true);
321 // Native gestures for 2-finger pinch do not allow dragging, so
322 // the centroid won't move during the gesture, and translation stays at zero
323 m_activeTranslation = QVector2D();
324 }
325 } else
326#endif // QT_CONFIG(gestures)
327 {
328 const bool containsReleasedPoints = event->isReleaseEvent();
329 QVector<QQuickEventPoint *> chosenPoints;
330 for (const QQuickHandlerPoint &p : currentPoints()) {
331 QQuickEventPoint *ep = event->pointById(p.id());
332 chosenPoints << ep;
333 }
334 if (!active()) {
335 // Verify that at least one of the points has moved beyond threshold needed to activate the handler
336 int numberOfPointsDraggedOverThreshold = 0;
337 QVector2D accumulatedDrag;
338 const QVector2D currentCentroid(centroid().scenePosition());
339 const QVector2D pressCentroid(centroid().scenePressPosition());
340
341 QStyleHints *styleHints = QGuiApplication::styleHints();
342 const int dragThreshold = styleHints->startDragDistance();
343 const int dragThresholdSquared = dragThreshold * dragThreshold;
344
345 double accumulatedCentroidDistance = 0; // Used to detect scale
346 if (event->isPressEvent())
347 m_accumulatedStartCentroidDistance = 0; // Used to detect scale
348
349 float accumulatedMovementMagnitude = 0;
350
351 for (QQuickEventPoint *point : qAsConst(chosenPoints)) {
352 if (!containsReleasedPoints) {
353 accumulatedDrag += QVector2D(point->scenePressPosition() - point->scenePosition());
354 /*
355 In order to detect a drag, we want to check if all points have moved more or
356 less in the same direction.
357
358 We then take each point, and convert the point to a local coordinate system where
359 the centroid is the origin. This is done both for the press positions and the
360 current positions. We will then have two positions:
361
362 - pressCentroidRelativePosition
363 is the start point relative to the press centroid
364 - currentCentroidRelativePosition
365 is the current point relative to the current centroid
366
367 If those two points are far enough apart, it might not be considered as a drag
368 anymore. (Note that the threshold will matched to the average of the relative
369 movement of all the points). Therefore, a big relative movement will make a big
370 contribution to the average relative movement.
371
372 The algorithm then can be described as:
373 For each point:
374 - Calculate vector pressCentroidRelativePosition (from the press centroid to the press position)
375 - Calculate vector currentCentroidRelativePosition (from the current centroid to the current position)
376 - Calculate the relative movement vector:
377
378 centroidRelativeMovement = currentCentroidRelativePosition - pressCentroidRelativePosition
379
380 and measure its magnitude. Add the magnitude to the accumulatedMovementMagnitude.
381
382 Finally, if the accumulatedMovementMagnitude is below some threshold, it means
383 that the points were stationary or they were moved in parallel (e.g. the hand
384 was moved, but the relative position between each finger remained very much
385 the same). This is then used to rule out if there is a rotation or scale.
386 */
387 QVector2D pressCentroidRelativePosition = QVector2D(point->scenePosition()) - currentCentroid;
388 QVector2D currentCentroidRelativePosition = QVector2D(point->scenePressPosition()) - pressCentroid;
389 QVector2D centroidRelativeMovement = currentCentroidRelativePosition - pressCentroidRelativePosition;
390 accumulatedMovementMagnitude += centroidRelativeMovement.length();
391
392 accumulatedCentroidDistance += qreal(pressCentroidRelativePosition.length());
393 if (event->isPressEvent())
394 m_accumulatedStartCentroidDistance += qreal((QVector2D(point->scenePressPosition()) - pressCentroid).length());
395 } else {
396 setPassiveGrab(point);
397 }
398 if (point->state() == QQuickEventPoint::Pressed) {
399 point->setAccepted(false); // don't stop propagation
400 setPassiveGrab(point);
401 }
402 if (QQuickWindowPrivate::dragOverThreshold(point))
403 ++numberOfPointsDraggedOverThreshold;
404 }
405
406 const bool requiredNumberOfPointsDraggedOverThreshold = numberOfPointsDraggedOverThreshold >= minimumPointCount() && numberOfPointsDraggedOverThreshold <= maximumPointCount();
407 accumulatedMovementMagnitude /= currentPoints().count();
408
409 QVector2D avgDrag = accumulatedDrag / currentPoints().count();
410 if (!xAxis()->enabled())
411 avgDrag.setX(0);
412 if (!yAxis()->enabled())
413 avgDrag.setY(0);
414
415 const qreal centroidMovementDelta = qreal((currentCentroid - pressCentroid).length());
416
417 qreal distanceToCentroidDelta = qAbs(accumulatedCentroidDistance - m_accumulatedStartCentroidDistance); // Used to detect scale
418 if (numberOfPointsDraggedOverThreshold >= 1) {
419 if (requiredNumberOfPointsDraggedOverThreshold && avgDrag.lengthSquared() >= dragThresholdSquared && accumulatedMovementMagnitude < dragThreshold) {
420 // Drag
421 if (grabPoints(chosenPoints))
422 setActive(true);
423 } else if (distanceToCentroidDelta > dragThreshold) { // all points should in accumulation have been moved beyond threshold (?)
424 // Scale
425 if (grabPoints(chosenPoints))
426 setActive(true);
427 } else if (distanceToCentroidDelta < dragThreshold && (centroidMovementDelta < dragThreshold)) {
428 // Rotate
429 // Since it wasn't a scale and if we exceeded the dragthreshold, and the
430 // centroid didn't moved much, the points must have been moved around the centroid.
431 if (grabPoints(chosenPoints))
432 setActive(true);
433 }
434 }
435 if (!active())
436 return;
437 }
438
439 // avoid mapping the minima and maxima, as they might have unmappable values
440 // such as -inf/+inf. Because of this we perform the bounding to min/max in local coords.
441 // 1. scale
442 dist = averageTouchPointDistance(centroid().scenePosition());
443 m_activeScale = dist / m_startDistance;
444 m_activeScale = qBound(m_minimumScale/m_startScale, m_activeScale, m_maximumScale/m_startScale);
445
446 // 2. rotate
447 QVector<PointData> newAngles = angles(centroid().scenePosition());
448 const qreal angleDelta = averageAngleDelta(m_startAngles, newAngles);
449 m_activeRotation += angleDelta;
450 m_startAngles = std::move(newAngles);
451
452 if (!containsReleasedPoints)
453 acceptPoints(chosenPoints);
454 }
455
456 const qreal totalRotation = m_startRotation + m_activeRotation;
457 const qreal rotation = qBound(m_minimumRotation, totalRotation, m_maximumRotation);
458 m_activeRotation += (rotation - totalRotation); //adjust for the potential bounding above
459 m_accumulatedScale = m_startScale * m_activeScale;
460
461 if (target() && target()->parentItem()) {
462 const QPointF centroidParentPos = target()->parentItem()->mapFromScene(centroid().scenePosition());
463 // 3. Drag/translate
464 const QPointF centroidStartParentPos = target()->parentItem()->mapFromScene(centroid().sceneGrabPosition());
465 m_activeTranslation = QVector2D(centroidParentPos - centroidStartParentPos);
466 // apply rotation + scaling around the centroid - then apply translation.
467 QPointF pos = QQuickItemPrivate::get(target())->adjustedPosForTransform(centroidParentPos,
468 m_startPos, m_activeTranslation,
469 m_startScale, m_activeScale,
470 m_startRotation, m_activeRotation);
471
472 if (xAxis()->enabled())
473 pos.setX(qBound(xAxis()->minimum(), pos.x(), xAxis()->maximum()));
474 else
475 pos.rx() -= qreal(m_activeTranslation.x());
476 if (yAxis()->enabled())
477 pos.setY(qBound(yAxis()->minimum(), pos.y(), yAxis()->maximum()));
478 else
479 pos.ry() -= qreal(m_activeTranslation.y());
480
481 target()->setPosition(pos);
482 target()->setRotation(rotation);
483 target()->setScale(m_accumulatedScale);
484 } else {
485 m_activeTranslation = QVector2D(centroid().scenePosition() - centroid().scenePressPosition());
486 }
487
488 qCDebug(lcPinchHandler) << "centroid" << centroid().scenePressPosition() << "->" << centroid().scenePosition()
489 << ", distance" << m_startDistance << "->" << dist
490 << ", startScale" << m_startScale << "->" << m_accumulatedScale
491 << ", activeRotation" << m_activeRotation
492 << ", rotation" << rotation
493 << " from " << event->device()->type();
494
495 emit updated();
496}
497
498/*!
499 \readonly
500 \qmlproperty QtQuick::HandlerPoint QtQuick::PinchHandler::centroid
501
502 A point exactly in the middle of the currently-pressed touch points.
503 The \l target will be rotated around this point.
504*/
505
506/*!
507 \readonly
508 \qmlproperty real QtQuick::PinchHandler::scale
509
510 The scale factor that will automatically be set on the \l target if it is not null.
511 Otherwise, bindings can be used to do arbitrary things with this value.
512 While the pinch gesture is being performed, it is continuously multiplied by
513 \l activeScale; after the gesture ends, it stays the same; and when the next
514 pinch gesture begins, it begins to be multiplied by activeScale again.
515*/
516
517/*!
518 \readonly
519 \qmlproperty real QtQuick::PinchHandler::activeScale
520
521 The scale factor while the pinch gesture is being performed.
522 It is 1.0 when the gesture begins, increases as the touchpoints are spread
523 apart, and decreases as the touchpoints are brought together.
524 If \l target is not null, its \l {Item::scale}{scale} will be automatically
525 multiplied by this value.
526 Otherwise, bindings can be used to do arbitrary things with this value.
527*/
528
529/*!
530 \readonly
531 \qmlproperty real QtQuick::PinchHandler::rotation
532
533 The rotation of the pinch gesture in degrees, with positive values clockwise.
534 It is 0 when the gesture begins. If \l target is not null, this will be
535 automatically applied to its \l {Item::rotation}{rotation}. Otherwise,
536 bindings can be used to do arbitrary things with this value.
537*/
538
539/*!
540 \readonly
541 \qmlproperty QVector2D QtQuick::PinchHandler::translation
542
543 The translation of the gesture \l centroid. It is \c (0, 0) when the
544 gesture begins.
545*/
546
547QT_END_NAMESPACE
548