1 | // Copyright (C) 2023 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include "qtouch3dinputhandler_p.h" |
5 | #include "abstract3dcontroller_p.h" |
6 | #include <QtCore/QTimer> |
7 | #include <QtCore/qmath.h> |
8 | |
9 | QT_BEGIN_NAMESPACE |
10 | |
11 | static const float maxTapAndHoldJitter = 20.0f; |
12 | static const int maxPinchJitter = 10; |
13 | #if defined (Q_OS_ANDROID) || defined(Q_OS_IOS) |
14 | static const int maxSelectionJitter = 10; |
15 | #else |
16 | static const int maxSelectionJitter = 5; |
17 | #endif |
18 | static const int tapAndHoldTime = 250; |
19 | static const float rotationSpeed = 200.0f; |
20 | static const float touchZoomDrift = 0.02f; |
21 | |
22 | /*! |
23 | * \class QTouch3DInputHandler |
24 | * \inmodule QtGraphs |
25 | * \brief Basic touch display based input handler. |
26 | * |
27 | * QTouch3DInputHandler is the basic input handler for touch screen devices. |
28 | * |
29 | * Default touch input handler has the following functionalty: |
30 | * \table |
31 | * \header |
32 | * \li Gesture |
33 | * \li Action |
34 | * \row |
35 | * \li Touch-And-Move |
36 | * \li Rotate graph within limits set for Q3DCamera |
37 | * \row |
38 | * \li Tap |
39 | * \li Select the item tapped or remove selection if none. |
40 | * May open the secondary view depending on the |
41 | * \l {QAbstract3DGraph::selectionMode}{selection mode}. |
42 | * \row |
43 | * \li Tap-And-Hold |
44 | * \li Same as tap. |
45 | * \row |
46 | * \li Pinch |
47 | * \li Zoom in/out within the allowable zoom range set for Q3DCamera. |
48 | * \row |
49 | * \li Tap on the primary view when the secondary view is visible |
50 | * \li Closes the secondary view. |
51 | * \note Secondary view is available only for Q3DBars and Q3DSurface graphs. |
52 | * \endtable |
53 | * |
54 | * Rotation, zoom, and selection can each be individually disabled using |
55 | * corresponding Q3DInputHandler properties. |
56 | */ |
57 | |
58 | /*! |
59 | * \qmltype TouchInputHandler3D |
60 | * \inqmlmodule QtGraphs |
61 | * \ingroup graphs_qml |
62 | * \instantiates QTouch3DInputHandler |
63 | * \inherits InputHandler3D |
64 | * \brief Basic touch display based input handler. |
65 | * |
66 | * TouchInputHandler3D is the basic input handler for touch screen devices. |
67 | * |
68 | * See QTouch3DInputHandler documentation for more details. |
69 | */ |
70 | |
71 | /*! |
72 | * Constructs the basic touch display input handler. An optional \a parent parameter can be given |
73 | * and is then passed to QObject constructor. |
74 | */ |
75 | QTouch3DInputHandler::QTouch3DInputHandler(QObject *parent) |
76 | : Q3DInputHandler(new QTouch3DInputHandlerPrivate(this), parent) |
77 | { |
78 | } |
79 | |
80 | /*! |
81 | * Destroys the input handler. |
82 | */ |
83 | QTouch3DInputHandler::~QTouch3DInputHandler() |
84 | { |
85 | } |
86 | |
87 | /*! |
88 | * Override this to change handling of touch events. |
89 | * Touch event is given in the \a event. |
90 | */ |
91 | void QTouch3DInputHandler::touchEvent(QTouchEvent *event) |
92 | { |
93 | Q_D(QTouch3DInputHandler); |
94 | QList<QTouchEvent::TouchPoint> points; |
95 | points = event->points(); |
96 | |
97 | if (!scene()->isSlicingActive() && points.size() == 2) { |
98 | d->m_holdTimer->stop(); |
99 | QPointF distance = points.at(i: 0).position() - points.at(i: 1).position(); |
100 | QPoint midPoint = ((points.at(i: 0).position() + points.at(i: 1).position()) / 2.0).toPoint(); |
101 | d->handlePinchZoom(distance: distance.manhattanLength(), pos: midPoint); |
102 | } else if (points.size() == 1) { |
103 | QPointF pointerPos = points.at(i: 0).position(); |
104 | if (event->type() == QEvent::TouchBegin) { |
105 | // Flush input state |
106 | d->m_inputState = QAbstract3DInputHandlerPrivate::InputStateNone; |
107 | if (scene()->isSlicingActive()) { |
108 | if (isSelectionEnabled()) { |
109 | if (scene()->isPointInPrimarySubView(point: pointerPos.toPoint())) |
110 | setInputView(InputViewOnPrimary); |
111 | else if (scene()->isPointInSecondarySubView(point: pointerPos.toPoint())) |
112 | setInputView(InputViewOnSecondary); |
113 | else |
114 | setInputView(InputViewNone); |
115 | } |
116 | } else { |
117 | // Handle possible tap-and-hold selection |
118 | if (isSelectionEnabled()) { |
119 | d->m_startHoldPos = pointerPos; |
120 | d->m_touchHoldPos = d->m_startHoldPos; |
121 | d->m_holdTimer->start(); |
122 | setInputView(InputViewOnPrimary); |
123 | } |
124 | // Start rotating |
125 | if (isRotationEnabled()) { |
126 | d->m_inputState = QAbstract3DInputHandlerPrivate::InputStateRotating; |
127 | setInputPosition(position: pointerPos.toPoint()); |
128 | setInputView(InputViewOnPrimary); |
129 | } |
130 | } |
131 | } else if (event->type() == QEvent::TouchEnd) { |
132 | setInputView(InputViewNone); |
133 | d->m_holdTimer->stop(); |
134 | // Handle possible selection |
135 | if (!scene()->isSlicingActive() |
136 | && QAbstract3DInputHandlerPrivate::InputStatePinching |
137 | != d->m_inputState) { |
138 | d->handleSelection(position: pointerPos); |
139 | } |
140 | } else if (event->type() == QEvent::TouchUpdate) { |
141 | if (!scene()->isSlicingActive()) { |
142 | d->m_touchHoldPos = pointerPos; |
143 | // Handle rotation |
144 | d->handleRotation(position: pointerPos); |
145 | } |
146 | } |
147 | } else { |
148 | d->m_holdTimer->stop(); |
149 | } |
150 | } |
151 | |
152 | QTouch3DInputHandlerPrivate::QTouch3DInputHandlerPrivate(QTouch3DInputHandler *q) |
153 | : Q3DInputHandlerPrivate(q), |
154 | m_holdTimer(0) |
155 | { |
156 | m_holdTimer = new QTimer(); |
157 | m_holdTimer->setSingleShot(true); |
158 | m_holdTimer->setInterval(tapAndHoldTime); |
159 | connect(sender: m_holdTimer, signal: &QTimer::timeout, context: this, slot: &QTouch3DInputHandlerPrivate::handleTapAndHold); |
160 | } |
161 | |
162 | QTouch3DInputHandlerPrivate::~QTouch3DInputHandlerPrivate() |
163 | { |
164 | m_holdTimer->stop(); |
165 | delete m_holdTimer; |
166 | } |
167 | |
168 | void QTouch3DInputHandlerPrivate::handlePinchZoom(float distance, const QPoint &pos) |
169 | { |
170 | Q_Q(QTouch3DInputHandler); |
171 | if (q->isZoomEnabled()) { |
172 | int newDistance = distance; |
173 | int prevDist = q->prevDistance(); |
174 | if (prevDist > 0 && qAbs(t: prevDist - newDistance) < maxPinchJitter) |
175 | return; |
176 | m_inputState = QAbstract3DInputHandlerPrivate::InputStatePinching; |
177 | Q3DCamera *camera = q->scene()->activeCamera(); |
178 | int zoomLevel = int(camera->zoomLevel()); |
179 | const int minZoomLevel = int(camera->minZoomLevel()); |
180 | const int maxZoomLevel = int(camera->maxZoomLevel()); |
181 | float zoomRate = qSqrt(v: qSqrt(v: zoomLevel)); |
182 | if (newDistance > prevDist) |
183 | zoomLevel += zoomRate; |
184 | else |
185 | zoomLevel -= zoomRate; |
186 | zoomLevel = qBound(min: minZoomLevel, val: zoomLevel, max: maxZoomLevel); |
187 | |
188 | if (q->isZoomAtTargetEnabled()) { |
189 | q->scene()->setGraphPositionQuery(pos); |
190 | m_controller->setGraphPositionQueryPending(true); |
191 | m_zoomAtTargetPending = true; |
192 | // If zoom at target is enabled, we don't want to zoom yet, as that causes |
193 | // jitter. Instead, we zoom next frame, when we apply the camera position. |
194 | m_requestedZoomLevel = zoomLevel; |
195 | m_driftMultiplier = touchZoomDrift; |
196 | } else { |
197 | camera->setZoomLevel(zoomLevel); |
198 | } |
199 | |
200 | q->setPrevDistance(newDistance); |
201 | } |
202 | } |
203 | |
204 | void QTouch3DInputHandlerPrivate::handleTapAndHold() |
205 | { |
206 | Q_Q(QTouch3DInputHandler); |
207 | if (q->isSelectionEnabled()) { |
208 | QPointF distance = m_startHoldPos - m_touchHoldPos; |
209 | if (distance.manhattanLength() < maxTapAndHoldJitter) { |
210 | m_inputState = QAbstract3DInputHandlerPrivate::InputStateSelecting; |
211 | q->setInputPosition(position: m_touchHoldPos.toPoint()); |
212 | q->scene()->setSelectionQueryPosition(m_touchHoldPos.toPoint()); |
213 | } |
214 | } |
215 | } |
216 | |
217 | void QTouch3DInputHandlerPrivate::handleSelection(const QPointF &position) |
218 | { |
219 | Q_Q(QTouch3DInputHandler); |
220 | if (q->isSelectionEnabled()) { |
221 | QPointF distance = m_startHoldPos - position; |
222 | if (distance.manhattanLength() < maxSelectionJitter) { |
223 | m_inputState = QAbstract3DInputHandlerPrivate::InputStateSelecting; |
224 | q->scene()->setSelectionQueryPosition(position.toPoint()); |
225 | } else { |
226 | m_inputState = QAbstract3DInputHandlerPrivate::InputStateNone; |
227 | q->setInputView(QAbstract3DInputHandler::InputViewNone); |
228 | } |
229 | q->setPreviousInputPos(position.toPoint()); |
230 | } |
231 | } |
232 | |
233 | void QTouch3DInputHandlerPrivate::handleRotation(const QPointF &position) |
234 | { |
235 | Q_Q(QTouch3DInputHandler); |
236 | if (q->isRotationEnabled() |
237 | && QAbstract3DInputHandlerPrivate::InputStateRotating == m_inputState) { |
238 | Q3DScene *scene = q->scene(); |
239 | Q3DCamera *camera = scene->activeCamera(); |
240 | float xRotation = camera->xRotation(); |
241 | float yRotation = camera->yRotation(); |
242 | QPointF inputPos = q->inputPosition(); |
243 | float mouseMoveX = float(inputPos.x() - position.x()) |
244 | / (scene->viewport().width() / rotationSpeed); |
245 | float mouseMoveY = float(inputPos.y() - position.y()) |
246 | / (scene->viewport().height() / rotationSpeed); |
247 | xRotation -= mouseMoveX; |
248 | yRotation -= mouseMoveY; |
249 | camera->setXRotation(xRotation); |
250 | camera->setYRotation(yRotation); |
251 | |
252 | q->setPreviousInputPos(inputPos.toPoint()); |
253 | q->setInputPosition(position: position.toPoint()); |
254 | } |
255 | } |
256 | |
257 | QT_END_NAMESPACE |
258 | |