| 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 |  | 
| 54 | QT_BEGIN_NAMESPACE | 
| 55 |  | 
| 56 | Q_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 |  | 
| 98 | QQuickPinchHandler::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 | */ | 
| 109 | void 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 | */ | 
| 124 | void 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 | */ | 
| 139 | void 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 | */ | 
| 154 | void 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) | 
| 164 | void QQuickPinchHandler::warnAboutMinMaxDeprecated() const | 
| 165 | { | 
| 166 |     qmlWarning(me: this) << "min and max constraints are now part of the xAxis and yAxis properties" ; | 
| 167 | } | 
| 168 |  | 
| 169 | void 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 |  | 
| 178 | void 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 |  | 
| 187 | void 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 |  | 
| 196 | void 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 |  | 
| 206 | bool 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 |  | 
| 267 | void 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 |  | 
| 289 | void 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 |  | 
| 548 | QT_END_NAMESPACE | 
| 549 |  |