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 | |