1 | // Copyright (C) 2023 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include "graphsglobal_p.h" |
5 | #include "q3dinputhandler_p.h" |
6 | #include "abstract3dcontroller_p.h" |
7 | |
8 | QT_BEGIN_NAMESPACE |
9 | |
10 | static const int halfSizeZoomLevel = 50; |
11 | static const int oneToOneZoomLevel = 100; |
12 | static const int driftTowardCenterLevel = 175; |
13 | static const float wheelZoomDrift = 0.1f; |
14 | |
15 | static const int nearZoomRangeDivider = 12; |
16 | static const int midZoomRangeDivider = 60; |
17 | static const int farZoomRangeDivider = 120; |
18 | |
19 | static const float rotationSpeed = 100.0f; |
20 | |
21 | /*! |
22 | * \class Q3DInputHandler |
23 | * \inmodule QtGraphs |
24 | * \brief Basic wheel mouse based input handler. |
25 | * |
26 | * Q3DInputHandler is the basic input handler for wheel mouse type of input devices. |
27 | * |
28 | * Default input handler has the following functionalty: |
29 | * \table |
30 | * \header |
31 | * \li Mouse action |
32 | * \li Action |
33 | * \row |
34 | * \li Drag with right button pressed |
35 | * \li Rotate graph within limits set for Q3DCamera. |
36 | * \row |
37 | * \li Left click |
38 | * \li Select item under cursor or remove selection if none. |
39 | * May open the secondary view depending on the |
40 | * \l {QAbstract3DGraph::selectionMode}{selection mode}. |
41 | * \row |
42 | * \li Mouse wheel |
43 | * \li Zoom in/out within the allowable zoom range set for Q3DCamera. |
44 | * \row |
45 | * \li Left click on the primary view when the secondary view is visible |
46 | * \li Closes the secondary view. |
47 | * \note Secondary view is available only for Q3DBars and Q3DSurface graphs. |
48 | * \endtable |
49 | * |
50 | * Rotation, zoom, and selection can each be individually disabled using |
51 | * corresponding properties of this class. |
52 | */ |
53 | |
54 | /*! |
55 | * \qmltype InputHandler3D |
56 | * \inqmlmodule QtGraphs |
57 | * \ingroup graphs_qml |
58 | * \instantiates Q3DInputHandler |
59 | * \brief Basic wheel mouse based input handler. |
60 | * |
61 | * InputHandler3D is the basic input handler for wheel mouse type of input devices. |
62 | * |
63 | * See Q3DInputHandler documentation for more details. |
64 | */ |
65 | |
66 | /*! |
67 | * \qmlproperty bool InputHandler3D::rotationEnabled |
68 | * |
69 | * Defines whether this input handler allows graph rotation. |
70 | * Defaults to \c{true}. |
71 | */ |
72 | |
73 | /*! |
74 | * \qmlproperty bool InputHandler3D::zoomEnabled |
75 | * |
76 | * Defines whether this input handler allows graph zooming. |
77 | * Defaults to \c{true}. |
78 | */ |
79 | |
80 | /*! |
81 | * \qmlproperty bool InputHandler3D::selectionEnabled |
82 | * |
83 | * Defines whether this input handler allows selection from the graph. |
84 | * Defaults to \c{true}. |
85 | */ |
86 | |
87 | /*! |
88 | * \qmlproperty bool InputHandler3D::zoomAtTargetEnabled |
89 | * |
90 | * Defines whether zooming changes the camera target to the position of the input |
91 | * at the time of the zoom. |
92 | * Defaults to \c{true}. |
93 | */ |
94 | |
95 | /*! |
96 | * \internal |
97 | */ |
98 | Q3DInputHandler::Q3DInputHandler(Q3DInputHandlerPrivate *d, QObject *parent) : |
99 | QAbstract3DInputHandler(d, parent) |
100 | { |
101 | QObject::connect(sender: this, signal: &QAbstract3DInputHandler::sceneChanged, |
102 | context: d, slot: &Q3DInputHandlerPrivate::handleSceneChange); |
103 | } |
104 | |
105 | /*! |
106 | * Constructs the basic mouse input handler. An optional \a parent parameter can be given |
107 | * and is then passed to QObject constructor. |
108 | */ |
109 | Q3DInputHandler::Q3DInputHandler(QObject *parent) : |
110 | QAbstract3DInputHandler(new Q3DInputHandlerPrivate(this), parent) |
111 | { |
112 | } |
113 | |
114 | /*! |
115 | * Destroys the input handler. |
116 | */ |
117 | Q3DInputHandler::~Q3DInputHandler() |
118 | { |
119 | } |
120 | |
121 | // Input event listeners |
122 | /*! |
123 | * Override this to change handling of mouse press events. |
124 | * Mouse press event is given in the \a event and the mouse position in \a mousePos. |
125 | */ |
126 | void Q3DInputHandler::mousePressEvent(QMouseEvent *event, const QPoint &mousePos) |
127 | { |
128 | Q_D(Q3DInputHandler); |
129 | #if defined(Q_OS_IOS) |
130 | Q_UNUSED(event); |
131 | Q_UNUSED(mousePos); |
132 | #else |
133 | if (Qt::LeftButton == event->button()) { |
134 | if (isSelectionEnabled()) { |
135 | if (scene()->isSlicingActive()) { |
136 | if (scene()->isPointInPrimarySubView(point: mousePos)) |
137 | setInputView(InputViewOnPrimary); |
138 | else if (scene()->isPointInSecondarySubView(point: mousePos)) |
139 | setInputView(InputViewOnSecondary); |
140 | else |
141 | setInputView(InputViewNone); |
142 | } else { |
143 | // update mouse positions to prevent jumping when releasing or repressing a button |
144 | d->m_inputState = QAbstract3DInputHandlerPrivate::InputStateSelecting; |
145 | setInputPosition(position: mousePos); |
146 | scene()->setSelectionQueryPosition(mousePos); |
147 | setInputView(InputViewOnPrimary); |
148 | } |
149 | } |
150 | } else if (Qt::MiddleButton == event->button()) { |
151 | if (isRotationEnabled()) { |
152 | // reset rotations |
153 | setInputPosition(position: QPoint(0, 0)); |
154 | } |
155 | } else if (Qt::RightButton == event->button()) { |
156 | if (isRotationEnabled()) { |
157 | // disable rotating when in slice view |
158 | if (!scene()->isSlicingActive()) |
159 | d->m_inputState = QAbstract3DInputHandlerPrivate::InputStateRotating; |
160 | // update mouse positions to prevent jumping when releasing or repressing a button |
161 | setInputPosition(position: mousePos); |
162 | } |
163 | } |
164 | #endif |
165 | } |
166 | |
167 | /*! |
168 | * Override this to change handling of mouse release events. |
169 | * Mouse release event is given in the \a event and the mouse position in \a mousePos. |
170 | */ |
171 | void Q3DInputHandler::mouseReleaseEvent(QMouseEvent *event, const QPoint &mousePos) |
172 | { |
173 | Q_UNUSED(event); |
174 | Q_D(Q3DInputHandler); |
175 | #if defined(Q_OS_IOS) |
176 | Q_UNUSED(mousePos); |
177 | #else |
178 | if (QAbstract3DInputHandlerPrivate::InputStateRotating == d->m_inputState) { |
179 | // update mouse positions to prevent jumping when releasing or repressing a button |
180 | setInputPosition(position: mousePos); |
181 | } |
182 | d->m_inputState = QAbstract3DInputHandlerPrivate::InputStateNone; |
183 | setInputView(InputViewNone); |
184 | #endif |
185 | } |
186 | |
187 | /*! |
188 | * Override this to change handling of mouse move events. |
189 | * Mouse move event is given in the \a event and the mouse position in \a mousePos. |
190 | */ |
191 | void Q3DInputHandler::mouseMoveEvent(QMouseEvent *event, const QPoint &mousePos) |
192 | { |
193 | Q_UNUSED(event); |
194 | Q_D(Q3DInputHandler); |
195 | #if defined(Q_OS_IOS) |
196 | Q_UNUSED(mousePos); |
197 | #else |
198 | if (QAbstract3DInputHandlerPrivate::InputStateRotating == d->m_inputState |
199 | && isRotationEnabled()) { |
200 | // Calculate mouse movement since last frame |
201 | float xRotation = scene()->activeCamera()->xRotation(); |
202 | float yRotation = scene()->activeCamera()->yRotation(); |
203 | float mouseMoveX = float(inputPosition().x() - mousePos.x()) |
204 | / (scene()->viewport().width() / rotationSpeed); |
205 | float mouseMoveY = float(inputPosition().y() - mousePos.y()) |
206 | / (scene()->viewport().height() / rotationSpeed); |
207 | // Apply to rotations |
208 | xRotation -= mouseMoveX; |
209 | yRotation -= mouseMoveY; |
210 | scene()->activeCamera()->setXRotation(xRotation); |
211 | scene()->activeCamera()->setYRotation(yRotation); |
212 | |
213 | setPreviousInputPos(inputPosition()); |
214 | setInputPosition(position: mousePos); |
215 | } |
216 | #endif |
217 | } |
218 | |
219 | #if QT_CONFIG(wheelevent) |
220 | /*! |
221 | * Override this to change handling of wheel events. |
222 | * The wheel event is given in the \a event. |
223 | */ |
224 | void Q3DInputHandler::wheelEvent(QWheelEvent *event) |
225 | { |
226 | Q_D(Q3DInputHandler); |
227 | if (isZoomEnabled()) { |
228 | // disable zooming if in slice view |
229 | if (scene()->isSlicingActive()) |
230 | return; |
231 | |
232 | // Adjust zoom level based on what zoom range we're in. |
233 | Q3DCamera *camera = scene()->activeCamera(); |
234 | int zoomLevel = int(camera->zoomLevel()); |
235 | const int minZoomLevel = int(camera->minZoomLevel()); |
236 | const int maxZoomLevel = int(camera->maxZoomLevel()); |
237 | if (zoomLevel > oneToOneZoomLevel) |
238 | zoomLevel += event->angleDelta().y() / nearZoomRangeDivider; |
239 | else if (zoomLevel > halfSizeZoomLevel) |
240 | zoomLevel += event->angleDelta().y() / midZoomRangeDivider; |
241 | else |
242 | zoomLevel += event->angleDelta().y() / farZoomRangeDivider; |
243 | zoomLevel = qBound(min: minZoomLevel, val: zoomLevel, max: maxZoomLevel); |
244 | |
245 | if (isZoomAtTargetEnabled()) { |
246 | if (!d->m_controller) |
247 | d->handleSceneChange(scene: scene()); |
248 | d->m_controller->setGraphPositionQueryPending(true); |
249 | scene()->setGraphPositionQuery(event->position().toPoint()); |
250 | d->m_zoomAtTargetPending = true; |
251 | // If zoom at target is enabled, we don't want to zoom yet, as that causes |
252 | // jitter. Instead, we zoom next frame, when we apply the camera position. |
253 | d->m_requestedZoomLevel = zoomLevel; |
254 | d->m_driftMultiplier = wheelZoomDrift; |
255 | } else { |
256 | camera->setZoomLevel(zoomLevel); |
257 | } |
258 | } |
259 | } |
260 | #endif |
261 | |
262 | /*! |
263 | * \property Q3DInputHandler::rotationEnabled |
264 | * |
265 | * \brief Whether this input handler allows graph rotation. |
266 | * |
267 | * Defaults to \c{true}. |
268 | */ |
269 | void Q3DInputHandler::setRotationEnabled(bool enable) |
270 | { |
271 | Q_D(Q3DInputHandler); |
272 | if (d->m_rotationEnabled != enable) { |
273 | d->m_rotationEnabled = enable; |
274 | emit rotationEnabledChanged(enable); |
275 | } |
276 | } |
277 | |
278 | bool Q3DInputHandler::isRotationEnabled() const |
279 | { |
280 | const Q_D(Q3DInputHandler); |
281 | return d->m_rotationEnabled; |
282 | } |
283 | |
284 | /*! |
285 | * \property Q3DInputHandler::zoomEnabled |
286 | * |
287 | * \brief Whether this input handler allows graph zooming. |
288 | * |
289 | * Defaults to \c{true}. |
290 | */ |
291 | void Q3DInputHandler::setZoomEnabled(bool enable) |
292 | { |
293 | Q_D(Q3DInputHandler); |
294 | if (d->m_zoomEnabled != enable) { |
295 | d->m_zoomEnabled = enable; |
296 | emit zoomEnabledChanged(enable); |
297 | } |
298 | } |
299 | |
300 | bool Q3DInputHandler::isZoomEnabled() const |
301 | { |
302 | const Q_D(Q3DInputHandler); |
303 | return d->m_zoomEnabled; |
304 | } |
305 | |
306 | /*! |
307 | * \property Q3DInputHandler::selectionEnabled |
308 | * |
309 | * \brief Whether this input handler allows selection from the graph. |
310 | * |
311 | * Defaults to \c{true}. |
312 | */ |
313 | void Q3DInputHandler::setSelectionEnabled(bool enable) |
314 | { |
315 | Q_D(Q3DInputHandler); |
316 | if (d->m_selectionEnabled != enable) { |
317 | d->m_selectionEnabled = enable; |
318 | emit selectionEnabledChanged(enable); |
319 | } |
320 | } |
321 | |
322 | bool Q3DInputHandler::isSelectionEnabled() const |
323 | { |
324 | const Q_D(Q3DInputHandler); |
325 | return d->m_selectionEnabled; |
326 | } |
327 | |
328 | /*! |
329 | * \property Q3DInputHandler::zoomAtTargetEnabled |
330 | * |
331 | * \brief Whether zooming should change the camera target so that the zoomed point |
332 | * of the graph stays at the same location after the zoom. |
333 | * |
334 | * Defaults to \c{true}. |
335 | */ |
336 | void Q3DInputHandler::setZoomAtTargetEnabled(bool enable) |
337 | { |
338 | Q_D(Q3DInputHandler); |
339 | if (d->m_zoomAtTargetEnabled != enable) { |
340 | d->m_zoomAtTargetEnabled = enable; |
341 | emit zoomAtTargetEnabledChanged(enable); |
342 | } |
343 | } |
344 | |
345 | bool Q3DInputHandler::isZoomAtTargetEnabled() const |
346 | { |
347 | const Q_D(Q3DInputHandler); |
348 | return d->m_zoomAtTargetEnabled; |
349 | } |
350 | |
351 | Q3DInputHandlerPrivate::Q3DInputHandlerPrivate(Q3DInputHandler *q) |
352 | : QAbstract3DInputHandlerPrivate(q), |
353 | m_rotationEnabled(true), |
354 | m_zoomEnabled(true), |
355 | m_selectionEnabled(true), |
356 | m_zoomAtTargetEnabled(true), |
357 | m_zoomAtTargetPending(false), |
358 | m_controller(0), |
359 | m_requestedZoomLevel(0.0f), |
360 | m_driftMultiplier(0.0f) |
361 | { |
362 | } |
363 | |
364 | Q3DInputHandlerPrivate::~Q3DInputHandlerPrivate() |
365 | { |
366 | } |
367 | |
368 | void Q3DInputHandlerPrivate::handleSceneChange(Q3DScene *scene) |
369 | { |
370 | if (scene) { |
371 | if (m_controller) { |
372 | QObject::disconnect(sender: m_controller, signal: &Abstract3DController::queriedGraphPositionChanged, |
373 | receiver: this, slot: &Q3DInputHandlerPrivate::handleQueriedGraphPositionChange); |
374 | } |
375 | |
376 | m_controller = qobject_cast<Abstract3DController *>(object: scene->parent()); |
377 | if (m_controller) { |
378 | QObject::connect(sender: m_controller, signal: &Abstract3DController::queriedGraphPositionChanged, |
379 | context: this, slot: &Q3DInputHandlerPrivate::handleQueriedGraphPositionChange); |
380 | } |
381 | } |
382 | } |
383 | |
384 | void Q3DInputHandlerPrivate::handleQueriedGraphPositionChange() |
385 | { |
386 | if (m_zoomAtTargetPending) { |
387 | // Check if the zoom point is on graph |
388 | QVector3D newTarget = m_controller->queriedGraphPosition(); |
389 | float currentZoom = m_requestedZoomLevel; |
390 | float previousZoom = q_ptr->scene()->activeCamera()->zoomLevel(); |
391 | q_ptr->scene()->activeCamera()->setZoomLevel(currentZoom); |
392 | float diffAdj = 0.0f; |
393 | |
394 | // If zooming in/out outside the graph, or zooming out after certain point, |
395 | // move towards the center. |
396 | if ((qAbs(t: newTarget.x()) > 2.0f |
397 | || qAbs(t: newTarget.y()) > 2.0f |
398 | || qAbs(t: newTarget.z()) > 2.0f) |
399 | || (previousZoom > currentZoom && currentZoom <= driftTowardCenterLevel)) { |
400 | newTarget = zeroVector; |
401 | // Add some extra correction so that we actually reach the center eventually |
402 | diffAdj = m_driftMultiplier; |
403 | if (previousZoom > currentZoom) |
404 | diffAdj *= 2.0f; // Correct towards center little more when zooming out |
405 | } |
406 | |
407 | float zoomFraction = 1.0f - (previousZoom / currentZoom); |
408 | |
409 | // Adjust camera towards the zoom point, attempting to keep the cursor at same graph point |
410 | QVector3D oldTarget = q_ptr->scene()->activeCamera()->target(); |
411 | QVector3D origDiff = newTarget - oldTarget; |
412 | QVector3D diff = origDiff * zoomFraction + (origDiff.normalized() * diffAdj); |
413 | if (diff.length() > origDiff.length()) |
414 | diff = origDiff; |
415 | q_ptr->scene()->activeCamera()->setTarget(oldTarget + diff); |
416 | |
417 | if (q_ptr->scene()->selectionQueryPosition() == Q3DScene::invalidSelectionPoint()) |
418 | m_zoomAtTargetPending = false; |
419 | } |
420 | } |
421 | |
422 | QT_END_NAMESPACE |
423 | |