| 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 | #ifndef QT_NO_OPENGL |
| 31 | |
| 32 | #include "private/glwidget_p.h" |
| 33 | #include "private/glxyseriesdata_p.h" |
| 34 | #include "private/qabstractseries_p.h" |
| 35 | #include <QtGui/QOpenGLShaderProgram> |
| 36 | #include <QtGui/QOpenGLContext> |
| 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 | GLWidget::GLWidget(GLXYSeriesDataManager *xyDataManager, QtCharts::QChart *chart, |
| 47 | QGraphicsView *parent) |
| 48 | : QOpenGLWidget(parent->viewport()), |
| 49 | m_program(nullptr), |
| 50 | m_shaderAttribLoc(-1), |
| 51 | m_colorUniformLoc(-1), |
| 52 | m_minUniformLoc(-1), |
| 53 | m_deltaUniformLoc(-1), |
| 54 | m_pointSizeUniformLoc(-1), |
| 55 | m_xyDataManager(xyDataManager), |
| 56 | m_antiAlias(parent->renderHints().testFlag(flag: QPainter::Antialiasing)), |
| 57 | m_view(parent), |
| 58 | m_selectionFbo(nullptr), |
| 59 | m_chart(chart), |
| 60 | m_recreateSelectionFbo(true), |
| 61 | m_selectionRenderNeeded(true), |
| 62 | m_mousePressed(false), |
| 63 | m_lastPressSeries(nullptr), |
| 64 | m_lastHoverSeries(nullptr) |
| 65 | { |
| 66 | setAttribute(Qt::WA_TranslucentBackground); |
| 67 | setAttribute(Qt::WA_AlwaysStackOnTop); |
| 68 | |
| 69 | QSurfaceFormat surfaceFormat; |
| 70 | surfaceFormat.setDepthBufferSize(0); |
| 71 | surfaceFormat.setStencilBufferSize(0); |
| 72 | surfaceFormat.setRedBufferSize(8); |
| 73 | surfaceFormat.setGreenBufferSize(8); |
| 74 | surfaceFormat.setBlueBufferSize(8); |
| 75 | surfaceFormat.setAlphaBufferSize(8); |
| 76 | surfaceFormat.setSwapBehavior(QSurfaceFormat::DoubleBuffer); |
| 77 | surfaceFormat.setRenderableType(QSurfaceFormat::DefaultRenderableType); |
| 78 | surfaceFormat.setSamples(m_antiAlias ? 4 : 0); |
| 79 | setFormat(surfaceFormat); |
| 80 | |
| 81 | connect(sender: xyDataManager, signal: &GLXYSeriesDataManager::seriesRemoved, |
| 82 | receiver: this, slot: &GLWidget::cleanXYSeriesResources); |
| 83 | |
| 84 | setMouseTracking(true); |
| 85 | } |
| 86 | |
| 87 | GLWidget::~GLWidget() |
| 88 | { |
| 89 | cleanup(); |
| 90 | } |
| 91 | |
| 92 | void GLWidget::cleanup() |
| 93 | { |
| 94 | makeCurrent(); |
| 95 | |
| 96 | delete m_program; |
| 97 | m_program = 0; |
| 98 | |
| 99 | foreach (QOpenGLBuffer *buffer, m_seriesBufferMap.values()) |
| 100 | delete buffer; |
| 101 | m_seriesBufferMap.clear(); |
| 102 | |
| 103 | doneCurrent(); |
| 104 | } |
| 105 | |
| 106 | void GLWidget::cleanXYSeriesResources(const QXYSeries *series) |
| 107 | { |
| 108 | makeCurrent(); |
| 109 | if (series) { |
| 110 | delete m_seriesBufferMap.take(akey: series); |
| 111 | } else { |
| 112 | // Null series means all series were removed |
| 113 | foreach (QOpenGLBuffer *buffer, m_seriesBufferMap.values()) |
| 114 | delete buffer; |
| 115 | m_seriesBufferMap.clear(); |
| 116 | } |
| 117 | doneCurrent(); |
| 118 | } |
| 119 | |
| 120 | static const char *vertexSource = |
| 121 | "attribute highp vec2 points;\n" |
| 122 | "uniform highp vec2 min;\n" |
| 123 | "uniform highp vec2 delta;\n" |
| 124 | "uniform highp float pointSize;\n" |
| 125 | "uniform highp mat4 matrix;\n" |
| 126 | "void main() {\n" |
| 127 | " vec2 normalPoint = vec2(-1, -1) + ((points - min) / delta);\n" |
| 128 | " gl_Position = matrix * vec4(normalPoint, 0, 1);\n" |
| 129 | " gl_PointSize = pointSize;\n" |
| 130 | "}" ; |
| 131 | static const char *fragmentSource = |
| 132 | "uniform highp vec3 color;\n" |
| 133 | "void main() {\n" |
| 134 | " gl_FragColor = vec4(color,1);\n" |
| 135 | "}\n" ; |
| 136 | |
| 137 | void GLWidget::initializeGL() |
| 138 | { |
| 139 | connect(sender: context(), signal: &QOpenGLContext::aboutToBeDestroyed, receiver: this, slot: &GLWidget::cleanup); |
| 140 | |
| 141 | initializeOpenGLFunctions(); |
| 142 | glClearColor(red: 0, green: 0, blue: 0, alpha: 0); |
| 143 | |
| 144 | m_program = new QOpenGLShaderProgram; |
| 145 | m_program->addShaderFromSourceCode(type: QOpenGLShader::Vertex, source: vertexSource); |
| 146 | m_program->addShaderFromSourceCode(type: QOpenGLShader::Fragment, source: fragmentSource); |
| 147 | m_program->bindAttributeLocation(name: "points" , location: 0); |
| 148 | m_program->link(); |
| 149 | |
| 150 | m_program->bind(); |
| 151 | m_colorUniformLoc = m_program->uniformLocation(name: "color" ); |
| 152 | m_minUniformLoc = m_program->uniformLocation(name: "min" ); |
| 153 | m_deltaUniformLoc = m_program->uniformLocation(name: "delta" ); |
| 154 | m_pointSizeUniformLoc = m_program->uniformLocation(name: "pointSize" ); |
| 155 | m_matrixUniformLoc = m_program->uniformLocation(name: "matrix" ); |
| 156 | |
| 157 | |
| 158 | // Create a vertex array object. In OpenGL ES 2.0 and OpenGL 2.x |
| 159 | // implementations this is optional and support may not be present |
| 160 | // at all. Nonetheless the below code works in all cases and makes |
| 161 | // sure there is a VAO when one is needed. |
| 162 | m_vao.create(); |
| 163 | QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); |
| 164 | |
| 165 | glEnableVertexAttribArray(index: 0); |
| 166 | |
| 167 | glDisable(GL_DEPTH_TEST); |
| 168 | glDisable(GL_STENCIL_TEST); |
| 169 | |
| 170 | #if !defined(QT_OPENGL_ES_2) |
| 171 | if (!QOpenGLContext::currentContext()->isOpenGLES()) { |
| 172 | // Make it possible to change point primitive size and use textures with them in |
| 173 | // the shaders. These are implicitly enabled in ES2. |
| 174 | glEnable(GL_PROGRAM_POINT_SIZE); |
| 175 | } |
| 176 | #endif |
| 177 | |
| 178 | m_program->release(); |
| 179 | } |
| 180 | |
| 181 | void GLWidget::paintGL() |
| 182 | { |
| 183 | render(selection: false); |
| 184 | |
| 185 | #ifdef QDEBUG_TRACE_GL_FPS |
| 186 | static QElapsedTimer stopWatch; |
| 187 | static int frameCount = -1; |
| 188 | if (frameCount == -1) { |
| 189 | stopWatch.start(); |
| 190 | frameCount = 0; |
| 191 | } |
| 192 | frameCount++; |
| 193 | int elapsed = stopWatch.elapsed(); |
| 194 | if (elapsed >= 1000) { |
| 195 | elapsed = stopWatch.restart(); |
| 196 | qreal fps = qreal(0.1 * int(10000.0 * (qreal(frameCount) / qreal(elapsed)))); |
| 197 | qDebug() << "FPS:" << fps; |
| 198 | frameCount = 0; |
| 199 | } |
| 200 | #endif |
| 201 | } |
| 202 | |
| 203 | void GLWidget::resizeGL(int w, int h) |
| 204 | { |
| 205 | m_fboSize.setWidth(w); |
| 206 | m_fboSize.setHeight(h); |
| 207 | m_recreateSelectionFbo = true; |
| 208 | m_selectionRenderNeeded = true; |
| 209 | } |
| 210 | |
| 211 | void GLWidget::mouseDoubleClickEvent(QMouseEvent *event) |
| 212 | { |
| 213 | QXYSeries *series = findSeriesAtEvent(event); |
| 214 | if (series) |
| 215 | emit series->doubleClicked(point: series->d_ptr->domain()->calculateDomainPoint(point: event->pos())); |
| 216 | } |
| 217 | |
| 218 | void GLWidget::mouseMoveEvent(QMouseEvent *event) |
| 219 | { |
| 220 | if (m_view->hasMouseTracking() && !event->buttons()) { |
| 221 | QXYSeries *series = findSeriesAtEvent(event); |
| 222 | if (series != m_lastHoverSeries) { |
| 223 | if (m_lastHoverSeries) { |
| 224 | if (chartSeries(cSeries: m_lastHoverSeries)) { |
| 225 | emit m_lastHoverSeries->hovered( |
| 226 | point: m_lastHoverSeries->d_ptr->domain()->calculateDomainPoint( |
| 227 | point: event->pos()), state: false); |
| 228 | } |
| 229 | } |
| 230 | if (series) { |
| 231 | emit series->hovered( |
| 232 | point: series->d_ptr->domain()->calculateDomainPoint(point: event->pos()), state: true); |
| 233 | } |
| 234 | m_lastHoverSeries = series; |
| 235 | } |
| 236 | } else { |
| 237 | event->ignore(); |
| 238 | } |
| 239 | } |
| 240 | |
| 241 | void GLWidget::mousePressEvent(QMouseEvent *event) |
| 242 | { |
| 243 | QXYSeries *series = findSeriesAtEvent(event); |
| 244 | if (series) { |
| 245 | m_mousePressed = true; |
| 246 | m_mousePressPos = event->pos(); |
| 247 | m_lastPressSeries = series; |
| 248 | emit series->pressed(point: series->d_ptr->domain()->calculateDomainPoint(point: event->pos())); |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | void GLWidget::mouseReleaseEvent(QMouseEvent *event) |
| 253 | { |
| 254 | if (chartSeries(cSeries: m_lastPressSeries)) { |
| 255 | emit m_lastPressSeries->released( |
| 256 | point: m_lastPressSeries->d_ptr->domain()->calculateDomainPoint(point: m_mousePressPos)); |
| 257 | if (m_mousePressed) { |
| 258 | emit m_lastPressSeries->clicked( |
| 259 | point: m_lastPressSeries->d_ptr->domain()->calculateDomainPoint(point: m_mousePressPos)); |
| 260 | } |
| 261 | if (m_lastHoverSeries == m_lastPressSeries |
| 262 | && m_lastHoverSeries != findSeriesAtEvent(event)) { |
| 263 | if (chartSeries(cSeries: m_lastHoverSeries)) { |
| 264 | emit m_lastHoverSeries->hovered( |
| 265 | point: m_lastHoverSeries->d_ptr->domain()->calculateDomainPoint( |
| 266 | point: event->pos()), state: false); |
| 267 | } |
| 268 | m_lastHoverSeries = nullptr; |
| 269 | } |
| 270 | m_lastPressSeries = nullptr; |
| 271 | m_mousePressed = false; |
| 272 | } else { |
| 273 | event->ignore(); |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | QXYSeries *GLWidget::findSeriesAtEvent(QMouseEvent *event) |
| 278 | { |
| 279 | QXYSeries *series = nullptr; |
| 280 | int index = -1; |
| 281 | |
| 282 | if (m_xyDataManager->dataMap().size()) { |
| 283 | makeCurrent(); |
| 284 | |
| 285 | if (m_recreateSelectionFbo) |
| 286 | recreateSelectionFbo(); |
| 287 | |
| 288 | m_selectionFbo->bind(); |
| 289 | |
| 290 | if (m_selectionRenderNeeded) { |
| 291 | m_selectionVector.resize(asize: m_xyDataManager->dataMap().size()); |
| 292 | render(selection: true); |
| 293 | m_selectionRenderNeeded = false; |
| 294 | } |
| 295 | |
| 296 | GLubyte pixel[4] = {0, 0, 0, 0}; |
| 297 | glReadPixels(x: event->pos().x(), y: m_fboSize.height() - event->pos().y(), |
| 298 | width: 1, height: 1, GL_RGBA, GL_UNSIGNED_BYTE, |
| 299 | pixels: (void *)pixel); |
| 300 | if (pixel[3] == 0xff) |
| 301 | index = pixel[0] + (pixel[1] << 8) + (pixel[2] << 16); |
| 302 | |
| 303 | glBindFramebuffer(GL_FRAMEBUFFER, framebuffer: defaultFramebufferObject()); |
| 304 | |
| 305 | doneCurrent(); |
| 306 | } |
| 307 | |
| 308 | if (index >= 0) { |
| 309 | const QXYSeries *cSeries = nullptr; |
| 310 | if (index < m_selectionVector.size()) |
| 311 | cSeries = m_selectionVector.at(i: index); |
| 312 | |
| 313 | series = chartSeries(cSeries); |
| 314 | } |
| 315 | if (series) |
| 316 | event->accept(); |
| 317 | else |
| 318 | event->ignore(); |
| 319 | |
| 320 | return series; |
| 321 | } |
| 322 | |
| 323 | void GLWidget::render(bool selection) |
| 324 | { |
| 325 | glClear(GL_COLOR_BUFFER_BIT); |
| 326 | |
| 327 | QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); |
| 328 | m_program->bind(); |
| 329 | |
| 330 | int counter = 0; |
| 331 | const auto &dataMap = m_xyDataManager->dataMap(); |
| 332 | for (auto i = dataMap.begin(), end = dataMap.end(); i != end; ++i) { |
| 333 | QOpenGLBuffer *vbo = m_seriesBufferMap.value(akey: i.key()); |
| 334 | GLXYSeriesData *data = i.value(); |
| 335 | |
| 336 | if (data->visible) { |
| 337 | if (selection) { |
| 338 | m_selectionVector[counter] = i.key(); |
| 339 | m_program->setUniformValue(location: m_colorUniformLoc, value: QVector3D((counter & 0xff) / 255.0f, |
| 340 | ((counter & 0xff00) >> 8) / 255.0f, |
| 341 | ((counter & 0xff0000) >> 16) / 255.0f)); |
| 342 | counter++; |
| 343 | } else { |
| 344 | m_program->setUniformValue(location: m_colorUniformLoc, value: data->color); |
| 345 | } |
| 346 | m_program->setUniformValue(location: m_minUniformLoc, value: data->min); |
| 347 | m_program->setUniformValue(location: m_deltaUniformLoc, value: data->delta); |
| 348 | m_program->setUniformValue(location: m_matrixUniformLoc, value: data->matrix); |
| 349 | bool dirty = data->dirty; |
| 350 | if (!vbo) { |
| 351 | vbo = new QOpenGLBuffer; |
| 352 | m_seriesBufferMap.insert(akey: i.key(), avalue: vbo); |
| 353 | vbo->create(); |
| 354 | dirty = true; |
| 355 | } |
| 356 | vbo->bind(); |
| 357 | if (dirty) { |
| 358 | vbo->allocate(data: data->array.constData(), count: data->array.count() * sizeof(GLfloat)); |
| 359 | dirty = false; |
| 360 | m_selectionRenderNeeded = true; |
| 361 | } |
| 362 | |
| 363 | glVertexAttribPointer(indx: 0, size: 2, GL_FLOAT, GL_FALSE, stride: 0, ptr: 0); |
| 364 | if (data->type == QAbstractSeries::SeriesTypeLine) { |
| 365 | glLineWidth(width: data->width); |
| 366 | glDrawArrays(GL_LINE_STRIP, first: 0, count: data->array.size() / 2); |
| 367 | } else { // Scatter |
| 368 | m_program->setUniformValue(location: m_pointSizeUniformLoc, value: data->width); |
| 369 | glDrawArrays(GL_POINTS, first: 0, count: data->array.size() / 2); |
| 370 | } |
| 371 | vbo->release(); |
| 372 | } |
| 373 | } |
| 374 | m_program->release(); |
| 375 | } |
| 376 | |
| 377 | void GLWidget::recreateSelectionFbo() |
| 378 | { |
| 379 | QOpenGLFramebufferObjectFormat fboFormat; |
| 380 | fboFormat.setAttachment(QOpenGLFramebufferObject::NoAttachment); |
| 381 | |
| 382 | delete m_selectionFbo; |
| 383 | |
| 384 | const QSize deviceSize = m_fboSize * devicePixelRatioF(); |
| 385 | m_selectionFbo = new QOpenGLFramebufferObject(deviceSize, fboFormat); |
| 386 | m_recreateSelectionFbo = false; |
| 387 | m_selectionRenderNeeded = true; |
| 388 | } |
| 389 | |
| 390 | // This function makes sure the series we are dealing with has not been removed from the |
| 391 | // chart since we stored the pointer. |
| 392 | QXYSeries *GLWidget::chartSeries(const QXYSeries *cSeries) |
| 393 | { |
| 394 | QXYSeries *series = nullptr; |
| 395 | if (cSeries) { |
| 396 | Q_FOREACH (QAbstractSeries *chartSeries, m_chart->series()) { |
| 397 | if (cSeries == chartSeries) { |
| 398 | series = qobject_cast<QXYSeries *>(object: chartSeries); |
| 399 | break; |
| 400 | } |
| 401 | } |
| 402 | } |
| 403 | return series; |
| 404 | } |
| 405 | |
| 406 | bool GLWidget::needsReset() const |
| 407 | { |
| 408 | return m_view->renderHints().testFlag(flag: QPainter::Antialiasing) != m_antiAlias; |
| 409 | } |
| 410 | |
| 411 | QT_CHARTS_END_NAMESPACE |
| 412 | |
| 413 | #endif |
| 414 | |