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
8QT_BEGIN_NAMESPACE
9
10static const int halfSizeZoomLevel = 50;
11static const int oneToOneZoomLevel = 100;
12static const int driftTowardCenterLevel = 175;
13static const float wheelZoomDrift = 0.1f;
14
15static const int nearZoomRangeDivider = 12;
16static const int midZoomRangeDivider = 60;
17static const int farZoomRangeDivider = 120;
18
19static 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 */
98Q3DInputHandler::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 */
109Q3DInputHandler::Q3DInputHandler(QObject *parent) :
110 QAbstract3DInputHandler(new Q3DInputHandlerPrivate(this), parent)
111{
112}
113
114/*!
115 * Destroys the input handler.
116 */
117Q3DInputHandler::~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 */
126void 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 */
171void 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 */
191void 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 */
224void 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 */
269void 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
278bool 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 */
291void 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
300bool 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 */
313void 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
322bool 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 */
336void 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
345bool Q3DInputHandler::isZoomAtTargetEnabled() const
346{
347 const Q_D(Q3DInputHandler);
348 return d->m_zoomAtTargetEnabled;
349}
350
351Q3DInputHandlerPrivate::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
364Q3DInputHandlerPrivate::~Q3DInputHandlerPrivate()
365{
366}
367
368void 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
384void 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
422QT_END_NAMESPACE
423

source code of qtgraphs/src/graphs/input/q3dinputhandler.cpp