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

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