| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the Qt Charts module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:GPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 3 or (at your option) any later version |
| ** approved by the KDE Free Qt Foundation. The licenses are as published by |
| ** the Free Software Foundation and appearing in the file LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #ifndef QT_NO_OPENGL |
| |
| #include "private/glwidget_p.h" |
| #include "private/glxyseriesdata_p.h" |
| #include "private/qabstractseries_p.h" |
| #include <QtGui/QOpenGLShaderProgram> |
| #include <QtGui/QOpenGLContext> |
| #include <QtGui/QOpenGLBuffer> |
| |
| //#define QDEBUG_TRACE_GL_FPS |
| #ifdef QDEBUG_TRACE_GL_FPS |
| # include <QElapsedTimer> |
| #endif |
| |
| QT_CHARTS_BEGIN_NAMESPACE |
| |
| GLWidget::GLWidget(GLXYSeriesDataManager *xyDataManager, QtCharts::QChart *chart, |
| QGraphicsView *parent) |
| : QOpenGLWidget(parent->viewport()), |
| m_program(nullptr), |
| m_shaderAttribLoc(-1), |
| m_colorUniformLoc(-1), |
| m_minUniformLoc(-1), |
| m_deltaUniformLoc(-1), |
| m_pointSizeUniformLoc(-1), |
| m_xyDataManager(xyDataManager), |
| m_antiAlias(parent->renderHints().testFlag(QPainter::Antialiasing)), |
| m_view(parent), |
| m_selectionFbo(nullptr), |
| m_chart(chart), |
| m_recreateSelectionFbo(true), |
| m_selectionRenderNeeded(true), |
| m_mousePressed(false), |
| m_lastPressSeries(nullptr), |
| m_lastHoverSeries(nullptr) |
| { |
| setAttribute(Qt::WA_TranslucentBackground); |
| setAttribute(Qt::WA_AlwaysStackOnTop); |
| |
| QSurfaceFormat surfaceFormat; |
| surfaceFormat.setDepthBufferSize(0); |
| surfaceFormat.setStencilBufferSize(0); |
| surfaceFormat.setRedBufferSize(8); |
| surfaceFormat.setGreenBufferSize(8); |
| surfaceFormat.setBlueBufferSize(8); |
| surfaceFormat.setAlphaBufferSize(8); |
| surfaceFormat.setSwapBehavior(QSurfaceFormat::DoubleBuffer); |
| surfaceFormat.setRenderableType(QSurfaceFormat::DefaultRenderableType); |
| surfaceFormat.setSamples(m_antiAlias ? 4 : 0); |
| setFormat(surfaceFormat); |
| |
| connect(xyDataManager, &GLXYSeriesDataManager::seriesRemoved, |
| this, &GLWidget::cleanXYSeriesResources); |
| |
| setMouseTracking(true); |
| } |
| |
| GLWidget::~GLWidget() |
| { |
| cleanup(); |
| } |
| |
| void GLWidget::cleanup() |
| { |
| makeCurrent(); |
| |
| delete m_program; |
| m_program = 0; |
| |
| foreach (QOpenGLBuffer *buffer, m_seriesBufferMap.values()) |
| delete buffer; |
| m_seriesBufferMap.clear(); |
| |
| doneCurrent(); |
| } |
| |
| void GLWidget::cleanXYSeriesResources(const QXYSeries *series) |
| { |
| makeCurrent(); |
| if (series) { |
| delete m_seriesBufferMap.take(series); |
| } else { |
| // Null series means all series were removed |
| foreach (QOpenGLBuffer *buffer, m_seriesBufferMap.values()) |
| delete buffer; |
| m_seriesBufferMap.clear(); |
| } |
| doneCurrent(); |
| } |
| |
| static const char *vertexSource = |
| "attribute highp vec2 points;\n" |
| "uniform highp vec2 min;\n" |
| "uniform highp vec2 delta;\n" |
| "uniform highp float pointSize;\n" |
| "uniform highp mat4 matrix;\n" |
| "void main() {\n" |
| " vec2 normalPoint = vec2(-1, -1) + ((points - min) / delta);\n" |
| " gl_Position = matrix * vec4(normalPoint, 0, 1);\n" |
| " gl_PointSize = pointSize;\n" |
| "}"; |
| static const char *fragmentSource = |
| "uniform highp vec3 color;\n" |
| "void main() {\n" |
| " gl_FragColor = vec4(color,1);\n" |
| "}\n"; |
| |
| void GLWidget::initializeGL() |
| { |
| connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, &GLWidget::cleanup); |
| |
| initializeOpenGLFunctions(); |
| glClearColor(0, 0, 0, 0); |
| |
| m_program = new QOpenGLShaderProgram; |
| m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexSource); |
| m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentSource); |
| m_program->bindAttributeLocation("points", 0); |
| m_program->link(); |
| |
| m_program->bind(); |
| m_colorUniformLoc = m_program->uniformLocation("color"); |
| m_minUniformLoc = m_program->uniformLocation("min"); |
| m_deltaUniformLoc = m_program->uniformLocation("delta"); |
| m_pointSizeUniformLoc = m_program->uniformLocation("pointSize"); |
| m_matrixUniformLoc = m_program->uniformLocation("matrix"); |
| |
| |
| // Create a vertex array object. In OpenGL ES 2.0 and OpenGL 2.x |
| // implementations this is optional and support may not be present |
| // at all. Nonetheless the below code works in all cases and makes |
| // sure there is a VAO when one is needed. |
| m_vao.create(); |
| QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); |
| |
| glEnableVertexAttribArray(0); |
| |
| glDisable(GL_DEPTH_TEST); |
| glDisable(GL_STENCIL_TEST); |
| |
| #if !defined(QT_OPENGL_ES_2) |
| if (!QOpenGLContext::currentContext()->isOpenGLES()) { |
| // Make it possible to change point primitive size and use textures with them in |
| // the shaders. These are implicitly enabled in ES2. |
| glEnable(GL_PROGRAM_POINT_SIZE); |
| } |
| #endif |
| |
| m_program->release(); |
| } |
| |
| void GLWidget::paintGL() |
| { |
| render(false); |
| |
| #ifdef QDEBUG_TRACE_GL_FPS |
| static QElapsedTimer stopWatch; |
| static int frameCount = -1; |
| if (frameCount == -1) { |
| stopWatch.start(); |
| frameCount = 0; |
| } |
| frameCount++; |
| int elapsed = stopWatch.elapsed(); |
| if (elapsed >= 1000) { |
| elapsed = stopWatch.restart(); |
| qreal fps = qreal(0.1 * int(10000.0 * (qreal(frameCount) / qreal(elapsed)))); |
| qDebug() << "FPS:" << fps; |
| frameCount = 0; |
| } |
| #endif |
| } |
| |
| void GLWidget::resizeGL(int w, int h) |
| { |
| m_fboSize.setWidth(w); |
| m_fboSize.setHeight(h); |
| m_recreateSelectionFbo = true; |
| m_selectionRenderNeeded = true; |
| } |
| |
| void GLWidget::mouseDoubleClickEvent(QMouseEvent *event) |
| { |
| QXYSeries *series = findSeriesAtEvent(event); |
| if (series) |
| emit series->doubleClicked(series->d_ptr->domain()->calculateDomainPoint(event->pos())); |
| } |
| |
| void GLWidget::mouseMoveEvent(QMouseEvent *event) |
| { |
| if (m_view->hasMouseTracking() && !event->buttons()) { |
| QXYSeries *series = findSeriesAtEvent(event); |
| if (series != m_lastHoverSeries) { |
| if (m_lastHoverSeries) { |
| if (chartSeries(m_lastHoverSeries)) { |
| emit m_lastHoverSeries->hovered( |
| m_lastHoverSeries->d_ptr->domain()->calculateDomainPoint( |
| event->pos()), false); |
| } |
| } |
| if (series) { |
| emit series->hovered( |
| series->d_ptr->domain()->calculateDomainPoint(event->pos()), true); |
| } |
| m_lastHoverSeries = series; |
| } |
| } else { |
| event->ignore(); |
| } |
| } |
| |
| void GLWidget::mousePressEvent(QMouseEvent *event) |
| { |
| QXYSeries *series = findSeriesAtEvent(event); |
| if (series) { |
| m_mousePressed = true; |
| m_mousePressPos = event->pos(); |
| m_lastPressSeries = series; |
| emit series->pressed(series->d_ptr->domain()->calculateDomainPoint(event->pos())); |
| } |
| } |
| |
| void GLWidget::mouseReleaseEvent(QMouseEvent *event) |
| { |
| if (chartSeries(m_lastPressSeries)) { |
| emit m_lastPressSeries->released( |
| m_lastPressSeries->d_ptr->domain()->calculateDomainPoint(m_mousePressPos)); |
| if (m_mousePressed) { |
| emit m_lastPressSeries->clicked( |
| m_lastPressSeries->d_ptr->domain()->calculateDomainPoint(m_mousePressPos)); |
| } |
| if (m_lastHoverSeries == m_lastPressSeries |
| && m_lastHoverSeries != findSeriesAtEvent(event)) { |
| if (chartSeries(m_lastHoverSeries)) { |
| emit m_lastHoverSeries->hovered( |
| m_lastHoverSeries->d_ptr->domain()->calculateDomainPoint( |
| event->pos()), false); |
| } |
| m_lastHoverSeries = nullptr; |
| } |
| m_lastPressSeries = nullptr; |
| m_mousePressed = false; |
| } else { |
| event->ignore(); |
| } |
| } |
| |
| QXYSeries *GLWidget::findSeriesAtEvent(QMouseEvent *event) |
| { |
| QXYSeries *series = nullptr; |
| int index = -1; |
| |
| if (m_xyDataManager->dataMap().size()) { |
| makeCurrent(); |
| |
| if (m_recreateSelectionFbo) |
| recreateSelectionFbo(); |
| |
| m_selectionFbo->bind(); |
| |
| if (m_selectionRenderNeeded) { |
| m_selectionVector.resize(m_xyDataManager->dataMap().size()); |
| render(true); |
| m_selectionRenderNeeded = false; |
| } |
| |
| GLubyte pixel[4] = {0, 0, 0, 0}; |
| glReadPixels(event->pos().x(), m_fboSize.height() - event->pos().y(), |
| 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, |
| (void *)pixel); |
| if (pixel[3] == 0xff) |
| index = pixel[0] + (pixel[1] << 8) + (pixel[2] << 16); |
| |
| glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebufferObject()); |
| |
| doneCurrent(); |
| } |
| |
| if (index >= 0) { |
| const QXYSeries *cSeries = nullptr; |
| if (index < m_selectionVector.size()) |
| cSeries = m_selectionVector.at(index); |
| |
| series = chartSeries(cSeries); |
| } |
| if (series) |
| event->accept(); |
| else |
| event->ignore(); |
| |
| return series; |
| } |
| |
| void GLWidget::render(bool selection) |
| { |
| glClear(GL_COLOR_BUFFER_BIT); |
| |
| QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); |
| m_program->bind(); |
| |
| int counter = 0; |
| const auto &dataMap = m_xyDataManager->dataMap(); |
| for (auto i = dataMap.begin(), end = dataMap.end(); i != end; ++i) { |
| QOpenGLBuffer *vbo = m_seriesBufferMap.value(i.key()); |
| GLXYSeriesData *data = i.value(); |
| |
| if (data->visible) { |
| if (selection) { |
| m_selectionVector[counter] = i.key(); |
| m_program->setUniformValue(m_colorUniformLoc, QVector3D((counter & 0xff) / 255.0f, |
| ((counter & 0xff00) >> 8) / 255.0f, |
| ((counter & 0xff0000) >> 16) / 255.0f)); |
| counter++; |
| } else { |
| m_program->setUniformValue(m_colorUniformLoc, data->color); |
| } |
| m_program->setUniformValue(m_minUniformLoc, data->min); |
| m_program->setUniformValue(m_deltaUniformLoc, data->delta); |
| m_program->setUniformValue(m_matrixUniformLoc, data->matrix); |
| bool dirty = data->dirty; |
| if (!vbo) { |
| vbo = new QOpenGLBuffer; |
| m_seriesBufferMap.insert(i.key(), vbo); |
| vbo->create(); |
| dirty = true; |
| } |
| vbo->bind(); |
| if (dirty) { |
| vbo->allocate(data->array.constData(), data->array.count() * sizeof(GLfloat)); |
| dirty = false; |
| m_selectionRenderNeeded = true; |
| } |
| |
| glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0); |
| if (data->type == QAbstractSeries::SeriesTypeLine) { |
| glLineWidth(data->width); |
| glDrawArrays(GL_LINE_STRIP, 0, data->array.size() / 2); |
| } else { // Scatter |
| m_program->setUniformValue(m_pointSizeUniformLoc, data->width); |
| glDrawArrays(GL_POINTS, 0, data->array.size() / 2); |
| } |
| vbo->release(); |
| } |
| } |
| m_program->release(); |
| } |
| |
| void GLWidget::recreateSelectionFbo() |
| { |
| QOpenGLFramebufferObjectFormat fboFormat; |
| fboFormat.setAttachment(QOpenGLFramebufferObject::NoAttachment); |
| |
| delete m_selectionFbo; |
| |
| const QSize deviceSize = m_fboSize * devicePixelRatioF(); |
| m_selectionFbo = new QOpenGLFramebufferObject(deviceSize, fboFormat); |
| m_recreateSelectionFbo = false; |
| m_selectionRenderNeeded = true; |
| } |
| |
| // This function makes sure the series we are dealing with has not been removed from the |
| // chart since we stored the pointer. |
| QXYSeries *GLWidget::chartSeries(const QXYSeries *cSeries) |
| { |
| QXYSeries *series = nullptr; |
| if (cSeries) { |
| Q_FOREACH (QAbstractSeries *chartSeries, m_chart->series()) { |
| if (cSeries == chartSeries) { |
| series = qobject_cast<QXYSeries *>(chartSeries); |
| break; |
| } |
| } |
| } |
| return series; |
| } |
| |
| bool GLWidget::needsReset() const |
| { |
| return m_view->renderHints().testFlag(QPainter::Antialiasing) != m_antiAlias; |
| } |
| |
| QT_CHARTS_END_NAMESPACE |
| |
| #endif |