1 | // Copyright (C) 2020 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 "qquickpincharea_p_p.h" |
5 | #include "qquickwindow.h" |
6 | |
7 | #include <QtCore/qmath.h> |
8 | #include <QtGui/qevent.h> |
9 | #include <QtGui/qguiapplication.h> |
10 | #include <QtGui/qstylehints.h> |
11 | #include <qpa/qplatformintegration.h> |
12 | #include <qpa/qplatformnativeinterface.h> |
13 | #include <private/qguiapplication_p.h> |
14 | #include <QVariant> |
15 | |
16 | #include <float.h> |
17 | |
18 | QT_BEGIN_NAMESPACE |
19 | |
20 | Q_LOGGING_CATEGORY(lcPA, "qt.quick.pincharea" ) |
21 | |
22 | /*! |
23 | \qmltype PinchEvent |
24 | \instantiates QQuickPinchEvent |
25 | \inqmlmodule QtQuick |
26 | \ingroup qtquick-input-events |
27 | \brief For specifying information about a pinch event. |
28 | |
29 | \b {The PinchEvent type was added in QtQuick 1.1} |
30 | |
31 | The \c center, \c startCenter, \c previousCenter properties provide the center position between the two touch points. |
32 | |
33 | The \c scale and \c previousScale properties provide the scale factor. |
34 | |
35 | The \c angle, \c previousAngle and \c rotation properties provide the angle between the two points and the amount of rotation. |
36 | |
37 | The \c point1, \c point2, \c startPoint1, \c startPoint2 properties provide the positions of the touch points. |
38 | |
39 | The \c accepted property may be set to false in the \c onPinchStarted handler if the gesture should not |
40 | be handled. |
41 | |
42 | \sa PinchArea |
43 | */ |
44 | |
45 | /*! |
46 | \qmlproperty QPointF QtQuick::PinchEvent::center |
47 | \qmlproperty QPointF QtQuick::PinchEvent::startCenter |
48 | \qmlproperty QPointF QtQuick::PinchEvent::previousCenter |
49 | |
50 | These properties hold the position of the center point between the two touch points. |
51 | |
52 | \list |
53 | \li \c center is the current center point |
54 | \li \c previousCenter is the center point of the previous event. |
55 | \li \c startCenter is the center point when the gesture began |
56 | \endlist |
57 | */ |
58 | |
59 | /*! |
60 | \qmlproperty real QtQuick::PinchEvent::scale |
61 | \qmlproperty real QtQuick::PinchEvent::previousScale |
62 | |
63 | These properties hold the scale factor determined by the change in distance between the two touch points. |
64 | |
65 | \list |
66 | \li \c scale is the current scale factor. |
67 | \li \c previousScale is the scale factor of the previous event. |
68 | \endlist |
69 | |
70 | When a pinch gesture is started, the scale is \c 1.0. |
71 | */ |
72 | |
73 | /*! |
74 | \qmlproperty real QtQuick::PinchEvent::angle |
75 | \qmlproperty real QtQuick::PinchEvent::previousAngle |
76 | \qmlproperty real QtQuick::PinchEvent::rotation |
77 | |
78 | These properties hold the angle between the two touch points. |
79 | |
80 | \list |
81 | \li \c angle is the current angle between the two points in the range -180 to 180. |
82 | \li \c previousAngle is the angle of the previous event. |
83 | \li \c rotation is the total rotation since the pinch gesture started. |
84 | \endlist |
85 | |
86 | When a pinch gesture is started, the rotation is \c 0.0. |
87 | */ |
88 | |
89 | /*! |
90 | \qmlproperty QPointF QtQuick::PinchEvent::point1 |
91 | \qmlproperty QPointF QtQuick::PinchEvent::startPoint1 |
92 | \qmlproperty QPointF QtQuick::PinchEvent::point2 |
93 | \qmlproperty QPointF QtQuick::PinchEvent::startPoint2 |
94 | |
95 | These properties provide the actual touch points generating the pinch. |
96 | |
97 | \list |
98 | \li \c point1 and \c point2 hold the current positions of the points. |
99 | \li \c startPoint1 and \c startPoint2 hold the positions of the points when the second point was touched. |
100 | \endlist |
101 | */ |
102 | |
103 | /*! |
104 | \qmlproperty bool QtQuick::PinchEvent::accepted |
105 | |
106 | Setting this property to false in the \c PinchArea::onPinchStarted handler |
107 | will result in no further pinch events being generated, and the gesture |
108 | ignored. |
109 | */ |
110 | |
111 | /*! |
112 | \qmlproperty int QtQuick::PinchEvent::pointCount |
113 | |
114 | Holds the number of points currently touched. The PinchArea will not react |
115 | until two touch points have initited a gesture, but will remain active until |
116 | all touch points have been released. |
117 | */ |
118 | |
119 | QQuickPinch::QQuickPinch() |
120 | : m_target(nullptr), m_minScale(1.0), m_maxScale(1.0) |
121 | , m_minRotation(0.0), m_maxRotation(0.0) |
122 | , m_axis(NoDrag), m_xmin(-FLT_MAX), m_xmax(FLT_MAX) |
123 | , m_ymin(-FLT_MAX), m_ymax(FLT_MAX), m_active(false) |
124 | { |
125 | } |
126 | |
127 | QQuickPinchAreaPrivate::~QQuickPinchAreaPrivate() |
128 | { |
129 | delete pinch; |
130 | } |
131 | |
132 | /*! |
133 | \qmltype PinchArea |
134 | \instantiates QQuickPinchArea |
135 | \inqmlmodule QtQuick |
136 | \ingroup qtquick-input |
137 | \inherits Item |
138 | \brief Enables simple pinch gesture handling. |
139 | |
140 | \b {The PinchArea type was added in QtQuick 1.1} |
141 | |
142 | A PinchArea is an invisible item that is typically used in conjunction with |
143 | a visible item in order to provide pinch gesture handling for that item. |
144 | |
145 | The \l enabled property is used to enable and disable pinch handling for |
146 | the proxied item. When disabled, the pinch area becomes transparent to |
147 | mouse/touch events. |
148 | |
149 | PinchArea can be used in two ways: |
150 | |
151 | \list |
152 | \li setting a \c pinch.target to provide automatic interaction with an item |
153 | \li using the onPinchStarted, onPinchUpdated and onPinchFinished handlers |
154 | \endlist |
155 | |
156 | Since Qt 5.5, PinchArea can react to native pinch gesture events from the |
157 | operating system if available; otherwise it reacts only to touch events. |
158 | |
159 | \sa PinchEvent, QNativeGestureEvent, QTouchEvent |
160 | */ |
161 | |
162 | /*! |
163 | \qmlsignal QtQuick::PinchArea::pinchStarted(PinchEvent pinch) |
164 | |
165 | This signal is emitted when the pinch area detects that a pinch gesture has |
166 | started: two touch points (fingers) have been detected, and they have moved |
167 | beyond the \l {QStyleHints}{startDragDistance} threshold for the gesture to begin. |
168 | |
169 | The \a pinch parameter (not the same as the \l {PinchArea}{pinch} |
170 | property) provides information about the pinch gesture, including the scale, |
171 | center and angle of the pinch. At the time of the \c pinchStarted signal, |
172 | these values are reset to the default values, regardless of the results |
173 | from previous gestures: pinch.scale will be \c 1.0 and pinch.rotation will be \c 0.0. |
174 | As the gesture progresses, \l pinchUpdated will report the deviation from those |
175 | defaults. |
176 | |
177 | To ignore this gesture set the \c pinch.accepted property to false. The gesture |
178 | will be canceled and no further events will be sent. |
179 | */ |
180 | |
181 | /*! |
182 | \qmlsignal QtQuick::PinchArea::pinchUpdated(PinchEvent pinch) |
183 | |
184 | This signal is emitted when the pinch area detects that a pinch gesture has changed. |
185 | |
186 | The \a pinch parameter provides information about the pinch |
187 | gesture, including the scale, center and angle of the pinch. These values |
188 | reflect changes only since the beginning of the current gesture, and |
189 | therefore are not limited by the minimum and maximum limits in the |
190 | \l {PinchArea}{pinch} property. |
191 | */ |
192 | |
193 | /*! |
194 | \qmlsignal QtQuick::PinchArea::pinchFinished(PinchEvent pinch) |
195 | |
196 | This signal is emitted when the pinch area detects that a pinch gesture has finished. |
197 | |
198 | The \a pinch parameter (not the same as the \l {PinchArea}{pinch} |
199 | property) provides information about the pinch gesture, including the |
200 | scale, center and angle of the pinch. |
201 | */ |
202 | |
203 | /*! |
204 | \qmlsignal QtQuick::PinchArea::smartZoom(PinchEvent pinch) |
205 | \since 5.5 |
206 | |
207 | This signal is emitted when the pinch area detects a smart zoom gesture. |
208 | This gesture occurs only on certain operating systems such as \macos. |
209 | |
210 | The \a pinch parameter provides information about the pinch |
211 | gesture, including the location where the gesture occurred. \c pinch.scale |
212 | will be greater than zero when the gesture indicates that the user wishes to |
213 | enter smart zoom, and zero when exiting (even though typically the same gesture |
214 | is used to toggle between the two states). |
215 | */ |
216 | |
217 | |
218 | /*! |
219 | \qmlpropertygroup QtQuick::PinchArea::pinch |
220 | \qmlproperty Item QtQuick::PinchArea::pinch.target |
221 | \qmlproperty bool QtQuick::PinchArea::pinch.active |
222 | \qmlproperty real QtQuick::PinchArea::pinch.minimumScale |
223 | \qmlproperty real QtQuick::PinchArea::pinch.maximumScale |
224 | \qmlproperty real QtQuick::PinchArea::pinch.minimumRotation |
225 | \qmlproperty real QtQuick::PinchArea::pinch.maximumRotation |
226 | \qmlproperty enumeration QtQuick::PinchArea::pinch.dragAxis |
227 | \qmlproperty real QtQuick::PinchArea::pinch.minimumX |
228 | \qmlproperty real QtQuick::PinchArea::pinch.maximumX |
229 | \qmlproperty real QtQuick::PinchArea::pinch.minimumY |
230 | \qmlproperty real QtQuick::PinchArea::pinch.maximumY |
231 | |
232 | \c pinch provides a convenient way to make an item react to pinch gestures. |
233 | |
234 | \list |
235 | \li \c pinch.target specifies the id of the item to drag. |
236 | \li \c pinch.active specifies if the target item is currently being dragged. |
237 | \li \c pinch.minimumScale and \c pinch.maximumScale limit the range of the Item.scale property, but not the \c PinchEvent \l {PinchEvent}{scale} property. |
238 | \li \c pinch.minimumRotation and \c pinch.maximumRotation limit the range of the Item.rotation property, but not the \c PinchEvent \l {PinchEvent}{rotation} property. |
239 | \li \c pinch.dragAxis specifies whether dragging in not allowed (\c Pinch.NoDrag), can be done horizontally (\c Pinch.XAxis), vertically (\c Pinch.YAxis), or both (\c Pinch.XAndYAxis) |
240 | \li \c pinch.minimum and \c pinch.maximum limit how far the target can be dragged along the corresponding axes. |
241 | \endlist |
242 | */ |
243 | |
244 | QQuickPinchArea::QQuickPinchArea(QQuickItem *parent) |
245 | : QQuickItem(*(new QQuickPinchAreaPrivate), parent) |
246 | { |
247 | Q_D(QQuickPinchArea); |
248 | d->init(); |
249 | setAcceptTouchEvents(true); |
250 | #ifdef Q_OS_MACOS |
251 | setAcceptHoverEvents(true); // needed to enable touch events on mouse hover. |
252 | #endif |
253 | } |
254 | |
255 | QQuickPinchArea::~QQuickPinchArea() |
256 | { |
257 | } |
258 | /*! |
259 | \qmlproperty bool QtQuick::PinchArea::enabled |
260 | This property holds whether the item accepts pinch gestures. |
261 | |
262 | This property defaults to true. |
263 | */ |
264 | bool QQuickPinchArea::isEnabled() const |
265 | { |
266 | Q_D(const QQuickPinchArea); |
267 | return d->enabled; |
268 | } |
269 | |
270 | void QQuickPinchArea::setEnabled(bool a) |
271 | { |
272 | Q_D(QQuickPinchArea); |
273 | if (a != d->enabled) { |
274 | d->enabled = a; |
275 | emit enabledChanged(); |
276 | } |
277 | } |
278 | |
279 | void QQuickPinchArea::touchEvent(QTouchEvent *event) |
280 | { |
281 | Q_D(QQuickPinchArea); |
282 | if (!d->enabled || !isVisible()) { |
283 | QQuickItem::touchEvent(event); |
284 | return; |
285 | } |
286 | |
287 | // A common non-trivial starting scenario is the user puts down one finger, |
288 | // then that finger remains stationary while putting down a second one. |
289 | // However QQuickWindow will not send TouchUpdates for TouchPoints which |
290 | // were not initially accepted; that would be inefficient and noisy. |
291 | // So even if there is only one touchpoint so far, it's important to accept it |
292 | // in order to get updates later on (and it's accepted by default anyway). |
293 | // If the user puts down one finger, we're waiting for the other finger to drop. |
294 | // Therefore updatePinch() must do the right thing for any combination of |
295 | // points and states that may occur, and there is no reason to ignore any event. |
296 | // One consequence though is that if PinchArea is on top of something else, |
297 | // it's always going to accept the touches, and that means the item underneath |
298 | // will not get them (unless the PA's parent is doing parent filtering, |
299 | // as the Flickable does, for example). |
300 | switch (event->type()) { |
301 | case QEvent::TouchBegin: |
302 | case QEvent::TouchUpdate: |
303 | d->touchPoints.clear(); |
304 | for (int i = 0; i < event->pointCount(); ++i) { |
305 | auto &tp = event->point(i); |
306 | if (tp.state() != QEventPoint::State::Released) { |
307 | d->touchPoints << tp; |
308 | tp.setAccepted(); |
309 | } |
310 | } |
311 | updatePinch(event, filtering: false); |
312 | break; |
313 | case QEvent::TouchEnd: |
314 | clearPinch(event); |
315 | break; |
316 | case QEvent::TouchCancel: |
317 | cancelPinch(event); |
318 | break; |
319 | default: |
320 | QQuickItem::touchEvent(event); |
321 | } |
322 | } |
323 | |
324 | void QQuickPinchArea::clearPinch(QTouchEvent *event) |
325 | { |
326 | Q_D(QQuickPinchArea); |
327 | qCDebug(lcPA, "clear: %" PRIdQSIZETYPE " touchpoints" , d->touchPoints.size()); |
328 | d->touchPoints.clear(); |
329 | if (d->inPinch) { |
330 | d->inPinch = false; |
331 | QPointF pinchCenter = mapFromScene(point: d->sceneLastCenter); |
332 | QQuickPinchEvent pe(pinchCenter, d->pinchLastScale, d->pinchLastAngle, d->pinchRotation); |
333 | pe.setStartCenter(d->pinchStartCenter); |
334 | pe.setPreviousCenter(pinchCenter); |
335 | pe.setPreviousAngle(d->pinchLastAngle); |
336 | pe.setPreviousScale(d->pinchLastScale); |
337 | pe.setStartPoint1(mapFromScene(point: d->sceneStartPoint1)); |
338 | pe.setStartPoint2(mapFromScene(point: d->sceneStartPoint2)); |
339 | pe.setPoint1(mapFromScene(point: d->lastPoint1)); |
340 | pe.setPoint2(mapFromScene(point: d->lastPoint2)); |
341 | emit pinchFinished(pinch: &pe); |
342 | if (d->pinch && d->pinch->target()) |
343 | d->pinch->setActive(false); |
344 | } |
345 | d->pinchStartDist = 0; |
346 | d->pinchActivated = false; |
347 | d->initPinch = false; |
348 | d->pinchRejected = false; |
349 | d->id1 = -1; |
350 | if (event) { |
351 | for (const auto &point : event->points()) { |
352 | if (event->exclusiveGrabber(point) == this) |
353 | event->setExclusiveGrabber(point, exclusiveGrabber: nullptr); |
354 | } |
355 | } |
356 | setKeepTouchGrab(false); |
357 | setKeepMouseGrab(false); |
358 | } |
359 | |
360 | void QQuickPinchArea::cancelPinch(QTouchEvent *event) |
361 | { |
362 | Q_D(QQuickPinchArea); |
363 | qCDebug(lcPA, "cancel: %" PRIdQSIZETYPE " touchpoints" , d->touchPoints.size()); |
364 | d->touchPoints.clear(); |
365 | if (d->inPinch) { |
366 | d->inPinch = false; |
367 | QPointF pinchCenter = mapFromScene(point: d->sceneLastCenter); |
368 | QQuickPinchEvent pe(d->pinchStartCenter, d->pinchStartScale, d->pinchStartAngle, d->pinchStartRotation); |
369 | pe.setStartCenter(d->pinchStartCenter); |
370 | pe.setPreviousCenter(pinchCenter); |
371 | pe.setPreviousAngle(d->pinchLastAngle); |
372 | pe.setPreviousScale(d->pinchLastScale); |
373 | pe.setStartPoint1(mapFromScene(point: d->sceneStartPoint1)); |
374 | pe.setStartPoint2(mapFromScene(point: d->sceneStartPoint2)); |
375 | pe.setPoint1(pe.startPoint1()); |
376 | pe.setPoint2(pe.startPoint2()); |
377 | emit pinchFinished(pinch: &pe); |
378 | |
379 | d->pinchLastScale = d->pinchStartScale; |
380 | d->sceneLastCenter = d->sceneStartCenter; |
381 | d->pinchLastAngle = d->pinchStartAngle; |
382 | d->lastPoint1 = pe.startPoint1(); |
383 | d->lastPoint2 = pe.startPoint2(); |
384 | updatePinchTarget(); |
385 | |
386 | if (d->pinch && d->pinch->target()) |
387 | d->pinch->setActive(false); |
388 | } |
389 | d->pinchStartDist = 0; |
390 | d->pinchActivated = false; |
391 | d->initPinch = false; |
392 | d->pinchRejected = false; |
393 | d->id1 = -1; |
394 | for (const auto &point : event->points()) { |
395 | if (event->exclusiveGrabber(point) == this) |
396 | event->setExclusiveGrabber(point, exclusiveGrabber: nullptr); |
397 | } |
398 | setKeepTouchGrab(false); |
399 | setKeepMouseGrab(false); |
400 | } |
401 | |
402 | void QQuickPinchArea::updatePinch(QTouchEvent *event, bool filtering) |
403 | { |
404 | Q_D(QQuickPinchArea); |
405 | |
406 | if (d->touchPoints.size() < 2) { |
407 | // A pinch gesture is not occurring, so stealing the grab is permitted. |
408 | setKeepTouchGrab(false); |
409 | setKeepMouseGrab(false); |
410 | // During filtering, there's no need to hold a grab for one point, |
411 | // because filtering happens for every event anyway. |
412 | // But if we receive the event via direct delivery, and give up the grab, |
413 | // not only will we not see any more updates, but any filtering parent |
414 | // (such as Flickable) will also not get a chance to filter them. |
415 | // Continuing to hold the grab in this case keeps tst_TouchMouse::pinchOnFlickable() working. |
416 | if (filtering && !d->touchPoints.isEmpty() && event->exclusiveGrabber(point: d->touchPoints.first()) == this) |
417 | event->setExclusiveGrabber(point: d->touchPoints.first(), exclusiveGrabber: nullptr); |
418 | } |
419 | |
420 | if (d->touchPoints.size() == 0) { |
421 | if (d->inPinch) { |
422 | d->inPinch = false; |
423 | QPointF pinchCenter = mapFromScene(point: d->sceneLastCenter); |
424 | QQuickPinchEvent pe(pinchCenter, d->pinchLastScale, d->pinchLastAngle, d->pinchRotation); |
425 | pe.setStartCenter(d->pinchStartCenter); |
426 | pe.setPreviousCenter(pinchCenter); |
427 | pe.setPreviousAngle(d->pinchLastAngle); |
428 | pe.setPreviousScale(d->pinchLastScale); |
429 | pe.setStartPoint1(mapFromScene(point: d->sceneStartPoint1)); |
430 | pe.setStartPoint2(mapFromScene(point: d->sceneStartPoint2)); |
431 | pe.setPoint1(mapFromScene(point: d->lastPoint1)); |
432 | pe.setPoint2(mapFromScene(point: d->lastPoint2)); |
433 | setKeepTouchGrab(false); |
434 | setKeepMouseGrab(false); |
435 | emit pinchFinished(pinch: &pe); |
436 | d->pinchStartDist = 0; |
437 | d->pinchActivated = false; |
438 | if (d->pinch && d->pinch->target()) |
439 | d->pinch->setActive(false); |
440 | } |
441 | d->initPinch = false; |
442 | d->pinchRejected = false; |
443 | return; |
444 | } |
445 | |
446 | QEventPoint touchPoint1 = d->touchPoints.at(i: 0); |
447 | QEventPoint touchPoint2 = d->touchPoints.at(i: d->touchPoints.size() >= 2 ? 1 : 0); |
448 | |
449 | if (touchPoint1.state() == QEventPoint::State::Pressed) |
450 | d->sceneStartPoint1 = touchPoint1.scenePosition(); |
451 | |
452 | if (touchPoint2.state() == QEventPoint::State::Pressed) |
453 | d->sceneStartPoint2 = touchPoint2.scenePosition(); |
454 | |
455 | qCDebug(lcPA) << "updating based on" << touchPoint1 << touchPoint2; |
456 | |
457 | QRectF bounds = clipRect(); |
458 | // Pinch is not started unless there are exactly two touch points |
459 | // AND one or more of the points has just now been pressed (wasn't pressed already) |
460 | // AND both points are inside the bounds. |
461 | if (d->touchPoints.size() == 2 |
462 | && (touchPoint1.state() == QEventPoint::State::Pressed || touchPoint2.state() == QEventPoint::State::Pressed) && |
463 | bounds.contains(p: touchPoint1.position()) && bounds.contains(p: touchPoint2.position())) { |
464 | d->id1 = touchPoint1.id(); |
465 | if (!d->pinchActivated) |
466 | qCDebug(lcPA, "pinch activating" ); |
467 | d->pinchActivated = true; |
468 | d->initPinch = true; |
469 | event->setExclusiveGrabber(point: touchPoint1, exclusiveGrabber: this); |
470 | event->setExclusiveGrabber(point: touchPoint2, exclusiveGrabber: this); |
471 | setKeepTouchGrab(true); |
472 | setKeepMouseGrab(true); |
473 | } |
474 | if (d->pinchActivated && !d->pinchRejected) { |
475 | const int dragThreshold = QGuiApplication::styleHints()->startDragDistance(); |
476 | QPointF p1 = touchPoint1.scenePosition(); |
477 | QPointF p2 = touchPoint2.scenePosition(); |
478 | qreal dx = p1.x() - p2.x(); |
479 | qreal dy = p1.y() - p2.y(); |
480 | qreal dist = qSqrt(v: dx*dx + dy*dy); |
481 | QPointF sceneCenter = (p1 + p2)/2; |
482 | qreal angle = QLineF(p1, p2).angle(); |
483 | if (d->touchPoints.size() == 1) { |
484 | // If we only have one point then just move the center |
485 | if (d->id1 == touchPoint1.id()) |
486 | sceneCenter = d->sceneLastCenter + touchPoint1.scenePosition() - d->lastPoint1; |
487 | else |
488 | sceneCenter = d->sceneLastCenter + touchPoint2.scenePosition() - d->lastPoint2; |
489 | angle = d->pinchLastAngle; |
490 | } |
491 | d->id1 = touchPoint1.id(); |
492 | if (angle > 180) |
493 | angle -= 360; |
494 | qCDebug(lcPA, "pinch \u2316 %.1lf,%.1lf \u21e4%.1lf\u21e5 \u2220 %.1lf" , |
495 | sceneCenter.x(), sceneCenter.y(), dist, angle); |
496 | if (!d->inPinch || d->initPinch) { |
497 | if (d->touchPoints.size() >= 2) { |
498 | if (d->initPinch) { |
499 | if (!d->inPinch) |
500 | d->pinchStartDist = dist; |
501 | d->initPinch = false; |
502 | } |
503 | d->sceneStartCenter = sceneCenter; |
504 | d->sceneLastCenter = sceneCenter; |
505 | d->pinchStartCenter = mapFromScene(point: sceneCenter); |
506 | d->pinchStartAngle = angle; |
507 | d->pinchLastScale = 1.0; |
508 | d->pinchLastAngle = angle; |
509 | d->pinchRotation = 0.0; |
510 | d->lastPoint1 = p1; |
511 | d->lastPoint2 = p2; |
512 | if (qAbs(t: dist - d->pinchStartDist) >= dragThreshold || |
513 | (pinch()->axis() != QQuickPinch::NoDrag && |
514 | (qAbs(t: p1.x()-d->sceneStartPoint1.x()) >= dragThreshold |
515 | || qAbs(t: p1.y()-d->sceneStartPoint1.y()) >= dragThreshold |
516 | || qAbs(t: p2.x()-d->sceneStartPoint2.x()) >= dragThreshold |
517 | || qAbs(t: p2.y()-d->sceneStartPoint2.y()) >= dragThreshold))) { |
518 | QQuickPinchEvent pe(d->pinchStartCenter, 1.0, angle, 0.0); |
519 | d->pinchStartDist = dist; |
520 | pe.setStartCenter(d->pinchStartCenter); |
521 | pe.setPreviousCenter(d->pinchStartCenter); |
522 | pe.setPreviousAngle(d->pinchLastAngle); |
523 | pe.setPreviousScale(d->pinchLastScale); |
524 | pe.setStartPoint1(mapFromScene(point: d->sceneStartPoint1)); |
525 | pe.setStartPoint2(mapFromScene(point: d->sceneStartPoint2)); |
526 | pe.setPoint1(mapFromScene(point: d->lastPoint1)); |
527 | pe.setPoint2(mapFromScene(point: d->lastPoint2)); |
528 | pe.setPointCount(d->touchPoints.size()); |
529 | emit pinchStarted(pinch: &pe); |
530 | if (pe.accepted()) { |
531 | d->inPinch = true; |
532 | event->setExclusiveGrabber(point: touchPoint1, exclusiveGrabber: this); |
533 | event->setExclusiveGrabber(point: touchPoint2, exclusiveGrabber: this); |
534 | setKeepTouchGrab(true); |
535 | // So that PinchArea works in PathView, grab mouse events too. |
536 | // We should be able to remove these setKeepMouseGrab calls when QTBUG-105567 is fixed. |
537 | setKeepMouseGrab(true); |
538 | d->inPinch = true; |
539 | if (d->pinch && d->pinch->target()) { |
540 | auto targetParent = pinch()->target()->parentItem(); |
541 | d->pinchStartPos = targetParent ? |
542 | targetParent->mapToScene(point: pinch()->target()->position()) : |
543 | pinch()->target()->position(); |
544 | d->pinchStartScale = d->pinch->target()->scale(); |
545 | d->pinchStartRotation = d->pinch->target()->rotation(); |
546 | d->pinch->setActive(true); |
547 | } |
548 | } else { |
549 | d->pinchRejected = true; |
550 | } |
551 | } |
552 | } |
553 | } else if (d->pinchStartDist > 0) { |
554 | qreal scale = dist ? dist / d->pinchStartDist : d->pinchLastScale; |
555 | qreal da = d->pinchLastAngle - angle; |
556 | if (da > 180) |
557 | da -= 360; |
558 | else if (da < -180) |
559 | da += 360; |
560 | d->pinchRotation += da; |
561 | QPointF pinchCenter = mapFromScene(point: sceneCenter); |
562 | QQuickPinchEvent pe(pinchCenter, scale, angle, d->pinchRotation); |
563 | pe.setStartCenter(d->pinchStartCenter); |
564 | pe.setPreviousCenter(mapFromScene(point: d->sceneLastCenter)); |
565 | pe.setPreviousAngle(d->pinchLastAngle); |
566 | pe.setPreviousScale(d->pinchLastScale); |
567 | pe.setStartPoint1(mapFromScene(point: d->sceneStartPoint1)); |
568 | pe.setStartPoint2(mapFromScene(point: d->sceneStartPoint2)); |
569 | pe.setPoint1(touchPoint1.position()); |
570 | pe.setPoint2(touchPoint2.position()); |
571 | pe.setPointCount(d->touchPoints.size()); |
572 | d->pinchLastScale = scale; |
573 | d->sceneLastCenter = sceneCenter; |
574 | d->pinchLastAngle = angle; |
575 | d->lastPoint1 = touchPoint1.scenePosition(); |
576 | d->lastPoint2 = touchPoint2.scenePosition(); |
577 | emit pinchUpdated(pinch: &pe); |
578 | updatePinchTarget(); |
579 | } |
580 | } |
581 | } |
582 | |
583 | void QQuickPinchArea::updatePinchTarget() |
584 | { |
585 | Q_D(QQuickPinchArea); |
586 | if (d->pinch && d->pinch->target()) { |
587 | qreal s = d->pinchStartScale * d->pinchLastScale; |
588 | s = qMin(a: qMax(a: pinch()->minimumScale(),b: s), b: pinch()->maximumScale()); |
589 | pinch()->target()->setScale(s); |
590 | QPointF pos = d->sceneLastCenter - d->sceneStartCenter + d->pinchStartPos; |
591 | if (auto targetParent = pinch()->target()->parentItem()) |
592 | pos = targetParent->mapFromScene(point: pos); |
593 | |
594 | if (pinch()->axis() & QQuickPinch::XAxis) { |
595 | qreal x = pos.x(); |
596 | if (x < pinch()->xmin()) |
597 | x = pinch()->xmin(); |
598 | else if (x > pinch()->xmax()) |
599 | x = pinch()->xmax(); |
600 | pinch()->target()->setX(x); |
601 | } |
602 | if (pinch()->axis() & QQuickPinch::YAxis) { |
603 | qreal y = pos.y(); |
604 | if (y < pinch()->ymin()) |
605 | y = pinch()->ymin(); |
606 | else if (y > pinch()->ymax()) |
607 | y = pinch()->ymax(); |
608 | pinch()->target()->setY(y); |
609 | } |
610 | if (d->pinchStartRotation >= pinch()->minimumRotation() |
611 | && d->pinchStartRotation <= pinch()->maximumRotation()) { |
612 | qreal r = d->pinchRotation + d->pinchStartRotation; |
613 | r = qMin(a: qMax(a: pinch()->minimumRotation(),b: r), b: pinch()->maximumRotation()); |
614 | pinch()->target()->setRotation(r); |
615 | } |
616 | } |
617 | } |
618 | |
619 | /*! \internal |
620 | PinchArea needs to filter touch events going to its children: in case |
621 | one of them stops event propagation by accepting the touch event, filtering |
622 | is the only way PinchArea can see the touch event. |
623 | |
624 | This method is called childMouseEventFilter instead of childPointerEventFilter |
625 | for historical reasons, but actually filters all pointer events (and the |
626 | occasional QEvent::UngrabMouse). |
627 | */ |
628 | bool QQuickPinchArea::childMouseEventFilter(QQuickItem *i, QEvent *e) |
629 | { |
630 | Q_D(QQuickPinchArea); |
631 | if (!d->enabled || !isVisible()) |
632 | return QQuickItem::childMouseEventFilter(i, e); |
633 | auto *te = static_cast<QTouchEvent*>(e); |
634 | switch (e->type()) { |
635 | case QEvent::TouchBegin: |
636 | clearPinch(event: te); |
637 | Q_FALLTHROUGH(); |
638 | case QEvent::TouchUpdate: { |
639 | const auto &points = te->points(); |
640 | d->touchPoints.clear(); |
641 | for (auto &tp : points) { |
642 | if (tp.state() != QEventPoint::State::Released) |
643 | d->touchPoints << tp; |
644 | } |
645 | updatePinch(event: te, filtering: true); |
646 | } |
647 | e->setAccepted(d->inPinch); |
648 | return d->inPinch; |
649 | case QEvent::TouchEnd: |
650 | clearPinch(event: te); |
651 | break; |
652 | default: |
653 | break; |
654 | } |
655 | |
656 | return QQuickItem::childMouseEventFilter(i, e); |
657 | } |
658 | |
659 | void QQuickPinchArea::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) |
660 | { |
661 | QQuickItem::geometryChange(newGeometry, oldGeometry); |
662 | } |
663 | |
664 | void QQuickPinchArea::itemChange(ItemChange change, const ItemChangeData &value) |
665 | { |
666 | QQuickItem::itemChange(change, value); |
667 | } |
668 | |
669 | bool QQuickPinchArea::event(QEvent *event) |
670 | { |
671 | Q_D(QQuickPinchArea); |
672 | if (!d->enabled || !isVisible()) |
673 | return QQuickItem::event(event); |
674 | |
675 | switch (event->type()) { |
676 | #if QT_CONFIG(gestures) |
677 | case QEvent::NativeGesture: { |
678 | QNativeGestureEvent *gesture = static_cast<QNativeGestureEvent *>(event); |
679 | switch (gesture->gestureType()) { |
680 | case Qt::BeginNativeGesture: |
681 | clearPinch(event: nullptr); // probably not necessary; JIC |
682 | d->pinchStartCenter = gesture->position(); |
683 | d->pinchStartAngle = 0.0; |
684 | d->pinchStartRotation = 0.0; |
685 | d->pinchRotation = 0.0; |
686 | d->pinchStartScale = 1.0; |
687 | d->pinchLastAngle = 0.0; |
688 | d->pinchLastScale = 1.0; |
689 | d->sceneStartPoint1 = gesture->scenePosition(); |
690 | d->sceneStartPoint2 = gesture->scenePosition(); // TODO we never really know |
691 | d->lastPoint1 = gesture->scenePosition(); |
692 | d->lastPoint2 = gesture->scenePosition(); // TODO we never really know |
693 | if (d->pinch && d->pinch->target()) { |
694 | d->pinchStartPos = d->pinch->target()->position(); |
695 | d->pinchStartScale = d->pinch->target()->scale(); |
696 | d->pinchStartRotation = d->pinch->target()->rotation(); |
697 | d->pinch->setActive(true); |
698 | } |
699 | break; |
700 | case Qt::EndNativeGesture: |
701 | clearPinch(event: nullptr); |
702 | break; |
703 | case Qt::ZoomNativeGesture: { |
704 | if (d->pinchRejected) |
705 | break; |
706 | qreal scale = d->pinchLastScale * (1.0 + gesture->value()); |
707 | QQuickPinchEvent pe(d->pinchStartCenter, scale, d->pinchLastAngle, 0.0); |
708 | pe.setStartCenter(d->pinchStartCenter); |
709 | pe.setPreviousCenter(d->pinchStartCenter); |
710 | pe.setPreviousAngle(d->pinchLastAngle); |
711 | pe.setPreviousScale(d->pinchLastScale); |
712 | pe.setStartPoint1(mapFromScene(point: d->sceneStartPoint1)); |
713 | pe.setStartPoint2(mapFromScene(point: d->sceneStartPoint2)); |
714 | pe.setPoint1(mapFromScene(point: d->lastPoint1)); |
715 | pe.setPoint2(mapFromScene(point: d->lastPoint2)); |
716 | pe.setPointCount(2); |
717 | d->pinchLastScale = scale; |
718 | if (d->inPinch) |
719 | emit pinchUpdated(pinch: &pe); |
720 | else |
721 | emit pinchStarted(pinch: &pe); |
722 | d->inPinch = true; |
723 | if (pe.accepted()) |
724 | updatePinchTarget(); |
725 | else |
726 | d->pinchRejected = true; |
727 | } break; |
728 | case Qt::SmartZoomNativeGesture: { |
729 | if (gesture->value() > 0.0 && d->pinch && d->pinch->target()) { |
730 | d->pinchStartPos = pinch()->target()->position(); |
731 | d->pinchStartCenter = mapToItem(item: pinch()->target()->parentItem(), point: pinch()->target()->boundingRect().center()); |
732 | d->pinchStartScale = d->pinch->target()->scale(); |
733 | d->pinchStartRotation = d->pinch->target()->rotation(); |
734 | d->pinchLastScale = d->pinchStartScale = d->pinch->target()->scale(); |
735 | d->pinchLastAngle = d->pinchStartRotation = d->pinch->target()->rotation(); |
736 | } |
737 | QQuickPinchEvent pe(gesture->position(), gesture->value(), d->pinchLastAngle, 0.0); |
738 | pe.setStartCenter(gesture->position()); |
739 | pe.setPreviousCenter(d->pinchStartCenter); |
740 | pe.setPreviousAngle(d->pinchLastAngle); |
741 | pe.setPreviousScale(d->pinchLastScale); |
742 | pe.setStartPoint1(gesture->position()); |
743 | pe.setStartPoint2(gesture->position()); |
744 | pe.setPoint1(mapFromScene(point: gesture->scenePosition())); |
745 | pe.setPoint2(mapFromScene(point: gesture->scenePosition())); |
746 | pe.setPointCount(2); |
747 | emit smartZoom(pinch: &pe); |
748 | } break; |
749 | case Qt::RotateNativeGesture: { |
750 | if (d->pinchRejected) |
751 | break; |
752 | qreal angle = d->pinchLastAngle + gesture->value(); |
753 | QQuickPinchEvent pe(d->pinchStartCenter, d->pinchLastScale, angle, 0.0); |
754 | pe.setStartCenter(d->pinchStartCenter); |
755 | pe.setPreviousCenter(d->pinchStartCenter); |
756 | pe.setPreviousAngle(d->pinchLastAngle); |
757 | pe.setPreviousScale(d->pinchLastScale); |
758 | pe.setStartPoint1(mapFromScene(point: d->sceneStartPoint1)); |
759 | pe.setStartPoint2(mapFromScene(point: d->sceneStartPoint2)); |
760 | pe.setPoint1(mapFromScene(point: d->lastPoint1)); |
761 | pe.setPoint2(mapFromScene(point: d->lastPoint2)); |
762 | pe.setPointCount(2); |
763 | d->pinchLastAngle = angle; |
764 | if (d->inPinch) |
765 | emit pinchUpdated(pinch: &pe); |
766 | else |
767 | emit pinchStarted(pinch: &pe); |
768 | d->inPinch = true; |
769 | d->pinchRotation = angle; |
770 | if (pe.accepted()) |
771 | updatePinchTarget(); |
772 | else |
773 | d->pinchRejected = true; |
774 | } break; |
775 | default: |
776 | return QQuickItem::event(event); |
777 | } |
778 | } break; |
779 | #endif // gestures |
780 | case QEvent::Wheel: |
781 | event->ignore(); |
782 | return false; |
783 | default: |
784 | return QQuickItem::event(event); |
785 | } |
786 | |
787 | return true; |
788 | } |
789 | |
790 | QQuickPinch *QQuickPinchArea::pinch() |
791 | { |
792 | Q_D(QQuickPinchArea); |
793 | if (!d->pinch) |
794 | d->pinch = new QQuickPinch; |
795 | return d->pinch; |
796 | } |
797 | |
798 | QT_END_NAMESPACE |
799 | |
800 | #include "moc_qquickpincharea_p.cpp" |
801 | |