| 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 Qt Charts module of the Qt Toolkit. | 
| 7 | ** | 
| 8 | ** $QT_BEGIN_LICENSE:GPL$ | 
| 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 General Public License Usage | 
| 18 | ** Alternatively, this file may be used under the terms of the GNU | 
| 19 | ** General Public License version 3 or (at your option) any later version | 
| 20 | ** approved by the KDE Free Qt Foundation. The licenses are as published by | 
| 21 | ** the Free Software Foundation and appearing in the file LICENSE.GPL3 | 
| 22 | ** included in the packaging of this file. Please review the following | 
| 23 | ** information to ensure the GNU General Public License requirements will | 
| 24 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. | 
| 25 | ** | 
| 26 | ** $QT_END_LICENSE$ | 
| 27 | ** | 
| 28 | ****************************************************************************/ | 
| 29 |  | 
| 30 | #include "declarativeopenglrendernode_p.h" | 
| 31 |  | 
| 32 | #include <QtGui/QOpenGLContext> | 
| 33 | #include <QtGui/QOpenGLFunctions> | 
| 34 | #include <QtGui/QOpenGLFramebufferObjectFormat> | 
| 35 | #include <QtGui/QOpenGLFramebufferObject> | 
| 36 | #include <QOpenGLShaderProgram> | 
| 37 | #include <QtGui/QOpenGLBuffer> | 
| 38 |  | 
| 39 | //#define QDEBUG_TRACE_GL_FPS | 
| 40 | #ifdef QDEBUG_TRACE_GL_FPS | 
| 41 | #  include <QElapsedTimer> | 
| 42 | #endif | 
| 43 |  | 
| 44 | QT_CHARTS_BEGIN_NAMESPACE | 
| 45 |  | 
| 46 | // This node draws the xy series data on a transparent background using OpenGL. | 
| 47 | // It is used as a child node of the chart node. | 
| 48 | DeclarativeOpenGLRenderNode::DeclarativeOpenGLRenderNode(QQuickWindow *window) : | 
| 49 |     QObject(), | 
| 50 |     m_texture(nullptr), | 
| 51 |     m_imageNode(nullptr), | 
| 52 |     m_window(window), | 
| 53 |     m_textureOptions(QQuickWindow::TextureHasAlphaChannel), | 
| 54 |     m_textureSize(1, 1), | 
| 55 |     m_recreateFbo(false), | 
| 56 |     m_fbo(nullptr), | 
| 57 |     m_resolvedFbo(nullptr), | 
| 58 |     m_selectionFbo(nullptr), | 
| 59 |     m_program(nullptr), | 
| 60 |     m_shaderAttribLoc(-1), | 
| 61 |     m_colorUniformLoc(-1), | 
| 62 |     m_minUniformLoc(-1), | 
| 63 |     m_deltaUniformLoc(-1), | 
| 64 |     m_pointSizeUniformLoc(-1), | 
| 65 |     m_renderNeeded(true), | 
| 66 |     m_antialiasing(false), | 
| 67 |     m_selectionRenderNeeded(true), | 
| 68 |     m_mousePressed(false), | 
| 69 |     m_lastPressSeries(nullptr), | 
| 70 |     m_lastHoverSeries(nullptr) | 
| 71 | { | 
| 72 |     initializeOpenGLFunctions(); | 
| 73 |  | 
| 74 |     connect(sender: m_window, signal: &QQuickWindow::beforeRendering, | 
| 75 |             receiver: this, slot: &DeclarativeOpenGLRenderNode::render); | 
| 76 | } | 
| 77 |  | 
| 78 | DeclarativeOpenGLRenderNode::~DeclarativeOpenGLRenderNode() | 
| 79 | { | 
| 80 |     cleanXYSeriesResources(series: 0); | 
| 81 |  | 
| 82 |     delete m_texture; | 
| 83 |     delete m_fbo; | 
| 84 |     delete m_resolvedFbo; | 
| 85 |     delete m_selectionFbo; | 
| 86 |     delete m_program; | 
| 87 |  | 
| 88 |     qDeleteAll(c: m_mouseEvents); | 
| 89 | } | 
| 90 |  | 
| 91 | static const char *vertexSourceCore = | 
| 92 |         "#version 150\n"  | 
| 93 |         "in vec2 points;\n"  | 
| 94 |         "uniform vec2 min;\n"  | 
| 95 |         "uniform vec2 delta;\n"  | 
| 96 |         "uniform float pointSize;\n"  | 
| 97 |         "uniform mat4 matrix;\n"  | 
| 98 |         "void main() {\n"  | 
| 99 |         "  vec2 normalPoint = vec2(-1, -1) + ((points - min) / delta);\n"  | 
| 100 |         "  gl_Position = matrix * vec4(normalPoint, 0, 1);\n"  | 
| 101 |         "  gl_PointSize = pointSize;\n"  | 
| 102 |         "}" ; | 
| 103 | static const char *fragmentSourceCore = | 
| 104 |         "#version 150\n"  | 
| 105 |         "uniform vec3 color;\n"  | 
| 106 |         "out vec4 fragColor;\n"  | 
| 107 |         "void main() {\n"  | 
| 108 |         "  fragColor = vec4(color,1);\n"  | 
| 109 |         "}\n" ; | 
| 110 |  | 
| 111 | static const char *vertexSource = | 
| 112 |         "attribute highp vec2 points;\n"  | 
| 113 |         "uniform highp vec2 min;\n"  | 
| 114 |         "uniform highp vec2 delta;\n"  | 
| 115 |         "uniform highp float pointSize;\n"  | 
| 116 |         "uniform highp mat4 matrix;\n"  | 
| 117 |         "void main() {\n"  | 
| 118 |         "  vec2 normalPoint = vec2(-1, -1) + ((points - min) / delta);\n"  | 
| 119 |         "  gl_Position = matrix * vec4(normalPoint, 0, 1);\n"  | 
| 120 |         "  gl_PointSize = pointSize;\n"  | 
| 121 |         "}" ; | 
| 122 | static const char *fragmentSource = | 
| 123 |         "uniform highp vec3 color;\n"  | 
| 124 |         "void main() {\n"  | 
| 125 |         "  gl_FragColor = vec4(color,1);\n"  | 
| 126 |         "}\n" ; | 
| 127 |  | 
| 128 | // Must be called on render thread and in context | 
| 129 | void DeclarativeOpenGLRenderNode::initGL() | 
| 130 | { | 
| 131 |     recreateFBO(); | 
| 132 |  | 
| 133 |     m_program = new QOpenGLShaderProgram; | 
| 134 |     if (QOpenGLContext::currentContext()->format().profile() == QSurfaceFormat::CoreProfile) { | 
| 135 |         m_program->addShaderFromSourceCode(type: QOpenGLShader::Vertex, source: vertexSourceCore); | 
| 136 |         m_program->addShaderFromSourceCode(type: QOpenGLShader::Fragment, source: fragmentSourceCore); | 
| 137 |     } else { | 
| 138 |         m_program->addShaderFromSourceCode(type: QOpenGLShader::Vertex, source: vertexSource); | 
| 139 |         m_program->addShaderFromSourceCode(type: QOpenGLShader::Fragment, source: fragmentSource); | 
| 140 |     } | 
| 141 |     m_program->bindAttributeLocation(name: "points" , location: 0); | 
| 142 |     m_program->link(); | 
| 143 |  | 
| 144 |     m_program->bind(); | 
| 145 |     m_colorUniformLoc = m_program->uniformLocation(name: "color" ); | 
| 146 |     m_minUniformLoc = m_program->uniformLocation(name: "min" ); | 
| 147 |     m_deltaUniformLoc = m_program->uniformLocation(name: "delta" ); | 
| 148 |     m_pointSizeUniformLoc = m_program->uniformLocation(name: "pointSize" ); | 
| 149 |     m_matrixUniformLoc = m_program->uniformLocation(name: "matrix" ); | 
| 150 |  | 
| 151 |     // Create a vertex array object. In OpenGL ES 2.0 and OpenGL 2.x | 
| 152 |     // implementations this is optional and support may not be present | 
| 153 |     // at all. Nonetheless the below code works in all cases and makes | 
| 154 |     // sure there is a VAO when one is needed. | 
| 155 |     m_vao.create(); | 
| 156 |     QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); | 
| 157 |  | 
| 158 | #if !defined(QT_OPENGL_ES_2) | 
| 159 |     if (!QOpenGLContext::currentContext()->isOpenGLES()) { | 
| 160 |         // Make it possible to change point primitive size and use textures with them in | 
| 161 |         // the shaders. These are implicitly enabled in ES2. | 
| 162 |         // Qt Quick doesn't change these flags, so it should be safe to just enable them | 
| 163 |         // at initialization. | 
| 164 |         glEnable(GL_PROGRAM_POINT_SIZE); | 
| 165 |     } | 
| 166 | #endif | 
| 167 |  | 
| 168 |     m_program->release(); | 
| 169 | } | 
| 170 |  | 
| 171 | void DeclarativeOpenGLRenderNode::recreateFBO() | 
| 172 | { | 
| 173 |     QOpenGLFramebufferObjectFormat fboFormat; | 
| 174 |     fboFormat.setAttachment(QOpenGLFramebufferObject::NoAttachment); | 
| 175 |  | 
| 176 |     int samples = 0; | 
| 177 |     QOpenGLContext *context = QOpenGLContext::currentContext(); | 
| 178 |  | 
| 179 |     if (m_antialiasing && (!context->isOpenGLES() || context->format().majorVersion() >= 3)) | 
| 180 |         samples = 4; | 
| 181 |     fboFormat.setSamples(samples); | 
| 182 |  | 
| 183 |     delete m_fbo; | 
| 184 |     delete m_resolvedFbo; | 
| 185 |     delete m_selectionFbo; | 
| 186 |     m_resolvedFbo = nullptr; | 
| 187 |  | 
| 188 |     m_fbo = new QOpenGLFramebufferObject(m_textureSize, fboFormat); | 
| 189 |     if (samples > 0) | 
| 190 |         m_resolvedFbo = new QOpenGLFramebufferObject(m_textureSize); | 
| 191 |     m_selectionFbo = new QOpenGLFramebufferObject(m_textureSize); | 
| 192 |  | 
| 193 |     delete m_texture; | 
| 194 |     uint textureId = m_resolvedFbo ? m_resolvedFbo->texture() : m_fbo->texture(); | 
| 195 |     m_texture = m_window->createTextureFromId(id: textureId, size: m_textureSize, options: m_textureOptions); | 
| 196 |     if (!m_imageNode) { | 
| 197 |         m_imageNode = m_window->createImageNode(); | 
| 198 |         m_imageNode->setFiltering(QSGTexture::Linear); | 
| 199 |         m_imageNode->setTextureCoordinatesTransform(QSGImageNode::MirrorVertically); | 
| 200 |         m_imageNode->setFlag(OwnedByParent); | 
| 201 |         if (!m_rect.isEmpty()) | 
| 202 |             m_imageNode->setRect(m_rect); | 
| 203 |         appendChildNode(node: m_imageNode); | 
| 204 |     } | 
| 205 |     m_imageNode->setTexture(m_texture); | 
| 206 |  | 
| 207 |     m_recreateFbo = false; | 
| 208 | } | 
| 209 |  | 
| 210 | // Must be called on render thread and in context | 
| 211 | void DeclarativeOpenGLRenderNode::setTextureSize(const QSize &size) | 
| 212 | { | 
| 213 |     m_textureSize = size; | 
| 214 |     m_recreateFbo = true; | 
| 215 |     m_renderNeeded = true; | 
| 216 |     m_selectionRenderNeeded = true; | 
| 217 | } | 
| 218 |  | 
| 219 | // Must be called on render thread while gui thread is blocked, and in context | 
| 220 | void DeclarativeOpenGLRenderNode::setSeriesData(bool mapDirty, const GLXYDataMap &dataMap) | 
| 221 | { | 
| 222 |     bool dirty = false; | 
| 223 |     if (mapDirty) { | 
| 224 |         // Series have changed, recreate map, but utilize old data where feasible | 
| 225 |         GLXYDataMap oldMap = m_xyDataMap; | 
| 226 |         m_xyDataMap.clear(); | 
| 227 |  | 
| 228 |         for (auto i = dataMap.begin(), end = dataMap.end(); i != end; ++i) { | 
| 229 |             GLXYSeriesData *data = oldMap.take(akey: i.key()); | 
| 230 |             const GLXYSeriesData *newData = i.value(); | 
| 231 |             if (!data || newData->dirty) { | 
| 232 |                 if (!data) | 
| 233 |                     data = new GLXYSeriesData; | 
| 234 |                 *data = *newData; | 
| 235 |             } | 
| 236 |             m_xyDataMap.insert(akey: i.key(), avalue: data); | 
| 237 |         } | 
| 238 |         // Delete remaining old data | 
| 239 |         for (auto i = oldMap.begin(), end = oldMap.end(); i != end; ++i) { | 
| 240 |             delete i.value(); | 
| 241 |             cleanXYSeriesResources(series: i.key()); | 
| 242 |         } | 
| 243 |         dirty = true; | 
| 244 |     } else { | 
| 245 |         // Series have not changed, so just copy dirty data over | 
| 246 |         for (auto i = dataMap.begin(), end = dataMap.end(); i != end; ++i) { | 
| 247 |             const GLXYSeriesData *newData = i.value(); | 
| 248 |             if (i.value()->dirty) { | 
| 249 |                 dirty = true; | 
| 250 |                 GLXYSeriesData *data = m_xyDataMap.value(akey: i.key()); | 
| 251 |                 if (data) | 
| 252 |                     *data = *newData; | 
| 253 |             } | 
| 254 |         } | 
| 255 |     } | 
| 256 |     if (dirty) { | 
| 257 |         markDirty(bits: DirtyMaterial); | 
| 258 |         m_renderNeeded = true; | 
| 259 |         m_selectionRenderNeeded = true; | 
| 260 |     } | 
| 261 | } | 
| 262 |  | 
| 263 | void DeclarativeOpenGLRenderNode::setRect(const QRectF &rect) | 
| 264 | { | 
| 265 |     m_rect = rect; | 
| 266 |  | 
| 267 |     if (m_imageNode) | 
| 268 |         m_imageNode->setRect(rect); | 
| 269 | } | 
| 270 |  | 
| 271 | void DeclarativeOpenGLRenderNode::setAntialiasing(bool enable) | 
| 272 | { | 
| 273 |     if (m_antialiasing != enable) { | 
| 274 |         m_antialiasing = enable; | 
| 275 |         m_recreateFbo = true; | 
| 276 |         m_renderNeeded = true; | 
| 277 |     } | 
| 278 | } | 
| 279 |  | 
| 280 | void DeclarativeOpenGLRenderNode::addMouseEvents(const QVector<QMouseEvent *> &events) | 
| 281 | { | 
| 282 |     if (events.size()) { | 
| 283 |         m_mouseEvents.append(l: events); | 
| 284 |         markDirty(bits: DirtyMaterial); | 
| 285 |     } | 
| 286 | } | 
| 287 |  | 
| 288 | void DeclarativeOpenGLRenderNode::takeMouseEventResponses(QVector<MouseEventResponse> &responses) | 
| 289 | { | 
| 290 |     responses.append(l: m_mouseEventResponses); | 
| 291 |     m_mouseEventResponses.clear(); | 
| 292 | } | 
| 293 |  | 
| 294 | void DeclarativeOpenGLRenderNode::renderGL(bool selection) | 
| 295 | { | 
| 296 |     glClearColor(red: 0, green: 0, blue: 0, alpha: 0); | 
| 297 |  | 
| 298 |     QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); | 
| 299 |     m_program->bind(); | 
| 300 |  | 
| 301 |     glClear(GL_COLOR_BUFFER_BIT); | 
| 302 |     glEnableVertexAttribArray(index: 0); | 
| 303 |  | 
| 304 |     glViewport(x: 0, y: 0, width: m_textureSize.width(), height: m_textureSize.height()); | 
| 305 |  | 
| 306 |     int counter = 0; | 
| 307 |     for (auto i = m_xyDataMap.begin(), end = m_xyDataMap.end(); i != end; ++i) { | 
| 308 |         QOpenGLBuffer *vbo = m_seriesBufferMap.value(akey: i.key()); | 
| 309 |         GLXYSeriesData *data = i.value(); | 
| 310 |  | 
| 311 |         if (data->visible) { | 
| 312 |             if (selection) { | 
| 313 |                 m_selectionVector[counter] = i.key(); | 
| 314 |                 m_program->setUniformValue(location: m_colorUniformLoc, value: QVector3D((counter & 0xff) / 255.0f, | 
| 315 |                                                                         ((counter & 0xff00) >> 8) / 255.0f, | 
| 316 |                                                                         ((counter & 0xff0000) >> 16) / 255.0f)); | 
| 317 |                 counter++; | 
| 318 |             } else { | 
| 319 |                 m_program->setUniformValue(location: m_colorUniformLoc, value: data->color); | 
| 320 |             } | 
| 321 |             m_program->setUniformValue(location: m_minUniformLoc, value: data->min); | 
| 322 |             m_program->setUniformValue(location: m_deltaUniformLoc, value: data->delta); | 
| 323 |             m_program->setUniformValue(location: m_matrixUniformLoc, value: data->matrix); | 
| 324 |  | 
| 325 |             if (!vbo) { | 
| 326 |                 vbo = new QOpenGLBuffer; | 
| 327 |                 m_seriesBufferMap.insert(akey: i.key(), avalue: vbo); | 
| 328 |                 vbo->create(); | 
| 329 |             } | 
| 330 |             vbo->bind(); | 
| 331 |             if (data->dirty) { | 
| 332 |                 vbo->allocate(data: data->array.constData(), count: data->array.count() * sizeof(GLfloat)); | 
| 333 |                 data->dirty = false; | 
| 334 |             } | 
| 335 |  | 
| 336 |             glVertexAttribPointer(indx: 0, size: 2, GL_FLOAT, GL_FALSE, stride: 0, ptr: 0); | 
| 337 |             if (data->type == QAbstractSeries::SeriesTypeLine) { | 
| 338 |                 glLineWidth(width: data->width); | 
| 339 |                 glDrawArrays(GL_LINE_STRIP, first: 0, count: data->array.size() / 2); | 
| 340 |             } else { // Scatter | 
| 341 |                 m_program->setUniformValue(location: m_pointSizeUniformLoc, value: data->width); | 
| 342 |                 glDrawArrays(GL_POINTS, first: 0, count: data->array.size() / 2); | 
| 343 |             } | 
| 344 |             vbo->release(); | 
| 345 |         } | 
| 346 |     } | 
| 347 | } | 
| 348 |  | 
| 349 | void DeclarativeOpenGLRenderNode::renderSelection() | 
| 350 | { | 
| 351 |     m_selectionFbo->bind(); | 
| 352 |  | 
| 353 |     m_selectionVector.resize(asize: m_xyDataMap.size()); | 
| 354 |  | 
| 355 |     renderGL(selection: true); | 
| 356 |  | 
| 357 |     m_selectionRenderNeeded = false; | 
| 358 | } | 
| 359 |  | 
| 360 | void DeclarativeOpenGLRenderNode::renderVisual() | 
| 361 | { | 
| 362 |     m_fbo->bind(); | 
| 363 |  | 
| 364 |     renderGL(selection: false); | 
| 365 |  | 
| 366 |     if (m_resolvedFbo) { | 
| 367 |         QRect rect(QPoint(0, 0), m_fbo->size()); | 
| 368 |         QOpenGLFramebufferObject::blitFramebuffer(target: m_resolvedFbo, targetRect: rect, source: m_fbo, sourceRect: rect); | 
| 369 |     } | 
| 370 |  | 
| 371 |     markDirty(bits: DirtyMaterial); | 
| 372 |  | 
| 373 | #ifdef QDEBUG_TRACE_GL_FPS | 
| 374 |     static QElapsedTimer stopWatch; | 
| 375 |     static int frameCount = -1; | 
| 376 |     if (frameCount == -1) { | 
| 377 |         stopWatch.start(); | 
| 378 |         frameCount = 0; | 
| 379 |     } | 
| 380 |     frameCount++; | 
| 381 |     int elapsed = stopWatch.elapsed(); | 
| 382 |     if (elapsed >= 1000) { | 
| 383 |         elapsed = stopWatch.restart(); | 
| 384 |         qreal fps = qreal(0.1 * int(10000.0 * (qreal(frameCount) / qreal(elapsed)))); | 
| 385 |         qDebug() << "FPS:"  << fps; | 
| 386 |         frameCount = 0; | 
| 387 |     } | 
| 388 | #endif | 
| 389 | } | 
| 390 |  | 
| 391 | // Must be called on render thread as response to beforeRendering signal | 
| 392 | void DeclarativeOpenGLRenderNode::render() | 
| 393 | { | 
| 394 |     if (m_renderNeeded) { | 
| 395 |         if (m_xyDataMap.size()) { | 
| 396 |             if (!m_program) | 
| 397 |                 initGL(); | 
| 398 |             if (m_recreateFbo) | 
| 399 |                 recreateFBO(); | 
| 400 |             renderVisual(); | 
| 401 |         } else { | 
| 402 |             if (m_imageNode && m_imageNode->rect() != QRectF()) { | 
| 403 |                 glClearColor(red: 0, green: 0, blue: 0, alpha: 0); | 
| 404 |                 m_fbo->bind(); | 
| 405 |                 glClear(GL_COLOR_BUFFER_BIT); | 
| 406 |  | 
| 407 |                 // If last series was removed, zero out the node rect | 
| 408 |                 setRect(QRectF()); | 
| 409 |             } | 
| 410 |         } | 
| 411 |         m_renderNeeded = false; | 
| 412 |     } | 
| 413 |     handleMouseEvents(); | 
| 414 |     m_window->resetOpenGLState(); | 
| 415 | } | 
| 416 |  | 
| 417 | void DeclarativeOpenGLRenderNode::cleanXYSeriesResources(const QXYSeries *series) | 
| 418 | { | 
| 419 |     if (series) { | 
| 420 |         delete m_seriesBufferMap.take(akey: series); | 
| 421 |         delete m_xyDataMap.take(akey: series); | 
| 422 |     } else { | 
| 423 |         foreach (QOpenGLBuffer *buffer, m_seriesBufferMap.values()) | 
| 424 |             delete buffer; | 
| 425 |         m_seriesBufferMap.clear(); | 
| 426 |         foreach (GLXYSeriesData *data, m_xyDataMap.values()) | 
| 427 |             delete data; | 
| 428 |         m_xyDataMap.clear(); | 
| 429 |     } | 
| 430 | } | 
| 431 |  | 
| 432 | void DeclarativeOpenGLRenderNode::handleMouseEvents() | 
| 433 | { | 
| 434 |     if (m_mouseEvents.size()) { | 
| 435 |         if (m_xyDataMap.size()) { | 
| 436 |             if (m_selectionRenderNeeded) | 
| 437 |                 renderSelection(); | 
| 438 |         } | 
| 439 |         Q_FOREACH (QMouseEvent *event, m_mouseEvents) { | 
| 440 |             const QXYSeries *series = findSeriesAtEvent(event); | 
| 441 |             switch (event->type()) { | 
| 442 |             case QEvent::MouseMove: { | 
| 443 |                 if (series != m_lastHoverSeries) { | 
| 444 |                     if (m_lastHoverSeries) { | 
| 445 |                         m_mouseEventResponses.append( | 
| 446 |                                     t: MouseEventResponse(MouseEventResponse::HoverLeave, | 
| 447 |                                                        event->pos(), m_lastHoverSeries)); | 
| 448 |                     } | 
| 449 |                     if (series) { | 
| 450 |                         m_mouseEventResponses.append( | 
| 451 |                                     t: MouseEventResponse(MouseEventResponse::HoverEnter, | 
| 452 |                                                        event->pos(), series)); | 
| 453 |                     } | 
| 454 |                     m_lastHoverSeries = series; | 
| 455 |                 } | 
| 456 |                 break; | 
| 457 |             } | 
| 458 |             case QEvent::MouseButtonPress: { | 
| 459 |                 if (series) { | 
| 460 |                     m_mousePressed = true; | 
| 461 |                     m_mousePressPos = event->pos(); | 
| 462 |                     m_lastPressSeries = series; | 
| 463 |                     m_mouseEventResponses.append( | 
| 464 |                                 t: MouseEventResponse(MouseEventResponse::Pressed, | 
| 465 |                                                    event->pos(), series)); | 
| 466 |                 } | 
| 467 |                 break; | 
| 468 |             } | 
| 469 |             case QEvent::MouseButtonRelease: { | 
| 470 |                 m_mouseEventResponses.append( | 
| 471 |                             t: MouseEventResponse(MouseEventResponse::Released, | 
| 472 |                                                m_mousePressPos, m_lastPressSeries)); | 
| 473 |                 if (m_mousePressed) { | 
| 474 |                     m_mouseEventResponses.append( | 
| 475 |                                 t: MouseEventResponse(MouseEventResponse::Clicked, | 
| 476 |                                                    m_mousePressPos, m_lastPressSeries)); | 
| 477 |                 } | 
| 478 |                 if (m_lastHoverSeries == m_lastPressSeries && m_lastHoverSeries != series) { | 
| 479 |                     if (m_lastHoverSeries) { | 
| 480 |                         m_mouseEventResponses.append( | 
| 481 |                                     t: MouseEventResponse(MouseEventResponse::HoverLeave, | 
| 482 |                                                        event->pos(), m_lastHoverSeries)); | 
| 483 |                     } | 
| 484 |                     m_lastHoverSeries = nullptr; | 
| 485 |                 } | 
| 486 |                 m_lastPressSeries = nullptr; | 
| 487 |                 m_mousePressed = false; | 
| 488 |                 break; | 
| 489 |             } | 
| 490 |             case QEvent::MouseButtonDblClick: { | 
| 491 |                 if (series) { | 
| 492 |                     m_mouseEventResponses.append( | 
| 493 |                                 t: MouseEventResponse(MouseEventResponse::DoubleClicked, | 
| 494 |                                                    event->pos(), series)); | 
| 495 |                 } | 
| 496 |                 break; | 
| 497 |             } | 
| 498 |             default: | 
| 499 |                 break; | 
| 500 |             } | 
| 501 |         } | 
| 502 |  | 
| 503 |         qDeleteAll(c: m_mouseEvents); | 
| 504 |         m_mouseEvents.clear(); | 
| 505 |     } | 
| 506 | } | 
| 507 |  | 
| 508 | const QXYSeries *DeclarativeOpenGLRenderNode::findSeriesAtEvent(QMouseEvent *event) | 
| 509 | { | 
| 510 |     const QXYSeries *series = nullptr; | 
| 511 |     int index = -1; | 
| 512 |  | 
| 513 |     if (m_xyDataMap.size()) { | 
| 514 |         m_selectionFbo->bind(); | 
| 515 |  | 
| 516 |         GLubyte pixel[4] = {0, 0, 0, 0}; | 
| 517 |         glReadPixels(x: event->pos().x(), y: m_textureSize.height() - event->pos().y(), | 
| 518 |                      width: 1, height: 1, GL_RGBA, GL_UNSIGNED_BYTE, | 
| 519 |                      pixels: (void *)pixel); | 
| 520 |         if (pixel[3] == 0xff) | 
| 521 |             index = pixel[0] + (pixel[1] << 8) + (pixel[2] << 16); | 
| 522 |     } | 
| 523 |  | 
| 524 |     if (index >= 0 && index < m_selectionVector.size()) | 
| 525 |         series = m_selectionVector.at(i: index); | 
| 526 |  | 
| 527 |     return series; | 
| 528 | } | 
| 529 |  | 
| 530 | QT_CHARTS_END_NAMESPACE | 
| 531 |  |