1 | // Copyright (C) 2021 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include "legendmarkeritem_p.h" |
5 | #include "qlegendmarker_p.h" |
6 | #include "chartpresenter_p.h" |
7 | #include "qlegend.h" |
8 | #include "qlegend_p.h" |
9 | #include "qlegendmarker.h" |
10 | #if QT_CONFIG(charts_scatter_chart) |
11 | #include "qscatterseries.h" |
12 | #include "scatterchartitem_p.h" |
13 | #endif |
14 | #if QT_CONFIG(charts_line_chart) |
15 | #include "qlineseries.h" |
16 | #endif |
17 | #if QT_CONFIG(charts_spline_chart) |
18 | #include "qsplineseries.h" |
19 | #endif |
20 | #include <QtGui/QPainter> |
21 | #include <QtWidgets/QGraphicsSceneEvent> |
22 | #include <QtWidgets/QGraphicsTextItem> |
23 | #include <QtWidgets/QGraphicsEllipseItem> |
24 | #include <QtWidgets/QGraphicsRectItem> |
25 | #include <QtWidgets/QGraphicsLineItem> |
26 | #include <QtGui/QTextDocument> |
27 | #include <QtCore/QtMath> |
28 | |
29 | QT_BEGIN_NAMESPACE |
30 | |
31 | LegendMarkerItem::LegendMarkerItem(QLegendMarkerPrivate *marker, QGraphicsObject *parent) : |
32 | QGraphicsObject(parent), |
33 | m_marker(marker), |
34 | m_defaultMarkerRect(0.0, 0.0, 10.0, 10.0), |
35 | m_markerRect(0.0, 0.0, -1.0, -1.0), |
36 | m_boundingRect(0,0,0,0), |
37 | m_textItem(new QGraphicsTextItem(this)), |
38 | m_markerItem(nullptr), |
39 | m_margin(3), |
40 | m_space(4), |
41 | m_markerShape(QLegend::MarkerShapeDefault), |
42 | m_hovering(false), |
43 | m_itemType(TypeRect) |
44 | { |
45 | m_textItem->document()->setDocumentMargin(ChartPresenter::textMargin()); |
46 | setAcceptHoverEvents(true); |
47 | } |
48 | |
49 | LegendMarkerItem::~LegendMarkerItem() |
50 | { |
51 | if (m_hovering) { |
52 | emit m_marker->q_ptr->hovered(status: false); |
53 | } |
54 | } |
55 | |
56 | void LegendMarkerItem::setPen(const QPen &pen) |
57 | { |
58 | m_pen = pen; |
59 | setItemBrushAndPen(); |
60 | } |
61 | |
62 | QPen LegendMarkerItem::pen() const |
63 | { |
64 | return m_pen; |
65 | } |
66 | |
67 | void LegendMarkerItem::setBrush(const QBrush &brush) |
68 | { |
69 | m_brush = brush; |
70 | setItemBrushAndPen(); |
71 | } |
72 | |
73 | QBrush LegendMarkerItem::brush() const |
74 | { |
75 | return m_brush; |
76 | } |
77 | |
78 | void LegendMarkerItem::setSeriesPen(const QPen &pen) |
79 | { |
80 | m_seriesPen = pen; |
81 | setItemBrushAndPen(); |
82 | } |
83 | |
84 | void LegendMarkerItem::setSeriesBrush(const QBrush &brush) |
85 | { |
86 | m_seriesBrush = brush; |
87 | setItemBrushAndPen(); |
88 | } |
89 | |
90 | void LegendMarkerItem::setSeriesLightMarker(const QImage &image) |
91 | { |
92 | m_seriesLightMarker = image; |
93 | |
94 | if (m_markerItem) { |
95 | if (image.isNull()) { |
96 | m_markerItem->setFlag(flag: QGraphicsItem::ItemStacksBehindParent, enabled: false); |
97 | } else { |
98 | m_markerItem->setFlag(flag: QGraphicsItem::ItemStacksBehindParent, |
99 | enabled: (effectiveMarkerShape() == QLegend::MarkerShapeFromSeries)); |
100 | } |
101 | } |
102 | updateMarkerShapeAndSize(); |
103 | } |
104 | |
105 | void LegendMarkerItem::setFont(const QFont &font) |
106 | { |
107 | QFontMetrics fn(font); |
108 | m_font = font; |
109 | |
110 | m_defaultMarkerRect = QRectF(0, 0, fn.height() / 2, fn.height() / 2); |
111 | if (effectiveMarkerShape() != QLegend::MarkerShapeFromSeries) |
112 | updateMarkerShapeAndSize(); |
113 | m_marker->invalidateLegend(); |
114 | } |
115 | |
116 | QFont LegendMarkerItem::font() const |
117 | { |
118 | return m_font; |
119 | } |
120 | |
121 | void LegendMarkerItem::setLabel(const QString label) |
122 | { |
123 | m_label = label; |
124 | m_marker->invalidateLegend(); |
125 | } |
126 | |
127 | QString LegendMarkerItem::label() const |
128 | { |
129 | return m_label; |
130 | } |
131 | |
132 | void LegendMarkerItem::setLabelBrush(const QBrush &brush) |
133 | { |
134 | m_textItem->setDefaultTextColor(brush.color()); |
135 | } |
136 | |
137 | QBrush LegendMarkerItem::labelBrush() const |
138 | { |
139 | return QBrush(m_textItem->defaultTextColor()); |
140 | } |
141 | |
142 | void LegendMarkerItem::setGeometry(const QRectF &rect) |
143 | { |
144 | if (!m_markerItem) |
145 | updateMarkerShapeAndSize(); |
146 | |
147 | const qreal width = rect.width(); |
148 | const qreal markerWidth = effectiveMarkerWidth(); |
149 | const qreal x = m_margin + markerWidth + m_space + m_margin; |
150 | QRectF truncatedRect; |
151 | const QString html = ChartPresenter::truncatedText(font: m_font, text: m_label, angle: qreal(0.0), |
152 | maxWidth: width - x, maxHeight: rect.height(), boundingRect&: truncatedRect); |
153 | m_textItem->setHtml(html.compare(other: QLatin1String("..." )) ? html : QString()); |
154 | #if QT_CONFIG(tooltip) |
155 | if (m_marker->m_legend->showToolTips() && html != m_label) { |
156 | m_textItem->setToolTip(m_label); |
157 | m_markerItem->setToolTip(m_label); |
158 | } else { |
159 | m_textItem->setToolTip(QString()); |
160 | m_markerItem->setToolTip(QString()); |
161 | } |
162 | #endif |
163 | m_textItem->setFont(m_font); |
164 | m_textItem->setTextWidth(truncatedRect.width()); |
165 | |
166 | qreal y = qMax(a: m_markerRect.height() + 2 * m_margin, b: truncatedRect.height() + 2 * m_margin); |
167 | |
168 | const QRectF &textRect = m_textItem->boundingRect(); |
169 | |
170 | m_textItem->setPos(ax: x - m_margin, ay: y / 2 - textRect.height() / 2); |
171 | setItemRect(); |
172 | |
173 | // The textMargin adjustments to position are done to make default case rects less blurry with anti-aliasing |
174 | m_markerItem->setPos(ax: m_margin - ChartPresenter::textMargin() |
175 | + (markerWidth - m_markerRect.width()) / 2.0, |
176 | ay: y / 2.0 - m_markerRect.height() / 2.0 + ChartPresenter::textMargin()); |
177 | |
178 | prepareGeometryChange(); |
179 | m_boundingRect = QRectF(0, 0, x + textRect.width() + m_margin, y); |
180 | } |
181 | |
182 | QRectF LegendMarkerItem::boundingRect() const |
183 | { |
184 | return m_boundingRect; |
185 | } |
186 | |
187 | QRectF LegendMarkerItem::markerRect() const |
188 | { |
189 | return m_markerRect; |
190 | } |
191 | |
192 | void LegendMarkerItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) |
193 | { |
194 | Q_UNUSED(option); |
195 | Q_UNUSED(widget); |
196 | |
197 | // Draw the light marker, if it is present. |
198 | if (!m_seriesLightMarker.isNull()) { |
199 | QRectF rect(m_markerItem->pos(), m_markerRect.size()); |
200 | |
201 | // shrink the image so the line is visible behind the marker |
202 | if (m_itemType == TypeLine && rect.width() > 4 && rect.height() > 4) |
203 | rect.adjust(xp1: 1.0, yp1: 1.0, xp2: -1.0, yp2: -1.0); |
204 | |
205 | painter->drawImage(r: rect, image: m_seriesLightMarker); |
206 | } |
207 | } |
208 | |
209 | QSizeF LegendMarkerItem::sizeHint(Qt::SizeHint which, const QSizeF& constraint) const |
210 | { |
211 | Q_UNUSED(constraint); |
212 | |
213 | QSizeF sh; |
214 | const qreal markerWidth = effectiveMarkerWidth(); |
215 | |
216 | switch (which) { |
217 | case Qt::MinimumSize: { |
218 | const QRectF labelRect = ChartPresenter::textBoundingRect(font: m_font, QStringLiteral("..." )); |
219 | sh = QSizeF(labelRect.width() + (2.0 * m_margin) + m_space + markerWidth, |
220 | qMax(a: m_markerRect.height(), b: labelRect.height()) + (2.0 * m_margin)); |
221 | break; |
222 | } |
223 | case Qt::PreferredSize: { |
224 | const QRectF labelRect = ChartPresenter::textBoundingRect(font: m_font, text: m_label); |
225 | sh = QSizeF(labelRect.width() + (2.0 * m_margin) + m_space + markerWidth + 1, |
226 | qMax(a: m_markerRect.height(), b: labelRect.height()) + (2.0 * m_margin)); |
227 | break; |
228 | } |
229 | default: |
230 | break; |
231 | } |
232 | |
233 | return sh; |
234 | } |
235 | |
236 | void LegendMarkerItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) |
237 | { |
238 | event->setAccepted(false); |
239 | m_hovering = true; |
240 | emit m_marker->q_ptr->hovered(status: true); |
241 | |
242 | QGraphicsObject::hoverEnterEvent(event); |
243 | } |
244 | |
245 | void LegendMarkerItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) |
246 | { |
247 | event->setAccepted(false); |
248 | m_hovering = false; |
249 | emit m_marker->q_ptr->hovered(status: false); |
250 | |
251 | QGraphicsObject::hoverLeaveEvent(event); |
252 | } |
253 | |
254 | QString LegendMarkerItem::displayedLabel() const |
255 | { |
256 | return m_textItem->toHtml(); |
257 | } |
258 | |
259 | void LegendMarkerItem::setToolTip(const QString &tip) |
260 | { |
261 | #if QT_CONFIG(tooltip) |
262 | m_textItem->setToolTip(tip); |
263 | m_markerItem->setToolTip(tip); |
264 | #endif |
265 | } |
266 | |
267 | QLegend::MarkerShape LegendMarkerItem::markerShape() const |
268 | { |
269 | return m_markerShape; |
270 | } |
271 | |
272 | void LegendMarkerItem::setMarkerShape(QLegend::MarkerShape shape) |
273 | { |
274 | m_markerShape = shape; |
275 | } |
276 | |
277 | void LegendMarkerItem::updateMarkerShapeAndSize() |
278 | { |
279 | const QLegend::MarkerShape shape = effectiveMarkerShape(); |
280 | |
281 | ItemType itemType = TypeRect; |
282 | QRectF newRect = m_defaultMarkerRect; |
283 | QXYSeries *xySeries = qobject_cast<QXYSeries *>(object: m_marker->series()); |
284 | |
285 | switch (shape) { |
286 | case QLegend::MarkerShapeFromSeries: { |
287 | if (xySeries) { |
288 | m_seriesLightMarker = xySeries->lightMarker(); |
289 | switch (xySeries->type()) { |
290 | #if QT_CONFIG(charts_scatter_chart) |
291 | case QAbstractSeries::SeriesTypeScatter: { |
292 | newRect.setSize(QSizeF(xySeries->markerSize(), xySeries->markerSize())); |
293 | |
294 | QScatterSeries *scatter = qobject_cast<QScatterSeries *>(object: m_marker->series()); |
295 | Q_ASSERT(scatter != nullptr); |
296 | switch (scatter->markerShape()) { |
297 | case QScatterSeries::MarkerShapeRectangle: |
298 | itemType = TypeRect; |
299 | break; |
300 | case QScatterSeries::MarkerShapeCircle: |
301 | itemType = TypeCircle; |
302 | break; |
303 | case QScatterSeries::MarkerShapeRotatedRectangle: |
304 | itemType = TypeRotatedRect; |
305 | break; |
306 | case QScatterSeries::MarkerShapeTriangle: |
307 | itemType = TypeTriangle; |
308 | break; |
309 | case QScatterSeries::MarkerShapeStar: |
310 | itemType = TypeStar; |
311 | break; |
312 | case QScatterSeries::MarkerShapePentagon: |
313 | itemType = TypePentagon; |
314 | break; |
315 | default: |
316 | qWarning() << "Unsupported marker type, TypeRect used" ; |
317 | break; |
318 | } |
319 | } |
320 | break; |
321 | #endif |
322 | #if QT_CONFIG(charts_line_chart) |
323 | case QAbstractSeries::SeriesTypeLine: |
324 | case QAbstractSeries::SeriesTypeSpline: { |
325 | if (m_seriesLightMarker.isNull()) { |
326 | newRect.setHeight(m_seriesPen.width()); |
327 | newRect.setWidth(qRound(d: m_defaultMarkerRect.width() * 1.5)); |
328 | } else { |
329 | newRect.setSize(QSizeF(xySeries->markerSize(), xySeries->markerSize())); |
330 | } |
331 | itemType = TypeLine; |
332 | } |
333 | break; |
334 | #endif |
335 | default: |
336 | // Use defaults specified above. |
337 | break; |
338 | } |
339 | } |
340 | } |
341 | break; |
342 | case QLegend::MarkerShapeCircle: |
343 | itemType = TypeCircle; |
344 | break; |
345 | case QLegend::MarkerShapeRotatedRectangle: |
346 | itemType = TypeRotatedRect; |
347 | break; |
348 | case QLegend::MarkerShapeTriangle: |
349 | itemType = TypeTriangle; |
350 | break; |
351 | case QLegend::MarkerShapeStar: |
352 | itemType = TypeStar; |
353 | break; |
354 | case QLegend::MarkerShapePentagon: |
355 | itemType = TypePentagon; |
356 | break; |
357 | default: |
358 | break; |
359 | } |
360 | |
361 | if (!m_markerItem || m_itemType != itemType) { |
362 | m_itemType = itemType; |
363 | QPointF oldPos; |
364 | if (m_markerItem) { |
365 | oldPos = m_markerItem->pos(); |
366 | delete m_markerItem; |
367 | } |
368 | |
369 | switch (itemType) { |
370 | case LegendMarkerItem::TypeRect: { |
371 | m_markerItem = new QGraphicsRectItem(this); |
372 | break; |
373 | } |
374 | case LegendMarkerItem::TypeCircle: { |
375 | m_markerItem = new QGraphicsEllipseItem(this); |
376 | break; |
377 | } |
378 | #if QT_CONFIG(charts_scatter_chart) |
379 | case LegendMarkerItem::TypeRotatedRect: { |
380 | QGraphicsPolygonItem *item = new QGraphicsPolygonItem(this); |
381 | item->setPolygon(RotatedRectangleMarker::polygon()); |
382 | m_markerItem = item; |
383 | break; |
384 | } |
385 | case LegendMarkerItem::TypeTriangle: { |
386 | QGraphicsPolygonItem *item = new QGraphicsPolygonItem(this); |
387 | item->setPolygon(TriangleMarker::polygon()); |
388 | m_markerItem = item; |
389 | break; |
390 | } |
391 | case LegendMarkerItem::TypeStar: { |
392 | QGraphicsPolygonItem *item = new QGraphicsPolygonItem(this); |
393 | item->setPolygon(StarMarker::polygon()); |
394 | m_markerItem = item; |
395 | break; |
396 | } |
397 | case LegendMarkerItem::TypePentagon: { |
398 | QGraphicsPolygonItem *item = new QGraphicsPolygonItem(this); |
399 | item->setPolygon(PentagonMarker::polygon()); |
400 | m_markerItem = item; |
401 | break; |
402 | } |
403 | #endif |
404 | default: |
405 | m_markerItem = new QGraphicsLineItem(this); |
406 | break; |
407 | } |
408 | |
409 | if (xySeries && shape == QLegend::MarkerShapeFromSeries |
410 | && !m_seriesLightMarker.isNull()) { |
411 | m_markerItem->setFlag(flag: QGraphicsItem::ItemStacksBehindParent, enabled: true); |
412 | } |
413 | |
414 | // Immediately update the position to the approximate correct position to avoid marker |
415 | // jumping around when changing markers |
416 | m_markerItem->setPos(oldPos); |
417 | } |
418 | setItemBrushAndPen(); |
419 | |
420 | if (newRect != m_markerRect) { |
421 | if (useMaxWidth() && m_marker->m_legend->d_ptr->maxMarkerWidth() < newRect.width()) |
422 | m_marker->invalidateAllItems(); |
423 | m_markerRect = newRect; |
424 | setItemRect(); |
425 | emit markerRectChanged(); |
426 | updateGeometry(); |
427 | } |
428 | } |
429 | |
430 | QLegend::MarkerShape LegendMarkerItem::effectiveMarkerShape() const |
431 | { |
432 | QLegend::MarkerShape shape = m_markerShape; |
433 | if (shape == QLegend::MarkerShapeDefault) |
434 | shape = m_marker->m_legend->markerShape(); |
435 | return shape; |
436 | } |
437 | |
438 | qreal LegendMarkerItem::effectiveMarkerWidth() const |
439 | { |
440 | return useMaxWidth() ? m_marker->m_legend->d_ptr->maxMarkerWidth() |
441 | : m_markerRect.width(); |
442 | } |
443 | |
444 | void LegendMarkerItem::setItemBrushAndPen() |
445 | { |
446 | if (m_markerItem) { |
447 | QAbstractGraphicsShapeItem *shapeItem = |
448 | qgraphicsitem_cast<QGraphicsRectItem *>(item: m_markerItem); |
449 | if (!shapeItem) |
450 | shapeItem = qgraphicsitem_cast<QGraphicsEllipseItem *>(item: m_markerItem); |
451 | |
452 | if (!shapeItem) |
453 | shapeItem = qgraphicsitem_cast<QGraphicsPolygonItem *>(item: m_markerItem); |
454 | |
455 | if (shapeItem) { |
456 | if (effectiveMarkerShape() == QLegend::MarkerShapeFromSeries) { |
457 | QPen pen = m_seriesPen; |
458 | QBrush brush = m_seriesBrush; |
459 | if (!m_seriesLightMarker.isNull()) { |
460 | // If there's a light marker set, don't draw the regular marker. |
461 | pen.setColor(Qt::transparent); |
462 | brush = QBrush(); |
463 | brush.setColor(Qt::transparent); |
464 | } |
465 | |
466 | shapeItem->setPen(pen); |
467 | shapeItem->setBrush(brush); |
468 | } else { |
469 | shapeItem->setPen(m_pen); |
470 | shapeItem->setBrush(m_brush); |
471 | } |
472 | } else { |
473 | // Must be line item, it has no brush. |
474 | QGraphicsLineItem *lineItem = |
475 | qgraphicsitem_cast<QGraphicsLineItem *>(item: m_markerItem); |
476 | if (lineItem) |
477 | lineItem->setPen(m_seriesPen); |
478 | } |
479 | } |
480 | } |
481 | |
482 | void LegendMarkerItem::setItemRect() |
483 | { |
484 | switch (m_itemType) { |
485 | case LegendMarkerItem::TypeRect: { |
486 | static_cast<QGraphicsRectItem *>(m_markerItem)->setRect(m_markerRect); |
487 | break; |
488 | } |
489 | case LegendMarkerItem::TypeCircle: { |
490 | static_cast<QGraphicsEllipseItem *>(m_markerItem)->setRect(m_markerRect); |
491 | break; |
492 | } |
493 | #if QT_CONFIG(charts_scatter_chart) |
494 | case LegendMarkerItem::TypeRotatedRect: { |
495 | static_cast<QGraphicsPolygonItem *>(m_markerItem) |
496 | ->setPolygon(RotatedRectangleMarker::polygon(x: m_markerRect.x(), y: m_markerRect.y(), |
497 | w: m_markerRect.width(), |
498 | h: m_markerRect.height())); |
499 | break; |
500 | } |
501 | case LegendMarkerItem::TypeTriangle: { |
502 | static_cast<QGraphicsPolygonItem *>(m_markerItem) |
503 | ->setPolygon(TriangleMarker::polygon(x: m_markerRect.x(), y: m_markerRect.y(), |
504 | w: m_markerRect.width(), h: m_markerRect.height())); |
505 | break; |
506 | } |
507 | case LegendMarkerItem::TypeStar: { |
508 | static_cast<QGraphicsPolygonItem *>(m_markerItem) |
509 | ->setPolygon(StarMarker::polygon(x: m_markerRect.x(), y: m_markerRect.y(), |
510 | w: m_markerRect.width(), h: m_markerRect.height())); |
511 | break; |
512 | } |
513 | case LegendMarkerItem::TypePentagon: { |
514 | static_cast<QGraphicsPolygonItem *>(m_markerItem) |
515 | ->setPolygon(PentagonMarker::polygon(x: m_markerRect.x(), y: m_markerRect.y(), |
516 | w: m_markerRect.width(), h: m_markerRect.height())); |
517 | break; |
518 | } |
519 | #endif |
520 | default: { |
521 | qreal y = m_markerRect.height() / 2.0; |
522 | QLineF line(0.0, y, m_markerRect.width(), y); |
523 | static_cast<QGraphicsLineItem *>(m_markerItem)->setLine(line); |
524 | break; |
525 | } |
526 | } |
527 | } |
528 | |
529 | bool LegendMarkerItem::useMaxWidth() const |
530 | { |
531 | return (m_marker->m_legend->alignment() == Qt::AlignLeft |
532 | || m_marker->m_legend->alignment() == Qt::AlignRight); |
533 | } |
534 | |
535 | QT_END_NAMESPACE |
536 | |
537 | #include "moc_legendmarkeritem_p.cpp" |
538 | |