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

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