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#include <private/chartpresenter_p.h>
30#include <QtCharts/QChart>
31#include <private/chartitem_p.h>
32#include <private/qchart_p.h>
33#include <QtCharts/QAbstractAxis>
34#include <private/qabstractaxis_p.h>
35#include <private/chartdataset_p.h>
36#include <private/chartanimation_p.h>
37#include <private/qabstractseries_p.h>
38#include <QtCharts/QAreaSeries>
39#include <private/chartaxiselement_p.h>
40#include <private/chartbackground_p.h>
41#include <private/cartesianchartlayout_p.h>
42#include <private/polarchartlayout_p.h>
43#include <private/charttitle_p.h>
44#include <QtCore/QRegularExpression>
45#include <QtCore/QTimer>
46#include <QtGui/QTextDocument>
47#include <QtWidgets/QGraphicsScene>
48#include <QtWidgets/QGraphicsView>
49
50QT_CHARTS_BEGIN_NAMESPACE
51
52ChartPresenter::ChartPresenter(QChart *chart, QChart::ChartType type)
53 : QObject(chart),
54 m_chart(chart),
55 m_options(QChart::NoAnimation),
56 m_animationDuration(ChartAnimationDuration),
57 m_animationCurve(QEasingCurve::OutQuart),
58 m_state(ShowState),
59 m_background(0),
60 m_plotAreaBackground(0),
61 m_title(0),
62 m_localizeNumbers(false)
63#ifndef QT_NO_OPENGL
64 , m_glWidget(0)
65 , m_glUseWidget(true)
66#endif
67{
68 if (type == QChart::ChartTypeCartesian)
69 m_layout = new CartesianChartLayout(this);
70 else if (type == QChart::ChartTypePolar)
71 m_layout = new PolarChartLayout(this);
72 Q_ASSERT(m_layout);
73}
74
75ChartPresenter::~ChartPresenter()
76{
77#ifndef QT_NO_OPENGL
78 delete m_glWidget.data();
79#endif
80}
81
82void ChartPresenter::setFixedGeometry(const QRectF &rect)
83{
84 if (rect == m_fixedRect)
85 return;
86 const bool isSame = m_fixedRect == m_rect;
87 m_fixedRect = rect;
88 if (m_fixedRect.isNull()) {
89 // Update to the latest geometry properly if changed
90 if (!isSame) {
91 updateGeometry(rect: m_rect);
92 m_layout->updateGeometry();
93 }
94 } else {
95 updateGeometry(rect: m_fixedRect);
96 }
97}
98
99void ChartPresenter::setGeometry(const QRectF rect)
100{
101 if (rect.isValid() && m_rect != rect) {
102 m_rect = rect;
103 if (!m_fixedRect.isNull())
104 return;
105 updateGeometry(rect);
106 }
107}
108
109void ChartPresenter::updateGeometry(const QRectF &rect)
110{
111 foreach (ChartItem *chart, m_chartItems) {
112 chart->domain()->setSize(rect.size());
113 chart->setPos(rect.topLeft());
114 }
115#ifndef QT_NO_OPENGL
116 if (!m_glWidget.isNull())
117 m_glWidget->setGeometry(rect.toRect());
118#endif
119 emit plotAreaChanged(plotArea: rect);
120}
121
122QRectF ChartPresenter::geometry() const
123{
124 return m_fixedRect.isNull() ? m_rect : m_fixedRect;
125}
126
127void ChartPresenter::handleAxisAdded(QAbstractAxis *axis)
128{
129 axis->d_ptr->initializeGraphics(parent: rootItem());
130 axis->d_ptr->initializeAnimations(options: m_options, duration: m_animationDuration, curve&: m_animationCurve);
131 ChartAxisElement *item = axis->d_ptr->axisItem();
132 item->setPresenter(this);
133 item->setThemeManager(m_chart->d_ptr->m_themeManager);
134 m_axisItems<<item;
135 m_axes<<axis;
136 m_layout->invalidate();
137}
138
139void ChartPresenter::handleAxisRemoved(QAbstractAxis *axis)
140{
141 ChartAxisElement *item = axis->d_ptr->m_item.take();
142 if (item->animation())
143 item->animation()->stopAndDestroyLater();
144 item->hide();
145 item->disconnect();
146 item->deleteLater();
147 m_axisItems.removeAll(t: item);
148 m_axes.removeAll(t: axis);
149 m_layout->invalidate();
150}
151
152
153void ChartPresenter::handleSeriesAdded(QAbstractSeries *series)
154{
155 series->d_ptr->initializeGraphics(parent: rootItem());
156 series->d_ptr->initializeAnimations(options: m_options, duration: m_animationDuration, curve&: m_animationCurve);
157 series->d_ptr->setPresenter(this);
158 ChartItem *chart = series->d_ptr->chartItem();
159 chart->setPresenter(this);
160 chart->setThemeManager(m_chart->d_ptr->m_themeManager);
161 chart->setDataSet(m_chart->d_ptr->m_dataset);
162 chart->domain()->setSize(geometry().size());
163 chart->setPos(geometry().topLeft());
164 chart->handleDomainUpdated(); //this could be moved to intializeGraphics when animator is refactored
165 m_chartItems<<chart;
166 m_series<<series;
167 m_layout->invalidate();
168}
169
170void ChartPresenter::handleSeriesRemoved(QAbstractSeries *series)
171{
172 ChartItem *chart = series->d_ptr->m_item.take();
173 chart->hide();
174 chart->cleanup();
175 series->disconnect(receiver: chart);
176 chart->deleteLater();
177 if (chart->animation())
178 chart->animation()->stopAndDestroyLater();
179 m_chartItems.removeAll(t: chart);
180 m_series.removeAll(t: series);
181 m_layout->invalidate();
182}
183
184void ChartPresenter::setAnimationOptions(QChart::AnimationOptions options)
185{
186 if (m_options != options) {
187 QChart::AnimationOptions oldOptions = m_options;
188 m_options = options;
189 if (options.testFlag(flag: QChart::SeriesAnimations) != oldOptions.testFlag(flag: QChart::SeriesAnimations)) {
190 foreach (QAbstractSeries *series, m_series)
191 series->d_ptr->initializeAnimations(options: m_options, duration: m_animationDuration,
192 curve&: m_animationCurve);
193 }
194 if (options.testFlag(flag: QChart::GridAxisAnimations) != oldOptions.testFlag(flag: QChart::GridAxisAnimations)) {
195 foreach (QAbstractAxis *axis, m_axes)
196 axis->d_ptr->initializeAnimations(options: m_options, duration: m_animationDuration, curve&: m_animationCurve);
197 }
198 m_layout->invalidate(); // So that existing animations don't just stop halfway
199 }
200}
201
202void ChartPresenter::setAnimationDuration(int msecs)
203{
204 if (m_animationDuration != msecs) {
205 m_animationDuration = msecs;
206 foreach (QAbstractSeries *series, m_series)
207 series->d_ptr->initializeAnimations(options: m_options, duration: m_animationDuration, curve&: m_animationCurve);
208 foreach (QAbstractAxis *axis, m_axes)
209 axis->d_ptr->initializeAnimations(options: m_options, duration: m_animationDuration, curve&: m_animationCurve);
210 m_layout->invalidate(); // So that existing animations don't just stop halfway
211 }
212}
213
214void ChartPresenter::setAnimationEasingCurve(const QEasingCurve &curve)
215{
216 if (m_animationCurve != curve) {
217 m_animationCurve = curve;
218 foreach (QAbstractSeries *series, m_series)
219 series->d_ptr->initializeAnimations(options: m_options, duration: m_animationDuration, curve&: m_animationCurve);
220 foreach (QAbstractAxis *axis, m_axes)
221 axis->d_ptr->initializeAnimations(options: m_options, duration: m_animationDuration, curve&: m_animationCurve);
222 m_layout->invalidate(); // So that existing animations don't just stop halfway
223 }
224}
225
226void ChartPresenter::setState(State state,QPointF point)
227{
228 m_state=state;
229 m_statePoint=point;
230}
231
232QChart::AnimationOptions ChartPresenter::animationOptions() const
233{
234 return m_options;
235}
236
237void ChartPresenter::createBackgroundItem()
238{
239 if (!m_background) {
240 m_background = new ChartBackground(rootItem());
241 m_background->setPen(Qt::NoPen); // Theme doesn't touch pen so don't use default
242 m_background->setBrush(QChartPrivate::defaultBrush());
243 m_background->setZValue(ChartPresenter::BackgroundZValue);
244 }
245}
246
247void ChartPresenter::createPlotAreaBackgroundItem()
248{
249 if (!m_plotAreaBackground) {
250 if (m_chart->chartType() == QChart::ChartTypeCartesian)
251 m_plotAreaBackground = new QGraphicsRectItem(rootItem());
252 else
253 m_plotAreaBackground = new QGraphicsEllipseItem(rootItem());
254 // Use transparent pen instead of Qt::NoPen, as Qt::NoPen causes
255 // antialising artifacts with axis lines for some reason.
256 m_plotAreaBackground->setPen(QPen(Qt::transparent));
257 m_plotAreaBackground->setBrush(Qt::NoBrush);
258 m_plotAreaBackground->setZValue(ChartPresenter::PlotAreaZValue);
259 m_plotAreaBackground->setVisible(false);
260 }
261}
262
263void ChartPresenter::createTitleItem()
264{
265 if (!m_title) {
266 m_title = new ChartTitle(rootItem());
267 m_title->setZValue(ChartPresenter::BackgroundZValue);
268 }
269}
270
271void ChartPresenter::startAnimation(ChartAnimation *animation)
272{
273 animation->stop();
274 QTimer::singleShot(msec: 0, receiver: animation, SLOT(startChartAnimation()));
275}
276
277void ChartPresenter::setBackgroundBrush(const QBrush &brush)
278{
279 createBackgroundItem();
280 m_background->setBrush(brush);
281 m_layout->invalidate();
282}
283
284QBrush ChartPresenter::backgroundBrush() const
285{
286 if (!m_background)
287 return QBrush();
288 return m_background->brush();
289}
290
291void ChartPresenter::setBackgroundPen(const QPen &pen)
292{
293 createBackgroundItem();
294 m_background->setPen(pen);
295 m_layout->invalidate();
296}
297
298QPen ChartPresenter::backgroundPen() const
299{
300 if (!m_background)
301 return QPen();
302 return m_background->pen();
303}
304
305void ChartPresenter::setBackgroundRoundness(qreal diameter)
306{
307 createBackgroundItem();
308 m_background->setDiameter(diameter);
309 m_layout->invalidate();
310}
311
312qreal ChartPresenter::backgroundRoundness() const
313{
314 if (!m_background)
315 return 0;
316 return m_background->diameter();
317}
318
319void ChartPresenter::setPlotAreaBackgroundBrush(const QBrush &brush)
320{
321 createPlotAreaBackgroundItem();
322 m_plotAreaBackground->setBrush(brush);
323 m_layout->invalidate();
324}
325
326QBrush ChartPresenter::plotAreaBackgroundBrush() const
327{
328 if (!m_plotAreaBackground)
329 return QBrush();
330 return m_plotAreaBackground->brush();
331}
332
333void ChartPresenter::setPlotAreaBackgroundPen(const QPen &pen)
334{
335 createPlotAreaBackgroundItem();
336 m_plotAreaBackground->setPen(pen);
337 m_layout->invalidate();
338}
339
340QPen ChartPresenter::plotAreaBackgroundPen() const
341{
342 if (!m_plotAreaBackground)
343 return QPen();
344 return m_plotAreaBackground->pen();
345}
346
347void ChartPresenter::setTitle(const QString &title)
348{
349 createTitleItem();
350 m_title->setText(title);
351 m_layout->invalidate();
352}
353
354QString ChartPresenter::title() const
355{
356 if (!m_title)
357 return QString();
358 return m_title->text();
359}
360
361void ChartPresenter::setTitleFont(const QFont &font)
362{
363 createTitleItem();
364 m_title->setFont(font);
365 m_layout->invalidate();
366}
367
368QFont ChartPresenter::titleFont() const
369{
370 if (!m_title)
371 return QFont();
372 return m_title->font();
373}
374
375void ChartPresenter::setTitleBrush(const QBrush &brush)
376{
377 createTitleItem();
378 m_title->setDefaultTextColor(brush.color());
379 m_layout->invalidate();
380}
381
382QBrush ChartPresenter::titleBrush() const
383{
384 if (!m_title)
385 return QBrush();
386 return QBrush(m_title->defaultTextColor());
387}
388
389void ChartPresenter::setBackgroundVisible(bool visible)
390{
391 createBackgroundItem();
392 m_background->setVisible(visible);
393}
394
395
396bool ChartPresenter::isBackgroundVisible() const
397{
398 if (!m_background)
399 return false;
400 return m_background->isVisible();
401}
402
403void ChartPresenter::setPlotAreaBackgroundVisible(bool visible)
404{
405 createPlotAreaBackgroundItem();
406 m_plotAreaBackground->setVisible(visible);
407}
408
409bool ChartPresenter::isPlotAreaBackgroundVisible() const
410{
411 if (!m_plotAreaBackground)
412 return false;
413 return m_plotAreaBackground->isVisible();
414}
415
416void ChartPresenter::setBackgroundDropShadowEnabled(bool enabled)
417{
418 createBackgroundItem();
419 m_background->setDropShadowEnabled(enabled);
420}
421
422bool ChartPresenter::isBackgroundDropShadowEnabled() const
423{
424 if (!m_background)
425 return false;
426 return m_background->isDropShadowEnabled();
427}
428
429void ChartPresenter::setLocalizeNumbers(bool localize)
430{
431 m_localizeNumbers = localize;
432 m_layout->invalidate();
433}
434
435void ChartPresenter::setLocale(const QLocale &locale)
436{
437 m_locale = locale;
438 m_layout->invalidate();
439}
440
441AbstractChartLayout *ChartPresenter::layout()
442{
443 return m_layout;
444}
445
446QLegend *ChartPresenter::legend()
447{
448 return m_chart->legend();
449}
450
451void ChartPresenter::setVisible(bool visible)
452{
453 m_chart->setVisible(visible);
454}
455
456ChartBackground *ChartPresenter::backgroundElement()
457{
458 return m_background;
459}
460
461QAbstractGraphicsShapeItem *ChartPresenter::plotAreaElement()
462{
463 return m_plotAreaBackground;
464}
465
466QList<ChartAxisElement *> ChartPresenter::axisItems() const
467{
468 return m_axisItems;
469}
470
471QList<ChartItem *> ChartPresenter::chartItems() const
472{
473 return m_chartItems;
474}
475
476ChartTitle *ChartPresenter::titleElement()
477{
478 return m_title;
479}
480
481template <int TSize>
482struct TextBoundCache
483{
484 struct element
485 {
486 quint32 lastUsed;
487 QRectF bounds;
488 };
489 QHash<QString, element> elements;
490 quint32 usedCounter = 0;
491 QGraphicsTextItem dummyText;
492
493 QRectF bounds(const QFont &font, const QString &text)
494 {
495 const QString key = font.key() + text;
496 auto elem = elements.find(key);
497 if (elem != elements.end()) {
498 usedCounter++;
499 elem->lastUsed = usedCounter;
500 return elem->bounds;
501 }
502 dummyText.setFont(font);
503 dummyText.setHtml(text);
504 const QRectF bounds = dummyText.boundingRect();
505 if (elements.size() >= TSize) {
506 auto elem = std::min_element(elements.begin(), elements.end(),
507 [](const element &a, const element &b) {
508 return a.lastUsed < b.lastUsed;
509 });
510 if (elem != elements.end()) {
511 const QString key = elem.key();
512 elements.remove(key);
513 }
514 }
515 elements.insert(key, {usedCounter++, bounds});
516 return bounds;
517 }
518 QTextDocument *document()
519 {
520 return dummyText.document();
521 }
522};
523
524QRectF ChartPresenter::textBoundingRect(const QFont &font, const QString &text, qreal angle)
525{
526 static TextBoundCache<32> textBoundCache;
527 static bool initMargin = true;
528 if (initMargin) {
529 textBoundCache.document()->setDocumentMargin(textMargin());
530 initMargin = false;
531 }
532
533 QRectF boundingRect = textBoundCache.bounds(font, text);
534
535 // Take rotation into account
536 if (angle) {
537 QTransform transform;
538 transform.rotate(a: angle);
539 boundingRect = transform.mapRect(boundingRect);
540 }
541
542 return boundingRect;
543}
544
545// boundingRect parameter returns the rotated bounding rect of the text
546QString ChartPresenter::truncatedText(const QFont &font, const QString &text, qreal angle,
547 qreal maxWidth, qreal maxHeight, QRectF &boundingRect)
548{
549 QString truncatedString(text);
550 boundingRect = textBoundingRect(font, text: truncatedString, angle);
551 if (boundingRect.width() > maxWidth || boundingRect.height() > maxHeight) {
552 // It can be assumed that almost any amount of string manipulation is faster
553 // than calculating one bounding rectangle, so first prepare a list of truncated strings
554 // to try.
555 static QRegularExpression truncateMatcher(QStringLiteral("&#?[0-9a-zA-Z]*;$"));
556
557 QVector<QString> testStrings(text.length());
558 int count(0);
559 static QLatin1Char closeTag('>');
560 static QLatin1Char openTag('<');
561 static QLatin1Char semiColon(';');
562 static QLatin1String ellipsis("...");
563 while (truncatedString.length() > 1) {
564 int chopIndex(-1);
565 int chopCount(1);
566 QChar lastChar(truncatedString.at(i: truncatedString.length() - 1));
567
568 if (lastChar == closeTag)
569 chopIndex = truncatedString.lastIndexOf(c: openTag);
570 else if (lastChar == semiColon)
571 chopIndex = truncatedString.indexOf(re: truncateMatcher);
572
573 if (chopIndex != -1)
574 chopCount = truncatedString.length() - chopIndex;
575 truncatedString.chop(n: chopCount);
576 testStrings[count] = truncatedString + ellipsis;
577 count++;
578 }
579
580 // Binary search for best fit
581 int minIndex(0);
582 int maxIndex(count - 1);
583 int bestIndex(count);
584 QRectF checkRect;
585
586 while (maxIndex >= minIndex) {
587 int mid = (maxIndex + minIndex) / 2;
588 checkRect = textBoundingRect(font, text: testStrings.at(i: mid), angle);
589 if (checkRect.width() > maxWidth || checkRect.height() > maxHeight) {
590 // Checked index too large, all under this are also too large
591 minIndex = mid + 1;
592 } else {
593 // Checked index fits, all over this also fit
594 maxIndex = mid - 1;
595 bestIndex = mid;
596 boundingRect = checkRect;
597 }
598 }
599 // Default to "..." if nothing fits
600 if (bestIndex == count) {
601 boundingRect = textBoundingRect(font, text: ellipsis, angle);
602 truncatedString = ellipsis;
603 } else {
604 truncatedString = testStrings.at(i: bestIndex);
605 }
606 }
607
608 return truncatedString;
609}
610
611QString ChartPresenter::numberToString(double value, char f, int prec)
612{
613 if (m_localizeNumbers)
614 return m_locale.toString(i: value, f, prec);
615 else
616 return QString::number(value, f, prec);
617}
618
619QString ChartPresenter::numberToString(int value)
620{
621 if (m_localizeNumbers)
622 return m_locale.toString(i: value);
623 else
624 return QString::number(value);
625}
626
627void ChartPresenter::updateGLWidget()
628{
629#ifndef QT_NO_OPENGL
630 // GLWidget pointer is wrapped in QPointer as its parent is not in our control, and therefore
631 // can potentially get deleted unexpectedly.
632 if (!m_glWidget.isNull() && m_glWidget->needsReset()) {
633 m_glWidget->hide();
634 delete m_glWidget.data();
635 m_glWidget.clear();
636 }
637 if (m_glWidget.isNull() && m_glUseWidget && m_chart->scene()) {
638 // Find the view of the scene. If the scene has multiple views, only the first view is
639 // chosen.
640 QList<QGraphicsView *> views = m_chart->scene()->views();
641 if (views.size()) {
642 QGraphicsView *firstView = views.at(i: 0);
643 m_glWidget = new GLWidget(m_chart->d_ptr->m_dataset->glXYSeriesDataManager(),
644 m_chart, firstView);
645 m_glWidget->setGeometry(geometry().toRect());
646 m_glWidget->show();
647 }
648 }
649 // Make sure we update the widget in a timely manner
650 if (!m_glWidget.isNull())
651 m_glWidget->update();
652#endif
653}
654
655QT_CHARTS_END_NAMESPACE
656
657#include "moc_chartpresenter_p.cpp"
658

source code of qtcharts/src/charts/chartpresenter.cpp